Posts

  • Mar 28, 2021

    task_struct - 1

    task_struct


    state

    state 필드는 task의 실행 상태를 나타내며, exit_state1는 task의 종료 상태를 나타낸다.
    사실 stateexit_state는 하나의 필드로 합쳐져있는것으로 보이나2, 실수를 줄이기 위해 실행 상태와 종료 상태를 분리한것으로 보인다.

    task의 실행 상태 (state)

    태스크의 실행 상태는 다음의 값을 가질 수 있다. (kernel 4.19 기준)
    이 때, 4, 5, 6, 7은 speical task state로 불린다.

    1. TASK_RUNNING
      • task가 정상적으로 실행되고 있을 때를 뜻한다. 실행이라 함은 CPU에서 직접 실행하고 있는 중, 그리고 런큐에서 CPU를 받기 위해 대기하는 중을 포함한다.
    2. 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 되어야 한다.
    3. TASK_UNINTERRUPTIBLE
      • 보통 I/O operation과 같이 TASK_RUNNING에 있어 중요한 작업을 처리하기 위한 상태이다.
      • 위와 다르게 task가 signal에 의해 깨어날 수 없다4. 명시적인 wakeup 계열 함수(completion_done() 등) 외에는 절대 깨어나지 않는다. 만약 task가 강제 종료해야 한다고 하더라도 SIGKILL을 받을 수 없기 때문에 종료할 수 없다.
    4. __TASK_STOPPED
      • SIGSTOP 등의 의해 태스크 실행 중단이 되었음을 뜻한다. 주로 TASK_WAKEKILL과 조합하여 많이 쓰인다. (TASK_STOPPED)
    5. __TASK_TRACED
      • ptrace에 의해 task가 trace되고 있음을 뜻한다. sys_ptrace로 target task에 attach하면 task state에 __TASK_TRACED를 적용한다. 주로 TASK_WAKEKILL과 조합하여 많이 쓰인다. (TASK_TRACED)
    6. 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로 재-바인딩 시켜준다.
    7. TASK_DEAD
      • TASK가 종료할 때, 종료했음을 의미하는 상태이다. 이 상태는 태스크 생존에 필수적이지 않은 모든 리소스(file descriptor 등)를 반환한 상태이며, 다른 태스크로 컨텍스트 스위칭 후, 이 태스크의 스택과 task_struct 또한 정리될 예정임을 뜻한다.
    8. TASK_WAKEKILL
      • SIGKILL을 받을 수 있는 상태임을 뜻한다. 이 상태는 다른 상태와 조합하여 쓰인다. 예를 들어, TASK_UNINTERRUPTIBLE | TASK_WAKEKILL = TASK_KILLABLE 임을 뜻하며, 다른 signal은 무시하지만 kill 시그널은 무시하지 않는다라는 뜻이다.
    9. TASK_WAKING
      • Task가 깨어나는 중임을 뜻한다.
      • 기존의 목적: 두 개 이상의 태스크가 타겟 태스크를 깨우려고 할 때, 한 개 태스크만이 깨울 수 있도록 만든다. 6
      • 코드를 읽어보면 딱히 그 목적보다, Task를 깨워서 다른 run queue로 enqueue 하기 전, 원하는 데이터를 재정비 할 수 있도록 상태를 저장해주는 느낌이다. (예, migration이 필요한 경우, task의 virtual runtime을 run queue의 virtual runtime base에 맞게 재조정 해준다.)
    10. TASK_NOLOAD
      • 이 상태를 할당받은 task는 load를 계산할 때, load 계산에 들어가지 않는다. 일반적으로 TASK_UNINTERRUPTIBLE | TASK_NOLOAD로 같이 쓰일 것이라고 설계자는 주장하고 있다. (코드에 잘 보이진 않는다.) 7
      • load 계산 시, activate task를 고려하며, activate task는 TASK_RUNNINGTASK_UNINTERRUPTIBLE을 포함한다.
      • TASK_UNINTERRUPTIBLE 상태는 TASK_RUNNING 중, I/O operation과 같이 중요한 일 때문에 불가피하게 blocking 상태로 들어갔음을 의미하며, blocking이 풀리면 바로 TASK_RUNNING 상태로 복귀할 것을 기대한다. 이 때문에 load로 계산한다.
      • TASK_INTERRUPTIBLE은 사용자 입력 대기 상태 등, 긴 시간동안 휴면 상태로 있어야 하는 태스크가 갖는 상태이며 load로 계산하지 않는다. (일반적으로 긴 시간동안 유휴하기 때문이다.)
    11. TASK_NEW
      • 태스크가 새롭게 생성되고 있음을 의미한다. wake_up_new_task()에서 TASK_RUNNING을 할당 받기 전, copy_process()sched_fork() 함수에서 세팅되는 상태이며, 절대 실행되지 않고, 시그널 핸들링도 처리하지 않고, 런큐에 들어갈 수도 없는 상태를 담보한다.

    1. https://yongshikmoon.github.io/2021/03/29/task_struct-2.html 

    2. 실제로 state와 exit_state는 비트 플래그가 겹치는 부분이 없다. 즉, 서로 상호배타적인 비트플래그를 갖는다. 

    3. RESTARTSYS라는 의미를 알 수 있듯이, syscall을 restart하라는 말이다. 다시 말하면 signal handling을 한 후, syscall을 restart하여 wait_event_interruptible()을 재실행하라는 의미이며, 이 때문에 syscall은 reenterancy가 있어야 한다라고 말한다. 

    4. signal_wake_up()함수를 보면 TASK_INTERRUPTIBLE state만 signal을 받아 깨어날 수 있음을 알 수 있다. 

    5. https://lwn.net/Articles/500338/, 구현은 많이 변했지만 주석은 읽어볼만하다. 

    6. https://lore.kernel.org/patchwork/patch/170913/ 

    7. 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_countpreempt_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_RESCHED0x8000000(32bit MSB)이며, 스케쥴링이 필요한 경우 관련 비트를 클리어 한다. (inverted flag bit)


    TIF_NEED_RESCHED vs PREEMPT_NEED_RESCHED

    thread_info에 동일한 의미의 플래그가 두 개 있다. 헷갈린다.

    관련 내용은 2013년 패치노트, sched: Add NEED_RESCHED to the preempt_count에 존재한다.

    동일한 의미의 플래그가 두 개 있는 이유를 요약하자면 다음과 같다.

    (이해를 위해 자체 추측 내용 또한 포함되어 있다.)

    1. TIF_NEED_RESCHED가 있고, 이 플래그는 리스케쥴링이 필요하다는 플래그였다.

    2. 다만, 리스케쥴링을 위해서는 preempt_count == 0이여야 했다. 즉, 현 태스크가 선점 가능해야 했다.

      • 주의) 물론 TIF_NEED_RESCHED가 셋팅 되어 있다고 해서, preempt_count == 0일 필요는 없다.
    3. 즉, 스케쥴링을 위해서는 TIF_NEED_RESCHEDpreempt_count동시에 체크해야 한다.

      • 이는 critical section이 필요한 작업으로 성능상 불이익을 초래한다.
    4. preempt_count만을 읽어서 리스케쥴링이 필요한지 체크할 수 있을까?

    해결법은 간단하다.

    TIF_NEED_RESCHED 플래그를 preempt_count로 미리 반영하는 것이다.

    다만, TIF_NEED_RESCHEDpreempt_count로 수시로 반영하면 atomic operation이 계속 발생하여 성능상 불이익이 발생한다.

    (preempt_count를 셋팅할 때, atomic operation으로 모든 코어가 동일한 값을 보도록 해야 한다.)

    필요할 때만 반영하도록 리스케쥴링 여부를 체크할 때, 바로 전에 TIF_NEED_RESCHEDpreempt_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)가 세팅된다.


    1. 가능성이라 한 이유는 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_kernel2를 트레이스 포인트로 찍어준다.

    $ 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 context

    0~5: preempt_count

    thread_infopreempt_count의 각 필드를 참조하여 출력하는 것으로 추정된다.

    lwn.net 3

    그 외적으로 특이한 부분은 없는 것 같다.

    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_entryirq_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_entrytrace_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* nextcontext_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 상태임을 알 수 있다.


    본 글은 “디버깅을 통해 배우는 리눅스 커널의 구조와 원리”를 참조하였습니다.


    1. trace가 가능한 커널 함수는 /sys/kernel/debug/tracing/available_tracers로 확인할 수 있다. 그 외의 함수를 트레이싱 하려고 하면 시스템이 다운될 수 있다. 

    2. secondary_start_kernel 함수는 부트 시퀀스에서 CPU 0가 아닌 다른 CPU를 부팅할 때 쓰는 함수이며, 부팅 이후는 절대 부를 일이 없는 함수이다. 

    3. Four short stories about preempt_count()