0%

Linux文件IO(二)

Linux文件IO(二)

多次打开同一个文件

  • 一个进程内多次 open 打开同一个文件,那么会得到多个不同的文件描述符 fd,同理在关闭文件的 时候也需要调用 close 依次关闭各个文件描述符。
  • 一个进程内多次 open 打开同一个文件,在内存中并不会存在多份动态文件。
  • 一个进程内多次 open 打开同一个文件,不同文件描述符所对应的读写位置偏移量是相互独立的。
  • image-20220112193343091
  • 实例
#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;
}
/* 将读写位置偏移量移动到文件头 */
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。

  • image-20220112193309416

  • 上图中打印显示读取出来的数据是 0x11/0x22/0x33/0x44,所以由此可知,即使多次打开同一个文件,内 存中也只有一份动态文件。

多次打开同一文件与O_APPEND

  • 假如不使用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);
}
  • image-20220112202602282

可见,每次fd1和fd2写入的内容都是交替出现的,也就是说一个标识符写入之后会自动更新另一个的偏移量

复制文件描述符

  • 在 Linux 系统中,open 返回得到的文件描述符 fd 可以进行复制,复制成功之后可以得到一个新的文件 描述符,使用新的文件描述符和旧的文件描述符都可以对文件进行 IO 操作,复制得到的文件描述符和旧的文件描述符拥有相同的权限,譬如使用旧的文件描述符对文件有读写权限,那么新的文件描述符同样也具 有读写权限;在 Linux 系统下,可以使用 dup 或 dup2 这两个系统调用对文件描述符进行复制,本小节就给 大家介绍这两个函数的用法以及它们之间的区别。 复制得到的文件描述符与旧的文件描述符都指向了同一个文件表,假设 fd1 为原文件描述符,fd2 为复 制得到的文件描述符,如下图所示:
  • image-20220112202809873

dup 函数

  • dup 函数用于复制文件描述符
#include <unistd.h>

int dup(int oldfd);
  • oldfd:需要被复制的文件描述符。
  • 成功时将返回一个新的文件描述符,由操作系统分配,分配置原则遵循文件描述符分配原则; 如果复制失败将返回-1,并且会设置 errno 值。
  • 同一个程序中使用一个描述符和复制得到的描述符进行写内容的时候,会自动连接起来而不是互相覆盖,因为两个描述符使用的是用一个偏移量

dup2函数

  • dup 系统调用分配的文件描述符是由系统分配的,遵循文件描述符分配原则,并不能自己指定一个文件 描述符,这是 dup 系统调用的一个缺陷;而 dup2 系统调用修复了这个缺陷,可以手动指定文件描述符,而 不需要遵循文件描述符分配原则,当然在实际的编程工作中,需要根据自己的情况来进行选择。
#include <unistd.h>

int dup2(int oldfd, int newfd);
  • oldfd:需要被复制的文件描述符。

  • newfd:指定一个文件描述符(需要指定一个当前进程没有使用到的文件描述符)。

  • 返回值:成功时将返回一个新的文件描述符,也就是手动指定的文件描述符 newfd;如果复制失败将返 回-1,并且会设置 errno 值。

共享文件

  • 文件共享指的是同一个文件(譬如磁盘上的同一个文件,对应同一个 inode)被 多个独立的读写体同时进行 IO 操作。多个独立的读写体大家可以将其简单地理解为对应于同一个文件的多 个不同的文件描述符,譬如多次打开同一个文件所得到的多个不同的 fd,或使用 dup()(或 dup2)函数复制 得到的多个不同的 fd 等。
  • 同时进行 IO 操作指的是一个读写体操作文件尚未调用 close 关闭的情况下,另一个读写体去操作文件
  • 文件共享的意义有很多,多用于多进程或多线程编程环境中,譬如我们可以通过文件共享的方式来实现 多个线程同时操作同一个大文件,以减少文件读写时间、提升效率。
  • 文件共享的核心是:如何制造出多个不同的文件描述符来指向同一个文件。譬如多次调用 open 函数重复打开同一个文件得到多个不同的文件描述符、使用 dup() 或 dup2()函数对文件描述符进行复制以得到多个不同的文件描述符。

同一个进程中多次调用 open 函数打开同一个文件

  • image-20220112205040312

不同进程中分别使用 open 函数打开同一个文件

  • image-20220112205112471

同一个进程中通过 dup(dup2)函数对文件描述符进行复制

  • image-20220112205156025

文件操作中的原子操作和竞争冒险问题

  • 假设有两个独立的进程 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 刚才写入到该文件中的数据。

  • image-20220112205422813

  • 以上给大家所描述的这样一种情形就属于竞争状态(也成为竞争冒险),操作共享资源的两个进程或 线程),其操作之后的所得到的结果往往是不可预期的,因为每个进程(或线程)去操作文件的顺序是不可 预期的,即这些进程获得 CPU 使用权的先后顺序是不可预期的,完全由操作系统调配,这就是所谓的竞争 状态。

解决上述问题用到的原子操作

  • 所谓原子操作,是有多步操作组成的一个操作,原子操作要么一步也不执行,一旦执行,必须要执行完所有 步骤,不可能只执行所有步骤中的一个子集。

  • 使用O_APPEND实现原子操作

    • 当 open 函数的 flags 参数中包含了 O_APPEND 标志,每次执行 write 写入 操作时都会将文件当前写位置偏移量移动到文件末尾,然后再写入数据,这里“移动当前写位置偏移量到文 件末尾、写入数据”这两个操作步骤就组成了一个原子操作,加入 O_APPEND 标志后,不管怎么写入数据 都会是从文件末尾写,这样就不会导致出现“进程 A 写入的数据覆盖了进程 B 写入的数据”这种情况了。
  • 使用pread()pwrite()进行操作

    • pread()和 pwrite()都是系统调用,与 read()、write()函数的作用一样,用于读取和写入数据。区别在于, pread()和 pwrite()可用于实现原子操作,调用 pread 函数或 pwrite 函数可传入一个位置偏移量 offset 参数, 用于指定文件当前读或写的位置偏移量,所以调用 pread 相当于调用 lseek 后再调用 read;同理,调用 pwrite 相当于调用 lseek 后再调用 write。所以可知,使用 pread 或 pwrite 函数不需要使用 lseek 来调整当前位置偏 移量,并会将“移动当前位置偏移量、读或写”这两步操作组成一个原子操作。
#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 函数返回值意义一样。

  • 虽然 pread(或 pwrite)函数相当于 lseek 与 pread(或 pwrite)函数的集合,但还是有下列区别:

    • 调用 pread 函数时,无法中断其定位和读操作(也就是原子操作);
    • 不更新文件表中的当前位置偏移量。
  • **fcntl()ioctl()**详见原子的《I.MX6U嵌入式Linux C应用编程指南》

截断文件

  • 使用系统调用 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。

  • 调用这两个函数并不会导致文件读写位置偏移量发生改变,所以截断之后一般需要重新设置文件当前 的读写位置偏移量,以免由于之前所指向的位置已经不存在而发生错误(譬如文件长度变短了,文件当前所 指向的读写位置已不存在)。