select系统调用的用途是:在一段指定时间以内,监听用户感兴趣的文件描述符上的可读、可写和异常事件。
#include
int select(int nfds,fd_set* readfds, fd_set* writefds, fd_set* exceptfds, struct timeval* timeout);
nfds
设置为被监听文件描述符的总数,即所有文件描述符中最大值加一。
readfds
,writefds
,exceptfds
分别表示可读,可写,异常等事件的文件描述符集合,其类型fd_set
仅包含一个整数数组,每个元素的每一位标记一个文件描述符,select中包含了以下宏来对fd_set
进行操作。
#include
FD_ZERO(fd_set* fdset); //清除fdset的所有位
FD_SET(int fd, fd_set* fdset); //设置fdset的位fd
FD_CLR(int fd, fd_set* fdset); //清除fdset的位fd
int FD_ISSET(int fd, fd_set* fdset); //测试fdset的位fd是否被设置
timeout
用来设置select
函数的超时时间。其中用到了timeval
结构体
struct timeval{
long tv_sec; //秒数
long tv_usec; //微秒数
}
select
成功时返回就绪文件的文件描述符,如果超时返回0,失败时返回-1并设置error。如果在select等待期间,程序收到信号,select立即返回-1,并设置errno为EINTR。
《Linux高性能服务器编程》P148
注:网络编程中select能处理的异常情况只有一种:socket上接收到带外数据。
下面的代码描述了select是如何处理socket处于两种不同的就绪状态,即接收到普通数据处于可读状态和接收到带外数据处于异常状态。
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
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]);
int ret = 0;
struct sockaddr_in address; //新建一个ipv4的socket结构体
bzero(&address, sizeof(address));
address.sin_family = AF_INET; //设置ipv4地址族
inet_pton(AF_INET, ip, &address.sin_addr); //将传进来的点分十进制转化为网络字节序表示的ip地址
address.sin_port = htons(port); //将传进来的主机字节序转化为网络字节序
int listenfd = socket(PF_INET, SOCK_STREAM, 0); //创建一个使用ipv4协议的TCP连接socket
assert(listenfd >= 0); //其值为假,则终止运行并弹出错误信息
ret = bind(listenfd, (struct sockaddr*)&address, sizeof(address));//将address所指向的地址分配给文件描述符listenfd
assert(ret != -1);
ret = listen(listenfd, 5);//开始监听socket,监听队列长度为5
assert(ret != -1);
struct sockaddr_in client_address;//创建一个客户端的socket连接用来存储客户端的地址信息
socklen_t client_addrlength = sizeof(client_address);
int connfd = accept(listenfd, (struct sockaddr*)&client_address, &client_addrlength);//接受listenfd和client_address的socket连接,并将相关信息保存下来
//accept是从监听队列中取出连接,并且将连接的客户地址存在client_address。
if(connfd < 0){
printf("errno is: %d\n", errno);
close(listenfd);
}
char buf[1024];
fd_set read_fds;
fd_set exception_fds;
FD_ZERO(&read_fds);
FD_ZERO(&exception_fds);
while(1){
memset(buf, '\0', sizeof(buf));
FD_SET(connfd, &read_fds);
FD_SET(connfd, &exception_fds);
ret = select(connfd + 1, &read_fds, NULL, &exception_fds, NULL);
if(ret < 0){
printf("selection failure\n");
break;
}
if(FD_ISSET(connfd, &read_fds)){ //read_fds中是否设置了connfd,是的话说明监听到了connfd上有可读事件
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)){//exception_fds中是否设置了connfd,是的话说明监听到了connfd上有异常事件
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;
}
}
poll系统调用是在指定时间内轮询一定数量的文件描述符,以测试其中是否有就绪者。
#include
int poll(struct pollfd* fds, nfds_t nfds, int timeout);
fds
是一个pollfd
类型的数组。它的定义如下:
struct pollfd{
int fd; //文件描述符
short events; //注册的事件
short revents; //实际发生的事件,由内核填充
}
nfds
参数指定了被监听事件集合fds
的大小。
timeout
参数指定了poll
的超时值。当timeout
为-1时,poll
调用永远堵塞,直到某个事件发生,当timeout
为0时,timeout
立即返回。
epoll
是linux
特有的I/O复用函数,它与select,poll的差异在于:
epoll
使用一组函数来完成任务而不是单个函数。epoll
把用户关心的文件描述符上的事件放在内核的一个事件表中,无须像select
和poll
一样每次调用都要传入文件描述符或者事件集。所以epoll
需要一个文件描述符来唯一表示该内核事件表。下面的函数用来创建内核事件表的文件描述符。
#include
int epoll_create(int size); //创建内核事件表的文件描述符
//size指定了内核事件表有多大
下面的函数用来操作epoll的内核事件表。
#include
int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event);
//成功返回0,失败返回-1并设置errno
epfd指的是某个内核事件表的文件描述符
fd指的是要操作的文件描述符
op指定操作类型,有以下三种:EPOLL_CTL_ADD,往事件表中注册fd上的事件。EPOLL_CTL_MOD,修改fd上的注册事件。EPOLL_CTL_DEL,删除fd上的注册事件。
event指定事件,其定义为:
struct epoll_event{
__uint32_t events; //epoll事件
epoll_data_t data; //用户数据
}
typedef union epoll_data{
void* ptr;
int fd;
uint32_t u32;
uint64_t u63;
}epoll_data_t;
这是epoll
系统调用的主要接口,作用是在一段时间内等待一组文件描述符上的事件。
#include
int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout);
timeout
的含义与poll
的timeout
含义相同。
maxevents
指定最多监听多少个事件,必须大于0 。
epoll_wait
如果检测到事件,就将所有的就绪事件从内核事件表中取出并复制到第二个参数events
指向的数组中。它不像select
和poll
的数组参数既用于传入用户注册的事件,又用于输出内核检测到的事件,这就极大的提高了应用程序索引就绪文件描述符的效率。
epoll
对文件描述符的操作有两种模式:LT
(Level Trigger,电平触发)和ET
(Edge Trigger,边沿触发)两种模式。LT
模式是默认的工作模式,相当于一个效率较高的poll
。ET
模式是epoll
的高效工作模式。
对于采用LT
模式的文件描述符,当epoll_wait
检测到其上有事件发生并将此事件通知应用程序后,应用程序可以不立即处理该事件。这样,当应用程序下一次调用epoll_wait
时,epoll_wait
还会再次向应用程序通告此事件,直到该事件被处理。
对于采用ET
模式的文件描述符,当epoll_wait
检测到其上有事件发生并将此事件通知应用程序后, 应用程序必须立即处理该事件,因为后续的epoll_wait
调用将不再向应用程序通知这一事件。
由此可见,ET
模式相较于LT
模式,减少了同一个epoll
事件被重复触发的次数,因此效率要比LT
模式高。
ET
模式的文件描述符都应该是非阻塞的。如果文件描述符时阻塞的,那么读或写操作将会因为没有后续的事件而一直处于阻塞状态。 EPOLLONESHOT事件可以实现一个socket连接在任一时刻都只被一个线程处理。对于注册了EPOLLONESHOT事件的文件描述符,操作系统最多触发其上注册的一个可读、可写或者异常事件,且只触发一次,除非使用epoll_ctl函数重置该文件描述符上注册的EPOLLONESHOT事件。
所以,一旦注册了EPOLLONESHOT事件的socket被某个线程处理完毕,该线程就应该立即重置这个socket上的EPOLLONESHOT事件,以确保这个socket下次可读时,其EPOLLIN事件能被触发,从而让其他工作线程有机会继续处理这个socket。
系统调用 | select | poll | epoll |
---|---|---|---|
事件集合 | 用户通过三个参数分别传入感兴趣的可读、可写以及异常等事件,内核通过对这些参数的在线修改来反馈其中的就绪事件。这使得用户每次调用select都要重复这三个参数 | 统一处理所有事件类型,因此只需一个事件集参数,用户通过pollfd,events传入感兴趣的事件,内核通过修改pollfd,revents反馈其中的就绪事件 | 内核通过一个事件表直接管理用户感兴趣的所有事件。因此每次调用epoll,wait时,无须反复传入用户感兴趣的事件,epoll_wait系统调用的参数events仅用来反馈就绪的事件 |
应用程序索引就绪事件的事件复杂度 | O(n) | O(n) | O(1) |
最大支持文件描述符数量 | 一般有最大值限制 | 65535 | 65535 |
工作模式 | LT | LT | 支持ET高效率模式 |
内核实现和工作效率 | 采用轮询的方式来检测就绪事件,算法的时间复杂度为O(n) | 采用轮询的方式来检测就绪事件,算法的时间复杂度为O(n) | 采用回调的方式来检测就绪事件,算法的时间复杂度为O(1) |