Linux 将进程的整数句柄与存储打开实例状态的内核对象分离。这个区别解释了偏移共享、复制、继承以及许多描述符泄漏。
描述符表和打开文件描述 #
文件描述符是一个非负整数,用于索引调用进程的描述符表。表项引用一个打开文件描述,在 Linux 内部由 struct file 表示。打开文件描述记录如下状态:
- 当前文件偏移;
O_APPEND和O_NONBLOCK等文件状态标志;- 打开的路径、访问模式、凭据和文件操作。
每次成功的 open() 通常都会创建一个新的打开文件描述,并在进程表中安装一个引用。因此,打开同一路径两次会创建独立偏移:
int a = open("events.log", O_RDONLY);
int b = open("events.log", O_RDONLY); /* independent description and offset */inode 标识文件系统对象,而打开文件描述标识对它的一次打开。取消链接或重命名路径不会使现有描述符失效。
描述符标志属于表项,而不是共享的打开文件描述。主要描述符标志是 FD_CLOEXEC,通过 fcntl(fd, F_GETFD/F_SETFD) 读取或更改。应优先使用 O_CLOEXEC、pipe2(O_CLOEXEC) 和 dup3(..., O_CLOEXEC) 等原子创建标志,因为在后续 fcntl() 调用中设置标志,会与另一个线程中的 fork() 加 execve() 竞争。
复制、fork() 和 execve()
#
dup()、dup2() 和 dup3() 创建另一个引用同一打开文件描述的描述符。因此,复制的描述符共享偏移和文件状态标志:
int input = open("input", O_RDONLY | O_CLOEXEC);
int stdin_copy = dup(input);
read(input, buf, 16); /* advances the shared offset */
read(stdin_copy, buf, 16); /* starts after those 16 bytes */新描述符拥有自己的描述符标志。特别是,dup() 和 dup2() 会清除新描述符上的 close-on-exec;dup3() 可以原子地设置它。dup2(oldfd, newfd) 会在必要时原子关闭 newfd 并复用该编号,从而避免先关闭再复制的竞争。
fork() 之后,父进程和子进程拥有独立的描述符表,但继承的表项引用相同的打开文件描述。这些引用之间仍共享偏移和状态标志变化。成功执行 execve() 后,除非表项设置了 FD_CLOEXEC,否则描述符仍保持打开;描述符表本身会保留到新程序中。
使用共享文件表创建的线程(如 POSIX 线程)会看到相同的描述符表项。因此,一个线程关闭或替换描述符可能影响另一个线程,且描述符编号可能立即被重用。
匿名管道 #
pipe() 创建一个内核字节流,并返回其读端和写端的描述符:
int fd[2];
if (pipe2(fd, O_CLOEXEC) == -1)
/* handle error */;
/* fd[0] is readable; fd[1] is writable. */传统的 fork() 管道必须在每个进程中关闭未使用的副本:
if (fork() == 0) {
close(fd[1]);
consume(fd[0]);
close(fd[0]);
_exit(0);
}
close(fd[0]);
produce(fd[1]);
close(fd[1]); /* the reader can observe EOF after every writer closes */关闭纪律是协议的一部分:
- 当管道仍有写入者时,空管道上的
read()会阻塞,除非设置了O_NONBLOCK。 - 只有在缓冲区为空且引用写端的所有描述符都已关闭后,
read()才返回零。 - 写满的管道上的
write()会阻塞,或在O_NONBLOCK下失败/部分成功。 - 所有读端关闭后执行
write()会引发SIGPIPE;如果该信号被忽略或处理,调用会失败并返回EPIPE。
管道是字节流,不保留一般消息边界。POSIX 保证不超过 PIPE_BUF 字节的写入不会与其他进程的写入交错。Linux 将 PIPE_BUF 定义为 4096 字节。更大的写入可能被拆分或交错;非阻塞写入也可能部分成功,因此调用方必须检查返回值。
O_NONBLOCK 是打开文件描述上的文件状态标志。通过一个复制的描述符设置它,会改变共享该描述的所有描述符所观察到的行为。
FIFO 作为命名会合点 #
mkfifo() 创建一个类型为 FIFO 的文件系统项:
if (mkfifo("/run/example/events.fifo", 0600) == -1 && errno != EEXIST)
/* handle error */;目录项不存储流内容。FIFO 打开期间,内核维护一个管道对象,进程通过打开路径访问它。打开后,读取、写入、原子性、EOF 和 SIGPIPE 遵循管道语义。
打开操作提供会合行为:
- 阻塞式
open(path, O_RDONLY)会等待写入者。 - 阻塞式
open(path, O_WRONLY)会等待读取者。 - 没有写入者时,非阻塞只读打开也会成功;没有写入者时读取返回零,或在存在写入者但没有可用数据时返回
EAGAIN。 - 在读取者打开 FIFO 之前,非阻塞只写打开会失败并返回
ENXIO。
Linux 允许使用 O_RDWR 打开 FIFO,即使没有对端,但 POSIX 将该行为留作未定义。保持两端打开也可能抑制 EOF 或造成自死锁,因此可移植协议应使用明确的读取者和写入者角色。
由于 FIFO 名称可能在检查和 open() 之间被替换,应将其放在受控目录中;在信任边界要求时验证所有权和类型,并避免使用先检查再打开的授权逻辑。
评论
在 GitHub 上阅读或评论(发表评论需要登录)