본문 바로가기

리눅스 커널 Linux kernel

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

 

1.인터럽트 처리과정

먼저 인터럽트란 주변 장치와 커널의 통신 방식으로 주변 장치나 CPU가 자신에게 발생한 사건을 리눅스 커널에게 알리는 메커니즘으로 외부 인터럽트와 트랩이 있습니다.

외부 인터럽트

현재 수행중인 태스크와 관련 없는 주변장치에서 발생된 비동기적 하드웨어 사건(비동기적이란 뜻은 언제 발생할지 그 시간을 정확히 알 수 없다는 뜻을 말합니다.)

트랩

동기적으로 발생하는 사건, 0으로 나누기, 세그멘테이션 폴트

인터럽트나 트랩이 발생하면, program counter를 정해진 특정 번지로 변경해 처리 루틴이 적절한 작업을 하게 됩니다.

리눅스는 외부 인터럽트와 트랩을 통일하게 처리합니다. 다양한 CPU에서 커널 내부구조의 수정 없이 인터럽트를 처리하기 위해 idt_table의 0~31까지는 트랩을 할당하고, 그 외 나머지 부분은 외부 인터럽트 핸들러를 위해 사용합니다.

 

트랩으로 사용되지 않는 외부 인터럽트 번호는 irq_desc테이블에 별도로 관리되고 이를 위해 리눅스 커널은 128을 제외한 32~255까지의 idt_table에 같은 인터럽트 핸들러 함수를 등록하였습니다. 이 함수는 do_IRQ() 함수를 호출하고 do_IRQ() 함수는 발생된 '외부 인터럽트' 번호를 가지고 irq_desc 테이블을 인덱싱하여 irq_desc_t 자료구조를 찾습니다. 이 자료구조 안의 action 리스트를 이용해 단일 인터럽트 라인을 공유하게 됩니다.

 

외부 인터럽트의 발생과정을 보면,

PC 환경에서 외부 인터럽트를 발생시킬 수 있는 주변 장치들은 하드웨어적으로 PIC라는 칩의 각 핀에 연결되어 있습니다. 외부 인터럽트가 발생되면 PIC를 통해 펄스 신호를 보내고 PIC는 수신한 신호를 적절한 번호로 변환하여 I/O포트에 저장합니다. CPU와 연결된 라인을 통해 외부 인터럽트가 발생했음을 알리고 PIC의 I/O포트를 읽어 외부 인터럽트의 벡터 번호를 확인해 알맞은 핸들러를 수행하는 방식으로 동작하게 됩니다.

 

외부 인터럽트 처리 루틴이 종료되면 커널은 인터럽트가 발생한 지점으로 돌아가야만 합니다. 이를 위해 외부 인터럽트 핸들러를 수행하기 전에 태스크가 어디까지 수행했는지를 기억해야 합니다.

 

트랩의 경우 트랩은 세 가지로 구분됩니다.

fault

fault를 일으킨 명령어 주소를 eip에 넣음

trap

trap을 일으킨 명령어의 다음 주소를 eip에 넣음. 시스템 콜

abort

심각한 에러로 eip저장 필요 없음. 태스크 강제종료

각각의 핸들러 수행 후 제어권을 넘겨야 하는데 이는 각 핸들러의 반환함수에서 일어납니다.

트랩

시스템콜

fork, vfork, clone

ret_from_fork()

ret_from_sys_call

ret_from_exception

외부 인터럽트

ret_from_intr

 

 

2.시스템 호출 처리 과정

시스템 호출이란 사용자 수준 응용들에게 커널이 자신의 서비스를 제공하는 인터페이스입니다. 결국 사용자가 운영체제의 기능이나 모듈을 활용하기 위해서는 반드시 시스템 호출을 사용해야 합니다. 시스템 호출을 커널로의 진입점이라고 생각할 수 있습니다. 각 시스템 호출들은 함수로 구현되어 있고 요청되었을 때 대응되는 함수를 호출해줍니다.

예를 들어 사용자 수준 응용 프로그램이 fork() 호출을 요청한다고 가정해봅시다. 이때 표준 C 라이브러리의 fork() 이름의 라이브러리 함수가 호출됩니다. 이는 트랩을 요청하는 대리격이라고 생각할 수 있습니다. 이 라이브러리 함수는 CPU내에 범용레지스터 중의 하나인 eax레지스터에 fork()함수에 할당되어 있는 고유번호 2를 넣고 0x80을 인자로 트랩을 겁니다. (eax 레지스터는 인텔의 경우를 말합니다.) 이렇게 트랩이 걸리면 제어권이 커널로 넘어가게 되고 CPU 모드가 사용자 수준에서 커널 수준으로 변화하게 됩니다. 커널은 현재 실행 중이던 태스크의 문맥을 저장하고 트랩 번호에 대응되는 엔트리에 등록되어 있는 함수를 호출합니다.

 

 

3.시스템 호출 구현

먼저 새로운 시스템 호출 번호를 부여합니다. 시스템 호출 번호가 저장되어 있는 경로를 찾아야 합니다. 참고로 현재 리눅스 3.10버전으로 진행하므로 버전에 따라 차이가 발생하는 것은 아실 것이라 믿습니다. 리눅스 소스가 들어있는 곳에서 arch/x86/syscalls/syscall_64.tbl 이 경로 아니면 syscall_64.tbl을 찾으시면 될 것 같네요. 64/32 둘 다 무관합니다. 다만 제가 64bit라서… 이 파일을 vim으로 여시고

위 그림과 같이 323번째 줄에 314번 sys_mycall이라는 새로운 시스템 호출을 등록합니다. 이로써 새로운 시스템 호출 번호가 등록되었습니다.

sys_mycall이 처리할 루틴을 등록해야 합니다. include/linux/syscalls.h 헤더를 수정하러 갑니다.

위에 것이 원래 그림으로 아래 그림의 844번째 줄에 직접 sys_mycall이 등록된 모습을 보실 수 있습니다. 이렇게 헤더에 호출할 함수를 넣었습니다.

이제 진짜 호출될 함수가 어떤 일을 할지를 작성해줍니다.

커널 소스 디렉토리 아래에

1.mysyscall/syscall.c 파일을 작성

2.mysyscall/Makefile 작성

필요한 파일들을 작성해줍니다.

그러고 나면 이제 커널의 Makefile을 수정해주어야 합니다. 커널의 Makefile은 커널 소스 디렉토리에 있습니다.

위의 것이 원래의 파일이고 아래가 수정한 결과입니다. 이렇게 수정해준 후

이제 정말 마지막으로 커널에 올라간 시스템 호출을 확인하기 위해 커널을 빌드하는 과정만을 남겨두었습니다.

커널 컴파일을 위해선 2~3시간 가량이 소요됩니다. 겨우 이거 하나 바꾸고 2~3시간이나 걸리다니, 생산성이 너무 떨어진다고 생각이 들었는데

처음에만 컴파일을 하는데 필요한 파일을 작성하느라 2~3시간이 걸린다고 합니다. 다음부터는 빨라진다고 하시네요.

또 커널 컴파일에 시간이 너무 오래 걸려 주로 모듈을 이용한다고 합니다.

컴파일이 종료되면, 실제 실행을 시켜봐야 합니다.

시스템 호출을 구현했으므로 어떤 디렉토리에서 위와 같은 코드를 실행시켜도 같은 동작을 합니다.

대신 syscall함수를 호출할 때 나오는 번호 314는 앞서 등록한 시스템 호출 번호여야 합니다.

이 파일을 저장하고 컴파일 해 실행시키면

저는 test.c에 코드를 넣고 위와 같이 컴파일 하였습니다.

이후 ./test1을 하시면 결과는 아무것도 나오지 않습니다. 여기서 dmesg라는 명령어로 커널의 로그를 보면

코드에서 준 인자 have a Funi today!!가 출력됨을 확인할 수 있습니다.