Chapter 6 인터럽트와 트랩 그리고 시스템 호출

 

인터럽트 처리 과정

인터럽트: 주변 장치와 커널이 통신하는 방식 중의 하나로, 주변 장치나 CPU가 자신에게 발생한 사건을 리눅스 커널에게 알리는 매커니즘.

외부에서 네트워크를 통해 패킷이 도착했음을 알리기 위해, 혹은 키보드가 눌렸음을 알리기 위해서 등 다양한 이유로 인터럽트가 발생된다. 인터럽트가 발생되면 운영체제는 왜 인터럽트가 발생했는지를 살펴보고 적절한 작업을 처리해야 한다. 이때 작업을 처리하는 함수를 인터럽트 핸들러(interrupt handler)라고 부른다.

 

시스템이 운영되는 도중 발생되는 인터럽트는 원인에 따라 2가지로 구분

1. '외부 인터럽트'로써, 현재 수행중인 태스크와 관련없는 주변장치에서 발생된 비동기적인 하드웨어적인 사건

2. 현재 수행중인 태스크와 관련있는 즉 동기적으로 발생하는 사건으로써 '트랩' 이라고 부른다. 소프트웨어 적인 사건이며 예외 처리(exception handling)라고도 한다. 예) 0으로 나누는 연산, 세그멘테이션 결함, 페이지 결함, 보호 결함, 시스템 호출 등

 

인터럽트 발생시 어떻게 운영체제의 인터럽트 핸들러가 수행되는가?

모든 CPU는 인터럽트가 발생하면 program counter(또는 instruction pointer)레지스터의 값을 미리 정해진 특성 번지로 변경하도록 정해져 있다.

 

리눅스의 인터럽트 관리 기법

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

 

PC환경에서 외부 인터럽트를 발생시킬 수 있는 주변장치들은 하드웨어적으로 PIC(Program-mable Interrupt Controller)라는 칩의 각 핀에 연결되어 있다. 또한 PIC는 CPU의 한 핀에 연결되어 있다. x86 CPU에서는 idt_table의 31번 엔트리까지를 '트랩' 핸들러가 사용하므로 PIC는 32번부터 사용 가능(리눅스 커널의 부팅중에 설정된다.)

 

리눅스 커널은 현재 발생한 '외부 인터럽트' 의 벡터 번호를 확인한 뒤, 이 번호를 통해 idt_table 인덱싱 하여 해당 엔트리에 있는 핸들러를 수행시켜주면 된다.

 

외부에서 인터럽트를 발생시킬 수 있는 라인은 한정된 ㅐ수이며 따라서 함부로 할당 하거나 무조건 독점하여 사용하는 것은 매우 불합리하다. 장치를 관리하는 디바이스 드라이버들은 인터럽트라는 귀중한 자원을 동적으로 할당 받거나 해제하기도 한다. 이를 위해 트랩으로 사용되지 않는 즉 외부 인터럽트를 위한 번호는 별도로 관리하는데 이것이 바로 irq_desc 테이블이다. 128을 제외한 32~255까지의 idt_table에는 같은 인터럽트 핸들러 함수가 등록 되어 있으며, 이 함수는 do_IRQ()라는 함수를 호출한다

 

do_IRQ() : 발생된 '외부 인터럽트' 번호를 가지고 irq_desc 테이블을 인덱싱하여 해당 외부 인터럽트 번호와 관련된 irq_desc_t자료구조를 찾는다. 이 자료구조 안에는 하나의 인터럽트를 공유할 수 있도록 action 이라는 자료구조의 리스트를 유지하고 있다. 바로 이 리스트를 이용하여 단일 인터럽트 라인을 공유하는 것이 가능해진다.

 

커널은 인터럽트를 받으면 즉시 인터럽트 핸들러를 호출하는것이 아니고 그 전에 문맥 저장(context save)를 한다. 또한 인터럽트 처리가 완료되면 저장되었던 문맥을 복원(context restore)해 주어야 한다.

 

인터럽트 핸들러를 수행하기 전에 태스크가 어디까지 수행했는지 기억해 두어야 한다. 그래야 인터럽트 핸들러가 서비스를 마쳤을 때 인터럽트 발생 전에 수행하던 태스크를 계속 수행할 수 있다.

 

idt_table에 등록 되어 있는 '외부인터럽트'를 위한 공통 핸들러 common_interupt, SAVE_ALL 매크로를 사용하여 인터럽트 발생 시점 수행 중이던 태스크의 문맥을 저장, do_IRQ()함수를 호출. do_IRQ()함수에 의해 실제 인터럽트 서비스가 수행되고, 종료되고 나면 ret_from_intr를 호출하는데 이곳에서는 SAVE_ALL 매크로를 통해 저장했던 문맥을 RESTORE_ALL 매크로를 통해 복원하는 등의 작업을 수행하게 된다.

 

트랩 자세히 알아보기

응용 프로그램 수준 태스크가 수행되다가 'page fault error'가 발생하면 어떻게 될까 ? 리눅스 커널은 폴트가 발생한 주소에 해당되는 데이터를 메모리에 적재하고 현재 태스크의 페이지 테이블을 수정해 준 뒤, 폴트를 발생시킨 명령어를 다시 수행시켜 준다.

 

devide by zero error의 경우는 다르다. 같은 트랩이라 할지라도 어떤 트랩이냐에 따라서 구분할 피료가 있다. 따라서 리눅스 커널은 트랩을 다시 세 가지로 구분한다.

1. fault : 이 경우 리눅스 커널은 fault를 일으킨 명령어 주소를 eip에 넣어 두었다가 해당 핸들러가 종료되고 나면 eip에 저장되어 있는 주소부터 다시 수행을 시작

2. trap : 이 경우 리눅스 커널은 trap을 일으킨 명령어의 다음 주소를 eip에 넣어 두었다가 그 다음부터 수행 한다. (시스템 콜 같은 것이 여기에 해당)

3. abort : 이는 심각한 에러인 경우, eip값을 저장해야할 필요가 없으며, 현재 태스크를 강제 종료 시킨다.

 

각각의 핸들러가 모두 수행된 뒤, 리눅스 커널은 수행 중이던 태스크의 어디로 제어를 넘겨야 할지, 다른 태스크를 수행시켜야 할지를 결정. 각각의 핸들러의 리턴 함수를 따로 작성함으로써 이루어진다. 시스템 콜을 제외한 트랩의 경우에는 ret_from_exception()을 호출, 일반적인 외부 인터럽트인 경우 ret_from_intr(), 0x80 인터럽트 (시스템콜) 인 경우에는 ret_from_sys_call(), 시스템 콜 중에서도 fork() vfrok() clone()의 경우에는 ret_from_fork() 함수를 사용하여 복귀 한다.

 

시스템 호출 처리 과정

시스템 호출 : 사용자 수준 응용 프로그램들에게 커널이 자신의 서비스를 제공하는 인터페이스. 따라서 사용자가 운영체제의 기능이나 모듈을 활용하기 위해서는 반드시 시스템 호출을 사용해야 한다. 따라서 시스템 호출은 커널로의 진입점(entrypoint)라고 볼 수 있다.

 

대표적인 예로는 새로운 태스크를 생성하는 sys_fork(), 파일의 내용을 읽는 sys_read(), 현재 실행중인 태스크의 실행 우선순위를 제어하는 sys_nice() 등이 있다.

 

리눅스 커널은 각 시스템 호출을 함수로(시스템 호출 핸들러) 구현해 놓고 각 시스템 호출이 요청되었을 때 대응되는 함수를 호출하여 서비스를 제공.

sys_fork() : ~/kernel/fork.c   sys_read() : fs/read_write.c 에 구현

 

시스템 콜에 할당되어 있는 고유한 번호는 어떻게 알 수 있을까? 리눅스 커널이 제공하는 모든 시스템 호출은 각각 고유한 번호를 갖는데 x86 CPU를 기준으로 ~/arch/x86/syscall_64.tbl 혹은 _32.btl 파일에 정의

각 테이블의 엔트리에는 시스템 호출 처리 함수의 시작점 주소가 들어 있고, 각 엔트리는 각 시스템 호출번호를 인덱스로 하여 접근된다.

 

 

새로운 시스템 호출 구현

최초의 리눅스 커널 프로그래밍을 해보자. sys_newsyscall() 이라는 이름의 새로운 시스템 호출 구현 예. 이 시스템 호출은 터미널에 "Hello Linux, I'm in Kernel" 이라는 문자열을 커널 수준에서 출력하는 일을 한다.

 

우선적으로 해야 할 일은 리눅스 커널에서 각 시스템 호출마다 유일하다고 했던 시스템 호출 번호를 하나 새로 할당하고 해당 시스템 콜을 처리할 함수를 지정하는 일이다.

각 시스템 호출에게 유일한 시스템 호출 번호와 함수를 정의하고 있는 ~arch/x86/kernel/syscalls/syscall_64_tbl 파일에는 앞서 살펴본 바와 같이 316번 까지의 시스템 호출 번호가 할당되어 있었따. 새로 추가하는 시스템 호출 newsyscall()을 위해 317번을 할당하고 시스템 호출 테이블에 새로운 시스템 호출 처리 함수를 등록하여 방금 할당한 317번 번호를 인자로 sys_call_table이 접근될 때 호출할 함수를 등록한다. 그런 뒤, 정상적인 컴파일을 우해 ~/include/linux/syscalls.h 파일을 열어 sys_newsyscall의 함수 원형을 등록 시킨다.

--------------

Ubuntu 18.04.2 LTS 버전에서 하는방법을 못찾음. 중단

 

+ Recent posts