一.相关概念
问:什么是I/O多路复用?
答:支持某个进程同时监控多个文件描述符(管道,socket)的可读、可写、异常状态,以便高效的处理并发的I/O事件。
二.多路I/O复用技术的三种实现方式
1.select
#define MAX_CLIENTS 10
#define BUFFER_SIZE 1024
int main()
{
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in addr = {
.sin_family = AF_INET,
.sin_port = htons(8080),
.sin_addr.s_addr = INADDR_ANY
};
bind(server_fd, (struct sockaddr*)&addr, sizeof(addr));
listen(server_fd, 5);
1.创建文件描述符集合
fd_set read_fds;//创建文件句柄集合(本质是一个bitmp)
int client_fds[MAX_CLIENTS] = {0};
int max_fd = server_fd;
while (1)
{
2.初始化文件描述符集合(将bitmap全部置为0)
FD_ZERO(&read_fds);
3.将需要进行监控的文件描述符加入集合以便内核进行监控
FD_SET(server_fd, &read_fds); // 监控服务端套接字
注意事项:
FD_SET是将bitmap中文件描述符(本质是一个正整数)对应的那一位有0变为1
// 添加所有客户端套接字到集合
for (int i = 0; i < MAX_CLIENTS; i++)
{
if (client_fds[i] > 0) {
FD_SET(client_fds[i], &read_fds);
if (client_fds[i] > max_fd) max_fd = client_fds[i];
}
}
4.调用 select,阻塞等待事件
int activity = select(max_fd + 1, &read_fds, NULL, NULL, NULL);
参数1:文件句柄的最大值+1;最大为1024;参数用于内核循环遍历bitmap次数,以便确认那些描述符需要被监控
参数2:读文件描述符集合。
参数3:写文件描述符集合
参数4:错误文件描述符集合
参数5:超时时间,NULL:永不超时 0:立即返回
注意事项:
作用:内核去遍历bitmap,然后确定哪些文件描述符需要被监控,如果所有文件描述符等待的资源都没有就绪则该进程阻塞。如果某个文件描述符所需资源到达,则将该文件描述改为就绪状态,并返回就绪文件描述符的个数。
if (activity < 0) {
perror("select() 错误");
break;
}
// 处理新连接
5.获取就绪状态的文件描述符
if (FD_ISSET(server_fd, &read_fds)) {//标示有新的连接请求过来
int new_fd = accept(server_fd, NULL, NULL);
for (int i = 0; i < MAX_CLIENTS; i++) {
if (client_fds[i] == 0) {
client_fds[i] = new_fd;
printf("新客户端连接: fd=%d\n", new_fd);
break;
}
}
}
// 处理客户端数据
for (int i = 0; i < MAX_CLIENTS; i++)
{
int fd = client_fds[i];
if (fd > 0 && FD_ISSET(fd, &read_fds)) {
char buffer[BUFFER_SIZE];
int len = recv(fd, buffer, sizeof(buffer), 0);
if (len <= 0)
{
close(fd);
client_fds[i] = 0; // 移除客户端
printf("客户端断开: fd=%d\n", fd);
} else {
buffer[len] = '\0';
printf("收到数据: %s\n", buffer);
send(fd, buffer, len, 0); // 回显数据
}
}
}
}
close(server_fd);
return 0;
}
总结:
第一步:创建文件描述符集合
第二步:初始化文件描述符集合
第三步:将需要监控的文件描述符放入集合以便监控
第四步:调用select进行进程阻塞,等待资源到达
第五步:获取资源就位的文件描述符并执行下一步
注意事项:
select 之后需要清空文件描述符集合(清空链表)然后给文件描述符集合重新赋值,应为select之后描述符集合只保留了被触发事件的文件描述符。
2.poll
poll和select一样
仅仅是将文件描述符集合的表现方式给替换了一下
select采用的是bitmap,而poll采用的是结构体数组,但解决了最大监听文件描述符个数1024的限制
struct pollfd
{
int fd; // 文件描述符
short events; // 监控的事件(输入)
short revents; // 实际发生的事件(输出)由内核控制
};
#define MAX_CLIENTS 10
int main()
{
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in addr =
{
.sin_family = AF_INET,
.sin_port = htons(8080),
.sin_addr.s_addr = INADDR_ANY
};
bind(server_fd, (struct sockaddr*)&addr, sizeof(addr));
listen(server_fd, 5);
int client_fds[MAX_CLIENTS] = {0};
1.创建文件描述符集合
struct pollfd read_fds[5];
while (1)
{
for (int i = 0; i < 5; i++)
{
if (client_fds[i] > 0)
{
2.将需要监控的文件描述符放入集合
read_fds[i].fd=server_fd;
read_fds[i].events=POLLIN; //关注可读事件
}
}
for(int i = 0; i < 5; i++)
{
3.调用 poll函数,阻塞等待事件
ret = poll(read_fds, 5, 5000);
参数2:文件描述符结构体中存有的文件描述符个数
参数3:具体数字:超时时间,单位是ms
0:不阻塞:直接返回
INFTIM:阻塞:资源未就绪时,进程一直等待
返回值:
-1:函数调用失败,同时会自动设置全局变量errno
0:数组中的文件描述符均未就绪,poll函数执行超时(此例子中5s超时)
if (ret == -1)
{
perror("poll error");
}
else if (ret == 0)
{
printf("Timeout, no data received.\n");
}
else
{
4.获取已就绪的事件
if (fds[i].revents & POLLIN)
{
5.执行后续操作并清除当前已执行的事件
fds[i].revents=0;
printf("Data is available on stdin.\n");
}
if (fds[i].revents & POLLERR)
{
printf("Error on stdin.\n");
}
}
}
}
注意事项:
每次调用poll函数之前,不需要初始化文件描述符集合(select需要调用FD_SERO()),但是需要在正确处理完成描述符对应的事件之后,手动对该事件进行清理(revents置为0,fds[i].revents=0)
3.epoll
#define MAX_EVENTS 10
#define PORT 8080
int set_nonblocking(int fd)
{
int flags = fcntl(fd, F_GETFL, 0);
作用:对文件描述符对应的文件进行加锁或解锁
参数1:文件描述符
参数2:操作指令
F_GETFL:取得文件描述符状态
F_SETFL:设置文件描述符状态
参数3: 可选参数 与第二个参数配合使用
返回值:
-1调用函数失败
其它:文件描述符的状态或锁信息
return fcntl(fd, F_SETFL, flags | O_NONBLOCK);//将文件设置为非阻塞状态
}
int main() {
int listen_fd, epfd, n;
struct sockaddr_in addr;
struct epoll_event ev, events[MAX_EVENTS];
listen_fd = socket(AF_INET, SOCK_STREAM, 0);
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY;
addr.sin_port = htons(PORT);
bind(listen_fd, (struct sockaddr*)&addr, sizeof(addr));
listen(listen_fd, SOMAXCONN);
1.创建 epoll 实例
epfd = epoll_create1(0);
注意事项:
epoll_create1(0)和epoll_create(1)一样
epoll_create1(EPOLL_CLOEXEC):若父进程通过fork创建子进程,子进程会自动关闭继承自父进程的epoll实例。
2.给epoll实例绑定需要监听的事件及文件描述符
ev.events = EPOLLIN;//读事件
ev.data.fd = listen_fd;//文件描述符
epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, &ev);
参数1:epoll实例
参数2:操作类型(EPOLL_CTL_ADD/MOD/DEL)
参数3:要操作的文件描述符
参数4:需要监控的事件及数据
while (1)
{
3.阻塞等待事件被触发,并返回就绪的事件列表
n = epoll_wait(epfd, events, MAX_EVENTS, -1);
参数2:返回就绪事件列表
参数3:最多接受就绪事件的个数
参数4:超时时间(毫秒,-1为阻塞)
for (int i = 0; i < n; i++)
{
if (events[i].data.fd == listen_fd) {
// 接受新连接
int conn_fd = accept(listen_fd, NULL, NULL);
set_nonblocking(conn_fd); // 非阻塞模式(ET 必须)
ev.events = EPOLLIN | EPOLLET;
注意事项:
EPOLLET:边缘触发模式(默认为水平触发),边缘触发模式必须提前设置文件描述符为非阻塞模式。
水平触发:只要文件描述符对应的资源就绪,且文件描述符未被关闭时,epoll_wait就会一直返回该就绪的文件描述符。所以成功处理该文件描述符对应的后续操作后,一定要及时关闭文件描述符
边缘触发:仅在文件描述符对应的资源就绪后触发一次,后续不管该文件描述有没被关闭,epoll_wait均不会返回该文件描述符
ev.data.fd = conn_fd;
epoll_ctl(epfd, EPOLL_CTL_ADD, conn_fd, &ev);
}
else
{
// 处理客户端数据
char buffer[1024];
int len = read(events[i].data.fd, buffer, sizeof(buffer));
if (len > 0) {
write(events[i].data.fd, buffer, len); // 回显
}
else
{
close(events[i].data.fd); // 关闭连接
}
}
}
}
return 0;
}
三.实现方式比较
select:文件描述符个数最大值为1024,每次都需要重新初始化文件描述符集合并重新赋值,且内核需要将此集合进行复制(用户态转为内核态),开销太大
poll:文件描述个数将不受限制,且不需要每次初始化文件描述符集合,但还是需要提前设置文件描述符数组的大小,需要内核进行复制
epoll解决了上述问题
四.示例代码
select
#include <stdio.h>
#include <sys/select.h>
#include <unistd.h>
int main() {
fd_set readfds;
struct timeval timeout;
int ret;
FD_ZERO(&readfds);
FD_SET(STDIN_FILENO, &readfds); // 监控标准输入
timeout.tv_sec = 5; // 5秒超时
timeout.tv_usec = 0;
ret = select(STDIN_FILENO + 1, &readfds, NULL, NULL, &timeout);
if (ret == -1) {
perror("select error");
} else if (ret == 0) {
printf("Timeout, no data received.\n");
} else {
if (FD_ISSET(STDIN_FILENO, &readfds)) {
printf("Data is available on stdin.\n");
}
}
return 0;
}
poll
#include <stdio.h>
#include <poll.h>
#include <unistd.h>
int main() {
struct pollfd fds[1];
int ret;
// 监控标准输入(fd=0)
fds[0].fd = STDIN_FILENO;
fds[0].events = POLLIN; // 关注可读事件
// 设置超时为 5000 毫秒(5秒)
ret = poll(fds, 1, 5000);
if (ret == -1) {
perror("poll error");
} else if (ret == 0) {
printf("Timeout, no data received.\n");
} else {
if (fds[0].revents & POLLIN) {
printf("Data is available on stdin.\n");
}
if (fds[0].revents & POLLERR) {
printf("Error on stdin.\n");
}
}
return 0;
}
epoll
#include <sys/epoll.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#define MAX_EVENTS 10
#define PORT 8080
int set_nonblocking(int fd) {
int flags = fcntl(fd, F_GETFL, 0);
return fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}
int main() {
int listen_fd, epfd, n;
struct sockaddr_in addr;
struct epoll_event ev, events[MAX_EVENTS];
// 创建监听 socket
listen_fd = socket(AF_INET, SOCK_STREAM, 0);
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY;
addr.sin_port = htons(PORT);
bind(listen_fd, (struct sockaddr*)&addr, sizeof(addr));
listen(listen_fd, SOMAXCONN);
// 创建 epoll 实例
epfd = epoll_create1(0);
ev.events = EPOLLIN;
ev.data.fd = listen_fd;
epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, &ev);
while (1) {
n = epoll_wait(epfd, events, MAX_EVENTS, -1);
for (int i = 0; i < n; i++) {
if (events[i].data.fd == listen_fd) {
// 接受新连接
int conn_fd = accept(listen_fd, NULL, NULL);
set_nonblocking(conn_fd); // 非阻塞模式(ET 必须)
ev.events = EPOLLIN | EPOLLET;
ev.data.fd = conn_fd;
epoll_ctl(epfd, EPOLL_CTL_ADD, conn_fd, &ev);
} else {
// 处理客户端数据
char buffer[1024];
int len = read(events[i].data.fd, buffer, sizeof(buffer));
if (len > 0) {
write(events[i].data.fd, buffer, len); // 回显
} else {
close(events[i].data.fd); // 关闭连接
}
}
}
}
return 0;
}