Linux Kernel(4.19) Hacks

rousalome.egloos.com

포토로그 Kernel Crash


통계 위젯 (화이트)

10200
629
98817


[리눅스커널] wait_event_interruptible() 분석 & 프로세스 상태 변경 Linux-Kernel Analysis

프로세스가 INTERRUPTIBLE 상태로 변경되는 가장 대표적인 함수는 wait_event_interruptible() 함수입니다.

wait queue 전체 흐름도
보통 wait_event_interruptible() 함수는 wake_up_interruptible() 함수와 함께 씁니다.

wait_event_interruptible() 함수와 wake_up_interruptible() 함수를 어떻게 쓰는지 예제를 들어서 설명을 하겠습니다.
[https://elixir.bootlin.com/linux/v4.14.70/source/sound/usb/line6/midi.c]
1 static void line6_midi_output_drain(struct snd_rawmidi_substream *substream)
2 {
3 struct usb_line6 *line6 =
4     line6_rawmidi_substream_midi(substream)->line6;
5 struct snd_line6_midi *midi = line6->line6midi;
6
7 wait_event_interruptible(midi->send_wait,
8  midi->num_active_send_urbs == 0);
9 }
10
11 static void midi_sent(struct urb *urb)
12 {
13 unsigned long flags;
14 int status;
15 int num;
16 struct usb_line6 *line6 = (struct usb_line6 *)urb->context;
....
17 if (num == 0) {
18 line6_midi_transmit(line6->line6midi->substream_transmit);
19 num = line6->line6midi->num_active_send_urbs;
20 }
21
22 if (num == 0)
23 wake_up(&line6->line6midi->send_wait);
24
25 spin_unlock_irqrestore(&line6->line6midi->lock, flags);
26 }

1단계: wait queue을 기다림 설정

wait event를 기다리도록 wait_event_interruptible() 함수를 호출합니다.
1 static void line6_midi_output_drain(struct snd_rawmidi_substream *substream)
2 {
3 struct usb_line6 *line6 =
4     line6_rawmidi_substream_midi(substream)->line6;
5 struct snd_line6_midi *midi = line6->line6midi;
6
7 wait_event_interruptible(midi->send_wait,
8  midi->num_active_send_urbs == 0);
9 }

위 코드 7~8번째 줄이 이 동작을 수행합니다.
midi->num_active_send_urbs == 0 이란 조건을 만족할 때 까지 wait queue 이벤트를 기다리는 것입니다.

이 때 프로세스는 TASK_INTERRUPTIBLE 상태에 진입하여 휴면에 진입합니다.

2단계: wait queue event를 트리거

wait queue event를 트리거해서 wait queue 이벤트를 등록하고 휴면에 진입한 프로세스를 깨웁니다.
11 static void midi_sent(struct urb *urb)
12 {
...
22 if (num == 0)
23 wake_up(&line6->line6midi->send_wait);

23번째 줄 코드가 이 동작을 실행합니다.

함수 전체 흐름도는 다음과 같습니다.
line6_midi_output_drain() midi_sent()
  [1]wait_event_interruptible()
     [2]wait_queue 이벤트를 등록하고 휴면
       [3]schedule()
           ...
[4] wake_up()
  [5] 깨어나 wait_event_interruptible() 함수에서 
      빠져나옴

이렇게 wait queue에 대한 흐름도를 정리했으니 wait_event_interruptible() 함수를 호출하면 어떻게 
프로세스가 TASK_INTERRUPTIBLE 상태로 변경되는지 알아봅시다.

wait_event_interruptible() 함수 코드를 보면 다음 코드로 치환됨을 알 수 있습니다.
[https://elixir.bootlin.com/linux/v4.14.70/source/include/linux/wait.h]
1 #define wait_event_interruptible(wq_head, condition) \
2 ({ \
3 int __ret = 0; \
4 might_sleep(); \
5 if (!(condition)) \
6 __ret = __wait_event_interruptible(wq_head, condition); \
7 __ret; \
8 })

4 번째 줄 코드에서 휴면에 진입할 수 있는 조건인지 체크한 후
__wait_event_interruptible() 함수를 호출합니다.

__wait_event_interruptible() 함수를 분석하면 ___wait_event() 함수를 호출한다는 사실을 알 수 있습니다.
[https://elixir.bootlin.com/linux/v4.14.70/source/include/linux/wait.h]
#define __wait_event_interruptible(wq_head, condition) \
___wait_event(wq_head, condition, TASK_INTERRUPTIBLE, 0, 0, \
      schedule())


___wait_event() 함수 동작은 크게 3단계로 분류할 수 있습니다.

1단계: 프로세스 상태를 TASK_INTERRUPTIBLE 상태로 변경
prepare_to_wait_event() 함수를 호출해서 프로세스 상태를 TASK_INTERRUPTIBLE 로 변경합니다.

2단계: 스케줄링 실행
schedule() 함수를 호출해서 태스크 스위칭을 실행합니다.

3단계: 깨어나서 __wait_event() 함수 실행 종료
wait_event_interruptible과 페어로 wake_up_interruptible 함수가 호출됐을 때 프로세스는 다시 실행을 재개합니다.

finish_wait() 함수 호출로 프로세스 상태를 다시 TASK_RUNNING 상태로 변경하고
wait queue 구조체 정보를 갱신한 후 __wait_event() 함수 실행을 마무리합니다.

__wait_event() 함수 전체 흐름도를 살펴봤으니 소스 구현부를 분석하겠습니다.
[https://elixir.bootlin.com/linux/v4.14.70/source/kernel/sched/wait.c]
1 #define ___wait_event(wq_head, condition, state, exclusive, ret, cmd) \
2 ({ \
3 __label__ __out; \
4 struct wait_queue_entry __wq_entry; \
5 long __ret = ret; /* explicit shadow */ \
6 \
7 init_wait_entry(&__wq_entry, exclusive ? WQ_FLAG_EXCLUSIVE : 0); \
8 for (;;) { \
9 long __int = prepare_to_wait_event(&wq_head, &__wq_entry, state);\
10 \
11 if (condition) \
12 break; \
13 \
14 if (___wait_is_interruptible(state) && __int) { \
15 __ret = __int; \
16 goto __out; \
17 } \
18 \
19 cmd; \
20 } \
21 finish_wait(&wq_head, &__wq_entry); \
22 __out: __ret; \
23 })

1단계 동작인 프로세스를 TASK_INTERRUPTIBLE 상태로 변경하는 흐름입니다.

9번째 줄 코드를 보겠습니다.
9 long __int = prepare_to_wait_event(&wq_head, &__wq_entry, state);\

prepare_to_wait_event() 함수를 호출해서 wait queue에 등록을 하고 프로세스를 TASK_INTERRUPTIBLE 상태로 변경합니다.

다음 2단계 동작인 스케줄링을 실행하는 코드입니다.
19 cmd; \

19번째 줄 코드인 cmd를 실행하면 schedule() 함수를 호출해서 스케줄링됩니다.

cmd 코드를 실행하면 왜 schedule() 함수를 호출할까요?

다음 코드와 같이 ___wait_event() 함수에 전달되는 인자를 알아볼 필요가 있습니다.
1 #define __wait_event_interruptible(wq_head, condition) \
2 ___wait_event(wq_head, condition, TASK_INTERRUPTIBLE, 0, 0, \
3       schedule())
4
5 #define ___wait_event(wq_head, condition, state, exclusive, ret, cmd) \
6 ({ \

3번째 줄 코드를 보면 6번째 인자로 schedule() 함수를 지정하는데 __wait_event() 함수 마지막 인자인 cmd가 이를 전달 받습니다. 따라서 cmd 코드를 실행하면 schedule() 함수가 호출되는 것입니다.

이번에 wait queue event를 받으면 실행하는 3 단계 동작을 분석하겠습니다.
8 for (;;) { \
9 long __int = prepare_to_wait_event(&wq_head, &__wq_entry, state);\
10 \
11 if (condition) \
12 break; \
13 \
14 if (___wait_is_interruptible(state) && __int) { \
15 __ret = __int; \
16 goto __out; \
17 } \
18 \
19 cmd; \
20 } \
21 finish_wait(&wq_head, &__wq_entry); \

wake_up_interruptible() 함수를 호출하면 wait queue를 등록한 프로세스가 깨어나 다시 실행을 재개하여
9번째 줄 코드를 실행합니다.

wait_event_interruptible 함수 두 번째 인자는 condition인데 condition이 true이면 for (;;) 문을 벗어나 21번째 줄 코드를 실행합니다.

이번에는 prepare_to_wait_event() 함수와 finish_wait() 함수를 차례로 분석합시다.
prepare_to_wait_event() 함수 코드는 다음과 같습니다.
[https://elixir.bootlin.com/linux/v4.14.70/source/kernel/sched/wait.c]
1 long prepare_to_wait_event(struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry, int state)
2 {
3 unsigned long flags;
4 long ret = 0;
5
6 spin_lock_irqsave(&wq_head->lock, flags);
7 if (unlikely(signal_pending_state(state, current))) {
8 list_del_init(&wq_entry->entry);
9 ret = -ERESTARTSYS;
10 } else {
11 if (list_empty(&wq_entry->entry)) {
12 if (wq_entry->flags & WQ_FLAG_EXCLUSIVE)
13 __add_wait_queue_entry_tail(wq_head, wq_entry);
14 else
15 __add_wait_queue(wq_head, wq_entry);
16 }
17 set_current_state(state);
18 }
19 spin_unlock_irqrestore(&wq_head->lock, flags);
20
21 return ret;
22 }

7~9번째 줄 코드를 보겠습니다.
7 if (unlikely(signal_pending_state(state, current))) {
8 list_del_init(&wq_entry->entry);
9 ret = -ERESTARTSYS;

프로세스가 시그널 팬딩 상태일 때 실행하는 코드입니다.
프로세스에게 시그널을 전달할 때 current->pending_signal 필드에 링크드 리스트를 등록합니다.

11~18번째 줄 코드는 시그널 팬딩 상태가 아니면 실행합니다.
11 if (list_empty(&wq_entry->entry)) {
12 if (wq_entry->flags & WQ_FLAG_EXCLUSIVE)
13 __add_wait_queue_entry_tail(wq_head, wq_entry);
14 else
15 __add_wait_queue(wq_head, wq_entry);
16 }
17 set_current_state(state);
18 }

11~16번째 줄 코드는 wait queue 연결 리스트 상태에 따라 wait queue 연결 리스트를 등록합니다.

17번째 줄 코드를 보면 드디어 프로세스를 TASK_INTERRUPTIBLE 상태로 변경합니다.

이번에는 wait queue 이벤트를 받으면 실행하는 3단계 동작 중 호출되는 finish_wait() 함수를 분석하겠습니다.
[https://elixir.bootlin.com/linux/v4.14.70/source/kernel/sched/wait.c]
1 void finish_wait(struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry)
2 {
3 unsigned long flags;
4
5 __set_current_state(TASK_RUNNING);
6
7 if (!list_empty_careful(&wq_entry->entry)) {
8 spin_lock_irqsave(&wq_head->lock, flags);
9 list_del_init(&wq_entry->entry);
10 spin_unlock_irqrestore(&wq_head->lock, flags);
11 }
12 }

먼저 5번째 줄 코드를 보겠습니다.
5 __set_current_state(TASK_RUNNING);

프로세스 상태를 다시 TASK_RUNNING 상태로 변경합니다.

다음 7~11번째 줄 코드는 wait queue 연결 리스트 자료 구조를 변경합니다.
7 if (!list_empty_careful(&wq_entry->entry)) {
8 spin_lock_irqsave(&wq_head->lock, flags);
9 list_del_init(&wq_entry->entry);
10 spin_unlock_irqrestore(&wq_head->lock, flags);
11 }

"이 포스팅이 유익하다고 생각되시면 댓글로 응원해주시면 감사하겠습니다.  
그리고 혹시 궁금점이 있으면 댓글로 질문 남겨주세요. 상세한 답글 올려드리겠습니다!"


#Reference 시스템 콜


Reference(워크큐)
워크큐(Workqueue) Overview


덧글

댓글 입력 영역