简要介绍select-poll-epoll以及IO多路复用

95 阅读3分钟

作者 basilguo@163.com

日期 Aug. 18, 2022

简介 本文介绍select-poll-epoll,IO多路复用

[TOC]

select、poll、和epoll是用于实现多路复用的,即一个线程使用它们即可使用多个socket。由此,线程不能被任何一个被管理的Socket阻塞,且任意一个Socket来数据之后都得告知select/poll/epoll线程。

0. IO多路复用(multiplexing)

操作系统在处理IO的时候,主要有两个阶段:

  1. 等待数据传输到IO设备;
  2. IO设备将数据复制到用户空间。

IO多路复用是指在多个文件描述符fd上执行IO操作的能力。通常输入操作如读取、接收以及接收消息调用将会在没有入数据时阻塞,这会导致错过其他fd的数据。

IO多路复用在调用时阻塞进程,返回时返回给进程fd集合,这些fd已经可以IO了(ready状态)。进程可以为这些fd处理IO,直到下一次IO多路复用调用。

IO多路复用使用场景:

  1. 当客户处理多个描述字时(一般是交互式输入和网络套接口),必须使用I/O复用。
  2. 当一个客户同时处理多个套接口时,而这种情况是可能的,但很少出现。
  3. 如果一个TCP服务器既要处理监听套接口,又要处理已连接套接口,一般也要用到I/O复用。
  4. 如果一个服务器即要处理TCP,又要处理UDP,一般要使用I/O复用。
  5. 如果一个服务器要处理多个服务或多个协议,一般要使用I/O复用。

最好的教程就是Man Page,而最好的解释方式则是使用代码真正运行。有关三个函数的在线man page以及Linux中的实现方式如下:

select

#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,
    fd_set *execptfds, struct timeval *timeout);

select监视3个fd集合,分别是读(readfds)、写(writefds)、异常(exceptfds),可能为NULLnfds时最高fd号加1。timeout值为NULL表示永久阻塞。

当3个fd集合中有一个或多个ready for IO时,或者超时时,select返回。

优点:直接将fd_set拷贝到了内核态。 缺陷:它有监视数量限制,即最大为FD_SETSIZE,是一个常数1024。fd_set不可重用。用户态和内核态切换仍有开销。多次遍历O(n)

sockfd = socket(AF_INET, SOCK_STREAM, 0);
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(2000);
addr.sin_addr.s_addr = IADDR_ANY;
bind(sockfd, (struct sockaddr*)&addr, sizeof(addr));
listen(sockfd, 5);

for (i=0; i<5; ++i)
{
	memset(&client, 0, sizeof(client));
	addrlen = sizeof(client);
	fds[i] = accept(sockfd, (struct sockaddr*)&client, &addrlen);
	if (fds[i] > max)
		max = fds[i];
}

while (1)
{
	FD_ZERO(&rset);       // rset本质是一个bitmap,默认最大为1024位
	for (i=0; i<5; ++i)
	{
		FD_SET(fds[i], &rset);
	}
	puts("round again");
	select(max+1, &rset, NULL, NULL, NULL);

	for (i=0; i<5; ++i)
	{
		if (FD_ISSET(fds[i], &rset))
		{
			memset(buffer, 0, MAXBUF);
			read(fds[i], buffer, MAXBUF);
			puts(buffer);
		}
	}
}

poll

/*
struct pollfd
{
	int fd;
	short events;
	short revents;
};
*/

for (i=0; i<5; ++i)
{
	memset(&client, 0, sizeof(client));
	addrlen = sizeof(client);
	pollfds[i].fd = accept(sockfd, (struct sockaddr*)&client, &addrlen);
	pollfds[i].events = POLLIN;
}
sleep(1);
while (1)
{
	puts("round again");
	poll(pollfds, 5, 50000);
	for (i=0; i<5; ++i)
	{
		if (pollfds[i].revents & POLLIN)
		{
			pollfds[i].revents = 0;
			memset(buffer, 0, MAXBUF);
			read(pollfds[i].fd, buffer, MAXBUF);
			puts(buffer);
		}
	}
}

epoll

struct epoll_event events[5];
int epfd = epoll_create(10);
...
...
for (i=0; i<5; ++i)
{
	static struct epoll_event ev;
	memset(&client, 0, sizeof(client));
	addrlen = sizeof(client);
	ev.data.fd = accept(sockfd, (struct sockaddr*)&client, &addrlen);
	ev.events = EPOLLIN;
	epoll_ctl(epfd, EPOLL_CTL_ADD, ev.data.fd, &ev);
}

while (1)
{
	puts("roud again");
	nfds = epoll_wait(epfd, events, 5, 10000);
	for (i=0; i<nfds; ++i)
	{
		memset(buffer, 0, MAXBUF);
		read(events[i].data.fd, buffer, MAXBUF);
		puts(buffer);
	}
}

参考文献

  1. select v.s. poll v.s. epoll
  2. Socket programming using the select system call - SoftPrayog
  3. I/O multiplexing: select, poll and epoll in Linux - SoftPrayog
  4. 【并发】IO多路复用select/poll/epoll介绍
  5. IO多路复用之select总结