文章目录
前言
传统的一客服端一线程的模式处理多个客户端的连接请求的模式在只有少数客户端及局域网中的连接的请求时还可以满足要求,但是一旦客户端数量多起来之后,这种模式显然就不合适了,不仅线程占用的资源很多,数量有限,不可能处理大量的连接,而且操作系统调度大量的线程是非常费时的,所以就必须转换思维方式,如果有一种系统功能可以集中检测多个socket的就绪状态,socket一旦就绪,系统马上发出通知让应用程序处理,这样就算是只有一个线程也可以处理大量的客服端连接请求,IO多路复用就是为了解决这个问题而生的,本篇文章主要介绍了linux上的三种IO多路复用模式select,poll,epoll。
扩展延伸:IO多路复用模式和嵌入式mcu的硬件中断颇有相似之处,都能同时监控多个部件的就绪状态,不过mcu检测系统是硬件实现的,IO多路复用是软件实现的,mcu检测的事件是电子信号,IO多路复用检测的是操作系统层级的软件事件。或许可以这样把linux与mcu做比喻,linux是中央大脑,处理复杂的思维活动,mcu是末端神经,对外部物理世界进行感知,并作出条件反射或把感知传递给中央大脑。
一、一客服端一线程
在介绍IO多路复用之前,先把一客服端一线程的模式先介绍一遍,这样更能凸显IO多路复用的优点。
一客服端一线程就是主线程处理listenFd的accept请求,一旦油客户端的连接请求来了,马上创建一个单独的线程专门处理这个客户端的连接请求。下面给出一客服端一线程的代码实现:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <errno.h>
#define MAX_SIZE 1024 // 每次recv接收的最大数据长度
volatile int g_run = true;
void *CilentTask(void *arg)
{
volatile int run = true;
char buff[MAX_SIZE]; // send和recv的缓冲区
int recNum; // 指示recv接收到的数据长度
int clientfd = *(int*)arg;
while(run) {
recNum = recv(clientfd, buff, MAX_SIZE, 0);
if (recNum > 0) {
buff[recNum] = '\0';
printf("recv msg from client: %s\n", buff);
send(clientfd, buff, recNum, 0);
} else if (recNum == 0) { // 客户端断开网络连接
close(clientfd);
break; // 退出线程
}
}
}
int main(int argc, char const *argv[])
{
int ret = 0;
int ListenFd = socket(AF_INET, SOCK_STREAM, 0);
if (ListenFd < 0) {
printf("create socket error: %s(errno: %d)\n", strerror(errno), errno);
return 1;
}
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(9999);
ret = bind(ListenFd, (struct sockaddr *)&servaddr, sizeof(servaddr));
if ( ret == -1) {
printf("bind socket error: %s(errno: %d)\n", strerror(errno), errno);
return 1;
}
ret = listen(ListenFd, 10);
if (ret == -1) {
printf("listen socket error: %s(errno: %d)\n", strerror(errno), errno);
return 1;
}
struct sockaddr_in client;
socklen_t len = sizeof(client);
int clientfd;
while(g_run) {
clientfd = accept(ListenFd, (struct sockaddr *)&client, &len);
if (clientfd < 0) {
printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
return 1;
}
pthread_t threadid;
pthread_create(&threadid, NULL, CilentTask, (void*)(&clientfd));
}
close(ListenFd);
return 0;
}
二、select
select系统调用可以监听多个文件描述符山过的可读,可写和异常事件。但是一个select可以检测的文件描述符的数量最大时1024(可以修改,但是需要重新编译内核,很麻烦),如果要检测超过1024的数量,可以多创建几个线程和对应的select。
这里先介绍select的API,然后给出一个select的应用实列
select API
#include <sys/select.h>
extern int select (int __nfds, fd_set *__restrict __readfds,
fd_set *__restrict __writefds,
fd_set *__restrict __exceptfds,
struct timeval *__restrict __timeout);
__nfds参数指定被监听文件的总数,通常设置为select监听的所有文件描述符的中的最大值加1,因为文件描述符是从0开始计数的。
__readfds,__writefds,__exceptfds参数分别对应文件描述符的集合,应用程序通过这三个参数传入感兴趣的文件描述符。select返回时,内核将修改它们来通知应用程序那些文件描述符符已经就绪。这三个参数是fd_set 结构体的指针类型,fd_set 结构体的定义如下:
#define __FD_SETSIZE 1024
/* Some versions of <linux/posix_types.h> define this macros. */
#undef __NFDBITS
/* It's easier to assume 8-bit bytes than to get CHAR_BIT. */
#define __NFDBITS (8 * (int) sizeof (__fd_mask))
#define __FD_ELT(d) ((d) / __NFDBITS)
#define __FD_MASK(d) ((__fd_mask) 1 << ((d) % __NFDBITS))
/* fd_set for select and pselect. */
typedef struct
{
/* XPG4.2 requires this member name. Otherwise avoid the name
from the global namespace. */
#ifdef __USE_XOPEN
__fd_mask fds_bits[__FD_SETSIZE / __NFDBITS];
# define __FDS_BITS(set) ((set)->fds_bits)
#else
__fd_mask __fds_bits[__FD_SETSIZE / __NFDBITS];
# define __FDS_BITS(set) ((set)->__fds_bits)
#endif
} fd_set;
fd_set结构体包含一个整型数组,该数组的每一个位标记一个文件描述符。fd_set可以容纳的文件描述符总数由__FD_SETSIZE 指定,一般为1024,这限制了select能同时处理文件描述符的总量.
由于位操作很繁琐,可以用下面的一系列宏访问fd_set上的位:
/* Access macros for `fd_set'. */
#define FD_SET(fd, fdsetp) __FD_SET (fd, fdsetp)
#define FD_CLR(fd, fdsetp) __FD_CLR (fd, fdsetp)
#define FD_ISSET(fd, fdsetp) __FD_ISSET (fd, fdsetp)
#define FD_ZERO(fdsetp) __FD_ZERO (fdsetp)
__timeout参数是用来设置超时时间的,一般我们设置为NULL,表示select将一直阻塞,知道某个文件描述符就绪。
select成功时返回就绪文件描述符的总数,如果在超时时间没有文件描述符就绪则返回0,失败返回-1并设置errno,如果在程序等待期间程序接收到信号,则select立即返回-1并设置errno为EINTR。
select 应用实例
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/select.h>
#include <netinet/in.h>
#include <errno.h>
#define MAX_SIZE 1024 // 每次recv 接收的最大数据长度
char g_buff[MAX_SIZE]; // send和recv的缓冲区
volatile int g_run = true;
int main(int argc, char const *argv[])
{
int ret = 0;
int ListenFd = socket(AF_INET, SOCK_STREAM, 0);
if (ListenFd < 0) {
printf("create socket error: %s(errno: %d)\n", strerror(errno), errno);
return 1;
}
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(9999);
ret = bind(ListenFd, (struct sockaddr *)&servaddr, sizeof(servaddr));
if ( ret == -1) {
printf("bind socket error: %s(errno: %d)\n", strerror(errno), errno);
return 1;
}
ret = listen(ListenFd, 10);
if (ret == -1) {
printf("listen socket error: %s(errno: %d)\n", strerror(errno), errno);
return 1;
}
fd_set setreadFd;
FD_ZERO(&setreadFd);
fd_set readFd;
FD_ZERO(&readFd);
fd_set setWriteFd;
FD_ZERO(&setWriteFd);
fd_set WriteFd;
FD_ZERO(&WriteFd);
FD_SET(ListenFd, &setreadFd);
int maxFd = 0;
maxFd = ListenFd;
int readyNum = 0;
int clientfd;
int recvNum = 0;
while(g_run) {
readFd = setreadFd;
WriteFd = setWriteFd;
readyNum = select(maxFd + 1, &readFd, &WriteFd, NULL, NULL);
if (readyNum < 0) {
printf("select error\n");
continue;
}
if (FD_ISSET(ListenFd, &readFd)) {
struct sockaddr_in client;
socklen_t len = sizeof(client);
if ((clientfd = accept(ListenFd, (struct sockaddr *)&client, &len)) == -1) {
printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
return 1;
}
FD_SET(clientfd, &setreadFd);
if (clientfd > maxFd) { // 更新maxFd值
maxFd = clientfd;
}
readyNum = readyNum - 1;
if (readyNum == 0) { // 待处理就绪事件为0
continue;
}
}
for (int i = ListenFd + 1; i <= maxFd; i++) {
if (FD_ISSET(i, &readFd)) {
recvNum = recv(i, g_buff, MAX_SIZE, 0);
if (recvNum > 0) {
g_buff[recvNum] = '\0';
printf("recv msg from client: %s\n", g_buff);
FD_SET(i, &setWriteFd);
} else if (recvNum == 0) {
FD_CLR(i, &setreadFd); //如果客服端断开连接,则把客户端的fd从检测集合中删除
close(i);
}
readyNum = readyNum -1;
if (readyNum == 0) { // 待处理就绪事件为0
break;
}
}
if (FD_ISSET(i, &WriteFd)) {
// 这里的发送buff size用的是recvNum,只是在demo里为了简洁方便才这么做,在实际环境中不应这么做,不能确定recvNum的值是对的。
send(i, g_buff, recvNum, 0);
FD_CLR(i, &setWriteFd); //发送完后把写事件从集合中清除
}
}
}
close(ListenFd);
return 0;
}
三、poll
poll系统调用和select类似,也是在指定时间内轮询一定数量的文件描述符,检查是否油就绪状态的文件描述符。与select不同的是,poll把事件和文件描述符都定义在一个结构体中,任何事件都被统一处理,内核每次修改的是revents成员,而events成员不变,所以每次调用poll不用重置pollfd类型的事件集参数:
poll API
__fortify_function int
poll (struct pollfd *__fds, nfds_t __nfds, int __timeout)
__fds参数是一个pollfd类型的数组,它指定我们感兴趣的文件描述符的可读,可写,异常等事件。pollfd结构体的定义如下
/* Data structure describing a polling request. */
struct pollfd
{
int fd; /* File descriptor to poll. */
short int events; /* Types of events poller cares about. */
short int revents; /* Types of events that actually occurred. */
};
fd指定关心的文件描述符
events告诉poll要监听该fd的那些事件
revents由内核修改,以通知应用程序fd上发生了哪些事件
__nfds参数指定被监听事件集合__fds的大小
__timeout参数指定超时时间,当值为-1时永远阻塞,为0时表示立即返回。
poll系统调用的返回值含义和select的一样。
poll应用实例
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <poll.h>
#include <netinet/in.h>
#include <errno.h>
#define MAX_SIZE 1024 // 每次recv 接收的最大数据长度
#define POLL_SIZE 1024
char g_buff[MAX_SIZE]; // send和recv的缓冲区
volatile int g_run = true;
int main(int argc, char const *argv[])
{
int ret = 0;
int ListenFd = socket(AF_INET, SOCK_STREAM, 0);
if (ListenFd < 0) {
printf("create socket error: %s(errno: %d)\n", strerror(errno), errno);
return 1;
}
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(9999);
ret = bind(ListenFd, (struct sockaddr *)&servaddr, sizeof(servaddr));
if ( ret == -1) {
printf("bind socket error: %s(errno: %d)\n", strerror(errno), errno);
return 1;
}
ret = listen(ListenFd, 10);
if (ret == -1) {
printf("listen socket error: %s(errno: %d)\n", strerror(errno), errno);
return 1;
}
struct pollfd fds[POLL_SIZE] = {0};
for (int i = 0; i < POLL_SIZE; i++) {
fds[i].fd = -1;
}
fds[ListenFd].fd = ListenFd;
fds[ListenFd].events = POLLIN;
int maxFd = ListenFd;
int readyNum = 0;
int clientfd;
int recvNum = 0;
while(g_run) {
int readyNum = poll(fds, maxFd + 1, -1);
if (readyNum < 0) {
printf("select error\n");
continue;
}
if ((fds[ListenFd].revents & POLLIN)) {
struct sockaddr_in client;
socklen_t len = sizeof(client);
if ((clientfd = accept(ListenFd, (struct sockaddr *)&client, &len)) == -1) {
printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
return 1;
}
fds[clientfd].fd = clientfd;
fds[clientfd].events = POLLIN;
if (clientfd > maxFd) { // 更新maxFd值
maxFd = clientfd;
}
readyNum = readyNum - 1;
if (readyNum == 0) { // 待处理就绪事件为0
continue;
}
}
for (int i = ListenFd + 1; i <= maxFd; i++) {
if (fds[i].revents & POLLIN) {
recvNum = recv(i, g_buff, MAX_SIZE, 0);
if (recvNum > 0) {
g_buff[recvNum] = '\0';
printf("recv msg from client: %s\n", g_buff);
fds[i].events = fds[i].events|POLLOUT;
} else if (recvNum == 0) {
fds[i].fd = -1; //如果客服端断开连接,则把客户端的fd从检测集合中删除
close(i);
}
readyNum = readyNum -1;
if (readyNum == 0) { // 待处理就绪事件为0
break;
}
} else if (fds[i].revents & POLLOUT) {
// 这里的发送buff size用的是recvNum,只是在demo里为了简洁方便才这么做,在实际环境中不应这么做,不能确定recvNum的值是对的。
send(i, g_buff, recvNum, 0);
fds[i].events = POLLIN;
}
}
}
close(ListenFd);
return 0;
}
四、epoll
epoll API
epoll是linux特有的IO复用函数。在实现和使用上和select、poll有很大的区别。首先它是由一组函数来完成任务,而不是单个函数。其次epoll把用户关心的文件描述符上的事件放在内核的一个事件表中,这样就不用像select和poll那样每次调用都重复传入文件描述符集和事件集到内核中。但epoll需要一个单独的文件描述符来唯一标识内核中的这个事件表。这文件描述符由这个api创建:
extern int epoll_create (int __size) __THROW;
__size没有作用,只是为了兼容,设置为1就可以了。该函数返回一个文件描述符将用为其他epoll函数的第一个参数,以指向要访问的内核事件表。
下面这个函数用来操作:
extern int epoll_ctl (int __epfd, int __op, int __fd,
struct epoll_event *__event) __THROW;
__op参数是操作类型:EPOLL_CTL_ADD 增加,EPOLL_CTL_MOD修改,EPOLL_CTL_DEL删除
__fd参数是要操作的文件描述符
__event参数描述事件类型,epoll支持的事件与poll的基本相同,但epoll有两个额外的事件类型EPOLLET和EPOLLONESHOT,它们对epoll的高效运作非常关键,后面将介绍它。data成员可以用于存储该文件描述符的用户数据,其定义如下:
typedef union epoll_data
{
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
struct epoll_event
{
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
} __EPOLL_PACKED;
epoll_data_t是一个联合体,用来存储用户数据,用的最多的是fd,用于保存从属的目标文件符,也可以ptr指定自定义的结构类型数据。
epoll——ctl成功时返回0,失败时返回-1,并设置errno。
epoll_wait()函数就是用于阻塞等待文件描述符集合上的事件
extern int epoll_wait (int __epfd, struct epoll_event *__events,
int __maxevents, int __timeout);
__events参数是一个指针类型,通常外部定义一个epoll_event 数组用于从内核中获取就绪的事件集合,把这个数组指针传入这个参数就可以了。
__maxevents参数就是指定每次从内核中取出就绪事件集合的最大数量,和前面说的epoll_event 数组容量大小相同。如果一次取了__maxevents个还没取完,则下次epoll_wait调用继续取。
__timeout是超时时间,-1表示永久阻塞(等有事件就绪时返回),0表示立即返回。
epoll_wait 调用的第二个参数用于输出epoll_wait 检测到的就绪事件,这样就不需要像select、epoll那样遍历整个检测文件描述符集合了,只需要遍历已就绪文件描述符集合即可,这极大的提高了应用程序索引就绪文件描述符的效率。
epoll 应用实例
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/epoll.h>
#include <netinet/in.h>
#include <errno.h>
#define MAX_SIZE 1024 // 每次recv 接收的最大数据长度
#define EPOLL_MAX_SIZE 1024
char g_buff[MAX_SIZE]; // send和recv的缓冲区
volatile int g_run = true;
int main(int argc, char const *argv[])
{
int ret = 0;
int ListenFd = socket(AF_INET, SOCK_STREAM, 0);
if (ListenFd < 0) {
printf("create socket error: %s(errno: %d)\n", strerror(errno), errno);
return 1;
}
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(9999);
ret = bind(ListenFd, (struct sockaddr *)&servaddr, sizeof(servaddr));
if ( ret == -1) {
printf("bind socket error: %s(errno: %d)\n", strerror(errno), errno);
return 1;
}
ret = listen(ListenFd, 10);
if (ret == -1) {
printf("listen socket error: %s(errno: %d)\n", strerror(errno), errno);
return 1;
}
int epollFd = epoll_create(1);
struct epoll_event ev;
ev.data.fd = ListenFd;
ev.events = EPOLLIN ;
ret = epoll_ctl(epollFd, EPOLL_CTL_ADD, ListenFd, &ev);
if(ret == -1) {
printf("epoll_ctl ListenFd error: %s(errno: %d)\n", strerror(errno), errno);
return 1;
}
int readyNum = 0;
int recvNum = 0;
struct epoll_event events[EPOLL_MAX_SIZE];
while(g_run) {
int readyNum = epoll_wait(epollFd, events, EPOLL_MAX_SIZE, -1);
if (readyNum < 0) {
printf("epoll_wait error\n");
continue;
}
printf("epoll_wait fd ...\n");
for (int i = 0; i < readyNum; i++) {
int fd = events[i].data.fd;
if (fd == ListenFd) {
struct sockaddr_in client;
socklen_t len = sizeof(client);
int clientfd;
if ((clientfd = accept(ListenFd, (struct sockaddr *)&client, &len)) == -1) {
printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
return 1;
}
struct epoll_event ev;
ev.data.fd = clientfd;
ev.events = EPOLLIN | EPOLLRDHUP | EPOLLHUP;
ret = epoll_ctl(epollFd, EPOLL_CTL_ADD, clientfd, &ev);
if(ret == -1) {
printf("epoll_ctl clientfd error: %s(errno: %d)\n", strerror(errno), errno);
return 1;
}
printf("accept fd ...\n");
} else if (events[i].events & (EPOLLRDHUP | EPOLLHUP | EPOLLERR)) { //客户端退出连接,要放在EPOLLIN处理的前面
struct epoll_event ev;
ev.data.fd = fd;
ev.events = EPOLLIN | EPOLLRDHUP | EPOLLHUP;
ret = epoll_ctl(epollFd, EPOLL_CTL_DEL, fd, &ev); // 移除对fd的检测
if(ret == -1) {
printf("epoll_ctl clientfd error: %s(errno: %d)\n", strerror(errno), errno);
return 1;
}
close(fd);
printf("close fd ...\n");
} else if (events[i].events & EPOLLIN) {
recvNum = recv(fd, g_buff, MAX_SIZE, 0);
if (recvNum > 0) {
g_buff[recvNum] = '\0';
printf("recv msg from client: %s\n", g_buff);
struct epoll_event ev;
ev.data.fd = fd;
ev.events = EPOLLIN | EPOLLOUT | EPOLLRDHUP |EPOLLHUP;
ret = epoll_ctl(epollFd, EPOLL_CTL_MOD, fd, &ev);
if(ret == -1) {
printf("epoll_ctl clientfd error: %s(errno: %d)\n", strerror(errno), errno);
return 1;
}
}
} else if (events[i].events & EPOLLOUT) {
// 这里的发送buff size用的是recvNum,只是在demo里为了简洁方便才这么做,在实际环境中不应这么做,不能确定recvNum的值是对的。
send(fd, g_buff, recvNum, 0);
struct epoll_event ev;
ev.data.fd = fd;
ev.events = EPOLLIN | EPOLLRDHUP | EPOLLHUP; // 删除写事件
ret = epoll_ctl(epollFd, EPOLL_CTL_MOD, fd, &ev);
if(ret == -1) {
printf("epoll_ctl clientfd error: %s(errno: %d)\n", strerror(errno), errno);
return 1;
}
} else {
printf("unknow erro...\n");
}
}
}
close(ListenFd);
return 0;
}
LT模式和ET模式
epoll对文件描述符的操作有2种模式:LT模式(电平)和ET模式(边沿),LT模式时默认模式,ET模式是eopll的高效模式。
对于采用LT模式,只要文件描述符事件处于就绪状态,每次epoll_wait调用都会通知应用程序处理,知道就绪状态被应用程序处理完。
对于采用ET模式,对于每一次文件描述符就绪事件的触发,epoll_wait值通知应用程序一次,如果应用程序在此次通知未处理,则下次epoll_wait调用不会再通知应用程序。所以应用程序要在此次处理的过程中把就绪事件处理完,不然就会漏掉数据。
ET模式再很大程度上降低了同一个epoll事件被重复触发的次数,因此效率比LT高。
EPOLLONESHOT
在并发程序中,当一个线程在读取了某个socket上的数据后开始处理这些数据,而在处理的过程中该socket上又有新的数据可读,此时事件被触发,又有一个新的线程被唤醒处理这些数据。于是出现了2个线程操作同一个socket的情况。我们期望在同一时刻只有一个线程在处理该socket,避免竟态条件的问题。epoll的EPOLLONESHOT事件可以解决这个问题。
对于注册了EPOLLONESHOT事件的文件描述符,操作系统最多只触发其上的一个可读可写或异常事件,且只触发一次,除非是用epoll_ctl重置该事件就可以再触发一次。
五、三组IO复用的比较
我们从事件集,最大支持文件描述符数量,工作模式,具体实现四个方面来比较它们的异同
事件集合
select:通过山歌参数分别传入可读可写及异常的事件集合,内核通过对这些参数的在线修改反馈其中的就绪事件。这使得每次调用select都要重置这三个参数,比较麻烦。
poll:统一处理所有事件类型,因此秩序一个事件集参数,用户通过pollfd.events传入感兴趣的事件集合,通过pollfd.revents反馈其中的就绪事件。和select一样每次poll调用都需要把pollfd事件集合传给内核
epoll:内核通过一个事件表直接管理事件集合,这样就不用每次调用epoll_wait都反复传入事件集合给内核了。epoll_wait的events参数仅用于反馈就绪的事件。
应用程序索引就绪文件描述符的事件复杂度
select:O(n)
poll:O(n)
epoll:O(1)
最大支持文件描述符数量
select:1024
poll:65535
epoll:65535
支持的工作模式
select:LT
poll: LT
epoll: LT和ET
内核的实现效率
select:采用轮询的方式检测就绪事件,算法时间复杂度为O(n)
poll:采用轮询的方式检测就绪事件,算法时间复杂度为O(n)
epoll:采用回调方式来检测就绪时间,算法时间复杂度为O(1)
就实现效率来说,epoll在连接数量多但活动连接少的情况下效率比poll,select高很多。如果活动连接也很多时,epoll的回调函数被触发的过于频繁,其效率未必比select和poll高
六、总结
本文章介绍了一线程一客户端模式,借此说明为什么需要IO多路复用,然后介绍了linux下三组IO多路复用的使用并给出了其应用实例,最后总结了三组IO多路复用的异同,方便在合适的场景选择合适的IO多路复用方式。