Linux0.11 prc文件系统的实现
实验要求
在 Linux 0.11上实现 procfs(proc 文件系统)内的psinfo结点。当读取此结点的内容时,可得到系统当前所有进程的状态信息。例如,用 cat命令显示 /proc/psinfo 的内容,可得到:
$ 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文件。
还是先将原理再讲实现。
实验原理
1. 磁盘与目录的抽象
这一部分看一下李老师的课程会比较好。
从一个抽象视图来了解:
在用户眼中,文件是一串字符流,或者说目录,及类似于 /dev/t1 /user/root/m1 诸如此类
从底层硬件角度来说,文件是存储在磁盘中的,由磁道,扇区,柱面构成。
因此需要一种抽象方式,形成由 字符流->inode->磁盘的映射。(其中inode可以理解为为了实现这个映射而设计的一种数据结构,,其中存储了与字符流,以及磁盘相关的信息)。
下面来一层一层讲讲自底向上是如何实现层层的抽象的。
1. 从磁盘到盘块号映射
https://blog.csdn.net/weixin_44673253/article/details/127105460
这部分看上课讲义即可,讲起来也比较麻烦。
2. 从inode到盘块号的映射
简单来说就是对于中间层数据结构inode,他会存储这个文件对应的盘块号,这样就知道了文件的存储位置。
https://blog.csdn.net/weixin_44673253/article/details/127118455
3.从文件名到inode文件结构的映射
https://blog.csdn.net/weixin_44673253/article/details/127139855
https://blog.csdn.net/weixin_44673253/article/details/127151902
将整个磁盘的盘块存储映射关系维护结构使得在用户眼里形成一堆有组织的文件(树状的目录树)
也就是说,从上往下来说用户按照上述目录树的结构去存储文件,去访问文件,管理文件,那么就把这种读写,最后通过磁盘上存取的映射关系转换成对磁盘的读写,这就是文件系统。
底层的这种结构就是对上层的实现,而上层就是对底层的抽象,用户眼里看到的就是这种目录树的结构,但是底层则是操作系统那种盘块号的样子,所以说,这也解释了为什么磁盘插在哪台电脑上,用户看到的东西都是一样的原因,因为里面的结构已经形成文件系统了,可以完成这个抽象,所以在另外一台机器上,根据程序就可以建立起这样一个视图。
4. 目录解析代码实现
https://blog.csdn.net/weixin_44673253/article/details/127151902
这些东西讲起来都有点麻烦,但只有理解了整个文件系统的抽象与映射,才能对整个的文件系统有一个了解。虽然这些东西对于本身的实验来说,其实没啥用,因为这个实验设计的比较简单,压根不需要你了解文件系统,你只需要知道创建一个新的文件类别的大概过程即可。不论怎样只需要记住下面一个图:
假设我要对一个 /var/data 目录进行解析,从而得到他在磁盘中的具体位置,那么大致流程如下,这是一个自顶向下的过程:
- 解析/var,在根目录的数据区中遍历,查找到 var在i节点组中的索引。
- 根据var在i节点中的索引 index_var,即可获得var文件的inode,根据inode数据结构,可以得到var文件的数据区。
- 根据var的数据区,遍历找到data在i节点组中的索引,并根据索引找到data的inode。
- inode中存在 data文件与磁盘块的映射关系,因此可以找到这个文件拥有的所有磁盘块索引,根据磁盘块索引,就可以找到所有的对应的磁盘文件。
2. 文件系统的基本概念
对于一个硬盘设备,通常都会划分出几个盘片,每个盘片存放着一个不同的文件系统。比如下图将一个硬盘分成了4个分区,包含了4个不同的文件系统。其中主引导扇区中存放着磁盘引导程序和分区表信息(指明硬盘上的每个分区的类型)。
对于Linux0.11内核采用的文件系统是MINIX文件系统,它的分布如下图所示:
① 引导块:用来引导设备的,通常在上电时由BIOS自动读入运行的数据。对于非引导设备的盘引导块内容为空。
② 超级快:相当于文件系统的描述符,定义如下:
struct super_block {
unsigned short s_ninodes;
unsigned short s_nzones;
unsigned short s_imap_blocks;
unsigned short s_zmap_blocks;
unsigned short s_firstdatazone;
unsigned short s_log_zone_size;
unsigned long s_max_size;
unsigned short s_magic;
/* These are only in memory */
struct buffer_head * s_imap[8];
struct buffer_head * s_zmap[8];
unsigned short s_dev;
struct m_inode * s_isup;
struct m_inode * s_imount;
unsigned long s_time;
struct task_struct * s_wait;
unsigned char s_lock;
unsigned char s_rd_only;
unsigned char s_dirt;
};
/*
s_ninode表示当前块设备的inode节点数。每个inode代表一个文件。
s_nzones表示当前块设备上以逻辑块为大小 (1KB)的逻辑块数。
s_imap_blocks与s_zmap_blocks分别表示当前块设备的inode节点位图和逻辑块位图所占用的逻辑块数。
逻辑块位图用于描述当前设备每个磁盘块的使用情况。如果为0,表示对应的磁盘块是空闲的,可以分配使用。当一个磁盘块被分配占用后,对应的逻辑块位图的比特位被置1
根据超级块数据结构中定义,s_zmap是一个数组,它占用了8块磁盘块大小,每个块大小 是1024字节,因此总共可以管理8192*8个比特位,每个比特位分别对应一个数据磁盘块,总计这8个磁盘块大小可以管理655356数据磁盘块。
inode节点位图是用来标记iNode节点的使用情况。与逻辑块位图 类似,当创建一个文件时候,我们分配一个iNode数据结构,并且些iNode实例对应的inode位图数组对应的位需要被置1。
s_firstdatazones表示当前块设备上数据区开始位置占用的第一个逻辑块的块号。
s_max_size表示当前块设备上,以字节为单位的最大文件 的长度。
s_magic表示文件系统的魔数,标示了文件系统的类型。
*/
④ i节点位图:类似逻辑块位图。(用来确定哪些文件逻辑块i 被使用了,哪些没被使用)
③ 逻辑块位图:每一位对应于逻辑块的使用情况,如果对应逻辑块使用了,则逻辑块位图的位置1. (用来确定哪些磁盘块已经被使用了,哪些磁盘块没被使用)
⑤ i节点:是目录与磁盘的桥接, 文件的属性描述。
⑥ 逻辑块:用来存储数据的逻辑单元。
对于i节点定义在文件/include/linux/fs.h中,i节点也就是中间层文件抽象的数据结构inode节点,如下所示:
struct m_inode {
unsigned short i_mode; //文件的类型和属性
unsigned short i_uid; //宿主用户id
unsigned long i_size; //文件的大小
unsigned long i_mtime; //文件的修改时间
unsigned char i_gid; //用户组id
unsigned char i_nlinks; //硬链接数
unsigned short i_zone[9]; //表示文件和磁盘的映射关系
//i_zone[6]如果你的文件大小只只用了7个逻辑块大小以内,那么这个数组每一个单源存储了一个逻辑块号
//i_zone[7]一次间接块号,如果占用的逻辑块大小大于7,小于512+7则占用一次逻辑块号
//i_zone[8]二次间接块号, 如果占用的逻辑块大小大于512 + 7,小于512 * 512 + 7则启动二次逻辑块号
/* these are in memory also */
struct task_struct * i_wait;
unsigned long i_atime;
unsigned long i_ctime;
unsigned short i_dev;
unsigned short i_num;
unsigned short i_count;
unsigned char i_lock;
unsigned char i_dirt;
unsigned char i_pipe;
unsigned char i_mount;
unsigned char i_seek;
unsigned char i_update;
};
i_mode是一个us类型的变量,用来保存文件的类型和属性,具体定义如下所示:
3. 文件系统底层操作函数
/*bitmap.c*/
free_block(int dev, int block)
/*:释放设备dev上数据区的逻辑块block,复位逻辑块block对应的逻辑块位图比特位*/
new_block(int dev)
/*向设备申请一个逻辑块,该函数会在获取该其超级块,并在超级块中的逻辑位图寻找第一个值为0的bit,然后在对应逻辑块位图置位该bit。为该逻辑块取得在高速缓冲区取得一个对应缓冲块,并更新标志,最后返回逻辑块号。*/
free_inode(struct m_inode* inode)
/*释放指定inode节点*/
new_inode(int dev)
/*为指定设备创建一个Inode节点*/
/*truncate.c*/
truncate(struct m_inode* inode)
/*将节点对应的文件长度截为0,主要调用free_ind和free_dind释放一次和二次间接块。*/
/*inode.c*/
wait_on_inode(struct m_inode * inode)
/*等待一个inode节点空闲*/
lock_inode(struct m_inode * inode)
/*锁定一个inode节点*/
unlock_inode(struct m_inode * inode)
/*解锁一个inode节点,并唤醒等待队列*/
invalidate_inodes(int dev)
/*扫描inode表数组,如果是指定设备使用的i节点就释放*/
sync_inodes(void)
/*把i节点表上的所有节点写入到高速缓冲区等待同步*/
bmap
/*文件数据块映射到盘块的处理操作,该函数把指定的文件数据块block对应到设备上逻辑块上,并返回逻辑块号。如果创建标志 置位,则在设备上对应逻辑块不存在时就申请新磁盘块,返回文件数据块block对应在设备上的逻辑块号(盘块号)。*/
iput、iget
/*
iput 释放一个inode节点。
主要是对i_count引用次数进行操作,把i节点引用数值减1并且若是管道i节点,则唤醒等待进程若是块设备文件i节点则刷新设备若i节点的链接计数为0,则释放该i节点占用的所有磁盘逻辑块,并释放该节点
iget 获得一个inode节点。
从设备上读取指定节点号的i节点到内存i节点表中,并返回该i节点指针首先在位于高速缓冲区的i节点表中寻找,若找到指定节点号的i节点则再经过一些判断处理后返回i节点指针否则通过设备号和指定i节点号,从设备中读取的i节点信息,放入在i节点表中申请的空闲节点中,并返回该i节点指针
*/
/*
read_inode write_inode
找到指定的设备,通过设备号找到他的超级块,超级块计算要读写的块号,调用bread将其读写入高速缓冲区中,读的话将高速缓冲区的b_data读到内存,释放高速缓冲区。写的话将数据写到高速缓冲区的b_data并设置dirt置位,等待系统sys_sync进行写盘,释放高速缓冲区。
*/
/*super.c*/
/*此文件中主要存放这对文件系统超级块的管理函数。*/
get_super()//函数在指定了设备号的情况下,返回对应的超级块的指针
put_super()//函数用于释放超级块,在调用函数umount()函数的会调用此函数
read_super()//函数用于把指定设备的文件系统超级块读入缓冲区,并登记到超级块数组,最后返回该超级块的指针
sys_umount()//系统调用用于卸载一个设备文件名的文件系统
sys_mount()//用于往一个目录上挂载一个文件系统
mount_root()//用于挂载根文件系统
/*namei.c*/
//实现了根据目录名或文件名寻找对应i节点的函数namei(),以及关于目录的建立和删除、目录项的建立和删除等操作和系统调用
下面再来介绍一下对于文件的读写:
本部分是文件系统的第三个部分,主要包含对块设备,字符设备,管道文件,普通文件的读写函数(用作给系统调用提供读写接口)以及系统调用sys_read()和sys_write()。关系如下图所示:
block_dev.c
block_write(int dev, long *pos, char *buf, int count)
int block_write(int dev, long * pos, char * buf, int count)
{
int block = *pos >> BLOCK_SIZE_BITS;
int offset = *pos & (BLOCK_SIZE-1);
int chars;
int written = 0;
struct buffer_head * bh;
register char * p;
while (count>0) {
chars = BLOCK_SIZE - offset;
if (chars > count)
chars=count;
if (chars == BLOCK_SIZE)
bh = getblk(dev,block);
else
bh = breada(dev,block,block+1,block+2,-1);
block++;
if (!bh)
return written?written:-EIO;
p = offset + bh->b_data;
offset = 0;
*pos += chars;
written += chars;
count -= chars;
while (chars-->0)
*(p++) = get_fs_byte(buf++);
bh->b_dirt = 1;
brelse(bh);
}
return written;
}
数据块写函数,向指定设备dev的偏移量pos处,从buf缓冲区开始写入count字节的数据。返回成功写入的字节数。
写数据流程:
① 根据偏移量pos会计算出开始写的盘号block,和在第一个数据块中的偏移量offset。
② 针对要写入的字节数count开始循环进行写操作。
2.1首先计算当前数据块剩余可写入数据的大小chars。如果剩余大小chars大于需要写入的数据count,则chars = count。
2.2 如果剩余可写入数据大小为一块数据的内容,则直接申请1块高速缓冲块。
2.3 否则就需要读入将被写入的数据的数据块,并预读取下两块数据,然后将块号递增1,为循环写入做准备。
block_read(int dev, unsigned long *pos, char *buf, int count)
数据块读函数,从指定设备dev的pos处读取数据,和write函数流程类似。
file_dev.c
提供普通文件的读写函数,供系统调用read()和write()使用
file_read(struct m_inode * inode, struct file * filp, char * buf, int count)
int file_read(struct m_inode * inode, struct file * filp, char * buf, int count)
{
int left,chars,nr;
struct buffer_head * bh;
if ((left=count)<=0)
return 0;
while (left) {
if (nr = bmap(inode,(filp->f_pos)/BLOCK_SIZE)) {
if (!(bh=bread(inode->i_dev,nr)))
break;
} else
bh = NULL;
nr = filp->f_pos % BLOCK_SIZE;
chars = MIN( BLOCK_SIZE-nr , left );
filp->f_pos += chars;
left -= chars;
if (bh) {
char * p = nr + bh->b_data;
while (chars-->0)
put_fs_byte(*(p++),buf++);
brelse(bh);
} else {
while (chars-->0)
put_fs_byte(0,buf++);
}
}
inode->i_atime = CURRENT_TIME;
return (count-left)?(count-left):-ERROR;
}
file文件读数据流程:
① 若要读取的字节数count <= 0直接返回,否则用left保存要读取的字节数开始循环读取。
② 利用bmap()函数获取该文件的读写指针所在位置的逻辑块号nr,若nr!=0则读取该逻辑块,若nr=0表示数据块不存在,缓冲块bh指向空。
③ 计算读写指针在该逻辑块中的偏移量nr,将该数据块剩余的数据BLOCK_SIZE-nr和剩余需要读取字节数left进行比较,如果BLOCK_SIZE-nr > left表示该快是最后一个数据块,反之还需要读取下一个数据块,调整文件指针后移。
file_write(struct m_inode * inode, struct file * filp, char * buf, int count) 和读函数类似
4. 读取文件流程(基于linux0.11)
一个普通用户要读取/etc/passwd :
- 看首字母是不是/,判断是绝对路径还是相对路径;绝对路径从root的inode开始找;
- 读root的inode数据块,从里面的dir_entry找有没有与/etc名称匹配的;
- 如果找到了,返回/etc的 inode_nr;
- 通过/etc的inode_nr,读取etc的inode;
- 通过etc的inode读取etc的数据块,etc是个目录,要查找目录里有没有和passwd匹配的名称;
- 如果找到了,返回passwd的inode_nr
- 通过passwd的inode_nr读取passwd中的数据。
实验实现
https://blog.csdn.net/weixin_43166958/article/details/104209842
大致的创建一个实验类别的过程可以参考上述链接和蓝桥云,实验流程和实现机制已经讲的很详细了,基本上不需要懂得文件系统的具体实现也可以完成以上实验。放一下我的proc.c
#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; })
char proc_buf[4096] ={'\0'};
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 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)
{
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(current->root->i_dev); /*磁盘设备号 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 信息*/
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;
}
int get_inodeinfo()
{
return -1;
}
int proc_read(int dev, unsigned long * pos, char * buf, int count)
{
int i;
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],buf + i+ *pos);
}
*pos += i;
return i;
return -1;
}
这样的话,最后一个实验也就完成了。