常见IO模型:
-
同步阻塞IO(Blocking IO):使用recv一直等数据直到拷贝到用户空间,并同步返回状态,这段时间内进程始终阻塞. 进程阻塞的本质是将进程的state的改变
state=TASK_INTERRUPTIBLE/TASK_RUNABLE
,剩下的交给操作系统进程调度算法. -
同步非阻塞IO(Non-blocking IO): socket设置为non-block,recv不管有没有获取到数据都返回,如果没有数据则等一段时间后在调用recv,如此循环. 只在检查无数据时是非阻塞的,有数据时仍然同步等待数据从内核到用户空间。
-
IO多路复用: 用Reactor模式较多,具体参考第2部分介绍
- select
//创建fds数组 sockfd = socket(...); bind(sockfd, addr); listen(sockfd,port); for...{ //通过listen_socket,创建connect_socket. listen_socket负责继续监听,connect_socket负责处理数据读写 fds[i] = accept(sockfd); } //创建bitmap/rset,并关联fds,最多1024bit while(1) { FD_ZERO(&bitmap); //bitmap由用户创建,并拷贝到内核更改,无法复用 for ...{ FD_SET(fds[i], &bitmap) } //此处多路复用器阻塞,等待有事件唤醒 select(max+1,&rset); //traversal fds,时间复杂度O(n) }
限制:
1. 一个进程最多能标记1024个fd
2. 用户态和内核态之间切换拷贝bitmap
3. bitmap无法重用
4. 需要完整遍历bitmap每一位 -
poll:
相较select改进:
- 通过pollfd数组,解除了bitmap最多可标记1024个socket的限制
- 可以重用pollfd,不需要每次创建bitmap
stuct pollfd{ int fd; //POLLIN 有数据可读, POLLOUT写事件允许, POLLERR 错误,POLLRDHUP流socket对端关闭连接;... short events; short revents; } ... for...{ pollfd_arr[i].fd = accept(listen_socket_fd); pollfd_arr[i].events = POLLIN; } while(1){ //此处多路复用器阻塞, poll(pollfd_arr, count,...); for i in count{ if (pollfd_arr[i].revents & POLLIN){ read(pollfd_arr[i].fd, buffer); pollfds[i].revents = 0; //重置状态 复用 } } }
-
epoll: bsd上是kqueue.
相比poll的改进:
- 不仅告诉调用方有数据,还能提供具体哪个sock有数据,避免了遍历查找
- events由用户态空间和内核空间共享避免了切换, 并且返回产生事件的句柄数量,以及内核排序好的句柄数组,调用方只需遍历有事件发生的fd数组.
struct epoll_event events[1024] int epfd = epoll_create ... for...{ static struct epoll_event ev; ev.data.fd = accept(listen_socket_fd,....); ev.events = EPOLLIN; epoll_ctl(epfd, ev.data.fd,&ev ...); whil(1){ //阻塞, 返回产生事件的个数,并且 events是内核已排序好的 int count = epoll_wait(epfd, events, ....); for(i in count){ read(events[i].data.fd, buffer..); } } }
-
异步非阻塞IO: 由操作系统内核负责读取socket数据,并写入用户指定的缓冲区,用户线程不会被阻塞,目前操作系统支持不太完善。
扩展
1. IO多路复用中常用的Reactor反应堆模式
主要角色:
-
handle : linux中称为文件描述符,windows中称为句柄,是对资源在os上的抽象,如socet\打开的文件\timer等,
-
synchronous event demultiplexer 同步多路事件分离器,本质是系统调用,如select poll epoll等,可以等待多个handle,在等待过程中所在线程牌挂起状态不消耗CPU时间。当某个句柄有事件产生时才会返回
-
Event Handler/Concrete Event Handler 定义一些钩子函数或称为回调方法,handle有事件产生就会调用钩子方法,在socket中一般有decode-process-encode这些过程,Concrete处理具体的业务逻辑
-
Initiation Dispatcher 初始事件分发器,用于管理event handler, 并调用多路分离器等待其返回时,将事件分发到event handle事件处理器进行处理
按线程划分:
-
单线程Reactor模型: 与NIO流程相似,一个线程处理所有的IO和process()过程,要求IO和CPU速度匹配,无处理较慢的请求,否则引起后续的请求积压
-
多线程Reactor模型: 一个或多个IO线程,多个工作处理线程,由于reactor既处理IO请求也响应连接请求容易出现瓶颈。
-
主从Reactor模型: 一个main reactor负责监听selector连接池,多个sub reactor 处理accept连接请求,性能较好。
线程池缺点是大并发下耗满线程,服务阻塞。对于大文件传输占用长时间数据读写操作的,可以考虑使用每个连接建立新的线程或异步非阻塞IO模式。并发数少用哪个都没有区别。异步非阻塞IO由于操作系统支持限制用的较少,大多高性能并发服务采用IO多路复用+多线程(线程池)的架构。
2. TCP粘包分包问题
每个UDP消息头部都有来源和端口号的消息头,一次write就是一条消息,接收端也以消息为单位提取,不完整的包会被丢弃,因此不会出现粘包。而TCP是面向流的协议接收端无法得知接收的数据是否已传完,因此要处理粘包、半包问题,解决方案有:
- 基于固定长度消息
- 基于消息边界符
- 消息头指定消息长度。