epoll 是在 2.6 内核中提出的,是之前的select和 poll的增强版本。相对于 select和 poll来说,epoll更加灵活,没有描述符限制。epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的 copy 只需一次。

一、epoll函数详解

#include <sys/epoll.h>
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event * events, 
			int maxevents, int timeout);
// 保存触发事件的某个文件描述符相关的数据(与具体使用方式有关)
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 */
};
// events 可以是以下几个宏的集合:
EPOLLIN :表示对应的文件描述符可以读(从缓冲区读数据,包括对端 SOCKET 正常关闭);
EPOLLOUT:表示对应的文件描述符可以写(向缓冲区写数据);
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET :将 EPOLL 设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个 socket 的话,需要再次把这个 socket 加入到 EPOLL 队列里

1、epoll_create()函数

int epoll_create(int size);
  • 功能:该函数生成一个 epoll 专用的文件描述符(创建一个 epoll 的句柄)。
  • 参数
  • size:用来告诉内核这个监听的数目一共有多大,参数 size 并不是限制了 epoll 所能监听的描述符最大个数,只是对内核初始分配内部数据结构的一个建议。自从 linux 2.6.8 之后,size 参数是被忽略的,也就是说可以填只有大于 0 的任意值。需要注意的是,当创建好 epoll 句柄后,它就是会占用一个 fd 值,在 linux 下如果查看 /proc/ 进程 id/fd/,是能够看到这个 fd 的,所以在使用完 epoll 后,必须调用 close() 关闭,否则可能导致 fd 被耗尽。
  • 返回值:成功:epoll 专用的文件描述符;失败:-1。

2、epoll_ctl()函数

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
  • 功能:epoll的事件注册函数,它不同于 select() 是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。
  • 参数
  • epfd:epoll 专用的文件描述符,epoll_create()的返回值 。
  • op:表示动作,用三个宏来表示:
  • EPOLL_CTL_ADD:注册新的 fd 到 epfd 中;
  • EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
  • EPOLL_CTL_DEL:从 epfd 中删除一个 fd。
  • fd:需要监听的文件描述符;
  • event:告诉内核要监听什么事件;
  • 返回值:成功:0,失败:-1。

3、epoll_wait()函数

int epoll_wait( int epfd, struct epoll_event * events, int maxevents, int timeout );
  • 功能:等待事件的产生,收集在 epoll 监控的事件中已经发送的事件,类似于 select调用。
  • 参数
  • epfd:epoll专用的文件描述符,epoll_create()的返回值;
  • events:分配好的 epoll_event 结构体数组,epoll 将会把发生的事件赋值到events 数组中(events 不可以是空指针,内核只负责把数据复制到这个 events 数组中,不会去帮助我们在用户态中分配内存);
  • maxevents:告知内核这个 events 有多大 ;
  • timeout:超时时间,单位为毫秒,为 -1 时,函数为阻塞。
  • 返回值:成功:返回满足监听条件的事件数目,如返回 0 表示已超时;失败:-1。

二、epoll高并发服务器的流程

#include <头文件>

int main(int argc, char const *argv[])
{
	lfd = socket();
	bind();
	listen();
	// 建立epoll模型,efd指向红黑树根节点, OPEN_MAX为监听节点数量(仅供内核参考)
	efd = epoll_create(OPEN_MAX);
	// tep:epoll_ctl参数(只是一个临时变量,用来设置单个fd属性),ep[]:epoll_wait参数
	struct epoll_event tep, ep[OPEN_MAX]; 
	// 指定lfd的监听事件为读事件,文件描述符为lfd
	tep.events = EPOLLIN; 
	tep.data.fd = lfd;
	// 添加lfd到红黑树,通过efd可以找到lfd
	res = epoll_ctl(efd, EPOLL_CTL_ADD, lfd, &tep);
	while(1)
	{
		// nready返回的是满足监听事件的总个数(可作为循环上限),如果为0则表示没有fd满足监听事件
		nready = epoll_wait(efd, ep, OPEN_MAX, -1);
		// 现在无需再轮询,在数组中的元素都是满足监听条件的
		for (int i = 0; i < nready; ++i)
		{
			if (!(ep[i].events & EPOLLIN))
				continue;
			if (ep[i].data.fd == lfd)
			{
				cfd = accept();
				tep.events = EPOLLIN;
				tep.data.fd = cfd;
				// 把已连接的客户端加入红黑树
				res = epoll_ctl(efd, EPOLL_CTL_ADD, cfd, &tep);
			}
			else
			{
				sockfd = ep[i].data.fd;
				recv(sockfd, buf, sizeof(buf), 0);
				int ret = strlen(buf);
				if (ret <= 0)
				{
					// // 删除该节点
					res = epoll_ctl(efd, EPOLL_CTL_DEL, sockfd, NULL);
					close(sockfd);
				}
				else
					/*事务处理*/
			}
		}
	}
	close(lfd);
	return 0;
}

三、epoll高并发服务器的demo

#pragma GCC diagnostic error "-std=c++11"
#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <arpa/inet.h>
#include <sys/epoll.h>
#include <errno.h>
#include <pthread.h>
#include <signal.h>
#include <ctype.h>
#include <vector>
using namespace std;

const int  OPEN_MAX = 5000;

void sys_err(const char *str)
{
    perror(str);
    exit(1);
}

int main(int argc, char **argv)
{
    int i, lfd, cfd, efd, res, sockfd;
    int n, num = 0;
    socklen_t clt_addr_len;
    ssize_t nready;
    char buf[512];

    struct sockaddr_in srv_addr, clt_addr;
    // 将地址结构清零(按字节),容易出错(后面两个参数容易颠倒)
    // memset(&srv_addr, 0, sizeof(srv_addr));
    // bzero也可以用来清零操作 
    bzero(&srv_addr, sizeof(srv_addr));
    srv_addr.sin_family = AF_INET;
    srv_addr.sin_port = htons(8080);
    srv_addr.sin_addr.s_addr = htonl(INADDR_ANY);

    int opt = 1;
    // 设置套接字选项
    setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    // 创建套接字
    lfd = socket(AF_INET, SOCK_STREAM, 0);
    
    // 绑定套接字
    bind(lfd, (struct sockaddr *)&srv_addr, sizeof(srv_addr));
    
    // 监听客户端的连接
    listen(lfd, 128);

    // 建立epoll模型,efd指向红黑树根节点, OPEN_MAX为监听节点数量(仅供内核参考)
    efd = epoll_create(OPEN_MAX);
    if (efd < 0)
        sys_err("epoll_create");

    // tep:epoll_ctl参数(只是一个临时变量,用来设置单个fd属性),ep[]:epoll_wait参数
    struct epoll_event tep, ep[OPEN_MAX]; 
    
    // 指定lfd的监听事件为读事件,文件描述符为lfd
    tep.events = EPOLLIN; 
    tep.data.fd = lfd;

    // 添加lfd到红黑树,通过efd可以找到lfd
    res = epoll_ctl(efd, EPOLL_CTL_ADD, lfd, &tep);
    if (res < 0)
        sys_err("epoll_ctl");

    while (1)
    {
        // epoll为server阻塞监听事件,ep为struct epoll_event 类型数组,OPEN_MAX为数组容量, -1表示阻塞
        // nready返回的是满足监听事件的总个数(可作为循环上限),如果为0则表示没有fd满足监听事件
        nready = epoll_wait(efd, ep, OPEN_MAX, -1);
        if (nready < 0)
            sys_err("epoll_wait");
        // 现在无需再轮询,在数组中的元素都是满足监听条件的
        for (int i = 0; i < nready; ++i) 
        {
            if (!(ep[i].events & EPOLLIN))
                continue;
            
            if (ep[i].data.fd == lfd)
            {
                clt_addr_len = sizeof(clt_addr);
                // 接收客户端的连接
                cfd = accept(lfd, (struct sockaddr *)&clt_addr, &clt_addr_len);
                memset(buf, 0, 512);
                cout << "客户端连接:" << inet_ntop(AF_INET, &clt_addr.sin_addr.s_addr, buf, sizeof(buf)) 
                     << "," << ntohs(clt_addr.sin_port) << endl;
                
                // 把已连接的客户端加入红黑树
                tep.events = EPOLLIN;
                tep.data.fd = cfd;
                res = epoll_ctl(efd, EPOLL_CTL_ADD, cfd, &tep);
                if (res < 0)
                    sys_err("epoll_ctl");
            }
            else // 数据读事件
            {
                sockfd = ep[i].data.fd;
                memset(buf, 0, 512);
                // 接收来自客户端的数据
                recv(sockfd, buf, sizeof(buf), 0);
                int ret = strlen(buf);
                if (ret < 0)
                {
                    // 收到RST标志
                    if (errno == ECONNRESET)
                    {
                        cout << "连接被重置" << endl;
                        // 删除该节点
                        res = epoll_ctl(efd, EPOLL_CTL_DEL, sockfd, NULL);
                        if (res < 0)
                            sys_err("epoll_ctl");
                        close(sockfd);
                    }
                    else
                        sys_err("read");
                }
                // 客户端关闭连接了
                else if (ret == 0)
                {
                    res = epoll_ctl(efd, EPOLL_CTL_DEL, sockfd, NULL);
                    if (res < 0)
                        sys_err("epoll_ctl");
                    close(sockfd);
                    cout << "客户端关闭:" << inet_ntop(AF_INET, &clt_addr.sin_addr.s_addr, buf, sizeof(buf))
                        << "," << ntohs(clt_addr.sin_port) << endl;
                }
                else
                {
                    for (int j = 0; j < ret; ++j)
                        buf[j] = toupper(buf[j]);
                    // 回射到客户端
                    send(sockfd, buf, ret, 0);
                    // 客户端写到标准输出
                    write(STDOUT_FILENO, buf, ret);
                }
            }
        }
    }
    close(lfd);
    return 0;
}

四、epoll高并发服务器总结

在 select/poll中,进程只有在调用一定的方法后,内核才对所有监视的文件描述符进行扫描,而 epoll事先通过 epoll_ctl() 来注册一个文件描述符,一旦基于某个文件描述符就绪时,内核会采用类似 callback 的回调机制(软件中断 ),迅速激活这个文件描述符,当进程调用 epoll_wait() 时便得到通知。

【优点】:

  • 监视的描述符数量不受限制,它所支持的 FD 上限是最大可以打开文件的数目,这个数字一般远大于 1024,举个例子,在 1GB 内存的机器上大约是 10 万左右,具体数目可以 cat /proc/sys/fs/file-max 察看,一般来说这个数目和系统内存关系很大。select的最大缺点就是进程打开的 fd 是有数量限制的。这对于连接数量比较大的服务器来说根本不能满足。虽然也可以选择多进程的解决方案( Apache 就是这样实现的),不过虽然 Linux 上面创建进程的代价比较小,但仍旧是不可忽视的,加上进程间数据同步远比不上线程间同步的高效,所以也不是一种完美的方案;
  • I/O 的效率不会随着监视 fd 的数量的增长而下降。select,poll 实现需要自己不断轮询所有 fd 集合,直到设备就绪,期间可能要睡眠和唤醒多次交替。而 epoll 其实也需要调用 epoll_wait() 不断轮询就绪链表,期间也可能多次睡眠和唤醒交替,但是它是设备就绪时,调用回调函数,把就绪 fd 放入就绪链表中,并唤醒在 epoll_wait() 中进入睡眠的进程。虽然都要睡眠和交替,但是 select和 poll在“醒着”的时候要遍历整个 fd 集合,而 epoll 在“醒着”的时候只要判断一下就绪链表是否为空就行了,这节省了大量的 CPU 时间。这就是回调机制带来的性能提升;
  • select,poll每次调用都要把 fd 集合从用户态往内核态拷贝一次,而 epoll 只要一次拷贝,这也能节省不少的开销。

【缺点】:

  • 不能跨平台。