Linux C应用编程:文件I/O

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 位用于表示文件的特殊权限,
  1.  111000000(二进制表示):表示文件所属者具有读、写、执行权限,而同组用户和其他用户不具有任 何权限;
  2. 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 多次打开同一文件

  1. 一个进程内多次 open 打开同一个文件,那么会得到多个不同的文件描述符 fd,同理在关闭文件的 时候也需要调用 close 依次关闭各个文件描述符。
  2. 一个进程内多次 open 打开同一个文件,在内存中并不会存在多份动态文件
  3. 一个进程内多次 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 等。

常见的三种文件共享的实现方式:

  1. 同一个进程中多次调用 open 函数打开同一个文件。
  2. 不同进程中分别使用 open 函数打开同一个文件
  3. 同一个进程中通过 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 字节长度

  • 27
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值