현재 커널에서 작동중인 모듈의 목록은 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)인 kmod가 modprobe를 실행시켜서 모듈을 읽어들이게 된다. 이때 modprobe는 다음중 하나의 방법을 이용해서 읽어들여야할 모듈을 찾게 된다.
만약 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, 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개 이상 붙어 있을때 이를 식별할 수 있도록 해준다.
어떤 역사적인 이유가 있는지 모르겠지만 대부분 프로그래밍입문 을 하는데 있어서 가장 먼저 "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를 호출해서 모듈을 삭제할때 호출된다.
일반적으로 printk를 이용하면 (함수이름의 어감 때문에) 특정한 메시지를 표준출력할 것으로 생각하는 경우가 많은데 printk는 유저를 위한 어떤 출력도 하지 않는다. 이름과는 달리 로그나 경고 메시지를 남기기 위한 커널로깅 목적으로 사용된다.
일반적인 로그관련 라이브러리나 함수들이 그렇듯이 printk도 우선순위(priority)를 가진다. 모두 8단계의 우선순위를 가지며 <1> KERN_ALERT 와 같은 방식으로 결정할 수 있다. 이들 우선순위에 대한 선언정보는 linux/kernel.h에서 확인할 수 있다. 만약 우선순위를 정하기 귀찮거나 정할 수 없다면 기본 우선순위 DEFAULT_MESSAGE_LOGLEVEL을 사용하면 된다.
만약 syslogd와 klogd가 실행중이라면 메시지는 /var/log/messages에 추가 된다. 다음은 실제 저장된 로그들이다.
Oct 6 01:15:39 localhost kernel: Hello World 1.
Oct 6 01:16:11 localhost kernel: Goodbye world 1.
|
커널 모듈을 컴파일하기 위해서는 특별한 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을 리턴하고 있다. 그런데 다른 값을 리턴하도록 하면 어떻게 될까 ? 지금한번 테스트 해보기 바란다.
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.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");
|
커널 모듈도 명령행 인자를 받아들일 수 있다. 그러나 일반적으로 이용하는 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);
|
보통 조금이라도 규모가 있는 시스템/유저 애플리케이션을 작성할 때는 소스의 관리를 위해서 함수/기능별로 소스를 분할해서 컴파일한다.
커널 모듈역시 이러한 분할 컴파일을 지원하는데, 아래의 형식을 따라주어야 한다.
모든 소스파일 혹은 하나 이상의 소스파일에 #define _NO_VERSION__ 이 포함되어 있어야 한다. 모듈 컴파일을 위해서 포함시키는 module.h 내에 커널 버젼정보가 포함되어 있으며 이 정보는 모듈에 전역적으로 사용되므로 _NO_VERSION__의 사용은 꽤나 중요해진다. 만약에 version.h를 직접 포함시켜야 되는 경우가 생긴다면 _NO_VERSION__을 정의하기 바란다. module.h에는 이게 정의되어 있지 않기 때문이다.
일반적인 방법으로 컴파일한다.
만들어진 여러개의 오브젝트파일을 하나로 만들어 줘야 한다. 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
|