Linux Kernel(4.19) Hacks

rousalome.egloos.com

포토로그 Kernel Crash


통계 위젯 (화이트)

98258
1323
114600


[리눅스커널] 스케줄링: CFS 세부 함수 분석 - 타임 슬라이스 관리 10. Process Scheduling

CFS 스케줄러는 지속적으로 프로세스의 타임 슬라이스를 관리합니다.

타임 슬라이스를 소진했을 때 선점 요청

프로세스가 자신에게 주어진 타임 슬라이스를 다 소진하면 선점됩니다. 즉 CPU에서 실행을 멈추고 CPU를 비우게 되는 것입니다.

이제 타임 슬라이스가 무엇인지 알아봤으니 커널에서 타임 슬라이스를 어떤 방식으로 관리하는지 소스 코드를 분석하겠습니다.

커널에서 다음 함수가 실행할 때 프로세스 타임 슬라이스를 업데이트합니다. 
scheduler_tick()
task_tick_fair()
check_preempt_tick()

위 함수들은 다음과 같은 역할을 수행합니다.
1. 프로세스가 타임 슬라이스를 소진했는지 점검합니다.
2. 프로세스 타임 슬라이스를 업데이트합니다.
3. 만약 프로세스가 타임 슬라이스를 모두 소진했으면 프로세스 struct thread_info 에 선점할 조건임을 마킹합니다.

scheduler_tick() 함수 분석하기

먼저 타이머 인터럽트가 발생한 후 호출되는 scheduler_tick() 함수를 보겠습니다.
[https://github.com/raspberrypi/linux/blob/rpi-4.19.y/kernel/sched/core.c]
1 void scheduler_tick(void)
2 {
3 int cpu = smp_processor_id();
4 struct rq *rq = cpu_rq(cpu);
5 struct task_struct *curr = rq->curr;
6 struct rq_flags rf;
7
8 sched_clock_tick();
9
10 rq_lock(rq, &rf);
11
12 update_rq_clock(rq);
13 curr->sched_class->task_tick(rq, curr, 0);
...
14 }

먼저 5 번째 줄 코드를 보겠습니다.
5 struct task_struct *curr = rq->curr;

런큐 필드 중 curr는 현재 CPU에서 실행 중인 프로세스의 태스크 디스크립터 주소를 가르킵니다. 이를 curr이란 포인터형 지역 변수에 저장합니다.

8 번째 줄 코드를 보겠습니다.
8 sched_clock_tick();

sched_clock_tick() 함수를 호출해서 스케줄 클럭 틱 정보를 업데이트합니다.

다음 13번째 줄 코드를 보겠습니다.
13 curr->sched_class->task_tick(rq, curr, 0);

프로세스 태스크 디스크립터 sched_class 필드 중 task_tick 메소드를 호출합니다. 일반 프로세스는 CFS 스케줄러 클래스를 쓰므로 task_tick 메소드는 task_tick_fair() 함수를 호출합니다.

T32 프로그램으로 CFS 스케줄러 클래스의 task_tick 메소드를 보면 task_tick_fair() 함수로 지정된 것을 확인할 수 있습니다.
(static void (*)()) fair_sched_class.task_tick = 0x8014FF34 = task_tick_fair

task_tick_fair() 함수 분석하기 

다음으로 task_tick_fair() 함수를 보겠습니다. 
[https://github.com/raspberrypi/linux/blob/rpi-4.19.y/kernel/sched/fair.c]
1 static void task_tick_fair(struct rq *rq, struct task_struct *curr, int queued)
2 {
3 struct cfs_rq *cfs_rq;
4 struct sched_entity *se = &curr->se;
5
6 for_each_sched_entity(se) {
7  cfs_rq = cfs_rq_of(se);
8  entity_tick(cfs_rq, se, queued);
9 }
...
}

8 번째 줄 코드를 보겠습니다.
8  entity_tick(cfs_rq, se, queued);

entity_tick() 함수를 호출합니다.

다음 entity_tick() 함수 코드 리뷰를 진행하겠습니다.
[https://github.com/raspberrypi/linux/blob/rpi-4.19.y/kernel/sched/fair.c]
1 static void
2 entity_tick(struct cfs_rq *cfs_rq, struct sched_entity *curr, int queued)
3 {
4 update_curr(cfs_rq);
5
6 update_load_avg(curr, UPDATE_TG);
7 update_cfs_shares(curr);
...
8
9 if (cfs_rq->nr_running > 1)
10  check_preempt_tick(cfs_rq, curr);
11 }

9 번째 줄 코드를 보겠습니다.
9 if (cfs_rq->nr_running > 1)
10  check_preempt_tick(cfs_rq, curr);

현재 런큐에서 Enequeue된 프로세스 개수가 1보다 많으면 check_preempt_tick() 함수를 호출합니다. 여기서 rq는 해당 CPU 런큐 구조체를 가르키는 포인터이고 curr는 프로세스 태스크 디스크립터 주소를 가르킵니다.

check_preempt_tick() 함수 분석하기

이어서 check_preempt_tick() 함수를 봅시다.
[https://github.com/raspberrypi/linux/blob/rpi-4.19.y/kernel/sched/fair.c]
01 static void
02 check_preempt_tick(struct cfs_rq *cfs_rq, struct sched_entity *curr)
03 {
04 unsigned long ideal_runtime, delta_exec;
05 struct sched_entity *se;
06 s64 delta;
07
08 ideal_runtime = sched_slice(cfs_rq, curr);
09 delta_exec = curr->sum_exec_runtime - curr->prev_sum_exec_runtime;
10 if (delta_exec > ideal_runtime) {
11  resched_curr(rq_of(cfs_rq));
...
12 }
13
14 if (delta_exec < sysctl_sched_min_granularity)
15  return;
16
17 se = __pick_first_entity(cfs_rq);
18 delta = curr->vruntime - se->vruntime;
19
20 if (delta < 0)
21  return;
22
23 if (delta > ideal_runtime)
24  resched_curr(rq_of(cfs_rq));
25 }

이 함수 동작은 다음과 같은 동작 단계로 분류할 수 있습니다.

1단계: 프로세스가 소진한 타임 슬라이스 읽기
현재 CPU에서 실행 중인 프로세스가 소진한 타임 슬라이스 정보를 읽습니다.

2단계: 프로세스 선점 요청
만약 프로세스가 타임 슬라이스를 소진했으면 resched_curr() 함수를 호출해서 선점 요청을 합니다. 즉, 현재 실행 중인 프로세스 struct thread_info flags 필드에 TIF_NEED_RESCHED 를 설정합니다.


프로세스 선점 요청을 하면 다음 조건에서 프로세스는 선점됩니다.
인터럽트를 핸들링한 후
시스템 콜을 핸들링한 후 유저 공간으로 복귀하기 전

함수 동작을 단계별로 리뷰했으니 이제 세부 코드를 분석하겠습니다.

먼저 8 번째 줄 코드를 보겠습니다.
8 ideal_runtime = sched_slice(cfs_rq, curr);

sched_slice() 함수를 호출해서 프로세스의 타임 슬라이스를 읽습니다. 

다음 9 번째 줄 코드를 분석하겠습니다.
9 delta_exec = curr->sum_exec_runtime - curr->prev_sum_exec_runtime;

프로세스가 사용한 타임 슬라이스 소진 시각을 계산해 delta_exec 지역 변수에 저장합니다.

이번 소절에서 가장 중요한 코드입니다.
10 if (delta_exec > ideal_runtime) {
11  resched_curr(rq_of(cfs_rq));
...
12 }

만약 delta_exec 가 ideal_runtime 보다 클 경우 resched_curr() 함수를 호출합니다. 프로세스가 타임 슬라이스를 소진했을 때 delta_exec 가 ideal_runtime 보다 큽니다. 이 조건에서 resched_curr() 함수를 호출해서 해당 프로세스에 대해 선점 요청을 합니다.

여기서 프로세스 선점 요청이란 어떤 동작을 의미일까요?  

    프로세스 struct thread_info 구조체 flags 필드에 TIF_NEED_RESCHED 를 설정하는 
     동작이다.

다음 14 번째 줄 코드를 보겠습니다.
14 if (delta_exec < sysctl_sched_min_granularity)
15  return;

프로세스가 부여받는 최소 실행 시각보다 타임 슬라이스가 작은 경우 15 번째 줄 코드를 실행해 함수를 바로 종료합니다.

다음 17~21 번 째 줄 코드를 보겠습니다.
17 se = __pick_first_entity(cfs_rq);
18 delta = curr->vruntime - se->vruntime;
19
20 if (delta < 0)
21  return;

현재 프로세스가 레드 블랙 트리에서 가장 왼쪽 노드에 있는지 체크합니다. 이 경우 21 번째 줄 코드를 실행해 함수를 종료합니다.

여기까지 스케줄러가 프로세스의 타임 슬라이스를 관리하는 코드를 분석했습니다. 분석 내용을 정리해보겠습니다.
1. 프로세스가 CPU에서 실행 중일 때 스케줄러는 프로세스가 타임 슬라이스를 모두 소진했는지 주기적으로 체크합니다. 
2. 만약 프로세스가 타임 슬라이스를 모두 소진하면 프로세스 struct thread_info 구조체 flags 필드에 TIF_NEED_RESCHED 를 설정합니다.

프로세스는 이 사실을 모릅니다. 프로세스는 자신에게 주어진 임무를 수행하려고 커널 코드를 수행 중일 것입니다. 프로세스가 타임 슬라이스를 모두 소진해 선점되면 프로세스는 실행 대기(TASK_RUNNING) 상태로 런큐에 남습니다. 선점 스케줄링 시 런큐에서 선점될 프로세스를 Dequeue되지 않습니다. 

이번 소절에서는 스케줄러가 프로세스가 타임 슬라이스를 관리하는 세부 동작을 살펴봤습니다. 이어서 다음에 vruntime 을 관리하는 세부 커널 코드를 살펴보겠습니다. 


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

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

Reference(프로세스 스케줄링)

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




핑백

덧글

댓글 입력 영역