资源描述
Linux课程设计
项目名称: 内核模块编程
姓 名: *****
班 级: 计算机*班
学 号: 2008****
指导教师: **
日 期: 2011.6.19—2011.6.24
目录
一.课程设计目的 2
二.任务描述 2
三.选题原因 2
四、准备阶段 3
五.整体思路 8
六.程序代码 8
七.遇到的问题 12
八.测试结果 13
九.总结 15
一.课程设计目的
通过课程设计对操作系统基本原理进行更深入的认识,以Linux为具体研究对象,分析理解操作系统底层实现,综合利用已学知识与技术,就Linux操作系统各功能方面进行模拟或实现
二.任务描述
编写proc文件系统相关的内核模块:
设计一个模块,该模块功能是列出系统中所有内核线程的程序名、PID号和进程状态。再设计一个带参数的模块,参数为进程的PID号,功能是列出进程的家族信息,包括父进程、兄弟进程和子进程的程序名、PID号。
三.选题原因
学习操作系统时,没有把内核这一块学好,所以想借此机会加强对操作系统底层的理解。
四、准备阶段
看到此题时不知道proc文件的作用,对内核的概念也很模糊,并且错误的以为内核利用一些命令来调用进程呢。很多东西都不理解,所以首先是看书上网逐一进行学习。下面是查找的一些资料。
1.关于linux内核编程书本上有详细介绍并且也比较好理解。首先把书上例题演示了一遍,对模块编程就掌握的差不多了。
模块是具有独立功能的程序,它可以被单独编译,但不能独立运行。它在运行时被链接到内核作为内核的一部分在内核空间运行,这与运行在用户空间的进程是不同的。模块通常由一组函数和数据结构组成,用来实现一种文件系统、一个驱动程序或其他内核上层的功能。
应用程序与内核模块程序的比较
$sN[ _H;`1o7f0
C语言应用程序LUPA开源社区#t)VsE+s7T
内核模块程序LUPA开源社区1~|-CX*C*U%s"q4L1p
使用函数LUPA开源社区g(`D c'hD
Libc库LUPA开源社区)l-KY5V:e4[5b
内核函数7si zK)O*n0
运行空间aWx7`h0
用户空间LUPA开源社区#I wYfTV+H
内核空间$~?k(e u7HQY4I0
运行权限!e{ O;ba`8X#H0
普通用户LUPA开源社区:X} AJ%A4u
超级用户LUPA开源社区W7P!MUbMLP
入口函数&xBJ(H:B[ H0
main()LUPA开源社区;fC.U o6F%e r H@8H
module_init()4f RQV j KZ~M6F0
出口函数LUPA开源社区2r)}v0Q%wWI
exit()LUPA开源社区cSr,o/Vzi
module_exit()LUPA开源社区v%K$IgD$j5[?(a
编译LUPA开源社区h%Ny mUZ2qt!A
Gcc –cLUPA开源社区@3_;M[[+@
Makefile
连接+H(iB f.w$v7J0
GccLUPA开源社区&wZKg!o&L
insmodLUPA开源社区Egc0E y C6O W
运行LUPA开源社区*o_$\&`G
直接运行LUPA开源社区'o!LK-Qo#dm D
insmodsS#HOn` v0
调试LUPA开源社区uT-Q;kd7x
Gdb~X.hw~ dP ]0
kdbug, kdb,kgdb等LUPA开源社区T ?o#F.H;m(Bg
2. 编译工具make :Make工具通过一个称为 Makefile 的文件来完成并自动维护编译工作。
课本上有提供一个超级通用的Makefile,此文件要与模块在同一个文件夹下。
ifneq ($(KERNELRELEASE),)
obj-m += nullparam.o
else
PWD := $(shell pwd)
KVER := $(shell uname -r)
KDIR := /lib/modules/$(KVER)/build
all:
$(MAKE) -C $(KDIR) M=$(PWD)
clean:
rm -rf *.o *.mod.c *.ko *.symvers *.order *.markers
endif
KERNELRELEASE是在内核源码的顶层Makefile中定义的一个变量,在第一次读取执行此Makefile时,KERNELRELEASE没有被定义, 所以make将读取执行else之后的内容。如果make的目标是clean,直接执行clean操作,然后结束。当make的目标为all时,-C $(KDIR) 指明跳转到内核源码目录下读取那里的Makefile;M=$(PWD) 表明然后返回到当前目录继续读入、执行当前的Makefile。当从内核源码目录返回时,KERNELRELEASE已被被定义,kbuild也被启动去解析kbuild语法的语句,make将继续读取else之前的内容。else之前的内容为kbuild语法的语句, 指明模块源码中各文件的依赖关系,以及要生成的目标模块名。obj-m += nullparam.o表示编译连接后将生成nullparam.o模块
2.用户级线程:指不需要内核支持而在用户程序中实现的线程,其不依赖于操作系统核心,应用进程利用线程库提供创建、同步、调度和管理线程的函数来控制用户线程。
内核线程:指需要内核的参与,由内核完成线程的调度。其依赖于操作系统核心,由内核的内部需求进行创建和撤销。
3.proc文件系统:proc即process的缩写,最初的proc文件系统只是存放进程的相关信息。但现在的/proc文件系统除此之外还包含系统的状态信息和配置信息。
/proc文件系统相当于内核的一个快照,该目录下的所有信息都是动态的从正在运行的内核中读取。
基于这种原因,/proc文件系统就成为了用户和内核之间交互的接口。一方面,用户可以从/proc文件系统中读取很多内核释放出来的信息;另一方面,内核也可以在恰当的时候从用户那里得到输入信息,从而改变内核的相关状态和配置。
相比传统的文件系统,/proc是一种特殊的文件系统,即虚拟文件系统。这里的虚拟是强调/proc文件系统下的所有文件都存在于内存中而不是磁盘上。也就是说/proc文件系统只占用内存空间,而不占用系统的外存空间。
/proc 虚拟文件系统可以广泛地用来报告内核的信息,也可以用来进行动态配置。它对于驱动程序和模块编程来说都是非常完整的。
4. proc_dir_entry结构:
proc文件系统的组织结构是proc_dir_entry结构,所有属于proc文件系统的文件,都对应一个proc_dir_entry结构,这个结构在proc_fs.h中定义。
struct proc_dir_entry {
//用来唯一标志proc_dir_entry结构的节点号
unsigned short low_ino;
unsigned short namelen;
//这个proc文件的名字。
const char *name;
//该proc文件的模式, 第一部分是文件的类型,第二部分是该文件的权限。
mode_t mode;
nlink_t nlink;
uid_t uid;
gid_t gid;
//即我们使用“ls”命令时,所显示出的文件大小。
unsigned long size;
struct inode_operations * proc_iops; /* 是一个inode_operations结构,其中设置了针对这个proc索引节点的操作函数,这样,我们就可以针对不同类型的proc文件,提供不同的方法,以完成不同的工作。*/
struct file_operations * proc_fops; /*是一个file_operations结构,其中放置了针对这个proc文件的操作函数,我们可以把对proc文件的读写操作,放在这个结构中,用以实现对/proc目录中的文件的读,写功能。*/
get_info_t *get_info; /*当用户向proc文件读取的数据小于一个页面大小时,可以使用这个函数向用户返回数据。*/
struct module *owner;
struct proc_dir_entry *next, *parent, *subdir; /*使用这些链表,在内存中,proc_dir_entry结构就以树的形式链接在一起。
*/
void *data;
read_proc_t *read_proc;
write_proc_t *write_proc;
atomic_t count; /*该结构的使用计数。当一个proc_dir_entry结构的count减为零时,会释放该结构*/
int deleted; /*这是一个删除标志,当我们调用remove_proc_entry函数要删除一个proc_dir_entry时,如果发现该结构还在使用,就会设置该标志并且退出。 */
kdev_t rdev;
/* read_proc_t *read_proc 和write_proc_t *write_proc:这两个函数提供了对proc文件进行操作的简单接口。我们知道,对于proc文件,我们可以从中读取核心数据,还可以向其中写入数据,因此,对于一些功能比较简单的proc文件,我们只要实现这两个函数(或其中之一)即可,而不用设置inode_operations结构,这样,整个操作比较简单。实际上,我们会在后面的分析中看到,在注册proc文件的时候,会自动为proc_fops设置一个缺省的file_operations结构,如果我们只实现了上面提到的两个读写操作,而没有设置自己file_operations结构,那么,会由缺省的inode_operations结构中的读写函数检查调用这两个函数。*/
};
5.函数create_proc_entry,由它创建并注册proc文件。struct proc_dir_entry *create_proc_entry(const char *name, mode_t mode, struct proc_dir_entry *parent) {…… }/*函数内容不必详细了解,只要会调用此函数即可。name为要创建的文件名,mode为创建文件的属性;parent指向该文件父目录的指针,如果创建的虚拟文件位于/proc下,则这个参数为NULL(表示 /proc 根目录)。*/
6. read_proc_t:
如果只是创建了虚拟文件,那么它并不能被读写。为此,我们必须为每个虚拟文件挂接读写函数,因为此题中只用到读函数,所以只看了函数指针read_proc字段的相关内容。类型定义如下:
typedef int (read_proc_t)(char *page, char **start, off_t off, int count, int *eof, void *data);
当用户读创建的虚拟文件时,该文件对应的read_proc函数将被调用。该函数将数据写入内核的缓冲区page中。page 参数是数据写入到的位置,其中 count 定义了可以写入的最大字符数。在返回多页数据(通常一页是 4KB)时,需要使用 start 和 off 参数。当所有数据全部写入之后,就需要设置 eof(文件结束参数)。data 表示的是私有数据。page 缓冲区在内核空间中。
7.删除proc文件
void remove_proc_entry(const char *name, struct proc_dir_entry *parent)
{ ………}
该函数在参数parent的所有孩子中查找指定的名字name,如果找到匹配的节点,那么,就将该结构从树结构中去掉。然后,如果删除的proc_dir_entry是目录结构,那么,就减少其父节点的链接数。
8. sprintf函数:
int sprintf( char *buffer, const char *format [, argument] ... );
除了前两个参数类型固定外,后面可以接任意多个参数。而它的精华,就在第二个参数:
格式化字符串上。;返回值:字符串长度。
输出格式:例如sprintf(s, "%8d", 123); //产生:" 123 ",宽度占8 个位置,右对齐。
五.整体思路:
内核的输出进到了内核缓冲区中, /proc 目录下的所有信息都是动态的从正在运行的内核中读取,在这里使用proc文件系统显示内核内容,即用proc文件系统实现内核模块与用户交互。用户空间使用shell脚本。(因为想练一下shell脚本,所以用户空间没有用一般函数。)
六.程序代码
#include<linux/init.h>
#include<linux/module.h>
#include<linux/sched.h>
#include<linux/list.h>
#include<linux/proc_fs.h>
MODULE_LICENSE("Dual BSD/GPL");
static struct proc_dir_entry *myfile;
static int mod_init_modtest(void);
static void mod_exit_modtest(void);
module_init(mod_init_modtest);
module_exit(mod_exit_modtest);
static int proc_read_nullparam(char *page,char **start,off_t off,int count,int *eof,void *data)
{
int record=0;
struct task_struct *p;
for_each_process(p){
record=record+sprintf(page+record,"name:%20s pid:%8d state:%8ld\n",p->comm,p->pid,p->state);
printk(KERN_INFO"sssssssssssssssssssssssssssssssssssfffffffffffffffffffffffffffffffffffssssssssssssssssssss%s\n",p->comm);//测试时用到的
}
return record;
}
int mod_init_modtest(void)
{
myfile=create_proc_entry("nullparam",0500,NULL);
myfile->read_proc=proc_read_nullparam;
return 0;
}
void mod_exit_modtest(void)
{
remove_proc_entry("nullparam",NULL);
printk(KERN_INFO"-----------Module_export_symbol was deleted!-------\n");
}
MODULE_AUTHOR("CHEN MENG YUAN");
MODULE_DESCRIPTION("module1:Module_export_symbol --sum_op--fac_op--");
MODULE_VERSION("Ver 2.6.35");
#include<linux/init.h>
#include<linux/module.h>
#include<linux/sched.h>
#include<linux/list.h>
#include<linux/proc_fs.h>
MODULE_LICENSE("Dual BSD/GPL");
static struct proc_dir_entry *myfile;
static int mod_init_modtest(void);
static void mod_exit_modtest(void);
module_init(mod_init_modtest);
module_exit(mod_exit_modtest);
//static struct task_struct *p,*allp;cannot definit here
static int pid=0;
module_param(pid,int,S_IRUGO);
static int proc_read_pidparam(char *page,char **start,off_t off,int count,int *eof,void *data)
{
int record=0;
struct task_struct *p,*tmp1,*tmp2;
for_each_process(p)
{
if(p->pid==pid){
record=record + sprintf(page+record,"my name:%15s pid:%8d\n",p->comm,p->pid);
record=record + sprintf(page+record,"Parent name:%15s pid:%8d\n",p->parent->comm,p->parent->pid);
list_for_each_entry(tmp1,&(p->children),sibling)
{
record=record + sprintf(page+record,"children name:%15s pid:%8d\n",tmp1->comm,tmp1->pid);
}
list_for_each_entry(tmp2,&(p->real_parent->children),sibling)
{
record=record + sprintf(page+record,"brother name:%15s pid:%8d\n",tmp2->comm,tmp2->pid);
}
}
}
return record;
}
int mod_init_modtest(void)
{
myfile=create_proc_entry("pidparam3",0500,NULL);
myfile->read_proc=proc_read_pidparam;
return 0;
}
void mod_exit_modtest(void)
{
remove_proc_entry("pidparam3",NULL);
printk(KERN_INFO"-----------Module_export_symbol was deleted!-------\n");
}
MODULE_AUTHOR("CHEN MENG YUAN");
MODULE_DESCRIPTION("module1:Module_export_symbol --sum_op--fac_op--");
MODULE_VERSION("Ver 2.6.35");
七.遇到的问题
当时编程时进程状态直接调用task_struct的state字段,没有进行深入研究,老师检查完,我查了一下state的相关知识。现现在列出一部分。
1. 进程状态(State)
进程执行时,它会根据具体情况改变状态。进程状态是调度和对换的依据。Linux中的进程主要有如下状态,如表4.1所示。
表4.1 Linux进程的状态
内核表示
含义
TASK_RUNNING
可运行
TASK_INTERRUPTIBLE
可中断的等待状态
TASK_UNINTERRUPTIBLE
不可中断的等待状态
TASK_ZOMBIE
僵死
TASK_STOPPED
暂停
TASK_SWAPPING
换入/换出
·可运行状态
处于这种状态的进程,要么正在运行、要么正准备运行。正在运行的进程就是当前进程(由current所指向的进程),而准备运行的进程只要得到CPU就可以立即投入运行,CPU是这些进程唯一等待的系统资源。系统中有一个运行队列(run_queue),用来容纳所有处于可运行状态的进程,调度程序执行时,从中选择一个进程投入运行。在后面我们讨论进程调度的时候,可以看到运行队列的作用。当前运行进程一直处于该队列中,也就是说,current总是指向运行队列中的某个元素,只是具体指向谁由调度程序决定。
·等待状态
处于该状态的进程正在等待某个事件(event)或某个资源,它肯定位于系统中的某个等待队列(wait_queue)中。Linux中处于等待状态的进程分为两种:可中断的等待状态和不可中断的等待状态。处于可中断等待态的进程可以被信号唤醒,如果收到信号,该进程就从等待状态进入可运行状态,并且加入到运行队列中,等待被调度;而处于不可中断等待态的进程是因为硬件环境不能满足而等待,例如等待特定的系统资源,它任何情况下都不能被打断,只能用特定的方式来唤醒它,例如唤醒函数wake_up()等。
·暂停状态
此时的进程暂时停止运行来接受某种特殊处理。通常当进程接收到SIGSTOP、SIGTSTP、SIGTTIN或 SIGTTOU信号后就处于这种状态。例如,正接受调试的进程就处于这种状态。
·僵死状态
进程虽然已经终止,但由于某种原因,父进程还没有执行wait()系统调用,终止进程的信息也还没有回收。顾名思义,处于该状态的进程就是死进程,这种进程实际上是系统中的垃圾,必须进行相应处理以释放其占用的资源。
Linux内核中对进程状态进行了如下宏定义。
#define TASK_RUNNING 0
#define TASK_INTERRUPTIBLE 1
#define TASK_UNINTERRUPTIBLE 2
#define TASK_ZOMBIE 4
#define TASK_STOPPED 8
在我的测试结果中,几乎都都是1号状态,即可中断的等待状态。
八.测试结果
改进输出格式后的测试结果:
查看内核版本信息
九.总结
通过此门课程设计真的学到了很多东西,linux学习只看课本不行,要进行linux环境下的实践才能真正掌握好linux,在课程设计过程中发现很多东西不会,一边查资料问同学,一边尝试,当有结果出来时真的很高兴,激发了我学习linux的兴趣。
课程设计完成了,不能说我很好的掌握了内核模块编程与proc文件系统,但自己感觉比以前是进步了,还是很高兴。
15
展开阅读全文