加载内核模块,实现新的系统调用:遍历系统当前所有进程的任务描述符,并将pid组织成树状结构显示


一、题目要求


在Linux内核中增加一个系统调用,并编写对应的linux应用程序。利用该系统调用能够遍历系统当前所有进程的任务描述符,并按进程父子关系将这些描述符所对应的进程id(PID)组织成树形结构显示。

二、分析


系统调用是内核为用户进程提供服务的一种方式。通过系统调用,内核能够提供给用户模式下的进程和硬件设备的接口,保护对内核所管理的资源的访问,提高系统安全,提高程序的可移植性。系统调用的概念如图1所示。
加载内核模块,实现新的系统调用:遍历系统当前所有进程的任务描述符,并将pid组织成树状结构显示_第1张图片
图1 系统调用的概念

题目中要求增加一项新的系统调用,可以实现对当前系统进程的遍历访问。其本质就是在内核中实现一个新的函数,调用该函数可以得到进程遍历的结果。增加系统调用的方式有两种:

其一,修改内核源码。我们在内核源码中,找到对应文件,增加新的系统调用编号,系统调用跳转表项和相应的例程。然后重新编译内核。利用编译好的内核重启系统,则该系统就支持我们新加的这项系统调用。

其二,加载内核模块。内核模块是一种没有经过链接,不能独立运行的目标文件,运行在内核空间中。经过链接装载到内核里面,成为内核的一部分,可以访问内核的公用符号,其概念如图2所示。我们可以设计一个内核模块,在其中实现程序逻辑,然后将其加载到内核中,这样也可实现在内核中增加新的系统调用。

加载内核模块,实现新的系统调用:遍历系统当前所有进程的任务描述符,并将pid组织成树状结构显示_第2张图片
图2 内核模块概念

显而易见的是,第二种方法符合模块化设计思想,根据需要动态使能/禁止模块,可以保证计算机资源。避免了第一种方法重新编译内核的麻烦,而且避免了使内核臃肿。我们下面程序实现通过加载内核模块的方式。

三、程序设计思路


根据分析,我们采用加载内核模块的方式来实现题目要求。那么程序设计思路就很清晰了,其主要步骤包括:

1、内核模块的代码框架

内核模块代码有自己的程序框架,包括需要的若干头文件,模块入口函数和模块退出函数,GPL许可下引入识别代码宏。

2、逻辑代码

根据题目要求,我们需要对当前系统中的进程进行遍历,并且按照父子关系进行树状结构输出结果。这一部分属于逻辑代码,我们可以实现一个函数processtree(),然后加入到内核模块代码中。通过修改原系统调用地址,来执行我们自定义的函数,从而实现该功能。

3、用户测试程序

编译好内核模块后,加载到系统中。我们需要知道该项系统调用是否成功。编写测试程序,调用该系统调用,打印返回结果查看。涉及问题是内核态数据和用户态数据的交换


四、程序设计

1、内核程序框架

内核模块程序包含的头文件包括:

其中,kernel.h包含了内核提供的一些基本的函数接口,包括内核打印函数printk(),它类似于我们在用户态下的printf()函数。

加载内核模块,实现新的系统调用:遍历系统当前所有进程的任务描述符,并将pid组织成树状结构显示_第3张图片


模块入口函数为init_addsyscall(),由module_init()宏指定,在模块被加载的时候被调用向系统注册。入口函数的返回值:0表示成功,非0表示失败。

模块的退出函数为exit_addsyscall(),由module_exit()宏指定,在模块被卸载时被调用向系统注销,主要来完成资源的清理工作。它被调用完毕后,就模块就被内核清除了。一个模块最少需要有入口和退出函数。



MODULE_LICENSE(“GPL”)表示设置模块遵守GPL证书,取消警告信息。

2、逻辑代码

最后结果需要以树状形式展示所有进程的父子关系。我们定义processtree()递归函数来访问遍历,并且将结果存储在数组中,以便提供给用户态访问。

加载内核模块,实现新的系统调用:遍历系统当前所有进程的任务描述符,并将pid组织成树状结构显示_第4张图片


我们在sys_mycall()中,从当前进程开始,递归调用processtree()函数,将进程信息存储在数组中。然后利用copy_to_user函数将内核信息传递给用户态下,用户态下的测试程序对结果进行展示。

加载内核模块,实现新的系统调用:遍历系统当前所有进程的任务描述符,并将pid组织成树状结构显示_第5张图片

3、测试程序

测试程序中利用syscall(num,para1,para2…)来进行系统调用,num表示该系统调用在系统调用跳转表中的号码。本程序中我们选用223号,在内核中将223本来对应的系统调用,临时链到我们自定义的sys_mycall()中。通过该系统调用后获得数组a,然后将其以树状结构打印出来。

加载内核模块,实现新的系统调用:遍历系统当前所有进程的任务描述符,并将pid组织成树状结构显示_第6张图片


4、编写Makefile文件

加载内核模块,实现新的系统调用:遍历系统当前所有进程的任务描述符,并将pid组织成树状结构显示_第7张图片


五、需要考虑的两个问题

1、进程个数确定

系统可运行的最大进程数,通过ulimit –u 查看有7896个。


我们通过ps –ef|wc –l命令实际查看当前运行进程数量为178个。


于是,我们定的存储进程信息的数组大小为512是够用的。

2、内核和用户态数据交换

我们在内核模块程序中,将进程遍历信息存储在数组中,然后需要将其传递给用户态下。采用copy_from_user()和copy_to_user()这两个函数,这两个函数负责在用户空间和内核空间传递数据。因此我们在测试程序中,将空数组a的地址作为参数传递给内核模块程序,在内核中使用copy_to_user()函数将内核中的数组信息传递给用户态下的地址。

六、运行程序过程

1、将编译出来的内核模块hello.ko加载到内核中

加载内核模块命令:insmod  xxx.ko

加载内核模块,实现新的系统调用:遍历系统当前所有进程的任务描述符,并将pid组织成树状结构显示_第8张图片

2、通过dmesg查看输出信息是否正确

加载内核模块,实现新的系统调用:遍历系统当前所有进程的任务描述符,并将pid组织成树状结构显示_第9张图片

3、运行测试程序,输出树状打印结果

加载内核模块,实现新的系统调用:遍历系统当前所有进程的任务描述符,并将pid组织成树状结构显示_第10张图片


说明:因结果较长,截取部分页面展示出来。

加载内核模块,实现新的系统调用:遍历系统当前所有进程的任务描述符,并将pid组织成树状结构显示_第11张图片

   4、卸载该模块

加载内核模块,实现新的系统调用:遍历系统当前所有进程的任务描述符,并将pid组织成树状结构显示_第12张图片


卸载内核模块命令:rmmod  xxx

至此,工作就完成了。

七、附录:源代码

1、内核模块程序hello.c

#include 
#include 
#include 
#include 
#include 
#include 

#define my_syscall_num 223
#define sys_call_table_address 0xc15b3000

static int counter = 0;
struct process
{
	int pid;
	int depth;
};

struct process a[512];

unsigned int clear_and_return_cr0(void);
void setback_cr0(unsigned int val);
asmlinkage long sys_mycall(char __user *buf);
int orig_cr0;
unsigned long *sys_call_table = 0;
static int (*anything_saved)(void);


void processtree(struct task_struct * p,int b)
{
	struct list_head * l;
	a[counter].pid = p -> pid;
	a[counter].depth = b;
	counter ++;
	for(l = p -> children.next; l != &(p->children); l = l->next)
	{
		struct task_struct *t = list_entry(l,struct task_struct,sibling);
		processtree(t,b+1);
	}
}

unsigned int clear_and_return_cr0(void)
{
	unsigned int cr0 = 0;
	unsigned int ret;
	asm("movl %%cr0, %%eax":"=a"(cr0));
	ret = cr0;
	cr0 &= 0xfffeffff;
	asm("movl %%eax, %%cr0"::"a"(cr0));
	return ret;
}

void setback_cr0(unsigned int val)//读取val的值到eax寄存器,再将eax寄存器的值放入cr0中
{
	asm volatile("movl %%eax, %%cr0"::"a"(val));
}

static int __init init_addsyscall(void)
{
	printk("hello,lihuan kernel\n");
	sys_call_table = (unsigned long *)sys_call_table_address;//获取系统调用服务首地址
	printk("%x\n",sys_call_table);
	anything_saved = (int(*)(void)) (sys_call_table[my_syscall_num]);//保存原始系统调用的地址
	orig_cr0 = clear_and_return_cr0();//设置cr0可更改
	sys_call_table[my_syscall_num]= (unsigned long)&sys_mycall;//更改原始的系统调用服务地址
	setback_cr0(orig_cr0);//设置为原始的只读cr0
	return 0;
}

asmlinkage long sys_mycall(char __user * buf)
{
    int b = 0;
	struct task_struct * p;
	printk("This is lihuan_syscall!\n");
/*	if(num%2==0) 
		{num=num%10000;}
	else 
		{num=num%100000;}
	return num;
*/
/*	for(i=0;i<20;i++)
		a[i]=15;

	if(copy_to_user(buf,a,20*sizeof(int)))
		return -EFAULT;
	else
		return sizeof(a);
*/
	for(p = current; p != &init_task; p = p->parent );
		processtree(p,b);
		
	if(copy_to_user((struct process *)buf,a,512*sizeof(struct process)))
		return -EFAULT;
	else
		return sizeof(a);
}

static void __exit exit_addsyscall(void)
{
	//设置cr0中对sys_call_table的更改权限。
	orig_cr0 = clear_and_return_cr0();//设置cr0可更改
	//恢复原有的中断向量表中的函数指针的值。
	sys_call_table[my_syscall_num]= (unsigned long)anything_saved;
	//恢复原有的cr0的值
	setback_cr0(orig_cr0);
	printk("call lihuan exit \n");
}

module_init(init_addsyscall);
module_exit(exit_addsyscall);
MODULE_LICENSE("GPL");

2、测试程序hello_test.c

#include 
#include 
#include 
#include 

struct process
{
	int pid;
	int depth;
};

struct process a[512];

int main()
{
	int i,j;

	printf("the result is:%d\n",syscall(223,&a));

	for(i = 0; i < 512; i++)
	{
		for(j = 0; j < a[i].depth; j++)
			printf("|-");
		printf("%d\n",a[i].pid);
	
		if(a[i+1].pid == 0)
			break;
	}

	return 0;
}

3、Makefile文件

KVERS = $(shell uname -r)

# Kernel modules
obj-m += hello.o

# Specify flags for the module compilation.
#EXTRA_CFLAGS=-g -O0

build: kernel_modules user_test

kernel_modules:
	make -C /lib/modules/$(KVERS)/build M=$(CURDIR) modules
user_test:
	gcc -o hello_test hello_test.c

clean:
	make -C /lib/modules/$(KVERS)/build M=$(CURDIR) clean



你可能感兴趣的:(Linux学习笔记)