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

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

Makefile

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

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

img

结合代码进行分析:

show_task_family.c

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

Makefile

# 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 循环,三个参数分别是 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 中。

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

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

相关阅读