IO多路复用简介
IO多路复用(IO多路转接)使得程序能同时监听多个文件描述符(多个缓冲区),提高程序性能,Linux下实现IO多路复用的系统调用主要有select,poll和epoll
I 文件中内容写入内存 O 内存中内容输出到文件
socket(fd文件描述符 对应于内核当中的一块读写缓冲区 )
这里的IO其实就是指对这块缓冲区的操作
a向写缓冲区中写数据 内核将写缓冲区的数据发到b底层 从底层中复制数据到 b的读缓冲区(内核中) b再从读缓冲区中读数据到应用程序(用户区中)
几种常见的IO模型
同步IO
1阻塞等待 阻塞IO模型(Blocking IO)
使用系统调用并一直阻塞到内核将数据准备好,之后再将数据从内核缓冲区复制到用户区中,在内核将数据准备好之前什么也干不了
下图函数调用期间 一直被阻塞 直到数据准备好 且从内核复制到用户程序时系统调用才解除阻塞 函数返回
![]() |
|---|
阻塞期间用户线程挂起 不会占用CPU资源。但只能一个线程维护一个IO,如果要用于并发需要开启大量线程用来维护网络连接(我们之前写的就是这种) 就是Linux中阻塞的read
2 非阻塞IO (Non-blocking IO)
内核在没准备好数据的时候会直接返回错误码,不会阻塞。调用程序不会挂起,而是不断轮询内核数据是否准备好。 非阻塞式IO的轮询会耗费大量cpu,通过将调节字描述符属性设置为非阻塞可使用该功能。 Linux中非阻塞read
如果服务器连接了成千上万个客户端,每一次调用recv都需要去遍历这成千上万的fd看是否有数据到达,非常低效
每次发起IO调用,在内核等待数据的过程中可以立即返回,用户线程不会阻塞,实时性较好。 但是在并发时多个线程不断轮询内核是否有数据,会占用大量CPU时间,效率不高。一般web服务器不采用此模式。
![]() |
|---|
3多路复用IO(IO Multiplexing)
类似于非阻塞,但是轮询不由用户线程执行而是由内核线程去轮询,内核监听程序听到数据准备好后,调用内核函数复制数据到用户态。
比如下图中的select 这个系统调用充当代理的角色,不断轮询select注册的所有需要IO的文件描述符,当多个套接字的任意一个的数据准备好了,用户线程再调用read将数据从内核区复制到用户区(复制的过程中 进程阻塞)。
![]() |
|---|
select 线性扫描所有监听的为念描述符,不管它们是否活跃(监听数量有上限 32位机1024 64位机 2048)
poll 同select但是数据结构不同,需要分配一个pollfd数据结构数组在内核中,没有大小限制(无监听上限) 不过需要很多复制操作
slect和poll。只会告诉你有几个fd接收到消息了,但是不会告诉你是哪几个fd还是需要后续自己去遍历
epoll 无大小限制(无监听上限)。使用一个文件描述符管理多个文件描述符,使用红黑树存储。同时用事件驱动代替轮询。epoll_ctl中注册的文件描述符在事件触发的时候会通过回调机制激活该文件描述符,epoll_wait函数返回。并且采用mmap虚拟内存映射技术减少用户态和内核态数据传输的开销。
epoll会告诉你有几个fd接收到消息了,也会告诉你是哪几个fd消息到了
系统不必维护大量线程 只需要一个线程就能处理大量的连接监听。本质上select/epoll系统调用是阻塞式的,属于同步IO,需要在读写事件就绪后,再有系统读写系统调用进行阻塞的读写。
4 信号驱动式IO
当内核数据准备就绪时通过信号来通知,当数据准备好内核会发送SIGIO信号,收到信号后再进行io操作。
![]() |
|---|
5异步IO(Asynchronous IO)
异步IO依赖信号处理函数进行通知,和前面的同步IO模型的不同点在于 前面的都是数据准备阶段的阻塞与非阻塞,但是异步IO直接通知IO操作是否完成
异步IO才是真正的非阻塞,主进程只负责做自己的事,等IO操作完成(数据成功从内核中复制到应用程序空间)时通过回调函数对数据进行处理
unix总异步io函数以aio或lio打头
![]() |
|---|
真正实现了非阻塞,吞吐量在几种模式中是最高的,
需要内核支持,异步IO在Linux2.6才引入,目前并不完善,其底层实现仍使用epoll
5种IO模型对比
![]() |
|---|
select API介绍
1 首先构造一个关于文件描述符的列表,将要监听的文件描述符添加到该列表中
2 调用select 监听该列表中的所有文件描述符。当这些描述符中的一个或多个进行IO操作时,该函数返回(没检测到列表中fd有做IO操作时阻塞)。
select对文件描述符列表的检测操作 由内核帮我们做了
3 select返回时 会告诉进程有多少(哪些)描述符进行了IO操作
#include <sys/time.h>#include <sys/types.h>#include <unistd.h>#include <sys/select.h>int select(int nfds,fd_set *readfds,fd_set *writefds,fd_set *exceptfds,struct timeval *timeout);//nfds 委托内核检测的最大文件描述符的值+1//readfds 委托内核要检测的是否可以进行读IO操作的文件描述符(文件描述符的读缓存区是否有数据---检测对方是否发送数据过来)的集合,此参数是个传入传出参数,传入想读的fd_set(想读对应fd数值位置1),传出当前可读的fd(可读的对应fd数值位置1)//sizeof(fd_set) = 128(字节)--1024位(正好就是一个进程最多可拥有的文件描述符的数量)//writefds委托内核要检测的是否可以进行写IO操作的文件描述符(文件描述符的写缓存区是否有空间可以进行写)的集合,此参数是个传入传出参数,传入想写的fd_set(想写对应fd数值位置1),传出当前可写的fd(可写的对应fd数值位置1 对应fd写缓冲区满了的位为0)//exceptfds 委托内核要检测的是否发生异常的文件描述符集合//timeout 此函数阻塞检测的最大时间 timeval {tv_sec//秒 tv_usec//微秒} 传NULL则为永久阻塞直到要检测的fd的状态发生变化。tv_sec tv_usec都为0 则select不会阻塞//返回-1 调用失败 n>0 检测的fd集合中有n个描述符发生了变化//下面函数都是对fd集合的二进制位操作void FD_CLR(int fd,fd_set *set);//将set中fd值对应的位置(第fd+1位 因为fd是从0开始的) 清0int FD_ISSET(int fd,fd_set *set);//判断set中fd值对应的位置(第fd+1位 因为fd是从0开始的) 是0还是1//是0返回0 是1返回1void FD_SET(int fd,fd_set *set);//将set中fd值对应的位置(第fd+1位 因为fd是从0开始的) 置1void FD_ZERO(fd_set *set);//将set的所有位清0
举个例子 说明select的工作流程
客户端A B C D连接到服务器分别对应文件描述符3,4,100,101
下面设置一个fd集合reads,并且调用select进行监听。虽然只有4个文件描述符要监听但是内核依旧会线性扫描0-nfds(101+1)中所有fd,如果在set中此fd+1值对应的位为1才会去检测这个文件描述符是否有数据。
![]() |
|---|
![]() |
做过reads这个fdset将内核中检测到的结果set传出 拷贝到用户态
select代码举例
//io多路复用之select#include <sys/time.h>#include <sys/types.h>#include <unistd.h>#include <arpa/inet.h>#include <string.h>#include <sys/select.h>int main(){//创建监听socketint lfd = socket(PF_INET, SOCK_STREAM, 0);struct sockaddr_in saddr;saddr.sin_port = htons(9999);saddr.sin_family = AF_INET;saddr.sin_addr.s_addr = INADDR_ANY; //服务器要绑定的地址 服务器任一网卡的ip//绑定bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));//监听listen(lfd, 8); //等待连接的最大队列长度为8//原BIO accept 接收连接 + while死循环与客户端通信//IOM 多路复用fd_set reads, reads_temp; //fd_set本质上是个数组存了1024位(128个字节) long int(8字节) long int [1024/64]//需要检测的是reads 交给内核去修改的是reads_tempFD_ZERO(&reads); //清空 所有位置为0//添加要监听的文件描述符到set中FD_SET(lfd, &reads); //监听连接的这个fd 是肯定要读的int maxfd = lfd;while (1){reads_temp = reads;//调用select 让内核帮助监听文件描述符int ret = select(maxfd + 1, &reads_temp, NULL, NULL, NULL); //只辅助监听 不检测是否能写 不检测是否有一样 永久阻塞if (ret == -1){perror("select:");exit(-1);}else if (ret == 0) //超时时间到了 并且在这段时间内要检测的fd的状态没有改变(没有新的可读信息){continue;}else if (ret > 0) //ret>0 ret中保存我们监听的fd中有几个的状态发生改变(有几个fd的读缓冲区数据发生变化--即有新的可读信息){//reads为传入传出参数 传出有新可读信息fd+1位为1if (FD_ISSET(lfd, &reads_temp)){//判断服务器监听描述符的读缓冲区是否有新的数据//如果有则 ISSET返回1 其实就是表示有新的客户端连接进来了!!!!!!!struct sockaddr_in client_addr;int len = sizeof(client_addr);int cfd = accept(lfd, (struct sockaddr *)&client_addr, &len); //接收这个新的连接 这里因为提前直到有新连接到来 调用accept不会阻塞 而是直接接收到那个连接请求//返回的新的用于和连接进来的客户端通信的cfd//我们需要将这个通信fd也加入select监听的fd集合中FD_SET(cfd, &reads);maxfd = maxfd > cfd ? maxfd : cfd;}//遍历其他集合中的文件描述符//lfd是最早申请的 所以他的值也一定比后面新建的fd值小for (int i = lfd + 1; i <= maxfd; i++){if (FD_ISSET(i, &reads_temp)){//fd = i对应的连接的客户端发来了数据char buf[1024] = {};int len = read(i, buf, sizeof(buf)); //调用read 因为缓冲区有数据 所以不会阻塞可以直接读if (len == -1){perror("read:");exit(-1);}else if (len == 0){//对方断开连接printf("client closed\n");//将这个通信fd移除close(i);FD_CLR(i, &reads);}else if (len > 0) //读到了数据{//数据处理printf("from client: %s \n", buf);write(i, buf, strlen(buf) + 1); //回写}}}}}close(lfd);return 0;}








