聊聊select, poll 和 epoll_wait
聊聊select, poll 和 epoll
假设项目上需要实现一个TCP的客户端和服务器从而进行跨机器的数据收发,我们很可能翻阅一些资料,然后写出如下的代码。
服务端
void func(int sockfd)
{
char buff[MAX];
int n;
// infinite loop for chat
for (;;) {
bzero(buff, MAX); // read the message from client and copy it in buffer
read(sockfd, buff, sizeof(buff));
// print buffer which contains the client contents
printf("From client: %s\t To client : ", buff);
bzero(buff, MAX);
n = ;
// copy server message in the buffer
while ((buff[n++] = getchar()) != '\n')
; // and send that buffer to client
write(sockfd, buff, sizeof(buff)); // if msg contains "Exit" then server exit and chat ended.
if (strncmp("exit", buff, ) == ) {
printf("Server Exit...\n");
break;
}
}
} // Driver function
int main()
{
int sockfd, connfd, len;
struct sockaddr_in servaddr, cli; // socket create and verification
sockfd = socket(AF_INET, SOCK_STREAM, );
if (sockfd == -) {
printf("socket creation failed...\n");
exit();
}
else
printf("Socket successfully created..\n");
bzero(&servaddr, sizeof(servaddr)); // assign IP, PORT
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(PORT); // Binding newly created socket to given IP and verification
if ((bind(sockfd, (SA*)&servaddr, sizeof(servaddr))) != ) {
printf("socket bind failed...\n");
exit();
}
else
printf("Socket successfully binded..\n"); // Now server is ready to listen and verification
if ((listen(sockfd, )) != ) {
printf("Listen failed...\n");
exit();
}
else
printf("Server listening..\n");
len = sizeof(cli); // Accept the data packet from client and verification
connfd = accept(sockfd, (SA*)&cli, &len);
if (connfd < ) {
printf("server acccept failed...\n");
exit();
}
else
printf("server acccept the client...\n"); // Function for chatting between client and server
func(connfd); // After chatting close the socket
close(sockfd);
}
客户端
void func(int sockfd)
{
char buff[MAX];
int n;
for (;;) {
bzero(buff, sizeof(buff));
printf("Enter the string : ");
n = ;
while ((buff[n++] = getchar()) != '\n')
;
write(sockfd, buff, sizeof(buff));
bzero(buff, sizeof(buff));
read(sockfd, buff, sizeof(buff));
printf("From Server : %s", buff);
if ((strncmp(buff, "exit", )) == ) {
printf("Client Exit...\n");
break;
}
}
} int main()
{
int sockfd, connfd;
struct sockaddr_in servaddr, cli; // socket create and varification
sockfd = socket(AF_INET, SOCK_STREAM, );
if (sockfd == -) {
printf("socket creation failed...\n");
exit();
}
else
printf("Socket successfully created..\n");
bzero(&servaddr, sizeof(servaddr)); // assign IP, PORT
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
servaddr.sin_port = htons(PORT); // connect the client socket to server socket
if (connect(sockfd, (SA*)&servaddr, sizeof(servaddr)) != ) {
printf("connection with the server failed...\n");
exit();
}
else
printf("connected to the server..\n"); // function for chat
func(sockfd); // close the socket
close(sockfd);
}
那么问题来了,如果有一个新的需求进来,现在需要你这个服务器程序同时支持多个客户端连接,你怎么办么呢?对的,这就引出了本文要聊的IO多路复用技术。
select,poll,epoll
select,poll,epoll都是IO多路复用的机制。I/O多路复用就是通过一种机制,一个进程可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。但select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。
select
int select (int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
select 函数监视的文件描述符分3类,分别是writefds、readfds、和exceptfds。调用后select函数会阻塞,直到有描述符就绪(有数据可读、可写、或者有exception),或者超时(timeout指定等待时间,如果立即返回设为null即可),函数返回。当select函数返回后,可以 通过遍历fdset,来找到就绪的描述符。select目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点。select的一个缺点在于单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,可以通过修改宏定义甚至重新编译内核的方式提升这一限制,但是这样也会造成效率的降低。
服务端示例
while (1) {
/*每次调用select前都要重新设置文件描述符和时间,因为事件发生后,文件描述符和时间都被内核修改啦*/
FD_ZERO(readfds);
/*添加监听套接字*/
FD_SET(srvfd, readfds);
s_srv_ctx->maxfd = srvfd; tv.tv_sec = 30;
tv.tv_usec = 0;
/*添加客户端套接字*/
for (i = 0; i < s_srv_ctx->cli_cnt; i++) {
clifd = s_srv_ctx->clifds[i];
/*去除无效的客户端句柄*/
if (clifd != -1) {
FD_SET(clifd, readfds);
}
s_srv_ctx->maxfd = (clifd > s_srv_ctx->maxfd ? clifd : s_srv_ctx->maxfd);
} /*开始轮询接收处理服务端和客户端套接字*/
retval = select(s_srv_ctx->maxfd + 1, readfds, NULL, NULL, &tv);
if (retval == -1) {
fprintf(stderr, "select error:%s.\n", strerror(errno));
return;
}
if (retval == 0) {
fprintf(stdout, "select is timeout.\n");
continue;
}
if (FD_ISSET(srvfd, readfds)) {
/*监听客户端请求*/
accept_client_proc(srvfd);
} else {
/*接受处理客户端消息*/
recv_client_msg(readfds);
}
}
}
poll
int poll (struct pollfd *fds, unsigned int nfds, int timeout);
不同于select使用三个位图来表示三个fdset的方式,poll使用一个pollfd的指针实现。
struct pollfd {
int fd; /* file descriptor */
short events; /* requested events to watch */
short revents; /* returned events witnessed */
};
pollfd结构包含了要监视的event和发生的event,不再使用select“参数-值”传递的方式。同时,pollfd并没有最大数量限制(但是数量过大后性能也是会下降。和select函数一样,poll返回后,需要轮询pollfd来获取就绪的描述符。从上面看,select和poll都需要在返回后,通过遍历文件描述符来获取已经就绪的socket。事实上,同时连接的大量客户端在一时刻可能只有很少的处于就绪状态,因此随着监视的描述符数量的增长,其效率也会线性下降。
服务端示例
for ( ; ; )
{
//获取可用描述符的个数
nready = poll(clientfds,maxi+1,INFTIM);
if (nready == -1)
{
perror("poll error:");
exit(1);
}
//测试监听描述符是否准备好
if (clientfds[0].revents & POLLIN)
{
cliaddrlen = sizeof(cliaddr);
//接受新的连接
if ((connfd = accept(listenfd,(struct sockaddr*)&cliaddr,&cliaddrlen)) == -1)
{
if (errno == EINTR)
continue;
else
{
perror("accept error:");
exit(1);
}
}
fprintf(stdout,"accept a new client: %s:%d\n", inet_ntoa(cliaddr.sin_addr),cliaddr.sin_port);
//将新的连接描述符添加到数组中
for (i = 1;i < OPEN_MAX;i++)
{
if (clientfds[i].fd < 0)
{
clientfds[i].fd = connfd;
break;
}
}
if (i == OPEN_MAX)
{
fprintf(stderr,"too many clients.\n");
exit(1);
}
//将新的描述符添加到读描述符集合中
clientfds[i].events = POLLIN;
//记录客户连接套接字的个数
maxi = (i > maxi ? i : maxi);
if (--nready <= 0)
continue;
}
//处理客户连接
handle_connection(clientfds,maxi);
}
epoll
epoll是在2.6内核中提出的,是之前的select和poll的增强版本。相对于select和poll来说,epoll更加灵活,没有描述符限制。epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。
epoll操作过程
epoll操作过程需要三个接口,分别如下:
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
1. int epoll_create(int size);
创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大,这个参数不同于select()中的第一个参数,给出最大监听的fd+1的值,参数size并不是限制了epoll所能监听的描述符最大个数,只是对内核初始分配内部数据结构的一个建议。
当创建好epoll句柄后,它就会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。
2. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
函数是对指定描述符fd执行op操作。
- epfd:是epoll_create()的返回值。
- op:表示op操作,用三个宏来表示:添加EPOLL_CTL_ADD,删除EPOLL_CTL_DEL,修改EPOLL_CTL_MOD。分别添加、删除和修改对fd的监听事件。
- fd:是需要监听的fd(文件描述符)
- epoll_event:是告诉内核需要监听什么事,struct epoll_event结构如下:
struct epoll_event {
__uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
}; //events可以是以下几个宏的集合:
EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
3. int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
等待epfd上的io事件,最多返回maxevents个事件。
参数events用来从内核得到事件的集合,maxevents告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size,参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。该函数返回需要处理的事件数目,如返回0表示已超时。
工作模式
epoll对文件描述符的操作有两种模式:LT(level trigger)和ET(edge trigger)。LT模式是默认模式,LT模式与ET模式的区别如下:
LT模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用epoll_wait时,会再次响应应用程序并通知此事件。
ET模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次响应应用程序并通知此事件。
LT模式:LT(level triggered)是缺省的工作方式,并且同时支持block和no-block socket,在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的。
ET模式:ET(edge-triggered)是高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了(比如,你在发送,接收或者接收请求,或者发送接收的数据少于一定量时导致了一个EWOULDBLOCK 错误)。但是请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once),ET模式在很大程度上减少了epoll事件被重复触发的次数,因此效率要比LT模式高。epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描 述符的任务饿死。
服务端示例
for ( ; ; )
{
//获取已经准备好的描述符事件
ret = epoll_wait(epollfd,events,EPOLLEVENTS,-1);
handle_events(epollfd,events,ret,listenfd,buf);
}
select的问题
根据fd_size的定义,它的大小为32个整数大小(32位机器为32*32,所有共有1024bits可以记录fd),每个fd一个bit,所以最大只能同时处理1024个fd每次要判断(有哪些event发生)这件事的成本很高,因为select(polling也是)采取主动轮询机制,
1.每一次调用select()都需要先从用户空间把FD_SET复制到内核空间。
为什么select不能像epoll一样,只做一次复制呢? 因为每一次调用select()前,FD_SET都可能被内核修改,而epoll提供了可共享的存储结构,所以不需要每次的用户态和内核态的数据复制。
2.kernel还要遍历每个fd
假设现实中,有1百万个客户端同时与一个服务器保持着tcp连接,而每一个时刻,通常只有几百上千个tcp连接是活跃的,这时候我们仍然使用select/poll机制,kernel必须在搜寻完100万个fd之后,才能找到其中状态是active的,这样资源消耗大而且效率低下。
poll与select的差别
描述fd集合的方式不同,poll使用 pollfd 结构而不是select结构fd_set结构,所以poll是链式的,没有最大连接数的限制,poll有一个特点是水平触发,也就是通知程序fd就绪后,这次没有被处理,那么下次poll的时候会再次通知同个fd已经就绪。
epoll
epoll没有fd数量限制, epoll没有这个限制,我们知道每个epoll监听一个fd,所以最大数量与能打开的fd数量有关,一个g的内存的机器上,能打开10万个左右, epoll不需要每次都从user space 将fd set复制到内核kernel, epoll在用epoll_ctl函数进行事件注册的时候,已经将fd复制到内核中,所以不需要每次都重新复制一次。
select 和 poll 都是主动轮询,需要遍历每个fd,epoll是被动触发方式,给fd注册了相应事件的时候,我们为每一个fd指定了一个回调函数,当数据准备好之后,就会把就绪的fd加入一个就绪的队列中,epoll_wait的工作方式实际上就是在这个就绪队列中查看有没有就绪的fd,如果有,就唤醒就绪队列上的等待者,然后调用回调函数。虽然select, poll, epoll都需要查看是否有fd就绪,但是epoll之所以是被动触发,就在于它只要去查找就绪队列中有没有fd,就绪的fd是主动加到队列中,epoll不需要逐个轮询确认。
一句话总结重点,select和poll只能通知有fd已经就绪了,但不能知道究竟是哪个fd就绪,所以select和poll就要去主动遍历一遍找到就绪的fd。而epoll则是不但可以知道有fd可以就绪,而且还具体可以知道就绪fd的编号,所以直接找到就可以,不用循环遍历一遍。
小结
select, poll是为了解決同时大量IO的场景,但是随着连接数增加,性能变差,epoll是select和poll的改进方案,在linux上可以取代select和poll,可以在一定程度上改善大量连接的性能问题。
聊聊select, poll 和 epoll_wait的更多相关文章
- 聊聊select, poll 和 epoll
聊聊select, poll 和 epoll 假设项目上需要实现一个TCP的客户端和服务器从而进行跨机器的数据收发,我们很可能翻阅一些资料,然后写出如下的代码. 服务端 void func(int s ...
- select poll epoll相关知识速记
缘起 面试的时候经常被问的一个很蛋疼的问题,经常被问,但是知识很零散,难记忆,看完就忘 select 作用 可以监视文件描述符是否可以读写,要求监视的文件描述符是非阻塞的 诞生背景 产生与上个世纪80 ...
- 高性能网络server--I/O复 select poll epoll_wait之间的差
一个.select 方式作为收集,最多只能监控1024描述叙事断裂的文件,内部使用位操作,相应的位置1或设置0,必须是可读.可写.三类除单独的事件,内部查询方法.将全部的套接字从内核到用户空间之间进行 ...
- select/poll/epoll on serial port
In this article, I will use three asynchronous conferencing--select, poll and epoll on serial port t ...
- 多进程、协程、事件驱动及select poll epoll
目录 -多线程使用场景 -多进程 --简单的一个多进程例子 --进程间数据的交互实现方法 ---通过Queues和Pipe可以实现进程间数据的传递,但是不能实现数据的共享 ---Queues ---P ...
- Python自动化 【第十篇】:Python进阶-多进程/协程/事件驱动与Select\Poll\Epoll异步IO
本节内容: 多进程 协程 事件驱动与Select\Poll\Epoll异步IO 1. 多进程 启动多个进程 进程中启进程 父进程与子进程 进程间通信 不同进程间内存是不共享的,要想实现两个进程间 ...
- select,poll,epoll的归纳总结区分
Select.Poll与Epoll比较 以下资料都是来自网上搜集整理.引用源详见文章末尾. 1 Select.Poll与Epoll简介 Select select本质上是通过设置或者检查存放fd标志位 ...
- select.poll,epoll的区别与应用
先讲讲同步I/O的五大模型 阻塞式I/O, 非阻塞式I/O, I/O复用,信号驱动I/O(SIGIO),异步I/O模型 而select/poll/epoll属于I/O复用模型 select函数 该函数 ...
- select poll epoll三者之间的比较
一.概述 说到Linux下的IO复用,系统提供了三个系统调用,分别是select poll epoll.那么这三者之间有什么不同呢,什么时候使用三个之间的其中一个呢? 下面,我将从系统调用原型来分析其 ...
随机推荐
- python selenium使用
安装selenium #Python pip install selenium #Anaconda3 conda install selenium 下载浏览器版本对应的驱动文件 chrome chro ...
- react性能优化最佳实践
1.PureComponent 的使用场景 PureComponent 和 Component 的区别是,PureComponent 自带 shouldComponentUpdate 生命周期函数,会 ...
- vue中的ref属性
1.什么是ref? ref是用于快速定位到dom结构,vue中一般不去操作dom结构,他是数据驱动的.jQuery会疯狂操作DOM {{msg}} mounted(){ let h = this.$r ...
- 【LeetCode】57. Insert Interval [Interval 系列]
LeetCode中,有很多关于一组interval的问题.大体可分为两类: 1.查看是否有区间重叠: 2.合并重叠区间; 3.插入新的区间: 4. 基于interval的其他问题 [ 做题通用的关键 ...
- 功能测试--聊天功能测试&微信聊天
微信聊天功能测试 发送对象 普通用户.公众号.群.其他特殊主体 衍生功能 转发.语音转文字.删除等 消息发送 单聊.群聊.语音.文字.图片.表情.链接.字符及长度 消息管理 发布通知.接受通知.发文件 ...
- 4.加密与token(node+express)
一. 敏感数据加密1.安装并引入中间件 npm install utility const utils = require('utility')2.加密方法 function ...
- SQLi —— 逗号,空格,字段名过滤突破
前言 出于上海大学生网络安全大赛的一道easysql,促使我积累这篇文章.因为放了大部分时间在Decade和Babyt5上,easysql一点没看,事后看了WP,发现看不懂怎么回事,于是了解了一番. ...
- 《并发编程的艺术》阅读笔记之Volatile
来源 在 JDK1.2 之前,Java的内存模型实现总是从主存(即共享内存)读取变量,是不需要进行特别的注意的.而在当前的 Java 内存模型下,线程可以把变量保存本地内存(比如机器的寄存器)中,而不 ...
- Mysql链接查询
连接查询--交叉连接将两张表的数据与另外一张表彼此交叉原理:1. 从第一张表一次取出每一条记录2. 取出每一条记录之后,与另外一张表的全部记录挨个匹配3. 没有任何匹配条件,所有的结果都会进行保留4. ...
- 理解java容器底层原理--手动实现HashMap
HashMap结构 HashMap的底层是数组+链表,百度百科找了张图: 先写个链表节点的类 package com.xzlf.collection2; public class Node { int ...