转载来源: https://www.cnblogs.com/Roboduster/p/16695083.html
下面就是磁盘管理的第3层抽象,从磁盘到文件,上一讲最后留下的盘块号并不符合我们日常使用计算机的习惯,而文件才是我们使用计算机更为常用的方式。这一部分就来细说第3层抽象:生磁盘到文件。这部分将解释如何从文件得到盘块号。
参考资料:
引入文件抽象的磁盘也称,File-cooked disk,这正与上一讲中的生磁盘 Raw disk 相对,也是比较有意思的一件事情。
前两层抽象的建立已经让人叹为观止,但是对于普通用户而言,并不了解盘块号这样的专业名词,所以引入更高一层的抽象:文件,这样普通用户操纵磁盘信息时更为方便和舒适。
用户眼中的文件是什么样子的?
我们要做的事情就是 建立字符流到盘块的映射关系。而文件抽象的初心是建立文件与盘块号的联系,这意味着这层抽象中操作系统将从文件字符流中推算盘块号。
下面举例说明一下上面提到的映射关系的作用:
这里留一个坑:删除了200~212行,对应磁盘盘块就出现了空白,这段空白怎么处理?
上文提到,操作系统负责根据映射关系解析文件字符流,这种映射需要一个映射表。映射表需要根据文件在磁盘中的存储结构来建立,下面介绍几种基本的文件存储结构及其映射表建立,我们也可以自行设计其他文件存储结构及映射:
文件使用顺序结构储存在磁盘上,文件字符流被按照顺序存放在连续的盘块里。
比如 1~100 字符放在6号盘块,101~200 字符就放在7号盘块,以此类推。
顺序结构的映射建立如下,如下图下半部分所示:
映射建立后,读写磁盘所需的盘块号就被映射表封装成了文件字符流的修改。联系上一讲的上层接口 盘块号,继续向下就实现了磁盘的访问。
顺序存储结构也有不妥之处:
所以每种存储结构的特点不同,我们需要根据文件的特性,来选择/设计存储结构。
文件使用链式结构储存在磁盘上,当前盘块中存放下一块盘块的位置指针,实际上就是链表在磁盘中的表现形式。如下图所示:
文件控制块 FCB 中需要存放的主要映射信息:第一个磁盘块的盘块号。
联想前面键盘和显示器中 Linux 0.11 文件的读取方式,使用了一个 inode 的结构来存储文件信息并据此进行读取,这显然是一种索引结构。
文件使用索引结构储存在磁盘上时,文件信息可以存放在不连续的磁盘盘块上,FCB 存储索引表,索引表存储盘块号,如下图所示(位置是19):
由于上述优点和可解决的缺点,实际操作系统 如 Linux 和 Unix,使用的文件存储都是 基于 索引存储 的 多级索引结构。如下图所示:
可见,如上图这样的索引架构:可以表示很大的文件,很小的文件高效访问,中等大小的文件访问速度也不慢。是一种比较优秀的综合方案。
这部分介绍文件层抽象使用磁盘的最核心代码。
其实这部分也可以联系 之前学习笔记10-键盘和鼠标 的内容一起理解,在驱动外设时已经使用了文件抽象,因此其中的过程跟这里很相似。
用户对文件的写操作,在系统接口层面具体行动是调用write()
,内核层功能代码sys_write()
如下所示:
// 在fs/read_write.c中
int sys_write(int fd, const char* buf, int count)
{
struct file *file = current->filp[fd];
struct m_inode *inode = file->inode;
if(S_ISREG(inode->i_mode)){
// file 包含了前面缺少的字符流信息
return file_write(inode, file, buf, count);
}
}
file=current->filp[fd];
如果对多进程图像还有点印象,current 就是当前进程的PCB,这里的意思就是 PCB 中的一个数组 flip 的 fd 号位置处 存储了一个文件。
flip 数组的来源:是从子进程根据父进程创建时就拷贝过来的 (sys_fork),最原始的父进程从操作系统进程拷贝。通过 sys_open 建立从文件inode 到进程PCB的链;
上图摘自我的笔记10,这个链本部分的配图也有。
struct m_inode *inode = file->inode;
获得 inode(文件本身的信息);这部分代码实现 人访问文件的图像 向 生磁盘读写数据的图像的转换;也就是这里的代码实现本文第1部分思路的实现。
如下图,我们要修改200~212字符,下面的参数意义就是:
file_write 的 工作过程:
这个读写指针是字符流形式的一个具体表现,刚打开时读写指针,随着读写,读写指针后移,也就形成了字符流的图像。
而 file_write 就是上面几步的顶层代码,每一步调用函数来完成:
int file_write(struct m_inode *inode, struct file *filp, char *buf, int count)
{
// pos 代表当前位置
off_t pos;
//1.得到字符流的读写位置,也就是200
if(filp->f_flags&O_APPEND){
//如果是追加,pos从文末开始,i_size就是文件大小。
pos=inode->i_size;
}
else{
//如果不是,就从上一次读写的位置继续。
pos=filp->f_pos;
}
.....
//2. 下面是核心代码,根据读写位置找到盘块号。
while(i<count)
{
//算出对应的块
block=create_block(inode, pos/BLOCK_SIZE);//起始位置和偏移量
//发送请求,放入电梯队列,bread展开就是 make_request。
bh=bread(inode->i_dev, block);//放入电梯队列后阻塞
//3.修改pos
//写入数据后,修改 pos
int c=pos%BLOCK_SIZE; char *p=c+bh->b_data;
// pos指向文件的读写位置(字符流的末尾位置)
bh->b_dirt=1; c=BLOCK_SIZE-c; pos+=c;
...
//一块一块地拷贝用户字符, 并且释放写出
while(c-->0) *(p++)=get_fs_byte(buf++);
brelse(bh);
}
filp->f_pos=pos;
}
计算盘块号,这是第3层文件抽象的核心工作。
while(i<count){
//create=1的_bmap,没有映射时创建映射
block=create_block(inode, pos/BLOCK_SIZE);
bh=bread(inode->i_dev, block);
...
此时再申请新的数据块时,逻辑块号需要放到一阶索引表的下一位(顺序); 我在学习这部分时感觉不是很直观,下图表述一间接索引的情况较好:
int _bmap(m_inode *inode, int block, int create)
{
if(block<7){
if(create&&!inode->i_zone[block]){
// 新申请一个数据块,返回它的块号放在i_zone[block]中
//这样0~6的inode就是直接索引数据
inode->i_zone[block]=new_block(inode->i_dev);
inode->i_ctime=CURRENT_TIME;
inode->i_dirt=1;
}
// 0~6 部分建表完成。
return inode->i_zone[block];
}
//
block-=7;
if(block<512){ //一个盘块号2个字节
bh=bread(inode->i_dev,inode->i_zone[7]);
return (bh->b_data)[block];
}
...
到这里从文件到盘块的映射表建立完成,接下来就会回到 file_write 中继续 bread 向磁盘发出请求,接入前2层抽象。
理解到这里,整个文件视图就可以建立了。
本部分用于记录一些需要拓展而不想占用正文篇幅的资料。
首先,用户要操作文件,分为两条路:
注意:如何找到下图文件表?如何打开文件(open那一句代码)?
实验 8 要实现一个 proc 文件,实现的效果是:输入 cat /proc/psinfo
,打出如下图所示的进程情况:
Linux0.11中 这些进程信息存放在 PCB 中,也就是说,并不在磁盘上,而 cat 命令要打开一个文件。所以要从PCB中取出相关信息 先放到内存中再读写放到磁盘文件中(见下图 cat 的一些程序,作用是持续打印,知道文件中没有信息)。
要实现这样的效果,还是沿着 4.1 中的思路,不过要将 该文件的 i_mode 设置为 proc 设备(S_ISPROC(inode -> i_mode);
接下来调用 proc_read()
从 PCB 中的 task_struct 中取出数据拷贝给 buffer 内存缓冲区。
执行读内存的相关操作,就实现了要求的效果。
具体实现代码框架如下图所示:
本文系转载,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。
本文系转载,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。