[Kernel of Linux] 9. Interrupt (2)
Kernel/Theory

[Kernel of Linux] 9. Interrupt (2)

반응형

지난 강의 요약 - Interrupt (1)

interrupt를 발생시키는 하드웨어들은 각기 배정된 IRQ line에 속해있다. 그리고 여러 line들을 통제해주는 컨트롤러가 필요한데 이것이 바로 PIC(Programmable Interrupt Controller)다.

각 line은 irq_desc 구조체를 가지고 있는데 여기에는 현재 line의 상태, handler, lock, action 같은 정보들이 담겨있다. action field에는 irqaction 구조체가 포인터로 연결되어 있다. 각 irqaction은 device의 ISR을 처리하기 위한 정보들이 담겨있다.

interrupt를 처리하기 위해 불리는 함수들이 존재한다. PIC으로부터 받은 vector를 이용해 어셈블리어로 작성된 함수로 점프하고 do_IRQ() 함수를 호출한다. 이 do_IRQ() 함수의 소스 코드를 살펴보면서 interrupt가 처리되는 동작을 이해할 수 있었고 lock을 처리하면서 생기는 발생 가능한 문제점이 있다는 것을 인지했다.


지난 강의에 이어서 기술한다.

1. Three cases that can happen

"CPU[i]가 mth IRQ line으로부터 interrupt를 처리하려는 상황"에서 발생 가능한 3가지 경우와 가능한 CPU[i]의 동작들에는 무엇이 있는지 살펴보자. 이전에 보았던 do_IRQ() 함수의 소스 코드와 함께 보는 걸 추천한다.

  1. m번째 IRQ line을 처리하는 다른 CPU가 없음
    - CPU[i]가 그냥 해당 라인 처리하면 됨.
  2. 다른 CPU[k]가 이미 m번째 IRQ line을 처리 중임 (새로 도착하고 ACK signal 보냄)
    - CPU[i]는 IRQm에 대해 아무것도 하지 않음. (action <- NIL(NULL))
    - CPU[i]가 IRQm의 state를 PENDING으로 바꾼다. 하지만 INPROGRESS가 CPU[k]에 의해 설정된 상태이기 때문에 action field는 NULL인 체로 아래로 내려간다. 그리고 그대로 IRQm을 나간다. 이렇게 하면 이전 포스팅에서 말했던 문제점을 어떻게 해결하고 있는지를 알 수 있다.
  3. CPU[k]가 IRQm의 ISR을 끝내고 난 뒤
    - CPU[k]는 handle_IRQ_event() 함수를 돌고 나온 뒤 아래에서 다시 PENDING flag가 설정되어 있는지 확인한다. 있다면 PENDING 지우고 다시 handle_IRQ_event() 함수를 호출하는 루프를 다시 돈다.
    - 즉, 후발 CPU가 같이 ISR을 처리하려고 하는 것이 아니라 PENDING만 설정해서 선발 CPU에게 더 있으니 이것도 처리해달라고 말하고 나가는 것이다. 이렇게 해서 충돌을 방지할 수 있는 것이다.

1.1 IRQ 처리 순서 정리

  1. IRQm에서 IRQ가 발생한다.
  2. APIC이 counter를 기반으로 한 중재 알고리즘을 통해 CPU를 선정한다.
  3. 예를 들어 CPU[i]가 선택됐을 때, 먼저 IRQm의 status에서 WAITING을 끄고 PENDING을 켠다.
  4. 이후 CPU[i]는 IRQm의 ISR을 처리할 CPU가 누가 될 것인지 결정하게 된다.
    case A) 아무도 IRQm을 처리하고 있지 않을 때
        -> CPU[i]가 IRQm 처리
    case B) 다른 CPU[k]가 IRQm 처리 중일 때 (IRQ_INPROGRESS is enabled)
        -> CPU[i]는 CPU[k]가 모든 IRQm을 연속적으로 처리하도록 한다. (IRQ_PEDNING을 설정함으로써)
            IRQm의 data에 접근하는 것은 어찌 되었든 serialized 해야 하기 때문이다.

1.2 A view from the source code

글로써 정리된 건 위에 있으니 아래 소스코드에서의 sequences를 보고 이해해보자.

case 1) No CPU is currently working on IRQm now

case 2) Other CPU[k] is already running handler for IRQm

case 3) CPU[k] is completing handler for IRQm

 

2. Race Condition

IRQ를 처리하는 데에 있어서 여러 race condition이 발생할 수 있는 경우가 있다.

IRQ request를 처리하고 있는 CPU는 해당 IRQ가 끝날 때까지 interrupt가 비활성화된다. 새로운 IRQ request를 위해 IRQ는 빠른 시간 내에 처리되어야 한다. 많은 IRQ lines를 처리하고 있는 PIC은 한 번에 하나의 line만을 컨트롤할 수 있으므로 CPU에게 처리를 요청한 후 CPU로부터 ACK signal을 받기 전까지는 block 된 상태다. 나머지 lines는 block이 풀릴 때까지 경쟁 상태에 놓여있기 된다. 많은 CPU들은 irq_desc 같은 shared variable에 접근하기 위해 lock을 획득하는 과정이 필요하다. 이는 CPU들끼리 경쟁 상태에 놓이는 효과를 불러일으킨다.

위에서 말한 모든 상황이 컴퓨터의 성능에 영향을 끼치는 행동이다. 왜냐하면 다음 동작을 수행하기까지의 시간이 오래 걸리면 걸릴수록 컴퓨터의 전체 성능이 저하되는 현상이 발생할 수 있기 때문이다.

소스 코드 상에서 확인해보자. 노란색 영역과 흰색 영역은 남한테 민폐를 끼치는 수준이 다르다. 흰색 영역에서는 이전에 lock을 풀고 오니까 다른 CPU가 access 할 수 있다. 하지만 노란색 영역에 있을 때에는 이 IRQ request를 보낸 PIC도 block 되어 있고, 현재 lock을 획득한 CPU는 다른 interrupt를 비활성화시켜놓고(그렇게 하지 않으면 interrupt가 제대로 끝나기도 전에 또 다른 interrupt가 걸릴 수 있다), 그리고 interrput 걸리기 이전에 돌아가던 process도 CPU를 뺏긴 상태다.

노란색 영역 =        Critical Top-Half Interrupt Handler
흰색    영역 = Non-Critical Top-Half Interrupt Handler

즉, 노란색 영역에 있는 시간은 성능을 위해 가능한 짧아야 한다는 것을 의미한다.

하지만 ISR 자체(흰색 영역)도 interrupt level에서 동작한다는 점에서 device의 handler가 오래 걸리면 이 IRQ가 끝날 때까지 기다려야 한다는 점에서 민폐를 끼칠 가능성이 있다는 것이다. 예를 들어 ftp 서버를 담당하고 있는 device에서 30GB 정도의 데이터를 전송하는데 이를 handler 내부에서 다 처리하게 만든다면 굉장히 오랜 시간이 소모되면서 결국 interrupt가 끝날 때까지 오랜 시간이 걸리게 될 것이다.

이걸 막기 위해 나중에라도 동작을 수행하도록 만든 것이 바로 SoftIRQ다. Hardware(device)가 IRQ를 걸었다면 이번에는 Software적으로 IRQ를 나중에 호출하도록 만든 것이므로 SoftIRQ다. 또 전체 IRQ를 처리하는 걸 나눠서 부르는데, Hardware IRQ가 실행되는 구간은 Top-Half, Software IRQ가 실행되는 구간을 Bottom-Half라고 한다.

SoftIRQ를 실행하기 위해 soft-irq pending bit를 설정한다. 이 bit가 설정되면 나중에 do_softirq() 함수를 부른다.

실제로 적용된 예시를 살펴보자. TCP/IP 네트워크 device를 예시로 들었다.

NIC(Network Interface Card) device에서 interrupt를 요청하고 PIC이 CPU를 선정한다. 이후 do_IRQ() 함수가 호출되고 ack() 함수를 통해 다시 PIC의 block 상태를 해소시킨다. 그리고 irq_desc의 정보를 업데이트한다. handler는 NIC가 받은 패킷을 메모리로 이동시키고 패킷의 내용을 체크해서 처리할 방향을 결정한다. 하지만 이 모든 과정을 전부 Top-Half에서 할 수는 없는 일이다. 그래서 이건 device driver의 디자인을 어떻게 잘 설계하느냐에 따라 성능이 결정될 것이다.

위에서 보여주는 디자인을 보자. 하드웨어인 NIC이 network로부터 packet을 받고 CPU한테 interrupt를 걸었다.

Top-Half에서 이뤄지는 로직은 packet을 위한 공간을 memory에 할당한 뒤에 NIC으로부터 packet을 memory로 옮기는 작업을 하고 Bottom-Half가 필요하다는 표시로 softirq pending bit을 설정한다. 그리고 interrupt는 끝이 난다.

OS는 계속해서 다음으로 실행할 task를 우선순위에 따라 ready list에서 찾는데, 만약 찾은 task가 soft irq를 처리하려고 한다면 do_softirq() 함수를 호출한다. 이때 내부 로직은 Top-Half에서 못했던 패킷을 실제로 처리하는 일을 수행한다.

반응형