select、poll、epoll三种IO多路复用的原理及其区别

本文详细介绍了select、poll和epoll三种网络IO多路复用技术的执行原理,比较了它们在处理高并发、文件描述符限制和性能开销方面的优缺点,着重强调了epoll通过红黑树和回调机制实现的高效性。
摘要由CSDN通过智能技术生成

参考

腾讯面试:请描述 select、poll、epoll 这三种IO多路复用技术的执行原理_哔哩哔哩_bilibili

select

int listenfd =socket(PF_INET,SOCK_STREAM, 0);
bind(listenfd,(struct sockaddr*)&address,sizeof(address));
listen(listenfd, 5);
fd_set read_fds;

for(int i=0;i<5;i++)
{
	fd[i]= accept(listenfd,(struct sockaddr*)&client, &addr len);if (Edlijb max)
	if(fd[i]>max)
	{
		max = fd[i];
	}
	
}

while(1){
	FD ZERO(&read fds);
	for (i=0; i<5;i++){
		FD_SET(fd[i], &read_fds)
	}
}
ret = select(max + 1, &read_fds, NULL, NULL, NULL);
for(i=0;i<5;i++){
	if(FD_ISSET(fd[i],&read_fds))
	{
		ret =recv(fd[i],buff,sizeof(buff)-1,0);
	}
}

  • 创建要监听的文件描述符集合fd[n]
  • 将文件描述符集合拷贝到bitmap中,其中bitmap的作用是标记哪一个文件描述符出现网络IO事件,比如内核检测到了3号文件描述符有网络事件发生,就会将bitmap[3]=1
  • select函数将bitmap传入内核,由内核负责检测网络IO事件到来的socket
  • 当网卡设备收到网络数据时,由DMA将网络数据拷贝到内核缓冲区。并由内核检测出该事件的到达
  • 内核修改对应bitmap的标志位,表示检测到了对应的文件描述符由网络IO事件
  • 内核将bitmap拷贝到用户态,用户态循环检测bitmap的标志位,来判断到底哪个文件描述符发生了网络IO事件

在上述过程中,可以发现select方式的四种不足

  • bitmap的数据结构是一个数组,意味着它能检测的文件描述符个数是有限的,一般最大能检测的数量是2048,这意味着select在高并发场景中表现会不佳
  • bitmap每次都需要从用户态拷贝到内核态进行检测,当内核检测完毕后再将bitmap拷贝到用户态,这种频繁的拷贝会导致系统性能开销较大
  • fdset无法做到重用,本质原因是因为内核检测后仍旧使用bitmap来保存检测结果,这意味着在下次检测时就必须重置bimap
  • bitmap并没有告诉用户到底哪个文件描述符的事件到达了,必须由用户使用O(n)的时间来对bitmap进行遍历才能得知具体网络事件到达的fd

poll

struct pollfd{
	int fd;//文件描述符
	short events;//注册的事件
	short revents;//实际发生的事件,由内核检测
}

int sscfd = socket(PF_INET,SOCK_STREAM, 0);
bind(sscfd,(struct sockaddr*)&address, sizeof(address));
listen(sscfd, 4);
struct pollfd fds[4];
for(i=0;i<4; i++){
	fds[i].fd=accept(sscfd,(struct sockaddr *)&cliAddr,&addrLen);
	fds[i].events = POLLIN;
}
sleep(1);
while(1){
	ret = poll(fds,4,4000)
	for(int i=0;i<4;i++{
		if(fds[i].revents & POLLIN)
		{
			fds[i].revents = 0;
			ret =recv(fds[i].fd,buff,sizeof(buff)-1,0);
		}
	}
}

poll与select的区别在于,传入内核检测事件到达的数据结构换成了一个结构体,观察这个结构体的数据成员,其中fd仍然是待检测的文件描述符,events是要检测的事件(读还是写),注意,这里有一个revents,这个数据成员用于将来内核检测到了对应的事件后对其进行写入,由于使用了revents来保存检测到的结果,因此也就解决了select中fdset的不可重用问题

struct pollfd{
	int fd;//文件描述符
	short events;//注册的事件
	short revents;//实际发生的事件,由内核检测
}

poll执行的原理步骤

  • 将待检测的文件描述符集合拷贝给pollfd这个结构体数组中,并一次性拷贝到内核态,由内核检测事件的发生
  • 在内核中无差别的遍历每个fd,判断是否有数据到达
  • 一旦检测到事件发生后,就将所有fd的状态从内核拷贝到用户,并返回已就绪的fd个数
  • 在用户态遍历具体就绪的fd,进行相应的事件处理

相较而言

  • poll使用结构体数组,破除了select文件描述符fd的个数限制
  • 同时解决了数据结构重用的问题

仍旧存在的问题在于

  • 仍旧需要频繁的将数据结构从用户态拷贝到内核态
  • 每次都需要对文件描述符表进行遍历,使用O(N)的时间来确认具体的事件

epoll

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;

struct epoll event everts[5];
int epoll_fd = epoll_create(5);
for(i=0;<5;i++){
	struct epoll_event event;
	event.data.fd=accept(sscfd,(struct sockaddr*)&cliAddr,&addrLen);
	event.events = EPOLLIN;
	epoll_ctl(epoll_fd, EPOLL CTL ADD, event.data.fd, &event);
}
while(1)
{
	int ret = epoll_wait(epoll_fd, events, 5, 2000);
	for(int i=0;i<ret;i++)
	{
		if(events[i].events & EPOLLIN)
		{
			recv(sockfd,buf, BUFFER_SIZE - 1,0);
		}
	}
}

某一进程调用 epoll_create 方法时,Linux 内核会创建一个 eventpoll 结构体,这个结构体中有两个成员与epoll的使用方式密切相关

struct eventpoll {
  ...
  /*红黑树的根节点,这棵树中存储着所有添加到epoll中的事件,
  也就是这个epoll监控的事件*/
  struct rb_root rbr;
  /*双向链表rdllist保存着将要通过epoll_wait返回给用户的、满足条件的事件*/
  struct list_head rdllist;
  ...
};

我们在调用 epoll_create 时,内核在 epoll 文件系统里建一个 file 结点,同时在内核 cache 里建了个红黑树用于存储以后 epoll_ctl 传来的 socket 外,还会再建立一个 rdllist 双向链表,用于存储准备就绪的事件当 epoll_wait 调用时,仅仅观察这个 rdllist 双向链表里有没有数据即可。有数据就返回,没有数据就sleep,等到 timeout 时间到后即使链表没数据也返回。所以epoll_wait 非常高效。 

所有添加到epoll中的事件都会与设备(如网卡)驱动程序建立回调关系,也就是说相应事件的发生时会调用这里的回调方法。这个回调方法在内核中叫做ep_poll_callback,它会把这样的事件放到上面的rdllist双向链表中。

当调用 epoll_wait 检查是否有发生事件的连接时,只是检查eventpoll对象中的rdllist双向链表是否有epitem元素而已,如果rdllist链表不为空,则这里的事件复制到用户态内存(使用共享内存提高效率)中,同时将事件数量返回给用户。因此epoll_waitx效率非常高。epoll_ctl在向epoll对象中添加、修改、删除事件时,从rbr红黑树中查找事件也非常快,也就是说epoll是非常高效的,它可以轻易地处理百万级别的并发连接。

总结

一颗红黑树,一张准备就绪句柄链表,少量的内核cache,就帮我们解决了大并发下的socket处理问题.

  • 执行epoll_create() 时,创建了红黑树和就绪链表;

  • 执行 epoll_ctl() 时,如果增加 socket 句柄,则检查在红黑树中是否存在,存在立即返回,不存在则添加到树干上,然后向内核注册回调函数,用于当中断事件来临时向准备就绪链表中插入数据;

  • 执行 epoll_wait() 时立刻返回准备就绪链表里的数据即可。

  • 基于回调函数将就绪事件放入就绪链表中,将遍历的时间O(n)降低到了O(1)
  • 只需要在epoll_ctl时传递一次文件描述符,epoll_wait()不需要再次传递文件描述符,降低了拷贝的开销
  • 基于红黑树+双链表的数据结构,没有最大连接数的限制
  • 23
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值