当前位置: 首页 > news >正文

深入理解Linux文件I/O:系统调用与标志位应用

目录

一、引入

二、标志位

1、什么是标志位?

2、标志位传递示例 

输出结果分析

关键点解释

三、文件描述符(File Descriptor)(先大概了解)

四、接口介绍:open()函数

1、命令查看

2、头文件

3、函数原型

4、参数说明

1. open的第一个参数pathname

2. open的第二个参数

1. 必需标志(必须指定且只能指定一个)

2. 可选标志(可组合使用)

补充说明

扩展:

3. open的第三个参数

基本权限位

特殊权限位

如何使用 mode_t?

1. 直接使用八进制数

2. 使用宏定义组合

3. 设置特殊权限

mode_t 的实际影响

5、返回值

五、接口介绍:close()函数

1、函数原型

2、参数说明

3、返回值

4、常见错误码(errno)

5、基本用法

6、深入理解close操作(了解)

7、注意事项(了解)

六、接口介绍:write()函数

1. write函数原型

2. 参数说明

3. 返回值

4、对文件进行写入操作示例

5. write函数的特点和注意事项

1. 部分写入

2. 阻塞与非阻塞

3. 原子性

4. 文件位置指针

七、接口介绍:read()函数

1、函数原型

2、参数说明

3、返回值

4、对文件进行读取操作示例

八、系统调用和库函数


一、引入

        操作系统提供多种文件访问方式,包括C语言接口、C++接口以及其他语言接口,同时也具备底层系统调用接口,系统调用才是文件操作最底层的实现方式。相较于高级语言库函数,系统调用更接近底层硬件。实际上,各种语言的库函数都是对系统接口的封装实现。

        无论是在Linux还是Windows平台运行C代码,C库函数都通过封装各自操作系统的系统调用接口来实现跨平台性。这种设计不仅保证了语言的通用性,也为二次开发提供了便利。

在学习系统文件I/O前,需要先掌握标志位的传递方法,这在系统文件I/O接口中会频繁使用:


二、标志位

1、什么是标志位?

        标志位(flag)是一种编程中常用的技术,它使用二进制位来表示不同的状态或选项。每个标志位通常对应一个特定的含义,通过位运算可以单独设置、清除或检查这些标志位。

标志位的优点包括:

  • 节省内存(多个布尔状态可以用一个整数的不同位表示)

  • 可以方便地组合多个状态(通过位或运算)

  • 可以高效地检查特定状态(通过位与运算)

2、标志位传递示例 

#include <stdio.h>// 定义三个标志位,每个标志位对应一个不同的二进制位
#define ONE   0x01    // 0000 0001 (二进制)
#define TWO   0x02    // 0000 0010
#define THREE 0x04    // 0000 0100void func(int flags) {// 检查flags是否包含ONE标志if (flags & ONE) printf("flags has ONE!\n");// 检查flags是否包含TWO标志if (flags & TWO) printf("flags has TWO!\n");// 检查flags是否包含THREE标志if (flags & THREE) printf("flags has THREE!\n");printf("\n");
}int main() {func(ONE);                  // 只传递ONE标志func(THREE);                // 只传递THREE标志func(ONE | TWO);            // 传递ONE和TWO标志的组合func(ONE | THREE | TWO);    // 传递所有三个标志的组合return 0;
}

输出结果分析

  1. func(ONE); 输出:flags has ONE!(只有ONE标志被设置)

  2. func(THREE); 输出:flags has THREE!(只有THREE标志被设置)

  3. func(ONE | TWO); 输出:

    flags has ONE!
    flags has TWO!(ONE和TWO标志被设置)
  4. func(ONE | THREE | TWO); 输出:

    flags has ONE!
    flags has TWO!
    flags has THREE!(所有三个标志都被设置)

关键点解释

  1. flags & ONE:这是一个位与运算,用于检查flags变量中是否设置了ONE标志位。如果结果为非零,则表示设置了该标志。

  2. ONE | TWO:这是一个位或运算,用于组合多个标志位。结果是一个同时包含ONE和TWO标志的值。

  3. 标志位的值选择:每个标志位对应一个不同的二进制位(0x01, 0x02, 0x04等),这样它们可以独立设置和检查而不会相互干扰。

这种标志位技术在系统编程、硬件接口和需要高效表示多个选项的场景中非常常见。


三、文件描述符(File Descriptor)(先大概了解)

        在Unix/Linux系统中,所有I/O操作都是通过文件描述符完成的。文件描述符是一个非负整数,用于标识打开的文件。系统为每个进程维护一个文件描述符表。

三个标准的文件描述符:

  • 0: 标准输入(stdin)

  • 1: 标准输出(stdout)

  • 2: 标准错误(stderr)


四、接口介绍:open()函数

    open()函数是Linux/Unix系统中用于打开或创建文件的核心系统调用之一,它是文件操作的基础。

1、命令查看

man 2 open

2、头文件

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

3、函数原型

系统接口中使用open函数打开文件,open函数的函数原型如下:

int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);

4、参数说明

1. open的第一个参数pathname

        open函数的第一个参数是pathname,表示要打开或创建的文件路径名,可以是相对路径或绝对路径。

  • 若pathname以路径的方式给出,则当需要创建该文件时,就在pathname路径下进行创建。
  • 若pathname以文件名的方式给出,则当需要创建该文件时,默认在当前路径下进行创建。(注意当前路径的含义)

2. open的第二个参数

        open函数的第二个参数是flags(文件打开方式标志位),表示打开文件的标志,控制文件的打开方式和行为。flags参数由以下一个或多个值通过位或(|)操作组合而成。

        例如,若想以只写的方式打开文件,但当目标文件不存在时自动创建文件,则第二个参数设置如下:

O_WRONLY | O_CREAT
1. 必需标志(必须指定且只能指定一个)
标志说明
O_RDONLY只读方式打开文件
O_WRONLY只写方式打开文件
O_RDWR读写方式打开文件
2. 可选标志(可组合使用)
标志说明
O_CREAT如果文件不存在,则创建它(需配合 mode 参数设置权限)
O_EXCL与 O_CREAT 一起使用,确保文件不存在时才创建(用于原子性创建文件)
O_TRUNC如果文件已存在且是普通文件,则截断为0字节(清空文件)
O_APPEND追加模式,每次写入都会自动追加到文件末尾(避免并发写入冲突)
O_NONBLOCK / O_NDELAY非阻塞模式打开文件(适用于 FIFO、管道、设备文件等)
O_SYNC同步 I/O,每次写操作都会等待数据真正写入物理存储(性能较低,但数据更安全)
O_NOFOLLOW如果路径是符号链接,则不跟随(防止符号链接攻击)
O_DIRECTORY如果路径不是目录,则打开失败(确保只打开目录)
O_CLOEXEC设置 close-on-exec 标志,exec 时自动关闭文件描述符(防止子进程继承)

补充说明

  • 必需标志O_RDONLY / O_WRONLY / O_RDWR)必须选且仅选一个

  • 可选标志可以通过 |(按位或)组合使用,例如:

    int fd = open("file.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
  • O_EXCL 必须与 O_CREAT 一起使用,否则无意义。

  • O_TRUNC 仅对普通文件有效,对目录、设备文件等无效。

  • O_APPEND 在多进程/多线程写入时能避免竞争条件(Race Condition)。

  • O_SYNC 会影响性能,但能确保数据持久化(适用于关键数据存储)。

扩展:

        系统接口open的第二个参数flags是整型,有32比特位,若将一个比特位作为一个标志位,则理论上flags可以传递32种不同的标志位。

实际上传入flags的每一个选项在系统当中都是以宏的方式进行定义的:

例如,O_RDONLY、O_WRONLY、O_RDWR和O_CREAT在系统当中的宏定义如下:

#define O_RDONLY         00
#define O_WRONLY         01
#define O_RDWR           02
#define O_CREAT        0100

        这些宏定义选项的二进制编码具有一个共同特征:每个选项的二进制序列中仅有一位为1(O_RDONLY选项除外,其二进制值为全0,表示默认选项)。不同选项的置1位各不相同,这使得open函数内部可以通过简单的"与"运算来检测特定选项是否被设置。

int open(arg1, arg2, arg3)
{if (arg2&O_RDONLY)//检查是否设置了O_RDONLY选项{}if (arg2&O_WRONLY)//检查是否设置了O_WRONLY选项{}if (arg2&O_RDWR)//检查是否设置了O_RDWR选项{}if (arg2&O_CREAT)//检查是否设置了O_CREAT选项{}//...
}

3. open的第三个参数

        在 Unix/Linux 系统调用中,mode_t 是一个数据类型,用于表示文件的权限模式(permission mode)。它通常是一个无符号整数类型(如 unsigned int),用于指定文件的访问权限。

        当使用O_CREAT创建新文件时,必须指定mode参数,表示新文件的权限。mode通常用八进制表示,如0644。

例如,设置mode=0666会赋予文件-rw-rw-rw-的权限。

        需要注意的是,实际文件权限会受到umask(文件创建掩码)的影响。计算公式为:实际权限 = mode & (~umask)。在默认umask=0002的情况下,当mode=0666时,实际创建的权限为0664(即-rw-rw-r--)。

若要完全按照mode参数设置权限,可以在创建文件前调用umask(0)将掩码清零。

umask(0); //将文件默认掩码设置为0

注意: 当不需要创建文件时,open的第三个参数可以不必设置。 

    open() 函数在创建文件(使用 O_CREAT 标志)时,需要指定文件的权限模式 mode_t。这个参数决定了文件的读、写、执行权限,以及特殊权限位(如 setuid、setgid 等)。

基本权限位

mode_t 由多个权限位组合而成,可以使用八进制数或宏定义来设置:

宏定义八进制值权限说明
S_IRUSR0400用户(owner)可读
S_IWUSR0200用户可写
S_IXUSR0100用户可执行
S_IRGRP0040组(group)可读
S_IWGRP0020组可写
S_IXGRP0010组可执行
S_IROTH0004其他用户(others)可读
S_IWOTH0002其他用户可写
S_IXOTH0001其他用户可执行
特殊权限位
宏定义八进制值权限说明
S_ISUID04000设置用户ID(setuid)
S_ISGID02000设置组ID(setgid)
S_ISVTX01000粘滞位(sticky bit)
如何使用 mode_t
1. 直接使用八进制数

最常见的用法是直接使用 3位八进制数 来设置权限:

int fd = open("example.txt", O_CREAT | O_WRONLY, 0644);
  • 0644 表示:

    • 用户(owner)64+2,即 rw-

    • 组(group)4r--

    • 其他用户(others)4r--

2. 使用宏定义组合

也可以使用宏定义组合:

int fd = open("example.txt", O_CREAT | O_WRONLY, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
  • 等同于 0644rw-r--r--

3. 设置特殊权限

例如,设置 setuid 权限(仅对可执行文件有效):

int fd = open("program", O_CREAT | O_WRONLY, S_IRWXU | S_ISUID);
  • S_IRWXU = 0700rwx------

  • S_ISUID = 04000(设置 setuid 位)

  • 最终权限:4700rws------

mode_t 的实际影响
  • open() 的 mode 参数仅在 O_CREAT 时生效(如果文件已存在,则不会修改权限)。

  • 最终权限会受到 umask 的影响:

    mode_t final_mode = mode & ~umask;

    例如,如果 umask=002,而 mode=0666,则实际权限是 0664rw-rw-r--)。

5、返回值

open函数的返回值是新打开文件的文件描述符。

  • 成功:成功时返回一个非负整数文件描述符。
  • 失败:失败时返回-1并设置errno。

我们可以尝试一次打开多个文件,然后分别打印它们的文件描述符: 

#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
int main()
{umask(0);int fd1 = open("log1.txt", O_RDONLY | O_CREAT, 0666);int fd2 = open("log2.txt", O_RDONLY | O_CREAT, 0666);int fd3 = open("log3.txt", O_RDONLY | O_CREAT, 0666);int fd4 = open("log4.txt", O_RDONLY | O_CREAT, 0666);int fd5 = open("log5.txt", O_RDONLY | O_CREAT, 0666);printf("fd1:%d\n", fd1);printf("fd2:%d\n", fd2);printf("fd3:%d\n", fd3);printf("fd4:%d\n", fd4);printf("fd5:%d\n", fd5);return 0;
}

运行程序后可以看到,打开文件的文件描述符是从3开始连续且递增的:

我们再尝试打开一个根本不存在的文件,也就是open函数打开文件失败: 

#include <stdio.h>                                                                                       
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{int fd = open("test.txt", O_RDONLY);printf("%d\n", fd);return 0;
}

运行程序后可以看到,打开文件失败时获取到的文件描述符是-1: 

        文件描述符本质上是一个指针数组的索引,该数组中的每个指针都指向一个已打开文件的文件信息。通过文件描述符即可访问对应的文件信息。

        当open函数成功打开文件时,系统会扩展指针数组并返回新增指针的索引值;若打开失败则直接返回-1。因此,连续成功打开多个文件时,获得的文件描述符是依次递增的。

        Linux进程默认打开三个标准文件描述符:0(标准输入)、1(标准输出)和2(标准错误)。这就是新打开文件时,文件描述符从3开始分配的原因。

open 函数的具体使用方式取决于应用场景:

  • 若目标文件不存在,需要创建新文件,则使用带三个参数的 open(第三个参数表示创建文件的默认权限)
  • 若文件已存在,则使用带两个参数的 open

五、接口介绍:close()函数

close()函数是Linux/Unix系统中用于关闭已打开文件描述符的重要系统调用。

1、函数原型

#include <unistd.h>int close(int fd);

2、参数说明

fd(文件描述符)

        要关闭的文件描述符(file descriptor),这是之前通过open()creat()pipe()dup()等函数获得的文件描述符。

3、返回值

  • 成功时返回0

  • 失败时返回-1,并设置errno来指示错误原因

4、常见错误码(errno)

  • EBADF:fd不是有效的已打开文件描述符

  • EINTR:close操作被信号中断

  • EIO:发生了I/O错误

5、基本用法

#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>int main() {int fd = open("example.txt", O_RDONLY);if (fd == -1) {perror("open failed");return 1;}// 使用文件描述符进行读写操作...if (close(fd) == -1) {perror("close failed");return 1;}return 0;
}

6、深入理解close操作(了解)

  1. 资源释放

    • 关闭文件描述符会释放内核为该文件分配的所有资源

    • 释放文件描述符本身,使其可被后续的open()pipe()等调用重用

  2. 缓冲区刷新

    • 对于输出文件,close操作会确保所有缓冲数据被写入磁盘

    • 对于使用mmap()映射的文件,close操作不会解除映射,但关闭后访问映射内存可能导致SIGBUS信号

  3. 文件锁释放

    • 进程终止时所有文件描述符会自动关闭

    • 关闭文件描述符会释放该进程在该文件上设置的所有锁(使用fcntl()设置的锁)

7、注意事项(了解)

  1. 多次关闭

    • 重复关闭同一个文件描述符是错误行为

    • 在多线程环境中尤其需要注意,可能引发竞态条件

  2. 信号中断处理

    • 如果close()被信号中断,某些系统上需要重新调用close()

    • 更安全的做法是使用以下模式:

      while (close(fd) == -1) {if (errno != EINTR) {perror("close error");break;}// 如果是被信号中断,则继续尝试关闭
      }
  3. 文件描述符泄漏

    • 忘记关闭文件描述符是常见编程错误

    • 长期运行的进程可能导致文件描述符耗尽

    • 建议在打开文件后立即考虑关闭操作,使用goto或RAII模式管理资源。


六、接口介绍:write()函数

    write() 是Linux/Unix系统中一个非常重要的低级文件I/O函数,用于将数据写入文件描述符对应的文件或设备。

1. write函数原型

#include <unistd.h>ssize_t write(int fd, const void *buf, size_t count);

2. 参数说明

  • fd:文件描述符,通常由open()函数返回

  • buf:指向要写入数据的缓冲区的指针

  • count:要写入的字节数

3. 返回值

  • 成功时:返回实际写入的字节数(可能小于请求的count

  • 失败时:返回-1,并设置errno

4、对文件进行写入操作示例

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{int fd = open("log.txt", O_WRONLY | O_CREAT, 0666);if (fd < 0){perror("open");return 1;}const char* msg = "hello syscall\n";for (int i = 0; i < 5; i++){write(fd, msg, strlen(msg));}close(fd);return 0;
}

运行程序后,在当前路径下就会生成对应文件,文件当中就是我们写入的内容:

5. write函数的特点和注意事项

1. 部分写入

write()可能会执行部分写入,即返回值小于请求的字节数。这种情况常见于:

  • 磁盘空间不足

  • 被信号中断

  • 非阻塞模式下资源暂时不可用

2. 阻塞与非阻塞

  • 常规文件通常不会阻塞

  • 管道、套接字等特殊文件可能阻塞

  • 可以设置O_NONBLOCK标志使操作非阻塞

3. 原子性

对于常规文件,小于PIPE_BUF大小的写入是原子的(通常为4096字节)

4. 文件位置指针

write()操作会更新文件的当前位置指针


七、接口介绍:read()函数

    read() 函数是Linux/Unix系统中用于从文件描述符读取数据的基本系统调用之一。它是文件I/O操作的核心函数之一,属于POSIX标准的一部分。

1、函数原型

#include <unistd.h>ssize_t read(int fd, void *buf, size_t count);

2、参数说明

  1. fd (file descriptor):文件描述符,是一个整数值,指向要读取的文件

    • 通常由open()函数返回

    • 标准输入的文件描述符是0

  2. buf:指向内存缓冲区的指针,用于存放读取到的数据。必须预先分配足够的内存空间

  3. count:请求读取的字节数。通常是缓冲区的大小

3、返回值

  • 成功时:返回实际读取的字节数

    • 可能小于请求的字节数(例如接近文件末尾时)

    • 返回0表示到达文件末尾(EOF)

  • 失败时:返回-1,并设置errno

    • 常见的errno值:

      • EAGAIN/EWOULDBLOCK:非阻塞I/O且无数据可读

      • EBADF:无效的文件描述符

      • EINTR:被信号中断

      • EIO:I/O错误

4、对文件进行读取操作示例

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{int fd = open("log.txt", O_RDONLY);if (fd < 0){perror("open");return 1;}char ch;while (1){ssize_t s = read(fd, &ch, 1);if (s <= 0){break;}write(1, &ch, 1); //向文件描述符为1的文件写入数据,即向显示器写入数据}close(fd);return 0;
}

运行程序后,就会将我们刚才写入文件的内容读取出来,并打印在显示器上:


八、系统调用和库函数

        总的来说,我们更加能明白开始时提到的“实际上,各种语言的库函数都是对系统接口的封装实现。”这句话!!!

在了解返回值之前,需要明确两个重要概念:系统调用和库函数

  • fopen、fclose、fread、fwrite这些是C标准库提供的函数,称为库函数(libc)
  • 而open、close、read、write、lseek等则是操作系统直接提供的接口,称为系统调用
  • 这与我们之前讲解操作系统概念时展示的系统架构图是一致的

        系统调用接口与库函数的关系十分清晰。可以明确地说,f#系列函数是对系统调用的封装,为二次开发提供了便利。

http://www.lryc.cn/news/592570.html

相关文章:

  • 广东省省考备考(第四十九天7.18)——判断推理:位置规律(听课后强化训练)
  • *SFT深度实践指南:从数据构建到模型部署的全流程解析
  • Linux | Bash 子字符串提取
  • Redis原理之哨兵机制(Sentinel)
  • Android性能优化之网络优化
  • 【锂电池剩余寿命预测】TCN时间卷积神经网络锂电池剩余寿命预测(Pytorch完整源码和数据)
  • 如何用Python并发下载?深入解析concurrent.futures 与期物机制
  • 安卓Android项目 报错:系统找不到指定文件
  • python学智能算法(二十四)|SVM-最优化几何距离的理解
  • 【52】MFC入门到精通——MFC串口助手(二)---通信版(发送数据 、发送文件、数据转换、清空发送区、打开/关闭文件),附源码
  • 『 C++ 入门到放弃 』- set 和 map 容器
  • Java Web项目Dump文件分析指南
  • 开源Docmost知识库管理工具
  • spring-cloud微服务部署转单体部署-feign直连调用
  • Windows Server 版本之间有什么区别?
  • 在断网情况下,网线直接连接 Windows 笔记本和 Ubuntu 服务器进行数据传输
  • 华为业务变革项目IPD基本知识
  • 【HCI log】Google Pixel 手机抓取hci log
  • 京东店铺入鼎的全面分析与自研难度评估
  • 70 gdb attach $pid, process 2021 is already traced by process 2019
  • CCF编程能力等级认证GESP—C++4级—20250628
  • 协作机器人操作与编程-PE系统示教编程和脚本讲解(直播回放)
  • 自动化面试题
  • 搜广推校招面经九十五
  • 基于 WinForm 与虹软实现人脸识别功能:从理论到实践
  • 关于我用AI编写了一个聊天机器人……(11)
  • 《每日AI-人工智能-编程日报》--2025年7月18日
  • [JS逆向] 微信小程序逆向工程实战
  • 加速度计和气压计、激光互补滤波融合算法
  • 6月零售数据超预期引发市场波动:基于AI多因子模型的黄金价格解析