您现在的位置是:网站首页> 编程资料编程资料
详细介绍Linux IO_linux shell_
2023-05-26
347人已围观
简介 详细介绍Linux IO_linux shell_
1.IO概述
分析一下写操作:
char *buf = malloc(MAX_BUF_SIZE);
strncpy(buf, src, , MAX_BUF_SIZE);
fwrite(buf, MAX_BUF_SIZE, 1, fp);
fclose(fp);
以下图为例:分析数据流写入硬盘的过程

malloc的buf对于图层中的application buffer,即应用程序的buffer;
调用fwrite后,把数据从application buffer 拷贝到了 CLib buffer,即C库标准IObuffer。
fwrite返回后,数据还在CLib buffer,如果这时候进程core掉。这些数据会丢失。没有写到磁盘介质上。当调用fclose的时候,fclose调用会把这些数据刷新到磁盘介质上。
除了fclose方法外,还有一个主动刷新操作fflush函数,不过fflush函数只是把数据从CLib buffer 拷贝到page cache 中,并没有刷新到磁盘上,从page cache刷新到磁盘上可以通过调用fsync函数完成。
fwrite是系统提供的最上层接口,也是最常用的接口。它在用户进程空间开辟一个buffer,将多次小数据量相邻写操作先缓存起来,合并,最终调用write函数一次性写入(或者将大块数据分解多次write调用)。
Write函数通过调用系统调用接口,将数据从应用层copy到内核层,所以write会触发内核态/用户态切换。当数据到达page cache后,内核并不会立即把数据往下传递。而是返回用户空间。数据什么时候写入硬盘,有内核IO调度决定,所以write是一个异步调用。这一点和read不同,read调用是先检查page cache里面是否有数据,如果有,就取出来返回用户,如果没有,就同步传递下去并等待有数据,再返回用户,所以read是一个同步过程。当然你也可以把write的异步过程改成同步过程,就是在open文件的时候带上O_SYNC标记。
数据到了page cache后,内核有pdflush线程在不停的检测脏页,判断是否要写回到磁盘中。把需要写回的页提交到IO队列——即IO调度队列。IO调度队列调度策略调度何时写回。
IO队列有2个主要任务。一是合并相邻扇区的,而是排序。合并相信很容易理解,排序就是尽量按照磁盘选择方向和磁头前进方向排序。因为磁头寻道时间是和昂贵的。
这里IO队列和我们常用的分析工具IOStat关系密切。IOStat中rrqm/s wrqm/s表示读写合并个数。avgqu-sz表示平均队列长度。
内核中有多种IO调度算法。当硬盘是SSD时候,没有什么磁道磁头,人家是随机读写的,加上这些调度算法反而画蛇添足。OK,刚好有个调度算法叫noop调度算法,就是什么都不错(合并是做了)。刚好可以用来配置SSD硬盘的系统。
从IO队列出来后,就到了驱动层(当然内核中有更多的细分层,这里忽略掉),驱动层通过DMA,将数据写入磁盘cache。
至于磁盘cache时候写入磁盘介质,那是磁盘控制器自己的事情。如果想要睡个安慰觉,确认要写到磁盘介质上。就调用fsync函数吧。可以确定写到磁盘上了。
2.linux IO子系统和文件系统读写流程

I/O子系统是个层次很深的系统,数据请求从用户空间最终到达磁盘,经过了复杂的数据流动。
read系统调用的处理分为用户空间和内核空间处理两部分。其中,用户空间处理只是通过0x80中断陷入内核,接着调用其中断服务例程,即sys_read以进入内核处理流程。
对于read系统调用在内核的处理,如上图所述,经过了VFS、具体文件系统,如ext2、页高速缓冲存层、通用块层、IO调度层、设备驱动层、和设备层。其中,VFS主要是用来屏蔽下层具体文件系统操作的差异,对上提供一个统一接口,正是因为有了这个层次,所以可以把设备抽象成文件。具体文件系统,则定义了自己的块大小、操作集合等。引入cache层的目的,是为了提高IO效率。它缓存了磁盘上的部分数据,当请求到达时,如果在cache中存在该数据且是最新的,则直接将其传递给用户程序,免除了对底层磁盘的操作。通用块层的主要工作是,接收上层发出的磁盘请求,并最终发出IO请求(BIO)。IO调度层则试图根据设置好的调度算法对通用块层的bio请求合并和排序,回调驱动层提供的请求处理函数,以处理具体的IO请求。驱动层的驱动程序对应具体的物理设备,它从上层取出IO请求,并根据该IO请求中指定的信息,通过向具体块设备的设备控制器发送命令的方式,来操纵设备传输数据。设备层都是具体的物理设备。
VFS层
内核函数sys_read是read系统调用在该层的入口点。它根据文件fd指定的索引,从当前进程描述符中取出相应的file对象,并调用vfs_read执行文件读取操作。vfs_read会调用与具体文件相关的read函数执行读取操作,file->f_op.read。然后,VFS将控制权交给了ext2文件系统。(ext2在此作为示例,进行解析)
Ext2文件系统层
通过ext2_file_operations结构知道,上述函数最终会调用到do_sync_read函数,它是系统通用的读取函数。所以说,do_sync_read才是ext2层的真实入口。该层入口函数 do_sync_read 调用函数 generic_file_aio_read ,后者判断本次读请求的访问方式,如果是直接 io (filp->f_flags 被设置了 O_DIRECT 标志,即不经过 cache)的方式,则调用 generic_file_direct_IO 函数;如果是 page cache 的方式,则调用 do_generic_file_read 函数。它会判断该页是否在页高速缓存,如果是,直接将数据拷贝到用户空间。如果不在,则调用page_cache_sync_readahead函数执行预读(检查是否可以预读),它会调用mpage_readpages。如果仍然未能命中(可能不允许预读或者其它原因),则直接跳转readpage,执行mpage_readpage,从磁盘读取数据。在mpage_readpages(一次读多个页)中,它会将连续的磁盘块放入同一个BIO,并延缓BIO的提交,直到出现不连续的块,则直接提交BIO,再继续处理,以构造另外的BIO。
page cache 结构
图5显示了一个文件的 page cache 结构。文件被分割为一个个以 page 大小为单元的数据块,这些数据块(页)被组织成一个多叉树(称为 radix 树)。树中所有叶子节点为一个个页帧结构(struct page),表示了用于缓存该文件的每一个页。在叶子层最左端的第一个页保存着该文件的前4096个字节(如果页的大小为4096字节),接下来的页保存着文件第二个4096个字节,依次类推。树中的所有中间节点为组织节点,指示某一地址上的数据所在的页。此树的层次可以从0层到6层,所支持的文件大小从0字节到16 T 个字节。树的根节点指针可以从和文件相关的 address_space 对象(该对象保存在和文件关联的 inode 对象中)中取得(更多关于 page cache 的结构内容请参见参考资料)。

mpage处理机制就是page cache层要处理的问题。
通用块层
在缓存层处理末尾,执行mpage_submit_bio之后,会调用generic_make_request函数。这是通用块层的入口函数。它将bio传送到IO调度层进行处理。
IO调度层
对bio进行合并、排序,以提高IO效率。然后,调用设备驱动层的回调函数,request_fn,转到设备驱动层处理。
设备驱动层
request函数对请求队列中每个bio进行分别处理,根据bio中的信息向磁盘控制器发送命令。处理完成后,调用完成函数end_bio以通知上层完成。
3.IO之流程与buffer概览

一般情况下,进程在io的时候,要依赖于内核中的一个buffer模块来和外存发生数据交换行为。另一个角度来说,数据从应用进程自己的buffer流动到外存,中间要先拷贝到内核的buffer中,然后再由内核决定什么时候把这些载有数据的内核buffer写出到外存。
“buffer cache”仅仅被内核用于常规文件(磁盘文件)的I/O操作。
内核中的buffer模块-“buffer cache”(buffer,cache的功能兼备)
一般情况下,read,write系统调用并不直接访问磁盘。这两个系统调用仅仅是在用户空间和内核空间的buffer之间传递目标数据。举个例子,下面的write系统调用仅仅是把3个字节从用户空间拷贝到内核空间的buffer之后就直接返回了
write(fd,”abc”,3);
在以后的某个时间点上,内核把装着“abc”三个字节的buffer写入(flush)磁盘。如果另外的进程在这个过程中想要读刚才被打开写的那个文件怎么办?答案是:内核会从刚才的buffer提供要读取的数据,而不是从磁盘读。
当前系统上第一次读一个文件时,read系统调用触发内核以blocrk为单位从磁盘读取文件数据,并把数据blocks存入内核buffer,然后read不断地从这个buffer取需要的数据,直到buffer中的数据全部被读完,接下来,内核从磁盘按顺序把当前文件后面的blocks再读入内核buffer,然后read重复之前的动作…
一般的文件访问,都是这种不断的顺序读取的行为,为了加速应用程序读磁盘,unix的设计者们为这种普遍的顺序读取行为,设计了这样的机制—-预读,来保证进程在想读后续数据的时候,这些后续数据已经的由内核预先从磁盘读好并且放在buffer里了。这么做的原因是磁盘的io访问比内存的io访问要慢很多,指数级的差别。
read,write从语义和概念上来说,本来是必须要直接和磁盘交互的,调用时间非常长,应用每次在使用这两个系统的时候,从表象上来说都是被卡住。而有了这些buffer,这些系统调用就直接和buffer交互就可以了,大幅的加速了应用执行。
Linux内核并没有规定”buffer cache”的尺寸上线,原则上来说,除了系统正常运行所必需和用户进程自身所必需的之外的内存都可以被”buffer cache”使用。而系统和用户进程需要申请更多的内存的时候,”buffer cache”的内存释放行为会被触发,一些长久未被读取,以及被写过的脏页就会被释放和写入磁盘,腾出内存,以便被需要的行为方使用。
”buffer cache”有五个flush的触发点:
1.pdflush(内核线程)定期flush;
2.系统和其他进程需要内存的时候触发它flush;
3.用户手工sync,外部命令触发它flush;
4.proc内核接口触发flush,”echo 3 >/proc/sys/vm/drop_caches;
5.应用程序内部控制flush。
这个”buffer cache”从概念上的理解就是这些了,实际上,更准确的说,linux从2.4开始就不再维护独立的”buffer cache”模块了,而是把它的功能并入了”page cache”这个内存管理的子系统了,”buffer cache”现在已经是一个unix系统族的普遍的历史概念了
高性能写文件
写100MB的数据
场景1,1次写1个字节,总共write 100M次;
场景2,1次写1K个字节,总共write 100K次;
场景3,1次写4K个字节,总共write 25K次;
场景4,1次写16k个字节,总共write大约不到7K次。
以上4种写入方式,内核写磁盘的次数基本相同,因为写磁盘的单位是block,而不是字节。现在的系统默认的block都是4k。
第1种性能非常差,user time和system time执行时间都很长,既然写盘次数都差不多,那他慢在哪儿呢?答案是系统调用的次数太多
第2种,user time和system time都显著降低,不过system time降低幅度更大
第2种以后,性能差别就不是很高了,第3种和第4种性能几乎一样
有兴趣的朋友可以试一试,如果你的服务器很好,可以适当放大测试样本。
总而言之,得出的结论是以block的尺寸为write(fd, sizeof(buf),buf)的调用单位就可以了,再大对性能也没什么太大的提高。
题外话:一个衡量涉及IO程序的好坏的粗略标准是“程序运行应该尽量集中在user time,避免大量的system time”以及“IO的时候肯定是需要一些应用层buf的,比如上述4个场景,匹配就可以了(比如场景3,场景1和场景2会导致系统调用次数太多,场景4使用的buf尺寸过于浪费)”
每个系统调用在返回的时候,会有一个从内核态向用户态切换的间隙,每次在这个间隙里面,系统要干两个事情—-递送信号和进程调度,其中进程调度会重新计算全部RUN状态进程的优先级。
系统调用太多的话,递送信号和进程调度引起的计算量是不容忽视的。
精确地flush “buffer cache”
在很多业务场景下,我们仅仅调用write()把需要写盘的数据推送至内核的”buffer cache”中,这是很不负责任的。或许我们应该不断地频繁地把”buffer cache”中的数据强制flush到磁盘,尽最大可能保证我们的业务数据尽量不因断电而丢失。
天下没有免费的午餐,既想要效率(写入内核buffer),又想要安全性(数据必须flush到外存介质中才安全),这似乎是很矛盾的。SUSv3(Single UNIX Specification Version 3)给了这种需求一个折中的解决方案,让OS尽量满足我们的苛刻的要求。介绍这个折中方案之前,有两个SUSv3提案的规范很重要,说明如下:
1.数据完整性同步(synchronized I/O data integrity)
一个常规文件所包含的信息有两种:文件元数据和文件内容数据。
文件元数据包括:文件所属用户、组、访问权限,文件尺寸,文件硬连接数目,最后访问时间戳,最后修改时间戳,最后文件元数据修改时间戳,文件数据块指针。
对于文件内容数据,大家应该都很清楚是什么东西。
对于写操作,这个规范规定了,写文件时保证文件内容数据和必要的文件元数据保持完整性即可。粗糙地举个例子来解释这个规范,某次flush内核中的数据到磁盘的时候,仅仅把文件内容数据写入磁盘即可,但是如果这次写文件导致了文件尺寸的变化,那么这个文件尺寸作为文件的元数据也需要被写入磁盘,必要信息保持同步。而其他的文件元数据,例如修改时间,访问时间一概略去,不需要同步。
2.文件完整性同步(synchronized I/O file integrity)
相对于数据完整性同步而言,这个规范规定了,所有内容数据以及元数据都要同步。
下面来介绍linux提供的几种flush内核缓冲数据的几种方案,相信看完之后,大家应该知道上述提及的折中方案是怎样的:)
1.int fsync(int fd);
文件完整性同步;
2.int fdatasync(int fd);
数据完整性同步。
fdatasync相对于fsync的意义在于,fdatasync大致仅需要一次磁盘操作,而fsync需要两次磁盘操作。举例说明一下,假如文件内容改变了,但是文件尺寸并没有发生变化,那调用fdatasync仅仅是把文件内容数据flush到磁盘,而fsync不仅仅把文件内容flush刷入磁盘,还要把文件的last modified time也同步到磁盘文
相关内容
- Shell中数组以及其相关操作的详细实例_linux shell_
- Shell脚本如何逐行处理文本文件_linux shell_
- Shell如何遍历包含空格的文本详解_linux shell_
- Shell中的数学运算使用_linux shell_
- Linux bc命令实现数学计算器_linux shell_
- Shell expr命令进行整数计算的实现_linux shell_
- 在Bash脚本中引入alias的方法_linux shell_
- Webshell基础知识深入讲解_linux shell_
- Linux系统诊断之内存基础深入详解_linux shell_
- Shell字符串截取的实现方法(非常详细)_linux shell_
