-
반응형
인터럽트가 발생한 후 처리해야 할 일이 많을 때는 어떻게 해야 할까? 인터럽트가 발생한 후 이를 처리해야 할 코드를 2단계로 분류하면 된다. 즉, 인터럽트 핸들러에서 바로 실행해야 할 부분과 조금 후 실행해도 되는 코드로 나눠서 처리하는 것이다. 이처럼 인터럽트가 발생한 후 조금 후에 인터럽트를 처리하는 코드를 실행하는 기법을 인터럽트 후반부 처리라고 말한다. 여기서 '조금 후'는 프로세스가 스케줄링할 수 있는 조건이라는 의미이다. 디바이스 드라이버에서 인터럽트 후반부 기법을 활용해 인터럽트 후반부를 처리하는 코드를 자주 볼 수 있다. 또한 네트워크 패킷 통신이나 멀티미디어를 처리하는 드라이버에서 인터럽트 ㅎ후반부 기법을 많이 적용한다. 그래서 리눅스 커널에서 제공하는 IRQ 스레드, Soft IRQ, 태스크릿과 같은 인터럽트 후반부 기법을 잘 알고 있어야 한다.
1. 인터럽트 후반부 기법이란?
인터럽트 핸들러에서 처리해야 할 일이 많을 때는 어떻게 해야 할까, 이때는 인터럽트 후반부 기법을 적용하면 된다.
- 인터럽트 후반부 기법을 적용하는 이유
- 인터럽트 핸들러(인터럽트 컨텍스트)에서 스케줄링 함수를 호출하면 생기는 문제점
- 인터럽트 후반부 기법의 종류와 각 기법의 차이점
인터럽트 후반부 기법을 적용하는 이유
인터럽트 후반부 기법을 적용하는 이유를 알기 위해서는 먼저 커널이 인터럽트를 처리하는 방식을 살펴볼 필요가 있다. '인터럽트 후반부'라는 용어 자체가 인터럽트를 후반부에 처리한다는 의미를 지니기 때문이다. 여기서 말하는 '인터럽트의 후반부'는 인터럽트 핸들러에서 인터럽트를 바로 처리해야 할 코드를 실행한 다음에 인터럽트를 처리하는 시점을 의미한다.
- 인터럽트가 발생하면 커널은 실행 중인 프로세스를 멈추고 인터럽트 벡터를 실행해서 인터럽트 핸들러를 실행한다.
- 인터럽트 핸들러는 짧고 빨리 실행돼야 한다.
- 인터럽트를 처리하는 구간이 인터럽트 컨텍스트이며, 이를 in_interrupt() 함수가 알려준다.
보다시피 인터럽트가 발생해서 arch_cpu_idle() 함수가 실행되는 도중 __irq_svc 인터럽트 벡터로 실행흐름이 바뀌었다. 이를 프로세스 입장에서 보면 arch_cpu_idle() 함수가 실행되는 도중 멈춘 것이다. 이처럼 인터럽트가 발생하면 실행 중인 프로세스를 멈추고 인터럽트 벡터로 이동해서 인터럽트 핸들러를 실행한다. 그래서 인터럽트 핸들러는 빨리 실행돼야 한다. 이 과정에서 임베디드 리눅스 개발자들은 다음과 같은 고민을 하게 된다. "인터럽트가 발생한 후 처리해야 할 동작이 많은 때가 있다. 하지만 인터럽트 핸들러는 빨리 실행돼야 한다." 인터럽트 핸들러에서 급하게 처리하지 않아도 되는 일은 조금 후에 해도 되지 않을까? 즉, 여러 고민 끝에 인터럽트가 발생했을 때 인터럽트를 처리할 코드를 다음과 같은 2단계로 나누게 된다.
- 빨리 실행할 코드: 인터럽트 핸들러 및 인터럽트 컨텍스트
- 실시간으로 빨리 실행하지 않아도 되는 코드: 인터럽트 후반부 기법
결국 다음과 같은 인터럽트 후반부 처리 기법들을 이끌어 냈다.
- IRQ 스레드
- Sort IRQ
- 태스크릿
- 워크큐
인터럽트 컨텍스트에서 많은 일을 하게 되면?
인터럽트 후반부 기법이 도입된 이유는 인터럽트 핸들러에서는 짧고 간결하게 코드가 실행돼야 하기 때문이다. 만약 인터럽트 핸들러에서 실행 시간이 오래 걸리면 시스템은 어떻게 동작할까?
인터럽트 컨텍스트에서 실행 시간이 오래 걸리면 대부분의 시스템은 오동작하게 된다. 가령 디바이스 드라이버를 개발하다 보면 다음과 같이 인터럽트 핸들러에 실수로 실행 시간이 오래 걸리는 코드를 작성할 수 있다.
- I/O을 시작하는 코드
- 과도한 while 반복문
- 유저 공간으로 uevent를 전달해서 인터럽트 발생을 알림
- 스케줄링을 지원하는 커널 함수를 호출
위와 같은 코드가 인터럽트 핸들러나 인터럽트 핸들러 내부의 서브 함수에서 동작하면 시스템 반응 속도가 아주 느려진다. 또한 커널 로그를 열어보면 평소에 볼 수 없는 에러 메시지를 볼 수도 있다. 인터럽트 컨텍스트에서 인터럽트를 처리하는 코드의 실행 시간을 어떻게 측정할 수 있을까? 이를 위해서는 ftrace 기능에서 지원하는 function_graph 트레이서로 인터럽트가 처리될 떄 걸리는 시간을 측정할 필요가 있다.
인터럽트 컨텍스트에서 발생한 커널 패닉
인터럽트를 처리하는 인터럽트 컨텍스트에서 실행 시간이 오래 걸리는 코드가 실행되면 시스템이 오동작할 가능성이 높다. 그런데 커널에서는 인터럽트 컨텍스트에서 시간이 오래 걸리는 함수를 호출하면 커널 패닉을 유발하자는 제약을 둔다.
- 첫째, 프로세스의 thread_info 구조체의 preempt_count 필드가 0x00010001 이니 현재 실행 중인 코드는 인터럽트 컨텍스트이다. 0x00010001을 HARDIRQ_MASK(0xF0000) 플래그와 AND 비트 연산한 결과가 1이기 때문이다.
- 둘째, 인터럽트 컨텍스트에서 schedule() 함수를 호출했다.
인터럽트 컨텍스트에서 스케줄링을 시도하니 커널은 이를 감지하고 커널 패닉을 유발하는 이유는 무엇일까? 인터럽트가 발생한 수 빨리 코드를 실행해야 하는데 스케줄링을 지원하는 함수가 호출되면 프로세스가 휴면 상태에 진입할 수 있기 때문이다.
이 같은 방식으로 커널은 인터럽트 컨텍스트에서 스케줄링을 지원하는 커널 함수의 호출을 제한한다. 비단 리눅스 커널뿐만 아니라 다른 운영체제에서도 '인터럽트를 처리할 때는 코드를 빨리 실행해야 한다'라는 문제를 안고 있었다. 이 과정에서 자연스럽게 임베디드 개발에서 Bottom Half와 Top Half라는 개념을 이끌어 냈으며, 이를 기준으로 인터럽트 후반부를 처리하기 시작했다.
반응형