1 文件I/O的基础操作
1.1 打开文件:open()
在 Linux 系统中要操作一个文件,需要先打开该文件,得到文件描述符,然后再对文件进行相应的读写 操作(或其他操作),最后在关闭该文件;
函数原型如下所示:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
函数参数和返回值含义如下:
- pathname:字符串类型,用于标识需要打开或创建的文件,可以包含路径(绝对路径或相对路径)信 息,如果 pathname 是一个符号链接,会对其进行解引用。
- flags:调用 open 函数时需要提供的标志,包括文件访问模式标志以及其它文件相关标志,这些标志使 用宏定义进行描述,都是常量;可通过位或运算(|)将多个标志进行组合。
- O_TRUNC:调用 open 函数打开文件时会将文件原本的内容全部丢弃,文件大小变为 0;
- O_APPEND:调用 open 函数打开文件, 当每次使用 write()函数对文件进行写操作时,都会自动把文件当前位置偏移量移动到文件末尾,从文件末 尾开始写入数据,也就是意味着每次写入数据都是从文件末尾开始。
- mode:此参数用于指定新建文件的访问权限,只有当 flags 参数中包含 O_CREAT 或 O_TMPFILE 标志 时才有效(O_TMPFILE 标志用于创建一个临时文件)
权限表示方法如下所示:
我们从低位从上看,每 3 个 bit 位分为一组,按照 rwx 顺序来分配权限位(特殊权限除外),分别表示:
- O---这 3 个 bit 位用于表示其他用户的权限;
- G---这 3 个 bit 位用于表示同组用户(group)的权限,即与文件所有者有相同组 ID 的所有用户;
- U---这 3 个 bit 位用于表示文件所属用户的权限,即文件或目录的所属者;
- S---这 3 个 bit 位用于表示文件的特殊权限,
- 111000000(二进制表示):表示文件所属者具有读、写、执行权限,而同组用户和其他用户不具有任 何权限;
- 100100100(二进制表示):表示文件所属者、同组用户以及其他用户都具有读权限,但都没有写、执 行权限
在实际编程中,我们可以直接使用 Linux 中 已经定义好的宏,不同的宏定义表示不同的权限,这些宏既可以单独使用,也可以通过位或运算将多个宏组合在一起,如下所示:
返回值:成功将返回文件描述符,文件描述符是一个非负整数;失败将返回-1。
1.2 写文件:write()
调用 write 函数可向打开的文件写入数据,其函数原型如下所示(可通过"man 2 write"查看):
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
- fd:文件描述符,打开或创建文件成功时得到的返回值。
- buf:指定写入数据对应的缓冲区。
- count:指定写入的字节数。
- 返回值:如果成功将返回写入的字节数(0 表示未写入任何字节),如果此数字小于 count 参数,这不 是错误,譬如磁盘空间已满,可能会发生这种情况;如果写入出错,则返回-1。
读写操作都是从文件的当前位置偏移量处开始,当前位置偏移量可以通过 lseek 系统调用进行设置。
1.3 读文件:read()
调用 read 函数可从打开的文件中读取数据,其函数原型如下所示(可通过"man 2 read"查看):
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
- fd:文件描述符。与 write 函数的 fd 参数意义相同。
- buf:指定用于存储读取数据的缓冲区。
- count:指定需要读取的字节数。
- 返回值:如果读取成功将返回读取到的字节数,实际读取到的字节数可能会小于 count 参数指定的字节 数,也有可能会为 0,譬如进行读操作时,当前文件位置偏移量已经到了文件末尾
1.4 关闭文件:close()
可调用 close 函数关闭一个已经打开的文件。
#include <unistd.h>
int close(int fd);
- fd:文件描述符,需要关闭的文件所对应的文件描述符。
- 返回值:如果成功返回 0,如果失败则返回-1。
1.5 设置读写偏移量:lseek()
当调用 read()或 write()函数对文件进行读写操作时,就会从当前读写位置偏移量开始进行数据读写。连续的调用 read()和 write()函数将使得读写按顺序递增。而lseek函数可以直接对当前读写位置偏移量进行设置。
#include <sys/types.h>
#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);
函数参数和返回值含义如下:
fd:文件描述符。
offset:偏移量,以字节为单位。
whence:用于定义参数 offset 偏移量对应的参考值,该参数为下列其中一种(宏定义):
- SEEK_SET:读写偏移量将指向 offset 字节位置处(从文件头部开始算);
- SEEK_CUR:读写偏移量将指向当前位置偏移量 + offset 字节位置处,offset 可以为正、也可以为 负,如果是正数表示往后偏移,如果是负数则表示往前偏移;
- SEEK_END:读写偏移量将指向文件末尾 + offset 字节位置处,同样 offset 可以为正、也可以为负, 如果是正数表示往后偏移、如果是负数则表示往前偏移。
返回值:成功将返回从文件头部开始算起的位置偏移量(字节为单位),也就是当前的读写位置;发生 错误将返回-1。
使用示例:
#include <sys/types.h>
#include <unistd.h>
off_t off = lseek(fd, 0, SEEK_SET); //将读写位置移动到文件开头处
off_t off = lseek(fd, 0, SEEK_END); //将读写位置移动到文件末尾
off_t off = lseek(fd, 100, SEEK_SET); //将读写位置移动到偏移文件开头 100 个字节处
off_t off = lseek(fd, 0, SEEK_CUR); //获取当前读写位置偏移量
2 Linux 系管理文件
2.1 进程退出方式
2.1.1 _exit() 和 _Exit() 函数
调用_exit()函数会 清除其使用的内存空间,并销毁其在内核中的各种数据结构,关闭进程的所有文件描述符,并结束进程、将控制权交给操作系统。_exit()和_Exit()两者等价,用法作用是一样的。_exit()函数原型如下所示:
#include <unistd.h>
void _exit(int status);
void _Exit(int status);
参数 status :0 表示正常结束、若为其它值则表示程序执行过程中检测到有错误发生。
2.1.2 exit()函数
exit()函数_exit()函数都是用来终止进程的,exit()是一个标准 C 库函数;而_exit()和_Exit()是系统调用。执行 exit()会执行一些清理工作,最后调用_exit()函数。exit()函数原型如下:
#include <stdlib.h>
void exit(int status);
参数 status :0 表示正常结束、若为其它值则表示程序执行过程中检测到有错误发生。
2.1.3 return 函数
一般原则程序执行正常退出 return 0,而执行函数出错退出 return -1
2.2 空洞文件
譬如有一个 test_file,该文件的大小是 4K(也就是 4096 个字节),如果通过 lseek 系统调用将该文件的读写偏移量移动到偏移文件头部 6000 个字节处,接下来使用 write()函数对文件进行写入操作,也就意味着 4096~6000 字节之间出现了一个空洞,这部分区域就被称为文件空洞,那么相应的该文件也被称为空洞文件。
- 使用 ls 命令查看空洞文件的大小,看到的大小是文件的逻辑大小,是包括了 空洞部分大小和真实数据部分大小;
- 使用 du 命令查看空洞文件时,看到的大小是文件实际占用存储块的大小。
2.3 多次打开同一文件
- 一个进程内多次 open 打开同一个文件,那么会得到多个不同的文件描述符 fd,同理在关闭文件的 时候也需要调用 close 依次关闭各个文件描述符。
- 一个进程内多次 open 打开同一个文件,在内存中并不会存在多份动态文件。
- 一个进程内多次 open 打开同一个文件,不同文件描述符所对应的读写位置偏移量是相互独立的。
2.4 复制文件描述符:dup 或 dup2
复制得到的文件描述符与旧的文件描述符都指向了同一个文件表,这两个文件描述符的属性是一样,譬如对文件的读写权限、文件状态标志、文件偏移量等,“复制”的含义 实则是复制文件表。同样,在使用完毕之后也需要使用 close 来关闭文件描述符。
2.4.1 dup 函数
dup 函数用于复制文件描述符,此函数原型如下所示(可通过"man 2 dup"命令查看):
#include <unistd.h>
int dup(int oldfd);
- oldfd:需要被复制的文件描述符。
- 返回值:成功时将返回一个新的文件描述符,由操作系统分配,分配置原则遵循文件描述符分配原则; 如果复制失败将返回-1,并且会设置 errno 值。
2.4.2 dup2 函数
相比于 dup函数 可以手动指定文件描述符,而不需要遵循文件描述符分配原则:
#include <unistd.h>
int dup2(int oldfd, int newfd);
- oldfd:需要被复制的文件描述符。
- newfd:指定一个文件描述符(需要指定一个当前进程没有使用到的文件描述符)。
- 返回值:成功时将返回一个新的文件描述符,也就是手动指定的文件描述符 newfd;如果复制失败将返 回-1,并且会设置 errno 值。
2.5 文件共享
所谓文件共享指的是同一个文件(譬如磁盘上的同一个文件,对应同一个 inode)被多个独立的读写体同时进行 IO 操作。同时进行 IO 操作指的是一个读写体操作文件尚未调用 close 关闭的情况下,另一个读写体去操作该文件。譬如,同一 个文件对应两个不同的文件描述符 fd1 和 fd2,当使用 fd1 对文件进行写操作之后,并没有关闭 fd1,而此时 使用 fd2 对文件再进行写操作,这其实就是一种文件共享。
多个独立的读写体,可以将其简单地理解为对应于同一个文件的多个不同的文件描述符,譬如多次打开同一个文件所得到的多个不同的 fd,或使用 dup()(或 dup2)函数复制得到的多个不同的 fd 等。
常见的三种文件共享的实现方式:
- 同一个进程中多次调用 open 函数打开同一个文件。
- 不同进程中分别使用 open 函数打开同一个文件
- 同一个进程中通过 dup(dup2)函数对文件描述符进行复制
2.5.1 竞争冒险
对于文件共享,存在着竞争冒险;
假设有两个独立的进程 A 和进程 B 都对同一个文件进行追加写操作(也就是在文件末尾写入数据),每一个进程都调用了 open 函数打开了该文件,但未使用 O_APPEND 标志。。假定此时进程 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 使用权的先后顺序是不可预期的,完全由操作系统调配,这就是所谓的竞争 状态。
2.5.2 原子操作
规避或消除在竞争冒险状态的操作--原子操作
所谓原子操作,是有多步操作组成的一个操作,原子操作要么一步也不执行,一旦执行,必须要执行完所有 步骤,不可能只执行所有步骤中的一个子集。
2.2.2.1 实现原子操作方式:O_APPEND
当 open 函数的 flags 参数中包含了 O_APPEND 标志,每次执行 write 写入 操作时都会将文件当前写位置偏移量移动到文件末尾,然后再写入数据,这里“移动当前写位置偏移量到文 件末尾、写入数据”这两个操作步骤就组成了一个原子操作,加入 O_APPEND 标志后,不管怎么写入数据 都会是从文件末尾写,这样就不会导致出现“进程 A 写入的数据覆盖了进程 B 写入的数据”这种情况了。
2.2.2.2 实现原子操作方式:pread()和 pwrite()
pread()和 pwrite()都是系统调用,与 read()、write()函数的作用一样,用于读取和写入数据。区别在于, pread()和 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);
- fd、buf、count 参数:与 read 或 write 函数意义相同。
- offset:表示当前需要进行读或写的位置偏移量。
- 返回值:返回值与 read、write 函数返回值意义一样。
2.6 截断文件:truncate()或 ftruncate()
使用系统调用 truncate()或 ftruncate()可将普通文件截断为指定字节长度
如 果文件目前的大小大于参数 length 所指定的大小,则多余的数据将被丢失,类似于多余的部分被“砍”掉 了;如果文件目前的大小小于参数 length 所指定的大小,则将其进行扩展,对扩展部分进行读取将得到空字 节"\0"。
#include <unistd.h>
#include <sys/types.h>
int truncate(const char *path, off_t length); // 直接使用文件路径 path 来指定目标文件
int ftruncate(int fd, off_t length); // 文件描述符 fd 来指定目标文件
参数 length:将文件截断为指定的 length 字节长度