操作系统课程设计分析报告-内核模块

Author Avatar
thychan Dec 06, 2016
  • Read this article on other devices

[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; // 可加载内核模块参数,这里默认值设置为“-1”
module_param(num, int, S_IRUGO); // 参数描述,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) //按用户指定的num打印进程控制块的数量
{
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参数。这个值还能够设置为可写,但是模块代码中将会需要检测状态变化并依据变化做出响应。

总结

在内核中,进程控制块被组织成多个双向链表,其中有一个双向链表包含所有的进程控制块,只需要访问该双向链表,就可以访问到所有进程控制块。

尽管这个模块功能比较简单,但通过这次课程设计,对可加载内核模块如何工作有了概要认识,大致了解了构建、加载、卸载了内核模块的方法,过程,同时了解了可加载内核模块自定义参数的设置。