read和write函数学习笔记

最近学C/C++相关的时候遇到的read和write函数的深层次理解,进行了一些摘要和总结。

1、起因

偶然得知文件的读写函数不是直接把数据写进磁盘的,而是需要经过缓冲区和多次拷贝步骤,才能够从用户内存到达磁盘。这时候我才明白,我根本不了解write/read函数。这个流程具体是怎么样的呢,文件读写的read,write,fread,fwrite这几个函数之间的关系又是什么样的呢?

2、相关概念

这里需要先了解几个基础的概念。

2.1、缓冲区

2.1.1、概念

缓冲区(Buffer),是内存空间的一部分。也就是说,计算机在内存中预留了一定的存储空间,用来暂时保存输入或输出的数据,这部分预留的空间就叫做缓冲区(缓存)。

2.1.2、作用

缓冲区是为了让低速的输入输出设备和高速的用户程序能够协调工作,并降低输入输出设备的读写次数。
有了缓冲区,就可以将数据先放入缓冲区中(内存的读写速度也远高于硬盘),然后程序可以继续往下执行,等所有的数据都准备好了,再将缓冲区中的所有数据一次性地写入硬盘,这样程序就减少了等待的次数,变得流畅起来。
缓冲区的另外一个好处是可以减少硬件设备的读写次数。其实我们的程序并不能直接读写硬件,它必须告诉操作系统,让操作系统内核(Kernel)去调用驱动程序,只有驱动程序才能真正的操作硬件。

2.1.3、缓冲区的类型

根据不同的标准,缓冲区可以有不同的分类。
根据缓冲区对应的是输入设备还是输出设备,可以分为输入缓冲区和输出缓冲区。
根据数据刷新(也可以称为清空缓冲区,就是将缓冲区中的数据“倒出”)的时机,可以分为全缓冲、行缓冲、不带缓冲
全缓冲:
在这种情况下,当缓冲区被填满以后才进行真正的输入输出操作。缓冲区的大小都有限制的,比如 1KB、4MB 等,数据量达到最大值时就清空缓冲区。
全缓冲的典型代表是对文件的读写。

全缓冲数据的清空条件:
一旦填满缓冲区,立刻将数据冲洗到文件
程序正常退出时,立刻将数据冲洗到文件
遇到 fflush() 强制冲洗时,立刻将数据冲洗到文件
关闭文件时,立刻将数据冲洗到文件
读取文件内容时,立刻将数据冲洗到文件
改变缓冲区类型时,立刻将数据冲洗到文件
注意:这里的文件其实并不是真的文件,这里的缓冲区也基本是指用户层c语言库的缓冲区。
行缓冲:
在这种情况下,当在输入或者输出的过程中遇到换行符时,才执行真正的输入输出操作。行缓冲的典型代表就是标准输入设备(也即键盘)和标准输出设备(也即显示器)。其他与全缓冲相同
不带缓冲
不带缓冲区,数据就没有地方缓存,必须立即进行输入输出。
getche()、getch() 就不带缓冲区,输入一个字符后立即就执行了,根本不用按下回车键。标准错误输出 stderr()是无缓冲的,perror() 也没有缓冲区。

2.1.4、修改缓冲区类型

滞留在缓冲区中的数据有时被称为脏数据(dirty data),脏数据的存在代表程序操作的结果与文件真实状态不一致,若未正常冲洗这些脏数据就退出程序则有可能会造成数据丢失。
这三种缓冲类型,可以通过函数 setbuf()/setvbuf() 来修改。

2.2、用户态、内核态

这个概念比较抽象,只搬运一些基础的说明,更深入的自行查找。
用户态和内核态是操作系统的两种运行状态。

内核态:处于内核态的 CPU 可以访问任意的数据,包括外围设备,比如网卡、硬盘等,处于内核态的 CPU 可以从一个程序切换到另外一个 程序,并且占用 CPU 不会发生抢占情况,一般处于特权级 0 的状态我们称之为内核态。
用户态:处于用户态的 CPU 只能受限的访问内存,并且不允许访问外围设备,用户态下的 CPU 不允许独占,也就是说 CPU 能够被其他程 序获取。

那么为什么要有用户态和内核态呢?

这个主要是访问能力的限制的考量,计算机中有一些比较危险的操作,比如设置时钟、内存清理,这些都需要在内核态下完成,如果随意进行危险操作,极容易导致系统崩坏。

2.3、C语言标准及C标准库、运行时库

【C89 (ANSI C)】 随着 C 语言在多个领域的推广、应用,一些新的特性不断被各种编译器实现并添加进来。于是,建立一个新的“无歧义、于具体平台无关的 C 语言定义” 成为越来越重要的事情。1983 年,ASC X3(ANSI 属下专门负责信息技术标准化的机构,现已改名为 INCITS)成立了一个专门的技术委员会J11(J11是委员会编号,全称是 X3J11),负责起草关于 C 语言的标准草案。1989 年,草案被 ANSI 正式通过成为美国国家标准,被称为 C89 标准。此后还相继出现了C90、C95、C99等标准。这里的数字都是指年份。所以:
c标准库,是针对c语言本身来说的,与平台无关。截至c11标准,它共包含29个头文件。
c运行库,是和平台相关的,也就说和操作系统相关,windows系统有windows的运行库,linux有linux的运行库。c运行库, 也就是c run time library(CRT) 是c语言中的概念,运行时库一般都是用汇编直接实现的。
glibc是Linux上使用最广泛的C标准库的实现。glibc库不但包含标准C库的所有头文件,还包含了所有POSIX库的头文件。
总结: 运行时库包括标准库,同时有包含自己平台相关的一些库。
(NOTE)注:这里有几个概念我之前一直不是很清楚,所以也简单整理了一下:

POSIX:可移植操作系统接口(英语:Portable Operating System Interface,缩写为POSIX)是IEEE为要在各种UNIX操作系统上运行软件,而定义API的一系列互相关联的标准的总称。定义的是unix的接口。
unix和linux:Linux 是一个类似 Unix 的操作系统,Unix 要早于 Linux,Linux 的初衷就是要替代 UNIX,并在功能和用户体验上进行优化,所以 Linux 模仿了 UNIX(但并没有抄袭 UNIX 的源码),使得 Linux 在外观和交互上与 UNIX 非常类似。基本上unix上有的,linux上都有。
Linux基本上逐步实现了POSIX兼容,但并没有参加正式的POSIX认证。也就是说unix的接口,linux基本都有,posix定义的接口,linux也基本都有。

3、数据读写磁盘大致流程

3.1、标准IO

有了上述概念,我们就可以简单说明一下文件读写的主要流程。首先对于处理文件的IO函数,我们可以分为标准IO库中的和系统库的IO函数。这里只按照最基本的文件读写流程涉及的函数来,对于标准IO:fopen->fread/fwrite->fclose。对于系统IO:open->read/write->closed。而标准IO函数在linux上就是基于这些UNIX的系统IO操作函数实现的。

(NOTE)注:系统IO:打开文件得到的是一个整数,称为文件描述符。标准IO:打开文件得到的是一个指针,称为文件指针。

因此使用文件读写IO后的整个数据流程如下图所示:
参考 https://zhuanlan.zhihu.com/p/67192901 中的图

用户层:
对于标准的fread/fwrite函数来说,都是带缓冲区的。以写文件数据为例,我们在调用fwrite之后就会将数据写到IObuffer(CLib buffer)中,这个IObuffer是用户层的c语言提供的。它是在FILE结构体中。我们使用标准IO函数的时候,每当我们打开一个文件时,c语言都会为其创建一个FILE类型的结构体,用以记录该文件的相关信息,比如文件大小,文件描述符之类的。当然,其内部也会有一个字符型指针指向一片动态开辟的空间,而这个空间就是缓冲区!

fwrite返回后,数据还在CLib buffer,这时候这些数据还可能会丢失。没有写到磁盘介质上。这时候需要等待我们的全缓冲满/fclose/主动fflush()等操作把数据从CLib buffer 拷贝到内核层的(page cache)高速缓冲块上。其中fclose内部实际上就是调用了fflush函数。但是这个时候数据还没有刷新到磁盘上。
内核层:
write/read函数是沟通用户态和内核态的桥梁。
当数据到达page cache后,内核并不会立即把数据往下传递。而是返回用户空间。数据什么时候写入硬盘,有内核IO调度决定,所以write是一个异步调用。这一点和read不同,read调用是先检查page cache里面是否有数据,如果有,就取出来返回用户,如果没有,就同步传递下去并等待有数据,再返回用户,所以read是一个同步过程。当然你也可以把write的异步过程改成同步过程,就是在open文件的时候带上O_SYNC标记。
write系统调用只是写入到PageCache页面高速缓存,脏页不会立刻写入到磁盘,而是由内核的flusher线程在满足一定阈值(一定时间间隔、脏页达到一定比例),调用sync函数将脏页同步到磁盘上(放入设备的IO请求队列)。sync系统调用只需将脏页提交到块设备IO队列就可以返回。所以我们看到sync函数返回值为void。同时sync函数返回后,并不等于写入磁盘结束,仍然会出现故障,此时sync函数是无法知晓的。对于可靠性要求比较高的应用,write提供的松散的异步语义是不够的,所以我们需要内核提供的同步IO来保证。常用的有fsync、msync、fdatasync、sync_file_range等。
具体的查阅各大网站。
https://zhuanlan.zhihu.com/p/497098013

也就是说,我们可以通过调用上述函数主动将内核层的高速缓存页的数据直接放进IO队列中并一路护送等待,直到进行真正的IO操作成功后才返回!
可以看出,数据想要从我们定义的buffer中写入到磁盘中可谓是经历了千山万水!
最后再总结一下fwrite()写文件的整个流程:FILE缓冲-----fflush---------〉内核缓冲--------fsync-----〉磁盘。如果调用write函数则为write---------〉内核缓冲--------fsync-----〉磁盘。fflush内部实际上也是调用了write()函数。
至于read和fread操作大致和write/fwrite相同。
(NOTE)拓展:

一般情况下,read,write系统调用并不直接访问磁盘。这两个系统调用仅仅是在用户空间和内核空间的buffer之间传递目标数据。
至于具体的IO调度队列,有2个主要任务。一是合并相邻扇区的,二是排序。合并相信很容易理解,排序就是尽量按照磁盘选择方向和磁头前进方向排序。因为磁头寻道时间是和昂贵的。内核中有多种IO调度算法。当硬盘是SSD时候,没有什么磁道磁头,人家是随机读写的,加上这些调度算法反而画蛇添足。OK,刚好有个调度算法叫noop调度算法,刚好可以用来配置SSD硬盘的系统。
从IO队列出来后,就到了驱动层(当然内核中有更多的细分层,这里忽略掉),驱动层通过DMA(Direct Memory Access,直接存储器访问),将数据写入磁盘cache。
fread就是C语言层的带libc buffer的读函数。
而read系统调用的处理分为用户空间和内核空间处理两部分。其中,用户空间处理只是通过0x80中断陷入内核,接着调用其中断服务例程,即sys_read以进入内核处理流程。具体来说:经过了VFS、具体文件系统,如ext2、页高速缓冲存层、通用块层、IO调度层、设备驱动层、和设备层。

”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系统族的普遍的历史概念了

更具体地自行查看 https://www.yht7.com/news/141725 该文,上面这段基本都是照搬。

3.2、直接IO

通过前面的总结,我我们可以知道,对文件进行读写操作的时候,实际上是需要经过层层关卡才能够完成的,这样做的主要目的就是为了降低IO次数。但是有时候我们不需要内核层繁琐地缓存机制,期望数据能够直接进行读写,比如说数据库管理系统这类应用,它们更倾向于选择它们自己的缓存机制,因为数据库管理系统往往比操作系统更了解数据库中存放的数据,数据库管理系统可以提供一种更加有效的缓存机制来提高数据库中数据的存取性能。这时候就需要用到直接IO的概念。
直接IO就是应用程序直接访问磁盘数据,而不经过内核缓冲区,即绕过内核缓冲区,自己管理I/O缓冲区,目的是减少一次内核缓冲区到用户程序缓存的数据拷贝。一般在性能要求较高、内存使用率要求较高的情况下使用,多用于系统自带缓存的情况。
打开方式:

int open(const char *pathname, int oflag, … /*, mode_t mode * / ) ;

这里面的flag提供了我们进行直接IO的方式。O_DIRECT:该描述符提供对直接 I/O 的支持
需要注意的是:使用 Direct I/O 有一个很大的限制:buffer 的内存地址、每次读写数据的大小、文件的 offset 三者都要与底层设备的逻辑块大小对齐(一般是 512 字节)。如果不满足对齐要求,系统会报 EINVAL(Invalid argument) 错误。
更多的直接IO相关的知识,有许多博文进行讲解。

(NOTE)O_SYNC:open有一个标志位O_SYNC,使每次write操作阻塞等待磁盘IO完成,文件数据和文件属性都更新。这个标志的效果和fsync及fdatasync类似的含义:使每次write都会阻塞到磁盘IO完成。

  • 23
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值