123 <unistd.h> 库 笔记 📚
1
Gemini(2.5-Pro) Prompts:
2
请生成系统性、结构化的学习笔记,要求涵盖各个知识点。关于:c++的库。
3
**内容要求**如下:
4
1. 先写介绍,再写正文,最后做总结。
5
2. 合理使用emoji;
6
3. 不能遗漏任何知识点;
7
4. 对每个API(函数、变量)给出名称、用途、原型、代码、注释;
8
5. 在专业的角度指明最佳实践、常见错误用法、陷阱等;
9
**格式要求**如下:
10
1. **多级标题**:用 `## 1 一级标题` → `### 1.1 二级标题` → `### 1.1.1 三级标题` 的层级编号;
11
2. **单级条目**:所有 `1.` `2.` `3.` 这样的单级列表,改用带圈数字 `①` `②` `③` 表示;
12
3. **禁止使用**:Markdown 的 `-` `*` 无序列表或 `1.` `2.` 有序列表语法;
13
4. 请自动生成层级编号(如 1.1、1.2),无需我手动输入数字。不要大标题,正文从## 1. 开始;
14
5. 在表格或代码块的前后添加空行;
1. 介绍 📜
1.1 什么是 <unistd.h>
?
<unistd.h>
是 POSIX (Portable Operating System Interface) 标准定义的 C/C++ 头文件。它并不是 C++ 标准库的一部分,但在类 Unix 系统(如 Linux, macOS, BSD 等)上广泛可用。它提供了一系列访问操作系统底层功能的接口,主要是围绕进程控制、文件系统操作和基本 I/O 进行的系统调用封装。在 C++ 程序中包含并使用它,可以让我们直接与操作系统进行交互,实现更底层的控制。
1.2 <unistd.h>
的重要性
对于需要进行系统级编程(如创建管理进程、操作文件描述符、管道通信、修改文件权限等)的 C++ 应用程序来说,<unistd.h>
是不可或缺的。它提供了许多标准 C++ 库(如 <iostream>
, <fstream>
)未直接暴露的底层操作接口。理解和掌握 <unistd.h>
中的函数是编写健壮、高效的系统级应用程序的基础。
1.3 POSIX 标准与可移植性
<unistd.h>
定义的功能遵循 POSIX 标准。这意味着,理论上,使用了 <unistd.h>
功能的代码在不同的 POSIX 兼容系统上应该是可移植的。然而,需要注意:
① 非 POSIX 系统:Windows 系统默认不提供 <unistd.h>
。虽然可以通过 Cygwin, MinGW (配合 MSYS) 或 WSL (Windows Subsystem for Linux) 等环境来模拟或提供 POSIX API,但这并非原生支持。因此,直接依赖 <unistd.h>
的代码在原生 Windows 环境下无法编译运行。
② 实现差异:尽管有标准,但不同 Unix-like 系统在某些函数的具体行为或可用性上可能存在细微差异。
③ 功能可用性:某些函数(如特定 exec
变体)或常量可能受特定 POSIX 标准版本或系统配置的影响。可以使用 sysconf()
等函数查询系统对特定 POSIX 功能的支持情况。
2. 核心 API 详解 🛠️
<unistd.h>
包含了大量的函数和常量。下面分类介绍其中最常用和重要的部分。
2.1 进程控制 (Process Control)
2.1.1 fork()
① 名称: fork
② 用途: 创建一个新进程,这个新进程是调用进程(父进程)的副本(子进程)。
③ 原型: pid_t fork(void);
④ 代码:
1
#include <iostream>
2
#include <unistd.h>
3
#include <sys/wait.h> // 需要 waitpid
4
5
int main() {
6
pid_t pid = fork(); // 创建子进程
7
8
if (pid < 0) {
9
// fork 失败
10
perror("fork failed");
11
return 1;
12
} else if (pid == 0) {
13
// 子进程执行的代码
14
std::cout << "子进程: PID = " << getpid() << ", 父进程 PID = " << getppid() << std::endl;
15
sleep(1); // 模拟子进程工作
16
std::cout << "子进程: 退出" << std::endl;
17
_exit(0); // 子进程用 _exit() 退出,避免清理父进程的 stdio 缓冲区
18
} else {
19
// 父进程执行的代码
20
std::cout << "父进程: PID = " << getpid() << ", 创建的子进程 PID = " << pid << std::endl;
21
int status;
22
waitpid(pid, &status, 0); // 等待子进程结束
23
if (WIFEXITED(status)) {
24
std::cout << "父进程: 子进程正常退出, 退出码 = " << WEXITSTATUS(status) << std::endl;
25
} else {
26
std::cout << "父进程: 子进程异常终止" << std::endl;
27
}
28
}
29
30
return 0;
31
}
⑤ 注释:
* fork()
对父进程返回子进程的 PID,对子进程返回 0,失败则返回 -1 并设置 errno
。
* 子进程会继承父进程大部分状态,包括内存空间(写时复制)、文件描述符、环境变量等。
* 父进程通常需要使用 wait()
或 waitpid()
来等待子进程结束并回收其资源,防止产生僵尸进程。
2.1.2 exec 系列函数 (execl, execlp, execv, execvp, execle, execve)
① 名称: exec
系列 (如 execl
, execvp
等)
② 用途: 在当前进程的地址空间内加载并执行一个新的程序。成功调用后,原程序的代码和数据被新程序替换,不会返回到调用点(除非出错)。
③ 原型 (示例):
* int execl(const char *path, const char *arg0, ..., (char *) NULL);
* int execlp(const char *file, const char *arg0, ..., (char *) NULL);
* int execv(const char *path, char *const argv[]);
* int execvp(const char *file, char *const argv[]);
* int execve(const char *pathname, char *const argv[], char *const envp[]);
* int execle(const char *path, const char *arg0, ..., (char *) NULL, char *const envp[]);
④ 代码 (使用 execvp
):
1
#include <iostream>
2
#include <unistd.h>
3
#include <sys/wait.h>
4
#include <vector>
5
#include <string>
6
7
int main() {
8
pid_t pid = fork();
9
10
if (pid < 0) {
11
perror("fork failed");
12
return 1;
13
} else if (pid == 0) {
14
// 子进程: 准备执行 'ls -l /tmp'
15
std::cout << "子进程: 尝试执行 ls 命令..." << std::endl;
16
17
// 参数列表: argv[0] 是程序名, 后面是参数, 最后必须是 NULL
18
char *args[] = {(char*)"ls", (char*)"-l", (char*)"/tmp", NULL};
19
20
// execvp: 'p' 表示会在 PATH 环境变量中搜索 'ls'
21
// 'v' 表示参数以数组形式传递
22
execvp("ls", args);
23
24
// 如果 execvp 返回,说明出错了
25
perror("execvp failed");
26
_exit(1); // 出错则子进程退出
27
} else {
28
// 父进程
29
std::cout << "父进程: 等待子进程完成 ls 命令..." << std::endl;
30
wait(NULL); // 等待子进程结束
31
std::cout << "父进程: 子进程已结束" << std::endl;
32
}
33
34
return 0;
35
}
⑤ 注释:
* exec
函数命名约定:
* l
(list): 参数以可变参数列表形式传递,以 NULL
结尾。
* v
(vector): 参数以字符串数组 ( char *argv[]
) 形式传递,数组最后一个元素必须是 NULL
。
* p
(path): 如果 path
或 file
参数不包含 /
,则会在 PATH
环境变量指定的目录中搜索可执行文件。
* e
(environment): 允许传递一个新的环境变量数组 envp
给新程序,否则新程序继承当前环境。
* 所有 exec
函数成功时不返回,失败时返回 -1 并设置 errno
。
* 通常在 fork()
创建的子进程中调用 exec
系列函数。
2.1.3 _exit() 与 exit()
① 名称: _exit
, exit
② 用途: 终止当前进程。
③ 原型:
* void _exit(int status);
(<unistd.h>
)
* void exit(int status);
(<stdlib.h>
)
④ 代码: (见 fork()
示例中的子进程部分)
⑤ 注释:
* _exit()
: 直接终止进程,不执行 atexit()
注册的函数,不刷新标准 I/O 缓冲区 (如 stdout
)。它是系统调用 _exit(2)
的封装。通常在 fork()
后的子进程中使用,避免干扰父进程的 I/O 缓冲区或执行不应由子进程执行的清理操作。
* exit()
: 执行标准库的清理程序(调用 atexit
注册的函数、刷新并关闭所有打开的标准 I/O 流),然后终止进程。通常在主程序或不需要担心 fork
后副作用的情况下使用。
* status
是进程的退出状态码,低 8 位可被父进程通过 wait()
或 waitpid()
获取。
2.1.4 wait() 与 waitpid()
① 名称: wait
, waitpid
② 用途: 等待子进程状态改变(通常是终止),并获取子进程信息。
③ 原型:
* pid_t wait(int *wstatus);
* pid_t waitpid(pid_t pid, int *wstatus, int options);
④ 代码: (见 fork()
和 execvp()
示例中的父进程部分)
⑤ 注释:
* wait()
: 阻塞,直到任意一个子进程终止。返回终止子进程的 PID,失败返回 -1。
* waitpid()
: 提供更灵活的等待方式。
* pid > 0
: 等待指定 PID 的子进程。
* pid == -1
: 等待任意子进程 (同 wait()
)。
* pid == 0
: 等待同一进程组的任意子进程。
* pid < -1
: 等待指定进程组 ID (-pid
) 的任意子进程。
* options
: 可以是 0 (阻塞等待) 或 WNOHANG
(非阻塞检查) 等。
* wstatus
: 如果非 NULL
,则用于存储子进程的状态信息。需要使用宏 (如 WIFEXITED
, WEXITSTATUS
, WIFSIGNALED
, WTERMSIG
等,定义在 <sys/wait.h>
) 来解析状态。
* 调用 wait
或 waitpid
是回收已终止子进程(僵尸进程)资源的标准方法。
2.1.5 getpid() 与 getppid()
① 名称: getpid
, getppid
② 用途: 获取当前进程的 ID (getpid
) 和父进程的 ID (getppid
)。
③ 原型:
* pid_t getpid(void);
* pid_t getppid(void);
④ 代码: (见 fork()
示例)
⑤ 注释:
* 这两个函数总是成功,且不需要特殊权限。
* 进程 ID (PID) 是操作系统用来唯一标识活动进程的非负整数。
* 父进程 ID (PPID) 指向创建当前进程的那个进程。如果父进程已退出,孤儿进程可能会被 init
进程 (PID 1) 或其他进程领养,getppid()
会返回新的父进程 PID。
2.1.6 sleep() 与 usleep()
① 名称: sleep
, usleep
② 用途: 使当前进程暂停执行指定的秒数 (sleep
) 或微秒数 (usleep
)。
③ 原型:
* unsigned int sleep(unsigned int seconds);
* int usleep(useconds_t usec);
(usleep
在 POSIX.1-2001 中被标记为废弃,推荐使用 nanosleep
from <time.h>
)
④ 代码:
1
#include <iostream>
2
#include <unistd.h>
3
4
int main() {
5
std::cout << "开始..." << std::endl;
6
std::cout << "暂停 2 秒..." << std::endl;
7
sleep(2); // 暂停 2 秒
8
std::cout << "暂停 500 毫秒 (500000 微秒)..." << std::endl;
9
usleep(500000); // 暂停 0.5 秒
10
std::cout << "结束." << std::endl;
11
return 0;
12
}
⑤ 注释:
* sleep()
: 如果进程在休眠期间收到信号且信号处理函数未中断系统调用,sleep()
可能会提前返回,返回值为剩余未休眠的秒数。如果休眠完成,返回 0。
* usleep()
: 用于更短时间的休眠。行为与信号交互方面类似 sleep
。由于精度和可移植性问题,现代编程更推荐使用 <time.h>
中的 nanosleep()
或 select()
/ pselect()
的超时参数。
* 暂停期间进程不消耗 CPU 时间 (处于阻塞状态)。
2.1.7 alarm()
① 名称: alarm
② 用途: 安排在指定的秒数后向调用进程发送 SIGALRM
信号。
③ 原型: unsigned int alarm(unsigned int seconds);
④ 代码:
1
#include <iostream>
2
#include <unistd.h>
3
#include <signal.h>
4
5
void alarm_handler(int sig) {
6
std::cout << "\n收到 SIGALRM 信号! (" << sig << ")" << std::endl;
7
// 在实际应用中,这里可能设置一个标志位,主循环检测
8
}
9
10
int main() {
11
signal(SIGALRM, alarm_handler); // 设置 SIGALRM 的处理函数
12
13
std::cout << "设置一个 3 秒后的闹钟。" << std::endl;
14
unsigned int remaining = alarm(3);
15
std::cout << "之前的闹钟剩余时间 (如果有): " << remaining << " 秒" << std::endl;
16
17
std::cout << "主程序继续执行,等待闹钟..." << std::endl;
18
// 模拟主程序工作
19
for (int i = 0; i < 5; ++i) {
20
std::cout << "." << std::flush;
21
sleep(1);
22
}
23
std::cout << "\n主程序结束。" << std::endl; // 可能在 alarm 触发前或后结束
24
25
return 0;
26
}
⑤ 注释:
* alarm(0)
会取消任何先前设置的闹钟。
* 每次调用 alarm()
会覆盖之前的设置。它返回上次调用 alarm()
时剩余的秒数。
* SIGALRM
的默认动作是终止进程。因此,通常需要配合 signal()
或 sigaction()
来设置信号处理函数。
* alarm
和 sleep
/ usleep
/ nanosleep
等函数可能相互影响,具体行为依赖于系统实现。
2.1.8 pause()
① 名称: pause
② 用途: 使调用进程挂起(睡眠),直到捕捉到一个信号。
③ 原型: int pause(void);
④ 代码:
1
#include <iostream>
2
#include <unistd.h>
3
#include <signal.h>
4
5
volatile sig_atomic_t signal_received = 0; // volatile 防止优化
6
7
void handler(int sig) {
8
std::cout << "\n在 handler 中: 收到信号 " << sig << std::endl;
9
signal_received = 1;
10
}
11
12
int main() {
13
signal(SIGINT, handler); // 捕获 Ctrl+C (SIGINT)
14
15
std::cout << "进程 PID: " << getpid() << std::endl;
16
std::cout << "发送 SIGINT (例如按 Ctrl+C) 来唤醒进程..." << std::endl;
17
18
while (signal_received == 0) {
19
pause(); // 挂起直到信号被捕获并处理
20
// pause 返回后,信号处理函数已经执行完毕
21
}
22
23
std::cout << "pause() 被信号唤醒,程序继续..." << std::endl;
24
std::cout << "主程序结束。" << std::endl;
25
26
return 0;
27
}
⑤ 注释:
* pause()
只有在捕获到一个信号 并且 该信号的处理函数返回后,pause()
才会返回。
* pause()
总是返回 -1,并设置 errno
为 EINTR
(表示被信号中断)。
* 存在竞态条件:如果在检查 signal_received
之后、调用 pause()
之前信号到达,pause()
可能会永久阻塞。更健壮的方法是使用 sigsuspend()
。
2.2 文件描述符与 I/O (File Descriptors & I/O)
2.2.1 文件描述符常量 (STDIN_FILENO, STDOUT_FILENO, STDERR_FILENO)
① 名称: STDIN_FILENO
, STDOUT_FILENO
, STDERR_FILENO
② 用途: 代表标准输入、标准输出和标准错误的文件描述符。它们是整数常量。
③ 原型: 这些是宏定义,通常在 <unistd.h>
中定义。
* STDIN_FILENO
(通常为 0)
* STDOUT_FILENO
(通常为 1)
* STDERR_FILENO
(通常为 2)
④ 代码:
1
#include <iostream>
2
#include <unistd.h>
3
#include <string.h> // for strlen
4
5
int main() {
6
const char* msg_out = "这是一条写到标准输出的消息。\n";
7
const char* msg_err = "这是一条写到标准错误的消息。\n";
8
9
// 使用 write 系统调用直接写入文件描述符
10
write(STDOUT_FILENO, msg_out, strlen(msg_out));
11
write(STDERR_FILENO, msg_err, strlen(msg_err));
12
13
// 可以用 isatty 检查文件描述符是否连接到终端
14
if (isatty(STDIN_FILENO)) {
15
const char* prompt = "标准输入是一个终端。\n";
16
write(STDOUT_FILENO, prompt, strlen(prompt));
17
}
18
19
return 0;
20
}
⑤ 注释:
* 文件描述符是小的非负整数,内核用它来标识进程打开的文件(或其他 I/O 资源,如管道、套接字)。
* 这三个标准描述符在进程启动时通常是自动打开的,分别连接到终端(默认)或通过重定向连接到文件或管道。
2.2.2 read()
① 名称: read
② 用途: 从文件描述符 fd
指向的文件读取数据到缓冲区 buf
。
③ 原型: ssize_t read(int fd, void *buf, size_t count);
④ 代码:
1
#include <iostream>
2
#include <unistd.h>
3
#include <fcntl.h> // for open()
4
#include <errno.h> // for errno
5
#include <string.h> // for strerror
6
7
#define BUFFER_SIZE 10
8
9
int main() {
10
char buffer[BUFFER_SIZE];
11
ssize_t bytes_read;
12
13
std::cout << "尝试从标准输入读取最多 " << BUFFER_SIZE -1 << " 字节 (输入后按 Enter):" << std::endl;
14
15
// 从标准输入 (fd=0) 读取
16
bytes_read = read(STDIN_FILENO, buffer, BUFFER_SIZE - 1); // 留一个位置给 '\0'
17
18
if (bytes_read < 0) {
19
// 读取错误
20
perror("read failed");
21
return 1;
22
} else if (bytes_read == 0) {
23
// 到达文件末尾 (EOF) 或管道/套接字关闭
24
std::cout << "读取到文件末尾 (EOF) 或连接关闭。" << std::endl;
25
} else {
26
// 成功读取
27
buffer[bytes_read] = '\0'; // 添加空终止符,以便作为字符串处理
28
std::cout << "成功读取 " << bytes_read << " 字节: \"" << buffer << "\"" << std::endl;
29
}
30
31
return 0;
32
}
⑤ 注释:
* fd
: 要读取的文件描述符。
* buf
: 指向存储读取数据的内存缓冲区的指针。
* count
: 最多尝试读取的字节数。
* 返回值:
* 成功: 返回实际读取的字节数 (可能小于 count
)。对于普通文件,到达文件末尾时返回 0。
* 失败: 返回 -1,并设置 errno
。常见错误有 EBADF
(无效描述符), EINTR
(被信号中断), EIO
(I/O 错误)。
* read
是一个阻塞调用(默认情况下)。如果数据不可用,它会等待。对于非阻塞 I/O,需要使用 fcntl()
设置 O_NONBLOCK
标志。
* 需要注意 read
不会自动添加空终止符 (\0
),处理文本数据时需要手动添加。
2.2.3 write()
① 名称: write
② 用途: 将缓冲区 buf
中的数据写入文件描述符 fd
指向的文件。
③ 原型: ssize_t write(int fd, const void *buf, size_t count);
④ 代码: (见 STDIN_FILENO
示例)
⑤ 注释:
* fd
: 要写入的文件描述符。
* buf
: 指向包含要写入数据的内存缓冲区的指针。
* count
: 尝试写入的字节数。
* 返回值:
* 成功: 返回实际写入的字节数。这个值可能小于 count
(例如,磁盘空间不足,或管道/套接字缓冲区满)。需要循环写入以确保所有数据都被发送。
* 失败: 返回 -1,并设置 errno
。常见错误 EBADF
, EINTR
, EPIPE
(写入已关闭的管道/套接字), ENOSPC
(设备无空间)。
* write
也是阻塞调用(默认)。
2.2.4 close()
① 名称: close
② 用途: 关闭一个打开的文件描述符,释放与之关联的内核资源。
③ 原型: int close(int fd);
④ 代码:
1
#include <iostream>
2
#include <unistd.h>
3
#include <fcntl.h> // for open()
4
#include <errno.h>
5
#include <string.h>
6
7
int main() {
8
const char* filename = "temp_close_test.txt";
9
// O_WRONLY: 只写, O_CREAT: 不存在则创建, O_TRUNC: 存在则清空
10
// 0644: 文件权限 (所有者读写, 组用户读, 其他用户读)
11
int fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC, 0644);
12
13
if (fd < 0) {
14
perror("open failed");
15
return 1;
16
}
17
std::cout << "文件 '" << filename << "' 打开成功, fd = " << fd << std::endl;
18
19
const char* data = "Hello, close!\n";
20
ssize_t written = write(fd, data, strlen(data));
21
if (written < 0) {
22
perror("write failed");
23
// 仍然尝试关闭文件描述符
24
} else {
25
std::cout << "写入 " << written << " 字节。" << std::endl;
26
}
27
28
// 关闭文件描述符
29
if (close(fd) == -1) {
30
perror("close failed");
31
// 即使关闭失败,文件描述符在进程退出时也会被关闭
32
return 1; // 但最好报告错误
33
}
34
35
std::cout << "文件描述符 " << fd << " 已关闭。" << std::endl;
36
unlink(filename); // 删除测试文件
37
38
return 0;
39
}
⑤ 注释:
* 成功返回 0,失败返回 -1 并设置 errno
。
* 关闭文件描述符非常重要,可以防止资源泄漏。一个进程能打开的文件描述符数量是有限的。
* close()
之后,该 fd
值可能会被后续的 open()
, pipe()
, socket()
等调用重用。
* 当进程退出时,所有未关闭的文件描述符会被自动关闭。但在长时间运行的程序中,显式 close()
是必须的。
2.2.5 lseek()
① 名称: lseek
② 用途: 重新定位文件描述符 fd
关联的打开文件的读写偏移量(文件指针)。
③ 原型: off_t lseek(int fd, off_t offset, int whence);
④ 代码:
1
#include <iostream>
2
#include <unistd.h>
3
#include <fcntl.h>
4
#include <errno.h>
5
#include <string.h>
6
7
int main() {
8
const char* filename = "temp_lseek_test.txt";
9
int fd = open(filename, O_RDWR | O_CREAT, 0644);
10
if (fd < 0) { perror("open failed"); return 1; }
11
12
write(fd, "0123456789", 10);
13
14
// 移动到文件开头
15
off_t pos = lseek(fd, 0, SEEK_SET);
16
if (pos == (off_t)-1) { perror("lseek SEEK_SET failed"); goto cleanup; }
17
std::cout << "移动到文件开头,当前位置: " << pos << std::endl;
18
19
// 从当前位置向后移动 5 字节
20
pos = lseek(fd, 5, SEEK_CUR);
21
if (pos == (off_t)-1) { perror("lseek SEEK_CUR failed"); goto cleanup; }
22
std::cout << "向后移动 5 字节,当前位置: " << pos << std::endl; // 应为 5
23
24
char buffer[3];
25
read(fd, buffer, 2); // 读取 '56'
26
buffer[2] = '\0';
27
std::cout << "读取 2 字节: \"" << buffer << "\", 当前位置: "
28
<< lseek(fd, 0, SEEK_CUR) << std::endl; // 应为 7
29
30
// 移动到距离文件末尾 3 字节处
31
pos = lseek(fd, -3, SEEK_END);
32
if (pos == (off_t)-1) { perror("lseek SEEK_END failed"); goto cleanup; }
33
std::cout << "移动到距末尾 3 字节处,当前位置: " << pos << std::endl; // 应为 7
34
35
write(fd, "XYZ", 3); // 覆盖 '789' 为 'XYZ'
36
37
// 获取当前文件大小
38
pos = lseek(fd, 0, SEEK_END);
39
std::cout << "写入后,文件大小 (当前位置): " << pos << std::endl; // 应为 10
40
41
cleanup:
42
close(fd);
43
unlink(filename);
44
return 0;
45
}
⑤ 注释:
* offset
: 位移量。
* whence
: 决定 offset
的基准点:
* SEEK_SET
: 从文件开头计算。offset
必须非负。
* SEEK_CUR
: 从当前文件指针位置计算。offset
可以是正或负。
* SEEK_END
: 从文件末尾计算。offset
可以是正或负。
* 返回值: 成功时返回相对于文件开头的新的偏移量(字节数)。失败时返回 (off_t)-1
并设置 errno
。
* lseek()
仅适用于可寻址(seekable)的文件描述符,如普通文件。对管道、套接字、终端等使用 lseek()
会失败 (ESPIPE
)。
* lseek(fd, 0, SEEK_CUR)
可用于获取当前文件指针位置而不改变它。
* lseek(fd, 0, SEEK_END)
可用于获取文件大小(如果文件指针移动到末尾)。
2.2.6 pipe()
① 名称: pipe
② 用途: 创建一个管道(pipe),用于进程间通信。管道是单向的字节流。
③ 原型: int pipe(int pipefd[2]);
④ 代码:
1
#include <iostream>
2
#include <unistd.h>
3
#include <sys/wait.h>
4
#include <string.h>
5
6
int main() {
7
int pipefd[2]; // pipefd[0] 用于读, pipefd[1] 用于写
8
9
if (pipe(pipefd) == -1) {
10
perror("pipe failed");
11
return 1;
12
}
13
14
pid_t pid = fork();
15
16
if (pid < 0) {
17
perror("fork failed");
18
close(pipefd[0]); // 关闭管道两端
19
close(pipefd[1]);
20
return 1;
21
} else if (pid == 0) {
22
// 子进程: 读取管道
23
close(pipefd[1]); // 子进程关闭写端
24
25
char buffer[100];
26
ssize_t bytes_read = read(pipefd[0], buffer, sizeof(buffer) - 1);
27
if (bytes_read > 0) {
28
buffer[bytes_read] = '\0';
29
std::cout << "子进程: 从管道读取到 \"" << buffer << "\"" << std::endl;
30
} else if (bytes_read == 0) {
31
std::cout << "子进程: 管道写端已关闭。" << std::endl;
32
} else {
33
perror("子进程 read failed");
34
}
35
close(pipefd[0]); // 关闭读端
36
_exit(0);
37
38
} else {
39
// 父进程: 写入管道
40
close(pipefd[0]); // 父进程关闭读端
41
42
const char* message = "来自父进程的消息!";
43
ssize_t written = write(pipefd[1], message, strlen(message));
44
if (written > 0) {
45
std::cout << "父进程: 向管道写入了 " << written << " 字节。" << std::endl;
46
} else {
47
perror("父进程 write failed");
48
}
49
close(pipefd[1]); // 关闭写端,这会导致子进程的 read 返回 0 (EOF)
50
51
wait(NULL); // 等待子进程结束
52
std::cout << "父进程: 子进程已结束。" << std::endl;
53
}
54
55
return 0;
56
}
⑤ 注释:
* pipefd
是一个包含两个文件描述符的数组:pipefd[0]
是管道的读端,pipefd[1]
是管道的写端。
* 数据写入 pipefd[1]
后,可以从 pipefd[0]
读出。遵循先进先出 (FIFO) 原则。
* 通常在 fork()
之前调用 pipe()
,然后在父子进程中根据需要关闭不使用的管道端(例如,父写子读时,父关闭读端,子关闭写端)。
* 如果所有持有管道写端描述符的进程都关闭了它,那么对管道读端的 read()
将返回 0 (EOF)。
* 如果所有持有管道读端描述符的进程都关闭了它,那么对管道写端的 write()
将失败,进程会收到 SIGPIPE
信号(默认终止进程),write()
返回 -1 且 errno
设为 EPIPE
。
* 管道有容量限制(通常几 KB 到几十 KB),写入超过容量会阻塞,直到有数据被读走。
2.2.7 dup() 与 dup2()
① 名称: dup
, dup2
② 用途: 复制一个现有的文件描述符。
③ 原型:
* int dup(int oldfd);
* int dup2(int oldfd, int newfd);
④ 代码:
1
#include <iostream>
2
#include <unistd.h>
3
#include <fcntl.h>
4
#include <string.h>
5
#include <errno.h>
6
7
int main() {
8
const char* filename = "output_dup.txt";
9
int fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC, 0644);
10
if (fd < 0) { perror("open failed"); return 1; }
11
12
std::cout << "文件 '" << filename << "' 打开, fd = " << fd << std::endl;
13
14
// 使用 dup2 将标准输出 (STDOUT_FILENO, fd=1) 重定向到文件 fd
15
int saved_stdout = dup(STDOUT_FILENO); // 保存原始的标准输出 fd
16
if (saved_stdout < 0) { perror("dup saved_stdout failed"); close(fd); return 1; }
17
18
if (dup2(fd, STDOUT_FILENO) == -1) { // 让 fd 1 指向文件
19
perror("dup2 failed");
20
close(fd);
21
close(saved_stdout);
22
return 1;
23
}
24
// 此后,所有写入 STDOUT_FILENO 的内容都会写入文件
25
26
// close(fd); // 现在 fd 可以关闭了,因为 fd 1 已经指向了同一个文件表项
27
28
std::cout << "这条消息会写入文件 '" << filename << "'。" << std::endl;
29
fprintf(stdout, "这条使用 fprintf(stdout, ...) 的消息也会写入文件。\n");
30
31
fflush(stdout); // 确保缓冲区刷新到文件
32
33
// 恢复标准输出
34
if (dup2(saved_stdout, STDOUT_FILENO) == -1) {
35
perror("dup2 restore failed");
36
// 严重错误,但继续尝试清理
37
}
38
close(saved_stdout); // 关闭保存的 fd
39
close(fd); // 如果之前没关,现在关
40
41
std::cout << "这条消息会再次输出到终端。" << std::endl;
42
43
unlink(filename); // 删除测试文件
44
return 0;
45
}
⑤ 注释:
* dup(oldfd)
: 创建一个新的文件描述符,该描述符是当前可用文件描述符中数值最小的那个,并且它指向与 oldfd
相同的底层文件表项(文件状态标志、当前文件偏移量等是共享的,但文件描述符标志如 FD_CLOEXEC
是独立的)。成功返回新的文件描述符,失败返回 -1。
* dup2(oldfd, newfd)
: 使 newfd
指向与 oldfd
相同的文件表项。如果 newfd
已经打开,dup2
会先原子地关闭它。如果 oldfd
等于 newfd
,dup2
不做任何事直接返回 newfd
。成功返回 newfd
,失败返回 -1。
* dup
和 dup2
常用于实现 I/O 重定向,尤其是在 fork()
和 exec()
结合使用时,子进程可以通过 dup2
将标准输入/输出/错误重定向到管道或文件,然后再执行新程序。
* 共享文件表项意味着通过任一描述符(oldfd
或新 fd
)修改文件状态(如 lseek
移动指针)会影响到另一个描述符。
2.3 文件系统操作 (File System Operations)
2.3.1 chdir()
① 名称: chdir
② 用途: 更改当前进程的工作目录。
③ 原型: int chdir(const char *path);
④ 代码:
1
#include <iostream>
2
#include <unistd.h>
3
#include <limits.h> // For PATH_MAX (though getcwd handles allocation)
4
#include <stdlib.h> // For free() if using getcwd(NULL, 0)
5
6
int main() {
7
char original_cwd[PATH_MAX];
8
if (getcwd(original_cwd, sizeof(original_cwd)) == NULL) {
9
perror("getcwd original failed");
10
return 1;
11
}
12
std::cout << "当前工作目录: " << original_cwd << std::endl;
13
14
// 尝试切换到 /tmp 目录
15
if (chdir("/tmp") == -1) {
16
perror("chdir to /tmp failed");
17
// return 1; // 可以选择退出或继续
18
} else {
19
std::cout << "成功切换到 /tmp" << std::endl;
20
char new_cwd[PATH_MAX];
21
if (getcwd(new_cwd, sizeof(new_cwd)) != NULL) {
22
std::cout << "切换后的工作目录: " << new_cwd << std::endl;
23
} else {
24
perror("getcwd after chdir failed");
25
}
26
27
// 切换回原来的目录
28
if (chdir(original_cwd) == -1) {
29
perror("chdir back to original failed");
30
return 1;
31
}
32
std::cout << "已切换回原目录。" << std::endl;
33
}
34
return 0;
35
}
⑤ 注释:
* path
: 指向目标目录路径的字符串。可以是绝对路径或相对路径(相对于当前工作目录)。
* 成功返回 0,失败返回 -1 并设置 errno
。常见错误 EACCES
(权限不足), ENOENT
(路径不存在), ENOTDIR
(路径组成部分不是目录)。
* 当前工作目录是进程解析相对路径的基础。它被子进程继承。
2.3.2 getcwd()
① 名称: getcwd
② 用途: 获取当前工作目录的绝对路径名。
③ 原型: char *getcwd(char *buf, size_t size);
④ 代码: (见 chdir()
示例)
⑤ 注释:
* buf
: 指向用于存储路径名的缓冲区。
* size
: 缓冲区 buf
的大小(字节数)。
* 返回值:
* 成功: 返回指向 buf
的指针。
* 失败: 返回 NULL
并设置 errno
。常见错误 ERANGE
(缓冲区太小), EACCES
(无权限读取父目录)。
* 如果 buf
是 NULL
,getcwd
会使用 malloc()
动态分配足够大的缓冲区。调用者之后需要使用 free()
释放返回的指针。这是 GNU 扩展,但在许多系统上可用 (getcwd(NULL, 0)
)。
* 路径名长度可能超过 PATH_MAX
,使用固定大小缓冲区时需谨慎。
2.3.3 unlink()
① 名称: unlink
② 用途: 从文件系统中删除一个名字(硬链接)。如果这个名字是文件的最后一个链接,并且没有进程打开该文件,则文件本身被删除,空间被回收。
③ 原型: int unlink(const char *pathname);
④ 代码: (见 close()
和 lseek()
示例最后的清理部分)
⑤ 注释:
* pathname
: 要删除的文件路径名。
* 成功返回 0,失败返回 -1 并设置 errno
。常见错误 EACCES
(无权限), ENOENT
(文件不存在), EISDIR
(尝试删除目录,应用 rmdir
)。
* 如果文件有多个硬链接,unlink
只减少链接计数。
* 如果文件被某个进程打开时 unlink
被调用,文件名会立即从目录中消失(其他进程无法再通过该名称 open
它),但文件内容会保留,直到最后一个持有该文件描述符的进程关闭它。这常用于创建临时文件:打开文件 -> unlink
文件名 -> 使用文件描述符 -> 关闭描述符(此时文件自动删除)。
2.3.4 rmdir()
① 名称: rmdir
② 用途: 删除一个空目录。
③ 原型: int rmdir(const char *pathname);
④ 代码:
1
#include <iostream>
2
#include <unistd.h>
3
#include <sys/stat.h> // For mkdir
4
#include <errno.h>
5
6
int main() {
7
const char* dirname = "temp_empty_dir";
8
9
// 创建一个临时目录
10
if (mkdir(dirname, 0755) == -1) { // 0755: rwxr-xr-x
11
// 可能已存在,如果不是 ENOTDIR 错误,尝试删除
12
if (errno != EEXIST) {
13
perror("mkdir failed");
14
return 1;
15
}
16
std::cout << "目录 '" << dirname << "' 可能已存在。" << std::endl;
17
} else {
18
std::cout << "目录 '" << dirname << "' 创建成功。" << std::endl;
19
}
20
21
// 删除空目录
22
if (rmdir(dirname) == -1) {
23
perror("rmdir failed");
24
// 可能因为目录非空 (ENOTEMPTY) 或权限不足 (EACCES)
25
return 1;
26
}
27
28
std::cout << "目录 '" << dirname << "' 删除成功。" << std::endl;
29
return 0;
30
}
⑤ 注释:
* pathname
: 要删除的目录路径名。
* 成功返回 0,失败返回 -1 并设置 errno
。常见错误 EACCES
, ENOENT
, ENOTDIR
, ENOTEMPTY
(目录非空)。
* 只能删除空目录。要删除非空目录,需要先递归删除其内容。
2.3.5 link()
① 名称: link
② 用途: 创建一个指向现有文件的新硬链接(hard link)。
③ 原型: int link(const char *oldpath, const char *newpath);
④ 代码:
1
#include <iostream>
2
#include <unistd.h>
3
#include <fcntl.h> // for open, O_CREAT
4
#include <errno.h>
5
6
int main() {
7
const char* original = "original_file.txt";
8
const char* hardlink = "hardlink_to_original.txt";
9
10
// 1. 创建一个原始文件
11
int fd = open(original, O_WRONLY | O_CREAT | O_TRUNC, 0644);
12
if (fd < 0) { perror("open original failed"); return 1; }
13
write(fd, "Some data", 9);
14
close(fd);
15
std::cout << "原始文件 '" << original << "' 已创建。" << std::endl;
16
17
// 2. 创建硬链接
18
if (link(original, hardlink) == -1) {
19
perror("link failed");
20
unlink(original); // 清理
21
return 1;
22
}
23
std::cout << "硬链接 '" << hardlink << "' 已创建,指向 '" << original << "'。" << std::endl;
24
25
// 3. 验证: 删除原始文件,硬链接应该仍然可以访问
26
if (unlink(original) == -1) {
27
perror("unlink original failed");
28
} else {
29
std::cout << "原始文件 '" << original << "' 已删除。" << std::endl;
30
int fd_link = open(hardlink, O_RDONLY);
31
if (fd_link >= 0) {
32
std::cout << "硬链接 '" << hardlink << "' 仍然可以打开。文件内容还在。" << std::endl;
33
close(fd_link);
34
} else {
35
perror("open hardlink failed after original unlink");
36
}
37
}
38
39
// 清理硬链接
40
unlink(hardlink);
41
42
return 0;
43
}
⑤ 注释:
* oldpath
: 现有文件的路径。
* newpath
: 要创建的新链接的路径。
* 成功返回 0,失败返回 -1。常见错误 EEXIST
(newpath
已存在), ENOENT
(oldpath
不存在), EPERM
(不允许链接目录,或跨文件系统链接)。
* 硬链接是文件系统中指向同一个 inode(文件元数据)的多个目录项。它们共享文件内容和大部分属性。删除任何一个链接(包括原始文件名)不会影响其他链接,直到最后一个链接被删除且文件不再被打开,文件内容才会被回收。
* 不能为目录创建硬链接(通常是为了防止文件系统循环)。
* 硬链接通常不能跨越不同的文件系统(挂载点)。
2.3.6 symlink()
① 名称: symlink
② 用途: 创建一个符号链接(symbolic link 或 soft link)。
③ 原型: int symlink(const char *target, const char *linkpath);
④ 代码:
1
#include <iostream>
2
#include <unistd.h>
3
#include <fcntl.h>
4
#include <errno.h>
5
6
int main() {
7
const char* target_file = "target_file.txt";
8
const char* symlink_name = "symlink_to_target";
9
10
// 1. 创建目标文件
11
int fd = open(target_file, O_WRONLY | O_CREAT | O_TRUNC, 0644);
12
if (fd < 0) { perror("open target failed"); return 1; }
13
write(fd, "Target content", 14);
14
close(fd);
15
std::cout << "目标文件 '" << target_file << "' 已创建。" << std::endl;
16
17
// 2. 创建符号链接
18
// target 参数存储在符号链接文件中,可以是相对或绝对路径,甚至可以是不存在的路径
19
if (symlink(target_file, symlink_name) == -1) {
20
perror("symlink failed");
21
unlink(target_file); // 清理
22
return 1;
23
}
24
std::cout << "符号链接 '" << symlink_name << "' 已创建,指向 '" << target_file << "'。" << std::endl;
25
26
// 3. 验证: 读取符号链接(通常会解析到目标文件)
27
char buffer[50];
28
// readlink() 可以读取链接本身的内容,而不是目标文件
29
// ssize_t len = readlink(symlink_name, buffer, sizeof(buffer)-1);
30
// if (len != -1) { buffer[len] = '\0'; std::cout << "Symlink content: " << buffer << std::endl; }
31
32
int fd_link = open(symlink_name, O_RDONLY); // 打开符号链接会打开目标文件
33
if (fd_link >= 0) {
34
ssize_t bytes = read(fd_link, buffer, sizeof(buffer)-1);
35
if (bytes > 0) {
36
buffer[bytes] = '\0';
37
std::cout << "通过符号链接读取到目标内容: \"" << buffer << "\"" << std::endl;
38
}
39
close(fd_link);
40
} else {
41
perror("open symlink failed");
42
}
43
44
// 4. 清理
45
unlink(symlink_name);
46
unlink(target_file);
47
48
return 0;
49
}
⑤ 注释:
* target
: 符号链接指向的路径字符串。这个字符串被存储在符号链接文件本身。它可以是任何字符串,包括不存在的路径(悬空链接)。
* linkpath
: 要创建的符号链接文件的路径。
* 成功返回 0,失败返回 -1。常见错误 EEXIST
(linkpath
已存在), EACCES
(无权限)。
* 符号链接是一个特殊类型的文件,其内容是它指向的另一个文件的路径名。
* 删除符号链接本身 (unlink(linkpath)
) 不会影响目标文件。删除目标文件会导致符号链接悬空。
* 符号链接可以指向目录,也可以跨文件系统。
2.3.7 access()
① 名称: access
② 用途: 检查调用进程是否对指定文件拥有特定的访问权限(读、写、执行)或文件是否存在。
③ 原型: int access(const char *pathname, int mode);
④ 代码:
1
#include <iostream>
2
#include <unistd.h>
3
#include <fcntl.h>
4
#include <errno.h>
5
6
int main() {
7
const char* filename = "temp_access_test.txt";
8
9
// 创建一个只读文件
10
int fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC, 0444); // r--r--r--
11
if (fd < 0) { perror("open failed"); return 1; }
12
write(fd, "read only", 9);
13
close(fd);
14
15
std::cout << "检查文件 '" << filename << "' 的权限:" << std::endl;
16
17
// 检查文件是否存在
18
if (access(filename, F_OK) == 0) {
19
std::cout << " - 文件存在 (F_OK)" << std::endl;
20
} else {
21
perror(" - access F_OK failed");
22
}
23
24
// 检查读权限
25
if (access(filename, R_OK) == 0) {
26
std::cout << " - 有读权限 (R_OK)" << std::endl;
27
} else {
28
perror(" - access R_OK failed"); // 可能因为文件不存在或其他错误
29
}
30
31
// 检查写权限
32
if (access(filename, W_OK) == 0) {
33
std::cout << " - 有写权限 (W_OK)" << std::endl;
34
} else {
35
// 预期失败,因为文件是只读的
36
if (errno == EACCES) {
37
std::cout << " - 没有写权限 (W_OK) - 符合预期 (EACCES)" << std::endl;
38
} else {
39
perror(" - access W_OK failed unexpectedly");
40
}
41
}
42
43
// 检查执行权限
44
if (access(filename, X_OK) == 0) {
45
std::cout << " - 有执行权限 (X_OK)" << std::endl;
46
} else {
47
if (errno == EACCES) {
48
std::cout << " - 没有执行权限 (X_OK) - 符合预期 (EACCES)" << std::endl;
49
} else {
50
perror(" - access X_OK failed unexpectedly");
51
}
52
}
53
54
unlink(filename);
55
return 0;
56
}
⑤ 注释:
* pathname
: 要检查的文件路径。
* mode
: 一个位掩码,指定要检查的权限,可以是以下值的按位或 (|):
* F_OK
: 检查文件是否存在。
* R_OK
: 检查读权限。
* W_OK
: 检查写权限。
* X_OK
: 检查执行权限 (如果是目录,则为搜索权限)。
* 返回值: 成功 (拥有权限或文件存在) 返回 0。失败 (无权限、文件不存在等) 返回 -1 并设置 errno
。
* 重要陷阱 (TOCTOU): access()
检查的是调用进程的 真实 用户/组 ID,而不是 有效 用户/组 ID (除非在某些特殊系统上)。这使得它不适用于 set-UID 或 set-GID 程序中判断权限后再执行操作,因为检查时的权限可能与实际执行操作时的权限(基于有效 ID)不同。存在时间窗口(Time-of-check to time-of-use, TOCTOU)漏洞:在 access()
检查和实际 open()
或其他操作之间,文件的权限或状态可能被改变。因此,最佳实践是直接尝试操作 (如 open()
) 并处理可能发生的错误,而不是先用 access()
检查。access()
主要用于快速检查文件是否存在或脚本中判断权限。
2.4 用户与组 ID (User & Group IDs)
2.4.1 getuid() 与 geteuid()
① 名称: getuid
, geteuid
② 用途: 获取进程的真实用户 ID (getuid
) 和有效用户 ID (geteuid
)。
③ 原型:
* uid_t getuid(void);
* uid_t geteuid(void);
④ 代码:
1
#include <iostream>
2
#include <unistd.h>
3
#include <sys/types.h> // For uid_t
4
5
int main() {
6
uid_t real_uid = getuid();
7
uid_t effective_uid = geteuid();
8
9
std::cout << "真实用户 ID (Real UID): " << real_uid << std::endl;
10
std::cout << "有效用户 ID (Effective UID): " << effective_uid << std::endl;
11
12
if (real_uid == effective_uid) {
13
std::cout << "真实 UID 和有效 UID 相同。" << std::endl;
14
} else {
15
// 这种情况通常发生在 set-UID 程序中
16
std::cout << "真实 UID 和有效 UID 不同 (可能是 set-UID 程序)。" << std::endl;
17
}
18
19
return 0;
20
}
⑤ 注释:
* 真实用户 ID (RUID): 启动该进程的用户的 ID。通常在登录时确定,子进程继承父进程的 RUID。
* 有效用户 ID (EUID): 内核用来决定进程对文件和其他资源的访问权限的 ID。通常等于 RUID。但在执行 set-UID 程序时,EUID 会临时变为该程序文件的所有者的 UID,从而获得该用户的权限。
* 这两个函数总是成功。
2.4.2 getgid() 与 getegid()
① 名称: getgid
, getegid
② 用途: 获取进程的真实组 ID (getgid
) 和有效组 ID (getegid
)。
③ 原型:
* gid_t getgid(void);
* gid_t getegid(void);
④ 代码:
1
#include <iostream>
2
#include <unistd.h>
3
#include <sys/types.h> // For gid_t
4
5
int main() {
6
gid_t real_gid = getgid();
7
gid_t effective_gid = getegid();
8
9
std::cout << "真实组 ID (Real GID): " << real_gid << std::endl;
10
std::cout << "有效组 ID (Effective GID): " << effective_gid << std::endl;
11
12
if (real_gid == effective_gid) {
13
std::cout << "真实 GID 和有效 GID 相同。" << std::endl;
14
} else {
15
// 这种情况通常发生在 set-GID 程序中
16
std::cout << "真实 GID 和有效 GID 不同 (可能是 set-GID 程序)。" << std::endl;
17
}
18
19
return 0;
20
}
⑤ 注释:
* 真实组 ID (RGID): 启动进程的用户的组 ID。
* 有效组 ID (EGID): 内核用来决定进程基于组的访问权限的 ID。通常等于 RGID。在执行 set-GID 程序时,EGID 会临时变为该程序文件的组 ID。
* 还有 保存的 set-user-ID (suid) 和 保存的 set-group-ID (sgid),以及 辅助组 ID 列表,这些更复杂,但 getgid
/getegid
获取的是主要的组 ID。
* 这两个函数总是成功。
2.4.3 setuid() 与 setgid()
① 名称: setuid
, setgid
② 用途: 设置进程的用户 ID (setuid
) 或组 ID (setgid
)。其行为依赖于进程当前的权限(是否是 root 或具有 CAP_SETUID
/CAP_SETGID
能力)。
③ 原型:
* int setuid(uid_t uid);
* int setgid(gid_t gid);
④ 代码: (注意:此代码通常需要 root 权限才能成功改变 UID/GID 到非自身的值)
1
#include <iostream>
2
#include <unistd.h>
3
#include <sys/types.h>
4
#include <errno.h>
5
#include <string.h> // strerror
6
7
int main() {
8
uid_t original_euid = geteuid();
9
uid_t target_uid = 1000; // 假设这是一个普通用户的 UID
10
11
std::cout << "当前有效 UID: " << original_euid << std::endl;
12
13
// 只有 root (EUID=0) 或具有 CAP_SETUID 能力的进程
14
// 才能将 EUID/RUID/SUID 设置为任意值。
15
// 非特权进程只能在 RUID, EUID, SUID 之间切换,
16
// 或将 EUID 设置为 RUID 或 SUID。
17
// POSIX.1 定义了复杂的规则。
18
19
if (original_euid == 0) {
20
std::cout << "以 root 身份运行,尝试将 UID 设置为 " << target_uid << std::endl;
21
// root 调用 setuid(非零) 会设置 RUID, EUID, SUID 都为该值
22
if (setuid(target_uid) == -1) {
23
perror("setuid failed");
24
return 1;
25
}
26
std::cout << "setuid 成功! 新有效 UID: " << geteuid() << ", 新真实 UID: " << getuid() << std::endl;
27
// 此时权限已永久降低 (通常情况下)
28
} else {
29
std::cout << "非 root 用户,无法随意改变 UID。" << std::endl;
30
// 尝试将 EUID 改回 RUID (如果它们不同的话,例如在 set-UID 程序中)
31
uid_t original_ruid = getuid();
32
if (original_euid != original_ruid) {
33
std::cout << "尝试将有效 UID 从 " << original_euid << " 恢复为真实 UID " << original_ruid << std::endl;
34
if (setuid(original_ruid) == -1) {
35
perror("setuid to real uid failed");
36
} else {
37
std::cout << "恢复有效 UID 成功! 新有效 UID: " << geteuid() << std::endl;
38
}
39
}
40
}
41
// setgid 的行为与 setuid 类似,但操作的是 GID (RGID, EGID, SGID)
42
43
return 0;
44
}
⑤ 注释:
* setuid(uid)
的行为:
* 特权进程 (EUID=0): RUID, EUID, 和 saved set-user-ID (SUID) 都被设置为 uid
。权限被永久改变(除非 uid
是 0)。
* 非特权进程 (EUID!=0): 只能将 EUID 设置为当前的 RUID 或当前的 SUID。RUID 和 SUID 不变。
* setgid(gid)
的行为与 setuid
类似,作用于 RGID, EGID, 和 saved set-group-ID (SGID)。
* 成功返回 0,失败返回 -1 并设置 errno
(EPERM
表示权限不足)。
* 最佳实践与安全: set-UID/set-GID 程序应尽可能早地放弃特权。通常的做法是:程序启动时具有高权限 (EUID=0 或 EUID=文件所有者),执行需要特权的操作,然后立即调用 setgid()
和 setuid()
将 EGID/EUID(以及可能的 RGID/RUID/SGID/SUID)降级为非特权用户的 ID。顺序很重要:通常先 setgid
再 setuid
。
* 现代 Linux 系统推荐使用 Capabilities (权能) 机制来更细粒度地控制权限,而不是依赖 set-UID root 程序。
2.5 终端控制 (Terminal Control)
2.5.1 isatty()
① 名称: isatty
② 用途: 判断一个文件描述符是否连接到一个终端设备。
③ 原型: int isatty(int fd);
④ 代码: (见 STDIN_FILENO
示例)
⑤ 注释:
* fd
: 要检查的文件描述符。
* 返回值: 如果 fd
连接到终端,返回 1。否则返回 0,并可能设置 errno
(例如 ENOTTY
表示不是终端,EBADF
表示 fd
无效)。
* 常用于判断标准输入/输出/错误是否连接到交互式终端,以便程序决定是进行交互式输入输出(如显示提示符)还是进行批处理式操作(如直接读写数据)。
2.5.2 ttyname()
① 名称: ttyname
② 用途: 获取连接到文件描述符 fd
的终端设备的路径名。
③ 原型: char *ttyname(int fd);
④ 代码:
1
#include <iostream>
2
#include <unistd.h>
3
#include <errno.h>
4
5
int main() {
6
// 检查标准输入是否是终端
7
if (isatty(STDIN_FILENO)) {
8
char* terminal_name = ttyname(STDIN_FILENO);
9
if (terminal_name != NULL) {
10
std::cout << "标准输入连接到终端: " << terminal_name << std::endl;
11
// 注意:ttyname 返回的指针可能指向静态内存,下次调用会被覆盖。
12
// 如果需要保存,应复制字符串。
13
} else {
14
perror("ttyname(STDIN_FILENO) failed");
15
}
16
} else {
17
std::cout << "标准输入不是一个终端。" << std::endl;
18
}
19
20
// 检查一个无效的 fd
21
int invalid_fd = 99;
22
if (isatty(invalid_fd)) {
23
std::cout << "Fd " << invalid_fd << " is a tty (unexpected)." << std::endl;
24
} else {
25
if (errno == EBADF) {
26
std::cout << "Fd " << invalid_fd << " is not a tty (errno=EBADF, expected)." << std::endl;
27
} else {
28
std::cout << "Fd " << invalid_fd << " is not a tty (errno=" << errno << ")." << std::endl;
29
}
30
}
31
32
return 0;
33
}
⑤ 注释:
* fd
: 必须是连接到终端的文件描述符。
* 返回值: 成功时返回指向终端设备路径名字符串的指针。该字符串存储在静态内存中,不可修改,且可能被后续调用 ttyname
或其他库函数覆盖。如果需要持久存储,应使用 strcpy
或 strdup
复制。失败(fd
不是终端或出错)时返回 NULL
并设置 errno
。
* ttyname_r()
是一个线程安全的可重入版本,它将路径名存入用户提供的缓冲区。
2.6 系统配置 (System Configuration)
2.6.1 sysconf()
① 名称: sysconf
② 用途: 在运行时获取 POSIX 系统定义的配置限制或选项的值。
③ 原型: long sysconf(int name);
④ 代码:
1
#include <iostream>
2
#include <unistd.h>
3
#include <errno.h>
4
5
int main() {
6
long value;
7
8
// 获取系统时钟频率 (ticks per second)
9
errno = 0; // 清零以便区分返回值 -1 是错误还是合法值
10
value = sysconf(_SC_CLK_TCK);
11
if (value == -1) {
12
if (errno != 0) {
13
perror("sysconf(_SC_CLK_TCK) failed");
14
} else {
15
std::cout << "_SC_CLK_TCK: 不支持或无限制 (-1)。" << std::endl;
16
}
17
} else {
18
std::cout << "系统时钟频率 (_SC_CLK_TCK): " << value << " Hz" << std::endl;
19
}
20
21
// 获取子进程最大数量
22
errno = 0;
23
value = sysconf(_SC_CHILD_MAX);
24
if (value == -1) {
25
if (errno != 0) {
26
perror("sysconf(_SC_CHILD_MAX) failed");
27
} else {
28
std::cout << "_SC_CHILD_MAX: 不支持或无限制 (-1)。" << std::endl;
29
}
30
} else {
31
std::cout << "每个用户的最大子进程数 (_SC_CHILD_MAX): " << value << std::endl;
32
}
33
34
// 检查 POSIX 版本
35
errno = 0;
36
value = sysconf(_SC_VERSION);
37
if (value == -1) {
38
// ... error check ...
39
std::cout << "_SC_VERSION: 查询失败或不支持。" << std::endl;
40
} else {
41
std::cout << "支持的 POSIX.1 版本 (_SC_VERSION): " << value << std::endl; // 如 200809L 表示 POSIX.1-2008
42
}
43
44
// 获取打开文件描述符的最大数量
45
errno = 0;
46
value = sysconf(_SC_OPEN_MAX);
47
if (value == -1) {
48
// ... error check ...
49
std::cout << "_SC_OPEN_MAX: 查询失败或无限制。" << std::endl;
50
} else {
51
std::cout << "每个进程最大打开文件描述符数 (_SC_OPEN_MAX): " << value << std::endl;
52
}
53
54
55
return 0;
56
}
⑤ 注释:
* name
: 一个整数常量 (宏定义在 <unistd.h>
或其他地方),指定要查询的系统配置项,如 _SC_ARG_MAX
(最大参数字节数), _SC_PAGESIZE
(页面大小), _SC_NPROCESSORS_ONLN
(在线处理器数) 等。
* 返回值: 成功时返回查询的值。如果 name
无效、系统不支持该选项或该选项没有限制,返回 -1。
* 重要: 因为 -1 也可能是某些配置项的有效值,所以在调用 sysconf()
前应将 errno
清零,调用后如果返回 -1,再检查 errno
是否非零来判断是真的出错了还是 -1 是合法返回值。
2.6.2 pathconf() 与 fpathconf()
① 名称: pathconf
, fpathconf
② 用途: 获取与文件或文件系统相关的配置限制或选项的值。pathconf
通过路径名查询,fpathconf
通过已打开的文件描述符查询。
③ 原型:
* long pathconf(const char *path, int name);
* long fpathconf(int fd, int name);
④ 代码:
1
#include <iostream>
2
#include <unistd.h>
3
#include <fcntl.h>
4
#include <errno.h>
5
6
int main() {
7
long value;
8
const char* filename = "/"; // 查询根目录所在文件系统的信息
9
10
// 查询文件名最大长度
11
errno = 0;
12
value = pathconf(filename, _PC_NAME_MAX);
13
if (value == -1) {
14
if (errno != 0) {
15
perror("pathconf(_PC_NAME_MAX) failed");
16
} else {
17
std::cout << filename << " 的 _PC_NAME_MAX: 无限制 (-1)。" << std::endl;
18
}
19
} else {
20
std::cout << filename << " 文件系统允许的最大文件名长度 (_PC_NAME_MAX): " << value << std::endl;
21
}
22
23
// 查询路径名最大长度
24
errno = 0;
25
value = pathconf(filename, _PC_PATH_MAX);
26
if (value == -1) {
27
if (errno != 0) {
28
perror("pathconf(_PC_PATH_MAX) failed");
29
} else {
30
std::cout << filename << " 的 _PC_PATH_MAX: 无限制 (-1)。" << std::endl;
31
}
32
} else {
33
std::cout << filename << " 文件系统允许的最大路径名长度 (_PC_PATH_MAX): " << value << std::endl;
34
}
35
36
// 使用 fpathconf 通过文件描述符查询
37
int fd = open(filename, O_RDONLY | O_DIRECTORY); // 打开目录需要 O_DIRECTORY (Linux)
38
if (fd < 0) {
39
perror("open / failed");
40
return 1;
41
}
42
43
errno = 0;
44
value = fpathconf(fd, _PC_LINK_MAX); // 查询文件的最大硬链接数
45
if (value == -1) {
46
if (errno != 0) {
47
perror("fpathconf(_PC_LINK_MAX) failed");
48
} else {
49
std::cout << "Fd " << fd << " 的 _PC_LINK_MAX: 无限制 (-1)。" << std::endl;
50
}
51
} else {
52
std::cout << "文件描述符 " << fd << " 指向的文件允许的最大硬链接数 (_PC_LINK_MAX): " << value << std::endl;
53
}
54
55
close(fd);
56
return 0;
57
}
⑤ 注释:
* path
: 文件或目录的路径名 (pathconf
)。
* fd
: 已打开的文件描述符 (fpathconf
)。
* name
: 一个整数常量 (宏定义在 <unistd.h>
等),指定要查询的文件相关配置项,如 _PC_LINK_MAX
(最大链接数), _PC_MAX_CANON
(最大规范输入行长度), _PC_PIPE_BUF
(管道原子写大小), _PC_CHOWN_RESTRICTED
(chown 是否受限) 等。
* 返回值和错误检查方式与 sysconf
类似:成功返回值,失败或无限制返回 -1,需要检查 errno
来区分。
2.7 其他函数与常量
2.7.1 getopt()
① 名称: getopt
(及其相关的全局变量 optarg
, optind
, opterr
, optopt
)
② 用途: 解析命令行选项(参数)。这是 C 程序中处理命令行参数的标准(但略显陈旧)方式。
③ 原型: int getopt(int argc, char * const argv[], const char *optstring);
④ 代码:
1
#include <iostream>
2
#include <unistd.h> // For getopt and extern variables
3
#include <string>
4
5
// 全局变量由 getopt 使用
6
extern char *optarg; // 指向当前选项的参数 (如果该选项需要参数)
7
extern int optind; // 下一个要处理的 argv 元素的索引
8
extern int opterr; // 如果非零 (默认), getopt 打印错误消息到 stderr
9
extern int optopt; // 当 getopt 检测到无效选项时,包含该无效选项字符
10
11
int main(int argc, char *argv[]) {
12
int opt;
13
std::string output_file = "";
14
bool verbose = false;
15
16
// opterr = 0; // 如果不想让 getopt 自动打印错误消息,取消注释此行
17
18
// optstring:
19
// "o:" 表示 -o 选项需要一个参数
20
// "v" 表示 -v 选项不需要参数
21
const char *optstring = "o:v";
22
23
// optind 初始化为 1,getopt 会从 argv[1] 开始处理
24
while ((opt = getopt(argc, argv, optstring)) != -1) {
25
switch (opt) {
26
case 'o':
27
output_file = optarg; // optarg 指向 -o 后面的参数
28
std::cout << "选项 -o 发现,参数: " << output_file << std::endl;
29
break;
30
case 'v':
31
verbose = true;
32
std::cout << "选项 -v 发现 (verbose)" << std::endl;
33
break;
34
case '?': // 处理无效选项或缺少参数的情况
35
if (optopt == 'o') {
36
fprintf(stderr, "选项 -%c 需要一个参数。\n", optopt);
37
} else if (isprint(optopt)) {
38
fprintf(stderr, "未知选项 `-%c`。\n", optopt);
39
} else {
40
fprintf(stderr, "未知选项字符 `\\x%x`。\n", optopt);
41
}
42
// usage(); // 通常这里会打印用法信息并退出
43
return 1;
44
default:
45
// 不应该到达这里
46
abort();
47
}
48
}
49
50
std::cout << "命令行选项解析完成。" << std::endl;
51
std::cout << "Verbose 模式: " << (verbose ? "开启" : "关闭") << std::endl;
52
if (!output_file.empty()) {
53
std::cout << "输出文件: " << output_file << std::endl;
54
} else {
55
std::cout << "未指定输出文件。" << std::endl;
56
}
57
58
// 处理非选项参数 (文件名等)
59
// getopt 处理完所有选项后,optind 指向第一个非选项参数
60
if (optind < argc) {
61
std::cout << "剩余的非选项参数:" << std::endl;
62
for (int i = optind; i < argc; ++i) {
63
std::cout << " argv[" << i << "] = " << argv[i] << std::endl;
64
}
65
} else {
66
std::cout << "没有非选项参数。" << std::endl;
67
}
68
69
return 0;
70
}
71
72
/* 编译运行示例:
73
g++ your_code.cpp -o your_program
74
./your_program -v -o output.log input1.txt input2.txt
75
./your_program -x # 无效选项
76
./your_program -o # 缺少参数
77
*/
⑤ 注释:
* argc
, argv
: main 函数的参数。
* optstring
: 一个字符串,定义了期望的选项字母。
* 字母本身 (如 v
) 表示一个布尔型选项 (-v
)。
* 字母后跟冒号 (如 o:
) 表示该选项需要一个参数 (-o filename
)。参数紧跟在选项后 (-ofilename
) 或用空格隔开 (-o filename
)。
* 字母后跟两个冒号 (如 f::
) (GNU 扩展) 表示参数是可选的。
* getopt
在每次调用时处理一个选项。它返回找到的选项字母,或在遇到无效选项或缺少参数时返回 ?
,或在处理完所有选项后返回 -1。
* getopt
会重新排列 argv
数组,将所有非选项参数移到最后。
* getopt
不是线程安全的,因为它使用外部全局变量。可使用 getopt_long
(处理长选项如 --verbose
) 或 getopt_long_only
(GNU 扩展),或者更现代的 C++ 参数解析库 (如 Boost.Program_options, argparse (Python风格的库), CLI11 等)。
2.7.2 _POSIX_VERSION 常量
① 名称: _POSIX_VERSION
② 用途: 一个宏常量,定义了系统声称符合的 POSIX.1 标准的版本。
③ 原型: #define _POSIX_VERSION 200809L
(示例值,表示 POSIX.1-2008)
④ 代码:
1
#include <iostream>
2
#include <unistd.h> // _POSIX_VERSION 定义在这里
3
4
int main() {
5
#ifdef _POSIX_VERSION
6
std::cout << "系统声称符合 POSIX.1 版本: " << _POSIX_VERSION << std::endl;
7
// 检查特定的版本或更高版本
8
#if _POSIX_VERSION >= 200112L // POSIX.1-2001
9
std::cout << " (至少支持 POSIX.1-2001)" << std::endl;
10
#endif
11
#if _POSIX_VERSION >= 200809L // POSIX.1-2008
12
std::cout << " (至少支持 POSIX.1-2008)" << std::endl;
13
#endif
14
#else
15
std::cout << "_POSIX_VERSION 未定义,系统可能不完全符合 POSIX.1。" << std::endl;
16
#endif
17
18
// 还可以查询更具体的 POSIX 选项是否支持,例如:
19
#ifdef _POSIX_THREADS
20
std::cout << "系统支持 POSIX 线程 (Threads) 选项。" << std::endl;
21
#endif
22
#ifdef _POSIX_REALTIME_SIGNALS
23
std::cout << "系统支持 POSIX 实时信号 (Realtime Signals) 选项。" << std::endl;
24
#endif
25
// 还有很多其他的 _POSIX_* 宏...
26
27
// 注意:编译时检查 (_POSIX_VERSION 宏) 和运行时检查 (sysconf(_SC_VERSION))
28
// 可能给出不同的信息,运行时检查通常更可靠地反映当前内核的能力。
29
30
return 0;
31
}
⑤ 注释:
* _POSIX_VERSION
的值是一个 long
型整数,格式通常是 YYYYMM L
,表示标准发布的年月。
* 这个宏在编译时可用,用于条件编译,根据系统支持的 POSIX 版本包含或排除代码。
* 除了 _POSIX_VERSION
,还有许多其他的 _POSIX_*
宏(如 _POSIX_THREADS
, _POSIX_SEMAPHORES
, _POSIX_MESSAGE_QUEUES
等)用于指示系统是否支持特定的 POSIX 选项。这些宏如果被定义,通常表示支持;其值(如果有)可能进一步指示支持的细节或版本。
* 使用 sysconf()
进行运行时检查通常是更推荐的方式,因为它反映了实际运行环境的能力,而不是编译环境的声明。
3. 最佳实践与注意事项 ⚠️
① 错误处理 (Error Handling):
* 检查返回值: 大部分 <unistd.h>
函数在失败时返回特定值(通常是 -1,少数如 fork
返回负值),成功时返回非负值。必须 检查这些返回值。
* 检查 errno
: 当函数返回错误指示时,全局变量 errno
(定义在 <errno.h>
) 会被设置为一个指示错误类型的正整数常量(如 EACCES
, ENOENT
, EINTR
)。应该在调用可能失败的函数 之前 清除 errno
(如果需要区分 -1 是合法值还是错误),或者在确认函数失败 之后 立即检查 errno
的值。
* 使用 perror()
或 strerror()
: perror(const char *s)
会向标准错误输出用户提供的前缀字符串 s
,后跟一个冒号和一个描述当前 errno
值的错误消息。strerror(int errnum)
返回一个指向描述错误码 errnum
的字符串的指针。使用它们可以提供更友好的错误信息。
* 处理 EINTR
: 某些阻塞的系统调用(如 read
, write
, pause
, wait
, sleep
)可能会被信号中断,此时它们会返回 -1 并将 errno
设置为 EINTR
。如果希望操作在被中断后继续,需要编写一个循环来重新尝试调用。
② 可移植性考量 (Portability):
* <unistd.h>
是 POSIX 标准,主要用于 Unix-like 系统。在 Windows 上原生不可用。如果需要跨平台,应考虑使用 C++ 标准库(如 <filesystem>
for 文件操作)、Boost 库或特定平台的 API(通过条件编译 #ifdef _WIN32
... #else
... #endif
)。
* 即使在 POSIX 系统间,也可能存在细微的行为差异或某些函数/选项的可用性差异。使用 sysconf
, pathconf
或编译时 _POSIX_*
宏来检查特性支持。
③ 信号处理 (Signal Handling Interactions):
* 许多 <unistd.h>
函数(特别是阻塞调用)会与信号交互。例如,sleep()
或 pause()
会被信号唤醒。read
/write
可能因 EINTR
而中断。设计信号处理函数时要小心(使用异步信号安全函数,避免复杂操作),并准备好处理系统调用被中断的情况。
* alarm()
发送 SIGALRM
,需要正确处理此信号。
④ 资源管理 (Resource Management):
* 文件描述符: 打开的文件描述符是有限资源。确保使用 close()
关闭不再需要的文件描述符。特别是在循环或长期运行的程序中。fork()
会复制文件描述符,父子进程可能需要关闭不需要的副本(如管道的两端)。使用 dup2
重定向后,旧的描述符通常可以关闭。考虑使用 fcntl()
设置 FD_CLOEXEC
标志,使得文件描述符在 exec
调用后自动关闭,避免泄露给新程序。
* 子进程: fork()
创建的子进程需要被父进程使用 wait()
或 waitpid()
回收,否则它们会变成僵尸进程,占用系统资源。如果父进程不关心子进程的退出状态,可以设置 SIGCHLD
信号的处理方式为 SIG_IGN
(忽略),或使用 double fork技巧让 init
进程接管子进程。
* 内存: fork()
后,父子进程有独立的地址空间(但初始时共享内存页,采用写时复制)。在子进程中分配的内存不会影响父进程,反之亦然。但在 exec
调用之前,子进程需要小心管理资源,因为它继承了父进程的状态。
⑤ 安全性 (Security):
* exec
系列: 执行外部程序时要非常小心。如果执行的命令或其参数来自用户输入或不可信来源,必须进行严格的验证和清理,防止命令注入攻击。尽量使用 execve
或 execvp
等接受参数数组的版本,而不是 execl
等需要自己构建命令行的版本,以避免 shell 元字符解析问题。如果必须使用 shell 执行命令 (system()
或 execl("/bin/sh", "sh", "-c", command, NULL)
), 输入必须被极其谨慎地转义。
* setuid
/setgid
: set-UID/GID 程序是安全风险点。遵循最小权限原则,尽快放弃不必要的特权。仔细检查所有输入和操作。考虑使用 Linux Capabilities 替代。
* access()
的 TOCTOU 问题: 不要依赖 access()
来做安全决策。直接尝试操作并处理错误。
* 文件操作: 创建文件时(如 open
的 O_CREAT
),注意设置合适的权限掩码。使用 unlink
创建临时文件时要注意竞态条件(虽然比 mktemp
好)。最好使用 mkstemp
或 mkdtemp
(在 <stdlib.h>
) 来安全地创建临时文件或目录。
4. 常见错误与陷阱 ❌
① fork()
后的资源共享与泄漏:
* 忘记父进程需要 wait()
或 waitpid()
回收子进程,导致僵尸进程堆积。
* 父子进程都持有同一个文件描述符,没有根据需要关闭副本(如管道读写端),可能导致意外行为(如 read
阻塞因为写端没关)或资源竞争。
* 在 fork()
之后,子进程中不应使用 C++ 标准库的 I/O 流(如 std::cout
)缓冲区中可能存在的、从父进程继承的数据,除非你知道自己在做什么。子进程通常应尽快调用 _exit()
或 exec()
。
② exec()
系列函数的选择:
* 混淆 l
/v
(参数列表 vs 参数数组) 或 p
(是否搜索 PATH)。选错可能导致执行失败或执行了非预期的程序。
* 忘记 exec
参数列表或数组必须以 NULL
结尾。
* 没有检查 exec
的返回值。如果 exec
返回,说明它失败了,子进程应该处理错误并退出 (_exit()
),否则它会继续执行 fork
之后的代码,这通常不是期望的行为。
③ wait()
/waitpid()
的僵尸进程问题:
* 完全忘记调用 wait
或 waitpid
。
* 只调用一次 wait
但可能有多个子进程结束。需要循环调用 wait
或 waitpid
(使用 WNOHANG
或在信号处理函数中) 直到没有更多子进程结束。
* 错误地解析 waitpid
返回的状态码 (wstatus
),没有使用 WIFEXITED
, WEXITSTATUS
等宏。
④ 文件描述符的误用:
* 忘记 close()
文件描述符,导致资源泄漏,最终可能耗尽可用 fd。
* 关闭了错误的描述符(如 close(1)
关闭了标准输出)。
* 对同一个 fd 调用 close()
多次(第二次及之后调用会失败并设置 errno
为 EBADF
)。
* dup2(oldfd, newfd)
后忘记关闭 oldfd
(如果不再需要)。
* 依赖 fd 数值(如假设 open
返回 3),更好的做法是保存返回值。
⑤ 缓冲区溢出 (read
/write
):
* 传递给 read
的 count
大于提供的缓冲区大小。
* read
后没有正确处理返回的字节数,假设读满了或忘记添加 \0
来处理字符串。
* write
时没有检查返回值,可能没有写入所有请求的字节,需要循环写入。
⑥ 权限问题:
* 执行需要特定权限的操作(如 setuid
, setgid
, chown
, unlink
受保护文件)但进程没有足够权限,却未检查错误。
* 错误地使用 access()
进行权限检查(TOCTOU 漏洞)。
* set-UID/GID 程序未能正确、及时地放弃特权。
⑦ pipe()
使用不当:
* fork
后父子进程没有关闭不使用的管道端点。
* 向一个读端已被关闭的管道写入(导致 SIGPIPE
或 EPIPE
错误)。
* 从一个写端已被关闭且缓冲区已空的管道读取(read
返回 0,表示 EOF,需要正确处理)。
5. 总结 ✨
<unistd.h>
是 C/C++ 在 Unix-like 系统上进行底层系统编程的核心接口。它提供了丰富的功能,涵盖进程创建与管理、文件 I/O、文件系统导航、用户/组信息查询与修改、系统配置查询等多个方面。
核心要点:
① POSIX 标准: <unistd.h>
是 POSIX 的一部分,旨在提供可移植性,但主要限于 Unix-like 环境。
② 系统调用封装: 它里面的许多函数是对底层系统调用的直接或间接封装。
③ 资源管理是关键: 必须小心管理文件描述符和子进程,防止泄漏和僵尸进程。
④ 错误处理不能少: 几乎所有函数都需要检查返回值和 errno
。
⑤ 安全需谨慎: 特别是涉及执行外部命令、权限变更和文件操作时。
掌握 <unistd.h>
是理解操作系统如何工作以及编写与操作系统紧密交互的应用程序的基础。虽然 C++ 提供了更高级、类型更安全的抽象(如 <thread>
, <filesystem>
, <process>
(提案中)),但在许多场景下,直接使用 <unistd.h>
提供的 POSIX API 仍然是必要或更有效的选择。理解其原理、最佳实践和常见陷阱对于编写健壮、高效的系统级代码至关重要。