Arm Linux Kernel Hacks

rousalome.egloos.com

포토로그 Kernel Crash


통계 위젯 (화이트)

41111
637
415417


[리눅스커널] 스케줄링: 스케줄링 핵심 schedule() 함수 분석하기 10. 프로세스 스케줄링

선점 스케줄링과 비선점 스케줄링의 진입 경로는 서로 다릅니다. 공통으로 둘다 schedule() 함수를 호출합니다. 이번 소절에는 스케줄링 핵심 동작을 __schedule() 함수 코드를 분석하면서 살펴보겠습니다.

우리는 스케줄링하면 schedule() 함수를 생각할 수 있는데 핵심 동작은 schedule() 함수에서 호출하는 __schedule() 함수에서 실행합니다.
 
__schedule() 함수에서 호출하는 주요 함수는 다음과 같습니다.

1. pick_next_task() 
다음에 CPU에서 실행할 프로세스 선택
다음에 실행할 프로세스 정보는 next 변수(태스크 디스크립터)로 로딩
2. clear_preempt_need_resched()
prev 프로세스의 struct thread_info flags 필드에 저장된 TIF_NEED_RESCHED 비트를 Clear 
current_thread_info()->preempt_count 0으로 초기화
3. trace_sched_switch()
sched_switch ftrace 메시지 출력

context_switch()
next 변수에 저장된 프로세스를 실행
prev 변수에 저장된 프로세스는 레지스터 백업 후 휴면에 진입

<< 스케줄링 핵심 schedule() 함수 분석하기 >>

'프로세스 스케줄링'에 대한 세부 구현 방식을 파악하려면 __schedule() 함수를 분석할 필요가 있습니다. 먼저 __schedule() 함수 코드를 소개합니다. 
[https://github.com/raspberrypi/linux/blob/rpi-4.19.y/kernel/sched/core.c]
1 static void __sched notrace __schedule(bool preempt)
2 {
3 struct task_struct *prev, *next;
4 unsigned long *switch_count;
5 struct rq_flags rf;
6 struct rq *rq;
7 int cpu;
8
9 cpu = smp_processor_id();
10 rq = cpu_rq(cpu);
11 prev = rq->curr;
...
12 next = pick_next_task(rq, prev, &rf);
13 clear_tsk_need_resched(prev);
14 clear_preempt_need_resched();
15
16 if (likely(prev != next)) {
17 rq->nr_switches++;
18 rq->curr = next;
19
20 ++*switch_count;
21
22 trace_sched_switch(preempt, prev, next);
23
24 rq = context_switch(rq, prev, next, &rf);
25 } else {
26 rq->clock_update_flags &= ~(RQCF_ACT_SKIP|RQCF_REQ_SKIP);
27 rq_unlock_irq(rq, &rf);
28 }
29
30 balance_callback(rq);
31 }

중요한 동작을 하는 코드니 상세히 분석하겠습니다.

먼저 분석할 코드는 9~11 번째 줄입니다.
9 cpu = smp_processor_id();
10 rq = cpu_rq(cpu);
11 prev = rq->curr;

9 번째 줄은 현재 프로세스 코드를 실행 중인 CPU 번호를 cpu이란 지역 변수로 저장하는 코드입니다. 10번째 줄은 cpu 번호를 입력으로 per-cpu 타입 변수인 런큐 주소를 읽습니다.

만약 현재 실행 중인 CPU 번호가 2이면 2 번째 런큐 per-cpu 주소를 읽어옵니다.


런큐는 per-cpu 타입 변수로 cpu 갯수 메모리 공간을 할당합니다. per-cpu 타입 변수는 cpu 갯수만큼 배열로 볼 필요가 있습니다.


11 번째 줄 코드는 중요한 정보를 담고 있습니다.
런큐 curr 필드에 저장된 태스크 디스크립터 주소를 prev에 저장합니다. 런큐 구조체 struct rq curr 필드는 어떤 정보를 저장할까요?  

    CPU에서 실행 중인 프로세스 태스크 디스크립터 주소다. 

다음 12~14번째 줄 코드를 보겠습니다.
12 next = pick_next_task(rq, prev, &rf);
13 clear_tsk_need_resched(prev);
14 clear_preempt_need_resched();

12 번째 줄 코드는 pick_next_task() 함수를 호출합니다. 여기서 pick_next_task() 함수는 어떤 정보를 next에 저장할까요?  

    다음에 실행할 프로세스의 태스크 디스크립터를 주소를 next 지역변수에 저장한다.


pick_next_task() 함수는 런큐에 있는 실행 대기(TASK_RUNNING) 상태 프로세스들 중에 우선순위가 가장 높은 프로세스를 선택합니다. 일반 프로세스인 경우 레브 블랙 트리 가장 왼쪽 노드에 있는 프로세스의 태스크 디스크립터를 반환합니다.


다음 13 번째 줄 코드를 보겠습니다.
13 clear_tsk_need_resched(prev);

prev 프로세스의 struct thread_info flags 필드에 저장된 TIF_NEED_RESCHED 비트를 클리어(Clear)합니다. flags 필드를 2에서 0으로 바꾸는 연산입니다. 


clear_tsk_need_resched() 함수 세부 코드를 열어 보겠습니다.
[https://github.com/raspberrypi/linux/blob/rpi-4.19.y/include/linux/sched.h]
01 static inline void clear_tsk_need_resched(struct task_struct *tsk)
02 {
03 clear_tsk_thread_flag(tsk,TIF_NEED_RESCHED);
04 }
05 static inline void clear_tsk_thread_flag(struct task_struct *tsk, int flag)
06 {
07 clear_ti_thread_flag(task_thread_info(tsk), flag);
08 }

clear_tsk_need_resched() 함수는 프로세스 태스크 디스크립터인 *tak 인자를 받아
03 번째 줄 코드와 같이 clear_tsk_thread_flag() 함수 첫 번째 인자로 전달합니다.

clear_tsk_thread_flag() 함수 두 번째 인자로 TIF_NEED_RESCHED를 전달합니다.
TIF_NEED_RESCHED는 2입니다.

다음 05 번째 줄 clear_tsk_thread_flag() 함수 코드를 보겠습니다.
07 번째 줄과 같이 task_thread_info(tsk) 함수를 호출해 프로세스 스택 최상단 주소를 반환합니다.

task_thread_info() 함수 선언부는 다음과 같습니다.
[https://github.com/raspberrypi/linux/blob/rpi-4.19.y/include/linux/sched.h]
define task_thread_info(task) ((struct thread_info *)(task)->stack)

07 번째 줄 코드를 알기 쉬운 형식으로 변환하면 다음과 같습니다.
07 clear_ti_thread_flag(task_thread_info(tsk), flag);
07 clear_ti_thread_flag(프로세스 스택 최상단 주소, 2(TIF_NEED_RESCHED));

다음 clear_ti_thread_flag() 함수 코드를 보겠습니다.
[https://github.com/raspberrypi/linux/blob/rpi-4.19.y/include/linux/thread_info.h]
09 static inline void clear_ti_thread_flag(struct thread_info *ti, int flag)
10 {
11 clear_bit(flag, (unsigned long *)&ti->flags);
12 }

[https://github.com/raspberrypi/linux/blob/rpi-4.19.y/arch/arm/include/asm/bitops.h]
13 #define clear_bit(nr,p) ATOMIC_BITOP(clear_bit,nr,p)

11 번째 줄 코드와 같이 clear_bit() 함수를 호출해 ATOMIC_BITOP() 매크로 함수를 실행합니다.

프로세스 struct thread_info 구조체 flags 필드는 여러 커널 함수에서 잦은 빈도로 접근합니다. clear_tsk_need_resched() 함수를 호출해 비트를 바꾸면 원자적(Atomic Operation)으로 비트를 변경할 수 있습니다. 원자적인 동작을 지원하는 커널 함수를 쓰면 커널 동기화 문제를 미리 방지할 수 있습니다.

프로세스 struct thread_info 구조체 flags 필드는 프로세스 스케줄링을 수행할 때 선점 여부를 결정하는 중요한 정보입니다.


이번에 볼 코드는 스케줄링 핵심 루틴인 16~24번째 줄입니다.
16 if (likely(prev != next)) {
17 rq->nr_switches++;
18 rq->curr = next;
19
20 ++*switch_count;
21
22 trace_sched_switch(preempt, prev, next);
23
24 rq = context_switch(rq, prev, next, &rf);

16 번째 줄 if 조건문을 체크해볼까요? 
prev와 next가 다른 경우 17 번째 줄 코드를 실행합니다. 일반적인 상황에서 prev와 next다릅니다.  

    다음에 실행될 프로세스가 현재 실행 중인 프로세스와 다른지 체크하는 동작입니다. 

17번째 줄 코드를 보겠습니다.
17 rq->nr_switches++;

런큐 struct rq 구조체 nr_switches 필드를 +1만큼 증감합니다. 컨텍스트 스위칭 횟수 정보를 저장합니다.  

    시스템에서 컨텍스트 스위칭 빈도를 점검하고 싶을 때 참고할 수 있는 데이터입니다.

다음 18번째 줄 코드입니다.
18 rq->curr = next;

next는 컨텍스트 스위칭으로 다음에 실행할 프로세스 태스트 디스크립터 주소입니다.
이 주소를 런큐 struct rq 구조체 curr 필드에 저장합니다.  

    struct rq 필드 중 curr는 현재 CPU를 점유하면서 실행 중인 프로세스 태스크 
    디스크립터 주소를 가르킵니다. 

이어서 22~24 번째 줄 코드를 보겠습니다.
22 trace_sched_switch(preempt, prev, next);
23
24 rq = context_switch(rq, prev, next, &rf);

22번째 줄 코드에서 trace_sched_switch() 함수를 호출해 sched_switch ftrace 이벤트 로그를 출력합니다. 여기서 trace_sched_switch() 함수가 context_switch() 함수 바로 이전에 실행한다는 점이 중요합니다. 이로 context_switch() 함수가 컨텍스트 스위칭을 수행한다는 사실을 알 수 있습니다.


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

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

Reference(프로세스 스케줄링)

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

# Reference: For more information on 'Linux Kernel';

디버깅을 통해 배우는 리눅스 커널의 구조와 원리. 1

디버깅을 통해 배우는 리눅스 커널의 구조와 원리. 2








핑백

덧글

댓글 입력 영역