- Mar 28, 2021
task_struct - 1
task_struct
state
state
필드는 task의 실행 상태를 나타내며,exit_state
1는 task의 종료 상태를 나타낸다.
사실state
와exit_state
는 하나의 필드로 합쳐져있는것으로 보이나2, 실수를 줄이기 위해 실행 상태와 종료 상태를 분리한것으로 보인다.task의 실행 상태 (state)
태스크의 실행 상태는 다음의 값을 가질 수 있다. (kernel 4.19 기준)
이 때, 4, 5, 6, 7은 speical task state로 불린다.TASK_RUNNING
- task가 정상적으로 실행되고 있을 때를 뜻한다. 실행이라 함은 CPU에서 직접 실행하고 있는 중, 그리고 런큐에서 CPU를 받기 위해 대기하는 중을 포함한다.
TASK_INTERRUPTIBLE
- task가 어떠한 이유(예, 사용자 입력 대기)로 blocking되었음을 뜻한다. 원인이 되는 이유가 해결되면 그 task는 깨어난다. (예, 인풋을 기다리는 SSH) 또는 이 task로 signal이 새로 들어오면, 그 signal을 처리해야 한다.
- Signal 처리 가능 여부가 상당히 중요한 요소인데, 다음과 같은 예를 보자.
- 커널 레벨에서 task는 blocking 되어야 할 이유가 있으면
wait_event_interruptible()
을 호출하고, task를TASK_INTERRUPTIBLE
상태로 만든다. 이 상태에서 task에게 시그널(예, SIGKILL)이 들어오면wait_event_interruptible()
은-ERESTARTSYS
을 반환한다.3 즉, blocking 이유가 해결되지 않은 상태에서 task가 깨어나고, 그 task는 signal handling을 마저 수행해야 한다. 그리고 다시 task는wait_event_interruptible()
를 호출하여 blocking 이유가 없어질때까지 다시 blocking 되어야 한다.
TASK_UNINTERRUPTIBLE
- 보통 I/O operation과 같이
TASK_RUNNING
에 있어 중요한 작업을 처리하기 위한 상태이다. - 위와 다르게 task가 signal에 의해 깨어날 수 없다4. 명시적인 wakeup 계열 함수(
completion_done()
등) 외에는 절대 깨어나지 않는다. 만약 task가 강제 종료해야 한다고 하더라도 SIGKILL을 받을 수 없기 때문에 종료할 수 없다.
- 보통 I/O operation과 같이
__TASK_STOPPED
SIGSTOP
등의 의해 태스크 실행 중단이 되었음을 뜻한다. 주로TASK_WAKEKILL
과 조합하여 많이 쓰인다. (TASK_STOPPED
)
__TASK_TRACED
ptrace
에 의해 task가 trace되고 있음을 뜻한다. sys_ptrace로 target task에 attach하면 task state에__TASK_TRACED
를 적용한다. 주로TASK_WAKEKILL
과 조합하여 많이 쓰인다. (TASK_TRACED
)
TASK_PARKED
- 커널 쓰레드의 parking/unparking과 관련된 상태이다.5
- CPU 핫플러그 기능에서 per-cpu kthread(예, ksoftirqd)는 해당 CPU 오프라인 시,
TASK_UNINTERRUPTIBLE
상태로 들어간다. 이 때,TASK_PARKED
도 같이 표기해 준다. 추후 CPU가 다시 온라인이 되면, per-cpu kthread 태스크는TASK_RUNNING
으로 복구 시킬 때, 상태가TASK_PARKED
임을 확인하며, 그 TASK를 해당하는 CPU로 재-바인딩 시켜준다.
TASK_DEAD
- TASK가 종료할 때, 종료했음을 의미하는 상태이다. 이 상태는 태스크 생존에 필수적이지 않은 모든 리소스(file descriptor 등)를 반환한 상태이며, 다른 태스크로 컨텍스트 스위칭 후, 이 태스크의 스택과
task_struct
또한 정리될 예정임을 뜻한다.
- TASK가 종료할 때, 종료했음을 의미하는 상태이다. 이 상태는 태스크 생존에 필수적이지 않은 모든 리소스(file descriptor 등)를 반환한 상태이며, 다른 태스크로 컨텍스트 스위칭 후, 이 태스크의 스택과
TASK_WAKEKILL
SIGKILL
을 받을 수 있는 상태임을 뜻한다. 이 상태는 다른 상태와 조합하여 쓰인다. 예를 들어,TASK_UNINTERRUPTIBLE | TASK_WAKEKILL = TASK_KILLABLE
임을 뜻하며, 다른 signal은 무시하지만 kill 시그널은 무시하지 않는다라는 뜻이다.
TASK_WAKING
- Task가 깨어나는 중임을 뜻한다.
- 기존의 목적: 두 개 이상의 태스크가 타겟 태스크를 깨우려고 할 때, 한 개 태스크만이 깨울 수 있도록 만든다. 6
- 코드를 읽어보면 딱히 그 목적보다, Task를 깨워서 다른 run queue로 enqueue 하기 전, 원하는 데이터를 재정비 할 수 있도록 상태를 저장해주는 느낌이다. (예, migration이 필요한 경우, task의 virtual runtime을 run queue의 virtual runtime base에 맞게 재조정 해준다.)
TASK_NOLOAD
- 이 상태를 할당받은 task는 load를 계산할 때, load 계산에 들어가지 않는다. 일반적으로
TASK_UNINTERRUPTIBLE | TASK_NOLOAD
로 같이 쓰일 것이라고 설계자는 주장하고 있다. (코드에 잘 보이진 않는다.) 7 - load 계산 시, activate task를 고려하며, activate task는
TASK_RUNNING
과TASK_UNINTERRUPTIBLE
을 포함한다. TASK_UNINTERRUPTIBLE
상태는TASK_RUNNING
중, I/O operation과 같이 중요한 일 때문에 불가피하게 blocking 상태로 들어갔음을 의미하며, blocking이 풀리면 바로TASK_RUNNING
상태로 복귀할 것을 기대한다. 이 때문에 load로 계산한다.TASK_INTERRUPTIBLE
은 사용자 입력 대기 상태 등, 긴 시간동안 휴면 상태로 있어야 하는 태스크가 갖는 상태이며 load로 계산하지 않는다. (일반적으로 긴 시간동안 유휴하기 때문이다.)
- 이 상태를 할당받은 task는 load를 계산할 때, load 계산에 들어가지 않는다. 일반적으로
TASK_NEW
- 태스크가 새롭게 생성되고 있음을 의미한다.
wake_up_new_task()
에서TASK_RUNNING
을 할당 받기 전,copy_process()
의sched_fork()
함수에서 세팅되는 상태이며, 절대 실행되지 않고, 시그널 핸들링도 처리하지 않고, 런큐에 들어갈 수도 없는 상태를 담보한다.
- 태스크가 새롭게 생성되고 있음을 의미한다.
-
https://yongshikmoon.github.io/2021/03/29/task_struct-2.html ↩
-
실제로 state와 exit_state는 비트 플래그가 겹치는 부분이 없다. 즉, 서로 상호배타적인 비트플래그를 갖는다. ↩
-
RESTARTSYS라는 의미를 알 수 있듯이, syscall을 restart하라는 말이다. 다시 말하면 signal handling을 한 후, syscall을 restart하여
wait_event_interruptible()
을 재실행하라는 의미이며, 이 때문에 syscall은 reenterancy가 있어야 한다라고 말한다. ↩ -
signal_wake_up()
함수를 보면 TASK_INTERRUPTIBLE state만 signal을 받아 깨어날 수 있음을 알 수 있다. ↩ -
https://lwn.net/Articles/500338/, 구현은 많이 변했지만 주석은 읽어볼만하다. ↩
-
https://lore.kernel.org/patchwork/patch/170913/ ↩
-
https://lore.kernel.org/lkml/alpine.LFD.2.11.1505112154420.1749@ja.home.ssi.bg/T/ ↩
- Mar 21, 2021
About preempt_count and TIF_NEED_RESCHED, PREEMPT_NEED_RESCHED
preempt_count
preempt_count
는preempt_disable
에 의해 증가되며preempt_enable
에 의해 감소되는thread_info
내의integer
값이다.즉,
preempt_count
가 0보다 크면 그 태스크는 선점 당하지 않으며,preempt_count
가 0이면 그 태스크는 선점 될 수 있다.
TIF_NEED_RESCHED
예측컨데,
thread info need reschedule
의 약자인 것 같다.현재 이 플래그가 세팅 된 태스크(
thread_info->flags
)는 선점 가능하며, 스케쥴링이 필요하다는 뜻이다.
PREEMPT_NEED_RESCHED
플래그 이름에서 알 수 있듯이, 이 플래그가 셋팅된 태스크는 선점 가능하며 스케쥴링이 필요하다는 뜻이다.
이 플래그는
thread_info->preempt_count
에 세팅이 된다.PREEMPT_NEED_RESCHED
는0x8000000(32bit MSB)
이며, 스케쥴링이 필요한 경우 관련 비트를 클리어 한다. (inverted flag bit)
TIF_NEED_RESCHED vs PREEMPT_NEED_RESCHED
thread_info
에 동일한 의미의 플래그가 두 개 있다. 헷갈린다.관련 내용은 2013년 패치노트, sched: Add NEED_RESCHED to the preempt_count에 존재한다.
동일한 의미의 플래그가 두 개 있는 이유를 요약하자면 다음과 같다.
(이해를 위해 자체 추측 내용 또한 포함되어 있다.)
-
TIF_NEED_RESCHED
가 있고, 이 플래그는 리스케쥴링이 필요하다는 플래그였다. -
다만, 리스케쥴링을 위해서는
preempt_count == 0
이여야 했다. 즉, 현 태스크가 선점 가능해야 했다.- 주의) 물론
TIF_NEED_RESCHED
가 셋팅 되어 있다고 해서,preempt_count == 0
일 필요는 없다.
- 주의) 물론
-
즉, 스케쥴링을 위해서는
TIF_NEED_RESCHED
와preempt_count
를 동시에 체크해야 한다.- 이는 critical section이 필요한 작업으로 성능상 불이익을 초래한다.
-
preempt_count
만을 읽어서 리스케쥴링이 필요한지 체크할 수 있을까?
해결법은 간단하다.
TIF_NEED_RESCHED
플래그를preempt_count
로 미리 반영하는 것이다.다만,
TIF_NEED_RESCHED
를preempt_count
로 수시로 반영하면 atomic operation이 계속 발생하여 성능상 불이익이 발생한다.(
preempt_count
를 셋팅할 때, atomic operation으로 모든 코어가 동일한 값을 보도록 해야 한다.)필요할 때만 반영하도록 리스케쥴링 여부를 체크할 때, 바로 전에
TIF_NEED_RESCHED
를preempt_count
에 반영한다.(플래그 반영 위치는 위 패치노트에 적혀 있다.)
TIF_NEED_RESCHED
를 반영함으로써, 스케쥴링이 필요하면preempt_count == 0
으로 될 수 있는 가능성1을 만들어 주고,스케쥴링이 불필요하면 무조건preempt_count != 0
으로 만들면 된다.PREEMPT_NEED_RESCHED := 0x80000000
일 때,스케쥴링이 필요한 경우,
preempt_count := preempt_count & (~PREEMPT_NEED_RESCHED)
스케쥴링이 불필요한 경우,
preempt_count := preempt_count | PREEMPT_NEED_RESCHED
로 만들어버린다.
이를 통해
preempt_count
만 체크하여preempt_count == 0
이면 리스케쥴링을 수행한다.즉, TIF_NEED_RESCHED는 리스케쥴링이 필요할 때 세팅되며, 바로 직전에는 PREEMPT_NEED_RESCHED(inverted)가 세팅된다.
-
가능성이라 한 이유는 preempt_count가 0이 아니면 TIF_NEED_RESCHED가 있더라도 리스케쥴 될 수 없기 때문이다. ↩
-
- Mar 20, 2021
About ftrace
#About ftrace
ftrace란?
리눅스 커널 개발자에게 커널 내부에서 어떤 함수가 불렸는지, 어떤 이벤트가 발생했는지 알려주는 트레이스 기능이다.
예를 들어, 목표 커널 함수1가 불릴 때 콜스택을 확인할 수 있다.
콜스택을 확인함으로써 목표 커널 함수가 어떤 함수에 의해 불리는지 확인할 수 있다. (커널 공부에 좋을듯)
ftrace 사용법
1. 트레이스 포인트 지정하기
임의의 커널 함수 foo()에 진입 할 때의, 콜스택을을 확인하고 싶은 경우 아래와 같은 명령어를 입력한다.
$ echo 0 > /sys/kernel/debug/tracing/tracing_on # trace off $ echo secondary_start_kernel > /sys/kernel/debug/tracing/set_ftrace_filter # secondary_start_kernel에 트레이스 포인트 설정 $ echo function > /sys/kernel/debug/tracing/current_tracer # tracer로 function 지정 $ echo foo > /sys/kerenl/debug/tracing/set_ftrace_filter # foo에 트레이스 포인트 설정 $ echo 1 > /sys/kernel/debug/tracing/options/func_stack_trace # 함수 entry에서 콜스택을 출력하도록 설정 <stack trace> $ echo 1 > /sys/kernel/debug/tracing/options/sym-offset # 콜스택에 호출 오프셋 출력하도록 설정 예) vfs_read+0x9c $ echo 1 > /sys/kernel/debug/tracing/tracing_on # trace on
특이한 부분에 대한 설명을 간략히 하고 넘어가면,
$ echo secondary_start_kernel > /sys/kernel/debug/tracing/set_ftrace_filter # secondary_start_kernel에 트레이스 포인트 설정
set_ftrace_filter
에 아무것도 설정하지 않고 ftrace를 키면, ftrace는 모든 커널 함수에 대하여 트레이싱을 한다.
모든 커널 함수에 의해 트레이스가 발생되면, 그 오버헤드가 엄청나 시스템은 락업 상태에 빠진다.
그러므로 부팅 이후 절대 불리지 않을 함수secondary_start_kernel
2를 트레이스 포인트로 찍어준다.$ echo function > /sys/kernel/debug/tracing/current_tracer # tracer로 function 지정
function
은 함수 호출(function entry)시, 원하는 정보를 찍는 트레이서이다.
그 외로는function_tracer
가 있으며function
과 비슷하나 function exit을 표현한다는 점이 다르다.
어떠한 트레이서도 설정하지 않으려면,nop
을 지정한다.2. 트레이스 뽑기
트레이스를 작동 시킨 후, 커널 함수
foo
를 목표 커널함수로 설정 하고나서, 트레이스를 뽑아본다.
뽑힌 트레이스는 메모리 링버퍼에 저장되어 있으며, 링버퍼의 내용을 읽어 파일로 저장한다.$ echo 0 > /sys/kernel/debug/tracing/tracing_on # trace off $ cp /sys/kernel/debug/tracing/trace trace_log # 링버퍼의 내용을 trace_log로 저장
3. function 트레이스 내용 분석
cat-2834 [003] d... 7890.883529: rpi_get_interrupt_info+0x14/0x6c <-show_interrupts+0x2e0/0x3e4 cat-2834 [003] d... 7890.883574: <stack trace> => rpi_get_interrupt_info+0x18/0x6c => show_interrupts+0x2e0/0x3e4 => seq_read+0x388/0x4d4 => proc_reg_read+0x70/0x98 => __vfs_read+0x48/0x16c => vfs_read+0x9c/0x164 => ksys_read+0x74/0xe8 => sys_read+0x18/0x1c => ret_fast_syscall+0x0/0x28 => 0x7e990520
먼저
d...
의 의미를 살펴보면 다음과 같다.d
: interrupt disabled (현재 컨텍스트에서의 인터럽트 허용 플래그와 같다.)n
: need resched (TIF_NEED_RESCHED, PREEMPT_NEED_RESCHED 플래그 상황에 따라 다르다.)h/s
: interrupt context or softIRQ context0~5
: preempt_countthread_info
의preempt_count
의 각 필드를 참조하여 출력하는 것으로 추정된다.그 외적으로 특이한 부분은 없는 것 같다.
4. Events 트레이스 내용 분석
Events는 커널에서 미리 정의되어있는 이벤트에 대하여 트레이스를 뽑게 도와준다.
각 이벤트는 미리 정의된 포맷으로 출력되며, 각 함수 기능에 따라 충분히 도움이 되는 정보를 제공한다.
예를 들어, IRQ의 경우 IRQ 번호, IRQ 핸들링 성공 여부 등을 출력한다. 그리고 sched_switch의 경우 이전 태스크와 다음 태스크의 정보를 출력해 준다.
아래는 events 트레이스를 ON 시키기 위한 명령어 들이다.$ echo 0 > /sys/kernel/debug/tracing/tracing_on # trace off $ echo 0 > /sys/kernel/debug/tracing/events/enable # ALL event traces off $ echo 1 > /sys/kernel/debug/tracing/events/sched/sched_switch/enable $ echo 1 > /sys/kernel/debug/tracing/events/irq/irq_handler_entry/enable $ echo 1 > /sys/kernel/debug/tracing/events/irq/irq_handler_exit/enable $ echo 1 > /sys/kernel/debug/tracing/tracing_on # trace on
이후, 트레이스 뽑기에서 나와있는 방법으로 트레이스를 뽑는다.
결과는 다음과 같다.kworker/u8:1-3871 [003] d... 33794.390123: sched_switch: prev_comm=kworker/u8:1 prev_pid=3871 prev_prio=120 prev_state=S ==> next_comm=kworker/u8:1 next_pid=4010 next_prio=120 <idle>-0 [000] d.h. 33794.391185: irq_handler_entry: irq=162 name=arch_timer <idle>-0 [000] dnh. 33794.391219: irq_handler_exit: irq=162 ret=handled
위에서
irq_handler_entry
와irq_handler_exit
에 의해 발생한 트레이스부터 보면irq 162
이 발생하여arch_timer
라는 irq handler를 실행하였고, 결과는 제대로 핸들링(handled
)되었다는것을 알 수 있다.
또한sched_switch
에 의해 발생한 트레이스는kworker(pid: 3871)
로부터 다른kworker(pid: 4010)
으로 컨텍스트 스위칭이 일어났다는것을 알 수 있다.
그 외에도 정말 많은 커널 이벤트들을 트레이스로 뽑을 수 있다.
뽑을 수 있는 기 정의된 이벤트들은/sys/kernel/debug/tracing/events/
를 보면 알 수 있다.
ftrace의 event는 어떻게 발생되어 기록되는가?
모든 이벤트 트레이스는
trace_ + event_name
함수를 호출하여 기록된다.
예를 들어,irq_handler_entry
는trace_irq_handler_entry
함수를 이용하여 트레이스가 기록된다.1. irq_handler_entry & irq_handler_exit
/* code at kernel/irq/handle.c */ trace_irq_handler_entry(irq, action); res = action->handler(irq, action->dev_id); trace_irq_handler_exit(irq, action, res);
위 코드에서 보듯이, IRQ 핸들러 실행 전후에 트레이스 기록을 수행한다.
위 함수들은 아래에 다음과 같이 매크로 형태로 정의되어 있다.TRACE_EVENT(irq_handler_entry, TP_PROTO(int irq, struct irqaction *action), TP_ARGS(irq, action), TP_STRUCT__entry( __field( int, irq ) __string( name, action->name ) ), TP_fast_assign( __entry->irq = irq; __assign_str(name, action->name); ), TP_printk("irq=%d name=%s", __entry->irq, __get_str(name)) );
2. sched_switch
static void __sched notrace __schedule(bool preempt) { ... if (likely(prev != next)) { rq->nr_switches++; rq->curr = next; ++*switch_count; trace_sched_switch(preempt, prev, next); /* Also unlocks the rq: */ rq = context_switch(rq, prev, next, &rf); } else { ... }
위에서
task_struct *prev
에서task_struct* next
로context_switch()
가 발생할 때,trace_sched_switch()
가 호출됨을 알 수 있다.
trace_schecd_switch()
내부를 보면task state
가 어떻게 정의되어 있는지 유추가 가능하다.TP_printk("prev_comm=%s prev_pid=%d prev_prio=%d prev_state=%s%s ==> next_comm=%s next_pid=%d next_prio=%d", __entry->prev_comm, __entry->prev_pid, __entry->prev_prio, (__entry->prev_state & (TASK_REPORT_MAX - 1)) ? __print_flags(__entry->prev_state & (TASK_REPORT_MAX - 1), "|", { TASK_INTERRUPTIBLE, "S" }, { TASK_UNINTERRUPTIBLE, "D" }, { __TASK_STOPPED, "T" }, { __TASK_TRACED, "t" }, { EXIT_DEAD, "X" }, { EXIT_ZOMBIE, "Z" }, { TASK_PARKED, "P" }, { TASK_DEAD, "I" }) : "R", __entry->prev_state & TASK_REPORT_MAX ? "+" : "", __entry->next_comm, __entry->next_pid, __entry->next_prio)
kworker/u8:1-3871 [003] d... 33794.390123: sched_switch: prev_comm=kworker/u8:1 prev_pid=3871 prev_prio=120 prev_state=S ==> next_comm=kworker/u8:1 next_pid=4010 next_prio=120
위에서는
prev_state=S
이므로,TASK_INTERRUPTIBLE
상태임을 알 수 있다.
본 글은 “디버깅을 통해 배우는 리눅스 커널의 구조와 원리”를 참조하였습니다.
Posts