리눅스에서 프로그램을 작성하면서 장치를 제어하기 위해서는 커널에 요청을 해야 한다고만 알고 있었습니다. 즉, 커널의 시스템 함수를 호출하고 그 결과를 받을 뿐이지 직접 장치를 직접 제어하지 않는다 라는 것이죠.

다시 말씀드려 장치를 open 하고 write 하는 등의 행동은 일반 애플리케이션에서 직접 수행하지 않고 커널의 도움을 받아야 한다는 것인데, 이와 같은 방법은 어렴풋이 알고 있었고, 당연하다라고만 생각해 왔습니다. 리눅스가 멀티타스킹 OS 이기 때문에 장치를 완전히 하나의 애플리케이션에서 독점한다면야 모를까 여러 애플리케이션이 서로 사용하겠다고 장치를 주물럭거리면 위험하겠지요. 안정성을 위해서도 커널이 장치와 애플리케이션 사이에서 안전하게 중간자 역활을 해 주는 것이 좋을 것입니다. 그러나 어떻게요?

저는 이 부분을 도서관과 비교하고 싶습니다.

도서관에 있는 책들을 시스템에 설치된 장치라고 한다면 도서관에서 근무하시는 직원 분을 커널이라고 할 수 있습니다. 책을 빌리려면 담을 넘어서 도둑질하는 방법도 있겠습니다만 정상적인 분이라면 도서관 직원분에게 부탁하겠지요. 직원 덕분에 수많은 책들은 그때그때 관리가 되기 때문에 손님이 찾는 책이 있는지, 대출이 되어 지금은 없는 지를 바로 알 수 있습니다.

그러나 직원 없이 일반 사용자가 자유롭게 사용하게 한다면 이와 같이 일목요연하게 관리한다는 것은 어려울 것입니다. 때로는 한 권의 책을 가지고 여러 사람이 싸움을 일으킬 수도 있고 말이죠.

뭐, 당연한 것을 그림까지 그렸느냐 하실지 모르겠습니다. ^^;

그러나 과연 일반 애플리케이션이 어떻게 커널 시스템을 호출하느냐 하는 것이죠. 커널도 실행 중인 프로그램 프로세스입니다. 그 위에 실행되는 제가 만든 애플리케이션도 하나의 프로세스입니다.

애플리케이션 내에서 같이 작성된 user_fun()을 호출해 보겠습니다. 그러면 user_fun()의 루틴을 실행하는 빨강색 점선 화살표 진행을 어떤 프로세스가 실행하나요? 당연히 애플리케이션 프로세스가 진행합니다.

그렇다면 이번에는 커널이 제공하는 open()을 호출해 보겠습니다.

그렇다면 open() 함수의 진행은 누가할까요? 네, 그림에서 보듯이 커널 프로세스가 진행합니다. 조금 이상하지 않나요? 함수를 호출하는 것은 실행 위치를 변경하는 것으로 호출한 프로세스가 계속 그 작업을 해야 하는데, user_fun()은 애플리케이션 프로세스가 계속 수행하지만 커널 내에 있는 함수는 애플리케이션에서 처리하는 것이 아니라 커널 프로세스에 의해 처리됩니다.

어떻게요? 갑자기 프로세스의 주도권이 바뀌었습니다.

인터럽트

저도 책에서 이부분에 대한 설명을 읽었을 때에는 아! 소리가 절로 나오더군요. 바로 인터럽트를 이용합니다. 그럼으로서 애플리케이션 영역과 커널 영역을 완벽하게 분리(isolation)하도록 노력했다는 것을 알 수 있었습니다.

즉, 간단한 파일 하나 열더라도 애플리케이션은 커널에 파일 열기를 요청해야 하느데,

  1. 커널의 파일 열기 루틴을 직접 호출하는 것이 아니라 커널 쪽으로 인터럽틉를 발생시킨다는 것이죠.

  2. 커널은 작업 중에 인터럽트가 발생했으므로 그 인터럽트를 처리해 주고,

  3. 결과를 인터럽트를 발생시킨 애플리케이션에 넘겨 줍니다.

커널은 평상시 작업을 진행하다가 인터럽트가 발생하면 발생한 인터럽트 번호에 따라 지정된 함수를 호출하게 됩니다. 각 인터럽트에 따라서 어떤 함수를 호출할지 함수 주소 정보를 가지고 있는데, 이것을 IDT(Interrupt Descriptor Table) 이라고 하며, IDT를 통해 실행되는 루틴을 ISR(Interrupt Service Routine)이라고 합니다.

커널은 IDT로부터 ISR 루틴의 주소를 구하고 함수를 호출한 후, 작업이 끝나면 다시 원래 하던 일을 계속하게 됩니다.

그러므로 우리가 자주 사용하는 open() 함수는 파일을 열기 위한 프로그램 코드가 들어 있는 것이 아니라 커널 쪽으로 파일을 open할 수 있도록 인터럽트를 발생시키는 코드가 들어 있다라는 것이죠.

sys_call_table

이와 같이 커널의 함수를 호출하는 것을 간단히 시스템 호출이라고 하는데, 이 시스템 호출을 위해 사용하는 인터럽트 번호는 80h입니다. 또한 시스템 호출에는 open, read, write, close() 등 다양한 함수가 존재하므로 80h 인터럽트에 대해 어떤 작업을 해야할지를 CPU의 EAX 레지스트리에 그 번호를 넣어 줍니다.

이제 커널은 EAX 레지스트리에 넣어진 번호에 따라 처리할 함수를 알고 있어야 겠지요. 바로 sys_call_table로 함수 포인터를 모아놓은 테이블이라고 생각하시면 편합니다. sys_call_table에서 각 번호에 대한 정보는 /usr/include/asm/unistd.h 파일에 정의되어 있습니다.

#define __NR_restart_syscall  0
#define __NR_exit             1
#define __NR_fork             2
#define __NR_read             3
#define __NR_write            4
#define __NR_open             5
#define __NR_close            6
#define __NR_waitpid          7
#define __NR_creat            8

               :

#define __NR_add_key          286
#define __NR_request_key      287
#define __NR_keyctl           288

즉, 이 번호에 맞추어 sys_call_table이 아래와 같이 함수 포인터로 작성되어 있습니다.

sys_call_table
0 sys_restart_syscall
1 sys_exit
2 sys_fork
3 sys_read
4 sys_write
5 sys_open
:                     :

커널은 80h 인터럽트가 발생하면 EAX레지스트리 값으로 sys_call_table에서 실제 실행할 함수의 주소를 구하고 호출하게 됩니다.

조금더 자세히 말씀을 드리면 프로그램에서 파일을 열기 위해 open() 함수를 호출하면 open()함수는,

  1. sys_open()을 처리하기 위한 번호, 5번을 eax 레지스트리에 저장하고,
    인터럽트 80h 를 발생 시킵니다.
  2. CPU는 IDT에서 80h 인터럽트 처리 함수인 system_call() 을 호출합니다.
  3. system_call()함수는 open()함수가 미리 eax 레지스트리에 넣어 두었던 번호, 5를 가지고 sys_call_table 에서 sys_open() 함수의 주소를 구한 후,
  4. 이 함수를 호출합니다.

실제로는 이것보다 훨씬 더 복잡합니다만 대충적인 흐름을 이해하시는데 도움이 되시리라 생각됩니다. 이를 그림으로 다시 정리해 보았습니다.