找回密码
 骑士注册

QQ登录

微博登录


黑客内核:编写属于你的第一个Linux内核模块

2014-06-24 09:00    评论: 15 收藏: 15 分享: 12    

调试内核代码

或许,内核中最常见的调试方法就是打印。如果你愿意,你可以使用普通的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
...

这里发生了什么呢?就像举行了一场比赛。我们认为readwrite是原子操作,或者从头到尾一次执行一个指令。然而,内核确实无序并发的,随便就重新调度了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 disylee 校对:wxy

本文由 LCTT 原创翻译,Linux中国 荣誉推出

123
查看其它分页:

发表评论


最新评论

我也要发表评论

[1]
发表于 2014-08-27 23:08 的评论:
为什么kali linux make 出现make: ***/lib/modules/3.12-kali1-686-pae/build:没有那个文件或目录。停止。
Zhang_Longqi 2015-02-13 16:11 回复
没有安装内核源码
[1]
发表于 2014-08-27 23:08 的评论:
为什么kali linux make 出现make: ***/lib/modules/3.12-kali1-686-pae/build:没有那个文件或目录。停止。
来自 - 山东威海 的 Chrome/Windows 用户 2014-11-27 16:45 1 回复
我也遇到了这个问题,你搞明白了嘛???
[1]
wuanshou 发表于 2014-07-22 13:51 的评论:
不明觉厉呀。我只会编译,还真不会编写
linux 2014-07-22 15:05 2 回复
啊哈,我觉得照着这篇文章,可以写一个~
wuanshou 2014-07-04 13:12 回复
不明学厉呀!我也就编辑过自己笔记本的驱动,还是就是升级了一个LINUX内核。
[1]
Zhang_Longqi 发表于 2014-06-26 15:37 的评论:
对于译文最好加上原文链接http://www.linuxvoice.com/be-a-kernel-hacker/有时候英较好的的同学可以去查看原文以及弥补翻译的不足,
还有就是文章里曼涉及到的重要链接不要丢失了,这是其中项目的github链接https://github.com/vsinitsyn/reverse
[2]
linux 发表于 2014-07-03 13:17 的评论:
有译文的原文链接的,只是文章较长,原文链接和翻译者等信息,都在最后一页。
linux 2014-07-03 13:24 回复
文末的这种信息,在我们的CMS里面没有单独拎出来,所以很难每页都显示,我想想办法~
另外,这个GIT链接,我查看了原文了,可能是翻译过程给漏了,我现在给补上,谢谢您。
breakersun 2014-07-03 11:32 回复
新装的ubuntu10.04, git上的源码编不通, 什么情况
root@leo-laptop:/home/root/code/git/reverse# make
make -C /lib/modules/2.6.32-53-generic/build M=/home/root/code/git/reverse modules
make[1]: Entering directory `/usr/src/linux-headers-2.6.32-53-generic'
  CC [M]  /home/root/code/git/reverse/reverse.o
/home/root/code/git/reverse/reverse.c:206: error: ‘noop_llseek’ undeclared here (not in a function)
make[2]: *** [/home/root/code/git/reverse/reverse.o] Error 1
make[1]: *** [_module_/home/root/code/git/reverse] Error 2
make[1]: Leaving directory `/usr/src/linux-headers-2.6.32-53-generic'
make: *** [all] Error 2
oxfffO 2014-06-24 17:03  新浪微博网友评论 回复
//@Linux_cn:转发微博
很笨很笨的晰一 2014-06-24 10:33  新浪微博网友评论 回复
马克杯
TinyOS开发者-ytc 2014-06-24 10:03  新浪微博网友评论 回复
Repost
cc诗诗ss-disylee 2014-06-24 10:03  新浪微博网友评论 2 回复
这篇真是累死人了,但是必须学习~因为正在的精华就是这类文章[good]
东莞文老师 2014-06-24 10:03  新浪微博网友评论 1 回复
[ok]

LCTT 译者

共计翻译: 177 篇 | 共计贡献: 1033
贡献时间:2014-05-21 -> 2017-03-19
访问我的 LCTT 主页 | 在 GitHub 上关注我

收藏

返回顶部

分享到微信

打开微信,点击顶部的“╋”,
使用“扫一扫”将网页分享至微信。