深入探究文件I/O

Posted 行稳方能走远

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了深入探究文件I/O相关的知识,希望对你有一定的参考价值。

目录

经过上一章内容的学习,相信各位读者对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 table(inode 表),inode table 中存放的是一个一个的inode(也成为inode
节点),不同的inode 就可以表示不同的文件,每一个文件都必须对应一个inode,inode 实质上是一个结构体,这个结构体中有很多的元素,不同的元素记录了文件了不同信息,譬如文件字节大小、文件所有者、文件对应的读/写/执行权限、文件时间戳(创建时间、更新时间等)、文件类型、文件数据存储的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 盘,其中的数据是可以被找回来的。
通过以上介绍可知,打开一个文件,系统内部会将这个过程分为三步:

  1. 系统找到这个文件名所对应的inode 编号;
  2. 通过inode 编号从inode table 中找到对应的inode 结构体;
  3. 根据inode 结构体中记录的信息,确定文件数据所在的block,并读出数据。

文件打开时的状态

当我们调用open 函数去打开文件的时候,内核会申请一段内存(一段缓冲区),并且将静态文件的数据内容从磁盘这些存储设备中读取到内存中进行管理、缓存(也把内存中的这份文件数据叫做动态文件、内核缓冲区)。打开文件后,以后对这个文件的读写操作,都是针对内存中这一份动态文件进行相关的操作,而并不是针对磁盘中存放的静态文件。
当我们对动态文件进行读写操作后,此时内存中的动态文件和磁盘设备中的静态文件就不同步了,数据的同步工作由内核完成,内核会在之后将内存这份动态文件更新(同步)到磁盘设备中。由此我们也可以联系到实际操作中,譬如说:
⚫ 打开一个大文件的时候会比较慢;
⚫ 文档写了一半,没记得保存,此时电脑因为突然停电直接掉电关机了,当重启电脑后,打开编写的文档,发现之前写的内容已经丢失。
想必各位读者在工作当中都遇到过这种问题吧,通过上面的介绍,就解释了为什么会出现这种问题。好,我们再来说一下,为什么要这样设计?
因为磁盘、硬盘、U 盘等存储设备基本都是Flash 块设备,因为块设备硬件本身有读写限制等特征,块设备是以一块一块为单位进行读写的(一个块包含多个扇区,而一个扇区包含多个字节),一个字节的改动也需要将该字节所在的block 全部读取出来进行修改,修改完成之后再写入块设备中,所以导致对块设备的读写操作非常不灵活;而内存可以按字节为单位来操作,而且可以随机操作任意地址数据,非常地很灵活,所以对于操作系统来说,会先将磁盘中的静态文件读取到内存中进行缓存,读写操作都是针对这份动态文件,而不是直接去操作磁盘中的静态文件,不但操作不灵活,效率也会下降很多,因为内存的读写速率远比磁盘读写快得多。
在Linux 系统中,内核会为每个进程(关于进程的概念,这是后面的内容,我们可以简单地理解为一个运行的程序就是一个进程,运行了多个程序那就是存在多个进程)设置一个专门的数据结构用于管理该进程,譬如用于记录进程的状态信息、运行特征等,我们把这个称为进程控制块(Process control block,缩写
PCB)。
PCB 数据结构体中有一个指针指向了文件描述符表(File descriptors),文件描述符表中的每一个元素索引到对应的文件表(File table),文件表也是一个数据结构体,其中记录了很多文件相关的信息,譬如文件状态标志、引用计数、当前文件的读写偏移量以及i-node 指针(指向该文件对应的inode)等,进程打开的所有文件对应的文件描述符都记录在文件描述符表中,每一个文件描述符都会指向一个对应的文件表,其示意图如下所示:

前面给大家介绍了inode,inode 数据结构体中的元素会记录该文件的数据存储的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.h>

char *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.h>

void 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 命令查看空洞文件时,其大小显示为4K,du 命令查看到的大小是文件实际占用存储块的大小。
本小节内容就讲解完了,最后再向各位抛出一个问题:若使用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 两个标志时会有什么作用?

多次打

以上是关于深入探究文件I/O的主要内容,如果未能解决你的问题,请参考以下文章

深入探究文件I/O

深入探究文件I/O

深入探究文件I/O

Linux_Unix系统编程chapter5 深入探究文件IO

File.delete有些文件不能删除,而Files.delete(path)可以,进行深入探究其原因

Nodejs cluster模块深入探究