C/C++ I/O多路复用技术(select/poll/epoll)

一.相关概念
问:什么是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;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

风吹沙丘

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值