--- title: 高级IO tags: [嵌入式Linux, Linux应用编程, 高级IO, 非阻塞IO, IO多路复用, select, poll, epoll, 异步IO, SIGIO, mmap, 存储映射IO, 文件锁, flock, fcntl, IMX6ULL] created: 2026-09-18 updated: 2026-09-18 pdf_ref: "《I.MX6U嵌入式Linux C应用编程指南V1.6》第十三章 高级I/O" --- # 高级IO > 💡 **关联知识**:[[03-外设与高级IO编程/02-GPIO与LED应用编程]];延伸阅读:[[Linux+C+C++技术体系梳理/2. Linux系统编程/17. IO多路复用]]、[[Linux+C+C++技术体系梳理/2. Linux系统编程/20. 共享内存与mmap]]、[[Linux+C+C++技术体系梳理/2. Linux系统编程/6. 信号机制]] 前面章节讨论的文件 I/O 都是"打开→read/write→close"的直线流程。本篇汇总文件 I/O 的五类高级用法,用来应对不同应用场景:**非阻塞 I/O**、**I/O 多路复用**、**异步 I/O**、**存储映射 I/O**、**文件锁**。它们是后面提高篇(LCD、输入设备、网络)反复用到的底层工具。 > 约定:本篇出现的设备路径 `/dev/input/event3` 是正点原子教程所用 Ubuntu 主机上鼠标对应的设备文件,在自己的机器上要先用 `sudo od -x /dev/input/eventX` 确认,不要照抄编号。 --- ## 1. 非阻塞 I/O ### 1.1 阻塞与非阻塞 "阻塞"就是**进入了休眠状态,交出了 CPU 控制权**。前面学过的 `wait()`、`pause()`、`sleep()` 都会阻塞。I/O 的阻塞则特指读写操作: - **阻塞式 I/O**:对文件发起读操作时,若数据未准备好、当前无数据可读,调用者会被挂起,直到有数据可读才被唤醒。 - **非阻塞式 I/O**:即使没有数据可读,也不会被阻塞,而是**立即返回错误**(`errno = EAGAIN`,即 "Resource temporarily unavailable")。 一个关键前提:**普通文件的读写永远不会阻塞**,不管读写多少字节,`read()`/`write()` 一定会在有限时间内返回。这是普通文件本质决定的。只有**管道文件、网络设备文件、字符设备文件**等才区分阻塞/非阻塞。 ### 1.2 两种打开方式 | 方式 | 做法 | 说明 | | ---- | ---- | ---- | | 默认(阻塞) | `open(path, O_RDONLY)` | 不指定 `O_NONBLOCK` 即为阻塞 | | 非阻塞 | `open(path, O_RDONLY \| O_NONBLOCK)` | 后续 I/O 以非阻塞方式进行 | | 改造已有 fd | `fcntl(fd, F_GETFL)` → `flag \|= O_NONBLOCK` → `fcntl(fd, F_SETFL, flag)` | 用于别人打开、自己继承来的 fd(如标准输入 0) | `open` 时无法指定 `O_ASYNC`,但 `O_NONBLOCK` 可以在 `open` 时直接指定。普通文件设置与否没有影响。 ### 1.3 阻塞 I/O 的困境 程序若先 `read` 鼠标、再 `read` 键盘,只要鼠标没数据,进程就卡在鼠标的 `read` 上,后面的键盘读取永远得不到执行——**无法实现并发读取**。用多线程/多进程可以绕开,但不是本节重点。非阻塞 + 轮询能解决"卡死",却带来新问题。 ### 1.4 阻塞 vs 非阻塞对比 | 维度 | 阻塞 I/O | 非阻塞 I/O | | ---- | -------- | ---------- | | 无数据时 | 挂起休眠,交出 CPU | 立即返回错误 | | CPU 占用 | 几乎为 0 | 轮询时接近 100% | | 并发读取 | 单线程做不到(会被第一个 I/O 卡住) | 轮询可以做到 | | 适用场景 | 单设备、愿意等待 | 多设备非阻塞 + 配合多路复用 | ```mermaid flowchart LR subgraph B[阻塞 I/O] B1[read 鼠标] --> B2{有数据?} B2 -- 否 --> B3[进程休眠 交出CPU] B3 --> B2 B2 -- 是 --> B4[返回数据] end subgraph N[非阻塞 I/O] N1[read 鼠标] --> N2{有数据?} N2 -- 否 --> N3[立即返回 EAGAIN] N3 --> N1 N2 -- 是 --> N4[返回数据] end ``` ### 1.5 完整例程:非阻塞方式同时读取鼠标与键盘 这是把"阻塞困境"改造成"轮询并发"的完整程序。键盘是标准输入 fd=0,需要 `fcntl` 手动改成非阻塞。 ```c #include #include #include #include #include #include #define MOUSE "/dev/input/event3" int main(void) { char buf[100]; int fd, ret, flag; /* 打开鼠标设备文件 */ fd = open(MOUSE, O_RDONLY | O_NONBLOCK); if (-1 == fd) { perror("open error"); exit(-1); } /* 将键盘(标准输入 fd=0)设置为非阻塞方式 */ flag = fcntl(0, F_GETFL); //先获取原来的 flag flag |= O_NONBLOCK; //将 O_NONBLOCK 标志添加到 flag fcntl(0, F_SETFL, flag); //重新设置 flag for ( ; ; ) { /* 读鼠标 */ ret = read(fd, buf, sizeof(buf)); if (0 < ret) printf("鼠标: 成功读取<%d>个字节数据\n", ret); /* 读键盘 */ ret = read(0, buf, sizeof(buf)); if (0 < ret) printf("键盘: 成功读取<%d>个字节数据\n", ret); } /* 关闭文件 */ close(fd); exit(0); } ``` 逐段说明: - `O_NONBLOCK` 让鼠标 `read` 无数据时立刻返回,循环不会被卡死。 - 键盘 fd 是继承来的,只能用 `F_GETFL`/`F_SETFL` 两步加标志,不能重新 `open`。 - 循环里虽然"并发"了,但空转轮询导致 **CPU 占用率极高**——这正是引出 I/O 多路复用的动机。 --- ## 2. I/O 多路复用 ### 2.1 概念 I/O 多路复用(IO multiplexing)用一个机制**同时监视多个文件描述符**,一旦其中某个可以执行 I/O,就通知应用程序去读写。它解决"进程阻塞在某个特定 I/O 系统调用上"的问题,是**外部阻塞、内部监视多路 I/O**:调用本身会阻塞,但阻塞期间内核替你盯着多个 fd。两个功能几乎相同的系统调用是 `select()` 和 `poll()`。 ```mermaid flowchart TD A[用户进程] --> B{select/poll 阻塞等待} B -->|鼠标就绪| C[返回, 可用集合中标记鼠标] B -->|键盘就绪| D[返回, 可用集合中标记键盘] B -->|超时/无就绪| E[返回 0 或错误] C --> F[对就绪fd执行 read/write 清除就绪态] D --> F F --> B ``` ### 2.2 select() 函数 ```c #include int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); ``` | 参数 | 含义 | | ---- | ---- | | `nfds` | 三个集合中**最大文件描述符编号值 + 1** | | `readfds` | 关心"是否可读"的 fd 集合,不关心传 `NULL` | | `writefds` | 关心"是否可写"的 fd 集合,不关心传 `NULL` | | `exceptfds` | 关心"异常情况"的 fd 集合(不是出错),不关心传 `NULL` | | `timeout` | `NULL`:一直阻塞;`0`(两成员为 0):不阻塞只轮询一次;否则为等待上限 | `fd_set` 是位掩码集合,全部操作由四个宏完成: | 宏 | 作用 | | -- | ---- | | `FD_ZERO(fd_set *set)` | 将集合初始化为空 | | `FD_SET(int fd, fd_set *set)` | 把 fd 加入集合 | | `FD_CLR(int fd, fd_set *set)` | 把 fd 从集合移除 | | `FD_ISSET(int fd, fd_set *set)` | fd 是集合成员则返回真 | 集合容量由 `FD_SETSIZE` 决定,Linux 下为 **1024**。使用前必须 `FD_ZERO` 初始化。 **返回值**(三种情况): | 返回值 | 含义 | | ------ | ---- | | `-1` | 出错,设置 `errno`(`EBADF` fd 非法、`EINTR` 被信号打断等) | | `0` | 超时,没有任何 fd 就绪,三个集合被清空 | | `> 0` | 处于就绪态的 fd 总数(同一 fd 在多个集合就绪会被重复统计) | **关键特性(值-结果参数)**:`select()` 返回时会**修改**传入的集合,使其只保留就绪的 fd。因此若在循环中重复调用,**每次调用前都必须重新 `FD_ZERO` 并重新 `FD_SET`**。 ### 2.3 poll() 函数 ```c #include int poll(struct pollfd *fds, nfds_t nfds, int timeout); ``` | 参数 | 含义 | | ---- | ---- | | `fds` | `struct pollfd` 数组,每个元素指定一个 fd 及关心的事件 | | `nfds` | 数组元素个数(`nfds_t` 实际为无符号整型) | | `timeout` | `-1`:一直阻塞;`0`:不阻塞只检查一次;`> 0`:最多阻塞 timeout 毫秒 | ```c struct pollfd { int fd; /* file descriptor */ short events; /* requested events (输入,调用者填写) */ short revents; /* returned events (输出,内核填写) */ }; ``` `events` 是调用者填写(关心的条件),`revents` 由内核在返回时填写,`poll()` 不会改动 `events`。常用事件标志: | 标志 | events 可填 | revents 可返回 | 说明 | | ---- | :---------: | :------------: | ---- | | `POLLIN` | ● | ● | 有数据可读 | | `POLLRDNORM` | ● | ● | 等价于 `POLLIN` | | `POLLRDBAND` | ● | ● | 可读优先级数据(Linux 通常不用) | | `POLLPRI` | ● | ● | 可读高优先级数据(GPIO 中断用它) | | `POLLRDHUP` | ● | ● | 对端套接字关闭 | | `POLLOUT` | ● | ● | 可写入数据 | | `POLLWRNORM` | ● | ● | 等价于 `POLLOUT` | | `POLLWRBAND` | ● | ● | 优先级数据可写 | | `POLLERR` | — | ● | 有错误发生 | | `POLLHUP` | — | ● | 出现挂断 | | `POLLNVAL` | — | ● | 文件描述符未打开 | 不关心某 fd 时,可把 `events` 设为 0,或把 `fd` 设为负数(`-fd`),后者会让该元素被忽略且 `revents` 恒为 0。实际编程中最常用的是 `POLLIN`、`POLLOUT`;GPIO 中断场景用 `POLLPRI`。 **返回值**与 `select()` 一致:`-1` 出错;`0` 超时;`> 0` 表示 `revents` 非 0 的元素个数。 ### 2.4 完整例程:select 实现同时读取键盘和鼠标 ```c #include #include #include #include #include #include #include #define MOUSE "/dev/input/event3" int main(void) { char buf[100]; int fd, ret = 0, flag; fd_set rdfds; int loops = 5; /* 打开鼠标设备文件,并设置为非阻塞 */ fd = open(MOUSE, O_RDONLY | O_NONBLOCK); if (-1 == fd) { perror("open error"); exit(-1); } /* 将键盘设置为非阻塞方式 */ flag = fcntl(0, F_GETFL); flag |= O_NONBLOCK; fcntl(0, F_SETFL, flag); /* 同时读取键盘和鼠标 */ while (loops--) { FD_ZERO(&rdfds); FD_SET(0, &rdfds); //添加键盘 FD_SET(fd, &rdfds); //添加鼠标 ret = select(fd + 1, &rdfds, NULL, NULL, NULL); if (0 > ret) { perror("select error"); goto out; } else if (0 == ret) { fprintf(stderr, "select timeout.\n"); continue; } /* 检查键盘是否为就绪态 */ if (FD_ISSET(0, &rdfds)) { ret = read(0, buf, sizeof(buf)); if (0 < ret) printf("键盘: 成功读取<%d>个字节数据\n", ret); } /* 检查鼠标是否为就绪态 */ if (FD_ISSET(fd, &rdfds)) { ret = read(fd, buf, sizeof(buf)); if (0 < ret) printf("鼠标: 成功读取<%d>个字节数据\n", ret); } } out: close(fd); exit(ret); } ``` 编译运行(主机上,非开发板):`gcc select_test.c -o select_test && sudo ./select_test`。先动鼠标或先按键都能正确读取。 ### 2.5 完整例程:poll 实现同时读取键盘和鼠标 把 `select` 换成 `poll`,逻辑完全一致: ```c #include #include #include #include #include #include #include #define MOUSE "/dev/input/event3" int main(void) { char buf[100]; int fd, ret = 0, flag; int loops = 5; struct pollfd fds[2]; /* 打开鼠标设备文件,并设置为非阻塞 */ fd = open(MOUSE, O_RDONLY | O_NONBLOCK); if (-1 == fd) { perror("open error"); exit(-1); } /* 将键盘设置为非阻塞方式 */ flag = fcntl(0, F_GETFL); flag |= O_NONBLOCK; fcntl(0, F_SETFL, flag); /* 填充 pollfd 数组:只关心数据可读 */ fds[0].fd = 0; fds[0].events = POLLIN; fds[0].revents = 0; fds[1].fd = fd; fds[1].events = POLLIN; fds[1].revents = 0; while (loops--) { ret = poll(fds, 2, -1); if (0 > ret) { perror("poll error"); goto out; } else if (0 == ret) { fprintf(stderr, "poll timeout.\n"); continue; } /* 检查键盘是否为就绪态 */ if (fds[0].revents & POLLIN) { ret = read(0, buf, sizeof(buf)); if (0 < ret) printf("键盘: 成功读取<%d>个字节数据\n", ret); } /* 检查鼠标是否为就绪态 */ if (fds[1].revents & POLLIN) { ret = read(fd, buf, sizeof(buf)); if (0 < ret) printf("鼠标: 成功读取<%d>个字节数据\n", ret); } } out: close(fd); exit(ret); } ``` **使用 select/poll 的共同注意事项**:监测到某个 fd 就绪后,**必须对该 fd 执行 I/O 以清除就绪状态**,否则该状态会一直存在,下一次调用会立刻返回。 ### 2.6 select / poll / epoll 对比 > ⚠️ **来源说明**:epoll 部分不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。原书明确说明"本书并不会介绍 epoll 相关内容",仅提示当需要检查大量文件描述符时可用 epoll 解决 select/poll 性能低的问题,且 epoll 在性能上与异步 I/O 相似、并有一些胜过异步 I/O 的优点。 | 维度 | select | poll | epoll | | ---- | ------ | ---- | ----- | | 数据结构 | `fd_set` 位图 | `pollfd` 数组 | 内核红黑树 + 就绪链表 | | fd 上限 | `FD_SETSIZE`=1024 | 无硬上限(受内存限制) | 无硬上限 | | 每次调用开销 | O(n),且需重建/拷贝集合 | O(n) 轮询 | O(1) 取就绪链表(`epoll_wait`) | | 是否修改入参 | 是(值-结果),需重复初始化 | 否(`revents` 与 `events` 分离) | 否 | | 触发模式 | 只有水平触发 | 只有水平触发 | 支持 LT 与 ET | | 大量 fd 场景 | 差 | 差 | 优 | | 少量 fd 场景 | 够用、可移植性好 | 够用 | 略重 | epoll 三个核心函数: ```c #include int epoll_create(int size); // 创建 epoll 实例,返回 epoll fd int epoll_create1(int flags); // 推荐,flags 可传 EPOLL_CLOEXEC int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); ``` `epoll_ctl` 的 `op` 取值:`EPOLL_CTL_ADD`(注册)、`EPOLL_CTL_MOD`(修改)、`EPOLL_CTL_DEL`(注销)。`struct epoll_event` 中 `events` 常用 `EPOLLIN` / `EPOLLOUT`,并可或上 `EPOLLET`(边沿触发)、`EPOLLONESHOT`(只通知一次)。`epoll_wait` 返回就绪事件个数,`timeout` 单位毫秒(`-1` 永久阻塞,`0` 立即返回)。 **LT(水平触发,默认)**:只要 fd 上还有数据可读,每次 `epoll_wait` 都会通知。编程简单,不易丢事件。 **ET(边沿触发)**:仅在状态**发生变化**时通知一次。若一次没把数据读干净,后续不再通知,事件就丢了。ET 必须把 fd 设为**非阻塞**,并用循环 `read` 到返回 `EAGAIN` 为止。 ```c #include #include #include #include #include #include #define MAX_EVENTS 10 int main(int argc, char *argv[]) { int epfd, nfds, fd, i; struct epoll_event ev, events[MAX_EVENTS]; if (2 != argc) { fprintf(stderr, "usage: %s \n", argv[0]); exit(-1); } /* 以非阻塞方式打开目标文件/设备 */ fd = open(argv[1], O_RDONLY | O_NONBLOCK); if (0 > fd) { perror("open error"); exit(-1); } /* 创建 epoll 实例 */ epfd = epoll_create1(0); if (0 > epfd) { perror("epoll_create1 error"); exit(-1); } /* 注册可读事件;若要边沿触发,改成 EPOLLIN | EPOLLET */ ev.events = EPOLLIN; ev.data.fd = fd; if (0 > epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev)) { perror("epoll_ctl error"); exit(-1); } for ( ; ; ) { nfds = epoll_wait(epfd, events, MAX_EVENTS, -1); if (0 > nfds) { perror("epoll_wait error"); exit(-1); } for (i = 0; i < nfds; i++) { if (events[i].events & EPOLLIN) { char buf[100]; /* LT:读一次即可;ET:必须循环读到 EAGAIN */ int ret = read(events[i].data.fd, buf, sizeof(buf)); if (0 < ret) printf("读到 %d 字节\n", ret); } } } close(fd); close(epfd); return 0; } ``` > ⚠️ **来源说明**:以下为 POSIX 异步 I/O 扩展知识,原书未涉及。原书的"异步 I/O"指 `O_ASYNC` + `SIGIO` 的**信号驱动 I/O**(见第 3 节)。 POSIX 异步 I/O(``)把"发起 I/O"与"等结果"真正分离,可在数据就绪或完成时通过信号/回调通知: ```c #include #include #include #include int main(void) { int fd = open("test.txt", O_RDONLY); char buf[256] = {0}; struct aiocb cb; struct sigevent sev; memset(&cb, 0, sizeof(cb)); cb.aio_fildes = fd; cb.aio_buf = buf; cb.aio_nbytes = sizeof(buf); cb.aio_offset = 0; /* 用信号通知:内核在 I/O 完成时发送 SIGRTMIN + 1 */ sev.sigev_notify = SIGEV_SIGNAL; sev.sigev_signo = SIGRTMIN + 1; cb.aio_sigevent = sev; aio_read(&cb); // 异步发起,立即返回 /* ... 主线程可继续做别的事 ... */ while (aio_error(&cb) == EINPROGRESS) ; // 演示用忙等;实战应 aio_suspend 或等信号 int n = aio_return(&cb); // 取真正的返回字节数 (void)n; close(fd); return 0; } ``` 要点:`aio_read`/`aio_write` 只负责**提交请求**;结果用 `aio_error()`(`EINPROGRESS` 表示未完成)与 `aio_return()` 获取;通知方式由 `aiocb.aio_sigevent` 决定——`SIGEV_SIGNAL` 发信号,`SIGEV_THREAD` 由线程回调。部分环境下需链接 `-lrt`(老 glibc)。 --- ## 3. 异步 I/O(信号驱动 I/O) ### 3.1 原理 I/O 多路复用是进程**主动查询**"谁能读写了";异步 I/O 反过来:**文件描述符可以执行 I/O 时,内核主动给进程发一个信号**。进程可以先做别的任务,收到信号再处理。因为绑定信号,异步 I/O 也常被称为**信号驱动 I/O**。 使用步骤: 1. 通过 `O_NONBLOCK` 使能非阻塞 I/O; 2. 通过 `O_ASYNC` 使能异步 I/O(`open` 时无法指定,必须用 `fcntl`); 3. 设置异步 I/O 事件的接收进程(`F_SETOWN`); 4. 为通知信号(默认 `SIGIO`)注册信号处理函数; 5. 进程执行其它任务,I/O 就绪时内核发 `SIGIO`,在信号处理函数里做 I/O。 ```c int flag = fcntl(fd, F_GETFL); flag |= O_ASYNC; // 使能异步 I/O fcntl(fd, F_SETFL, flag); fcntl(fd, F_SETOWN, getpid()); // 把当前进程设为异步 I/O 所有者 ``` ### 3.2 完整例程:以异步 I/O 方式读取鼠标 ```c #include #include #include #include #include #include #include #define MOUSE "/dev/input/event3" static int fd; static void sigio_handler(int sig) { static int loops = 5; char buf[100] = {0}; int ret; if (SIGIO != sig) return; ret = read(fd, buf, sizeof(buf)); // 在信号处理函数中读取 if (0 < ret) printf("鼠标: 成功读取<%d>个字节数据\n", ret); loops--; if (0 >= loops) { close(fd); exit(0); } } int main(void) { int flag; /* 打开鼠标设备文件<使能非阻塞 I/O> */ fd = open(MOUSE, O_RDONLY | O_NONBLOCK); if (-1 == fd) { perror("open error"); exit(-1); } /* 使能异步 I/O */ flag = fcntl(fd, F_GETFL); flag |= O_ASYNC; fcntl(fd, F_SETFL, flag); /* 设置异步 I/O 的所有者 */ fcntl(fd, F_SETOWN, getpid()); /* 为 SIGIO 信号注册信号处理函数 */ signal(SIGIO, sigio_handler); for ( ; ; ) sleep(1); } ``` ### 3.3 异步 I/O 的两个缺陷与优化 | 缺陷 | 说明 | | ---- | ---- | | 信号可能丢失 | `SIGIO` 是标准(非实时、不可靠)信号,不支持排队。处理函数执行期间又来了多次 `SIGIO`,只会传递一次,其余丢失 | | 无法得知发生了什么事件 | 处理函数里直接 `read()`,并不知道 fd 到底可读、可写还是异常 | 优化方法: - **用实时信号替换 SIGIO**:`fcntl(fd, F_SETSIG, SIGRTMIN);`(`arg=0` 则恢复默认 `SIGIO`)。使用 `F_SETSIG` 需要先定义 `_GNU_SOURCE` 宏。 - **用 `sigaction()` + `SA_SIGINFO` 注册处理函数**,通过 `siginfo_t` 获取更多信息: | 字段 | 含义 | | ---- | ---- | | `si_signo` | 引发处理函数的信号值 | | `si_fd` | 发生异步 I/O 事件的 fd | | `si_code` | 发生了什么事件(见下表) | | `si_band` | 位掩码,值与 `poll()` 的 `revents` 相同 | | `si_code` | `si_band` 掩码值 | 说明 | | --------- | ---------------- | ---- | | `POLL_IN` | `POLLIN \| POLLRDNORM` | 可读取数据 | | `POLL_OUT` | `POLLOUT \| POLLWRNORM \| POLLWRBAND` | 可写入数据 | | `POLL_MSG` | `POLLIN \| POLLRDNORM \| POLLMSG` | 不使用 | | `POLL_ERR` | `POLLERR` | I/O 错误 | | `POLL_PRI` | `POLLPRI \| POLLRDNORM` | 可读高优先级数据 | | `POLL_HUP` | `POLLHUP \| POLLERR` | 出现宕机 | ### 3.4 完整例程:优化后的异步 I/O ```c #define _GNU_SOURCE //使用 F_SETSIG 需要定义该宏 #include #include #include #include #include #include #include #define MOUSE "/dev/input/event3" static int fd; static void io_handler(int sig, siginfo_t *info, void *context) { static int loops = 5; char buf[100] = {0}; int ret; if (SIGRTMIN != sig) return; /* 判断鼠标是否可读 */ if (POLL_IN == info->si_code) { ret = read(fd, buf, sizeof(buf)); if (0 < ret) printf("鼠标: 成功读取<%d>个字节数据\n", ret); loops--; if (0 >= loops) { close(fd); exit(0); } } } int main(void) { struct sigaction act; int flag; /* 打开鼠标设备文件<使能非阻塞 I/O> */ fd = open(MOUSE, O_RDONLY | O_NONBLOCK); if (-1 == fd) { perror("open error"); exit(-1); } /* 使能异步 I/O */ flag = fcntl(fd, F_GETFL); flag |= O_ASYNC; fcntl(fd, F_SETFL, flag); /* 设置异步 I/O 的所有者 */ fcntl(fd, F_SETOWN, getpid()); /* 指定实时信号 SIGRTMIN 作为异步 I/O 通知信号 */ fcntl(fd, F_SETSIG, SIGRTMIN); /* 为实时信号 SIGRTMIN 注册信号处理函数 */ act.sa_sigaction = io_handler; act.sa_flags = SA_SIGINFO; sigemptyset(&act.sa_mask); sigaction(SIGRTMIN, &act, NULL); for ( ; ; ) sleep(1); } ``` ### 3.5 异步 I/O 与 epoll 的适用性 异步 I/O 与 epoll 在需要**同时检查大量(如数千个)文件描述符**时,性能相比 select/poll 有显著优势。原因:异步 I/O 中内核"记住"要检查的 fd,仅在可 I/O 时才发信号;select/poll 内部却是轮询检查所有 fd。fd 数量不多时,select/poll 依然是简单可靠的方案。 --- ## 4. 存储映射 I/O ### 4.1 原理 存储映射 I/O(memory-mapped I/O)把一个**文件映射到进程地址空间的一块内存**:读这块内存等于读文件,写这块内存等于写文件,无需调用 `read()`/`write()`。其实质是**共享**——磁盘文件直接与映射区关联,映射区可看作应用层与内核层之间的共享内存,与 IPC 的共享内存很相似。 ```mermaid flowchart LR A[普通 I/O] --> A1[磁盘文件] -->|read 拷贝| A2[内核缓冲区] -->|拷贝| A3[用户缓冲区] A3 -->|write 拷贝| A2 -->|拷贝| A1 B[存储映射 I/O] --> B1[磁盘文件] -->|mmap 建立映射| B2[进程映射区] B2 -->|直接读写, 无需read/write| B1 ``` ### 4.2 mmap() 与 munmap() ```c #include void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset); int munmap(void *addr, size_t length); ``` | 参数 | 含义 | | ---- | ---- | | `addr` | 映射区起始地址,通常传 `NULL` 由系统选择 | | `length` | 映射长度(字节),`length` 不能大于文件大小 | | `prot` | 保护要求:`PROT_EXEC`/`PROT_READ`/`PROT_WRITE`/`PROT_NONE`,可位或组合 | | `flags` | 必须二选一:`MAP_SHARED`/`MAP_PRIVATE`,可再或其它标志 | | `fd` | 要映射的文件描述符 | | `offset` | 文件映射偏移量,通常为 0,必须为系统页大小整数倍 | `flags` 的关键取值: | 标志 | 说明 | | ---- | ---- | | `MAP_SHARED` | 写入映射区会更新到文件,并允许其它进程共享 | | `MAP_PRIVATE` | 写时复制一份私有副本,对映射区的修改不写回文件;解除映射后修改丢弃 | | `MAP_FIXED` | 强制使用 `addr` 指定的地址,失败才放弃,不利于移植,通常不用 | | `MAP_ANONYMOUS` | 匿名映射,忽略 `fd` 和 `offset`,不涉及文件,无法与其它进程共享 | | `MAP_LOCKED` | 对映射区上锁 | **返回值**:成功返回映射区起始地址;失败返回 `(void *)-1`,即 `MAP_FAILED`,并设置 `errno`。 **页对齐**:`addr` 和 `offset` 通常要求是系统页大小整数倍,可用 `sysconf(_SC_PAGE_SIZE)` 或 `sysconf(_SC_PAGESIZE)` 获取页大小。`length` 无对齐要求:映射区实际大小是页大小的整数倍,尾部多余字节被置 0,可访问但不反映到文件;访问映射区末尾之后的内存会触发 `SIGBUS`。 **与映射区相关的两个信号**: | 信号 | 触发条件 | | ---- | -------- | | `SIGSEGV` | 映射区被指定为只读,进程却试图写入 | | `SIGBUS` | 映射区某部分在访问时已不存在(如另一进程把文件截断) | **解除映射**:`munmap(addr, length)`,`addr` 必须是页大小整数倍。进程终止时自动解除映射,但**`close()` 关闭文件不会解除映射**。 ### 4.3 mprotect() 与 msync() ```c int mprotect(void *addr, size_t len, int prot); // 修改已有映射区的保护要求 int msync(void *addr, size_t length, int flags); // 把映射区数据刷回磁盘 ``` `msync` 的 `flags`: | 标志 | 说明 | | ---- | ---- | | `MS_ASYNC` | 异步同步,调用后不等数据写完磁盘即返回 | | `MS_SYNC` | 同步同步,等数据全部写入磁盘后才返回 | | `MS_INVALIDATE` | 可选,使同一文件的其它映射失效 | 注意:`munmap()` **不会**把映射区内容写到磁盘;`MAP_SHARED` 下由内核在某个时刻自动更新文件;`MAP_PRIVATE` 下解除映射后修改全部丢弃。 ### 4.4 完整例程:用存储映射 I/O 复制文件 ```c #include #include #include #include #include #include #include #include int main(int argc, char *argv[]) { int srcfd, dstfd; void *srcaddr; void *dstaddr; int ret; struct stat sbuf; if (3 != argc) { fprintf(stderr, "usage: %s \n", argv[0]); exit(-1); } /* 打开源文件 */ srcfd = open(argv[1], O_RDONLY); if (-1 == srcfd) { perror("open error"); exit(-1); } /* 打开目标文件 */ dstfd = open(argv[2], O_RDWR | O_CREAT | O_TRUNC, 0664); if (-1 == dstfd) { perror("open error"); ret = -1; goto out1; } /* 获取源文件的大小,并把目标文件设为相同大小 */ fstat(srcfd, &sbuf); ftruncate(dstfd, sbuf.st_size); /* 将源文件映射到内存区域中 */ srcaddr = mmap(NULL, sbuf.st_size, PROT_READ, MAP_SHARED, srcfd, 0); if (MAP_FAILED == srcaddr) { perror("mmap error"); ret = -1; goto out2; } /* 将目标文件映射到内存区域中 */ dstaddr = mmap(NULL, sbuf.st_size, PROT_WRITE, MAP_SHARED, dstfd, 0); if (MAP_FAILED == dstaddr) { perror("mmap error"); ret = -1; goto out3; } /* 直接操作映射区完成复制 */ memcpy(dstaddr, srcaddr, sbuf.st_size); /* 清理工作 */ out4: munmap(dstaddr, sbuf.st_size); //解除目标文件映射 out3: munmap(srcaddr, sbuf.st_size); //解除源文件映射 out2: close(dstfd); //关闭目标文件 out1: close(srcfd); //关闭源文件并退出 exit(ret); } ``` 测试:`echo "hello" > srcfile && ./testApp srcfile dstfile && cat dstfile`,内容与源文件一致。 ### 4.5 匿名映射与共享内存 > ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。原书在 `mmap` 的 `flags` 中介绍了 `MAP_ANONYMOUS` 标志("不涉及文件,映射区域无法和其它进程共享"),本节在此基础上补充其典型用法。这里说的"无法共享"指**无亲缘关系的进程**无法访问匿名映射(没有文件/路径可供其它进程引用);但 `MAP_SHARED | MAP_ANONYMOUS` 建立的内存会随 `fork()` 被子进程继承,因此**父子进程之间可以共享**。 不涉及文件时使用 `MAP_ANONYMOUS`(与 `MAP_SHARED` 搭配,用于 fork 出的父子进程共享内存): ```c #include #include #include #include #include int main(void) { /* 匿名映射一块 4096 字节、父子进程共享的内存,忽略 fd,fd 传 -1 */ char *p = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0); if (MAP_FAILED == p) return -1; pid_t pid = fork(); if (0 == pid) { // 子进程 strcpy(p, "hello from child"); } else if (0 < pid) { // 父进程 wait(NULL); /* 读到的正是子进程写入的数据,说明映射区被共享 */ write(STDOUT_FILENO, p, strlen(p)); } munmap(p, 4096); return 0; } ``` ### 4.6 普通 I/O 与存储映射 I/O 对比 | 维度 | 普通 I/O(read/write) | 存储映射 I/O(mmap) | | ---- | ---------------------- | -------------------- | | 数据拷贝 | 用户缓冲区↔内核缓冲区多次拷贝 | 直接操作映射区,减少拷贝 | | 函数调用链 | 层层调用,开销较大 | 直接内存访问 | | 大数据量 | 效率低 | 效率高 | | 小数据量 | 方便、划算 | 不划算(页对齐、映射开销) | | 文件大小 | 无限制 | 映射时长度固定,且不能大于文件 | | 典型场景 | 小文件、普通读写 | 视频/图像处理、Framebuffer(LCD)编程 | --- ## 5. 文件锁 ### 5.1 概念与分类 多个进程同时操作同一文件容易产生竞争,导致内容混乱。文件锁是内核提供的锁机制,用来对"文件"这一共享资源加锁保护。它与互斥锁/自旋锁/读写锁的区别是:后者主要用在多线程环境做线程同步,文件锁用在对同一文件的多个进程间。 | 类型 | 特点 | | ---- | ---- | | 建议性锁 | 本质是协议。程序访问前先加锁,成功了再访问;但不加锁也能直接访问,对不遵守协议的程序没有约束力 | | 强制性锁 | 内核会检查每次 I/O,非锁拥有者无法访问。对性能影响大,一般不建议使用 | Linux 提供 `flock()`、`fcntl()`、`lockf()` 三个加锁函数。 ### 5.2 flock() 函数 ```c #include int flock(int fd, int operation); ``` | 参数/标志 | 说明 | | --------- | ---- | | `LOCK_SH` | 共享锁,可被多个进程同时拥有 | | `LOCK_EX` | 排它锁(互斥锁),同时只能被一个进程拥有 | | `LOCK_UN` | 解锁 | | `LOCK_NB` | 非阻塞方式获取锁,无法获取立即返回,`errno=EWOULDBLOCK`(可与上面位或组合) | 返回值:成功 0;失败 -1 并设置 `errno`。同一文件不会同时具有共享锁和互斥锁。 **flock 规则**: - 同一进程对文件多次加锁不会死锁,新锁替换旧锁; - 文件关闭时自动解锁;进程终止时其建立的锁全部释放; - 一个进程不能解另一个进程持有的锁; - `fork()` 创建的子进程**不继承**父进程的锁; - 用 `dup()`/`dup2()`/`F_DUPFD` 复制出的 fd 共享同一把锁,用其中任意一个都能解锁;但若不显式解锁,只有**所有** fd 都关闭后锁才释放。 ### 5.3 完整例程:flock 加锁/解锁(建议性锁) 程序 1:持有排它锁,收到 `SIGINT` 后解锁退出。 ```c #include #include #include #include #include #include #include #include static int fd = -1; //文件描述符 /* 信号处理函数 */ static void sigint_handler(int sig) { if (SIGINT != sig) return; flock(fd, LOCK_UN); //解锁 close(fd); printf("进程 1: 文件已解锁!\n"); } int main(int argc, char *argv[]) { if (2 != argc) { fprintf(stderr, "usage: %s \n", argv[0]); exit(-1); } /* 打开文件 */ fd = open(argv[1], O_WRONLY); if (-1 == fd) { perror("open error"); exit(-1); } /* 以非阻塞方式对文件加锁(排它锁) */ if (-1 == flock(fd, LOCK_EX | LOCK_NB)) { perror("进程 1: 文件加锁失败"); exit(-1); } printf("进程 1: 文件加锁成功!\n"); /* 为 SIGINT 信号注册处理函数 */ signal(SIGINT, sigint_handler); for ( ; ; ) sleep(1); } ``` 程序 2:加锁失败也继续读写,用来验证"建议性锁"不阻止 I/O。 ```c #include #include #include #include #include #include #include #include int main(int argc, char *argv[]) { char buf[100] = "Hello World!"; int fd; int len; if (2 != argc) { fprintf(stderr, "usage: %s \n", argv[0]); exit(-1); } /* 打开文件 */ fd = open(argv[1], O_RDWR); if (-1 == fd) { perror("open error"); exit(-1); } /* 以非阻塞方式对文件加锁(排它锁) */ if (-1 == flock(fd, LOCK_EX | LOCK_NB)) perror("进程 2: 文件加锁失败"); else printf("进程 2: 文件加锁成功!\n"); /* 写文件 */ len = strlen(buf); if (0 > write(fd, buf, len)) { perror("write error"); exit(-1); } printf("进程 2: 写入到文件的字符串<%s>\n", buf); /* 将文件读写位置移动到文件头 */ if (0 > lseek(fd, 0x0, SEEK_SET)) { perror("lseek error"); exit(-1); } /* 读文件 */ memset(buf, 0x0, sizeof(buf)); //清理 buf if (0 > read(fd, buf, len)) { perror("read error"); exit(-1); } printf("进程 2: 从文件读取的字符串<%s>\n", buf); /* 解锁、退出 */ flock(fd, LOCK_UN); close(fd); exit(0); } ``` 测试:`touch infile`,先后台跑 `testApp1 infile &`(打印加锁成功并一直持锁),再跑 `testApp2 infile`,会看到进程 2 加锁失败但读写仍然成功——这就是建议性锁。`kill -2 ` 给进程 1 发 `SIGINT` 解锁后,再跑进程 2 就能加锁成功。 ### 5.4 fcntl() 函数加锁 ```c #include #include int fcntl(int fd, int cmd, ... /* struct flock *flockptr */ ); ``` 与锁相关的 `cmd`: | cmd | 作用 | | --- | ---- | | `F_GETLK` | 测试某把锁能否加上;若会被现有锁阻塞,则把现有锁信息写回 `flockptr`,否则把 `l_type` 改为 `F_UNLCK` | | `F_SETLK` | 加锁/解锁,失败立即返回,`errno` 为 `EACCES` 或 `EAGAIN` | | `F_SETLKW` | `F_SETLK` 的阻塞版本(W=wait),等待锁可用 | ```c struct flock { short l_type; /* F_RDLCK / F_WRLCK / F_UNLCK */ short l_whence; /* SEEK_SET / SEEK_CUR / SEEK_END */ off_t l_start; /* 加锁区域起始偏移 */ off_t l_len; /* 加锁区域长度 */ pid_t l_pid; /* 阻塞当前进程的锁的持有者PID,F_GETLK 时有效 */ }; ``` | `l_type` | 含义 | | -------- | ---- | | `F_RDLCK` | 共享读锁 | | `F_WRLCK` | 独占写锁 | | `F_UNLCK` | 解锁一个区域 | **fcntl 与 flock 的两大区别**:`flock` 只能锁整个文件,`fcntl` 可锁文件的某个区域(精确到字节);`flock` 只支持建议性锁,`fcntl` 支持建议性与强制性锁。 **区域规则**: - 锁区域可从文件末尾或越过末尾开始,但不能在文件起始位置之前开始; - `l_len = 0` 表示锁区域从起始位置一直延伸到文件最大偏移处,且是动态的(后续追加的数据也在锁范围内); - 锁整个文件:`l_whence = SEEK_SET`、`l_start = 0`、`l_len = 0`。 **读锁/写锁兼容性**(与线程读写锁类似):多个进程可在同一字节上持有共享读锁;同一字节上只能有一个写锁。已有读锁的字节上加不了写锁;已有写锁的字节上加不了任何锁。 | 当前锁 \ 请求锁 | 读锁 F_RDLCK | 写锁 F_WRLCK | | --------------- | :----------: | :----------: | | 无锁 | 可 | 可 | | 读锁 | 可(共享) | 不可(阻塞/失败) | | 写锁 | 不可(阻塞/失败) | 不可(阻塞/失败) | 加锁权限要求:加读锁需对文件有读权限,加写锁需有写权限。 ### 5.5 完整例程:fcntl 对文件加锁/解锁 ```c #include #include #include #include #include #include #include int main(int argc, char *argv[]) { struct flock lock = {0}; int fd = -1; char buf[] = "Hello World!"; /* 校验传参 */ if (2 != argc) { fprintf(stderr, "usage: %s \n", argv[0]); exit(-1); } /* 打开文件 */ fd = open(argv[1], O_WRONLY); if (-1 == fd) { perror("open error"); exit(-1); } /* 对整个文件加独占性写锁 */ lock.l_type = F_WRLCK; //独占性写锁 lock.l_whence = SEEK_SET; //文件头部 lock.l_start = 0; //偏移量为 0 lock.l_len = 0; //长度 0 表示到文件末尾 if (-1 == fcntl(fd, F_SETLK, &lock)) { perror("加锁失败"); exit(-1); } printf("对文件加锁成功!\n"); /* 对文件进行写操作 */ if (0 > write(fd, buf, strlen(buf))) { perror("write error"); exit(-1); } /* 解锁 */ lock.l_type = F_UNLCK; fcntl(fd, F_SETLK, &lock); close(fd); exit(0); } ``` 同一进程也可对**不同区域**分别加锁,例如对 100~200 字节加写锁、400~500 字节加读锁,只要区域不重叠: ```c struct flock wr_lock = {0}, rd_lock = {0}; ftruncate(fd, 1024); wr_lock.l_type = F_WRLCK; wr_lock.l_whence = SEEK_SET; wr_lock.l_start = 100; wr_lock.l_len = 100; fcntl(fd, F_SETLK, &wr_lock); //对 100~200 加写锁 rd_lock.l_type = F_RDLCK; rd_lock.l_whence = SEEK_SET; rd_lock.l_start = 400; rd_lock.l_len = 100; fcntl(fd, F_SETLK, &rd_lock); //对 400~500 加读锁 ``` 若两个区域重叠,新锁会替换旧锁涉及的公共部分。 ### 5.6 建议性锁与强制性锁 开启强制性锁机制:设置文件的 **Set-Group-ID(`S_ISGID`)位为 1,并清除组执行权限(`S_IXGRP`)**: ```c fstat(fd, &sbuf); fchmod(fd, (sbuf.st_mode & ~S_IXGRP) | S_ISGID); ``` 并非所有发行版都支持强制性锁,可写测试程序验证:父进程加写锁,子进程对被写的文件加读锁——若系统支持强制性锁,子进程 `read()` 会失败;若不支持,`read()` 会成功。需要注意:在强制性锁机制下,文件被加写锁会阻塞其它进程的读写,因此测试时应把文件显式设为**非阻塞**。 ### 5.7 lockf() `lockf()` 是一个库函数,内部基于 `fcntl()` 实现,是对 fcntl 锁的封装,用法不再展开。 --- ## 6. 编译与实验方法 所有示例在主机上即可验证,不需要开发板: ```bash # 编译(主机 gcc) gcc select_test.c -o select_test gcc poll_test.c -o poll_test gcc async_test.c -o async_test gcc mmap_copy.c -o mmap_copy gcc flock1.c -o testApp1 gcc flock2.c -o testApp2 gcc fcntl_lock.c -o fcntl_lock gcc epoll_demo.c -o epoll_demo # 读设备文件需要 root 权限 sudo ./select_test ``` 若要在 ALPHA/Mini I.MX6U 开发板上运行,先设置交叉编译工具环境,再用 `arm-linux-gnueabihf-gcc` 编译: ```bash source /opt/fsl-imx-x11/4.1.15-2.1.0/environment-setup-cortexa7hf-neon-poky-linux-gnueabi arm-linux-gnueabihf-gcc select_test.c -o testApp file testApp # 应是 32 位 ARM 可执行文件 scp testApp root@<开发板IP>:/home/root/ ``` **调试要点**: - 非阻塞 `read` 返回 -1 且 `errno=EAGAIN` 属正常,用 `perror` 会打印 "Resource temporarily unavailable"。 - 找鼠标设备文件:`sudo od -x /dev/input/eventX`,移动/点击鼠标有输出即为目标设备。 - `select` 每次循环都要重新 `FD_ZERO`/`FD_SET`,否则第二次就绪集合会残留。 - `poll` 判断就绪用 `revents & POLLIN`,不要用 `==`。 - `mmap` 失败要拿返回值与 `MAP_FAILED` 比较,不能与 `NULL` 比较。 --- ## 7. 跨平台对比(IMX6ULL vs STM32 vs RK3568) | 维度 | IMX6ULL(Cortex-A7 + Linux) | STM32(Cortex-M) | RK3568(Cortex-A55 + Linux) | | ---- | ---------------------------- | ----------------- | ---------------------------- | | 运行环境 | 完整 Linux,进程/文件抽象 | 裸机或 RTOS,无文件系统抽象 | 完整 Linux,性能更强 | | I/O 模型 | select/poll/epoll、信号驱动、mmap、文件锁 | 靠中断 + DMA + 状态机轮询 | 同 IMX6ULL,且 fd 数量/吞吐更高 | | "文件"概念 | 一切皆文件,设备即 `/dev` 节点 | 无(寄存器直接读写) | 一切皆文件 | | 并发读取多设备 | I/O 多路复用,单线程即可 | 多中断/DMA,或 RTOS 多任务 | 多路复用 + 多线程 | | 大数据量传输 | mmap/Framebuffer 直接映射显存 | DMA 搬运 | mmap/DMA,带宽更高 | | 多进程互斥 | flock/fcntl 文件锁 | RTOS 信号量/互斥量(无文件锁) | 同 IMX6ULL | | 适用 | 工业 HMI、网关、中低端嵌入式 Linux | 实时控制、低成本节点 | 边缘计算、多路视频、AI 网关 | 结论:Linux 应用层的高级 I/O 能力是"Cortex-A + Linux"相对于 MCU 的核心差异之一;同一套 select/poll/epoll/mmap 代码在 IMX6ULL 与 RK3568 上基本可移植,仅性能与 fd 规模不同。 --- ## 8. 深度面试题 ### Q1:select、poll、epoll 三者的区别是什么?各自适用什么场景? **答**:三者的目标都是"同时监视多个 fd",但实现和复杂度不同。 - `select` 用 `fd_set` 位图,最大 fd 受 `FD_SETSIZE`(1024)限制;每次调用都要把集合从用户态拷到内核态,返回时还会**修改集合**,所以循环中必须重新初始化;内核内部轮询检查,复杂度 O(n)。 - `poll` 用 `struct pollfd` 数组,没有 1024 上限,`events` 与 `revents` 分离,不用重建集合,但内核仍是轮询,O(n)。 - `epoll` 在内核维护红黑树 + 就绪链表,`epoll_ctl` 注册一次,`epoll_wait` 直接取就绪链表,复杂度 O(1)(就绪数),无 fd 上限,且支持 LT/ET。 适用:fd 少且要求可移植——select/poll 足够;fd 多(成百上千,如高并发服务器)——epoll 明显更优。三者本质都是"同步 I/O 多路复用",仍需自己读写数据。 ### Q2:为什么 select 在循环中每次都要重新 FD_ZERO / FD_SET? **答**:因为 `readfds`/`writefds`/`exceptfds` 是**值-结果参数**。调用前,集合表示"我关心哪些 fd";返回后,内核把它们**改写**成"哪些 fd 已就绪",未就绪的位被清除。如果下次不重新初始化,集合里残留的只是上一轮的就绪 fd,会漏掉其它 fd、语义也错误。`poll` 不存在这个问题,因为它把输入 `events` 和输出 `revents` 分开存放。 ### Q3:epoll 的 LT 和 ET 有什么区别?ET 模式编程要注意什么? **答**:**LT(水平触发,默认)**:只要 fd 上还有未处理的数据,每次 `epoll_wait` 都会通知,直到数据被读完。编程简单,不容易丢事件。**ET(边沿触发,`EPOLLET`)**:仅在 fd 状态**发生变化**(由不可读变可读、可写变可写)时通知一次;如果这次没把数据读干净,内核不会再通知,剩余数据可能长期得不到处理。 ET 编程要点:① 必须把 fd 设为**非阻塞**,否则循环读到没数据时会阻塞住;② 收到事件后要**循环 `read`/`write` 直到返回 `EAGAIN`/`EWOULDBLOCK`**,确保把内核缓冲区读空;③ 通常配合 `EPOLLONESHOT` 避免多线程重复处理同一个 fd。ET 的好处是减少无效唤醒、提升高并发性能。 ### Q4:mmap 相比 read/write 有什么优势和陷阱? **答**:优势:① 减少数据在内核缓冲区与用户缓冲区之间的拷贝,直接访问映射区;② 省去层层函数调用,大数据量下效率高;③ 可用于进程间共享(`MAP_SHARED`)、映射设备显存(Framebuffer)。陷阱:① 映射长度固定,且 `length` 不能大于文件大小;② 映射区大小是页大小整数倍,尾部多余字节写不进文件;③ `addr`/`offset` 要求页对齐;④ 写入只读映射区触发 `SIGSEGV`,文件被截断后再访问映射区触发 `SIGBUS`;⑤ `munmap` 不会主动写回磁盘,需要时用 `msync`;⑥ 小数据量下不如 read/write 划算。 ### Q5:flock 和 fcntl 文件锁有什么区别?fork/dup 时锁如何表现? **答**:区别:① 粒度——`flock` 只能锁整个文件,`fcntl` 可锁文件的任意字节区域;② 类型——`flock` 只能建议性锁,`fcntl` 支持建议性与强制性锁(强制性锁需文件设置 `S_ISGID` 且清除 `S_IXGRP`);③ `fcntl` 提供 `F_GETLK` 测试、`F_SETLKW` 阻塞等待,功能更丰富。 fork/dup 表现:① `fork()` 出的子进程**不继承**父进程的文件锁(否则父子可同时写同一文件,锁就失去意义);② `dup()`/`dup2()` 复制出的 fd 与原 fd 引用**同一把锁**,用任意一个都能解锁;③ 锁在**文件描述符关闭时自动释放**,且进程终止时全部释放;④ `flock` 下要所有引用该锁的 fd 都关闭才释放,而 `fcntl` 下**任一**相关 fd 关闭即可释放。 --- **内容来源**:《I.MX6U嵌入式Linux C应用编程指南》第十三章 高级I/O