哈工大操作系统实验9 proc文件系统的实现

哈工大操作系统实验9 proc文件系统的实现

该篇文章是哈工大操作系统实验9——proc文件系统的实现完成笔记,其中包含了详细的步骤和相关代码,并有截图说明。实验内容都成功通过了,但是因为内容较多,记录中难免会有疏忽,如有发现错误,欢迎大家留言和我联系。

文件系统有点难理解,一开始我一直没有搞太明白file、inode、block等之间的关系,后来结合源码、注释书籍、实验内容终于弄清晰了。欢迎大家一键三连:点赞、关注加收藏,感谢大家的支持。

理论知识

实验内容推荐大家学习对应的视频课程:

  • L28 生磁盘的使用
  • L29 用文件使用磁盘
  • L30 文件使用磁盘的实现
  • L31 目录与文件系统
  • L32 目录解析代码实现

另外推荐阅读《注释》书籍:

  • 第12章 文件系统(fs)

实验内容:

image

内核实现

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的含义可以参考这个图:

image

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类型文件的读取。原理可以参考这个图:

image

4)创建 fs/proc.c 文件,实现根据设备编号,获取到进程信息、硬盘信息和节点信息,然后把这些内容写入到用户空间的 buf,使得 cat 指令可以正确获取 proc 文件内容。

proc.c文件的层级和char_dev.c、block_dev.c、file_dev.c的层级是一致的。

关于具体实现方面:

  • 获取进程信息函数,这个实现通过遍历进程列表,获取其信息即可。
  • 获取硬盘信息和节点信息,这些信息在磁盘的超级块中有存储,获取超级块的信息,然后进行处理,下图帮助会很大,注释书籍中对应的章节也有详细说明了。

image

#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

如果编译成功通过,就可以查看到如下信息:

image

实验报告

完成实验后,在实验报告中回答如下问题:

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函数首尾开关中断处理。

参考资料

从以下资料得到了不少帮助,特此表示感谢。

完。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

晴空闲雲

感谢家人们的投喂

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值