0
点赞
收藏
分享

微信扫一扫

操作系统leb5实验报告

高子歌 2022-01-21 阅读 56

实验名称:实验5: proc文件系统编程

实验目的

  • 1、熟悉Linux命令
  • 2、熟悉系统API并编程

实验内容

在leb4中,虽然我们可以通过带参数的模块获取进程号为pid的家族信息,但是我们想获得另外一个进程p2的家族信息就不太方便了,我们只能先卸载该模块,然后以进程p2的pid为参数重新加载该模块。此外,如果使用printk产生输出信息,这些信息和系统其他信息混杂在一起,不利于程序自动提取分析。
本次实验我们仍然实现leb4子任务(2)的功能,但不使用模块参数的方式,而是通过proc文件系统实现用户态和核心态通信,进程的pid 由proc文件传入,进程家族信息也通过proc文件访问。

实验环境

  1. VMware
  2. Fedora7

实验作业

我们在leb4中已经接触到了proc文件系统的基本概念,并且编写程序读取了其中几个文件的内容,扫除了对proc文件系统的神秘感。接下来将改进实验4的不足之处,利用内核模块编程在/proc目录中增加若干文件,用户通过对这些文件的读/写来访问模块提供的功能。通过本次实验,掌握了一种用户态和核心态通信的方法。

实例

procfs_example.c

#include<linux/init.h>
#include<linux/module.h>
#include<linux/proc_fs.h>
#include<linux/sched.h>
#include<asm/uaccess.h>

MODULE_LICENSE("GPL");

#define BUFSIZE 1024

static char global_buffer[BUFSIZE];
static struct proc_dir_entry *example_dir,*hello_file,*current_file,*symlink;

static int proc_read_current(char *page,char** start,off_t off, int count,int *eof,void *data)
{
	int len;
	len=sprintf(page,"current process-->name:%s gid:%d pid:%d\n",current->comm,current->gid,current->pid);//把字符串输出到page	
	return len;
}

static int proc_read_hello(char* page,char **start,off_t off,int count,int *eof,void *data)
{
	int len;
	len=sprintf(page,"%s",global_buffer);//把字符串输出到page
	return len;
}

static int proc_write_hello(struct file *file,const char* buffer,unsigned long count,void *data)
{
	int len;
	if(count>BUFSIZE-1)
		len=BUFSIZE-1;
	else
		len=count;
	if(copy_from_user(global_buffer,buffer,len))
		return -EFAULT;
	global_buffer[len]='\0';
	return len;

}

static int proc_init(void)
{
	example_dir=proc_mkdir("proc_test",NULL);
	example_dir->owner=THIS_MODULE;
	
	current_file=create_proc_read_entry("current",0444,example_dir,proc_read_current,NULL);
	
	current_file->owner=THIS_MODULE;
	
	hello_file=create_proc_entry("hello",0644,example_dir);
	strcpy(global_buffer,"hello\n");
	hello_file->read_proc=proc_read_hello;
	hello_file->write_proc=proc_write_hello;
	hello_file->owner=THIS_MODULE;
	
	symlink=proc_symlink("current_too",example_dir,"current");
	symlink->owner=THIS_MODULE;
	return 0;	
}

static void proc_exit(void)
{
	remove_proc_entry("current_too",example_dir);
	remove_proc_entry("hello",example_dir);
	remove_proc_entry("current",example_dir);
	remove_proc_entry("proc_test",NULL);
}

module_init(proc_init);
module_exit(proc_exit);


上面的程序实际上是一个内核模块,按照实验4介绍的方法编译生成procfs_example.ko文件,然后使用insmod 命令加载,我们知道 proc_init函数将被调用。proc_init首先在/proc目录下创建子目录 proc_test,然后在这个目录下创建两个文件 current和 hello。程序还创建了一个符号链接文件current_too,读current_too的效果跟读current是一样的。

运行图:
![](https://img-blog.csdnimg.cn/img_convert/a261f78ddbbe2e79ad0b5d5938dc9a1f.png#from=url&id=Ly47P&margin=[object Object]&originHeight=266&originWidth=544&originalType=binary&ratio=1&status=done&style=none)
current是只读的,它显示当前进程的pid,gid和可执行程序名等信息,所以第一次执行cat /proc/proc_test/current就是运行cat命令,产生进程的信息,然后该进程就结束了。而后面执行cat /proc/proc_testlcurrent_too输出的是再次运行 cat 命令所产生的进程信息,所以两次信息并不一样。hello文件是可读/写的,其内容被初始化为“hello”,见代码第55行。
这里我们再次强调模块代码和进程的关系,模块不是进程,模块加载后就变成内核的一部分,进程访问内核服务时可能会执行模块的某些代码,如前面的4个命令行(三个cat 和一个echo)都运行了模块的部分代码。最后,可以卸载该模块,proc_exit函数被调用,我们将发现前面创建的proc目录和文件都消失了。

proc文件的核心数据结构

与磁盘文件系统不同的是,proc文件系统的信息驻留在内存中,这意味着proc文件系统不能占太多空间,有两个因素确保了这一点,一是 proc文件系统自身规模很小,二是 proc文件(甚至包括一些proc目录)的内容是根据用户临时需要动态生成的,不需要永久性地占用内存。
proc文件系统保存在内存中的元数据用struct proc_dir_entry结构描述,它既可以描述目录也可以描述文件,定义如下:

<proc_fs.h>
struct proc_dir_entry {
	unsigned int low_ino;	//inode号
	unsigned short namelen;
	const char *name;
	mode_t mode;
	nlink_t nlink;//子目录和软链接的数目
	uid_t uid;
	gid_t gid;
	loff_t size;
	const struct inode_operations *proc_iops;
	const struct file_operations *proc_fops;
	get_info_t *get_info;
	struct module *owner;
	struct proc_dir_entry *next, *parent, *subdir;
	void *data;
	read_proc_t *read_proc;
	write_proc_t *write_proc;
	atomic_t count; /* use count */ 
	int deleted; /* delete flag */ 
	kdev_t rdev; 
};

proc文件系统实际可看成各个结点均是proc_dir_entry的树形结构,每个结点都保存了其父结点( parent 成员)、孩子结点 ( subdir链表)和兄弟结点(next 成员)的信息,便于维护管理。
对于proc编程人员来说,创建和初始化 proc_dir_entry的工作基本可借助内核API完成,只有少部分proc_dir_entry的成员项需要程序员手工控制。

proc文件系统编程接口

下面介绍几个内核函数,通过这些函数可以请求内核在 proc文件系统中创建或删除文件/目录,这些函数的原型在 linux/proc_fs.h 文件中。

(1)创建目录—proc_mkidr(

函数原型为、

struct proc_dir_entry*proc_mkdir(const char *name, struct proc_dir_entry *parent)

该函数将创建一个目录,目录名为name,父目录为 parent。如果要在根目录下创建子目录,则指定parent 为NULL。如果函数创建失败,返回NULL,否则返回新建proc_dir_entry项的地址。

(2)创建文件—create_proc_entry()

函数原型为

struct proc_dir_entry *create_proc_entry(const char*name, mode_t mode,struct proc_dir_entry *parent)

该函数将创建一个proc文件/目录,名字为name,文件类型和访问权限为mode,其父目录为parent。如果想在proc文件系统的根目录下创建,则指定参数 parent为NULL。如果函数创建失败,返回NULL,否则返回新建 proc_dir_entry项的地址。注意,创建的文件和目录不能用常规文件系统的rm,rmdir命令删除,只能用下文介绍的
remove_proc_entry来删除。

(3)创建只读文件—create_proc_read_entry()

函数原型为

struct proc_dir_entry *create_proc_read_entry(const char *name, mode_t mode,structproc_dir_entry*base, read_proc_t *read_proc, void * data)

该函数创建一个只读的 proc文件,其实它只是简单地调用create_proc_entry,并将返回结构的read_proc域的值置为read_procdata域置为data。如果函数创建失败,返回NULL,否则返回新建proc_dir_entry项的地址。

(4)创建符号链接——proc_symlink()

函数原型为

struct proc_dir_entry *proc_symlink(const char *name, struct proc_dir_entry *parent, char *dest)

该函数在 parent目录下创建一个名字为name 的符号链接文件,链接的目标是dest。

(5)删除文件/目录——remove_proc_entry()

函数原型为

void remove_proc_entry(const char *name, struct proc_dir_entry *parent)

该函数删除在parent目录下名为name的proc结点。如果要删除的文件正在使用中,置proc_dir_entry 结构中的deleted标志,否则直接删除。

(6)读/写文件接口——read_proc和 write_proc

仅创建一个proc文件是不够的,文件还要有内容,要么通过该文件将内核信息提供给用户,要么用户通过写该文件影响内核行为。
最简单的处理方法是用户自己提供 proc_dir_entry 结构的read_procwrite_proc成员。这两个函数都是回调函数,也就是说,当对该文件进行读/写时,系统会自动调用它们。用法
读文件接口函数的原型如下:

int (*read_proc)(char*page, char**start, off_t off, int count, int *eof, void *data);

实际上有三种不同的实现方式,感兴趣的读者可以查看内核函数proc_file_read的源代码实现,从中得到详细的信息。三种方式中最简单的一种实现是除了参数 page之外,忽略read_proc 的所有参数,将文件的所有内容都写入参数 page指向的缓冲区,然后返回文件的长度即可。这种实现方式必须有一个前提,就是proc文件的数据量很小,不会超过一个页面。
以/proc/proc_test/current 相应的读函数 proc_read_current的实现为例,因为它只读取当前进程的pid,gid和可执行程序名,信息量绝对不会超过一个页面,所以可以采用上面提到的实现方式。sprintf是核心态函数,声明在 linux/kernel.h中,但它的用法和标准库函数一样; current是一个宏,指向当前进程的task_struct,使用它需要加头文件linux/sched.h。
写文件接口函数的原型如下:

int (*write_proc)(struct file *file, const char *buffer,unsigned long count, void *data);

该函数将buffer 开始的缓冲区中的count个字节写入file 中。data是私有数据,一般不需要关心。由于 buffer一般是用户空间的指针,指向用户空间的缓冲区。因此应先调用copy_from_user 将数据复制到内核空间中来。这里介绍两个内核函数 copy_to_user 和copy_from_user,其原型如下:

unsigned long copy_to_user(void __user*to, const void *from, unsigned long count);
unsigned long copy_from_user(void *to,const void _user*from, unsigned long count);

使用时要包含头文件asm/uaccess.h,前者将内核空间地址 from开始的count个字节复制到用户空间指针to所指向的缓冲区,后者将用户空间地址from开始的count个字节复制到内核空间指针to所指向的缓冲区。
示例/proc/proc_test/hello文件的写函数proc_write_hello非常简单,hello文件使用缓冲区global_buffer保存数据,所以写文件 hello不过就是调用copy_from_user将用户态数据复制到global_buffer而已。另外值得注意的是,write_proc的参数里面根本没有文件偏移,所以用户态程序调用write函数向/proc/proc_test/hello写数据时要一次性写完,如果多次调用write,后面写的数据将覆盖前面的数据。

实验结果

#include<linux/init.h>
#include<linux/module.h>
#include<linux/proc_fs.h>
#include<linux/sched.h>
#include<asm/uaccess.h>


MODULE_LICENSE("GPL");
//参数存放
static int fipid =1;
module_param(fipid,int,S_IRUGO);
//进程信息指针
struct task_struct *p,*xdp,*erp;
struct list_head *h;
struct pid * kpid;
#define BUFSIZE 1024

static char global_buffer[BUFSIZE];
static struct proc_dir_entry *example_dir,*hello_file,*current_file;

static int proc_read_current(char *page,char** start,off_t off, int count,int *eof,void *data)
{
	int len=0;
	p=find_task_by_pid(fipid);
	if(p->parent==NULL)
	{
		printk(KERN_ALERT "4\n");
		len=sprintf(page, "PID号:\t%d\n无父进程",p->pid);
	}
	else 
	{
		len+=sprintf(page, "PID号:\t%d\n父进程PID号:\t%d\n父进程程序名:\t%s\n",p->pid,p->parent->pid,p->parent->comm);
	}
	list_for_each(h,&p->sibling)
	{
		xdp=list_entry(h,struct task_struct,sibling);
		len+=sprintf(page+len, "兄弟进程PID号:	%d\n",xdp->pid);
		len+=sprintf(page+len, "兄弟进程程序名:	%s\n",xdp->comm);
	}	
	list_for_each(h,&p->children)
	{
		erp=list_entry(h,struct task_struct,sibling);
		len+=sprintf(page+len,  "孩子进程PID号:	%d\n",erp->pid);
		len+=sprintf(page+len, "孩子进程程序名:	%s\n",erp->comm);
	}
	return len;
}

static int proc_read_hello(char* page,char **start,off_t off,int count,int *eof,void *data)
{
	int len;
	len=sprintf(page,"%d",fipid);//把字符串输出到page
	return len;
}

static int proc_write_hello(struct file *file,const char* buffer,unsigned long count,void *data)
{
	int len;
	if(count>BUFSIZE-1)
		len=BUFSIZE-1;
	else
		len=count;
	if(copy_from_user(global_buffer,buffer,len))
		return -EFAULT;
	global_buffer[len]='\0';
	printk(KERN_ALERT "12\n");
	fipid=simple_strtol(buffer,NULL,0);
	printk(KERN_ALERT "fpid :%d\n",fipid);
	return fipid;

}

static int proc_init(void)
{
	example_dir=proc_mkdir("proc_test",NULL);
	example_dir->owner=THIS_MODULE;
	
	hello_file=create_proc_entry("pid",0644,example_dir);
	strcpy(global_buffer,"1\n");
	hello_file->read_proc=proc_read_hello;
	hello_file->write_proc=proc_write_hello;
	hello_file->owner=THIS_MODULE;

	current_file=create_proc_read_entry("family",0444,example_dir,proc_read_current,NULL);
	current_file->owner=THIS_MODULE;
	

	
	return 0;	
}

static void proc_exit(void)
{

	remove_proc_entry("family",example_dir);
	remove_proc_entry("pid",example_dir);
	remove_proc_entry("proc_test",NULL);
}

module_init(proc_init);
module_exit(proc_exit);


运行图:
![](https://img-blog.csdnimg.cn/img_convert/a9084290f5100a87b52602ecf9313725.png#from=url&id=hYI6S&margin=[object Object]&originHeight=43&originWidth=354&originalType=binary&ratio=1&status=done&style=none)
查看初始默认pid为1;
![](https://img-blog.csdnimg.cn/img_convert/9a0d8494eb8a49b1e5e2929b73023804.png#from=url&id=DVsTe&margin=[object Object]&originHeight=534&originWidth=594&originalType=binary&ratio=1&status=done&style=none)
family中显现的也是pid为1的进程信息
![](https://img-blog.csdnimg.cn/img_convert/94715e69e0eba323bf48613fae55e973.png#from=url&id=ZWt10&margin=[object Object]&originHeight=138&originWidth=387&originalType=binary&ratio=1&status=done&style=none)
将其他存在的pid号输入到pid文件中,再次打开family,发现其中的信息改变了。
完成实验要求,实验结束。

实验总结

每一次实验让我学到了在平时课堂不可能学到的东西。不一定我的实验能够完成得有多么完美,但是我总是很投入的去研究去学习。但是每完成一个任务我都兴奋不已。总体而言我的实验算是达到了老师的基本要求。总结一下有以下体会。
1、网络真的很强大,用在学习上将是一个非常高效的助手。几乎所有的资料都能够在网上找到。也因为这样,整个实验下来,我浏览的相关网页已经超过了30个(不完全统计)。当然网上的东西很乱很杂,自己要能够学会筛选。
2、敢于攻坚,越是难的问题,越是要有挑战的心理。这样就能够达到废寝忘食的境界。当然这也是不提倡熬夜的,毕竟有了精力才能够打持久战。但是做实验一定要有状态,能够在吃饭,睡觉,上厕所都想着要解决的问题,这样你不成功都难。
[

](https://blog.csdn.net/hml666888/article/details/80473792)

举报

相关推荐

0 条评论