Linux:I/O复用——poll、epoll


poll


poll系统调用和select类似,也是在指定时间内轮询一定数量的文件描述符,以测试其中是否有就绪者。poll的原型如下:

#include <poll.h>
int poll(struct pollfd *fds, int nfds, int timeout);
  • fds:struct pollfd类型的数组,用来传递用户关注的文件描述符以及事件类型,pollfd结构体如下:
    struct pollfd
    {
    	int fd;  // 文件描述符
    	short events;  // 关注的事件类型
    	short revents; // 由内核填充,poll返回时用来标注就绪的事件类型
    }
    
  • nfds:数组的大小
  • timeout:定时时间,单位毫秒,-1表示一直阻塞直到有事件就绪,timeout 为 0 时, poll 调用将立即返回。
  • 返回值:成功返回就绪文件描述符的总数,超时返回 0,失败返回-1

poll支持的事件类型

(可以在events中填充的事件类型,如果关注多种事件类型,可以使用按位或将这些事件合到一起)

描述
POLLIN数据可读(包括普通数据和优先数据)
POOLLOUT数据可写(包括普通数据和优先数据)
POLLRDHUPTCP连接被对方关闭,对方关闭了写操作,它由 GNU 引入,使用时,需要在代码开始处定义_GNU_SOURCE
POLLHUP挂起,管道写端关闭,读端会收到该事件
POLLERR错误
POLLNVAL文件描述符没有打开

使用poll实现TCP服务器

服务器端代码

// ser.c
#define _GNU_SOURCE  //POLLRDHUP
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <string.h>
#include <unistd.h>

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include <poll.h>
#define NUM 100

// 根据ip地址与端口号创建套接字,返回创建的套接字的文件描述符
int  CreateSocket(char *ip, short port)
{
    int listenfd = socket(AF_INET, SOCK_STREAM, 0);
    if(listenfd == -1) return -1;

    struct sockaddr_in ser_addr;
    memset(&ser_addr, 0, sizeof(ser_addr));

    ser_addr.sin_family = AF_INET;
    ser_addr.sin_port = htons(port);
    ser_addr.sin_addr.s_addr = inet_addr(ip);

    int res = bind(listenfd, (struct sockaddr*)&ser_addr, sizeof(ser_addr));
    if(res == -1) return -1;

    res = listen(listenfd, 5);
    if(res == -1) return -1;

    return listenfd;
}

//初始化文件描述符数组
void InitFds(struct pollfd *fds)
{
	int i = 0;
	for(; i < NUM; ++i)
	{
		fds[i].fd = -1;
		fds[i].events = 0; 	
		//revents由内核填充,所以这里不用设置
	}
}

// 向fds数组中插入一个fd, 关注的事件类型为events
void InsertFds(struct pollfd *fds, int fd, short events)
{
	int i = 0;
	for(; i<NUM; ++i)
	{
		if(fds[i].fd == -1)
		{
			fds[i].fd = fd;
			fds[i].events = events;
			break;
		}
	}
}

void DealReadyEvent(struct pollfd *fds, int listenfd)
{
	int i = 0;
	for(; i<NUM; ++i)
	{
		if(fds[i].fd == -1)	 continue;
		if(fds[i].fd == listenfd)  //是监听套接字
		{
			if(fds[i].revents & POLLIN)
			{
				struct sockaddr_in cli_addr;
				socklen_t len = sizeof(cli_addr);
				int c =  accept(listenfd, (struct sockaddr*)&cli_addr, &len);
				if(c == -1)  continue;
				
				printf("one client link success\n");
				InsertFds(fds, c, POLLIN | POLLRDHUP);
			}
		}
		else  //是链接套接字
		{
			if(fds[i].revents & POLLRDHUP)
			{
				printf("%d client over\n", fds[i].fd);
				close(fds[i].fd);
				fds[i].fd = -1;
				fds[i].events = 0;
			}
			else if(fds[i].revents & POLLIN)
			{
				char buff[128] = {0};
				int n = recv(fds[i].fd, buff, 127, 0);
				if(n<=0)
				{
					printf("%d client error\n", fds[i].fd);
					close(fds[i].fd);
					fds[i].fd = -1;
					fds[i].events = 0;;
				}
				else
				{
					printf("%d: %s\n", fds[i].fd, buff);
					send(fds[i].fd, "OK",2, 0);
				}	
			}
		}
	}
}
int main()
{
	int listenfd  = CreateSocket("192.168.133.132", 6000);
	assert(listenfd != -1);

	struct pollfd fds[NUM];
	InitFds(fds);

	InsertFds(fds, listenfd, POLLIN);
	
	while(1)
	{
		int n = poll(fds, NUM, -1);
		if(n <= 0)
		{
			printf("poll error\n");
			continue;
		}
		DealReadyEvent(fds, listenfd);
	}
}	

客户端代码

// cli.c
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <string.h>
#include <unistd.h>

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h> //字节序的转换
#include <arpa/inet.h>  //IP地址转换

int main()
{
	int sockfd = socket(AF_INET, SOCK_STREAM, 0);//创建套接字
	assert(-1 != sockfd);
    
	struct sockaddr_in ser_addr;
	memset(&ser_addr, 0, sizeof(ser_addr));
	ser_addr.sin_family = AF_INET;
	ser_addr.sin_port = htons(6000);
	ser_addr.sin_addr.s_addr = inet_addr("192.168.133.132"); 
    
	int res = connect(sockfd, (struct sockaddr*)&ser_addr, sizeof(ser_addr));//指定连接的服务器端的 IP 地址和端口
	assert(-1 != res);
    
	while(1)
	{
		printf("input: ");
		char buff[128] = {0};
		fgets(buff, 127, stdin);
		if(strncmp(buff, "end", 3) == 0)
		{
			break;
		} 
        
        send(sockfd, buff, strlen(buff) - 1, 0);
        
		memset(buff, 0, 128);
		recv(sockfd, buff, 127, 0);
		printf("%s\n", buff);
	} 
    
    close(sockfd);
	exit(0);
}

运行结果:
在这里插入图片描述

poll总结

1、poll所能关注的文件描述符的个数理论上是没有限制的。文件描述符的取值范围也是没有限制的

2、poll支持的事件类型比select更多

3、poll返回时,内核修改的是数组元素中revents成员,与用户填充的关注的事件类型不冲突,所以每次调用poll之前,不需要重新设置这个数组

前三点是与select不一样的,后面的几点是与select相同的

4、poll返回时,也仅仅是返回就绪文件描述符的个数,并没有指定是哪几个文件描述符就绪。所以用户程序检测就绪文件的时间复杂度为O(n)

5、poll每次调用时,也是需要将用户空间的数组传递拷贝给内核,poll返回时又将内核的拷贝到用户空间

6、poll内核也是采用轮询的方式

7、poll也只能工作在LT模式

epoll


epoll的原型

epoll 是 Linux 特有的 I/O 复用函数。它在实现和使用上与 select、 poll 有很大差异。

首先, epoll 使用一组函数来完成任务,而不是单个函数。其次, epoll 把用户关心的文件描述符上的事件放在内核里的一个事件表中。从而无需像 select 和 poll 那样每次调用都要重复传入文件描述符或事件集。但 epoll 需要使用一个额外的文件描述符,来唯一标识内核中的这个事件表。 该文件描述符由epoll_create创建

epoll 相关的函数如下:

  • epoll_create():用于创建内核事件表
  • epoll_ctl():用于操作内核事件表
  • epoll_wait():用于在一段超时时间内等待一组文件描述符上的事件

创建内核事件表

#include <sys/epoll.h>
int epoll_create(int size);  
  • 成功返回内核事件表的文件描述符,失败返回-1
  • size:size参数现在并不起作用,只是给内核一个提示,告诉它事件表需要多大
  • 内核事件表:在内核中创建的一个记录用户关注的文件描述符和事件的集合,其数据结构为红黑树

操作内核事件表

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
  • epfd: epoll_create方法返回的文件描述符,指定要操作的内核事件表的文件描述符
  • op: 指定操作类型,添加 EPOLL_CTL_ADD, 修改 EPOLL_CTL_MOD, 删除 EPOLL_CTL_DEL
  • fd: 指定要操作的文件描述符
  • event:指定事件,它是 epoll_event 结构指针类型, epoll_event 的定义如下:
    struct epoll_event
    {
    	uint32_t events;  //事件的集合
    	epoll_data_t data;//用户数据,data.fd 文件描述符
    };
    
    其中, events 成员描述事件类型, epoll 支持的事件类型与 poll 基本相同,表示 epoll 事件的宏是在 poll 对应的宏前加上‘E’ ,比如 epoll 的数据可读事件是 EPOLLIN。但是 epoll 有两个额外的事件类型—EPOLLET 和 EPOLLONESHOT。 data 成员用于存储用户数据,是一个联合体,其定义如下:
    typedef union epoll_data
    {
    	int fd;  //文件描述符
    	void *ptr;//用户数据
    	uint32_t u32;
    	uint64_t u64;
    }epoll_data_t;
    
  • 成功返回0,失败返回-1

epoll_wait:真正执行I/O复用的方法

epoll_wait函数在一段超时时间内等待一组文件描述符上的事件,原型如下:

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
  • expfd:这次处理的内核事件表的文件描述符
  • events:用户数组,这个用户数组所有的都是内核填充的,用来返回所有就绪的文件描述符以及事件,这个数组仅仅在 epoll_wait 返回时保存内核检测到的所有就绪事件,而不像 select 和 poll 的数组参数那样既用于传入用户注册的事件,又用于输出内核检测到的就绪事件。这就极大地提高了应用程序索引就绪文件描述符的效率
  • maxevents:events数组的长度,即指定最多监听多少个事件
  • timeout:定时时间,单位为毫秒,如果 timeout 为 0,则 epoll_wait 会立即返回,如果timeout 为-1,则 epoll_wait 会一直阻塞,直到有事件就绪。
  • 返回值:成功返回就绪的文件描述符的个数,出错返回-1,超时返回0

epoll实现TCP服务器

服务器端代码:

//ser.c
#define _GNU_SOURCE 
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <string.h>
#include <unistd.h>

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include <sys/epoll.h>
#define NUM 100

// 根据ip地址与端口号创建套接字,返回创建的套接字的文件描述符
int  CreateSocket(char *ip, short port)
{
    int listenfd = socket(AF_INET, SOCK_STREAM, 0);
    if(listenfd == -1) return -1;

    struct sockaddr_in ser_addr;
    memset(&ser_addr, 0, sizeof(ser_addr));

    ser_addr.sin_family = AF_INET;
    ser_addr.sin_port = htons(port);
    ser_addr.sin_addr.s_addr = inet_addr(ip);

    int res = bind(listenfd, (struct sockaddr*)&ser_addr, sizeof(ser_addr));
    if(res == -1) return -1;

    res = listen(listenfd, 5);
    if(res == -1) return -1;

    return listenfd;
}

void DealReadyEvents(struct epoll_event *events, int n, int epfd, int listenfd)
{
	int i = 0;
	for(; i<n; ++i)
	{
		int fd = events[i].data.fd;
		if(fd == listenfd)  //监听的套接字
		{
			struct sockaddr_in cli_addr;
			socklen_t len = sizeof(cli_addr);
			int c = accept(listenfd, (struct sockaddr*)&cli_addr,&len);
			if(c == -1)	continue;
			
			struct epoll_event event;
			event.events = EPOLLIN | EPOLLRDHUP;
			event.data.fd = c;
			int res = epoll_ctl(epfd, EPOLL_CTL_ADD, c, &event);
			assert(res != -1);
			
			printf("one client link success\n");	
		}
		else  //链接的套接字
		{
			if(events[i].events & EPOLLRDHUP)  //客户端关闭
			{
				int res = epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
				assert(res != -1);
				close(fd); 
			}
			else if(events[i].events & EPOLLIN)  //客户端发送来了数据
			{
				char buff[128] = {0};
				int n = recv(fd, buff, 127, 0);
				if(n <= 0)
				{
					printf("%d client error\n", fd);
					int res = epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
					assert(res != -1);
					close(fd); 
				}
				else  //出错
				{
					printf("%d: %s\n",fd, buff);
					send(fd, "OK", 2,0);	
				}
			}
			else
			{
				printf("error\n");
			}
		}
	}
}

int main()
{
	int listenfd = CreateSocket("192.168.133.132", 6000);
	assert(-1 != listenfd);

	int epfd = epoll_create(5);
	assert(epfd != -1);
	
	struct epoll_event events;
	events.events = EPOLLIN;
	events.data.fd = listenfd;
	int res = epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &events);
	assert(res != -1);
	
	while(1)
	{
		struct epoll_event events[NUM];
		int n = epoll_wait(epfd, events, NUM, -1);
		if(n<=0)
		{
			printf("epoll error \n");
			continue;
		}
		DealReadyEvents(events, n,epfd, listenfd);
	}
	exit(0);
}

客户端代码同poll客户端

执行结果
在这里插入图片描述

epoll总结

1、epoll不再是一个接口,而是一组接口

2、能够监听的文件描述符个数没有限制,值的范围也没有限制

3、关注的事件类型更多,比poll多了EPOLLET和EPOLLONESHOT

4、epoll在内核中维护用户关注的事件类型,每次epoll_wait时不需要传递用户空间的数据。epoll_wait返回时,仅仅返回就绪的文件描述符和事件,相比于select和poll效率更高

5、epoll_wait返回的仅仅是就绪的事件,所以用户程序检测就绪文件描述符的时间复杂度就为O(1)

6、epoll内核采用的是回调的方式检测事件就绪

7、epoll不仅支持LT模式,也支持高效的ET模式

epoll的LT与ET模式

  • LT模式:Level Trigger,电平触发,当epoll_wait将就绪事件返回后,如果用户程序没有处理该就绪事件,下一次epoll_wait还会通知这个就绪事件,LT是默认的工作模式
  • ET模式:Edge Trigger,边沿触发,当epoll_wait将就绪事件返回后,用户没有处理该就绪事件,则下一次epoll_wait不会通知该就绪事件。即同一个就绪事件只会通知一次,相比于LT模式,效率更高

EPOLLONESHOT事件

即使使用高效的ET模式,一个socket上的某个事件还是可能被触发多次。这在并发程序中是线程不安全的,可能会出现两个线程同时操作一个socket的局面。如果希望一个socket连接的任一时刻都只被一个线程处理,可以使用 EPOLLONESHOT 事件实现。

对于注册了EPOLLONESHOT事件的文件描述符,操作系统最多触发其上注册的一个可读、可写或者异常事件,且只触发一次,这样,当一个线程在处理某个socket时,其他线程是不可能有机会操作该socket的。除非我们使用epoll_ctl函数重置该文件描述符上注册的EPOLLONESHOT事件。

注意:注册了EPOLLONESHOT事件的socket一旦被某个线程处理完毕,该线程就应该立即重置这个socket上的EPOLLONESHOT事件,以确保这个socket下一次可读时,其EPOLLIN事件能被触发,进而让其他工作线程有机会继续处理这个socket

三组I/O复用函数的比较


这3组系统调用都能同时监听多个文件描述符。它们将等待由timeout参数指定的超时时间,直到一个或多个文件描述符上有事件发生时返回,返回值就是就绪的文件描述符的数量。返回0表示没有事件发生,出错返回-1

select、poll、epoll区别:

selectpollepoll
事件集合用户通过3个参数分别传入感兴趣的可读、可写及异常等事件,内核通过对这些参数的在线修改来反馈其中的就绪事件。这使得用户每次调用select都要重置这3个参数统一处理所有事件类型,因此只需要一个事件集参数,用户通过pollfd.events传入感兴趣的事件,内核通过修改pollfd.revents反馈其中就绪的事件内核通过一个事件表直接管理用户感兴趣的所有事件。因此每次调用epoll_wait时,无须反复传入用户感兴趣的事件。epoll_wait系统调用的参数events仅用来反馈就绪的事件
应用程序索引就绪文件的时间复杂度O(n)O(n)O(1)
最大支持文件描述符个数一般有最大值限制65535(系统允许打开的最大文件描述符数目)65535
工作模式LTLTLT与ET
内核实现和工作效率采用轮询方式来检测就绪事件,算法时间复杂度为O(n)采用轮询方式来检测就绪事件,算法时间复杂度为O(n)采用回调方式来检测就绪事件,算法时间复杂度为O(1)

参考文献

[1]游双. Linux高性能服务器编程. 机械工业出版社,2013.5

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值