Linux 커널 모듈 개발 입문 — Hello World부터 모듈 파라미터, /proc 인터페이스까지

커널에 기능을 하나 추가하려고 매번 전체 커널을 재컴파일하고 재부팅해야 한다면 개발 사이클이 너무 느려진다. 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) clean

obj-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가 값 하나짜리 속성에 적합하다면, /procseq_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 세 가지만으로도 커널과 사용자 공간을 잇는 기본 골격은 충분히 만들 수 있다.

참고

답글 남기기