Linux Kernel(4.19) Hacks

rousalome.egloos.com

포토로그 Kernel Crash


통계 위젯 (화이트)

175162
807
85243


[리눅스커널] 워크큐(Workqueue) - 딜레이워크(delayed_work)는 어떻게 실행하나? 8장. 워크큐

딜레이 워크를 실행하려면 어떤 함수를 호출해야 할까요? schedule_delayed_work() 함수를 실행하면 됩니다.

먼저 딜레이 워크를 실행하는 드라이버 코드를 예제로 열어 보겠습니다.
[https://elixir.bootlin.com/linux/v4.14.43/source/drivers/mmc/host/bcm2835.c#L651]
1 static
2 bool bcm2835_send_command(struct bcm2835_host *host, struct mmc_command *cmd)
3 {
...
3 if (!cmd->data && cmd->busy_timeout > 9000)
4 timeout = DIV_ROUND_UP(cmd->busy_timeout, 1000) * HZ + HZ;
5 else
6 timeout = 10 * HZ;
7 schedule_delayed_work(&host->timeout_work, timeout);
 
7번째 코드를 살펴보겠습니다.
struct delayed_work 구조체 주소를 &host->timeout_work 변수로 전달합니다. 두 번째 인자는 timeout인데 HZ 단위 시각 정보입니다. 

딜레이 워크를 실행하려면 이렇게 워크를 지연할 시각 정보를 두 번째 인자로 전달해야 합니다. 이때 지연하는 시각 정보는 HZ 단위이라는 점 기억해야 합니다. 

HZ는 1초 안에 동적 타이머를 실행하는 횟수로 진동수라고 말합니다. 리눅스 커널에서 상대 시각을 처리할 때 쓰는 개념입니다.

다음과 같이 6번째 줄 코드가 실행하면 10초만큼 지연해서 딜레이 워크를 실행합니다.
6 timeout = 10 * HZ;

간단히 딜레이 워크를 실행하는 코드를 확인했습니다. 워크 코드를 상세히 분석하고 ftrace 로그를 점검했으면 그리 낯설지는 않을 겁니다.

딜레이 워크를 실행하는 schedule_delayed_work() 함수를 분석하겠습니다.
[https://elixir.bootlin.com/linux/v4.14.43/source/include/linux/workqueue.h#L604]
1 static inline bool schedule_delayed_work(struct delayed_work *dwork,
2  unsigned long delay)
3 {
4 return queue_delayed_work(system_wq, dwork, delay);
5 }

4번째 줄 코드를 보면 queue_delayed_work() 함수를 호출합니다. 
여기서 queue_delayed_work() 함수 첫 번째 인자로 struct workqueue_struct 타입 system_wq를 전달합니다. system_wq는 시스템 워크큐이니 schedule_delayed_work() 함수를 호출하면 시스템 워크큐를 쓰는 겁니다.

schedule_delayed_work() 함수 첫 번째 인자인dwork와 두 번째 인자인 delay는 그대로 queue_delayed_work() 함수에 전달합니다.

이어서 queue_delayed_work() 함수를 보겠습니다.
[https://elixir.bootlin.com/linux/v4.14.43/source/kernel/workqueue.c#L1550]
1 static inline bool queue_delayed_work(struct workqueue_struct *wq,
2       struct delayed_work *dwork,
3       unsigned long delay)
4{
5 return queue_delayed_work_on(WORK_CPU_UNBOUND, wq, dwork, delay);
6}

5번째 줄 코드를 보면 첫 번째 인자로 WORK_CPU_UNBOUND를 추가한 다음 queue_delayed_work_on() 함수를 호출합니다. 

바로 queue_delayed_work_on() 함수를 살펴보겠습니다.
[https://elixir.bootlin.com/linux/v4.14.43/source/kernel/workqueue.c#L1550]
1 bool queue_delayed_work_on(int cpu, struct workqueue_struct *wq,
2    struct delayed_work *dwork, unsigned long delay)
3 {
4 struct work_struct *work = &dwork->work;
5 bool ret = false;
6 unsigned long flags;
7
8 /* read the comment in __queue_work() */
9 local_irq_save(flags);
10
11 if (!test_and_set_bit(WORK_STRUCT_PENDING_BIT, work_data_bits(work))) {
12 __queue_delayed_work(cpu, wq, dwork, delay);
13 ret = true;
14 }
15
16 local_irq_restore(flags);
17 return ret;
18 }

워크를 워크큐에 큐잉하기 전에 호출한 queue_work_on() 함수에서 봤던 코드와 유사합니다. 딜레이 워크도 이 함수에와 유사한 동작을 수행하는 것으로 보입니다.

코드 분석 전 우선 4번째 줄 지역 변수 선언부를 봅시다.
4 struct work_struct *work = &dwork->work;

struct delayed_work 구조체 첫 번째 멤버인 워크 주소를 work에 저장합니다.

다음 11~14번째 줄 코드를 분석하겠습니다.
11 if (!test_and_set_bit(WORK_STRUCT_PENDING_BIT, work_data_bits(work))) {
12 __queue_delayed_work(cpu, wq, dwork, delay);
13 ret = true;
14 }

struct work_struct.data 멤버와 WORK_STRUCT_PENDING_BIT 매크로와 AND 비트 연산을 한 다음 결괏값이 0이면 if 문 내 코드를 실행합니다.

11~14번째 줄 코드를 이해하기 쉽게 표현하면 다음과 같습니다.
if ( !(work->data == WORK_STRUCT_PENDING_BIT)) {
work->data =| WORK_STRUCT_PENDING_BIT;
__queue_delayed_work(cpu, wq, dwork, delay);
ret = true;
}

work_data_bits(work) 함수를 호출해서 struct work_struct.data 멤버에 접근한 후 WORK_STRUCT_PENDING_BIT가 아니면 12~13번 줄 코드를 실행합니다.

이렇게 work->data와 WORK_STRUCT_PENDING_BIT를 비교하는 이유는 뭘까요? 딜레이 워크를 중복해서 실행하지 않기 위해서입니다. 딜레이 워크도 워크와 같이 실행 요청을 하면 struct delayed_work.work.data 멤버에 WORK_STRUCT_PENDING_BIT(1) 값을 저장합니다. 

딜레이 워크 실행 요청을 했으면 work->data가 WORK_STRUCT_PENDING_BIT(1) 로 변경됩니다. 딜레이 워크를 워커 쓰레드에서 실행하기 전 queue_delayed_work_on() 함수를 두 번 호출하면 딜레이 워크를 중복 실행 요청한다고 보는 겁니다.  정리하면 딜레이 워크를 중복해서 워크큐에 실행 요청하지 않게 처리하는 예외 처리 코드입니다.

이제 딜레이 워크를 실행하는 핵심 함수인 __queue_delayed_work()를 살펴보겠습니다.
[https://elixir.bootlin.com/linux/v4.14.43/source/kernel/workqueue.c#L1550]
1 static void __queue_delayed_work(int cpu, struct workqueue_struct *wq,
2 struct delayed_work *dwork, unsigned long delay)
3 {
4 struct timer_list *timer = &dwork->timer;
5 struct work_struct *work = &dwork->work;
6
7 WARN_ON_ONCE(!wq);
8 WARN_ON_ONCE(timer->function != delayed_work_timer_fn ||
9      timer->data != (unsigned long)dwork);
10 WARN_ON_ONCE(timer_pending(timer));
11 WARN_ON_ONCE(!list_empty(&work->entry));
12
13 if (!delay) {
14 __queue_work(cpu, wq, &dwork->work);
15 return;
16 }
17
18 dwork->wq = wq;
19 dwork->cpu = cpu;
20 timer->expires = jiffies + delay;
21
22 if (unlikely(cpu != WORK_CPU_UNBOUND))
23 add_timer_on(timer, cpu);
24 else
25 add_timer(timer);
26}

7~11번 예외 처리 코드를 보겠습니다.
7 WARN_ON_ONCE(!wq);
8 WARN_ON_ONCE(timer->function != delayed_work_timer_fn ||
9      timer->data != (unsigned long)dwork);
10 WARN_ON_ONCE(timer_pending(timer));
11 WARN_ON_ONCE(!list_empty(&work->entry));

__queue_delayed_work() 함수에 전달된 인자를 점검하는 코드입니다. 
인자가 예상된 값이 아니면 WARN() 매크로를 실행해서 커널 로그로 콜스택을 출력합니다.
WARN() 매크로는 코드 흐름에 소프트웨어적인 오류가 있을 때 실행합니다. 그래서 소스 코드를 보다가 WARN() 매크로를 보면 뭔가 논리적인 오류가 있는 조건이라고 봐야합니다.

WARN 매크로를 실행하면 다음과 같은 디버깅 정보를 커널 로그로 출력합니다.
[출처: https://www.unix.com/programming/148285-what-unbalanced-irq.html
https://www.linuxquestions.org/questions/programming-9/problem-with-interrupt-handle-770992/]
WARNING: at kernel/irq/manage.c:225 __enable_irq+0x3b/0x57()
Unbalanced enable for IRQ 4
Modules linked in: svsknfdrvr [last unloaded: osal_linux]
Pid: 634, comm: ash Tainted: G W 2.6.28 #1
Call Trace:
[<c011a7f9>] warn_slowpath+0x76/0x8d
[<c012fac8>] profile_tick+0x2d/0x57
[<c011ed72>] irq_exit+0x32/0x34
[<c010f22c>] smp_apic_timer_interrupt+0x41/0x71
[<c01039ec>] apic_timer_interrupt+0x28/0x30
[<c011b2b4>] vprintk+0x1d3/0x300
[<c013a2af>] __setup_irq+0x11c/0x1f2
[<c013a177>] __enable_irq+0x3b/0x57
[<c013a506>] enable_irq+0x37/0x54
[<c68c9156>] svsknfdrvr_open+0x5e/0x65 [svsknfdrvr]
[<c016440a>] chrdev_open+0xce/0x1a4
[<c016433c>] chrdev_open+0x0/0x1a4
[<c01602f7>] __dentry_open+0xcc/0x23a
[<c016049a>] nameidata_to_filp+0x35/0x3f
[<c016b3c5>] do_filp_open+0x16f/0x6ef
[<c0278fd5>] tty_write+0x1a2/0x1c9
[<c0160128>] do_sys_open+0x42/0xcb

다시 코드 분석으로 돌아갑시다.

7번째 줄 코드는 워크큐 주소가 NULL이면 WARN() 매크로를 실행해서 콜스택을 커널 로그로 출력합니다.

8번째 줄 코드는 타이머 핸들러 함수가 delayed_work_timer_fn() 인지 점검하고 타이머 핸들러 매개 인자가 dwork인지 점검합니다.

10번째 줄 코드는 딜레이 워크 타이머가 현재 실행 중인지 확인하고 11번 줄 코드는 워크 내 링크드 리스트가 NULL이 아닌지 점검합니다.

모두 디바이스 드라이버 개발자가 딜레이 워크를 중복 실행하거나 인자를 잘못 전달할 경우 에러 메시지를 출력하는 코드입니다. 이런 루틴을 Sanity Check 코드라고도 합니다. 리눅스 커널 많은 핵심 함수 앞부분에 이런 코드가 많습니다.

13번째 줄 코드를 보겠습니다.
13 if (!delay) {
14 __queue_work(cpu, wq, &dwork->work);
15 return;
16 }

delay이라는 인자는 HZ 단위 지연 시각 정보를 저장하고 있습니다. 만약 딜레이 워크에 지연 시각을 0으로 설정하면 delay는 0이니 if문 조건을 만족해서 14번째 줄 코드를 실행합니다. __queue_work() 함수를 호출해서 바로 워크를 워크큐에 큐잉합니다.

이후 15번째 줄 코드와 같이 바로 __queue_delayed_work() 함수를 빠져나옵니다. 이 코드로 재미있는 사실을 알 수 있습니다. 워크 대신에 딜레이 워크를 선언하고 지연시각을 0으로 설정하면 딜레이 워크를 워크로 쓸 수 있다는 점입니다.

디바이스 드라이버에서 딜레이 워크 선언부를 보면 HZ 시각만큼 지연해서 워크를 실행한다고 믿지 말고 schedule_delayed_work(); 함수 두 번째로 전달하는 지연시각을 확인해야겠습니다.

다음 18번째 줄 코드를 보겠습니다.
18 dwork->wq = wq;
19 dwork->cpu = cpu;
20 timer->expires = jiffies + delay;

18~19번 줄 코드를 보면 워크큐 주소를 dwork->wq에 저장하고 실행 중인 cpu 번호를 dwork->cpu에 저장합니다.

20번 줄 코드는 현재 시각 정보를 담고 있는 jiffies에 HZ단위 지연 시각인 delay를 더해서 timer->expires 멤버에 저장합니다. 딜레이 워크용 동적 타이머 만료 시각을 저장하는 코드입니다. 

딜레이 워크에서 가장 중요한 코드입니다.

다음 코드는 딜레이 워크용 동적 타이머를 실행하는 동작입니다.
22 if (unlikely(cpu != WORK_CPU_UNBOUND))
23 add_timer_on(timer, cpu);
24 else
25 add_timer(timer); 

add_timer_on() 혹은 add_timer() 함수를 호출해서 동적 타이머를 실행합니다.

여기까지 __queue_delayed_work() 함수부터 schedule_delayed_work() 함수까지 딜레이 워크를 실행하는 코드를 분석했습니다. 

이 코드 흐름으로 딜레이 워크는 워크와 어떤 차이가 있을까요? 워크는 schedule_work() 함수를 호출하면 워크를 워커풀 구조체 멤버인 struct worker_pool.worklist 링크드 리스트에 큐잉했습니다.

딜레이 워크의 경우 schedule_delayed_work() 함수를 호출하면 딜레이 워크를 워크큐에 바로 큐잉하지는 않습니다. 단지 동적 타이머를 실행해서 지연 시각 후에 동적 타이머 핸들러인 delayed_work_timer_fn() 함수가 실행되도록 합니다. 그래서 이번 소절에서 schedule_delayed_work() 함수를 호출하면 딜레이 워크를 워크큐에 큐잉하지 않고 단지 실행 요청한다고 설명을 드린 겁니다.

그럼 딜레이 워크는 워크큐에 언제 큐잉할까요? 딜레이 워크 동적 타이머 핸들러인 delayed_work_timer_fn() 함수가 실행될 때 수행합니다. 상세분석은 다음에 이어집니다.

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

Reference(프로세스 관리)
4.9 프로세스 컨택스트 정보는 어떻게 저장할까?
 4.9.1 컨택스트 소개
 4.9.2 인터럽트 컨택스트 정보 확인하기
 4.9.3 Soft IRQ 컨택스트 정보 확인하기
 4.9.4 선점 스케줄링 여부 정보 저장
4.10 프로세스 디스크립터 접근 매크로 함수
 4.10.1 current_thread_info()
 4.10.2 current 매크로란
4.11 프로세스 디버깅
 4.11.1 glibc fork 함수 gdb 디버깅
 4.11.2 유저 프로그램 실행 추적 




    덧글

    댓글 입력 영역