ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [리눅스 커널] Interrupt - Driver interrupt handler
    Linux/kernel 2023. 12. 29. 14:56

    글의 참고

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

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

    - https://stackoverflow.com/questions/7685294/request-threaded-irq-is-used-in-the-driver-why-not-request-irq-what-are-the

    - https://linux-kernel-labs.github.io/refs/heads/master/labs/interrupts.html 

    - https://wiki.linuxfoundation.org/realtime/documentation/technical_details/threadirq


    글의 전제

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

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


    글의 내용

    - Overview

    " 이 글에서는 core 및 subsystem engineers 가 아닌, peripherals engineers 입장에서 interrrupt handling 및 registering with interrupt handler 시에 사용하는 APIs 들에 심도있게 분석해볼 것이다. 그래서 이 글은 다음과 같은 내용들에 대해 알아볼 것 이다.

    1. Threaded interrupt handler
    2. request_threaded_irq() 함수
    3. struct irqaction 구조체

     

     

     

    - Linux-RT and background about introduction to threaded interrupt

    1. Real-time performance in non-preemptive linux kernel

    " 리눅스 커널 v2.4 전 까지는 preemption 을 지원하지 않았다. non-preemptive linux kernel 의 system call 프로세스는 다음과 같다. 

     

     

     

    " 위 block diagram 에서 시작은 high-priority task 부터 시작한다. high-priority task 는 요청받은 작업을 열심히 처리하다가, external event 를 받아야 작업을 이어나 갈 수 있는 상태가 된다. 즉, 언제 발생할 지 알 수 없는(비동기) externel event 를 기다리느라 sleep 에 빠지는 것이다(예를 들어, disk read operation). scheduler 는 CPU 를 효율적으로 사용하기 위해 다른 low-priority task 에게 CPU 제어권을 넘기고, low-priority task 가 자신에게 할당된 작업을 처리하기 시작한다. 

     

    " 그런데, low-priority task 에서 read() 와 같은 system call 을 유발하는 함수를 호출할 일이 생겨, user space 에서 kernel space 로 넘아가게 되었다. 이 때, 주의할 점은 이 시점에 task switching 은 발생하지 않는다는 것이다. 단지, user space 에서 kernel space 로만 바뀌는 것 뿐이다. 

     

    " 이 때, high-priority task 가 기다리던 external event 가 발생했다(T0). 그러나, kernel 은 interrupt 에 대해 즉각적으로 응답 하지 않는다. 왜냐면, kernel mode 에서 수행되는 일부 작업(예를 들어, critical section 에서 작업) 들은 external events(interrupts) 및들에 의해서 선점되는 것을 원치 않았기 때문에 kernel mode 로 진입할 때, interrupt 를 disabled 하고 진입한다(CPU interrupts 를 disabled 하거나 IRQ number 를 masked 한다). 결국, interrupt request 를 즉각적으로 처리할 필요가 없으므로, kernel mode 에서 기존에 수행하던 low-priority task 를 계속 실행한다.

     

    " T1 이 되면, kernel 은 critical section 작업을 모두 마무리했기 때문에, interrupt 를 다시 enable 한다(여기서 오해하면 안되는 내용이 있다. linux kernel 에서 interrupt 를 disabled 하는 시간은 굉장히 짧다. 즉, kernel 도 interrupt disabled 을 오래 유지하면 안디는 걸 알기 때문에, interrupt disabled 을 길게 유지하지 않는다. 위에 그림은 각 영역의 역할을 구분짓게 위해 조금 큼직하게 그렸는데, 절대 interrupt 가 오랫동안 disabled 되었다고 오해하면 안된다. 마찬가지로, interrupt 가 발생해서 interrupt service routine 으로 전환되는 과정도 상당히 짧은 시간에 이루어진다). interrupt 가 다시 enabled 되면, CPU 는 즉각적으로 interrup vector table 로 jump 한다. 이 시점에 low-priority task 는 interrupt 에 의해 선점당했다고 볼 수 있다. 이와 동시에 interrupt context 로 진입했다고 볼 수 있다.

     

    " 리눅스 커널은 interrupt 가 발생했을 때, 곧 바로 interrupt 를 처리할 driver interrupt handler(interrupt service routine) 가 실행하지 않는다. driver interrupt handler 가 호출되기 까지 꾀나 많은 사전 작업들을 수행한다. 예를 들어, 다음과 같은 작업들을 수행한다.

    1. interrupt controller 로 부터 발생한 interrupt 의 hardware interrupt ID(hwirq) 를 읽는다.
    2. hwirq 를 IRQ number 로 translate 한다.
    3. 발생한 interrupt 에 대해 ack 및 mask
    4. driver interrupt handler(interrupt service routine) 호출

     

     

    " T0 과 T2, 즉, 실제 interrupt 가 발생한 시점으로부터 driver interrupt handler 가 호출까지의 delay 를 `interrupt latency[참고1 참고2 참고3]` 라고 한다. interrupt latency 는 크게 2 파트로 나뉜다.

    1. hardware 에 의한 delay - peripehral 에서 발생한 interrupt 가 interrupt controller 를 거쳐서 최종 CPU 에게 도달하기 까지 에서의 걸린 시간(`peripheral -> interrupt controller -> CPU`).
     
    2. software 에 의한 delay - critical section 을 보호하기 위해서 interrupt 를 disabled 된 시간 + driver interrupt handler 를 실행되기 전까지 수행되는 사전 작업들(위에서 이미 언급)

     

     

    " driver interrupt handler(ISR) 가 호출되면, interrupt 처리를 진행한다. 그러다가, T3 시점에 driver interrupt handler 는 sleep state 에 있는 high-priority task 를 wake-up 시킨 뒤, runnable state 로 변경한다(runqueue 에 삽입 됨). driver interrupt handler 가 모든 interrupt handling 를 마무리되면, code 는 low-priority task 가 처리하고 있던 system call 로 되돌아 가게 된다. 즉, non-preemptive linux kernel 에서는 kernel mode 에서 interrupt 가 발생해서 다시 kernel mode 로 복귀할 때는 preemption point 가 없었다(예를 들어, need_resched() 같은 함수를 호출하는 곳이 없었음). low-priority task 가 남은 system call 을 모두 처리하고, user space 로 return 하는 시점이 되어서야 preemption point 가 나타났다.

     

    " 결국 오랜 기다림 끝에, T4 시점에, 드디어 scheduler 는 high-priority task 를 schedule 하게 된다. 여기서, T3 ~ T4 사이에 delay 를 `Task Response Time` 이라고 한다.  

     

     

     

    2. Real-time performance in preemptive linux kernel

    " v2.6 은 v2.4 와는 다르게, CONFIG_PREEMPT 컨피그를 제공한다. 이 옵션을 설정하면, kernel code 에서도 preemption point 가 나타낸다(kernel codes 에는 굉장히 많은 critical sections 들이 존재한다. 일반적으로, critical section 은 동기화 코드로 둘러싸여 있다. 당연한 얘기지만, CONFIG_PREEMPT 컨피그에서 추가된 kernel code 의 preemption points 들은 critical section 내부에는 작성되어 있지 않다). 아래 그림은 preemptive linux kernel(CONFIG_PREEMPT 컨피그가 설정된 후에) 의 system call 프로세스는 다음과 같다.

     

     

     

    " T0 ~ T3 까지는 v2.4 와 다르지 않다. 차이는 T4 에서 발생한다. v2.4 에서는 preemption point 는 항시 user space 로 복귀하는 시점에만 있었다. v2.6 에서는, 즉, preemptive kernel 에서는 interrupt 가 return 될 때, 이전 process(kenrel mode 에서 system call 을 처리중 인 low-priority task 를 의미) 가 critical section 내에 있지 않다면, 이 시점에 scheduling 이 가능한 프로세스가 있는지를 검사한다. 그리고, 이전 프로세스보다 우선 순위가 높은 프로스세가 있을 경우(high-prioriy task), 해당 프로세스를 실행한다. 그러나, 이전 process(low-priority task) 가 critical section 에 있었다면, 선점은 불가능하다. 

     

    " non-preemptive kernel 에서는 system call 을 통해서 kernel space 로 진입한 process 는 다른 processes(process context) 들에 의해서 절대로 선점될 수 가 없다(이 말이 kernel space 자체가 선점 될 수 없다는 말이 아니다). 그러나, interrupt context 는 모든 process context (kernel mode or user mode 상관없이) 를 선점할 수 있다(interrupt context 는 커널에서 굉장히 높은 권한을 가지고 있고, interrupt contexts 들끼리도 선점이 가능하다).

     

    " preemptive kernel 에 도입되면서, 전반적인 task response time 은 감소했다. 그러나, real-time performance 측면에서 보면, `부하가 어떻게 걸리건 간에(부하가 크건 적건) task response time 은 항상 일정해야 한다` 는 것이다(real-time 이란, 2 가지 의미가 합쳐진 단어다. 즉, `실시간(빠른) 응답성+ 일관된(응답 시간이 항상 일정) 응답성`). 그렇다면, task response time 에 가장 큰 영향을 주는 요소가 뭘까?

    1. preemption disabled - kernel 에서 동기화를 위해서 spinlock 과 같은 preemption 을 허용하지 않는 lock 메커니즘을 사용하거나 shared resource 에 대한 lock 을 소유하고 있어서 preemp_count 를 증가시킨 경우(preemption disabled). 

    2. interrupt context - interrupt context(hwirq, softirq, tasklet, timer 등) 는 항상 process context 를 선점할 수 있다.

     

     

    " 위와 같은 상황에서는 CONFIG_PREEMPT 컨피그가 설정되어 있어라도, task response time 이 불확실해진다. 위 문제들을 해결하기 위해서 다음과 같은 방법들을 생각해 볼 수 있다.

    1. preemption disabled - locks 을 오래 소유하고 있거나 preemption disabled 이 오랫동안 유지되는 곳을 찾아서 다른 동기화 기법을 적용해 볼 수 있는지를 고려해야 한다. 

    2. interrupt context - 사실, 위 block diagram 에서 T4 시점에 soft interrupt(softirq) 가 triggered 되거나, 새로운 interrupt 가 발생하거나, driver tasklet 이 실행을 기다리고 있다면, high-priority task 가 실행될 수 없다. T4 에서 대기 중인 bottom half tasks 들이 없어야지만, high-priority task 가 실행될 수 있다. 이 말은 결국, interrupt context 사용을 자제 해야 한다는 뜻이다.

     

     

     

    3. Real-time performance in linux kernel

    " 지금까지 내용을 보면, real-time linux 에서 performance 를 깍아 먹는 가장 큰 요인은 interrupt 라는 것을 알았을 것이다. 그렇다면, interrupt 를 효율적으로 처리해야 할 텐데, 구체적으로 어떻게 해야 할까? 이럴 때는 실제 RTOS 에서 어떻게 interrupt 를 handling 하는지를 살펴보면 도움이 된다. 대부분의 RTOS interupt handler 는 정말 심플하다. 그냥, thread 에 우선 순위를 부여한 후에 실행한다. 그게 끝이다. 정말 대부분의 device driver 의 interrupt handler 가 앞에 언급한 대로 작성 되어있다.

     

    " 즉, interrupt handler 에서 적절하게 우선 순위가 할당된 thread 만 실행하고 interrupt handler 가 종료되기 때문에, interrupt context 가 굉장히 짧고, 모든 interrupt handling 처리를 thread 에게 위임하기 때문에, interrupt context 와 process context 에 대한 구분이 의미가 없을정도다. software 구조를 이와 같이 설계하면, CPU 를 가지고 경쟁하는 요소들은 이제 processes 들 끼리 밖에 없다. 즉, process priority 값만 가지고 경쟁이 가능해지는 것이다(대신 우선 순위를 상당히 신중하게 할당해야 한다).

     

    " 리눅스 커널에서 peripheral interrupt handling 은 2 가지 파트로 나뉜다.

    1. top half - 시간에 상당히 예민하고 빨리 처리해야 할 일들은 여기에 작성한다. 그리고, interrupt context 에서 실행된다는 점을 살려 반드시 안정성이 보장 되어야 하는 코드만 작성한다.

    2. bottom half - 늦게 실행되어도 되는 부분은 여기에 작성한다(사실상 `top half` 외에 작업들은 모두 여기에 작성한다). 

     

     

    " 리눅스 커널의 bottom half 중에 softirq, tasklet 은 interrupt context 에서 실행되기 때문에, 모든 process 보다 항상 먼저 실행된다. 이렇게 context 에 따라 CPU 선점권에 대한 우선 순위를 더 높게 가져가는 방식은 큰 문제를 야기할 수 있다. 예를 들어, tasklet 은 interrupt context 에서 동작하고, kernel engineer 가 아닌, driver engineer 에서 사용할 수 있도록 설계되었다. 만약, 역량이 낮은 driver engineer 가 tasklet 을 남용할 경우, system performance 에 악영향을 주게 된다.

     

    " 리눅스 커널도 CPU 의 경쟁 관계를 일관성있게 유지하기 위해 interrupt context 사용을 최소화하고, interrupt 를 thread 로 다루는 기법을 커널에 추가했다. 흔히, `threaded irq` 라고 하는 이 기법은 driver interrupt handler 의 bottom half 를 thread 로 할당하는 것이다. 그래서, kernel 의 preemption point 에서 흔히 thread 라고 불리는 kernel thread, user space thread, threaded irq 가 CPU 를 가지고 경쟁하게 된다.  

Designed by Tistory.