Linux Kernel(4.19) Hacks

rousalome.egloos.com

포토로그 Kernel Crash


통계 위젯 (화이트)

91258
1323
114593


[라즈베리파이] 스케줄링: 스케줄링 클래스란 10. Process Scheduling

리눅스 커널에서는 기본으로 5개의 스케줄러를 제공합니다. 
+ stop 스케줄러, deadline 스케줄러, RT 스케줄러, CFS 스케줄러, 아이들 스케줄러

리눅스 커널은 5개의 스케줄러를 프로세스가 유연하게 쓸수 있도록 스케줄러 세부 동작을 모듈화했습니다. 이를 스케줄러 클래스라고 하며 5개의 스케줄러를 스케줄러 클래스로 정의합니다.
 
프로세스는 자신이 실행할 스케줄러 클래스 정보를 갖고 있습니다. 
즉, 모든 프로세스들은 태스크 디스크립터에 자신이 선택하거나 부모 프로세스로부터 물려 받은 스케줄러 클래스 정보를 갖고 있는 것입니다. 대부분 일반 프로세스들은 CFS 스케줄러 클래스에 등록해서 CFS 스케줄러로 스케줄링됩니다. 

대부분 일반 프로세스들은 CFS 스케줄러 클래스에 등록해서 CFS 스케줄러로 스케줄링됩니다. 

하지만 프로세스 우선 순위가 올라가서 선점되지 않고 CPU를 점유하면서 지속적으로 어떤 일을 수행하기 위해선 CFS 스케줄러 클래스보다 우선 순위가 높은 RT 스케줄러를 써야 합니다. 이 때 프로세스는 간단히 스케줄러 클래스만 바꿔주면 스케줄러를 사용할 수 있습니다.

커널은 프로세스가 유연하게 스케줄러를 변경할 수 있는 인터페이스인 스케줄러 클래스를 제공하는 것입니다. 리눅스 커널은 프로세스가 다양한 상황에서 유연하게 스케줄링할 수 있도록 5개의 스케줄러를 제공합니다.


이렇게 스케줄링 클래스는 다양한 스케줄러가 공존하면서 유연하게 실행하도록 스케줄링 동작을 모듈화한 자료구조입니다. 또한 스케줄링 클래스는 스케줄러 별 세부 함수 연산을 할 수 있는 함수 포인터 형식의 메소드를 제공합니다. 즉, 스케줄링 클래스 메소드를 통해 리눅스 커널에서 제공하는 5가지 스케줄러 세부 함수에 접근합니다.
 

위 그림을 보면서 스케줄링 클래스 개념에 대해서 조금 더 알아봅시다.

프로세스는 생성됨과 동시에 스케줄링 클래스를 등록합니다. 이후 리눅스 커널에서 지원하는 5개 스케줄러 세부 함수를 호출하기 위해서는 current->sched_class에 등록된 함수 메소드를 호출합니다.

리눅스 커널에서는 기본으로 5개의 스케줄러를 지원합니다. 5개 스케줄러에 대한 소개는 다음 테이블에서 확인할 수 있습니다.
 

각각 5개의 스케줄러 동작을 모듈화해서 struct sched_class 이란 구조체로 선언하고 프로세스 태스크 디스크립터 sched_class 필드에 등록하는 것입니다.

스케줄링 클래스를 도입한 이유는 무엇일까요?
가장 큰 이유는 프로세스가 스케줄링 클래스를 통해 유연하게 스케줄러를 변경할 수 있습니다. 

이해를 돕기 위해 한 가지 예를 들겠습니다. 
대부분 일반 프로세스는 CFS 스케줄러에 의해 실행 흐름이 관리됩니다. 그런데 프로세스 실행 중 중요한 데이터나 코드가 선점되지 않고 CPU를 점유하면서 지속적으로 처리해야 하는 상황이 생길 수 있습니다. 이 때 CFS 스케줄러보다 RT 스케줄러를 쓰면 계속 CPU를 점유하면서 실행할 수 있습니다.

이럴 때 리눅스 커널에서 지원하는 5가지 스케줄러 클래스 중 RT 클래스를 스케줄링 클래스로 등록하면 됩니다.

이번에 커널에서 스케줄링 클래스는 왜 알아야 하는지 생각해 봅니다.
스케줄링 클래스가 어떤 자료구조로 구현돼 있는지 모르면 스케줄링 관련 코드를 읽을 수 없습니다. 그 이유는 스케줄러 세부 함수들은 모두 스케줄링 클래스를 통해 접근하는 구조로 설계됐기 때문입니다. 따라서 스케줄링 코드 전체 흐름을 이해하기 위해 스케줄링 클래스를 이해해야 합니다. 커널 스케줄러 세부 알고리즘만큼 코드 전체 흐름도를 파악하는 것이 중요합니다.

스케줄링 클래스 자료구조 소개
스케줄링 클래스는 리눅스 커널이 기본으로 지원하는 5가지 스케줄러 세부 동작을 모듈화한 자료 구조입니다.

먼저 struct sched_class 구조체를 소개하겠습니다.
[https://elixir.bootlin.com/linux/v4.14.70/source/kernel/sched/sched.h]
1 struct sched_class {
2 const struct sched_class *next;
3
4 void (*enqueue_task) (struct rq *rq, struct task_struct *p, int flags);
5 void (*dequeue_task) (struct rq *rq, struct task_struct *p, int flags);
6 void (*yield_task) (struct rq *rq);
7 bool (*yield_to_task) (struct rq *rq, struct task_struct *p, bool preempt);
8
9 void (*check_preempt_curr) (struct rq *rq, struct task_struct *p, int flags);
10
11 struct task_struct * (*pick_next_task) (struct rq *rq,
12 struct task_struct *prev,
13 struct rq_flags *rf);
14 void (*put_prev_task) (struct rq *rq, struct task_struct *p);
15 int  (*select_task_rq)(struct task_struct *p, int task_cpu, int sd_flag, int flags);
16 void (*migrate_task_rq)(struct task_struct *p);
17 void (*task_woken) (struct rq *this_rq, struct task_struct *task);
18 void (*set_cpus_allowed)(struct task_struct *p,
 const struct cpumask *newmask);

2 번째 줄 *next 필드를 제외하고 모든 필드가 포인터 형식 메소드입니다.
다음 테이블에서 스케줄링 클래스 필드 중 중요한 항목을 확인할 수 있습니다.
enqueue_task: 프로세스가 실행 가능한 상태로 진입
dequeue_task: 프로세스가 더 이상 실행 가능한 상태가 아닐때
yield_task: 프로세스가 스스로 yield() 시스템콜을 실행했을 때
check_preempt_curr: 현재 실행 중인 프로세스를 선점(preempt)할 수 있는지 검사
pick_next_task: 실행할 다음 프로세스를 선택
put_prev_task: 실행중인 태스크를 다시 내부 자료구조에 큐잉
load_balance: 코어 스케줄러가 태스크 부하를 분산하고자 할때
set_curr_task: 태스크의 스케줄링 클래스나 태스크 그룹을 바꿀때
task_tick: 타이머 틱 함수가 호출

struct sched_class 자료구조에 대해 소개를 했으니 이어서 5가지 스케줄러 클래스에 대해서 살펴보겠습니다.

세부 스케줄러 클래스 소개
다음 코드와 같이 struct sched_class 구조체로 스케줄러 클래스를 정의할 수 있습니다. 
[https://elixir.bootlin.com/linux/v4.14.70/source/kernel/sched/sched.h]
1 extern const struct sched_class stop_sched_class;
2 extern const struct sched_class dl_sched_class;
3 extern const struct sched_class rt_sched_class;
4 extern const struct sched_class fair_sched_class;
5 extern const struct sched_class idle_sched_class;

1~5 번째 줄에서 볼 수 있는 5가지 전역 변수를 스케줄러 클래스라고 합니다. 각각 전역 변수를 열어보면 struct sched_class 구조체에 스케줄러 별로 실행하는 함수를 볼 수 있습니다. 예를 들어 RT Class 스케줄러 클래스 선언부를 보겠습니다.
[https://elixir.bootlin.com/linux/v4.14.70/source/kernel/sched/rt.c]
1 const struct sched_class rt_sched_class = {
2 .next = &fair_sched_class,
3 .enqueue_task = enqueue_task_rt,
4 .dequeue_task = dequeue_task_rt,
5 .yield_task = yield_task_rt,
6
7 .check_preempt_curr = check_preempt_curr_rt,
8
9 .pick_next_task = pick_next_task_rt,
10 .put_prev_task = put_prev_task_rt,
...
11 .get_rr_interval = get_rr_interval_rt,
12
13 .prio_changed = prio_changed_rt,
14 .switched_to = switched_to_rt,
15
16 .update_curr = update_curr_rt,
17 };

위 코드를 보면 struct sched_class 구조체 필드에 RT 스케줄러 세부 함수를 선언했습니다. 이렇게 스케줄러 동작을 모듈화한 struct sched_class 구조체 필드에 스케줄러별 세부 함수를 등록한 것이 스케줄러 클래스라고 볼 수 있습니다.

다음 그림에서 5가지 스케줄러 클래스와 세부 함수를 볼 수 있습니다.
 

정리하면 스케줄러 클래스는 스케줄러 세부 동작을 표현한 인터페이스용 객체라고 볼 수 있습니다.

스케줄링 클래스 사이에 우선 순위가 있습니다.
 

우선 순위가 가장 큰 스케줄링 클래스는 stop_sched_class인데 오른쪽 화살표 방향 순서 우선 순위가 줄어듭니다. RT 스케줄링 클래스(rt_sched_class)는 CFS 스케줄링 클래스(fair_sched_class)보다 우선 순위가 높습니다.

그러면 스케줄러에서 스케줄링 클래스 우선 순위에 따라 스케줄링 클래스를 처리하는 코드는 무엇일까요? 정답은 for_each_class() 함수입니다. 스케줄링 클래스를 순회할 때 for_each_class() 함수를 호출합니다.

for_each_class() 함수 구현부를 보면 stop_sched_class 전역변수로 시작해서 class->next에 지정된 다음 우선 순위 스케줄러 클래스 전역 변수에 접근합니다. 이는 우선 순위 방향으로 연결된 스케줄링 클래스를 순차적으로 호출하는 것입니다.

이해를 돕기 위해 stop_sched_class 부터 idle_sched_class 전역 변수 선언부를 소개합니다.
1 const struct sched_class stop_sched_class = {
2 .next = &dl_sched_class,
...
3 const struct sched_class dl_sched_class = {
4 .next = &rt_sched_class,
...
5 const struct sched_class rt_sched_class = {
6 .next = &fair_sched_class,
...
7 const struct sched_class fair_sched_class = {
8 .next = &idle_sched_class,
...
9 const struct sched_class idle_sched_class = {
10 /* .next is NULL */

스케줄링 클래스 구조체인 struct sched_class 의 첫 번째 필드는 struct sched_class next입니다. next 필드에 다음 우선 순위 스케줄링 클래스를 지정하는 것입니다.

다음 코드와 같이 struct sched_class 구조체 첫 번째 필드는 const struct sched_class *next입니다.
[https://elixir.bootlin.com/linux/v4.14.70/source/kernel/sched/sched.h]
1 struct sched_class {
2 const struct sched_class *next;
3
4 void (*enqueue_task) (struct rq *rq, struct task_struct *p, int flags);

이번에는 커널 스케줄링 공통 코드에서 for_each_class() 함수를 어떻게 쓰는지 알아봅시다.
[https://elixir.bootlin.com/linux/v4.14.70/source/kernel/sched/core.c]
1 void check_preempt_curr(struct rq *rq, struct task_struct *p, int flags)
2 {
3 const struct sched_class *class;
4
5 if (p->sched_class == rq->curr->sched_class) {
6 rq->curr->sched_class->check_preempt_curr(rq, p, flags);
7 } else {
8 for_each_class(class) {
9 if (class == rq->curr->sched_class)
10 break;
11 if (class == p->sched_class) {
12 resched_curr(rq);
13 break;
14 }
15 }
16 }

check_preempt_curr() 함수 8 번째 줄 코드에서 for_each_class(class) 를 볼 수 있습니다. for_each_class() 코드는 stop_sched_class 전역변수로 시작해서 다음 전역 변수를 순회하는 동작입니다.
[https://elixir.bootlin.com/linux/v4.14.70/source/kernel/sched/sched.h]
#define for_each_class(class)
   for (class = sched_class_highest; class; class = class->next)

#define sched_class_highest (&stop_sched_class)

for_each_class() 함수 구현부를 보면 stop_sched_class 전역변수로 시작해서 class->next에 지정된 다음 우선 순위 스케줄러 클래스 전역 변수에 접근합니다. 이는 우선 순위 방향으로 연결된 스케줄링 클래스를 순차적으로 호출하는 것입니다.

우선 순위가 가장 큰 스케줄링 클래스는 stop_sched_class인데 오른쪽 화살표 방향으로 우선 순위가 줄어듭니다.

배운 내용을 참고해서 다음 코드는 어떻게 해석할 수 있을까요?
8 for_each_class(class) {
9 if (class == rq->curr->sched_class)
10 break;
11 if (class == p->sched_class) {
12 resched_curr(rq);
13 break;
14 }
15 }

먼저 8~15 번째 사이 코드 실행 조건을 봅시다. 이 구간 코드는 stop_sched_class 전역 변수부터 idle_sched_class 전역 변수까지 순회합니다.

다음 9 번째 줄 코드를 보면 현재 CPU를 점유하면서 실행 중인 프로세스의 스케줄링 클래스와 같은지 점검합니다. 

만약 현재 CPU를 점유하면서 실행 중인 프로세스가 RT 스케줄링 클래스에 등록돼 있으면
stop_sched_class, dl_sched_class 변수를 순회한 다음 rt_sched_class 전역 변수와 비교한 후 10 번째 줄 break 문에서 for_each_class 문을 빠져 나올 것입니다. RT 스케줄링 클래스로 등록된 프로세스는 current->sched_class = &rt_sched_class로 지정돼 있기 때문입니다.


"혹시 궁금한 점이 있으면 댓글로 질문 남겨주세요. 아는 한 성실히 답변 올려드리겠습니다!" 

Thanks,
Austin Kim(austindh.kim@gmail.com)

Reference(프로세스 스케줄링)

스케줄링 소개
프로세스 상태 관리
   어떤 함수가 프로세스 상태를 바꿀까?
스케줄러 클래스
런큐
CFS 스케줄러
   CFS 관련 세부 함수 분석  
선점 스케줄링(Preemptive Scheduling)   
프로세스는 어떻게 깨울까?
스케줄링 핵심 schedule() 함수 분석
컨택스트 스위칭
스케줄링 디버깅
   스케줄링 프로파일링
     CPU에 부하를 주는 테스트   
     CPU에 부하를 주지 않는 테스트





핑백

덧글

댓글 입력 영역