노무현 대통령 배너


2006. 3. 28. 10:11

[본문스크랩] 리눅스 커널의 주요한 두 흐름

일반적으로 리눅스를 이용한 임베디드 시스템의 개발은 다음과 같은 순서로 진행된다.

1) 특정한 용도에 맞추어진 임베디드 하드웨어 설계 및 제작
2) 해당 임베디드 하드웨어에 리눅스 커널 포팅
3) 특정한 용도에 맞는 디바이스 드라이버의 개발
4) 이러한 디바이스를 접근해 적당한 작업을 수행할 응용프로그램 개발


특정한 디바이스를 접근하기 위해 작성한 디바이스 드라이버는 전 단계에서 포팅한 리눅스 커널의 일부가 된다. 따라서 우리는 우리가 작성한 디바이스 드라이버가 리눅스 커널에 어떻게 끼워지는지, 끼워진 후 어떤 흐름에 의해 동작을 하는지 알아야 한다.

일반적으로 리눅스 커널의 포팅은 커널의 내용을 자세히 몰라도 가능하지만 디바이스 드라이버의 개발은 포팅과는 전혀 다른 방향에서 접근해야 한다. 즉, 리눅스 커널의 흐름을 정확히 알아야 디바이스 드라이버의 정확한 개발이 가능하다.

리눅스 커널의 주요한 두 흐름


일반적으로 리눅스 커널로의 진입은 hardware interrupt와 system call에 의해서이다. 따라서 hardware interrupt에 의해 수행되는 routine과 system call에 의해 수행되는 routine의 구조와 흐름의 파악이 커널을 이해하는데 꼭 필요하다. 즉, 우리는 hardware interrupt routine과 system call routine을 이해함으로써 리눅스 커널의 대부분을 이해할 수 있다.

hardware interrupt routine

hardware interrupt routine은 디바이스 컨트롤러 - 예를 들어 이더넷 카드의 제어칩이나 하드디스크의 제어칩 - 의 물리적인 신호에 의해서 시작되는 routine이다. 그렇다면 이러한 신호는 왜 필요한가?

디바이스 컨트롤러는 이 신호를 이용해 밖에서 오는 데이터의 도착이나 밖으로 나갈 데이터를 모두 보냈음을 알린다.

예를 들어, 이더넷의 경우 밖에서 오는 데이터가 랜선을 통해 컨트롤러 안의 특정한 데이터 저장 영역에 도착하게 된다. 데이터가 정상적으로 도착할 경우 컨트롤러는 물리적인 인터럽트 신호를 통해서 CPU에 데이터의 도착을 알린다. CPU의 다음 동작은 칩내에 도착해 있는 데이터를 메모리로 읽어 가는 것이어야 한다. 이러한 CPU의 동작을 제어하는 루틴이 바로 인터럽트 핸들러의 한 부분이다.

이더넷을 통해 밖으로 데이터를 내보낼 경우도 보자. 먼저 CPU에 의해 메모리로부터 컨트롤러의 데이터 저장영역으로 데이터가 쓰여져야 하고, 다음으로 컨트롤러는 이 데이터를 랜선을 통해서 밖으로 내 보내야 한다. 컨트롤러가 랜선을 통해 데이터를 내 보내고 있는 동안에는 컨트롤러의 데이터 저장영역은 사용할 수가 없다. 컨트롤러는 데이터 저장영역에 있는 데이터를 밖으로 모두 내보내고 나면 인터럽트를 통해서 CPU 에 데이터 저장영역을 또 쓸 수 있음을 알린다.

지금까지 우리는 인터럽트의 필요성과 그에 따른 CPU의 동작을 보았다.

이상에서 hardware interrupt routine의 주요한 내용은 디바이스를 접근해서 도착한 데이터를 읽어오거나 또는 디바이스에 새로운 데이터를 쓰는 것이다.

다음으로 hardware interrupt routine의 구체적인 동작을 들여다 보자.

새로 도착한 데이터의 경우 일단은 디바이스로부터 메모리로 데이터를 읽어오는 동작이 있어야 하고, 다음으로 메모리로 읽어온 데이터를 적당한 프로세스에게 전달해 주어야 한다. 리눅스에서 앞의 동작을 보통 인터럽트 핸들러의 top half라 하고 뒤의 동작을 bottom half라 한다.

굳이 이렇게 인터럽트 핸들러를 두 부분으로 나눈 이유는 다음과 같다. top 부분에서는 신속하게 디바이스를 접근함으로써 디바이스가 빠른 시간 내에 다시 데이터를 받거나 하는 동작을 수행하게 한다. 보통 이 부분에서는 또 다른 인터럽트를 허용하지 않음으로써 이를 가능하게 한다. bottom half에서는 인터럽트를 열어놓음으로써 또 다른 인터럽트에 대한 응답성을 좋게 한다. 즉, top half에서는 디바이스에서 데이터를 읽어오는 작업을 하며, bottom half에서는 읽어온 데이터를 적절히 처리해 적당한 process에게 전달을 한다.

리눅스에서 hardware interrupt routine의 일반적인 흐름은 다음과 같다.




그림에서 CPU가 process 영역 수행 중에 hardware interrupt가 발생하면 CPU는 hardware interrupt routine으로 뛰어 들어간다. interrupt routine 내에서 routine 전후에 process 영역의 문맥을 저장하고 복구한다. do_IRQ 함수 내에 top half와 bottom half routine이 모두 포함되며 이 부분에서 인터럽트에 대한 처리를 한다.

timer interrupt에 의해 interrupt routine이 수행될 경우 현재 process의 time slice가 0 이 될 수 있으며, 이 경우 schedule 함수에서 새로운 process를 선택해서 그 process로 작업이 전환될 수도 있다.

do_signal 함수에서는 현재 process에게 전달된 signal이 있는지 확인하여 있을 경우에는 signal handler를 수행한다.


system call routine

다음은 system call routine을 보자.

system call routine은 process에 의해 시작되는 routine이다. i386 계열의 CPU의 경우 int(interrupt 의 약자)란 명령어, arm의 경우 swi(software interrupt의 약자)란 명령어, mips의 경우 syscall(system call의 약자)이란 명령어 등을 사용한다. 즉, process에 의해 진입하는 커널루틴을 system call routine 내지는 software interrupt routine이라고 한다.

이러한 system call은 process에서 커널을 접근하는 방법인데 그렇다면 왜 이러한 system call이 필요한가?

process는 system call을 통해서 process 영역의 데이터를 커널로 내려 보내기를 요청하거나 또는 커널의 데이터를 process 영역으로 가져오기를 요청한다. 즉, 디바이스가 인터럽트를 통해서 CPU가 디바이스로부터 데이터를 읽어 가거나 디바이스에 데이터를 쓰기를 요청하듯이 process는 system call을 통해서 커널에서 process의 데이터를 읽어가거나 process 영역으로 데이터를 써 주기를 요청한다. 다음의 예를 보자.

네트워크 통신을 하는 process의 경우 일반적으로 socket을 생성해서 그 socket에 데이터를 쓰거나 읽기를 반복한 후 그 socket을 닫음으로써 통신을 마친다. socket에 데이터를 쓰고자 할 경우 process는 쓰고자 하는 데이터를 만든 후 write 등의 system call 함수를 통해 커널영역으로 데이터를 보낸다. 커널영역에서는 이 데이터를 적당히 가공한 후에 디바이스 컨트롤러로 쓴 후에 process 영역으로 리턴한다. 또 socket으로부터 데이터를 읽고자 할 경우 process는 read 등의 system call 함수를 통해 커널로부터 데이터를 읽기를 요청한다. 커널영역에서는 이 프로세스의 socket을 접근해서 도착한 데이터 - 이 데이터는 hardware interrupt routine의 bottom half에 의해서 전달된다 - 가 있는지를 본 후 있으면 socket으로부터 데이터를 process 영역으로 읽어준 후 process 영역으로 리턴 한다. socket에 도착한 데이터가 없을 경우 현재 process의 진행을 임시 중단한 후 스케쥴링을 통해 다른 process를 수행한다. 흔히 말하는 sleep이니 wait니 blocking이니 하는 용어는 이러한 상황에서 쓰인다.

물론 process는 system call을 통해서 이외에도 다른 여러 가지 작업을 커널에 요청한다.

System call routine의 일반적인 흐름은 다음과 같다.




현재 process의 swi등의 명령어에 의해 수행되는 system call routine은 다른 부분은 hardware interrupt routine과 같으나 sys_func 함수 호출하는 부분이 다르다. 이 부분에서 커널영역의 여러 데이터를 건드릴 수 있다. 중간에 두 개의 줄 사이에서 interrupt를 허용한다. 따라서 이 부분에서 interrupt routine이 겹칠 수 있다. 이 부분에 대해서는 추후에 좀 더 다루고자 한다.

두 루틴간의 상호 작용

다음 그림을 보자.




이 그림의 왼쪽은 hardware interrupt routine이며 오른쪽은 software interrupt routine이다. 이처럼 두 루틴은 일반적으로 중간에 공유버퍼를 두고 데이터를 주고 받는다. 그리고 우리가 작성할 디바이스 드라이버는 이 두 루틴에 모두 포함된다.

마무리

이상에서 우리는 hardware interrupt routine과 system call routine의 일반적인 동작을 보았다. 이 두 routine에는 디바이스를 접근하는 routine이 포함되며, 이러한 디바이스를 접근하는 routine을 우리는 device driver라고 한다. 우리가 작성하는 device driver는 hardware interrupt routine과 system call routine에 모두 포함된다. 즉, 커널의 일부가 되어 해당 디바이스를 접근한다. 따라서 이 두 routine의 흐름을 알지 않고서, 즉 커널의 흐름을 알지 않고서 거기에 끼워 넣어질 device driver를 제대로 작성하기란 불가능하다.

저자: 서민우