linux学习笔记14 epoll函数

epoll函数的原理和select函数类似,但是select是创建了一个文件描述符表,而epoll函数是创建了一个树用来存放文件描述符和需要检测的状态,并且在返回时不仅仅返回需要处理的文件描述符个数,还可以返回所有的文件描述符。

epoll接口总共3个:

int epoll_create(int size);//该函数生成一个专用的文件描述符,也就是epoll的根节点

int size:epoll树能存储的最大描述符个数,如果实际应用中超过可以自动扩大,所以这个参数意义不大。

返回值是根节点的文件描述符epfd。

 

 int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
//该函数对epoll树进行添加,删除,更改操作

int epfd:就是create创建的根节点

int op:包括以下三个宏

EPOLL_CTL_ADD:注册新的fd到epfd中;
EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
EPOLL_CTL_DEL:从epfd中删除一个fd;(使用删除宏,最后一个参数event可以填NULL)

int fd:要处理的文件描述符

struct epoll_event *event:就是将这个结构体挂到epoll树上

struct epoll_event该结构体就是构成epoll的基本单元

typedef union epoll_data {
     void *ptr;
     int fd;
     __uint32_t u32;
     __uint64_t u64;
} epoll_data_t;//联合体,就是这几个参数公用一块内存,每次定义只能定义联合体中的一个成员,常用fd

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中

//events和select函数中的fd_set *readfds, fd_set *writefds, fd_set *exceptfds三个函数作用类似

事件宏更通俗的解释如下:

EPOLLIN : 表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT: 表示对应的文件描述符可以写;
EPOLLPRI: 表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR: 表示对应的文件描述符发生错误;                                                                                                        EPOLLHUP: 表示对应的文件描述符被挂断;注意:这个事件是默认会关注的,不需要你特意加入epoll队列,所有加入epoll的文件描述符都会关注这个事件。一般这个事件发生在管道或者socket通信,表示对端关闭了自己这边,比如客户端使用了shutdown(sockfd, SHUT_WR),关闭自己的写端,就会触发服务端的EPOLLHUP事件。
EPOLLET: 将 EPOLL设为边缘触发(Edge Triggered)模式(默认为水平触发),这是相对于水平触发(Level Triggered)来说的。
EPOLLONESHOT: 只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里

返回值:成功操作就返回0,错误返回-1

 

 int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
//等待事件的产生,类似于select函数

int epfd:就是create创建的根节点

struct epoll_event * events:这边应该传入epoll_event结构体的数组,用来传出需要处理的文件描述符

int maxevents:告诉内核events数组的大小

int timeout:超时时间(单位毫秒,0会立即返回,-1是阻塞等待,大于0是阻塞该数值时间)。

 

个人理解

epoll总共三个函数如下:

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);

epoll的底层是由红黑树实现的,所以epoll_create函数是创造一个树,并返回根节点,也就是efd文件描述符。epoll_ctl是对这棵树的操作,可以实现增加节点,删除节点,修改节点等操作。而epoll_wait则是监视树上的节点,是否发生对应需要监视的事件,如果发生了,就返回。

这里重点讲解一下增加节点的过程。当想要增加节点时,使用epoll_ctl函数,并且op填写EPOLL_CTL_ADD宏,fd就是要监视的文件描述符,最重要的是epoll_event结构体,注意一个文件描述符对应一个epoll_event结构体。这个结构体有两个成员,events表示需要epoll监视的事件,并且可以多选。意思就是如果当对应的文件描述符发生events中的事件时,epoll_wait会解除阻塞,并返回该文件描述符对应的epoll_event结构体。epoll_event结构体的另一个成员data是一个联合体,这个data对于epoll函数并没有作用,只是当epoll_wait得到活跃文件描述符时,就会得到文件描述符对应的epoll_event,根据data做相关操作。

现假设已经添加一个节点,然后使用epoll_wait后成功返回了,这时,epoll_event数组中就是一系列监听到的活跃的文件描述符对应的epoll_event结构体。活跃的文件描述符就是指在增加节点时,让epoll关注的事件发生了。在这些结构体中,events成员不再是当时增加节点的events了,而是本次epoll_wait被监听到的事件。

 

注意:写事件和读事件触发的条件

水平触发

1. 对于读操作
只要内核缓冲区内容不为空,LT模式返回读就绪。

2. 对于写操作
只要内核缓冲区还不满,LT模式会返回写就绪。

边缘触发

1. 对于读操作
(1)当内核缓冲区由不可读变为可读的时候,即内核缓冲区由空变为不空的时候。

(2)当有新数据到达时,即内核缓冲区中的待读数据变多的时候

(3)当内核缓冲区有数据可读,且应用进程对相应的描述符进行EPOLL_CTL_MOD 修改EPOLLIN事件时。

2. 对于写操作
(1)当内核缓冲区由不可写变为可写时。

(2)当有旧数据被发送走,即内核缓冲区中的内容变少的时候。

(3)当内核缓冲区有空间可写,且应用进程对相应的描述符进行EPOLL_CTL_MOD 修改EPOLLOUT事件时。

 

#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <ctype.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/epoll.h>

using namespace std;


int main(int argc, const char *argv[])
{
	if (argc<2)
	{
		cout<<"input :./a.out port";
		exit(1);	
	}

	int sfd, cfd;
	int port=atoi(argv[1]);

	char buf[256];
	int n;

	struct sockaddr_in addr_server;
	addr_server.sin_port=htons(port);
	addr_server.sin_addr.s_addr=htonl(INADDR_ANY);
	addr_server.sin_family=AF_INET;

	struct sockaddr_in addr_client;
	socklen_t addrlen=sizeof(addr_client);

	sfd=socket(AF_INET, SOCK_STREAM, 0);
	bind(sfd, (struct sockaddr*) &addr_server, sizeof(addr_server));
	listen(sfd, 20);
	cout<<"start to accept......\n";
	
	struct epoll_event all[300];//创建epoll_event数组,作为epoll_wait的参数
	struct epoll_event ev;//作为epoll_ctl的参数
	int epfd=epoll_create(300);//创建一个根节点文件描述符是epfd,最大存放量为300的epoll树
	ev.events=EPOLLIN;
	ev.data.fd=sfd;
	epoll_ctl(epfd, EPOLL_CTL_ADD, sfd, &ev);//将用于接收客户端的文件描述符存入树种

	while (1)
	{
		int sol=epoll_wait(epfd, all, sizeof(all)/sizeof(all[0]), -1);//阻塞等待事件发生
		for (int i=0; i<sol; i++)//sol是事件发生个数,具体的文件描述符存储在all数组中每个结构体的fd中
		{
			int fd=all[i].data.fd;
			if (fd==sfd)//如果是有客户端需要加入
			{
				cfd=accept(sfd, (struct sockaddr*) &addr_client, &addrlen);
				if (cfd==-1)
				{
					cout<<"accept is error!";
					exit(1);
				}
				ev.events=EPOLLIN;
				ev.data.fd=cfd;
				epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);//把新的文件描述符挂到树上

				char ip[64];
				cout<<"the client ip is"<<inet_ntop(AF_INET, &addr_client.sin_addr.s_addr, ip, sizeof(ip))<<endl;
				cout<<"the client port is"<<ntohs(addr_client.sin_port)<<endl;
			}
			else
			{
				if(!all[i].events&EPOLLIN) continue;//如果不是监督读状态,就省略
				int n=read(fd, buf, sizeof(buf));
				if (n==0)
				{
					close(fd);
					epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);//有客户端断开链接了,就删去文件描述符
				}else if (n==-1)
				{
					cout<<"read is error!";
					exit(1);
				}else
				{
					for (int i=0; i<n; i++)
						buf[i]=toupper(buf[i]);
					cout<<"receive: "<<buf<<endl;
					write(fd, buf, n);
				}
			}
		}
	}
	close(sfd);
	return 0;
}

epoll的边沿非阻塞触发模式

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <ctype.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <errno.h>

int main(int argc, const char* argv[])
{
    if(argc < 2)
    {
        printf("eg: ./a.out port\n");
        exit(1);
    }
    struct sockaddr_in serv_addr;
    socklen_t serv_len = sizeof(serv_addr);
    int port = atoi(argv[1]);

    // 创建套接字
    int lfd = socket(AF_INET, SOCK_STREAM, 0);
    // 初始化服务器 sockaddr_in 
    memset(&serv_addr, 0, serv_len);
    serv_addr.sin_family = AF_INET;                   // 地址族 
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);    // 监听本机所有的IP
    serv_addr.sin_port = htons(port);            // 设置端口 
    // 绑定IP和端口
    bind(lfd, (struct sockaddr*)&serv_addr, serv_len);

    // 设置同时监听的最大个数
    listen(lfd, 36);
    printf("Start accept ......\n");

    struct sockaddr_in client_addr;
    socklen_t cli_len = sizeof(client_addr);

    // 创建epoll树根节点
    int epfd = epoll_create(2000);
    // 初始化epoll树
    struct epoll_event ev;

    // 设置边沿触发
    ev.events = EPOLLIN;
    ev.data.fd = lfd;
    epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);

    struct epoll_event all[2000];
    while(1)
    {
        // 使用epoll通知内核fd 文件IO检测
        int ret = epoll_wait(epfd, all, sizeof(all)/sizeof(all[0]), -1);
        printf("================== epoll_wait =============\n");

        // 遍历all数组中的前ret个元素
        for(int i=0; i<ret; ++i)
        {
            int fd = all[i].data.fd;
            // 判断是否有新连接
            if(fd == lfd)
            {
                // 接受连接请求
                int cfd = accept(lfd, (struct sockaddr*)&client_addr, &cli_len);
                if(cfd == -1)
                {
                    perror("accept error");
                    exit(1);
                }
                // 设置文件cfd为非阻塞模式
                int flag = fcntl(cfd, F_GETFL);
                flag |= O_NONBLOCK;
                fcntl(cfd, F_SETFL, flag);

                // 将新得到的cfd挂到树上
                struct epoll_event temp;
                // 设置边沿触发
                temp.events = EPOLLIN | EPOLLET;
                temp.data.fd = cfd;
                epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &temp);
                
                // 打印客户端信息
                char ip[64] = {0};
                printf("New Client IP: %s, Port: %d\n",
                    inet_ntop(AF_INET, &client_addr.sin_addr.s_addr, ip, sizeof(ip)),
                    ntohs(client_addr.sin_port));
                
            }
            else
            {
                // 处理已经连接的客户端发送过来的数据
                if(!all[i].events & EPOLLIN) 
                {
                    continue;
                }

                // 读数据
                char buf[5] = {0};
                int len;
                // 循环读数据
                while( (len = recv(fd, buf, sizeof(buf), 0)) > 0 )
                {
                    // 数据打印到终端
                    write(STDOUT_FILENO, buf, len);
                    // 发送给客户端
                    send(fd, buf, len, 0);
                }
                if(len == 0)
                {
                    printf("客户端断开了连接\n");
                    ret = epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
                    if(ret == -1)
                    {
                        perror("epoll_ctl - del error");
                        exit(1);
                    }
                    close(fd);
                }
                else if(len == -1)
                {
                    if(errno == EAGAIN)
                    {
                        printf("缓冲区数据已经读完\n");
                    }
                    else
                    {
                        printf("recv error----\n");
                        exit(1);
                    }
                }
            }
        }
    }

    close(lfd);
    return 0;
}

其他:

在看muduo库时,我很好奇在客户端的文件描述符被关闭以后,epoll队列中会发生什么事件,网上众说纷纭,有的说会触发EPOLLIN和EPOLLRDHUP事件,有的说会触发EPOLLIN和EPOLLRDHUP,那到底触发什么呢。用下面的例子来测试

服务端程序:

#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <ctype.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/epoll.h>
 
using namespace std;
 
 
int main(int argc, const char *argv[])
{
	if (argc<2)
	{
		cout<<"input :./a.out port";
		exit(1);	
	}
 
	int sfd, cfd;
	int port=atoi(argv[1]);
 
	char buf[256];
	int n;
	int count=0;
 
	struct sockaddr_in addr_server;
	addr_server.sin_port=htons(port);
	addr_server.sin_addr.s_addr=htonl(INADDR_ANY);
	addr_server.sin_family=AF_INET;
 
	struct sockaddr_in addr_client;
	socklen_t addrlen=sizeof(addr_client);
 
	sfd=socket(AF_INET, SOCK_STREAM, 0);
	bind(sfd, (struct sockaddr*) &addr_server, sizeof(addr_server));
	listen(sfd, 20);
	cout<<"start to accept......\n";
	
	struct epoll_event all[300];//创建epoll_event数组,作为epoll_wait的参数
	struct epoll_event ev;//作为epoll_ctl的参数
	int epfd=epoll_create(300);//创建一个根节点文件描述符是epfd,最大存放量为300的epoll树
	ev.events=EPOLLIN;
	ev.data.fd=sfd;
	epoll_ctl(epfd, EPOLL_CTL_ADD, sfd, &ev);//将用于接收客户端的文件描述符存入树种
 
	while (1)
	{
		count++;
		int sol=epoll_wait(epfd, all, sizeof(all)/sizeof(all[0]), -1);//阻塞等待事件发生
		for (int i=0; i<sol; i++)//sol是事件发生个数,具体的文件描述符存储在all数组中每个结构体的fd中
		{
			int fd=all[i].data.fd;
			if (fd==sfd)//如果是有客户端需要加入
			{
				cfd=accept(sfd, (struct sockaddr*) &addr_client, &addrlen);
				if (cfd==-1)
				{
					cout<<"accept is error!";
					exit(1);
				}
				ev.events=EPOLLIN|EPOLLHUP|EPOLLRDHUP;//关注这三个事件
				//注释2:ev.events=EPOLLIN|EPOLLHUP;
				ev.data.fd=cfd;
				epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);//把新的文件描述符挂到树上
 
				char ip[64];
				cout<<"the client ip is"<<inet_ntop(AF_INET, &addr_client.sin_addr.s_addr, ip, sizeof(ip))<<endl;
				cout<<"the client port is"<<ntohs(addr_client.sin_port)<<endl;
			}
			else
			{
				cout<<"--------event:"<<all[i].events<<endl;
				if(all[i].events&EPOLLIN) cout<<"---EPOLLIN--\n";
				if(all[i].events&EPOLLHUP) cout<<"---EPOLLHUP--\n";
				if(all[i].events&EPOLLRDHUP) cout<<"---EPOLLRDHUP--\n";
				int n=read(fd, buf, sizeof(buf));
				if (n==0)
				{
					cout<<"close";
					close(fd);
					epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);//有客户端断开链接了,就删去文件描述符
				}else if (n==-1)
				{
					cout<<"read is error!";
					exit(1);
				}else
				{
					for (int i=0; i<n; i++)
						buf[i]=toupper(buf[i]);
					cout<<"receive: "<<buf<<endl;
					write(fd, buf, n);
				}
			}
			cout<<"轮次:"<<count<<endl;
		}
	}
	close(sfd);
	return 0;
}

客户端程序:

int main(int argc, const char* argv[])
{
	if (argc<2)//需要传入端口号
	{
		cout<<"input ./a.out port"<<endl;
		exit(1);
	}
	int sfd;//客户端只需要一个负责读写的文字描述符就够了
	int port=atoi(argv[1]);//传入的是char*类型的端口号,转换成int型
        /*定义sockaddr_in结构体*/
	struct sockaddr_in addr;
	addr.sin_family=AF_INET;
	addr.sin_port=htons(port);
	inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr.s_addr);
//将ip转换成int型存入sockaddr_in结构体中
	
	socklen_t addrlen;
	sfd=socket(AF_INET, SOCK_STREAM, 0);
	connect(sfd, (struct sockaddr*) &addr, sizeof(addr));
	char buf[1024];
	fgets(buf, sizeof(buf), stdin);//从终端读取字符串
	write(sfd, buf, strlen(buf));
	int n=read(sfd, buf, sizeof(buf));
	if (n==-1)
	{
		cout<<"error"<<endl;
		exit(1);
	}
	else if (n>0)
			cout<<buf;
	close(sfd);
    //注释2:shutdown(sfd, SHUT_WR);
}

最后显示结果是:

是触发事件EPOLLIN和EPOLLRDHUP

然后网上也有传言说如果客户端使用shutdown关闭写端,就会触发EPOLLHUP事件,我使用注释2的地方重新测试了:,结果是

并没有EPOLLHUP事件发生,个人理解EPOLLHUP事件是在服务端如果出现关闭等事件,才会出现吧,EPOLLHUP事件发生的环境有待于发掘。

客户端的输入都是一样的如下图所示:

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: `epoll` 函数Linux 系统中一个用于处理大量并发连接的 I/O 多路复用机制。它通过维护一个文件描述符集合来监测多个描述符的状态,以便在发生 I/O 事件时快速通知程序。 使用 `epoll` 的基本流程如下: 1. 创建 `epoll` 句柄:使用 `epoll_create` 或 `epoll_create1` 函数创建一个 `epoll` 句柄。 2. 注册文件描述符:使用 `epoll_ctl` 函数向 `epoll` 句柄中添加需要监测的文件描述符,并为每个文件描述符设置监测事件。 3. 进行等待:使用 `epoll_wait` 函数阻塞等待,直到有一个或多个文件描述符准备就绪。 4. 处理事件:当有文件描述符就绪时,处理相应的 I/O 事件。 5. 重复步骤 3 和 4,不断监测文件描述符的状态,处理 I/O 事件。 常见的使用方式是:创建一个线程,在线程中调用 `epoll_wait` 函数,当有文件描述符就绪时通过回调函数处理相应的 I/O 事件。 ### 回答2: epoll函数Linux系统中一种高效的I/O事件通知机制,用于管理大量的文件描述符。其使用方式如下: 1. 创建一个epoll句柄: int epoll_create(int size); 创建一个epoll实例,并返回一个文件描述符,size表示期望监听的文件描述符数量,通常可以设置为任意正整数。 2. 注册文件描述符和事件: int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); epfd为epoll实例的文件描述符,op为操作类型(EPOLL_CTL_ADD表示添加,EPOLL_CTL_MOD表示修改,EPOLL_CTL_DEL表示删除),fd为需要监听的文件描述符,event为事件类型结构体指针。 3. 开始监听文件描述符事件: int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); epfd为epoll实例的文件描述符,events为存储事件的数组,maxevents表示最大监听的事件数量,timeout表示等待时间(-1表示一直等待,0表示立即返回,>0表示超时时间)。 4. 对返回的事件进行处理: 在epoll_wait函数返回后,可以遍历events数组,根据每个事件的文件描述符和事件类型进行处理。 5. 关闭epoll实例: close(epfd); 使用完epoll实例后,需要调用close函数关闭,释放相关资源。 以上就是epoll函数的基本使用流程。通过epoll可以高效地监听大量的文件描述符事件,减少系统资源的消耗。在实际开发中,可以根据需要设置不同的事件类型和回调函数,实现具体的业务逻辑。 ### 回答3: epoll函数Linux中用于处理I/O事件的一种高效机制。它可以监视一组文件描述符,并在其中的任意一个文件描述符上发生事件时进行相应的处理。 使用epoll函数的基本步骤如下: 1. 调用epoll_create函数创建一个epoll的句柄,该句柄被用于后续的相关操作。 2. 使用epoll_ctl函数epoll句柄中注册需要监视的文件描述符和事件。通过该函数可以实现添加、修改和删除文件描述符以及相应事件的功能。 3. 使用epoll_wait函数等待事件的发生。epoll_wait会一直阻塞,直到有文件描述符上的事件发生。一旦有事件发生,epoll_wait会返回所发生事件的文件描述符和相应的事件类型。 4. 根据返回的事件类型,进行相应的处理。 epoll函数有三个基本的系统调用: 1. epoll_create函数用来创建一个epoll实例,返回一个epoll句柄。 2. epoll_ctl函数用于操作epoll实例,可以实现添加、修改和删除文件描述符以及相应事件的功能。 3. epoll_wait函数用于等待事件的发生,一旦事件发生则返回相应的文件描述符和事件类型。 epoll函数的使用优点包括: - 支持大量的连接,可以监视数万个文件描述符。 - 存储监视文件描述符的数据结构(epoll实例)可以重复利用,避免了每次都需要重新设置的问题。 - 使用epoll_wait函数进行等待事件的发生,避免了轮询的方式,提高了效率。 总之,epoll函数Linux中用于处理I/O事件的一种高效机制,可以通过创建、操作epoll实例和等待事件的发生来实现对文件描述符的监视和相应事件的处理。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值