Linux网络编程epoll

举报
xcc-2022 发表于 2022/10/24 21:05:21 2022/10/24
【摘要】 3.epollepoll是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率,因为它会复用文件描述符集合来传递结果而不用迫使开发者每次等待事件之前都必须重新准备要被侦听的文件描述符集合,另一点原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符...

3.epoll

epoll是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率,因为它会复用文件描述符集合来传递结果而不用迫使开发者每次等待事件之前都必须重新准备要被侦听的文件描述符集合,另一点原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了。

目前epoll是linux大规模并发网络程序中的热门首选模型。

epoll除了提供select/poll那种IO事件的水平触发(Level Triggered)外,还提供了边沿触发(Edge Triggered),这就使得用户空间程序有可能缓存IO状态,减少epoll_wait/epoll_pwait的调用,提高应用程序效率。

可以使用cat命令查看一个进程可以打开的socket描述符上限。

cat /proc/sys/fs/file-max

如有需要,可以通过修改配置文件的方式修改该上限值。

sudo vi /etc/security/limits.conf

在文件尾部写入以下配置,soft软限制,hard硬限制。如下图所示。

  soft nofile 65536
  hard nofile 100000
image-20221001164619507

基础API

  1. 创建一个epoll句柄,参数size用来告诉内核监听的文件描述符的个数,跟内存大小有关。
#include <sys/epoll.h>
	int epoll_create(int size)		
		size:监听数目(内核参考值)
		返回值:成功:非负文件描述符;失败:-1,设置相应的errno
  1. 控制某个epoll监控的文件描述符上的事件:注册、修改、删除。

    #include <sys/epoll.h>
    	int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
    		epfd:	为epoll_creat的句柄
    		op:		表示动作,用3个宏来表示:
    			EPOLL_CTL_ADD 上树(注册新的fd到epfd)EPOLL_CTL_MOD (修改已经注册的fd的监听事件),
    			EPOLL_CTL_DEL 下树(从epfd删除一个fd);
             fd : 上树,下树的文件描述符
        	event :   上树的节点;告诉内核需要监听的事件
    
    		struct epoll_event {
    			__uint32_t events; /* 需要监听的事件 */
    			epoll_data_t data; /* 需要监听的文件描述符 */
    		};
    		typedef union epoll_data {
    			void *ptr;
    			int fd;
    			uint32_t u32;
    			uint64_t u64;
    		} epoll_data_t;
    
    		EPOLLIN :	表示对应的文件描述符可以读(包括对端SOCKET正常关闭)
    		EPOLLOUT:	表示对应的文件描述符可以写
    		EPOLLPRI:	表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来)
    		EPOLLERR:	表示对应的文件描述符发生错误
    		EPOLLHUP:	表示对应的文件描述符被挂断;
    		EPOLLET: 	将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)而言的
    		EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
    		返回值:成功:0;失败:-1,设置相应的errno
    
    将cfd上树
       int epfd =  epoll_create(1);
    struct epoll_event ev;
    ev. data.fd = cfd;
    ev.events = EPOLLIN;
    epoll_ctl(epfd, EPOLL_CTL_ADD,cfd, &ev);
    
    1. . 等待所监控文件描述符上有事件的产生,类似于select()调用。

      	#include <sys/epoll.h>
      	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_wait(epfd,rev,1024,-1)

工作原理:

image-20221001164239833

#include <stdio.h>
#include <fcntl.h>
#include "wrap.h"
#include <sys/epoll.h>
int main(int argc, char *argv[])
{
	//创建套接字 绑定
	int lfd = tcp4bind(8000,NULL);
	//监听
	Listen(lfd,128);
	//创建树
	int epfd = epoll_create(1);
	//将lfd上树
	struct epoll_event ev,evs[1024];
	ev.data.fd = lfd;
	ev.events = EPOLLIN;
	epoll_ctl(epfd,EPOLL_CTL_ADD,lfd,&ev);
	//while监听
	while(1)
	{
		int nready = epoll_wait(epfd,evs,1024,-1);//监听
		printf("epoll wait _________________\n");
		if(nready <0)
		{
			perror("");
			break;
		}
		else if( nready == 0)
		{
			continue;
		}
		else//有文件描述符变化
		{
			for(int i=0;i<nready;i++)
			{
				//判断lfd变化,并且是读事件变化
				if(evs[i].data.fd == lfd && evs[i].events & EPOLLIN)
				{
					struct sockaddr_in cliaddr;
					char ip[16]="";
					socklen_t len = sizeof(cliaddr);
					int cfd = Accept(lfd,(struct sockaddr *)&cliaddr,&len);//提取新的连接

					printf("new client ip=%s port =%d\n",inet_ntop(AF_INET,&cliaddr.sin_addr.s_addr,ip,16)
							,ntohs(cliaddr.sin_port));
                    //设置cfd为非阻塞
					int flags = fcntl(cfd,F_GETFL);//获取的cfd的标志位
					flags |= O_NONBLOCK;
					fcntl(cfd,F_SETFL,flags);
					//将cfd上树
					ev.data.fd =cfd;
					ev.events =EPOLLIN | EPOLLET;
					epoll_ctl(epfd,EPOLL_CTL_ADD,cfd,&ev);
				
				}
				else if( evs[i].events & EPOLLIN)//cfd变化 ,而且是读事件变化
				{
						char buf[4]="";
						//如果读一个缓冲区,缓冲区没有数据,如果是带阻塞,就阻塞等待,如果
						//是非阻塞,返回值等于-1,并且会将errno 值设置为EAGAIN
						int n = read(evs[i].data.fd,buf,sizeof(buf));
						if(n < 0)//出错,cfd下树
						{
							//如果缓冲区读干净了,这个时候应该跳出while(1)循环,继续监听
							if(errno == EAGAIN)
							{
								break;
							
							}
							//普通错误
							perror("");
							close(evs[i].data.fd);//将cfd关闭
							epoll_ctl(epfd,EPOLL_CTL_DEL,evs[i].data.fd,&evs[i]);
							break;
						}
						else if(n == 0)//客户端关闭 ,
							{
								printf("client close\n");
								close(evs[i].data.fd);//将cfd关闭
								epoll_ctl(epfd,EPOLL_CTL_DEL,evs[i].data.fd,&evs[i]);//下树
								return 1;
							}
						else
						{
							//	printf("%s\n",buf);
							write(STDOUT_FILENO,buf,4);
							write(evs[i].data.fd,buf,n);
						
                        }

					}
			}
		
		}

	}
	
	return 0;
}

但是我把buf设置成4个字节会出现剩余的会充当下一次写

image-20221001215933144

这种是水平触发,默认为水平触发

我们设置为边沿触发,加宏

ev.events =EPOLLIN | EPOLLET;//读写都会触发

我们可以在cfd那里加while持续读,但是read会阻塞,就无法回去监听了,所以设置cfd不阻塞

//设置cfd为非阻塞
int flags = fcntl(cfd,F_GETFL);//获取的cfd的标志位
flags |= O_NONBLOCK;
fcntl(cfd,F_SETFL,flags);

但是如果读一个缓冲区,缓冲区没有数据,如果是带阻塞,就阻塞等待

如果是非阻塞,返回值等于-1,并且会将errno 值设置为EAGAIN

//如果缓冲区读干净了,这个时候应该跳出while(1)循环,继续监听
if(errno == EAGAIN)
{
	break;
}

因为设置为水平触发,只要缓存区有数据epoll_wait就会被触发,epoll_wait是一个系统调用,尽量少调用所以尽量使用边沿触发,边沿出触发数据来一次只触发一次,这个时候要求一次性将数据读完,所以while循环读,读到最后read默认带阻塞,不能让read阻塞,因为不能再去监听,设置cfd为非阻塞,read读到最后一次返回值为-1.判断errno的值为EAGAIN,代表数据读干净

工作中 边沿触发 + 非阻塞 = 高速模式

epoll的工作方式

水平触发 LT

边沿触发 ET

img

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。