本节将更新哈工大《操作系统》课程第九个 Lab 实验 proc文件系统的实现。按照实验书要求,介绍了非常详细的实验操作流程,并提供了超级无敌详细的代码注释。
实验目的:
- 掌握虚拟文件系统的实现原理;
- 实践文件、目录、文件系统等概念。
实验任务:
在 Linux 0.11 上实现 procfs(proc 文件系统)内的 psinfo 结点。当读取此结点的内容时,可得到系统当前所有进程的状态信息。例如,用 cat 命令显示 /proc/psinfo 的内容,可得到:
cat
:显示文件内容、将文件内容合并输出到终端或其他文件。
- procfs,它是一个
虚拟文件系统
,通常被 mount(挂载) 到 /proc 目录上,通过虚拟文件和虚拟目录的方式提供访问系统参数的机会;- 这些虚拟的文件和目录并没有真实地存在在磁盘上,而是内核中各种数据的一种直观表示,是完全存在于内存中的。虽然是虚拟的,但它们都可以通过标准的系统调用(open()、read() 等)访问。
实现思路:
Linux 0.11 使用的是 Minix 的文件系统,这是一个典型的基于 inode 的文件系统,《注释》一书对它有详细描述。它的每个文件都要对应至少一个 inode,而inode
中记录着文件的各种属性,包括文件类型。文件类型有普通文件、目录、字符设备文件和 9b 块设备文件等。在内核中,每种类型的文件都有不同的处理函数与之对应。我们可以增加一种新的文件类型——proc 文件,并在相应的处理函数内实现 procfs 要实现的功能。
一、增加 proc 文件
1、在 include/sys/stat.h
文件定义中新增 proc
文件的 宏定义 和 测试宏:
//proc文件的宏定义/宏函数
#define S_IFPROC 0030000
#define S_ISPROC(m) (((m) & S_IFMT) == S_IFPROC) //测试m是否是proc文件
2、修改 fs/namei.c
文件,让mknod()
支持新的文件类型
在
proc
目录下创建psinfo
、hdinfo
、inodeinfo
等文件,需要调用mknod
函数 ->sys_mknod
函数,则需要让它支持新的文件类型,成功创建inode
int sys_mknod(const char * filename, int mode, int dev)
{
// ....
inode->i_mode = mode;
if (S_ISBLK(mode) || S_ISCHR(mode) || S_ISPROC(mode))
inode->i_zone[0] = dev;
inode->i_mtime = inode->i_atime = CURRENT_TIME;
3、修改 init/main.c
文件,创建进程proc
文件
procfs 的初始化工作应该在根文件系统挂载之后开始,包括两个步骤:
- 建立 /proc 目录,通过用户态调用
mkdir()
->sys_mkdir()
; - 建立 /proc 目录下的各个结点,通过用户态调用
mknod()
->sys_mknod()
。
/* 新增用户态接口 mkdir 和 mknode 系统调用*/
_syscall2(int,mkdir,const char*,name,mode_t,mode)
_syscall3(int,mknod,const char *,filename,mode_t,mode,dev_t,dev)
void init(void)
{
int pid,i;
setup((void *) &drive_info);
(void) open("/dev/tty0",O_RDWR,0);
(void) dup(0);
(void) dup(0);
/* 创建proc目录 和 文件 */
mkdir("/proc",0755);
mknod("/proc/psinfo",S_IFPROC|0444,0);
mknod("/proc/hdinfo",S_IFPROC|0444,1);
mknod("/proc/inodeinfo",S_IFPROC|0444,2);
// ....
- 参数
0755
(对应 rwxr-xr-x),表示只允许 root 用户改写此目录 - 参数
S_IFPROC|0444
做为 mode 值,表示这是一个 proc 文件,权限为 0444(r–r–r–),对所有用户只读。 - mknod() 的第三个参数
dev
用来说明结点所代表的设备编号。
此时若调用
cat /proc/psinfo
会报错,因为内核在对 psinfo 进行读操作时不能正确处理,所以还要创建proc_read
函数,并在sys_read()
中打补丁。
二、实现 proc 文件可读
1、创建 fs/proc.c
文件,实现根据设备编号,把不同的内容写入到用户空间的 buf,使得 cat
指令可以正确获取 proc
文件内容。
#include <linux/kernel.h>
#include <linux/sched.h>
#include <asm/segment.h>
#include <linux/fs.h>
#include <stdarg.h>
#include <unistd.h>
// 内联汇编定义一个宏,用于在位图中测试特定位是否被设置
#define set_bit(bitnr,addr) ({ \
register int __res ; \
__asm__("bt %2,%3;setb %%al":"=a" (__res):"a" (0),"r" (bitnr),"m" (*(addr))); \
__res; })
// 定义一个缓冲区,用于存储进程、硬盘或 inode 信息,大小为 4096 字节
char proc_buf[4096] ={'\0'};
// 声明一个外部函数 vsprintf,用于将格式化输出写入字符串
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;
}
// 获取进程信息函数,并将其格式化写入 proc_buf
int get_psinfo()
{
int read = 0;
read += sprintf(proc_buf+read,"%s","pid\tstate\tfather\tcounter\tstart_time\n");
struct task_struct **p;
// 遍历所有进程
for(p = &FIRST_TASK ; p <= &LAST_TASK ; ++p)
if (*p != NULL)
{
// 进程ID 进程状态 父进程ID 时间片 启动时间
read += sprintf(proc_buf+read,"%d\t",(*p)->pid);
read += sprintf(proc_buf+read,"%d\t",(*p)->state);
read += sprintf(proc_buf+read,"%d\t",(*p)->father);
read += sprintf(proc_buf+read,"%d\t",(*p)->counter);
read += sprintf(proc_buf+read,"%d\n",(*p)->start_time);
}
return read;
}
// 获取硬盘信息(参考fs/super.c mount_root()函数)
int get_hdinfo()
{
int read = 0;
int i,used;
struct super_block * sb;
sb=get_super(0x301); /*磁盘设备号 3*256+1*/
/*Blocks信息:总块数、已用块数、空闲块数*/
read += sprintf(proc_buf+read,"Total blocks:%d\n",sb->s_nzones);
used = 0;
i=sb->s_nzones;
while(--i >= 0)
{
if(set_bit(i&8191,sb->s_zmap[i>>13]->b_data))
used++;
}
read += sprintf(proc_buf+read,"Used blocks:%d\n",used);
read += sprintf(proc_buf+read,"Free blocks:%d\n",sb->s_nzones-used);
/*Inodes 信息:inode数量、已用/空闲inode数量*/
read += sprintf(proc_buf+read,"Total inodes:%d\n",sb->s_ninodes);
used = 0;
i=sb->s_ninodes+1;
while(--i >= 0)
{
if(set_bit(i&8191,sb->s_imap[i>>13]->b_data))
used++;
}
read += sprintf(proc_buf+read,"Used inodes:%d\n",used);
read += sprintf(proc_buf+read,"Free inodes:%d\n",sb->s_ninodes-used);
return read;
}
// 获取inode信息,inode 号和第一个数据区块号
int get_inodeinfo()
{
int read = 0;
int i;
struct super_block * sb;
struct m_inode *mi;
sb=get_super(0x301); /*磁盘设备号 3*256+1*/
i=sb->s_ninodes+1;
i=0;
while(++i < sb->s_ninodes+1)
{
if(set_bit(i&8191,sb->s_imap[i>>13]->b_data))
{
mi = iget(0x301,i);
read += sprintf(proc_buf+read,"inr:%d;zone[0]:%d\n",mi->i_num,mi->i_zone[0]);
iput(mi);
}
if(read >= 4000)
{
break;
}
}
return read;
}
// proc文件读取函数,根据设备号决定读取信息,并将读取的信息复制到用户空间的缓冲区
int proc_read(int dev, unsigned long * pos, char * buf, int count)
{
int i;
if(*pos % 1024 == 0)
{
if(dev == 0)
get_psinfo();
if(dev == 1)
get_hdinfo();
if(dev == 2)
get_inodeinfo();
}
for(i=0;i<count;i++)
{
if(proc_buf[i+ *pos ] == '\0')
break;
put_fs_byte(proc_buf[i+ *pos],buf + i+ *pos);
}
*pos += i;
return i;
}
2、修改 fs/Makefile
文件中的编译规则:
OBJS= open.o read_write.o inode.o file_table.o buffer.o super.o \
block_dev.o char_dev.o file_dev.o stat.o exec.o pipe.o namei.o \
bitmap.o fcntl.o ioctl.o truncate.o proc.o
//......
### Dependencies:
proc.o : proc.c ../include/linux/kernel.h ../include/linux/sched.h \
../include/linux/head.h ../include/linux/fs.h ../include/sys/types.h \
../include/linux/mm.h ../include/signal.h ../include/asm/segment.h
3、修改 fs/read_write.c
文件,在 sys_read
中添加对 proc
文件的处理函数补丁,这样cat
指令才能成功读到内容。
/*新增proc_read函数外部调用*/
extern int proc_read(int dev,char* buf,int count,unsigned long *pos);
int sys_read(unsigned int fd,char * buf,int count)
{
// ....
if (inode->i_pipe)
return (file->f_mode&1)?read_pipe(inode,buf,count):-EIO;
/*新增proc_read调用*/
if (S_ISPROC(inode->i_mode))
return proc_read(inode->i_zone[0],&file->f_pos,buf,count);
// ....
}
- inode->i_zone[0],这就是 mknod() 时指定的 dev :设备编号
- &file->f_pos,f_pos 是上一次读文件结束时“文件位置指针”的指向
- buf,指向用户空间,就是 read() 的第二个参数,用来接收数据
- count,就是 read() 的第三个参数,说明 buf 指向的缓冲区大小
三、编译并运行
1、编译并运行
cd oslab_Lab8/linux-0.11
make all
../run
2、测试结果
在 Bochs 中进行测试:
cat /proc/psinfo
cat /proc/hdinfo
cat /proc/inodeinfo