저자: 서민우
일반적으로 리눅스를 이용한 임베디드 시스템의 개발은 다음과 같은 순서로 진행된다.
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 를 수행한다.
리눅스 커널 2.6의 주요 구조와 응용 세미나
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 를 제대로 작성하기란 불가능하다.