ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [리눅스 커널] Interrupt - high level reference
    Linux/kernel 2023. 8. 3. 02:27

    글의 참고

    - http://books.gigatux.nl/mirror/kerneldevelopment/0672327201/ch06lev1sec7.html

    - http://www.makelinux.net/ldd3/chp-10-sect-3.shtml

    - https://elixir.bootlin.com/linux/v4.10.5/source/kernel/irq/manage.c#L98

    - https://www.kernel.org/doc/html/v4.16/core-api/genericirq.html


    글의 전제

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

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


    글의 내용

    - Overview

    " low-level IRQ handler 는 주로 칩 제조사에서 만든다. 그러나, high-level IRQ handler 는 제품 제조사에서 많이 사용하는데, 핸들러의 리턴값과 기본적인 인터럽트 API 를 정확히 숙지하고 있어야, 오동작에 대한 대처를 빠르게 할 수 있다. 이 글에서는 앞에 언급된 2가지 내요에 대해 설명한다.

     

     

    - High-level Interrupt handler return value 

    " 인터럽트 핸들러의 원형은 다음과 같다.

    typedef irqreturn_t (*irq_handler_t)(int, void *);

     

    " 아주 심플한 원형이다. 여기서 리턴값인 `irqreturn_t`를 자세히 보자. typedef로 `enum irqreturn`을 irqreturn_t 으로 이름을 바꿔서 사용했다. 여기서 저 리턴값들에 대해 자세히 알아야 나중에 인터럽트 핸들러를 제대로 만들 수 있다. 

     

    /**
     * enum irqreturn
     * @IRQ_NONE		interrupt was not from this device or was not handled
     * @IRQ_HANDLED		interrupt was handled by this device
     * @IRQ_WAKE_THREAD	handler requests to wake the handler thread
     */
    enum irqreturn {
    	IRQ_NONE		= (0 << 0),
    	IRQ_HANDLED		= (1 << 0),
    	IRQ_WAKE_THREAD		= (1 << 1),
    };
    
    typedef enum irqreturn irqreturn_t;

     

    " 인터럽트 핸들러의 반환값은 `인터럽트를 공유하고 있냐`와 `후반부 처리를 할 것이냐`로 나눌 수 있다.

    - IRQ_NONE " 이 반환값은 해당 인터럽트가 등록될 때, IRQF_SHARED로 등록되어 있을때만 의미가 있다. 이 값을 반환할 경우, 해당 디바이스에서 발생한 인터럽트가 아니므로 여기서 처리하지 않겠다는 뜻임. 예를 들어 a-device.c와 b-device.c  파일이 있을 때, 2개의 파일(디바이스) 모두가 GPIO04을 인터럽트 라인으로 사용해야 한다고 치자. 그렇면 2개의 파일안에는 인터럽트 핸들러에 GPIO04에 대한 인터럽트 번호를 할당받아서 인터럽트 핸들러에 등록하는 코드가 있을 것이다. 이때 핸들러를 등록할 때, 반드시 인터럽트 플래그에 IRQF_SHARED를 작성해야만 인터럽트 라인을 공유할 수 있다. IRQF_SHARED가 설정된 인터럽트 핸들러들은 인터럽트 발생 시 커널에 등록된 순서대로 호출이 되는데, 만약 첫 번째로 등록된 핸들러에서 IRQF_NONE을 반환한다는 것은 자기가 처리할 인터럽트가 아니라는 뜻이다. 즉, 자기가 처리해야 할 인터럽트가 아니기 때문에 다음 인터럽트 핸들러에게 기회가 넘어간다. 예를 들어, a-device.c의 인터럽트 핸들러에서 IRQF_NONE을 반환할 경우,  b-device.c로 인터럽트 처리가 넘어간다. 만약 IRQF_HANDLED를 반환하면 하면 자기가 발생시킨 인터럽트가 맞기 때문에 다음 인터럽트 핸들러에게는 기회가 사라진다.

    - IRQ_HANDLE " 발생한 인터럽트가 자기 때문에 발생한 인터럽트가 맞다는 의미로 반환하는 값이다. 그러면서 동시에 해당 인터럽트를 잘 처리했다는 뜻으로 반환값이기도 하다. 그리고 threaded IRQ가 등록된 hard-IRQ에서 이 값을 반환하면 threaded-IRQ handler를 깨우지 않는다.

    - IRQ_WAKE_THREAD " 이 반환값은 인터럽트 핸들러 등록 시 Threaded IRQ Handler가 등록되어 있을 때만 의미가 있다. 다른 상황에서는 의미가 전혀없다. Top-half(hard-IRQ)에서 처리해야 할 일이 많을 경우, bottom-half로 일을 넘겨야 하는데 이때 top-half에서 IRQF_WAKE_THREAD를 리턴하면 된다. Top-half에서 이 값을 리턴한다는 것은 bottom-half(threaded irq handler)로 work를 위임하겠다는 뜻이다.

     

     

     

    - High-level interrupt control API 

    - in_interrupt

    " `in_interrupt` 함수가 호출되는 지점에서 만약 `0`을 반환하면, 해당 소스 코드가 실행되는 곳은 `프로세스 컨택스트`를 의미한다. 만약, `0`이 아닌 값을 반환하면, `인터럽트 컨택스트`를 의미한다. 이 함수는 `hard interrupt context`와 `bottom half hanlder` 내에서 호출될 경우, `0`이 아닌 값을 반환한다. 즉, 모든 인터럽트 컨택스트를 감지한다.

     

    - in_irq

    " `in_irq`함수는 `hard interrupt context` 만 감지한다. 즉, `0`이 아닌 값을 반환하면, 현재 컨택스트는 `hardware interrupt context`임을 나타낸다. `bottom half handler`에서 이 함수를 호출하면, `0`을 반환한다. 

     

    - irqs_disabled

    " 현재 프로세서의 인터럽트가 비활성화 상태인지를 반환한다. 만약, `0`을 반환하면 비활성화 상태를 의미한다. 

     

    - local_irq_disable

    " `local`은 현재 프로세서를 의미한다. 즉, 현재 프로세서에 모든 인터럽트를 비활성화한다. `x86` 기준으로 이 명령어는 단순하게 `cli` 어셈블리 명령어로 구현되어 있다.

     

    - local_irq_enable

    " `local`은 현재 프로세서를 의미한다. 즉, 현재 프로세서에 모든 인터럽트를 활성화한다.  `x86` 기준으로 이 명령어는 단순하게 `sti` 어셈블리 명령어로 구현되어 있다(arm64에서 `local_irq_enable -> raw_local_irq_enable(arch_local_irq_enable)` 순으로 호출된다)

     

    - `local_irq_disable` vs `local_irq_save`
    "둘은 기능적으로는 동일한 의미를 갖는다. 즉, 인터럽트르 비활성화한다. 그런데, 차이가 있다면, `local_irq_save` 함수는 현재 인터럽트 상태를 저장해서 `local_irq_restore` 함수가 호출될 때, 이전 인터럽트 상태를 복구시킨다는 것이다. 이 짓을 왜할까?

    " 예를 들어, 현재 인터럽트 상태가 이미 비활성화라고 치자. 여기서 인터럽트가 이미 비활성화인데도, `local_irq_disable` 함수를 호출해서, 또 비활성 시켰다고치자. 개발자는 여기서 착각에 빠지게 된다. `이 위치에서는 인터럽트가 반드시 활성화일테니, local_irq_disable() 함수를 호출해서 비활성화를 시켜야 해!` 라고 말이다. 그리고, critical section 이 끝나는 지점에서 `local_irq_enable` 함수를 호출한다.

    " 이제 인터럽트가 활성화됬다. 인터럽트도 잘 처리되었고 모든게 완벽하다. 그렇지 않다. `local_irq_disable` 함수가 호출되기 전에는 인터럽트가 비활성화 상태였는데, critical section 작업 이후에 인터럽트가 활성화됬다. 즉, 이전 상태와 정반대가 됬다. 만약, 이전 상태가 인터럽트를 반드시 비활성화해야 하는 상태였다면, 이 문제는 시스템에 굉장히 큰 문제를 가져다 줄 수 있다. 그러므로, 이전 상태를 다시 복구시켜야 줘야한다. 즉, critical section 전에 인터럽트가 비활성화 였다면, critical section 후에도 인터럽트는 비활성화여야 한다는 것이다. 그럴려면, 이전 상태를 저장하고 있다가, critical section 이 마무리되면 복구시켜야 한다. `local_irq_save()` 함수는 현재 인터럽트 상태를 저장한 다음에 인터럽트를 비활성화한다. 그리고, `local_irq_restore()` 함수는 local_irq_save() 함수에서 저장한 인터럽트 상태를 복구한다. 

     

    - local_irq_save

    " `local`은 현재 프로세서를 의미한다. 즉, 현재 프로세서에 모든 인터럽트를 비활성화한다. 단, 현재 인터럽트 플래그 상태를 저장하기 위해, CPU 플래그 레지스터의 정보를 저장한다. 

     

     

    - local_irq_resotre

    `local`은 현재 프로세서를 의미한다. 즉, 현재 프로세서에 모든 인터럽트를 활성화한다. 단, 이전 인터럽트 플래그 상태를 전달해서 이전 인터럽트 상태로 복구한다.

     

     

    - disable_irq

    " 모든 프로세서들의 특정 인터럽트를 비활성화하고 싶다면, 이 함수를 호출하면 된다. `disable_irq` 함수는 인터럽트 라인 번호를 인자로 받는다. `local_irq_XXX` 함수들은 프로세서 플래그 레지스터를 통해서 인터럽트를 비활성화/활성화 하는 함수들이었다. 그러나, disable_irq() 함수는 프로세서 내부적으로가 아닌 외부 인터럽트 컨트롤러의 특정 인터럽트 라인을 비활성화 시키는 함수다[참고1]. 모든 프로세서가 특정 인터럽트 라인을 MASK 하는 가장 간단한 방법은 외부 인터럽트 컨트롤러에게 해당 인터럽트 라인을 MASK 하라고 명령하는 것이 제일 효율적이다.

     

    " 그런데, `disable_irq` 함수는 실제로 하드웨어 레벨에서 인터럽트 라인을 비활성화 시키지 않는다. 아래 문서를 보면, 하드웨적으로 인터럽트 라인을 마스크하는게 아니라, 해당 인터럽트 핸들러를 마스크한다. 이 기능이 만들어진 이유는 하드웨어 레벨에서 인터럽트가 비활성화 상태면, 엣지 트리거 인터럽트가 트리거될 경우, 해당 인터럽트는 인지도 못하고 사라져버린다. 그래서 `disable_irq` 함수가 호출되면, `IRQ_DISABLED` 플래그가 설정되는데, 이 때, 인터럽트가 들어오면 `IRQ_PENDING` 으로 재설정한다. 추후에 `enable_irq` 함수가 호출되면, 제일 먼저 `IRQ_PENDING` 비트가 설정되어 있는지를 검사한다. 그리고, 만약 해당 플래그가 SET 되어 있으면, 소프트웨어적으로 펜딩되어 있는 인터럽트를 프로세서에게 다시 전송한다. 이런 기술을 `Delayed interrupt disable` 이라고 한다. 이걸 사용하려면, 반드시 `CONFIG_HARDIRQS_SW_RESEND` 플래그를 설정하고 커널을 빌드해야 한다. 근데, 하드웨어적으로 `Delayed interrupt disable` 기술을 지원해주지 못하나? 

    Delayed interrupt disable

    This per interrupt selectable feature, which was introduced by Russell King in the ARM interrupt implementation, does not mask an interrupt at the hardware level when `disable_irq()` is called. The interrupt is kept enabled and is masked in the flow handler when an interrupt event happens. This prevents losing edge interrupts on hardware which does not store an edge interrupt event while the interrupt is disabled at the hardware level. When an interrupt arrives while the IRQ_DISABLED flag is set, then the interrupt is masked at the hardware level and the IRQ_PENDING bit is set. When the interrupt is re-enabled by `enable_irq()` the pending bit is checked and if it is set, the interrupt is resent either via hardware or by a software resend mechanism. (It’s necessary to enable `CONFIG_HARDIRQS_SW_RESEND` when you want to use the delayed interrupt disable feature and your hardware is not capable of retriggering an interrupt.) The delayed interrupt disable is not configurable.

    - 참고 : https://www.kernel.org/doc/html/v4.16/core-api/genericirq.html

     

     

    " disable_irq() 함수를 사용할 때, 주의점이 있다. 먼저, disable_irq() 함수는 연속해서 여러 번 호출(nested) 될 수 있다. 이럴 경우, 인터럽트를 다시 활성화하면 disable_irq() 함수가 호출된 횟수만큼 enable_irq() 함수도 동일한 횟수로 호출되야 해당 인터럽트 라인이 활성화된다.

    10.3.2.1 Disabling a single interrupt

    Calling any of these functions may update the mask for the specified irq in the programmable interrupt controller (PIC), thus disabling or enabling the specified IRQ across all processors. Calls to these functions can be nested—if disable_irq is called twice in succession, two enable_irq calls are required before the IRQ is truly reenabled. It is possible to call these functions from an interrupt handler, but enabling your own IRQ while handling it is not usually good practice.

    `disable_irq` not only disables the given interrupt but also waits for a currently executing interrupt handler, if any, to complete. Be aware that if the thread calling `disable_irq` holds any resources (such as spinlocks) that the interrupt handler needs, the system can deadlock. `disable_irq_nosync` differs from `disable_irq` in that it returns immediately. Thus, using `disable_irq_nosync` is a little faster but may leave your driver open to race conditions.

    - 참고 : http://www.makelinux.net/ldd3/chp-10-sect-3.shtml

     

     

    " disable_irq() 함수는 동기화 문제에서도 주의가 필요하다. 예를 들어, 3번 인터럽트가 발생했고, 3번 인터럽트 핸들러가 리소스 A를 사용한다고 치자. 그런데, 다른 프로세서에서 열심히 실행중이던 스레드가 disable_irq(3) 함수를 호출했다. 이 스레드는 인터럽트 핸들러의 처리가 끝날 때 까지 잠에 들게된다(`disable_irq` 함수안에 `sychronize_irq` 함수때문에 잠에 들게된다). 그런데, 여기서 문제가 발생했다. 잠든 스레드가 리소스 A에 대한 `락`을 가지고 있는 놈이었다. 인터럽트 핸들러가 리소스 A를 사용해야 하는데, 잠든 스레드가 리소스 A에 대한 락을 가지고 있어서, 3번 인터럽트 핸들러가 완료를 하지 못한다. 이렇게 데드락이 발생하게 된다. 이런 상황을 대비해서 disable_irq_nosync() 함수가 존재한다. 이 함수는 인자로 전달된 인터럽트 라인만 비활성화하고 즉각적으로 반환한다.

    // kernel/irq/manage.c - v4.10.5
    ...
    /**
     *	disable_irq_nosync - disable an irq without waiting
     *	@irq: Interrupt to disable
     *
     *	Disable the selected interrupt line.  Disables and Enables are
     *	nested.
     *	Unlike disable_irq(), this function does not ensure existing
     *	instances of the IRQ handler have completed before returning.
     *
     *	This function may be called from IRQ context.
     */
    void disable_irq_nosync(unsigned int irq)
    {
    	__disable_irq_nosync(irq);
    }
    EXPORT_SYMBOL(disable_irq_nosync);
    
    /**
     *	disable_irq - disable an irq and wait for completion
     *	@irq: Interrupt to disable
     *
     *	Disable the selected interrupt line.  Enables and Disables are
     *	nested.
     *	This function waits for any pending IRQ handlers for this interrupt
     *	to complete before returning. If you use this function while
     *	holding a resource the IRQ handler may need you will deadlock.
     *
     *	This function may be called - with care - from IRQ context.
     */
    void disable_irq(unsigned int irq)
    {
    	if (!__disable_irq_nosync(irq))
    		synchronize_irq(irq);
    }
    EXPORT_SYMBOL(disable_irq);
    ...

     

    " 참고로, 다수의 디바이스가 공유하는 인터럽트 라인을 `disable_irq` 함수를 통해 비활성화 하는 것은 좋지 못하다.

     

     

    - enable_irq

    " 모든 프로세서들의 특정 인터럽트를 활성화하고 싶다면, 이 함수를 호출하면 된다.

Designed by Tistory.