「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

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

img

结合代码进行分析:

show_all_kernel_thread.c

 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);

Makefile

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

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

img

结合代码进行分析:

show_task_family.c

 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);

Makefile

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

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

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

img

这里的 *.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# 将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 是刚刚选中的那个进程号。

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

相关阅读