[TOC]
题目:
•理解Linux内核模块的思想、方法,实现以下任意一个内核模块
–① 通过内核模块显示进程控制块信息:遍历进程树,输出进程PID和进程的可执行文件名
–② 设计一个内核模块,实现一个虚拟文件系统,该系统驻留在主存中,可以初始化和安装、卸载、创建、复制、追加、删除文件(目录)
•编写makefile,编译内核模块
•加载内核模块、测试命令
•卸载内核模块
本次课程设计所实现的是内核模块①.
实验环境: 虚拟机Ubuntu16.04.1LTS 内核: Linux 4.4.0-36-generic x86_64
模块代码
传统计算机程序的运行生命周期相当简单。加载器为程序分配内存,然后加载程序和所需要的动态链接库。指令从一些入口开始执行(传统C/C++程序以main()函数作为入口),语句被执行,异常被抛出,动态内存被分配和释放,程序最终运行完成。当程序退出时,操作系统识别任何内存泄露,并释放到内存池。
内核模块不是应用程序,从一开始就没有main()函数。内核模块和普通应用程序的区别有:
- 非顺序执行:内核模块使用初始化函数将自身注册并处理请求,初始化函数运行后就结束了。内核模块处理的请求在模块代码中定义。这和常用于图形用户界面(graphical-user interface,GUI)应用的事件驱动编程模型比较类似。
- 没有自动清理:任何由内核模块申请的内存,必须要模块卸载时手动释放,否则这些内存将无法使用,直到系统重启。
- 不要使用printf()函数:内核代码无法访问为Linux用户空间编写的库。内核模块运行在内核空间,它有自己独立的地址空间。内核空间和用户空间的接口被清晰的定义和控制。内核模块可以通过printk()函数输出信息,这些输出可以在用户空间查看到。
- 会被中断:内核模块一个概念上困难的地方在于他们可能会同时被多个程序/进程使用。构建内核模块时需要小心,以确保在发生中断的时候行为一致和正确。BeagleBone有一个单核处理器(目前为止),但是我们仍然需要考虑多进程同时访问对模块的影响。
- 更高级的执行特权:通常内核模块会比用户空间程序分配更多的CPU周期。这看上去是一个优势,然而需要特别注意内核模块不会影响到系统的综合性能。
- 无浮点支持:对用户空间应用,内核代码使用陷阱(trap)来实现整数到浮点模式的转换。然而在内核空间中这些陷阱难以使用。替代方案是手工保存和恢复浮点运算,这是最好的避免方式,并将处理留给用户空间代码。
listpro.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
| #include <linux/init.h> // 用于标记函数的宏,如__init、__exit #include <linux/module.h> // 加载内核模块到内核使用的核心头文件 #include <linux/kernel.h> // 包含内核使用的类型、宏和函数 #include <linux/sched.h> //task_struct在linux/sched.h文件里定义(在使用for_each_process宏的时需要引入这个头文件) MODULE_LICENSE("GPL"); static int num=-1; module_param(num, int, S_IRUGO); * @brief 可加载内核模块初始化函数 * static关键字限制了该函数的可见范围为当前C文件。 * @return 当执行成功返回0 */ static int list_init(void) { struct task_struct *task, *p; struct list_head *pos; int count=0; p = NULL; pos = &(init_task.tasks); task = &init_task; printk(KERN_ALERT "\n========listpro_start=======\n"); printk(KERN_ALERT "PID\tCOMM\n"); if(num!=-1) { p=list_entry(pos, struct task_struct, tasks); for(count=0;count<num;count++) { printk(KERN_ALERT "%d\t%s\n", p->pid, p->comm); p=list_entry(p->tasks.next, struct task_struct, tasks); } } else { for_each_process(task) { count++; printk(KERN_ALERT "%d\t%s\n", task->pid, task->comm); } } printk("已打印进程控制块的数量为%d!", count); return 0; } * @brief 可加载内核模块清理函数和初始化函数类似,它是静态(static)的。 */ static void list_exit(void) { printk(KERN_ALERT "Goodbye !"); printk(KERN_ALERT "\n=======listpro_end=======\n"); } module_init(list_init); module_exit(list_exit);
|
除了列表1注释中描述的点之外,还有一些补充的点:
- 第6行:语句MODULE_LICENSE(“GPL”)提供了(通过modinfo)该模块的许可条款,这让使用这个内核模块的用户能够确保在使用自由软件。由于内核是基于GPL发布的,许可的选择会影响内核处理模块的方式。如果对于非GPL代码选择“专有”许可,内核将会把模块标记为“污染的(tainted)”,并且显示警告。对GPL有非污染(non-tainted)的替代品,比如“GPL版本2”、“GPL和附加权利”、“BSD/GPL双许可”、“MIT/GPL双许可”和“MPL/GPL双许可”。更多内容可以查看linux/module.h头文件。
- 第7行:num(int类型)被声明为静态,并且被初始化为-1。在内核模块中应该避免使用全局变量,这比在应用程序编程时更加重要,因为全局变量被整个内核共享。应该使用static关键字来限制变量在模块中的作用域。如果必须使用全局变量,在变量名上增加前缀确保在模块中是唯一的。
- 第8行:module_param(name, type, permissions)宏有三个参数,名字(展示给用户的参数名和模块中的变量名)、类型(参数类型,即byte、int、uint、long、ulong、short、ushort、bool、逆布尔invbool或字符指针之一)和权限(S_IRUGO意味着运行用户/组/其他只有读权限)。
- 第15和49行:函数可以是任何名字(如list_init()和list_exit()),但是必须向module_init()和module_exit()宏传入相同的名字,如第56和57行。
- 第23行:printk()和printf()行数的使用方式类似,可以在内核模块代码的任何地方调用该函数。唯一重要却别是当调用printk()函数时,必须提供日志级别。日志级别在linux/kern_levels.h头文件中定义,它的值为KERN_EMERG、KERN_ALERT、KERN_CRIT、KERN_ERR、KERN_WARNING、KERN_NOTICE、KERN_INFO、KERN_DEBUG和KERN_DEFAULT之一。该头文件通过linux/printk.h文件被包含在linux/kernel.h头文件中。
从本质上讲,当模块加载时,list_init()函数将会执行。当模块卸载时,list_exit()函数会被执行。
构建模块代码
构建内核模块需要Makefile文件,事实上是一个特殊的kbuild Makefile。构建本文示例的内核模块所需要的Makefile文件如下:
Makefile
1 2 3 4 5 6 7
| obj-m+=listpro.o LINUX_KERNEL :=/lib/modules/$(shell uname -r)/build all: make -C $(LINUX_KERNEL) M=$(shell pwd) modules clean: make -C $(LINUX_KERNEL) M=$(shell pwd) clean
|
Makefile文件第一行被成为目标定义,它定义了需要构建的模块(listpro.o)。obj-m定义了可加载模块目标。当模块需要从多个目标文件构建时,语法会变得更加复杂,但这个Makefile文件对构建示例模块已经足够了。
Makefile文件中需要提醒的内容和普通Makefile文件类似。
- (shell uname -r)命令返回当前内核构建版本,这确保了一定程度的可移植性。
- C选项在执行任何make任务前将目录切换到内核目录。
- M=$(PWD)变量赋值告诉make命令实际工程文件存放位置。对于外部内核模块来说,modules目标是默认目标。另一种目标是modules_install,它将安装模块(make命令必须使用超级用户权限执行且需要提供模块安装路径)。
一切都很顺利的情况下(如已经按照前文描述安装了Linux内核头文件),构建内核模块的过程是很简单快捷的。构建步骤如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| chan@ubuntu-vm:~/Desktop/kernel$ ls -l 总用量 8 -rw------- 1 chan chan 1162 12月 6 17:37 listpro.c -rw------- 1 chan chan 176 12月 6 17:56 Makefile chan@ubuntu-vm:~/Desktop/kernel$ make make -C /lib/modules/4.4.0-38-generic/build M=/home/chan/Desktop/kernel modules make[1]: Entering directory '/usr/src/linux-headers-4.4.0-38-generic' CC [M] /home/chan/Desktop/kernel/listpro.o Building modules, stage 2. MODPOST 1 modules CC /home/chan/Desktop/kernel/listpro.mod.o LD [M] /home/chan/Desktop/kernel/listpro.ko make[1]: Leaving directory '/usr/src/linux-headers-4.4.0-38-generic' chan@ubuntu-vm:~/Desktop/kernel$ ls -l 总用量 36 -rw------- 1 chan chan 1162 12月 6 17:37 listpro.c -rw-rw-r-- 1 chan chan 5664 12月 6 18:06 listpro.ko -rw-rw-r-- 1 chan chan 867 12月 6 18:06 listpro.mod.c -rw-rw-r-- 1 chan chan 3008 12月 6 18:06 listpro.mod.o -rw-rw-r-- 1 chan chan 4752 12月 6 18:06 listpro.o -rw------- 1 chan chan 176 12月 6 17:56 Makefile -rw-rw-r-- 1 chan chan 44 12月 6 18:06 modules.order -rw-rw-r-- 1 chan chan 0 12月 6 18:06 Module.symvers chan@ubuntu-vm:~/Desktop/kernel$
|
现在,在构建目录中能够看见一个listpro.ko可加载内核模块,它的文件扩展名为.ko。
测试可加载内核模块
该模块目前能够使用内核模块工具加载:
1 2 3 4 5 6 7 8 9 10 11 12
| chan@ubuntu-vm:~/Desktop/kernel$ sudo insmod listpro.ko [sudo] chan 的密码: chan@ubuntu-vm:~/Desktop/kernel$ lsmod Module Size Used by listpro 16384 0 vmw_vsock_vmci_transport 28672 1 ... scsi_transport_spi 32768 1 mptspi e1000 135168 0 pata_acpi 16384 0 fjes 28672 0 chan@ubuntu-vm:~/Desktop/kernel$
|
通过modinfo命令,可以获得模块的信息,这个命令能够识别出模块的描述、作者和定义的任何模块参数:
1 2 3 4 5 6 7 8
| chan@ubuntu-vm:~/Desktop/kernel$ modinfo listpro.ko filename: /home/chan/Desktop/kernel/listpro.ko license: GPL srcversion: E5799ABCE0644B7E481E418 depends: vermagic: 4.4.0-38-generic SMP mod_unload modversions parm: num:int chan@ubuntu-vm:~/Desktop/kernel$
|
模块可以通过rmmod命令卸载:
1 2
| chan@ubuntu-vm:~/Desktop/kernel$ sudo rmmod listpro.ko chan@ubuntu-vm:~/Desktop/kernel$
|
重复上述步骤,可以在内核日志(/var/log/kern.log)中看见使用printk()函数输出的结果。可加载内核模块加载和卸载时的输出(中间省略),如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| Dec 6 18:08:12 ubuntu-vm kernel: [ 4428.437869] ========listpro_start======= Dec 6 18:08:12 ubuntu-vm kernel: [ 4428.437874] PID COMM Dec 6 18:08:12 ubuntu-vm kernel: [ 4428.437877] 1 systemd Dec 6 18:08:12 ubuntu-vm kernel: [ 4428.437878] 2 kthreadd Dec 6 18:08:12 ubuntu-vm kernel: [ 4428.437880] 3 ksoftirqd/0 Dec 6 18:08:12 ubuntu-vm kernel: [ 4428.437882] 5 kworker/0:0H Dec 6 18:08:12 ubuntu-vm kernel: [ 4428.437883] 7 rcu_sched ... Dec 6 18:08:12 ubuntu-vm kernel: [ 4428.438353] 7461 kworker/2:0 Dec 6 18:08:12 ubuntu-vm kernel: [ 4428.438355] 8161 kworker/1:1 Dec 6 18:08:12 ubuntu-vm kernel: [ 4428.438356] 8167 sudo Dec 6 18:08:12 ubuntu-vm kernel: [ 4428.438358] 8168 insmod Dec 6 18:13:53 ubuntu-vm kernel: [ 4428.438359] 已打印进程控制块的数量为267! Dec 6 18:13:53 ubuntu-vm kernel: [ 4768.952967] Goodbye !<1>[ 4768.952971] Dec 6 18:13:53 ubuntu-vm kernel: [ 4768.952971] =======listpro_end=======
|
测试可加载内核模块自定义参数
列表1中的代码同时包含了自定义参数,它允许在初始化时向内核模块传递参数。这个功能能够这样测试:
1 2
| chan@ubuntu-vm:~/Desktop/kernel$ sudo insmod listpro.ko num=7 chan@ubuntu-vm:~/Desktop/kernel$
|
这时如果查看/var/log/kern.log文件,会看见如下新增日志消息.
1 2 3 4 5 6 7 8 9 10 11 12
| Dec 6 18:18:07 ubuntu-vm kernel: [ 5023.185921] ========listpro_start======= Dec 6 18:18:07 ubuntu-vm kernel: [ 5023.185926] PID COMM Dec 6 18:18:07 ubuntu-vm kernel: [ 5023.185928] 0 swapper/0 Dec 6 18:18:07 ubuntu-vm kernel: [ 5023.185930] 1 systemd Dec 6 18:18:07 ubuntu-vm kernel: [ 5023.185931] 2 kthreadd Dec 6 18:18:07 ubuntu-vm kernel: [ 5023.185933] 3 ksoftirqd/0 Dec 6 18:18:07 ubuntu-vm kernel: [ 5023.185934] 5 kworker/0:0H Dec 6 18:18:07 ubuntu-vm kernel: [ 5023.185935] 7 rcu_sched Dec 6 18:18:07 ubuntu-vm kernel: [ 5023.185937] 8 rcu_bh Dec 6 18:19:53 ubuntu-vm kernel: [ 5023.185938] 已打印进程控制块的数量为7! Dec 6 18:19:53 ubuntu-vm kernel: [ 5129.251625] Goodbye !<1>[ 5129.251629] Dec 6 18:19:53 ubuntu-vm kernel: [ 5129.251629] =======listpro_end=======
|
模块被加载后,在/sys/module/目录下将出现以模块名命名的目录, 它提供了用户直接访问自定义参数状态的方式。例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| chan@ubuntu-vm:~/Desktop/kernel$ cd /sys/module/listpro/ chan@ubuntu-vm:/sys/module/listpro$ ls -l 总用量 0 -r--r--r-- 1 root root 4096 12月 6 18:37 coresize drwxr-xr-x 2 root root 0 12月 6 18:37 holders -r--r--r-- 1 root root 4096 12月 6 18:37 initsize -r--r--r-- 1 root root 4096 12月 6 18:37 initstate drwxr-xr-x 2 root root 0 12月 6 18:37 notes drwxr-xr-x 2 root root 0 12月 6 18:37 parameters -r--r--r-- 1 root root 4096 12月 6 18:37 refcnt drwxr-xr-x 2 root root 0 12月 6 18:37 sections -r--r--r-- 1 root root 4096 12月 6 18:37 srcversion -r--r--r-- 1 root root 4096 12月 6 18:37 taint --w------- 1 root root 4096 12月 6 18:37 uevent chan@ubuntu-vm:/sys/module/listpro$
|
如果此模块存在perm不为0的命令行参数则在此模块的目录下将出现parameters目录,包含一系列以参数名命名的文件节点,这些文件的权限值等于perm,文件的内容为参数的值。自定义参数查看步骤为:
1 2 3 4 5 6
| chan@ubuntu-vm:/sys/module/listpro$ cd parameters chan@ubuntu-vm:/sys/module/listpro/parameters$ ls num chan@ubuntu-vm:/sys/module/listpro/parameters$ cat num -1 chan@ubuntu-vm:/sys/module/listpro/parameters$
|
这里num变量的状态可以查看到,并且读取这个值不需要超级用户权限。这是因为在定义内核参数的时候使用了S_IRUGO参数。这个值还能够设置为可写,但是模块代码中将会需要检测状态变化并依据变化做出响应。
总结
在内核中,进程控制块被组织成多个双向链表,其中有一个双向链表包含所有的进程控制块,只需要访问该双向链表,就可以访问到所有进程控制块。
尽管这个模块功能比较简单,但通过这次课程设计,对可加载内核模块如何工作有了概要认识,大致了解了构建、加载、卸载了内核模块的方法,过程,同时了解了可加载内核模块自定义参数的设置。