本章将深入探究文件 I/O 中涉及到的一些问题、原理以及所对应的解决方法,譬如 Linux 系统下文件是如何进行管理的、调用函数返回错误该如何处理、 open 函数的 O_APPEND、 O_TRUNC 标志以及等相关问题。
1 Linux 系统如何管理文件
1.1静态文件与indoe
文件在没有被打开的情况下一般都是存放在磁盘中的,譬如电脑硬盘、移动硬盘、 U 盘等外部存储设备,文件存放在磁盘文件系统中,并且以一种固定的形式进行存放,我们把他们称为静态文件。
文件储存在硬盘上,硬盘的最小存储单位叫做“扇区”(Sector),每个扇区储存 512 字节(相当于 0.5KB),操作系统读取硬盘的时候,不会一个个扇区地读取,这样效率太低,而是一次性连续读取多个扇区,即一次性读取一个“块”(block)。这种由多个扇区组成的“块”,是文件存取的最小单位。“块”的大小,最常见的是 4KB,即连续八个 sector 组成一个 block。
所以由此可以知道,静态文件对应的数据都是存储在磁盘设备不同的“块”中,那么问题来了,我们在程序中调用 open 函数是如何找到对应文件的数据存储“块”的呢,难道仅仅通过指定的文件路径就可以实现?这里我们就来简单地聊一聊这内部实现的过程。
我们的磁盘在进行分区、格式化的时候会将其分为两个区域,一个是数据区,用于存储文件中的数据;另一个是 inode 区,用于存放 inode table(inode 表), inode table 中存放的是一个一个的 inode(也成为 inode节点),不同的 inode 就可以表示不同的文件,每一个文件都必须对应一个 inode, inode 实质上是一个结构体,这个结构体中有很多的元素,不同的元素记录了文件了不同信息,譬如文件字节大小、文件所有者、文件对应的读/写/执行权限、文件时间戳(创建时间、更新时间等)、文件类型、文件数据存储的 block(块)位置等等信息,如图中所示(这里需要注意的是,文件名并不是记录在 inode 中)。
所以由此可知, inode table 表本身也需要占用磁盘的存储空间。每一个文件都有唯一的一个 inode,每一个 inode 都有一个与之相对应的数字编号,通过这个数字编号就可以找到 inode table 中所对应的 inode。在 Linux 系统下,我们可以通过”ls -i”命令查看文件的 inode 编号,如下所示:
上图中 ls 打印出来的信息中,每一行前面的一个数字就表示了对应文件的 inode 编号。除此之外,还可以使用 stat 命令查看,用法如下:
通过以上介绍可知,打开一个文件,系统内部会将这个过程分为三步:
系统找到这个文件名所对应的 inode 编号;
通过 inode 编号从 inode table 中找到对应的 inode结构体;
根据 inode 结构体中记录的信息,确定文件数据所在的 block,并读出数据。
1.2文件打开时的状态
当我们调用 open 函数去打开文件的时候,内核会申请一段内存(一段缓冲区),并且将静态文件的数据内容从磁盘这些存储设备中读取到内存中进行管理、缓存(也把内存中的这份文件数据叫做动态文件、内核缓冲区)。打开文件后,以后对这个文件的读写操作,都是针对内存中这一份动态文件进行相关的操作,而并不是针对磁盘中存放的静态文件。
当我们对动态文件进行读写操作后,此时内存中的动态文件和磁盘设备中的静态文件就不同步了,数据的同步工作由内核完成,内核会在之后将内存这份动态文件更新(同步)到磁盘设备中。由此我们也可以联系到实际操作中,譬如说:
- 打开一个大文件的时候会比较慢;
- 文档写了一半,没记得保存,此时电脑因为突然停电直接掉电关机了,当重启电脑后,打开编写的文档,发现之前写的内容已经丢失。
因为磁盘、硬盘、 U 盘等存储设备基本都是 Flash 块设备,因为块设备硬件本身有读写限制等特征,块设备是以一块一块为单位进行读写的(一个块包含多个扇区,而一个扇区包含多个字节),一个字节的改动也需要将该字节所在的 block 全部读取出来进行修改,修改完成之后再写入块设备中,所以导致对块设备的读写操作非常不灵活;而内存可以按字节为单位来操作,而且可以随机操作任意地址数据,非常地很灵活,所以对于操作系统来说,会先将磁盘中的静态文件读取到内存中进行缓存,读写操作都是针对这份动态文件,而不是直接去操作磁盘中的静态文件,因为内存的读写速率远比磁盘读写快得多。
在 Linux 系统中,内核会为每个进程设置一个专门的数据结构用于管理该进程,譬如用于记录进程的状态信息、运行特征等,我们把这个称为进程控制块(Process control block,缩写PCB)。
PCB 数据结构体中有一个指针指向了文件描述符表(File descriptors),文件描述符表中的每一个元素索引到对应的文件表(File table),文件表也是一个数据结构体,其中记录了很多文件相关的信息,譬如文件状态标志、引用计数、当前文件的读写偏移量以及 i-node 指针(指向该文件对应的 inode)等,进程打开的所有文件对应的文件描述符都记录在文件描述符表中,每一个文件描述符都会指向一个对应的文件表,其示意图如下所示:
前面给大家介绍了 inode, inode 数据结构体中的元素会记录该文件的数据存储的 block(块),也就是说可以通过 inode 找到文件数据存在在磁盘设备中的那个位置,从而把文件数据读取出来。
2 返回错误处理与errno
在 Linux 系统下对常见的错误做了一个编号,每一个编号都代表着每一种不同的错误类型,当函数执行发生错误的时候,操作系统会将这个错误所对应的编号赋值给 errno 变量,每一个进程(程序)都维护了自己的 errno 变量,它是程序中的全局变量,该变量用于存储就近发生的函数执行错误编号,也就意味着下一次的错误码会覆盖上一次的错误码。所以由此可知道,当程序中调用函数发生错误的时候,操作系统内部会通过设置程序的 errno 变量来告知调用者究竟发生了什么错误!
errno 本质上是一个 int 类型的变量,用于存储错误编号,但是需要注意的是,并不是执行所有的系统调用或 C 库函数出错时,操作系统都会设置 errno,那我们如何确定一个函数出错时系统是否会设置 errno 呢?其实这个通过 man 手册便可以查到
当函数返回错误时会设置 errno,当然这里是以 open 函数为例,其它的系统调用也可以这样查找。
在我们的程序当中如何去获取系统所维护的这个errno变量呢?只需要在我们程序当中包含<errno.h>头文件即可,你可以直接认为此变量就是在<errno.h>头文件中的申明的。
1
2
3
4
5
6
7
8
int main(void)
{
printf("%d\n", errno);
return 0;
}
2.1strerror函数
前面给大家说到了 errno 变量,但是 errno 仅仅只是一个错误编号,对于开发者来说,即使拿到了 errno也不知道错误为何?还需要对比源码中对此编号的错误定义,可以说非常不友好,这里介绍一个 C 库函数strerror(),该函数可以将对应的 errno 转换成适合我们查看的字符串信息,其函数原型如下所示(可通过”man 3 strerror”命令查看,注意此函数是 C 库函数,并不是系统调用):
1
2
char *strerror(int errnum);
errnum: 错误编号 errno。
返回值: 对应错误编号的字符串描述信息。
2.2perror函数
除了 strerror 函数之外,我们还可以使用 perror 函数来查看错误信息,一般用的最多的还是这个函数,调用此函数不需要传入 errno,函数内部会自己去获取 errno 变量的值,调用此函数会直接将错误提示字符串打印出来,而不是返回字符串,除此之外还可以在输出的错误提示字符串之前加入自己的打印信息,函数原型如下所示(可通过”man 3 perror”命令查看):
1
2
void perror(const char *s);
s:在错误提示字符串信息之前,可加入自己的打印信息,也可不加,不加则传入空字符串即可。
返回值:void 无返回值。
3 exit、_exit、__Exit
当程序在执行某个函数出错的时候,如果此函数执行失败会导致后面的步骤不能在进行下去时,应该在出错时终止程序运行,不应该让程序继续运行下去,那么如何退出程序、终止程序运行呢?有过编程经验的都知道使用 return,一般原则程序执行正常退出 return 0,而执行函数出错退出 return -1。
在 Linux 系统下,进程(程序)退出可以分为正常退出和异常退出,注意这里说的异常并不是执行函数出现了错误这种情况,异常往往更多的是一种不可预料的系统异常,可能是执行了某个函数时发生的、也有可能是收到了某种信号等,这里我们只讨论正常退出的情况。
在 Linux 系统下,进程正常退出除了可以使用 return 之外,还可以使用 exit()、 _exit()以及__Exit(),下面我们分别介绍。
3.1_exit()和_Exit()函数
main 函数中使用 return 后返回, return 执行后把控制权交给调用函数,结束该进程。调用_exit()函数会清除其使用的内存空间,并销毁其在内核中的各种数据结构,关闭进程的所有文件描述符,并结束进程、将控制权交给操作系统。
_exit()函数原型如下所示:
1
2
3
void _exit(int status);调用函数需要传入 status 状态标志, 0 表示正常结束、若为其它值则表示程序执行过程中检测到有错误发生。
_Exit()函数原型如下所示:
1
2
3
void _Exit(int status);_exit()和__Exit()两者等价,用法作用是一样的,需要注意的是这 2 个函数都是系统调用。
3.2exit()函数
exit()函数-exit()函数都是用来终止进程的, exit()是一个标准 C 库函数,而_exit()和-Exit()是系统调用。执行 exit()会执行一些清理工作,最后调用_exit()函数。
exit()函数原型如下:
1
2
3
void exit(int status);该函数是一个标准 C 库函数,使用该函数需要包含头文件<stdlib.h>,该函数的用法和_exit()/-Exit()是一样的。
4 空洞文件
什么是空洞文件(hole file)?之前介绍了 lseek()系统调用,使用 lseek 可以修改文件的当前读写位置偏移量,此函数不但可以改变位置偏移量,并且还允许文件偏移量超出文件长度,这是什么意思呢?譬如有一个 test_file,该文件的大小是 4K(也就是 4096 个字节),如果通过 lseek 系统调用将该文件的读写偏移量移动到偏移文件头部 6000 个字节处,大家觉得不能这样操作,但事实上 lseek 函数确实可以这样操作。
接下来使用 write()函数对文件进行写入操作,也就是说此时将是从偏移文件头部 6000 个字节处开始写入数据,也就意味着 4096~6000 字节之间出现了一个空洞,因为这部分空间并没有写入任何数据,所以形成了空洞,这部分区域就被称为文件空洞,那么相应的该文件也被称为空洞文件。
文件空洞部分实际上并不会占用任何物理空间,直到在某个时刻对空洞部分进行写入数据时才会为它分配对应的空间,但是空洞文件形成时,逻辑上该文件的大小是包含了空洞部分的大小的,这点需要注意。
空洞文件有什么用呢?空洞文件对多线程共同操作文件是及其有用的,有时候我们创建一个很大的文件,如果单个线程从头开始依次构建该文件需要很长的时间,有一种思路就是将文件分为多段,然后使用多线程来操作,每个线程负责其中一段数据的写入;这个有点像我们现实生活当中施工队修路的感觉,比如说修建一条高速公路,单个施工队修筑会很慢,这个时候可以安排多个施工队,每一个施工队负责修建其中一段,最后将他们连接起来。
来看一下实际中空洞文件的两个应用场景:
- 在使用迅雷下载文件时,还未下载完成,就发现该文件已经占据了全部文件大小的空间,这也是空洞文件;下载时如果没有空洞文件,多线程下载时文件就只能从一个地方写入,这就不能发挥多线程的作用了;如果有了空洞文件,可以从不同的地址同时写入,就达到了多线程的优势;
- 在创建虚拟机时,你给虚拟机分配了 100G 的磁盘空间,但其实系统安装完成之后,开始也不过只用了 3、 4G 的磁盘空间,如果一开始就把 100G 分配出去,资源是很大的浪费。
5 O_APPEND和O_TRUNC标志
open 函数的时候介绍了一些 open 函数的 flags 标志,譬如 O_RDONLY、O_WRONLY、 O_CREAT、 O_EXCL 等,本小节再给大家介绍两个标志,分别是 O_APPEND 和 O_TRUNC。
5.1 O_TRUNC 标志
O_TRUNC 这个标志的作用非常简单,如果使用了这个标志,调用 open 函数打开文件的时候会将文件原本的内容全部丢弃,文件大小变为 0 。
5.2 O_APPEND 标志
如果 open 函数携带了 O_APPEND 标志,调用 open 函数打开文件,当每次使用 write()函数对文件进行写操作时,都会自动把文件当前位置偏移量移动到文件末尾,从文件末尾开始写入数据,也就是意味着每次写入数据都是从文件末尾开始。
当读取文件时, O_APPEND 标志并不会影响读位置偏移量,即使使用了 O_APPEND标志,读文件位置偏移量默认情况下依然是文件头 。
使用了 O_APPEND 标志,即使是通过 lseek 函数也是无法修改写文件时对应的位置偏移量,写入数据依然是从文件末尾开始。
6 多次打开同一个文件
- 一个进程内多次 open 打开同一个文件,那么会得到多个不同的文件描述符 fd,同理在关闭文件的时候也需要调用 close 依次关闭各个文件描述符。
- 一个进程内多次 open 打开同一个文件,在内存中并不会存在多份动态文件。
- 一个进程内多次 open 打开同一个文件,不同文件描述符所对应的读写位置偏移量是相互独立的。
多次打开同一文件进行读操作与 O_APPEND 标志:
一个进程中两次调用 open 函数打开同一个文件,分别得到两个文件描述符 fd1 和 fd2,使用这两个文件描述符对文件进行写入操作,那么它们是分别写(各从各的位置偏移量开始写)还是接续写(一个写完,另一个接着后面写)?
因为这两个文件描述符所对应的读写位置偏移量是相互独立的,所以是分别写。如果想要实现接续写,使用 O_APPEND 标志来解决这个问题。
7 复制文件描述符
在 Linux 系统中, open 返回得到的文件描述符 fd 可以进行复制,复制成功之后可以得到一个新的文件描述符,使用新的文件描述符和旧的文件描述符都可以对文件进行 IO 操作,复制得到的文件描述符和旧的文件描述符拥有相同的权限,在 Linux 系统下,可以使用 dup 或 dup2 这两个系统调用对文件描述符进行复制。
复制得到的文件描述符与旧的文件描述符都指向了同一个文件表,假设 fd1 为原文件描述符, fd2 为复制得到的文件描述符,如下图所示:
7.1 dup函数
dup 函数用于复制文件描述符,此函数原型如下所示(可通过”man 2 dup”命令查看):
1
2
3
int dup(int oldfd);
oldfd: 需要被复制的文件描述符。
返回值: 成功时将返回一个新的文件描述符,由操作系统分配,分配置原则遵循文件描述符分配原则;如果复制失败将返回-1,并且会设置 errno 值。
由前面的介绍可知,复制得到的文件描述符与原文件描述符都指向同一个文件表,所以它们的文件读写偏移量是一样的,那么可以在不使用O_APPEND标志的情况下,通过文件描述符复制来实现接续写。
7.2 dup2函数
dup 系统调用分配的文件描述符是由系统分配的,遵循文件描述符分配原则,并不能自己指定一个文件描述符;而 dup2可以手动指定文件描述符,而不需要遵循文件描述符分配原则。
dup2 函数原型如下所示(可以通过”man 2 dup2”命令查看):
1
2
3
int dup2(int oldfd, int newfd);
- oldfd: 需要被复制的文件描述符。
- newfd: 指定一个文件描述符(需要指定一个当前进程没有使用到的文件描述符)。
- 返回值: 成功时将返回一个新的文件描述符,也就是手动指定的文件描述符newfd;如果复制失败将返回-1,并且会设置 errno 值。
8 文件共享
文件共享指的是同一个文件(譬如磁盘上的同一个文件,对应同一个 inode)被多个独立的读写体同时进行 IO 操作。多个独立的读写体大家可以将其简单地理解为对应于同一个文件的多个不同的文件描述符,譬如多次打开同一个文件所得到的多个不同的 fd,或使用 dup()(或 dup2)函数复制得到的多个不同的 fd 等。
同时进行 IO 操作指的是一个读写体操作文件尚未调用 close 关闭的情况下,另一个读写体去操作文件,当使用 fd1 对文件进行写操作之后,并没有关闭 fd1,而此时使用 fd2 对文件再进行写操作,这其实就是一种文件共享。
文件共享的意义有很多,多用于多进程或多线程编程环境中, 譬如我们可以通过文件共享的方式来实现多个线程同时操作同一个大文件,以减少文件读写时间、提升效率。
文件共享的核心是:如何制造出多个不同的文件描述符来指向同一个文件。其实方法在上面的内容中都已经给大家介绍过了,譬如多次调用open 函数重复打开同一个文件得到多个不同的文件描述符、使用 dup()或 dup2()函数对文件描述符进行复制以得到多个不同的文件描述符。
常见的三种文件共享的实现方式
- 同一个进程中多次调用 open 函数打开同一个文件
![]()
- 不同进程中分别使用 open 函数打开同一个文件
- 同一个进程中通过 dup(dup2)函数对文件描述符进行复制
对于文件共享,存在着竞争冒险,这个是需要大家关注的。除此之外,我们还需要关心的是文件共享时,不同的读写体之间是分别写还是接续写,这些细节问题大家都要搞清楚。
9 原子操作与竞争冒险
Linux 是一个多任务、多进程操作系统,系统中往往运行着多个不同的进程、任务,多个不同的进程就有可能对同一个文件进行 IO 操作,此时该文件便是它们的共享资源,它们共同操作着同一份文件;操作系统级编程不同于裸机编程,裸机程序中不存在进程、多任务这种概念,而在 Linux 系统中,我们必须要留意到多进程环境下可能会导致的竞争冒险。
9.1 竞争冒险简介
竞争冒险不但存在于 Linux 应用层、也存在于 Linux 内核驱动层。
假设有两个独立的进程 A 和进程 B 都对同一个文件进行追加写操作(也就是在文件末尾写入数据),每一个进程都调用了 open 函数打开了该文件,但未使用 O_APPEND 标志。每个进程都有它自己的进程控制块 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 使用权的先后顺序是不可预期的,完全由操作系统调配,这就是所谓的竞争状态。
既然存在竞争状态,那么该如何规避或消除这种状态呢?接下来给大家介绍原子操作。
9.2原子操作
所谓原子操作,是有多步操作组成的一个操作,原子操作要么一步也不执行,一旦执行,必须要执行完所有步骤,不可能只执行所有步骤中的一个子集。
9.2.1 O_APPEND 实现原子操作
在上一小节提到的示例中,进程 A 和进程 B 都对同一个文件进行追加写操作,导致进程 A 写入的数据覆盖了进程 B 写入的数据,解决办法就是将“先定位到文件末尾,然后写”这两个步骤组成一个原子操作即可,那如何使其变成一个原子操作呢?答案就是 O_APPEND 标志。
当 open 函数的 flags 参数中包含了 O_APPEND 标志,每次执行 write 写入操作时都会将文件当前写位置偏移量移动到文件末尾,然后再写入数据,这里“移动当前写位置偏移量到文件末尾、写入数据”这两个操作步骤就组成了一个原子操作,加入 O_APPEND 标志后,不管怎么写入数据都会是从文件末尾写,这样就不会导致出现“进程 A 写入的数据覆盖了进程 B 写入的数据”这种情况了。
9.2.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”命令来查看):
1
2
3
4
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);
- fd、 buf、 count 参数与 read 或 write 函数意义相同。
- offset: 表示当前需要进行读或写的位置偏移量。
- 返回值: 返回值与read、 write 函数返回值意义一样。
虽然 pread(或 pwrite)函数相当于 lseek 与 pread(或 pwrite)函数的集合,但还是有下列区别:
调用 pread函数时,无法中断其定位和读操作(也就是原子操作);
不更新文件表中的当前位置偏移量。
9.2.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 返回错误;如果指定的文件不存在,则创建这个文件,这里就提供了一种机制,保证进程是打开文件的创建者,将“判断文件是否存在、创建文件”这两个步骤合成为一个原子操作。
10 fcntl 和 ioctl
10.1 fcntl函数
fcntl()函数可以对一个已经打开的文件描述符执行一系列控制操作,譬如复制一个文件描述符(与 dup、dup2 作用相同)、获取/设置文件描述符标志、获取/设置文件状态标志等,类似于一个多功能文件描述符管理工具箱。
fcntl()函数原型如下所示(可通过”man 2 fcntl”命令查看):
1
2
3
4
int fcntl(int fd, int cmd, ... /* arg */ )
fd: 文件描述符。
cmd: 操作命令。此参数表示我们将要对 fd 进行什么操作, cmd 参数支持很多操作命令,大家可以打开 man 手册查看到这些操作命令的详细介绍,这些命令都是以 F_XXX 开头的,譬如 F_DUPFD、 F_GETFD、F_SETFD 等,不同的 cmd 具有不同的作用, cmd 操作命令大致可以分为以下 5 种功能:
- 复制文件描述符(cmd=F_DUPFD 或 cmd=F_DUPFD_CLOEXEC)
- 获取/设置文件描述符标志(cmd=F_GETFD 或 cmd=F_SETFD)
- 获取/设置文件状态标志(cmd=F_GETFL 或 cmd=F_SETFL)
- 获取/设置异步 IO 所有权(cmd=F_GETOWN 或 cmd=F_SETOWN)
- 获取/设置记录锁(cmd=F_GETLK 或 cmd=F_SETLK)
…: fcntl 函数是一个可变参函数,第三个参数需要根据不同的 cmd 来传入对应的实参,配合 cmd 来使用。
返回值: 执行失败情况下,返回-1,并且会设置 errno;执行成功的情况下,其返回值与 cmd(操作命令)有关,譬如 cmd=F_DUPFD(复制文件描述符)将返回一个新的文件描述符、 cmd=F_GETFD(获取文件描述符标志)将返回文件描述符标志、 cmd=F_GETFL(获取文件状态标志)将返回文件状态标志等。
10.2 ioctl函数
ioctl()可以认为是一个文件 IO 操作的杂物箱,可以处理的事情非常杂、不统一,一般用于操作特殊文件或硬件外设,此函数将会在进阶篇中使用到,譬如可以通过 ioctl 获取 LCD 相关信息等,本小节只是给大家引出这个系统调用,暂时不会用到。此函数原型如下所示(可通过”man 2 ioctl”命令查看):
1
2
3
int ioctl(int fd, unsigned long request, ...);
fd: 文件描述符。
request: 此参数与具体要操作的对象有关,没有统一值,表示向文件描述符请求相应的操作;后面用到的时候再给大家介绍。
…: 此函数是一个可变参函数,第三个参数需要根据 request 参数来决定,配合 request 来使用。返回值: 成功返回 0,失败返回-1。
11 截断文件
使用**系统调用 truncate()或 ftruncate()**可将普通文件截断为指定字节长度,其函数原型如下所示:
1
2
3
4
5
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 以指示错误原因。