Linux Kernel Module
LinuxEmbedded
커널 모듈은 실행 중인 커널에 동적으로 로드/언로드할 수 있는 코드다. 드라이버를 커널에 빌트인하지 않고도 하드웨어를 지원할 수 있다.
최소 모듈
#include <linux/module.h>
#include <linux/init.h>
static int __init my_init(void)
{
pr_info("module loaded\n");
return 0; /* 0 = 성공, 음수 = 에러 코드 */
}
static void __exit my_exit(void)
{
pr_info("module unloaded\n");
}
module_init(my_init);
module_exit(my_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("name");
MODULE_DESCRIPTION("description");
__init: 초기화 후 메모리에서 해제된다__exit: 커널에 빌트인되면 아예 컴파일에서 제외된다pr_info:printk(KERN_INFO ...)의 축약.dmesg에서 확인
Makefile
obj-m += my_module.o
# 여러 파일로 구성된 모듈
# my_driver-objs := main.o hw.o
# obj-m += my_driver.o
KDIR ?= /lib/modules/$(shell uname -r)/build
all:
$(MAKE) -C $(KDIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KDIR) M=$(PWD) clean
# 크로스 컴파일
cross:
$(MAKE) -C $(KDIR) M=$(PWD) ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- modules
make # 빌드
sudo insmod my_module.ko # 로드
lsmod | grep my_module # 확인
dmesg | tail # 커널 로그
sudo rmmod my_module # 언로드
modinfo my_module.ko # 모듈 정보
모듈 파라미터
로드 시 또는 런타임에 값을 전달할 수 있다.
#include <linux/moduleparam.h>
static int debug_level = 0;
static char *device_name = "default";
module_param(debug_level, int, 0644);
MODULE_PARM_DESC(debug_level, "Debug verbosity (0-3)");
module_param(device_name, charp, 0644);
MODULE_PARM_DESC(device_name, "Device name");
# 로드 시 전달
sudo insmod my_module.ko debug_level=2 device_name="sensor0"
# 런타임 변경 (퍼미션 0644이므로 가능)
echo 3 > /sys/module/my_module/parameters/debug_level
cat /sys/module/my_module/parameters/debug_level
세 번째 인자가 sysfs 퍼미션이다. 0이면 sysfs에 노출되지 않는다. 0644면 읽기/쓰기 가능.
캐릭터 디바이스
유저스페이스에서 /dev/mydev로 접근할 수 있는 디바이스를 만드는 패턴이다.
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#define DEVICE_NAME "mydev"
#define BUF_SIZE 256
static dev_t dev_num;
static struct cdev my_cdev;
static struct class *dev_class;
static char kbuf[BUF_SIZE];
static ssize_t dev_read(struct file *f, char __user *buf,
size_t count, loff_t *off)
{
size_t len = strlen(kbuf);
if (*off >= len) return 0;
if (*off + count > len) count = len - *off;
if (copy_to_user(buf, kbuf + *off, count)) return -EFAULT;
*off += count;
return count;
}
static ssize_t dev_write(struct file *f, const char __user *buf,
size_t count, loff_t *off)
{
if (count > BUF_SIZE - 1) count = BUF_SIZE - 1;
if (copy_from_user(kbuf, buf, count)) return -EFAULT;
kbuf[count] = '\0';
return count;
}
static struct file_operations fops = {
.owner = THIS_MODULE,
.read = dev_read,
.write = dev_write,
};
static int __init chardev_init(void)
{
if (alloc_chrdev_region(&dev_num, 0, 1, DEVICE_NAME) < 0)
return -1;
cdev_init(&my_cdev, &fops);
if (cdev_add(&my_cdev, dev_num, 1) < 0)
goto fail_cdev;
dev_class = class_create(DEVICE_NAME);
if (IS_ERR(dev_class))
goto fail_class;
if (IS_ERR(device_create(dev_class, NULL, dev_num, NULL, DEVICE_NAME)))
goto fail_device;
return 0;
fail_device:
class_destroy(dev_class);
fail_class:
cdev_del(&my_cdev);
fail_cdev:
unregister_chrdev_region(dev_num, 1);
return -1;
}
static void __exit chardev_exit(void)
{
device_destroy(dev_class, dev_num);
class_destroy(dev_class);
cdev_del(&my_cdev);
unregister_chrdev_region(dev_num, 1);
}
module_init(chardev_init);
module_exit(chardev_exit);
MODULE_LICENSE("GPL");
초기화 순서: alloc_chrdev_region → cdev_init → cdev_add → class_create → device_create. 해제는 정확히 역순이다.
# 테스트
echo "hello" > /dev/mydev
cat /dev/mydev # "hello"
Platform Driver
Device Tree 노드와 매칭되는 드라이버 패턴이다. 대부분의 임베디드 디바이스 드라이버가 이 구조를 따른다.
#include <linux/platform_device.h>
#include <linux/of.h>
static int my_probe(struct platform_device *pdev)
{
struct resource *res;
void __iomem *base;
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
base = devm_ioremap_resource(&pdev->dev, res);
if (IS_ERR(base))
return PTR_ERR(base);
dev_info(&pdev->dev, "probed, base=%p\n", base);
return 0;
}
static const struct of_device_id my_match[] = {
{ .compatible = "vendor,my-device" },
{ }
};
MODULE_DEVICE_TABLE(of, my_match);
static struct platform_driver my_drv = {
.probe = my_probe,
.driver = {
.name = "my-device",
.of_match_table = my_match,
},
};
module_platform_driver(my_drv);
MODULE_LICENSE("GPL");
module_platform_driver는 module_init + platform_driver_register를 한 줄로 줄여준다. Device Tree의 자세한 구조는 별도 문서에서 다룬다.
메모
copy_to_user/copy_from_user를 반드시 사용한다- 유저스페이스 포인터를 직접 역참조하면 커널 패닉.
__user어노테이션이 있는 포인터는 반드시 이 함수로 복사
- 유저스페이스 포인터를 직접 역참조하면 커널 패닉.
- 에러 처리와
goto cleanup- 커널 코드에서는
goto로 해제 경로를 역순 실행하는 것이 표준 패턴이다.goto fail_class;식으로 해당 시점까지의 리소스만 해제
- 커널 코드에서는
devm_함수를 쓰면 해제가 자동devm_kzalloc,devm_ioremap_resource,devm_request_irq등. 디바이스 제거 시 자동 해제- platform driver의 probe에서는 가능한 한
devm_계열을 쓴다
class_createAPI 변경 (커널 6.4+)class_create(THIS_MODULE, name)→class_create(name)으로 변경되었다- 커널 버전에 따라 컴파일 에러가 날 수 있다
- 모듈 로드 순서
depmod가 모듈 간 의존성을 분석하고,modprobe가 의존성을 자동 로드한다insmod는 단일 모듈만 로드. 의존성 해결을 하지 않는다