IO多路复用(select、poll、epoll)
一直都对IO多路复用搞不清楚,写篇文章将所看到的内容记录一下,防止遗忘。
IO多路复用
IO多路复用就是使用内核机制来轮询一组文件描述符,监视这写fd是否有IO事件发生,如果有IO发生程序会被告之。
IO 多路复用的方式主要有 select、poll、epoll,这三个函数都会进行阻塞,所以可以放在 while(true)循环里使用,不会造成 CPU 的空转
select
select()系统调用提供了一种实现同步多路复用的机制
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
select 只有一个函数,调用 select 时,需要将监听句柄和最大等待时间作为参数传递进去,select 会发生阻塞,直到一个事件发生了,或者等到最大 1 秒钟(tv 定义了这个时间长度)就返回。
select方法的参数
-
读文件描述符集合、写文件描述符集合、异常描述符集合、超时时间
-
一般更关心文件读操作,所以其他位置可以置为null,第二个参数一般是引用一个bitmap用来代表哪些文件描述符是被监听的。
select源码
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <wait.h>
#include <signal.h>
#include <errno.h>
#include <sys/select.h>
#include <sys/time.h>
#include <unistd.h>
#define MAXBUF 256
void child_process(void)
{
sleep(2);
char msg[MAXBUF];
struct sockaddr_in addr = {0};
int n, sockfd,num=1;
srandom(getpid());
/* Create socket and connect to server */
sockfd = socket(AF_INET, SOCK_STREAM, 0);
addr.sin_family = AF_INET;
addr.sin_port = htons(2000);
addr.sin_addr.s_addr = inet_addr("127.0.0.1");
connect(sockfd, (struct sockaddr*)&addr, sizeof(addr));
printf("child {%d} connected \n", getpid());
while(1){
int sl = (random() % 10 ) + 1;
num++;
sleep(sl);
sprintf (msg, "Test message %d from client %d", num, getpid());
n = write(sockfd, msg, strlen(msg)); /* Send message */
}
}
int main()
{
char buffer[MAXBUF];
int fds[5];
struct sockaddr_in addr;
struct sockaddr_in client;
int addrlen, n,i,max=0;;
int sockfd, commfd;
fd_set rset;
for(i=0;i<5;i++)
{
if(fork() == 0)
{
child_process();
exit(0);
}
}
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 = INADDR_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);
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);
}
}
}
return 0;
}
源码中比较重要的就是main部分,上半部分主要做了两件事:
-
创建socket客户端
-
创建了5个文件描述符,并把这五个文件描述符放入了数组中。
select函数的执行流程:
-
select是阻塞函数,当没有数据时,会一直阻塞在select那一行。
-
当有数据时将rset中对应的那一位置为1
-
select函数返回,不再阻塞
-
遍历文件描述符数组,判断哪个fd被置位了。
-
读取数据,然后处理
select函数的缺点:
-
bitmap默认大小为1024,虽然可以调整,但还是有限度的。
-
rset每次循环都必须重新置位为0,不可重复使用。
-
尽管将rset从用户态拷贝到内核态,由内核态判断是否有数据,但是还是有拷贝的开销。
-
当有数据时select就会返回,但是select函数并不知道哪个文件描述符有数据了,后面还需要再次对文件描述符数组进行遍历。效率比较低。
poll
与select()不同,它具有低效的三个基于位掩码的文件描述符集,poll()使用单个nfds pollfd结构数据。原型更简单:
int poll (struct pollfd *fds, unsigned int nfds, int timeout);
poll的参数:
-
自定义的结构体数组
-
数组的长度
-
超时时间
pollfd 结构对事件和返回事件有不同的字段,因此不需要每次都创建它
struct pollfd {
int fd;
short events;
short revents;
};
-
fd: 文件描述符
-
events:在意的事件是什么,如果在于读就是pollin,如果在意写就是pollout
-
revents:对events的回馈,开始时为0,当有数据可读时就置为pollin,类似于select的rset
将上面示例更改为使用poll:
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);
}
}
}
就像对 select 所做的那样,我们需要检查每个 pollfd 对象以查看其文件描述符是否已准备好,但不需要每次迭代都构建集合
poll的执行流程:
-
将5个fd从用户态拷贝到内核态
-
poll为阻塞方法,执行poll方法,如果有数据会将fd对应的revents置位pollin
-
poll方法返回
-
循环遍历,查找哪个fd被置位为pollin了
-
将revents重新位置0,便于复用
-
对置位的fd进行读取和处理
相比于select解决了bitmap大小限制问题和rset不可重用的情况。
epoll
Epoll系统调用在内核中创建和管理上下文。将任务分为 3 个步骤:
-
使用 epoll_create 在内核中创建上下文
-
使用 epoll_ctl 在上下文中添加和删除文件描述符
-
使用 epoll_wait 等待上下文中的事件
将上面的代码示例改为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("round 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);
}
}
epoll的执行流程:
-
当有数据的时候,会把相应的文件描述符"置位",但是epoll没有revent标志位,所以并不是真正的置位。这时候会把有数据的文件描述符放到队首。
-
epoll会返回有数据的文件描述符的个数
-
根据返回的个数 读取前N个文件描述即可
-
读取、 处理
Epoll 、select 和 poll
-
可以在等待时添加和删除文件描述符
-
epoll_wait 仅返回具有就绪文件描述符的对象
-
epoll 具有更好的性能 O(1) 而不是 O(n)
-
epoll 可以表现为水平触发或边缘触发
-
epoll 是特定于 Linux 的,因此不可移植