0%

【OS】HDU-OS-Lab2-Linux内核模块编程

Linux内核采用了整体结构,上一个实验体会了编译内核时间的冗长与繁杂,一步错就要重新编译,这虽然提高了效率,但同时也让后续的维护变得困难,在这个基础上,Linux内核引入了动态模块机制加以改进。

视频教程地址:

https://www.bilibili.com/video/av47412869/

源码地址:

https://github.com/leslievan/Operator_System/tree/master/Operator_System_Lab2

实验内容

  • 设计一个模块,要求列出系统中所有内核线程的程序名、PID、进程状态、进程优先级、父进程的PID。
  • 设计一个带参数的模块,其参数为某个进程的PID号,模块的功能是列出该进程的家族信息,包括父进程、兄弟进程和子进程的程序名、PID号及进程状态。
  • 请根据自身情况,进一步阅读分析程序中用到的相关内核函数的源码实现。

代码设计

实验分为两部分,一个让设计出一个不带参数的模块,功能是列出所有内核线程的程序名,称这个模块为show_all_kernel_thread,另一个要求是设计出一个带参数的模块,参数为某个进程的PID号,列出这个进程的父进程、子进程和兄弟进程,称这个模块为show_task_family

show_all_kernel_thread

可以根据功能写出大致的流程图:

结合代码进行分析:

show_all_kernel_thread.cview raw___
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
#include "linux/init.h"
#include "linux/module.h"
#include "linux/kernel.h"
#include "linux/sched/signal.h"
#include "linux/sched.h"

MODULE_LICENSE("GPL");

// 模块在加载的时候会运行init函数
static int __init show_all_kernel_thread_init(void)
{
// 格式化输出头
struct task_struct *p;
printk("%-20s%-6s%-6s%-6s%-6s", "Name", "PID", "State", "Prio", "PPID");
printk("--------------------------------------------");

// for_each_process(p)的作用是从0开始遍历进程链表中的所有进程
for_each_process(p)
{
// p最开始指向进程链表中第一个进程,随着循环不断进行p也不断后移直至链表尾
if (p->mm == NULL)
{
// 打印进程p的相关信息
printk("%-20s%-6d%-6d%-6d%-6d", p->comm, p->pid, p->state, p->prio,
p->parent->pid);
}
}

return 0;
}

// 模块在加载的时候会运行exit函数
static void __exit show_all_kernel_thread_exit(void)
{
printk("[ShowAllKernelThread] Module Uninstalled.");
}

module_init(show_all_kernel_thread_init);
module_exit(show_all_kernel_thread_exit);
Makefileview raw___
1
2
3
4
5
6
7
8
9
obj-m := show_all_kernel_thread.o
# kernel directory 源码所在文件夹,这里直接指向了系统文件中的内核源码,也可以将该路径改为你下载的源码路径
KDIR := /lib/modules/$(shell uname -r)/build
# 当前路径
PWD := $(shell pwd)
default:
make -C $(KDIR) M=$(PWD) modules
clean:
make -C $(KDIR) M=$(PWD) clean

这里用到了名为for_each_process(p)的宏定义,可以从include/linux/sched/signal.h中找到这个宏定义的具体代码:

1
2
#define for_each_process(p) \
for (p = &init_task ; (p = next_task(p)) != &init_task ; )

这个宏定义较为简单这里不过多解释,有问题可在评论区一起探讨。

show_task_family

同样地,可以根据功能写出大致的流程图:

结合代码进行分析:

show_task_family.cview raw___
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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
// created by 19-03-26
// Arcana
#include "linux/init.h"
#include "linux/module.h"
#include "linux/kernel.h"
#include "linux/moduleparam.h"
#include "linux/pid.h"
#include "linux/list.h"
#include "linux/sched.h"

MODULE_LICENSE("GPL");
static int pid;
module_param(pid, int, 0644);

static int __init show_task_family_init(void)
{
struct pid *ppid;
struct task_struct *p;
struct task_struct *pos;
char *ptype[4] = {"[I]", "[P]", "[S]", "[C]"};

// 通过进程的PID号pid一步步找到进程的进程控制块p
ppid = find_get_pid(pid);
if (ppid == NULL)
{
printk("[ShowTaskFamily] Error, PID not exists.\n");
return -1;
}
p = pid_task(ppid, PIDTYPE_PID);

// 格式化输出表头
printk("%-10s%-20s%-6s%-6s\n", "Type", "Name", "PID", "State");
printk("------------------------------------------\n");

// Itself
// 打印自身信息
printk("%-10s%-20s%-6d%-6d\n", ptype[0], p->comm, p->pid, p->state);

// Parent
// 打印父进程信息
printk("%-10s%-20s%-6d%-6d\n", ptype[1], p->real_parent->comm,
p->real_parent->pid, p->real_parent->state);

// Siblings
// 遍历父进程的子,即我的兄弟进程,输出信息
// 「我」同样是父进程的子进程,所以当二者进程PID号一致时,跳过不输出
list_for_each_entry(pos, &(p->real_parent->children), sibling)
{
if (pos->pid == pid)
continue;
printk("%-10s%-20s%-6d%-6d\n", ptype[2], pos->comm, pos->pid,
pos->state);
}

// Children
// 遍历「我」的子进程,输出信息
list_for_each_entry(pos, &(p->children), sibling)
{
printk("%-10s%-20s%-6d%-6d\n", ptype[3], pos->comm, pos->pid,
pos->state);
}

return 0;
}

static void __exit show_task_family_exit(void)
{
printk("[ShowTaskFamily] Module Uninstalled.\n");
}

module_init(show_task_family_init);
module_exit(show_task_family_exit);
Makefileview raw___
1
2
3
4
5
6
7
8
9
# created by 19-03-26
# Arcana
obj-m := show_task_family.o
KDIR := /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
default:
make -C $(KDIR) M=$(PWD) modules
clean:
make -C $(KDIR) M=$(PWD) clean

这个模块中最复杂的部分是list_for_each_entry。它是位于include/linux/list.h中的一个宏定义:

1
2
3
4
5
6
7
8
9
/*
struct task_struct *pos;
list_for_each_entry(pos, &pos->children, sibling);
*/

#define list_for_each_entry(pos, head, member) \
for (pos = __container_of((head)->next, pos, member); \
&pos->member != (head); \
pos = __container_of(pos->member.next, pos, member))

可以看到,展开之后它是一个清晰的for循环,三个参数分别是posheadmember,关于这个宏定义建议大家仔细阅读书本7.3.4。这是一个非常奇妙的操作,基于此它可以将任意一个结构体都附上链表的功能,只需要将一个叫做list_head的数据结构放在结构体中,这一部分理解起来可能稍微复杂,这里只讲解用法,有兴趣的同学可以自行研究。

pos是数据项类型的指针,比如这里需要使用task_struct类型的数据,所以在上面的示例中,将pos声明为task_struct *类型.

剩下两个参数结合下图理解:

这里的*.children*.sibling均为list_head *类型的变量,是task_struct的一个成员,在这里,parent.children.next指向的是children1.sibling,而children4.sibling.next指向的是parent.children,它是一个双向循环链表,这里只标注出了next的一侧,隐去了prev的一侧。

第二个参数head是表头的地址,在这里就表示为&parent.children,第三个参数member指的是在链表中,list_head *的位置,可能会混淆的是,task_struct中的两个成员变量childrensibling都是list_head *类型,为什么选择sibling而不是children呢?我个人的理解是,children只是一个引子,代表一个参照物,真正进行中转的变量是sibling,才疏学浅,表达不太准确,有兴趣的同学可以自行查阅资料。

编译 & 安装

模块编译的命令与第一个实验中内核编译的命令是一致的,实际上make命令能做的远不止编译内核和编译模块。

上面一共四个文件,分为两个文件夹储存,这里将文件夹命名为AB,把show_all_kernel_thread.c和与之对应的Makefile文件放到A中,自然地,把show_task_family.c和与之对应的Makefile文件放到B中。

1
2
3
# 将A改为当前目录,开始编译
cd A
make

编译成功之后你的A目录下应该有这些文件,同理你可以在B目录下进行同样的操作:

1
2
3
4
5
6
7
8
9
.
├── Makefile
├── modules.order
├── Module.symvers
├── show_all_kernel_thread.c
├── show_all_kernel_thread.ko
├── show_all_kernel_thread.mod.c
├── show_all_kernel_thread.mod.o
└── show_all_kernel_thread.o

检验文件后,开始安装模块

1
2
3
4
5
6
7
8
9
# 安装
# without parameter
sudo insmod show_all_kernel_thread.ko
# with parameter
sudo insmod show_task_family pid=xxxx

# 卸载(在整个文件结束之后
sudo rmmod show_all_kernel_thread
sudo rmmod show_task_family

测试

前文提到,当模块被加载时,会运行init函数,在退出时,会运行exit函数。printk函数将输出打印到了日志中,可以使用dmesg命令查看系统日志,如果有遗留下的痕迹,且是正确答案,则代表测试成功。

show_all_kernel_thread要求显示出所有内核线程,测试步骤可如下:

1
2
make && sudo insmod show_all_kernel_thread.ko
dmesg

开启另一个终端,输入ps命令:

1
ps -aux

此时会显示出所有线程,线程名带有[]的即为内核线程,稍微挑选一二能对上即可。


show_task_family要求显示出某一个进程的家族关系,测试步骤可如下:

使用pstree命令查看进程树:

1
pstree -p

选择一个既有兄弟进程又有子进程的进程(建议使用systemd,使用此进程作为测试,可以看到这个进程的PID。

1
2
make && sudo insmod show_task_family.ko pid=xxxx
dmesg

开启另一个终端,输入pstree命令:

1
pstree -p xxxx

xxxx是刚刚选中的那个进程号。

对比进程树与系统日志中的记录,选择一二能对上即可。

相关阅读