01-高级IO.md 46 KB


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/OI/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_NONBLOCKfcntl(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 卡住) 轮询可以做到
适用场景 单设备、愿意等待 多设备非阻塞 + 配合多路复用
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 手动改成非阻塞。

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

#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()

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() 函数

#include <sys/select.h>

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 出错,设置 errnoEBADF fd 非法、EINTR 被信号打断等)
0 超时,没有任何 fd 就绪,三个集合被清空
> 0 处于就绪态的 fd 总数(同一 fd 在多个集合就绪会被重复统计)

关键特性(值-结果参数)select() 返回时会修改传入的集合,使其只保留就绪的 fd。因此若在循环中重复调用,每次调用前都必须重新 FD_ZERO 并重新 FD_SET

2.3 poll() 函数

#include <poll.h>

int poll(struct pollfd *fds, nfds_t nfds, int timeout);
参数 含义
fds struct pollfd 数组,每个元素指定一个 fd 及关心的事件
nfds 数组元素个数(nfds_t 实际为无符号整型)
timeout -1:一直阻塞;0:不阻塞只检查一次;> 0:最多阻塞 timeout 毫秒
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。实际编程中最常用的是 POLLINPOLLOUT;GPIO 中断场景用 POLLPRI

返回值select() 一致:-1 出错;0 超时;> 0 表示 revents 非 0 的元素个数。

2.4 完整例程:select 实现同时读取键盘和鼠标

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/select.h>

#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,逻辑完全一致:

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <poll.h>

#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
是否修改入参 是(值-结果),需重复初始化 否(reventsevents 分离)
触发模式 只有水平触发 只有水平触发 支持 LT 与 ET
大量 fd 场景
少量 fd 场景 够用、可移植性好 够用 略重

epoll 三个核心函数:

#include <sys/epoll.h>

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_ctlop 取值:EPOLL_CTL_ADD(注册)、EPOLL_CTL_MOD(修改)、EPOLL_CTL_DEL(注销)。struct epoll_eventevents 常用 EPOLLIN / EPOLLOUT,并可或上 EPOLLET(边沿触发)、EPOLLONESHOT(只通知一次)。epoll_wait 返回就绪事件个数,timeout 单位毫秒(-1 永久阻塞,0 立即返回)。

LT(水平触发,默认):只要 fd 上还有数据可读,每次 epoll_wait 都会通知。编程简单,不易丢事件。 ET(边沿触发):仅在状态发生变化时通知一次。若一次没把数据读干净,后续不再通知,事件就丢了。ET 必须把 fd 设为非阻塞,并用循环 read 到返回 EAGAIN 为止。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/epoll.h>

#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 <file>\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(<aio.h>)把"发起 I/O"与"等结果"真正分离,可在数据就绪或完成时通过信号/回调通知:

#include <aio.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>

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。

    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 方式读取鼠标

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <signal.h>

#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 到底可读、可写还是异常

优化方法:

  • 用实时信号替换 SIGIOfcntl(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

#define _GNU_SOURCE             //使用 F_SETSIG 需要定义该宏
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <signal.h>

#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 的共享内存很相似。

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()

#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);
参数 含义
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 匿名映射,忽略 fdoffset,不涉及文件,无法与其它进程共享
MAP_LOCKED 对映射区上锁

返回值:成功返回映射区起始地址;失败返回 (void *)-1,即 MAP_FAILED,并设置 errno

页对齐addroffset 通常要求是系统页大小整数倍,可用 sysconf(_SC_PAGE_SIZE)sysconf(_SC_PAGESIZE) 获取页大小。length 无对齐要求:映射区实际大小是页大小的整数倍,尾部多余字节被置 0,可访问但不反映到文件;访问映射区末尾之后的内存会触发 SIGBUS

与映射区相关的两个信号

信号 触发条件
SIGSEGV 映射区被指定为只读,进程却试图写入
SIGBUS 映射区某部分在访问时已不存在(如另一进程把文件截断)

解除映射munmap(addr, length)addr 必须是页大小整数倍。进程终止时自动解除映射,但close() 关闭文件不会解除映射

4.3 mprotect() 与 msync()

int mprotect(void *addr, size_t len, int prot);      // 修改已有映射区的保护要求
int msync(void *addr, size_t length, int flags);     // 把映射区数据刷回磁盘

msyncflags

标志 说明
MS_ASYNC 异步同步,调用后不等数据写完磁盘即返回
MS_SYNC 同步同步,等数据全部写入磁盘后才返回
MS_INVALIDATE 可选,使同一文件的其它映射失效

注意:munmap() 不会把映射区内容写到磁盘;MAP_SHARED 下由内核在某个时刻自动更新文件;MAP_PRIVATE 下解除映射后修改全部丢弃。

4.4 完整例程:用存储映射 I/O 复制文件

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <string.h>

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 <srcfile> <dstfile>\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应用编程指南》内容,为扩展知识。原书在 mmapflags 中介绍了 MAP_ANONYMOUS 标志("不涉及文件,映射区域无法和其它进程共享"),本节在此基础上补充其典型用法。这里说的"无法共享"指无亲缘关系的进程无法访问匿名映射(没有文件/路径可供其它进程引用);但 MAP_SHARED | MAP_ANONYMOUS 建立的内存会随 fork() 被子进程继承,因此父子进程之间可以共享

不涉及文件时使用 MAP_ANONYMOUS(与 MAP_SHARED 搭配,用于 fork 出的父子进程共享内存):

#include <sys/types.h>
#include <sys/mman.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>

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() 函数

#include <sys/file.h>

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 后解锁退出。

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/file.h>
#include <signal.h>

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 <file>\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。

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/file.h>
#include <string.h>

int main(int argc, char *argv[])
{
    char buf[100] = "Hello World!";
    int fd;
    int len;

    if (2 != argc) {
        fprintf(stderr, "usage: %s <file>\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 <pid> 给进程 1 发 SIGINT 解锁后,再跑进程 2 就能加锁成功。

5.4 fcntl() 函数加锁

#include <unistd.h>
#include <fcntl.h>

int fcntl(int fd, int cmd, ... /* struct flock *flockptr */ );

与锁相关的 cmd

cmd 作用
F_GETLK 测试某把锁能否加上;若会被现有锁阻塞,则把现有锁信息写回 flockptr,否则把 l_type 改为 F_UNLCK
F_SETLK 加锁/解锁,失败立即返回,errnoEACCESEAGAIN
F_SETLKW F_SETLK 的阻塞版本(W=wait),等待锁可用
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_SETl_start = 0l_len = 0

读锁/写锁兼容性(与线程读写锁类似):多个进程可在同一字节上持有共享读锁;同一字节上只能有一个写锁。已有读锁的字节上加不了写锁;已有写锁的字节上加不了任何锁。

当前锁 \ 请求锁 读锁 F_RDLCK 写锁 F_WRLCK
无锁
读锁 可(共享) 不可(阻塞/失败)
写锁 不可(阻塞/失败) 不可(阻塞/失败)

加锁权限要求:加读锁需对文件有读权限,加写锁需有写权限。

5.5 完整例程:fcntl 对文件加锁/解锁

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

int main(int argc, char *argv[])
{
    struct flock lock = {0};
    int fd = -1;
    char buf[] = "Hello World!";

    /* 校验传参 */
    if (2 != argc) {
        fprintf(stderr, "usage: %s <file>\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 字节加读锁,只要区域不重叠:

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

fstat(fd, &sbuf);
fchmod(fd, (sbuf.st_mode & ~S_IXGRP) | S_ISGID);

并非所有发行版都支持强制性锁,可写测试程序验证:父进程加写锁,子进程对被写的文件加读锁——若系统支持强制性锁,子进程 read() 会失败;若不支持,read() 会成功。需要注意:在强制性锁机制下,文件被加写锁会阻塞其它进程的读写,因此测试时应把文件显式设为非阻塞

5.7 lockf()

lockf() 是一个库函数,内部基于 fcntl() 实现,是对 fcntl 锁的封装,用法不再展开。


6. 编译与实验方法

所有示例在主机上即可验证,不需要开发板:

# 编译(主机 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 编译:

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",但实现和复杂度不同。

  • selectfd_set 位图,最大 fd 受 FD_SETSIZE(1024)限制;每次调用都要把集合从用户态拷到内核态,返回时还会修改集合,所以循环中必须重新初始化;内核内部轮询检查,复杂度 O(n)。
  • pollstruct pollfd 数组,没有 1024 上限,eventsrevents 分离,不用重建集合,但内核仍是轮询,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