当前位置: 首页 > news >正文

建设电子商务网站论文wordpress登录之后强制绑定邮箱

建设电子商务网站论文,wordpress登录之后强制绑定邮箱,徐汇品牌网站建设,网站推广策划思维导图目录Linux 系统如何管理文件静态文件与inode文件打开时的状态返回错误处理与errnostrerror 函数perror 函数exit、_exit、_Exit_exit()和_Exit()函数exit()函数空洞文件概念实验测试O_APPEND 和O_TRUNC 标志O_TRUNC 标志O_APPEND 标志多次打开同一个文件验证一些现象多次打开同… 目录Linux 系统如何管理文件静态文件与inode文件打开时的状态返回错误处理与errnostrerror 函数perror 函数exit、_exit、_Exit_exit()和_Exit()函数exit()函数空洞文件概念实验测试O_APPEND 和O_TRUNC 标志O_TRUNC 标志O_APPEND 标志多次打开同一个文件验证一些现象多次打开同一文件进行读操作与O_APPEND 标志复制文件描述符dup 函数dup2 函数文件共享原子操作与竞争冒险竞争冒险简介原子操作fcntl 和ioctlfcntl 函数ioctl 函数截断文件I/O 缓冲文件I/O 的内核缓冲刷新文件I/O 的内核缓冲区㈠、fsync()函数㈡、fdatasync()函数㈢、sync()函数直接I/O绕过内核缓冲stdio 缓冲㈠、setvbuf()函数㈡、setbuf()函数㈢、setbuffer()函数fflush刷新stdio缓冲区I/O 缓冲小节文件描述符与FILE 指针互转检查或复位状态feof()函数ferror()函数clearerr()函数经过上一章内容的学习相信各位读者对Linux 系统应用编程中的基础文件I/O 操作有了一定的认识和理解了能够独立完成一些简单地文件I/O 编程问题如果你的工作中仅仅只是涉及到一些简单文件读写操作相关的问题其实上一章的知识内容已经够你使用了。 当然作为大部分读者来说我相信你不会止步于此、还想学习更多的知识内容那本章笔者将会同各位读者一起来深入探究文件I/O 中涉及到的一些问题、原理以及所对应的解决方法譬如Linux 系统下文件是如何进行管理的、调用函数返回错误该如何处理、open 函数的O_APPEND、O_TRUNC 标志以及等相关问题。 Linux 系统如何管理文件 静态文件与inode 文件在没有被打开的情况下一般都是存放在磁盘中的譬如电脑硬盘、移动硬盘、U 盘等外部存储设备文件存放在磁盘文件系统中并且以一种固定的形式进行存放我们把他们称为静态文件。 文件储存在硬盘上硬盘的最小存储单位叫做“扇区”Sector每个扇区储存512 字节相当于0.5KB操作系统读取硬盘的时候不会一个个扇区地读取这样效率太低而是一次性连续读取多个扇区即一次性读取一个“块”block。这种由多个扇区组成的“块”是文件存取的最小单位。“块”的大小最常见的是4KB即连续八个sector 组成一个block。 所以由此可以知道静态文件对应的数据都是存储在磁盘设备不同的“块”中那么问题来了我们在程序中调用open 函数是如何找到对应文件的数据存储“块”的呢难道仅仅通过指定的文件路径就可以实现这里我们就来简单地聊一聊这内部实现的过程。 我们的磁盘在进行分区、格式化的时候会将其分为两个区域一个是数据区用于存储文件中的数据另一个是inode 区用于存放inode tableinode 表inode table 中存放的是一个一个的inode也成为inode 节点不同的inode 就可以表示不同的文件每一个文件都必须对应一个inodeinode 实质上是一个结构体这个结构体中有很多的元素不同的元素记录了文件了不同信息譬如文件字节大小、文件所有者、文件对应的读/写/执行权限、文件时间戳创建时间、更新时间等、文件类型、文件数据存储的block块位置等等信息如图3.1.1 中所示这里需要注意的是文件名并不是记录在inode 中这个问题后面章节内容再给大家讲。 图3.1.1 inode table 与inode 所以由此可知inode table 表本身也需要占用磁盘的存储空间。每一个文件都有唯一的一个inode每一个inode 都有一个与之相对应的数字编号通过这个数字编号就可以找到inode table 中所对应的inode。在Linux 系统下我们可以通过ls -i命令查看文件的inode 编号如下所示 所以由此可知inode table 表本身也需要占用磁盘的存储空间。每一个文件都有唯一的一个inode每一个inode 都有一个与之相对应的数字编号通过这个数字编号就可以找到inode table 中所对应的inode。在Linux 系统下我们可以通过ls -i命令查看文件的inode 编号如下所示 上图中ls 打印出来的信息中每一行前面的一个数字就表示了对应文件的inode 编号。除此之外还可以使用stat 命令查看用法如下 由以上的介绍大家可以联系到实际操作中譬如我们在Windows 下进行U 盘格式化的时候会有一个“快速格式化”选项如下所示 如果勾选了“快速格式化”选项在进行格式化操作的时候非常的快而如果不勾选此选项直接使用普通格式化方式将会比较慢那说明这两种格式化方式是存在差异的其实快速格式化只是删除了U 盘中的inode table 表真正存储文件数据的区域并没有动所以使用快速格式化的U 盘其中的数据是可以被找回来的。 通过以上介绍可知打开一个文件系统内部会将这个过程分为三步 系统找到这个文件名所对应的inode 编号通过inode 编号从inode table 中找到对应的inode 结构体根据inode 结构体中记录的信息确定文件数据所在的block并读出数据。 文件打开时的状态 当我们调用open 函数去打开文件的时候内核会申请一段内存一段缓冲区并且将静态文件的数据内容从磁盘这些存储设备中读取到内存中进行管理、缓存也把内存中的这份文件数据叫做动态文件、内核缓冲区。打开文件后以后对这个文件的读写操作都是针对内存中这一份动态文件进行相关的操作而并不是针对磁盘中存放的静态文件。 当我们对动态文件进行读写操作后此时内存中的动态文件和磁盘设备中的静态文件就不同步了数据的同步工作由内核完成内核会在之后将内存这份动态文件更新同步到磁盘设备中。由此我们也可以联系到实际操作中譬如说 ⚫ 打开一个大文件的时候会比较慢 ⚫ 文档写了一半没记得保存此时电脑因为突然停电直接掉电关机了当重启电脑后打开编写的文档发现之前写的内容已经丢失。 想必各位读者在工作当中都遇到过这种问题吧通过上面的介绍就解释了为什么会出现这种问题。好我们再来说一下为什么要这样设计 因为磁盘、硬盘、U 盘等存储设备基本都是Flash 块设备因为块设备硬件本身有读写限制等特征块设备是以一块一块为单位进行读写的一个块包含多个扇区而一个扇区包含多个字节一个字节的改动也需要将该字节所在的block 全部读取出来进行修改修改完成之后再写入块设备中所以导致对块设备的读写操作非常不灵活而内存可以按字节为单位来操作而且可以随机操作任意地址数据非常地很灵活所以对于操作系统来说会先将磁盘中的静态文件读取到内存中进行缓存读写操作都是针对这份动态文件而不是直接去操作磁盘中的静态文件不但操作不灵活效率也会下降很多因为内存的读写速率远比磁盘读写快得多。 在Linux 系统中内核会为每个进程关于进程的概念这是后面的内容我们可以简单地理解为一个运行的程序就是一个进程运行了多个程序那就是存在多个进程设置一个专门的数据结构用于管理该进程譬如用于记录进程的状态信息、运行特征等我们把这个称为进程控制块Process control block缩写 PCB。 PCB 数据结构体中有一个指针指向了文件描述符表File descriptors文件描述符表中的每一个元素索引到对应的文件表File table文件表也是一个数据结构体其中记录了很多文件相关的信息譬如文件状态标志、引用计数、当前文件的读写偏移量以及i-node 指针指向该文件对应的inode等进程打开的所有文件对应的文件描述符都记录在文件描述符表中每一个文件描述符都会指向一个对应的文件表其示意图如下所示 前面给大家介绍了inodeinode 数据结构体中的元素会记录该文件的数据存储的block块也就是说可以通过inode 找到文件数据存在在磁盘设备中的那个位置从而把文件数据读取出来。 以上就是本小节给大家介绍到所有内容了上面给大家所介绍的内容后面的学习过程中还会用到虽然这些理论知识对大家的编程并没有什么影响但是会帮助大家理解文件IO 背后隐藏的一些理论知识其实这些理论知识还是非常浅薄的、只是一个大概的认识其内部具体的实现是比较复杂的当然这个不是我们学习Linux 应用编程的重点操作系统已经帮我们完成了这些具体的实现我们要做的仅仅只是调用操作系统提供API 函数来完成自己的工作。 好了废话不多说我们接着看下一小节内容。 返回错误处理与errno 在上一章节中笔者给大家编写了很多的示例代码大家会发现这些示例代码会有一个共同的特点那就是当判断函数执行失败后会调用return 退出程序但是对于我们来说我们并不知道为什么会出错什么原因导致此函数执行失败因为执行出错之后它们的返回值都是-1。 难道我们真的就不知道错误原因了吗其实不然在Linux 系统下对常见的错误做了一个编号每一个编号都代表着每一种不同的错误类型当函数执行发生错误的时候操作系统会将这个错误所对应的编号赋值给errno 变量每一个进程程序都维护了自己的errno 变量它是程序中的全局变量该变量用于存储就近发生的函数执行错误编号也就意味着下一次的错误码会覆盖上一次的错误码。所以由此可知道当程序中调用函数发生错误的时候操作系统内部会通过设置程序的errno 变量来告知调用者究竟发生了什么错误 errno 本质上是一个int 类型的变量用于存储错误编号但是需要注意的是并不是执行所有的系统调用或C 库函数出错时操作系统都会设置errno那我们如何确定一个函数出错时系统是否会设置errno 呢其实这个通过man 手册便可以查到譬如以open 函数为例执行man 2 open打开open 函数的帮助信息找到函数返回值描述段如下所示 从图中红框部分描述文字可知当函数返回错误时会设置errno当然这里是以open 函数为例其它的系统调用也可以这样查找大家可以自己试试 在我们的程序当中如何去获取系统所维护的这个errno 变量呢只需要在我们程序当中包含errno.h头文件即可你可以直接认为此变量就是在errno.h头文件中的申明的好我们来测试下 #include stdio.h #include errno.h int main(void) {printf(%d\n, errno);return 0; }以上的这段代码是不会报错的大家可以自己试试 strerror 函数 前面给大家说到了errno 变量但是errno 仅仅只是一个错误编号对于开发者来说即使拿到了errno 也不知道错误为何还需要对比源码中对此编号的错误定义可以说非常不友好这里介绍一个C 库函数 strerror()该函数可以将对应的errno 转换成适合我们查看的字符串信息其函数原型如下所示可通过man 3 strerror命令查看注意此函数是C 库函数并不是系统调用 #include string.hchar *strerror(int errnum);首先调用此函数需要包含头文件string.h。 函数参数和返回值如下 errnum错误编号errno。 返回值对应错误编号的字符串描述信息。 测试 接下来我们测试下测试代码如下 #include sys/types.h #include sys/stat.h #include fcntl.h #include unistd.h #include stdio.h #include errno.h #include string.h int main(void) {int fd;/* 打开文件*/fd open(./test_file, O_RDONLY);if (-1 fd) {printf(Error: %s\n, strerror(errno));return -1;}close(fd);return 0; }编译源代码在Ubuntu 系统下运行测试下在当前目录下并不存在test_file 文件测试打印结果如下 从打印信息可以知道strerror 返回的字符串是No such file or directory所以从打印信息可知我们就可以很直观的知道open 函数执行的错误原因是文件不存在 perror 函数 除了strerror 函数之外我们还可以使用perror 函数来查看错误信息一般用的最多的还是这个函数调用此函数不需要传入errno函数内部会自己去获取errno 变量的值调用此函数会直接将错误提示字符串打印出来而不是返回字符串除此之外还可以在输出的错误提示字符串之前加入自己的打印信息函数原型如下所示可通过man 3 perror命令查看 #include stdio.hvoid perror(const char *s);需要包含stdio.h头文件。 函数参数和返回值含义如下 s在错误提示字符串信息之前可加入自己的打印信息也可不加不加则传入空字符串即可。 返回值void 无返回值。 测试 接下来我们进行测试测试代码如下所示 #include sys/types.h #include sys/stat.h #include fcntl.h #include unistd.h #include stdio.h int main(void) {int fd;/* 打开文件*/fd open(./test_file, O_RDONLY);if (-1 fd) {perror(open error);return -1;}close(fd);return 0; }编译源代码在Ubuntu 系统下运行测试下在当前目录下并不存在test_file 文件测试打印结果如下 从打印信息可以知道perror 函数打印出来的错误提示字符串是No such file or directory跟strerror 函数返回的字符串信息一样open error便是我们附加的打印信息而且从打印信息可知perror 函数会在附加信息后面自动加入冒号和空格以区分。 以上给大家介绍了strerror、perror 两个C 库函数都是用于查看函数执行错误时对应的提示信息大家用哪个函数都可以这里笔者推荐大家使用perror在实际的编程中这个函数用的还是比较多的当然除了这两个之外其它其它一些类似功能的函数这里就不再给大家介绍了意义不大 exit、_exit、_Exit 当程序在执行某个函数出错的时候如果此函数执行失败会导致后面的步骤不能在进行下去时应该在出错时终止程序运行不应该让程序继续运行下去那么如何退出程序、终止程序运行呢有过编程经验的读者都知道使用return一般原则程序执行正常退出return 0而执行函数出错退出return -1前面我们所编写的示例代码也是如此。 在Linux 系统下进程程序退出可以分为正常退出和异常退出注意这里说的异常并不是执行函数出现了错误这种情况异常往往更多的是一种不可预料的系统异常可能是执行了某个函数时发生的、也有可能是收到了某种信号等这里我们只讨论正常退出的情况。 在Linux 系统下进程正常退出除了可以使用return 之外还可以使用exit()、_exit()以及_Exit()下面我们分别介绍。 _exit()和_Exit()函数 main 函数中使用return 后返回return 执行后把控制权交给调用函数结束该进程。调用_exit()函数会清除其使用的内存空间并销毁其在内核中的各种数据结构关闭进程的所有文件描述符并结束进程、将控制权交给操作系统。_exit()函数原型如下所示 #include unistd.h void _exit(int status);调用函数需要传入status 状态标志0 表示正常结束、若为其它值则表示程序执行过程中检测到有错误发生。使用示例如下 #include sys/types.h #include sys/stat.h #include fcntl.h #include unistd.h #include stdio.h int main(void) {int fd;/* 打开文件*/fd open(./test_file, O_RDONLY);if (-1 fd) {perror(open error);_exit(-1);}close(fd);_exit(0); }用法很简单大家可以自行测试 _Exit()函数原型如下所示 #include stdlib.h void _Exit(int status);_exit()和_Exit()两者等价用法作用是一样的这里就不再讲了需要注意的是这2 个函数都是系统调用。 exit()函数 exit()函数_exit()函数都是用来终止进程的exit()是一个标准C 库函数而_exit()和_Exit()是系统调用。执行exit()会执行一些清理工作最后调用_exit()函数。exit()函数原型如下 #include stdlib.h void exit(int status);该函数是一个标准C 库函数使用该函数需要包含头文件stdlib.h该函数的用法和_exit()/_Exit()是一样的这里就不再多说了。 本小节就给大家介绍了3 中终止进程的方法 ⚫ main 函数中运行return ⚫ 调用Linux 系统调用_exit()或_Exit() ⚫ 调用C 标准库函数exit()。 不管你用哪一种都可以结束进程但还是推荐大家使用exit()其实关于return、exit、_exit/_Exit()之间的区别笔者在上面只是给大家简单地描述了一下甚至不太确定我的描述是否正确因为笔者并不太多去关心其间的差异对这些概念的描述会比较模糊、笼统如果大家看不明白可以自己百度搜索相关的内容当然对于初学者来说不太建议大家去查找这些东西至少对你现阶段来说意义不是很大。好本小节就介绍这么多我们接着学习下一小节的内容。 空洞文件 概念 什么是空洞文件hole file在上一章内容中笔者给大家介绍了lseek()系统调用使用lseek 可以修改文件的当前读写位置偏移量此函数不但可以改变位置偏移量并且还允许文件偏移量超出文件长度这是什么意思呢譬如有一个test_file该文件的大小是4K也就是4096 个字节如果通过lseek 系统调用将该文件的读写偏移量移动到偏移文件头部6000 个字节处大家想一想会怎样如果笔者没有提前告诉大家大家觉得不能这样操作但事实上lseek 函数确实可以这样操作。 接下来使用write()函数对文件进行写入操作也就是说此时将是从偏移文件头部6000 个字节处开始写入数据也就意味着4096~6000 字节之间出现了一个空洞因为这部分空间并没有写入任何数据所以形成了空洞这部分区域就被称为文件空洞那么相应的该文件也被称为空洞文件。 文件空洞部分实际上并不会占用任何物理空间直到在某个时刻对空洞部分进行写入数据时才会为它分配对应的空间但是空洞文件形成时逻辑上该文件的大小是包含了空洞部分的大小的这点需要注意。 那说了这么多空洞文件有什么用呢空洞文件对多线程共同操作文件是及其有用的有时候我们创建一个很大的文件如果单个线程从头开始依次构建该文件需要很长的时间有一种思路就是将文件分为多段然后使用多线程来操作每个线程负责其中一段数据的写入这个有点像我们现实生活当中施工队修路的感觉比如说修建一条高速公路单个施工队修筑会很慢这个时候可以安排多个施工队每一个施工队负责修建其中一段最后将他们连接起来。 来看一下实际中空洞文件的两个应用场景 ⚫ 在使用迅雷下载文件时还未下载完成就发现该文件已经占据了全部文件大小的空间这也是空洞文件下载时如果没有空洞文件多线程下载时文件就只能从一个地方写入这就不能发挥多线程的作用了如果有了空洞文件可以从不同的地址同时写入就达到了多线程的优势 ⚫ 在创建虚拟机时你给虚拟机分配了100G 的磁盘空间但其实系统安装完成之后开始也不过只用了3、4G 的磁盘空间如果一开始就把100G 分配出去资源是很大的浪费。 关于空洞文件这里就介绍这么多上述描述当中多次提到了线程这个概念关于线程这是后面的内容这里先不给大家讲。 实验测试 这里我们进行相关的测试新建一个文件把它做成空洞文件示例代码如下所示 #include sys/types.h #include sys/stat.h #include fcntl.h #include unistd.h #include stdio.h #include string.h #include stdlib.h int main(void) {int fd;int ret;char buffer[1024];int i;/* 打开文件*/fd open(./hole_file, O_WRONLY | O_CREAT | O_EXCL,S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);if (-1 fd) {perror(open error);exit(-1);}/* 将文件读写位置移动到偏移文件头4096 个字节(4K)处*/ret lseek(fd, 4096, SEEK_SET);if (-1 ret) {perror(lseek error);goto err;}/* 初始化buffer 为0xFF */memset(buffer, 0xFF, sizeof(buffer));/* 循环写入4 次每次写入1K */for (i 0; i 4; i) {ret write(fd, buffer, sizeof(buffer));if (-1 ret) {perror(write error);goto err;}}ret 0; err:/* 关闭文件*/close(fd);exit(ret); }示例代码中我们使用open 函数新建了一个文件hole_file在Linux 系统中新建文件大小是0也就是没有任何数据写入此时使用lseek函数将读写偏移量移动到4K 字节处再使用write 函数写入数据0xFF每次写入1K一共写入4 次也就是写入了4K 数据也就意味着该文件前4K 是文件空洞部分而后4K 数据才是真正写入的数据。 接下来进行编译测试首先确保当前文件目录下不存在hole_file 文件测试结果如下 使用ls 命令查看到空洞文件的大小是8K使用ls 命令查看到的大小是文件的逻辑大小自然是包括了空洞部分大小和真实数据部分大小当使用du 命令查看空洞文件时其大小显示为4Kdu 命令查看到的大小是文件实际占用存储块的大小。 本小节内容就讲解完了最后再向各位抛出一个问题若使用read 函数读取文件空洞部分读取出来的将会是什么关于这个问题大家可以先思考下至于结果是什么笔者这里便不给出答案了大家可以自己动手编写代码进行测试以得出结论。 O_APPEND 和O_TRUNC 标志 在上一章给大家讲解open 函数的时候介绍了一些open 函数的flags 标志譬如O_RDONLY、 O_WRONLY、O_CREAT、O_EXCL 等本小节再给大家介绍两个标志分别是O_APPEND 和O_TRUNC接下来对这两个标志分别进行介绍。 O_TRUNC 标志 O_TRUNC 这个标志的作用非常简单如果使用了这个标志调用open 函数打开文件的时候会将文件原本的内容全部丢弃文件大小变为0这里我们直接测试即可测试代码如下所示 #include sys/types.h #include sys/stat.h #include fcntl.h #include unistd.h #include stdio.h #include stdlib.h int main(void) {int fd;/* 打开文件*/fd open(./test_file, O_WRONLY | O_TRUNC);if (-1 fd) {perror(open error);exit(-1);}/* 关闭文件*/close(fd);exit(0); }在当前目录下有一个文件test_file测试代码中使用了O_TRUNC 标志打开该文件代码中仅仅只是打开该文件之后调用close 关闭了文件并没有对其进行读写操作接下来编译运行来看看测试结果 在测试之前test_file 文件中是有数据的文件大小为8760 个字节执行完测试程序后再使用ls 命令查看文件大小时发现test_file 大小已经变成了0也就是说明文件之前的内容已经全部被丢弃了。这就是 O_TRUNC 标志的作用了大家可以自己动手试试。 O_APPEND 标志 接下里聊一聊O_APPEND 标志如果open 函数携带了O_APPEND 标志调用open 函数打开文件当每次使用write()函数对文件进行写操作时都会自动把文件当前位置偏移量移动到文件末尾从文件末尾开始写入数据也就是意味着每次写入数据都是从文件末尾开始。这里我们直接进行测试测试代码如下所示 #include sys/types.h #include sys/stat.h #include fcntl.h #include unistd.h #include stdio.h #include stdlib.h #include string.h int main(void) {char buffer[16];int fd;int ret;/* 打开文件*/fd open(./test_file, O_RDWR | O_APPEND);if (-1 fd) {perror(open error);exit(-1);}/* 初始化buffer 中的数据*/memset(buffer, 0x55, sizeof(buffer));/* 写入数据: 写入4 个字节数据*/ret write(fd, buffer, 4);if (-1 ret) {perror(write error);goto err;}/* 将buffer 缓冲区中的数据全部清0 */memset(buffer, 0x00, sizeof(buffer));/* 将位置偏移量移动到距离文件末尾4 个字节处*/ret lseek(fd, -4, SEEK_END);if (-1 ret) {perror(lseek error);goto err;}/* 读取数据*/ret read(fd, buffer, 4);if (-1 ret) {perror(read error);goto err;}printf(0x%x 0x%x 0x%x 0x%x\n, buffer[0], buffer[1],buffer[2], buffer[3]);ret 0; err:/* 关闭文件*/close(fd);exit(ret); }测试代码中会去打开当前目录下的test_file 文件使用可读可写方式并且使用了O_APPEND 标志前面笔者给大家提到过open 打开一个文件默认的读写位置偏移量会处于文件头但测试代码中使用了 O_APPEND 标志如果O_APPEND 确实能生效的话也就意味着调用write 函数会从文件末尾开始写代码中写入了4 个字节数据都是0x55之后,使用lseek 函数将位置偏移量移动到距离文件末尾4 个字节处读取4 个字节也就是读取文件最后4 个字节数据之后将其打印出来如果上面笔者的描述正确的话打印出来的数据就是我们写入的数据如果O_APPEND 不能生效则打印出来数据就不会是0x55接下来编译测试 从上面打印信息可知读取出来的数据确实等于0x55说明O_APPEND 标志确实有作用当调用write() 函数写文件时会自动把文件当前位置偏移量移动到文件末尾。 当然本小节内容还并没有结束这其中还涉及到一些细节问题需要大家注意首先第一点O_APPEND 标志并不会影响读文件当读取文件时O_APPEND 标志并不会影响读位置偏移量即使使用了O_APPEND 标志读文件位置偏移量默认情况下依然是文件头关于这个问题大家可以自己进行测试编程是一个实践性很强的工作有什么不能理解的问题可以自己编写程序进行测试。 大家可能会想到使用lseek 函数来改变write()时的写位置偏移量其实这种做法并不会成功这就是笔者给大家提的第二个细节使用了O_APPEND 标志即使是通过lseek 函数也是无法修改写文件时对应的位置偏移量注意笔者这里说的是写文件并不包括读写入数据依然是从文件末尾开始lseek 并不会该变写位置偏移量这个问题测试方法很简单也就是在write 之前使用lseek 修改位置偏移量这里笔者就不再给大家测试了我还是那句话编程是一个实践性很强的工作大家只需要把示例代码3.5.2 进行简单地修改即可 其实关于第二点细节原因很简单当执行write()函数时检测到open 函数携带了O_APPEND 标志所以在write 函数内部会自动将写位置偏移量移动到文件末尾当然这里也只是笔者的一个简单地猜测至于是不是这样笔者也无从考证。 到这里本小节的内容就暂时介绍完了为什么说是“暂时”因为后面的内容中还会聊到O_APPEND 标志最后笔者再给大家出一个小问题大家可以自己动手测试。 ◆ 当open 函数同时携带了O_APPEND 和O_TRUNC 两个标志时会有什么作用 多次打开同一个文件 大家看到这个小节标题可能会有疑问同一个文件还能被多次打开事实确实如此同一个文件可以被多次打开譬如在一个进程中多次打开同一个文件、在多个不同的进程中打开同一个文件那么这些操作都是被允许的。本小节就来探讨下多次打开同一个文件会有一些什么现象以及相应的细节问题 验证一些现象 ⚫ 一个进程内多次open 打开同一个文件那么会得到多个不同的文件描述符fd同理在关闭文件的时候也需要调用close 依次关闭各个文件描述符。 针对这个问题我们编写测试代码进行测试如下所示 #include sys/types.h #include sys/stat.h #include fcntl.h #include unistd.h #include stdio.h #include stdlib.h int main(void) {int fd1, fd2, fd3;int ret;/* 第一次打开文件*/fd1 open(./test_file, O_RDWR);if (-1 fd1) {perror(open error);exit(-1);}/* 第二次打开文件*/fd2 open(./test_file, O_RDWR);if (-1 fd2) {perror(open error);ret -1;goto err1;}/* 第三次打开文件*/fd3 open(./test_file, O_RDWR);if (-1 fd3) {perror(open error);ret -1;goto err2;}/* 打印出3 个文件描述符*/printf(%d %d %d\n, fd1, fd2, fd3);close(fd3);ret 0; err2:close(fd2); err1:/* 关闭文件*/close(fd1);exit(ret); }上述示例代码中通过3 次调用open 函数对test_file 文件打开了3 次每一个调用传参一样最后将 3 次得到的文件描述符打印出来在当前目录下存在test_file 文件接下来编译测试看看结果如何 从打印结果可知三次调用open 函数得到的文件描述符分别为6、7、8通过任何一个文件描述符对文件进行IO 操作都是可以的但是需要注意是调用open 函数打开文件使用的是什么权限则返回的文件描述符就拥有什么权限文件IO 操作完成之后在结束进程之前需要使用close 关闭各个文件描述符。 在图3.6.1 中细心的读者可能会发现调用open 函数得到的最小文件描述符是6在上一章节内容中给大家提到过程序中分配得到的最小文件描述符一般是3但这里竟然是6这是为何其实这个问题跟 vscode 有关说明3、4、5 这3 个文件描述符已经被vscode 软件对应的进程所占用了而当前这里执行 testApp 文件是在vscode 软件提供的终端下进行的所以vscode 可以认为是testApp 进程的父进程相反 testApp 进程便是vscode 进程的子进程子进程会继承父进程的文件描述符。关于子进程和父进程这些都是后面的内容这里暂时不给大家进行介绍这是只是给大家简单地解释一下免得大家误会 其实可以直接在Ubuntu 系统的Terminal 终端执行testApp这时你会发现打印出来的文件描述符分别是3、4、5这里就不给大家演示了。 ⚫ 一个进程内多次open 打开同一个文件在内存中并不会存在多份动态文件。 当调用open 函数的时候会将文件数据文件内容从磁盘等块设备读取到内存中将文件数据在内存中进行维护内存中的这份文件数据我们就把它称为动态文件这是前面给大家介绍的内容这里再简单地提一下。这里出现了一个问题如果同一个文件被多次打开那么该文件所对应的动态文件是否在内存中也存在多份也就是说多次打开同一个文件是否会将其文件数据多次拷贝到内存中进行维护 关于这个问题各位读者可以简单地思考一下这里我们直接编写代码进行测试测试代码如下所示 #include sys/types.h #include sys/stat.h #include fcntl.h #include unistd.h #include stdio.h #include stdlib.h #include string.h int main(void) {char buffer[4];int fd1, fd2;int ret;/* 创建新文件test_file 并打开*/fd1 open(./test_file, O_RDWR | O_CREAT | O_EXCL,S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);if (-1 fd1) {perror(open error);exit(-1);}/* 再次打开test_file 文件*/fd2 open(./test_file, O_RDWR);if (-1 fd2) {perror(open error);ret -1;goto err1;}/* 通过fd1 文件描述符写入4 个字节数据*/buffer[0] 0x11;buffer[1] 0x22;buffer[2] 0x33;buffer[3] 0x44;ret write(fd1, buffer, 4);if (-1 ret) {perror(write error);goto err2;}/* 将读写位置偏移量移动到文件头*/buffer[0] 0x11;buffer[1] 0x22;buffer[2] 0x33;buffer[3] 0x44;ret write(fd1, buffer, 4);if (-1 ret) {perror(write error);goto err2;}/* 将读写位置偏移量移动到文件头*/ret lseek(fd2, 0, SEEK_SET);if (-1 ret) {perror(lseek error);goto err2;}/* 读取数据*/memset(buffer, 0x00, sizeof(buffer));ret read(fd2, buffer, 4);if (-1 ret) {perror(read error);goto err2;}printf(0x%x 0x%x 0x%x 0x%x\n, buffer[0], buffer[1],buffer[2], buffer[3]);ret 0; err2:close(fd2); err1:/* 关闭文件*/close(fd1);exit(ret); } 当前目录下不存在test_file 文件上述代码中第一次调用open 函数新建并打开test_file 文件第二次调用open 函数再次打开它新建文件时文件大小为0首先通过文件描述符fd1 写入4 个字节数据0x11/0x22/0x33/0x44从文件头开始写然后再通过文件描述符fd2 读取4 个字节数据也是从文件头开始读取。假如内存中只有一份动态文件那么读取得到的数据应该就是0x11、0x22、0x33、0x44如果存在多份动态文件那么通过fd2 读取的是与它对应的动态文件中的数据那就不是0x11、0x22、0x33、 0x44而是读取出0 个字节数据因为它的文件大小是0。 接下来进行编译测试 上图中打印显示读取出来的数据是0x11/0x22/0x33/0x44所以由此可知即使多次打开同一个文件内存中也只有一份动态文件。 ⚫ 一个进程内多次open 打开同一个文件不同文件描述符所对应的读写位置偏移量是相互独立的。 同一个文件被多次打开会得到多个不同的文件描述符也就意味着会有多个不同的文件表而文件读写偏移量信息就记录在文件表数据结构中所以从这里可以推测不同的文件描述符所对应的读写偏移量是相互独立的并没有关联在一起并且文件表中i-node 指针指向的都是同一个inode如下图所示 测试的方法很简单只需在示例代码3.6.2 中简单地修改即可将lseek 函数调用去掉然后在编译测试如果读出的数据依然是0x11/0x22/0x33/0x44则表示第三点结论成立这里不再给大家演示。 Tips多个不同的进程中调用open()打开磁盘中的同一个文件同样在内存中也只是维护了一份动态文件多个进程间共享它们有各自独立的文件读写位置偏移量。 动态文件何时被关闭呢当文件的引用计数为0 时系统会自动将其关闭同一个文件被打开多次文件表中会记录该文件的引用计数如图3.1.5 所示引用计数记录了当前文件被多少个文件描述符fd 关联。 多次打开同一文件进行读操作与O_APPEND 标志 重复打开同一个文件进行写操作譬如一个进程中两次调用open 函数打开同一个文件分别得到两个文件描述符fd1 和fd2使用这两个文件描述符对文件进行写入操作那么它们是分别写各从各的位置偏移量开始写还是接续写一个写完另一个接着后面写其实这个问题3.6.1 小节中已经给出了答案因为这两个文件描述符所对应的读写位置偏移量是相互独立的所以是分别写接下来我们还是编写代码进行测试测试代码如下所示 #include sys/types.h #include sys/stat.h #include fcntl.h #include unistd.h #include stdio.h #include stdlib.h #include string.h int main(void) {unsigned char buffer1[4], buffer2[4];int fd1, fd2;int ret;int i;/* 创建新文件test_file 并打开*/fd1 open(./test_file, O_RDWR | O_CREAT | O_EXCL,S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);if (-1 fd1) {perror(open error);exit(-1);}/* 再次打开test_file 文件*/fd2 open(./test_file, O_RDWR);if (-1 fd2) {perror(open error);ret -1;goto err1;}/* buffer 数据初始化*/buffer1[0] 0x11;buffer1[1] 0x22;buffer1[2] 0x33;buffer1[3] 0x44;buffer2[0] 0xAA;buffer2[1] 0xBB;buffer2[2] 0xCC;buffer2[3] 0xDD;/* 循环写入数据*/for (i 0; i 4; i) {ret write(fd1, buffer1, sizeof(buffer1));if (-1 ret) {perror(write error);goto err2;}ret write(fd2, buffer2, sizeof(buffer2));if (-1 ret) {perror(write error);goto err2;}}/* 将读写位置偏移量移动到文件头*/ret lseek(fd1, 0, SEEK_SET);if (-1 ret) {perror(lseek error);goto err2;}/* 读取数据*/for (i 0; i 8; i) {ret read(fd1, buffer1, sizeof(buffer1));if (-1 ret) {perror(read error);goto err2;}printf(%x%x%x%x, buffer1[0], buffer1[1],buffer1[2], buffer1[3]);}printf(\n);ret 0; err2:close(fd2); err1:/* 关闭文件*/close(fd1);exit(ret); }重复两次打开test_file 文件分别得到两个文件描述符fd1、fd2首先通过fd1 写入4 个字节数据0x11、0x22、0x33、0x44到文件中接着再通过fd2 写入4 个字节数据0xaa、0xbb、 0xcc、0xdd到文件中循环写入4 此最后再将写入的数据读取出来将其打印到终端。如果它们是分别写那么读取出来的数据就应该是aabbccdd……因为通过fd1 写入的数据被fd2 写入的数据给覆盖了如果它们是接续写那么读取出来的数据应该是11223344aabbccdd……接下里我们编译测试 从打印结果可知它们确实是分别写。如果想要实现接续写也就是当通过fd1 写入完成之后通过fd2 写入的数据是接在fd1 写入的数据之后那么该怎么做呢当然可以写入数据之前通过lseek 函数将文件偏移量移动到文件末尾如果是这样做会存在一些问题关于这个问题后面再给大家介绍这里我们给大家介绍使用O_APPEND 标志来解决这个问题也就是将分别写更改为接续写。 前面给大家介绍了open 函数的O_APPEND 标志当open 函数使用O_APPEND 标志在使用write 函数进行写入操作时会自动将偏移量移动到文件末尾也就是每次写入都是从文件末尾开始这里结合本小节的内容我们再来讨论O_APPEND 标志在多次打开同一个文件进行写操作时使用O_APPEND 标志会有什么样的效果接下来进行测试 #include sys/types.h #include sys/stat.h #include fcntl.h #include unistd.h #include stdio.h #include stdlib.h #include string.h int main(void) {unsigned char buffer1[4], buffer2[4];int fd1, fd2;int ret;int i;/* 创建新文件test_file 并打开*/fd1 open(./test_file, O_RDWR | O_CREAT | O_EXCL | O_APPEND,S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);if (-1 fd1) {perror(open error);exit(-1);}/* 再次打开test_file 文件*/fd2 open(./test_file, O_RDWR | O_APPEND);if (-1 fd2) {perror(open error);ret -1;goto err1;}/* buffer 数据初始化*/buffer1[0] 0x11;buffer1[1] 0x22;buffer1[2] 0x33;buffer1[3] 0x44;buffer2[0] 0xAA;buffer2[1] 0xBB;buffer2[2] 0xCC;buffer2[3] 0xDD;/* 循环写入数据*/for (i 0; i 4; i) {ret write(fd1, buffer1, sizeof(buffer1));if (-1 ret) {perror(write error);goto err2;}ret write(fd2, buffer2, sizeof(buffer2));if (-1 ret) {perror(write error);goto err2;}}/* 将读写位置偏移量移动到文件头*/ret lseek(fd1, 0, SEEK_SET);if (-1 ret) {perror(lseek error);goto err2;}/* 读取数据*/for (i 0; i 8; i) {ret read(fd1, buffer1, sizeof(buffer1));if (-1 ret) {perror(read error);goto err2;}printf(%x%x%x%x, buffer1[0], buffer1[1],buffer1[2], buffer1[3]);}printf(\n);ret 0; err2:close(fd2); err1:/* 关闭文件*/close(fd1);exit(ret); }open 函数添加了O_APPEND 标志其它内容并没有动过接下来编译测试。 从打印出来的数据可知加入了O_APPEND 标志后分别写已经变成了接续写。关于O_APPEND 标志还涉及到一个原子操作的问题后面再给大家介绍本小节内容到此 复制文件描述符 在Linux 系统中open 返回得到的文件描述符fd 可以进行复制复制成功之后可以得到一个新的文件描述符使用新的文件描述符和旧的文件描述符都可以对文件进行IO 操作复制得到的文件描述符和旧的文件描述符拥有相同的权限譬如使用旧的文件描述符对文件有读写权限那么新的文件描述符同样也具有读写权限在Linux 系统下可以使用dup 或dup2 这两个系统调用对文件描述符进行复制本小节就给大家介绍这两个函数的用法以及它们之间的区别。 复制得到的文件描述符与旧的文件描述符都指向了同一个文件表假设fd1 为原文件描述符fd2 为复制得到的文件描述符如下图所示 因为复制得到的文件描述符与旧的文件描述符指向的是同一个文件表所以可知这两个文件描述符的属性是一样譬如对文件的读写权限、文件状态标志、文件偏移量等所以从这里也可知道“复制”的含义实则是复制文件表。同样在使用完毕之后也需要使用close 来关闭文件描述符。 dup 函数 dup 函数用于复制文件描述符此函数原型如下所示可通过man 2 dup命令查看 #include unistd.h int dup(int oldfd);首先使用此函数需要包含头文件unistd.h。 函数参数和返回值含义如下 oldfd需要被复制的文件描述符。 返回值成功时将返回一个新的文件描述符由操作系统分配分配置原则遵循文件描述符分配原则如果复制失败将返回-1并且会设置errno 值。 测试 由前面的介绍可知复制得到的文件描述符与原文件描述符都指向同一个文件表所以它们的文件读写偏移量是一样的那么是不是可以在不使用O_APPEND 标志的情况下通过文件描述符复制来实现接续写接下来我们编写一个程序进行测试测试代码如下所示 #include sys/types.h #include sys/stat.h #include fcntl.h #include unistd.h #include stdio.h #include stdlib.h #include string.h int main(void) {unsigned char buffer1[4], buffer2[4];int fd1, fd2;int ret;int i;/* 创建新文件test_file 并打开*/fd1 open(./test_file, O_RDWR | O_CREAT | O_EXCL,S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);if (-1 fd1) {perror(open error);exit(-1);}/* 复制文件描述符*/fd2 dup(fd1);if (-1 fd2) {perror(dup error);ret -1;goto err1;}printf(fd1: %d\nfd2: %d\n, fd1, fd2);/* buffer 数据初始化*/buffer1[0] 0x11;buffer1[1] 0x22;buffer1[2] 0x33;buffer1[3] 0x44;buffer2[0] 0xAA;buffer2[1] 0xBB;buffer2[2] 0xCC;buffer2[3] 0xDD;/* 循环写入数据*/for (i 0; i 4; i) {ret write(fd1, buffer1, sizeof(buffer1));if (-1 ret) {perror(write error);goto err2;}ret write(fd2, buffer2, sizeof(buffer2));if (-1 ret) {perror(write error);goto err2;}}/* 将读写位置偏移量移动到文件头*/ret lseek(fd1, 0, SEEK_SET);if (-1 ret) {perror(lseek error);goto err2;}/* 读取数据*/for (i 0; i 8; i) {ret read(fd1, buffer1, sizeof(buffer1));if (-1 ret) {perror(read error);goto err2;}printf(%x%x%x%x, buffer1[0], buffer1[1],buffer1[2], buffer1[3]);}printf(\n);ret 0; err2:close(fd2); err1:/* 关闭文件*/close(fd1);exit(ret); }测试代码中我们使用了dup 系统调用复制了文件描述符fd1得到另一个新的文件描述符fd2分别通过fd1 和fd2 对文件进行写操作最后读取写入的数据来判断是分别写还是接续写接下来编译测试 由打印信息可知fd1 等于6复制得到的新的文件描述符为7遵循fd 分配原则打印出来的数据显示为接续写所以可知通过复制文件描述符可以实现接续写。 dup2 函数 dup 系统调用分配的文件描述符是由系统分配的遵循文件描述符分配原则并不能自己指定一个文件描述符这是dup 系统调用的一个缺陷而dup2 系统调用修复了这个缺陷可以手动指定文件描述符而不需要遵循文件描述符分配原则当然在实际的编程工作中需要根据自己的情况来进行选择。 dup2 函数原型如下所示可以通过man 2 dup2命令查看 #include unistd.h int dup2(int oldfd, int newfd);同样使用该命令也需要包含unistd.h头文件。 函数参数和返回值含义如下 oldfd需要被复制的文件描述符。 newfd指定一个文件描述符需要指定一个当前进程没有使用到的文件描述符。 返回值成功时将返回一个新的文件描述符也就是手动指定的文件描述符newfd如果复制失败将返回-1并且会设置errno 值。 测试 接下来编写一个简单地测试程序如下所示 #include sys/types.h #include sys/stat.h #include fcntl.h #include unistd.h #include stdio.h #include stdlib.h int main(void) {int fd1, fd2;int ret;/* 创建新文件test_file 并打开*/fd1 open(./test_file, O_RDWR | O_CREAT | O_EXCL,S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);if (-1 fd1) {perror(open error);exit(-1);}/* 复制文件描述符*/fd2 dup2(fd1, 100);if (-1 fd2) {perror(dup error);ret -1;goto err1;}printf(fd1: %d\nfd2: %d\n, fd1, fd2);ret 0;close(fd2); err1:/* 关闭文件*/close(fd1);exit(ret); }测试代码使用dup2 函数复制文件描述符fd1指定新的文件描述符为100复制成功之后将其打印出来结果如下所示 由打印信息可知复制得到的文件描述符fd2 等于100正是我们在dup2 函数中指定的文件描述符。本小节的内容到这里结束了最后再强调一点文件描述符并不是只能复制一次实际上可以对同一个文件描述符fd 调用dup 或dup2 函数复制多次得到多个不同的文件描述符。 文件共享 什么是文件共享所谓文件共享指的是同一个文件譬如磁盘上的同一个文件对应同一个inode被多个独立的读写体同时进行IO 操作。多个独立的读写体大家可以将其简单地理解为对应于同一个文件的多个不同的文件描述符譬如多次打开同一个文件所得到的多个不同的fd或使用dup()或dup2函数复制得到的多个不同的fd 等。 同时进行IO 操作指的是一个读写体操作文件尚未调用close 关闭的情况下另一个读写体去操作文件前面给大家编写的示例代码中就已经涉及到了文件共享的内容了譬如3.6 小节中编写的示例代码中同一个文件对应两个不同的文件描述符fd1 和fd2当使用fd1 对文件进行写操作之后并没有关闭fd1而此时使用fd2 对文件再进行写操作这其实就是一种文件共享。 文件共享的意义有很多多用于多进程或多线程编程环境中譬如我们可以通过文件共享的方式来实现多个线程同时操作同一个大文件以减少文件读写时间、提升效率。 文件共享的核心是如何制造出多个不同的文件描述符来指向同一个文件。其实方法在上面的内容中都已经给大家介绍过了譬如多次调用open 函数重复打开同一个文件得到多个不同的文件描述符、使用dup() 或dup2()函数对文件描述符进行复制以得到多个不同的文件描述符。 常见的三种文件共享的实现方式 (1)同一个进程中多次调用open 函数打开同一个文件各数据结构之间的关系如下图所示 这种情况非常简单多次调用open 函数打开同一个文件会得到多个不同的文件描述符并且多个文件描述符对应多个不同的文件表所有的文件表都索引到了同一个inode 节点也就是磁盘上的同一个文件。 (2)不同进程中分别使用open 函数打开同一个文件其数据结构关系图如下所示 进程1 和进程2 分别是运行在Linux 系统上两个独立的进程理解为两个独立的程序在他们各自的程序中分别调用open 函数打开同一个文件进程1 对应的文件描述符为fd1进程2 对应的文件描述符为 fd2fd1 指向了进程1 的文件表1fd2 指向了进程2 的文件表2各自的文件表都索引到了同一个inode 节点从而实现共享文件。 (3)同一个进程中通过dupdup2函数对文件描述符进行复制其数据结构关系如下图所示 这种方式上一小节已经给大家进行了详细讲解这里不再重述 对于文件共享存在着竞争冒险这个是需要大家关注的下一小节将会向大家介绍。除此之外我们还需要关心的是文件共享时不同的读写体之间是分别写还是接续写这些细节问题大家都要搞清楚。 原子操作与竞争冒险 Linux 是一个多任务、多进程操作系统系统中往往运行着多个不同的进程、任务多个不同的进程就有可能对同一个文件进行IO 操作此时该文件便是它们的共享资源它们共同操作着同一份文件操作系统级编程不同于大家以前接触的裸机编程裸机程序中不存在进程、多任务这种概念而在Linux 系统中我们必须要留意到多进程环境下可能会导致的竞争冒险。 竞争冒险简介 本小节给大家竞争冒险这个概念如果学习过Linux 驱动开发的读者对这些概念应该并不陌生也就意味着竞争冒险不但存在于Linux 应用层、也存在于Linux 内核驱动层。 假设有两个独立的进程A 和进程B 都对同一个文件进行追加写操作也就是在文件末尾写入数据每一个进程都调用了open 函数打开了该文件但未使用O_APPEND 标志此时各数据结构之间的关系如图3.8.2 所示。每个进程都有它自己的进程控制块PCB有自己的文件表意味着有自己独立的读写位置偏移量但是共享同一个inode 节点也就是对应同一个文件。假定此时进程A 处于运行状态B 未处于等待运行状态进程A 调用了lseek 函数它将进程A 的该文件当前位置偏移量设置为1500 字节处假设这里是文件末尾刚好此时进程A 的时间片耗尽然后内核切换到了进程B进程B 执行lseek 函数也将其对该文件的当前位置偏移量设置为1500 个字节处文件末尾。然后进程B 调用write 函数写入了100 个字节数据那么此时在进程B 中该文件的当前位置偏移量已经移动到了1600 字节处。B 进程时间片耗尽内核又切换到了进程A使进程A 恢复运行当进程A 调用write 函数时是从进程A 的该文件当前位置偏移量1500 字节处开始写入此时文件1500 字节处已经不再是文件末尾了如果还从1500 字节处写入就会覆盖进程B 刚才写入到该文件中的数据。 其上述假设工作流程图如下图所示 以上给大家所描述的这样一种情形就属于竞争状态也成为竞争冒险操作共享资源的两个进程或线程其操作之后的所得到的结果往往是不可预期的因为每个进程或线程去操作文件的顺序是不可预期的即这些进程获得CPU 使用权的先后顺序是不可预期的完全由操作系统调配这就是所谓的竞争状态。 既然存在竞争状态那么该如何规避或消除这种状态呢接下来给大家介绍原子操作。 原子操作 在上一章给大家介绍open 函数的时候就提到过“原子操作”这个概念了同样在Linux 驱动编程中也有这个概念相信学习过Linux 驱动编程开发的读者应该有印象。 从上一小节给大家提到的示例中可知上述的问题出在逻辑操作“先定位到文件末尾然后再写”它使用了两个分开的函数调用首先使用lseek 函数将文件当前位置偏移量移动到文件末尾、然后在使用write 函数将数据写入到文件。既然知道了问题所在那么解决办法就是将这两个操作步骤合并成一个原子操作所谓原子操作是有多步操作组成的一个操作原子操作要么一步也不执行一旦执行必须要执行完所有步骤不可能只执行所有步骤中的一个子集。 (1)O_APPEND 实现原子操作 在上一小节给大家提到的示例中进程A 和进程B 都对同一个文件进行追加写操作导致进程A 写入的数据覆盖了进程B 写入的数据解决办法就是将“先定位到文件末尾然后写”这两个步骤组成一个原子操作即可那如何使其变成一个原子操作呢答案就是O_APPEND 标志。 前面已经给大家多次提到过了O_APPEND 标志但是并没有给大家介绍O_APPEND 的一个非常重要的作用那就是实现原子操作。当open 函数的flags 参数中包含了O_APPEND 标志每次执行write 写入操作时都会将文件当前写位置偏移量移动到文件末尾然后再写入数据这里“移动当前写位置偏移量到文件末尾、写入数据”这两个操作步骤就组成了一个原子操作加入O_APPEND 标志后不管怎么写入数据都会是从文件末尾写这样就不会导致出现“进程A 写入的数据覆盖了进程B 写入的数据”这种情况了。 (2)pread()和pwrite() pread()和pwrite()都是系统调用与read()、write()函数的作用一样用于读取和写入数据。区别在于 pread()和pwrite()可用于实现原子操作调用pread 函数或pwrite 函数可传入一个位置偏移量offset 参数用于指定文件当前读或写的位置偏移量所以调用pread 相当于调用lseek 后再调用read同理调用pwrite 相当于调用lseek 后再调用write。所以可知使用pread 或pwrite 函数不需要使用lseek 来调整当前位置偏移量并会将“移动当前位置偏移量、读或写”这两步操作组成一个原子操作。 pread、pwrite 函数原型如下所示可通过man 2 pread或man 2 pwrite命令来查看 #include unistd.h ssize_t pread(int fd, void *buf, size_t count, off_t offset); ssize_t pwrite(int fd, const void *buf, size_t count, off_t offset);首先调用这两个函数需要包含头文件unistd.h。 函数参数和返回值含义如下 fd、buf、count 参数与read 或write 函数意义相同。 offset表示当前需要进行读或写的位置偏移量。 返回值返回值与read、write 函数返回值意义一样。 虽然pread或pwrite函数相当于lseek 与pread或pwrite函数的集合但还是有下列区别 ⚫ 调用pread 函数时无法中断其定位和读操作也就是原子操作 ⚫ 不更新文件表中的当前位置偏移量。 关于第二点我们可以编写一个简单地代码进行测试测试代码如下所示 #include sys/types.h #include sys/stat.h #include fcntl.h #include unistd.h #include stdio.h #include stdlib.h int main(void) {unsigned char buffer[100];int fd;int ret;/* 打开文件test_file */fd open(./test_file, O_RDWR);if (-1 fd) {perror(open error);exit(-1);}/* 使用pread 函数读取数据(从偏移文件头1024 字节处开始读取) */ret pread(fd, buffer, sizeof(buffer), 1024);if (-1 ret) {perror(pread error);goto err;}/* 获取当前位置偏移量*/ret lseek(fd, 0, SEEK_CUR);if (-1 ret) {perror(lseek error);goto err;}printf(Current Offset: %d\n, ret);ret 0; err:/* 关闭文件*/close(fd);exit(ret); }在当前目录下存在一个文件test_file上述代码中会打开test_file 文件然后直接使用pread 函数读取 100 个字节数据从偏移文件头部1024 字节处读取完成之后再使用lseek 函数获取到文件当前位置偏移量并将其打印出来。假如pread 函数会改变文件表中记录的当前位置偏移量则打印出来的数据应该是 1024 100 1124如果不会改变文件表中记录的当前位置偏移量则打印出来的数据应该是0接下来编译代码测试 从上图中可知打印出来的数据为0正如前面所介绍那样pread 函数确实不会改变文件表中记录的当前位置偏移量同理pwrite 函数也是如此大家可以把pread 换成pwrite 函数再次进行测试不出意外打印出来的数据依然是0。 如果把pread 函数换成read或write函数那么打印出来的数据就是100 了因为读取了100 个字节数据相应的当前位置偏移量会向后移动100 个字节。 (3)创建一个文件 前面给大家介绍open 函数的O_EXCL 标志的时候也提到了原子操作其中介绍到O_EXCL 可以用于测试一个文件是否存在如果不存在则创建此文件如果存在则返回错误这使得测试和创建两者成为一个原子操作。接下来给大家创建文件中存在着的一个竞争状态。 假设有这么一个情况进程A 和进程B 都要去打开同一个文件、并且此文件还不存在。进程A 当前正在运行状态、进程B 处于等待状态进程A 首先调用open(“./file”, O_RDWR)函数尝试去打开文件结果返回错误也就是调用open 失败接着进程A 时间片耗尽、进程B 运行同样进程B 调用open(“./file”, O_RDWR)尝试打开文件结果也失败接着进程B 再次调用open(“./file”, O_RDWR | O_CREAT, …)创建此文件这一次open 执行成功文件创建成功接着进程B 时间片耗尽、进程A 继续运行进程A 也调用 open(“./file”, O_RDWR | O_CREAT, …)创建文件函数执行成功如下图所示 从上面的示例可知进程A 和进程B 都会创建出同一个文件同一个文件被创建两次这是不允许的那如何规避这样的问题呢那就是通过使用O_EXCL 标志当open 函数中同时指定了O_EXCL 和 O_CREAT 标志如果要打开的文件已经存在则open 返回错误如果指定的文件不存在则创建这个文件这里就提供了一种机制保证进程是打开文件的创建者将“判断文件是否存在、创建文件”这两个步骤合成为一个原子操作有了原子操作就保证不会出现图3.9.3 中所示的情况。 fcntl 和ioctl 本小节给大家介绍两个新的系统调用fcntl()和ioctl()。 fcntl 函数 fcntl()函数可以对一个已经打开的文件描述符执行一系列控制操作譬如复制一个文件描述符与dup、 dup2 作用相同、获取/设置文件描述符标志、获取/设置文件状态标志等类似于一个多功能文件描述符管理工具箱。fcntl()函数原型如下所示可通过man 2 fcntl命令查看 #include unistd.h #include fcntl.h int fcntl(int fd, int cmd, ... /* arg */ )函数参数和返回值含义如下 fd文件描述符。 cmd操作命令。此参数表示我们将要对fd 进行什么操作cmd 参数支持很多操作命令大家可以打开man 手册查看到这些操作命令的详细介绍这些命令都是以F_XXX 开头的譬如F_DUPFD、F_GETFD、 F_SETFD 等不同的cmd 具有不同的作用cmd 操作命令大致可以分为以下5 种功能 ⚫ 复制文件描述符cmdF_DUPFD 或cmdF_DUPFD_CLOEXEC ⚫ 获取/设置文件描述符标志cmdF_GETFD 或cmdF_SETFD ⚫ 获取/设置文件状态标志cmdF_GETFL 或cmdF_SETFL ⚫ 获取/设置异步IO 所有权cmdF_GETOWN 或cmdF_SETOWN ⚫ 获取/设置记录锁cmdF_GETLK 或cmdF_SETLK 这里列举出来并不需要全部学会每一个cmd 的作用因为有些内容并没有给大家提及到譬如什么异步IO、锁之类的概念在后面的学习过程中当学习到相关知识内容的时候再给大家介绍。 …fcntl 函数是一个可变参函数第三个参数需要根据不同的cmd 来传入对应的实参配合cmd 来使用。 返回值执行失败情况下返回-1并且会设置errno执行成功的情况下其返回值与cmd操作命令有关譬如cmdF_DUPFD复制文件描述符将返回一个新的文件描述符、cmdF_GETFD获取文件描述符标志将返回文件描述符标志、cmdF_GETFL获取文件状态标志将返回文件状态标志等。 fcntl 使用示例 (1)复制文件描述符 前面给大家介绍了dup 和dup2用于复制文件描述符除此之外我们还可以通过fcntl 函数复制文件描述符可用的cmd 包括F_DUPFD 和F_DUPFD_CLOEXEC 这里就只介绍F_DUPFD F_DUPFD_CLOEXEC 暂时先不讲。 当cmdF_DUPFD 时它的作用会根据fd 复制出一个新的文件描述符此时需要传入第三个参数第三个参数用于指出新复制出的文件描述符是一个大于或等于该参数的可用文件描述符没有使用的文件描述符如果第三个参数等于一个已经存在的文件描述符则取一个大于该参数的可用文件描述符。 测试代码如下所示 #include sys/types.h #include sys/stat.h #include fcntl.h #include unistd.h #include stdio.h #include stdlib.h int main(void) {int fd1, fd2;int ret;/* 打开文件test_file */fd1 open(./test_file, O_RDONLY);if (-1 fd1) {perror(open error);exit(-1);}/* 使用fcntl 函数复制一个文件描述符*/fd2 fcntl(fd1, F_DUPFD, 0);if (-1 fd2) {perror(fcntl error);ret -1;goto err;}printf(fd1: %d\nfd2: %d\n, fd1, fd2);ret 0;close(fd2); err:/* 关闭文件*/close(fd1);exit(ret); }在当前目录下存在test_file 文件上述代码会打开此文件得到文件描述符fd1之后再使用fcntl 函数复制fd1 得到新的文件描述符fd2并将fd1 和fd2 打印出来接下来编译运行 可知复制得到的文件描述符是7因为在执行fcntl 函数时传入的第三个参数是0也就时指定复制得到的新文件描述符必须要大于或等于0但是因为0~6 都已经被占用了所以分配得到的fd 就是7如果传入的第三个参数是100那么fd2 就会等于100大家可以自己动手测试。 (2)获取/设置文件状态标志 cmdF_GETFL 可用于获取文件状态标志cmdF_SETFL 可用于设置文件状态标志。cmdF_GETFL 时不需要传入第三个参数返回值成功表示获取到的文件状态标志cmdF_SETFL 时需要传入第三个参数此参数表示需要设置的文件状态标志。 这些标志指的就是我们在调用open 函数时传入的flags 标志可以指定一个或多个通过位或| 运算符组合但是文件权限标志O_RDONLY、O_WRONLY、O_RDWR以及文件创建标志O_CREAT、 O_EXCL、O_NOCTTY、O_TRUNC不能被设置、会被忽略在Linux 系统中只有O_APPEND、O_ASYNC、O_DIRECT、O_NOATIME 以及O_NONBLOCK 这些标志可以被修改这里面有些标志并没有给大家介绍过后面我们在用到的时候再给大家介绍。所以对于一个已经打开的文件描述符可以通过这种方式添加或移除标志。 测试代码如下 #include sys/types.h #include sys/stat.h #include fcntl.h #include unistd.h #include stdio.h #include stdlib.h int main(void) {int fd;int ret;int flag;/* 打开文件test_file */fd open(./test_file, O_RDWR);if (-1 fd) {perror(open error);exit(-1);}/* 获取文件状态标志*/flag fcntl(fd, F_GETFL);if (-1 flag) {perror(fcntl F_GETFL error);ret -1;goto err;}printf(flags: 0x%x\n, flag);/* 设置文件状态标志,添加O_APPEND 标志*/ret fcntl(fd, F_SETFL, flag | O_APPEND);if (-1 ret) {perror(fcntl F_SETFL error);goto err;}ret 0; err:/* 关闭文件*/close(fd);exit(ret); }上述代码会打开test_file 文件得到文件描述符fd之后调用fcntl(fd, F_GETFL)来获取到当前文件状态标志flag并将其打印来接着调用fcntl(fd, F_SETFL, flag | O_APPEND)设置文件状态标志在原标志的基础上添加O_APPEND 标志。接下来编译测试: 以上给大家介绍了fcntl 函数的两种用法除了这两种用法之外还有其它多种不同的用法这里暂时先不介绍了后面学习到相应知识点的时候再给大家讲解。 ioctl 函数 ioctl()可以认为是一个文件IO 操作的杂物箱可以处理的事情非常杂、不统一一般用于操作特殊文件或硬件外设此函数将会在进阶篇中使用到譬如可以通过ioctl 获取LCD 相关信息等本小节只是给大家引出这个系统调用暂时不会用到。此函数原型如下所示可通过man 2 ioctl命令查看 #include sys/ioctl.h int ioctl(int fd, unsigned long request, ...);使用此函数需要包含头文件sys/ioctl.h。 函数参数和返回值含义如下 fd文件描述符。 request此参数与具体要操作的对象有关没有统一值表示向文件描述符请求相应的操作后面用到的时候再给大家介绍。 …此函数是一个可变参函数第三个参数需要根据request 参数来决定配合request 来使用。 返回值成功返回0失败返回-1。 关于ioctl 函数就给大家介绍这么多目的仅仅只是给大家引出这个系统调用我们将会在第二篇进阶篇中给大家细说。 截断文件 使用系统调用truncate()或ftruncate()可将普通文件截断为指定字节长度其函数原型如下所示 #include unistd.h #include sys/types.h int truncate(const char *path, off_t length); int ftruncate(int fd, off_t length);这两个函数的区别在于ftruncate()使用文件描述符fd 来指定目标文件而truncate()则直接使用文件路径path 来指定目标文件其功能一样。 这两个函数都可以对文件进行截断操作将文件截断为参数length 指定的字节长度什么是截断如果文件目前的大小大于参数length 所指定的大小则多余的数据将被丢失类似于多余的部分被“砍”掉了如果文件目前的大小小于参数length 所指定的大小则将其进行扩展对扩展部分进行读取将得到空字节\0。 使用ftruncate()函数进行文件截断操作之前必须调用open()函数打开该文件得到文件描述符并且必须要具有可写权限也就是调用open()打开文件时需要指定O_WRONLY 或O_RDWR。 调用这两个函数并不会导致文件读写位置偏移量发生改变所以截断之后一般需要重新设置文件当前的读写位置偏移量以免由于之前所指向的位置已经不存在而发生错误譬如文件长度变短了文件当前所指向的读写位置已不存在。 调用成功返回0失败将返回-1并设置errno 以指示错误原因。 使用示例 示例代码3.11.1 演示了文件的截断操作分别使用ftruncate()和truncate()将当前目录下的文件file1 截断为长度0、将文件file2 截断为长度1024 个字节。 #include stdio.h #include stdlib.h #include unistd.h #include sys/types.h #include sys/stat.h #include fcntl.h int main(void) {int fd;/* 打开file1 文件*/if (0 (fd open(./file1, O_RDWR))) {perror(open error);exit(-1);}/* 使用ftruncate 将file1 文件截断为长度0 字节*/if (0 ftruncate(fd, 0)) {perror(ftruncate error);exit(-1);}/* 使用truncate 将file2 文件截断为长度1024 字节*/if (0 truncate(./file2, 1024)) {perror(truncate error);exit(-1);}/* 关闭file1 退出程序*/close(fd);exit(0); }上述代码中首先使用open()函数打开文件file1得到文件描述符fd接着使用ftruncate()系统调用将文件截断为0 长度传入file1 文件对应的文件描述符接着调用truncate()系统调用将文件file2 截断为1024 字节长度传入file2 文件的相对路径。 接下来进行测试在当前目录下准备两个文件file1 和file2如下所示 可以看到file1 和file2 文件此时均为592 字节大小接下来运行测试代码 程序运行之后file1 文件大小变成了0而file2 文件大小变成了1024 字节与测试代码想要实现的功能是一致的。 I/O 缓冲 出于速度和效率的考虑系统I/O 调用即文件I/Oopen、read、write 等和标准C 语言库I/O 函数即标准I/O 函数在操作磁盘文件时会对数据进行缓冲本小节将讨论文件I/O 和标准I/O 这两种I/O 方式的数据缓冲问题并讨论其对应用程序性能的影响。 除此之外本小节还讨论了屏蔽或影响缓冲的一些技术手段以及直接I/O 技术—绕过内核缓冲直接访问磁盘硬件。 文件I/O 的内核缓冲 read()和write()系统调用在进行文件读写操作的时候并不会直接访问磁盘设备而是仅仅在用户空间缓冲区和内核缓冲区kernel buffer cache之间复制数据。譬如调用write()函数将5 个字节数据从用户空间内存拷贝到内核空间的缓冲区中 write(fd, Hello, 5); //写入5 个字节数据调用write()后仅仅只是将这5 个字节数据拷贝到了内核空间的缓冲区中拷贝完成之后函数就返回了在后面的某个时刻内核会将其缓冲区中的数据写入刷新到磁盘设备中所以由此可知系统调用write() 与磁盘操作并不是同步的write()函数并不会等待数据真正写入到磁盘之后再返回。如果在此期间其它进程调用read()函数读取该文件的这几个字节数据那么内核将自动从缓冲区中读取这几个字节数据返回给应用程序。 与此同理对于读文件而言亦是如此内核会从磁盘设备中读取文件的数据并存储到内核的缓冲区中当调用read()函数读取数据时read()调用将从内核缓冲区中读取数据直至把缓冲区中的数据读完这时内核会将文件的下一段内容读入到内核缓冲区中进行缓存。 我们把这个内核缓冲区就称为文件I/O 的内核缓冲。这样的设计目的是为了提高文件I/O 的速度和效率使得系统调用read()、write()的操作更为快速不需要等待磁盘操作将数据写入到磁盘或从磁盘读取出数据磁盘操作通常是比较缓慢的。同时这一设计也更为高效减少了内核操作磁盘的次数譬如线程 1 调用write()向文件写入数据abcd线程2 也调用write()向文件写入数据1234这样的话数据abcd和 1234都被缓存在了内核的缓冲区中在稍后内核会将它们一起写入到磁盘中只发起一次磁盘操作请求加入没有内核缓冲区那么每一次调用write()内核就会执行一次磁盘操作。 前面提到当调用write()之后内核稍后会将数据写入到磁盘设备中具体是什么时间点写入到磁盘这个其实是不确定的由内核根据相应的存储算法自动判断。 通过前面的介绍可知文件I/O 的内核缓冲区自然是越大越好Linux 内核本身对内核缓冲区的大小没有固定上限。内核会分配尽可能多的内核来作为文件I/O 的内核缓冲区但受限于物理内存的总量如果系统可用的物理内存越多那自然对应的内核缓冲区也就越大操作越大的文件也要依赖于更大空间的内核缓冲。 刷新文件I/O 的内核缓冲区 强制将文件I/O 内核缓冲区中缓存的数据写入刷新到磁盘设备中对于某些应用程序来说可能是很有必要的例如应用程序在进行某操作之前必须要确保前面步骤调用write()写入到文件的数据已经真正写入到了磁盘中诸如一些数据库的日志进程。 联系到一个实际的使用场景当我们在Ubuntu 系统下拷贝文件到U 盘时文件拷贝完成之后通常在拔掉U 盘之前需要执行sync 命令进行同步操作这个同步操作其实就是将文件I/O 内核缓冲区中的数据更新到U 盘硬件设备所以如果在没有执行sync 命令时拔掉U 盘很可能就会导致拷贝到U 盘中的文件遭到破坏 控制文件I/O 内核缓冲的系统调用 Linux 中提供了一些系统调用可用于控制文件I/O 内核缓冲包括系统调用sync()、syncfs()、fsync()以及fdatasync()。 ㈠、fsync()函数 系统调用fsync()将参数fd 所指文件的内容数据和元数据写入磁盘只有在对磁盘设备的写入操作完成之后fsync()函数才会返回其函数原型如下所示 #include unistd.h int fsync(int fd);参数fd 表示文件描述符函数调用成功将返回0失败返回-1 并设置errno 以指示错误原因。 前面提到了元数据这个概念元数据并不是文件内容本身的数据而是一些用于记录文件属性相关的数据信息譬如文件大小、时间戳、权限等等信息这里统称为文件的元数据这些信息也是存储在磁盘设备中的在3.1 小节中介绍过。 使用示例 #include stdio.h #include stdlib.h #include sys/types.h #include sys/stat.h #include fcntl.h #include unistd.h #define BUF_SIZE 4096 #define READ_FILE ./rfile #define WRITE_FILE ./wfile static char buf[BUF_SIZE]; int main(void) {int rfd, wfd;size_t size;/* 打开源文件*/rfd open(READ_FILE, O_RDONLY);if (0 rfd) {perror(open error);exit(-1);}/* 打开目标文件*/wfd open(WRITE_FILE, O_WRONLY | O_CREAT | O_TRUNC, 0664);if (0 wfd) {perror(open error);exit(-1);}/* 拷贝数据*/while(0 (size read(rfd, buf, BUF_SIZE)))write(wfd, buf, size);/* 对目标文件执行fsync 同步*/fsync(wfd);/* 关闭文件退出程序*/close(rfd);close(wfd);exit(0); }代码没什么好说的主要就是拷贝完成之后调用fsync()函数对目标文件的数据进行了同步操作整个操作完成之后close 关闭源文件和目标文件、退出程序。 ㈡、fdatasync()函数 系统调用fdatasync()与fsync()类似不同之处在于fdatasync()仅将参数fd 所指文件的内容数据写入磁盘并不包括文件的元数据同样只有在对磁盘设备的写入操作完成之后fdatasync()函数才会返回其函数原型如下所示 #include unistd.h int fdatasync(int fd);㈢、sync()函数 系统调用sync()会将所有文件I/O 内核缓冲区中的文件内容数据和元数据全部更新到磁盘设备中该函数没有参数、也无返回值意味着它不是对某一个指定的文件进行数据更新而是刷新所有文件I/O 内核缓冲区。其函数原型如下所示 #include unistd.h void sync(void);在Linux 实现中调用sync()函数仅在所有数据已经写入到磁盘设备之后才会返回然后在其它系统中 sync()实现只是简单调度一下I/O 传递在动作未完成之后即可返回。 控制文件I/O 内核缓冲的标志 调用open()函数时指定一些标志也可以影响到文件I/O 内核缓冲譬如O_DSYNC 标志和O_SYNC 标志这些标志在2.3 小节并未向大家介绍过联系本小节所学内容接下来向大家简单地介绍下。 ㈠、O_DSYNC 标志 在调用open()函数时指定O_DSYNC 标志其效果类似于在每个write()调用之后调用fdatasync()函数进行数据同步。譬如 fd open(filepath, O_WRONLY | O_DSYNC);㈡、O_SYNC 标志 在调用open()函数时指定O_SYNC 标志使得每个write()调用都会自动将文件内容数据和元数据刷新到磁盘设备中其效果类似于在每个write()调用之后调用fsync()函数进行数据同步譬如 fd open(filepath, O_WRONLY | O_SYNC);对性能的影响 在程序中频繁调用fsync()、fdatasync()、sync()或者调用open 时指定O_DSYNC 或O_SYNC 标志对性能的影响极大大部分的应用程序是没有这种需求的所以在大部分应用程序当中基本不会使用到。 直接I/O绕过内核缓冲 从Linux 内核2.4 版本开始Linux 允许应用程序在执行文件I/O 操作时绕过内核缓冲区从用户空间直接将数据传递到文件或磁盘设备把这种操作也称为直接I/Odirect I/O或裸I/Oraw I/O。 在有些情况下这种操作通常是很有必要的例如某应用程序的作用是测试磁盘设备的读写速率那么在这种应用需要下我们就需要保证read/write 操作是直接访问磁盘设备而不经过内核缓冲如果不能得到这样的保证必然会导致测试结果出现比较大的误差。 然后对于大多数应用程序而言使用直接I/O 可能会大大降低性能这是因为为了提高I/O 性能内核针对文件I/O 内核缓冲区做了不少的优化譬如包括按顺序预读取、在成簇磁盘块上执行I/O、允许访问同一文件的多个进程共享高速缓存的缓冲区。如果应用程序使用直接I/O 方式将无法享受到这些优化措施所带来的性能上的提升直接I/O 只在一些特定的需求场合譬如磁盘速率测试工具、数据库系统等。 我们可针对某一文件或块设备执行直接I/O要做到这一点需要在调用open()函数打开文件时指定 O_DIRECT 标志该标志至Linux 内核2.4.10 版本开始生效譬如 fd open(filepath, O_WRONLY | O_DIRECT);直接I/O 的对齐限制 因为直接I/O 涉及到对磁盘设备的直接访问所以在执行直接I/O 时必须要遵守以下三个对齐限制要求 ⚫ 应用程序中用于存放数据的缓冲区其内存起始地址必须以块大小的整数倍进行对齐 ⚫ 写文件时文件的位置偏移量必须是块大小的整数倍 ⚫ 写入到文件的数据大小必须是块大小的整数倍。 如果不满足以上任何一个要求调用write()均为以错误返回Invalid argument。以上所说的块大小指的是磁盘设备的物理块大小block size常见的块大小包括512 字节、1024 字节、2048 以及4096 字节那我们如何确定磁盘分区的块大小呢可以使用tune2fs 命令进行查看如下所示 tune2fs -l /dev/sda1 | grep Block size-l 后面指定了需要查看的磁盘分区可以使用df -h 命令查看Ubuntu 系统的根文件系统所挂载的磁盘分区 通过上图可知Ubuntu 系统的根文件系统挂载在/dev/sda1 磁盘分区下接着下使用tune2fs 命令查看该分区的块大小 从上图可知/dev/sda1 磁盘分区的块大小为4096 个字节。 直接I/O 测试与普通I/O 对比测试 接下来编写一个使用直接I/O 方式写文件的测试程序和一个使用普通I/O 方式写文件的测试程序进行对比。 演示了以直接I/O 方式写文件的操作首先我们需要在程序开头处定义一个宏定义 _GNU_SOURCE原因在于后面open()函数需要指定O_DIRECT 标志这个宏需要我们在程序中定义了 O_DIRECT 宏之后才能使用否则编译程序就会报错提示O_DIRECT 未定义。 Tips_GNU_SOURCE 宏可用于开启/禁用Linux 系统调用和glibc 库函数的一些功能、特性要打开这些特性需要在应用程序中定义该宏定义该宏之后意味着用户应用程序打开了所有的特性默认情况下_GNU_SOURCE 宏并没有被定义所以当使用到它控制的一些特性时应用程序编译将会报错定义该宏的方式有两种 ⚫ 直接在源文件中定义#define _GNU_SOURCE ⚫ gcc 编译时使用-D 选项定义_GNU_SOURCE 宏 gcc -D_GNU_SOURCE -o testApp testApp.cgcc 的-D 选项可用于定义一个宏并且该宏定义在整个源码工程中都是生效的是一个全局宏定义。使用以上哪种方式都可以。 /** 使用宏定义O_DIRECT 需要在程序中定义宏_GNU_SOURCE** 不然提示O_DIRECT 找不到**/ #define _GNU_SOURCE #include stdio.h #include stdlib.h #include sys/types.h #include sys/stat.h #include fcntl.h #include unistd.h /** 定义一个用于存放数据的buf起始地址以4096 字节进行对其**/ static char buf[8192] __attribute((aligned (4096))); int main(void) {int fd;int count;/* 打开文件*/fd open(./test_file,O_WRONLY | O_CREAT | O_TRUNC | O_DIRECT,0664);if (0 fd) {perror(open error);exit(-1);}/* 写文件*/count 10000;while(count--) {if (4096 ! write(fd, buf, 4096)) {perror(write error);exit(-1);}}/* 关闭文件退出程序*/close(fd);exit(0); }前面提到过使用直接I/O 方式需要满足3 个对齐要求程序中定义了一个static 静态数组buf将其作为数据存放的缓冲区在变量定义后加了__attribute((aligned (4096)))修饰使其起始地址以4096 字节进行对其。 Tipsattribute 是gcc 支持的一种机制也可以写成__attribute可用于设置函数属性、变量属性以及类型属性等对此不了解的读者请自行查找资料学习本书不会对此进行介绍 程序中调用open()函数是指定了O_DIRECT 标志使用直接I/O最后通过while 循环将数据写入文件中循环10000 次每次写入4096 个字节数据也就是总共写入4096*10000 个字节约等于40MB。首次调用write()时其文件读写位置偏移量为0之后均以4096 字节进行递增所以满足直接I/O 方式的位置偏移量必须是块大小的整数倍这个要求每次写入大小均是4096 字节所以满足了数据大小必须是块大小的整数倍这个要求。 接下来编译测试 通过time 命令测试可知每次执行程序需要花费2.7 秒左右的时间使用直接I/O 方式向文件写入约 40MB 数据大小。 Tips对于直接I/O 方式的3 个对齐限制大家可以自行进行验证譬如修改上述示例代码使之不满足 3 个对齐条件种的任何一个然后编译程序进行测试会发生write()函数会报错均是“Invalid argument”错误。 对示例代码4.9.2 进行修改使其变成普通I/O 方式其它功能相同最终修改后的示例代码如下所示 #include stdio.h #include stdlib.h #include sys/types.h #include sys/stat.h #include fcntl.h #include unistd.h static char buf[8192]; int main(void) {int fd;int count;/* 打开文件*/fd open(./test_file, O_WRONLY | O_CREAT | O_TRUNC, 0664);if (0 fd) {perror(open error);exit(-1);}/* 写文件*/count 10000;while(count--) {//循环10000 次每次写入4096 个字节数据if (4096 ! write(fd, buf, 4096)) {perror(write error);exit(-1);}}/* 关闭文件退出程序*/close(fd);exit(0); }再次进行测试 使用time 命令得到的程序运行时间大约是0.13~0.14 秒左右相比直接I/O 方式的2.7 秒时间上提升了20 倍左右测试大小不同、每次写入的大小不同均会导致时间上的差别原因在于直接I/O 方式每次write()调用均是直接对磁盘发起了写操作而普通方式只是将用户空间下的数据拷贝到了文件I/O 内核缓冲区中并没直接操作硬件所以消耗的时间短硬件操作占用的时间远比内存复制占用的时间大得多 直接I/O 方式效率、性能比较低绝大部分应用程序不会使用直接I/O 方式对文件进行I/O 操作通常只在一些特殊的应用场合下才可能会使用那我们可以使用直接I/O 方式来测试磁盘设备的读写速率这种测试方式相比普通I/O 方式就会更加准确。 stdio 缓冲 介绍完文件I/O 的内核缓冲后接下来我们聊一聊标准I/O 的stdio 缓冲。 标准I/Ofopen、fread、fwrite、fclose、fseek 等是C 语言标准库函数而文件I/Oopen、read、write、 close、lseek 等是系统调用虽然标准I/O 是在文件I/O 基础上进行封装而实现譬如fopen 内部实际上调用了open、fread 内部调用了read 等但在效率、性能上标准I/O 要优于文件I/O其原因在于标准I/O 实现维护了自己的缓冲区我们把这个缓冲区称为stdio 缓冲区接下来我们聊一聊标准I/O 的stdio 缓冲。 前面提到了文件I/O 内核缓冲这是由内核维护的缓冲区而标准I/O 所维护的stdio 缓冲是用户空间的缓冲区当应用程序中通过标准I/O 操作磁盘文件时为了减少调用系统调用的次数标准I/O 函数会将用户写入或读取文件的数据缓存在stdio 缓冲区然后再一次性将stdio 缓冲区中缓存的数据通过调用系统调用I/O文件I/O写入到文件I/O 内核缓冲区或者拷贝到应用程序的buf 中。 通过这样的优化操作当操作磁盘文件时在用户空间缓存大块数据以减少调用系统调用的次数使得效率、性能得到优化。使用标准I/O 可以使编程者免于自行处理对数据的缓冲无论是调用write()写入数据、还是调用read()读取数据。 对stdio 缓冲进行设置 C 语言提供了一些库函数可用于对标准I/O 的stdio 缓冲区进行相关的一些设置包括setbuf()、setbuffer() 以及setvbuf()。 ㈠、setvbuf()函数 调用setvbuf()库函数可以对文件的stdio 缓冲区进行设置譬如缓冲区的缓冲模式、缓冲区的大小、起始地址等。其函数原型如下所示 #include stdio.h int setvbuf(FILE *stream, char *buf, int mode, size_t size);使用该函数需要包含头文件stdio.h。 函数参数和返回值含义如下 streamFILE 指针用于指定对应的文件每一个文件都可以设置它对应的stdio 缓冲区。 buf如果参数buf 不为NULL那么buf 指向size 大小的内存区域将作为该文件的stdio 缓冲区因为 stdio 库会使用buf 指向的缓冲区所以应该以动态分配在堆内存譬如malloc在7.6 小节介绍或静态的方式在堆中为该缓冲区分配一块空间而不是分配在栈上的函数内的自动变量局部变量。如果buf 等于NULL那么stdio 库会自动分配一块空间作为该文件的stdio 缓冲区除非参数mode 配置为非缓冲模式。 mode参数mode 用于指定缓冲区的缓冲类型可取值如下 ⚫ _IONBF不对I/O 进行缓冲无缓冲。意味着每个标准I/O 函数将立即调用write()或者read()并且忽略buf 和size 参数可以分别指定两个参数为NULL 和0。标准错误stderr 默认属于这一种类型从而保证错误信息能够立即输出。 ⚫ _IOLBF采用行缓冲I/O。在这种情况下当在输入或输出中遇到换行符\n时标准I/O 才会执行文件I/O 操作。对于输出流在输出一个换行符前将数据缓存除非缓冲区已经被填满当输出换行符时再将这一行数据通过文件I/O write()函数刷入到内核缓冲区中对于输入流每次读取一行数据。对于终端设备默认采用的就是行缓冲模式譬如标准输入和标准输出。 ⚫ _IOFBF采用全缓冲I/O。在这种情况下在填满stdio 缓冲区后才进行文件I/O 操作read、write。对于输出流当fwrite 写入文件的数据填满缓冲区时才调用write()将stdio 缓冲区中的数据刷入内核缓冲区对于输入流每次读取stdio 缓冲区大小个字节数据。默认普通磁盘上的常规文件默认常用这种缓冲模式。 size指定缓冲区的大小。 返回值成功返回0失败将返回一个非0 值并且会设置errno 来指示错误原因。 需要注意的是当stdio 缓冲区中的数据被刷入到内核缓冲区或被读取之后这些数据就不会存在于缓冲区中了数据被刷入了内核缓冲区或被读走了。 ㈡、setbuf()函数 setbuf()函数构建与setvbuf()之上执行类似的任务其函数原型如下所示 #include stdio.h void setbuf(FILE *stream, char *buf);setbuf()调用除了不返回函数结果void外就相当于 setvbuf(stream, buf, buf ? _IOFBF : _IONBF, BUFSIZ); 要么将buf 设置为NULL 以表示无缓冲要么指向由调用者分配的BUFSIZ 个字节大小的缓冲区BUFSIZ 定义于头文件stdio.h中该值通常为8192。 ㈢、setbuffer()函数 setbuffer()函数类似于setbuf()但允许调用者指定buf 缓冲区的大小其函数原型如下所示 #include stdio.h void setbuffer(FILE *stream, char *buf, size_t size);setbuffer()调用除了不返回函数结果void外就相当于 setvbuf(stream, buf, buf ? _IOFBF : _IONBF, size);关于标准I/O 库stdio 缓冲相关的内容就给大家介绍这么多接下来我们进行一些测试来说明无缓冲、行缓冲以及全缓冲区之间的区别。 标准输出printf()的行缓冲模式测试 我们先看看下面这个简单地示例代码调用了printf()函数区别在于第二个printf()没有输出换行符。 #include stdio.h #include stdlib.h #include unistd.h int main(void) {printf(Hello World!\n);printf(Hello World!);for ( ; ; )sleep(1); }printf()函数是标准I/O 库函数向终端设备标准输出输出打印信息编译测试 运行之后可以发现只有第一个printf()打印的信息显示出来了第二个并没有显示出来这是为什么呢这就是stdio 缓冲的问题前面提到了标准输出默认采用的是行缓冲模式printf()输出的字符串写入到了标准输出的stdio 缓冲区中只有输出换行符时不考虑缓冲区填满的情况才会将这一行数据刷入到内核缓冲区也就是写入标准输出文件终端设备因为第一个printf()包含了换行符所以已经刷入了内核缓冲区而第二个printf 并没有包含换行符所以第二个printf 输出的Hello World!还缓存在stdio 缓冲区中需要等待一个换行符才可输出到终端。 联系4.8.2 小节介绍的格式化输入scanf()函数程序中调用scanf()函数进行阻塞用户通过键盘输入数据只有在按下回车键换行符键时程序才会接着往下执行因为标准输入默认也是采用了行缓冲模式。 譬如对示例代码4.9.4 进行修改使标准输出变成无缓冲模式修改后代码如下所示 #include stdio.h #include stdlib.h #include unistd.h int main(void) {/* 将标准输出设置为无缓冲模式*/if (setvbuf(stdout, NULL, _IONBF, 0)) {perror(setvbuf error);exit(0);}printf(Hello World!\n);printf(Hello World!);for ( ; ; )sleep(1); }在使用printf()之前调用setvbuf()函数将标准输出的stdio 缓冲设置为无缓冲模式接着编译运行 可以发现该程序却能够成功输出两个“Hello World!”并且白色的光标在第二个“Hello World!”后面意味着输出没有换行与程序中第二个printf 没有加换行符的效果是一直。 所以通过以上两个示例代码对比可知标准输出默认是行缓冲模式只有输出了换行符时才会将换行符这一行字符进行输出显示也就是刷入到内核缓冲区在没有输出换行符之前会将数据缓存在stdio 缓冲区中。 fflush刷新stdio缓冲区 无论我们采取何种缓冲模式在任何时候都可以使用库函数fflush()来强制刷新将输出到stdio 缓冲区中的数据写入到内核缓冲区通过write()函数stdio 缓冲区该函数会刷新指定文件的stdio 输出缓冲区此函数原型如下所示 #include stdio.h int fflush(FILE *stream);参数stream 指定需要进行强制刷新的文件如果该参数设置为NULL则表示刷新所有的stdio 缓冲区。 函数调用成功返回0否则将返回-1并设置errno 以指示错误原因。 接下来我们对示例代码4.9.4 进行修改在第二个printf 后面调用fflush()函数修改后示例代码如下所示 #include stdio.h #include stdlib.h #include unistd.h int main(void) {printf(Hello World!\n);printf(Hello World!);fflush(stdout); //刷新标准输出stdio 缓冲区for ( ; ; )sleep(1); }运行测试 可以看到打印了两次“Hello World!”这就是fflush()的作用了强制刷新stdio 缓冲区。 除了使用库函数fflush()之外还有其它方法会自动刷新stdio 缓冲区吗是的使用库函数fflush()是一种强制刷新的手段在一些其它的情况下也会自动刷新stdio 缓冲区譬如当文件关闭时、程序退出时接下来我们进行演示。 ㈠、关闭文件时刷新stdio 缓冲区 同样还是直接对示例代码4.9.4 进行修改在调用第二个printf 函数后关闭标准输出如下所示 #include stdio.h #include stdlib.h #include unistd.h int main(void) {printf(Hello World!\n);printf(Hello World!);fclose(stdout); //关闭标准输出for ( ; ; )sleep(1); }至于运行结果文档中就不贴出来了运行结果与图4.9.7 是一样的。所以由此可知文件关闭时系统会自动刷新该文件的stdio 缓冲区。 ㈡、程序退出时刷新stdio 缓冲区 可以看到上面使用的测试程序中在最后都使用了一个for 死循环让程序处于休眠状态无法退出为什么要这样做呢原因在于程序退出时也会自动刷新stdio 缓冲区这样的话就会影响到测试结果。同样对 示例代码4.9.4 进行修改去掉for 死循环让程序结束修改完之后如下所示 #include stdio.h #include stdlib.h #include unistd.h int main(void) {printf(Hello World!\n);printf(Hello World!); }运行结果如下 从结果可知当程序退出时确实会自动刷新stdio 缓冲区。但是与程序退出方式有关如果使用exit()、 return 或像上述示例代码一样不显式调用相关函数或执行return 语句来结束程序这些情况下程序终止时会自动刷新stdio 缓冲区如果使用_exit 或_Exit()终止程序则不会刷新这里各位读者可以自行测试、验证。 关于刷新stdio 缓冲区相关内容最后进行一个总结 ⚫ 调用fflush()库函数可强制刷新指定文件的stdio 缓冲区 ⚫ 调用fclose()关闭文件时会自动刷新文件的stdio 缓冲区 ⚫ 程序退出时会自动刷新stdio 缓冲区注意区分不同的情况。 关于本小节内容就给大家介绍这么多笔者觉得已经非常详细了如果还有不太理解的地方希望大家能够自己动手进行测试、验证然后总结出相应的结论前面笔者一直强调编程是一门实践性很强的工作一定要学会自己分析、验证。 I/O 缓冲小节 本小节对前面学习的内容进行一个简单地总结概括说明文件I/O 内核缓冲区和stdio 缓冲区之间的联系与区别以及各种stdio 库函数如下图所示 从图中自上而下首先应用程序调用标准I/O 库函数将用户数据写入到stdio 缓冲区中stdio 缓冲区是由stdio 库所维护的用户空间缓冲区。针对不同的缓冲模式当满足条件时stdio 库会调用文件I/O系统调用I/O将stdio 缓冲区中缓存的数据写入到内核缓冲区中内核缓冲区位于内核空间。最终由内核向磁盘设备发起读写操作将内核缓冲区中的数据写入到磁盘或者从磁盘设备读取数据到内核缓冲区。 应用程序调用库函数可以对stdio 缓冲区进行相应的设置设置缓冲区缓冲模式、缓冲区大小以及由调用者指定一块空间作为stdio 缓冲区并且可以强制调用fflush()函数刷新缓冲区而对于内核缓冲区来说应用程序可以调用相关系统调用对内核缓冲区进行控制譬如调用fsync()、fdatasync()或sync()来刷新内核缓冲区或通过open 指定O_SYNC 或O_DSYNC 标志或者使用直接I/O 绕过内核缓冲区open 函数指定O_DIRECT 标志。 文件描述符与FILE 指针互转 在应用程序中在同一个文件上执行I/O 操作时还可以将文件I/O系统调用I/O与标准I/O 混合使用这个时候我们就需要将文件描述符和FILE 指针对象之间进行转换此时可以借助于库函数fdopen()、 fileno()来完成。 库函数fileno()可以将标准I/O 中使用的FILE 指针转换为文件I/O 中所使用的文件描述符而fdopen() 则进行着相反的操作其函数原型如下所示 #include stdio.h int fileno(FILE *stream);FILE *fdopen(int fd, const char *mode);首先使用这两个函数需要包含头文件stdio.h。 对于fileno()函数来说根据传入的FILE 指针得到整数文件描述符通过返回值得到文件描述符如果转换错误将返回-1并且会设置errno 来指示错误原因。得到文件描述符之后便可以使用诸如read()、write()、 lseek()、fcntl()等文件I/O 方式操作文件。 fdopen()函数与fileno()功能相反给定一个文件描述符得到该文件对应的FILE 指针之后便可以使用诸如fread()、fwrite()等标准I/O 方式操作文件了。参数mode 与fopen()函数中的mode 参数含义相同具体参考表4.4.1 中所述若该参数与文件描述符fd 的访问模式不一致则会导致调用fdopen()失败。 当混合使用文件I/O 和标准I/O 时需要特别注意缓冲的问题文件I/O 会直接将数据写入到内核缓冲区进行高速缓存而标准I/O 则会将数据写入到stdio 缓冲区之后再调用write()将stdio 缓冲区中的数据写入到内核缓冲区。譬如下面这段代码 #include stdio.h #include stdlib.h #include unistd.h int main(void) {printf(print);write(STDOUT_FILENO, write\n, 6);exit(0); }执行结果你会发现先输出了write字符串信息接着再输出了print字符串信息产生这个问题的原因很简单大家自己去思考下 检查或复位状态 调用fread()读取数据时如果返回值小于参数nmemb 所指定的值表示发生了错误或者已经到了文件末尾文件结束end-of-file但fread()无法具体确定是哪一种情况在这种情况下可以通过判断错误标志或end-of-file 标志来确定具体的情况。 feof()函数 库函数feof()用于测试参数stream 所指文件的end-of-file 标志如果end-of-file 标志被设置了则调用 feof()函数将返回一个非零值如果end-of-file 标志没有被设置则返回0。 其函数原型如下所示 #include stdio.h int feof(FILE *stream);当文件的读写位置移动到了文件末尾时end-of-file 标志将会被设置。 if (feof(file)) {/* 到达文件末尾*/ } else {/* 未到达文件末尾*/ }ferror()函数 库函数ferror()用于测试参数stream 所指文件的错误标志如果错误标志被设置了则调用ferror()函数将返回一个非零值如果错误标志没有被设置则返回0。 其函数原型如下所示 #include stdio.h int ferror(FILE *stream);当对文件的I/O 操作发生错误时错误标志将会被设置。 if (ferror(file)) {/* 发生错误*/ } else {/* 未发生错误*/ }clearerr()函数 库函数clearerr()用于清除end-of-file 标志和错误标志当调用feof()或ferror()校验这些标志后通常需要清除这些标志避免下次校验时使用到的是上一次设置的值此时可以手动调用clearerr()函数清除标志。 clearerr()函数原型如下所示 #include stdio.h void clearerr(FILE *stream);此函数没有返回值调用将总是会成功 对于end-of-file 标志除了使用clearerr()显式清除之外当调用fseek()成功时也会清除文件的end-of-file 标志。 使用示例 #include stdio.h #include stdlib.h int main(void) {FILE *fp NULL;char buf[20] {0};/* 打开文件*/if (NULL (fp fopen(./testApp.c, r))) {perror(fopen error);exit(-1);}printf(文件打开成功!\n);/* 将读写位置移动到文件末尾*/if (0 fseek(fp, 0, SEEK_END)) {perror(fseek error);fclose(fp);exit(-1);}/* 读文件*/if (10 fread(buf, 1, 10, fp)) {if (feof(fp))printf(end-of-file 标志被设置,已到文件末尾!\n);clearerr(fp); //清除标志}/* 关闭文件*/fclose(fp);exit(0); }
http://www.dnsts.com.cn/news/120343.html

相关文章:

  • 做设计找图片的网站建设部网站电话
  • 戴尔公司网站设计特色网站备案多少岁可以做
  • 怎么用ftp工具上传网站源码成品直播源码
  • 电子商务网站建设训练总结wordpress图片播放
  • 移动服务器建设的电影网站用哪个网站做简历更好
  • 网站 标准自考网页设计素材
  • 阜阳网站建设阜阳长沙网站推广工具
  • 网站制作优势友情链接检索数据分析
  • 泌阳县住房和城乡建设局网站哈尔滨市建筑企业管理站
  • 设计网站建设价格老鹰主机 wordpress
  • 网站排名分析 用户需求怎么看公司是不是外包
  • 网站建设分工表郑州建设网站报价
  • h5页面网站模板做网站的得花多钱
  • 接做网站私活网站开发流程 ppt
  • 河北高端网站制作什么作为国内的主要门户网站
  • 信阳市商务局网站加油站建设公告网络商城如何推广
  • 里水九江网站建设东莞洪梅网站建设
  • 动态背景设置网站江苏工程建设信息官方网站
  • 企业网站推广建设dw网站站点正确建设方式
  • it网站制作策划上海建设银行官网网站6
  • seo网站分析报告渭南网站建设网站排名优化
  • 邯郸网站设计建设网站 尺寸
  • 哈尔滨免费建站模板曹县住房和城乡建设部网站
  • 成都分销网站建设大学生健康咨询网站建设方案
  • 网站建设的价网站开发树形图
  • js 网站制作济南软月建站
  • 建网站卖广告装修图片
  • jsp网站 值班设计ui
  • 南昌专业做网站站群网站
  • 那个网站专门做二手衣服电子商务公司注册资金最低多少