文章目录
实验目的
- 掌握虚拟文件系统的实现原理
- 实践文件、目录、文件系统等概念
实验内容
在Linux0.11上实现procfs(proc文件系统)内的psinfo节点,当读取此节点的内容的时候,可得到系统当前所有进程的状态信息,例如,用cat
命令显示/proc/procfo
的内容,可得到:
# cat /proc/psinfo
pid state father counter start_time
0 1 -1 0 0
1 1 0 28 1
4 1 1 1 73
3 1 1 27 63
6 0 4 12 817
# cat /proc/hdinfo
total_blocks: 62000;
free_blocks: 39037;
used_blocks: 22963;
procfs
及其节点要在内核启动时自动创建,相关功能的实现放在fs/proc.c
文件。
实验过程
- 第一步: 增加新文件类型,在
include/sys/stat.h
文件中定义了几种文件类型和相应的测试宏:
//已有的宏定义
#define S_IFMT 00170000 //文件类型(都是8进制表示)
#define S_IFREG 0100000 //普通文件
#define S_IFCHAR 0020000 //字符设备文件
#define S_ISREG(m) (((m) & S_IFMT) == S_IFREG) //测试m是否是普通文件
#define S_ISCHAR(m) (((m) & S_IFMT) == S_IFCHAR) //测试m是否是字符设备文件
//proc文件的宏定义/宏函数
#define S_IFPROC 0030000
#define S_ISPROC(m) (((m) & S_IFMT) == S_IFPROC) //测试m是否是proc文件
- 第二步: 让
mknod()
支持新的文件类型,文件/proc/psinfo
以及/proc/hdinfo
索引节点需要通过mknod()
系统调用建立,所以要让它支持新的文件类型,直接修改fs/namei.c
文件中的sys_mknod()函数的代码,在其中增加关于proc文件系统的判断:
if(S_ISBLK(mode) || S_ISCHAR(mode) || S_ISPROC(mode))
inode->izone[0] = dev;
- 第三步: 进程
proc
文件初始化,可以在系统启动时就建立那些proc
文件系统(根目录/proc
)下的proc
文件(其下的子文件/proc/psinfo
、/proc/hdinfo
),通常Linux
操作系统也都是在系统启动以后就直接创建那些proc
文件的,显然要修改系统启动时的调用的main()
,因为是要创建文件,所以当然应该在文件系统已经挂载以后才可以操作,在main()
的最后会从内核态切换到用户态,并调用init()
,而init()
要做的第一件事就是挂载根文件系统:
void init(void)
{
setup((void *) &drive_info);
}
显然在执行setup((void *) &drive_info)
的时候,也就是根文件系统挂载以后就可以创建proc
文件了,首先建立/proc
目录,然后再建立该目录下的各个proc
文件节点,建立目录用mkdir()
,建立文件用mknod()
。
现在可以调用mkdir()
来创建proc
目录,调用mknod()
来创建proc
目录下的各个proc
文件节点了。
内核初始化的全部工作是在main()
中完成,而/init/main()
在最后从内核态切换到用户态,并调用init()
。init()
做的第一件事情就是挂载根文件系统:setup((void *) &drive_info);
procfs
的初始化工作应该在根文件系统挂载之后开始。它包括两个步骤:
- 建立
/proc
目录 - 建立
/proc
目录下的各个结点
建立目录和结点分别需要调用mkdir()和mknod()系统调用。因为初始化时已经在用户态,所以不能直接调用sys_mkdir()和sys_mknod()。必须在初始化代码所在文件中实现这两个系统调用的用户态接口,即API:
_syscall2(int,mkdir,const char*,name,mode_t,mode)
_syscall3(int,mknod,const char *,filename,mode_t,mode,dev_t,dev)
mkdir()
时mode
参数的值可以是“0755”(rwxr-xr-x)
,表示只允许root
用户改写此目录,其它人只能进入和读取此目录。
procfs是一个只读文件系统,所以用mknod()
建立psinfo
结点时,必须通过mode
参数将其设为只读。建议使用“S_IFPROC|0444”
做为mode
值,表示这是一个proc
文件,权限为0444(r--r--r--)
,对所有用户只读。mknod()
的第三个参数dev
用来说明结点所代表的设备编号。对于procfs
来说,此编号可以完全自定义。proc
文件的处理函数将通过这个编号决定对应文件包含的信息是什么。例如,可以把0
对应psinfo
,1
对应hdinfo
,2对应inodeinfo
。
上述步骤完成以后,就可以使用make all
编译内核,然后./run
运行内核,使用ll /proc
可以看到:
inode->i_mode
就是通过mknod()
设置的mode
。信息中的XXX
和你设置的S_IFPROC
有关。通过此值可以了解mknod()
工作是否正常。这些信息说明内核在对psinfo
进行读操作时不能正确处理,向cat
返回了EINVAL
错误。因为还没有实现处理函数,所以这是很正常的。
注意:博主在此没截使用cat命令的图片,但是就是这个么道理,嘻嘻。
这些信息至少说明,psinfo
被正确open()
了。所以我们不需要对sys_open()
动任何手脚,唯一要打补丁的,是sys_read()
。
- 第四步: 让
proc
文件(psinfo、hdinfo、inodeinfo
)可读open()
没有问题,那么需要修改的就是sys_read()了
。
首先在fs/read_write.c
中添加extern
,表示proc_read
函数是从外部调用的。
在.c文件中要引入另一个文件,而且是另一个.c文件的全局变量或者函数,就需要用extern来说明一下。
然后仿照其他if
语句,添加proc
文件的proc_read()
调用。
- 第五步: 实现上述的
proc_read()
函数,用于读取proc
文件内容,实现在fs/proc.c
文件下。
#include <linux/sched.h>
#include <linux/kernel.h>
#include <asm/segment.h>
#include <stdarg.h>
#include <stddef.h>
extern int vsprintf(char * buf, const char * fmt, va_list args);
//Linux0.11没有sprintf(),该函数是用于输出结果到字符串中的,所以就实现一个,这里是通过vsprintf()实现的。
int sprintf(char *buf, const char *fmt, ...){
va_list args; int i;
va_start(args, fmt);
i=vsprintf(buf, fmt, args);
va_end(args);
return i;
}
int proc_read(int dev, char * buf, int count, unsigned long * pos){
struct task_struct ** p;
int output_count=0;
char * proc_buf=NULL;
int file_size=0;
int offset=*pos;
struct super_block * sb;
struct buffer_head * bh;
int total_blocks, total_inodes;
int used_blocks=0, free_blocks=0;
int i,j,k;
char * db=NULL;
//硬盘总共有多少块(空闲 + 非空闲),有多少inode索引节点等信息都放在super块中。
sb=get_super(current->root->i_dev);
total_blocks = sb->s_nzones;
total_inodes=sb->s_ninodes;
s_imap_blocks = sb->s_imap_blocks;
s_zmap_blocks = sb->s_zmap_blocks;
//psinfo: 对应的就是输出系统此时的全部进程的状态信息
if(dev==0)
{
proc_buf=(char *)malloc(sizeof(char *)*1024);
file_size=sprintf(proc_buf,"pid\tstate\tfather\tcounter\tstart_time\n");
//这里借鉴了,进程切换函数schedule()的代码,也就是遍历系统全部的进程。
for(p = &LAST_TASK ; p >= &FIRST_TASK ; --p)
if(*p)
file_size+=sprintf(proc_buf+file_size,"%d\t%d\t%d\t%d\t%d\n",(*p)->pid,(*p)->state,(*p)->father,(*p)->counter,(*p)->start_time);
*(proc_buf+file_size)='\0';
}
//hdinfo: 打印出硬盘的一些信息,
//s_imap_blocks、ns_zmap_blocks、
//total_blocks、free_blocks、used_blocks、total_inodes
if(dev==1)
{
for(i=0;i<sb->s_zmap_blocks;i++)
{
bh=sb->s_zmap[i];
db=(char*)bh->b_data;
for(j=0;j<1024;j++){
for(k=1;k<=8;k++){
if((used_blocks+free_blocks)>=total_blocks)
break;
if( *(db+j) & k)
used_blocks++;
else
free_blocks++;
}
}
}
proc_buf=(char*)malloc(sizeof(char*)*512);
file_size=sprintf(proc_buf,"s_imap_blocks:%d\ns_zmap_blocks:%d\n",s_imap_blocks,s_zmap_blocks);
file_size+=sprintf(proc_buf+file_size,"total_blocks:%d\nfree_blcoks:%d\nused_blocks:%d\ntotal_indoes:%d\n",total_blocks,free_blocks,used_blocks,total_inodes);
}
//将proc_buf缓冲区的内容放入文件
while(count>0)
if(offset>file_size)
break;
put_fs_byte(*(proc_buf+offset),buf++);
offset++;
output_count++;
count--;
}
//重置文件的pos位置,也就是指向文件末尾的指针
(*pos)+=output_count;
free(proc_buf);
return output_count;
}
由于添加了一个文件proc.c
,所以需要改下fs/Makefile
,
上述的代码,是用的这位同学的,在此表示感谢!
(但是我搞不清为什么是proc_dev,添加的文件不是proc.c吗,如果有人可以解答,可以评论下。)
然后make all
编译内核,./run
运行内核,输出cat
命令,即可查看psinfo
(当前系统进程状态信息)和hdinfo
(硬盘信息)的信息。
实验问题
- 如果要求你在psinfo之外再实现另一个结点,具体内容自选,那么你会实现一个给出什么信息的结点?为什么?
我会实现meminfo、cpuinfo这些节点,分别对应的信息是系统内存信息和cpu的信息,原因是
我只知道这两个名词了
- 一次read()未必能读出所有的数据,需要继续read(),直到把数据读空为止。而数次read()之间,进程的状态可能会发生变化。你认为后几次read()传给用户的数据,应该是变化后的,还是变化前的? 如果是变化后的,那么用户得到的数据衔接部分是否会有混乱?如何防止混乱? 如果是变化前的,那么该在什么样的情况下更新psinfo的内容?
我认为后几次read()传递给用户的数据应该是变化前的,因为读完一部分数据之后,之前读取的进程状态信息可能已经发生了改变了,那么读给proc_buf缓冲区的内容还是读之前的,所以可能会导致读到的数据出现混乱(数据不正确?),要使得数据读的正确、不混乱,就要在读的时候让想要变化的进程先等待,等读完了数据放到了文件中以后,再唤醒要更新的进程。
注:我的答案很可能不准确,请别被我误导了。
HIT-OS-LAB参考资料:
1.《操作系统原理、实现与实践》-李治军、刘宏伟 编著
2.《Linux内核完全注释》
3.两个哈工大同学的实验源码
4.Linux-0.11源代码
(上述资料,如果有需要的话,请主动联系我))