Chapter 6 - Interrupt/Trap & System call(1)

2022. 9. 19. 01:37개발 관련 책 읽기/리눅스 커널 내부구조

인터럽트

 인터럽트란 주변 장치와 커널이 통신하는 방식 중의 하나로,주변 장치나 CPU가 자신에게 발생한 사건을 리눅스 커널에게 알리는 매커니즘이다.외부에서 네트워크를 통해 패킷이 도착했을음 알리기 위해,혹은 키보드가 눌렸음을 알리기 위해서 등 다양한 이유로 인터럽트가 발생된다. 인터럽트가 발생하면 Kernel은 어떠한 이유로 인터럽트가 발생했는지 살펴보고 인터럽트 핸들러를 호출해 작업을 처리해야 한다.

만약 네트워크 장치(LAN카드)로부터의 인터럽트라면 패킷을 수신하여 TCP/IP 스택을 통해 데이터를 수신하고,키보드 인터럽트라면 어떤 키가 눌렸느지 확인하여 응용 S/W에게 전달해준다.

 

인터럽트는 외부 인터럽트와 트랩으로 나뉘게 된다.

 - 외부 인터럽트 : 주변장치에서 발생된 비동기적인 하드웨어 event를 의미.

 - Trap:소프트웨어적인 event이며 예외 처리라고 한다.

ex)divide_by_zero, segment fault , page fault , protection fault , system call

 

그러면 어떻게 운영체제의 인터럽트 핸들러가 수행되는 것일까? 모든 CPU는 인터럽트가 발생하면 PC(또는 Instruction Register)레지스터의 값을 미리 정해진 특정 번지로 변경하도록 정해져 있다.

 

ARM계열 CPU라면 인터럽트가 발생하는 순간 (0x00000000+offset)번지로 점프한다. 이때 offset은 인터럽트의 종류에 따라 결정되는데 reset 일 경우 offset = 0 , undefined instruction인 경우 4, software interrupt인 경우 8이다. 운영체제는 해당 번지에 실제 인터럽트 핸들러를 등록하는 대신에 다른위치에 핸들러를 작성해 두고, 해당 번지에는 인터럽트 핸들러 주소로 jump하는 명령어만을 기록해 놓는 방식이다.

 

이러한 핸들러 테이블을 IDT(Interrupt Descriptor Table)또는 IVT(Interrupt Vector Table)이라 부른다.

 

인터럽트 & 트랩의 처리

 

리눅스는 외부 인터럽트와 트랩을 동일한 방식으로 처리한다. 구체적으로 인터럽트와 트랩을 처리하기 위한 루틴을 함수로 구현해 놓은 뒤,각 함수의 시작 주소를 리눅스의 IDT인 idt_table 이라는 이름의 배열에 기록해 둔다.idt_table의 0~31까지 32개의 엔트리를 CPU의 "트랩" 핸들러를 위해 할당하고, 그 외의 엔트리는 "외부 인터럽트"의 핸들러를 위해 사용한다.

 

PC환경에서 외부 인터럽트를 발생시킬 수 잇는 주변장치들은 하드웨어적으로 PIC(Programmable Interrput Controller)라는 칩의 각 핀에 연결되어 있다. 또한 PIC는 CPU의 한 핀에 연결되어 있다.

 

위의 그림은 x86 CPU를 가정하고 있으며,0번 엔트리는 "divide_by_zero error"예외이고, 1번 엔트리는 "debug", 2번 엔트리는 "nmi",3번 엔트리는 "int 3"으로 설정되어 있다. 또한 외부 장치들은 32번 인터럽트부터 사용할 수 있으므로, timer는 32번,keyboard는 33번 인터럽트를 사용한다고 가정하고 있다.

 

만약 태스크가 실행 중에 "divide by zero error"가 발생했다고 가정한다면 커널 내의 "devide by zeor error"를 발견한 루틴에서 0x00번을 인자로 트랩을 발생시킨다. 그럼 커널에 트랩이 걸리게 되고, 커널은 idt_table에서 0x00번째 엔트리에 등록되어 있는 핸들러를 호출한다.

 

만약 timer로부터 인터럽트가 발생했다면

  1. timer는 PIC와 연결된 선에 펄스를 보내게 된다.
  2. 그럼 PIC는 수신한 펄스를 적절한 번호로 변환하고 이를 I/O포트에 저장하여 CPU가 버스를 통해 읽을 수 있도록 한다.
  3. 그런 뒤 CPU에게 "외부 인터럽트"가 발생했음을 알리기 위해 CPU와 연결된 라인에 펄스를 보낸다.
  4. 이때 CPU는 외부 인터럽트가 발생했음을 알게 되고 PIC의 I/O 포트를 읽어서 발생한 "외부 인터럽트"의 벡터 번호 확인후,인터럽트 선을 원래대로 복원시켜서 PIC가 다른 "외부 인터럽트"를 받을 수 있도록 한다.
  5. 리눅스 커널은 현재 발생한 "외부 인터럽트"의 벡터 번호를 확인한 뒤, 이 번호를 통해 idt_table을 인덱싱 하여 해당 핸들러를 호출하게 된다.

 

그런데 외부에서 인터럽트를 발생시킬 수 있는 라인은 한정된 개수이며 따라서 함부로 할당 하거나 무조건 독점하여 사용하는 것은 매우 비효율적이다.

 

또한 장치를 관리하는 디바이스 드라이버들은 인터럽트라는 귀중한 자원을 동적으로 할당 받거나 해제하기도 한다.이를 위해 트랩으로 사용되지 않는 즉 외부 인터럽트를 위한 번호는 별도로 관리하는데 이것이 바로 irq_desc 테이블이다.

 

이를 위해 리눅스 커널은 128번을 제외한(128번은 시스템 콜 용도) 32~255까지의 idt_table 에는 같은 핸들러 함수가 등록 되어 있으며, 이 함수는 do_IRQ()라는 함수를 호출한다.do_IRQ() 함수가 하는 일은 발생된 "외부 인터럽트" 번호를 가지고 irq_desc 테이블을 인덱싱하여 해당 "외부 인터럽트" 번호와 관련된 irq_desc_t 자료구조를 찾는다.

 

이 자료구조에는 하나의 인터럽트를 공유할 수 있도록 action 이라는 자료구조의 리스트를 유지하고 있다. 바로 이 리스트를 이용하여 단일 인터럽트 라인을 공유하는 것이 가능해지는 것이다.

 

 
irq_desc

커널이 인터럽트를 받으면 즉시 인터럽트 핸들러를 호출하는 것이 아니다. 핸들러 호출 전에 문맥 저장을 해야한다.또한 인터럽트가 완료되면 문맥을 복원도 해줘야 한다.

 

트랩

태스크가 수행중에 "page fault error"가 발생하면 커널은 폴트가 발생한 주소에 해당되는 데이터를 메모리에 적재하고 현재 태스크의 페이지 테이블을 수정해 준 뒤,폴트를 발생시킨 명령어를 다시 수행한다

리눅스 커널은 트랩을 다시 3가지로 구분한다.

  1. Fault : fault를 일으킨 명령어 주소를 eip에 넣어 두었다가 해당 핸들러가 종료되고 나면 eip에 저장되어 있는 주소부터 다시 수행한다(ex. page fault )
  2. Trap : trap을 일으킨 명령어의 다음 주소를 eip에 넣어 두었다가 그 다음부터 다시 수행한다(ex. system call)
  3. Abort : 이는 심각한 에러인 경우이므로 eip를 저장해야할 필요가 없으며,현재 태스크를 강제 종료한다.(ex. divide by zero)

 

각각의 핸들러를 수행한 뒤, 커널은 수행 중이던 태스크의 어디로 제어를 넘겨야 할지 결정해야 한다.

 

시스템 콜을 제외한 트랩의 경우에는 ret_from_exception()을 호출하며,일반적인 외부 인터럽트인 경우에는 ret_from_intr(), 0x80인터럽트 즉, 시스템 콜인 경우에는 ret_from_sys_call(),시스템 콜 중에서도 fork(),vfork(),clone()인 경우에는 ret_from_fork()함수를 사용하여 복귀한다.

 

 

 

반응형