操作系统leb4实验报告

实验名称:实验4:内核模块

实验目的

  • 模块是Linux系统的一种特有机制,可用以动态扩展操作系统内核功能。编写实现某些特定功能的模块,将其作为内核的一部分在管态下运行。

实验内容

  1. 本实验由两个子任务组成:

(1)设计一个模块,该模块的功能是列出系统中所有内核线程的程序名、PID号和进程状态。
(2)设计一个带参数的内核模块,其参数为某个进程的PID号,该模块的功能是列出该进程的家族信息,包括父进程、兄弟进程和子进程的程序名、PID号。

实验环境

  1. VMware
  2. Ubuntu

实验作业

一、Linux内核模块简介

Linux内核是单体的( monolithic),即编译器把各个内核组件链接生成一个大的可执行文件。另一种内核结构是微内核(microkernel),它只把一些最基本的功能,如进程间通信、同步原语,做入内核,其他(如文件系统、存储器管理、设备驱动等)都作为用户态进程出现,相对普通的应用程序来讲,它们可以看成服务器进程,为应用程序提供服务。微内核有许多理论上的优势,如模块化更好、易于移植、更加稳定、不易崩溃等,但是在性能方面一直比不上单体内核,因为微内核体系导致的进程间通信开销非常大。
Linux的内核模块( module)机制不仅可以弥补单体内核相对微内核的一些不足,而且对性能没有影响。内核模块是一个目标文件,可以动态载入内核,也可以动态卸载。实际上,Linux中大多数设备驱动程序或文件系统都以模块方式实现,因为它们数目繁多,体积庞大,不适合直接编译在内核中。而通过模块机制,在需要使用它们的时候再临时加载,是最适合不过的。另外一个明显的好处是,当采用模块技术进行开发时,用户修改代码后只需重新编译加载模块,而不必重新编译内核和引导系统。
当一个模块被加载到内核中的时候,它就成为内核代码的一部分,与其他内核代码的地位完全相同。模块自身不是一个独立的进程,当前进程运行时调用到模块代码时,我们认为该段代码就代表当前进程在核心态运行。值得注意的是,由于模块中的代码与内核中其他部分的代码地位相同,因此模块中的一个代码错误就可能导致整个系统的崩溃。

二、内核模块编程实例

测试代码:

hello.c

#include<linux/init.h>
#include<linux/module.h>
MODULE_LICENSE("GPL");

static int hello_init(void)
{
	printk(KERN_ALERT"Hello,world\n");
	return 0;
}

static void hello_exit(void)
{
	printk(KERN_ALERT"Goodbye\n");
}

module_init(hello_init);
module_exit(hello_exit);

Makefile

ifneq ($(KERNELRELEASE),)
	obj-m :=hello.o
else
	KDIR :=/lib/modules/$(shell uname -r)/build
	PWD :=$(shell pwd)

default:
	$(MAKE) -C $(KDIR) M=$(PWD) modules
clean:
	$(MAKE) -C $(KDIR) M=$(PWD) clean
endif

运行图:
![image.png](https://img-blog.csdnimg.cn/img_convert/86eae8f92db68142e26c6711a373301f.png#clientId=u11c57951-47a3-4&from=paste&height=67&id=ubfea3769&margin=[object Object]&name=image.png&originHeight=90&originWidth=671&originalType=binary&ratio=1&size=15326&status=done&style=none&taskId=u3e35fa4c-f285-4f3c-9cf7-61d3fedd8af&width=503)
![image.png](https://img-blog.csdnimg.cn/img_convert/cb2478153732b8167e9ee9022df23606.png#clientId=u11c57951-47a3-4&from=paste&height=22&id=u55de023a&margin=[object Object]&name=image.png&originHeight=30&originWidth=659&originalType=binary&ratio=1&size=5881&status=done&style=none&taskId=ue466fe73-06d0-4355-8c36-709aeb422ac&width=494)
![](https://img-blog.csdnimg.cn/img_convert/8ce3379a0a645b07a440afa1d3649ee0.png#from=url&id=CnCp5&margin=[object Object]&originHeight=64&originWidth=602&originalType=binary&ratio=1&status=done&style=none)

三、模块编程基础知识

先简要介绍内核模块编程和用户态编程的区别。首先,内核模块编程不能使用C函数库,内核模块只能使用一些内核函数,以hello模块为例,输出信息时使用内核函数 printk,而不是标准库函数 printf。其次,内核模块代码运行在核心态,这意味着函数使用的栈是核心栈,该空间极为有限,一般是 4KB或8KB,所以不要定义占用空间很大的自动变量,如果确实需要使用大的结构,建议使用动态分配的空间。最后,内核代码不能使用浮点运算,这样做是为了节省开销。
hello模块最前面两行包含两个内核头文件,Linux内核的大部分头文件位于内核源代码include目录下,所以 include目录是默认指定的。以linux/init.h来说,实际就是内核目录下的include/linux/init.h文件。宏module_init和 module_exit的定义在 linux/init.h中,但为什么要包含linux/module.h 就不是十分明显了,实际上 hello模块还调用了另外一个内核函数printk,该函数的原型声明在 linux/kernel.h中,但是 linux/module.h间接包含了linux/kernel.h,因为大部分实际内核模块都用到了linux/module.h,出于介绍的目的,在此我们用linux/module.h取代了linux/kernel.h。
内核模块程序里面没有main函数,每个模块都应该定义两个函数,一个函数用来初始化,常用来注册和申请资源,该函数返回0,表示初始化成功(见示例第8行),其他值表示初始化失败;另一个函数用来退出,常用来注销和释放资源。一般这两个函数分别用宏module_init和 module_exit 来标识,module_init标记的函数在加载模块时调用,module_exit标记的函数在卸载模块时调用。对于 hello模块来说,该模块仅有的两个函数 hello_init和hello_exit便起上述作用,只是它们的功能极为简单,各自输出一行信息而已。
示例第3行调用MODULE_LICENSE告诉内核该模块采用GPL许可,其他允许的许可还包括"GPL v2"、“GPL and additional rights”、“Dual BSD/GPL” “Dual MITIGPL”、“Dual
MPLGPL”,内核均认为以上许可是与GPL兼容的。有些商业公司发布的模块仅以二进制方式分发,不提供源代码,这种情况下模块许可被认为"Proprietary”,内核开发者一般不愿意对这种模块的用户提供技术帮助。如果未给模块指定许可,加载模块时很可能出现“modulelicense 'unspecified' taints kernel”之类的信息。
示例第7和12行调用的内核输出函数 printk 原型如下:asmlinkage int printk(const char*fmt,…);
可以看到,printk 与标准C库函数 printf几乎一样,实际上两者用法也类似。但是printk的 fmt首部一般是如KERN_ALERT之类的优先级,如果没有优先级,则采用系统默认值。printk允许的优先级范围从0~7,值越低表示优先级越高,它们的符号标记依次是KERN_EMERG,KERN_ALERT,KERN_CRIT, KERN_ERR ,KERN_WARNING ,KERN_NOTICE,KERN_INFO和KERN_DEBUG。优先级和系统设置决定了printk中的信息以何种方式输出,这其中牵涉较多内容,在此就不详细介绍了。如果用户在文本控制台环境下,printk 产生的信息可以直接显示,但如果用户在X图形界面下的仿真终端环境下,信息不会直接显示。无论如何,printk信息一般会被添加到文件/var/log/messages 的尾部,所以总是可以通过如下命令获取 printk的输出信息:
#tail /var/log/messages,当然也可以通过dmesg命令查看内核缓冲
在加载模块时还可以传递参数,模块参数通过宏module_param声明,该宏定义在文件linux/moduleparam.h中。
module_param带三个参数,
第一个参数是变量名;
第二个参数是变量类型,目前支持的有int,charp(字符指针),long 等;
第三个参数是许可标志位,目前设为S_IRUGO即可。稍微修改一下hello.c来说明如何控制模块参数。
修改后代码:

#include<linux/init.h>
#include<linux/module.h>
#include<linux/moduleparam.h>
MODULE_LICENSE("GPL");
static char * my_string="parameter";
static int my_int =1;
module_param(my_string,charp,S_IRUGO);
module_param(my_int,int,S_IRUGO);

static int helloin(void)
{
	printk(KERN_ALERT "Hello,world\n");
	printk(KERN_ALERT "%s %d \n",my_string,my_int);
	return 0;
	
}

static void helloout(void)
{
	printk(KERN_ALERT "Goodbye\n");

}

module_init(helloin);

module_exit(helloout);

运行图:
![](https://img-blog.csdnimg.cn/img_convert/6573faf9a296ea2db2441d76a94589a1.png#from=url&id=LQsgy&margin=[object Object]&originHeight=84&originWidth=629&originalType=binary&ratio=1&status=done&style=none)

实验结果

主要结构体:
![](https://img-blog.csdnimg.cn/img_convert/1f92d6a8ae260e14005fc9081d4d3d4b.png#from=url&id=eXWk2&margin=[object Object]&originHeight=766&originWidth=994&originalType=binary&ratio=1&status=done&style=none)

任务1 readprocess

![image.png](https://img-blog.csdnimg.cn/img_convert/961ab616de043b9bd5be92dcff0c4c21.png#clientId=u11c57951-47a3-4&from=paste&id=bzsPV&margin=[object Object]&name=image.png&originHeight=582&originWidth=249&originalType=url&ratio=1&size=29117&status=done&style=none&taskId=ub2325653-d7b2-4dd9-a7ed-ab123e3e446)
设计一个模块,该模块的功能是列出系统中所有内核线程的程序名、PID号和进程状态。
实验代码:
read.c

#include<linux/init.h>
#include<linux/module.h>
#include<linux/sched/signal.h>


MODULE_LICENSE("GPL");
MODULE_AUTHOR("ygg");
MODULE_DESCRIPTION("read process info");

static int readin(void)
{
	struct task_struct *p;
	printk(KERN_ALERT "名称\t进程号\t进程状态\n");
	for_each_process(p)
	{
		printk(KERN_ALERT "%s\t%d\t%ld\n",p->comm,p->pid,p->state);
	}
	return 0;
	
}

static void readout(void)
{
	printk(KERN_ALERT "Goodbye\n");

}

module_init(readin);
module_exit(readout);

Makefile

ifneq ($(KERNELRELEASE),)
	obj-m :=read.o
else
	KDIR:=/lib/modules/$(shell uname -r)/build
	PWD:=$(shell pwd)

default:
	$(MAKE) -C $(KDIR) M=$(PWD) modules
clean:
	$(MAKE) -C $(KDIR) M=$(PWD) clean
endif

注意:for_each_process的定位在linux/sched/signal.h中,如果没有修改会出现错误:
![](https://img-blog.csdnimg.cn/img_convert/4ff1489ade45a505fe07d8c286d20d9f.png#from=url&id=MpPzn&margin=[object Object]&originHeight=71&originWidth=937&originalType=binary&ratio=1&status=done&style=none)
运行图:
![](https://img-blog.csdnimg.cn/img_convert/5120a234eb986b944d902828fafe2fab.png#from=url&id=Gpmha&margin=[object Object]&originHeight=745&originWidth=967&originalType=binary&ratio=1&status=done&style=none)

任务2 find

设计带参数的内核模块,参数为某个进程的PID号,功能列出进程的家族信息,包括父进程,兄弟进程和子进程的程序名,pid号,我们可以利用传参将参数pid传入到程序中,在利用结构体读出需要的数据。
![image.png](https://img-blog.csdnimg.cn/img_convert/0233fbfc41aa05e84e1f3efae2c77854.png#clientId=u11c57951-47a3-4&from=paste&id=u1c0168ae&margin=[object Object]&name=image.png&originHeight=577&originWidth=273&originalType=url&ratio=1&size=28844&status=done&style=none&taskId=u229622c1-6aeb-4bb1-a482-f489ca0ba4f)​
测试代码:find.c

#include<linux/init.h>
#include<linux/module.h>
#include<linux/sched/signal.h>
#include<linux/moduleparam.h>
#include<linux/sched.h>

MODULE_LICENSE("GPL");
MODULE_AUTHOR("ygg");
MODULE_DESCRIPTION("find process info");
static int fipid =33912;
module_param(fipid,int,S_IRUGO);

static int findin(void)
{
	struct task_struct *p,*xdp,*erp;
	struct list_head *h;
	struct pid * kpid;
	printk(KERN_ALERT "%d\n",fipid);;
	printk(KERN_ALERT "1\n");
	kpid=find_get_pid(fipid);
	printk(KERN_ALERT "2\n");
	p=pid_task(kpid,PIDTYPE_PID);
	printk(KERN_ALERT "3\n");
	printk(KERN_ALERT "PID号:\t%d\n",p->pid);
	if(p->parent==NULL)
	{
		//printk(KERN_ALERT "4\n");
		printk(KERN_ALERT "无父进程");
	}
	else 
	{
		//printk(KERN_ALERT "5\n");
		printk(KERN_ALERT "父进程PID号:\t%d\n",p->parent->pid);
		//printk(KERN_ALERT "6\n");
		printk(KERN_ALERT "父进程程序名:\t%s\n",p->parent->comm);
	}
	//printk(KERN_ALERT "7\n");	
	list_for_each(h,&p->sibling)
	{
		//printk(KERN_ALERT "8\n");
		xdp=list_entry(h,struct task_struct,sibling);
		//printk(KERN_ALERT "9\n");
		printk(KERN_ALERT "兄弟进程PID号:	%d\n",xdp->pid);
		printk(KERN_ALERT "兄弟进程程序名:	%s\n",xdp->comm);
	}	
	//printk(KERN_ALERT "10\n");
	list_for_each(h,&p->children)
	{
		erp=list_entry(h,struct task_struct,sibling);
		printk(KERN_ALERT "孩子进程PID号:	%d\n",erp->pid);
		printk(KERN_ALERT "孩子进程程序名:	%s\n",erp->comm);
	}
	
	return 0;
}

static void findout(void)
{
	printk(KERN_ALERT "Goodbye\n");

}

module_init(findin);
module_exit(findout);

运行图:
![](https://img-blog.csdnimg.cn/img_convert/007fdd2407aa483c83ce8651fb304b10.png#from=url&id=lgJ5v&margin=[object Object]&originHeight=43&originWidth=908&originalType=binary&ratio=1&status=done&style=none)
![](https://img-blog.csdnimg.cn/img_convert/2cdc8b83cfa8dc34ffb9e10bb9ce13aa.png#from=url&id=Pebi1&margin=[object Object]&originHeight=318&originWidth=826&originalType=binary&ratio=1&status=done&style=none)
注意:中途出现错误:
![](https://img-blog.csdnimg.cn/img_convert/b48a3af7af229cef6b406db0b5a9e017.png#from=url&id=zLrjv&margin=[object Object]&originHeight=258&originWidth=726&originalType=binary&ratio=1&status=done&style=none)
解决方案:
在写内核模块的时候,有时需要根据pid获得此pid对应进程的task_struct。
在linux 2.6.24之前,用get_task_by_pid这个宏即可,声明在/include/linux/sched.h。
之后直到linux 2.6.30,变成了get_task_by_vpid。但是后来这个宏虽然还存在,但是没有被内核EXPORT,因此也就无法在模块中调用,对应的策略是使用pid_task函数。
解决的方法是使用 pid_task 来替代。
查了一下pid_task的定义,发现它的参数类型与find_task_by_vpid不一样,需要使用find_vpid来转换一下。

#if LINUX_VERSION_CODE < KERNEL_VERSION(2,6,24)
if (!(pcb_tmp = find_task_by_pid(pid))) {
#elif LINUX_VERSION_CODE < KERNEL_VERSION(2,6,31)
if (!(pcb_tmp = find_task_by_vpid(pid))) {
#else
if (!(pcb_tmp = pid_task(find_vpid(pid), PIDTYPE_PID))) {
#endif

测试了一下,可以正常工作

实验总结

通过这次实验,我了解到了所有进程都是pid为1的init进程的后代。
![image.png](https://img-blog.csdnimg.cn/img_convert/a5f39a3015391c3d5ecbc59e76a4ad92.png#clientId=u11c57951-47a3-4&from=paste&id=u172efded&margin=[object Object]&name=image.png&originHeight=206&originWidth=509&originalType=url&ratio=1&size=36042&status=done&style=none&taskId=u2316321e-7fe8-4825-b592-8c92cb35b92)
结构体list_head包含两个指针成员:next,prev。这两个指针成员都是list_head类型,以此构成链表。实际应用中,list_head结构体往往实例化为其他结构体的成员,例如task_struct中的children,sibling。
这次实验中出现了许多错误,我学会了利用互联网解决问题,通过定位源码,理解源码解决问题,提高了编程能力、调试能力以及对linux的理解。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值