Arm Linux Kernel Hacks

rousalome.egloos.com

포토로그 Kernel Crash


통계 위젯 (화이트)

46111
637
415422


[리눅스커널] 스케줄링: TASK_UNINTERRUPTIBLE 상태로 바뀔 때 호출되는 함수 10. 프로세스 스케줄링

TASK_UNINTERRUPTIBLE 상태로 바뀔 때 호출하는 함수 분석
다음 함수가 호출될 때 프로세스 상태를 TASK_UNINTERRUPTIBLE로 바꿉니다.
io_wait_event()
mutex_lock() 
usleep_range() 
msleep()
wait_for_completion()

io_wait_event()

io_wait_event() 함수를 호출할 때도 프로세스는 TASK_UNINTERRUPTIBLE 상태로 바뀝니다.

io_wait_event() 코드를 봅시다.
[https://elixir.bootlin.com/linux/v4.19.30/source/include/linux/wait.h]
1 #define io_wait_event(wq_head, condition)
2 do {
3 might_sleep();
4 if (condition)
5 break;
6 __io_wait_event(wq_head, condition);
7 } while (0)
8
9 #define __io_wait_event(wq_head, condition)
10 (void)___wait_event(wq_head, condition, TASK_UNINTERRUPTIBLE, 0, 0,
11     io_schedule())

io_wait_event() 함수에서 __io_wait_event() 함수를 호출하는데,
___wait_event() 함수 3 번째 인자로 TASK_UNINTERRUPTIBLE 를 전달해서 프로세스 상태를 바꿉니다.

__mutex_lock_common()

뮤텍스는 리눅스 커널에서 커널 동기화 기법 중 대표 선수 중 하나입니다.
임계 영역 구간 코드를 실행 중에 특정 프로세스만 뮤텍스를 획득해서 다른 프로세스가 임계 영역 접근을 막는 기법입니다.


뮤텍스 동작은 크게 fastpath와 slowpath로 나눌 수 있습니다.

fastpath는 뮤텍스를 다른 프로세스가 획득하지 않았을 때 실행하는 흐름이고,
slowpath는 뮤텍스를 다른 프로세스가 획득했거나 해제하는 흐름일 때 실행하는 동작입니다.


프로세스가 뮤텍스를 획득하는 과정을 대화로 풀어서 설명을 드려볼까요?
A 프로세스: 뮤텍스를 획득하고 싶습니다.

커널: 이미 B란 프로세스가 뮤텍스를 획득한 상태다.

A 프로세스: 그러면 뮤텍스를 계속 기다려야 할까요?

커널: 네가 뮤텍스를 획득하기 위해 계속 기다리면 다른 프로세스가 CPU에서 일을 못한다. 너를 TASK_UNINTERRUPTIBLE 상태로 바꾸고 휴면에 진입 시켜주마. B란 프로세스가 뮤텍스를 해제하면 널 깨워줄께.

이렇게 slowpath 뮤텍스 동작 흐름에서 프로세스를 TASK_UNINTERRUPTIBLE 상태로 바꿉니다.
조금 더 구체적으로 이미 프로세스가 뮤텍스를 획득했을 때 다른 프로세스가 뮤텍스를 획득하려고 하면, 자신을 뮤텍스 wait list에 등록하고 TASK_UNINTERRUPTIBLE 상태로 바꾼 후 휴면에 진입합니다.

뮤텍스 동작에 대해 간단히 알아봤으니 이제 관련 코드를 보겠습니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/locking/mutex.c]
1 void __sched mutex_lock(struct mutex *lock)
2 {
3 might_sleep();
4
5 if (!__mutex_trylock_fast(lock))
6 __mutex_lock_slowpath(lock);
7}

6번째 줄 코드를 보면 __mutex_lock_slowpath() 함수를 호출합니다.

__mutex_lock_slowpath() 함수 코드를 보면 TASK_UNINTERRUPTIBLE 를 2 번째 인자로 __mutex_lock() 함수를 호출합니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/locking/mutex.c]
1 static noinline void __sched
2 __mutex_lock_slowpath(struct mutex *lock)
3 {
4 __mutex_lock(lock, TASK_UNINTERRUPTIBLE, 0, NULL, _RET_IP_);
5 }

이어서 __mutex_lock() 함수 코드를 보겠습니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/locking/mutex.c]
1 static int __sched
2 __mutex_lock(struct mutex *lock, long state, unsigned int subclass,
3      struct lockdep_map *nest_lock, unsigned long ip)
4 {
5 return __mutex_lock_common(lock, state, subclass, nest_lock, ip, NULL, false);
6 }

__mutex_lock() 함수 2번째 인자가 long state입니다. 
__mutex_lock_slowpath() 함수에서 long state 인자로 TASK_UNINTERRUPTIBLE 플래그를 전달합니다.

다음은 프로세스 상태를 TASK_UNINTERRUPTIBLE로 바꾸는 __mutex_lock_common() 함수 구현부입니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/locking/mutex.c]
1 static __always_inline int __sched
2 __mutex_lock_common(struct mutex *lock, long state, unsigned int subclass,
3     struct lockdep_map *nest_lock, unsigned long ip,
4     struct ww_acquire_ctx *ww_ctx, const bool use_ww_ctx)
5 {
...
6 set_current_state(state);
7 for (;;) {
...
7 schedule_preempt_disabled();

6 번째 줄 코드와 같이 set_current_state() 함수를 호출해 프로세스 상태를 바꿉니다. 함수 인자로 전달되는 state가 TASK_UNINTERRUPTIBLE 이니 TASK_UNINTERRUPTIBLE 상태로 바뀌는 것입니다.

이렇게 다른 프로세스가 뮤텍스를 잡고 있으면 15번째 줄 코드와 같이 자신은 슬립에 들어갑니다. 나중에 뮤텍스를 획득한 프로세스가 뮤텍스를 해제할 때까지 휴면 상태에 있는 것입니다.

usleep_range()

max와 min으로 지정한 시각만큼 슬립에 들어가는 usleep_range() 함수를 실행해도 TASK_UNINTERRUPTIBLE 상태로 바꿉니다.

usleep_range() 함수 코드를 봅시다.
[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/time/timer.c]
1 void __sched usleep_range(unsigned long min, unsigned long max)
2 {
3 ktime_t exp = ktime_add_us(ktime_get(), min);
4 u64 delta = (u64)(max - min) * NSEC_PER_USEC;
5
6 for (;;) {
7 __set_current_state(TASK_UNINTERRUPTIBLE);
8 /* Do not return before the requested sleep time has elapsed */
9 if (!schedule_hrtimeout_range(&exp, delta, HRTIMER_MODE_ABS))
10 break;
11 }
12 }

7번째 줄 코드를 보면 프로세스 상태를 TASK_UNINTERRUPTIBLE 로 바꿉니다.

msleep() 

msleep() 함수는 밀리 초 단위로 딜레이를 줄 때 호출합니다. 주로 리눅스 드라이버에서 많이 활용합니다.

msleep() 함수를 쓰는 예제 코드를 잠깐 봅시다.
[https://elixir.bootlin.com/linux/v4.19.30/source/drivers/i2c/busses/i2c-tegra.c]
01 static int tegra_i2c_flush_fifos(struct tegra_i2c_dev *i2c_dev)
02 {
...
03 while (i2c_readl(i2c_dev, offset) & mask) {
04 if (time_after(jiffies, timeout)) {
05 dev_warn(i2c_dev->dev, "timeout waiting for fifo flushn");
06 return -ETIMEDOUT;
07 }
08 msleep(1);
09 }
10 return 0;
11 }

3~10번째 줄 코드는 while 문입니다.
3번째 줄 코드에서 i2c_readl() 함수를 호출해서 i2c 버스에서 약속된 값을 읽습니다.

그런데 9 번째 줄 코드에서 msleep(1) 함수를 호출해서 1밀리초 만큼 딜레이를 줍니다. 위 코드는 디바이스 드라이버 관점으로 다음과 같이 동작합니다.

    하드웨어적으로 i2c 버스에서 어떤 값을 읽을 때 딜레이를 줘서 i2c 버스가 실행할 
   마진을 주자.

예제 코드를 봤으니 msleep() 함수 코드를 보겠습니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/time/timer.c]
1 void msleep(unsigned int msecs)
2 {
3 unsigned long timeout = msecs_to_jiffies(msecs) + 1;
4
5 while (timeout)
6 timeout = schedule_timeout_uninterruptible(timeout);
7 }
8
9 signed long __sched schedule_timeout_uninterruptible(signed long timeout)
10 {
11 __set_current_state(TASK_UNINTERRUPTIBLE);
12 return schedule_timeout(timeout);
13 }

msleep() 함수에서 6 번째 줄 코드와 같이 schedule_timeout_uninterruptible() 함수를 호출합니다. schedule_timeout_uninterruptible() 함수 코드를 보면 11 번째 줄과 같이 프로세스를 TASK_UNINTERRUPTIBLE 상태로 바꿉니다.

이 후 12 번째 줄 코드를 실행해서 schedule_timeout() 함수를 실행합니다.

wait_for_completion() 

wait_for_completion() 함수를 호출하면 프로세스 상태는 TASK_UNINTERRUPTIBlE로 바뀝니다. 
[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/sched/completion.c]
1 void __sched wait_for_completion(struct completion *x)
2 {
3 wait_for_common(x, MAX_SCHEDULE_TIMEOUT, TASK_UNINTERRUPTIBLE);
4 }

3번째 줄 코드에서 wait_for_common() 함수를 호출할 때 3 번째 인자로 TASK_UNINTERRUPTIBlE 상태를 지정합니다.

wait_for_common() 함수를 호출하면 다음 흐름으로 함수 호출이 수행됩니다.
 
 
그러면 어느 함수에서 프로세스 상태를 UNINTERRUPTIBlE로 바꿀까요?

do_wait_for_common() 코드를 보면 알 수 있습니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/kernel/sched/completion.c]
1 static inline long __sched
2 do_wait_for_common(struct completion *x,
3    long (*action)(long), long timeout, int state)
4 {
5 if (!x->done) {
6 DECLARE_WAITQUEUE(wait, current);
7
8 __add_wait_queue_entry_tail_exclusive(&x->wait, &wait);
9 do {
10 if (signal_pending_state(state, current)) {
11 timeout = -ERESTARTSYS;
12 break;
13 }
14 __set_current_state(state);
15 spin_unlock_irq(&x->wait.lock);
16 timeout = action(timeout);
17 spin_lock_irq(&x->wait.lock);
18 } while (!x->done && timeout);
19 __remove_wait_queue(&x->wait, &wait);
20 if (!x->done)
21 return timeout;
22 }
23 if (x->done != UINT_MAX)
24 x->done--;
25 return timeout ?: 1;
26 }

14번째 줄 코드를 보면 __set_current_state() 함수를 호출해서 프로세스 상태를 TASK_UNINTERRUPTIBLE로 바꿉니다.
14 __set_current_state(state);

다음으로 16 번째 줄 코드를 실행해서 action으로 등록한 콜백 함수를 호출합니다.
16 timeout = action(timeout);

wait_for_completion() 함수 실행 시 콜 스택을 참고하면 action 포인터를 실행하면 schedule_timeout() 함수를 호출함을 알 수 있습니다.


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

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

Reference(프로세스 스케줄링)

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

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

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

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


 




핑백

덧글

댓글 입력 영역