Linux Kernel(4.19) Hacks

rousalome.egloos.com

포토로그 Kernel Crash


통계 위젯 (화이트)

230224
1178
109352


[리눅스커널][스케줄링] 선점 스케줄링 지연 함수 preempt_enable()/preempt_disable() 소개 10. Process Scheduling

선점 스케줄링 지연 함수 preempt_enable()/preempt_disable() 소개

리눅스 커널에서 선점 스케줄링을 동작을 잠시 지연할 수 있는 함수를 지원합니다. 
preempt_disable(): 선점 지연 활성화
preempt_enable():  선점 지연 비활성화

preempt_disable() 함수 이름은 "선점"을 의미하는 "preempt" 단어에 "불능"란 단어인 "disable"의 조합입니다.
즉, preempt_disable() 함수를 실행하면 선점 스케줄을 지연할 수 있습니다.

마찬가지로 preempt_enable() 함수를 실행하면 선점 스케줄링을 활성화합니다.    

preempt_disable() 함수와 preempt_enable() 함수의 기본 원리를 알아보기 앞서 커널 모드에서 선점 스케줄링을 시작한는 조건을 떠올려 봅시다. 
커널 모드에서 커널 함수 코드가 실행할 때 다음 조건에서 선점 스케줄링 실행을 시도합니다.
조건
  :프로세스 struct thread_info 구조체 preempt_count 필드가 0 이고 flags 필드가 TIF_NEED_RESCHED일 때
언제
  :인터럽트 핸들링 후 

1초에 수 백번 발생하는 인터럽트 빈도만큼 커널 모드에서 선점 스케줄링을 점검하는 것입니다. 
이렇게 선점 스케줄링을 시도하는 빈도가 높으니 우리가 작성한 어떤 코드도 실행 도중 선점될 수 있있습니다.
  
선점 스케줄링을 점검할 때 프로세스 struct thread_info 구조체 preempt_count 필드가 0인지를 점검합니다. 만약 preempt_count 필드가 0이고 flags 필드가 _TIF_NEED_RESCHED 이면 선점 스케줄링을 시작합니다.

그런데 preempt_disable() 함수를 실행하면 preempt_count 필드를 +1만큼 증감시킵니다.
선점 스케줄링 실행 조건을 점검할 때 프로세스 struct thread_info 구조체 preempt_count 필드가 1이면 선점 스케줄링을 시도하지 않기 때문입니다.

preempt_enable() 함수는 preempt_disable() 함수와 반대로 동작합니다.
강제로 +1만큼 증감시킨 preempt_count 필드를 -1만큼 증감해서 원래 값으로 복원시킵니다. 
또한 스케줄링 조건을 점검한 후 일을 만족하면 바로 스케줄링을 실행합니다.

그러면 preempt_disable() 함수와 preempt_enable() 함수는 어떻게 호출해야 선점 스케줄링을 지연할 수 있을까요? 선점 스케줄링을 실행하면 안되는 코드 블락 시작과 끝 부분에 preempt_disable() 함수와 preempt_enable() 함수를 각각 호출하면 됩니다. 

preempt_disable() 함수와 preempt_enable() 함수에 대해 소개했으니 이번엔 코드 분석으로 세부 동작을 알아봅시다.

먼저 분석할 preempt_disable() 코드는 다음과 같습니다.
[https://elixir.bootlin.com/linux/v4.14.70/source/include/linux/preempt.h]
1 #define preempt_disable() \
2 do { \
3 preempt_count_inc(); \
4 barrier(); \
5 } while (0)

3번째 줄 코드를 보면 preempt_count_inc() 함수를 호출할 뿐 특별한 동작은 없습니다.

4번째 줄 코드에서 barrier() 함수를 호출해서 GCC 컴파일러가 코드 위치를 바꾸지 못하도록 설정합니다.

preempt_count_inc() 함수 코드를 보겠습니다. 
[https://elixir.bootlin.com/linux/v4.14.70/source/include/linux/preempt.h]
1 #define preempt_count_inc() preempt_count_add(1)
2 #define preempt_count_add(val) __preempt_count_add(val)

1~2번째 줄 코드를 보면 preempt_count_inc() 함수는 __preempt_count_add(1) 함수로 치환됩니다.

다음 __preempt_count_add() 코드를 보겠습니다.
[https://elixir.bootlin.com/linux/v4.14.70/source/include/asm-generic/preempt.h]
1 static __always_inline volatile int *preempt_count_ptr(void)
2 {
3 return &current_thread_info()->preempt_count;
4 }
5
6 static __always_inline void __preempt_count_add(int val)
7 {
8 *preempt_count_ptr() += val;
9 }

__preempt_count_add() 함수 8 번째 줄 코드를 보면 *preempt_count_ptr() 에 인자인 val을 더합니다.
__preempt_count_add() 함수를 호출할 때 val 인자가 1이니 *preempt_count_ptr()에 1만큼 더하는 연산입니다.

1 번째 줄 코드를 보면 preempt_count_ptr() 함수는 &current_thread_info()->preempt_count 를 반환합니다. 

current_thread_info() 함수를 호출해서 프로세스의 최상단 스택 공간 메모리에 접근해서 후 struct thread_info 구조체 preempt_count 필드에
+1만큼 증감하는 연산입니다.

프로세스 struct thread_info 구조체 preempt_count 필드를 +1만큼 증감했으니 선점 스케줄링을 시도할 수 없는 조건입니다.

이번엔 선점 스케줄링을 활성화하는 preempt_enable() 함수 코드를 보겠습니다.
[https://elixir.bootlin.com/linux/v4.14.70/source/include/linux/preempt.h]
1 #define preempt_enable() \
2 do { \
3 barrier(); \
4 if (unlikely(preempt_count_dec_and_test())) \
5 __preempt_schedule(); \
6 } while (0)

먼저 3 번째 줄 코드를 보겠습니다.
3 barrier(); \

메모리 베리어를 실행하는 코드로 GCC 컴파일러가 코드 최적화를 위해 코드 순서를 바꾸지 않도록 지정합니다.

다음 4~5번째 줄 코드입니다.
4 if (unlikely(preempt_count_dec_and_test())) \
5 __preempt_schedule(); \

preempt_count_dec_and_test() 함수는 프로세스 struct thread_info preempt_count 필드를 -1만큼 감소시킨 후 선점 스케줄링을 실행할 조건이면 1을 반환합니다. 이 조건에서 __preempt_schedule() 함수를 호출합니다.

다음 소절에서 알아보겠지만, __preempt_schedule() 함수는 스케줄링을 실행하는 역할을 수행합니다.

함수에 동작 흐름에 대해 살펴봤으니 코드 분석으로 세부 동작을 분석할 차례입니다.

preempt_count_dec_and_test() 함수 코드를 보면서 세부 동작을 점검합시다.
[https://elixir.bootlin.com/linux/v4.14.70/source/include/asm-generic/preempt.h]
1 #define preempt_count_dec_and_test() \
2 ({ preempt_count_sub(1); should_resched(0); })
3
4 static __always_inline bool should_resched(int preempt_offset)
5 {
6 return unlikely(preempt_count() == preempt_offset &&
7 tif_need_resched());
8 }

preempt_count_dec_and_test() 2 번째 줄 코드를 보면 2 개 함수를 호출합니다.
먼저 preempt_count_sub() 함수를 호출해서 프로세스 struct thread_info 구조체 preempt_count 필드를 -1만큼 증감합니다.

먼저 2번째 줄 preempt_count_sub() 함수를 실행하면 처리하는 함수 흐름을 살펴봅시다.

2번째 줄 preempt_count_sub() 함수를 실행하면 __preempt_count_sub() 함수를 호출해 인자로 전달된 val 값만큼 뺄셈 연산을 수행합니다.

current_thread_info()->preempt_count 값을 -1만큼 증감합니다.
[https://elixir.bootlin.com/linux/v4.14.70/source/include/linux/preempt.h]
#define preempt_count_sub(val) __preempt_count_sub(val)

[https://elixir.bootlin.com/linux/v4.14.70/source/include/asm-generic/preempt.h]
static __always_inline void __preempt_count_sub(int val)
{
*preempt_count_ptr() -= val;
}

프로세스의 최상단 스택 공간 메모리에 접근 후 struct thread_info 구조체 preempt_count 필드를 -1만큼 증감하는 연산입니다.

다음 preempt_count_dec_and_test() 함수에서 호출하는 should_resched() 함수를 분석합니다.

[https://elixir.bootlin.com/linux/v4.14.70/source/include/asm-generic/preempt.h]
1 #define preempt_count_dec_and_test() \
2 ({ preempt_count_sub(1); should_resched(0); })
3
4 static __always_inline bool should_resched(int preempt_offset)
5 {
6 return unlikely(preempt_count() == preempt_offset &&
7 tif_need_resched());
8 }

should_resched() 함수 6~7 번째 줄 코드를 보면 다음 조건을 점검합니다.
프로세스 struct thread_info 구조체 preempt_count 필드가 0인지 여부
프로세스 struct thread_info 구조체 flags 필드가 TIF_NEED_RESCHED인지 여부



tif_need_resched() 함수 코드를 보면 프로세스 struct thread_info 구조체 flags 필드가 TIF_NEED_RESCHED 인지를 체크합니다.
[https://elixir.bootlin.com/linux/v4.14.70/source/include/linux/thread_info.h]
#define tif_need_resched() test_thread_flag(TIF_NEED_RESCHED)


preempt_enable() 함수 세부 동작을 정리하면 다음과 같습니다.
프로세스 struct thread_info 구조체 preempt_count 필드를 -1만큼 증감
프로세스 struct thread_info 구조체 preempt_count 필드가 0이고 flags 필드가 TIF_NEED_RESCHED 점검
           : 만약 위 조건을 만족하면 __preempt_schedule() 함수를 호출해서 스케줄링을 
             실행 

선점 스케줄링을 지연할 코드 블락이 실행될 동안에 다음 상황이면 누군가 프로세스 struct thread_info 구조체 flags 필드를 TIF_NEED_RESCHED로 설정하면 스케줄링을 실행하는 것입니다. 

정리하면 current_thread_info()->preempt_count를 1만큼 감소시킨 결과값이 0이고 tif_need_resched() 함수가 1을 반환하면 __preempt_schedule() 함수를 실행하는 것입니다.

__preempt_schedule() 함수 실행으로 스케줄링을 시도합니다.

__preempt_schedule() 함수 분석
이어서 __preempt_schedule() 함수 코드를 분석하겠습니다. 
[https://elixir.bootlin.com/linux/v4.14.70/source/include/asm-generic/preempt.h]
1 #define __preempt_schedule() preempt_schedule()

__preempt_schedule() 함수는 preempt_schedule() 함수로 치환됩니다.

[https://elixir.bootlin.com/linux/v4.14.70/source/kernel/sched/core.c]
1 asmlinkage __visible void __sched notrace preempt_schedule(void)
2 {
3
4 if (likely(!preemptible()))
5 return;
6
7 preempt_schedule_common();
8 }

4 번째 줄 코드를 보겠습니다.
preemptible() 함수를 호출해서 선점 스케줄링을 시작할 조건인지 점검합니다.

이 후 7 번째 줄 코드와 같이 preempt_schedule_common() 함수를 호출합니다.
[https://elixir.bootlin.com/linux/v4.14.70/source/kernel/sched/core.c]
01 static void __sched notrace preempt_schedule_common(void)
02 {
03 do {
04 preempt_disable_notrace();
05 preempt_latency_start(1);
06 __schedule(true);
07 preempt_latency_stop(1);
08 preempt_enable_no_resched_notrace();
09
10 } while (need_resched());
11 }

06 번째 줄 코드를 보면 __schedule() 함수를 호출해서 스케줄링을 시작합니다.
__schedule() 함수 호출 전에 preempt_enable_no_resched_notrace() 함수를 호출해서 다시 프로세스 struct thread_info 구조체 flags 필드를 +1만큼 증감시킵니다.

이는 06 번째 줄 코드인 __schedule() 함수가 실행할 동안에 선점 스케줄링을 지연하는 것입니다. __schedule() 함수가 실행할 동안에 선점 스케줄링을 수행하면 다시 __schedule() 함수에 호출되는 것을 막기 위한 코드입니다.

언제 선점 스케줄링을 지연해야 할까?
그러면 언제 preempt_disable() 함수와 preempt_enable() 함수를 호출해서 선점 스케줄링을 잠시 비활성화할까요? 특정 상황과 조건에서 어떤 코드 구간이 실행할 때 선점이 되면 안 될 때가 있습니다. 예를 들어 하드웨어 디바이스에 정확한 딜레이를 주거나 실행 시간을 정확히 지켜야 하는 경우입니다.

preempt_disable() 함수와 preempt_enable() 함수를 써서 선점 스케줄링을 지연하는 예제를 들어 보겠습니다.

선점 스케줄링으로 코드 실행이 멈추면 안 되는 코드 블락 시작과 끝 부분에 preempt_disable() 함수와 preempt_enable() 함수를 호출하면 됩니다. 위 함수를 쓸 때 주의해야 할 사항은 preempt_disable() 함수와 preempt_enable() 함수를 페어로 같이 써야 한다는 점입니다.

만약 어떤 함수 코드가 다음과 같다고 가정합시다. 
1 preempt_disable();
2 /* 선점 불가 코드 구간 시작
3 A();
4 B();
5 */ 선점 불가 코드 구간 종료
6 preempt_enable();

선점이 되면 안되는 코드 시작점인 1 번째 줄에 preempt_disable() 함수를 호출해서 선점을 지연합니다. 다음 6 번째 줄과 같이 preempt_enable() 함수를 호출해서 선점을 다시 활성화합니다.

이번에 preempt_disable() 함수와 preempt_enable() 함수를 호출해서 선점을 지연하는 예제 코드를 소개합니다.
[https://elixir.bootlin.com/linux/v4.14.70/source/net/packet/af_packet.c]
1 static int packet_create(struct net *net, struct socket *sock, int protocol,
2 int kern)
3 {
...
4
5 preempt_disable();
6 sock_prot_inuse_add(net, &packet_proto, 1);
7 preempt_enable();
8
9 return 0;
...
10 }

5 번째와 7 번째 줄 코드를 보면 각각 preempt_disable() 함수와 preempt_enable() 함수를 호출합니다. 이 코드를 해석하면 sock_prot_inuse_add() 함수가 실행하는 동안 선점 스케줄링을 지연하려는 동작입니다.

이번엔 다른 예제 코드를 소개합니다.
[https://elixir.bootlin.com/linux/v4.14.70/source/kernel/signal.c]
1 static void print_fatal_signal(int signr)
2 {
3 struct pt_regs *regs = signal_pt_regs();
4 pr_info("potentially unexpected fatal signal %d.\n", signr);
...
5 preempt_disable();
6 show_regs(regs);
7 preempt_enable();
8 }

print_fatal_signal() 함수는 시그널 처리 도중 에러 상황이 발생했을 때 호출됩니다.

위 목적은 선점 스케줄링 관점으로 분석하면 show_regs() 함수가 실행할 동안에만 선점 스케줄링을 지연하도록 설정하는 것입니다.

6번째 줄 show_regs(regs); 함수 전 후로 preempt_disable() 함수와 preempt_enable() 함수를 호출합니다. 두 함수가 show_regs(regs); 함수를 감쏴고 있는 것입니다.

5번째 줄 코드는 preempt_disable() 함수를 호출해서 선점을 지연하고 7번째 줄 코드에서는 preempt_enable() 함수를 호출해서 선점을 활성화 합니다. show_regs(regs); 함수가 실행할 동안에 선점 스케줄링이 발생하지 않도록 선젘 스케줄링을 지연하는 것 입니다.

5번째 줄 코드를 실행하면 프로세스 struct thread_info 구조체 preempt_count 필드를 +1만큼 증감합니다.

그런데 6번째 줄 코드인 show_regs() 함수 실행 중에 인터럽트가 발생하면 __irq_svc 인터럽트 벡터가 실행합니다. __irq_svc 레이블에서 인터럽트 핸들링을 마무리한 다음 프로세스 struct thread_info 구조체 preempt_count 필드에 접근해서 선점 스케줄링 실행 여부를 점검합니다.

preempt_disable() 함수를 이미 호출해서 struct thread_info 구조체 preempt_count 필드 값을 +1만큼 증감시켰으니 __irq_svc 레이블에서 선점 스케줄링을 실행하지 않습니다.

preempt_disable() 함수를 호출한 다음 스케줄링 동작을 수행하는 함수를 쓰면 리눅스 시스템은 오동작합니다.


preempt_disable()/preempt_enable() 함수를 호출할 때 주의해야 할 사항이 있습니다.

다음과 같이 드라이버를 초기화하는 코드를 예를 들겠습니다.
1 void configure_something_driver(void) 
2 {
3 preempt_disable();
4
5 do_something();
6 mdelay(100);
7 do_something();
8
9 preempt_enable();
10 }

5~7 번 코드가 실행할 때 Preemption이 되면 안된다고 판단했습니다.
그래서 3 번째와 9 번째 줄에 preempt_disable() 함수와 preempt_enable() 함수를 추가해서 5~7 번째 코드 구간에서 Preemption으로부터 보호한 것입니다.

위 코드를 실행하면 커널 패닉이 발생하거나 이상한 커널 로그를 출력합니다. 그 이유는 무엇일까요? 6 번째 줄 mdelay() 함수를 호출하면 schedule() 함수를 호출하기 때문입니다.

리눅스 커널 입장에서 다음 함수와 같이 해석할 수 있다는 것입니다.
1 void configure_something_driver(void) 
2 {
3 preempt_disable();
4
5 do_something();
6 schedule();
7 do_something();
8
9 preempt_enable();
10 }

위 코드는 다음과 같이 수정하면 문제가 발생하지 않을 것입니다. 
1 void configure_something_driver(void) 
2 {
3 preempt_disable();
4 do_something();
5 preempt_enable();
6
7 mdelay(100);
...
8 }

리눅스 커널 메일링 리스트에도 유사한 내용을 확인할 수 있습니다.
[출처: https://lkml.org/lkml/2010/5/13/638]
Sleeping in the kernel with preemption disabled is considered to be a
bug.  So the scheduler will print an error and a stack dump when this
happens.

In contrast, it is OK to do the following:

preempt_disable();
do_something();
preempt_enable();
schedule();
preempt_disable();
do_something_else();
preempt_enable();

preempt_disable() 함수를 호출해서 선점 스케줄링을 지연하고 휴면에 들어가면 안된다는 내용입니다.


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

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

Reference(프로세스 스케줄링)

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

핑백

덧글

댓글 입력 영역