发布:20140603 Write your first Linux Kernel module

@GOLinux @disylee 明早发布~
累死我了,花了3+小时才校对完。最终译文综合了你们二位的成果,谢谢。可能最终认真读这篇文章的不会很多,但是这才是真正有价值的技术文章啊。
This commit is contained in:
wxy 2014-06-24 01:30:39 +08:00
parent 5a90103907
commit d02585cd6d
3 changed files with 370 additions and 864 deletions

View File

@ -0,0 +1,370 @@
黑客内核编写属于你的第一个Linux内核模块
================================================================================
> 曾经多少次想要在内核游荡?曾经多少次茫然不知方向?你不要再对着它迷惘,让我们指引你走向前方……
内核编程常常看起来像是黑魔法,而在亚瑟 C 克拉克的眼中它八成就是了。Linux内核和它的用户空间是大不相同的抛开漫不经心你必须小心翼翼因为你编程中的一个bug就会影响到整个系统。浮点运算做起来可不容易堆栈固定而狭小而你写的代码总是异步的因此你需要想想并发会导致什么。而除了所有这一切之外Linux内核只是一个很大的、很复杂的C程序它对每个人开放任何人都去读它、学习它并改进它而你也可以是其中之一。
学习内核编程的最简单的方式也许就是写个内核模块一段可以动态加载进内核的代码。模块所能做的事是有限的——例如他们不能在类似进程描述符这样的公共数据结构中增减字段LCTT译注可能会破坏整个内核及系统的功能。但是在其它方面他们是成熟的内核级的代码可以在需要时随时编译进内核这样就可以摒弃所有的限制了。完全可以在Linux源代码树以外来开发并编译一个模块这并不奇怪它称为树外开发如果你只是想稍微玩玩而并不想提交修改以包含到主线内核中去这样的方式是很方便的。
在本教程中,我们将开发一个简单的内核模块用以创建一个**/dev/reverse**设备。写入该设备的字符串将以相反字序的方式读回“Hello World”读成“World Hello”。这是一个很受欢迎的程序员面试难题当你利用自己的能力在内核级别实现这个功能时可以使你得到一些加分。在开始前有一句忠告你的模块中的一个bug就会导致系统崩溃虽然可能性不大但还是有可能的和数据丢失。在开始前请确保你已经将重要数据备份或者采用一种更好的方式在虚拟机中进行试验。
### 尽可能不要用root身份 ###
> 默认情况下,**/dev/reverse**只有root可以使用因此你只能使用**sudo**来运行你的测试程序。要解决该限制,可以创建一个包含以下内容的**/lib/udev/rules.d/99-reverse.rules**文件:
>
> SUBSYSTEM=="misc", KERNEL=="reverse", MODE="0666"
>
> 别忘了重新插入模块。让非root用户访问设备节点往往不是一个好主意但是在开发其间却是十分有用的。这并不是说以root身份运行二进制测试文件也不是个好主意。
#### 模块的构造 ####
由于大多数的Linux内核模块是用C写的除了底层的特定于体系结构的部分所以推荐你将你的模块以单一文件形式保存例如reverse.c。我们已经把完整的源代码放在GitHub上——这里我们将看其中的一些片段。开始时我们先要包含一些常见的文件头并用预定义的宏来描述模块
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Valentine Sinitsyn <valentine.sinitsyn@gmail.com>");
MODULE_DESCRIPTION("In-kernel phrase reverser");
这里一切都直接明了,除了**MODULE\_LICENSE()**它不仅仅是一个标记。内核坚定地支持GPL兼容代码因此如果你把许可证设置为其它非GPL兼容的“Proprietary”[专利]),某些特定的内核功能将在你的模块中不可用。
### 什么时候不该写内核模块 ###
> 内核编程很有趣,但是在现实项目中写(尤其是调试)内核代码要求特定的技巧。通常来讲,在没有其它方式可以解决你的问题时,你才应该在内核级别解决它。以下情形中,可能你在用户空间中解决它更好:
> - 你要开发一个USB驱动 —— 请查看[libusb][1]。
> - 你要开发一个文件系统 —— 试试[FUSE][2]。
> - 你在扩展Netfilter —— 那么[libnetfilter\_queue][3]对你有所帮助。
> 通常,内核里面代码的性能会更好,但是对于许多项目而言,这点性能丢失并不严重。
由于内核编程总是异步的,没有一个**main()**函数来让Linux顺序执行你的模块。取而代之的是你要为各种事件提供回调函数像这个
static int __init reverse_init(void)
{
printk(KERN_INFO "reverse device has been registered\n");
return 0;
}
static void __exit reverse_exit(void)
{
printk(KERN_INFO "reverse device has been unregistered\n");
}
module_init(reverse_init);
module_exit(reverse_exit);
这里,我们定义的函数被称为模块的插入和删除。只有第一个的插入函数是必要的。目前,它们只是打印消息到内核环缓冲区(可以在用户空间通过**dmesg**命令访问);**KERN\_INFO**是日志级别(注意,没有逗号)。**\_\_init**和**\_\_exit**是属性 —— 联结到函数或者变量的元数据片。属性在用户空间的C代码中是很罕见的但是内核中却很普遍。所有标记为**\_\_init**的会在初始化后释放内存以供重用还记得那条过去内核的那条“Freeing unused kernel memory…[释放未使用的内核内存……]”信息吗?)。**\_\_exit**表明,当代码被静态构建进内核时,该函数可以安全地优化了,不需要清理收尾。最后,**module\_init()**和**module\_exit()**这两个宏将**reverse\_init()**和**reverse_exit()**函数设置成为我们模块的生命周期回调函数。实际的函数名称并不重要,你可以称它们为**init()**和**exit()**,或者**start()**和**stop()**,你想叫什么就叫什么吧。他们都是静态声明,你在外部模块是看不到的。事实上,内核中的任何函数都是不可见的,除非明确地被导出。然而,在内核程序员中,给你的函数加上模块名前缀是约定俗成的。
这些都是些基本概念 - 让我们来做更多有趣的事情吧。模块可以接收参数,就像这样:
# modprobe foo bar=1
**modinfo**命令显示了模块接受的所有参数,而这些也可以在**/sys/module//parameters**下作为文件使用。我们的模块需要一个缓冲区来存储参数 —— 让我们把这大小设置为用户可配置。在**MODULE_DESCRIPTION()**下添加如下三行:
static unsigned long buffer_size = 8192;
module_param(buffer_size, ulong, (S_IRUSR | S_IRGRP | S_IROTH));
MODULE_PARM_DESC(buffer_size, "Internal buffer size");
这儿我们定义了一个变量来存储该值封装成一个参数并通过sysfs来让所有人可读。这个参数的描述最后一行出现在modinfo的输出中。
由于用户可以直接设置**buffer\_size**,我们需要在**reverse\_init()**来清除无效取值。你总该检查来自内核之外的数据 —— 如果你不这么做,你就是将自己置身于内核异常或安全漏洞之中。
static int __init reverse_init()
{
if (!buffer_size)
return -1;
printk(KERN_INFO
"reverse device has been registered, buffer size is %lu bytes\n",
buffer_size);
return 0;
}
来自模块初始化函数的非0返回值意味着模块执行失败。
### 导航 ###
> 但你开发模块时Linux内核就是你所需一切的源头。然而它相当大你可能在查找你所要的内容时会有困难。幸运的是在庞大的代码库面前有许多工具使这个过程变得简单。首先是Cscope —— 在终端中运行的一个比较经典的工具。你所要做的,就是在内核源代码的顶级目录中运行**make cscope && cscope**。Cscope和Vim以及Emacs整合得很好因此你可以在你最喜爱的编辑器中使用它。
> 如果基于终端的工具不是你的最爱,那么就访问[http://lxr.free-electrons.com][4]吧。它是一个基于web的内核导航工具即使它的功能没有Cscope来得多例如你不能方便地找到函数的用法但它仍然提供了足够多的快速查询功能。
现在是时候来编译模块了。你需要你正在运行的内核版本头文件(**linux-headers**,或者等同的软件包)和**build-essential**或者类似的包。接下来该创建一个标准的Makefile模板
obj-m += reverse.o
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
现在,调用**make**来构建你的第一个模块。如果你输入的都正确在当前目录内会找到reverse.ko文件。使用**sudo insmod reverse.ko**插入内核模块,然后运行如下命令:
$ dmesg | tail -1
[ 5905.042081] reverse device has been registered, buffer size is 8192 bytes
恭喜了!然而,目前这一行还只是假象而已 —— 还没有设备节点呢。让我们来搞定它。
#### 混杂设备 ####
在Linux中有一种特殊的字符设备类型叫做“混杂设备”或者简称为“misc”。它是专为单一接入点的小型设备驱动而设计的而这正是我们所需要的。所有混杂设备共享同一个主设备号10因此一个驱动(**drivers/char/misc.c**)就可以查看它们所有设备了,而这些设备用次设备号来区分。从其他意义来说,它们只是普通字符设备。
要为该设备注册一个次设备号(以及一个接入点),你需要声明**struct misc\_device**,填上所有字段(注意语法),然后使用指向该结构的指针作为参数来调用**misc\_register()**。为此,你也需要包含**linux/miscdevice.h**头文件:
static struct miscdevice reverse_misc_device = {
.minor = MISC_DYNAMIC_MINOR,
.name = "reverse",
.fops = &reverse_fops
};
static int __init reverse_init()
{
...
misc_register(&reverse_misc_device);
printk(KERN_INFO ...
}
这儿我们为名为“reverse”的设备请求一个第一个可用的动态的次设备号省略号表明我们之前已经见过的省略的代码。别忘了在模块卸下后注销掉该设备。
static void __exit reverse_exit(void)
{
misc_deregister(&reverse_misc_device);
...
}
fops字段存储了一个指针指向一个**file\_operations**结构在Linux/fs.h中声明而这正是我们模块的接入点。**reverse\_fops**定义如下:
static struct file_operations reverse_fops = {
.owner = THIS_MODULE,
.open = reverse_open,
...
.llseek = noop_llseek
};
另外,**reverse\_fops**包含了一系列回调函数(也称之为方法),当用户空间代码打开一个设备,读写或者关闭文件描述符时,就会执行。如果你要忽略这些回调,可以指定一个明确的回调函数来替代。这就是为什么我们将**llseek**设置为**noop\_llseek()**,(顾名思义)它什么都不干。这个默认实现改变了一个文件指针,而且我们现在并不需要我们的设备可以寻址(这是今天留给你们的家庭作业)。
#### 关闭和打开 ####
让我们来实现该方法。我们将给每个打开的文件描述符分配一个新的缓冲区并在它关闭时释放。这实际上并不安全如果一个用户空间应用程序泄漏了描述符也许是故意的它就会霸占RAM并导致系统不可用。在现实世界中你总得考虑到这些可能性。但在本教程中这种方法不要紧。
我们需要一个结构函数来描述缓冲区。内核提供了许多常规的数据结构链接列表双联的哈希表树等等之类。不过缓冲区常常从头设计。我们将调用我们的“struct buffer”
struct buffer {
char *data, *end, *read_ptr;
unsigned long size;
};
**data**是该缓冲区存储的一个指向字符串的指针,而**end**指向字符串结尾后的第一个字节。**read_ptr**是**read()**开始读取数据的地方。缓冲区的size是为了保证完整性而存储的 —— 目前,我们还没有使用该区域。你不能假设使用你结构体的用户会正确地初始化所有这些东西,所以最好在函数中封装缓冲区的分配和收回。它们通常命名为**buffer\_alloc()**和**buffer\_free()**。
static struct buffer *buffer_alloc(unsigned long size)
{
struct buffer *buf;
buf = kzalloc(sizeof(*buf), GFP_KERNEL);
if (unlikely(!buf))
goto out;
...
out:
return buf;
}
内核内存使用**kmalloc()**来分配,并使用**kfree()**来释放;**kzalloc()**的风格是将内存设置为全零。不同于标准的**malloc()**,它的内核对应部分收到的标志指定了第二个参数中请求的内存类型。这里,**GFP_KERNEL**是说我们需要一个普通的内核内存不是在DMA或高内存区中以及如果需要的话函数可以睡眠重新调度进程。**sizeof(*buf)**是一种常见的方式,它用来获取可通过指针访问的结构体的大小。
你应该随时检查**kmalloc()**的返回值访问NULL指针将导致内核异常。同时也需要注意**unlikely()**宏的使用。它(及其相对宏**likely()**)被广泛用于内核中,用于表明条件几乎总是真的(或假的)。它不会影响到控制流程,但是能帮助现代处理器通过分支预测技术来提升性能。
最后,注意**goto**语句。它们常常为认为是邪恶的但是Linux内核以及一些其它系统软件采用它们来实施集中式的函数退出。这样的结果是减少嵌套深度使代码更具可读性而且非常像更高级语言中的**try-catch**区块。
有了**buffer\_alloc()**和**buffer\_free()****open**和**close**方法就变得很简单了。
static int reverse_open(struct inode *inode, struct file *file)
{
int err = 0;
file->private_data = buffer_alloc(buffer_size);
...
return err;
}
**struct file**是一个标准的内核数据结构,用以存储打开的文件的信息,如当前文件位置(**file->f\_pos**)、标志(**file->f\_flags**),或者打开模式(**file->f\_mode**)等。另外一个字段**file->privatedata**用于关联文件到一些专有数据它的类型是void *,而且它在文件拥有者以外,对内核不透明。我们将一个缓冲区存储在那里。
如果缓冲区分配失败,我们通过返回否定值(**-ENOMEM**来为调用的用户空间代码标明。一个C库中调用的**open(2)**系统调用(如 **glibc**)将会检测这个并适当地设置**errno** 。
#### 学习如何读和写 ####
“read”和“write”方法是真正完成工作的地方。当数据写入到缓冲区时我们放弃之前的内容和反向地存储该字段不需要任何临时存储。**read**方法仅仅是从内核缓冲区复制数据到用户空间。但是如果缓冲区还没有数据,**revers\_eread()**会做什么呢?在用户空间中,**read()**调用会在有可用数据前阻塞它。在内核中你就必须等待。幸运的是有一项机制用于处理这种情况就是wait queues
想法很简单。如果当前进程需要等待某个事件,它的描述符(**struct task_struct**存储current信息被放进非可运行睡眠中状态并添加到一个队列中。然后**schedule()**就被调用来选择另一个进程运行。生成事件的代码通过使用队列将等待进程放回**TASK\_RUNNING**状态来唤醒它们。调度程序将在以后在某个地方选择它们之一。Linux有多种非可运行状态最值得注意的是**TASK\_INTERRUPTIBLE**(一个可以通过信号中断的睡眠)和**TASK\_KILLABLE**(一个可被杀死的睡眠中的进程)。所有这些都应该正确处理,并等待队列为你做这些事。
一个用以存储读取等待队列头的天然场所就是结构缓冲区,所以从为它添加**wait\_queue\_head_t read\_queue**字段开始。你也应该包含**linux/sched.h**头文件。可以使用DECLARE\_WAITQUEUE()宏来静态声明一个等待队列。在我们的情况下,需要动态初始化,因此添加下面这行到**buffer\_alloc()**
init_waitqueue_head(&buf->read_queue);
我们等待可用数据;或者等待**read\_ptr != end**条件成立。我们也想要让等待操作可以被中断通过Ctrl+C。因此“read”方法应该像这样开始
static ssize_t reverse_read(struct file *file, char __user * out,
size_t size, loff_t * off)
{
struct buffer *buf = file->private_data;
ssize_t result;
while (buf->read_ptr == buf->end) {
if (file->f_flags & O_NONBLOCK) {
result = -EAGAIN;
goto out;
}
if (wait_event_interruptible
(buf->read_queue, buf->read_ptr != buf->end)) {
result = -ERESTARTSYS;
goto out;
}
}
...
我们让它循环,直到有可用数据,如果没有则使用**wait\_event\_interruptible()**(它是一个宏,不是函数,这就是为什么要通过值的方式给队列传递)来等待。好吧,如果**wait\_event\_interruptible()**被中断它返回一个非0值这个值代表**-ERESTARTSYS**。这段代码意味着系统调用应该重新启动。**file->f\_flags**检查以非阻塞模式打开的文件数:如果没有数据,返回**-EAGAIN**。
我们不能使用**if()**来替代**while()**,因为可能有许多进程正等待数据。当**write**方法唤醒它们时,调度程序以不可预知的方式选择一个来运行,因此,在这段代码有机会执行的时候,缓冲区可能再次空出。现在,我们需要将数据从**buf->data** 复制到用户空间。**copy\_to\_user()**内核函数就干了此事:
size = min(size, (size_t) (buf->end - buf->read_ptr));
if (copy_to_user(out, buf->read_ptr, size)) {
result = -EFAULT;
goto out;
}
如果用户空间指针错误,那么调用可能会失败;如果发生了此事,我们就返回**-EFAULT**。记住,不要相信任何来自内核外的事物!
buf->read_ptr += size;
result = size;
out:
return result;
}
为了使数据在任意块可读,需要进行简单运算。该方法返回读入的字节数,或者一个错误代码。
写方法更简短。首先,我们检查缓冲区是否有足够的空间,然后我们使用**copy\_from\_userspace()**函数来获取数据。再然后**read\_ptr**和结束指针会被重置,并且反转存储缓冲区内容:
buf->end = buf->data + size;
buf->read_ptr = buf->data;
if (buf->end > buf->data)
reverse_phrase(buf->data, buf->end - 1);
这里, **reverse\_phrase()**干了所有吃力的工作。它依赖于**reverse\_word()**函数,该函数相当简短并且标记为内联。这是另外一个常见的优化;但是,你不能过度使用。因为过多的内联会导致内核映像徒然增大。
最后,我们需要唤醒**read\_queue**中等待数据的进程,就跟先前讲过的那样。**wake\_up\_interruptible()**就是用来干此事的:
wake_up_interruptible(&buf->read_queue);
耶!你现在已经有了一个内核模块,它至少已经编译成功了。现在,是时候来测试了。
### 调试内核代码 ###
> 或许,内核中最常见的调试方法就是打印。如果你愿意,你可以使用普通的**printk()** (假定使用**KERN\_DEBUG**日志等级。然而那儿还有更好的办法。如果你正在写一个设备驱动这个设备驱动有它自己的“struct device”可以使用**pr\_debug()**或者**dev\_dbg()**:它们支持动态调试(**dyndbg**)特性,并可以根据需要启用或者禁用(请查阅**Documentation/dynamic-debug-howto.txt**)。对于单纯的开发消息,使用**pr\_devel()**除非设置了DEBUG否则什么都不会做。要为我们的模块启用DEBUG请添加以下行到Makefile中
> CFLAGS_reverse.o := -DDEBUG
>
> 完了之后,使用**dmesg**来查看**pr_debug()**或**pr_devel()**生成的调试信息。
> 或者,你可以直接发送调试信息到控制台。要想这么干,你可以设置**console_loglevel**内核变量为8或者更大的值**echo 8 /proc/sys/kernel/printk**),或者在高日志等级,如**KERN_ERR**,来临时打印要查询的调试信息。很自然,在发布代码前,你应该移除这样的调试声明。
> 注意内核消息出现在控制台不要在Xterm这样的终端模拟器窗口中去查看这也是在内核开发时建议你不在X环境下进行的原因。
### 惊喜,惊喜! ###
编译模块,然后加载进内核:
$ make
$ sudo insmod reverse.ko buffer_size=2048
$ lsmod
reverse 2419 0
$ ls -l /dev/reverse
crw-rw-rw- 1 root root 10, 58 Feb 22 15:53 /dev/reverse
一切似乎就位。现在,要测试模块是否正常工作,我们将写一段小程序来翻转它的第一个命令行参数。**main()**(再三检查错误)可能看上去像这样:
int fd = open("/dev/reverse", O_RDWR);
write(fd, argv[1], strlen(argv[1]));
read(fd, argv[1], strlen(argv[1]));
printf("Read: %s\n", argv[1]);
像这样运行:
$ ./test 'A quick brown fox jumped over the lazy dog'
Read: dog lazy the over jumped fox brown quick A
它工作正常!玩得更逗一点:试试传递单个单词或者单个字母的短语,空的字符串或者是非英语字符串(如果你有这样的键盘布局设置),以及其它任何东西。
现在,让我们让事情变得更好玩一点。我们将创建两个进程,它们共享一个文件描述符(及其内核缓冲区)。其中一个会持续写入字符串到设备,而另一个将读取这些字符串。在下例中,我们使用了**fork(2)**系统调用而pthreads也很好用。我也省略打开和关闭设备的代码并在此检查代码错误又来了
char *phrase = "A quick brown fox jumped over the lazy dog";
if (fork())
/* Parent is the writer */
while (1)
write(fd, phrase, len);
else
/* child is the reader */
while (1) {
read(fd, buf, len);
printf("Read: %s\n", buf);
}
你希望这个程序会输出什么呢?下面就是在我的笔记本上得到的东西:
Read: dog lazy the over jumped fox brown quick A
Read: A kcicq brown fox jumped over the lazy dog
Read: A kciuq nworb xor jumped fox brown quick A
Read: A kciuq nworb xor jumped fox brown quick A
...
这里发生了什么呢?就像举行了一场比赛。我们认为**read**和**write**是原子操作,或者从头到尾一次执行一个指令。然而,内核确实无序并发的,随便就重新调度了**reverse\_phrase()**函数内部某个地方运行着的写入操作的内核部分。如果在写入操作结束前就调度了**read()**操作呢就会产生数据不完整的状态。这样的bug非常难以找到。但是怎样来处理这个问题呢
基本上,我们需要确保在写方法返回前没有**read**方法能被执行。如果你曾经编写过一个多线程的应用程序你可能见过同步原语如互斥锁或者信号。Linux也有这些但有些细微的差别。内核代码可以运行进程上下文用户空间代码的“代表”工作就像我们使用的方法和终端上下文例如一个IRQ处理线程。如果你已经在进程上下文中和并且你已经得到了所需的锁你只需要简单地睡眠和重试直到成功为止。在中断上下文时你不能处于休眠状态因此代码会在一个循环中运行直到锁可用。关联原语被称为自旋锁但在我们的环境中一个简单的互斥锁 —— 在特定时间内只有唯一一个进程能“占有”的对象 —— 就足够了。处于性能方面的考虑,现实的代码可能也会使用读-写信号。
锁总是保护某些数据在我们的环境中是一个“struct buffer”实例而且也常常会把它们嵌入到它们所保护的结构体中。因此我们添加一个互斥锁struct mutex lock到“struct buffer”中。我们也必须用**mutex\_init()**来初始化互斥锁;**buffer\_alloc**是用来处理这件事的好地方。使用互斥锁的代码也必须包含**linux/mutex.h**。
互斥锁很像交通信号灯 —— 要是司机不看它和不听它的,它就没什么用。因此,在对缓冲区做操作并在操作完成时释放它之前,我们需要更新**reverse\_read()**和**reverse\_write()**来获取互斥锁。让我们来看看**read**方法 —— **write**的工作原理相同:
static ssize_t reverse_read(struct file *file, char __user * out,
size_t size, loff_t * off)
{
struct buffer *buf = file->private_data;
ssize_t result;
if (mutex_lock_interruptible(&buf->lock)) {
result = -ERESTARTSYS;
goto out;
}
我们在函数一开始就获取锁。**mutex\_lock\_interruptible()**要么得到互斥锁然后返回,要么让进程睡眠,直到有可用的互斥锁。就像前面一样,**\_interruptible**后缀意味着睡眠可以由信号来中断。
while (buf->read_ptr == buf->end) {
mutex_unlock(&buf->lock);
/* ... wait_event_interruptible() here ... */
if (mutex_lock_interruptible(&buf->lock)) {
result = -ERESTARTSYS;
goto out;
}
}
下面是我们的“等待数据”循环。当获取互斥锁时,或者发生称之为“死锁”的情境时,不应该让进程睡眠。因此,如果没有数据,我们释放互斥锁并调用**wait\_event\_interruptible()**。当它返回时,我们重新获取互斥锁并像往常一样继续:
if (copy_to_user(out, buf->read_ptr, size)) {
result = -EFAULT;
goto out_unlock;
}
...
out_unlock:
mutex_unlock(&buf->lock);
out:
return result;
最后,当函数结束,或者在互斥锁被获取过程中发生错误时,互斥锁被解锁。重新编译模块(别忘了重新加载),然后再次进行测试。现在你应该没发现毁坏的数据了。
### 接下来是什么? ###
现在你已经尝试了一次内核黑客。我们刚刚为你揭开了这个话题的外衣里面还有更多东西供你探索。我们的第一个模块有意识地写得简单一点在从中学到的概念在更复杂的环境中也一样。并发、方法表、注册回调函数、使进程睡眠以及唤醒进程这些都是内核黑客们耳熟能详的东西而现在你已经看过了它们的运作。或许某天你的内核代码也将被加入到主线Linux源代码树中 —— 如果真这样,请联系我们!
--------------------------------------------------------------------------------
via: http://www.linuxvoice.com/be-a-kernel-hacker/
译者:[GOLinux](https://github.com/GOLinux) [disylee](https://github.com/disylee) 校对:[wxy](https://github.com/wxy)
本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创翻译,[Linux中国](http://linux.cn/) 荣誉推出
[1]:http://www.libusb.org/
[2]:http://fuse.sf.net/
[3]:http://www.linuxvoice.com/be-a-kernel-hacker/www.netfilter.org/projects/libnetfilter_queue
[4]:http://lxr.free-electrons.com/

View File

@ -1,369 +0,0 @@
编写属于你的第一个Linux内核模块
================================================================================
> 曾经多少次想要在内核游荡?曾经多少次茫然不知方向?你不要再对着它迷惘,让我们指引你走向前方……
内核编程常常看起来像是黑魔法,而在亚瑟 C 克拉克的眼中它八成就是了。Linux内核和它的用户空间是大不相同的抛开漫不经心你必须小心翼翼因为你编程中的一个bug就会影响到整个系统。浮点数学做起来可不容易堆栈固定而渺小而你写的代码总是异步的因此你需要想想怎样让它并发。而除了所有这一切之外Linux内核只是一个很大的、很复杂的C程序它对每个人开放任何人都去读它、学习它并改进它而你也可以是其中之一。
> “开始内核编程的最简单的方式
> 是写模块——一段代码
> 可以用来动态加载进内核。”
可能开始内核编程的最简单的方式就是写模块——一段可以动态加载进内核并从内核移除的代码。模块所能做的事是有限的——例如他们不能添加或移除像进程描述符这样的常规数据结构域。但是在其它方面他们是成熟的内核级的代码可以在需要时随时编译进内核这样就可以摒弃所有的限制了。完全可以在Linux源代码树以外来开发并编译一个模块这并不奇怪它称为树外开发如果你只是想稍微玩玩而并不想提交修改以包含到主线内核中去这样的方式是很方便的。
在本教程中,我们将开发一个简单的内核模块用以创建一个**/dev/reverse**设备。写入该设备的字符串将以逆序的方式读回“Hello World”读成“World Hello”。这是一个流行的节目采访智力游戏而当你展示能力来实施时你也可能获得一些奖励分。在开始前有一句忠告你的模块中的一个bug会导致系统崩溃虽然可能性不大但还是有可能的和数据丢失。在开始前请确保你已经将重要数据备份或者采用一种更好的方式在虚拟机中进行试验。
### 尽可能避免root身份 ###
> 默认情况下,**/dev/reverse**只有root可以使用因此你不得不使用**sudo**来测试该程序。要解决该问题,可以创建一个包含以下内容的**/lib/udev/rules.d/99-reverse.rules**文件:
>
> SUBSYSTEM=="misc", KERNEL=="reverse", MODE="0666"
>
> 别忘了重新插入模块。让设备节点让非root用户访问这往往不是一个好主意但是在开发其间却是十分有用的这不是说以root身份运行二进制测试文件也不是个好主意。
#### 模块的构造 ####
由于大多数的Linux内核模块是用C写的除了低级别特定架构部分所以推荐你将模块以单一文件形式保存例如reverse.c。我们已经把完整的源代码放在GitHub上——这里我们将看其中的一些片段。开始时我们先要包含一些常见的文件头并用预定义的宏来描述模块
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Valentine Sinitsyn <valentine.sinitsyn@gmail.com>");
MODULE_DESCRIPTION("In-kernel phrase reverser");
这里一切都直接明了,除了**MODULE_LICENSE()**它不仅仅是一个标记。内核坚定地支持GPL兼容代码因此如果你把许可证设置为其它非GPL兼容的“专利”特定的内核功能将在你的模块中不可用。
### 什么时候不该写内核模块 ###
> 内核编程很有趣,但是在现实项目中写(尤其是调试)内核代码要求特定的技巧。通常来讲,在没有其它方式解决你的问题时,你才应该沉入内核级别。可能你可以待在用户空间中,如果:
> - 你开发一个USB驱动 —— 请查看[libusb][1]。
> - 你开发一个文件系统 —— 试试[FUSE][2]。
> - 你在扩展Netfilter —— 那么[libnetfilter_queue][3]对你有所帮助。
>
> 通常,本地内核代码会干得更好,但是对于许多项目而言,这点性能丢失并不严重。
由于内核编程总是异步的没有Linux顺序执行得**main()**函数来运行你的模块。取而代之的是,你为各种事件提供了回调函数,像这个:
static int __init reverse_init(void)
{
printk(KERN_INFO "reverse device has been registered\n");
return 0;
}
static void __exit reverse_exit(void)
{
printk(KERN_INFO "reverse device has been unregistered\n");
}
module_init(reverse_init);
module_exit(reverse_exit);
这儿,我们定义了函数,用来访问模块的插入和移除功能,只有第一个是必要的。目前,它们只是打印消息到内核环缓冲区(可以通过**dmesg**命令从用户空间访问);**KERN_INFO**是日志等级(注意,没有逗号)。**_init**和**_exit**是属性 —— 联结到函数的元数据片或者变量。属性在用户空间的C代码中是很罕见的但是内核中却很普遍。所有标记为**_init**的,会在初始化后再生(还记得那条老旧的“释放未使用的内核内存……”信息?)。**__exit**表明,当代码被静态构建进内核时,该函数可以安全地优化。最后,**module_init()**和**module_exit()**这两个宏将**reverse_init()**和**reverse_exit()**函数设置成为我们模块的生命周期回调函数。实际的函数名称并不重要,你可以称它们为**init()**和**exit()**,或者**start()**和**stop()**,你想叫什么就叫什么吧。在你的模块外,它们被申明成为静态的和不可见的。事实上,内核中的任何函数都是不可见的,除非明确地被导出。然而,在内核程序员中,给你的函数加上模块名前缀是约定俗成的。
这些是基本要素 —— 让我们把事情变得更有趣些。模块可以接收参数,就像这样:
# modprobe foo bar=1
**modinfo**命令显示了所有模块接受的参数,而这些也可以在**/sys/module//parameters**下作为文件使用。我们的模块需要一个缓冲区来存储短语 —— 让我们把这大小设置为用户可配置。添加**MODULE_DESCRIPTION()**以下的三行:
static unsigned long buffer_size = 8192;
module_param(buffer_size, ulong, (S_IRUSR | S_IRGRP | S_IROTH));
MODULE_PARM_DESC(buffer_size, "Internal buffer size");
这儿我们定义了一个变量来存储该值将其包裹到一个参数中并通过sysfs来让所有人可读。参数的描述最后一行会出现在modinfo的输出中。
由于用户可以直接设置**buffer_size**,我们需要在**reverseinit()**来清除它。你总该检查来自内核外的数据 —— 如果你不这么做,你就是会将你自身置于内核异常之中,设置造成安全漏洞。
static int __init reverse_init()
{
if (!buffer_size)
return -1;
printk(KERN_INFO
"reverse device has been registered, buffer size is %lu bytes\n",
buffer_size);
return 0;
}
来自模块初始化函数的非0返回值意味着模块执行失败。
### 导航 ###
> 但你开发模块时Linux内核就是你所需一切的源头。然而它相当大你可能在查找你所要的内容时会有困难。幸运的是在浏览庞大的代码库时有工具可以帮助你干得轻松一点。首先是Cscope —— 在终端中运行的一个令人肃然起敬的工具。你所要做的,就是在内核源代码的顶级目录中运行**make cscope && cscope**。Cscope和Vim以及Emacs整合得很好因此你可以在使用你最喜爱的编辑器舒适地工作时来使用它。
> 如果基于终端的工具不是你的最爱,那么就访问[http://lxr.free-electrons.com][4]吧。它是一个基于web的内核导航工具即使它的功能没有Cscope来得多例如你不能方便地找到函数的用法但它仍然提供了足够多的快速查询功能。
现在是时候来编译模块了。你将需要用于正在运行的内核版本的头文件(**linux-headers**,或者同等软件包)和**build-essential**或者类似的包。接下来该创建一个标准的Makefile模板
obj-m += reverse.o
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
现在,调用**make**来构建你的第一个模块。如果你输入的都正确在当前目录内会发现reverse.ko文件。使用**sudo insmod reverse.ko**插入,然后运行:
$ dmesg | tail -1
[ 5905.042081] reverse device has been registered, buffer size is 8192 bytes
恭喜了!然而,目前这一行还只是在逗你玩而已 —— 还没有设备节点呢。让我们来修复它。
#### 混杂设备 ####
在Linux中有一种特殊的字符设备类型叫做“混杂设备”或者简称为“misc”。它设计用于只有一个单一接入点的小型设备驱动而这正是我们所需要的。所有混杂设备共享同一个主设备号10因此一个驱动(**drivers/char/misc.c**)就可以查看它们所有设备了,而这些设备用次设备号来区分。在所有其它意义上,它们只是普通字符设备。
要为该设备注册一个次设备号(以及一个接入点),你需要声明**struct misc_device**,填上所有字段(注意语法),然后使用指针指向该结构函数来调用**misc_register()**。为了这个能工作,你也需要包含**linux/miscdevice.h**头文件:
static struct miscdevice reverse_misc_device = {
.minor = MISC_DYNAMIC_MINOR,
.name = "reverse",
.fops = &reverse_fops
};
static int __init reverse_init()
{
...
misc_register(&reverse_misc_device);
printk(KERN_INFO ...
}
这儿我们为名为“reverse”的设备请求一个第一个可用的动态的次设备号省略号表明我们已经见过的省略的代码。别忘了在模块卸下后注销掉该设备。
static void __exit reverse_exit(void)
{
misc_deregister(&reverse_misc_device);
...
}
fops字段存储了一个指针指向结构函数**file_operations**在Linux/fs.h中已声明而这真是我们模块的接入点。**reverse_fops**定义如下:
static struct file_operations reverse_fops = {
.owner = THIS_MODULE,
.open = reverse_open,
...
.llseek = noop_llseek
};
再者,**reverse_fops**包含了一系列回调函数(也称之为方法),当用户空间代码打开一个设备时,就会执行。从该设备读取,向该设备写入,或者关闭文件描述符。如果你忽略了所有这些,就会使用一个灵敏的回调函数来替代。这就是为什么我们明确给**noop_llseek()**设置了**llseek**方法,而它却什么也不干(就像名称中暗指的)。默认部署改变了文件指针,我们现在也不想我们的设备被找到(这是你们的今天的回家作业)。
#### 我在关闭时打开 ####
让我们实施该方法。我们将分配一个新的缓冲区给每个打开的文件描述符并在它关闭时释放。这事实上并不安全如果一个用户空间应用程序泄漏了描述符也许是故意的它就会霸占RAM并使系统不可用。在现实世界中你总得考虑到这些可能性。但在本教程中这种方法可以接受。
我们需要一个结构函数来描述缓冲区。内核提供了许多常规的数据结构链接列表双联的哈希表树等等之类。然而缓冲区常常从零开始实施。我们将调用我们的“struct buffer”
struct buffer {
char *data, *end, *read_ptr;
unsigned long size;
};
**data**是该缓冲区存储的一个指向字符串的指针,而最后部分是字符串结尾后的第一个字节。**read_ptr**是**read()**开始读取数据的地方。缓冲区大小为了完整性而存储 —— 目前,我们还没有使用该区域。你不能假设使用你结构体的用户会正确地初始化所有这些东西,所以最好在函数中封装缓冲区分配和解除。它们通常命名为**buffer_alloc()**和**buffer_free()**。
static struct buffer *buffer_alloc(unsigned long size)
{
struct buffer *buf;
buf = kzalloc(sizeof(*buf), GFP_KERNEL);
if (unlikely(!buf))
goto out;
...
out:
return buf;
}
内核内存使用**kmalloc()**来分配,并使用**kfree()**来释放;**kzalloc()**的风格是将内存设置为全零。不同于标准的**malloc()**,它的内核对应部分收到的标志指定了第二个参数中请求的内存类型。这里,**GFP_KERNEL**是说我们需要一个普通的内核内存不是在DMA或高内存中以及函数可以按需睡眠重新编排进程。**sizeof(*buf)**是一种常见的方式,它用来获取可通过指针访问的结构体的大小。
你应该随时检查**kmalloc()**的返回值解应用NULL指针将导致内核异常。同时也需要注意**unlikely()**宏的使用。它(及其相对宏**likely()**)被广泛用于内核中,用于表明条件几乎总是真的(或假的)。它不会影响到控制流,但是能帮助现代处理器通过分支预测技术来提升性能。
最后,注意**gotos**。它们常常为认为是邪恶的但是Linux内核以及一些其它系统软件采用它们来实施集中式的函数退出。这样的结果是减少嵌套深度使代码更具可读性而且非常像更高级语言中的**try-catch**区块。
有了**buffer_alloc()**和**buffer_free()****open**和**close**方法就变得很简单了。
static int reverse_open(struct inode *inode, struct file *file)
{
int err = 0;
file->private_data = buffer_alloc(buffer_size);
...
return err;
}
**struct file**是一个标准的内核数据结构,用以存储打开的文件的信息,如当前文件位置(**file->fpos**),标志(**file->flags**),或者打开模式(**file->fmode**)。另外一个字段**file->privatedata**用于关联文件到一些专有数据它的类型是void *,而且它在文件拥有者以外对内核不透明。我们将一个缓冲区存储在那里。
如果缓冲区分配失败,我们通过返回否定值(**-ENOMEM**)来为调用的用户空间代码标明。
#### 学会读写 ####
“read”和“write”方法是真正完成工作的地方。当数据写入到缓冲区时我们就丢弃它里头先前的内容并在没有任何临时存储时将短语恢复原状。**read**方法仅仅是从内核缓冲区复制数据到用户空间。但是如果缓冲区还没有数据,**reverseread()**会做什么呢?在用户空间中,**read()**调用会在有可用数据前阻塞它。在内核中你必须等待。幸运的是有一项机制用于处理这种情况就是wait queues
想法很简单。如果当前进程需要等待某个事件,它的描述符(**struct task_struct**存储为current被放进非可运行睡眠中状态并添加到一个队列中。然后**schedule()**就被调用来选择另一个进程运行。生成事件的代码通过使用队列将等待进程放回**TASKRUNNING**状态来唤醒它们。调度程序将在以后在某个地方选择它们之一。Linux有多种非可运行状态最值得注意的是**TASKINTERRUPTIBLE**(一个可以通过信号中断的睡眠)和**TASKKILLABLE**(一个可被杀死的睡眠中的进程)。所有这些都应该正确处理,并等待队列为你做这些事。
一个用以存储读取等待队列头的天然场所就是结构缓冲区,所以从为它添加**wait_queue_head_t read_queue**字段开始。你也应该包含**linux/sched.h**。可以使用DECLARE_WAITQUEUE()宏来静态声明一个等待队列。在我们这种情况下,需要动态初始化,因此添加下面这行到**buffer_alloc()**
init_waitqueue_head(&buf->read_queue);
我们等待可用数据;或者等待**read_ptr != end**条件成立。我们也想要让等待操作可以被中断通过Ctrl+C。因此“read”方法应该像这样开始
static ssize_t reverse_read(struct file *file, char __user * out,
size_t size, loff_t * off)
{
struct buffer *buf = file->private_data;
ssize_t result;
while (buf->read_ptr == buf->end) {
if (file->f_flags & O_NONBLOCK) {
result = -EAGAIN;
goto out;
}
if (wait_event_interruptible
(buf->read_queue, buf->read_ptr != buf->end)) {
result = -ERESTARTSYS;
goto out;
}
}
...
我们让它循环,直到有可用数据,如果没有则使用**wait_event_interruptible()**(它是一个宏,不是函数,这就是为什么要给队列传递值)来等待。好吧,如果**wait_event_interruptible()**被中断它返回一个非0值这个值代表**-ERESTARTSYS**。这段代码意味着系统调用应该重新启动。**file->f_flags**检查以非阻塞模式打开的文件数:如果没有数据,返回**-EAGAIN**。
我们不能使用**if()**来替代**while()**,因为可能有许多进程正等待数据。当**write**方法唤醒它们时,调度程序选择一个来以不可预知的方式运行,因此,在这段代码有机会执行的时候,缓冲区可能再次空出。现在,我们需要将数据从**buf->data** 复制到用户空间。**copytouser()**内核函数就干了此事:
size = min(size, (size_t) (buf->end - buf->read_ptr));
if (copy_to_user(out, buf->read_ptr, size)) {
result = -EFAULT;
goto out;
}
如果用户空间指针错误,那么调用可能会失败;如果发生了此事,我们就返回**-EFAULT**。记住,不要相信任何来自内核外的事物!
buf->read_ptr += size;
result = size;
out:
return result;
}
为了让数据能读入到专有组块中,需要进行简单运算。该方法返回读入的字节数,或者一个错误代码。
写方法更简短。首先,我们检查缓冲区是否有足够的空间,然后我们使用**copy_from_userspace()**函数来获取数据。再然后**read_ptr**和结束指针会被重置,缓冲区内容会被撤销掉:
buf->end = buf->data + size;
buf->read_ptr = buf->data;
if (buf->end > buf->data)
reverse_phrase(buf->data, buf->end - 1);
这里, **reverse_phrase()**干了所有吃力的工作。它依赖于**reverse_word()**函数,该函数相当简短并且标记为内联。这是另外一个常见的优化;但是,你不能过度使用。因为积极的内联会导致内核映像徒然增大。
最后,我们需要唤醒**read_queue**中等待数据的进程,就跟先前讲过的那样。**wake_up_interruptible()**就是用来干此事的:
wake_up_interruptible(&buf->read_queue);
唷!你现在已经有了一个内核模块,它至少已经编译成功了。现在,是时候来测试了。
### 调试内核代码 ###
> 或许,内核中最常见的调试方法就是打印。如果你愿意,你可以使用普通的**printk()** (假定使用**KERN_DEBUG**日志等级。然而那儿还有更好的办法。如果你正在写一个设备驱动这个设备驱动有它自己的“struct device”可以使用**pr_debug()**或者**dev_dbg()**:它们支持动态调试(**dyndbg**)特性,并可以根据需要启用或者禁用(请查阅**Documentation/dynamic-debug-howto.txt**)。对于单纯的开发消息,使用**prdevel()**该函数没有操作符除非设置了DEBUG。要为我们的模块启用DEBUG请添加以下行到Makefile中
> CFLAGS_reverse.o := -DDEBUG
>
> 完了之后,使用**dmesg**来查看**pr_debug()**或**pr_devel()**生成的调试信息。
> 或者,你可以直接发送调试信息到控制台。要想这么干,你可以设置**console_loglevel**内核变量为8或者更大的值**echo 8 /proc/sys/kernel/printk**),或者在高日志等级,如**KERN_ERR**,来临时打印要查询的调试信息。很自然,在发布代码前,你应该移除这样的调试声明。
> 注意出现在控制台的内核消息而不要在Xterm这样的终端模拟器窗口中去查看那也是你在内核开发时经常会建议你不要再X环境下进行的原因。
### 惊喜,惊喜! ###
编译模块,然后加载进内核:
$ make
$ sudo insmod reverse.ko buffer_size=2048
$ lsmod
reverse 2419 0
$ ls -l /dev/reverse
crw-rw-rw- 1 root root 10, 58 Feb 22 15:53 /dev/reverse
一切似乎就位。现在,要测试模块是否正常工作,我们将写一段小程序来翻转它的第一个命令行参数。**main()**(没有错误检查)可能看上去像这样:
int fd = open("/dev/reverse", O_RDWR);
write(fd, argv[1], strlen(argv[1]));
read(fd, argv[1], strlen(argv[1]));
printf("Read: %s\n", argv[1]);
像这样运行:
$ ./test 'A quick brown fox jumped over the lazy dog'
Read: dog lazy the over jumped fox brown quick A
它工作正常!玩得更逗一点:试试传递单个单词或者单个字母的短语,空的字符串或者是非英语字符串(如果你有这样的键盘布局设置),以及其它任何东西。
现在,让我们让事情变得更好玩一点。我们将创建两个进程,它们共享一个文件描述符(因而还有内核缓冲区)。其中一个会持续写入字符串到设备,而另一个将读取这些字符串。在下例中,我们使用了**fork(2)**系统调用而pthreads也很好用。我也忽略了打开和关闭设备以及错误检查部分的代码又来了
char *phrase = "A quick brown fox jumped over the lazy dog";
if (fork())
/* Parent is the writer */
while (1)
write(fd, phrase, len);
else
/* child is the reader */
while (1) {
read(fd, buf, len);
printf("Read: %s\n", buf);
}
你希望这个程序会输出什么呢?下面就是在我的笔记本上得到的东西:
Read: dog lazy the over jumped fox brown quick A
Read: A kcicq brown fox jumped over the lazy dog
Read: A kciuq nworb xor jumped fox brown quick A
Read: A kciuq nworb xor jumped fox brown quick A
...
这里发生了什么呢?举行了一场比赛。我们认为**read**和**write**是很小的,或者从头到尾一次执行一个指令。然而,内核是并发的野兽,它可以很容易地重排**reverse_phrase()**函数内部某个地方运行着的内核模式部分的写入操作。如果进行**read()**操作的进程在写入操作结束前就被编排进去就会产生数据不连续状态。这些bug非常难以排除。但是怎样来处理这个问题呢
基本上,我们需要确保在写方法返回前没有**read**方法能被执行。如果你曾经编写过一个多线程的应用程序你可能见过同步原语如互斥锁或者信号。Linux也有这些但有些细微的差别。内核代码可以运行在进程条件中“代表”用户空间代码工作就像我们的方法那样以及运行在中断条件中例如在IRQ处理器中。如果你的程序处于进程条件中并且你需要的锁已经被拿走你的程序就会睡眠并重试直至成功。在中断条件中是无法睡眠的因此代码在循环中流转直到有可用的锁为止。关联原语被称为自旋锁但在我们的环境中一个简单的互斥锁 —— 在特定时间内只有唯一一个进程能“占有”的对象 —— 就足够了。处于性能方面的考虑,现实的代码可能也会使用读-写信号。
锁总是保护某些数据在我们的环境中是一个“struct buffer”实例而且也常常会把它们嵌入到它们所保护的结构体中。因此我们添加一个互斥锁struct mutex lock到“struct buffer”中。我们也必须用**mutex_init()**来初始化互斥锁;**buffer_alloc**是用来处理这件事的好地方。使用互斥锁的代码也必须包含**linux/mutex.h**。
互斥锁很像交通信号灯 —— 除非驱动查看并跟踪信号,否则它没什么用。因此,在对缓冲区做操作并在操作完成时释放它之前,我们需要更新**reverse_read()**和**reverse_write()**来获取互斥锁。让我们来看看**read**方法 —— **write**的工作原理相同:
static ssize_t reverse_read(struct file *file, char __user * out,
size_t size, loff_t * off)
{
struct buffer *buf = file->private_data;
ssize_t result;
if (mutex_lock_interruptible(&buf->lock)) {
result = -ERESTARTSYS;
goto out;
}
我们在函数一开始就获取锁。**mutex_lock_interruptible()**要么抓取互斥锁然后返回,要么让进程睡眠,直到有可用的互斥锁。就像前面一样,**_interruptible**后缀意味着睡眠可以由信号来中断。
while (buf->read_ptr == buf->end) {
mutex_unlock(&buf->lock);
/* ... wait_event_interruptible() here ... */
if (mutex_lock_interruptible(&buf->lock)) {
result = -ERESTARTSYS;
goto out;
}
}
下面是我们的“等待数据”循环。当持有互斥锁,或者发生称之为“死锁”的情境时,不应该让进程睡眠。因此,如果没有数据,我们释放互斥锁并调用**wait_event_interruptible()**。当它返回时,我们重新获取互斥锁并像往常一样继续:
if (copy_to_user(out, buf->read_ptr, size)) {
result = -EFAULT;
goto out_unlock;
}
...
out_unlock:
mutex_unlock(&buf->lock);
out:
return result;
最后,当函数结束,或者在互斥锁被占有过程中发生错误时,互斥锁被解锁。重新编译模块(别忘了重新加载),然后再次进行测试。现在你应该没发现毁坏的数据了。
### 接下来是什么? ###
现在你体验了一把内核侵入。我们刚刚为你揭开了今天话题的外衣里面还有更多东西供你探索。我们的第一个模块是有意识地写得简单一点在从中学到的概念在更复杂的环境中也一样。并发、方法表、注册回调函数、使进程睡眠以及唤醒进程这些都是内核黑客们耳熟能详的东西而现在你已经看过了它们的运作。或许某天你的内核代码也将被加入到主线Linux源代码树中 —— 如果真这样,请联系我们!
--------------------------------------------------------------------------------
via: http://www.linuxvoice.com/be-a-kernel-hacker/
译者:[GOLinux](https://github.com/GOLinux) 校对:[校对者ID](https://github.com/校对者ID)
本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创翻译,[Linux中国](http://linux.cn/) 荣誉推出
[1]:http://www.libusb.org/
[2]:http://fuse.sf.net/
[3]:http://www.linuxvoice.com/be-a-kernel-hacker/www.netfilter.org/projects/libnetfilter_queue
[4]:http://lxr.free-electrons.com/

View File

@ -1,495 +0,0 @@
如何编写你的第一个Linux内核模块
================================================================================
>你是否曾经想过开始去攻击一个内核?然而却不知道从何开始?让我们来告诉你该怎么做…
内核程序通常被视为一个黑色魔术。从Arthur C Clarke的理念来说确实是这样。Linux内核与用户空间的确存在很多不同许多抽象的东西被搁置你需要格外小心当一个bug存在代码中时将会影响到整个系统。这不是一个简单的方法去改浮点数堆栈既被固定了而且很小你写的代码通常是异步的所以你需要考虑到并发性的问题。尽管如此Linux内核是一个庞大且复杂的C程序对每个人都提供开源阅读、学习和改进你也可以成为其中的一份子。
> “The easiest way to start kernel programming
> is to write a module a piece of code that
> can be dynamically loaded into the kernel.”
> “最简单的方法来开始写内核程序
> 就是写一个内核模块 - 这段代码
> 可以动态加载到内核中。”
可能开始学习内核程序的最简单方式来就是先编写一个模块——一段代码可以动态加载到你的内核和卸载。但是也存在一些限制比如说什么模块可以这样做——例如它们不能添加或者删除常用数据结构的一些字段例如过程描述。但是在其它方面它们是成熟的内核级别代码根据需要并经常被编译到内核中因此删除所有受限。在Linux源代码树开发和编译一个模块这不出乎医疗应该成为out-of-tree构建是完全有可以的如果你只想玩一下而不去提交你主线内核的改动这是非常方便的。
在这个教程中,我们将开发一个简单的内核模块,创建**/dev/reverse**设备。当字符写到这个设备就回次序颠倒读出来就像“Hello World”会变成“World Hello”。这是一个很受欢迎的程序员面试难题当你利用自己的能力在内核级别实现这个功能时可以使你得到一些加分。在我们开始之前会有一句提示有一个bug寸在你的模块有可能导致你的系统崩溃不太可能但是可能和数据丢失。
###尽可能避开root用户 ###
>默认情况下,**/dev/reverse**只能对根用户可用,所以你只能通过**sudo**来运行你的测试程序。为了修复这个问题,创建一个**/lib/udev/rules.d/99-reverse.rules**文件,其中包含:
>
> SUBSYSTEM=="misc", KERNEL=="reverse", MODE="0666"
>不要忘记重新插入模块。通常来说使设备接点可访问对非根用户来说通常不是一个好主意,但是在开发过程中的确非常有用。更不值得一提的是作为根用户运行测试二进制文件也不是一个好主意。
#### 关于模块的剖析 ####
由于大多数Linux内核模块都是用C编写的除了底层的特定于体系结构的部分所以建议把你的内核保存在一个独立的文件中叫做reverse.c).我们会将整份源代码放在GitHub中——在这里不我们可以看到一些小片段。首先我们要把一些常见的头文件和描述该模块使用预定义的宏包含进来
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Valentine Sinitsyn <valentine.sinitsyn@gmail.com>");
MODULE_DESCRIPTION("In-kernel phrase reverser");
这一切其实都很简单,除了**MODULE_LICENSE()**之外这不是一个纯粹的标记。内核尤其钟情于GPLcompatible 代码所以如果你设置一些非GPL兼容的认证也就是说“私有认证”某些特定的内核功能在你的模块中将不可用。
### 什么时候不该一个内核模块 ###
>- 你开发一个USB驱动时 - 需要查阅[libusb][1].
>- 你开发一个系统文件时 - 试试 [FUSE][2].
>- 你扩展Netfilter - [libnetfilter_queue][3] 也许能够对你有所帮助。
>通常,本地代码会运行得更好,但是很多程序来说这种情况并不是至关重要的。
由于内核程序通常是异步的,它并没有 **main()** 功能Linux是按顺序运行你的模块。取而代之你将会提供各种事件的回滚例如
static int __init reverse_init(void)
{
printk(KERN_INFO "reverse device has been registered\n");
return 0;
}
static void __exit reverse_exit(void)
{
printk(KERN_INFO "reverse device has been unregistered\n");
}
module_init(reverse_init);
module_exit(reverse_exit);
这里,我们定义的函数被称为模块的插入和删除。只有第一个是必要的。目前,他们简单地发送一条内核指令给缓存中(可以通过用户界面发送 **dmesg** 命令);**KERN_INFO**是一个日至级别(切记这里没有逗号).**__init** 和 **__exit** 都是特性 - 把元数据添加到函数或者变量。在用户界面中C代码的属性是很罕见的但是在内核中却很普遍。标有**__init**都会经过初始化以后被回收(要记得之前释放未使用的内核内寸...这条消息?)。**__exit** 表示,当代码静态构建到内核中时,函数会安全地优化。最后,**module_init()** 和**module_exit()**宏设置**reverse_init()** 和 **reverse_exit()**作为我们模块生命周期的回滚。实际的函数名称并不重要;你可以按着你的想法命名为**init()** 和**exit()** 或者 **start()****stop()**。他们都是静态声明,你在外部模块是看不到的。实际上,许多内核中的模块是可视的除非是明显的导出。然而,在函数前面添加你的函数作为前缀通常是内核程序员约定俗成的做法。
这些都是些基本概念 - 让我们来做更多有趣的事情吧。模块可以添加参数,例如:
# modprobe foo bar=1
**modinfo**显示了所有模块可以接受的参数,这些也可以在**/sys/module//parameters**文件中找到。我们的模块需要一个缓存区来存储参数 - 让我们来调整大小使用户可配置。在**MODULE_DESCRIPTION()**添加如下三行:
static unsigned long buffer_size = 8192;
module_param(buffer_size, ulong, (S_IRUSR | S_IRGRP | S_IROTH));
MODULE_PARM_DESC(buffer_size, "Internal buffer size");
这里我们定义一个变量来存储值封装成一个参数使得每个人都可以通过sysfs对其进行阅读。这个参数的描述在最后一行出现在modinfo的输出里。
由于用户可以直接设置**buffer_size**,我们需要在**reverse_init()**中对其进行清理。你应该经常检查来自内核意外数据 - 付过你不这样做,你将是在给自己制造内核异常甚至是安全漏洞。
static int __init reverse_init()
{
if (!buffer_size)
return -1;
printk(KERN_INFO
"reverse device has been registered, buffer size is %lu bytes\n",
buffer_size);
return 0;
}
来自模块初始化函数的非0返回值意味着执行失败。
### 导航 ###
>当你正在开发模块时linux内核就是无限的源头。然而它确实很大你也许在寻找的过程中会遇到很多困难。幸运的是在庞大的代码库面前有许多工具使这个过程变得简单。首先有一个叫Cscope的工具——在终端运行中一个比较经典的工具。简单地运行**make cscope && cscope**在源代码一级目录。Cscope很好地集成了Vim和Emacs于是你又可以不离开你喜爱地编辑工具。
>基于终端工具的使用也许不是你的强项,请访问[http://lxr.free-electrons.com][4].这是一个基于网站的内核导航工具没有太多的类似Cscope的特性例如你不能轻易地查找函数的用法但是它还是可以给你提供快速的查找。
现在,是时候编译模块了。你需要你正在运行的内核版本头文件(**linux-headers**或者同等包)和**build-essential** 或者类是。接下来是时候创建一个叫Makefile的样板
obj-m += reverse.o
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
现在,使用**make**来创建你的第一个模块。如果你正确输入,你将会找到在当前目录里找到 **reverse.ko**。插入**sudo insmod reverse.ko**并运行:
$ dmesg | tail -1
[ 5905.042081] reverse device has been registered, buffer size is 8192 bytes
祝贺你!可是,现在这一行告诉你一个假象 —— 这里并不存在设备接点。让我们修复它吧。
#### 混杂设备 ####
在Linux中有一种特殊的字符设备类型成为“miscellaneous”简写“misc”。这是专为单点小型设备驱动而涉及的而且能恰好满足我们的需求。所有的misc设备共享相同的主设备号10所以一个驱动程序(**drivers/char/misc.c**)可以查看到他们的所有设备,你可以从他们的次设备号分辨出他们。从其他意义来说,他们也只是普通的字符设备。
为了给设备注册一个次要设备号(和一个接入点),你需要先声明**struct misc_device**,填写字段(注意语法),并调用**misc_register()**这个结构的指针。为此,你还需要把**linux/miscdevice.h** 头文件包含进去:
static struct miscdevice reverse_misc_device = {
.minor = MISC_DYNAMIC_MINOR,
.name = "reverse",
.fops = &reverse_fops
};
static int __init reverse_init()
{
...
misc_register(&reverse_misc_device);
printk(KERN_INFO ...
}
在这里我们要求第一个可用的动态的次设备号命名为“reverse”;省略号表示已经省略了的代码,这些省略了的代码是我们之前看过的。别忘了在模块卸载后注销该设备:
static void __exit reverse_exit(void)
{
misc_deregister(&reverse_misc_device);
...
}
fop字段存储了一个指向**file_operations**在linux/fs.h中声明结构的指针这是指向我们模块的接入点。**reverse_fops**定义如下:
static struct file_operations reverse_fops = {
.owner = THIS_MODULE,
.open = reverse_open,
...
.llseek = noop_llseek
};
另外,**reverse_fops**包含了一组将要执行的回调函数(也成为方法),当用户空间代码打开了一个设备,从中进行读写或者关闭文件描述。如果你忽略了这些,那么将会有一个只能的回退来取代它。这就是我们要明确地给**noop_llseek()**设置**llseek**的方法,(顾名思义)而它却什么都不干。默认地实现是改变了文件的指针,我们也不想我们的设备被识别(这将是你们今天的家庭作业)。
#### 在结束时开启 ####
让我们来实现这个方法。我们将会为每一个文件描述的打开分配一个新的缓冲区来给它自由关闭。这确实是不安全:如果一个用户空间应用的漏洞描述(或者是故意的),但它可能占用着内存,导致系统无法正常使用。在真实应用中,你应该经常考虑到这些可能性。但是在本教程中,这个方法是可接受的。
我们需要一个结构来描述缓冲区。内核提供了许多通用的数据结构链表双键链表哈希表树等等。然而缓冲区通常从零开始实现的。我们将调用我们的“struct buffer”:
struct buffer {
char *data, *end, *read_ptr;
unsigned long size;
};
**data**是一个寸在缓冲区指向字符串的指针数据,而结尾是在字符串结束后的第一个字节。缓冲区的大小是为完整性而存储的 —— 目前,我们不使用该字段。你不应该认为你的用户结构会正确地初始化所有这些,所以你应更好地在函数中封装缓冲区和重新分配分配。他们通常命名为**buffer_alloc()** 和 **buffer_free()**.
static struct buffer *buffer_alloc(unsigned long size)
{
struct buffer *buf;
buf = kzalloc(sizeof(*buf), GFP_KERNEL);
if (unlikely(!buf))
goto out;
...
out:
return buf;
}
Kernel memory is allocated with **kmalloc()** and freed with **kfree()**; the **kzalloc()** flavour sets the memory to all-zeroes. Unlike standard **malloc()**, its kernel counterpart receives flags specifying the type of memory requested in the second argument. Here, **GFP_KERNEL** means we need a normal kernel memory (not in DMA or high-memory zones) and the function can sleep (reschedule the process) if needed. **sizeof(*buf)** is a common way to get the size of a structure accessible via pointer.
内核内存使用通过**kmalloc()**来分配并**kfree()**来释放内存;**kzalloc()**会将内存设置为全零。不像标准的**malloc()**,它的内核版本接收标志在第二个参数指定内存的类型。这里,**GFP_KERNEL**意味着我们需要一个正常的内核内存(并非直接内存存取或者高内存区)以及函数可以按需休眠(重新安排流程)。**sizeof(*buf)**是一种常用的方式通过指针来获取结构的大小。
你应该经常检查**kmalloc()**的返回值;非空指针将会导致内核崩溃。另外还要注意到**unlikely()**宏的用法。它(与**likely()**宏相反)广泛用于表示内核中判断条件几乎是真(或假)。它不影响流控制,但是有助于现代处理器提高分支预测的性能。
最后,注意**gotos**。他们通常被认为是邪恶的然而在Linux内核中和一些其它系统软件利用他们集中实现函数退出。这将导致更少的深度嵌套和可读代码就更像**try-ctach**块中使用的更高级语言。
有**buffer_alloc()** 和 **buffer_free()**的存在,**open** 和 **close**的方法就回变得非常简单。
static int reverse_open(struct inode *inode, struct file *file)
{
int err = 0;
file->private_data = buffer_alloc(buffer_size);
...
return err;
}
**struct file**是一个标准的内核数据结构,用于存储关于打开的文件信息,例如当前文件位置(**file->f_pos**),标志(**file->f_flags**),或者打开方式(**file->f_mode**).另外一个字段, **file->private_data**常用于关联文件和一些任意数据。它的类型是void *,但是对内核意外的文件拥有者是不透明的。我们将把它存储在缓冲区内。
如果缓存区分配失败,我们通过返回负值显示调用用户空间代码(**-ENOMEM**).C库中利用**open(2)**系统调用(可能是, **glibc**)将会进行检测并适当地设置**errno** 。
#### 学习如何读和写 ####
读和写的方法才是真正工作的完成。当数据在缓冲区被读和写时,我们放弃之前的内容和反向地存储该字段,不需要任何临时存储。读的方法是简单地复制内核缓冲区到用户空间。但是如果还是没有数据在缓冲区时,什么是**reverse_read()**方法应该做的呢?在用户空间,**read()**调用会阻塞,知道数据可用。在内核中,你需要等待。幸运的是,有一个叫做“等待列队”的机制。
这个想法是很简单的。如果一个当前的进程需要等待一些事件,它的描述(a **struct task_struct** 存储为 ‘当前’)被放在非运行状态sleeping并添加到队列。接着**schedule()**会计划选择另一个进程来运行。生成事件的代码通过使用队列把等待事件放进**TASK_RUNNING** 状态来唤醒它们。调度表会选择在未来的某个点选择它们中的一个。Linux有许多非运行进程状态尤其是**TASK_INTERRUPTIBLE**(睡眠状态可以被一个信号中断)和**TASK_KILLABLE**(正在睡眠状态的进程可以被阻断)。所有这些都会被正确把握,而等待队列将会为你安排好这一切。
存储我们的读写队列头的一个天然场所是结构缓存区,所以通过添加**wait_queue_head_t read_queue**字段开始。你将会将**linux/sched.h**包含其中。一个等待队列将会通过DECLARE_WAITQUEUE()宏被静态声明。在我们的例子中,是需要动态初始化的,于是我们添加**buffer_alloc()**这一行:
init_waitqueue_head(&buf->read_queue);
我们等待可用数据;或者**read_ptr != end**条为真时。我们需要等待可终端例如通过Ctrl+C).所以读的方法应该这样开始:
static ssize_t reverse_read(struct file *file, char __user * out,
size_t size, loff_t * off)
{
struct buffer *buf = file->private_data;
ssize_t result;
while (buf->read_ptr == buf->end) {
if (file->f_flags & O_NONBLOCK) {
result = -EAGAIN;
goto out;
}
if (wait_event_interruptible
(buf->read_queue, buf->read_ptr != buf->end)) {
result = -ERESTARTSYS;
goto out;
}
}
...
我们会进行循环,直到有可用数据出现,并且在如果不是的情况下使用**wait_event_interruptible()**(这是一个宏,不是一个函数,这就是为什么队列要通过一个值来传送)来等待。如果是**wait_event_interruptible()**的情况下,很好,会中断,并返回一个非零值,我们将它翻译为**-ERESTARTSYS**.这段代码意味着系统调用将会重启。**file->f_flags**在一个非阻塞模式检查打开的文件数量:如果没有数据,我们会返回**-EAGAIN**.
我们不能使用**if()**来取代**while()**,由于还有许多进程正在等待数据。当**write**方法唤醒他们,调度表会用不可预知的方法来选择一个运行,那么这段代码将在适时有一个机会执行,缓冲区会再次清空。现在我们需要从**buf->data**复制数据到用户空间。**copy_to_user()**内核函数是实现如下:
size = min(size, (size_t) (buf->end - buf->read_ptr));
if (copy_to_user(out, buf->read_ptr, size)) {
result = -EFAULT;
goto out;
}
如果用户空间指针是错误的,这个调用将可能失败;如果发生这种情况,我们会返回**-EFAULT**.切记不要相信任何来自内核意外的东西!
buf->read_ptr += size;
result = size;
out:
return result;
}
为了使数据在任意块可读,简单的算法往往是必要的。这个方法返回了读或者一段错误代码的字节数。
这个写的方法更简单和更简短。首先,我们检查缓冲区是否有足够的空间,接着我们使用**copy_from_userspace()**功能来获取数据。接着**read_ptr**和结束指针重置和缓冲区内容是相反的:
buf->end = buf->data + size;
buf->read_ptr = buf->data;
if (buf->end > buf->data)
reverse_phrase(buf->data, buf->end - 1);
这里,**reverse_phrase()**干了所有重活。这有赖于**reverse_word()**函数,既简短有可以做内联标记。这是另一种常见的优化;然而,你不应该过度使用它,因为内敛使用会使内核镜像变得不必要的大。
最后,我们需要唤醒正在**read_queue**等待数据的进程,正如先前面熟。**wake_up_interruptible()**实现如下:
wake_up_interruptible(&buf->read_queue);
喲!你现在拥有一个至少可以成功调试的内核模块。现在是时候来测试它了。
### 调试内核代码 ###
>也许在内核中大多数常用的调试方法是打印。你可以使用纯**printk()**(大概和**KERN_DEBUG**日志级别相似)如果你愿意。然而,也有更好的方法。使用**pr_debug()** 或者 **dev_dbg()**,如果你正在写一个有“struct device”设备驱动他们支持动态调试(**dyndbg**)特征并可以启用或者禁止请求(请看**Documentation/dynamic-debug-howto.txt**)。对于纯开发的信息,使用**pr_devel()**除非DEBUG被定义这将成为一个空操作。要为我们的模块启用DEBUG包括
>
> CFLAGS_reverse.o := -DDEBUG
>
>在 Makefile中。在这之后使用**dmesg**来查看通过**pr_debug()** 或者 **pr_devel()**生成调试信息。
>或者,你可以通过发送调试信息到控制台。做这一步,你也可以设置**console_loglevel**内核变量为8甚至更高(**echo 8 /proc/sys/kernel/printk**)或者临时在日志级别高的问题上打印调试信息例如**KERN_ERR**.自然而然,你将会在你发布代码之前删除这类调试语句。
>注意内核在控制台出现的内核信息并不是像Xterm的一个在终端模拟窗口这就是为什么你会发现建议不要在X环境下开发内核。
### 惊喜,惊喜! ###
编译模块并加载大内核:
$ make
$ sudo insmod reverse.ko buffer_size=2048
$ lsmod
reverse 2419 0
$ ls -l /dev/reverse
crw-rw-rw- 1 root root 10, 58 Feb 22 15:53 /dev/reverse
一切似乎都就绪了。现在,来测试模块如何工作,我们将要写一段小程序来反转第一个命令行参数。在**main()**函数(sans error checking)你可以看到如下:
int fd = open("/dev/reverse", O_RDWR);
write(fd, argv[1], strlen(argv[1]));
read(fd, argv[1], strlen(argv[1]));
printf("Read: %s\n", argv[1]);
像这样运行:
$ ./test 'A quick brown fox jumped over the lazy dog'
Read: dog lazy the over jumped fox brown quick A
它正常工作了!玩得跟尽兴一点:尝试传递一个单词或者一个字母的字符串,空的或者是非英语字符串(如果你有一个键盘布局设置)以及其它人和东西。
现在让我们把事情变得更棘手一点。我们将要创建2个进程用于共享文件描述因此还有内核缓冲区。一个将持续写字符串到设备中去而另一个回去读取它们。**fork(2)**系统笤俑在下面例子中用到,但是线程还是会很好地工作。我也省略了开关设备的代码和错误检查(再次提到):
char *phrase = "A quick brown fox jumped over the lazy dog";
if (fork())
/* Parent is the writer */
while (1)
write(fd, phrase, len);
else
/* child is the reader */
while (1) {
read(fd, buf, len);
printf("Read: %s\n", buf);
}
你希望这段程序输出什么呢?下面是我从我的笔记本电脑上获得的:
Read: dog lazy the over jumped fox brown quick A
Read: A kcicq brown fox jumped over the lazy dog
Read: A kciuq nworb xor jumped fox brown quick A
Read: A kciuq nworb xor jumped fox brown quick A
...
这里接下来会发生什么?这是一场比赛。我们认为**read**和**write**是原子,或者从一开始到结束执行一个指令。然而内核是一个并发的猛兽,它可以很轻易地重新安排在内核**write**操作部分运行的进程,这个进程在**reverse_phrase()**函数中。如果进程是在作者有机会完成之前,**read()**是按计划执行的,它将会看到数据处于不一致的状态。这些错误真的很难调试出来。那么要如何修复它呢?
基本上,我们需要确保没有**read**方法可以被执行知道写方法有返回。如果你曾经设定一个多线程应用你可能已经看到原始同步像互斥锁和信号。Linux也同样有这些但是存在细微差别。内核代码可以运行进程上下文用户空间代码的“代表”工作就像我们使用的方法和终端上下文例如一个IRQ处理线程。如果你已经在进程上下文中和你所需要的锁你只需要简单地睡眠和重试直到成功位置。在终端上下文时你不能处于休眠状态因此代码会在一个循环中运行直到锁是可使用。相应的原子成为自旋锁但在我们的例子中是一个简单的互斥 —— 这个对象只有一个进程可以在特定的时间“hold”住 —— 这就足够了。根据性能因素,在现实的代码中也可以使用读写信号。
锁通常用来保护一些数据(在我们的例子中,一个“就够缓冲区”实例),是一种非常常见的嵌入结构。所以我们结构缓冲区中添加一个互斥锁(‘结构互斥锁’).我们必须初始化使用**mutex_init()**对锁进行初始化;**buffer_alloc()**恰好用来处理这个。这段代码使用锁也必须包含**linux/mutex.h**.
一个锁就像一个交通灯 ——除非司机看它和遵守信号否则将起不了作用。所以我们需要升级**reverse_read()** 和 **reverse_write()** 来确保在对缓冲区做任何事情或者释放时完成获得互斥锁。让我们来看一看**read**方法 —— **write**也是用同样的方式工作的:
static ssize_t reverse_read(struct file *file, char __user * out,
size_t size, loff_t * off)
{
struct buffer *buf = file->private_data;
ssize_t result;
if (mutex_lock_interruptible(&buf->lock)) {
result = -ERESTARTSYS;
goto out;
}
我们需要在每个函数的开始有锁。**mutex_lock_interruptible()**也可以获取锁或者将进程返回至休眠状态,直到锁是可利用。正如之前,**_interruptible**后缀意味着休眠状态也可以被一个信号阻断。
while (buf->read_ptr == buf->end) {
mutex_unlock(&buf->lock);
/* ... wait_event_interruptible() here ... */
if (mutex_lock_interruptible(&buf->lock)) {
result = -ERESTARTSYS;
goto out;
}
}
下面是”我们等待数据“的循环。当有互斥锁时,你不应该处于休眠状态,或可能发生一种情况叫”死锁“。所以,如果没有数据,我们要释放锁并调用**wait_event_interruptible()**.当它返回时,我们需要锁并继续和往常一样:
if (copy_to_user(out, buf->read_ptr, size)) {
result = -EFAULT;
goto out_unlock;
}
...
out_unlock:
mutex_unlock(&buf->lock);
out:
return result;
最后,当函数结束时互斥对象已经被解锁了或者当一个错误发生时互斥锁被持有。重新编译模块(不要忘记重新加载它)并再次运行第二个测试。现在你应该看到没有损坏的数据了。
### 接下来做什么呢? ###
现在你已经尝试了做内核黑客了。我们只是触及表面的话题还有更多呢。我们的第一个模块是特意设成简单的然而你学到的概念将在更复杂的场景中保持不变。并发性、方法表、注册回调将进程设置成睡眠状态和唤醒它们是每个内核黑客需要使用的现在你已经看到它们所有都在行动啦。也许总有一天内核代码会在主线Linux代码树上 —— 如果这一天到来,请联系我们!
--------------------------------------------------------------------------------
via: http://www.linuxvoice.com/be-a-kernel-hacker/
译者disylee(https://github.com/译者ID) 校对:[校对者ID](https://github.com/校对者ID)
本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创翻译,[Linux中国](http://linux.cn/) 荣誉推出
[1]:http://www.libusb.org/
[2]:http://fuse.sf.net/
[3]:http://www.linuxvoice.com/be-a-kernel-hacker/www.netfilter.org/projects/libnetfilter_queue
[4]:http://lxr.free-electrons.com/