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
可以根据功能写出大致的流程图:
结合代码进行分析:
#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);
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
中找到这个宏定义的具体代码:
#define for_each_process(p) \
for (p = &init_task ; (p = next_task(p)) != &init_task ; )
这个宏定义较为简单这里不过多解释,有问题可在评论区一起探讨。
show_task_family
同样地,可以根据功能写出大致的流程图:
结合代码进行分析:
// 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);
# 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
中的一个宏定义:
/*
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 循环,三个参数分别是 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
中。
# 将A改为当前目录,开始编译
cd A
make
编译成功之后你的 A 目录下应该有这些文件,同理你可以在 B 目录下进行同样的操作:
.
├── 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
检验文件后,开始安装模块
# 安装
# 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
要求显示出所有内核线程,测试步骤可如下:
make && sudo insmod show_all_kernel_thread.ko
dmesg
开启另一个终端,输入 ps
命令:
ps -aux
此时会显示出所有线程,线程名带有 []
的即为内核线程,稍微挑选一二能对上即可。
show_task_family
要求显示出某一个进程的家族关系,测试步骤可如下:
使用 pstree
命令查看进程树:
pstree -p
选择一个既有兄弟进程又有子进程的进程(建议使用 systemd
,使用此进程作为测试,可以看到这个进程的 PID。
make && sudo insmod show_task_family.ko pid=xxxx
dmesg
开启另一个终端,输入 pstree
命令:
pstree -p xxxx
xxxx
是刚刚选中的那个进程号。
对比进程树与系统日志中的记录,选择一二能对上即可。