Kernel Module实战指南(二):内核模块和应用程序的区别

Introduction

你已经写出了第一个内核模块Hello World!有没有发现内核模块和应用程序写法的不同?
下面我将从概念和原理上进行介绍,内核模块和应用程序为什么不同。

内核模块和应用程序的区别

出入口

应用程序的入口始终是main()函数,而出口是main()函数的return。
内核模块的入口是init_module(),而出口是cleanup_module()。

函数库

应用程序可以调用很多C标准库中的函数,如printf()。这些函数都编译到libc中,只有当你的代码在编译的链接阶段时,才会和libc中实际的函数地址绑定。
内核模块则不同,内核中不存在标准库,自然没有类似printf()之类的函数。不过内核中提供了一些函数/符号,你可以通过下面的命令查看:

$ cat /proc/kallsyms | grep printk
...
0000000000000000 T printk
0000000000000000 T printk_emit
...

所有内核中可以用的函数/符号都在/proc/kallsyms,换句话说,你不能使用这里之外的函数/符号。

CPU执行模式

在Intel x86架构上中,有四种模式,也叫ring 0 - ring 3,模式之间的权限不同,这里的权限指的是对硬件设备的操作,如读写内存,读写硬盘等。
Linux使用其中两种模式,即内核模式/特权模式/supervisor mode/ring 0,以及用户模式/非特权模式/user mode/ring 3。
应用程序跑的代码,包括所调用的C标准库,都是跑在用户模式中。而内核模块跑的代码,都是跑在内核模式中。用户模式想要进入内核模式,入口之一便是系统调用函数。

用户态和内核态之间的交互

用户态和内核态进行交互的方式之一,是上面我们提到过系统调用函数。什么是系统调用函数?你可以简单认为,libc调用的下层函数,就是系统调用函数,如libc中open()的实现,最终需要调用系统调用函数__NR_open(),进入内核,在内核态中访问硬盘,打开文件。
另一种方式是设备驱动程序,Linux的设备驱动都存放在/dev下面,当用户态程序和设备驱动交互时,设备驱动在内核态执行代码逻辑,然后将结果返回给用户态。

设备驱动

查看硬盘驱动:

$ls -l /dev/sda*
brw-rw---- 1 root disk 8, 0 Jan 26 23:00 /dev/sda
brw-rw---- 1 root disk 8, 1 Jan 26 23:00 /dev/sda1
brw-rw---- 1 root disk 8, 2 Jan 26 23:00 /dev/sda2
brw-rw---- 1 root disk 8, 3 Jan 26 23:00 /dev/sda3

如果你找不到sda,那么你可以查询ls -l /dev/hda*。
这里需要简单介绍一下,列的含义。第一列的b,是block的意思,代码这个设备是块设备(对应的是c, char字符设备)。

块设备:存储单位一般是固定大小的块,支持随机读写。对于读写请求有缓存,可以选择最佳的位置存储。(有没有想到硬盘的寻址算法?)
字符设备:存储大小不定,存储流式数据,一般不支持随机读写,只支持顺序读写。

8的意思是,驱动的主版本号是8,这个主版本号是给内核看的。
后面的0-3的意思是,副版本号是0-3,这个副版本号是给设备驱动程序看的,内核不关心。
这里只是抛砖迎玉,后面我们再详细介绍设备驱动。

Summary

我们进一步深入了内核模块,介绍了内核模块和应用程序的区别。虽然概念性内容偏多,但这却是理解内核模块的必经之路。

(本文出自csprojectedu.com,转载请注明出处)