「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
可以根据功能写出大致的流程图:
结合代码进行分析:
1#include "linux/init.h"
2#include "linux/module.h"
3#include "linux/kernel.h"
4#include "linux/sched/signal.h"
5#include "linux/sched.h"
6
7MODULE_LICENSE("GPL");
8
9// 模块在加载的时候会运行init函数
10static int __init show_all_kernel_thread_init(void)
11{
12 // 格式化输出头
13 struct task_struct *p;
14 printk("%-20s%-6s%-6s%-6s%-6s", "Name", "PID", "State", "Prio", "PPID");
15 printk("--------------------------------------------");
16
17 // for_each_process(p)的作用是从0开始遍历进程链表中的所有进程
18 for_each_process(p)
19 {
20 // p最开始指向进程链表中第一个进程,随着循环不断进行p也不断后移直至链表尾
21 if (p->mm == NULL)
22 {
23 // 打印进程p的相关信息
24 printk("%-20s%-6d%-6d%-6d%-6d", p->comm, p->pid, p->state, p->prio,
25 p->parent->pid);
26 }
27 }
28
29 return 0;
30}
31
32// 模块在加载的时候会运行exit函数
33static void __exit show_all_kernel_thread_exit(void)
34{
35 printk("[ShowAllKernelThread] Module Uninstalled.");
36}
37
38module_init(show_all_kernel_thread_init);
39module_exit(show_all_kernel_thread_exit);
1obj-m := show_all_kernel_thread.o
2# kernel directory 源码所在文件夹,这里直接指向了系统文件中的内核源码,也可以将该路径改为你下载的源码路径
3KDIR := /lib/modules/$(shell uname -r)/build
4# 当前路径
5PWD := $(shell pwd)
6default:
7 make -C $(KDIR) M=$(PWD) modules
8clean:
9 make -C $(KDIR) M=$(PWD) clean
这里用到了名为 for_each_process(p)
的宏定义,可以从 include/linux/sched/signal.h
中找到这个宏定义的具体代码:
1#define for_each_process(p) \
2 for (p = &init_task ; (p = next_task(p)) != &init_task ; )
这个宏定义较为简单这里不过多解释,有问题可在评论区一起探讨。
show_task_family
同样地,可以根据功能写出大致的流程图:
结合代码进行分析:
1// created by 19-03-26
2// Arcana
3#include "linux/init.h"
4#include "linux/module.h"
5#include "linux/kernel.h"
6#include "linux/moduleparam.h"
7#include "linux/pid.h"
8#include "linux/list.h"
9#include "linux/sched.h"
10
11MODULE_LICENSE("GPL");
12static int pid;
13module_param(pid, int, 0644);
14
15static int __init show_task_family_init(void)
16{
17 struct pid *ppid;
18 struct task_struct *p;
19 struct task_struct *pos;
20 char *ptype[4] = {"[I]", "[P]", "[S]", "[C]"};
21
22 // 通过进程的PID号pid一步步找到进程的进程控制块p
23 ppid = find_get_pid(pid);
24 if (ppid == NULL)
25 {
26 printk("[ShowTaskFamily] Error, PID not exists.\n");
27 return -1;
28 }
29 p = pid_task(ppid, PIDTYPE_PID);
30
31 // 格式化输出表头
32 printk("%-10s%-20s%-6s%-6s\n", "Type", "Name", "PID", "State");
33 printk("------------------------------------------\n");
34
35 // Itself
36 // 打印自身信息
37 printk("%-10s%-20s%-6d%-6d\n", ptype[0], p->comm, p->pid, p->state);
38
39 // Parent
40 // 打印父进程信息
41 printk("%-10s%-20s%-6d%-6d\n", ptype[1], p->real_parent->comm,
42 p->real_parent->pid, p->real_parent->state);
43
44 // Siblings
45 // 遍历父进程的子,即我的兄弟进程,输出信息
46 // 「我」同样是父进程的子进程,所以当二者进程PID号一致时,跳过不输出
47 list_for_each_entry(pos, &(p->real_parent->children), sibling)
48 {
49 if (pos->pid == pid)
50 continue;
51 printk("%-10s%-20s%-6d%-6d\n", ptype[2], pos->comm, pos->pid,
52 pos->state);
53 }
54
55 // Children
56 // 遍历「我」的子进程,输出信息
57 list_for_each_entry(pos, &(p->children), sibling)
58 {
59 printk("%-10s%-20s%-6d%-6d\n", ptype[3], pos->comm, pos->pid,
60 pos->state);
61 }
62
63 return 0;
64}
65
66static void __exit show_task_family_exit(void)
67{
68 printk("[ShowTaskFamily] Module Uninstalled.\n");
69}
70
71module_init(show_task_family_init);
72module_exit(show_task_family_exit);
1# created by 19-03-26
2# Arcana
3obj-m := show_task_family.o
4KDIR := /lib/modules/$(shell uname -r)/build
5PWD := $(shell pwd)
6default:
7 make -C $(KDIR) M=$(PWD) modules
8clean:
9 make -C $(KDIR) M=$(PWD) clean
这个模块中最复杂的部分是 list_for_each_entry
。它是位于 include/linux/list.h
中的一个宏定义:
1/*
2 struct task_struct *pos;
3 list_for_each_entry(pos, &pos->children, sibling);
4*/
5
6#define list_for_each_entry(pos, head, member) \
7 for (pos = __container_of((head)->next, pos, member); \
8 &pos->member != (head); \
9 pos = __container_of(pos->member.next, pos, member))
可以看到,展开之后它是一个清晰的 for 循环,三个参数分别是 pos
、head
和 member
,关于这个宏定义建议大家仔细阅读书本 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
中的两个成员变量 children
和 sibling
都是 list_head *
类型,为什么选择 sibling
而不是 children
呢?我个人的理解是,children
只是一个引子,代表一个参照物,真正进行中转的变量是 sibling
,才疏学浅,表达不太准确,有兴趣的同学可以自行查阅资料。
编译 & 安装
模块编译的命令与第一个实验中内核编译的命令是一致的,实际上 make
命令能做的远不止编译内核和编译模块。
上面一共四个文件,分为两个文件夹储存,这里将文件夹命名为 A
和 B
,把 show_all_kernel_thread.c
和与之对应的 Makefile
文件放到 A
中,自然地,把 show_task_family.c
和与之对应的 Makefile
文件放到 B
中。
1# 将A改为当前目录,开始编译
2cd A
3make
编译成功之后你的 A 目录下应该有这些文件,同理你可以在 B 目录下进行同样的操作:
1.
2├── Makefile
3├── modules.order
4├── Module.symvers
5├── show_all_kernel_thread.c
6├── show_all_kernel_thread.ko
7├── show_all_kernel_thread.mod.c
8├── show_all_kernel_thread.mod.o
9└── show_all_kernel_thread.o
检验文件后,开始安装模块
1# 安装
2# without parameter
3sudo insmod show_all_kernel_thread.ko
4# with parameter
5sudo insmod show_task_family pid=xxxx
6
7# 卸载(在整个文件结束之后
8sudo rmmod show_all_kernel_thread
9sudo rmmod show_task_family
测试
前文提到,当模块被加载时,会运行 init
函数,在退出时,会运行 exit
函数。printk
函数将输出打印到了日志中,可以使用 dmesg
命令查看系统日志,如果有遗留下的痕迹,且是正确答案,则代表测试成功。
show_all_kernel_thread
要求显示出所有内核线程,测试步骤可如下:
1make && sudo insmod show_all_kernel_thread.ko
2dmesg
开启另一个终端,输入 ps
命令:
1ps -aux
此时会显示出所有线程,线程名带有 []
的即为内核线程,稍微挑选一二能对上即可。
show_task_family
要求显示出某一个进程的家族关系,测试步骤可如下:
使用 pstree
命令查看进程树:
1pstree -p
选择一个既有兄弟进程又有子进程的进程(建议使用 systemd
,使用此进程作为测试,可以看到这个进程的 PID。
1make && sudo insmod show_task_family.ko pid=xxxx
2dmesg
开启另一个终端,输入 pstree
命令:
1pstree -p xxxx
xxxx
是刚刚选中的那个进程号。
对比进程树与系统日志中的记录,选择一二能对上即可。