select系统调用是用来让我们的程序监视多个文件描述符的状态变化的。程序会阻塞到select这里,直到被监听的文件描述符一个或者多个发生了状态变化。
int select(int nfds, fd_set* read_fds, fd_set* write_fds,
fd_set* except_fds, struct timeval* timeout);
首先,我们可以看一下这个结构的定义:
/* The fd_set member is required to be an array of longs. */
typedef long int __fd_mask;
typedef struct{
/* something */
__fd_mask fds_bits[__FD_SETSIZE / __NFDBITS];
/* something */
};
注:上面代码节选自
中,其中我只保留了便于理解的部分。
从定义中我们可以看出,其实fd_set
结构就是一个long型数组,或者说,它代表一种数据结构----“位图”。使用位图中对应的位来表示要监视的文件描述符。
select提供了一组操纵位图的接口:
void FD_CLR(int fd, fd_set *set); // 用来清除描述位图set中相关fd的位
int FD_ISSET(int fd, fd_set *set); // 用来测试描述位图set中相关fd的位是否为真
void FD_SET(int fd, fd_set *set); // 用来设置描述位图set中相关fd的位
void FD_ZERO(fd_set* set); // 用来清空位图set中的所有位
这个结构的定义:
/* A time value that is accurate to the nearest
microsecond but also has a range of years.
*/
struct timeval{
__time_t tv_sec; /* Second. */
__suseconds_t tv_usec; /* Microseconds. */
};
注:上面代码节选自
中,其中我只保留了便于理解的部分。
执行成功
其他结果
socket内核中, 发送缓冲区中的可用字节数(发送缓冲区的空闲位置大小), 大于等于低水位标记
SO_SNDLOWAT;
socket的写操作被关闭(close或者shutdown). 对一个写操作被关闭的socket进行写操作, 会触发SIGPIPE
信号;
socket使用非阻塞connect连接成功或失败之后;
socket上有未处理的错误。
使用select实现一个本地回显程序。
#include
#include
#include
#include
int main() {
fd_set read_fds;
FD_ZERO(&read_fds); //初始化fd_set结构
FD_SET(STDIN_FILENO, &read_fds); //监听标准输入
while(1){
printf("> ");
fflush(stdout);
int ret = select(STDIN_FILENO+1, &read_fds, NULL, NULL, NULL);
if(ret < 0){
perror("select");
continue;
}
if(FD_ISSET(STDIN_FILENO, &read_fds)){
char buf[1024] = {0};
read(STDIN_FILENO, buf, sizeof(buf) - 1); //读取键盘输入
printf("Echo: %s\n", buf);
} else {
printf("error! invalid fd\n");
continue;
}
FD_ZERO(&read_fds);
FD_SET(STDIN_FILENO, &read_fds);
}
return 0;
}
int poll(struct pollfd* fds, nfds_t nfds, int timeout);
struct pollfd{
int fd; /* File descriptor to poll. */
short int events; /* Types of events poller cares about. */
short int revents; /* Types of events that actually occurred. */
};
注:上面代码节选自
中
events和revents的取值列表:
事件 | 描述 | 是否可作为输入 | 是否可作为输出 |
---|---|---|---|
POLLIN | 数据可读(包括普通数据和优先数据) | 是 | 是 |
POLLRDNORM | 普通数据可读 | 是 | 是 |
POLLPRI | 高优先级数据可读,eg: TCP带外数据 | 是 | 是 |
POLLOUT | 数据可写(包括普通数据和优先数据) | 是 | 是 |
POLLWRNORM | 普通数据可写 | 是 | 是 |
POLLWRBAND | 优先级带数据可写 | 是 | 是 |
POLLRDHUP | TCP连接被对方关闭,或对方关闭了写操作 | 是 | 是 |
POLLERR | 错误 | 否 | 是 |
POLLHUP | 挂起 | 否 | 是 |
POLLNVAL | 文件描述符未打开 | 否 | 是 |
执行成功
其他结果
同select
使用poll实现一个本地回显程序。
#include
#include
#include
int main(){
struct pollfd poll_fd;
poll_fd.fd = STDIN_FILEIN;
poll_fd.events = POLLIN;
while(1){
printf("> ");
fflush(stdout);
int ret = poll(&poll_fd, 1, -1);
if(0 == ret){
printf("poll timeout\n");
continue;
}else if(ret < 0){
perror("poll");
continue;
}
if(POLLIN == poll_fd.revents){
char buf[1024] = {0};
read(STDIN_FILENO, buf, sizeof(buf) - 1);
printf("stdin: %s", buf);
}
}
return 0;
}
int epoll_create(int size);
在Linux2.6.8版本后,这个参数会被忽略。但是还是要注意,这个参数不可以传入小于0的数。
epoll_create返回一个操作epoll的句柄。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event);
这个结构体的定义是这样的:
typedef union epoll_data{
void* ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
struct epoll_event{
uint32_t events; /* Epoll events. */
epoll_data_t data; /* User data variable. */
} __EPOLL_PACKED;
上面代码节选自
中,我只保留了便于理解的部分。
其中events可以是以下几个宏的集合:
int epoll_wait(int epfd, struct epoll_event* events, int max_events, int timeout);
执行成功
其他结果
while(true){
struct epoll_event epoll_events[1024];
int n = epoll_wait(epollfd, epoll_events, 1024, 1000);
if(n < 0){
if(EINTR == errno) //被信号中断
continue;
break; //出错,直接退出
} else if(0 == n) { //超时
continue;
}
for(size_t i = 0; i != n; ++i){
if(epoll_events[i].events & POLLIN){
//TODO:处理可读事件
} else if(epoll_events[i].events & POLLOUT) {
//TODO:处理可写事件
} else if(epoll_events[i].events & POLLERR) {
//TODO:处理异常事件
}
}
}
当进程调用epoll_create时,Linux内核会创建一个eventpoll结构体,这个结构体中有两个成员和epoll的使用密切相关:
struct eventpoll{
/*something*/
/* 红黑树的根节点,这棵树中存储着所有添加到epoll中需要监控的事件。 */
struct rb_root rbr;
/* 双链表的头节点,双链表中存储着要通过epoll_wait返回给用户的满足条件的事件。 */
struct list_head rblist;
/*something*/
};
每一个epoll对象都会在内核中创建一个eventpoll结构体,通过epoll_ctl方法向epoll对象添加进来的事件,这些事件都会挂载到eventpoll中的红黑树上,事件的结构类型是这样的:
struct epitem{
struct rb_node rbn; /* 红黑树节点 */
struct list_head rdllink; /* 双向链表节点 */
struct epoll_filefd ffd; /* 事件句柄信息 */
struct eventpoll* ep; /* 指向其所属的 eventpoll 对象 */
struct epoll_event event; /* 期待发生的事件类型 */
};
所有添加到epoll对象中的事件都会与设备驱动程序建立回调关系,当事件发生响应,就会调用这个回调方法。这个回调方法在内核中叫ep_poll_callback,它主要的作用是将发生响应的事件添加到eventpoll中的双链表上。
当调用epoll_wait检查事件是否发生的时候,只需要检查eventpoll对象中的rdlist双链表中是否存在epitem(即事件)元素即可。如果存在,则把rdlist中的事件拷贝到用户态,同时通过返回值将rdlist中的元素数目(事件发生的个数)返回给用户。
先看一下这个例子:
已经把一个tcp socket添加到epoll描述符中,这个时候socket的客户端写入了2KB数据,服务器端调用epoll_wait,epoll_wait返回,然后调用read,可在服务器端分配的缓冲区只有1KB,所以一次只读取了1KB数据,继续调用epoll_wait…
当epoll检测到socket上事件就绪的时候,可以选择只处理一部分数据,或者不立即进行处理。例如上面的例子,由于第一次调用epoll_wait,服务器端只读取了1KB数据,在第二次调用epoll_wait时,epoll_wait仍然会立即返回并通知socket读事件就绪,直到socket缓冲区中所有的数据都读取,epoll_wait才不会因为这个socket而立即返回。
LT模式支持阻塞读写和非阻塞读写。
注:select和poll其实就相当于是epoll的LT模式。
当epoll_wait返回通知socket读时间就绪时,必须立即处理,而且要一次处理完。例如上面的例子,第一次调用epoll_wait时,只读取了1KB数据,那么第二次调用epoll_wait时,epoll_wait就不会再返回。
ET模式支持非阻塞读写。注意:使用ET模式的时候,需要将监听的文件描述符设置为非阻塞。
注:Nginx默认用的就是epoll的ET模式。
/* epoll LT模式的回显服务器 */
#include
#include
#include
#include
#include
#include
#include
#include
#define CHECK_ERROR(str, n) do{if(n < 0){ perror(str); exit(EXIT_FAILURE); }}while(0)
void ProcessConnect(int listen_fd, int epoll_fd){
struct sockaddr_in client_addr;
socklen_t len = sizeof(client_addr);
int connect_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &len);
if(connect_fd < 0){
perror("accept");
exit(EXIT_FAILURE);
}
printf("client %s:%d connect\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
struct epoll_event ev;
ev.data.fd = connect_fd;
ev.events = EPOLLIN;
int ret = epoll_ctl(epoll_fd, EPOLL_CTL_ADD, connect_fd, &ev);
if(ret < 0){
perror("epoll_ctl");
exit(EXIT_FAILURE);
}
}
void ProcessRequest(int connect_fd, int epoll_fd){
char buf[1024] = {0};
ssize_t read_size = read(connect_fd, buf, sizeof(buf) - 1);
if(read_size < 0){
perror("read");
return;
}
if(0 == read_size){
close(connect_fd);
epoll_ctl(epoll_fd, EPOLL_CTL_DEL, connect_fd, NULL);
printf("client say: goodbye\n");
return;
}
printf("client say: %s\n", buf);
write(connect_fd, buf, strlen(buf));
}
void CorrectUsage(){
printf("Usage: ./epoll_server [ip] [port]\n");
}
int main(int argc, char* argv[]) {
if(argc != 3){
CorrectUsage();
exit(EXIT_FAILURE);
}
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(atoi(argv[2]));
addr.sin_addr.s_addr = inet_addr(argv[1]);
int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
CHECK_ERROR("socket", listen_fd);
int ret = bind(listen_fd, (struct sockaddr*)&addr, sizeof(addr));
CHECK_ERROR("bind", ret);
ret = listen(listen_fd, 10);
CHECK_ERROR("listen", ret);
int epoll_fd = epoll_create(5);
CHECK_ERROR("epoll_create", epoll_fd);
struct epoll_event event;
event.events = EPOLLIN;
event.data.fd = listen_fd;
ret = epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_fd, &event);
CHECK_ERROR("epoll_ctl", ret);
while(1){
struct epoll_event events[10];
int size = epoll_wait(epoll_fd, events, sizeof(events) / sizeof(events[0]), -1);
if(size < 0){
perror("epoll_wait");
continue;
}
if(0 == size){
printf("epoll timeout\n");
continue;
}
for(int i = 0; i < size; ++i){
if(!(events[i].events & EPOLLIN)){
continue;
}
if(events[i].data.fd == listen_fd){
ProcessConnect(listen_fd, epoll_fd);
}else{
ProcessRequest(events[i].data.fd, epoll_fd);
}
}
}
return 0;
}
参考:Epoll的惊群效应
前面介绍了一下epoll的底层原理,可以看出它相对于之前的IO多路复用接口做了很多优化:
对于多连接,并且多连接中只有一部分连接比较活跃时,比较适合用epoll。例如:各种互联网APP的入口服务器,就很适合用epoll。