本地 Man 手册库
离线、单文件、可搜索。选择左侧条目,或从下方进入。
离线、单文件、可搜索。选择左侧条目,或从下方进入。
对应手册: Linux man-pages 6.20 (2026)
分类章节: 第 2 节 —— 系统调用(System Calls)
目录代号:man2
mmap - 将文件或设备映射到进程虚拟内存空间(以及 munmap 解除内存映射)
标准 C 库 (libc, -lc)
#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags,
int fd, off_t offset);
int munmap(void *addr, size_t length);有关功能测试宏(Feature Test Macros)的要求,请参见【版本与特性宏】章节。
mmap() 在调用进程的虚拟地址空间中创建一段新的内存映射区域。
addraddr 为 NULL:内核将自动选择一个对齐到页边界(page-aligned)的未占用地址来创建映射。这是创建新映射最通用、最具可移植性的方法。addr 不为 NULL:内核会将其视为放置映射的建议地址(hint)。在 Linux 上,内核会选取邻近的页边界(且该地址必须大于或等于 /proc/sys/vm/mmap_min_addr 所配置的安全阈值),并尝试在该处创建映射。若该地址已被其他映射占用,内核会自动分配另一个新地址(该地址可能与提示地址无关)。调用返回最终实际分配的映射起始地址。lengtherrno 为 EINVAL。protprot 参数描述映射内存页的访问保护权限,不能与底层文件的打开模式(open 模式)冲突。其值可以为 PROT_NONE,或是以下标志的按位或(OR)组合:
| 保护标志 | 含义说明 |
|---|---|
PROT_EXEC |
允许执行该内存区域中的代码指令。 |
PROT_READ |
允许读取该内存区域的数据。 |
PROT_WRITE |
允许向该内存区域写入数据。 |
PROT_NONE |
禁止访问该内存页(任何读、写、执行均触发段错误)。 |
flagsflags 决定对映射区域的修改是否对其他映射相同区域的进程可见,以及修改是否会同步更新到底层支持文件。
flags 必须且只能包含以下三大互斥基础类型之一:
MAP_SHARED:
msync(2))。MAP_SHARED_VALIDATE (自 Linux 4.15 引入):
MAP_SHARED 基本一致,唯一的关键区别在于:MAP_SHARED 会静默忽略 flags 中无法识别的未知标志;而 MAP_SHARED_VALIDATE 会严格校验传入的所有标志,一旦包含未知标志,调用立即失败并返回错误 EOPNOTSUPP。MAP_SYNC)强制要求搭配该类型使用。MAP_PRIVATE:
mmap() 调用之后对底层文件所做的修改是否会反映到已映射区域是未定义(unspecified)的行为。除上述三者之一外,还可按需按位或(OR)组合以下可选标志位:
| 标志名称 | 引入版本 | 功能说明与工程要点 |
|---|---|---|
MAP_ANONYMOUS(或 MAP_ANON) |
Linux 2.4 | 匿名映射。映射不与任何文件关联;内存页初始内容清零。参数 fd 将被忽略(为保证跨平台可移植性,建议传 -1),offset 应设为 0。配合 MAP_SHARED 可用于父子进程或跨进程匿名共享内存。 |
MAP_FIXED |
POSIX.1 | 强制精确使用 addr 地址。不将 addr 视为提示,映射必须精准落在 addr。addr 必须页对齐。极度危险:若该范围与进程内任何现有映射重叠,原有映射的重叠部分将被强制静默丢弃与覆盖! |
MAP_FIXED_NOREPLACE |
Linux 4.17 | 安全固定地址映射。强制使用 addr,但绝不覆盖任何既有映射。若目标区间存在冲突,调用直接失败并设置错误为 EEXIST。可用于多线程环境下无竞态(atomic)的固定地址映射尝试。 |
MAP_POPULATE |
Linux 2.5.46 | 预填充页表(Prefault)。为映射区域预先建立页表项。若为文件映射,将触发内核预读(read-ahead),显著降低后续访问时的缺页中断(page fault)停顿。自 Linux 2.6.23 起支持私有映射。 |
MAP_LOCKED |
Linux 2.5.37 | 锁定物理内存。类似 mlock(2),将映射页锁定在 RAM 中防止换出。内核会尝试预填充,但即使物理内存不足无法锁住也不会报错 ENOMEM(语义弱于 mlock)。若对 major fault 绝对敏感,建议结合显式 mlock(2)。 |
MAP_NONBLOCK |
Linux 2.5.46 | 非阻塞预读。仅在与 MAP_POPULATE 联用时有意义。不发起阻塞式磁盘 I/O,仅为已在物理内存(Page Cache)中的页建立页表项。 |
MAP_NORESERVE |
早期支持 | 不预留交换空间(Swap Space)。当物理内存耗尽时,后续写入可能因无法分配物理页而收到 SIGSEGV 信号。详见 /proc/sys/vm/overcommit_memory。 |
MAP_GROWSDOWN |
早期支持 | 向下生长(用于栈)。告知虚拟内存子系统该映射用于向下生长的线程栈。访问映射区底部的保护页(guard page)会自动扩展映射区域。 |
MAP_HUGETLB |
Linux 2.6.32 | 大页映射(Huge Pages)。使用系统大页分配映射(如 2MB / 1GB 大页),大幅减轻 TLB 缺失压力。 |
MAP_HUGE_2MBMAP_HUGE_1GB |
Linux 3.8 | 与 MAP_HUGETLB 配合,通过宏编码直接选择特定的大页尺寸(如 2MB 或 1GB)。 |
MAP_STACK |
Linux 2.6.27 | 进程/线程栈分配。当前在 Linux 上为 no-op,但被 glibc 线程库规范使用,以便未来内核支持或跨 BSD 平台可移植。 |
MAP_SYNC |
Linux 4.15 | 同步持久化映射。仅与 MAP_SHARED_VALIDATE 联用,且仅支持 DAX(持久化内存 Direct Access)。保证写入在崩溃或重启后依然持久保留。 |
MAP_32BIT |
Linux 2.4.20 | 仅适用于 x86-64 架构 64 位程序。将映射分配在虚拟地址空间的前 2GB 范围内。 |
MAP_UNINITIALIZED |
Linux 2.6.33 | 不清零匿名内存页。专为嵌入式设备极限性能优化设计,需内核启用 CONFIG_MMAP_ALLOW_UNINITIALIZED。 |
MAP_DENYWRITE、MAP_EXECUTABLE、MAP_FILE 属于历史遗留或兼容标志,在现代 Linux 内核中已被完全忽略。
fd 与偏移量 offsetfd 中自 offset 偏移处读取 length 字节进行初始化。offset 必须是系统页面大小(通过 sysconf(_SC_PAGE_SIZE) 获取,通常为 4096 字节)的整数倍。mmap() 成功返回后,可以立即调用 close(fd),这绝不会损坏或解除已建立的内存映射。munmap()int munmap(void *addr, size_t length);munmap() 系统调用用于删除调用进程在指定虚拟地址范围 [addr, addr + length) 内的映射关系。addr 必须是系统页面大小的整数倍(而 length 不需要是页大小的整数倍)。内核会自动将覆盖该区间的整页全部解除映射。close(fd))不会解除映射。SIGSEGV(段错误)。若指定的地址范围内根本不存在任何有效映射,munmap() 也不会报错,依然返回 0。mmap():
void *)。MAP_FAILED(定义为 ((void *) -1)),并设置 errno 指示具体的错误类型。mmap() 失败时返回的是 MAP_FAILED(即 (void *) -1),绝对不是 NULL!
切勿编写 if (addr == NULL) 判断,这会导致错误处理完全失效,进而引发严重的安全漏洞或不可控崩溃。
munmap():
0。-1,并设置 errno(通常为 EINVAL)。| 错误宏 | 含义与产生原因 |
|---|---|
EACCES |
文件描述符所指对象并非普通文件;或请求了文件映射但 fd 没有以可读模式打开;或指定了 MAP_SHARED 且设置了 PROT_WRITE,但 fd 未以可读写(O_RDWR)方式打开;或设置了 PROT_WRITE 但目标文件为仅追加(append-only)。 |
EAGAIN |
底层文件已被加锁;或者被锁定的内存总量超过了资源限制(参见 setrlimit(2) 的 RLIMIT_MEMLOCK)。 |
EBADF |
fd 不是一个有效的文件描述符(且未设置 MAP_ANONYMOUS)。 |
EEXIST |
flags 中指定了 MAP_FIXED_NOREPLACE,但 addr 与 length 所覆盖的区域与现有的映射发生冲突。 |
EINVAL |
addr、length 或 offset 传参非法(例如未按页边界对齐、数值溢出超出有效范围)。 |
EINVAL |
(自 Linux 2.6.12 起) length 参数等于 0。 |
EINVAL |
flags 中未包含 MAP_PRIVATE、MAP_SHARED 或 MAP_SHARED_VALIDATE 中的任意一个。 |
ENFILE |
达到了系统允许打开的文件总数上限。 |
ENODEV |
指定文件所在的文件系统不支持内存映射(例如某些特殊网络或虚拟文件系统)。 |
ENOMEM |
系统物理内存或虚拟交换空间不足。 |
ENOMEM |
进程所允许的最大映射数量(/proc/sys/vm/max_map_count)已达上限。在 munmap() 时若从现有映射中间挖空,会导致一个映射分裂为两个较小映射,同样可能因超过该上限而报错 ENOMEM。 |
ENOMEM |
(自 Linux 4.7 起) 超出了进程的 RLIMIT_DATA 限制。或者 addr 超出了 CPU 的虚拟地址空间上限。 |
EOVERFLOW |
32 位系统且启用大文件扩展(64 位 off_t)时:length 所占页数与 offset 所占页数之和溢出了 32 位 unsigned long。 |
EPERM |
请求了 PROT_EXEC,但目标文件所在的文件系统在挂载时启用了 noexec 标志。 |
EPERM |
操作被文件封印机制拦截阻止(参见 fcntl(2) 的文件封印 F_ADD_SEALS)。 |
EPERM |
指定了 MAP_HUGETLB,但调用进程缺乏 CAP_IPC_LOCK 权限,且不属于 hugetlb_shm_group 用户组。 |
ETXTBSY |
设置了 MAP_DENYWRITE,但 fd 指定的文件已被以可写方式打开。 |
SIGSEGV(段错误):
PROT_WRITE)的区域执行写操作。MAP_GROWSDOWN)之外的受限保护页。SIGBUS(总线错误):
ftruncate(2) 截断缩短了文件,导致映射范围越过了文件尾端)。| 接口 (Interface) | 属性 (Attribute) | 属性值 (Value) |
|---|---|---|
mmap(), munmap() |
线程安全性 (Thread safety) | MT-Safe(多线程并发安全) |
PROT_WRITE 隐式包含了 PROT_READ。PROT_READ 是否隐式包含 PROT_EXEC 因 CPU 架构和内核 NX 位保护而异。为了编写健壮的可移植程序,只要需要执行代码,就必须显式设置 PROT_EXEC。addr 设为 NULL,并在 flags 中省去 MAP_FIXED。系统会自动选择互不冲突的安全地址。MAP_ANONYMOUS、MAP_POPULATE、MAP_HUGETLB 等大多数扩展标志均为 Linux 专属定义。使用时推荐在包含头文件前定义:
#define _GNU_SOURCE
#include <sys/mman.h>mmap() 封装函数。最初该封装函数直接调用同名系统调用;mmap2(2) 系统调用(其 offset 参数以 4096 字节为基本单位,从而允许 32 位系统访问超过 4GB 的超大文件);现代 glibc 的 mmap() 封装函数在底层透明调用 mmap2(2) 并自动转换偏移量。mmap()、msync() 与 munmap() 的 POSIX 兼容系统上,<unistd.h> 中会定义 _POSIX_MAPPED_FILES 宏且值大于 0。fork)由 mmap() 创建的内存映射在进程执行 fork(2) 时会被子进程完整继承,且保留完全相同的保护属性;然而在调用 execve(2) 加载新程序镜像时,所有映射都会被自动销毁解除。
内存映射总是以系统页面大小(通常为 4KB)为最小粒度分配管理的:
SIGBUS 信号。MAP_FIXED 的准则与多线程竞态漏洞MAP_FIXED 会无条件解绑并强行覆盖目标地址范围内的任何既有映射!在多线程程序中盲目使用 MAP_FIXED 极易引发严重内存损坏。
/proc/self/maps 发现了一段空闲地址,准备使用 MAP_FIXED 映射到该地址;而在同一瞬间,线程 B 调用了 malloc()、dlopen() 或 pthread_create(),其内部在同一地址空间动态创建了内存映射。此时线程 A 的 mmap(MAP_FIXED) 会无声无息地彻底踩踏并销毁线程 B 的映射,导致整个进程随机崩溃。MAP_FIXED_NOREPLACE。遇到地址占用时会安全报错 EEXIST,保障原子性与线程安全。MAP_FIXED,唯一安全的前提是该目标地址区间已经在之前由当前模块通过一次占位映射(如匿名不可访问映射)预先保留。st_atime(最后访问时间):在 mmap() 与对应的 munmap() 期间的任意时刻可能更新;首次读取引用映射页时必然触发更新。st_ctime / st_mtime(状态改变与修改时间):对带有 PROT_WRITE 和 MAP_SHARED 的文件映射区域写入后,会在随后的 msync(2)(带有 MS_SYNC 或 MS_ASYNC)之前由内核更新。使用 MAP_HUGETLB 时:
mmap():offset 必须是底层大页大小(例如 2MB 或 1GB)的整数倍;内核会自动将 length 向上取整对齐到大页尺寸。munmap():addr 和 length 都必须是底层大页尺寸的整数倍。与常见的 malloc(3) 实现不同,mmap() 并不阻止创建体积超过 PTRDIFF_MAX(指针差值最大值)的对象:
PTRDIFF_MAX,属于未定义行为(Undefined Behavior)。PTRDIFF_MAX。PTRDIFF_MAX 通常约为 0x7fffffff(约 2GB)。开发时应避免映射跨度超过此限制的单一巨型内存块。MAP_NORESERVE 无法抵御 OOM-killer:在 Linux 上,MAP_NORESERVE 并不能真正保证进程免于崩溃。默认情况下,一旦系统全局物理内存耗尽,任何进程都可能在任意时刻被内核 OOM-killer 强制终止。length == 0 的历史兼容性:SUSv3 规定长度为 0 时必须报错。但在 Linux 2.6.12 之前,mmap() 传入 length == 0 竟会成功返回 addr 而不创建任何映射;自 Linux 2.6.12 起已严格修复并返回 EINVAL。close 且映射被 munmap,后续建立的新映射依然可能读取到此脏数据。在 tmpfs(5)(如 POSIX 共享内存 shm_open(3))上,即使在 unmap 前调用 msync(2) 也无法彻底避免该现象。mmap 高性能读取并打印文件指定字节区间该程序演示如何通过页对齐处理安全映射文件区间,并利用 write(2) 直接输出映射内存。
#include <err.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
int fd;
char *addr;
off_t offset, pa_offset;
size_t length;
ssize_t s;
struct stat sb;
long page_size;
if (argc < 3 || argc > 4) {
fprintf(stderr, "用法: %s <文件路径> <偏移量offset> [长度length]\n", argv[0]);
exit(EXIT_FAILURE);
}
fd = open(argv[1], O_RDONLY);
if (fd == -1)
err(EXIT_FAILURE, "打开文件失败 (open)");
if (fstat(fd, &sb) == -1)
err(EXIT_FAILURE, "获取文件状态失败 (fstat)");
offset = atoll(argv[2]);
page_size = sysconf(_SC_PAGE_SIZE);
if (page_size == -1)
err(EXIT_FAILURE, "获取系统页大小失败");
/* mmap 要求 offset 必须按页大小对齐,因此向下取整计算页起始偏移 */
pa_offset = offset & ~(page_size - 1);
if (offset >= sb.st_size) {
fprintf(stderr, "错误: 目标偏移量已超出文件物理末尾\n");
exit(EXIT_FAILURE);
}
if (argc == 4) {
length = atoll(argv[3]);
if (offset + length > (size_t)sb.st_size)
length = sb.st_size - offset; /* 避免越界读取 */
} else {
length = sb.st_size - offset; /* 默认读取至文件末尾 */
}
/* 映射长度需包含对齐偏移造成的额外页头部字节 */
size_t map_length = length + (offset - pa_offset);
addr = mmap(NULL, map_length, PROT_READ, MAP_PRIVATE, fd, pa_offset);
if (addr == MAP_FAILED)
err(EXIT_FAILURE, "内存映射失败 (mmap)");
/* 实际数据指针从 addr + (offset - pa_offset) 开始 */
s = write(STDOUT_FILENO, addr + (offset - pa_offset), length);
if (s != (ssize_t)length) {
if (s == -1)
err(EXIT_FAILURE, "写入标准输出失败 (write)");
fprintf(stderr, "警告: 部分写入\n");
exit(EXIT_FAILURE);
}
/* 释放映射并关闭文件 */
if (munmap(addr, map_length) == -1)
err(EXIT_FAILURE, "解除映射失败 (munmap)");
close(fd);
return EXIT_SUCCESS;
}MAP_ANONYMOUS | MAP_SHARED)实现跨进程共享内存通信#include <err.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <unistd.h>
struct shared_data {
int counter;
char message[64];
};
int main(void)
{
/* 创建父子进程共享的匿名内存映射,无需任何磁盘文件支持 */
struct shared_data *data = mmap(NULL, sizeof(*data),
PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_ANONYMOUS,
-1, 0);
if (data == MAP_FAILED)
err(EXIT_FAILURE, "匿名共享映射创建失败");
data->counter = 100;
snprintf(data->message, sizeof(data->message), "Hello from Parent!");
pid_t pid = fork();
if (pid == -1)
err(EXIT_FAILURE, "fork 失败");
if (pid == 0) {
/* 子进程:读取并直接修改共享内存中的数据 */
printf("[子进程 PID %d] 读取初始数据: counter=%d, msg='%s'\n",
getpid(), data->counter, data->message);
data->counter += 50;
snprintf(data->message, sizeof(data->message), "Modified by Child!");
munmap(data, sizeof(*data));
exit(EXIT_SUCCESS);
}
/* 父进程:等待子进程退出后读取最新更新 */
wait(NULL);
printf("[父进程 PID %d] 检测到更新: counter=%d, msg='%s'\n",
getpid(), data->counter, data->message);
if (munmap(data, sizeof(*data)) == -1)
err(EXIT_FAILURE, "解除映射失败");
return EXIT_SUCCESS;
}shm_open(3) - POSIX 共享内存对象打开接口shm_overview(7) - POSIX 共享内存概述与开发指南proc(5) - /proc/[pid]/maps、/proc/[pid]/smaps 虚拟内存映射分布调试文件对应手册: Linux man-pages 6.20 (2026)
分类章节: 第 2 节 —— 系统调用(System Calls)
目录代号:man2
open, openat, creat - 打开并可能创建文件或设备,返回关联的文件描述符
标准 C 库 (libc, -lc)
#include <fcntl.h>
int open(const char *path, int flags, ...
/* mode_t mode */ );
int creat(const char *path, mode_t mode);
int openat(int dirfd, const char *path, int flags, ...
/* mode_t mode */ );
/* 单独在 openat2(2) 中记录: */
int openat2(int dirfd, const char *path,
const struct open_how *how, size_t size);openat():
自 glibc 2.10 起:
_POSIX_C_SOURCE >= 200809L
glibc 2.10 之前:
_ATFILE_SOURCEopen() 系统调用打开由 path 指定的文件。若目标文件不存在,且在 flags 中指定了 O_CREAT,则 open() 可以选择创建该文件。
open() 成功时返回一个文件描述符 (file descriptor, fd),这是一个非负整数,代表当前进程打开文件描述符表的索引。
execve(2) 保持打开状态(即 fcntl(2) 的 FD_CLOEXEC 标志默认处于禁用状态);通过指定 O_CLOEXEC 标志可以改变该默认行为。lseek(2))。文件描述符 (File Descriptor) 与 打开文件描述 (Open File Description, OFD) 是两个截然不同的概念:
open() 调用会在系统内核级打开文件表中分配一个打开文件描述 (OFD)。OFD 负责记录当前的文件偏移量以及文件状态标志(如 O_APPEND、O_NONBLOCK)。dup(2)、dup2(2) 或通过 fork(2) 复制文件描述符,两个不同的描述符将指向同一个底层的 OFD,共享相同的文件偏移量和状态标志。open(),将生成两个独立的 OFD,二者的文件偏移量完全相互独立。参数 flags 必须且只能包含以下访问模式之一:
| 标志 | 含义 |
|---|---|
O_RDONLY |
以只读方式打开文件 |
O_WRONLY |
以只写方式打开文件 |
O_RDWR |
以可读可写方式打开文件 |
除了基础访问模式外,flags 参数还可以按位或(|)组合零个或多个可选标志。标志位可分为两大类:
open() 调用本身的创建或打开语义。这些标志不能通过 fcntl(2) 查询或修改:
O_CLOEXECO_CREATO_DIRECTORYO_EXCLO_NOCTTYO_NOFOLLOWO_TMPFILEO_TRUNCfcntl(2) 的 F_GETFL 获取,部分标志可通过 F_SETFL 动态修改:
O_APPEND, O_ASYNC, O_DIRECT, O_NONBLOCK, O_SYNC 等)。O_APPEND:
以追加模式打开文件。在执行每次 write(2) 之前,内核原子地将文件偏移量移动至文件末尾(等效于先执行 lseek(2))。
在 NFS 网络文件系统上,多进程同时以 O_APPEND 写入可能会损坏文件,因为 NFS 协议本身不支持文件追加语义,需由客户端内核模拟,容易引发竞态条件。
O_ASYNC:
启用信号驱动 I/O(Signal-driven I/O)。当文件描述符就绪时向进程发送信号(默认为 SIGIO,可用 fcntl(2) 修改)。仅适用于终端、伪终端、套接字、管道以及 FIFO。注:建议通过 fcntl(2) 启用该标志,避免在 open() 中直接使用。
O_CLOEXEC(自 Linux 2.6.23 起):
为返回的新文件描述符自动设置 FD_CLOEXEC(close-on-exec)标志。这避免了多线程程序在 fork(2) 与 execve(2) 之间并发调用 open() 时可能导致的文件描述符泄露竞态条件。
O_CREAT:
若指定路径的文件不存在,则创建该文件为普通文件。
mode,用于指定新文件的访问权限掩码(受进程的 umask 影响,实际权限为 mode & ~umask)。O_DIRECT(自 Linux 2.4.10 起):
尝试绕过内核页缓存(Page Cache)进行直接 I/O。用户空间缓冲区与文件系统传输直接交互,适用于数据库等拥有自研缓存系统的中间件。用户空间缓冲区的内存地址、文件偏移量以及传输字节长度通常必须按底层设备或文件系统的块大小(通常为 512 字节或 4096 字节)对齐。
O_DIRECTORY:
要求被打开的文件必须是目录。若 path 不是目录,则 open() 失败并设置 errno 为 ENOTDIR。
O_DSYNC:
按照 POSIX 同步 I/O 数据完整性完成(Synchronized I/O data integrity completion)方式执行写入。写操作必须等待数据及允许后续读取所需的元数据(如文件大小变化)刷入底层硬件才返回。
O_EXCL:
与 O_CREAT 组合使用:若文件已存在,则 open() 失败并返回 EEXIST 错误。这保证了文件创建的原子性,避免多进程检查文件是否存在时产生竞态。
O_LARGEFILE:
允许在 32 位系统上打开文件大小无法用 31 位表示(大于 2GB)的大文件。64 位系统上该标志默认隐式开启。
O_NOATIME(自 Linux 2.6.8 起):
在读取文件时不更新其最后访问时间戳(st_atime)。调用进程的有效 UID 必须与文件属主匹配,或者拥有特权(CAP_FOWNER)。常见于备份工具与搜索索引程序。
O_NOCTTY:
若 path 指向一个终端设备,该设备不会成为调用进程的控制终端(即使该进程此前没有控制终端)。
O_NOFOLLOW:
若 path 的最后一部分是一个符号链接,则打开失败并返回 ELOOP 错误。与 O_PATH 标志联用时,则允许直接打开符号链接本身获取其描述符。
O_NONBLOCK 或 O_NDELAY:
以非阻塞模式打开文件。对普通文件通常没有影响,主要用于 FIFO、管道、套接字与设备驱动。在设备未就绪时立即返回而不会阻塞进程。
O_PATH(自 Linux 2.6.39 起):
仅用于获取文件在文件系统层级中的句柄(文件描述符),而不实际打开文件内容。返回的描述符不能用于 read(2)、write(2) 等 I/O 调用,但可以作为 openat(2) 的 dirfd 参数、配合 fstat(2) 查询元数据、传递给 dup(2) 或通过 UNIX 域套接字传递。
O_SYNC:
按照 POSIX 同步 I/O 文件完整性完成(Synchronized I/O file integrity completion)方式执行写入。不仅刷入数据,还会刷入包括最后修改时间(st_mtime)在内的全部元数据。
O_TMPFILE(自 Linux 3.11 起):
在指定目录下创建一个无名的隐式临时文件。必须与 O_RDWR 或 O_WRONLY 一同指定,path 必须为目录路径。该临时文件关闭后自动从文件系统中彻底销毁,亦可在后续通过 linkat(2) 将其原子原子地链接为常规目录项。
O_TRUNC:
若文件已存在且为普通文件,并且打开模式包含写权限(O_RDWR 或 O_WRONLY),则将文件长度截断为 0 字节。
int creat(const char *path, mode_t mode);creat() 是早期 UNIX 遗留的历史接口,完全等效于:
open(path, O_WRONLY | O_CREAT | O_TRUNC, mode);int openat(int dirfd, const char *path, int flags, ...
/* mode_t mode */ );openat() 的运行机制与 open() 类似,但扩展支持了基于基准目录文件描述符 dirfd 进行路径解析:
path 是绝对路径(以 / 开头),则忽略 dirfd,行为与 open() 完全一致。path 为相对路径,且 dirfd 的取值为特殊常量 AT_FDCWD,则相对于调用进程的当前工作目录(CWD)解析路径,行为等同于 open()。path 为相对路径,且 dirfd 是此前通过 open()(通常带有 O_DIRECTORY 或 O_PATH)打开的目录的文件描述符,则相对于 dirfd 所指向的目录进行相对路径解析。为什么要使用 openat()?
chdir(2)),避免竞态。openat2(2) 能够实现更加严密的 RESOLVE_* 安全隔离)。当指定了 O_CREAT 或 O_TMPFILE 时,必须提供 mode 参数。它由以下权限位宏进行按位或构成:
| 权限宏 | 八进制值 | 描述 |
|---|---|---|
S_IRWXU |
0700 |
文件所有者拥有读、写与执行权限 |
S_IRUSR |
0400 |
文件所有者拥有读权限 |
S_IWUSR |
0200 |
文件所有者拥有写权限 |
S_IXUSR |
0100 |
文件所有者拥有执行权限 |
S_IRWXG |
0070 |
用户组拥有读、写与执行权限 |
S_IRGRP |
0040 |
用户组拥有读权限 |
S_IWGRP |
0020 |
用户组拥有写权限 |
S_IXGRP |
0010 |
用户组拥有执行权限 |
S_IRWXO |
0007 |
其他用户拥有读、写与执行权限 |
S_IROTH |
0004 |
其他用户拥有读权限 |
S_IWOTH |
0002 |
其他用户拥有写权限 |
S_IXOTH |
0001 |
其他用户拥有执行权限 |
S_ISUID |
04000 |
设置用户 ID 位 (Set-user-ID) |
S_ISGID |
02000 |
设置组 ID 位 (Set-group-ID) |
S_ISVTX |
01000 |
粘滞位 (Sticky bit) |
-1,并设置全局变量 errno 以指示具体的错误原因。open(), openat(), creat() 可能发生以下错误:
| 错误宏 | 含义与产生原因 |
|---|---|
EACCES |
权限不足:路径前缀中的某个目录禁止搜索,或者文件不存在且对父目录无写权限,或者文件本身禁止所请求的访问模式。 |
EBADF |
(针对 openat())path 是相对路径,但 dirfd 既不是 AT_FDCWD,也不是有效的打开文件描述符。 |
EBUSY |
指定了 O_EXCL,但 path 指向一个正在被系统占用的块设备(如已挂载)。 |
EDQUOT |
指定了 O_CREAT,但用户的磁盘配额(inode 或数据块)已耗尽。 |
EEXIST |
指定了 `O_CREAT |
EFAULT |
path 指针超出了进程可访问的虚拟地址空间。 |
EFBIG / EOVERFLOW |
文件过大,32 位应用程序未启用 -D_FILE_OFFSET_BITS=64 试图打开超过 2GB 的大文件。 |
EINTR |
在等待打开慢速设备(如 FIFO 或终端)时被信号中断处理程序打断(参见 signal(7))。 |
EINVAL |
无效参数:如 flags 取值非法,底层文件系统不支持 O_DIRECT,或指定了 O_TMPFILE 但未指定 O_WRONLY 或 O_RDWR。在内核 6.4+ 中同时指定 O_CREAT 与 O_DIRECTORY 也会返回此错误。 |
EISDIR |
请求写操作(指定了 O_WRONLY 或 O_RDWR),但 path 指向一个目录。 |
ELOOP |
解析路径时解析符号链接遇到过多层级(链接循环),或指定了 O_NOFOLLOW 且路径末尾为符号链接。 |
EMFILE |
进程已达单个进程允许打开的文件描述符上限(参见 getrlimit(2) 的 RLIMIT_NOFILE)。 |
ENAMETOOLONG |
路径名总长度或单个路径分量长度超出系统限制(PATH_MAX 或 NAME_MAX)。 |
ENFILE |
系统范围内打开的文件总数达到系统限制(见 /proc/sys/fs/file-max)。 |
ENODEV |
path 对应一个设备特殊文件,但系统中没有对应的硬件设备。 |
ENOENT |
未指定 O_CREAT 且目标文件不存在;或者路径中的某级前导目录不存在。 |
ENOMEM |
内核内存不足。 |
ENOSPC |
需要创建文件或写入,但目标存储设备没有可用空间。 |
ENOTDIR |
路径中作为目录出现的分量实际上不是目录;或指定了 O_DIRECTORY 但目标不是目录;或 openat() 的 dirfd 不是目录。 |
ENXIO |
指定了 `O_NONBLOCK |
EOPNOTSUPP |
所在文件系统不支持 O_TMPFILE 特性。 |
EPERM |
指定了 O_NOATIME 但有效 UID 与文件属主不一致且没有特权;或者该操作被文件密封(File Seal,见 fcntl(2))禁止。 |
EROFS |
目标文件位于只读文件系统上,但请求了写访问(O_WRONLY, O_RDWR, O_TRUNC 等)。 |
ETXTBSY |
目标文件是当前正在执行的可执行二进制文件,或者当前正在用作 Swap 交换文件,禁止写模式打开。 |
EWOULDBLOCK |
指定了 O_NONBLOCK,但文件上存在不兼容的文件租约(Lease,参见 fcntl(2))。 |
open(), creat(): POSIX.1-2001, POSIX.1-2008, POSIX.1-2024。openat(): POSIX.1-2008, POSIX.1-2024。openat2(2): Linux 专有(从 Linux 5.6 引入)。O_DIRECT, O_NOATIME, O_PATH, O_TMPFILE 均为 Linux 特有特性。open() 包装函数在所有支持的架构上统一改为使用 openat() 系统调用实现。open() 与 creat() 存在于早期的 Version 6 AT&T UNIX 与 Version 7 AT&T UNIX。在早期 UNIX 中,open() 无法创建不存在的文件,因此必须使用 creat();随后 O_CREAT 标志被加入 open(),使 creat() 退化为历史兼容函数。openat() 由 Solaris 率先引入,后随 Linux 2.6.16(glibc 2.4)被加入 Linux 内核,并最终被 POSIX.1-2008 采纳为标准。O_CLOEXEC 自 Linux 2.6.23 和 POSIX.1-2008 引入,彻底解决了多线程并发下的文件描述符泄漏安全漏洞。O_TMPFILE 自 Linux 3.11 引入,显著增强了临时文件创建的安全性。POSIX 规范定义了同步 I/O 的不同层级:
O_DSYNC(数据完整性完成):只将数据本身以及后续成功读取该数据所必需的元数据(如文件长度)刷入磁盘。如果只是覆盖写而不改变文件大小,不会强刷最后修改时间(st_mtime)。O_SYNC(文件完整性完成):无论写操作是否影响文件大小,都会将数据以及所有更新后的文件元数据(包括时间戳)全部刷入硬件。在对 I/O 延迟敏感的场景下,使用
O_DSYNC往往比O_SYNC节省一次针对 inode 元数据块的随机写操作。
当使用私有内存映射(包括堆上分配的普通内存缓冲区)进行 O_DIRECT I/O 时,绝不能与 fork(2) 并发执行。如果在异步 I/O 未完成时调用了 fork(),写时复制(COW)机制会导致父子进程的数据页面分裂,进而造成难以复现的数据损坏与未定义行为。
在 Linux 中,可以通过以下 proc 文件系统路径直观查看进程的打开文件情况:
/proc/[pid]/fd/:该目录下包含了进程所有打开文件描述符的符号链接。/proc/[pid]/fdinfo/[fd]:记录了该描述符的当前偏移量(pos)、打开标志(flags)及挂载 ID(mnt_id)。以下是使用 open() 与 openat() 的完整、可编译 C 语言示例程序:
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
int main(void)
{
const char *test_dir = "/tmp";
const char *filename = "antigravity_open_demo.txt";
const char *content = "Hello, Linux System Calls with open() and openat()!\n";
/* 1. 使用 open() 打开目录,获取目录的文件描述符 */
int dir_fd = open(test_dir, O_RDONLY | O_DIRECTORY | O_CLOEXEC);
if (dir_fd == -1) {
perror("open(directory) 失败");
exit(EXIT_FAILURE);
}
/* 2. 使用 openat() 在指定目录下创建并截断新文件(权限 0644) */
int file_fd = openat(dir_fd, filename,
O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC,
0644);
if (file_fd == -1) {
perror("openat(file) 失败");
close(dir_fd);
exit(EXIT_FAILURE);
}
/* 3. 写入内容 */
ssize_t written = write(file_fd, content, strlen(content));
if (written == -1) {
perror("write 失败");
} else {
printf("成功写入 %zd 字节数据到 %s/%s\n", written, test_dir, filename);
}
/* 4. 关闭文件描述符并清理测试文件 */
close(file_fd);
unlinkat(dir_fd, filename, 0);
close(dir_fd);
return 0;
}对应手册: Linux man-pages 6.20 (2026)
分类章节: 第 2 节 —— 系统调用(System Calls)
目录代号:man2
read - 从文件描述符读取数据
标准 C 库 (libc, -lc)
#include <unistd.h>
ssize_t read(int fd, void buf[count], size_t count);read() 系统调用尝试从文件描述符 fd 引用的打开文件中读取最多 count 字节的数据,并将其存入起始地址为 buf 的内存缓冲区中。
read() 立即返回 0。pipe、FIFO、套接字 socket 或终端终端设备),数据将从流的当前头部开始连续读取,且这些对象没有文件偏移量的概念。count 为 0 的情形:
count 为 0,read() 可能会对传入的参数执行错误检查(如检测 fd 是否有效),若检测到错误则返回相应错误码。count = 0 的 read() 调用将直接返回 0,且不产生任何其他副作用。SSIZE_MAX 上限:
count 大于 SSIZE_MAX,其结果属于实现定义行为(implementation-defined)。关于 Linux 系统的具体传输上限,请参见「注意事项 (NOTES)」章节。成功调用 read() 时,其实际读取并返回的字节数小于请求的 count 字节数属于完全正常的情况,并不代表发生错误。产生短读的常见原因包括:
count;ssize_t,非负整数)。0 明确表示已到达文件末尾 (EOF)。-1,并设置全局变量 errno 以指示具体的错误原因。| 错误宏 | 含义与产生原因 |
|---|---|
EAGAIN |
文件描述符 fd 指向非套接字文件,且该描述符已被设置为非阻塞模式(O_NONBLOCK),但当前无可读数据,继续读取将会导致进程阻塞。参见 open(2) 中关于 O_NONBLOCK 的说明。 |
EAGAIN 或 EWOULDBLOCK |
文件描述符 fd 指向套接字(socket),且已被设置为非阻塞模式(O_NONBLOCK),本次读取将会导致阻塞。POSIX.1-2001 允许在此情形下返回二者之一,便携式应用程序应当同时检测这两种错误码。 |
EBADF |
参数 fd 不是一个有效的文件描述符,或者该描述符未以可读模式打开(如以 O_WRONLY 标志打开)。 |
EFAULT |
参数 buf 指向的内存地址超出了调用进程可访问的有效虚拟地址空间(非法指针或未映射地址)。 |
EINTR |
在成功读取到任何有效数据之前,该系统调用被捕获到的信号中断;参见 signal(7)。 |
EINVAL |
fd 关联的对象不适合进行读取操作;或者文件以 O_DIRECT 标志打开,但 buf 地址、count 大小或底层文件偏移量未满足内存/块设备的对齐要求。 |
EINVAL |
fd 是通过 timerfd_create(2) 创建的计时器描述符,但传递给 read() 的缓冲区大小不正确(要求必须至少为 8 字节,即 sizeof(uint64_t))。 |
EIO |
底层物理 I/O 错误。常见于硬件故障(如磁盘坏道、磁带错误);或后台进程组成员尝试从其控制终端读取数据,而该进程忽略/阻塞了 SIGTTIN 信号,或其所属进程组已被孤立(orphaned)。在网络文件系统(NFS)上,如果此前持有的建议锁丢失(lost lock),也会导致返回此错误(参见 fcntl(2))。 |
EISDIR |
参数 fd 指向一个目录(普通目录不允许直接使用 read() 读取其原始二进制目录项,应使用 readdir(3))。 |
根据 fd 所连接的具体底层硬件、虚拟文件系统或特殊驱动设备,还可能引发其他特定设备的专有错误码。
read() 系统调用自早期的 Version 7 AT&T UNIX 便已存在,是 UNIX 操作系统与 POSIX 体系中最为核心的基础 I/O 原语之一。
在 Linux 系统上,read()(以及类似的读写系统调用如 pread(2)、write(2))单次传输的最大字节数限制为:
无论是在 32 位系统还是 64 位系统上,这一上限均严格成立。如果用户请求的 count 超出此范围,内核会将其截断至该最大值并执行读取,返回实际传输的字节数。
在 NFS 网络文件系统上,连续读取少量数据可能仅在第一次调用时更新文件的访问时间戳(st_atime),随后的调用可能不会继续更新。
st_atime 的更新委托给服务端,而命中客户端缓存的本地读取不会向服务端发送 RPC 请求。根据 POSIX.1-2008/SUSv4 规范(Section XSI 2.9.7),并发线程或多进程对普通文件进行 read() 和 readv(2) 操作时,对文件偏移量的修改必须保证互斥与原子性。
然而在 Linux 3.14 之前的内核实现中存在已知缺陷:若两个进程通过 fork(2) 或 dup(2) 共享同一个底层打开文件描述(Open File Description, OFD),并发发起 read() 时,内核在更新偏移量时缺乏互斥保证,导致两个进程可能错误地读取到相互重叠的数据块。该问题已在 Linux 3.14 中得到彻底修复。
close(2) — 关闭文件描述符fcntl(2) — 操控文件描述符属性与文件锁ioctl(2) — 控制设备 I/Olseek(2) — 重新定位读写文件偏移量open(2) — 打开或创建文件pread(2) — 在指定偏移量处读取文件(不更新文件偏移量)readv(2) — 分散读取数据到多个缓冲区(Scatter-Gather I/O)select(2) — 同步 I/O 多路复用write(2) — 向文件描述符写入数据sendfile(2) — 内核级文件到文件零拷贝传输splice(2) — 管道零拷贝数据移动epoll(7) — I/O 事件通知机制以下完整 C 示例展示了如何安全地使用 read() 读取文件内容,妥善处理局部读取(短读)以及被信号中断(EINTR)的边界情况:
#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#define BUFFER_SIZE 4096
/**
* 安全完整读取指定字节数的辅助函数
* 自动处理 EINTR 信号中断与短读情况
*/
ssize_t read_all(int fd, void *buf, size_t count)
{
size_t bytes_read = 0;
char *ptr = (char *)buf;
while (bytes_read < count) {
ssize_t res = read(fd, ptr + bytes_read, count - bytes_read);
if (res < 0) {
if (errno == EINTR) {
/* 被信号中断,重试读取 */
continue;
}
/* 遇到其他严重错误 */
return -1;
}
if (res == 0) {
/* 提前到达文件末尾 EOF */
break;
}
bytes_read += (size_t)res;
}
return (ssize_t)bytes_read;
}
int main(int argc, char *argv[])
{
const char *filepath = "/etc/issue";
if (argc > 1) {
filepath = argv[1];
}
int fd = open(filepath, O_RDONLY);
if (fd < 0) {
perror("open failed");
return EXIT_FAILURE;
}
char buffer[BUFFER_SIZE + 1];
ssize_t n = read_all(fd, buffer, BUFFER_SIZE);
if (n < 0) {
perror("read failed");
close(fd);
return EXIT_FAILURE;
}
buffer[n] = '\0';
printf("成功从 '%s' 读取了 %zd 字节数据:\n---\n%s\n---\n", filepath, n, buffer);
if (close(fd) < 0) {
perror("close failed");
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}对应手册: Linux man-pages 6.20 (2026)
分类章节: 第 2 节 —— 系统调用(System Calls)
目录代号:man2
sendfile - 在文件描述符之间直接传输数据(内核级数据拷贝 / 零拷贝)
标准 C 库 (libc, -lc)
#include <sys/sendfile.h>
ssize_t sendfile(int out_fd, int in_fd, off_t *_Nullable offset, size_t count);sendfile() 在一个文件描述符与另一个文件描述符之间直接复制数据。由于该复制过程完全在内核空间中完成,因此 sendfile() 比 read(2) 与 write(2) 的组合更加高效——后者需要将数据在内核空间与用户空间之间来回拷贝(造成额外的上下文切换与内存复制开销)。
in_fd:
mmap(2) 操作的文件(即不能是套接字 socket)。out_fd 是管道(pipe),则 sendfile() 会在内核中解糖(desugar)转换为 splice(2),并适用 splice(2) 的相应限制规则。out_fd:
out_fd 必须是一个套接字(socket)。out_fd 可以是任意文件。如果输出目标支持寻址定位(seekable),sendfile() 会相应地推进其文件偏移量。offset:
off_t *)。sendfile() 从该指针所指的偏移量位置开始读取 in_fd 的数据。当调用返回时,该变量会被更新为紧跟最后读取字节之后的下一个偏移量。在此模式下,sendfile() 不会改变 in_fd 自身的文件偏移量。in_fd 当前的文件偏移量处开始读取,并且调用完成后会直接推进并更新 in_fd 的当前文件偏移量。count:
out_fd 的字节数。
sendfile() 调用写入的字节数可能少于请求的字节数(例如受到底层网络缓冲区大小或文件末尾限制)。若仍有未发送完的字节,调用者应当准备好循环重试。详见【注意事项】。-1,并设置全局变量 errno 以指示具体错误原因。| 错误宏 | 含义与产生原因 |
|---|---|
EAGAIN |
使用了 O_NONBLOCK 设置非阻塞 I/O,且写入操作会发生阻塞。 |
EBADF |
输入文件描述符 in_fd 未以可读方式打开,或输出文件描述符 out_fd 未以可写方式打开。 |
EFAULT |
内存地址无效(例如 offset 指向了非法的用户空间地址)。 |
EINVAL |
描述符无效或已被锁定;或者 in_fd 不支持类似 mmap(2) 的操作;或者 count 为负数。 |
EINVAL |
out_fd 设置了 O_APPEND(追加模式)标志。sendfile() 目前不支持追加写入。 |
EIO |
从 in_fd 读取时发生了未指定的底层 I/O 错误。 |
ENOMEM |
内核内存不足,无法从 in_fd 读取数据。 |
EOVERFLOW |
count 过大,该传输操作会导致超出输入文件或输出文件允许的最大大小。 |
ESPIPE |
offset 不为 NULL,但输入文件是不可寻址定位的(not seekable)。 |
其他 UNIX 系统(如 FreeBSD, Solaris)中也实现了名为 sendfile() 的接口,但它们具有不同的参数签名和语义。因此,在编写需要高度跨平台移植的程序时不应直接依赖该 Linux 调用。
无(None)。本系统调用为 Linux 特有实现,不属于 POSIX.1 标准规范。
sendfile()。out_fd 支持常规文件,但在 2.6.x 早期被收窄仅支持 socket;直到 Linux 2.6.33 才彻底恢复并支持输出到任意文件。sendfile() 未考虑 64 位大文件偏移量。Linux 2.4 引入了 sendfile64() 系统调用(提供 64 位宽的 offset 参数)。在现代 Linux 和 glibc 环境下,开启 _FILE_OFFSET_BITS=64 后,glibc 会透明映射到 64 位实现。单次传输上限:
sendfile() 单次调用最多传输 0x7ffff000(即 2,147,479,552 字节,约 2GB - 4KB)的数据,返回实际传输的字节数。这在 32 位和 64 位系统上均成立。传输大文件时必须使用循环处理。
配合 TCP_CORK 优化:
如果使用 sendfile() 向 TCP 套接字发送文件,但需要在文件内容前先发送协议头(如 HTTP Response Header),建议使用 setsockopt(2) 开启 TCP_CORK 选项。这可以将头部数据与文件内容合并在同一个 TCP 报文中发出,最小化数据包数量,显著提升吞吐量。
优雅降级 (Fallback):
若应用程序在某些老旧内核或特殊文件系统上遇到 sendfile() 返回 EINVAL 或 ENOSYS,建议在代码中编写回退逻辑,降级为常规的 read(2) + write(2) 循环。
零拷贝内存一致性保护:
当 out_fd 是支持零拷贝的网络套接字或管道时,调用方必须确保源文件 in_fd 被传输的数据块在接收端彻底消费完成之前不被修改,否则可能导致对端接收到脏数据。
与 splice(2) 的关系:
Linux 特有的 splice(2) 支持在任意两个文件描述符之间传输数据(前提是其中至少一个是管道)。自 Linux 5.12 起,若 out_fd 为管道,sendfile() 内核底层直接复用 splice 机制。
#define _GNU_SOURCE
#include <sys/sendfile.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <errno.h>
/**
* @brief 将指定文件全量零拷贝发送至客户端 socket
* @param client_socket 目标网络连接描述符
* @param filepath 本地文件路径
* @return 成功返回发送的总字节数,失败返回 -1
*/
ssize_t send_file_zero_copy(int client_socket, const char *filepath) {
int file_fd = open(filepath, O_RDONLY);
if (file_fd < 0) {
perror("open failed");
return -1;
}
struct stat stat_buf;
if (fstat(file_fd, &stat_buf) < 0) {
perror("fstat failed");
close(file_fd);
return -1;
}
off_t offset = 0;
size_t remain_bytes = stat_buf.st_size;
ssize_t total_sent = 0;
// 循环发送,应对单次传输未完全发送或上限为 2GB 的场景
while (remain_bytes > 0) {
ssize_t sent = sendfile(client_socket, file_fd, &offset, remain_bytes);
if (sent <= 0) {
if (sent < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) {
// 非阻塞 socket 缓冲区满,可在 epoll/select 等可写事件后继续
continue;
}
if (sent < 0 && errno == EINTR) {
// 被信号中断,重试
continue;
}
perror("sendfile error");
close(file_fd);
return -1;
}
total_sent += sent;
remain_bytes -= (size_t)sent;
}
close(file_fd);
return total_sent;
}对应手册: Linux man-pages 6.20 (2026)
分类章节: 第 2 节 —— 系统调用(System Calls)
目录代号:man2
splice - 在管道与文件描述符之间直接移动数据(内核级数据移动 / 零拷贝)
标准 C 库 (libc, -lc)
#define _GNU_SOURCE /* 参见 feature_test_macros(7) */
#define _FILE_OFFSET_BITS 64
#include <fcntl.h>
ssize_t splice(int fd_in, off_t *_Nullable off_in,
int fd_out, off_t *_Nullable off_out,
size_t size, unsigned int flags);splice() 在两个文件描述符之间移动数据,无需在内核地址空间与用户空间之间来回拷贝数据。它最多将 size 字节的数据从文件描述符 fd_in 传输到文件描述符 fd_out。
splice() 调用的核心前提是:fd_in 和 fd_out 中至少有一个必须引用管道(pipe)。
针对输入描述符 fd_in 和偏移量指针 off_in:
fd_in 引用管道:
off_in 必须为 NULL。因为管道是不可寻址(nonseekable)的流式设备,无法指定偏移量。fd_in 不引用管道且 off_in 为 NULL:
fd_in 当前的文件偏移量处开始读取,并在调用完成后自动推进并更新 fd_in 的文件偏移量。fd_in 不引用管道且 off_in 不为 NULL:
off_in 必须指向一个指定读取起始偏移量的 off_t 变量。splice() 会从该偏移量读取数据,并在返回时将 off_in 指向的值更新为紧跟最后读取字节之后的偏移量。fd_in 自身的文件偏移量保持不变。针对输出描述符 fd_out 和偏移量指针 off_out,适用完全对等的语义规则:
fd_out 引用管道,则 off_out 必须为 NULL。fd_out 不引用管道且 off_out 为 NULL,则从当前文件偏移量写入并更新该偏移量。fd_out 不引用管道且 off_out 不为 NULL,则写入到指定偏移量并仅更新指针指向的值,不改变 fd_out 本身的文件偏移量。flags 参数是一个位掩码,由以下零个或多个常量按位或(OR)组合而成:
| 标志位 | 含义与行为解析 |
|---|---|
SPLICE_F_MOVE |
尝试直接移动内存页(Page Move)而非拷贝。这仅仅是对内核的建议/提示(hint):若内核无法直接转移页面引用,或管道缓冲区未包含完整物理页,依然会回退到内存页拷贝。历史说明:该标志的初始内核实现存在缺陷与安全性问题,因此自 Linux 2.6.21 起已变成空操作(no-op,但仍允许传入以保持源码兼容);未来内核版本可能会恢复正确高效的实现。 |
SPLICE_F_NONBLOCK |
非阻塞 I/O。使针对 splice 管道的操作变为非阻塞模式。但请注意:若拼接的目标/源描述符(非管道端)可能阻塞且其自身未设置 O_NONBLOCK,则 splice() 仍可能会被阻塞。 |
SPLICE_F_MORE |
提示内核在后续的 splice 调用中还会有更多数据到来。当 fd_out 引用网络套接字(socket)时这是一个极具价值的优化提示(语义类似于 send(2) 中的 MSG_MORE 以及 tcp(7) 中的 TCP_CORK 选项,能有效合并 TCP 报文段)。 |
SPLICE_F_GIFT |
在 splice() 中未使用(该标志仅为 vmsplice(2) 专用保留)。 |
0 表示到达输入流末尾(EOF)。若 fd_in 是管道,意味着管道中已无数据,且由于写端已全部关闭,继续阻塞等待也无意义。-1,并设置全局变量 errno 以指示具体的错误原因。| 错误宏 | 含义与产生原因 |
|---|---|
EAGAIN |
指定了 SPLICE_F_NONBLOCK,或某个文件描述符标记了非阻塞模式(O_NONBLOCK),且当前操作会发生阻塞。 |
EBADF |
其中一个或两个文件描述符无效,或者未以正确的读写模式打开(例如 fd_in 不可读或 fd_out 不可写)。 |
EINVAL |
目标底层文件系统不支持 splice 操作。 |
EINVAL |
目标输出文件以追加模式(O_APPEND)打开。splice() 不支持写入带有追加标志的文件。 |
EINVAL |
两个文件描述符均未引用管道(必须至少有一个是管道)。 |
EINVAL |
为不可寻址定位的设备(如管道)指定了非 NULL 的偏移量参数(off_in 或 off_out)。 |
EINVAL |
fd_in 和 fd_out 引用了同一个管道。 |
ENOMEM |
内核内存不足,无法分配管道缓冲区或内部控制结构。 |
ESPIPE |
off_in 或 off_out 不为 NULL,但对应的文件描述符引用的是管道。 |
无(None)。本系统调用为 Linux 特有(Linux-specific),不属于 POSIX.1 标准规范。
splice()。fd_in 和 fd_out 中恰好只能有一个是管道。系统调用 splice()、vmsplice(2) 与 tee(2) 为用户空间程序提供了对内核缓冲区的精细控制能力。内核使用与管道完全相同的缓冲区结构(pipe_buffer)来实现这一体系:
splice():在任意文件描述符与管道缓冲区之间双向搬运数据,或在两个管道缓冲区之间搬运。tee(2):在两个管道缓冲区之间“复制”数据(仅复制页面引用,不发生物理数据拷贝)。vmsplice(2):将用户空间内存页直接挂载/“复制”到管道缓冲区中。虽然接口逻辑概念被称为“移动”或“复制”,但内核实际上完全避免了真实的物理内存拷贝。
内核将管道缓冲区实现为指向内核物理内存页(struct page)的引用计数指针数组。当内核在管道之间传递数据时,仅在目标管道的缓冲区中创建指向该物理页的新指针,并递增该物理页的引用计数(page_ref_inc)。因此,拷贝的只是指针元数据,物理内存中的数据本身从未移动。
在传统的 32 位 x86 和 ARM 平台上,off_t 的默认宽度是 32 位(仅支持最大 2GB 偏移)。为了确保代码在处理大文件时的可移植性,若代码中使用了非空的 off_in / off_out,必须在引入头文件之前显式定义:
#define _FILE_OFFSET_BITS 64由于 splice() 要求必须有至少一端为管道,若要在两个非管道描述符之间传输数据(例如从 file_fd 到 socket_fd,或者从 socket_fd 到另一个 socket_fd),常见的工程范式是利用一对管道作为中转站:
[文件描述符 A] ---- splice ----> [管道 Pipe] ---- splice ----> [文件描述符 B]这种方法使得数据始终在内核页缓存与管道页之间流转,全程不需要进出用户空间内存缓冲区,比传统的 read() + write() 节省多次上下文切换与 CPU 内存拷贝开销。
#define _GNU_SOURCE
#define _FILE_OFFSET_BITS 64
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
/**
* @brief 使用中间管道通过两次 splice 在任意描述符之间零拷贝传输数据
* @param fd_in 源文件描述符
* @param fd_out 目标文件描述符
* @param len 传输字节数
* @return 成功传输的字节数,遇到错误返回 -1
*/
ssize_t transfer_splice(int fd_in, int fd_out, size_t len) {
int pipefd[2];
if (pipe(pipefd) < 0) {
perror("pipe creation failed");
return -1;
}
size_t total_transferred = 0;
while (total_transferred < len) {
size_t chunk = len - total_transferred;
// 步骤 1:从源文件 splice 到管道写端
ssize_t bytes_in = splice(fd_in, NULL, pipefd[1], NULL, chunk, SPLICE_F_MOVE | SPLICE_F_MORE);
if (bytes_in < 0) {
if (errno == EINTR) continue;
perror("splice in failed");
close(pipefd[0]);
close(pipefd[1]);
return -1;
}
if (bytes_in == 0) {
// 源文件已到达 EOF
break;
}
// 步骤 2:从管道读端 splice 到目标文件
size_t bytes_to_write = (size_t)bytes_in;
while (bytes_to_write > 0) {
ssize_t bytes_out = splice(pipefd[0], NULL, fd_out, NULL, bytes_to_write, SPLICE_F_MOVE | SPLICE_F_MORE);
if (bytes_out < 0) {
if (errno == EINTR) continue;
perror("splice out failed");
close(pipefd[0]);
close(pipefd[1]);
return -1;
}
bytes_to_write -= (size_t)bytes_out;
total_transferred += (size_t)bytes_out;
}
}
close(pipefd[0]);
close(pipefd[1]);
return (ssize_t)total_transferred;
}#define _GNU_SOURCE
#define _FILE_OFFSET_BITS 64
#include <err.h>
#include <fcntl.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
int main(void) {
int fd;
int pfd[2];
off_t off;
const char s[12] = "Hello, world";
// 创建或打开目标文件
fd = open("out.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd == -1)
err(EXIT_FAILURE, "open");
// 创建管道
if (pipe(pfd) == -1)
err(EXIT_FAILURE, "pipe");
// 将测试数据写入管道
if (write(pfd[1], s, sizeof(s)) != sizeof(s))
err(EXIT_FAILURE, "write");
close(pfd[1]); // 关闭写端以产生 EOF 条件
// 从管道 splice 到常规文件,并通过 &off 指定写入偏移量
off = 10;
if (splice(pfd[0], NULL, fd, &off, sizeof(s), 0) != sizeof(s))
err(EXIT_FAILURE, "splice");
close(pfd[0]);
printf("写入完成,更新后的文件偏移量为: %jd\n", (intmax_t)off);
close(fd);
unlink("out.txt"); // 清理测试文件
return EXIT_SUCCESS;
}对应手册: Linux man-pages 6.20 (2026)
分类章节: 第 2 节 —— 系统调用(System Calls)
目录代号:man2
tee - 复制管道内容(内核级零拷贝流分支复制)
标准 C 库 (libc, -lc)
#define _GNU_SOURCE /* 参见 feature_test_macros(7) */
#include <fcntl.h>
ssize_t tee(int fd_in, int fd_out, size_t size, unsigned int flags);tee() 系统调用最多将 size 字节的数据从文件描述符 fd_in 所引用的管道,复制到文件描述符 fd_out 所引用的管道中。
非消耗性读取(Non-consuming)特性:
tee() 不会消耗从 fd_in 中复制的数据!
这意味着被 tee() 复制的数据依然完整保留在源管道 fd_in 的缓冲区中,随后仍可被常规的 read(2)、splice(2) 或后续的 tee() 系统调用再次读取或移动。
fd_in:源管道的文件描述符(必须引用管道)。fd_out:目标管道的文件描述符(必须引用管道,且不能与 fd_in 引用同一个管道)。size:希望复制的最大字节数。flags:控制复制行为的位掩码参数,由以下零个或多个常量按位或(OR)组合而成:| 标志位 | 含义与行为解析 |
|---|---|
SPLICE_F_MOVE |
当前对 tee() 无效果(无操作 no-op,保留用于兼容 splice(2) 接口规范)。 |
SPLICE_F_NONBLOCK |
非阻塞 I/O。当源管道无数据可读或目标管道缓冲区已满时,不进入休眠等待,而是立即返回 -1 并将 errno 设置为 EAGAIN。具体非阻塞语义细节参见 splice(2)。 |
SPLICE_F_MORE |
当前对 tee() 无效果,但未来内核版本可能实现;参见 splice(2)。 |
SPLICE_F_GIFT |
对 tee() 未使用/无效;保留为 vmsplice(2) 专用。 |
0 表示当前无数据可传输(即源管道为空),并且此时阻塞已无意义,因为引用 fd_in 管道写端的所有写进程均已关闭(即写端已遇 EOF)。-1,并设置全局变量 errno 以指示具体的错误原因。| 错误宏 | 含义与产生原因 |
|---|---|
EAGAIN |
flags 中指定了 SPLICE_F_NONBLOCK,或者其中一个管道描述符被标记为非阻塞(O_NONBLOCK),且当前复制操作会引发阻塞。 |
EINVAL |
fd_in 或 fd_out 未引用管道设备;或者 fd_in 与 fd_out 引用了同一个管道。 |
ENOMEM |
内核内存不足,无法为目标管道分配新的管道缓冲区结构(struct pipe_buffer)或元数据。 |
EBADF |
fd_in 或 fd_out 不是有效的文件描述符,或未以适当权限打开。 |
Linux 特有。本系统调用为 Linux 专属接口(Linux-specific),非 POSIX.1 标准定义。
tee() 系统调用。从概念上讲,tee() 负责在两个管道之间“复制”数据。然而在 Linux 内核实现深处,根本不发生任何实际的物理内存数据拷贝。
内核底层的管道缓冲区使用 struct pipe_buffer 管理物理内存页(struct page)。当 tee() 执行时,内核仅在目标管道的缓冲区环形队列中追加指向源物理页的引用指针,并递增对应内存页的引用计数(get_page() / 引用递增)。
因此:
Linux 提供了三个紧密协作的管道零拷贝系统调用:
splice(2):数据移动(消费型)。在管道与任意文件/套接字之间传输数据,或者在两个管道间搬运数据(会消耗源端数据)。tee(2):数据分流(非消费型)。在两个管道之间复制数据,保留源管道数据供后续消费。vmsplice(2):用户空间与内核管道嫁接。将用户内存页直接映射插入到管道中。在网络代理、视频多路推流、实时日志归档等场景中,常用 tee() 与 splice() 组合构建高效的流分发系统:
+------------------> [管道 B] ---- splice ----> [网络 Socket / 实时处理]
| (tee 复制指针引用)
[数据源] ---> [管道 A]
|
+------------------> (splice 消费数据) -------> [本地文件 / 日志落盘]fcntl(fd, F_SETPIPE_SZ, size) 进行动态调整)。tee() 将数据复制到目标管道时,如果目标管道的缓冲区已满且未被消费端读取,tee() 可能会阻塞(若未设置 SPLICE_F_NONBLOCK)。以下程序使用 tee(2) 与 splice(2) 实现了类似 shell 命令 tee 的核心功能:从标准输入读取数据,在输出到标准输出的同时,将数据写入到指定的文件中。全程无需经过用户空间内存缓冲区。
#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
int fd;
ssize_t size, ssize;
if (argc != 2) {
fprintf(stderr, "用法: %s <目标文件>\n", argv[0]);
exit(EXIT_FAILURE);
}
fd = open(argv[1], O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd == -1) {
perror("open 失败");
exit(EXIT_FAILURE);
}
for (;;) {
/*
* 步骤 1: 使用 tee() 将 stdin (管道) 数据复制到 stdout (管道)。
* 注意:tee() 仅复制页引用,不会消耗 stdin 中的数据!
*/
size = tee(STDIN_FILENO, STDOUT_FILENO,
INT_MAX, SPLICE_F_NONBLOCK);
if (size < 0) {
if (errno == EAGAIN)
continue;
perror("tee 错误");
exit(EXIT_FAILURE);
}
if (size == 0)
break; // stdin 管道写端已关闭,传输完成
/*
* 步骤 2: 使用 splice() 将 stdin 中刚才被 tee 复制过的数据消费掉,
* 并直接写入到目标磁盘文件中。
*/
while (size > 0) {
ssize = splice(STDIN_FILENO, NULL, fd, NULL,
size, SPLICE_F_MOVE);
if (ssize < 0) {
perror("splice 写入文件错误");
exit(EXIT_FAILURE);
}
size -= ssize;
}
}
close(fd);
exit(EXIT_SUCCESS);
}# 编译程序
gcc -O2 tee_demo.c -o tee_demo
# 测试:通过管道输入,并同时在终端显示和写入日志
date | ./tee_demo out.log | cat
cat out.log在某些高性能微服务或旁路监控系统中,需要将一份数据流无损分发给两个独立的消费线程或进程:
#define _GNU_SOURCE
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
/**
* @brief 将源管道中的数据同时广播分发至目标管道 A 和目标管道 B
* @param p_in 源管道读端描述符
* @param p_outA 旁路管道写端描述符(保留副本)
* @param p_outB 主路管道写端描述符(消费数据)
* @param len 请求传输的最大字节数
* @return 实际分发的字节数
*/
ssize_t tee_broadcast(int p_in, int p_outA, int p_outB, size_t len) {
// 1. 将数据从 p_in 零拷贝复制给旁路管道 p_outA(不消耗 p_in 数据)
ssize_t teed = tee(p_in, p_outA, len, SPLICE_F_NONBLOCK);
if (teed < 0) {
if (errno != EAGAIN) {
perror("tee 到旁路管道失败");
return -1;
}
teed = 0; // 旁路满时可根据业务选择跳过或重试
}
// 2. 将数据从 p_in splice 移动消费至主路管道 p_outB
size_t to_splice = (teed > 0) ? (size_t)teed : len;
ssize_t spliced = splice(p_in, NULL, p_outB, NULL, to_splice, SPLICE_F_MOVE);
if (spliced < 0) {
perror("splice 到主路管道失败");
return -1;
}
return spliced;
}