[NIO]linux的I/O多路复用

前言 上篇提到i/o多路复用,是通过单进程监听多个文件描述的状态,达到减少线程阻塞的目的。 内核(kernel)利用文件描述符(file descriptor)来访问文件。 文件描述符是非负整数。 打开现存文件或新建文件时(包括socket被打开),内核会返回一个文件描述符。 读写文件也需要使用文件描述符来指定待读写的文件。在linux环境下,进入/proc目录可以看到许多代表文件描述符的文件夹。 linux i/o多路复用的系统调用接口有三种,分别是 select,poll,epoll。 接口 作为一个学java的,了解一下java底层调用的函数,还是挺有助于理解的。 i/o多路复用原理 linux(2.6+)内核的事件wakeup callback机制,是linux i/o多路复用的原理。内核管理一个process的睡眠队列,当socket事件发生的时候,唤醒队列的process,调用callback函数完成通知。总体上会涉及两大逻辑:(1)睡眠等待逻辑;(2)唤醒逻辑。 1.睡眠等待逻辑:涉及select、poll、epoll_wait的阻塞等待逻辑 select、poll、epoll_wait陷入内核,判断监控的socket是否有关心的事件发生了,如果没,则为当前process构建一个wait_entry节点,然后插入到监控socket的sleep_list 进入循环的schedule直到关心的事件发生了 关心的事件发生后,将当前process的wait_entry节点从socket的sleep_list中删除。 2.唤醒逻辑。 socket的事件发生了,然后socket顺序遍历其睡眠队列,依次调用每个wait_entry节点的callback函数 直到完成队列的遍历或遇到某个wait_entry节点是排他的才停止。 一般情况下callback包含两个逻辑:1.wait_entry自定义的私有逻辑;2.唤醒的公共逻辑,主要用于将该wait_entry的process放入CPU的就绪队列,让CPU随后可以调度其执行。 select #include <sys/select.h> #include <sys/time.h> int select(int max_fd, fd_set *readset, fd_set *writeset, fd_set *exceptset, struct timeval *timeout) FD_ZERO(int fd, fd_set* fds) //清空集合 FD_SET(int fd, fd_set* fds) //将给定的描述符加入集合 FD_ISSET(int fd, fd_set* fds) //将给定的描述符从文件中删除 FD_CLR(int fd, fd_set* fds) //判断指定描述符是否在集合中 select 方法的第一个参数max_fd指待测试的fd(fd即文件描述符,一个socket会有一个文件描述符)个数,它的值是待测试的最大文件描述符加1,文件描述符从0开始到max_fd-1都将被测试。中间三个参数readset、writeset和exceptset指定要让内核测试读、写和异常条件的fd集合,如果不需要测试的可以设置为NULL。 select被调用的时候,被监控的readset(假设对socket的读事件感兴趣)会从用户空间复制到内核空间,然后遍历监听的socket,如果在超时或者有一个或多个socket产生了读事件,那么select唤醒线程,注意这里只是唤醒,并没有返回就绪的fd,接下来线程要再次遍历readset,收集可读事件。 select的问题是: 监听的socket数量有限,为了减少fd拷贝的性能损耗,限定了1024个文件描述符 线程被唤醒的时候,需要再次遍历fd列表。 poll #include <poll.h> int poll(struct pollfd fds[], nfds_t nfds, int timeout); typedef struct pollfd { int fd; // 需要被检测或选择的文件描述符 short events; // 对文件描述符fd上感兴趣的事件 short revents; // 文件描述符fd上当前实际发生的事件*/ } pollfd_t; poll换了个数据结构,解决了select其中一个问题:监听的数量有限。但实际上并有解决拷贝的性能损耗和需要再次遍历fd列表获取就绪事件。...

January 31, 2019 · 土川

[NIO]五个I/O模型

前言 不先了解一下Linux的IO模型,看java的nio真是一脸懵逼。。 linux的io模型 Blocking (I/O阻塞IO模型) 刚学java的时候想必学的都是阻塞IO传输,底层调用就是上面的图锁展示的过程,进程调用recvfrom,进入阻塞状态,然后系统将数据从网卡/硬盘读取到内核,由从内核复制到用户态,最终返回给进程,进程继续运行。 这是比较耗时和浪费CPU的做法,需要阻塞数据到达,数据复制 Nonblocking I/O(非阻塞IO模型) 底层轮询调用recvfrom,系统会立刻返回读取结果,如果读取不到数据,则开启下一次调用,直到数据返回。 这种模式不用阻塞数据到达,需要阻塞数据复制。但是处于轮询状态的进程又是另一种意义上的阻塞,所以其实效率没有提高多少。 I/O Multiplexing(多路复用) Unix/Linux 环境下的 I/O 复用模型包含三组系统调用,分别是 select、poll 和 epoll,在历史上依次出现。 select 有三个文件描述符集(readfds),分别是可读文件描述符集(writefds)、可写文件描述符集和异常文件描述符集(exceptfds)。进程将文件描述符(socket也有文件描述符表示)注册到感兴趣的文件描述符集中, 在这种模式下,select先被调用,进程处于阻塞状态,直至一个或多个事件返回。然后使用recvfrom读取数据。 这种模式需要阻塞数据到达,数据复制。但是BIO由于一次只等待一个数据到达,所以性能上多路复用更优。 Signal-Driven I/O(信号驱动I/O) 进程告诉内核,某个socket 的某个事件发生时,向进程发送信号。接收到信号后,对应的函数回去处理事件。 这种模式不用阻塞数据到达,需要阻塞数据复制 想想,如果数据复制完再通知进程,不就不用阻塞了。于是有下面的异步IO的模型出现。 Asynchronous I/O (异步I/O) 这就是信号驱动I/O的升级版,完全异步,进程无阻塞。对于大部分平台来说,底层利用的还是非异步模型结合回调函数来实现。 遗憾的是,linux的网络IO中是不存在异步IO的,linux的网络IO处理的第二阶段总是阻塞等待数据copy完成的。真正意义上的网络异步IO是Windows下的IOCP(IO完成端口)模型。 对比 总结 Unix网络编程」中说道,按照POSIX标准中的术语,同步指的是I/O动作会导致用户进程阻塞,异步则刚好相反。按照这种分类,上边5种I/O模型中,只有AIO一种是异步的,其他都是同步的。 但是这些只是相对的,程序往往是多线程运行,拿Java来说,主线程调用select操作是阻塞的,但是数据复制这个阻塞过程放到子线程中,对主线程来说没有影响。这也是为什么java的NIO称为同步非阻塞IO。 参考 IO复用

January 27, 2019 · 土川