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事件发生的环境有待于发掘。

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值