ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [리눅스 커널] Interrupt - Softirq
    Linux/kernel 2023. 10. 18. 19:46

    글의 참고

    - http://www.wowotech.net/irq_subsystem/soft-irq.html

    - https://lwn.net/Articles/831678/

    - http://www.wowotech.net/irq_subsystem/workqueue.html

    - https://lwn.net/Articles/520076/

    - https://lwn.net/Articles/274695/

    - https://stackoverflow.com/questions/20887918/why-softirq-is-used-for-highly-threaded-and-high-frequency-uses

    - https://zhuanlan.zhihu.com/p/480267700


    글의 전제

    - 밑줄로 작성된 글은 강조 표시를 의미한다.

    - 그림 출처는 항시 그림 아래에 표시했다.


    글의 내용

    - Overview

    1. softirq

    " softirq 는 리눅스 커널의 bottom-half 기법 중 하나다. 굉장히 초기 시절부터 사용되오던 기법으로 소프트웨어적으로 hardware interrupt 와 비슷한 메커니즘을 만들어준다. softirq 는 일반 드라이버에서 사용되는 기법은 아니다. 오직 커널 내부적으로만 사용되는 기법이다. 그러나, 드라이버에서 종종 사용되는 tasklet 이 softirq 를 기반으로 하기 때문에 softirq 를 제대로 이해한다면 tasklet 를 사용하는데 있어서 최적의 코드를 작성할 수 있을 것이다.

     

    " 위에서 말했다시피 softirq 는 커널에서만 내부적으로 사용된다. 거기다가 동적으로 추가될 수 없다. 무조건 정적으로, 사용할 수 softirq 의 개수가 정해져있다. 왜 개수가 고정되었을까?

    1. softirq 는 hardware interrupt 를 모방해서 만들어졌다. 그리고, 하드웨어 인터럽트 개수 또한 고정되어 있다.
    2. softirq 는 higi-priorty task 다. 즉, softirq 가 많이 사용될 수 록, 전체적인 반응성이 낮아질 수 있다.

     

    " 위와 같은 이유들로 리눅스 커널은 정적이면서 적은 개수의 softirq 만 지원하고 있다.

     

     

     

    2. Basic concept

    " 인터럽트 처리 모듈은 모든 OS 에서 굉장히 중요한 모듈이다. 왜냐면, 퍼포먼스와 직접적으로 연관이 있기 때문이다. 일반적으로, 인터럽트가 발생하면 해당 인터럽트가 마무리 될 때 까지 다른 인터럽트는 실행되지 않는다. 이게 가장 큰 문제다. 인터럽트 처리가 오래 걸릴 경우 다른 인터럽트를 받을 수 없기 때문이다. 즉, 인터럽트 핸들러는 절대로 많은 일을 해서는 안된다. 더 정확히 표현하면, 절대 오래 걸릴일은 하면 안된다. 인터럽트 핸들러에서 해도 되는 일은 생각보다 많지 않다.

    1. 리얼 타임으로 응답해야 하는 경우는 인터럽트 핸들러에서 응답해준다.

    2. 하드웨어 관련 작업들을 수행해도 된다. 예를 들어, 메모리를 읽거나, ACK를 응답하거나, 하드웨어 레지스터 등을 읽는 것을 수행할 수 있다.

    3. 만약, shared interrupt 를 사용할 경우, 자신 때문에 발생한 하드웨어 인터럽트인지 확인하기 위해 다른 하드웨어 작업들이 필요할 수 있다. 이런 경우도 인터럽트 핸들러에서 수행한다.

     

    " 이외의 작업들은 대개 bottom-half 에서 수행한다. 이렇게 bottom-half 기법이 등장하면서 인터럽트를 Off 하는 시간이 줄어들고, 그에 따라 시스템의 반응성이 굉장히 좋아졌다. 

     

     

     

    3. preempt_count

    " preempt_count 는 현재 동작하고 있는 스레드의 상태, 선점이 될 수 있는지, sleep 될 수 있는지 여부등을 나타낸다.

    The purpose of this counter is to describe the current state of whatever thread has been running, whether it can be preempted, and whether it is allowed to sleep. To do so, it must track a number of different states, so it is split into several sub-fields

    - 참고 : https://lwn.net/Articles/831678/

     

     

    " preempt_count 는 32비트로 이루어져 있으며, 각 필드의 내용은 다음과 같다.

    - preempt_count[7:0] = preempt-disable count
    " 현재 프로세스가 선점될 수 있는지 없는지를 나타낸다. 만약, preempt_count[7:0] 가 0 이 아니면, 선점을 허용하지 않는다는 뜻이다(preempt_disable 함수를 통해서 preempt_count[7:0] 를 1 증가시키면 된다). 만약, preempt_count[7:0] 가 0 이면, 현재 태스크가 선점될 수 있음을 의미한다. 그리고, preempt_count[7:0] 는 지금까지 선점이 disable 된 횟수를 기록한다. 즉, preempt_disable 함수가 몇 번 호출됬는지를 기록한다.

    - preempt_count[15:8] = software interrupt count
    " 실행중이던 스레드가 software interrupt 에 의해서 몇 번이나 선점되었는지를 기록한다. softirq 에는 2가지 사용 시나리오가 있다.
    1. softirq[8] - 하나의 CPU 에서 softirq 는 반드시 순차적으로 실행되어야 한다. 즉, 하나의 CPU 에서 여러 개의 softirq 가 실행될 수 는 없다. 그래서, softirq count 의 8번째 비트를 통해서 현재 태스크가 softirq context 임을 나타낸다. 
    2. softirq[15:9] - 필요에 따라 process context 에서 softirq 를 disable 해야하는 경우가 존재한다. 이럴 때, local_bh_enable / local_bh_disable 함수를 통해 bottom-half 기법들을 활성화 / 비활성화 할 수 있다. 이 때, softirq count[15:9] 가 감소 및 증가한다.
    // kernel/softirq.c - v6.5
    /*
     * SOFTIRQ_OFFSET usage:
     *
     * On !RT kernels 'count' is the preempt counter, on RT kernels this applies
     * to a per CPU counter and to task::softirqs_disabled_cnt.
     *
     * - count is changed by SOFTIRQ_OFFSET on entering or leaving softirq
     *   processing.
     *
     * - count is changed by SOFTIRQ_DISABLE_OFFSET (= 2 * SOFTIRQ_OFFSET)
     *   on local_bh_disable or local_bh_enable.
     *
     * This lets us distinguish between whether we are currently processing
     * softirq and whether we just have bh disabled.
     */


    - preempt_count[19:16] = hardware interrupt count
    " 현재 실행중인 hardware interrupt handler 의 nesting depth 를 알려준다. irq_enter 함수가 호출되면 hardirq context 에 진입했다는 것을 의미하고, irq_exit 함수가 호출되면 hardirq context 를 빠져나왔음을 의미한다. irq_enter 함수안에 preempt_count_add(HARDIRQ_OFFSET) 함수가 hardirq count 를 1 증가시킴으로써 현재 hardirq context 라는 것을 알린다. irq_exit 함수안에서 preempt_count_sub(HARDIRQ_OFFSET) 함수가 hardirq count 를 1 감소시킴으로써 현재 hardirq context 빠져나왔음을 알린다. hardirq count 가 4비트 라는 것은 15단계 충접 및 선점이 있을 수 있음을 의미한다.

    v2.6 이전에서는 hardirq nesting 이 가능했을지 모르지만, 현재는 불가능하다. 그래서, 실제로는 hardirq count 는 0 아니면 1 이다(참고로, depth level 을 늘리는 만큼, 스택 스위칭도 많이 발생할 수 있으므로, hardirq count 를 늘리는 것은 결코 좋은 방법이 아니다) 

    - preempt_count[23:20] = Non-maskable interrupt count
    " 실행중이던 스레드가 Non-maskable interrupt 에 의해서 몇 번이나 선점되었는지를 기록한다.

    - preempt_count[31] = Reschedule needed 
    " 이 비트가 SET 되어 있을 경우, CPU 를 제일 먼저(처음)으로 할당해야 할 만큼 우선 순위가 높은 태스크가 실행을 기다리고 있다는 것을 의미한다. 이 비트는 preempt_count 가 0 인 경우에만 설정이 가능하다.

     

     

     

    4. various contexts

    " 리눅스 커널에서는 preempt_count 값에 따라 다양한 실행 컨택스트를 제공한다. 

    // include/linux/preempt.h - v6.5
    #define nmi_count()	(preempt_count() & NMI_MASK)
    #define hardirq_count()	(preempt_count() & HARDIRQ_MASK)
    #ifdef CONFIG_PREEMPT_RT
    # define softirq_count()	(current->softirq_disable_cnt & SOFTIRQ_MASK)
    #else
    # define softirq_count()	(preempt_count() & SOFTIRQ_MASK)
    #endif
    #define irq_count()	(nmi_count() | hardirq_count() | softirq_count())
    
    /*
     * Macros to retrieve the current execution context:
     *
     * in_nmi()		- We're in NMI context
     * in_hardirq()		- We're in hard IRQ context
     * in_serving_softirq()	- We're in softirq context
     * in_task()		- We're in task context
     */
    #define in_nmi()		(nmi_count())
    #define in_hardirq()		(hardirq_count())
    #define in_serving_softirq()	(softirq_count() & SOFTIRQ_OFFSET)
    #define in_task()		(!(in_nmi() | in_hardirq() | in_serving_softirq()))
    
    /*
     * The following macros are deprecated and should not be used in new code:
     * in_irq()       - Obsolete version of in_hardirq()
     * in_softirq()   - We have BH disabled, or are processing softirqs
     * in_interrupt() - We're in NMI,IRQ,SoftIRQ context or have BH disabled
     */
    #define in_irq()		(hardirq_count())
    #define in_softirq()		(softirq_count())
    #define in_interrupt()		(irq_count())
    1. hardirq context(IRQ context) : 이 컨택스트는 실제 하드웨어 인터럽트를 처리하고 있는 상태를 의미한다. 만약, hardirq count 가 0 보다 크다면, 현재 hardirq context 에 있음을 의미한다. 그런데, hardirq count 가 1 이면, hardirq context 지만, nested context 가 아님을 의미한다. 만약, hardirq count 가 1 보다 크면, 현재 hardirq context 는 nested context 임을 의미한다. 예를 들어, x86 에서 system call 을 호출하기 위해서 `int` 명령어를 사용하는데, 이 때 시스템 콜 인터럽트는 `트랩 게이트` 로 설정한다. 그런데, 트랩 게이트 인터럽트는 다른 하드웨어 인터럽트에 의해 선점이 가능하다.

    2. softirq context 는 조금 애매하지만, 2개로 나눠볼 수 있다. 
    - real softirq context by softirq handler : 실제 하드웨어 인터럽트가 발생해서 생성된 softirq context 를 의미한다. 이 때, softirq 는 softirq handler 에 의해서 처리된다.
    - softirq context for only synchronization : 동기화를 목적으로 process context 에서도 단독으로 local_bh_disable / local_bh_enable 함수가 사용될 수 있다. 예를 들어, process context 에서 동기화를 목적으로 critical section 에 진입하기전에 local_bh_disble 함수를 호출했다. 그리고, 아직 작업이 끝나지 않았는데 인터럽트가 발생해서 실행 흐름을 인터럽트에게 선점당했다. 인터럽트의 기본적인 처리만 끝낸 후 반환하고 softirq 를 실행하려고 했는데 현재 softirq 의 상태가 disabled 였다. 이런 경우는 hardirq 가 리턴되는 시점에 softirq 가 실행될 수 없다(`in_interrupt` 에서 true 를 리턴). 해당 softirq 는 pending 되고 process context 로 실행이 복귀된다. 여기서 중요한 점은 process context 에서 local_bh_disable 함수를 호출하는 시점부터 softirq context 로 전환된다는 점이다. 왜냐면, local_bh_disable() 함수를 호출하면, preempt_count[15:9] 가 증가하기 때문에, softirq_count() 함수를 호출하면 양수를 반환하게 된다. 그러므로, 현재 코드가 softirq context 에서 실행되고 있는 것으로 간주된다. 물론, 이 시점에 softirq context 를 처리하는 주체는 softirq handler 는 아니다. softirq context 에서 critical section 작업을 마무리하고 local_bh_enable 함수를 호출하면 do_softirq() 함수가 호출되고 softirq handler 및 ksoftirqd 를 통해서 pending softirq 들을 처리한다. 만약, 현재 softirq 를 처리하는 주체가 softirq handler 인지를 알고 싶다면, in_serving_softirq 함수를 호출하면 된다. 이 함수는 softirq_count 에서 8번째 비트만 검사해서 softirq handler 가 실행중인지를 판단한다.


    3. process context 는 in_task 매크로 함수로 표현할 수 있다.

    - NMI 를 처리중이 아니여야 하고
    - hardirq 를 처리중이 아니여야 하며
    - softirq handler 가 실행중이 아니여야 한다.

     

     

    " softirq_count 와 in_serving_softirq 의 차이가 뭘까? softirq_count 함수는 BH 까지 감지해서 softirq context 상태를 검사한다. 즉, SOFTIRQ_OFFSET 과 SOFT_DISABLE_OFFSET 모두를 검사한다. 그러나, in_serving_softirq 함수는 BH 의 여부는 판단하지 않는다. softirq handler 가 실제로 실행중 인지를 검사해서 softirq context 상태를 검사한다. 즉, SOFTIRQ_OFFSET 만 거마한다. 

    Replace the deprecated in_interrupt() with !in_task() because in_interrupt() returns true for BH disabled even if the call happens in the task context. in_task() is the right interface to differentiate task context from NMI, hard IRQ and softirq contexts.

    - 참고 : https://lore.kernel.org/lkml/YfPgRU7Swo0VczUd@dhcp22.suse.cz/T/

     

     

    " 그리고, 리눅스에서는 atomic context 라는 것도 존재한다. 아래 코드에서 atomic context 의 정의를 보면, `preempt_count != 0` 이기 때문에 다양한 정의가 만들어 질 수 있다. 정리하면, atomic context 는 아래의 3가지 중에 하나만 만족해도 atomic context 라고 부를 수 있다.

    1. 인터럽트 컨택스트에서 실행 중(only preemt_count[23:8] > 0)

    2. 프로세스 컨택스트에서 실행 중. 그러나, process scheduling(선점) 이 허용되지 않음. 즉, sleep / context switch 를 허용하지 않음. 그러나, 인터럽트는 허용함(only preempt_count[7:0] > 0).

    3. 인터럽트 컨택스트에서 실행 중이면서 process scheduling(선점) 이 허용되지 않음(both preempt_count[7:0] > 0 && preemt_count[23:8] > 0)

     

     

    " 위에 내용을 보면 atomic context 는 1:1 로 대응되는 상황이라고 보기가 어렵다. 그렇기 때문에, 특정한 상황을 판별해야 하는 경우에는 사용하지 말아야 한다.

    // include/linux/preempt.h - v6.5
    /*
     * Are we running in atomic context?  WARNING: this macro cannot
     * always detect atomic context; in particular, it cannot know about
     * held spinlocks in non-preemptible kernels.  Thus it should not be
     * used in the general case to determine whether sleeping is possible.
     * Do not use in_atomic() in driver code.
     */
    #define in_atomic()	(preempt_count() != 0)

     

     

    " 참고로, atomic context 에서는 sleep 은 당연히 안되고, user space 를 참조하는 API 도 사용해서는 안된다.

    code which is running in atomic context carefully follows a number of rules, including (1) no access to user space, and, crucially, (2) no sleeping.

    - 참고 : https://lwn.net/Articles/274695/

     

     

    " 아래는 preempt_count 를 필드별로 분리할 때 쓰이는 OFFSET & MASK 를 나타낸다. 계산법은 아래 예시를 참고하자.

    // include/linux/preempt.h - v6.5
    /*
     * We put the hardirq and softirq counter into the preemption
     * counter. The bitmask has the following meaning:
     *
     * - bits 0-7 are the preemption count (max preemption depth: 256)
     * - bits 8-15 are the softirq count (max # of softirqs: 256)
     *
     * The hardirq count could in theory be the same as the number of
     * interrupts in the system, but we run all interrupt handlers with
     * interrupts disabled, so we cannot have nesting interrupts. Though
     * there are a few palaeontologic drivers which reenable interrupts in
     * the handler, so we need more than one bit here.
     *
     *         PREEMPT_MASK:	0x000000ff
     *         SOFTIRQ_MASK:	0x0000ff00
     *         HARDIRQ_MASK:	0x000f0000
     *             NMI_MASK:	0x00f00000
     * PREEMPT_NEED_RESCHED:	0x80000000
     */
    #define PREEMPT_BITS	8
    #define SOFTIRQ_BITS	8
    #define HARDIRQ_BITS	4
    #define NMI_BITS	4
    
    #define PREEMPT_SHIFT	0
    #define SOFTIRQ_SHIFT	(PREEMPT_SHIFT + PREEMPT_BITS) // 8(8+0)
    #define HARDIRQ_SHIFT	(SOFTIRQ_SHIFT + SOFTIRQ_BITS) // 16(8+8)
    #define NMI_SHIFT	(HARDIRQ_SHIFT + HARDIRQ_BITS) // 20(16+4)
    
    #define __IRQ_MASK(x)	((1UL << (x))-1)
    
    #define PREEMPT_MASK	(__IRQ_MASK(PREEMPT_BITS) << PREEMPT_SHIFT)
    #define SOFTIRQ_MASK	(__IRQ_MASK(SOFTIRQ_BITS) << SOFTIRQ_SHIFT)
    #define HARDIRQ_MASK	(__IRQ_MASK(HARDIRQ_BITS) << HARDIRQ_SHIFT)
    #define NMI_MASK	(__IRQ_MASK(NMI_BITS)     << NMI_SHIFT)
    
    #define PREEMPT_OFFSET	(1UL << PREEMPT_SHIFT) // 0
    #define SOFTIRQ_OFFSET	(1UL << SOFTIRQ_SHIFT) // 2^8
    #define HARDIRQ_OFFSET	(1UL << HARDIRQ_SHIFT) // 2^16
    #define NMI_OFFSET	(1UL << NMI_SHIFT) // 2^20
    
    #define SOFTIRQ_DISABLE_OFFSET	(2 * SOFTIRQ_OFFSET) // 2^9
    
    #define PREEMPT_DISABLED	(PREEMPT_DISABLE_OFFSET + PREEMPT_ENABLED)
    - __IRQ_MASK(PREEMPT_BITS) = (1<<8)-1 = 0b0001 0000 0000 - 0b1 = 0b1111 1111
    " __IRQ_MASK(PREEMPT_BITS) << PREEMPT_SHIFT(0) = 0b1111 1111(0x0000_00FF)

    - __IRQ_MASK(SOFTIRQ_BITS) = (1<<8)-1 = 0b0001 0000 0000 - 0b1 = 0b1111 1111
    " __IRQ_MASK(SOFTIRQ_BITS) << SOFTIRQ_SHIFT(8) = 0b1111 1111 0000 0000(0x0000_FF00)

    - __IRQ_MASK(HARDIRQ_BITS) = (1<<4)-1 = 0b0001 0000 - 0b1 = 0b1111
    "  __IRQ_MASK(HARDIRQ_BITS) << HARDIRQ_SHIFT(16) = 0b1111 0000 0000 0000 0000(0x000F_0000)

    - __IRQ_MASK(NMI_BITS) = (1<<4)-1 = 0b0001 0000 - 0b1 = 0b1111
    " __IRQ_MASK(NMI_BITS) << NMI_SHIFT(20) = 0b1111 0000 0000 0000 0000 0000(0x00F0_0000)

     

     

    " 코드를 분석할 때, 아래 매크로들에 주의할 필요가 있다. `irq_stat.__softirq_pending` 가 전역 변수인 점과 매크로가 코드로 치환된다는 점을 이용해서 피연산자를 전달하지 않고도 값을 읽거나 쓰고있다. 이런 기법은 커널에서 상당히 자주 사용되기 때문에 주의할 필요가 있다(비슷한 예로 `wait-queue` 가 있다). 참고로, local_softirq_pending 은 현재 CPU 에 pending softirq 를 반환한다. set_softirq_pending 은 주로 pending softirq 를 0 으로 초기화할 때 사용한다. or_softirq_pending 은 새로운 softirq 요청이 들어오면 mark 하는 용도로 사용된다.

    // include/linux/interrupt.h - v6.5
    ifndef local_softirq_pending
    
    #ifndef local_softirq_pending_ref
    #define local_softirq_pending_ref irq_stat.__softirq_pending
    #endif
    
    #define local_softirq_pending()	(__this_cpu_read(local_softirq_pending_ref))
    #define set_softirq_pending(x)	(__this_cpu_write(local_softirq_pending_ref, (x)))
    #define or_softirq_pending(x)	(__this_cpu_or(local_softirq_pending_ref, (x)))
    
    #endif /* local_softirq_pending */
    // kernel/softirq.c - v6.5
    /*
       - No shared variables, all the data are CPU local.
       - If a softirq needs serialization, let it serialize itself
         by its own spinlocks.
       - Even if softirq is serialized, only local cpu is marked for
         execution. Hence, we get something sort of weak cpu binding.
         Though it is still not clear, will it result in better locality
         or will not.
    
       Examples:
       - NET RX softirq. It is multithreaded and does not require
         any global serialization.
       - NET TX softirq. It kicks software netdevice queues, hence
         it is logically serialized per device, but this serialization
         is invisible to common code.
       - Tasklets: serialized wrt itself.
     */
    
    #ifndef __ARCH_IRQ_STAT
    DEFINE_PER_CPU_ALIGNED(irq_cpustat_t, irq_stat);
    EXPORT_PER_CPU_SYMBOL(irq_stat);
    #endif

     

    " 그런데, softirq 는 queue 가 없기 때문에, 연속해서 2개가 요청될 경우에 후자의 요청으로 오버라이트 되어버린다. 즉, 동일한 softirq 요청에 대해서 한 번만 처리한다는 뜻이다. 왜 이렇게 만들었을까? 실제 하드웨어 인터럽트와 동일한 메커니즘을 사용하기 때문이다. 하드웨어 인터럽트 또한 하나의 pending interrupt 만 저장한다.

     

     

    - Softirq machanism

    1. softirq interrupt vector

    " softirq 는 hardware interrupt 와 1:1 로 대응되기 때문에 마치 hardware interrupt 가 발생하는 것과 같은 메커니즘으로 동작해야 한다. 그래서, hardware interrupt number 와 마찬가지로 soft interrupt number 또한 존재한다. 당연한 얘기지만, 이 번호들도 hardware interrupt number 와 같이 유일해야 한다. 물론, softirq 는 순수 소프트웨어이기 때문에 특정 하드웨어 기능을 요구하지 않는다.

    // include/linux/interrupt.h - v6.5
    /* PLEASE, avoid to allocate new softirqs, if you need not _really_ high
       frequency threaded job scheduling. For almost all the purposes
       tasklets are more than enough. F.e. all serial device BHs et
       al. should be converted to tasklets, not to softirqs.
     */
    
    enum
    {
    	HI_SOFTIRQ=0,
    	TIMER_SOFTIRQ,
    	NET_TX_SOFTIRQ,
    	NET_RX_SOFTIRQ,
    	BLOCK_SOFTIRQ,
    	IRQ_POLL_SOFTIRQ,
    	TASKLET_SOFTIRQ,
    	SCHED_SOFTIRQ,
    	HRTIMER_SOFTIRQ,
    	RCU_SOFTIRQ,    /* Preferable RCU should always be the last softirq */
    
    	NR_SOFTIRQS
    };
    - HI_SOFTIRQ & TASKLET_SOFTIRQ : `HI_SOFTIRQ` 는 높은 우선 순위를 갖는 tasklet 에게만 할당된다. `TASKLET_SOFTIRQ` 은 보통의 우선 순위를 갖는 tasklet 에게 할당된다.

    - TIMER_SOFTIRQ : system tick 이 발생할 때 마다 호출되는 softirq timer 위해 사용되는 softirq 

    - NET_TX_SOFTIRQ & NET_RX_SOFTIRQ : 네트워크에서 데이터를 전송 및 수신할 때 사용되는 softirq

    - SCHED_SOFTIRQ : CPU`s 들간에 로드 밸런스를 위해서 사용되는 softirq

    - HRTIMER_SOFTIRQ : high-resolution timer 에서 사용되는 softirq

    - RCU_SOFTIRQ : RCU 에서 사용되는 softirq

     

     

    2. How to register a softirq

    : 위에서 봤다시피 softirq 는 `정적(static)` 으로 선언되어 있다. 즉, softirq 의 개수와 기능이 컴파일 시점에 고정된다. 이런 구조는 인터럽트 디스크립터와 상당히 유사함을 알 수 있다. 

    // include/linux/interrupt.h - v6.5
    struct softirq_action
    {
    	void	(*action)(struct softirq_action *);
    };

     

     

    " softirq 의 개수가 정적으로 고정되어 있다보니 배열을 선언해서 관리하는 것이 가능하다. 예를 들어, softirq_vec[TIMER_SOFTIRQ] 는 softirq timer handler 가 저장되어 있게된다.

    // kernel/softirq.c - v6.5
    static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;

     

     

    " softirq_vec 에서 눈 여겨볼 점은 2 가지가 존재한다.

    1. Per-CPU 가 아닌 정적 전역 변수로 점이다. 즉, 모든 CPU 가 공유한다.
    " 왜 softirq_vec 는 Per-CPU 로 선언하지 않았을까? softirq 는 실제 하드웨어 인터럽트와 비슷한 개념으로 만들었기 때문에, 하드웨어 인터럽트와 비교해보면 쉽게 알 수 있다. softirq_vec 는 인터럽트 테이블과 같은 역할을 한다. 일단, 인터럽트 벡터 테이블이 동적으로 변경되는 경우는 없다. 거의 시스템 부팅 시점에 모두 초기화가 완료된다. 그리고, 거의 수정되지 않는다. 즉, read-only 다. 거기다가 인터럽트 핸들러는 CPU 마다 다른 기능을 하지 않는다. 예를 들어, 페이지 폴트 핸들러가 하는 일이 CPU 마다 다를까? 그렇지 않다. 위와 같은 특성으로 softirq_vec 는 Per-CPU 로 선언할 필요가 없다.


    2. SMP 환경에서만 sofirq_vec 주소는 cache line 에 aligned 된다.
    " 대개 ___cacheline_aligned 속성을 사용하는 이유는 false sharing 막기 위해서다. 즉, 전혀 연관성 없는 데이터 때문에  softirq 관련 데이터가 캐쉬에서 제거되는 것을 원치않기 때문이다.

     

     

    " hardirq 는 하드웨어 내부적으로 컨트롤이 된다. 예를 들어, 인터럽트가 발생하면, 해당 인터럽트는 레지스터에 MASK 된다. 그리고, 인터럽트 핸들러가 작업을 마무리 하면 EOI 를 응답하고 다시 UN-MASK 된다. softirq 또한 이러한 비슷한 메커니즘을 소프트웨어적으로 구현한다.

    // include/asm-generic/hardirq.h - v6.5
    typedef struct {
    	unsigned int __softirq_pending;
    #ifdef ARCH_WANTS_NMI_IRQSTAT
    	unsigned int __nmi_count;
    #endif
    } ____cacheline_aligned irq_cpustat_t;
    
    DECLARE_PER_CPU_ALIGNED(irq_cpustat_t, irq_stat);

     

    " 위에서 __softirq_pending 필드가 softirq 의 레지스터 역할을 한다. 커널은 softirq 요청이 들어오면, __softirq_pending 필드에 해당 softirq 에 대응하는 비트를 SET 한다. 그리고, softirq 를 처리하는 시점에는 __softirq_pending 필드에 SET 된 softirq 요청들을 처리한다.

     

     

    3. How to register a softirq handler ?

    " softirq 핸들러를 등록하려면, open_softirq 함수를 사용하면 된다. 방법은 인터럽트를 등록하는 방법과 완전 동일하다. 인터럽트를 등록할 때, 인터럽트 벡터와 벡터에 저장할 인터럽트 핸들러 주소가 필요하다. softirq 도 동일하다. 아래 nr 은 softirq vector 를 의미하며, action 은 softirq handler 를 의미한다.

    // kernel/softirq.c - v6.5
    void open_softirq(int nr, void (*action)(struct softirq_action *))
    {
    	softirq_vec[nr].action = action;
    }

     

    " softirq_vec 는 모든 CPU 가 공유하는 변수인데 왜 동기화 메커니즘으로 보호하지 않을까? softirq register 절차는 시스템 초기화 과정에서 완료되는 부분이다. 그 이후에는 open_softirq 함수를 호출할 일이 없다. 즉, BSP 가 혼자 처리하는 절차이기 때문에 동기화 메커니즘이 필요없다.

     

     

     

    4. How to trigger a softirq

    " `raise_softirq` 함수는 인자로 전달된 softirq number(nr) 를 받아서 해당 softirq 를 실행한다. softirq 가 실행되는 대부분의 시나리오가 interrupt handler 내부이다 보니 마치 인터럽트가 비활성화된 상태에서만 호출해야 할 것 같지만, 그렇지 않다. 즉, 인터럽트를 비활성화 하지 않은 경우에도 사용할 수 있다.

    // kernel/softirq.c - v6.5
    void raise_softirq(unsigned int nr)
    {
    	unsigned long flags;
    
    	local_irq_save(flags);
    	raise_softirq_irqoff(nr);
    	local_irq_restore(flags);
    }

     

     

    " 그래서 softirq 는 2가지 인터페이스를 제공한다.

    1. raise_softirq : 해당 인터페이스를 호출하면 함수안에서 내부적으로 local interrupt 를 비활성화한다. 
    2. raise_softirq_irqoff : caller 측에서 인터럽트가 이미 비활성화가 되어있는 경우에 사용한다.

     

     

    " raise_softirq_irqoff 함수는 요청받은 softirq(nr) 을 트리거하고, ksoftirqd 에서 해당 요청을 처리가 가능한지를 확인한다.

    // kernel/softirq.c - v6.5
    /*
     * This function must run with irqs disabled!
     */
    inline void raise_softirq_irqoff(unsigned int nr)
    {
    	__raise_softirq_irqoff(nr);
    
    	/*
    	 * If we're in an interrupt or softirq, we're done
    	 * (this also catches softirq-disabled code). We will
    	 * actually run the softirq once we return from
    	 * the irq or softirq.
    	 *
    	 * Otherwise we wake up ksoftirqd to make sure we
    	 * schedule the softirq soon.
    	 */
    	if (!in_interrupt() && should_wake_ksoftirqd()) // --- 2
    		wakeup_softirqd();
    }
    // kernel/softirq.c - v6.5
    void __raise_softirq_irqoff(unsigned int nr)
    {
    	lockdep_assert_irqs_disabled();
    	trace_softirq_raise(nr);
    	or_softirq_pending(1UL << nr); // --- 1
    }
    1. 인자로 전달된 softirq number(nr) 에 대응하는 softirq 요청이 있음을 커널에 알린다. Per-CPU 로 선언되는 irq_stat.__softirq_pending 변수에 softirq number 에 대응하는 비트를 SET 한다. 후에 __do_softirq 함수에서 __softirq_pending 변수를 체크해서 softirq 를 처리한다.

    2. raise_softirq 함수를 호출했다고 해서 즉각적으로 softirq 를 실행할 수 있는게 아니다. 현재 처리중 인 인터럽트(NMI, hardirq, softirq)가 없어야 한다. 만약에 처리중 인 인터럽트가 있다면 요청한 softirq 를 즉각적으로 처리할 수 없으므로, __raise_softirq_irqoff 함수를 이용해서 일단 요청한 softirq 를 pending 처리를 해놓는 것이다. 만약, 처리중 인 인터럽트가 없다면 ksoftirqd 를 wake-up 시켜서 스케줄링 되도록 한다.

     

     

    " 그런데, raise_softirq 함수를 호출했는데 왜 즉각적으로 실행하지 않을까? softirq, tasklet, workqueue, threaded-irq 기법들은 모두 `deferred work` 에 속한다. 즉, 어느정도 지연을 감수해야 한다는 뜻이다. softirq & tasklet 이 workqueue 보다 퍼포먼스가 좋다고 하는 것은 전자가 interrut context 에서 실행되기 때문이다. 즉, context switch 이 발생할 일이 없기 때문에, 일단 한 번 실행되면 방해없이 쭉 실행할 수 있다(context switch 는 타이머 인터럽트 혹은 더 높은 우선 순위의 태스크가 런큐에 들어가면 발생하게 된다. 그런데, interrupt context 에서는 타이머 인터럽트에 의한 context switch 는 발생하지 않게된다. 그렇다면, 더 높은 우선 순위의 태스크가 런큐에 들어가 있는 경우밖에 없는데, interrupt context 환경에서는 이러한 일도 발생할 일이 없다).

     

     

    " 그러나, workqueue 는 process context 라서 context switch 이 발생할 수 있다(대표적으로 타이머 인터럽트에 의한 context switch). 이렇게 되면 workqueue 는 일단 한 번 실행되더라도 언제 끝 마칠지를 보장하지 못한다. 결국 요약하면 다음과 같다.

     

      softirq workqueue
    공통점 deferred work 중에 하나이기 때문에, 언제 실행될지 알 수 없다. deferred work 중에 하나이기 때문에, 언제 실행될지 알 수 없다.
    차이점 한 번 실행되면 선점될 가능성이 낮음. 즉, 방해없이 계속 실행될 가능성이 높음 한 번 실행 되더라도 선점될 가능성이 높음 

     

    " 만약, 즉각적인 실행을 원한다면 deferred work 가 아닌 hardirq handler 에 작성해야 한다.  

     

     

    " should_wake_ksoftirqd 함수는 !RT-kernel 에서 무조건 true 를 반환한다.

    #ifdef CONFIG_PREEMPT_RT
    static inline bool should_wake_ksoftirqd(void)
    {
    	return !this_cpu_read(softirq_ctrl.cnt);
    }
    #else
    static inline bool should_wake_ksoftirqd(void)
    {
    	return true;
    }
    #endif

     

     

    " wake_up_process 함수는 인자로 전달된 스레드를 TASK_RUNNING 상태로 변경한 뒤, 런큐에 삽입한다. `ksoftirqd` 는 DEFINE_PER_CPU 매크로를 통해서 생성된 것을 보면 프로세서별로 존재하는 것을 알 수 있다.

    // kernel/softirq.c - v6.5
    DEFINE_PER_CPU(struct task_struct *, ksoftirqd);
    ....
    
    /*
     * we cannot loop indefinitely here to avoid userspace starvation,
     * but we also don't want to introduce a worst case 1/HZ latency
     * to the pending events, so lets the scheduler to balance
     * the softirq load for us.
     */
    static void wakeup_softirqd(void)
    {
    	/* Interrupts are disabled: no need to stop preemption */
    	struct task_struct *tsk = __this_cpu_read(ksoftirqd);
    
    	if (tsk)
    		wake_up_process(tsk);
    }

     

    " 그런데, 런큐에 삽입된 ksoftirqd 는 언제 실행될까? ksoftirqd 는 RT-kernel 이 아닐 경우, SCHED_OTHER 정책을 사용한다(만약, RT 면 SCHED_FIFO 정책을 사용한다). SCHED_OTHER 정책은 `time-sharing scheduler policy` 로 타이머 인터럽트에 맞춰서 스케줄링 되는 방식을 의미한다. 이 정책은 다른 프로세스를 선점할 수 있는 정책이 아니기 때문에 언제 실행되지를 알 수 가 없다. 대부분의 일반 프로세스들은 이 정책을 사용한다. 정리하면, ksoftirqd 는 그냥 보통의 우선 순위를 갖기 때문에 시간이 되면 알아서 실행된다고 봐야한다. 참고로, RT-kernel 커널에서는 SCHED_FIFO & SCHED_RR 정책을 사용하는 프로세스들은 SCHED_OTHER 정책을 사용하는 프로세스를 즉각적으로 선점해버린다. 그래서, SCHED_OTHER 프로세스들은 기아 현상을 겪을 수 있다.

     

    " 그런데, 왜 ksoftirqd 는 SCHED_OTHER 정책을 사용할까? softirq 처리 과정에서 자기 자신을 re-raise 하는 경우도 적지 않게 존재한다. 이렇게 자기 자신을 re-raise 하는게 이상해 보일 수 있지만, 로직 상 어쩔 수 없이 사용하는 경우도 있기 마련이다. re-raise 과정이 많아 질 수록 많은 수 의 pending softirq 가 발생하고, 그 만큼 __do_softirq 에서 loop 를 많이 순환하게 된다. 만약, ksoftirqd 가 높은 우선 순위를 갖는다면, 많은 수 의 softirq 를 처리하기 위해 계속 스케줄링 될 것 이다. 결과적으로, user thread 는 스케줄러에 선택을 받지 못하게 되고 기아 현상이 발생하게 될 것이다. 그래서, ksoftirqd 에게 SCHED_OTHER 정책을 사용하는 것이다.

     

    " 만약, 리얼 타임 퍼포먼스를 내고 싶다면 어떻게 할까? 리눅스 커널은 인터럽트가 반환되는 시점에 인터럽트에 의한 선점당했던 태스크보다 우선 순위가 높은 태스크가 있는지를 검사한다. 만약, higher-priority task 가 존재한다면 해당 태스크를 스케줄링한다. 그렇지 않다면, 이전에 인터럽트에 의해 선점당했던 태스크를 다시 스케줄링한다.

     

     

     

    - How to enable / disable a softirq ? and, How to handle synchronization between softirq contexts ?

    " softirq 는 소프트웨어적으로 마치 hardirq 가 발생한 것처럼 동작해야 하기 때문에, 이미 실행중인 softirq 가 있다면, 동일 CPU 에서 다른 softirq 에 의해서 선점되는 것을 막아야 한다. 아래는 enable / disable hardirq 와 enable / disable softirq 관련 API 를 정리했다.

    - enable / disable local hardirq : local_irq_enable / local_irq_disable
    - enable / disable local bottom-half : local_bh_enable / local_bh_disable

     

     

    " softirq 얘기를 하다가 왠 `bh` 가 나왔을까? bh 는 `bottom-half` 의 약자다. local_bh_enable / local_bh_disable 함수는 tasklet & softirq 모두에서 사용되기 때문에 bh 가 붙은 것 이다.

    // include/linux/bottom_half.h - v6.5
    #if defined(CONFIG_PREEMPT_RT) || defined(CONFIG_TRACE_IRQFLAGS)
    extern void __local_bh_disable_ip(unsigned long ip, unsigned int cnt);
    #else
    static __always_inline void __local_bh_disable_ip(unsigned long ip, unsigned int cnt)
    {
    	preempt_count_add(cnt); // --- 1
    	barrier();
    }
    #endif
    
    static inline void local_bh_disable(void)
    {
    	__local_bh_disable_ip(_THIS_IP_, SOFTIRQ_DISABLE_OFFSET); // --- 1
    }
    1. softirq 를 disable 하는 방법은 생각보다 심플하다. 단지, 현재 동작중인 프로세스의 preempt_count 에서 softirq[15:8] 파트에 2를 더하는 것이다(SOFTIRQ_DISABLE_OFFSET = 2^9). 근데, 왜 2^9을 더할까? softirq count 의 8번째 비트는 실제 softirq handler 가 실행중임을 의미한다. 즉, 실제 softirq 가 처리중임을 나타내는 것은 softirq count[8] 이 나타낸다. 그렇다면, softirq count[15:9] 는 무엇을 의미할까? bottom-half synchronization depth 를 의미한다. 이 내용은 `various context` 섹션을 참고하자. 참고로, 첫 번째 인자인 `ipi` 는 디버깅용으로 사용하는 값이므로 자세한 설명은 생략한다.

     

     

    " local_bh_enable 함수는 disable 보다 훨씬 복잡하다. 왜냐면, softirq 를 처리하는 코드도 포함되어 있기 때문이다.

    // include/linux/bottom_half.h - v6.5
    static inline void local_bh_enable(void)
    {
    	__local_bh_enable_ip(_THIS_IP_, SOFTIRQ_DISABLE_OFFSET);
    }
    // kernel/softirq.c - v6.5
    #ifdef CONFIG_PREEMPT_RT 
    #else
    void __local_bh_enable_ip(unsigned long ip, unsigned int cnt)
    {
    	WARN_ON_ONCE(in_hardirq());  // --- 1
    	lockdep_assert_irqs_enabled();
    #ifdef CONFIG_TRACE_IRQFLAGS
    	local_irq_disable();
    #endif
    	/*
    	 * Are softirqs going to be turned on now:
    	 */
    	if (softirq_count() == SOFTIRQ_DISABLE_OFFSET)
    		lockdep_softirqs_on(ip);
    	/*
    	 * Keep preemption disabled until we are done with
    	 * softirq processing:
    	 */
    	__preempt_count_sub(cnt - 1); // --- 2
    
    	if (unlikely(!in_interrupt() && local_softirq_pending())) { // --- 3
    		/*
    		 * Run softirq if any pending. And do it in its own stack
    		 * as we may be calling this deep in a task call stack already.
    		 */
    		do_softirq();
    	}
    
    	preempt_count_dec(); // --- 4
    #ifdef CONFIG_TRACE_IRQFLAGS
    	local_irq_enable();
    #endif 
    	preempt_check_resched(); // --- 5
    }
    #endif
    1. in_hardirq 는 irq_enter 함수가 호출되는 시점부터 양수를 반환한다. local_bh_enable 함수가 호출된 상황에서 in_hardirq 함수가 양수를 반환하면, 이 상황은 어떤 상황일까? 아래와 같은 상황일 확률이 높다.

    local_irq_disable();
    ……
    local_bh_disable();
    ${CRITICAL_SECTION}
    local_bh_enable(); -> do_softirq -> __do_softirq -> local_irq_enable
    ……
    local_irq_enable();

    그런데, 이 코드는 조금 문제가 있다. local_bh_enable 함수가 호출되면, 순차적으로 do_softirq -> __do_softirq 함수가 호출된다. 문제는 __do_softirq 함수에서 local_irq_enable 함수가 호출된다는 것이다.이 코드를 작성한 개발자의 의도는 __do_softirq 함수안에서 hardirq 에 대한 안전을 보장받으려고 했는데, 그게 의미가 없어진 것이다. 그래서, 경고 문구를 출력하게 된다. 참고로, in_hardirq 함수가 0을 반환하면, hardirq 컨택스트가 아니라는 것을 의미한다.


    2. local_bh_disable 함수는 softirq count 에서 SOFTIRQ_DISABLE_OFFSET 을 softirq count 에 더한다. 그렇다면, local_bh_enable 에서는 그대로 SOFTIRQ_DISABLE_OFFSET 를 더해야 한다. 그런데, `SOFTIRQ_DISABLE_OFFSET - 1` 을 빼고 있다. 왜 그럴까? 아래와 같은 상황을 가정해보자.
    ....
    local_bh_disable(); -> 이 함수가 호출되는 순간부터 이후의 영역은 softirq context 가 된다. 즉, sleep 은 허용되지 않는다.
    ${CRITICAL_SECTION}
    local_bh_enable(); -> softirq context process context 를 원복한다.
    ....

    위에 크리티컬 섹션에 있는 공유 데이터는 process context 와 softirq context 에서 접근한다고 가정하자. 이 때, process context 에서 softirq context 가 접근하는 것을 막기 위해 local_bh_disable 함수를 호출한다. 이제 이 시점부터는 더 이상 process context 가 아닌, softirq context 가 된다. 그러므로, context switch 를 발생시키는 코드를 작성할 수 없다. 그런데, process context 에서 critical section 을 작업하는 도중에 인터럽트가 발생했다. 위에 작성된 코드의 동기화 기법으로는 hardirq 가 critical section 에 진입하는 것을 막을 수 없다. 결국 hardirq handler 가 실행되었고, hardirq handler 에서 softirq 를 요청했다. 그런데, hardirq 가 리턴되는 시점에 bottom-half 가 비활성화 되어있으므로 softirq 를 처리하지 못하고, __pending_softirq 만 체크하고 종료된다. 결국, hardirq 가 리턴되면 다시 critical section 을 진행하던 코드로 리턴되고 process context 는 local_bh_enable 함수가 호출되기 전까지 작업을 계속 수행한다. local_bh_enable 함수가 호출되면 pending softirq 가 처리된다.


    do_softirq 함수가 호출되기 위한 조건을 확인해보자. `!in_interrupt` 가 있다. 즉, softirq count == 0 이여야 do_softirq 를 호출할 수 있다. 그러므로, SOFTIRQ_DISABLE_OFFSET 를 빼야한다. 그런데, `if (unlikely(!in_interrupt() && local_softirq_pending()))` 에 진입했지만, do_softirq 함수가 호출되기전에 인터럽트가 발생해서 선점될 가능성이 있다.

    ....
    if (unlikely(!in_interrupt() && local_softirq_pending())) {
          <---------------- 인터럽트 발생.
          do_softirq
    }
    ....

    인터럽트는 발생할 수 있다. 그런데, 이 시점이 좋지 않다. 인터럽트도 활성화 상태이고 softirq 도 활성화 되어있고, 다른 프로세서에서 선점도 가능하다. softirq 를 비활성화해야 하는데, 그럴 수 가 없다. 왜냐면, 조건문에 들어오기 위해서는 softirq 는 활성화 되어있어야 한다. 그렇다면, 조건문안에서 다시 비활성화를 할까? 그렇기에는 do_softirq -> __do_softrq 에서 softirq 는 다시 비활성화를 한다. 그렇다면, 인터럽트를 비활성화해야 할까? bottom-half 설계 원칙은 인터럽트를 활성화하는 것이다. 방법은 preempt-disable count 로 증가시켜서 해당 프로세스가 선점되는 것을 막는것이다. 인터럽트가 리턴 시점에 이전 프로세스가 preempt-disable 되어있으면 해당 프로세스로 복귀하게 된다. 


    만약, pending softirq 를 처리하는 태스크가 선점될 경우 다음에 어떤 CPU 에게 할당될 지를 알 수가 없다. 왜 다른 CPU 에서 이전에 수행하던 일을 이어서하면 안될까? pending softirq 는 Per-CPU 데이터이기 때문이다. 즉, softirq 는 CPU 마다 독립적인 데이터이기 때문이다. 그렇기 때문에 preempt disable count 를 증가시켜야 한다. 그런데, 앞에서 분명히 local_bh_disable 함수가 호출되었을 것이다. 그렇면, softirq count >= SOFTIRQ_DISABLE_OFFSET 가 되있을 것이다. 여기서 `SOFTIRQ_DISABLE_OFFSET - 1` 는 0x0001_1111_1111 로 이걸 뺄 경우 아래의 softirq count 값은 아래의 MAX 와 MIN 사이에 존재하게 된다. 결국, preempt disable count 를 활성화시킨다.

    1. MAX : 0b1111 1110 0000 0000 − 0b0000 0001 1111 1111 = 0b1111 1100 0000 0001
    2. MIN : 0b0000 0010 0000 0000 - 0b0000 0001 1111 1111 = 0b0000 0000 0000 0001

    3. pending softirq 를 처리하는 코드 템플릿이다. 이건 마치 공식처럼 사용된다. 이 글 여러곳에서 해당 코드 템플릿을 확인할 수 있을 것이다.


    4. 왜 softirq count 에서 1을 빼는 것이 아니라, preempt_coun 전체에서 1을 빼는 것일까? 위에 `
    SOFTIRQ_DISABLE_OFFSET - 1` 를 계산할 때 사용했던 표를 다시 가져오자. 아래 2개의 최종 결과에서 1을 빼보자. 
    1. MAX : 0b1111 1110 0000 0000 − 0b0000 0001 1111 1111 = 0b1111 1100 0000 0001
    2. MIN : 0b0000 0010 0000 0000 - 0b0000 0001 1111 1111 = 0b0000 0000 0000 0001

    아래의 결과를 위에 최초 MAX와 MIN 에 대응해서 비교해보자.
    0b1111 1100 0000 0001 - 1 = 0b1111 1100 0000 0000 
    0b0000 0000 0000 0001 - 1 = 0b0000 0000 0000 0000

    정리하면 다음과 같다. 최종적으로 local_bh_disable 함수가 호출되기 전에 preempt_count 와 local_bh_enable 호출된 후에 preempt_count 가 동일한 것을 볼 수 있다.
    1. Before `local_bh_diable` : 0b1111 1100 0000 0000
    2. After `local_bh_disable` : 0b1111 1110 0000 0000
    3. After `__preemt_count_sub(SOFTIRQ_DISABLE_OFFSET - 1)` : 0b1111 1100 0000 0001
    4. After `preempt_count_dec` : 0b1111 1100 0000 0000 

    5. 현재 태스크의 preempt_count 가 0 이면, preemption 을 준비한다.

     

    " local_bh_disable / local_bh_enable 은 Local CPU 에만 적용되기 때문에, Local CPU 의 process context 에서 softriq 로 인해 critical section 이 더렵히지는 것을 막기위해 사용된다. 그렇다면, Local CPU 내에서 다수의 softirq context 가 발생하면 어떻게 될까? 리눅스 커널의 softirq 처리 메커니즘은 `batch + serialization` 을 기반으로 한다. 즉, softirq 를 처리해야 할 시점이 오면, 한 번에 여태까지 쌓여있던 pending softirq 들을 모두 처리한다. 그리고, 동일 CPU 내에서 softirq 처리 중 이라면, 다른 softirq 가 선점하지 못하도록 구현이 되어있다. 즉, 동일 CPU 내에서 softirq 들끼리의 race 는 신경쓸 필요가 없다.

     

    " 그런데, 여러 CPU 에서 동일한 softirq 가 실행될 수 도 있다. 이 때, softirq 들끼리 전역 변수가 공유된다면 local_bh_disable / local_bh_enable 로는 이 문제를 해결할 수 없다. 이런 경우는, spinlock 과 같은 전역 동기화 메커니즘이 사용될 수 밖에 없다(이러한 문제 때문에 tasklet 이 등장했다).

     

     

    - When to start a softirq ? 

    " softirq 가 즉각적으로 처리되기 위해서는 `__do_softirq` 함수가 호출되어야 한다. 그런데, 이 함수가 직접적으로 호출되는 경우는 리눅스 커널 v6.5 를 기준으로 없다고 무방하다. 대개, 각 아키텍처사에서 do_softirq_own_stack 함수를 구현해야 할 때, 내부적으로 __do_softirq 함수를 호출하는 것을 제외하고는, 외부에서 직접적으로 호출되는 경우는 득히 드물다. 즉, __do_softirq 함수는 간접적으로만 호출된다. 대표적으로 3가지 경우가 존재한다. 

    In practice, a softirq raised out of a hardware-interrupt handler will often be run immediately after that hardware handler finishes, but that is not necessarily the case. Softirqs can also be raised out of any (kernel) context, not just while responding to hardware interrupts; the RCU softirq, for example, is not tied to any hardware interrupt at all.

     - 참고 : https://lwn.net/Articles/925540/
    1. irq_exit : hardirq interrupt handler 가 끝나는 시점에 __do_softirq 함수를 호출한다.

    2. local_bh_enable, spin_unlock_bh : process context 에서 critical section 을 작업할 때, 간간히 bottom-half 를 막아야 하는 경우들이 있다. critical section 작업이 끝나면, __do_softirq 가 호출된다.

    3. ksoftirqd : 위에 2개 모두 함수 내부에서 직접적으로 __do_softirq 함수를 호출하기 때문에 latency 가 없다고 볼 수 있다. 그러나, ksoftirqd 는 즉각적으로 실행되지 않고 스케줄링이 되어야 __do_softirq 함수를 호출할 수 있다.

     

     

    - What is softirq stack? 

    " 기존에는 인터럽트가 발생할 경우, 해당 인터럽트 핸들러는 선점한 태스크의 스택을 사용했다. 그런데, 이러한 방식은 스택 오버 플로우 및 보안에 대한 심각한 문제를 야기할 수 있다. 그래서, 기술에 발전함에 따라 인터럽트 발생 시, 인터럽트 핸들러에게 하드웨어적으로 별도의 스택을 제공할 수 있는 기술이 만들어졌다. 리눅스 커널은 v2.6.32 부터 Per-CPU stack 을 사용해서 hardirq 와 softirq 에게 별도의 스택을 제공해주고 있다. 즉, 각 CPU 마다 별도의 hardirq stack 과 softirq stack 을 제공한다는 뜻이다. 

     

    " softirq 는 특별한 환경이 아니라면, harirq 에 의해서만 선점되어진다. 선점당한 softirq context 에 대한 정보가 현재 hardirq stack 에 저장되고, 이 시점에 SP 의 위치가 softirq stack 에서 hardirq stack 으로 변경된다. 그리고, CPU 는 hardirq handler 를 실행하게 된다. 그런데, 몇몇 아키텍처에서는 hardirq -> softirq 로 복귀할 때, hardirq stack 을 softirq 가 쓸 수 있도록 하는 기술들이 제안되었다. 이 내용은 아키텍처마다 다르고, 하드웨어 디펜던시가 강하기 때문에 자세한 설명은 생략하도록 한다(해당 내용에 대한 부분은 `CONFIG_HAVE_IRQ_EXIT_ON_IRQ_STACK` 컨피그를 참고하자).

     

     

     

    - What is ksoftirqd ?

    " ksoftirqd 를 알기 위해서는 이 스레드가 왜 생성되었는지를 알아봐야 한다. 아래 밑줄 친 내용을 토대로 알 수 있는 것은 ksoftirqd 는 softirq 에서 처리할 일이 많은 경우 시스템 퍼포먼스에 영향을 주기 때문에, 대신해서 일을 처리해 줄 용도로 ksoftirqd 를 만들었다.

    Readers who have looked at the process mix on their systems may be wondering where the ksoftirqd processes fit into the picture. These processes exist to offload softirq processing when the load gets too heavy.

    - 참고 : https://lwn.net/Articles/520076/

    The function that normally processes software interrupts (__do_softirq()) will pass over all of the raised softirqs and process them. Once that is done, it checks if more softirqs are pending; should that be the case, it will go back to the beginning — but only for a maximum of ten times. If that count is exceeded, the kernel stops processing softirqs and, instead, wakes the per-CPU ksoftirqd kernel thread to continue the work.

    - 참고 : https://lwn.net/Articles/925540/

     

     

    1. When to trigger ksoftirqd ? 

    " 그렇다면, ksoftirqd 는 언제 트리거될까? 이 말은 다른 표현으로 하면 `softirq 가 부하가 많은 지점이 어디일까?` 이다. softirq 를 처리하는 함수인 `__do_softirq` 내부를 살펴봐야한다. 뒤에 `__do_softirq` 를 분석하는 곳에서 알아보도록 한다.

     

     

    2. Anything else ?" 그렇다면, ksofirqd 는 처리해야 할 softirq 가 많을때 만 실행될까? 최초의 목적은 그랬지만, 지금은 그렇지 않다. 대표적으로 raise_softirq 함수를 호출할 때 도 ksoftirqd 를 사용해서 softirq 를 처리 하려고 한다.

     

    " ksoftirqd 는 결국 커널 스레드이기 때문에 스케줄러에게 선택되어야 실행될 수 있다. 그리고, 스레드이기 때문에 process context 에서 동작하게 된다. 그런데, ksoftriqd 는 softirq 를 처리하기 때문에, softirq context 임을 보장받아야 한다. 어떻게 해야할까? 가장 강력한 방법은 local_irq_disable / local_irq_enable 을 통해 hardirq / softirq 모두를 막는 것이다. 그러나, 이러한 coarse-grained 방식은 퍼포먼스에 좋지 못하다. 그리고, ksoftirqd 에서 인터럽트는 막는 것은 bottom-half 의 설계 원칙에 위배된다. 그래서, ksoftirqd 는 wakeup 초기에만 인터럽트를 비활성화하고, local_bh_disable 함수가 호출된 후에는 인터럽트를 다시 활성화한다.

     

     

    3. Note

    " 만약에 ksoftirqd 가 CPU[0] 에서 이미 생성되어 있다면, 커널은 CPU[0] 에서 softirq 처리할 때, 직접적으로 __do_softrq 를 호출하지 않는다. 대신에, 모든 softirq 에 대한 처리를 ksoftirqd 에게 위임한다. 아래 lwn 참조글에서는 `running` 이라고 되어있지만, v6.5 기준으로 `invoke_softirq()` 함수를 분석해보면 ksoftirqd 가 실행 중일 때 가 아닌, 생성만 되어 있다면 모든 softriq 에 대한 처리를 ksoftirqd 에게 위임한다.

    Whenever the ksoftirqd thread is running on a given CPU, the kernel will not even try to process software interrupts there; it will just leave them for the thread to handle.

    - 참고 : https://lwn.net/Articles/925540/

     

     

    4. ksoftirqd context

    " ksoftirqd 의 핸들러는 run_ksoftirqd 함수다.

    #ifdef CONFIG_PREEMPT_RT
    ....
    #else
    static inline void ksoftirqd_run_begin(void)
    {
    	local_irq_disable();
    }
    
    static inline void ksoftirqd_run_end(void)
    {
    	local_irq_enable();
    }
    #endif
    // kernel/softirq.c - v6.5
    static void run_ksoftirqd(unsigned int cpu)
    {
    	ksoftirqd_run_begin(); // --- 1
    	if (local_softirq_pending()) { // --- 1
    		/*
    		 * We can safely run softirq on inline stack, as we are not deep
    		 * in the task stack here.
    		 */
    		__do_softirq(); // --- 2
    		ksoftirqd_run_end(); 
    		cond_resched(); // --- 3
    		return;
    	}
    	ksoftirqd_run_end();
    }
    1. 여기서 인터럽트를 비활성화 하는 이유는 __do_softirq 에서 softirqd_handle_begin 함수가 호출되기 까지 버티기 위해서다. softirqd_handle_begin 함수가 호출되면 softirq 가 disabled 되기 때문에, 이 때 까지만 버티는 것이다. 만약, 이 사이에 인터럽트를 비활성화하지 않으면 ksoftirqd 가 인터럽트에 선점될 수 도 있다. 이 시점에서는 선점당한 ksoftirqd 가 softirq 를 disable 하지 못해서 인터럽트가 리턴하는 시점에 pending softirq 를 확인하고 softirq 처리를 요청할 수 가 있다. 이걸 막기 위해 인터럽트를 disable 하는 것이다. 그런데, local_irq_disable 는 이 상황에 사용하기에 너무 강력한 lock 이 아닐까? 좀 더 find-grained 한 방식을 사용할 수 는 없었을까? 이게 최선인 듯 싶다. 이전 wakeup_softirqd 함수를 보면 아래와 같다.

    void wakeup_softirqd(void)
     
    {
        /* Interrupts are disabled: no need to stop preemption */
        struct task_struct *tsk = __get_cpu_var(ksoftirqd);
     
        if (tsk && tsk->state != TASK_RUNNING)
        wake_up_process(tsk);
    }
    이전에는 ksoftirqd 가 현재 실행중이면 wake_up_process 함수를 호출하지 않았지만, 현재는 저 조건문(`tsk->state!=TASK_RUNNING`이 사라졌다. 만약, 이게 있었다면 `preempt-disable` 로도 괜찮지 않았을까 싶은데, 이제는 반드시 인터럽트 비활성화가 필요해 보인다.

    그런데, 왜 run_ksoftirqd 함수에서는 `in_interrupt` 함수를 검사하지 않을까? 2가지 중에 하나가 아닐까 싶다.
    1. ksoftirqd 가 스케줄링되면,  __do_softirq 를 무조건 실행하기 위해서다. 
    2. run_ksoftirqd 함수가 호출되는 시점에 반드시 `!in_interrupt` 가 보장되기 때문이다.
    예전 run_ksoftirqd 함수는 __do_softirq 가 아닌 do_softirq 를 호출해서 __do_softirq 를 호출하는 식으로 작성되어 있었다[참고1](__do_softirq 대신 do_softirq 함수로 작성한 건 스택 문제 때문이었다). do_softirq 함수안에 in_interrupt 검사하는 코드가 존재했었는데... 확인이 더 필요하다.

    2. do_softirq 가 아닌 __do_softirq 함수를 호출하는 이유는 ksoftirqd 에게 할당된 스택으로 충분히 커버가 가능하기 때문이다[참고1] 

    3. cond_resched 매크로 함수는 `conditionally re-schedule` 의 약자로 조건부 스케줄링 이라고도 한다. 즉, 특정 조건이 맞을 때, 현재 태스크를 선점해서 새로운 태스크를 스케줄링 하는 것이다. 그 조건은 현재 태스크 플래그에 TIF_NEED_RESCHED 플래그가 SET 되어있으면 된다. 결국, need_resched 함수를 호출했을 때, true 이면 스케줄링 하겠다는 것이다. 당연한 얘기지만, 스케줄링 함수이기 때문에 interrupt context 에서는 사용하면 안된다.

     

     

    - How to handle softirq

    " softirq 는 언제 처리될까? 일반적으로는 하드웨어 인터럽트가 종료되는 시점에 pending softirq 를 확인 후 처리하는 것이 가장 적합하다. 실제 커널 소스에서 확인해보면, hardirq(`handle_arch_irq`) 처리가 끝나는 시점에 irq_exit 함수가 실행되면서 softirq 가 스케줄링된다. irq_exit 함수가 호출되기전에 실행되는 generic_handle_arch_irq, handle_arch_irq 함수들은 모두 hardirq 를 처리하는 핸들러를 의미한다. 이 함수뿐만이 아니다. 리눅스 커널에서 irq_exit 전에 호출되는 모든 인터럽트 핸들러는 hardirq 인터럽트 핸들러라고 생각하면 된다. 

    // kernel/irq/handle.c - v6.5
    #ifdef CONFIG_GENERIC_IRQ_MULTI_HANDLER
    ....
    
    /**
     * generic_handle_arch_irq - root irq handler for architectures which do no
     *                           entry accounting themselves
     * @regs:	Register file coming from the low-level handling code
     */
    asmlinkage void noinstr generic_handle_arch_irq(struct pt_regs *regs)
    {
    	struct pt_regs *old_regs;
    
    	irq_enter();
    	old_regs = set_irq_regs(regs);
    	handle_arch_irq(regs);
    	set_irq_regs(old_regs);
    	irq_exit();
    }
    #endif

     

     

    "  참고로, softirq 를 `트리거` 하는 것과 `처리` 하는 것은 전혀 다른 내용이다.

     

      trigger handle
    softirq softirq 요청하는 것을 의미한다. 즉, raise_softirq 계열 함수를 호출하는 것을 의미한다(Per-CPU irq_cpustat.__softirq_pending 필드에 softirq 를 저장한다). pending softirq 를 처리하는 것을 의미한다. 즉, __do_softirq 함수가 호출하는 것을 의미한다(Per-CPU irq_cpustat.__softirq_pending 필드의 내용을 읽어서 처리한다).

     

     

    " `irq_exit` 함수가 내부적으로 `__irq_exit_rcu` 함수를 호출하는데, 실제 이 함수안에서 softirq 처리 루틴이 실행된다.

    // kernel/softirq.c - v6.5
    /**
     * irq_exit - Exit an interrupt context, update RCU and lockdep
     *
     * Also processes softirqs if needed and possible.
     */
    void irq_exit(void)
    {
    	__irq_exit_rcu();
    	ct_irq_exit();
    	 /* must be last! */
    	lockdep_hardirq_exit();
    }
    // kernel/softirq.c - v6.5
    static inline void __irq_exit_rcu(void)
    {
    #ifndef __ARCH_IRQ_EXIT_IRQS_DISABLED
    	local_irq_disable(); // --- 1
    #else
    	lockdep_assert_irqs_disabled();
    #endif
    	account_hardirq_exit(current);
    	preempt_count_sub(HARDIRQ_OFFSET); // --- 2
    	if (!in_interrupt() && local_softirq_pending()) // --- 3
    		invoke_softirq();
    
    	tick_irq_exit();
    }
    1. arm64 같은 경우 v6.5 를 기준으로 `#define __ARCH_IRQ_EXIT_IRQS_DISABLED 1` 로 정의되어 있다. 그렇기 때문에, local_irq_disable 함수를 호출하지 않는다. 그러나, intel 같은 경우는 __ARCH_IRQ_EXIT_IRQS_DISABLED 매크로를 정의하고 있지 않기 때문에 irq_exit 함수가 종료된 이후에도 계속 로컬 인터럽트가 비할성화된 채로 남아있게 된다. 그런데, softirq 는 일반적으로 ksoftirqd 가 wakeup 해야 처리하게 될 텐데 그 사이에 interrupt 를 비활성화해도 되는지가 의문이다. ksoftirqd 가 타이머 인터럽트에 의해서 스케줄링 될 텐데, 인터럽트를 비활성화하면 어떻게 ksoftirqd 가 wakeup 하지? 아래 내용은 

    irq: Don't re-enable interrupts at the end of irq_exit
    Commit 74eed01
    "irq: Ensure irq_exit() code runs with interrupts disabled" restore interrupts flags in the end of irq_exit() for archs that don't define __ARCH_IRQ_EXIT_IRQS_DISABLED.
    However always returning from irq_exit() with interrupts disabled should not be a problem for these archs. Prior to
    this commit this was already happening anytime we processed pending softirqs anyway.

    - 참고 : https://github.com/torvalds/linux/commit/4cd5d1115c2f752ca94a0eb461b36d88fb37ed1e

    2. irq_exit 함수가 호출되는 시점은 hardirq 가 끝났음을 의미하고, 동시에 pending softirq 를 처리함을 의미한다. preempt_count 에서 HARDIRQ_OFFSET 을 뺀다는 것은 hardirq 가 끝났음을 의미한다.


    3. 현재 실행 컨택스트가 인터럽트 컨택스트라는 것은 이미 다른 인터럽트(NMI, hardirq, softirq) 가 처리되고 있다는 뜻이다. 리눅스 커널에서는 실제 하드웨어 인터럽트 nesting 을 금지하고 있지만, 인터럽트 핸들러 nesting 은 될 수 있다. 그러나, softirq 는 hardirq 와 같이 긴급한 인터럽트를 처리하는 소프트웨어 메커니즘이기 때문에, 중간에 다른 softirq 에 선점당하면 안된다. 그러므로, 이미 실행 중인 인터럽트(NMI, hardirq, softirq)가 있다면 방해하지 않고 그냥 종료한다. 만약, 인터럽트 컨택스트가 아니라면, 현재 pending softirq 가 있는지 확인한다. 존재할 경우, 해당 softirq 를 처리하기 위해 invoke_softirq 함수를 실행한다.

     

     

    " invoke_softirq 함수는 몇 가지 조건을 판단해서 즉각적으로 pending softirq 를 처리할지 나중에 처리할 지를 결정한다. 

    // kernel/softirq.c - v6.5
    #ifdef CONFIG_PREEMPT_RT
    static inline void invoke_softirq(void)
    {
    	if (should_wake_ksoftirqd())
    		wakeup_softirqd();
    }
    #else
    static inline void invoke_softirq(void)
    {
    	if (!force_irqthreads() || !__this_cpu_read(ksoftirqd)) { // --- 1
    #ifdef CONFIG_HAVE_IRQ_EXIT_ON_IRQ_STACK // --- 2
    		/*
    		 * We can safely execute softirq on the current stack if
    		 * it is the irq stack, because it should be near empty
    		 * at this stage.
    		 */
    		__do_softirq();
    #else
    		/*
    		 * Otherwise, irq_exit() is called on the task stack that can
    		 * be potentially deep already. So call softirq in its own stack
    		 * to prevent from any overrun.
    		 */
    		do_softirq_own_stack();
    #endif
    	} else {
    		wakeup_softirqd();
    	}
    }
    #endif
    1. `!__this_cpu_read(ksoftirqd)` 는 ksoftirqd 가 생성되었는지를 판단한다. 만약, 초기 부팅시점에 아직 ksoftirqd 가 생성되지 않았는데 wakeup_softirq 함수를 호출하면 이슈가 발생할 수 있기 때문이다.

    If there is heavy softirq activity, the softirq system will attempt to awaken ksoftirqd and will stop the traditional back-of-interrupt softirq processing. This is all well and good, but only if the ksoftirqd kthreads already exist, which is not the case during early boot, in which case the system hangs.

    - 참고 : https://github.com/torvalds/linux/commit/1c0c4bc1ceb580851b2d76fdef9712b3bdae134b

    참고로, non-booting time 이거나 !RT-kernel 에서 `if (!force_irqthreads() || !__this_cpu_read(ksoftirqd))` 조건문이 참이될 경우는 극히 드물것으로 보인다. 즉, 직접적으로 __do_softrq 를 호출하기 보다는 거의 대다수가 wakeup_softirq 함수를 호출할 것으로 판단된다.

    2. 아키텍처마다 디테일한 내용은 다르겠지만, 인터럽트가 발생하면 별도의 스택을 영역을 할당해서 거기서 인터럽트 핸들러를 실행한다. 이렇게 하는 이유는 인터럽트 핸들러가 이전에 실행중이던 스레드와 스택 영역을 공유하기 때문이다. `스택 스위칭` 이라는 용어가 생기기 전에는 인터럽트가 발생하면 이전 프로세스가 사용하던 스택 영역을 그대로 사용했다. 그런데, 인터럽트 핸들러가 이 스택 영역을 초과해서 사용할 경우 말 그대로 `스택 오버 플로우` 발생하게 된다. 그래서, 많은 아키텍처에서 인터럽트가 발생할 경우 별도의 스택 영역을 할당하는 `스택 스위칭` 이라는 개념을 만들었다. 인터럽트 핸들러에게 할당된 스택은 인터럽트 핸들러가 반환되는 시점에(x86 같은 경우 `iret` 명령어를 실행) 해제된다. 그런데, CONFIG_HAVE_IRQ_EXIT_ON_IRQ_STACK 컨피그는 hardirq 에서 사용하던 스택을 softirq 도 사용하겠다는 뜻이다.

    Architecture doesn't only execute the irq handler on the irq stack but also irq_exit(). This way we can process softirqs on this irq stack instead of switching to a new one when we call __do_softirq() in the end of an hardirq. This spares a stack switch and improves cache usage on softirq processing.

    - 참고 : https://cateee.net/lkddb/web-lkddb/HAVE_IRQ_EXIT_ON_IRQ_STACK.html

    __do_softirq 함수가 호출되면 hardirq 가 사용했던 스택을 재사용 하게 된다. 그런데, 이 시점에서는 hardirq 가 이미 끝났기 때문에 해당 스택은 깔끔할 것이라고 판단할 수 있다. do_softirq_own_stack 함수가 호출되면 hardirq 가 사용했던 스택을 재사용하지 못한다. 그래서, 이전 인터럽트에 선점당했던 스레드의 커널 스택영역을 사용하게 된다. 그러나, 이렇게 되면 당연히 문제가 발생한다. 그래서, 아키텍처마다 별도의 softirq 스택을 제공해야 한다. 그래서, do_softirq_own_stack 함수는 아키텍처마다 다르게 구현되어있다.

    arm64 에서는 CONFIG_HAVE_IRQ_EXIT_ON_IRQ_STACK 는 설정되어 있지 않다. 즉, hardirq 에서 사용하던 스택을 공유하지 않고, __do_softirq 는 별도의 스택을 사용하게 된다(CONFIG_HAVE_SOFTIRQ_ON_OWN_STACK).

     

     

    " 대망의 pending softirq 를 처리하는 핵심 함수인 `__do_softirq` 다. 

    // kernel/softirq.c - v6.5
    /*
     * We restart softirq processing for at most MAX_SOFTIRQ_RESTART times,
     * but break the loop if need_resched() is set or after 2 ms.
     * The MAX_SOFTIRQ_TIME provides a nice upper bound in most cases, but in
     * certain cases, such as stop_machine(), jiffies may cease to
     * increment and so we need the MAX_SOFTIRQ_RESTART limit as
     * well to make sure we eventually return from this method.
     *
     * These limits have been established via experimentation.
     * The two things to balance is latency against fairness -
     * we want to handle softirqs as soon as possible, but they
     * should not be able to lock up the box.
     */
    #define MAX_SOFTIRQ_TIME  msecs_to_jiffies(2)
    #define MAX_SOFTIRQ_RESTART 10
    ....
    
    asmlinkage __visible void __softirq_entry __do_softirq(void)
    {
    	unsigned long end = jiffies + MAX_SOFTIRQ_TIME;
    	unsigned long old_flags = current->flags;
    	int max_restart = MAX_SOFTIRQ_RESTART;
    	struct softirq_action *h;
    	bool in_hardirq;
    	__u32 pending;
    	int softirq_bit;
    
    	/*
    	 * Mask out PF_MEMALLOC as the current task context is borrowed for the
    	 * softirq. A softirq handled, such as network RX, might set PF_MEMALLOC
    	 * again if the socket is related to swapping.
    	 */
    	current->flags &= ~PF_MEMALLOC;
    
    	pending = local_softirq_pending(); // --- 2
    
    	softirq_handle_begin(); // --- 1
    	in_hardirq = lockdep_softirq_start(); // --- 1
    	account_softirq_enter(current);
    
    restart:
    	/* Reset the pending bitmask before enabling irqs */
    	set_softirq_pending(0); // --- 2
    
    	local_irq_enable(); // --- 2
    
    	h = softirq_vec; // --- 3
    
    	while ((softirq_bit = ffs(pending))) { // --- 3
    		unsigned int vec_nr;
    		int prev_count;
    
    		h += softirq_bit - 1; // --- 3
    
    		vec_nr = h - softirq_vec; // --- 3
    		prev_count = preempt_count();
    
    		kstat_incr_softirqs_this_cpu(vec_nr);
    
    		trace_softirq_entry(vec_nr);
    		h->action(h); // --- 4
    		trace_softirq_exit(vec_nr);
    		if (unlikely(prev_count != preempt_count())) {
    			pr_err("huh, entered softirq %u %s %p with preempt_count %08x, exited with %08x?\n",
    			       vec_nr, softirq_to_name[vec_nr], h->action,
    			       prev_count, preempt_count());
    			preempt_count_set(prev_count);
    		}
    		h++; // --- 3
    		pending >>= softirq_bit; // --- 3
    	}
    
    	if (!IS_ENABLED(CONFIG_PREEMPT_RT) &&
    	    __this_cpu_read(ksoftirqd) == current)
    		rcu_softirq_qs();
    
    	local_irq_disable(); // --- 2
    
    	pending = local_softirq_pending(); 
    	if (pending) {
    		if (time_before(jiffies, end) && !need_resched() && // --- 5
    		    --max_restart)
    			goto restart;
    
    		wakeup_softirqd();
    	}
    
    	account_softirq_exit(current);
    	lockdep_softirq_end(in_hardirq); // --- 1
    	softirq_handle_end(); // --- 1
    	current_restore_flags(old_flags, PF_MEMALLOC);
    }
    // kernel/softirq.c - v6.5
    #ifdef CONFIG_PREEMPT_RT
    ....
    #else
    static inline void softirq_handle_begin(void)
    {
    	__local_bh_disable_ip(_RET_IP_, SOFTIRQ_OFFSET);
    }
    
    static inline void softirq_handle_end(void)
    {
    	__local_bh_enable(SOFTIRQ_OFFSET);
    	WARN_ON_ONCE(in_interrupt());
    }
    #endif
    1. ksoftirqd 가 실행되면, run_ksoftirqd 함수가 호출된다. 이 때, 인터럽트가 비활성화된다. 그런데, 모든 bottom-half 기법들은 인터럽트가 활성화된 상태로 처리되어야 한다. 인터럽트가 활성화되면 다른 softirq context 에서 선점을 해올 수 있다. 그러므로, softirq 를 비활성화하기 위해 softirq_handle_begin 함수를 호출한다.

    2. 먼저 현재까지 요청된 pending softirq 들을 가져온다. 그리고, `set_softirq_pending(0)` 을 통해서 이 이후에 softirq 는 다음에 처리하도록 pending 한다. 이렇게 하는 이유는 뒤에서 인터럽트를 다시 활성화(local_irq_enable)하면 softirq 를 요청하는 인터럽트들이 발생하면서 __softirq_pending 변수가 다시 더럽혀진다. __do_softirq 함수는 무한 루프를 돌면서 현재까지 발생한 pending softirq 를 처리하기 때문에 softirq 가 계속 발생하면 처리해야 할 작업들이 계속 많아지기 때문에 퍼포먼스에 영향을 끼칠 수 있다. 그렇기 때문에, cut-line 을 정할 필요가 있다.

    __do_softirq 에서 local_irq_enable 함수를 호출하는 이유가 뭘까? __do_softirq 함수가 호출되기 전에 인터럽트가 이미 비활성화된 경우들이 존재하기 때문이다. run_ksoftirqd 함수가 대표적인 예시다. deferred work 들은 일반적으로 인터럽트가 활성화된 채로 동작을 하게된다. __do_softirq 함수도 마찬가지다. 어차피 하드웨어 인터럽트에 선점 당하더라도, 앞에서 softirq_handle_begin 함수를 통해서 softirq context 를 만들어놨기 때문에 다른 softirq 들에게는 선점되지 않는다. local_irq_enable 이후부터 다시 __pending_sofritq 가 쌓이기 시작한다.

    루프가 끝난 뒤, local_irq_disable 함수를 호출하는데, 이건 왜 호출할까? 이전에 쌓여있던 softirq 를 처리하는 동안 하드웨어 인터럽트를 활성화하면서 새로운 softirq 들이 쌓여있을 것이다. 이걸 다시 처리하기 위해서 하드웨어 인터럽트를 비활성화하는 것이다. 만약 여기서 비활성화 하지 않으면 __pending_softirq 에 계속 쌓이게 되서 경계를 알 수 가 없게된다. 그런데, __do_softirq 함수가 종료할 때 까지 비활성화 상태를 유지한다. 언제 다시 활성화할까? run_ksoftirqd 함수가 리턴되는 시점에 다시 활성화된다.

    local_irq_restore 가 아닌, local_irq_enable 인 이유가 뭘까? bottom-half 는 인터럽트를 `무조건` 활성화시켜야 하기 때문이다.

    3. ffs 는 `find first bit set` 의 약자로 LSB 부터 탐색하면서 제일 먼저 SET 된 비트를 반환한다. 당연히, 몇 번째 비트인지를 반환한다(/include/asm-generic/bitops/ffs.h). `h += softirq_bit - 1` 에서 -1 을 하는 이유는 ffs 가 비트를 반환할 때, 0번째 비트를 1로 반환하기 때문이다. 31번째 비트는 32를 반환한다. 즉, `softirq_bit - 1` 는 제일 우선순위가 높은 softirq number 를 반환한다. 그런데, `h` 의 값은 어떻게 계산될까? 아래를 보면 알겠지만, h 는 결국 softirq vector 를 나타낸다.

    1. Before (softirq_bit = ffs(pending))  : h == softirq_vec
    2. After `h += softirq_bit - 1` : (h + softirq_bit - 1) == (softirq_vec + softirq_bit - 1) --->  softirq_vec[softirq_bit - 1]
    3. vec_nr = h - softirq_vec ---> (softirq_vec[softirq_bit - 1] - softirq_vec) ---> ((softirq_vec + softirq_bit - 1) - softirq_vec) ---> softirq_bit - 1

    4. softirq 의 action handler 를 실행한다.

    5. 왜 다시 pending softirq 를 체크할까? 하드웨어 인터럽트를 다시 활성화했기 때문에, 이전 softirq 를 처리하는 사이에도 수 많은 softirq 가 쌓여있을 수 있다. 이럴 경우, 다시 softirq 를 처리하도록 한다. 그런데, 조건이 있다. 3가지 조건이 있는데 이 모두가 충족되어야 re-process 를 시작한다.

    1. softirq 를 처리하는 절차는 2ms 를 넘지 않아야 한다. 만약, 2ms 를 넘지 않았다면 softirq 를 다시 처리 할 수 있는 조건은 된 셈이다. 그런데, 왜 2ms 일까? 이 값은 경험적인 수치다. 이러한 `경험적인 방식`을 `heuristic mechanism` 이라고 하는데 커널 커뮤니티에서 가장 논란이 많은 부분이기도 하다. 그런데, 처리해야 할 softirq 가 아직 남아있는데, 2ms 이상 소요됬다면 어떻게 해야할까? 이 때는 ksoftirqd 에게 나머지 일을 위임하게 된다. 

    2. softirq 를 처리하고 있는 프로세스에 `TIF_NEED_RESCHED` 플래그가 SET 되어있는지를 검사한다. 이 플래그가 SET 되어있을 경우, 해당 프로세스보다 우선순위가 더 높은 프로세스가 실행을 기다리고 있다는 뜻이다. 즉, 이 경우도 softirq 에 대한 처리를 ksoftirqd 에게 위임하게 된다.

    3. softirq 를 처리하는데 2ms 를 넘지 않았고, 우선 순위가 높은 프로세스가 대기중이 아니라면, 10번 카운트한다. 즉, (1), (2) 조건이 모두 충족되면 10번 루프를 다시 돌아서 softirq 를 처리한다는 뜻이다. 당연히 이것 또한 `heuristic mechanism` 이다.

     

Designed by Tistory.