EPOLL
回顾select、poll机制:
- select与poll本质上都是监控描述符集内的事件,返回就绪态的文件描述符个数,同个文件描述符个数去遍历集合,找出是具体哪一个文件描述符有事件发生。
- poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态, 但是它没有最大连接数的限制,原因是它是基于链表来存储的.
- 他们的时间复杂度均为O(n)
epoll的引入
目前epell是linux大规模并发网络程序中的热门首选模型。
epoll是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率,因为它会复用文件描述符集合来传递结果而不用迫使开发者每次等待事件之前都必须重新准备要被侦听的文件描述符集合,另一点原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了。
epoll除了提供select/ poll那种IO事件的电平触发(Level Triggered)外,还提供了边沿触发(Edge Triggered),这就使得用户空间程序有可能缓存IO状态,减少epoll_wait/epoll_pwait的调用,提高应用程序效率。
select() 和 poll() 在“醒着”的时候要遍历整个 fd 集合,而 epoll 在“醒着”的时候只要判断一下就绪链表是否为空就行了,这节省了大量的 CPU 时间。这就是回调机制带来的性能提升,时间复杂度为O(1)。
扩展
- 一个进程打开最大的文件描述符数量可用如下命令查看(一般为40W):
cat /proc/sys/fs/file-max
该数量取决于内存的大小,因为每个文件描述符在内核中都会开辟对应一个FILE结构体。 - 以上为最大值,而系统启动时会调用一个API对文件描述符数量进行设置,对应配置文件为:/etc/security/limits.conf
用户可写入一下配置,soft软限制,hard硬限制。- nofile - 对应max number of open files,soft为用户自定义值,hard为用户最大定义值。
多路复用的三种机制区别可见总结篇
https://blog.csdn.net/weixin_42889383/article/details/102460106
EPOLL机制
epoll不在是通过将文件描述符保存在数组中,而是用树来管理的,用struct epoll_event 描述符封装了文件描述符以及对应的事件状态。例如当有客户连接时,初始化描述符并将其插入树中。
用树的特性的好处是:插入删除简单、遍历查找快。
API
- 创建一个epoll句柄,参数size用来告诉内核监听的文件描述符个数
int epoll_create(int size)
size:告诉内核监听的数目
- 控制某个epoll监控的文件描述符上的事件:注册、修改、删除。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
参数1epfd:为epoll_creat的句柄
参数2op:表示动作,用3个宏来表示:
EPOLL_CTL_ADD(注册新的fd到epfd),
EPOLL_CTL_MOD(监听fd的模式,LT或ET)
1. 对于LT为水平触发(缺省方式),同时支持block和no-block,例如监控的文件描述符集中有读事件发生,如果读数据时没有对缓冲区一次性读完,那么内核将继续在通知你去读,这种编程出错可能性小,传统的select/poll都是这种模型的代表。
2. 对于ET为边沿触发(高速工作方式),只支持no-block,如果有读事件发生,一次性未读完,那些内核将不会在通知,导致该任务变为未就绪态。
EPOLL_CTL_DEL(从epfd删除一个fd);
参数3fd:指定 fd
参数4event:告诉内核需要监听的事件
struct epoll_event {
__uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
事件的状态:
EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭)
EPOLLOUT:表示对应的文件描述符可以写
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来)
EPOLLERR:表示对应的文件描述符发生错误
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来
说的
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需
要再次把这个socket加入到EPOLL队列里
- 等待所监控文件描述符上有事件的产生,类似于select()调用。
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout)
events:用来从内核得到事件的集合,
maxevents:告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size,
timeout:是超时时间
-1:阻塞
0:立即返回,非阻塞
>0:指定微秒
返回值:成功返回有多少文件描述符就绪,时间到时返回0,出错返回-1
EPOLL用法
阻塞模式下与select、poll相似,epoll一般用于非阻塞状态下,并且操作客户端为边沿模式。即对一个事件发送的只处理一次,不管有没有处理完。
附上代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <sys/epoll.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include "wrap.h"
#define SERV_PORT 8000
#define MAX_LOG 20
#define MAX_EPOLL 500
#define MAX_LINE 100
int mz_ipv4_create_socket(void)
{
int listenfd;
struct sockaddr_in servaddr;
//1.socket
listenfd = Socket( AF_INET, SOCK_STREAM, 0);
//2.修改套接字选项,调整2MSL时间
int b_reuse = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &b_reuse, sizeof(int));
//3.绑定IP和服务器端口号
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
Bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
//4.设置监听
Listen(listenfd, MAX_LOG);
//5.返回套接字文件描述符
return listenfd;
}
int msg_handle(int sockfd)
{
int bytes,len = 0;
char buf[MAX_LINE];
char *s = buf;
int flag = 1;
while(flag){
bytes = recv(sockfd, s, 5, 0);
if(bytes < 0){
if(errno == EAGAIN){
printf("no data\n");
break;
}
perror("recv err");
return -1;
}
s += bytes;
len += bytes;
if(buf[len-1] == '\n')
flag = 0;
}
if(Write(sockfd, buf, len)<0)
perror("send data err");
return 0;
}
int main(int argc, char *argv[])
{
int listenfd, epollfd,nready,sockfd;
int i = 0,ret;
struct sockaddr_in clientaddr;
socklen_t clientaddr_len;
struct epoll_event ev, events[MAX_EPOLL];
//1.创建epoll模型
epollfd = epoll_create(MAX_EPOLL);
if(epollfd < 0)
perr_exit("epoll_create");
//2.创建套接字并绑定IP与端口
listenfd = mz_ipv4_create_socket();
fcntl( listenfd, F_SETFL, O_NONBLOCK);
//3.将服务器网络进程加入epoll树监控
ev.data.fd = listenfd;
ev.events = EPOLLIN;
ret = epoll_ctl(epollfd, EPOLL_CTL_ADD, listenfd, &ev);
if(ret < 0)
perr_exit("the server insert error");
//4.进入轮询监控
while(1){
nready = epoll_wait(epollfd, events, MAX_EPOLL, -1);
if(nready < 0)
perr_exit("epoll_wait");
for( i = 0; i < nready; i++){
//4.1服务器读事件(连接客户端)
if(events[i].data.fd == listenfd){
clientaddr_len = sizeof(clientaddr);
sockfd = Accept(listenfd, (struct sockaddr *)&clientaddr, &clientaddr_len);
ev.data.fd = sockfd;
ev.events = EPOLLIN | EPOLLET;
ret = epoll_ctl(epollfd, EPOLL_CTL_ADD, sockfd, &ev);
printf("client[%d] connect success!\n",i);
if(ret < 0)
if(send(sockfd, "connect failed\n", 16, 0)<0)
perror("send client err failed");
continue;
}else{
sockfd = events[i].data.fd;
ret = msg_handle(sockfd);
if(ret == -2){
ret = epoll_ctl(epollfd, EPOLL_CTL_DEL, sockfd, NULL);
printf("client[%d] close!\n",i);
Close(sockfd);
}
continue;
}
}
}
return 0;
}