文章目录
			
		  
		
        不同于传统的“一个进程处理一个客户端请求”的方式,IO复用可以让一个进程处理多个客户端的请求,更加节省资源。
前置知识
- 了解socket编程
- 了解五种IO模型
- (红黑树)
为什么需要IO复用
一个简单地服务端可能是这样的:1
2
3
4
5
6
7
8调用socket()创建套接字
bind()绑定地址和端口
listen()监听套接字
while(1){
	调用accept()连接客户端
	fork()创建进程B来处理客户端的需求/使用新的线程来执行任务
}
释放资源
当使用上面这种方式来处理客户端的请求时,如果客户端数量特别多,服务端就会创建很多进程或线程来执行任务。这种一个进程/线程对应一个客户端的方式其实是挺浪费资源的,如果让一个进程或线程就能够处理多个客户端的连接,那么就能够减少很多不必要的资源浪费。IO就可以解决这个问题。
三种IO复用方法
select
- 函数API - 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23- int select (int maxfdp, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); 
 参数:
 maxfdp是一个整数值,是指集合中所有文件描述符的范围,即所有文件描述符的最大值加1,因为文件描述符是从0开始的
 readfds检测可读的文件描述符集合
 writefds检测可写的问价描述符集合
 exceptfds检测异常条件出现的文件描述符
 timeout超时时间
 阻塞:设置NULL,会一直阻塞,直到有描述符准备好IO
 立即返回:必须设置timeval结构体,但其中的值为0
 等待一段时间:在规定时间内如果发生IO活动就马上返回,如果一直没有就等超时后再返回。
 返回值:
 返回发生所检测操作的fd总数,错误时返回SOCKET_ERROR。
 发生io活动的fd存储在相应的参数中(会删除所有传入的fd,只留下发生io活动的)
 fd_set
 为long类型数组,存储文件描述符。可以用下面几个宏来设置。
 FD_ZERO(fd_set *fdset) 将指定的文件描述符集清空
 FD_SET(fd_set *fdset) 用于在文件描述符集合中增加一个新的文件描述符。
 FD_CLR(fd_set *fdset) 用于在文件描述符集合中删除一个文件描述符。
 FD_ISSET(int fd,fd_set *fdset) 用于测试指定的文件描述符是否在该集合中。
 struct timeval{
 long tv_sec; //seconds
 long tv_usec; //microseconds,时间单位比其他的要更小
 };
- 使用示例: 
poll
- poll原理跟select基本是一样的,但是fd数量没有了限制。
- 函数API - 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11- struct pollfd { 
 int fd; /* file descriptor */
 short events; /* requested events to watch */
 short revents; /* returned events witnessed */
 };
 int poll (struct pollfd *fds, unsigned int nfds, int timeout);
 参数:
 pollfd数组传入要检测的IO活动,和返回发生的IO活动
 nfds表示文件描述符的最大值加1.
 timeout表示超时的毫秒数,负数表示无限阻塞,0表示马上返回。
- 使用示例: 
epoll
- 函数API - 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48- struct epoll_event { 
 __uint32_t events; /* Epoll events */
 epoll_data_t data; /* User data variable */
 };
 typedef union epoll_data {
 void *ptr;
 int fd;
 __uint32_t u32;
 __uint64_t u64;
 } epoll_data_t;
 //创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大
 int epoll_create(int size);
 参数:
 size以前是用来作fd数目参考,linux2.6.8之后已经不用了
 返回值:
 如果成功返回一个非负的文件描述符,失败返回-1.
 //epoll描述符的控制接口
 int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
 参数:
 epfd,epoll实例的文件描述符。
 op是请求的操作:
 EPOLL_CTL_ADD
 EPOLL_CTL_MOD
 EPOLL_CTL_DEL
 fd是op操作对应的文件描述符
 event标识要检测的io操作,event中的events按位存储发生的事件信息:
 EPOLLIN:监测读操作。
 EPOLLOUT:写操作。
 EPOLLRDHUP:流socket对端关闭连接或关闭写连接。
 EPOLLPRI:紧急数据可读
 EPOLLERR:关联的文件描述符发生错误
 EPOLLHUP:发生挂断。
 EPOLLET:设置该fd边缘触发模式
 EPOLLONESHOT:用来保证同一SOCKET只能被一个线程处理,不会跨越多个线程。
 返回值:
 成功返回0,错误返回-1并设置errno。
 //来获取发生的IO事件
 int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
 参数:
 epfd,epoll实例的文件描述符。
 events,返回的io操作
 maxevents,要监控的最大文件描述符
 timeout表示超时的毫秒数,负数表示无限阻塞,0表示马上返回。
 返回值:
 返回发生IO事件的fd个数,没有发生返回0,发生错误返回-1.
- epoll的两种工作模式 
 水平触发(LT,Level Trigger):默认的模式,如果发生的IO操作没有被处理,下次仍然会继续提醒。并且同时支持 Blocking 和 No-Blocking。
 边缘触发(ET,Edge Trigger):高速模式,发生的IO操作只提醒一次,如果没有被处理,下次就不再提醒了。效率要比 LT 模式高。只支持 No-Blocking,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。
- 使用示例:
三者区别
select
- 单进程支持最大连接数FD_SETSIZE个,一般32位机器位1024,64位为2048。可以重新编译内核修改数量,但性能无法保证。
- bitmap存储fd。
- 将消息从内核空间拷贝到用户空间。
- 每次调用后都需要对所有的fd(一个描述符对应一个客户端连接)进行遍历,随着fd数量增加,性能下降。
- 各个平台都有实现,跨平台效果好。
- 超时精度为纳秒,连接数量少时,实时性较好,适用核反应堆、金融平台等场景。
poll
- 无数量限制,与select本质上没有区别,用链表存储fd。但连接数较多时无法保证性能。
- 链表存储fd
- 将消息从内核空间拷贝到用户空间。(同select)
- 每次调用后都需要对所有的fd(一个描述符对应一个客户端连接)进行遍历,随着fd数量增加,性能下降。(同select)
- 只有新一点的系统支持。
- 超时精度为毫秒。
epoll
- 连接数很大,1G内存机器可以10万左右连接,2G内存可以20万连接。
- 传入fd用红黑树存储,发生io操作的fd用双向链表存储。
- 使用mmap来与内核空间共享内存。
- 不会由于连接数量增加导致性能过分下降,只有首次调用epoll_ctl拷贝fd,每次调用epoll_wait不拷贝。(由于采用回调函数实现。只有活跃的客户端才会调用回调函数,所以epoll会因为活跃的连接数过多而性能下降)
- Linux平台专用。
- 超时精度为毫秒。
三者适用场景
简单地说来,select和poll适合连接数量小、活跃数量多、实时性要求高的情况。而epoll适合客户端的连接数量很大,活跃数量小的情况。