Recommanded Free YOUTUBE Lecture: <% selectedImage[1] %>

리눅스 커널 모듈 프로그래밍 - 2.4

리눅스 커널 모듈 프로그래밍 - 2.4

윤 상배

dreamyun@yahoo.co.kr

교정 과정
교정 0.82003년 10월 15일 23시
최초 문서작성


1절. 소개

1.1절. 이 문서에 대해서

이 문서는 The Linux Kernel Module Programming Guide을 참고했으며, 많은 부분 원문을 그대로 번역하였다. 그러나 본문을 실제 테스트 하면서 내용이 미흡한 부분을 보완하였으며, 몇몇 틀린 부분에 대한 수정도 이루어졌다.


1.2절. 커널 모듈이란 ?

커널 모듈에 대해서 이해하고 프로그래밍을 하기 위해서 당연히 여러분은 C언어와 리눅스 시스템에 대한 기본적인 이해를 하고 있어야 한다. 이 문서는 리눅스(유닉스) 시스템과 C에 대한 기본 이해를 하고 있다는 가정하에 작성될 것이다.

커널 모듈이란 필요에 따라 커널에 로드하거나 언로드 할 수 있는 특정한 기능을 수행하는 코드(프로그램)이다. 이렇게 하므로써 쉽게 커널의 기능을 확장할 수 있을 뿐만 아니라 운영체제를 리부팅 하지 않고도 원하는 기능을 수행할 수 있도록 만들 수 있다.

예를 들어서 어떤 하드웨어를 제어하기 위한 문자 장치(device drive)를 작성해야 한다고 생각해보자. 만약 모듈기능을 제공하지 않는 커널이라면 커널을 직접수정하는 방식을 동원해서 커널에 필요한 기능을 추가시켜야 할 것이다. 프로그램 자체가 어려워지는 것은 물론이고 기능을 테스트 하기 위해서는 계속적인 리부팅 작업이 필요하게 되므로 개발기간 역시 극적으로 늘어날 수 밖에 없을 것이다. 또한 커널에 필요한 기능이 추가될 때마다 커널에 계속해서 코드가 추가 됨으로 커널의 크기도 매우 커지게 될것이다. 사운드카드를 위한 기능을 추가했는데 해당 사운드카드를 가지지 않는 유저도 있을 것이다. 이럴 경우는 그야말로 쓸데 없는 자원낭비가 되는 셈이다. ` 커널 모듈로써 작동하도록 만들었다면 쓸데없는 기능을 하는 모듈은 언로드 시키면 그만이다.

이 문서는 리눅스 커널 2.4를 기준으로 작성되었다.


2절. 커널 모듈 프로그래밍의 기본

2.1절. 커널에 모듈 적재시키기

현재 커널에서 작동중인 모듈의 목록은 lsmod 명령을 통해서 확인할 수 있다.

# lsmod
Module                  Size  Used by    Tainted: P  
via82cxxx_audio        18304   1  (autoclean)
uart401                 6560   0  (autoclean) [via82cxxx_audio]
ac97_codec              9504   0  (autoclean) [via82cxxx_audio]
sound                  59052   0  (autoclean) [via82cxxx_audio uart401]
soundcore               4324   4  (autoclean) [via82cxxx_audio sound]
autofs                 10948   0  (autoclean) (unused)
ne2k-pci                5568   1 
8390                    6736   0  [ne2k-pci]
ipchains               37704   0 
ide-scsi                8192   0 
scsi_mod               95848   1  [ide-scsi]
ide-cd                 27360   0  (autoclean)
cdrom                  28480   0  (autoclean) [ide-cd]
usb-uhci               21764   0  (unused)
usbcore                51744   1  [usb-uhci]
ext3                   61568   4 
			
lsmod는 /proc/modules 파일의 내용을 그대로 출력한다.

그럼 커널은 이러한 모듈을 어떻게 찾아서 적재시키는 걸까. 커널이 어떤 모듈을 포함하고자 할때 해당 모듈이 아직 커널에 적재되어 있지 않다면 모듈 데몬(daemon)인 kmodmodprobe를 실행시켜서 모듈을 읽어들이게 된다. 이때 modprobe는 다음중 하나의 방법을 이용해서 읽어들여야할 모듈을 찾게 된다.

  • softdog, ppp와 같은 모듈이름을 직접 찾는다.

  • char-major-10-30 과 같은 일반적인 식별자(generic identifier)를 이용한다.

만약 modprobe가 식별자를 이용할 경우, 해당 식별자에 대한 진짜 모듈이름을 알아와야 할것이다. 이에 대한 정보는 /etc/modules.conf에 저장되어 있다.

alias char-major-10-30 softdog
			
별칭목록을 확인함으로써 식별자를 위해서 softdog.o모듈을 적재시켜야 된다는 정보를 얻을 수 있게된다.

다음 modprobe는 /lib/modules/version/module.dep파일을 검사한다. 여기에는 해당모듈이 실행되기위해 필요한 다른 모듈들 즉 모듈의존성에 관한 정보들이 있어서 softdog.o를 적재하기 위해서 다른 모듈이 필요한지 확인하고 미리 적재시킨다. 이 파일은 depmod -a명령으로 생성시킬 수 있다. 예를 들어 msdos.o 모듈은 fat.o모듈이 우선적으로 적재되어 있어야만 한다. modprobe는 modeule.dep파일을 참조해서 의존성을 검사하게 된다.

마지막으로 modprobe는 insmod를 이용해서 원하는 모듈을 적재하기 위해서 우선적으로 필요한 모듈을 적재시키게 된다. insmod는 /lib/modules/version/을 직접참조해서 모듈을 적재한다. 최종적으로 여러분이 msdos 모듈을 올리기를 원한다면 다음과 같이 하면 된다.

# insmod /lib/modules/2.5.1/kernel/fs/fat/fat.o
# insmod /lib/modules/2.5.1/kernel/fs/msdos/msdos.o
			
그러나 위와 같이 할경우 모듈 의존성을 직접 검사해줘야 하는데, 이럴 경우 modprobe를 이용하면 된다.
 
# modprobe -a msdos
			

리눅스에서 사용되는 modprobe, insmod, depmod와 같은 프로그램은 modutils(혹은 mod-utils) 패키지에 포함된다.

그럼 /etc/modules.conf를 간략하게 살펴보고 이번장을 끝마치도록 하겠다.

# This file is automatically generated by update-modules

path[misc]=/lib/modules/2.4.?/local
alias eth0 ne2k-pci
alias eth1 ne2k-pci
			
'#'은 주석을 위해서 사용되며 공백라인은 무시된다.

path[misc]는 misc모듈을 찾을 경로의 지정을 위해서 사용된다.

alias 는 kmode가 식별자 eth0을 호출 했을 때 ne2k-pci를 호출하도록 한다. alias는 꽤 중요하게 사용될 수 있는데 하나의 시스템에 동일한 장치가 2개 이상 붙어 있을때 이를 식별할 수 있도록 해준다.


2.2절. 초간단 모듈제작 : Hello World

어떤 역사적인 이유가 있는지 모르겠지만 대부분 프로그래밍입문 을 하는데 있어서 가장 먼저 "Hello World"를 출력하는 코드를 장성하는 데에서 부터 시작한다. Hello World 출력 코드와 관련된 재미있는 글이 있는데 한번 읽어 보기 바란다. Hello World의 변천사

여기에서도 "Hello World"를 출력하는 코드를 만드는 것으로 모듈 프로그래밍으로의 발걸음을 내딛도록 하겠다. 이것은 매우 간단한 모듈인데, 아직 컴파일 방법을 다루지는 않을 것이다. 모듈 컴파일은 2.3절에서 다루도록 하겠다.

#include <linux/module.h>
#include <linux/kernel.h>

int init_module(void)
{
    printk("<1>Hello World 1.\n");
    return 0;
}

void cleanup_module(void)
{
    printk(KERN_ALERT "Goodbye world 1.\n");
}
			
커널모듈은 최소한 2개의 함수를 가지고 있어야만 한다. 하나는 init_module()라는 이름의 시작(초기화)함수로써 insmod에 의해서 커널로 적재될때 호출된다. 다른 하나는 cleanup_module()라는 이름의 종료함수로써 rmmod를 호출해서 모듈을 삭제할때 호출된다.


2.2.1절. printk()에 대해서

일반적으로 printk를 이용하면 (함수이름의 어감 때문에) 특정한 메시지를 표준출력할 것으로 생각하는 경우가 많은데 printk는 유저를 위한 어떤 출력도 하지 않는다. 이름과는 달리 로그나 경고 메시지를 남기기 위한 커널로깅 목적으로 사용된다.

일반적인 로그관련 라이브러리나 함수들이 그렇듯이 printk도 우선순위(priority)를 가진다. 모두 8단계의 우선순위를 가지며 <1> KERN_ALERT 와 같은 방식으로 결정할 수 있다. 이들 우선순위에 대한 선언정보는 linux/kernel.h에서 확인할 수 있다. 만약 우선순위를 정하기 귀찮거나 정할 수 없다면 기본 우선순위 DEFAULT_MESSAGE_LOGLEVEL을 사용하면 된다.

만약 syslogdklogd가 실행중이라면 메시지는 /var/log/messages에 추가 된다. 다음은 실제 저장된 로그들이다.

Oct  6 01:15:39 localhost kernel: Hello World 1.
Oct  6 01:16:11 localhost kernel: Goodbye world 1.
				


2.3절. 커널 모듈 컴파일 하기

커널 모듈을 컴파일하기 위해서는 특별한 gcc 옵션과 더불어 몇가지 값들의 정의(symbols define)가 필요하다. 이유는 커널모듈 컴파일시 사용되는 커널 헤더들이 커널버젼에 매우 의존적일 수 있기 때문이다.

이러한 정의는 gcc의 -D옵션을 이용하거나 혹은 #define 선행처리자를 이용하면 된다. 이번 장에서는 커널컴파일을 하기 위해서 필요한 내용들에 대해서 다룰 것이다.

  • -c : 커널모듈은 독립적으로 실행되지 않으며 (main함수 자체를 포함하고 있지 않다) object파일 형태로 커널에 링크되어서 실행된다. 결과적으로 -c 옵션을 이용해서 오브젝트 형태로 만들어 주어야 한다.

  • -O2 : 커널은 inline함수를 매우 많이 사용하며, 그런 이유로 모듈은 반드시 최적화(optimization) 옵션을 사용해서 컴파일 되어야 한다. 최적화 옵션을 사용하지 않을 경우 어셈블러 매크로등을 사용하는데 있어서 문제가 생길수 있다. 이럴경우 모듈의 적제가 실패하게 될것이다.

  • -D__KERNEL__ : 이 코드가 유저 프로세스가 아닌 커널모드에서 작동할 것이라는걸 커널헤더에 알려준다.

  • -W -Wall : 모듈 프로그램은 커널에 매우 민감한 영향을 끼칠 수 있으며 커널을 다운 시킬 수도 있다. 그러므로 가능한한 모든 종류의 경고메시지를 검사해야할 필요가 있다. 이 옵션을 사용하면 컴파일러가 발생시킬수 있는 모든 경고메시지를 출력한다.

  • -DMODULE : 커널모듈로 작성되는 코드라는걸 알려주기 위해서 사용한다.

이외에도 컴파일에 사용될 헤더파일을 찾기 위해서 -I대신에 -isystem을 사용하며 "unused varaiable"과 같은 경고 메시지의 출력을 위해서 -W -Wall을 이용할 것이다. -isystem은 gcc-3.x이상에서 지원되는 옵션이다.

참고: -isystem 도 -I 처럼 헤더파일의 경로 지정을 위해서 사용된다는 점에서 비슷하다. -I의 경우 표준 (헤더파일_시스템 경로를 검사하기 전에 -I로 지정된 경로를 먼저 검사하는 반면 -isystem은 가장 마지막에 지정된 경로에 대한 검사를 한다.

다음은 커널 모듈을 컴파일하기 위한 전형적인 Makefile이다.

TARGET  := hello
WARN    := -W -Wall -Wstrict-prototypes -Wmissing-prototypes
INCLUDE := -isystem /lib/modules/`uname -r`/build/include
CFLAGS  := -O2 -DMODULE -D__KERNEL__ ${WARN} ${INCLUDE}
CC      := gcc
	
${TARGET}.o: ${TARGET}.c

.PHONY: clean

clean:
    rm -rf ${TARGET}.o
			
쉽게 이해 가능할 것이다. make를 실행하면 hello.c를 컴파일하고 그결과 커널 모듈(오브젝트) 파일인 hello.o를 생성해낸다. 생성된 커널 모듈은 insmod ./hello.o를 통해서 적재 할 수 있다. 이걸로 당신은 최초의 커널 모듈작성에 성공했다. 예상외로 간단하지 않은가 ? 적재된 커널모듈은 rmmod hello로 제거할 수 있다. printk()출력은 /var/log/message에 쌓일 것이다. 확인해 보기 바란다.

2.2절에 있는 예제를 보면 init_module()에서 0을 리턴하고 있다. 그런데 다른 값을 리턴하도록 하면 어떻게 될까 ? 지금한번 테스트 해보기 바란다.


2.4절. Hello World 2

init함수와 cleanup함수의 이름이 반드시 init_module()와 cleanup_module()로 작성되어야 한다는 것은 (비록 혼동을 줄여주긴 하겠지만) 왠지 이치에 맞지 않는것 같다.

리눅스 커널 2.4부터는 이들 고정된 이름대신 다른 이름으로 사용가능하며, 이를 위해서 module_init()와 module_exit()함수를 제공한다.

예제 : hello_re.c

#include <linux/module.h>
#include <linux/tty.h>
#include <linux/init.h>
#include <linux/kernel.h>

int hello_init(void)
{
    printk(KERN_ALERT "HELLO, World\n");
    return 0;
}

void hello_exit(void)
{
    printk(KERN_ALERT "bye bye\n");
}

module_init(hello_init);
module_exit(hello_exit);
			


2.5절. Hello World 3 : 라이센스와 모듈에 대한 정보

커널모듈은 다른 시스템/유저프로그램에 비해 운영체제에 더욱민감한 영향을 끼칠 수 있다. 그런이유로 최소한 커널모듈에는 커널작성자에 대한 정보가 들어가도록 작성하는게 좋을 것이다.

리눅스 커널 2.4이상에서 지금까지 우리가 작성한 커널 모듈을 적재하려고 하면 다음과 경고 메시지를 출력할 것이다.

# insmod ./hello.o
Warning: loading ./hello.o will taint the kernel: no license
See http://www.tux.org/lkml/#export-tainted for information about tainted modules
			

참고: 라이센스정보관련 경고메시지 출력은 커널 옵션을 어떻게 주고 컴파일 했느냐에 따라 출력되지 않을 수도 있다. 몇몇 배포판의 경우 경고메시지가 출력되지 않을 것이다.

특히 많은 개발자들은 해당 모듈이 GPL(혹은 이와 비슷한)과 같은 공개된 라이스정책을 따르는지 그렇지 않은지에 대해서 민감할 수 있는데, MODULE_LICENSE() 매크로를 이용해서 라이센스를 명시할 수 있다. 이러한 라이센스에 대한 메커니즘은 linux/module.h에 정의되어 있다.

이와 비슷하게 MODULE_DESCRIPTION()과 MODULE_AUTHOR()매크로를 이용해서 모듈의 원저작자와 모듈에 대한 간단한 설명을 곁들일 수도 있다.

이러한 모든 매크로는 linux/module.h에 정의 되어있다. 이들 매크로 값들은 커널에 의해서 직접 이용되지는 않지만 objdump와 같은 도구를 이용할때 모듈에 대한 정보를 얻는데 도움을 준다.

# objdump -s hello_li.o 
...
 0000 6b65726e 656c5f76 65727369 6f6e3d32  kernel_version=2
 0010 2e342e32 30006c69 63656e73 653d4750  .4.20.license=GP
 0020 4c000000 00000000 00000000 00000000  L...............
 0030 00000000 00000000 00000000 00000000  ................
 0040 61757468 6f723d79 756e6472 65616d20  author=yundream 
 0050 3c79756e 64726561 6d406a6f 696e632e  <yundream@joinc.
 0060 636f2e6b 723e0064 65736372 69707469  co.kr>.descripti
 0070 6f6e3d41 2073696d 706c6520 64726976  on=A simple driv
 0080 65720064 65766963 653d7465 73746465  er.device=testde
 0090 76696365 00                          vice.           
			
다음은 이들 메크로를 포함시킨 예이다.

예제 : hello_li.c

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#define DRIVER_AUTHOR "yundream <yundream@joinc.co.kr>"
#define DRIVER_DESC   "A simple driver"

int init_hello_3(void);
void cleanup_hello_3(void);


static int init_hello_4(void)
{
   printk("<2>Hello, world 4\n");
   return 0;
}


static void cleanup_hello_4(void)
{
   printk("<2>Goodbye, world 4\n");
}


module_init(init_hello_4);
module_exit(cleanup_hello_4);

MODULE_LICENSE("GPL");

MODULE_AUTHOR(DRIVER_AUTHOR);
MODULE_DESCRIPTION(DRIVER_DESC);
MODULE_SUPPORTED_DEVICE("testdevice");
			


2.6절. 명령행 인자의 처리

커널 모듈도 명령행 인자를 받아들일 수 있다. 그러나 일반적으로 이용하는 argc/argv 기법을 사용할 수는 없다.

모듈로의 아규먼트 전달은 MODULE_PARM()매크로를 통해서 이루어진다. MODULE_PARM()매크로는 2개의 인자를 가진다. 첫 번째 인자는 값이 저장될 변수명이고, 두번째 인자는 저장될 데이터의 타입을 나타낸다. 데이터 타입은 "b" : 바이트, "h": short int, "i": integer, "l": long int, "s":string(문자열)가 있다. 문자열은 char * 타입이며 insmod로 호출될때 메모리가 할당된다. 다음은 간단한 활용예이다.

int myint = 3;
char *mystr;

MODULE_PARM (myint, "i");
MODULE_PARM (mystr, "s");
			

배열도 지원되는데, '-'를 이용해서 배열의 최소크기와 최대크기를 지정할 수 있다. 이는 주어질수 있는 인자의 최소와 최대 갯수를 정할 수 있음을 의미한다.

int myshortArray[4];
MODULE_PARM(myintArray, "2-4i");
			

이제 실제 모듈을 실행시키면서 인자를 넘기는 방법을 알아보도록 하자. 인자는 [변수명]=[값]의 형태로 넘어간다. 만약 모듈 코드상에 MODULE_PARM(myint, "i"); 로 되어 있다면 다음과 같은 방법으로 인자를 넘긴다.

# insmod ./hello.o myint=50
			
꽤나 독특한 방법으로 넘기고 있음을 알 수 있다.

배열의 경우에는 인자가 지정한 최대/최소의 범위를 벗어날 경우 에러메시지를 출력하며 모듈이 적재되지 않는다. 일반 애플리케이션에서 수행하는 argc를 통한 아규먼트 갯수 검사와 비슷한 형태라고 보면 된다. 배열의 각 요소는 ','를 통해서 구분되어 진다.

int myarray[4];

MODULE_PARM(myarray, "2-4");
			
와 같이 되어 있다고 할때, 다음과 같은 방법으로 값을 넘길 수 있다.
# insmod ./hello.o myarray=1,4,3
			
다음은 간단한 예제코드이다.

예제 : hello_arg.c

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
MODULE_LICENSE("GPL");
MODULE_AUTHOR("yundream@joinc.co.kr");

static short int myshort = 1;
static int myint = 420;
static long int mylong = 9999;
static char *mystring = "blah";

MODULE_PARM (myshort, "h");
MODULE_PARM (myint, "i");
MODULE_PARM (mylong, "l");
MODULE_PARM (mystring, "s");


static int __init hello_5_init(void)
{
   printk(KERN_ALERT "Hello, world 5\n=============\n");
   printk(KERN_ALERT "myshort is a short integer: %hd\n", myshort);
   printk(KERN_ALERT "myint is an integer: %d\n", myint);
   printk(KERN_ALERT "mylong is a long integer: %ld\n", mylong);
   printk(KERN_ALERT "mystring is a string: %s\n", mystring);
   return 0;
}


static void __exit hello_5_exit(void)
{
   printk(KERN_ALERT "Goodbye, world 5\n");
}


module_init(hello_5_init);
module_exit(hello_5_exit);
			


2.7절. 모듈별 분할 컴파일

보통 조금이라도 규모가 있는 시스템/유저 애플리케이션을 작성할 때는 소스의 관리를 위해서 함수/기능별로 소스를 분할해서 컴파일한다.

커널 모듈역시 이러한 분할 컴파일을 지원하는데, 아래의 형식을 따라주어야 한다.

  1. 모든 소스파일 혹은 하나 이상의 소스파일에 #define _NO_VERSION__ 이 포함되어 있어야 한다. 모듈 컴파일을 위해서 포함시키는 module.h 내에 커널 버젼정보가 포함되어 있으며 이 정보는 모듈에 전역적으로 사용되므로 _NO_VERSION__의 사용은 꽤나 중요해진다. 만약에 version.h를 직접 포함시켜야 되는 경우가 생긴다면 _NO_VERSION__을 정의하기 바란다. module.h에는 이게 정의되어 있지 않기 때문이다.

  2. 일반적인 방법으로 컴파일한다.

  3. 만들어진 여러개의 오브젝트파일을 하나로 만들어 줘야 한다. x86하에서는 d -m elf_i386 -r -o <module name.o> <1st src file.o> <2nd src file.o>

다음은 모듈분할 방식으로 작성된 커널 모듈 예제들이다.

예제 : start.c

#include <linux/kernel.h>       /* We're doing kernel work */
#include <linux/module.h>       /* Specifically, a module */

int init_module(void)
{
  printk("Hello, world - this is the kernel speaking\n");
  return 0;
}
			

예제 : stop.c

#if defined(CONFIG_MODVERSIONS) && ! defined(MODVERSIONS)
   #include <linux/modversions.h> /* Will be explained later */
   #define MODVERSIONS
#endif        
#include <linux/kernel.h>  /* We're doing kernel work */
#include <linux/module.h>  /* Specifically, a module  */
#define __NO_VERSION__     /* It's not THE file of the kernel module */
#include <linux/version.h> /* Not included by module.h because of
	                                      __NO_VERSION__ */
	
void cleanup_module()
{
   printk("<1>Short is the life of a kernel module\n");
}
			

다음은 컴파일을 위한 Makefile이다.

CC=gcc
MODCFLAGS := -O -Wall -DMODULE -D__KERNEL__
hello.o:    start.o stop.o
	ld -m elf_i386 -r -o hello.o start.o stop.o

start.o: start.c
	${CC} ${MODCFLAGS} -c start.c

stop.o: stop.c
	${CC} ${MODCFLAGS} -c stop.c