epoll select的限制 条件触发 边缘触发

40次阅读

共计 4119 个字符,预计需要花费 11 分钟才能阅读完成。

结论: epoll 要优于 select , 编程模型基本一致;
请注意, 不论是 epoll 还是 select 都不是具有并发能力的服务器, 仅仅是 io 复用
题外话: 在 io 复用中把监听套接字设为非阻塞
觉得理论麻烦的, 可以直接往下拉, 有代码例子;
select 的缺陷:
1. 在 sys/select.h 中 __FD_SETSIZE 为 1024 , 意味默认情况最多监控 1024 个描述符, 当然可以修改这个文件重新编译
2.select 的实现 , 在 fs/select.c 中:

int do_select(int n, fd_set_bits *fds, s64 *timeout)
{
// 略
for (i = 0; i < n; ++rinp, ++routp, ++rexp)
{
// 略
}
}
在实现中 , 每次将经过 n-1 次循环, 随着描述符越多, 性能线性下降

3.select 的 3 个 fd_set(read,write,except) 都是值 - 结果:
例如:

fd_set rset,allset;
while(1){
rset = allset; // 每次需要重置
select(maxfd+1, rset, ….)
}
这种每次重置意味不断的从用户空间往内核空间中复制数据,
操作系统一旦轮询完成后, 再将数据置位, 然后复制到用户空间,
就是这种不断的复制造成了性能下降, 还不得不这么干;

epoll 解决了 select 缺陷;epoll 给每个需要监听的描述符都设置了一个或多个 event :
struct epoll_event event;
event.events = EPOLLIN; // 读操作 , 如要监控多个可以 EPOLLIN|EPOLLOUT;
event.data.fd = listensock; // 赋值要监听的描述符, 就像 FD_SET(listensock,&rset);

epoll 的描述符限制可以查看 cat /proc/sys/fs/epoll/max_user_watches
复制数据也仅仅在 epoll_ctl (…,EPOLL_CTL_ADD,…) 添加时 (EPOLL_CTL_ADD), 复制一次到 epoll_create 所创建的红黑树中

*3. 最重要的是,epoll_wait 并不会像 select 去轮询, 而是在内部给监听的描述符一个 callback.
一旦对应的事件发生 (例如:EPOLLIN) 就将此事件添加到一个链表中.
epoll_wait(此函数类似 select) 的功能就是去链表中收集发生事件相应的 struct epoll_event;

总的来说:epoll_create 在操作系统中创建一个用于存放 struct epoll_event 的空间 epoll_ctl 在空间内 添加, 修改, 删除 struct epoll_event (内含描述符);epoll_wait 收集已经发生事件的描述符;close 关闭 (减少引用) epoll
一个类似 select 的 echo 服务器修改版本:
echo.c
#define EPOLL_SIZE 100

int listensock = socket(AF_INET,SOCK_STREAM,0);
struct sockaddr_in serv_addr, cli_addr;
socklen_t socklen = sizeof(serv_addr);
memset(&serv_addr,0,socklen);
memset(&cli_addr,0,socklen);
serv_addr.sin_addr.s_addr = INADDR_ANY;
serv_addr.sin_port = htons(PORT);
serv_addr.sin_family = AF_INET;

if(bind(listensock,(SA*)&serv_addr,sizeof(serv_addr)) < 0){
perror(“bind”);
return 0;
}

if(listen(listensock,BACKLOG) < 0){
perror(“listen”);
return 0;
}

// 向操作系统请求 创建一个 epoll , 参数看 man
int epfd = epoll_create(EPOLL_SIZE);
if(epfd < 0){
perror(“epoll_create”);
return 0;
}

// 监听 listensock , 类似 FD_SET(listensock, &rset)
struct epoll_event event;
event.events = EPOLLIN; // 读取 . man 中有详细解释
event.data.fd = listensock;

if(epoll_ctl(epfd,EPOLL_CTL_ADD,listensock,&event) < 0) // 把 listensock 注册到 epfd, 用于监听 event 事件
{
perror(“epoll ctl”);
return 0;
}

// 分配一块内存. 在 epoll_wait 返回后将存放发生事件的结构体
struct epoll_event * ep_events = malloc(sizeof(struct epoll_event) * EPOLL_SIZE);
if(ep_events == NULL){
perror(“malloc”);
return 0;
}

puts(“server is running”);
int n = 0;
int client_fd = 0;
char buf[BUFSIZ];
int len = 0,return_bytes = 0, tmp_fd = 0;

while(1){

// 收集已经发生事件的描述符 , 返回值与 select 一致
n = epoll_wait(epfd,ep_events,EPOLL_SIZE,-1);
if(n < 0){
perror(“epoll_wait”);
break;
}
printf(“%d events returned\n”, n);
for (int i = 0; i < n ; ++i){

// 如果有人连接
if(listensock == ep_events[i].data.fd){
socklen = sizeof(cli_addr);
client_fd = accept(listensock,(SA*)&cli_addr,&socklen);

// 如果有人连接, 则加入 epoll
event.events = EPOLLIN; // 读取
event.data.fd = client_fd;
if(epoll_ctl(epfd,EPOLL_CTL_ADD,client_fd,&event) < 0){
perror(“epoll add failed”);
continue;
}
printf(“accepted,client_fd:%d,ip:%s,port:%d\n”,
client_fd,inet_ntoa(cli_addr.sin_addr),ntohs(cli_addr.sin_port));
}
else {
tmp_fd =ep_events[i].data.fd;
readagain:
len = read(ep_events[i].data.fd,buf,BUFSIZ);

// 如果出错
if(len < 0) {
if(errno == EINTR)
goto readagain;
else{
return_bytes = snprintf(buf,BUFSIZ-1,
“clientfd:%d errorno:%d\n”,tmp_fd,errno);
buf[return_bytes] = 0;
epoll_ctl(epfd,EPOLL_CTL_DEL,tmp_fd,NULL); // 从 epoll 中移除
close(tmp_fd); //close(socket)
perror(buf);
}
} else if(0 == len){

// 对端断开
return_bytes = snprintf(buf,BUFSIZ-1,
“clientfd:%d closed\n”,tmp_fd);
buf[return_bytes] = 0;
epoll_ctl(epfd,EPOLL_CTL_DEL,tmp_fd,NULL);
close(tmp_fd);
puts(buf);
} else{
//echo
write(tmp_fd,buf,len);
}

}
}
}

条件触发: epoll 的默认行为就是条件触发 (select 也是条件触发);
通过修改代码得结论

先添加一个 BUFF_SIZE
#define BUFF_SIZE 4
把 read(ep_events[i].data.fd,buf,BUFSIZ) 的 BUFSIZ 修改成 BUFF_SIZE
然后 telnet 此服务器 : telnet 127.0.0.1 9988 , 随意写一些数据给服务器先看结果, 这是我这里的输出:
fuck@ubuntu:~/sockettest$ ./epoll_serv
server is running
1 events returned
accepted,client_fd:5,ip:127.0.0.1,port:54404
1 events returned
1 events returned
1 events returned
1 events returned
1 events returned
1 events returned
让 read 最多只能读 4 个字节的唯一原因是 , 证明什么是条件触发
通过结果得到结论: 只要此缓冲区内还有数据, epoll_wait 将不断的返回, 这个就是默认情况下 epoll 的条件触发;

边缘触发: 这个需要先做个实验才能理解, 纯文字估计不太好理解; 唯一需要修改的代码是在 accept 下面的一行:
event.events = EPOLLIN | EPOLLET; // EPOLLET 就是边缘触发
注意 read 那行代码, 最多接受的字节最好在 1~4 之间:read(…,…, 4); 否则效果不明显接着 telnet , 尝试一下每次写给服务器超过 5 个字节
我这里就不贴服务器输出了 , 可以看到这时 epoll_wait 无论怎么样只会返回一次了 ;
先给结论: 只有当客户端写入 (write,send,…) , epoll_wait 才会返回且只返回一次;
注意与条件触发的不同: 条件触发情况下只要接受缓冲区有数据即返回, 边缘触发不会;
由于这种只返回一次的特性 , EPOLLET 一般情况下都将采用非阻塞 O_NONBLOCK 的方式来读取;

正文完
 0