读取一个文件的时候,操作系统发生了什么?
一、读取一个文件的时候,操作系统发生了什么
1、根据读指针计算文件内容在硬盘的位置
我们知道一个文件对应一个inode,inode里记录了文件内容的一些信息,如图:
我们看到inode里记录了文件每个数据块的逻辑块号在硬盘中对应的块号,所以我们根据读指针和硬盘逻辑块的大小算出逻辑块号,然后根据逻辑块号从inode的映射表中找到对应的硬盘块号。
2、根据硬盘块号,把数据读取出来
读取函数是bread(block read):
struct buffer_head * bread(int dev,int block){ struct buffer_head * bh; // 先从buffer链表中获取一个buffer if (!(bh=getblk(dev,block))) panic("bread: getblk returned NULL\n"); // 之前已经读取过并且有效,则直接返回 if (bh->b_uptodate) return bh; // 返回读取硬盘的数据 ll_rw_block(READ,bh); //ll_rw_block会锁住bh,所以会先阻塞在这然后等待唤醒 wait_on_buffer(bh); // 底层读取数据成功后会更新该字段为1,否则就是读取出错了 if (bh->b_uptodate) return bh; brelse(bh); return NULL;}
分三部分分析bread函数:
根据设备号和块号从buffer链表中获取缓存的数据,操作系统在硬盘上面实现了一层缓存系统,对于文件的读写进行了缓存处理。比如我们读取了一个文件的某一部分内容,如果下次继续读取这部分内容,则不需要再从硬盘读取,直接从缓存中读取就行。这样就提高了读取的速度,因为我们知道硬盘的读取是非常慢的操作。当然操作系统会对数据的有效性进行维护(b_uptodate字段等于1说明有效)。如果缓存失效,则调用ll_rw_block函数进行硬盘读取。因为硬盘读取非常慢,所以这时候进程会阻塞。通过wait_on_buffer函数实现进程的阻塞。等到进程被唤醒的时候再次通过b_uptodate字段判断是否读取成功。b_uptodate字段会在数据读取成功的时候设置为1。static inline void wait_on_buffer(struct buffer_head * bh){ cli(); while (bh->b_lock) sleep_on(&bh->b_wait); sti();}
继续分析ll_rw_block函数:
void ll_rw_block(int rw, struct buffer_head * bh){ unsigned int major; if ((major=MAJOR(bh->b_dev)) >= NR_BLK_DEV || !(blk_dev[major].request_fn)) { printk("Trying to read nonexistent block-device\n\r"); return; } // 新建一个读写硬盘数据的请求 make_request(major,rw,bh);}
ll_rw_block函数的逻辑非常简单,直接调用make_request。分析这个函数之前我们先了解一下struct request结构体和一些硬盘读取的内容。硬盘对应上层的读写操作,维护了一个结构体struct blk_dev_struct。该结构体记录了请求硬盘操作的任务队列和处理函数。struct request结构体则记录了请求硬盘任务的一些上下文。比如操作的类型(读或写),读取的扇区、扇区数、保存读写数据的指针。
继续分析make_request函数:
static void make_request(int major,int rw, struct buffer_head * bh){ struct request * req; int rw_ahead; ... // 请求队列1/3用于读,2/3用于写repeat: if (rw == READ) req = request+NR_REQUEST; else req = request+((NR_REQUEST*2)/3); /* find an empty request */ while (--req >= request) // 小于0说明该结构没有被使用 if (req->dev<0) break; // 没有找到可用的请求结构 if (req < request) { // 预读写则直接返回 if (rw_ahead) { unlock_buffer(bh); return; } // 阻塞等待可用的请求结构 sleep_on(&wait_for_request); // 被唤醒后重新查找 goto repeat; } req->dev = bh->b_dev; req->cmd = rw; req->errors=0; req->sector = bh->b_blocknr<<1; // 一块等于两个扇区所以乘以2,即左移1位,比如要读地10块,则读取第二十个扇区 req->nr_sectors = 2;// 一块等于两个扇区,即读取的扇区是2 req->buffer = bh->b_data; req->waiting = NULL; req->bh = bh; req->next = NULL; // 插入请求队列 add_request(major+blk_dev,req);}
该函数就是生成一个struct request节点插入到请求硬盘操作的队列中。
继续分析add_request函数:
static void add_request(struct blk_dev_struct * dev, struct request * req){ struct request * tmp; req->next = NULL; cli(); if (req->bh) req->bh->b_dirt = 0; // 当前没有请求项,插入队列,开始处理请求 if (!(tmp = dev->current_request)) { dev->current_request = req; sti(); (dev->request_fn)(); return; } // 如果已经在处理队列中的请求,那么使用电梯算法插入相应的位置,等待处理。 for ( ; tmp->next ; tmp=tmp->next) if ((IN_ORDER(tmp,req) || !IN_ORDER(tmp,tmp->next)) && IN_ORDER(req,tmp->next)) break; req->next=tmp->next; tmp->next=req; sti();}
不管是名列前茅个任务节点还是后续的任务节点。都由request_fn对应的函数逐个进行处理。硬盘操作对应的处理函数是do_hd_request。do_hd_request函数根据request结构体中的上下文,对硬盘控制器发送操作命令,比如需要读取的操作类型、读取的扇区等。并且设置回调函数read_intr(因为我们分析的是读取操作)。这时候进程就阻塞了。等到硬盘控制器从硬盘中读取数据成功后,会触发中断。在中断处理函数中会执行刚才我们设置的回调read_intr。read_intr函数从硬盘控制器的数据寄存器中把数据读取进来。如果还没读取完毕,则继续等待后续硬盘中断。如果全部读取成功则唤醒进程。
二、Linux IO栈
Linux IO栈是指一组相互协作的软件层,用于处理输入/输出(I/O)操作,主要有以下几层:
虚拟文件系统层:该层屏蔽了底层不同文件系统的差异性,向应用程序提供统一的文件IO接口。VFS支持三种类型的系统:基于磁盘的文件系统、网络文件系统、特殊文件系统。PageCache层:由于磁盘和内存之间存在巨大的差异,Linux内核添加了页缓存,把磁盘抽象成一个个大小固定的页。通过将部分页缓存到内存中来提高磁盘的性能。这点和将内存分配为页以将部分内存缓存到CPU Cache上的思路是一致的。映射层:映射层由多个文件系统以及块设备文件组成,主要完成计算逻辑块号和物理块号等操作。通用块层:和VFS类似,屏蔽底层不同设备驱动的差异性,提供统一的、首相的通用块层API。IO调度层:IO调度层介于通用块层和块设备驱动层之间,主要是为了减少磁盘IO次数,增大磁盘整体吞吐量。IO调度层会将多个读写请求进行排序和合并,提供多种算法来适应不同的场景。块设备驱动层:提供对物理块设备(磁盘等)的驱动程序。每类物理块设备都需要对应的驱动程序。物理设备层:物理磁盘设备。在Linux整个IO栈中,可以看出整体都采用了封装的思想:经过内核的层层封装,抹平了不同物理设备之间的差异,提供给应用程序的则是跨越不同底层设备都通用的API。同时,栈中还加入了诸如PageCache层和IO调度层等来增加IO效率和性能。在我之前简单学习CPU时,就了解了CPU为了快速执行指令所采取的多级流水线架构和超标量架构等,在这次对IO栈的学习中也了解到了IO栈对IO性能的优化。
延伸阅读1:操作系统写入文件的过程
如果是修改文件之前的内容,则先把这块内容读取到内存,然后修改内存的数据,最后回写硬盘。如果是追加性写入,则先在硬盘申请一个新的数据块,并且修改位图、inode信息。然后把新块读取到内存,接着修改内存数据,最后回写到硬盘。

猜你喜欢LIKE
相关推荐HOT
更多>>
Linux系统开机加电后发生了什么?
一、Linux系统开机加电后发生了什么电脑启动后,CPU逻辑电路被设计为只能运行内存中的程序,没有能力直接运行存在于软盘或硬盘中的操作系统,如...详情>>
2023-10-16 20:17:25
Medium Android客户端都使用了哪些开源项目?
1、RetrofitRetrofit是一个适用于 Android 和 Java 的类型安全的 HTTP 客户端,是一个针对Android平台的网络请求库,它基于OkHttp封装了网络请...详情>>
2023-10-16 19:21:33
MySql什么是数据库架构设计?
一、MySql什么是数据库架构设计MySQL是一种流行的关系型数据库管理系统,它被广泛用于Web应用程序和企业应用程序的开发。在MySQL中,数据库架构...详情>>
2023-10-16 13:21:22
JVM中的堆区为什么叫堆(heap),与数据结构中的堆是一个概念吗?
一、JVM中的堆区为什么叫堆(heap)JVM中的堆区和数据结构中的堆并不是一个概念。JVM中的堆区之所以叫做堆,是因为它的物理存储结构类似于堆(h...详情>>
2023-10-16 11:24:35热门推荐
OceanBase相比其他开源的noSQL数据库有什么优点?
沸Flash动画制作的原理是什么?
热为什么GIL让多线程变得如此鸡肋?
热为什么要用Lambda表达式?
新苹果电脑mac怎么安装SQL server?
C、C++、C#的区别?
Linux系统开机加电后发生了什么?
Python解释器,终端,编辑器区别和联系?
Medium Android客户端都使用了哪些开源项目?
MyBatis流行的原因?
transform和opacity动画为什么性能好?
JButton的e.getSource()和e.getActionCommand()方法有什么区别?
webpack中的url-loader和file-loader有哪些区别?
为什么 SwiftUI 用 “some View” 作为视图类型?
技术干货






