文件IO:文件描述符、打开与关闭

文件IO:文件描述符、打开与关闭

1. 系统调用与库函数概述

1.1 应用程序与内核的交互

  • 内核(Kernel):操作系统的核心部分,负责管理硬件资源和提供服务。
  • 保护机制:应用程序不能直接访问内核空间或直接进行数据访问、数据后续等操作,必须经过保护。
  • 系统调用(System Call)
    • 内核提供的接口,允许应用程序请求内核服务。
    • 调用过程涉及从**用户空间(User Space)内核空间(Kernel Space)**的切换。
    • 这种空间切换会占用资源,产生开销。
    • 缺点:系统调用与内核版本强相关。不同操作系统版本或平台可能有不同的系统调用实现,导致应用程序开发需要针对不同平台进行适配,增加了开发难度。

1.2 库函数(Library Functions)的作用

  • 定义:库函数是位于用户空间实现的函数,是对底层系统调用的封装。
  • 优势
    1. 屏蔽底层差异:开发者只需关注库函数接口,无需关心下层系统调用的具体适配细节。
    2. 提高开发效率:简化了编程模型,提高了程序的可移植性。
  • 底层机制
    • 库函数在实现功能时,最终仍会通过系统调用与内核进行服务请求。
    • 即使是在用户空间,调用库函数仍会引起用户空间到内核空间的切换。
  • 文件 I/O 与标准 I/O
    • 文件 I/O(File I/O):直接封装系统调用的函数接口。与硬件/内核强耦合,每次调用必定引起系统调用。适用于对系统调用频率要求不高或需要精确控制的场景。
    • 标准 I/O(Standard I/O):在文件 I/O 基础上进一步封装,带有缓冲区机制,减少系统调用次数。

2. 文件描述符(File Descriptor, FD)

2.1 文件在内核中的描述

  • 每个文件在系统内核空间中都有一个结构体描述,用于记录文件信息。
  • 结构体名称:struct file(注意是小写 f 开头,区别于大写的 File 结构体)。
  • 应用程序无法直接访问内核空间的结构体,需通过文件 I/O API 进行请求。

2.2 文件描述符的定义与范围

  • 定义:应用程序通过文件 I/O API 与内核交互后,内核返回一个整型值(索引),用于标识打开的文件。这个整型值称为文件描述符(FD)。
  • 数据类型:整型(int)。
  • 范围
    • 默认范围:​0​1023(共 1024 个)。
    • 属性:非负整数(Non-negative integer)。
    • 可扩展:系统默认限制为 1024,但可以通过配置修改扩充(嵌入式程序中 1024 通常够用)。
  • 分配机制
    • 从最小的可用整数开始分配。
    • 如果中间的 FD 被关闭释放,下次打开文件时会优先复用最小的空闲 FD。

2.3 标准文件描述符

在终端运行程序时,前三个 FD 被默认占用:

  • 0:标准输入(Standard Input, stdin)。
  • 1:标准输出(Standard Output, stdout)。
  • 2:标准错误(Standard Error, stderr)。
  • 用户文件:用户程序打开的新文件,FD 从 3 开始依次递增。
  • 意义:确保程序的输入输出错误信息能正确显示在当前终端,而不是错乱到其他终端。

3. 使用 man 手册(man pages)

3.1 man 手册的重要性

  • 程序员必备工具,用于查询接口细节、复习接口、解决编程错误。
  • Ubuntu 系统提供离线版本,也可查阅在线版本。

3.2 手册章节分类

查询时需注意章节号,不同章节对应不同类型的内容:

  • Section 1:可执行程序及 Shell 命令(例如 man 1 ls)。
  • Section 2:系统调用(System Calls),主要包含文件 I/O 接口(例如 man 2 open)。
  • Section 3:库函数(Library Functions),主要包含标准 I/O 接口(例如 man 3 printf)。
    • 注意:某些接口名可能在 Section 2 和 Section 3 中都存在(同名接口),需区分是系统调用实现还是标准 I/O 实现。

3.3 手册内容解读

  • NAME:命令或函数名称及简短描述。
  • SYNOPSIS:函数原型及所需包含的头文件(Header Files)。
    • 编程时必须包含对应的头文件,否则链接时会找不到符号。
  • DESCRIPTION:函数的详细描述,包括参数作用、功能行为。
  • RETURN VALUE:返回值说明。
    • 成功:通常返回非负值(如 FD)。
    • 失败:通常返回 -1
  • Linux vs POSIX
    • Linux 手册:涵盖接口最全,支持 Linux 特有扩展。
    • POSIX 手册:对应 Unix 标准接口。
    • 兼容性:Linux 兼容 POSIX 标准。如果在本地 man 查到接口即可使用;若需严格跨平台兼容,需参考 POSIX 标准手册确认接口支持情况。

4. open 函数详解与实践

4.1 函数原型与头文件

查阅 man 2 open 可得:

#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:文件路径名。
  • flags:打开文件的标志位(访问模式 + 状态标志)。
  • mode:文件权限模式(仅当创建文件时有效)。

4.2 参数详解

  1. flags(标志位)

    • 访问模式(必须指定其中一个):
      • O_RDONLY:只读。
      • O_WRONLY:只写。
      • O_RDWR:读写。
    • 状态标志(可选,可组合):
      • O_CREAT:如果文件不存在,则创建文件。需配合第三个参数 mode 使用。
      • O_EXCL:与 O_CREAT 联合使用。如果文件已存在,则 open 失败。用于确保文件是新建的,避免覆盖。
      • O_TRUNC:如果文件存在且可写,将文件长度截断为 0。
    • 组合方式:多个标志位使用按位或运算(|)连接。例如:O_RDWR | O_CREAT
    • 原理:标志位本质是整数值,二进制位表示不同权限,通过位运算合并。
  2. mode(权限模式)

    • 仅在指定 O_CREAT 标志时有效。
    • 指定新建文件的存取权限。
    • 写法
      • 八进制数值:如 0664
      • 宏定义:如 S_IRUSR | S_IWUSR | ...(较繁琐,推荐数值写法)。
    • 注意:最终权限受 umask 影响。

4.3 返回值处理

  • 成功:返回文件描述符(FD),值为 ​\ge 0 的整数。
  • 失败:返回 -1
  • 编程习惯
    • 必须检查返回值。
    • 使用 if (fd < 0)if (fd == -1) 判断错误。
    • 错误处理后可打印提示信息或退出。

4.4 代码实践示例

场景 1:打开已存在文件

int fd = open("1.txt", O_RDONLY);
if (fd < 0) {
    printf("open error\n");
} else {
    printf("open success, fd = %d\n", fd);
}
  • 若文件不存在,open 失败返回 -1。

场景 2:文件不存在时创建

// 使用 3 个参数,添加 O_CREAT
int fd = open("3.txt", O_RDWR | O_CREAT, 0664);
if (fd < 0) {
    printf("open error\n");
} else {
    printf("open success, fd = %d\n", fd);
}
  • 3.txt 不存在,则创建该文件,权限设置为 0664
  • 3.txt 已存在,则直接打开,不会清空内容(除非加 O_TRUNC)。

场景 3:确保文件不存在才创建(独占创建)

// 添加 O_EXCL 标志
int fd = open("4.txt", O_RDWR | O_CREAT | O_EXCL, 0664);
  • 4.txt 已存在,open 失败返回 -1。
  • 4.txt 不存在,创建成功。
  • 用途:防止意外覆盖已有文件。

5. close 函数详解

5.1 函数原型

查阅 man 2 close 可得:

#include <unistd.h>
int close(int fd);
  • 参数:需要关闭的文件描述符 fd
  • 返回值
    • 成功:返回 0
    • 失败:返回 -1(通常只有在 fd 无效时发生)。

5.2 关闭文件的重要性

  • 资源释放:打开文件会占用文件描述符资源。
  • 避免泄露
    • 若程序长期运行(如后台 daemon 进程)且不停打开文件而不关闭,会导致 FD 耗尽(达到 1024 限制)。
    • 耗尽后无法再打开新文件,导致程序异常。
  • 编程习惯openclose 应配对使用,操作完成后立即关闭。

5.3 代码示例

if (close(fd) == 0) {
    printf("close success\n");
} else {
    printf("close error\n");
}

6. 文件权限与 umask(掩码)

6.1 umask 的作用

  • 定义:用户文件创建掩码(User File Creation Mask)。
  • 影响:在创建文件时,系统会从指定的 mode 权限中减去(屏蔽)umask 指定的权限,得到最终的文件权限。
  • 原因:为了安全,系统默认限制新文件的权限,防止创建出权限过大的文件。

6.2 权限计算原理

  • 公式

    \text{最终权限} = \text{指定模式 (mode)} \ \& \ \sim\text{umask}

    即:指定权限与 umask 的反码进行按位与运算。

  • 示例

    • 指定模式:0666(二进制 110 110 110)。
    • 默认 umask:0002(二进制 000 000 010)。
    • 计算过程:
      1. umask 取反:~0002 -> ...111 111 101
      2. 按位与:0666 & ~0002
      3. 结果:0664(二进制 110 110 100)。
    • 现象:代码中指定 0666,实际创建文件权限为 0664,是因为 umask 屏蔽了其他用户的写权限。
  • 修改 umask

    • 命令行:umask 0000(临时修改当前终端环境)。
    • 函数:mode_t umask(mode_t mask);(在程序中修改,返回旧掩码)。

7. 资源限制与 ulimit

7.1 文件打开数量限制

  • 前文提到的默认 1024 个文件描述符限制,可通过 ulimit 命令查看。
  • 命令
    • ulimit -n:查看当前用户进程可打开的最大文件描述符数量(默认为 1024)。
    • ulimit -a:查看当前环境所有资源限制(包括文件大小、进程数、打开文件数等)。
  • 修改:可通过 ulimit -n 2048 临时修改当前会话限制。

7.2 编程中的限制查询

  • 虽然存在 getrlimit 等函数接口(man 3man 2),但部分接口较过时。
  • 建议在程序中如需修改限制,参考最新文档使用新接口。
  • 一般嵌入式或常规应用中,1024 的限制通常足够使用。

8. 总结与建议

  1. 学习路径:遇到新接口 -> 查 man 手册(确认章节) -> 阅读 Synopsis 和 Description -> 编写代码验证 -> 处理返回值和错误。
  2. 头文件:务必包含 man 手册中提示的头文件,否则编译链接会出错。
  3. 错误处理:系统调用和库函数都可能失败,必须检查返回值(特别是 -1 情况)。
  4. 资源管理:打开的文件必须关闭,避免资源泄露。
  5. 权限意识:创建文件时注意 mode 参数与实际 umask 的结合效果。
  6. 标志位使用:理解 O_CREATO_EXCL 等标志位的组合逻辑,避免意外覆盖文件。
Conda常用命令 2026-02-25
文件IO:读写 2026-02-25

评论区