哈工大操作系统实验9 proc文件系统的实现
该篇文章是哈工大操作系统实验9——proc文件系统的实现完成笔记,其中包含了详细的步骤和相关代码,并有截图说明。实验内容都成功通过了,但是因为内容较多,记录中难免会有疏忽,如有发现错误,欢迎大家留言和我联系。
文件系统有点难理解,一开始我一直没有搞太明白file、inode、block等之间的关系,后来结合源码、注释书籍、实验内容终于弄清晰了。欢迎大家一键三连:点赞、关注加收藏,感谢大家的支持。
理论知识
实验内容推荐大家学习对应的视频课程:
- L28 生磁盘的使用
- L29 用文件使用磁盘
- L30 文件使用磁盘的实现
- L31 目录与文件系统
- L32 目录解析代码实现
另外推荐阅读《注释》书籍:
- 第12章 文件系统(fs)
实验内容:
内核实现
1)在 include/sys/stat.h 文件定义中新增 proc 文件的 宏定义 和 判断宏。
在stat.h文件中定义了文件的相关的宏和判断方法,我们可以把proc的宏定义和判断宏也放在这里。
// proc文件的宏定义/宏函数。其中 S_IFMT 表示文件类型屏蔽码。
#define S_IFPROC 0030000 // 八进制表示法
#define S_ISPROC(m) (((m) & S_IFMT) == S_IFPROC) // 测试m是否是proc文件,m表示文件类型和属性
// (m) & S_IFMT 会屏蔽掉权限位。
// S_IFMT=00170000,二进制表示为 1 111 000 000 000 000,
// 取 & 就相当于屏蔽掉低12位了,只取高4位,高4位就是表示目录类型的
m的含义可以参考这个图:
2)修改 fs/namei.c 文件,让 mknod() 支持新的文件类型。
mknod函数用于创建一个特殊文件或普通文件节点(node),因为我们新增加了一个文件类型,所以这里需要让它支持新的文件类型。
int sys_mknod(const char * filename, int mode, int dev)
{
// ....
inode->i_mode = mode;
// 设置该 i 节点的属性模式。如果要创建的是块设备文件或者是字符设备文件,又或者是新增加的proc文件
// 则令 i 节点的直接块指针 0 等于设备号。
if (S_ISBLK(mode) || S_ISCHR(mode) || S_ISPROC(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()。
// init()函数中
/* 创建proc目录和文件 */
// 参数 0755(对应 rwxr-xr-x),表示只允许 root 用户改写此目录
mkdir("/proc",0755);
// S_IFPROC 表示是PROC文件
// 0444是一个8进制数,二进制表示为100100100,对应权限r--r--r--,表示对所有用户都是只读
// S_IFPROC|0444 表示这个文件是PROC文件,并且只读
mknod("/proc/psinfo",S_IFPROC|0444,0);
mknod("/proc/hdinfo",S_IFPROC|0444,1);
mknod("/proc/inodeinfo",S_IFPROC|0444,2);
- 创建目录 /proc 的参数 0755 是一个八进制表示的数(对应 rwxr-xr-x),表示只允许 root 用户改写此目录;
- 参数 S_IFPROC|0444 作为 mode 值,表示这是一个 proc 文件,权限为 0444(r–r–r–),对所有用户只读;
- mknod() 的第三个参数 dev 用来说明结点所代表的设备编号。在后面实现 proc_read 的时候,通过这个编号判断是读取哪个文件的信息。
虽然相关的文件都已经创建好了,但是内核还没实现对应的读取方法,此时若调用 cat /proc/psinfo 会报错,所以还要创建 proc_read 函数,并在 sys_read() 中新增加PROC类型文件的读取。原理可以参考这个图:
4)创建 fs/proc.c 文件,实现根据设备编号,获取到进程信息、硬盘信息和节点信息,然后把这些内容写入到用户空间的 buf,使得 cat 指令可以正确获取 proc 文件内容。
proc.c文件的层级和char_dev.c、block_dev.c、file_dev.c的层级是一致的。
关于具体实现方面:
- 获取进程信息函数,这个实现通过遍历进程列表,获取其信息即可。
- 获取硬盘信息和节点信息,这些信息在磁盘的超级块中有存储,获取超级块的信息,然后进行处理,下图帮助会很大,注释书籍中对应的章节也有详细说明了。
#include <linux/kernel.h> // 内核头文件。含有一些内核常用函数的原形定义。
#include <linux/sched.h> // 调度程序头文件,定义了任务结构 task_struct、初始任务 0 的数据,还有一些有关描述符参数设置和获取的嵌入式汇编函数宏语句。
#include <asm/segment.h> // 段操作头文件。定义了有关段寄存器操作的嵌入式汇编函数。
#include <linux/fs.h> // 文件系统头文件。定义文件表结构(file,buffer_head,m_inode 等)。
#include <stdarg.h> // 标准参数头文件。以宏的形式定义变量参数列表。主要说明了-个类型(va_list)和三个宏(va_start, va_arg 和 va_end),用于vsprintf、vprintf、vfprintf 函数。
#include <unistd.h> // Linux 标准头文件。定义了各种符号常数和类型,并申明了各种函数。
// 内联汇编定义一个宏,用于在位图中测试特定位是否被设置
#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(),该函数是用于输出结果到字符串中的,所以就实现一个。可以参考 printf() 。
// 其中 buf 是输出字符串缓冲区;参数 fmt 是格式字符串;
// 返回写入字符串的字符数。
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)
{
// set_bit实际上是test_bit
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, "i_num: %d; i_zone[0]: %d\n", mi->i_num, mi->i_zone[0]);
iput(mi);
}
if (read >= 4000) // proc_buf缓冲区最大4096
{
break;
}
}
return read;
}
// proc文件读取函数,根据设备号决定读取信息,并将读取的信息复制到用户空间的缓冲区
// 其中参数dev表示设备号;pos表示位置指针;buf表示用户空间的缓冲区;nr表示欲读字节数。
// 返回已读取的字节数。
int proc_read(int dev, unsigned long *pos, char *buf, int nr)
{
int i;
// 检测要读取哪个信息。在main.c创建对应文件节点的时候,指定了设备号。
if (dev == 0)
get_psinfo();
if (dev == 1)
get_hdinfo();
if (dev == 2)
get_inodeinfo();
// 将 proc_buf 的内容复制到 buf 中。
for (i = 0; i < nr; i++)
{
if (proc_buf[i + *pos] == '\0')
break;
put_fs_byte(proc_buf[i + *pos], buf + i + *pos);
}
*pos += i; // 更新位置指针。读取多少字节,就往后移动多少字节。
return i;
}
5)修改 fs/Makefile 文件中的编译规则。
# 增加 proc.o
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
6)修改 fs/read_write.c 文件,在 sys_read 中添加对 proc 文件的处理函数补丁,这样cat指令才能成功读到内容。
sys_read 就是文件系统顶层的读取方法实现,通过在这里判断设备类型,然后调用不同的设备各自的读取方法实现。
/* 新增proc_read调用 */
if (S_ISPROC(inode->i_mode))
return proc_read(inode->i_zone[0],&file->f_pos,buf,count);
printk("(Read)inode->i_mode=%06o\n\r",inode->i_mode);
return -EINVAL;
编译运行
# 在 oslab 目录下
$ cd ./linux-0.11
$ make all
$ ../run
如果编译成功通过,就可以查看到如下信息:
实验报告
完成实验后,在实验报告中回答如下问题:
1) 如果要求你在 psinfo 之外再实现另一个结点,具体内容自选,那么你会实现一个给出什么信息的结点?为什么?
我会考虑实现打印内存使用情况。因为操作系统就是管理硬件,统一对上层应用提供服务的,而硬件中,内存也是很重要的一个。
2) 一次 read() 未必能读出所有的数据,需要继续 read(),直到把数据读空为止。而数次 read() 之间,进程的状态可能会发生变化。你认为后几次 read() 传给用户的数据,应该是变化后的,还是变化前的? + 如果是变化后的,那么用户得到的数据衔接部分是否会有混乱?如何防止混乱? + 如果是变化前的,那么该在什么样的情况下更新 psinfo 的内容?
我认为应该是变化前的数据。如果前一部分的数据是变化前的,后一部分是变化后的,可能会导致进程的信息不一致,这个想想有点蛋疼( ╯□╰ )。
当 pos 位置指针为0时,才更新内容,*pos位置指针为0时,表示从头读取,此时可以重新获取所有进程信息。
另外从程序的实现上考虑:在读取 psinfo 信息时,读取到的信息是先保存在内核的 proc_buf 中,而后才复制到用户空间的 buf 中。在从 proc_buf -> 用户空间buf 这个阶段不管系统内进程如何切换,都不会影响 proc_buf 中的内容(多进程同时改动proc_buf内容不考虑)。但是如果要保证写proc_buf时,进程信息都是那一瞬间的所有进程信息,可以考虑在get_psinfo函数首尾开关中断处理。
参考资料
从以下资料得到了不少帮助,特此表示感谢。
完。