多路io复用技术
多路io复用技术
1.应用场景
当一台服务处理大量连接请求的时候有两种高效的的方法:
- 多线程:对于每个单独的连接都使用单独的线程处理
- 多路io复用:监听多个连接,当有事件发生时立即处理
由于多线程带来的系统资源开销、上下文切换等问题现如今的常用技术是io多路复用
多路io复用在一个线程里处理,提高了该线程的利用率不会空闲等待
2.select
2.1概述
将监听的socket集合通过select传入内核,内核对select传入的socket集合进行遍历标记可读或者可写,然后将事件集合拷贝回用户态,用户态在进行遍历
2.2代码
sockfd = socket(AF_INET,SOCK_STREAM,0);
memset(&addr,0,sizeof(int));
addr.sin_family = AF_INET;
addr.sin_port = htons(2000);
addr.sin_addr.s_addr = INADDR_ANY;
bind(sockfd,(struct sockaddr*)&client,&addrlen);
listen(sockfd,5);
for(int i = 0;i < 5;++i){
memset(&client,0,sizeof(client));
addrlen = sizeof(client);
ads[i] = accept(sockfd,(struct sockaddr*)&client,&addrlen);
if(fds[i] > max)
max = fds[i];
}
whlie(1){
FD_ZERO(&rset);
for(int i = 0;i < 5;++i){
FD_SET(fds[i],&rset);
}
puts("round again");
select(max+1,&rset,NULL,NULL,NULL);
for(int i = 0;i < 5;++i){
if(FD_ISSET(fds[i],&rset)){
memset(buffer,0,MAXBUF);
puts(buffer);
}
}
}
2.3仍存在的问题
由于传入的端口集使用位图的数据结构故有大小限制
数据由用户态拷贝到内核态的开销
rset需要每次重新初始化的开销
事件处理逻辑O(n)的时间复杂度
3.poll
3.1概述
相对于select采用了结构体数组,没有了使用位图时监听端口的大小限制,其余跟select并无太大出入
3.2代码
struct pollfd{
int fd;
short events;
short revents;
}
for(int i = 0;i < 5;++i){
memeset(&client,0,sizoof(client));
addrlen = sizeof(client);
pollfds[i].fd = accept(sockfd,(struct sockaddr*) &client,&addrlen);
pollfds[i].events = POLLIN;
}
sleep(1);
while(1){
puts("round again");
poll(pollfds,5,50000);
for(int i = 0;i < 5;++i){
if(pollfds[i].revents & POLLIN){
pollfds[i].revents = 0;
memset(buffer,0,MAXBUF);
read(pollfds[i].fd,buffer,MAXBUF);
puts(buffer);
}
}
}
3.3仍存在的问题
- 数据从用户态拷贝到内核态的开销
- O(n)时间复杂度的事件处理逻辑
4.epoll
4.1概述
系统的epoll内核采用红黑树,当我们注册epoll事件的时候就会进行红黑树的插入操作O(logn),而不用拷贝整个socket集合,节省了大量的拷贝和内存分配
在内核中会使用链表存储事件,epollwait() 返回时只会返回就绪的文件描述符的数量,这样用户就不用进行O(n)的遍历
epoll并不存在共享内存的概念,epoll会进行内核态数据拷贝到用户态这是普遍存在的错误观念
epollwait()的第二参数就传入在用户态开辟空间的epoll_event数组,如果是共享内存则应该需要设计为传入一个epoll_event内存指针即可
4.2代码
struct epoll_event events[5];
int epfd = epoll_create(10);
...
...
for(int i = 0;i < 5;++i){
static struct epoll_event ev;
memset(&client,0,sizeof(client));
int addrlen = sizeof(client);
ev.data.fd = accept(sockfd,(struct sockaddr*)&client,&addrlen);
ev.events = EPOLLIN;
epoll_ctl(epfd,EPOLL_CTL_ADD,ev.data.fd,&ev);
}
while(1){
puts("round again");
nfds = epoll_wait(epfd,events,5,10000);
for(int i = 0;i < nfds;++i){
memset(buffer,0,MAXBUF);
puts(buffer);
}
}
4.3 LT模式和ET模式
- LT(边缘模式)当epoll内核监听的socket集合中有可读事件发生时,epollwait() 系统调用只会返回一次,即使用户没有调用read系统调用读取数据也只会返回一次,通常会搭配非阻塞socket使用,循环不断读取事件知道返回EAGAIN和EWOULDBLOCK状态码
- ET(水平模式)当epoll内核监听的socket集合中有可读事件发生的时候,服务端会不断地从epollwait苏醒知道数据read()完毕才可以
边缘触发常常比水平触发效率更高因为,边缘触发减少了epoll_wait()系统调用的次数(系统调用需要开销)
参考资料