Linux&Apue(0.4.2):epoll多路复用实现服务器多路并发

(一)epoll多路复用实现服务器多路并发

在聊epoll()之前,我们首先聊聊为什么需要epoll()。我们知道select会进行FD数组的遍历,但主要还会受1024个最大容量限制。而poll虽然解决了通过使用链表保存FD的方式解决了最大容量限制,但其不管是“活跃”的socket还是“不活跃”的socket的也会进行遍历,这就导致了在并发数很大的情况下,他们的工作效率都比较低。

(1) epoll的基础知识

epoll:是Linux内核为处理大批量文件描述符而作了改进的poll,是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。
特点
①获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了。
②epoll除了提供select/poll那种IO事件的水平触发(Level Triggered)外,还提供了边缘触发(Edge Triggered),这就使得用户空间程序有可能缓存IO状态,减少epoll_wait/epoll_pwait的调用,提高应用程序效率。
实现的基本方式
① 调用epoll_create()建立一个epoll对象(在epoll文件系统中为这个句柄对象分配资源)
②调用epoll_ctl向epoll对象中添加这100万个连接的套接字
③调用epoll_wait收集发生的事件的连接
拓展
什么是水平触发,边缘触发?

触发方式功能
水平触发(LT)缺省的工作方式,并且同时支持block和no-block socket.在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表。
边缘触发(ET)高速工作方式,只支持non-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了(比如,你在发送,接收或者接收请求,或者发送接收的数据少于一定量时导致了一个EWOULDBLOCK 错误)。但是请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once),不过在TCP协议中,ET模式的加速效用仍需要更多的benchmark确认。

1.1 epoll的系统调用

#include<sys/epoll.h>		//头文件包含
1.1.1 epoll_create()
int epoll_create(int size);

参数size:指定了我们想要通过epoll实例来检查的文件描述符个数。(从Linux 2.6.8开始:size参数被忽略,但是依然要大于0)
返回值
成功:返回一个非负数识别描述符
失败:返回-1
拓展
从2.6.27版内核以来,Linux支持了一个新的系统调用epoll_create1()。
①去掉了无用的参数size
②增加了一个可用来修改系统调用行为的flags参数

参数功能
EPOLL_CLOEXEC它使得内核在新的文件描述符上启动了执行即关闭标志

注意
当创建好epoll句柄后,它就是会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。

1.1.2 epoll_ctl()

epoll_ctl():能够修改由文件描述符epfd所代表的epoll实例中的兴趣列表。

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *ev);
参数功能
epfdepoll_create()的返回值
op指定需要执行的操作
fd指明了要修改兴趣列表中的哪一个文件描述符的设定
ev指向结构体epoll_event的指针

返回值
成功:返回0
失败:返回-1
①参数op的可选值

可选值功能
EPOLL_CTL_ADD将描述符fd添加到epoll实例中的兴趣列表中去。对于fd上我们感兴趣的事件,都指定在ev所指向的结构体中。如果我们试图向兴趣列表中添加一个已存在的文件描述符,epoll_ctl()将出现EEXIST错误。
EPOLL_CTL_MOD修改描述符上设定的事件,需要用到由ev所指向的结构体中的信息。如果我们试图修改不在兴趣列表中的文件描述符,epoll_ctl()将出现ENOENT错误。
EPOLL_CTL_DEL将文件描述符fd从epfd的兴趣列表中移除,该操作忽略参数ev。关闭一个文件描述符会自动将其从所有的epoll实例的兴趣列表移除。

②参数fd的对象

允许不允许
管道、FIFO、套接字、POSIX消息队列、inotify实例、终端、设备,甚至是另一个epoll实例的文件描述符普通文件或目录的文件描述符

epoll_event结构体

typedef union epoll_data
{
	void *ptr; 			 /* Pointer to user-defind data */
	int fd;				 /* File descriptor */
	uint32_t u32;		/* 32-bit integer */
	uint64_t u64;		 /* 64-bit integer */
} epoll_data_t;
struct epoll_event
{
	uint32_t events;		 /* epoll events(bit mask) */
	epoll_data_t data;		 /* User data */
};

③参数ev对fd的设置
events字段:是一个位掩码,它指定了我们为待检查的描述符fd上所感兴趣的事件集合。
data字段:是一个联合体,当描述符fd稍后称为就绪态时,联合的成员可用来指定传回给调用进程的信息。
events=以下宏的集合

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

注意
①在编程时就不应该同事使用epoll_data联合体中的两个成员,比如,有时为了方便确定那个描述符就绪,以及获取自己定义的一些内容,我们常常ptr和fd同时使用,结果发现程序会崩溃。
②每次添加/修改/删除被侦听文件描述符都需要调用epoll_ctl,所以要尽量少地调用epoll_ctl,防止其所引来的开销抵消其带来的好处。有的时候,应用中可能存在大量的短连接(比如说Web服务器),epoll_ctl将被频繁地调用,可能成为这个系统的瓶颈。

1.1.3 epoll_wait()

epoll_wait():返回epoll实例中处于就绪态的文件描述符信息,单个epoll_wait()调用能够返回多个就绪态文件描述符的信息。

int epoll_wait(int epfd, struct epoll_event *evlist, int maxevents, int timeout);
参数功能
epfdepoll_create()的返回值
struct epoll_event *evlist从内核得到事件的集合
maxevents每次能处理的最大事件数
timeout超时时间(大于0:阻塞至timeout毫秒。等于0:立即返回。等于-1:将永久阻塞)

(二)epoll实现服务器多路并发

2.1 服务器多路并发流程图

在这里插入图片描述

2.2 服务器多路并发代码

2.2.1 长选项命令解析&判断命令参数的导入
#include<stdio.h>
#include<errno.h>
#include<string.h>
#include<unistd.h>
#include<stdlib.h>
#include<ctype.h>
#include<time.h>
#include<pthread.h>
#include<getopt.h>
#include<libgen.h>	
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<sys/epoll.h>
#include<sys/resource.h>

#define ARRAY_SIZE(x)		(sizeof(x)/sizeof(x[0]))  //字节长除于首字节=个数
#define MAX_EVENTS		512    //设置事件数

int socket_server_init(char *listen_ip,int listen_port);
static inline void print_usage(char *progname);	
void	set_socket_rlimit(void);

int main(int argc,char **argv)
{
	
	int		daemon_run=0;
	int		serv_port=0;
	int		listen_fd;
	char	*progname=NULL;
	int		opt;
	int 	i,k;
	int		conn_fd;
	int		max_fd=0;
	char	buf[1024];
	int		rv;
	int		found;
	int		epoll_fd;
	struct	epoll_event	event;
	struct	epoll_event	event_array[MAX_EVENTS];
	int		events;

	//一、命令帮助提示
	//另外:打印命令提示进行函数抽象	
	progname=basename(argv[0]);	//从arg[0]中截取文件名
	//1.opts结构体定义
	struct option	opts[]=
	{	
		{"daemon",no_argument,NULL,'b'},	//守护进程
		{"port",required_argument,NULL,'p'},
		{"help",no_argument,NULL,'h'},
		{0,0,0,0}

	};
	
	//2.命令获取与解析
	while((opt=getopt_long(argc,argv,"d:p:h",opts,NULL))!=-1)
	{
		switch(opt)
		{
			case'd':
				daemon_run=1;
				break;
			case'p':
				serv_port=atoi(optarg);
				break;
			case'h':
				print_usage(progname);
				return	EXIT_SUCCESS;
			//出错
			default:
				break;
		}
	}
	//3.判断输入值是否正确
	//serv_port置-1,还需要if(!...)吗
	if(!serv_port)
	{
		print_usage(progname);
		return -1;
	}
	//判断是否需要守护进程(即是否要后台运行)
	if(daemon_run)
	{
		daemon(0,0);
	}

	//判断服务器是否监听端口
	if((listen_fd=socket_server_init(NULL,serv_port))<0)
	{
		printf("ERROR:%s server listen on port %d failure",argv[0],serv_port);
		return -2;
	}

2.2.2 epoll实现

//二、epoll
	set_socket_rlimit();			//虽说epoll没有限制,但是linux本身创建的socket是有限制的。
	//判断epoll创建是否成功
	if((epoll_fd=epoll_create(MAX_EVENTS))<0)
	{
		printf("epoll_create() failure:%s\n",strerror(errno));
		return -3;
	}

	event.events=EPOLLIN;
	event.data.fd=listen_fd;

	for( ; ;)
	{
		//判断等待感兴趣的事件	
		events=epoll_wait(epoll_fd,event_array,MAX_EVENTS,-1);
		if(events<0)
		{
			printf("epoll failure:%s\n",strerror(errno));
			break;
		}
		else if(events==0)
		{
			printf("epoll get timeout\n");
			continue;
		}
	
		//events>0   事件进来
		for(i=0;i<events;i++)
		{
			if((event_array[i].events&EPOLLERR)||(event_array[i].events&EPOLLHUP)) //ERR:有错误发生, HUP:socket对端关闭
			{
				printf("epoll_wait get error on fd[%d]:%s\n",event_array[i].data.fd,strerror(errno));
				epoll_ctl(epoll_fd,EPOLL_CTL_DEL,event_array[i].data.fd,NULL);//将fd从epfd的列表中移除
				close(event_array[i].data.fd);
			}
		}

		//新客户端连接
		if(event_array[i].data.fd==listen_fd)	
		{
			if((conn_fd=accept(listen_fd,(struct sockaddr *)NULL,NULL))<0)
			{
				printf("accept new client failure:%s\n",strerror(errno));
				continue;
			}
			else 
			{		
				event.data.fd=conn_fd;
				event.events=EPOLLIN;
				if(epoll_ctl(epoll_fd,EPOLL_CTL_ADD,conn_fd,&event)<0)	//将fd添加到pfd列表中
				{
					printf("epoll add client socket failure:%s\n",strerror(errno));
					close(event_array[i].data.fd);
					continue;
				}
				printf("epool add new client socket[%d] sueccess.\n",conn_fd);
			}
			
		}
		else //已连客户:1.数据请求 2.中断连接
		{
			 //判断是否已连接
			 if((rv=read(event_array[i].data.fd,buf,sizeof(buf)))<=0)
			 {	
				 printf("socket[%d] read failure or get disconnect and will be removed.\n",event_array[i].data.fd);
				 epoll_ctl(epoll_fd,EPOLL_CTL_DEL,event_array[i].data.fd,NULL);//将fd从pfd列表中删除
				 close(event_array[i].data.fd);
				 continue;	//结束循环
			 }
			 else
			 {
				 printf("socket[%d] read get %d bytes data \n",event_array[i].data.fd,rv);
				 //字符:小转大字母
				 for(k=0;k<rv;k++)
				 {
					 buf[k]=toupper(buf[k]);
				 }
				 if(write(event_array[i].data.fd,buf,rv)<0)
				 {
					 printf("socket[%d] write failure:%s\n",event_array[i].data.fd,strerror(errno));
					 epoll_ctl(epoll_fd,EPOLL_CTL_DEL,event_array[i].data.fd,NULL);//将fd从pfd列表中删除
				 	 close(event_array[i].data.fd);
				 }				 
			 }
		}
	}
CleanUp:
	close(listen_fd);
	return 0;
}
2.2.3 命令提示打印函数
//命令提示打印
static inline void print_usage(char *progname)		//了解static inlin 意义
{
	printf("Usage:%s [OPTION]...\n",progname);
	printf("%s is a  socket server program\n",progname);
	printf("Mandatory arguments:long or  short options\n");

	printf("-b[daemon] set program running on background\n");
	printf("-p[port] socket server port address\n");
	printf("-h[help] display this help information\n");

	printf("\nExample: %s -b -p 1111 \n",progname);
	return ;
}
2.2.4 server_init
int socket_server_init(char *listen_ip,int listen_port)
{

	int		on=1;
	int		rv=0;
	int		listen_fd;
	struct sockaddr_in	servaddr;

	//1.socket
	if((listen_fd=socket(AF_INET,SOCK_STREAM,0))<0)
	{
		printf("socket () create a TCP socket failure:%s\n",strerror(errno));
		return -1;
	}
	//端口重用
	setsockopt(listen_fd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on));

	//初始化操作
	memset(&servaddr,0,sizeof(servaddr));
	servaddr.sin_family=AF_INET;
	servaddr.sin_port=htons(listen_port);

	//判断IP地址,并初始化。意义:这里if else相当于两用。监听特定IP,或监听所有IP。
	if(!listen_ip)
	{
		servaddr.sin_addr.s_addr=htonl(INADDR_ANY);
		printf("listen other IP address [%d]",listen_ip);
	}
	else 
	{
		if(inet_pton(AF_INET,listen_ip,&servaddr.sin_addr)<=0)
		{
			printf("inet_pton() set listen IP address failure.\n");
			rv=-2;
			goto CleanUp;
		}
	}

	//2.bind
	if(bind(listen_fd,(struct sockaddr*)&servaddr,sizeof(servaddr))<0)
	{	
		printf("bind() bind the TCP socket failure: %s\n",strerror(errno));
		rv=-3;
		goto CleanUp;
	}
	//3.listen
	if(listen(listen_fd,13)<0)
	{
		printf("bind() bind the TCP socket failure: %s\n",strerror(errno));
		rv=-4;
		goto CleanUp;
	}
CleanUp:
	if(rv<0)
	{
		close(listen_fd);
	}
	else
	{
		rv=listen_fd;
		return rv;
	}
}
2.2.5 socket_rlimit 限制设置
void set_socket_rlimit(void)
{
	struct rlimit limit={0};

	getrlimit(RLIMIT_NOFILE,&limit);
	limit.rlim_cur=limit.rlim_max;
	setrlimit(RLIMIT_NOFILE,&limit);
	printf("set socket open fd max count to %d\n",limit.rlim_max);
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值