메뉴 바로가기 검색 및 카테고리 바로가기 본문 바로가기

한빛출판네트워크

한빛랩스 - 지식에 가능성을 머지하다 / 강의 콘텐츠 무료로 수강하시고 피드백을 남겨주세요. ▶︎

IT/모바일

리눅스 디바이스 드라이버 심플 소개(1)

한빛미디어

|

2007-08-20

|

by HANBIT

17,352

제공 : 한빛 네트워크
저자 : Valerie Henson
역자 : 노재현
원문 : /dev/hello_world: A Simple Introduction to Device Drivers under Linux

태초때부터 컴퓨터 언어를 배울때 제일 처음 만들었던 프로그램은 "Hello, World!"를 출력하는 프로그램이었습니다.(Hello World 모음 을 보면 300개가 넘는 각 언어로 만들어진 "Hello, World!" 프로그램을 볼 수 있어요). 이번 글에서도 마찬가지로 Hello, World를 시작으로 간단한 리눅스 커널 모듈을 만드는 방법을 배워보도록 할께요. 다음과 같은 총 3가지 방법을 이용해서 배워볼 거예요. printk(), /proc, /dev

준비 : 커널 모듈을 컴파일 하기위해서 준비해야할 사항

커널 모듈이란 작동중인 커널에 로딩되거나 언로딩 될 수 있도록 구성된 일종의 커널 코드라고 보시면 됩니다. 커널 모듈은 사실상 커널의 일부로서 실행이 되고 커널과 아주 가까운 영역에서 작동하기 때문에 컴파일을 하기 위해서는 커널의 코드가 필요하게 됩니다. 최소한 커널의 헤더 파일들과 설정 파일들이 컴파일 하는데 필요하게 됩니다. 물론 컴파일을 위해서 컴파일러는 기본적으로 필요하겠죠? 여기서는 데비안(Debian), 페도라(Fedora) 그리고 리눅스 커널의 소스 코드를 다운받아서 커널 모듈을 컴파일 하는데 필요한 환경을 준비해 보도록 하겠습니다.

커널 소스의 위치, 권한, 특권에 대한 설명 : 커널 소스는 보통 root 유저에게 권한이 주어진 /usr/src/linux 디렉토리에 위치합니다. 요즘엔 커널 소스를 root 유저가 아닌 유저의 홈 디렉토리에 저장하는 방법이 많이 권유되고 있기도 합니다. 이 글에서 사용되는 명령들은 모두 일반 유저로 실행이 되고, 특별히 root 유저의 권한이 필요할 때에만 sudo 명령을 이용해서 명령을 실행하도록 하고 있습니다. sudo 명령어를 설정하려면 sudo(8), visudo(8), sudoers(5) 맨페이지를 찾아보시면 됩니다.

데비안 환경에서 커널 모듈을 컴파일 하기 위해서 필요한 환경 설정하기

데비안에 있는 패키지 관리자를 이용하면 커널 모듈을 빌드 하기 위해서 필요한 패키지를 설치할 수 있습니다. 다음과 같은 명령으로 설치합니다.
$ sudo apt-get install module-assistant
간단합니다. 이제 커널 모듈을 컴파일 할 준비가 다 되었습니다. 더 자세한 내용을 알고 싶으신 독자는 데비안 리눅스 커널 핸드북 을 참고하시면 좋겠습니다.

페도라에서 설정하기

페도라에 있는 커널 개발 패키지를 설치하면 커널 모듈 개발에 필요한 모든 커널 헤더와 툴이 설치되게 됩니다. 다음과 같이 실행해 주세요.
$ sudo yum install kernel-devel 
간단하죠? 더 자세한 내용은 페도라 릴리즈 노트 에서 확인해 주세요.

리눅스 소스코드를 받아서 설정하기

먼저 커널을 설정하고 컴파일 한 후에 설치를 해야 합니다. 그리고 재부팅을 해서 커널을 시작하면 됩니다. 하지만 이 과정은 위의 과정들에 비해서 다소 어려운 편이고 여기서는 간단하게만 살펴보도록 하겠습니다.

리눅스 소스코드는 http://kernel.org 에 있고, 최신 안정판 소스 코드의 링크가 첫페이지에 링크되어 있습니다. 가장 최신 소스 코드를 다운 받습니다. 예를 들어 현재 안정판 최신 버전의 소스 코드는 다음의 위치에 있습니다. http://kernel.org/pub/linux/kernel/v2.6/linux-2.6.21.5.tar.bz2. 빠른 속도로 다운 받기위해서는 http://kernel.org/mirrors/ 에서 가장 가까운 미러 서버를 찾으면 됩니다. 가장 쉬운 다운로드 방법은 wget 이라는 유틸리티를 이용하는 것입니다.
$ wget -c http://kernel.org/pub/linux/kernel/v2.6/linux-.tar.bz2 
커널 소스의 압축을 해제합니다.
$ tar xjvf linux-.tar.bz2 
이제 커널의 소스코드가 linux-/ 디렉토리에 저장되어 있을 것입니다. 커널 소스가 있는 디렉토리로 이동해서 다음과 같이 환경설정을 시작합니다.
$ cd linux- 
$ make menuconfig  
커널 소스를 컴파일 하고 설치하기 위한 make의 타겟들이 굉장히 많이 있는데요. 어떤 타겟을 사용할 수 있을지 확인하려면 다음과 같이 입력해 봅니다.
$ make help 
대부분의 배포본에서 작동하는 타겟은 다음과 같습니다.
$ make tar-pkg 
빌드가 끝난 후에 커널을 설치하려면 다음과 같이 입력합니다.
$ sudo tar -C / -xvf linux-.tar 
그리고 나서 소스 코드 트리가 있는 디렉토리로 심볼릭 링크를 생성합니다.
$ sudo ln -s  /lib/modules/"uname -r"/build 
이제 커널 모듈을 컴파일 하기 위한 준비가 끝났습니다. 더 진행하기 전에 새로 재부팅해서 새로 빌드된 커널을 시작하도록 합니다.

printk()를 이용한 "Hello, World"

첫 번째 모듈에서는 커널에 있는 기능을 이용해서 시작해 보도록 하겠습니다. printk를 이용해서 "Hello, world!" 를 출력하게 될 것인데요, 이 printk 함수는 커널에서의 printf 함수라고 보시면 됩니다. printk 함수는 메시지를 커널의 메시지 버퍼에 저장했다가 /var/log/messages 파일에 복사하게 됩니다.(syslogd 의 설정에 따라 조금씩 다를 수도 있습니다)

hello printk module tarball 파일을 다운받은 후에 다음과 같이 압축을 해제합니다.
$ tar xzvf hello_printk.tar.gz
압축을 해제 하면 2개의 파일이 있습니다: 하나는 Makefile로 모듈을 컴파일하는데 필요한 규칙들을 기술하는데 사용되고, hello_printk.c 파일은 모듈의 소스 코드입니다. 우선 Makefile을 먼저 보도록 하지요.
obj-m := hello_printk.o 
obj-m 은 커널 모듈을 만들기 위해서 빌드 해야하는 파일들의 리스트가 됩니다. ".o" 파일은 ".c"확장자를 가지는 소스 코드로 부터 생성되는 오브젝트 파일이라고 알고 계시면 됩니다.
KDIR := /lib/modules/$(shell uname -r)/build 
KDIR은 커널 소스의 저장위치를 나타냅니다.
PWD := $(shell pwd) 
PWD는 현재 디렉토리를 나타내며, 우리가 만들고 있는 커널 모듈의 코드가 있는 위치이기도 합니 다.
default: 
    $(MAKE) -C $(KDIR) M=$(PWD) modules 
default는 기본 타겟명령으로서 make 명령을 실행했을 때 인자로 다른 타겟을 지정해 주지 않는다면 default 타겟에 있는 명령이 실행되게 됩니다. default 타겟에 있는 명령은 make를 커널 소스가 있는 디렉토리를 작업 디렉토리로 지정하고 실행을 하되 $(PWD)에 있는 모듈만 컴파일을 하라고 하는 것입니다. 위와 같이 명령을 이용하면 커널 소스 트리에 설정된 모듈 컴파일 규칙들을 이용할 수가 있게 됩니다.

그럼 이제 hello_printk.c 파일을 보겠습니다.
#include 
#include  
위 두 줄은 커널 모듈을 컴파일 하는데 필요한 헤더파일을 포함하고 있습니다. 위 파일을 포함시키면 나중에 보겠지만 module_init() 매크로 등이 포함되게 됩니다.
static int __init 
hello_init(void) 
{ 
    printk("Hello, world!n"); 
    return 0; 
} 
static void __exit
hello_exit(void)
{
        printk("Goodbye, world!n");
}

module_exit(hello_exit);
다음은 모듈의 초기화 함수로서 모듈이 처음 로딩되었을때 실행되는 함수입니다. __init 키워드는 커널에게 이 함수가 딱 한 번만 실행될 것이라는 걸 알려주는 역할을 하게 됩니다. printk는 전에 말했듯이 커널 메시지 버퍼에 "Hello, world!"라는 문자열을 저장하는 역할을 하게 됩니다. printk 함수에서의 포맷팅은 대부분 printf 함수와 동일합니다.
module_init(hello_init); 
module_init 매크로는 커널에게 모듈이 로딩되었을때 호출되어야 하는 함수를 알려주는 역할을 합니다. 그리고 모듈이 하게 되는 모든 일들이 바로 이 초기화 함수가 호출하는 함수에 의해서 처리가 되게 됩니다.
static void __exit 
hello_ext(void) 
{ 
    printk("Goodbye, world!n"); 
} 
  
module_ext(hello_exit); 
마찬가지로 exit 함수도 한 번만 실행이 되고 module_exit 매크로를 이용해서 exit 함수를 지정해 줄 수 있습니다. __exit 키워드가 커널에게 모듈의 언로딩 시에 이 함수를 한 번만 호출해야 한다는 것을 알려주는 역할을 합니다.
MODULE_LICENSE("GPL"); 
MODULE_AUTHOR("valerie Henson "); 
MODULE_DESCRIPTION(""Hello, world!" minimal module"); 
MODULE_VERSION("printk"); 
MODULE_LICENSE 매크로는 커널에게 모듈이 어떤 라이센스 하에서 커널을 이용하게 되는지를 알려주는 역할을 합니다. 사용하는 라이센스에 따라서 사용할 수 있는 심볼(함수 혹은 변수 등)에 제한이 생기게 됩니다. GPLv2 라이센스를 선택했다면(지금 우리가 선택한 것처럼) 커널 내의 모든 심볼을 사용할 수 있습니다. 특정 라이센스를 가진 모듈 혹은 MODULE_LICENSE를 지정하지 않아서 non-GPLv2로 라이센스가 설정된 커널은 불안정 할 수도 있기때문에 이런 커널에서 발생된 버그는 리포팅을 해도 커널 개발자들에 의해서 수정되지 않을 수 있습니다. 나머지 MODULE_로 시작하는 매크로들은 모듈에 대한 정보를 제공하는 용도로 사용됩니다.

자 이제 모듈을 컴파일 해서 코드를 실행해 보겠습니다. 모듈이 있는 디렉토리로 이동해서 다음과 같이 컴파일을 합니다.
$ cd hello_printk 
$ make 
컴파일이 다 되었으면 insmod 명령을 이용해서 모듈을 로딩합니다. 그리고 나서 모듈이 출력하는 메시지를 보기 위해서 dmesg 명령을 이용해 보겠습니다.
$ sudo insmod ./hello_printk.ko 
$ dmesg | tail 
제대로 모듈이 로딩되었다면 다음과 같은 "Hello, world!" 메시지를 볼 수 있습니다. 이제 모듈을 커널에서 언로딩 해보겠습니다. rmmod 명령을 이용해서 하고 마찬가지로 dmesg로 메시지를 보겠습니다.
$ sudo rmmod hello_printk 
$ dmesg | tail  
성공적으로 컴파일과 실행을 해 보았습니다.

/proc 파일 시스템을 이용한 Hello, World!

커널과 유저 프로그램의 소통에서 가장 쉽고 또 가장 많이 사용하는 방법은 /proc 파일 시스템을 이용하는 것입니다. /proc 파일 시스템은 가상의 파일 시스템으로써 파일에서 어떤 내용을 읽게 되면 커널에 있는 값을 읽을 수 있고, 파일에 쓰여진 데이터는 커널의 의해서 사용이 될 수 있습니다. /proc 파일을 더 설명하기 전에 말씀드릴게 있는데, 모든 유저프로그램과 커널과의 소통은 시스템 콜을 통해서 이루어지게 된다는 것입니다. 시스템 콜을 이용한다는 것은 즉 이미 만들어져 있는 시스템 콜 중에서 필요한 시스템 콜을 찾아서 사용한다거나 새로운 시스템 콜을 만들거나(이 경우 시스템 콜 번호를 추가하고 커널이 수정되어야 한다), 아니면 특수한 파일을 만들어서 ioctl() 함수를 이용하는 것을 뜻한다(복잡하고 버그의 위험성이 크다). /proc 파일 시스템을 이용하면 정해진 방법을 통해서 커널과 유저 프로그램이 데이터를 교환할 수 있고, 모듈이 작동하기 위해 충분한 기능을 제공 받을 수 있습니다.

여기서는 /proc 파일 시스템에 파일을 하나 만들고 이 파일을 읽었을때 "Hello, world!"를 리턴할 수 있도록 해볼 것입니다. 파일 이름은 /proc/hello_world로 할 것입니다. 우선 hello_proc module tarball 을 다운로드 받아서 압축을 풀고 hello_proc.c 파일부터 보도록 하겠습니다.
#include  
#include 
#include  
우선 proc_fs 에 대한 헤더 파일을 포함하고 있는데, 이 파일을 포함하면 /proc 파일 시스템을 등록하는데 필요한 기능을 사용할 수 있게 됩니다.

다음 함수는 /proc 파일 시스템에 있는 파일을 읽었을때 호출되게 되는 함수입니다. 여기서는 단순히 "Hello, world!" 리턴하는 기능만 넣게 될 것이기 때문에 일반적인 read 함수의 구현에 비하면 굉장히 간단합니다.
static int 
hello_read_proc(char *buffer, char **start, off_t offset, int size, int *eof, 
void *data) 
{ 
이 함수에서 사용하고 있는 인자에 대해서 설명을 좀 해보도록 하겠습니다. buffer 는 커널 버퍼에 대한 포인터로써 여기에 우리가 출력하고 싶은 메시지를 출력하게 될 것입니다. start는 지금과 같은 간단한 모듈에서는 사용되지 않기 때문에 건너뛰도록 하겠습니다. offset은 파일의 어느 위치부터 읽기 시작할지를 나타내는데 여기서는 0만 허용하도록 하겠습니다. size 는 버퍼의 크기를 나타내며 이 변수를 이용해서 버퍼에 쓸때 버퍼의 크기 이상으로 쓰지 않도록 하겠습니다. eof는 end of file의 줄임말입니다. data 역시 start와 마찬가지로 여기서는 사용되지 않습니다.

자 이제 함수의 내용을 보겠습니다.
        char *hello_str = "Hello, world!n";
        int len = strlen(hello_str); /* 비어있는 문자열인지 확인하기 위해서 길이를 계산한다. */
        /*
         * 전체 스트링을 한 번에 읽어들이는 연산만 허용한다.
         */
        if  
이제 /proc 파일 시스템에 모듈을 등록할 차례입니다. 
static int __init
hello_init(void)
{
        /*
         * /proc 디렉토리 안에 "hello_world"라는 파일을 만들고 파일을 읽었을때 hello_read_proc() 함수를
         * 호출하도록 합니다.
         */
        if (create_proc_read_entry("hello_world", 0, NULL, hello_read_proc,
                                    NULL) == 0) {
                printk(KERN_ERR
                       "Unable to register "Hello, world!" proc filen");
                return -ENOMEM;
        }

        return 0;
}

module_init(hello_init);
/proc 파일 시스템에서 제거하는 함수도 만들어 봅시다.(이 함수를 만들어 주지 않으면 모듈이 언로딩 되고 나서 /proc/hello_world에 누군가가 접근을 하게 될 경우 커널에 치명적인 오류를 유발시킬 수 있습니다)
static void __exit
hello_exit(void)
{
        remove_proc_entry("hello_world", NULL);
}

module_exit(hello_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Valerie Henson ");
MODULE_DESCRIPTION(""Hello, world!" minimal module");
MODULE_VERSION("proc");
이제 모듈을 컴파일 해서 로딩할 준비가 되었습니다. 
  
$ cd hello_proc 
$ make 
$ sudo insmod ./hello_proc.ko 
이제 다음과 같이 /proc/hello_world라는 파일이 생성되었습니다.
$ cat /proc/hello_world 
Hello, world! 
이제 더 많은 /proc 파일들을 만들어 볼 수 있을 것입니다. 이것보다 더 진보된 모듈을 만들어보고 싶으신 분은 seq_file 함수를 이용해서 /proc 파일에 쓰는 기능을 만들 수 있습니다. 더 자세한 내용은 Driver porting: The seq_file interface 를 참고해주세요.


역자 노재현님은 어렸을 때부터 컴퓨터를 접하게 된 덕에 프로그래밍을 오랫동안 정겹게 하고 있는 프로그래머 입니다. 특히나 게임 및 OS 개발에 관심이 많으며, 심심할 때면 뭔가 새로운 프로그램을 만들어내는 것을 좋아합니다. 다음에서 웹 관련 개발을 한 후에 현재는 www.osguru.net이라는 OS관련 웹사이트를 운영하며 넥슨에서 게임 개발을 하고 있습니다.
* e-mail: wonbear@gmail.com
* homepage: http://www.oguru.net
TAG :
댓글 입력
자료실

최근 본 상품0