一. 并发服务器介绍
① 单循环服务器:一次只能处理一个客户端任务的服务器。
② 并发服务器:能够同时处理多个客户端任务的服务器。
③ TCP并发服务端:
- 多进程:安全、资源开销大、并发量小
- 多线程:相对进程资源开销小,相对并发量大,系统有崩溃的风险
- 线程池:提前创建好大量线程管理起来,避免反复创建线程带来时间消耗(设计模式:生产者–消费者 )。
- IO多路复用: 对多个文件描述符的监测(select/poll/epoll),复用一个进程。(文件 –> fd)
④ 使用多线程实现并发服务器
//client.c
#include "head.h"
#define SER_ADDR "192.168.0.138"
#define SER_PORT 50000
int CreateTcpConnection(char *pIp, int Port)
{
int sockfd = 0;
int ret = 0;
struct sockaddr_in seraddr;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == sockfd)
{
perror("fail to socket");
return -1;
}
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(Port);
seraddr.sin_addr.s_addr = inet_addr(pIp);
ret = connect(sockfd, (struct sockaddr *)&seraddr, sizeof(seraddr));
if (-1 == ret)
{
perror("fail to connect");
return -1;
}
return sockfd;
}
int main(void)
{
int sockfd = 0;
char tmpbuff[4096] = {0};
int cnt = 0;
ssize_t nret = 0;
sockfd = CreateTcpConnection(SER_ADDR, SER_PORT);
while (1)
{
sprintf(tmpbuff, "hello world —- %d", cnt);
cnt++;
nret = send(sockfd, tmpbuff, strlen(tmpbuff), 0);
if (-1 == nret)
{
perror("fail to send");
return -1;
}
memset(tmpbuff, 0, sizeof(tmpbuff));
nret = recv(sockfd, tmpbuff, sizeof(tmpbuff), 0);
if (-1 == nret)
{
perror("fail to recv");
return -1;
}
else if (0 == nret)
{
break;
}
printf("RECV:%s\\n", tmpbuff);
sleep(1);
}
close(sockfd);
return 0;
}
//server.c
#include "head.h"
#define SER_ADDR "192.168.0.138"
#define SER_PORT 50000
int CreateListenSocket(char *pIp, int Port)
{
int sockfd = 0;
int ret = 0;
struct sockaddr_in seraddr;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == sockfd)
{
perror("fail to socket");
return -1;
}
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(Port);
seraddr.sin_addr.s_addr = inet_addr(pIp);
ret = bind(sockfd, (struct sockaddr *)&seraddr, sizeof(seraddr));
if (-1 == ret)
{
perror("fail to bind");
return -1;
}
ret = listen(sockfd, 10);
if (-1 == ret)
{
perror("fail to listen");
return -1;
}
return sockfd;
}
void *handerclientfun(void *arg)
{
char tmpbuff[4096] = {0};
ssize_t nret = 0;
int confd = (int)arg;
while (1)
{
memset(tmpbuff, 0, sizeof(tmpbuff));
nret = recv(confd, tmpbuff, sizeof(tmpbuff), 0);
if (-1 == nret)
{
perror("fail to recv");
return NULL;
}
else if (0 == nret)
{
break;
}
printf("RECV:%s\\n", tmpbuff);
sprintf(tmpbuff, "%s — echo", tmpbuff);
nret = send(confd, tmpbuff, strlen(tmpbuff), 0);
if (-1 == nret)
{
perror("fail to send");
return NULL;
}
}
return NULL;
}
int main(void)
{
int sockfd = 0;
int confd = 0;
pthread_t tid;
sockfd = CreateListenSocket(SER_ADDR, SER_PORT);
while (1)
{
confd = accept(sockfd, NULL, NULL);
if (-1 == confd)
{
perror("fail to accept");
return -1;
}
pthread_create(&tid, NULL, handerclientfun, (void *)confd);
}
close(confd);
close(sockfd);
return 0;
}
二、Linux常用IO模型
2.1 阻塞IO(默认)
① 效率高,调用 fgets/read/fread/recv /accept等函数,如果文件当前无数据可读或者没有数据到达时,那么调用者将应用程序挂起、进入休眠阻塞状态,直到有数据可读时才会解除阻塞,不占用 CPU;
//read.c
#include "head.h"
int main(void)
{
int fd = 0;
char tmpbuff[4096] = {0};
mkfifo("/tmp/myfifo", 0664);
fd = open("/tmp/myfifo", O_RDONLY);
if (-1 == fd)
{
perror("fail to open");
return -1;
}
while (1)
{
memset(tmpbuff, 0, sizeof(tmpbuff));
read(fd, tmpbuff, sizeof(tmpbuff)); //数据没来会阻塞
printf("FIFO:%s\\n", tmpbuff);
memset(tmpbuff, 0, sizeof(tmpbuff));
gets(tmpbuff);
printf("STDIN:%s\\n", tmpbuff);
}
close(fd);
return 0;
}
//write.c
#include "head.h"
int main(void)
{
int fd = 0;
char tmpbuff[4096] = {0};
mkfifo("/tmp/myfifo", 0664);
fd = open("/tmp/myfifo", O_WRONLY);
if (-1 == fd)
{
perror("fail to open");
return -1;
}
while (1)
{
gets(tmpbuff);
write(fd, tmpbuff, strlen(tmpbuff));
if (0 == strcmp(tmpbuff, ".quit"))
{
break;
}
}
close(fd);
return 0;
}
2.2 非阻塞IO
① 把文件描述符设置为非阻塞,效率低,因为没有数据时, CPU也会在任务中执行代码,轮询检测数据是否到来,浪费CPU资源
② fcntl函数
int fcntl(int fd, int cmd, … /* arg */ );
功能:对已打开的文件描述符 fd 执行各类控制操作,具体操作由 cmd 指定。
参数:
fd:要操作的文件描述符(普通文件、socket、管道、FIFO 都支持)
cmd:控制命令(决定第三个参数是否存在、类型)
arg:可选参数,随 cmd 变化(整数 / 结构体指针)
返回值:
失败-1且errno
成功:不同的cmd返回值不一样
//read.c write.c同上
#include "head.h"
int main(void)
{
int fd = 0;
char tmpbuff[4096] = {0};
int flags = 0;
char *pret = 0;
ssize_t nret = 0;
mkfifo("/tmp/myfifo", 0664);
fd = open("/tmp/myfifo", O_RDONLY);
if (-1 == fd)
{
perror("fail to open");
return -1;
}
//获得fd文件描述符对应的属性
flags = fcntl(fd, F_GETFL);
//在当前属性的基础上加上非阻塞属性
flags |= O_NONBLOCK;
//再将新属性设置到文件描述符中
fcntl(fd, F_SETFL, flags);
flags = fcntl(0, F_GETFL);
flags |= O_NONBLOCK;
fcntl(0, F_SETFL, flags);
while (1)
{
memset(tmpbuff, 0, sizeof(tmpbuff));
nret = read(fd, tmpbuff, sizeof(tmpbuff));
if (nret > 0) //有数据才会触发
{
printf("FIFO:%s\\n", tmpbuff);
}
memset(tmpbuff, 0, sizeof(tmpbuff));
pret = gets(tmpbuff);//有数据才会触发
if (pret != NULL)
{
printf("STDIN:%s\\n", tmpbuff);
}
}
close(fd);
return 0;
}
2.3 信号驱动IO(异步IO)
① 效率高
② 驱动的信号个数有限,应用层无法区分多个IO事件
③ 实现方法:将文件描述符设置为异步IO并指定异步IO通知进程对象,内核监测文件描述符是否有事件发生,当有事件发生时通知用户层,用户层可以捕捉SIGIO信号来处理该文件描述符的事件
//read.c write.c同上
#include "head.h"
int fd = 0;
void handler(int signo)
{
char tmpbuff[4096] = {0};
memset(tmpbuff, 0, sizeof(tmpbuff));
read(fd, tmpbuff, sizeof(tmpbuff));
printf("FIFO:%s\\n", tmpbuff);
return;
}
int main(void)
{
int flags = 0;
char tmpbuff[4096] = {0};
signal(SIGIO, handler);
mkfifo("/tmp/myfifo", 0664);
fd = open("/tmp/myfifo", O_RDONLY);
if (-1 == fd)
{
perror("fail to open");
return -1;
}
//在文件描述符原来的标志位基础上新增O_ASYNC,异步IO标志
flags = fcntl(fd, F_GETFL);
flags |= O_ASYNC;
fcntl(fd, F_SETFL, flags);
//指定异步IO给当前进程发送信号
fcntl(fd, F_SETOWN, getpid());
while (1)
{
memset(tmpbuff, 0, sizeof(tmpbuff));
gets(tmpbuff);
printf("STDIN:%s\\n", tmpbuff);
}
close(fd);
return 0;
}
2.4 IO多路复用(select/poll/epoll)
2.4.1 select
① 在一个进程中,同时监测多个IO。
② 调用 select()会一直阻塞,直到某一个或多个文件描述符成为就绪态(可以读或写)。
③ 步骤:
- 创建文件描述符集合
- 添加需要监测的文件描述符到集合
- 通知内核开始监测
- 由IO事件到达时,监测返回结果
④ 使用方法:select会监听文件描述符集合,当集合中至少有一个文件描述符产生事件时,select不再阻塞,它会将产生事件的文件描述符留在集合中,把没有产生事件的文件描述符从集合中去除。
⑤ 函数接口
void FD_CLR(int fd, fd_set *set);
功能:将文件描述符fd从文件描述符集合中清除
int FD_ISSET(int fd, fd_set *set);
功能:判断文件描述符fd是否仍在集合中
void FD_SET(int fd, fd_set *set);
功能:将文件描述符fd加入文件描述符集合中
void FD_ZERO(fd_set *set);
功能:将文件描述符集合清0
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
功能:通知内核监听文件描述符集合中的文件描述符是否产生事件
参数:
nfds:集合中最大文件描述符的值+1
readfds:读文件描述符集合
writefds:写文件描述符集合
exceptfds:其余文件描述符集合
timeout:超时时间
NULL: 阻塞等待
返回值:
成功返回实际产生事件的文件描述符个数
失败返回-1
时间达到没有事件发生返回0
练习1:使用管道实现select
//read.c write.c 同上
#include "head.h"
int main(void)
{
int fd = 0;
char tmpbuff[4096] = {0};
fd_set rdfds; //文件描述符集合
fd_set tmpfds;//文件描述符集合
int maxfd = 0;
int nready = 0;
mkfifo("/tmp/myfifo", 0664);
fd = open("/tmp/myfifo", O_RDONLY);
if (-1 == fd)
{
perror("fail to open");
return -1;
}
FD_ZERO(&rdfds);
FD_SET(fd, &rdfds); //将fd文件描述符加入到集合中
FD_SET(0, &rdfds);//将0文件描述符加入到集合中
maxfd = fd;
while (1)
{
tmpfds = rdfds;
nready = select(maxfd+1, &tmpfds, NULL, NULL, NULL);
if (-1 == nready)
{
perror("fail to select");
return -1;
}
if (FD_ISSET(fd, &tmpfds)) //fd在集合中,会移除0
{
memset(tmpbuff, 0, sizeof(tmpbuff));
read(fd, tmpbuff, sizeof(tmpbuff));
printf("FIFO:%s\\n", tmpbuff);
}
else if (FD_ISSET(0, &tmpfds)) //0在集合中,会移除fd
{
memset(tmpbuff, 0, sizeof(tmpbuff));
gets(tmpbuff);
printf("STDIN:%s\\n", tmpbuff);
}
}
close(fd);
return 0;
}
练习2:通过select实现TCP并发服务器模型,允许多个客户端同时访问服务器
//client.c
#include "head.h"
#define SER_ADDR "192.168.0.138"
#define SER_PORT 50000
int CreateTcpConnection(char *pIp, int Port)
{
int sockfd = 0;
int ret = 0;
struct sockaddr_in seraddr;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == sockfd)
{
perror("fail to socket");
return -1;
}
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(Port);
seraddr.sin_addr.s_addr = inet_addr(pIp);
ret = connect(sockfd, (struct sockaddr *)&seraddr, sizeof(seraddr));
if (-1 == ret)
{
perror("fail to connect");
return -1;
}
return sockfd;
}
int main(void)
{
int sockfd = 0;
char tmpbuff[4096] = {0};
int cnt = 0;
ssize_t nret = 0;
sockfd = CreateTcpConnection(SER_ADDR, SER_PORT);
while (1)
{
sprintf(tmpbuff, "hello world —- %d", cnt);
cnt++;
nret = send(sockfd, tmpbuff, strlen(tmpbuff), 0);
if (-1 == nret)
{
perror("fail to send");
return -1;
}
memset(tmpbuff, 0, sizeof(tmpbuff));
nret = recv(sockfd, tmpbuff, sizeof(tmpbuff), 0);
if (-1 == nret)
{
perror("fail to recv");
return -1;
}
else if (0 == nret)
{
break;
}
printf("RECV:%s\\n", tmpbuff);
sleep(1);
}
close(sockfd);
return 0;
}
//server.c
#include "head.h"
#define SER_ADDR "192.168.0.138"
#define SER_PORT 50000
int CreateListenSocket(char *pIp, int Port)
{
int sockfd = 0;
int ret = 0;
struct sockaddr_in seraddr;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == sockfd)
{
perror("fail to socket");
return -1;
}
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(Port);
seraddr.sin_addr.s_addr = inet_addr(pIp);
ret = bind(sockfd, (struct sockaddr *)&seraddr, sizeof(seraddr));
if (-1 == ret)
{
perror("fail to bind");
return -1;
}
ret = listen(sockfd, 10);
if (-1 == ret)
{
perror("fail to listen");
return -1;
}
return sockfd;
}
int handerclientfun(int confd)
{
char tmpbuff[4096] = {0};
ssize_t nret = 0;
memset(tmpbuff, 0, sizeof(tmpbuff));
nret = recv(confd, tmpbuff, sizeof(tmpbuff), 0);
if (-1 == nret)
{
perror("fail to recv");
return -1;
}
else if (0 == nret)
{
return 0;
}
printf("RECV:%s\\n", tmpbuff);
sprintf(tmpbuff, "%s — echo", tmpbuff);
nret = send(confd, tmpbuff, strlen(tmpbuff), 0);
if (-1 == nret)
{
perror("fail to send");
return -1;
}
return 0;
}
int main(void)
{
int sockfd = 0;
int confd = 0;
pthread_t tid;
fd_set rdfds;
fd_set tmpfds;
int maxfd = 0;
int nready = 0;
int i = 0;
int ret = 0;
sockfd = CreateListenSocket(SER_ADDR, SER_PORT);
FD_ZERO(&rdfds);
FD_SET(sockfd, &rdfds);
maxfd = sockfd;
while (1)
{
tmpfds = rdfds;
nready = select(maxfd+1, &tmpfds, NULL, NULL, NULL);
if (-1 == nready)
{
perror("fail to select");
return -1;
}
if (FD_ISSET(sockfd, &tmpfds))
{
confd = accept(sockfd, NULL, NULL);
if (-1 == confd)
{
perror("fail to accept");
FD_CLR(sockfd, &rdfds);
close(sockfd);
return -1;
}
FD_SET(confd, &rdfds);
maxfd = confd > maxfd ? confd : maxfd;
}
for (i = 4; i <= maxfd;i++)
{
if (FD_ISSET(i, &tmpfds))
{
ret = handerclientfun(i);
if (-1 == ret)
{
FD_CLR(i, &rdfds);
close(i);
continue;
}
}
}
}
return 0;
}
2.4.2 epoll
① 实现方法:
- 创建套接字并监听: 使用 socket、bind、listen 创建并启动一个监听套接字。
- 创建 epoll 实例: 使用 epoll_create 创建一个 epoll 实例。
- 注册监听套接字到 epoll: 使用 epoll_ctl 将监听套接字添加到 epoll 实例中。
- 等待事件: 使用 epoll_wait 等待文件描述符上的事件。
- 处理事件: 根据触发的事件类型(新的连接或现有连接上的数据)进行相应处理。
② epoll 的优点如下:
-
高效的事件通知机制:epoll 是 Linux 下多路复用 I/O 接口 select/poll 的增强版本。在大量并发连接但仅有少量活跃的场景下,它能显著提升系统 CPU 利用率。一方面,epoll 会复用文件描述符集合来传递结果,开发者无需在每次等待事件前重新准备待监听的文件描述符集合;另一方面,获取事件时无需遍历整个被监听的描述符集,只需遍历那些被内核 I/O 事件异步唤醒并加入就绪队列的描述符即可。
-
丰富的触发模式:epoll 除了提供 select/poll 支持的电平触发(Level Triggered)模式外,还提供了边沿触发(Edge Triggered)模式。边沿触发模式允许用户空间程序缓存 I/O 状态,减少 epoll_wait 的调用次数,从而进一步提高应用程序效率。
-
高效的数据结构:epoll 内部采用红黑树(一种自平衡二叉查找树)来管理文件描述符集合,查找、插入和删除操作的时间复杂度均为 O(log n),保证了大规模文件描述符管理的高效性。
-
内核态维护事件表:epoll 将事件表直接创建在内核空间,避免了每次等待事件时在应用层与内核层之间反复拷贝文件描述符集合的开销。
-
精确的事件返回:epoll_wait 直接返回已就绪的事件集合,应用程序无需遍历整个监听集合进行判断,返回的即为已触发的事件,减少了无效遍历。
-
双模式工作能力:epoll 既可以工作在电平触发模式,也可以工作在边缘触发模式,为不同场景提供了灵活的选择。
③ 函数接口
(1)epoll_create
int epoll_create(int size);
功能:创建内核监听事件表
参数:size:事件表中事件个数(忽略)
返回值:
成功返回文件描述符
失败返回-1
(2)epoll_ctl
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
功能:操作epoll的事件表
参数:
epfd:epoll事件表的文件描述符
op:操作的方法
EPOLL_CTL_ADD 向事件表中添加事件
EPOLL_CTL_MOD 修改事件表中的事件
EPOLL_CTL_DEL 从事件表中删除事件
fd:要操作的文件描述符
event:文件描述符关联的事件
返回值:
成功返回0
失败返回-1
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_IN:是否可以进行读操作
EPOLL_OUT: 是否可以进行写操作
EPOLLET:设置为边沿触发模式(默认为水平触发)
(3)epoll_wait
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
功能:监听事件表中的事件是否发生并等待返回监测到的事件结果
参数:
epfd:文件描述符集合
events:存放产生事件的数组的空间首地址
maxevents:存放产生事件的数组的大小,监测的事件最大个数
timeout:超时时间
-1:阻塞直到事件发生
返回值:
成功返回发生事件的文件描述符个数
失败返回-1
时间到达没有产生事件返回0
练习1:使用管道实现epoll
#include "head.h"
int main(void)
{
int epfd = 0;
int fd = 0;
int ret = 0;
int nready = 0;
int i = 0;
char tmpbuff[4096] = {0};
struct epoll_event event;
struct epoll_event revents[2];
mkfifo("/tmp/myfifo", 0664);
fd = open("/tmp/myfifo", O_RDONLY);
if (-1 == fd)
{
perror("fail to open");
return -1;
}
epfd = epoll_create(2);
if (-1 == epfd)
{
perror("fail to epoll_create");
return -1;
}
event.events = EPOLLIN;
event.data.fd = fd;
ret = epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event);
if (-1 == ret)
{
perror("fail to epoll_ctl");
return -1;
}
event.events = EPOLLIN;
event.data.fd = 0;
ret = epoll_ctl(epfd, EPOLL_CTL_ADD, 0, &event);
if (-1 == ret)
{
perror("fail to epoll_ctl");
return -1;
}
while (1)
{
nready = epoll_wait(epfd, revents, 2, -1);
if (-1 == nready)
{
perror("fail to epoll_wait");
return -1;
}
for (i = 0; i < nready; i++)
{
if (revents[i].data.fd == fd)
{
memset(tmpbuff, 0, sizeof(tmpbuff));
read(fd, tmpbuff, sizeof(tmpbuff));
printf("FIFO:%s\\n", tmpbuff);
}
else if (revents[i].data.fd == 0)
{
memset(tmpbuff, 0, sizeof(tmpbuff));
gets(tmpbuff);
printf("STDIN:%s\\n", tmpbuff);
}
}
}
close(fd);
return 0;
}
练习2:通过epoll实现TCP并发服务器模型,允许多个客户端同时访问服务器
//server.c client.c同上
#include "head.h"
#define SER_ADDR "192.168.0.138"
#define SER_PORT 50000
int CreateListenSocket(char *pIp, int Port)
{
int sockfd = 0;
int ret = 0;
struct sockaddr_in seraddr;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == sockfd)
{
perror("fail to socket");
return -1;
}
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(Port);
seraddr.sin_addr.s_addr = inet_addr(pIp);
ret = bind(sockfd, (struct sockaddr *)&seraddr, sizeof(seraddr));
if (-1 == ret)
{
perror("fail to bind");
return -1;
}
ret = listen(sockfd, 10);
if (-1 == ret)
{
perror("fail to listen");
return -1;
}
return sockfd;
}
int handerclientfun(int confd)
{
char tmpbuff[4096] = {0};
ssize_t nret = 0;
memset(tmpbuff, 0, sizeof(tmpbuff));
nret = recv(confd, tmpbuff, sizeof(tmpbuff), 0);
if (-1 == nret)
{
perror("fail to recv");
return -1;
}
else if (0 == nret)
{
return 0;
}
printf("RECV:%s\\n", tmpbuff);
sprintf(tmpbuff, "%s — echo", tmpbuff);
nret = send(confd, tmpbuff, strlen(tmpbuff), 0);
if (-1 == nret)
{
perror("fail to send");
return -1;
}
return 0;
}
int AddEvent(int epfd, int fd)
{
struct epoll_event env;
int ret = 0;
env.events = EPOLLIN;
env.data.fd = fd;
ret = epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &env);
if (-1 == ret)
{
perror("fail to epoll_ctl");
return -1;
}
return 0;
}
int DelEvent(int epfd, int fd)
{
int ret = 0;
ret = epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
if (-1 == ret)
{
perror("fail to epoll_ctl");
return -1;
}
return 0;
}
int main(void)
{
int sockfd = 0;
int confd = 0;
int nready = 0;
int i = 0;
int ret = 0;
int epfd = 0;
struct epoll_event retenv[4096];
sockfd = CreateListenSocket(SER_ADDR, SER_PORT);
epfd = epoll_create(4096);
if (-1 == epfd)
{
perror("fail to epoll_create");
return -1;
}
AddEvent(epfd, sockfd);
while (1)
{
nready = epoll_wait(epfd, retenv, 4096, -1);
if (-1 == nready)
{
perror("fail to epoll_wait");
return -1;
}
for (i = 0; i < nready; i++)
{
if (sockfd == retenv[i].data.fd)
{
confd = accept(sockfd, NULL, NULL);
if (-1 == confd)
{
perror("fail to accept");
DelEvent(epfd, sockfd);
close(sockfd);
continue;
}
AddEvent(epfd, confd);
}
else
{
ret = handerclientfun(retenv[i].data.fd);
if (-1 == ret)
{
DelEvent(epfd, retenv[i].data.fd);
close(retenv[i].data.fd);
continue;
}
}
}
}
return 0;
}
2.4.3 select和epoll的区别
① select特点:
- select监听的文件描述符集合本质是一个数组(位图),有上限限制,最多允许监测1024个文件描述符;
- select监听的文件描述符集合在应用层,当有事件发生时,需要由内核层向应用层发送通知,反复数据拷贝,资源开销大;
- select只能工作在水平触发模式(低速模式);
- select需要用户手动查找产生事件的文件描述符;
② poll是使用链表保存文件描述符集合,理论上没有文件描述符限制;其他特点和select一样。
③ epoll特点:
- epoll监听的事件表没有上限限制;
- epoll监听的事件表在内核中,内核可以监听事件表中的文件描述符集合是否有事件发生,所以无需完成用户层和内核层空间的拷贝,减小资源开销;
- epoll可以工作在水平触发(低速模式,默认模式)和边沿触发模式(高速模式);
- epoll可以自动获得产生事件的文件描述符;
- epoll 在内核中使用红黑树(一种自平衡二叉搜索树,属于树形结构)来存储和管理所有已注册的被监控文件描述符(fd)集合。
- 完整的 epoll 内核实现包含两个核心数据结构:
-
数据结构类型作用时间复杂度 红黑树(rbr) 树形结构 存储所有被监控的 fd 及其事件配置(监控全集) 增删查 O(log n) 就绪链表(rdllist) 双向链表 存储当前已触发事件的就绪 fd(结果集) 插入/读取 O(1)
2.4.4 IO多路复用的应用场景





