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_regioncdev_initcdev_addclass_createdevice_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_drivermodule_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_create API 변경 (커널 6.4+)
    • class_create(THIS_MODULE, name)class_create(name)으로 변경되었다
    • 커널 버전에 따라 컴파일 에러가 날 수 있다
  • 모듈 로드 순서
    • depmod가 모듈 간 의존성을 분석하고, modprobe가 의존성을 자동 로드한다
    • insmod는 단일 모듈만 로드. 의존성 해결을 하지 않는다