커널에 기능을 하나 추가하려고 매번 전체 커널을 재컴파일하고 재부팅해야 한다면 개발 사이클이 너무 느려진다. Loadable Kernel Module(LKM)은 이 문제를 insmod/rmmod만으로 런타임에 코드를 넣고 뺄 수 있게 해준다. 이 글에서는 최소 구성의 Hello World 모듈부터, 모듈 파라미터로 로드 시점 설정값을 받는 방법, /proc 인터페이스로 커널과 사용자 공간을 연결하는 방법까지 실제로 빌드·로드해서 정리한다.
모듈 구조와 빌드
커널 모듈은 module_init()/module_exit()로 등록한 초기화·정리 함수 한 쌍이 전부다. 빌드는 커널 소스 트리를 직접 쓰지 않고, 이미 설치된 커널 헤더의 Kbuild 시스템을 M= 옵션으로 호출하는 방식(out-of-tree 빌드)을 쓴다.
obj-m += hello.o
obj-m += paramtest.o
obj-m += procstat.o
KDIR := /lib/modules/$(shell uname -r)/build
all:
$(MAKE) -C $(KDIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KDIR) M=$(PWD) cleanobj-m에 나열한 이름마다 동명의 .c 파일이 .ko로 빌드된다. 헤더는 배포판 패키지(Ubuntu 기준 linux-headers-$(uname -r))로 설치돼 있어야 한다.
Hello World 모듈
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
MODULE_LICENSE("GPL");
MODULE_AUTHOR("junorion");
MODULE_DESCRIPTION("Minimal hello world kernel module");
static int __init hello_init(void)
{
pr_info("hello: module loaded (pid=%d)\n", current->pid);
return 0;
}
static void __exit hello_exit(void)
{
pr_info("hello: module unloaded\n");
}
module_init(hello_init);
module_exit(hello_exit);빌드 후 insmod으로 올리면 pr_info()가 커널 로그(dmesg)에 그대로 찍힌다.
$ make
CC [M] hello.o
LD [M] hello.ko
BTF [M] hello.ko
$ sudo insmod hello.ko
$ sudo dmesg | tail -3
[ 1557.729939] hello: loading out-of-tree module taints kernel.
[ 1557.729945] hello: module verification failed: signature and/or required key missing - tainting kernel
[ 1557.745068] hello: module loaded (pid=3991)
$ lsmod | grep hello
hello 12288 0
$ sudo rmmod hello
$ sudo dmesg | tail -1
[ 1573.567321] hello: module unloaded
서명되지 않은 out-of-tree 모듈이라 로드 시점에 두 가지 taint 경고가 함께 뜬다 — 실습 환경에서는 정상이며, 아래 주의사항에서 다시 짚는다.
모듈 파라미터로 로드 시점 값 전달하기
module_param()으로 선언한 변수는 insmod 명령줄 인자로 값을 받을 수 있고, 퍼미션 비트에 따라 /sys/module/<이름>/parameters/ 아래에서 조회·수정도 가능하다.
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/moduleparam.h>
MODULE_LICENSE("GPL");
static int count = 3;
module_param(count, int, 0644);
MODULE_PARM_DESC(count, "Number of times to print the greeting");
static char *greeting = "hello";
module_param(greeting, charp, 0444);
MODULE_PARM_DESC(greeting, "Greeting message to print");
static int __init paramtest_init(void)
{
int i;
for (i = 0; i < count; i++)
pr_info("paramtest: %s (%d/%d)\n", greeting, i + 1, count);
return 0;
}
static void __exit paramtest_exit(void)
{
pr_info("paramtest: unloaded\n");
}
module_init(paramtest_init);
module_exit(paramtest_exit);$ sudo insmod paramtest.ko greeting='hi-junorion' count=2
$ sudo dmesg | tail -2
[ 1580.251186] paramtest: hi-junorion (1/2)
[ 1580.251191] paramtest: hi-junorion (2/2)
$ cat /sys/module/paramtest/parameters/count
2
$ cat /sys/module/paramtest/parameters/greeting
hi-junorion
$ sudo rmmod paramtest
count는 0644 퍼미션이라 sysfs 파일로도 노출되지만, module_param() 자체는 런타임에 다시 쓴 값을 모듈 코드가 자동으로 반영하지는 않는다 — sysfs 쓰기를 실제로 반영하려면 param_set 콜백을 직접 구현해야 한다.
/proc 인터페이스로 커널-사용자 공간 연결하기
sysfs가 값 하나짜리 속성에 적합하다면, /proc은 seq_file을 통해 임의 형식의 텍스트를 만들어낼 수 있다. 커널 5.6부터 /proc 핸들러는 file_operations 대신 proc_ops 구조체를 쓴다는 점이 자주 헷갈리는 부분이다.
#include <linux/init.h>
#include <linux/module.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
MODULE_LICENSE("GPL");
#define PROC_NAME "kmoddemo"
static atomic_t read_count = ATOMIC_INIT(0);
static int procstat_show(struct seq_file *m, void *v)
{
atomic_inc(&read_count);
seq_printf(m, "reads so far: %d\n", atomic_read(&read_count));
return 0;
}
static int procstat_open(struct inode *inode, struct file *file)
{
return single_open(file, procstat_show, NULL);
}
static const struct proc_ops procstat_fops = {
.proc_open = procstat_open,
.proc_read = seq_read,
.proc_lseek = seq_lseek,
.proc_release = single_release,
};
static struct proc_dir_entry *procstat_entry;
static int __init procstat_init(void)
{
procstat_entry = proc_create(PROC_NAME, 0444, NULL, &procstat_fops);
if (!procstat_entry)
return -ENOMEM;
pr_info("procstat: /proc/%s created\n", PROC_NAME);
return 0;
}
static void __exit procstat_exit(void)
{
proc_remove(procstat_entry);
pr_info("procstat: /proc/%s removed\n", PROC_NAME);
}
module_init(procstat_init);
module_exit(procstat_exit);$ sudo insmod procstat.ko
$ cat /proc/kmoddemo
reads so far: 1
$ cat /proc/kmoddemo
reads so far: 2
$ cat /proc/kmoddemo
reads so far: 3
$ sudo rmmod procstat
$ sudo dmesg | tail -2
[ 1586.728730] procstat: /proc/kmoddemo created
[ 1586.771873] procstat: /proc/kmoddemo removed
읽을 때마다 read_count가 증가하는 걸로 알 수 있듯, cat은 파일을 매번 새로 open·read·close한다 — 상태를 유지하려면 모듈 전역 변수(여기서는 atomic_t)에 저장해야 한다.
주의사항
- kernel taint: 서명 안 된 out-of-tree 모듈을 로드하면
TAINT_OOT_MODULE·TAINT_UNSIGNED_MODULE비트가 켜진다(/proc/sys/kernel/tainted로 확인 가능). 배포판 문제 보고 시 taint된 커널의 버그 리포트는 우선순위가 낮게 취급되는 경우가 많다. - dmesg 읽기 권한:
kernel.dmesg_restrict=1(대부분의 배포판 기본값)이면 일반 사용자는dmesg를 읽을 수 없고sudo가 필요하다. - rmmod 실패:
lsmod출력 마지막 열이 0이 아니면 다른 모듈이나 열린 파일 핸들이 참조 중이라는 뜻이다 — 먼저 참조를 해제해야 언로드된다. - GPL 심볼:
MODULE_LICENSE("GPL")없이는EXPORT_SYMBOL_GPL()로 노출된 커널 심볼(예: 일부 트레이싱/락 관련 API)을 링크할 수 없다. - 커널 버전 차이:
/proc핸들러 구조체가 5.6 이전엔file_operations, 이후엔proc_ops다. LTS 배포판을 여러 버전 동시 지원해야 한다면 이 지점에서 컴파일이 깨지기 쉽다.
마무리
세 모듈 모두 Ubuntu 24.04(커널 7.0)에서 실제로 빌드·로드·언로드까지 확인했다. Hello World 수준을 넘어 실제 드라이버나 트레이싱 도구를 만들려면 여기서 다루지 않은 문자 디바이스(cdev), sysfs kobject, netlink 소켓 같은 인터페이스가 더 필요하지만, module_init/module_param/proc_ops 세 가지만으로도 커널과 사용자 공간을 잇는 기본 골격은 충분히 만들 수 있다.