day5 高速缓冲区等文件操作
相关文件
- buffer.c
- bitmap.c
- truncate.c
- inode.c
总结一下就是buffer.c提供bread方法,来对除了所有驱动程序以外所有的程序提供统一的内存模拟。bread方法通过dev和block参数,从对应的设备里同步一个逻辑块到高速缓冲中,其他的程序对设备的使用则都是命中bread返回的缓冲对象。
然后还一个关键的文件inode.c则提供对节点的直接访问,我们根据昨天的文件系统已经知道,任何文件在根目录下不管是文件夹或者文件或者管道等等,都是作为一个节点指针指向一个节点对象的,节点对象里有节点的信息以及对于的数据块,那我们读取文件内容的时候就是先读出节点的数据块映射,然后找到数据块号,然后再通过块号读取实际的文件内容的。所有的这一切都是通过buffer高速缓冲区来实现的。
buffer.c
buffer.c作为最底层的模块,他是内核和外设的高速缓冲区,linux根据用户机器的内存大小分配了相应的缓冲区用来作为高速缓冲,自此,除了驱动程序会在高速缓冲区和真正的外设之间操作。其他所有内核以及用户程序操作的都是高速缓冲区的内容。
buffer定义了下面的buffer_head数据结构,使用的buffer用dev和block作为参数做hash算法算出key值,多个key值后面是一个双向链表链接。buffer模块的高级方法是bread,大概逻辑是从hash链表里查询是否已经存在的缓冲,有直接返回。如果不存在则从free_list里选择,选择的时候也很有技巧,空闲块只是没有进程引用了,但是可能还是有锁或者数据是脏的待会写到磁盘。linux里用了b_dirty<<4+b_lock的权重,选一个最低的空闲块,如果申请到了,则调用底层的块设备读写方法调用驱动程序从磁盘把数据填充到缓冲中。但代码中有大量的关中断开中断,都是为了保证因为中断导致的异步编程的正确性,很有参考意义。
buffer.c里面几乎所有方法都使用了wait_on_buffer,我们知道当有程序要对缓冲区进行写之前,要对缓冲区进行加锁操作,加锁是对缓冲对象的b_lock字段赋值,但操作系统面对的一个充满中断的计算机,假象这么一个场景,比如下面这行伪代码,那么假如执行到了!bh->b_lock的时候,返回的true说明没有锁。这时候!一个硬件中断来了,要修改这个缓冲区,cpu会保存现场,然后把ip指向修改的那条操作,把缓冲区修改了,当中断事件完成之后,cpu从寄存器恢复上下文,回到return bh这行代码了,但这时候bh其实已经被修改了,导致程序出错了。所以wait_on_buffer就是上来先关中断,然后才是代码,完成后开中断。注意这儿可能大家会有误解,关了中断了,如果等待了,那cpu一直处于关闭中断的状态,这岂不是有问题?其实是没事的,这儿关了中断判断资源是否被锁定,如果锁定会调用sleep_on函数(这个后面会细说),主动让出cpu的使用权,制定schedule调度,当从寄存器恢复调度的进程的上下文时,中断标志也会切换回被调度的新进程的标志。所以一定要明确一点,eflags的标志其实都是跟进程绑定的,一点调度后就会切换上下文环境包括eflags的参数!
if (!bh->b_lock)
return bh;
// b_count是引用数,每当有进程使用该缓冲区的时候,引用数+1,当b_count为0的时候说明没有进程再使用了,就是一个空闲缓冲块了
struct buffer_head
{
char *b_data; /* pointer to data block (1024 bytes) *///指针。
unsigned long b_blocknr; /* block number */// 块号。
unsigned short b_dev; /* device (0 = free) */// 数据源的设备号。
unsigned char b_uptodate; // 更新标志:表示数据是否已更新。
unsigned char b_dirt; /* 0-clean,1-dirty *///修改标志:0 未修改,1 已修改.
unsigned char b_count; /* users using this block */// 使用的用户数。
unsigned char b_lock; /* 0 - ok, 1 -locked */// 缓冲区是否被锁定。
struct task_struct *b_wait; // 指向等待该缓冲区解锁的任务。
struct buffer_head *b_prev; // hash 队列上前一块(这四个指针用于缓冲区的管理)。
struct buffer_head *b_next; // hash 队列上下一块。
struct buffer_head *b_prev_free; // 空闲表上前一块。
struct buffer_head *b_next_free; // 空闲表上下一块。
};
bitmap.c
封装了位图和对应的块之间的操作
// free_block
1. 在dev的超级块的逻辑块位图中查找该block的有效性
2. 根据dev和block在高速缓冲区中查找是否存在,如果存在,释放掉
3. 将超级块中逻辑块位图该block的位置0
4. 将超级块缓冲区的dirty标志置1
// new_block
1. 在dev的超级块的逻辑块位图查找第一个不是0的位,如果没有,则磁盘已满
2. 将第一个找到的0位置1,设置超级块的dirty标志
3. 根据置1的位的位置计算块号,在高速缓冲区根据dev和block申请一个,并把内容全部清0,并设置该块的已更新和已修改标志
truncate.c
封装了删除某个节点的内容操作,从昨天的内容我们已经知道了,一个节点存放的数据是放在zone数组里的,其中前7个是直接块,第8个是一级块,第9个是二级块。该模块就是处理任何节点的删除工作的
inode.c
从名字也可以看到这个是节点模块,大部分高级业务是跟该模块打交道而不会去直接操作缓冲池。从前面的文章我们已经知道了linux中文件系统,也知道了文件系统在linux中的重要性。任何文件和文件夹在文件系统中都被描述成一个节点,b_data中还为了兼容大文件提供了一级和二级索引。
因为节点,bitmap,块都是存放在外存中,之所以分为超级块,节点块,map块,数据块也只不过是我们人为的规定的,因为规则才有了文件系统。所以任何数据的读入都是通过设备号dev和逻辑块号block,调用buffer模块来读入的。
inode模块read_inode,write_inode用到了超级块和imap节点算法,算出在文件系统中节点所在的实际节点块然后调用buffer模块的读写(隔离了io设备,只跟缓冲区打交道)。
## 将指定i 节点信息写入设备
## 要先计算节点的块号,然后从缓冲区读出该块内容,根据inum算出在块内节点的偏移量,然后把节点信息写入高速缓冲区内,高速缓冲区会在一定的时间内从高速缓冲区同步到对应块设备内
static void write_inode(struct m_inode * inode)
{
struct super_block * sb;
struct buffer_head * bh;
int block;
// 首先锁定该i 节点,如果该i 节点没有被修改过或者该i 节点的设备号等于零,则解锁该i 节点,
// 并退出。
lock_inode(inode);
if (!inode->i_dirt || !inode->i_dev) {
unlock_inode(inode);
return;
}
// 获取该i 节点的超级块。
if (!(sb=get_super(inode->i_dev)))
panic("trying to write inode without device");
// 该i 节点所在的逻辑块号= (启动块+超级块) + i 节点位图占用的块数+ 逻辑块位图占用的块数+
// (i 节点号-1)/每块含有的i 节点数。
block = 2 + sb->s_imap_blocks + sb->s_zmap_blocks +
(inode->i_num-1)/INODES_PER_BLOCK;
// 从设备上读取该i 节点所在的逻辑块。
if (!(bh=bread(inode->i_dev,block)))
panic("unable to read i-node block");
// 将该i 节点信息复制到逻辑块对应该i 节点的项中。
((struct d_inode *)bh->b_data)
[(inode->i_num-1)%INODES_PER_BLOCK] =
*(struct d_inode *)inode;
// 置缓冲区已修改标志,而i 节点修改标志置零。然后释放该含有i 节点的缓冲区,并解锁该i 节点。
bh->b_dirt=1;
inode->i_dirt=0;
brelse(bh);
unlock_inode(inode);
}
super.c
超级块模块,超级块数组大小是8,意味着可以同时存在8个文件系统,除了1个根文件系统无法卸载安装其他都可以随时从系统中安装和卸载。超级块其实逻辑很简单,除了前面文字介绍的根文件字段以外,内存中多保存了一些信息,最重要的就是把超级块的节点map和块map读到了缓冲中并在结构里标记了。
put_super就是判断除了根文件块或者已被mount的块释放,释放就是把节点map和数据块map的高速缓冲释放掉,然后把super块自己的对象给释放掉(dev改成0)
read_super调用底层驱动从dev设备读出1号块到高速缓冲中,data数据排列就是我们定义的super_block的结构顺序(这也是为什么说c是写底层的好语言,因为原始啊- -!结构的编译出来的大小和汇编基本上差不多)。然后根据节点位图和数据块map大小从后面的块中读出数据放在对应的map字段中。
mount和unmount我们平时使用linux已经应该很熟悉了,首先我们有了根文件系统了,我们就可以找一个文件夹来作为某块分区的挂载点,那么当我们cd进入这个文件夹后,实际上就进入了这个分区内了,使用的超级块也是该分区的超级块了
mount_root这个方法是在main方法里加载的,根据编译的时候定义的ROOT_DEV设备中读取超级块以及第一个节点(就是我们平时用的"/"目录),如果出错系统就开不了机啦。然后就是调用超级块信息打印出总逻辑块以及剩余块,总i节点块和剩余块
// 内存中磁盘超级块结构。
struct super_block
{
unsigned short s_ninodes; // 节点数。
unsigned short s_nzones; // 逻辑块数。
unsigned short s_imap_blocks; // i 节点位图所占用的数据块数。
unsigned short s_zmap_blocks; // 逻辑块位图所占用的数据块数。
unsigned short s_firstdatazone; // 第一个数据逻辑块号。
unsigned short s_log_zone_size; // log(数据块数/逻辑块)。(以2 为底)。
unsigned long s_max_size; // 文件最大长度。
unsigned short s_magic; // 文件系统魔数。
/* These are only in memory */
struct buffer_head *s_imap[8]; // i 节点位图缓冲块指针数组(占用8 块,可表示64M)。
struct buffer_head *s_zmap[8]; // 逻辑块位图缓冲块指针数组(占用8 块)。
unsigned short s_dev; // 超级块所在的设备号。
struct m_inode *s_isup; // 被安装的文件系统根目录的i 节点。(isup-super i)
struct m_inode *s_imount; // 被安装到的i 节点。
unsigned long s_time; // 修改时间。
struct task_struct *s_wait; // 等待该超级块的进程。
unsigned char s_lock; // 被锁定标志。
unsigned char s_rd_only; // 只读标志。
unsigned char s_dirt; // 已修改(脏)标志。
};