______9________1.1课程设计目的实现内核编译相关操作1.2相关原理介绍内核,是一个______的核心。它负责管理系统的进程、______、设备驱动程序[1]、文件和网络系统,决定着系统的性能和稳定性。Linux的一个重要的特点就是其______的公开性,Linux修补漏洞速度快以及对最新软件技术的利用方便,而Linux的内核则是这些特点的最直接的代表。拥有了内核的源程序[2]我们可以了解系统是如何工作的,通过通读源代码,我们就可以了解系统的工作原理。其次,我们可以针对自己的情况,量体裁衣,定制适合自己的系统,当然这样就需要重新编译内核。在不需要对内核进行重新编译的情况下,内核模块[3]可以动态的载入内核或从内核移出改变内核,极大缩短了驱动编写和内核开发的时间。1.3Linux内核模块程序结构一个Linux内核模块主要由如下几个部分组成:(1)模块加载函数通过insmod或modprobe命令加载内核模块时,模块的加载函数会自动被内核执行,完成本模块的相关初始化工作。(2)模块卸载函数当通过rmmod命令卸载某模块时,模块的卸载函数会自动被内核执行,完成与模块加载函数相反的功能。(3)模块许可证声明许可证(LICENSE)声明描述内核模块的许可权限,这一部分是必须声明的,如果不声明LICENSE,模块被加载时,将收到内核被污染(module license‘unspecified’taints kernel)的警告。在Linux 2.6内核中,可接受的LICENSE有“GPL”、“GPL v2”、“GPL and additional rights”、“Dual BSD/GPL”、“Dual MPL/GPL”和“Proprietary”。大多数情况下,内核模块应遵循GPL兼容许可权。其中最常用的许可是GPL和DualBSD/GPL。(4)其他可选部分模块参数,模块参数是模块被加载的时候可以被传递给它的值,它本身对应模块内部的全局变量[4];模块导出符号,内核模块可以导出符号(symbol,对应于函数或变量),这样其它模块可以使用本模块中的变量或函数;模块作者等信息声明。1.4内核模块的编译在Linux 2.6内核中,模块的编译需要配置过的内核源代码;编译过程首先回到内核目录下读取顶层的Makefile文件,然后返回模块源码所在目录,经过编译、链接后生成的内核模块文件的后缀为.ko。故内核模块的编译需要自己写Makefile文件,当在命令行中执行make命令时,将调用Makefile文件。二设计实现2.1内核线程[5]查看设计一个模块,该模块功能是列出系统中所有内核线程的程序名、PID号和进程状态[6]。该内核模块的功能类似于命令ps,只不过该模块专查看内核线程信息。首先在文件开始声明一下模块的许可证,即在文件中加入:MODULE_LICENSE("GPL");根据内核模块编程的模式,一个内核模块应该至少包含两个函数。一个初始化函数,还有一个退出(干一些收尾清理的工作)的函数,当内核模块被rmmod卸载时被执行。从内核版本2.3.13开始,可以为初始化和结束函数起任意的名字。在该模块内。两个函数分别命名为:static intkernel_thread_init(void)和static void kernel_thread_exit(void)。调用宏module_init()和module_exit()去注册初始化和退出这两个函数,即:module_init(kernel_thread_init);module_exit(kernel_thread_exit);到此内核模块基本框架基本完成。为了获取到所有的内核线程,可以使用宏for_each_process()。在内核中有内核线程组长链表[7],每个线程组长通过task_struc结构的tasks成员加入该链表中。利用for_each_process()可以访问到链表中的每一个进程。具体实现如下:structtask_struct *p;for_each_process(p)(/相关函数及操作)对遍历到的每一个线程,读取它的线程号、线程名称以及线程状态并输出。当线程状态为0时输出runnale,为-1时输出unrunnable,为其他时则输出stopped。在这里输出函数并不能使用printf,printf是用户空间的输出函数,内核空间使用的是printk,因为内核没有链接标准的C函数库。而实际上printk和printf的功能类似,printk是在内核中运行的向控制台[8]输出显示的函数。printk日志输出的级别一共有8个,由高到低分别为:KERN_EMERG""、KERN_ALERT""、KERN_CRIT""、KERN_ERR""、KERN_WARNING""、KERN_NOTICE""、KERN_INFO""、KERN_DEBUG"",默认采用的级别是DEFAULT_ MESSAGE_LOGLEVEL(这个默认级别一般为,即与KERN_WARNING在一个级别上)。2.2带参模块的实现设计一个带参数的模块,参数为进程的PID号,功能是列出进程的家族信息,包括父进程、兄弟进程和子进程的程序名、PID号。该模块的基本框架和上一个模块类似,声明模块许可证,注册初始化和结束函数。不同之处是在该模块中涉及到了模块参数。在Linux操作系统内核[9]中提供了一种模块带参数的机制,是模块的编写者可以在加载模块的时候提供一下信息,这些参数对于模块来说都是一个全局变量。定义一个模块参数可通过module_param()实现:module_param(name,type,perm);参数name是用户可见的参数名,也是模块中存放模块参数的变量名。参数type代表参数的类型,它可以是byte、short、int、long等类型。最后一个参数perm制定了模块在sysfs文件系统[10]下对应的文件权限,可以使八进制[11]的,也可以是S_Ifoo的定义形式,如S_RUGO|S_IWUSR等。在该模块中,定义一个进程的PID作为参数,以实现任意进程家族信息的查询,默认进程号为1,即在不添加参数情况下查看进程号为1的进程家族信息,具体实现如下:staticintpid=1;module_param(pid,int,0644);为找到指定PID的进程,可以使用for_each_process(p)遍历内核所有进程,查找进程号为PID的进程。找到具体进程后,获取进程的名称。接下来判断进程的父进程是否存在,若存在,在输出父进程信息:if(p->real_parent==NULL)(printk("No Parentn");)else(printk("Parent : %d %sn",p->real_parent->pid,p->real_parent->comm);)下一步就是该搜索线程号为PID线程的兄弟进程及子进程。在Linux中采用多个链表确保有效查找系统里的进程,双向链表[12]list_head内核中广泛的使用。因为list_head一般嵌入到啮合数据结构中,为了便于访问链表中的数据,内核提供了一系列的宏来实现链表的常规操作[2]。在这里使用list_for_each()和list_entry()来实现兄弟进程和子进程的查找。在上一步中得到了要查找的进程,可以由此进程得到他父进程的所有子进程组成的链表p->real_parent->children和此进程子进程的链表p->children。定义一个list_head结构体用于list_entry()中:structlist_head *pp;宏list_for_each(pp,&p->real_parent->children)遍历p->real_parent->children链表,每次pp指向一个对象成员,而宏list_entry(pp,structtask_struct,sibling);进一步得到该对象的指针。具体实现兄弟进程和子进程遍历如下:structtask_struct *p,*psibling;list_for_each(pp,&p->real_parent->children)(psibling=list_entry(pp,structtask_struct,sibling);printk("sibling %d %s n",psibling->pid,psibling->comm);)list_for_each(pp,&p->children)(psibling=list_entry(pp,structtask_struct,sibling);printk("children %d %s n",psibling->pid,psibling->comm);)上述过程将输出得到的兄弟进程和子进程的pid号和进程名称。2.3Makefile文件的编写Make工具最主要也是最基本的功能就是通过makefile文件来描述源程序之间的相互关系并自动维护编译工作。而makefile文件需要按照某种语法进行编写,文件中需要说明如何编译各个源文件并连接生成可执行文件[13],并要求定义源文件之间的依赖关系。makefile文件是许多编译器--包括Windows NT下的编译器--维护编译信息的常用方法,只是在集成开发环境中,用户通过友好的界面修改makefile文件而已。在Linux内核模块编程中,Makefile文件模版都类似,具体结构如下:ifneq ((KERNELRELEASE),)obj-m :=目标文件.oelseKDIR :=/lib/modules/(shell uname -r)/buildPWD :=(shellpwd)default:(MAKE) -C (KDIR) M=(PWD) modulesclean:(MAKE) -C (KDIR) M=(PWD) cleanendifKERNELRELEASE是在内核源码的顶层Makefile中定义的一个变量,在第一次读取执行此Makefile时,KERNELRELEASE没有被定义,所以make将读取执行else之后的内容。如果make的目标是clean,直接执行clean操作,然后结束。当make的目标为all时,-C(KDIR)指明跳转到内核源码目录下读取那里的Makefile;M=(PWD)表明然后返回到当前目录继续读入、执行当前的Makefile。当从内核源码目录返回时,KERNELRELEASE已被被定义,此时第一行的ifneq成功,make将继续读取else之前的内容。ifneq的内容为kbuild语法的语句,指明模块源码中各文件的依赖关系,以及要生成的目标模块名。三测试总结3.1内核线程查看测试(1)切换到内核模块所在路径kernel_threads,在命令行中输入make,得到如下结果:
______9
________
1.1课程设计目的
实现内核编译相关操作
1.2相关原理介绍
内核,是一个______的核心。它负责管理系统的进程、______、设备驱动程序[1]、文件和网络系统,决定着系统的性能和稳定性。
Linux的一个重要的特点就是其______的公开性,Linux修补漏洞速度快以及对最新软件技术的利用方便,而Linux的内核则是这些特点的最直接的代表。拥有了内核的源程序[2]我们可以了解系统是如何工作的,通过通读源代码,我们就可以了解系统的工作原理。其次,我们可以针对自己的情况,量体裁衣,定制适合自己的系统,当然这样就需要重新编译内核。在不需要对内核进行重新编译的情况下,内核模块[3]可以动态的载入内核或从内核移出改变内核,极大缩短了驱动编写和内核开发的时间。
1.3Linux内核模块程序结构
一个Linux内核模块主要由如下几个部分组成:
(1)模块加载函数
通过insmod或modprobe命令加载内核模块时,模块的加载函数会自动被内核执行,完成本模块的相关初始化工作。
(2)模块卸载函数
当通过rmmod命令卸载某模块时,模块的卸载函数会自动被内核执行,完成与模块加载函数相反的功能。
(3)模块许可证声明
许可证(LICENSE)声明描述内核模块的许可权限,这一部分是必须声明的,如果不声明LICENSE,模块被加载时,将收到内核被污染(module license‘unspecified’taints kernel)的警告。在Linux 2.6内核中,可接受的LICENSE有“GPL”、“GPL v2”、“GPL and additional rights”、“Dual BSD/GPL”、“Dual MPL/GPL”和“Proprietary”。大多数情况下,内核模块应遵循GPL兼容许可权。其中最常用的许可是GPL和DualBSD/GPL。
(4)其他可选部分
模块参数,模块参数是模块被加载的时候可以被传递给它的值,它本身对应模块内部的全局变量[4];模块导出符号,内核模块可以导出符号(symbol,对应于函数或变量),这样其它模块可以使用本模块中的变量或函数;模块作者等信息声明。
1.4内核模块的编译
在Linux 2.6内核中,模块的编译需要配置过的内核源代码;编译过程首先回到内核目录下读取顶层的Makefile文件,然后返回模块源码所在目录,经过编译、链接后生成的内核模块文件的后缀为.ko。故内核模块的编译需要自己写Makefile文件,当在命令行中执行make命令时,将调用Makefile文件。
二设计实现
2.1内核线程[5]查看
设计一个模块,该模块功能是列出系统中所有内核线程的程序名、PID号和进程状态[6]。
该内核模块的功能类似于命令ps,只不过该模块专查看内核线程信息。首先在文件开始声明一下模块的许可证,即在文件中加入:
MODULE_LICENSE("GPL");
根据内核模块编程的模式,一个内核模块应该至少包含两个函数。一个初始化函数,还有一个退出(干一些收尾清理的工作)的函数,当内核模块被rmmod卸载时被执行。从内核版本2.3.13开始,可以为初始化和结束函数起任意的名字。在该模块内。两个函数分别命名为:static intkernel_thread_init(void)和static void kernel_thread_exit(void)。调用宏module_init()和module_exit()去注册初始化和退出这两个函数,即:
module_init(kernel_thread_init);
module_exit(kernel_thread_exit);
到此内核模块基本框架基本完成。
为了获取到所有的内核线程,可以使用宏for_each_process()。在内核中有内核线程组长链表[7],每个线程组长通过task_struc结构的tasks成员加入该链表中。利用for_each_process()可以访问到链表中的每一个进程。具体实现如下:
structtask_struct *p;
for_each_process(p)
{
//相关函数及操作
}
对遍历到的每一个线程,读取它的线程号、线程名称以及线程状态并输出。当线程状态为0时输出runnale,为-1时输出unrunnable,为其他时则输出stopped。
在这里输出函数并不能使用printf,printf是用户空间的输出函数,内核空间使用的是printk,因为内核没有链接标准的C函数库。而实际上printk和printf的功能类似,printk是在内核中运行的向控制台[8]输出显示的函数。printk日志输出的级别一共有8个,由高到低分别为:KERN_EMERG""、KERN_ALERT""、KERN_CRIT""、KERN_ERR""、KERN_WARNING""、KERN_NOTICE""、KERN_INFO""、KERN_DEBUG"",默认采用的级别是DEFAULT_ MESSAGE_LOGLEVEL(这个默认级别一般为,即与KERN_WARNING在一个级别上)。
2.2带参模块的实现
设计一个带参数的模块,参数为进程的PID号,功能是列出进程的家族信息,包括父进程、兄弟进程和子进程的程序名、PID号。
该模块的基本框架和上一个模块类似,声明模块许可证,注册初始化和结束函数。不同之处是在该模块中涉及到了模块参数。在Linux操作系统内核[9]中提供了一种模块带参数的机制,是模块的编写者可以在加载模块的时候提供一下信息,这些参数对于模块来说都是一个全局变量。定义一个模块参数可通过module_param()实现:
module_param(name,type,perm);
参数name是用户可见的参数名,也是模块中存放模块参数的变量名。参数type代表参数的类型,它可以是byte、short、int、long等类型。最后一个参数perm制定了模块在sysfs文件系统[10]下对应的文件权限,可以使八进制[11]的,也可以是S_Ifoo的定义形式,如S_RUGO|S_IWUSR等。
在该模块中,定义一个进程的PID作为参数,以实现任意进程家族信息的查询,默认进程号为1,即在不添加参数情况下查看进程号为1的进程家族信息,具体实现如下:
staticintpid=1;
module_param(pid,int,0644);
为找到指定PID的进程,可以使用for_each_process(p)遍历内核所有进程,查找进程号为PID的进程。找到具体进程后,获取进程的名称。接下来判断进程的父进程是否存在,若存在,在输出父进程信息:
if(p->real_parent==NULL)
{
printk("No Parentn");
}
else
{
printk("Parent : %d %sn",p->real_parent->pid,p->real_parent->comm);
}
下一步就是该搜索线程号为PID线程的兄弟进程及子进程。在Linux中采用多个链表确保有效查找系统里的进程,双向链表[12]list_head内核中广泛的使用。因为list_head一般嵌入到啮合数据结构中,为了便于访问链表中的数据,内核提供了一系列的宏来实现链表的常规操作[2]。在这里使用list_for_each()和list_entry()来实现兄弟进程和子进程的查找。
在上一步中得到了要查找的进程,可以由此进程得到他父进程的所有子进程组成的链表p->real_parent->children和此进程子进程的链表p->children。定义一个list_head结构体用于list_entry()中:
structlist_head *pp;
宏list_for_each(pp,&p->real_parent->children)遍历p->real_parent->children链表,每次pp指向一个对象成员,而宏list_entry(pp,structtask_struct,sibling);进一步得到该对象的指针。具体实现兄弟进程和子进程遍历如下:
structtask_struct *p,*psibling;
list_for_each(pp,&p->real_parent->children)
{
psibling=list_entry(pp,structtask_struct,sibling);
printk("sibling %d %s n",psibling->pid,psibling->comm);
}
list_for_each(pp,&p->children)
{
psibling=list_entry(pp,structtask_struct,sibling);
printk("children %d %s n",psibling->pid,psibling->comm);
}
上述过程将输出得到的兄弟进程和子进程的pid号和进程名称。
2.3Makefile文件的编写
Make工具最主要也是最基本的功能就是通过makefile文件来描述源程序之间的相互关系并自动维护编译工作。而makefile文件需要按照某种语法进行编写,文件中需要说明如何编译各个源文件并连接生成可执行文件[13],并要求定义源文件之间的依赖关系。makefile文件是许多编译器--包括Windows NT下的编译器--维护编译信息的常用方法,只是在集成开发环境中,用户通过友好的界面修改makefile文件而已。在Linux内核模块编程中,Makefile文件模版都类似,具体结构如下:
ifneq ($(KERNELRELEASE),)
obj-m :=目标文件.o
else
KDIR :=/lib/modules/$(shell uname -r)/build
PWD :=$(shellpwd)
default:
$(MAKE) -C $(KDIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KDIR) M=$(PWD) clean
endif
KERNELRELEASE是在内核源码的顶层Makefile中定义的一个变量,在第一次读取执行此Makefile时,KERNELRELEASE没有被定义,所以make将读取执行else之后的内容。如果make的目标是clean,直接执行clean操作,然后结束。当make的目标为all时,-C$(KDIR)指明跳转到内核源码目录下读取那里的Makefile;M=$(PWD)表明然后返回到当前目录继续读入、执行当前的Makefile。当从内核源码目录返回时,KERNELRELEASE已被被定义,此时第一行的ifneq成功,make将继续读取else之前的内容。ifneq的内容为kbuild语法的语句,指明模块源码中各文件的依赖关系,以及要生成的目标模块名。
三测试总结
3.1内核线程查看测试
(1)切换到内核模块所在路径kernel_threads,在命令行中输入make,得到如下结果:
题目解答
答案
4总结 一课程设计简介操作系统 内存 源代码