第 3 章 深入探究文件 I/O

3.1 Linux 系统如何管理文件

3.1.1 静态文件与 inode

静态文件:文件存放在磁盘文件系统中,并且以一种固定的形式进行存放,我们把他们称为静态文件。

磁盘在进行分区、格式化的时候会将其分为两个区域,一个是数据区,用于存储文件中的数据,另一个是inode 区,用于存放 inode table(inode 表)。不同的 inode 就可以表示不同的文件,每一个文件都必须对应一个inode。

在这里插入图片描述

打开一个文件,系统内部会将这个过程分为三步:

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

3.1.2 文件打开时的状态

当我们调用 open 函数去打开文件的时候,内核会申请一段内存(一段缓冲区),并且将静态文件的数据内容从磁盘这些存储设备中读取到内存中进行管理、缓存(也把内存中的这份文件数据叫做动态文件、内核缓冲区)。打开文件后,以后对这个文件的读写操作,都是针对内存中这一份动态文件进行相关的操作,而并不是针对磁盘中存放的静态文件。

当我们对动态文件进行读写操作后,此时内存中的动态文件和磁盘设备中的静态文件就不同步了,数据的同步工作由内核完成,内核会在之后将内存这份动态文件更新(同步)到磁盘设备中。

3.2 返回错误处理与 errno

在 Linux 系统下对常见的错误做了一个编号,每一个编号都代表着每一种不同的错误类型,当函数执行发生错误的时候,操作系统会将这个错误所对应的编号赋值给 errno 变量。

每一个进程(程序)都维护了自己的 errno 变量,它是程序中的全局变量,该变量用于存储就近发生的函数执行错误编号,也就意味着下一次的错误码会覆盖上一次的错误码。errno 本质上是一个 int 类型的变量,用于存储错误编号。

在我们的程序当中如何去获取系统所维护的这个errno变量呢?只需要在我们程序当中包含<errno.h>头文件即可。

#include <stdio.h>
#include <errno.h>

int main(void)
{
    printf("%d\n", errno);
    return 0;
}

3.2.1 strerror 函数

C 库函数strerror(),该函数可以将对应的 errno 转换成适合我们查看的字符串信息

#include <string.h>
char *strerror(int errnum);

函数参数和返回值如下:

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;
}

在这里插入图片描述

从打印信息可以知道,strerror 返回的字符串是"No such file or directory",所以从打印信息可知,我们就可以很直观的知道 open 函数执行的错误原因是文件不存在。

3.2.2 perror 函数

使用 perror 函数来查看错误信息,一般用的最多的还是这个函数,调用此函数不需要传入 errno,函数内部会自己去获取 errno 变量的值,调用此函数会直接将错误提示字符串打印出来,而不是返回字符串,除此之外还可以在输出的错误提示字符串之前加入自己的打印信息。

#include <stdio.h>
void perror(const char *s);

函数参数和返回值含义如下:

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;
}

在这里插入图片描述

从打印信息可以知道,perror 函数打印出来的错误提示字符串是"No such file or directory",跟 strerror 函数返回的字符串信息一样,"open error"便是我们附加的打印信息,而且从打印信息可知,perror 函数会在附加信息后面自动加入冒号和空格以区分。

3.3 exit、_exit、_Exit

在 Linux 系统下,进程正常退出除了可以使用 return 之外,还可以使用 exit()、_exit()以及_Exit()

3.3.1 _exit()和_Exit()函数

main 函数中使用 return 后返回,return 执行后把控制权交给调用函数,结束该进程。调用_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()两者等价,用法作用是一样的。

3.3.1 exit()函数

exit()函数_exit()函数都是用来终止进程的,exit()是一个标准 C 库函数,而_exit()和_Exit()是系统调用。执行 exit()会执行一些清理工作,最后调用_exit()函数。

#include <stdlib.h>
void exit(int status);

exit()函数的用法和_exit()/_Exit()是一样的。

3.4 空洞文件

譬如有一个 test_file,该文件的大小是 4K(也就是 4096 个字节),如果通过 lseek 系统调用将该文件的读写偏移量移动到偏移文件头部 6000 个字节处,接下来使用 write()函数对文件进行写入操作,也就是说此时将是从偏移文件头部 6000 个字节处开始写入数据,也就意味着 4096~6000 字节之间出现了一个空洞,因为这部分空间并没有写入任何数据,所以形成了空洞,这部分区域就被称为文件空洞,那么相应的该文件也被称为空洞文件。

新建一个文件把它做成空洞文件,示例代码如下所示:

#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数据才是真正写入的数据。

在这里插入图片描述

使用 ls 命令查看到空洞文件的大小是 8K,使用 ls 命令查看到的大小是文件的逻辑大小,自然是包括了空洞部分大小和真实数据部分大小;当使用 du 命令查看空洞文件时,其大小显示为 4K,du 命令查看到的大小是文件实际占用存储块的大小。

3.5 O_APPEND 和 O_TRUNC 标志

3.5.1 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,也就是说明文件之前的内容已经全部被丢弃了。

3.5.2 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()函数写文件时,会自动把文件当前位置偏移量移动到文件末尾。

3.6 多次打开同一个文件

一个进程内多次 open 打开同一个文件,那么会得到多个不同的文件描述符 fd**,同理在关闭文件的时候也需要调用 close 依次关闭各个文件描述符。**

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
     int fd1, fd2, fd3;
     int ret;
     /* 第一次打开文件 */
     fd1 = open("./test_file", O_RDWR);
     if (-1 == fd1) {
         perror("open error");
         exit(-1);
     }
     /* 第二次打开文件 */
     fd2 = open("./test_file", O_RDWR);
     if (-1 == fd2) {
         perror("open error");
         ret = -1;
         goto err1;
     }
     /* 第三次打开文件 */
     fd3 = open("./test_file", O_RDWR);
     if (-1 == fd3) {
         perror("open error");
         ret = -1;
         goto err2;
     }
     /* 打印出 3 个文件描述符 */
     printf("%d %d %d\n", fd1, fd2, fd3);
     close(fd3);
     ret = 0;
err2:
     close(fd2);
err1:
     /* 关闭文件 */
     close(fd1);
     exit(ret);
}

通过 3 次调用 open 函数对 test_file 文件打开了 3 次,每一个调用传参一样,最后将3 次得到的文件描述符打印出来,在当前目录下存在 test_file 文件,接下来编译测试,看看结果如何:

在这里插入图片描述

从打印结果可知,三次调用 open 函数得到的文件描述符分别为 6、7、8,通过任何一个文件描述符对文件进行 IO 操作都是可以的,但是需要注意是,调用 open 函数打开文件使用的是什么权限,则返回的文件描述符就拥有什么权限,文件 IO 操作完成之后,在结束进程之前需要使用 close 关闭各个文件描述符。

一个进程内多次 open 打开同一个文件,在内存中并不会存在多份动态文件。

#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。

在这里插入图片描述

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

3.7 复制文件描述符

在 Linux 系统中,open 返回得到的文件描述符 fd 可以进行复制,复制成功之后可以得到一个新的文件描述符,使用新的文件描述符和旧的文件描述符都可以对文件进行 IO 操作,复制得到的文件描述符和旧的文件描述符拥有相同的权限。

复制得到的文件描述符与旧的文件描述符都指向了同一个文件表,假设 fd1 为原文件描述符,fd2 为复制得到的文件描述符,如下图所示:

在这里插入图片描述

因为复制得到的文件描述符与旧的文件描述符指向的是同一个文件表,所以可知,这两个文件描述符的属性是一样,譬如对文件的读写权限、文件状态标志、文件偏移量等,所以从这里也可知道“复制”的含义实则是复制文件表。同样,在使用完毕之后也需要使用 close 来关闭文件描述符。

3.7.1 dup 函数

dup 函数用于复制文件描述符。

#include <unistd.h>
int dup(int oldfd);

函数参数和返回值含义如下:

oldfd:需要被复制的文件描述符。

返回值:成功时将返回一个新的文件描述符,由操作系统分配,分配置原则遵循文件描述符分配原则;如果复制失败将返回-1,并且会设置 errno 值。

复制得到的文件描述符与原文件描述符都指向同一个文件表,所以它们的文件读写偏移量是一样的,那么是不是可以在不使用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,
             S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
     if (-1 == fd1) {
         perror("open error");
         exit(-1);
     }
     /* 复制文件描述符 */
     fd2 = dup(fd1);
     if (-1 == fd2) {
         perror("dup error");
         ret = -1;
         goto err1;
     }
     printf("fd1: %d\nfd2: %d\n", fd1, fd2);
     /* 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);
}

测试代码中,我们使用了 dup 系统调用复制了文件描述符 fd1,得到另一个新的文件描述符 fd2,分别通过 fd1 和 fd2 对文件进行写操作,最后读取写入的数据来判断是分别写还是接续写,接下来编译测试:

在这里插入图片描述

由打印信息可知,fd1 等于 6,复制得到的新的文件描述符为 7(遵循 fd 分配原则),打印出来的数据显示为接续写,所以可知,通过复制文件描述符可以实现接续写。

3.7.2 dup2 函数

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

#include <unistd.h>
int dup2(int oldfd, int newfd);

函数参数和返回值含义如下:

oldfd:需要被复制的文件描述符。

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

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

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
     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);
     }
     /* 复制文件描述符 */
     fd2 = dup2(fd1, 100);
     if (-1 == fd2) {
         perror("dup error");
         ret = -1;
         goto err1;
     }
     printf("fd1: %d\nfd2: %d\n", fd1, fd2);
     ret = 0;
     close(fd2);
err1:
     /* 关闭文件 */
     close(fd1);
     exit(ret);
}

测试代码使用 dup2 函数复制文件描述符 fd1,指定新的文件描述符为 100,复制成功之后将其打印出来,结果如下所示:

在这里插入图片描述

由打印信息可知,复制得到的文件描述符 fd2 等于 100,正是我们在 dup2 函数中指定的文件描述符。

文件描述符并不是只能复制一次,实际上可以对同一个文件描述符 fd 调用 dup 或 dup2 函数复制多次,得到多个不同的文件描述符。

3.8 文件共享

文件共享指的是同一个文件(譬如磁盘上的同一个文件,对应同一个 inode)被多个独立的读写体同时进行 IO 操作。多个独立的读写体大家可以将其简单地理解为对应于同一个文件的多个不同的文件描述符,譬如多次打开同一个文件所得到的多个不同的 fd,或使用 dup()(或 dup2)函数复制得到的多个不同的 fd 等。

文件共享的意义有很多,多用于多进程或多线程编程环境中,譬如我们可以通过文件共享的方式来实现多个线程同时操作同一个大文件,以减少文件读写时间、提升效率。

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

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

在这里插入图片描述

多次调用 open 函数打开同一个文件会得到多个不同的文件描述符,并且多个文件描述符对应多个不同的文件表,所有的文件表都索引到了同一个 inode 节点,也就是磁盘上的同一个文件。

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

在这里插入图片描述

进程 1 和进程 2 分别是运行在 Linux 系统上两个独立的进程(理解为两个独立的程序),在他们各自的程序中分别调用 open 函数打开同一个文件,进程 1 对应的文件描述符为 fd1,进程 2 对应的文件描述符为fd2,fd1 指向了进程 1 的文件表 1,fd2 指向了进程 2 的文件表 2;各自的文件表都索引到了同一个 inode 节点,从而实现共享文件。

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

在这里插入图片描述

3.9 原子操作与竞争冒险

3.9.1 竞争冒险简介

竞争冒险不但存在于 Linux 应用层、也存在于 Linux 内核驱动层。操作共享资源的两个进程(或线程),其操作之后的所得到的结果往往是不可预期的,因为每个进程(或线程)去操作文件的顺序是不可预期的,即这些进程获得 CPU 使用权的先后顺序是不可预期的,完全由操作系统调配,这就是所谓的竞争状态。

在这里插入图片描述

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

3.9.2 原子操作

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

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 函数时,无法中断其定位和读操作(也就是原子操作)。

不更新文件表中的当前位置偏移量。

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
     unsigned char buffer[100];
     int fd;
     int ret;
     /* 打开文件 test_file */
     fd = open("./test_file", O_RDWR);
     if (-1 == fd) {
         perror("open error");
         exit(-1);
     }
     /* 使用 pread 函数读取数据(从偏移文件头 1024 字节处开始读取) */
     ret = pread(fd, buffer, sizeof(buffer), 1024);
     if (-1 == ret) {
         perror("pread error");
         goto err;
     }
     /* 获取当前位置偏移量 */
     ret = lseek(fd, 0, SEEK_CUR);
     if (-1 == ret) {
         perror("lseek error");
         goto err;
     }
     printf("Current Offset: %d\n", ret);
     ret = 0;
err:
     /* 关闭文件 */
     close(fd);
     exit(ret);
}

在这里插入图片描述

上述代码中会打开 test_file 文件,然后直接使用 pread 函数读取100 个字节数据,从偏移文件头部 1024 字节处,读取完成之后再使用 lseek 函数获取到文件当前位置偏移量,并将其打印出来。假如 pread 函数会改变文件表中记录的当前位置偏移量,则打印出来的数据应该是1024 + 100 = 1124;如果不会改变文件表中记录的当前位置偏移量,则打印出来的数据应该是 0。

3.10 fcntl 和 ioctl

3.10.1 fcntl 函数

fcntl()函数可以对一个已经打开的文件描述符执行一系列控制操作,譬如复制一个文件描述符(与 dup、dup2 作用相同)、获取/设置文件描述符标志、获取/设置文件状态标志等。类似于一个多功能文件描述符管理工具箱。

#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ )

函数参数和返回值含义如下:

fd:文件描述符。

cmd:操作命令。此参数表示我们将要对 fd 进行什么操作,cmd 参数支持很多操作命令,这些命令都是以 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);

第三个参数需要根据不同的 cmd 来传入对应的实参,配合 cmd 来使用。

返回值:执行失败情况下,返回-1,并且会设置 errno;执行成功的情况下,其返回值与 cmd(操作命令)有关,譬如 cmd=F_DUPFD(复制文件描述符)将返回一个新的文件描述符、cmd=F_GETFD(获取文件描述符标志)将返回文件描述符标志、cmd=F_GETFL(获取文件状态标志)将返回文件状态标志等。

fcntl 使用示例

复制文件描述符

/* 使用 fcntl 函数复制一个文件描述符 */
 fd2 = fcntl(fd1, F_DUPFD, 0);

获取/设置文件状态标志

/* 获取文件状态标志 */
 flag = fcntl(fd, F_GETFL);
/* 设置文件状态标志, 在原标志的基础上添加 O_APPEND 标志。*/
 ret = fcntl(fd, F_SETFL, flag | O_APPEND);

3.10.2 ioctl 函数

ioctl()可以认为是一个文件 IO 操作的杂物箱,可以处理的事情非常杂、不统一,一般用于操作特殊文件或硬件外设。

#include <sys/ioctl.h>
int ioctl(int fd, unsigned long request, ...);

函数参数和返回值含义如下:

fd:文件描述符。

request:此参数与具体要操作的对象有关,没有统一值,表示向文件描述符请求相应的操作

…:此函数是一个可变参函数,第三个参数需要根据 request 参数来决定,配合 request 来使用。

返回值:成功返回 0,失败返回-1。

3.11 截断文件

使用系统调用 truncate()或 ftruncate()可将普通文件截断为指定字节长度

#include <unistd.h>
#include <sys/types.h>
int truncate(const char *path, off_t length);
int ftruncate(int fd, off_t length);s

这两个函数的区别在于:ftruncate()使用文件描述符 fd 来指定目标文件,而 truncate()则直接使用文件路径 path 来指定目标文件,其功能一样。

这两个函数都可以对文件进行截断操作,将文件截断为参数 length 指定的字节长度,什么是截断?如果文件目前的大小大于参数 length 所指定的大小,则多余的数据将被丢失,类似于多余的部分被“砍”掉了;如果文件目前的大小小于参数 length 所指定的大小,则将其进行扩展,对扩展部分进行读取将得到空字节"\0"。

使用 ftruncate()函数进行文件截断操作之前,必须调用 open()函数打开该文件得到文件描述符,并且必须要具有可写权限,也就是调用 open()打开文件时需要指定 O_WRONLY 或 O_RDWR。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main(void)
{
     int fd;
     /* 打开 file1 文件 */
     if (0 > (fd = open("./file1", O_RDWR))) {
         perror("open error");
         exit(-1);
     }
     /* 使用 ftruncate 将 file1 文件截断为长度 0 字节 */
     if (0 > ftruncate(fd, 0)) {
         perror("ftruncate error");
         exit(-1);
     }
     /* 使用 truncate 将 file2 文件截断为长度 1024 字节 */
     if (0 > truncate("./file2", 1024)) {
         perror("truncate error");
         exit(-1);
     }
     /* 关闭 file1 退出程序 */
     close(fd);
     exit(0);
}

首先使用 open()函数打开文件 file1,得到文件描述符 fd,接着使用 ftruncate()系统调用将文件截断为 0 长度,传入 file1 文件对应的文件描述符;接着调用 truncate()系统调用将文件 file2 截断为 1024字节长度,传入 file2 文件的相对路径。

在这里插入图片描述

在这里插入图片描述

程序运行之后,file1 文件大小变成了 0,而 file2 文件大小变成了 1024 字节。

#include"..\ucos-ii\includes.h" /* uC/OS interface */ #include "..\ucos-ii\add\osaddition.h" #include "..\inc\drv.h" #include <string.h> #include <math.h> #pragma import(__use_no_semihosting_swi) // ensure no functions that use semihosting ///******************任务定义***************/// OS_STK Main_Stack[STACKSIZE*8]={0, }; //Main_Test_Task堆栈 void Main_Task(void *Id); //Main_Test_Task #define Main_Task_Prio 12 /**************已经定义的OS任务************* tcp监控任务 11 以太网物理层监控任务 8 触摸屏任务 9 键盘任务 10 lcd刷新任务 59 系统任务 1 *****************************************************/ ///*****************事件定义*****************/// OS_EVENT *Nand_Rw_Sem; //Nand_Flash读写控制权旗语 //and you can use it as folloeing: // Nand_Rw_Sem=OSSemCreate(1); //创建Nand-Flash读写控制权旗语,初值为1满足互斥条件// // OSSemPend(Nand_Rw_Sem,0,&err); // OSSemPost(Nand_Rw_Sem); OS_EVENT *Uart_Rw_Sem; //Uart读写控制权旗语 //and you can use it as folloeing: // Uart_Rw_Sem=OSSemCreate(1); //创建Uart读写控制权旗语,初值为1满足互斥条件// // OSSemPend(Uart_Rw_Sem,0,&err); // OSSemPost(Uart_Rw_Sem); ////////////////////////////////////////////////////////// void initOSGUI() //初始化操作系统的图形界面 { initOSMessage(); initOSList(); initOSDC(); initOSCtrl(); initOSFile(); } ///////////////////////////////////////////////////// // Main function. // ////////////////////////////////////////////////////
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值