前言

之前成功编译了内核,这次学习如何修改增加删除内核模块,为了保证内核的纯净,我特意重新编译安装了一个新的5.11.8的内核,其他内核同理。
本文原创,创作不易,转载请注明!!!
本文链接
个人博客:https://ronglin.fun/?p=169
PDF链接:见博客网站
CSDN: https://blog.csdn.net/RongLin02/article/details/115422459

通过内核模块显示进程控制块信息

环境

主机:联想Y7000P;64位windows10;CPU:i7-9750H;显卡:GTX 1660 Ti;内存:16G
虚拟机:Ubuntu 18.04 LTS;硬盘100G;内存4G;64位;4核心
Linux内核:5.11.8

实验简述

首先先看实验内容,实验内容来自指导书,如有侵权联系我删除:

实验说明

在内核中,所有进程控制块都被一个双向链表连接起来,该链表中的第一个进程控制块为init_task。编写一个内核模块,模块接收用户传递的一个参数num,num指定要打印的进程控制块的数量;若用户不指定num或者num<0,模块则打印所有进程控制块的信息。需要打印的进程控制块信息有:进程PID和进程的可执行文件名。

解决方案

(1)定义模块参数
该模块需要接受用户传递的参数,在使用该参数之前,需要在代码中预先定义好该参数,将该参数的类型设置为整型,并且在sysfs文件系统中的权限是只读的。定义的方法为:
static int num=-1;
module _param(num, int, S_IRUGO);
该参数的初始值被设置为-1。-1将作为打印所有进程控制块的标记,默认值为-1,意味着当用户不传入任何参数时,模块将打印所有的进程的信息。
(2)访问进程控制块链表
在内核中,进程控制块被组织成多个双向链表,其中有一个双向链表包含所有的进程
精袈
块,只需要访问该双向链表,就可以访问到所有进程控制块。Linux内核中几乎所有双向
都采用相同的数据结构来实现,内核中定义list_head通用数据结构,其定义如下:

struct list_head {
    struct list head *next,*prev;
};

list_head中,next指向链表中的下一个list_head 数据结构,prev指向链表中的前一个list_head 数据结构。之所以说该结构是用于实现一个通用的双向链表是因为:如果一个数据结构包含list_head结构,开发者就可以通过内核提供的一组宏创建并操作一个双向链表,而该链表中元素的类型为该数据结构,如图12-1所示:
在这里插入图片描述
在进程控制块task_struct中,包含一个名为tasks 的成员,该成员的类型为list_head,这意味着进程控制块能够通过该成员将进程控制块串成一个双向链表。Linux内核通过该成员将所有的进程都放入同一个双向链表,因为 list_head 结构中的next 和 prev指针并不是指向包含list_head 的数据结构,而是指向另一个list_head数据结构。为了访问包含list_head的数据结构,内核提供一个宏:

list_entry(ptr,type,member);

在该宏中, ptr是一个指向list_head的指针, type是包含list_head的数据结构类型,而member是list_head在该数据结构中的成员名。例如,若一个进程控制块中的tasks 的地址为p,为了访问该进程控制块,可以采用:

list_entry(p,struct task _struct,tasks);

该宏便会返回该进程控制块的地址。
知道如何使用双向链表后,就可以方便地访问内核中所有的进程控制块,因为它通过tasks成员串成一个双向链表,如果得到一个进程控制块的地址p,开发者可以通过:

list_entry(p->tasks.next, struct task _struct, tasks);

访问该双向链表中的下一个进程控制块。在该双向链表中,第一个进程控制块为init_task,如果开发者发现下一个进程控制块为init_task时,说明已经完整地遍历过所有进程控制块。
内核定义宏for_each process用于遍历所有的进程控制块,开发者通过该宏就能将所有的进程控制块访问一遍,该宏展开的形式为:

for (= &init_task ; (p= list_entry((p)->tasks.next, struct task_struct, tasks) !=&init_task ; )

(3)输出进程控制块信息
进程控制块中包含进程大部分信息,根据实验要求,模块需要打印进程的 pid和可执行文件名,在进程控制块的数据结构中,成员pid为进程的PD,而成员comm包含进程的可执行文件名。在内核中,模块可以通过 printk( )内核函数将这些信息打印到系统日志中。

程序框架

在这里插入图片描述
在这里插入图片描述

实操

实验说明太多了,看的有点头大,幸好它给了源码,直接上手实操,出了问题再看资料。

模块文件知识

先把程序框架中的代码敲出来,exp_exit()函数中就添加一句

printk("Good bye,Ronglin\n");

先大概的讲述一下c文件的内容:
头文件声明
头两行是模块头文件,头文件module.h和 init.h是必不可少的。module.h包含加载模块需要的函数和符号定义; init.h中包含模块初始化和清理函数的定义。如果在加载时允许用户传递参数,模块还应该包含moduleparam.h头文件
模块许可声明
从内核v2.4.10版本开始,模块必须通过MODULE_LICENSE宏声明此模块的许可证,否则在加载此模块时,内核会显示“kernel tainted”(内核被污染)的警告信息。从linux/module.h文件中可看到,被内核接受的许可证有GPL,GPL v2,GPL and additional rights,Dual BSD/GPL,Dual MPL/GPL,Dual MIT/GPL和Proprietary。
初始化和清理函数
声明内核模块必须调用宏module_init和 module_exit 去注册初始化与清理函数。在模块源代码的最后两行已声明该模块被加载时的初始化函数是exp_init(),模块被卸载时的清理函数是 exp_exit()。需要注意,初始化与清理函数必须在宏module_init和 module_exit使用前定义,否则会出现编译错误。这两个函数配对使用,例如,当module_init()申请一个资源,那么module_exit()中就应释放这个资源,使得模块不留下任何副作用。
一般来说有这上述3部分足以。

大概了解了c文件结构,再来看看makefile文件的结构

obj-m:=listprocess.o
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
PWD       := $(shell pwd)

default:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
	obj-m:=listprocess.o

其实核心部分就是这一句,obj-m:=listprocess.o,生成的内核模块名为listprocess.ko。如果需要生成一个名为mymodule.ko 的内核模块,并且该内核模块的源代码来源于modulesrc1.c和 modulesrc2.c两个文件, makefile文件应该写成如下形式:

obj -m:-mymodule.o
module objs:-modulesrc1.o modulesrc2.o

如果用户采用这种makefile,在调用make命令时,需要将内核源代码所在目录作为一个参数传递给make命令。
例如,如果v2.6的内核源代码位于/usr/src/linux-2.6目录下,用户模块源代码所在目录应该使用的make命令为:

make -C/usrlsrc/linux-2.6 M='pwd'modules

makefile还提供另一种形式,用户可指定内核源代码所在的目录,而不用每次都把该目录作为参数传递给make命令。对于“hello world!”示例,在 makefile 中指定内核源代码的方式为上面的开头的代码。
在这个makefile文件中,KERNELDIR指定内核的源代码目录,该目录通过当前运行内核使用的模块目录中的build符号链接指定。
巴拉巴拉这么一大堆理论看着头疼马上开始实操。

安装模块

将两个文件放在Ubuntu下,并且命名为mykernel。本机文件结构如下图
在这里插入图片描述
mykernel文件夹下,直接make编译,如果没有make指令的话,可以输入sudo apt install make安装make。
输入

sudo make

直接make编译报错了,看输出信息
在这里插入图片描述

error: function declaration isn’t a prototype [-Werror=strict-prototypes]

查了资料,是函数参数表没指明,如果没有参数表应该写void而不能为空,用nano打开.c文件,改一下,输入

sudo nano listprocess.c

exp_initexp_exit的括号里加上void
在这里插入图片描述
然后Ctrl+O写入,回车确定,然后Ctrl+X退出,然后再输入sudo make编译
在这里插入图片描述
又报错了,提示

obj-m:=listprocess.o
/bin/sh: 1: obj-m:=listprocess.o: not found
Makefile:6: recipe for target 'default' failed
make: *** [default] Error 127

用nano打开makefile,然后然后删除最后一句,输入

sudo nano Makefile

在这里插入图片描述
删除最后一句的obj-m:=listprocess.o
然后Ctrl+O写入,回车确定,然后Ctrl+X退出,然后再输入sudo make编译
没有报错了
开始安装,输入

sudo insmod listprocess.ko

注意是.ko文件,然后输入dmesg查看效果
有了pid,成功了,卸载的话输入

sudo rmmod listprocess.ko

然后输入dmesg查看效果
在这里插入图片描述
卸载成功,模块测试成功,=w=

补充:
问题:for_each_process报错
原因:有些人编译的时候会有报错没找到 for_each_process 函数,可能是对于不同的内核版本,for_each_process 位置不同,可以去查看一下源码,找一下for_each_process 函数。以本人尝试结果为例,4.11以后,for_each_process 在 include/linux/sched/signal.h中。
解决方案:在.c文件的头中加入一行代码#include <linux/sched/signal.h>就行了

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐