一、实验内容
在Linux-0.11上实现procfs(proc文件系统):/proc
,该目录下有2个文件:
(1)psinfo(进程信息)
当读取此文件的内容时,可得到系统当前所有进程的状态信息。
(2)hdinfo(硬盘信息)
当读取此文件的内容时,可得到系统硬盘的使用情况。
说明:
①procfs要在内核启动时自动创建。
②相关功能实现在 fs/proc.c 文件内。
二、实现procfs
1.增加新文件类型(在include/sys/stat.h
文件中)
(0)原因
在HIT oslab之设备管理 (显示器 + 键盘)讲过,unix/linux哲学认为“一切皆文件”。每个文件都要对应至少一个 inode,而 inode 中记录着文件的各种属性,包括文件类型。文件类型有普通文件、目录、字符设备文件和块设备文件等。在内核中,每种类型的文件都有不同的处理函数与之对应。
inode:index node 索引节点。
(1)方法
①定义一个类型宏 S_IFPROC,其值应在 0010000 到 0100000 之间,但后四位八进制数必须是 0(这是 S_IFMT 的限制,分析测试宏可知原因),而且不能和已有的任意一个 S_IFXXX 相同;
注意:C 语言中以 “0” 直接接数字的常数是八进制数。
②定义一个测试宏 S_ISPROC(m),形式仿照其它的 S_ISXXX(m)
2.把proc文件当作设备文件就可以根据设备号来处理不同的proc文件
(1)逻辑
如果把HIT oslab之设备管理 (显示器 + 键盘)搞明白了,先根据文件类型做一次分支,如果是设备文件接下来就是根据设备号来做一次分支了。
(2)在fs/namei.c
中对sys_mknod()
按如下进行修改:
sys_mknod函数的核心功能:创建inode。
这样就能根据路径/proc/psinfo
找到psinfo的inode了。
3.procfs的初始化
(0)前言
上文要实现根据路径/proc/psinfo
找到psinfo的inode了。首先得有proc目录啊!
而要有proc目录,首先得有根目录啊!而挂载根目录是1号进程在执行init函数时实现的。(注意:此时已经是
用户态
了!!!)
所以,procfs 的初始化工作应该在根文件系统挂载之后开始。
(1)建立 /proc 目录;建立 /proc 目录下的各个文件(以psinfo
为例子)。
由于此时是用户态,不能直接调用sys_mkdir()建立目录和sys_mknod()建立inode。
所以,又是通过系统调用的方式来实现!
1)在init/main.c
中做如下修改
说明:
①保证下图#define
存在,若不存在则自行添加
②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 对应 meminfo,2 对应 cpuinfo。
⑤mkdir时会创建proc目录的inode
⑥S_IFPROC | 0444
真妙啊! 怪不得S_IFPROC的最后4个八进制数是0!这样做一个mode既告知了文件属性,又告知了文件权限。妙不可言啊!
(2)验证
说明:
①03和设置的S_IFPROC(0030000)吻合,0444也和设置的文件权限吻合。
②之所以是EINVAL,是因为还没有实现针对psinfo
的处理函数。但验证了psinfo可以被正确open()。
4.让proc文件可读
(0)逻辑
cat /proc/psinfo
的流程如下:
①fd = open("proc/psinfo");
②read(fd),并把psinfo文件的内容打印到屏幕上。
read函数请求系统调用,让
sys_read
系统调用函数完成读文件的操作。
在sys_read函数中,就有如下内容:
和上文验证结果吻合。
(1)增加对proc文件的处理函数proc_read
(在fs/read_write.c
的sys_read
函数中)
参数说明:
①inode->i_zone[0]
,这就是mknod()
时指定的dev
——设备编号;
②buf
,指向用户空间,就是read()
的第二个参数,用来接收数据;
③count
,就是read()
的第三个参数,说明buf
指向的缓冲区大小;
④&file->f_pos
,f_pos
是上一次读文件结束时“文件位置指针”的指向。这里必须传指针,因为处理函数需要根据传给 buf 的数据量修改 f_pos 的值。
三、实现proc 文件的处理函数proc_read
1.在linux-0.11/fs
目录下新建proc_dev.c
2.proc_dev.c的源码及注释
#include <linux/kernel.h> //用到了malloc(), free()
#include <stdarg.h> //用到了va_start()等和vsprintf()
#include <linux/sched.h> //遍历进程是仿照schedule函数,其在sched.c中
#include <asm/segment.h> //使用了put_fs_byte
#define MAX_NUM 1024 //malloc申请的内存空间不超过1页;
/* 仿照printf函数实现sprintf:将格式化数据写入缓冲区
*成功返回写入buf的字符个数,失败返回-1
* */
//extern int vsprintf(char * buf, const char * fmt, va_list args);
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) {
/*由于buf是用户段的地址空间,所以内核段还得申请一段地址空间
*之后再把内核段数据传输到用户段的地址空间
* */
char *proc_buf = NULL;
int printf_num = 0; //接收sprintf的返回值,表征输出的字符个数;
int tmp_pn; //可能sprintf会返回-1
unsigned long offset = *pos; //博客中解释
int output_num = 0; //根据output_num来更新pos;
struct task_struct **p; //为了遍历系统所有进程
switch (dev) {
case 0:
/** 处理psinfo文件 **/
proc_buf = (char *)malloc(sizeof(char) * MAX_NUM);
printf_num = sprintf(proc_buf, "pid\tstate\tfather\tcounter\tstart_time\n");
/* 遍历系统的所有进程(参照schedule函数的处理)*/
for(p = &FIRST_TASK; p <= &LAST_TASK; p++) {
if(*p) {
tmp_pn = sprintf(proc_buf + printf_num, "%3d\t%5d\t%6d\t%7d\t%10d\n",
(*p)->pid, (*p)->state, (*p)->father, (*p)->counter, (*p)->start_time);
if(tmp_pn < 0) {
//proc_buf空间不够,需要增大MAX_NUM
return -1;
}
printf_num += tmp_pn;
}
}
*(proc_buf + printf_num) = '\0';
break;
default:
break;
}
/* 将内核段数据proc_buf传输到用户段的地址空间buf*/
while(count > 0) {
if(offset > printf_num)
break;
put_fs_byte(*(proc_buf + offset), buf++);
count--;
if(*(proc_buf + offset) == '\0')
break;
offset++;
output_num++;
}
(*pos) += output_num;
free(proc_buf);
return output_num;
}
解释:
①pos的定义在linux/fs.h
中,如下所示:
off_t类型用于指示文件的偏移量, 实际是unsigned long。
疑惑:
感觉pos在proc_read函数中没发挥作用啊。
每次执行cat /proc/psinifo
,都会重新向proc_buf处写内容,然后把内容传输到buf中。但是,从运行结果来看,每次都是从偏移0个字符开始传输(pos = 0,这可能是一种default)。而且确实也应该从proc_buf的0处开始传输。
3.修改fs/Makefile
四、验证
1.cat /proc/psinfo
的结果
五、补充
1.创建/proc/hdinfo
、/proc/inodeinfo
(在init/main.c
的init函数中)
2.在上述proc_read函数中,增加处理hdinfo
(即dev = 1)的内容
(1)选取如下内容作为要打印的hard disk information
①i节点位图所占块数:s_imap_blocks
②逻辑块位图所占块数:s_zmap_blocks
③i节点的总数:s_ninodes;
④逻辑块的总数:s_nzones;
⑤已用的逻辑块数:used_blocks
⑥空闲的逻辑块数:free_blocks
通过②计算出⑤~⑥;
(2)①~④在超级块(super_block)中定义
struct super_block * sb = get_super(current->root->i_dev);
解释:
- 1个文件系统对应1个超级块。
- 当前进程(其结构体,如下所示)的root字段指根目录i节点结构。
- 根目录i节点(其结构体,如下所示)的i_dev字段指明了根目录所在设备的编号。
- 综上所述:get_super(current->root->i_dev)找到了文件系统对应的超级块。而文件系统又是对磁盘(准确说是外部存储设备)的抽象,从而可以打印出hard disk information。
(3)proc_dev.c的源码及注释
#include <linux/kernel.h> //用到了malloc(), free()
#include <stdarg.h> //用到了va_start()等和vsprintf()
#include <linux/sched.h> //遍历进程是仿照schedule函数,其在sched.c中
#include <asm/segment.h> //使用了put_fs_byte
#define MAX_NUM 1024 //malloc申请的内存空间不超过1页;
#define MAX_B_DATA 1024 //一个位图占1024B
/* 仿照printf函数实现sprintf:将格式化数据写入缓冲区
*成功返回写入buf的字符个数,失败返回-1
* */
//extern int vsprintf(char * buf, const char * fmt, va_list args);
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) {
/*由于buf是用户段的地址空间,所以内核段还得申请一段地址空间
*之后再把内核段数据传输到用户段的地址空间
* */
char *proc_buf = NULL;
int printf_num = 0; //接收sprintf的返回值,表征输出的字符个数;
int tmp_pn; //可能sprintf会返回-1
unsigned long offset = *pos; //博客中解释
int output_num = 0; //根据output_num来更新pos;
struct task_struct **p; //为了遍历系统所有进程
/* 与硬盘信息相关的变量*/
struct super_block *sb;
unsigned short sib; // sib指s_imap_blocks
unsigned short szb; // szb指s_zmap_blocks
unsigned short ninodes; // 指s_ninodes;
unsigned short nzones; // 指s_nzones;
int used_blocks = 0;
int free_blocks = 0;
struct buffer_head *bh; // 逻辑块位图缓冲块指针;
int i; // i号逻辑块位图缓冲块
char bd; // 逻辑块位图, 只用1位来指示盘块的使用情况,而char是8bit;
int j; // 1024个bd;
int k; // bd的每1bit;
int flag = 0; // 当used_blocks + free_blocks == nzones,就不应该再统计了。
switch (dev) {
case 0:
/** 处理psinfo文件 **/
proc_buf = (char *)malloc(sizeof(char) * MAX_NUM);
printf_num = sprintf(proc_buf, "pid\tstate\tfather\tcounter\tstart_time\n");
/* 遍历系统的所有进程(参照schedule函数的处理)*/
for(p = &FIRST_TASK; p <= &LAST_TASK; p++) {
if(*p) {
tmp_pn = sprintf(proc_buf + printf_num, "%3d\t%5d\t%6d\t%7d\t%10d\n",
(*p)->pid, (*p)->state, (*p)->father, (*p)->counter, (*p)->start_time);
if(tmp_pn < 0) {
// proc_buf空间不够,需要增大MAX_NUM
return -1;
}
printf_num += tmp_pn;
}
}
*(proc_buf + printf_num) = '\0';
break;
case 1:
/** 处理hdinfo文件 */
// 获得超级块
sb = get_super(current->root->i_dev);
/* 获取1~4信息 */
sib = sb->s_imap_blocks;
szb = sb->s_zmap_blocks;
ninodes = sb->s_ninodes;
nzones = sb->s_nzones;
/* 计算5~6 */
for(i = 0; i < szb; i++) {
if(flag)
break;
bh = sb->s_zmap[i]; // s_zmap指逻辑块位图缓冲块指针数组
for(j = 0; j < MAX_B_DATA; j++) {
if(flag)
break;
bd = *(bh->b_data + j);
for(k = 0; k < 8; k++) {
if(used_blocks + free_blocks == nzones) {
flag = 1;
break;
}
if(bd & (1 << k))
used_blocks++;
else
free_blocks++;
}
}
}
/* 写入proc_buf */
proc_buf = (char *)malloc(sizeof(char) * MAX_NUM);
printf_num = sprintf(proc_buf,
"s_imap_blocks: %d\ns_zmap_blocks: %d\ns_ninodes: %d\ns_nzones: %d\nused_blocks: %d\nfree_blocks: %d\n",
sib, szb, ninodes, nzones, used_blocks, free_blocks);
*(proc_buf + printf_num) = '\0';
break;
default:
break;
}
/* 将内核段数据proc_buf传输到用户段的地址空间buf*/
while(count > 0) {
if(offset > printf_num)
break;
put_fs_byte(*(proc_buf + offset), buf++);
count--;
if(*(proc_buf + offset) == '\0')
break;
offset++;
output_num++;
}
(*pos) += output_num;
free(proc_buf);
return output_num;
}
打印出hdinfo内容的核心部分:
buffer_head数据结构:
(4)结果
①used_blocks + free_blocks = 65536 != 62000
解释:
- 65536是理论值
- 62000是实际值,理由如下:
虽然逻辑块位图有8个缓冲块,但是实际硬盘可能小于64MB,猜测多统计了些free_blocks
②used_blocks + free_blocks = 62000
if(used_blocks + free_blocks == nzones) {
flag = 1;
break;
}
为啥多统计了些used_blocks?
原来超出实际值后,都是用1表示的。所以算成了used_blocks。