epoll

epoll

本质上红黑树 :节点多或少不影响epoll的效率。

epoll 使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的 copy 只需一次。epoll可以理解为event poll,不同于忙轮询和无差别轮询,epoll会把哪个流发生了怎样的I/O事件通知我们。所以我们说epoll实际上是 事件驱动(每个事件关联上fd)的,此时我们对这些流的操作都是有意义的。(复杂度降低到了O(1))

优点
1、select和poll工作过程中存在内核/用户空间数据的频繁拷贝问题,在epoll中内核和用户区使用的是共享内存(基于mmap内存映射区实现),省去了不必要的内存拷贝。
2、程序猿需要对select和poll返回的集合进行判断才能知道哪些文件描述符是就绪的,通过epoll可以 直接得到已就绪的文件描述符集合 ,无需再次检测。
3、使用 epoll 没有最大文件描述符的限制,仅受系统中进程能打开的最大文件数目限制。


epoll的操作函数

#include <sys/epoll.h>
// 创建epoll实例,通过一棵红黑树管理待检测集合
int epoll_create(int size);
// 管理红黑树上的文件描述符(添加、修改、删除)
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
// 检测epoll树中是否有就绪的文件描述符
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

任务的添加和检测是拆分开的。

epoll_create()

int epoll_create(int size);

epoll_create() 函数的作用是创建一个红黑树模型的实例,用于管理待检测的文件描述符的集合。
失败:返回 - 1
成功:返回一个有效的文件描述符,通过这个文件描述符就可以访问创建的 epoll 实例


epoll_ctl

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

它完成的工作就是把fd和epoll_event 结构体一并记录在epoll树上;

  • epfd:epoll树的实例,epoll_create()的返回值

  • op:对epfd的操作
    EPOLL_CTL_ADD:往 epoll 模型中添加新的节点
    EPOLL_CTL_MOD:修改 epoll 模型中已经存在的节点
    EPOLL_CTL_DEL:删除 epoll 模型中的指定的节点

  • fd:文件描述符,即要添加 / 修改 / 删除的文件描述符

  • event:epoll 事件,用来修饰第三个参数对应的文件描述符的,指定检测这个文件描述符的什么事件;
    EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
    EPOLLOUT:表示对应的文件描述符可以写;
    EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
    EPOLLERR:表示对应的文件描述符发生错误;
    EPOLLHUP:表示对应的文件描述符被挂断;
    EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
    EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里

data:用户数据变量,需要自己初始化,这是一个联合体类型,通常情况下使用里边的 fd 成员,用于存储待检测的文件描述符的值,在调用 epoll_wait() 函数的时候这个值会被传出。

/ 联合体, 多个变量共用同一块内存        
typedef union epoll_data {
 	void        *ptr;
	int          fd;	// 通常情况下使用这个成员, 和epoll_ctl的第三个参数相同即可
	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_data_t data:用户数据,对内核没有作用;

每一个epoll实例都对应一个文件描述符fd,他们之间的对应关系通过epoll里面的data实现的;而events表示这个文件描述符fd需要被检测什么事件。

当把epoll_event传递给内核,内核检测到这个文件描述符对应的事件触发了,会修改并传出这个epoll_event(通过epoll_wait的传参是采用指针实现的);这个epoll_event里面的对应的data数据就是我们传进去而指定的数据。
一般data使用int fd;相当于这个data是储存的就是对这个事件的备注信息:这个事件属于哪一个文件描述符;

当数据较多的时候,可以创建一块堆内存,由ptr指向data。


epoll_wait

epoll_wait() 检测所有添加到epoll树上的节点是否已经处于就绪状态;如果有就绪状态的fd,它就会返回有哪些fd变成就绪

如果没有会一直保持阻塞。

int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
  • epfd:epoll_create () 函数的返回值,通过这个参数找到 epoll 实例

  • events:传出参数;events则是分配好的 epoll_event结构体数组,epoll将会把发生的事件 复制(所以这些被复制的epoll_event是之前就写好的) 到 events数组中(events不可以是空指针,内核只负责把数据复制到这个 events数组中,不会去帮助我们在用户态中分配内存;内核这种做法效率很高)。
    所以我们可以通过读epoll_event 中的data知道这个事件对应的是哪一个文件描述符。
    返回值等于数组的大小。

  • maxevents:修饰第二个参数,结构体数组的容量(元素个数)他是一个数组的首地址

  • timeout:实例中没有已就绪的文件描述符,该函数阻塞的时长,单位 ms 毫秒
         0:函数不阻塞,不管 epoll 实例中有没有就绪的文件描述符,函数被调用后都直接返回;
         大于 0:如果 epoll 实例中没有已就绪的文件描述符,函数阻塞对应的毫秒数再返回;
         -1:函数一直阻塞,直到 epoll 实例中有已就绪的文件描述符之后才解除阻塞

  • 函数返回值:
         成功:
             等于 0:函数是阻塞被强制解除了,没有检测到满足条件的文件描述符;
             大于 0:检测到的已就绪的文件描述符的总个数,且第二个参数有若干有效的元素,且等于总个数。
         失败:返回 - 1


epoll的使用

操作步骤

1、设置服务器性质:设置套接字、端口复用、绑定;

int lfd = socket(AF_INET, SOCK_STREAM, 0);

int opt = 1;
setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

int ret = bind(lfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));

2、注册监听用lfd

listen(lfd, 128);

3、创建epoll树

int epfd = epoll_create(100);

epoll的实例是一个int型数据,也是一个文件描述符。

4、将用于监听的套接字添加到epoll实例中
epoll_event :epoll的实例;
epfd :epoll树。

struct epoll_event ev;
ev.events = EPOLLIN;    // 检测lfd读读缓冲区是否有数据
ev.data.fd = lfd;
int ret = epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);

ev.data.fd = lfd:每一个文件描述符都需要一个epoll_event 去表述它,所以给每一个fd创建一个epoll_event 实例并完成初始化;
ev.events = EPOLLIN:初始化决定对这个文件描述符需要监视它的什么事件。 ;
注意:ev.data.fd的值要和第四个参数文件描述符保持一致。

检测添加到epoll实例中的文件描述符是否已就绪,并将这些已就绪的文件描述符进行处理

5、创建epoll实例的数组,这个数组作为参数传入内核,经过修改再传出;这个数组的大小等于被检测出的就绪文件描述符个数。

epoll_ctl会把编写好的epoll_event放入epoll树中,以供之后提取;
epoll_wait会把就绪的epoll_event提取出来,放入数组中。

struct epoll_event evs[1024];
int size = sizeof(evs) / sizeof(struct epoll_event);

需要先创建一个 struct epoll_event evs[1024]用于存放epoll实例;由于每一个epoll_event 都存放有对应的fd,可以通过fd的对比来判断他是不是lfd。

6、进入while循环,使用epoll_wait进行检测;根据检测结果进行其他处理。


建立新的链接

 while(1)
    {
    	//epoll_wait检测文件描述符状态;timeout设置为-1(不阻塞);
    	int num = epoll_wait(epfd, evs, size, -1);
        for(int i=0; i<num; ++i)
        {
            // 取出当前的文件描述符
            int curfd = evs[i].data.fd;

epoll_wait返回最大的文件描述符+1;
被检测到数据将被存放在数组evs[]下;我们使用一个 中间量int curfd = evs[i].data.fd来依次遍历整个epoll_event数组

            // 判断这个文件描述符是不是用于监听的
            if(curfd == lfd)
            {
                // 建立新的连接,得到新连接的cfd;
                int cfd = accept(curfd, NULL, NULL);
                // 给新cfd创建对应的epoll_event,并添加到epoll模型中, 下一轮循环的时候就可以被检测了
                ev.events = EPOLLIN;    // 设置事件类型
                ev.data.fd = cfd;		// 设置fd

				//加入epoll实例中
                ret = epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);
                if(ret == -1)
                {
                    perror("epoll_ctl-accept");
                    exit(0);
                }
            }

struct epoll_event ev;ev是早早被定义可以重复使用;
我们只需要把epoll_event加入epoll实例中就可以了,这是由于拷贝进入epoll实例中;所以即使我们使用同一个ev往epoll实例中加入,但依旧可以构件这个树。

读和取

     		else
            {
                // 处理通信的文件描述符
                // 接收数据
                char buf[1024];
                memset(buf, 0, sizeof(buf));
                int len = recv(curfd, buf, sizeof(buf), 0);
                if(len == 0)
                {
                    printf("客户端已经断开了连接\n");
                    // 数据读完了,这里认为是客户端断开连接,所以紧接着就close了
                    epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);
                    close(curfd);
                    //也可以选择暂时无数据
                }
                else if(len > 0)
                {
                    printf("客户端say: %s\n", buf);
                    send(curfd, buf, len, 0);
                }
                else
                {
                    perror("recv");
                    exit(0);
                } 
            }

memset(buf, 0, sizeof(buf)): 由于每一次读的长度不一样长,如果每一次buf不置为0很有可能导致二次读的和第一次读的混在一起;这是由于第二次读是从头开始覆盖第一次的值;
关于 epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL):
在这里可能存在两种状态:
1、服务器真的断开连接了,那我们就应该关闭该连接的文件描述符;
2、服务器暂时不来数据了;我们不关闭fd,让它人在epoll树中,等待下一次检测。
但是我们不通过判断的话并不清楚是哪一种情况,索性直接关闭连接并且从epoll数中剔除,下次再来请重新连接。


epoll的工作模式

工作模式是针对每一个文件描述符而言的;
修改是通过修改epoll_event:

	struct epoll_evebt ev;
	ev.events = EPOLLIN | EPOLLET;
	ev.data.fd = fd;

水平模式(默认工作模式)

如果有一个文件描述符就绪,如果不对它进行处理,每次处理时epoll都会对用户进行通知。

特点:以读写事件为例

读事件

1、当读事件就绪时,如果一次性读完,那么缓存区里就没有数据了,下一次检测时不会再通知;因为该文件描述符从就绪状态转为未就绪状态(缓存区无数据)。
如果没有读完,下一次检测仍然会通知;只要读缓存区有数据就是就绪状态。

写事件

1、实际上epoll检测的写缓存区是否可写,如果文件描述符对应的空间都被写满那就是未就绪状态
2、一般情况下写缓冲区都是有空间的,所以可以不用检测直接往里面写

边沿模式

如果有一个文件描述符就绪,有且只会通知一次。所以占用资源少,但用起来复杂。

读事件

1、如果缓冲区读了一半,但下一次检测不会通知的,这个时候epoll_wait处于阻塞状态;当下一条信息来到是才会再次通知。

2、如何保证读完了呢?
使用while循环去read,但是read时阻塞的,当读完所有数据就会阻塞进程。
所以要修改为非阻塞的;读完了缓存区读不到数据返回-1,退出循环。

决定是否阻塞时取决于文件描述符,而不是read函数。

//得到fd的flags属性
int flag = fcntl(cfd, F_GETFL);
//添加新属性
flag |= O_NONBLOCK;                                                        
fcntl(cfd, F_SETFL, flag);
int len = 0;

while((len = recv(cfd, buf, sizeof(buf), 0)) > 0)
{
    // 数据处理...
}

实例修改
注意需要 在添加入epoll树之前完成修改

         if(curfd == lfd)
            {
                // 建立新的连接,得到新连接的cfd;
                int cfd = accept(curfd, NULL, NULL);
                // 给新cfd创建对应的epoll_event,并添加到epoll模型中, 下一轮循环的时候就可以被检测了
                ev.events = EPOLLIN | EPOLLET;    // 设置事件类型
                ev.data.fd = cfd;		// 设置fd

				//修改cfd的属性
				int flag = fcntl(cfd, F_GETFL);
				flag |= O_NONBLOCK;                                                        
				fcntl(cfd, F_SETFL, flag);
				
				//将修改后的cfd加入epoll实例中
                ret = epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);
                .........
                
读取数据
		while(1){
				int num = epoll_wait(epfd, evs, size, -1);
				for(int i = 0;i < num; i++)
				{
					if(evs[i].data.fd = lfd)
						........
					else
					{	
						while(1)
						{
							int len = recv(cfd, buf, sizeof(buf), 0);
							if(len == -1)
							{
								if(errno = EAGAIN) //头文件error.h
								{
									printf("数据读取完毕");
                                    epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);
                                    close(curfd);
                               	    break;
								}
								else
								{
									perror("recv error");
									exit(1);
								}
							}
						}
					}
				}
		        }

写事件

1、检测写缓冲区是否可写,如果时可写的,epoll在检测时可写会通知一次;如果下一次还可写就不会通知;直到写缓冲区被写满了,且写缓冲区又能写了,这时候才会通知一次。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值