Linux——I/O复用(2)—— poll和epoll

poll原型

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

int poll(struct pollfd *fds, int nfds, int timeout);
fds: struct pollfd类型的数组, 用来传递用户关注的文件描述符以及事件类型 
struct pollfd
{ 
 	int fd; // 文件描述符 
 	short events; // 关注的事件类型 如果关注多种事件类型,可以使用按位或‘|’将这些事件合到一起 
 	short revents; // 由内核填充,poll返回时用来标注就绪的事件类型 
};
  • nfds: 数组的大小

  • timeout:超时时间,以毫秒为单位, -1表示一直阻塞到有事件就绪

  • 返回值: == 0 超时< 0 出错 > 0 就绪的文件描述符的个数

  • fds参数是一个pollfd类型的数组,它指定所有我们感兴趣的文件描述符上发生的可读、可写和异常等事件。其中fd成员指定文件描述符;events成员告诉poll监听fd上的哪些事件,它是一系列事件的按位或;revents成员则由内核修改,以通知应用程序fd上实际发生了哪些事件。

每一个文件描述符都用一个结构体表示,其实传入参数时,传入的是一个结构体数组,数组可以超过1024,可以更大,可以收纳的文件描述符就更多,其次,poll的事件类型也更多。

poll支持的事件类型

在这里插入图片描述

  • 数据可读是指,接受缓冲区中有数据,读事件就会就绪;
  • 数据可写是指,发送缓冲区内有空间,send不会被阻塞,此时写事件就会就绪

因此,在与一个客户端建立连接的初期,接受缓冲区是空的,所以读事件没有就绪,但是发送缓冲区是空的,所以写事件一开始就是就绪的。

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);//-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 是 Linux 特有的 I/O 复用函数。它在实现和使用上与 select、 poll 有很大差异。

首先, epoll 使用一组函数来完成任务,而不是单个函数。其次, epoll 把用户关心的文件描述符上的事件放在内核里的一个事件表中。从而无需像 select 和 poll 那样每次调用都要重复传入文件描述符或事件集进行遍历。
epoll就绪之后会直接把就绪文件的描述符返回给你,但 epoll 需要使用一个额外的文件描述符,来唯一标识内核中的这个事件表。 该文件描述符由epoll_create创建,内核实现方式是在文件描述符上注册回调函数来检测时间。

举个例子:我坐在讲台上,注册回调函数,我就说:如果你们谁完成作业了,就交上来。我检测10人,100人是没有区别的。我坐在讲台,拿走多少作业是没有区别的!!!select和poll得循环去检测谁做作业了。而epoll是我坐在讲台上等,等5秒钟就超时,有多少收多少,开销是相同的,时间复杂度是O(1)

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

epoll总结

  • epoll不再是一个接口,而是一组接口
  • 能够监听的文件描述符个数没有限制,值的范围也没有限制
  • 关注的事件类型更多,比poll多了EPOLLET和EPOLLONESHOT
  • epoll在内核中维护用户关注的事件类型,每次epoll_wait时不需要传递用户空间的数据。epoll_wait返回时,仅仅返回就绪的文件描述符和事件,相比于select和poll效率更高
  • epoll_wait返回的仅仅是就绪的事件,所以用户程序检测就绪文件描述符的时间复杂度就为O(1)
  • epoll内核采用的是回调的方式检测事件就绪
  • epoll不仅支持LT模式,也支持高效的ET模式

epoll更高效的原因

  • select,poll,epoll虽然都会返回就绪的文件描述符的数量,但是select和poll不会明确地支出是哪些文件描述符已经就绪,而epoll会,造成的区别就是,系统调用返回后,调用select和poll的程序需要遍历监听整个文件描述符找到谁处于就绪状态,而epoll直接处理即可;
  • select、poll都需要将有关文件描述符的数据结构拷贝进内核,最后再拷贝出来,而epoll创建的有关文件描述符的数据结构本身就存在与内核态中,系统调用返回时利用mmap()文件映射内存加速与内核空间的消息传递:即epoll使用mmap减少复制开销
  • select、poll采用轮询的方式来检查文件描述符是否处于就绪态,而epoll采用回调机制,造成的结果就是,随着fd的增加,select和poll的效率会线性降低,而epoll不会受到太大的影响,除非活跃的socket很多。
  • epoll的边缘触发模式效率高,系统不会充斥大量不关心的就绪文件描述符,虽然epoll性能最好,但是在连接数少并且连接都十分活跃的情况下,select和poll的性能可能都比epoll号,毕竟epoll的通知机制需要很多函数回调。

epoll的LT与ET模式

epoll 对文件描述符有两种操作模式:LT(Level Trigger,电平触发)模式和 ET(EdgeTrigger,边沿触发)模式。

  • LT 模式是默认的工作模式。(也是select和poll具备的)当往 epoll 内核事件表中注册一个文件描述符上的 EPOLLET 事件时,epoll 将以高效的 ET模式来操作该文件描述符。 对于 LT 模式操作的文件描述符,当 epoll_wait检测到其上有事件发生并将此事件通知应用程序后,应用程序可以不立即处理该事件。这样,当应用程序下一次调用 epoll_wait时,还会再次向应用程序通告此事件,直到该事件被处理
  • 对于 ET 模式操作的文件描述符,当 epoll_wait 检测到其上有事件发生并将此事件通知应用程序后,应用程序必须立即处理该事件,因为后续的epoll_wait 调用将不再向应用程序通知这一事件。所以 ET 模式在很大程度上降低了同一个 epoll 事件被重复触发的次数,因此效率比 LT 模式高

EPOLLONESHOT事件

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

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

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

3种IO的比较

在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值