简单直观的理解,I/O(输入输出)多路复用,指的是用一个线程来处理多个I/O连接。linux下常用的I/O复用有三种:select,poll,epoll。
1.select
select函数声明如下:
#include<sys/select.h>
#include<sys/time.h>
int select(int maxfdpl,fd_set *readset,fd_set *writeset,fd_set *exceptset,const struct timeval *timeout);
1.1、select特点
需要注意的是,函数第一个参数maxfdp1,是监听的描述符最大值+1。
第二、三、四个参数分别是监听读、写、异常事件的描述符数组指针。
第四个参数是监听时间。
select使用简单,有较好的通用性。select的时间精度是微秒,常见使用的地方有串口通信,休眠,GPIO引脚检测。在每调用一次之后,需要重新设置监听描述符数组,和超时时间。
1.2、select缺点
select只能监听读、写、异常这三个事件
select描述符最大值有限制,linux内核中是1024
select函数是将待监听的描述符数组传给内核,由内核监听,然后内核返回就绪的描述符,并修改监听事件值。因此存在用户空间切换到内核,内核再切换到用户空间这样的来回切换。因此,当监听的描述符数量巨大,而每次又只有少量描述符上有事件就绪时就存在大量的切换,导致效率低下。
2.poll
poll的函数声明如下:
int poll(struct pollfd fdarray[],unsigned long nfds,int timeout);
struct pollfd{
int fd;
short events;
short revents;
};
2.1 poll特点
poll通过pollfd数组来传递需要关注的事件,不需要计算最大描述符。
poll基于链表实现,因此没有最大值限制。
pollfd中的events字段和revents分别用于标示关注的事件和发生的事件,因此,pollfd数组只需要被初始化一次。
poll的实现机制与select类似,其对应内核中的sys_poll,只不过poll向内核传递pollfd数组,然后对pollfd中的每个描述符进行poll,相比处理fdset来说,poll效率更高。
poll返回后,同样需要对pollfd中的每个元素检查其revents值,以此判断事件是否发生。
2.2 poll缺点
与select相似,同样存在大量的换入换出。
需要轮询pollfd来获取就绪的描述符。
poll时间精度是毫秒。
3.epoll
epoll函数声明如下:
#include <sys/epoll.h>
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
使用epoll实现并发服务器示例如下:
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/epoll.h>
#define MAX_EVENTS 1024 //最多可以等待多少个事件
int main()
{
int server = 0;
struct sockaddr_in saddr = {0};
int client = 0;
struct sockaddr_in caddr = {0};
socklen_t asize = 0;
int len = 0;
char buf[32] = {0};
int maxfd;
int ret = 0;
int i = 0;
server = socket(PF_INET, SOCK_STREAM, 0);
if( server == -1 )
{
printf("server socket error\n");
return -1;
}
saddr.sin_family = AF_INET;
saddr.sin_addr.s_addr = htonl(INADDR_ANY);
saddr.sin_port = htons(8888);
if( bind(server, (struct sockaddr*)&saddr, sizeof(saddr)) == -1 )
{
printf("server bind error\n");
return -1;
}
if( listen(server, 128) == -1 )
{
printf("server listen error\n");
return -1;
}
printf("server start success\n");
struct epoll_event event, events[MAX_EVENTS];
/*创建epoll*/
int epollInstance = epoll_create(0);
if (epollInstance == -1)
{
printf("Failed to create epoll instance\n");
}
/*将服务器添加进入event中*/
event.events = EPOLLIN;
event.data.fd = server;
if (epoll_ctl(epollInstance, EPOLL_CTL_ADD, server, &event) == -1)
{
printf("Failed to add server socket to epoll instance");
}
while( 1 )
{
int numEventsReady = epoll_wait(epollInstance, events, MAX_EVENTS, -1);
if (numEventsReady == -1)
{
printf("Failed to wait for events");
return -1;
}
for(i = 0; i < numEventsReady; i++)
{
if(events[i].data.fd == server)
{
/*有客户端连接上来了*/
asize = sizeof(caddr);
client = accept(server, (struct sockaddr*)&caddr, &asize);
printf("client is connect\n");
event.events = EPOLLIN | EPOLLET;
event.data.fd = client;
if (epoll_ctl(epollInstance, EPOLL_CTL_ADD, client, &event) == -1)
{
printf("Failed to add client socket to epoll instance");
return -1;
}
}
else
{
/*处理客户端的请求*/
len = read(events[i].data.fd, buf, 1024);
if(len == 0)
{
printf("client is disconnect\n");
close(events[i].data.fd);
}
else
{
/*对接收到的数据进行处理*/
printf("read buf : %s\n", buf);
write(events[i].data.fd, buf, len);
}
}
}
}
close(server);
return 0;
}
3.1 epoll特点
epoll常用于网络开发中,实现并发服务器。
epoll在底层实现了自己的高速缓存区,并且建立了一个红黑树用于存放socket,另外维护了一个链表用来存放准备就绪的事件。
epoll能显著减少程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率,因为它不会复用文件描述符集合来传递结果而迫使开发者每次等待事件之前都必须重新准备要被侦听的文件描述符集合。
epoll获取事件的时候,无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了。
3.2 epoll优点
支持大数值的描述符集,大数值的描述符集不直线影响执行效率。
参考博文: