文章目录
Select
select 有一个文件描述符集合(fd_set
),里面是一个整数数组,每个整数的每一位对应一个文件描述符,类似于位图
!!!!
这是一个同时接受普通数据和带外数据的程序:
int main( int argc, char* argv[] )
{
if( argc <= 2 )
{
printf( "usage: %s ip_address port_number\n", basename( argv[0] ) );
return 1;
}
const char* ip = argv[1];
int port = atoi( argv[2] );
printf( "ip is %s and port is %d\n", ip, port );
int ret = 0;
struct sockaddr_in address;
bzero( &address, sizeof( address ) );
address.sin_family = AF_INET;
inet_pton( AF_INET, ip, &address.sin_addr );
address.sin_port = htons( port );
int listenfd = socket( PF_INET, SOCK_STREAM, 0 );
assert( listenfd >= 0 );
ret = bind( listenfd, ( struct sockaddr* )&address, sizeof( address ) );
assert( ret != -1 );
ret = listen( listenfd, 5 );
assert( ret != -1 );
struct sockaddr_in client_address;
socklen_t client_addrlength = sizeof( client_address );
int connfd = accept( listenfd, ( struct sockaddr* )&client_address, &client_addrlength );
if ( connfd < 0 )
{
printf( "errno is: %d\n", errno );
close( listenfd );
}
char remote_addr[INET_ADDRSTRLEN];
printf( "connected with ip: %s and port: %d\n", inet_ntop( AF_INET, &client_address.sin_addr, remote_addr, INET_ADDRSTRLEN ), ntohs( client_address.sin_port ) );
char buf[1024];
fd_set read_fds;
fd_set exception_fds;
FD_ZERO( &read_fds );
FD_ZERO( &exception_fds );
int nReuseAddr = 1;
setsockopt( connfd, SOL_SOCKET, SO_OOBINLINE, &nReuseAddr, sizeof( nReuseAddr ) );
while( 1 )
{
memset( buf, '\0', sizeof( buf ) );
FD_SET( connfd, &read_fds );
FD_SET( connfd, &exception_fds );
//每次调用select前重新设置文件描述符,因为会被内核修改
ret = select( connfd + 1, &read_fds, NULL, &exception_fds, NULL );
printf( "select one\n" );
if ( ret < 0 )
{
printf( "selection failure\n" );
break;
}
if ( FD_ISSET( connfd, &read_fds ) )
{
ret = recv( connfd, buf, sizeof( buf )-1, 0 );
if( ret <= 0 )
{
break;
}
printf( "get %d bytes of normal data: %s\n", ret, buf );
}
else if( FD_ISSET( connfd, &exception_fds ) )
{
ret = recv( connfd, buf, sizeof( buf )-1, MSG_OOB );
if( ret <= 0 )
{
break;
}
printf( "get %d bytes of oob data: %s\n", ret, buf );
}
}
close( connfd );
close( listenfd );
return 0;
}
select 缺点:
- 1.
select会修改传递的fd_sets
(UNP中把他叫做:值-结果参数),这样就不能重用它们,所以必须在每次调用select前重新设置文件描述符. - 2.
手动循环遍历查找
.要找出引发事件的描述符,您必须手动迭代集合中的所有描述符,并在每个描述符上调用FD_ISSET。如果你有2,000个这样的描述符,并且只有其中一个是活动的 - 而且可能是最后一个 - 你每次等待都会浪费CPU周期。 - 3.
支持的文件描述符数目有限.
以#define __FD_SETSIZE 1024
为限.虽然某些操作系统允许您通过在包含sys / select.h之前重新定义FD_SETSIZE来破解此限制,但这不是可移植的。实际上,Linux会忽略这种黑客攻击并且限制将保持不变。 - 4.
当描述符在select集合中被监听时其他的线程不能修改它
。假设你有一个管理线程检测到sock1等待输入数据的时间太长需要关闭它,以便重新利用sock1来服务其他工作线程。但是它还在select的监听集合中。如果此时这个套接字被关闭会发生什么?select的man手册中有解释:如果select正在监听的套接字被其他线程关闭,结果是未定义的。 - 5.
如果另外一个线程突然决定通过sock1发送数据,在等待select返回之前不能监听这个套接字的写事件
(还得等待select返回,然后重新FD_SET,添加到对应的集合中)。 - 6.
选择监听的事件类型是有限的
;例如,检查一个远程套接字是否关闭你只有两种方法:(1) 监听它的读事件(2)尝试实际去读取这个套接字的数据来探测它是否关闭(当关闭时会返回0)。你希望从这个套接字中读取数据这种方法是可行的,但是如果你是在发送文件完全不需要关心读事件该怎么办了?(没看懂) - 7.当填充描述符集合时,select会给你带来额外的负担,因为你
需要计算描述符中的最大值并把它当作函数参数传递给select
。
什么时候还需要使用select:
当然操作系统开发人员也会意识到这些缺陷,并且在设计poll接口时解决了大部分问题,因此你会问,还有任何理由使用select吗?为什么不直接淘汰它了?其实还有两个理由使用它:
- 1.第一个原因是
可移植性
。select已经存在很长时间了,你可以确定每个支持网络和非阻塞套接字的平台都会支持select,而它可能还不支持poll。另一种选择是你仍然使用poll然后在那些没有poll的平台上使用select来模拟它。 - 2.第二个原因是
select的超时时间理论上可以精确到纳秒级别。而poll和epoll的精度只有毫秒级
。这对于桌面或者服务器系统来说没有任何区别,因为它们不会运行在纳秒精度的时钟上,但是在某些与硬件交互的实时嵌入式平台,降低控制棒关闭核反应堆.可能是需要的。(这就可以作为一个更加精确的sleep()来用)
只有在上面提到的原因中你必须使用select没有其他选择。但是如果你编写的程序永远不会处理超过一定数量的连接(例如:200),此时select和poll之间选择不在于性能,而是取决于个人爱好或者其他原因。
Poll
poll是一个比较新的接口,它可能是在有人试图编写高性能网络服务时被创建的。它的设计更加出色并且解决了select中的大多数问题。在绝大多数情况下你应该在poll和epoll/libevent之间做选择。
在使用poll之前开发人员需要使用监听的事件类型和描述符来初始化pollfd结构体,然后调用poll()。下面是一个典型的程序流程:基本与select相同
// The structure for two events
struct pollfd fds[2];
// Monitor sock1 for input
fds[0].fd = sock1;
fds[0].events = POLLIN;
// Monitor sock2 for output
fds[1].fd = sock2;
fds[1].events = POLLOUT;
// Wait 10 seconds
int ret = poll( &fds, 2, 10000 );
// Check if poll actually succeed
if ( ret == -1 )
// report error and abort
else if ( ret == 0 )
// timeout; no event detected
else
{
// 如果我们检测到事件,请将其归零,以便我们可以重用该结构
if ( pfd[0].revents & POLLIN )
pfd[0].revents = 0;
// input event on sock1
if ( pfd[1].revents & POLLOUT )
pfd[1].revents = 0;
// output event on sock2
}
Poll优点:
- 1.
它监听的描述符数量没有限制,可以超过1024。(因为其是用链表写的)
- 2.它不会修改struct pollfd数据中传递的数据。因此,只要将生成事件的描述符的revents成员设置为零,就可以在poll()调用之间重用它
- 3.相比于select来说可以更好的控制事件。例如,它可以检测对端套接字是否关闭而不需要监听它的读事件。
Poll缺点:
其实就是将上面select 的缺点中减去刚刚提到的三个优点就行.
- 2.和select一样必须通过遍历描述符列表来查找哪些描述符产生了事件。更糟糕的是在内核空间也需要通过遍历来找到哪些套接字正在被监听,然后再重新遍历整个列表来设置事件。
- 4.和select一样它也不能在描述符被监听的状态下修改或者关闭套接字。会出现未定义行为
什么时候应该选择使用Poll:
- 跨平台
- 同一时刻你的应用程序监听的套接字少于1000(这种情况下使用epoll不会得到任何益处)。
- 您的应用程序需要一次监视超过1000个套接字,但连接非常短暂(这是一个接近的情况,但很可能在这种情况下,您不太可能看到使用epoll的任何好处,因为epoll 的加速将这些新描述符添加到集合中会浪费等待 - 见下文
- 您的应用程序的设计方式不是在另一个线程正在等待它们更改事件(即您没有使用kqueue或IO完成端口移植应用程序)。
select/poll 的共同的缺点
一 返回后需要遍历fd集合找到就绪的fd,但fd集合就绪 的描述符很少;
二 select/poll 均需将 fd 集合在用户态和内核态之间来回拷贝。
Epoll
epoll是Linux(也是Linux)中最新,最好,最新的轮询方法。好吧,它实际上是在2002年添加到内核中的,所以它并不是那么新。它与poll和select不同,它保留了内核中当前监视的描述符和相关事件的信息,并导出API以添加/删除/修改它们。
要使用epoll,需要做更多的准备工作。开发人员需要:
- 通过调用epoll_create创建epoll描述符;
- 初始化一个epoll_event 结构
- 调用epoll_ctl ( … EPOLL_CTL_ADD)将描述符添加到监视集中
- 调用epoll_wait()函数并传递20个事件结构体的存储空间。和前面的两个轮询接口不同,这个函数接受的是空的结构体,然后只会将被触发的事件填充到结构体中。例如这里监听了200个描述符,其中5个描述符有事件被触发,epoll_wait()会返回数值5,然后填充传递进来的20个存储空间中的前5个空间。如果有50个描述符有事件被触发,前面20个会被复制到用户程序中,其余30个会保存在队列中,不会丢失。
- 然后遍历这些被返回的描述符,因为epoll只会返回有事件被触发的描述符所以这里的遍历非常高效。
/ Create the epoll descriptor. Only one is needed per app, and is used to monitor all sockets.
// The function argument is ignored (it was not before, but now it is), so put your favorite number here
int pollingfd = epoll_create( 0xCAFE );
if ( pollingfd < 0 )
// report error
// Initialize the epoll structure in case more members are added in future
struct epoll_event ev = { 0 };
// Associate the connection class instance with the event. You can associate anything
// you want, epoll does not use this information. We store a connection class pointer, pConnection1
ev.data.ptr = pConnection1;
// Monitor for input, and do not automatically rearm the descriptor after the event
ev.events = EPOLLIN | EPOLLONESHOT;
// Add the descriptor into the monitoring list. We can do it even if another thread is
// waiting in epoll_wait - the descriptor will be properly added
if ( epoll_ctl( epollfd, EPOLL_CTL_ADD, pConnection1->getSocket(), &ev ) != 0 )
// report error
// Wait for up to 20 events (assuming we have added maybe 200 sockets before that it may happen)
struct epoll_event pevents[ 20 ];
// Wait for 10 seconds
int ready = epoll_wait( pollingfd, pevents, 20, 10000 );
// Check if epoll actually succeed
if ( ret == -1 )
// report error and abort
else if ( ret == 0 )
// timeout; no event detected
else
{
// Check if any events detected
for ( int i = 0; i < ret; i++ )
{
if ( pevents[i].events & EPOLLIN )
{
// Get back our connection pointer
Connection * c = (Connection*) pevents[i].data.ptr;
c->handleReadEvent();
}
}
}
EPoll的优点:
- 1.
epoll只会返回有事件发生的描述符
,所以不需要遍历所有监听的描述符来找到哪些描述符产生了事件。 - 2.
你可以将处理对应事件的方法和所需要的数据附加到被监听的描述符上
。在上面的例子中我们附加了一个类的指针,这样就可以直接调用处理对应事件的方法。 - 3.你
可以在任何时间添加或者删除套接字
,即使有其他线程正在epoll_wait函数中。你甚至可以修改正在被监听描述符的事件,不会产生任何影响。这种行为是被官方支持的而且有文档说明。这样就可以使我们在写代码时有更大的灵活性。 - 4.因为内核知道所有被监听的描述符,所以即使没有人调用 epoll_wait(),内核也可以记录发生的事件,这允许实现一些有趣的特性,例如边沿触发,这将在另一篇文章中讲到。
- 5.epoll_wait()函数可以让多个线程等待同一个 epoll 队列而且推荐设置为边沿触发模式,这在其他轮询方式中是不可能实现的
EPoll的缺点:
- 1.
改变监听事件的类型(例如从读事件改为写事件)需要调用epoll_ctl系统调用,而这在poll中只需要在用户空间简单的设置一下对应的掩码
。如果需要改变5000个套接字的监听事件类型就需要5000次系统调用和上下文切换(直到2014年epoll_ctl函数仍然不能批量操作,每个描述符只能单独操作),这在poll中只需要循环一次pollfd结构体。 - 2.
每一个被accept()的套接字都需要添加到集合中,在epoll中必须使用epoll_ctl来添加–这就意味着每一个新的连接都需要两次系统调用
,而在poll中只需要一次。如果你的服务有非常多的短连接它们都接受或者发送少量数据,epoll所花费的时间可能比poll更长。(解释了上文) - 3.
epoll是Linux上独有的
,虽然其他平台上也有类似的机制但是他们的区别非常大,例如边沿触发这种模式是非常独特的(FreeBSD的kqueue对它的支持非常粗糙)。 - 4.高性能服务器的处理逻辑非常复杂,因此更加难以调试。尤其是对于边沿触发(ET),如果你错过了某次读/写操作可能导致死锁(这是什么鬼??
什么情况下使用EPoll:
- 1.
你的程序通过多个线程来处理大量的网络连接
。如果你的程序只是单线程的那么将会失去epoll的很多优点。并且很有可能不会比poll更好。 - 2.
你需要监听的套接字数量非常大(至少1000)
;如果监听的套接字数量很少则使用epoll不会有任何性能上的优势甚至可能还不如poll。 - 3.
你的网络连接相对来说都是长连接
;就像上面提到的epoll处理短连接的性能还不如poll因为epoll需要额外的系统调用来添加描述符到集合中。 - 4.你的应用程序依赖于Linux上的其他特性
如果上面的条件都不成立,你更应该使用 poll
EPoll的内部实现:
epoll_create 在内核的高速cache
(牵涉到kmem_cache,slab等)中建一棵红黑树以及两条链表。
epoll_ctl的add在 红黑树上添加fd结点,并且注册fd的回调函数(内核定义的回调函数),内核在检测到某fd就绪时会调用回调函数将fd 添加到就绪链表中。
epoll_wait 将 rdlist 中的fd返回。
epoll_wait 获取句柄时,检测 rdlist 中事件有无就行了,ovflist 只是怕在通过共享内存从内核传递就绪fd到用户
的时候,产生新的事件的一个暂存的地方!!!!!
(1)什么是活跃的连接?
正在进行请求与响应的连接
(2)红黑树是如何知道有事件到来的?
网卡发现报文后,内核可以直接从地址定位到事件(感觉这个就可以回答开心学长 的提问了,哈哈哈)
(3)事件的操作
当需要操作(添加,删除,修改)某个事件时,操作的是树上的节点,不用遍历所有事件
;插入、删除、查找的最坏时间复杂度都为 O(logn)
。
当某个事件发生时,系统把它加入到链表中。
对 Epoll 的一些感悟:
- 如果对 listenfd 设置 ET 模式,accept 只要没有接受完 全连接队列,这些连接就不会再次被触发了!!!就是从 rdlist 上删除了呗
- LT 的话就是会将没处理完的放在 rdlist 的最前面,然后跟着下次需要返回的一起返回
原文来源:
https://www.ulduzsoft.com/2014/01/select-poll-epoll-practical-difference-for-system-architects/