• select, poll 和 epoll


    Unix I/O 模型

    select, poll 和 epoll 都是 I/O多路复用技术的一种,I/O多路复用是 Unix I/O模型中的一种。下面简述下 unix 五种 I/O模型(以调用 recvfrom 函数读取数据为例):

    (1)阻塞式(blocking I/O):应用程序调用 recvfrom 函数后,发起系统调用,一直阻塞等待数据准备好及复制到用户空间才返回。

    (2)非阻塞式(non-blocking I/O):应用程序调用 recvfrom 函数后,发起系统调用,如数据未准备后不会阻塞,而是返回 EWOULDBLOCK 错误码;不断轮询直到数据准备好后,复制数据到用户空间再返回。

    (3)多路复用式(synchronous I/O multiplexing):应用程序首先调用 select、poll 或 epoll_wait 函数,发起系统调用,阻塞监听多个socket;当某个 socket 可读时返回可读条件,随后再调用 recvfrom 函数,复制数据到用户空间再返回。

    (4)信号驱动式:应用程序首先建立信号处理程序,发起 sigcation系统调用;当数据准备好后,内核会发送 SIGIO 信号到信号处理程序,接着应用程序调用 recvfrom 函数,发起系统调用,复制数据到用户空间后返回。

    (5)异步式(asynchronous I/O):应用程序调用 recvfrom 函数后,会调用 aio_read 函数(使用 glibc 情况下),发起系统调用并返回。当数据准备好并复制到用户空间时,内核会递交信号到指定的信号处理程序进行处理。

    Linux 中的异步I/O 实现有:

    1. glibc 中的 aio,又称之为 asynchronous POSIX I/O,Linux 2.5 及以上支持,使用用户态多线程模拟异步,问题比较多,已经被抛弃;
    2. libaio,真正的异步I/O,Linux原生aio,Linux内核aio,Linux 2.6.22 及以上支持,只支持 Direct I/O 进行磁盘读写,无法利用页缓存;
    3. libeio,自称为 truly asynchronous POSIX I/O,使用多线程模拟异步,实现比glibc 较高效,且支持页缓存;
    4. io_uring,Linux 5.1 及以上支持,并有 liburing 库可使用。

    Java 中的 AIO 在 Windows 平台上使用 I/O Completion Ports(IOCP),在 Linux 上使用多线程模拟实现。

    select

    select, pselect, FD_CLR, FD_ISSET, FD_SET, FD_ZERO - synchronous I/O multiplexing

    -- Linux Programmer's Manual

    select 监听多个 fd_set,包括:

    1. readfds,可读的文件描述符集合;
    2. writefds,可写的文件描述符集合;
    3. exceptfds,异常的文件描述符集合。

    API:

    #include <sys/select.h>
    // 监控文件描述符集合,阻塞等待 I/O 就绪
    int select(int nfds, fd_set *restrict readfds,
               fd_set *restrict writefds,
               fd_set *restrict exceptfds,
               struct timeval *restrict timeout);
    // 从 set 中清除一个 fd
    void FD_CLR(int fd, fd_set *set);
    // 检查一个 fd 是否在 set 中
    int  FD_ISSET(int fd, fd_set *set);
    // 添加一个 fd 到 set 中
    void FD_SET(int fd, fd_set *set);
    // 清空 set 中的所有 fd
    void FD_ZERO(fd_set *set);
    int pselect(int nfds, fd_set *restrict readfds,
                      fd_set *restrict writefds,
                			fd_set *restrict exceptfds,
                      const struct timespec *restrict timeout,
                      const sigset_t *restrict sigmask);
    

    特点:

    1. 最多能监听 FD_SETSIZE(32位系统下为1024,64位系统下为2048)个文件描述符;
    2. 文件描述符集合(fd_set)会在用户空间和内核空间之间拷贝;
    3. 线性遍历 fd,寻找 ready 的 fd。

    poll

    poll, ppoll - wait for some event on a file descriptor

    -- Linux Programmer's Manual

    poll,Linux 2.1.23 引入,跟 select 类似都是等待 fd 集合中的一个 fd 就绪;不同的是,poll 采用一个 pollfd array 来作为 fd 集合,没有 FD_SETSIZE 限制,但受限于 RLIMIT_NOFILE(resource limit number opened files,一个进程能打开的最大文件数)。pollfd 结构如下:

    struct pollfd {
      // 文件描述符
      int   fd;
      // 请求的事件,作为输入参数,以位掩码方式
      short events;
      // 返回的事件,作为输出参数,以位掩码方式
      short revents;
    };
    

    API:

    #include <poll.h>
    int poll(struct pollfd *fds, nfds_t nfds, int timeout);
    int ppoll(struct pollfd *fds, nfds_t nfds,
                     const struct timespec *tmo_p, const sigset_t *sigmask);
    

    特点:

    1. 没有最大文件描述符限制;
    2. 仍然有内核空间和用户空间之间的 fd 集合拷贝;
    3. 仍然是线性扫描 fd 集合。

    epoll

    epoll - I/O event notification facility

    -- Linux Programmer's Manual

    epoll,Linux 2.5.45 引入,其中一个关键的概念就是 epoll instance,其位于内核中,从用户空间的视角可以认为其由两个 list 构成:

    1. interest list(也称之为 epoll set),保存注册的待监控的文件描述符集合,采用红黑树管理(red-black tree),添加和删除 fd 的复杂度都是 O(log(n));
    2. ready list,就绪的文件描述符集合,是 interest list 的子集,采用双向链表管理,并使用 mmap技术将其映射到进程的地址空间,减少不必要的拷贝。
    #include <sys/epoll.h>
    // 创建一个 epoll 实例并返回一个文件描述符指向该实例
    int epoll_create(int size);
    int epoll_create1(int flags);
    // 注册、删除和修改 interest list 中的文件描述符
    int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
    // 等待 I/O事件(读取 ready list 中的文件描述符),如无会阻塞调用线程
    int epoll_wait(int epfd, struct epoll_event *events,
                   int maxevents, int timeout);
    int epoll_pwait(int epfd, struct epoll_event *events,
                    int maxevents, int timeout,
                    const sigset_t *sigmask);
    int epoll_pwait2(int epfd, struct epoll_event *events,
                     int maxevents, const struct timespec *timeout,
                     const sigset_t *sigmask);
    

    epoll 支持两种 I/O 事件触发方式:

    1. 水平触发(level-triggered,LT),默认方式,监控的文件描述符处于可读或可写的状态时,一直报告可读或可写信号,直到变为不可读或不可写状态。
    2. 边缘触发(edge-triggered,ET),对应 flag 为 EPOLLET,监控的文件描述符状态改变时仅报告一次信号。例如:从不可读状态转变成可读状态时报告一次可读信号。处理边缘触发的事件要求使用非阻塞的文件描述符,轮询 read 或 write,直到 read 或 write 返回 EAGAIN 再退出。nginx、netty 使用边缘触发方式。
    // 水平触发
    ret = read(fd, buf, sizeof(buf));
    
    // 边缘触发
    while(true) {
        ret = read(fd, buf, sizeof(buf);
        if (ret == EAGAIN) break;
    }
    

    参考

    1. 《Unix网络编程》第一卷
    2. 五种IO模型介绍和对比
    3. Linux异步IO实现方案总结
  • 相关阅读:
    IDEA一些介绍
    win32控制台程序使用CfileDialog进行文件读取
    判断GPS、网络是否开启
    使用高德地图SDK获取定位信息
    #子线程消息被阻挡
    strlen与sizeof
    C++中路径操作
    20155235 《网络攻防》 实验一 逆向及Bof基础实践说明
    20155235 《信息安全系统设计基础》课程总结
    2017-2018-1 20155235 《信息安全系统设计基础》第十四周学习总结
  • 原文地址:https://www.cnblogs.com/lshare/p/16160992.html
Copyright © 2020-2023  润新知