Linux 内核模块开发入门

Linux 内核模块开发入门

一 内核模块是什么

理论上的内核架构

单内核(monolithic kernel)

所有系统功能均运行于内核中, 在特权模式运行. Linux原来算是典型的单内核.

缺点: 添加新功能需要重新编译内核.

微内核 (micro kernel )

基本功能(进程间通信 [IPC]、调度、基本的输入/输出 [I/O] 和内存管理)运行于内核中,

其他功能(驱动程序、网络堆栈和文件系统)则运行于非特权模式.

Windows, OS/X 都是基于微内核设计. 缺点: 性能较低

类比一下, 比如dde-daemon 就是一个典型的单内核架构, 所有功能均静态编译到单个的二进制文件中.

实现上的内核架构

作为一个实用的内核系统, Linux自然不会被理论所束缚. 机智的引入了内核模块的概念.

所以, LKM虽然是单内核, 但是完全可以高效的实现微内核的特性.

PS: 实际上Windows也不算纯粹的微内核实现, 为了优化性能, 做了一些妥协, 将部分常用功能集成到内核中.

举个例子: dde-dock看起来就像实际上的内核实现, 基本功能通过dde-dock二进制实现, 附加功能功能
通过plugin机制实现.

内核的庐山真面目

ls /lib/modules/4.2.0-1-amd64/kernel/sound/
ac97_bus.ko  core  drivers  firewire  hda  i2c  isa  pci  pcmcia  soc  soundcore.ko  synth  usb

这些.ko文件就是内核模块. 这后缀有点眼熟. 有的像so文件.

file hello_klm.ko
hello_klm.ko: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), BuildID[sha1]=50ccef078b08697f6dcb4eab451b46dec3108522, not stripped

实际上.ko可以看做一个增强版的.so, 依旧是ELF格式文件, 不过是增加了.init.text /.exit.text/.modinfo 等区段

这些ko文件是怎么来的呢? 废话少说, 看源码:

#include 
#include 

int init_module() {
    printk(KERN_WARNING, "module init");
    return 0;
}

void cleanup_module() {
    printk(KERN_WARNING, "module cleanup");
}

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Iceyer");
MODULE_DESCRIPTION("Hello Linux Kernel Module");

写过Windows动态库(.DLL)的同学是不是看起来很熟悉, 这就是DLL的加载/卸载的两个入口函数.

内核在动态加载/卸载模块时会调用这两个接口.

二 Hello Linux Kernel Module

使用/proc文件

cat /proc/cpuinfo

sudo mkdir /proc/hello #failed

/proc是比较特殊的文件系统, 只能通过内核接口创建.

最小的内核模块就可以从尝试创建一个/proc文件开始

创建/proc文件

static int proc_show(struct seq_file *m, void *v) {
    seq_printf(m, "Hello Linux Kernel Module \n");
    return 0;
}

static int hello_proc_open(struct inode *inode, struct  file *file) {
    return single_open(file, proc_show, NULL);
}

static struct file_operations proc_ops = {
    .owner = THIS_MODULE,
    .open = hello_proc_open,
};

int init_module() {
    Proc_File = proc_create(PROCFS_NAME, 0644, NULL, &proc_ops);
    return 0;
}

void cleanup_module() {
    remove_proc_entry(PROCFS_NAME, NULL);
}

内核与用户空间通信

1 system call

高效, 标准, 稳定. 如果syscall能够满足需求, 尽量使用syscall.

缺点: system call是一个静态的函数表, 其内容是完全固定的, 无法进行自定义.

2 设备驱动 /dev/***

通过ioctl来访问内核空间资源, 基本上算是唯一选择.

禁止使用的方法:

在内核读写文件. 这是刚开始学习内核编程容易犯下的错误. 除非在进行一些文件系统相关
的开发工作. 不然你永远在自己的模块不应该调用vfs相关的接口.

Drawing

文件系统是用户态的概念, 内核在没有加载相应模块的时候是无法了解具体的文件系统类型的,
如(ntfs/ext4). 如果你在内核中读取/dev/sda5的内容, 那你是否能够读取成功就取决与
你的系统是否加载了/dev/sda5这个分区的文件系统模块. 否则是无法成功的.

简单的设备驱动

static int copy_user_buffer(const char* src, char* dst, const size_t max_len)
{
    size_t i  = 0;
    for (; i < max_len && src[i]; ++i) {
        put_user(src[i],  &dst[i]);
    }
    return i;
}

struct file_operations Fops = {
    .read = device_read,
    .unlocked_ioctl = device_ioctl,
    .open = device_open,
    .release = device_release,
};

long device_ioctl(struct file *file, unsigned int ioctl_num,     unsigned long ioctl_param) {
    buffer = (u8 *)ioctl_param;
    switch (ioctl_num) {
    case IOCTL_GET_USERNAME:
        copy_user_buffer("Iceyer", buffer, 5);
    }
    return 0;
}

内核直接执行用户空间命令

static int umh_wall( char *msg )
{
    char *argv[] = { "/usr/bin/wall", "-n", msg,  NULL };
    static char *envp[] = {
        "HOME=/",
        "TERM=linux",
        "PATH=/sbin:/bin:/usr/sbin:/usr/bin", NULL
    };
    return call_usermodehelper(argv[0], argv, envp, UMH_WAIT_PROC);
}

umh_wall("Your system will poweroff in 60s !!!!");

主要用于设备驱动/文件系统, 用于调用用户空间的可执行程序来对设备进行初始化等操作.

三 Dynamic Kernel Module Support(DKMS)

在Debian上, 简单来说, DKMS将源码打包成一个deb包, 每次升级内核是, 重新编译一个适配当前版本内核
的ko文件.

Drawing

如何使用DKMS保护一个非开源的模块.

预编译一个obj, DKMS编译的时候直接连接过去.

如果内核升级修改接口 会导致编译失败.

四 调试与帮助

基本调试手段

printk

官方拒绝支持类似gdb的调试工具, 据说Linus认为调试工具会给开发者错误的指引,
导致开发者通过程序的表象来分析错误而不是从代码逻辑上分析错误, 导致存在潜在问题.

所以, 最通用的调试手段就是printk了.

kdump

对于一些内核崩溃的情况, 可以考虑使用kdump来分析崩溃后core文件

kgdb

kgdb是非官方支持工具, 需要重新编译内核

四 调试与帮助

常用命令

insmod/rmmod
modprobe
modinfo

参考资料

The Linux Kernel Module Programming Guide

[Linux 可加载内核模块剖析] (http://www.ibm.com/developerworks/cn/linux/l-lkm/)

[Linux 内核剖析] (http://www.ibm.com/developerworks/cn/linux/l-linux-kernel/)

发表评论

电子邮件地址不会被公开。 必填项已用*标注