IO复用的三种模型
select模型:只能管理1024个客户端连接
poll模型:可以管理更多的客户端连接,但是连接越多,性能线性下降
epoll模型:只要内存足够,管理的连接数没有上限,性能不会下降
阻塞/非阻塞IO
阻塞:程序在调用结果返回之前,会等待,得到结果之后才会返回
非阻塞:不管调用是否得到结果,程序都不会等待,立即返回
缺省阻塞的函数:connect(), send(), revc(), accept()
select模型:
事件:select()等待事件的发生(读事件,写事件)
1)新客户端的连接请求accept;
2)客户端有报文到达recv,可以读;
3)客户端连接已断开;
4)可以向客户端发送报文send,可以写。
写事件:
TCP有缓存区,如果缓冲区己填填满,send函数会阻塞
如果发送端关闭了socket,缓冲区中的数据会继续发送给接收端
可以用发送端发快一点,接收端收慢一点,会出现发送端停一会,发一会儿。
如果tcp缓冲区没有满,那么socket连接是可写的(一般情况是填不满的,所以如果关心可写事件,select会立即返回)
tcp发送缓存区2.5M, 接收缓存区1M
getsockopt(sockfd, SOL_SOCKET, SO_SNDBUF, &bufsize, &optlen);
getsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &bufsize, &optlen);
在高并发和流媒休传输场景中,缓冲区有填满的可能
百度查询TCP的缓存区
超时机制
第5个参数,超时时间。
>0表示超过多久没有事件发生,则返回=0
NULL,表示不设置超时,直到有事件发生才返回
水平触发
如果事件和数据已经在缓存冲区里,程序调用select()时会报告事件,数据也不会丢失。
服务端select()之前先sleep 20秒,客户端发送完数据后退出,
服务端sleep完后,调用select(),都可以接收到客户端连接请求,发送数据,以及断开链接的事件,一个都没有丢失
如果select()己经报告了事件,但是程序没有处理它,下次调用select()的时个会重接报告。
性能测试
1000000/s个报文
存在的问题
支持的连接数太小,才1024, 调整的意义不大
每次调用 select(), 要把fdset从用户态拷贝到内核, 调用select()之后,把fdset从内核态拷贝到用户态
select()返回后,需要遍历bitmap, 效率比较低
poll模型
poll和select本质上没有区别,弃用了bitmap,采用数组表示法
每次调用poll()要把数组从用户态拷贝到内核,调用poll()之后把数组从内核态拷贝到用户态
poll()返回后,需要遍历数组,效率比较低
man 3 poll
poll(struct pollfd fds[], nfds_t nfds, int timeout);
pollfd这个结构体在man poll里
ppoll只是多了一个信号屏蔽参数
poll(fd[], maxfd, -1)
epoll模型
epoll只在有linux下才有,windows中没有,包含sys/epoll.h头文件
水平触发&边缘触发
select和poll采用水平触发
epoll有水平触发和边缘触发两种机制
水平触发:
如果接收缓冲区不为空,表示有数据可读,如果数据没有被读取完,再次调用epoll_wait的时候,读事件一直触发
如果发送缓冲区没有满,表示可以写入数据,只要缓冲区没有写满,再次调用epoll_wait的时候,写事件一直触发
读:
有客户端连接上来,会触发读事件,如果一直不处理客户端连接(注释此语句块代码),就会一直触发
如果客户端有报文过来,会触发读事件,不处理(注释此语句块代码), 一直触发
写:
如果关注客户端的写事件,可以向客户端缓冲发送报文的话,一直触发写事件,直到缓冲区写满
每个连接的客户端都有发送缓冲区,各自填满为止
边缘触发:
socket加入epoll后,如果接收缓冲区不为空,触发可读事件,如果有新的数据到达,再次触发可读事件
边缘触发(如果有客户端连接请求未处理,只触发一次)
epoll触发可读事件后,不管程序有没有处理可读的事件,epoll都不会再触发,只有当新的数据到达时,才再次触发可读事件
可写事件:
socket加入epoll后,如果发送缓冲区不为空,触发可写事件,如果发送缓冲区由满变成有空时,再次触发可写事件
网上有文章说边缘触发模式下,只要发送缓冲区有变化,就会触发写事件,是不对的。
IO复用的场景要使用非阻塞IO
1.当数据达到socket缓冲区的时候,可能会因为某些原因被内核丢弃,比如校验和错误,这时候,如果采用了阻塞的IO
唤醒的程序读到不到数据,accept和recv函数就会阻塞
2.达到缓冲区的数据有可能被别人取走,比如多个进程accept同一个socket时引发的惊群现象,只有一个连接到来,
但是所有的监听进程都会被唤醒,最终只有一个进程可以accept到这个请求,其他进程accept会被阻塞
3.ET边缘触发模式必须要使用非阻塞IO,因为程序中需要循环读和写,直到EAGAN出现,如果使用阻塞IO容易被阻塞住
读的方法:
如果接收缓冲区中有事件没有处理或有数据没有读完
水平触发:epoll_wait会重复报告,不必担心遗漏事件
边缘触发:epoll_wait不会重复报告,程序要用一个循环处理全部的事件或读取全部的数据
写的方法:
想写就直接写,出现EAGAIN就别写了
水平触发:epoll_wait会重复报告,如果不想写了,可以注销事件,否则一直触发
边缘触发:epoll_wait不会重复报告,不想写了就不写,不必注销事件
百度为什么IO多路复用要搭配非阻塞IO