1. 系统调用与库函数概述
1.1 应用程序与内核的交互
- 内核(Kernel):操作系统的核心部分,负责管理硬件资源和提供服务。
- 保护机制:应用程序不能直接访问内核空间或直接进行数据访问、数据后续等操作,必须经过保护。
- 系统调用(System Call):
- 内核提供的接口,允许应用程序请求内核服务。
- 调用过程涉及从**用户空间(User Space)到内核空间(Kernel Space)**的切换。
- 这种空间切换会占用资源,产生开销。
- 缺点:系统调用与内核版本强相关。不同操作系统版本或平台可能有不同的系统调用实现,导致应用程序开发需要针对不同平台进行适配,增加了开发难度。
1.2 库函数(Library Functions)的作用
- 定义:库函数是位于用户空间实现的函数,是对底层系统调用的封装。
- 优势:
- 屏蔽底层差异:开发者只需关注库函数接口,无需关心下层系统调用的具体适配细节。
- 提高开发效率:简化了编程模型,提高了程序的可移植性。
- 底层机制:
- 库函数在实现功能时,最终仍会通过系统调用与内核进行服务请求。
- 即使是在用户空间,调用库函数仍会引起用户空间到内核空间的切换。
- 文件 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 参数详解
-
flags(标志位):
- 访问模式(必须指定其中一个):
O_RDONLY:只读。O_WRONLY:只写。O_RDWR:读写。
- 状态标志(可选,可组合):
O_CREAT:如果文件不存在,则创建文件。需配合第三个参数mode使用。O_EXCL:与O_CREAT联合使用。如果文件已存在,则open失败。用于确保文件是新建的,避免覆盖。O_TRUNC:如果文件存在且可写,将文件长度截断为 0。
- 组合方式:多个标志位使用按位或运算(
|)连接。例如:O_RDWR | O_CREAT。 - 原理:标志位本质是整数值,二进制位表示不同权限,通过位运算合并。
- 访问模式(必须指定其中一个):
-
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 限制)。
- 耗尽后无法再打开新文件,导致程序异常。
- 编程习惯:
open与close应配对使用,操作完成后立即关闭。
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)。 - 计算过程:
- umask 取反:
~0002->...111 111 101 - 按位与:
0666 & ~0002 - 结果:
0664(二进制110 110 100)。
- umask 取反:
- 现象:代码中指定
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 3或man 2),但部分接口较过时。 - 建议在程序中如需修改限制,参考最新文档使用新接口。
- 一般嵌入式或常规应用中,1024 的限制通常足够使用。
8. 总结与建议
- 学习路径:遇到新接口 -> 查
man手册(确认章节) -> 阅读 Synopsis 和 Description -> 编写代码验证 -> 处理返回值和错误。 - 头文件:务必包含
man手册中提示的头文件,否则编译链接会出错。 - 错误处理:系统调用和库函数都可能失败,必须检查返回值(特别是
-1情况)。 - 资源管理:打开的文件必须关闭,避免资源泄露。
- 权限意识:创建文件时注意
mode参数与实际umask的结合效果。 - 标志位使用:理解
O_CREAT、O_EXCL等标志位的组合逻辑,避免意外覆盖文件。