Linux下的三种 IO 复用
目录
一、Select
1、函数 API
2、使用限制
3、使用 Demo
二、Poll
三、epoll
0、 实现原理
1、函数 API
2、简单代码模板
3、LT/ET 使用过程
(1)LT 水平触发
(2)ET边沿触发
4、使用 Demo
四、参考链接
一、Select
在 Linux 中,select 就是一种经典的 I/O 复用机制。它允许服务器在一个线程内监控多个 I/O 事件(比如多个客户端的连接状态)。当服务器调用 select(),它会依次“询问”每个连接是否有事件发生,如果有事件发生了就立即处理。这样,服务器不需要为每个连接创建线程,使用单线程就可以服务于多个客户端,从而节省了资源,提升了效率。
1、函数 API
在实际使用 select
时,我们会用到几个重要的函数和宏,分别是 select()
本身,以及操作 fd_set
结构的 FD_ZERO
、FD_SET
、FD_CLR
、FD_ISSET
等宏函数。
#include <sys/select.h>
/* select() 是 I/O 复用的核心函数,用来等待多个文件描述符的状态变化。参数说明 :nfds :要监控的文件描述符的数量,通常是 fd_set 中最大的文件描述符值加 1。readfds :监控是否有数据可读的文件描述符集合。writefds :监控是否有数据可写的文件描述符集合。exceptfds:监控异常事件的文件描述符集合。timeout :超时时间,NULL 表示无限等待,超时后 select 返回 0。返回值:成功时,返回就绪的文件描述符的总数。出错时,返回 -1,并设置 errno 以指示错误类型。
*/
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);/*在 select 中,我们使用 fd_set 结构来标记哪些文件描述符需要被监控。fd_set 是一个位数组(bitmap),每个位代表一个文件描述符的位置。如果某个位被设置为 1,表示我们希望 select 监控这个文件描述符。这里有几个重要的宏函数,用于操作 fd_set。
*/
// 将 fd_set 清空,所有位清零。
FD_ZERO(&fd_set)
// 将指定的文件描述符 fd 加入 fd_set,即把 fd_set 中 fd 的位设置为 1。
FD_SET(fd, &fd_set)
// 将指定的文件描述符 fd 从 fd_set 中移除,即把 fd_set 中 fd 的位清零。
FD_CLR(fd, &fd_set)
// 检查 fd_set 中指定的文件描述符 fd 是否被设置为 1,若为 1 表示该文件描述符有事件发生。
FD_ISSET(fd, &fd_set)
2、使用限制
- 连接数限制:
select
在大部分系统中最多支持 1024 个连接,如果 fd 并发特别多,可以考虑poll
或epoll
(强烈推荐,更适合高并发场景)。
- 函数返回:
select()
返回 IO 就绪的 fd 个数,而且参数 fd_set 将被刷新,只记录准备就绪的 IO 的 fd,未就绪的 fd 将被移除。这块很容易混淆,若 fd_set 不是一次性的,建议在执行 select 之前进行备份,每次执行 select 时使用临时变量传参。
# 1、监听 fd 集合:3、4、5内核空间 fd_set 结构
+---+---+---+---+---+---+---+---+
| 0 | 0 | 0 | 1 | 1 | 1 | 0 |...|
+---+---+---+---+---+---+---+---+0 1 2 3 4 5 6 ...
(内核监控文件描述符 3、4、5 的状态)# 2、执行 select 后,只有 fd 4 准备就绪,
# 则其余 fd 在 fd_set 中全部被剔除(置为 0)内核空间 fd_set 更新
+---+---+---+---+---+---+---+---+
| 0 | 0 | 0 | 0 | 1 | 0 | 0 |...|
+---+---+---+---+---+---+---+---+0 1 2 3 4 5 6 ...
(仅文件描述符 4 发生事件,保留 1)
- 灵活阻塞:
select
本身是阻塞的,也可传参 timeval 变量,设置阻塞事件,该时间可以根据业务场景合理安排。若时间太小,则浪费 CPU 资源,CPU 会无故的频繁切换内核态和用户态;若时间太长,又可能无法及时处理 IO 时间。
3、使用 Demo
下面代码实现了基于 select 的多并发服务器。
int tcp_Server_Select(int argc, char **args)
{char server_ip[MAX_IP_LENGTH] = "127.0.0.1";uint16_t server_port = 8088;// 可自定义服务器绑定的 IP 与 端口if ( argc >= 1 ){strcpy(server_ip, args[1]);}if ( argc >= 2 ){server_port = atoi(args[2]);}// 记录客户端 fd int clients_fd[FD_SETSIZE - 2];int max_fd = -1, clients_count = -1;// select 监听的 fd 列表,其中 set_tmp 是负责传参,poll_set 负责全局fd_set set_tmp, poll_set;for ( int i = 0; i < FD_SETSIZE - 2; i++ ){clients_fd[i] = -1;}//创建 TCP 监听套接字int listen_fd = socket(AF_INET, SOCK_STREAM, 0);if ( listen_fd < 0 ){log_error("Create Socket fd Failed");printf("Create Server FD Failed\n");return FAILURE;}//服务器端口复用int yes = 1;setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));//给服务器 socket 绑定 ip 和端口信息struct sockaddr_in server;server.sin_family = AF_INET;server.sin_port = htons(server_port);server.sin_addr.s_addr = inet_addr(server_ip);int result = bind(listen_fd, (struct sockaddr *)&server, sizeof(server));if (result == -1){log_error("Failed to bind Server Net Address");printf("Failed to bind Server Net Address");return FAILURE;}// 调用listenlisten(listen_fd, 10);// select fd_set 置空FD_ZERO(&poll_set);// 将服务器的监听 fd 也添加到 fd_set 中,负责监听是否有新的客户端接入FD_SET(listen_fd, &poll_set);max_fd = listen_fd;printf("TCP Server Listen On %s:%hu with fd %d\n", server_ip, server_port, listen_fd);while(1){// tmp 变量只在本次循环有效,所以需要使用 poll_set 保存变量,每次循环开始重新赋值。set_tmp = poll_set;int ready_count = select(max_fd + 1, &set_tmp, NULL, NULL, NULL);if ( ready_count < 0 ){printf("Failed to execute select\n");log_error("Failed to execute select");break;}else if ( ready_count > 0 ){// 先检查是否有新的 TCP 客户端接入printf("ready count %d\n", ready_count);if ( FD_ISSET(listen_fd, &set_tmp) ){struct sockaddr_in client;socklen_t len = sizeof(client);// accept 调用一次接入一个 tcp 客户端int client_fd = accept(listen_fd, (struct sockaddr *)&client, &len);for ( int i = 0; i < FD_SETSIZE - 2; i++ ){// 记录新的客户端 fdif ( clients_fd[i] == -1 ){clients_fd[i] = client_fd;if ( clients_count < i + 1 ){clients_count = i + 1;}printf("client fd %d --- i %d --- clients_count %d\n", client_fd, i, clients_count);break;}}log_info("FD SetSize %d", FD_SETSIZE);if ( clients_count < FD_SETSIZE - 1 ){// 若未达到连接边界,则将新的客户端 fd 添加到监听集合中FD_SET(client_fd, &poll_set);max_fd = client_fd > max_fd? client_fd: max_fd;//输出客户端信息char ip[MAX_IP_LENGTH] = "";unsigned short port = ntohs(client.sin_port);inet_ntop(AF_INET, &client.sin_addr.s_addr, ip, MAX_IP_LENGTH);printf("client %s is connected %hu port\n", ip, port);log_info("client %s is connected %hu port", ip, port);}else{printf("Number of Clients reaches max limit\n");}ready_count--;}// 处理客户端 IO 事件for ( int i = 0; i < clients_count && ready_count > 0; i++ ){int client_fd = clients_fd[i];if ( client_fd < 0 ){continue;}// 若该客户端准备就绪,则执行 recv 接受消息if ( FD_ISSET(client_fd, &set_tmp) ){printf("client fd %d with i %d\n", client_fd, i);char msg[MAX_MSG_LENGTH] = "";char msg_res[MAX_MSG_LENGTH] = "Recevied Successfully";int len = recv(client_fd, msg, sizeof(msg), 0);// 异常情况,将剔除客户端if ( len <= 0 ){printf("Release Fd %d\n", client_fd);close(client_fd);clients_fd[i] = -1;FD_CLR(client_fd, &poll_set);}else{printf("TCP Client Send: %s\n", msg);if ( send(client_fd, msg_res, strlen(msg_res), 0) > 0 ){printf("---- Response Successfully With %s\n\n", msg_res);}to_lower_case(msg);//printf("--%s--\n", msg);// 客户端主动退出if ( !strcmp("exit", msg) ) {close(client_fd);clients_fd[i] = -1;FD_CLR(client_fd, &poll_set);printf("Close Socket FD %d\n", client_fd);}}--ready_count;}}}}close(listen_fd);return 0;
}
二、Poll
poll 是 select 的一种改进版本,它消除了 select 的文件描述符数量限制,API 函数使用起来稍有不同。poll 函数与 select 原理相似,也是一种基于轮询的 I/O 多路复用机制,它通过一个 struct pollfd
结构体数组来管理多个文件描述符。
#include <poll.h>
/* Type used for the number of file descriptors. */
typedef unsigned long int nfds_t;struct pollfd {int fd; // 文件描述符short events; // 监听事件short revents; // 就绪事件
};/*fds: 监听的 fd 列表nfds:监听的 fd 数量timeout:监听阻塞超时时间,< 0 永远等待;0 立即返回;> 0 等待的毫秒数
*/
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
// return:表示此时有多少个监控的描述符就绪,若超时则为0,出错为-1。
Event 类型如下所示,感兴趣的可以看下:
poll 和 select 函数一样,两者都需要遍历整个文件描述符集合来检查状态,因此性能都会随着文件描述符数量的增加而线性下降。
poll 与 select 的不同之处:
(1)poll 函数采用链表的方式替代原来 select 中 fd_set 结构,因此可监听文件描述符数量不受限,在处理大量文件描述符时可能更具优势。
(2)poll 函数返回后,可以通过 pollfd 结构中的内容进行处理就绪文件描述符,相比 select 效率要高。避免了 select 需要重置文件描述符集合的问题。
(3)需要维护一个 struct pollfd 结构体数组,这可能会增加一些编程复杂性。
poll 的使用示例如下:
#include <stropts.h>
#include <poll.h>
...
struct pollfd fds[2];
int timeout_msecs = 500;
int ret;
int i;/* Open STREAMS device. */
fds[0].fd = open("/dev/dev0", ...);
fds[1].fd = open("/dev/dev1", ...);
fds[0].events = POLLOUT | POLLWRBAND;
fds[1].events = POLLOUT | POLLWRBAND;ret = poll(fds, 2, timeout_msecs);if (ret > 0) {/* An event on one of the fds has occurred. */for ( i=0; i < 2; i++ ) {if (fds[i].revents & POLLWRBAND) {/* Priority data may be written on device number i. */
...}if (fds[i].revents & POLLOUT) {/* Data may be written on device number i. */
...}if (fds[i].revents & POLLHUP) {/* A hangup has occurred on device number i. */
...}}
}
三、epoll
重头戏来了,下面介绍 linux 中的高并发 IO 复用 epoll,很多服务器(例如 nginx)部署在 linux 中时都会使用 epoll 机制实现该并发 IO 操作,避免阻塞。
0、 实现原理
epoll 将“维护等待队列”和“阻塞进程”两个步骤分开。先用epoll_ctl
函数维护监听队列,再调用epoll_wait
函数阻塞进程。这种设计提高了效率,特别是在需要监视的 socket 相对固定的场景下。
在内核中,epoll 使用红黑树来跟踪所有待检测的文件描述符。红黑树是一种高效的数据结构(时间复杂度O(logN)),支持快速查找、插入和删除操作。这使得 epoll 能够高效地管理大量文件描述符。
epoll 采用事件驱动的方式,仅在文件描述符状态发生变化时才会通知应用程序。这避免了每次遍历整个文件描述符集合的问题,从而提高了性能。epoll 使用一个双向链表来记录就绪事件,在执行 epoll_ctl
的 add
操作时,不仅将文件描述符放到红黑树上,而且也注册了回调函数,内核在检测到某文件描述符可读/可写时会调用回调函数
,将该文件描述符放在就绪链表
中。用户调用epoll_wait
时,只需检查这个列表是否有存在注册的事件(红黑树)
即可,避免了遍历所有文件描述符。
1、函数 API
#include <sys/epoll.h>/**/
struct epoll_event
{uint32_t events; /* 指定要监听的事件类型 */epoll_data_t data; /* 用户数据变量 */
} __EPOLL_PACKED;/*
epoll_data_t是一个共用体,其 4 个成员中使用最多的是 fd,它指定事件所从属的目标文件描述符。ptr成员可以用来指定与fd相关的用户数据。但由于epoll_data_t是一个共用体,我们不能同时使用其ptr成员和fd成员,因此,如果要将文件描述符和用户数据关联起来,以实现快速的数据访问,只能放弃使用epoll_data_t的fd成员,而在ptr指向的用户数据中包含fd。
*/
typedef union epoll_data
{void *ptr; // 指定与fd相关的用户数据int fd; // 指定事件所从属的目标文件描述符uint32_t u32;uint64_t u64;
} epoll_data_t;/*创建 epoll 实例, 并返回该实例的 fd。该函数会在内核中新建红黑树用于存储 epoll_ctl 管理的 fd,还会新建双向链表用于记录已就绪的 fd。需要注意,在使用完 epoll 后,必须调用 close() 关闭该 fd,否则会浪费描述符资源。返回值: 成功时返回一个文件描述符(非负整数),失败时返回 -1 并设置 errno。
*/
int epoll_create1(int flags);/*添加、修改或删除监听的文件描述符参数:epfd: epoll 实例的文件描述符。op: 操作类型,可以是以下之一:EPOLL_CTL_ADD: 注册新的文件描述符到 epoll 实例中。EPOLL_CTL_MOD: 修改已注册的文件描述符的事件。EPOLL_CTL_DEL: 从 epoll 实例中删除文件描述符。fd: 需要监听的文件描述符。event: 指向 epoll_event 结构的指针,用于指定事件和用户数据。返回值: 成功时返回 0,失败时返回 -1 并设置 errno。
*/
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);/*等待事件发生,当事件发生时,会将对应的 fd 添加到 epoll 就绪队列中。参数:epfd: epoll 实例的文件描述符。events: 用于存储发生事件的数组。maxevents: 数组的最大长度。timeout: 超时时间(毫秒)。如果为 -1,则无限等待;如果为 0,则立即返回。返回值: 成功时返回就绪的文件描述符数量,失败时返回 -1 并设置 errno。
*/
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
Event 类型 | |
| 表示对应的文件描述符上有数据可读 |
| 表示对应的文件描述符上可以写入数据 |
| 表示对端已经关闭连接,或者关闭了写操作端的写入 |
| 表示有紧急数据可读 |
| 表示发生错误 |
| 表示文件描述符被挂起 |
| 表示将 epoll 设置为边缘触发模式。在边缘触发模式下,事件只有在状态发生变化时才会报告一次,而不是像水平触发模式那样只要条件满足就持续报告。 |
| 表示将事件设置为一次性事件。设置了这个标志后,当事件处理完后,epoll 会自动删除该事件,无需再次手动调用 |
2、简单代码模板
- 创建epoll实例:通过
epoll_create
函数创建一个epoll对象。 - 维护监听列表:使用
epoll_ctl
函数添加、删除或修改需要监视的文件描述符。 - 接收数据:当文件描述符收到数据后,中断程序会操作epoll对象,而不是直接操作进程。
- 阻塞和唤醒进程:当进程运行到
epoll_wait
时,内核会将进程放入epoll对象的等待队列中,阻塞进程。当文件描述符接收到数据,中断程序一方面修改就绪列表,另一方面唤醒epoll等待队列中的进程
#include <sys/epoll.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>#define MAX_EVENTS 10int main() {int epoll_fd = epoll_create1(0);if (epoll_fd == -1) {perror("epoll_create1");exit(EXIT_FAILURE);}struct epoll_event event;struct epoll_event events[MAX_EVENTS];int listen_sock = /* ... */; // 初始化监听套接字event.data.fd = listen_sock;event.events = EPOLLIN;if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_sock, &event) == -1) {perror("epoll_ctl");exit(EXIT_FAILURE);}while (1) {int nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);if (nfds == -1) {perror("epoll_wait");exit(EXIT_FAILURE);}for (int i = 0; i < nfds; i++) {if (events[i].data.fd == listen_sock) {// 处理新连接} else {// 处理现有连接的数据}}}close(epoll_fd);return 0;
}
3、LT/ET 使用过程
摘自Linux下的I/O复用技术 — epoll如何使用(epoll_create、epoll_ctl、epoll_wait) 以及 LT/ET 使用过程解析_主动去触发epoll事件-CSDN博客https://blog.csdn.net/JMW1407/article/details/107963618
(1)LT 水平触发
Level Triggered
socket
接收缓冲区不为空
,说明有数据可读, 读事件一直触发socket
发送缓冲区不满
,说明可以继续写入数据 ,写事件一直触发- 符合思维习惯,epoll_wait返回的事件就是socket的状态
LT 处理过程:
- accept 一个连接,添加到 epoll 中监听 EPOLLIN 事件.
- 当 EPOLLIN 事件到达时,读取 fd 中的数据并处理 .
- 当需要写出数据时,把数据 write 到 fd 中;如果数据较大,无法一次性写出,那么在 epoll 中监听EPOLLOUT 事件 。
- 当 EPOLLOUT 事件到达时,继续把数据 write 到 fd 中;如果数据写出完毕,那么在 epoll 中关闭EPOLLOUT 事件。
//LT模式的工作流程
void lt( epoll_event* events, int number, int epollfd, int listenfd )
{char buf[ BUFFER_SIZE ];for ( int i = 0; i < number; i++ ){int sockfd = events[i].data.fd;if ( sockfd == listenfd ){struct sockaddr_in client_address;socklen_t client_addrlength = sizeof( client_address );int connfd = accept( listenfd, ( struct sockaddr* )&client_address, &client_addrlength );addfd( epollfd, connfd, false );}else if ( events[i].events & EPOLLIN ){//只要socket读缓存中还有未读出的数据,这段代码就被触发printf( "event trigger once\n" );memset( buf, '\0', BUFFER_SIZE );int ret = recv( sockfd, buf, BUFFER_SIZE-1, 0 );if( ret <= 0 ){close( sockfd );continue;}printf( "get %d bytes of content: %s\n", ret, buf );}else{printf( "something else happened \n" );}}
}
(2)ET边沿触发
Edge Triggered
socket
的接收缓冲区状态变化
时触发读事件,即空的接收缓冲区刚接收到数据时触发读事件(从无到有)socket
的发送缓冲区状态变化时触发写事件,即满的缓冲区刚空出空间时触发读事件(从有到无)- 仅在状态变化时触发事件
ET 处理流程
- accept 一个一个连接,添加到 epoll 中监听 EPOLLIN|EPOLLOUT 事件;
- 当 EPOLLIN 事件到达时,读取 fd 中的数据并处理,read 需要一直读,直到返回 EAGAIN 为止
- 当需要写出数据时,把数据 write 到fd中,直到数据全部写完,或者 write 返回 EAGAIN
- 当 EPOLLOUT 事件到达时,继续把数据 write 到fd中,直到数据全部写完,或者 write 返回 EAGAIN
从 ET 的处理过程中可以看到,ET 的要求是需要一直读写,直到返回 EAGAIN,否则就会遗漏事件。而 LT 的处理过程中,直到返回 EAGAIN 不是硬性要求,但通常的处理过程都会读写直到返回 EAGAIN,但 LT 比 ET 多了一个开关 EPOLLOUT 事件的步骤
当我们使用 ET 模式的 epoll 时,我们应该按照以下规则设计:
- 在接收到一个 I/O 事件通知后,立即处理该事件。程序在某个时刻应该在相应的文件描述符上尽可能多地执行I/O。
- 在ET模式下,在使用epoll_ctl注册文件描述符的事件时,应该把描述符设置为非阻塞的(非常重要)。
因为程序采用循环(ET里面采用while循环,看清楚呦,LE是if判断)来对文件描述符执行尽可能多的I/O,而文件描述符又被设置为可阻塞的,那么最终当没有更多的I/O可执行时,I/O系统调用就会阻塞。基于这个原因,每个被检查的文件描述符通常应该置为非阻塞模式,在得到I/O事件通知后重复执行I/O操作,直到相应的系统调用(比如read(),write())以错误码EAGAIN或EWOULDBLOCK的形式失败。
//ET模式的工作流程
void et( epoll_event* events, int number, int epollfd, int listenfd )
{char buf[ BUFFER_SIZE ];for ( int i = 0; i < number; i++ ){int sockfd = events[i].data.fd;if ( sockfd == listenfd ){struct sockaddr_in client_address;socklen_t client_addrlength = sizeof( client_address );int connfd = accept( listenfd, ( struct sockaddr* )&client_address, &client_addrlength );addfd( epollfd, connfd, true );}else if ( events[i].events & EPOLLIN ){//这段代码不会被重复触发,所以我们循环读取数据,以确保把socket读缓存中的所有数据读出printf( "event trigger once\n" );while( 1 ){memset( buf, '\0', BUFFER_SIZE );int ret = recv( sockfd, buf, BUFFER_SIZE-1, 0 );if( ret < 0 ){//对于非阻塞IO,下面条件成立表示数据已经全部读取完毕。//此后,epoll就能再次触发sockfd上的EPOLLIN事件,已驱动下一次读操作if( ( errno == EAGAIN ) || ( errno == EWOULDBLOCK ) ){printf( "read later\n" );break;}close( sockfd );break;}else if( ret == 0 ){close( sockfd );}else{printf( "get %d bytes of content: %s\n", ret, buf );}}}else{printf( "something else happened \n" );}}
}
4、使用 Demo
基于 epoll 实现的高并发 TCP 服务器。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/epoll.h>
#include <errno.h>
#include <fcntl.h>#define PORT 8080
#define MAX_EVENTS 1024
#define BUFFER_SIZE 1024void set_nonblocking(int sockfd) {int opts;opts = fcntl(sockfd, F_GETFL);if (opts < 0) {perror("fcntl(F_GETFL)");exit(EXIT_FAILURE);}opts = (opts | O_NONBLOCK);if (fcntl(sockfd, F_SETFL, opts) < 0) {perror("fcntl(F_SETFL)");exit(EXIT_FAILURE);}
}int main() {int listen_fd, conn_fd, nfds, epoll_fd;struct sockaddr_in server_addr, client_addr;socklen_t client_len = sizeof(client_addr);struct epoll_event ev, events[MAX_EVENTS];char buffer[BUFFER_SIZE];int done = 0;// 创建监听套接字if ((listen_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {perror("socket");exit(EXIT_FAILURE);}// 设置地址复用int opt = 1;if (setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) == -1) {perror("setsockopt");close(listen_fd);exit(EXIT_FAILURE);}// 绑定端口和地址memset(&server_addr, 0, sizeof(server_addr));server_addr.sin_family = AF_INET;server_addr.sin_addr.s_addr = INADDR_ANY;server_addr.sin_port = htons(PORT);if (bind(listen_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {perror("bind");close(listen_fd);exit(EXIT_FAILURE);}// 监听端口if (listen(listen_fd, SOMAXCONN) == -1) {perror("listen");close(listen_fd);exit(EXIT_FAILURE);}// 创建 epoll 实例epoll_fd = epoll_create1(0);if (epoll_fd == -1) {perror("epoll_create1");close(listen_fd);exit(EXIT_FAILURE);}// 将监听套接字添加到 epoll 实例中ev.events = EPOLLIN;ev.data.fd = listen_fd;if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_fd, &ev) == -1) {perror("epoll_ctl: listen_fd");close(listen_fd);close(epoll_fd);exit(EXIT_FAILURE);}printf("Server is listening on port %d\n", PORT);// 主循环:等待事件并处理while (!done) {nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);if (nfds == -1) {perror("epoll_wait");close(listen_fd);close(epoll_fd);exit(EXIT_FAILURE);}for (int i = 0; i < nfds; ++i) {if (events[i].data.fd == listen_fd) {// 处理新的连接请求while ((conn_fd = accept(listen_fd, (struct sockaddr *)&client_addr, &client_len)) != -1) {set_nonblocking(conn_fd);ev.events = EPOLLIN | EPOLLET; // 边缘触发模式ev.data.fd = conn_fd;if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, conn_fd, &ev) == -1) {perror("epoll_ctl: conn_fd");close(conn_fd);continue;}printf("New connection from %s:%d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));}if (errno != EAGAIN && errno != EWOULDBLOCK) {perror("accept");done = 1;}} else {// 处理客户端数据或断开连接int client_fd = events[i].data.fd;ssize_t bytes_read = read(client_fd, buffer, sizeof(buffer) - 1);if (bytes_read == -1) {if (errno != EAGAIN && errno != EWOULDBLOCK) {perror("read");close(client_fd);epoll_ctl(epoll_fd, EPOLL_CTL_DEL, client_fd, NULL); // 从 epoll 监听队列中删除文件描述符}} else if (bytes_read == 0) {// 客户端关闭连接printf("Closed connection on descriptor %d\n", client_fd);close(client_fd);epoll_ctl(epoll_fd, EPOLL_CTL_DEL, client_fd, NULL); // 从 epoll 监听队列中删除文件描述符} else {// 回显数据给客户端buffer[bytes_read] = '\0';write(client_fd, buffer, bytes_read);}}}}close(listen_fd);close(epoll_fd);return 0;
}
四、参考链接
Linux下的I/O复用技术 — epoll如何使用(epoll_create、epoll_ctl、epoll_wait) 以及 LT/ET 使用过程解析_主动去触发epoll事件-CSDN博客https://blog.csdn.net/JMW1407/article/details/107963618 还在用多线程?试试 Linux select 这个‘神操作’吧!
https://mp.weixin.qq.com/s/sRXjRUZS1BVx1ZtBsZifug
相关文章:
Linux下的三种 IO 复用
目录 一、Select 1、函数 API 2、使用限制 3、使用 Demo 二、Poll 三、epoll 0、 实现原理 1、函数 API 2、简单代码模板 3、LT/ET 使用过程 (1)LT 水平触发 (2)ET边沿触发 4、使用 Demo 四、参考链接 一、Select 在…...
微服务即时通讯系统的实现(服务端)----(2)
目录 1. 语音识别子服务的实现1.1 功能设计1.2 模块划分1.3 模块功能示意图1.4 接口的实现 2. 文件存储子服务的实现2.1 功能设计2.2 模块划分2.3 模块功能示意图2.4 接口的实现 3. 用户管理子服务的实现3.1 功能设计3.2 模块划分3.3 功能模块示意图3.4 数据管理3.4.1 关系数据…...
数据库原理-期末复习基础知识第二弹
1、数据的逻辑独立性是指 外模式/模式映像 当模式改变的时候,由数据库管理员对各个外模式/模式的映像做出相应改变,使外模式保持不变。由于应用程序是按照外模式进行编写的,故应用程序不必修改,保证了数据与程序的逻辑独立性。 …...
智能云在线编辑网站(完结篇)
开始及初步计划 1.前端tiptip编辑器框架vue3 2.后端Pythonflaskmysql 3.大模型调用:飞桨系列(ppasr) 前言:以此篇谨记从软件杯到天津生成式ai答辩过程及结束。 『如蚍蜉见青天,双肩难挑日月』,感叹世事多…...
多源传感器构建机器人的Gazebo模型
构建包含GNSS、IMU、LiDAR、Camera传感器的Gazebo模型涉及多个步骤,包括设置工作环境、创建URDF文件、安装必要的Gazebo插件和依赖项。以下是一个详细的步骤指南,帮助你开始构建这个Gazebo模型。 1. 设置工作环境 首先,确保你已经安装了ROS…...
linux中top 命令返回数据解释
当您在 Linux 终端中运行 top 命令时,它会显示一个动态更新的系统状态视图,其中包括许多有关系统性能的数据。下面是对 top 命令返回数据的详细解释: 标题栏 top - 22:46:12 up 2 days, 3:14, 1 user, load average: 0.05, 0.07, 0.09 22:46:12:当前时间。up 2 days, 3:14…...
【Vue3】【Naive UI】<NDropdown>标签
【Vue3】【Naive UI】 标签 基本设置自定义渲染交互事件其他属性 【VUE3】【Naive UI】<NCard> 标签 【VUE3】【Naive UI】<n-button> 标签 【VUE3】【Naive UI】<a> 标签 【VUE3】【Naive UI】<…...
ADS学习笔记 7. 超外差接收机设计
基于ADS2023 update2 更多ADS学习笔记:ADS学习笔记 1. 功率放大器设计ADS学习笔记 2. 低噪声放大器设计ADS学习笔记 3. 功分器设计ADS学习笔记 4. 微带分支定向耦合器设计ADS学习笔记 5. 微带天线设计ADS学习笔记 6. 射频发射机设计 目录 -1、射频接收机性能指标…...
新型大语言模型的预训练与后训练范式,阿里Qwen
前言:大型语言模型(LLMs)的发展历程可以说是非常长,从早期的GPT模型一路走到了今天这些复杂的、公开权重的大型语言模型。最初,LLM的训练过程只关注预训练,但后来逐步扩展到了包括预训练和后训练在内的完整…...
k8s 1.28 二进制安装与部署
第一步 :配置Linux服务器 #借助梯子工具 192.168.196.100 1C8G kube-apiserver、kube-controller-manager、kube-scheduler、etcd、kubectl、haproxy、keepalived 192.168.196.101 1C8G kube-apiserver、kube-controller-manager、kube-scheduler、etcd、kubectl、…...
Ubuntu 常用解压与压缩命令
.zip文件 unzip FileName.zip # 解压 zip DirName.zip DirName # 将DirName本身压缩 zip -r DirName.zip DirName # 压缩,递归处理,将指定目录下的所有文件和子目录一起压缩 zip DirName.zip DirName 行为: 只压缩 DirName 目录本身ÿ…...
使用ECharts创建带百分比标注的环形图
在数据可视化领域,环形图是一种非常有效的图表类型,它能够清晰地展示各部分与整体的关系。今天,我们将通过ECharts来创建一个带百分比标注的环形图,并详细解释如何实现这一效果。 1. 数据准备 首先,我们定义了一些基础…...
lvs虚拟服务器之LVS-NAT模式
一.集群 二.LVS:虚拟服务器:工作在传输层,解决高并发 三.LVS-NAT一.集群1.概念:集群就是一组计算机集群核心:任务调度集群目的提高性能,降低成本,提高可扩展性,增强可靠性集群分类HA:高可用集群(High Availability Cluster):避免单…...
虚拟机添加硬盘驱动,Windows 系统添加 VirtIO 驱动(Windows ISO 安装镜像添加驱动)
为什么要在 ISO 镜像里添加驱动?而不是在进系统以后装驱动?一切都是形势所迫。如果你也是爱折腾的人,那么这也会成为一个有用的方案之一。 最近,因为在给公司研究部署 OpenStack,在制作初始镜像(也就是…...
20241128解决Ubuntu20.04安装libesd0-dev异常的问题
20241128解决Ubuntu20.04安装libesd0-dev异常的问题 2024/11/28 16:36 缘起:中科创达的高通CM6125开发板的Android10的编译环境需要。 安装异常:rootrootrootroot-X99-Turbo:~$ rootrootrootroot-X99-Turbo:~$ sudo apt-get install libesd0-dev Readi…...
Linux命令进阶·如何切换root以及回退、sudo命令、用户/用户组管理,以及解决创建用户不显示问题和Ubuntu不显示用户名只显示“$“符号问题
目录 1. root用户(超级管理员) 1.1 用于账户切换的系统命令——su 1.2 退回上一个用户命令——exit 1.3 普通命令临时授权root身份执行——sudo 1.3.1 为普通用户配置sudo认证 2. 用户/用户组管理 2.1 用户组管理 2.2 用户管理 2.2.1 …...
基于链表的基础笔试/面试题
1. 反转链表 问题描述:反转一个单向链表。 示例: 输入:1 → 2 → 3 → 4 → 5 输出:5 → 4 → 3 → 2 → 1 class ListNode {int val;ListNode next;ListNode(int x) {val x;} }public class LinkedList {public ListNode …...
文件比较和文件流
文件比较和文件流 一、文本比较工具 diff1.基本用法1.1输出格式 2.常用选项 二、文件流1.文件的打开模式2.文件流的分类ifstreamofstreamfstrem区别 3.文件流的函数1. 构造函数2. is_open 用于判断文件是否打开3. open4. getline5. close6. get()7. read8. write9. put10. gcou…...
unity如何让一个物体拥有按钮功能
在 Unity 中,要让一个物体(例如一个 3D 模型、UI 元素或其他对象)变成一个按钮,你需要为它添加交互功能。这通常意味着让物体能够响应点击事件,像 UI 按钮那样触发某些行为。对于 3D 物体,可以通过 射线检测…...
【RISC-V CPU Debug 专栏 1 -- RISC-V debug 规范】
文章目录 RISC-V Debug调试用例支持的功能限制和不包括的内容RISC-V 调试架构的主要组件用户与调试主机调试翻译器调试传输硬件调试传输模块(DTM)调试模块(DM)调试功能触发模块版本介绍RISC-V Debug RISC-V 调试规范为 RISC-V 处理器提供了一套标准化的调试接口和功能,旨…...
【论文阅读】Federated learning backdoor attack detection with persistence diagram
目的:检测联邦学习环境下,上传上来的模型是不是恶意的。 1、将一个模型转换为|L|个PD,(其中|L|为层数) 如何将每一层转换成一个PD? 为了评估第𝑗层的激活值,我们需要𝑐个输入来获…...
IDEA Maven 打包找不到程序包错误或找不到符号,报错“程序包不存在“
参考文章:https://blog.csdn.net/yueeryuanyi/article/details/14211090 问题:IDEA Maven 打包找不到程序包错误或找不到符号,报错“程序包不存在“编译都没问题 解决思路 – >【清除缓存】 1. 强制刷新Maven缓存 选择 Maven 标签,Exe…...
MySQL数据库做题笔记
题目链接https://leetcode.cn/problems/invalid-tweets-ii/description/https://leetcode.cn/problems/invalid-tweets-ii/description/ # Write your MySQL query statement below SELECT tweet_id FROM Tweets where LENGTH(content)>140 OR (length(content)-length(rep…...
100个python经典面试题详解(新版)
应老粉要求,每晚加餐一个最新面试题 包括Python面试中常见的问题,涵盖列表、元组、字符串插值、比较操作符、装饰器、类与对象、函数调用方式、数据结构操作、序列化、数据处理函数等多个方面。 旨在帮助数据科学家和软件工程师准备面试或提升Python技能。 7、Python面试题…...
Leetcode3232:判断是否可以赢得数字游戏
题目描述: 给你一个 正整数 数组 nums。 Alice 和 Bob 正在玩游戏。在游戏中,Alice 可以从 nums 中选择所有个位数 或 所有两位数,剩余的数字归 Bob 所有。如果 Alice 所选数字之和 严格大于 Bob 的数字之和,则 Alice 获胜。 如…...
Python 爬虫实战基于 Class 的天气查询与反爬虫练习
需求: 要实现一个简单的天气查询爬虫,使用 requests 库来获取网页内容,使用 BeautifulSoup 来解析网页并提取天气信息。以下是一个基本示例,展示了如何抓取天气信息并输出当天的温度和天气状况。 以下是使用 class 类方式实现带有…...
C语言——库函数
常用的函数 https://cplusplus.com/reference/ 没事儿多看看 1 数学函数 #include <math.h> #include <stdio.h> int main() {printf("%lf\n", sqrt(4));//开平方根——>double类型printf("%lf\n", pow(2, 10));//求几次方的——>do…...
软件测试丨Pytest 第三方插件与 Hook 函数
Pytest不仅是一个用于编写简单和复杂测试的框架,还有大量的第三方插件以及灵活的Hook函数供我们使用,这些功能大大增强了其在软件测试中的应用。通过使用Pytest,测试开发变得简便、安全、高效,同时也能帮助我们更快地修复Bug&…...
[ACTF2020 新生赛]BackupFile--详细解析
信息搜集 让我们寻找源文件,目录扫描: 找到了/index.php.bak文件,也就是index.php的备份文件。 后缀名是.bak的文件是备份文件,是文件格式的扩展名。 我们访问这个路径,就会直接下载该备份文件。 我们把.bak后缀删掉…...
ElasticSearch的学习
介绍 ElasticSearch(简称ES)是一个开源的分布式搜索和数据分析引擎,是用Java开发并且是当前最流行的开源的企业级搜索引擎,能够达到近实时搜索,它专门设计用于处理大规模的文本数据和实现高性能的全文检索。 Elastic…...
机器学习6-梯度下降法
梯度下降法 目的 梯度下降法(Gradient Descent)是一个算法,但不是像多元线性回归那样是一个具体做回归任务的算法,而是一个非常通用的优化算法来帮助一些机器学习算法求解出最优解的,所谓的通用就是很多机器学习算法都是用它,甚…...
算法之旅:LeetCode 拓扑排序由简入繁完全攻略
前言 欢迎来到我的算法探索博客,在这里,我将通过解析精选的LeetCode题目,与您分享深刻的解题思路、多元化的解决方案以及宝贵的实战经验,旨在帮助每一位读者提升编程技能,领略算法之美。 👉更多高频有趣Lee…...
vue3项目中使用星火API
在node环境epxress中使用讯飞ai接口进行二次封装,通过ai对话回复提取,获得ai提取的文章摘要 本文章只是简单使用,更复杂功能比如调用星火API制作对话机器人可以查看文档,对于初次使用星火AI接口或许有帮助 讯飞星火大模型API-大模…...
蓝桥杯第 23 场 小白入门赛
一、前言 好久没打蓝桥杯官网上的比赛了,回来感受一下,这难度区分度还是挺大的 二、题目总览 三、具体题目 3.1 1. 三体时间【算法赛】 思路 额...签到题 我的代码 // Problem: 1. 三体时间【算法赛】 // Contest: Lanqiao - 第 23 场 小白入门赛 …...
Cause: java.sql.SQLException: No value specified for parameter 4
问题 执行更新sql时报错,异常栈如下 org.springframework.jdbc.BadSqlGrammarException: ### Error updating database. Cause: java.sql.SQLException: No value specified for parameter 4 ### The error may exist in com/my/mapper/MyMapper.java (best gue…...
第五课 Unity资源导入工作流效率优化(AssetGraph工具)
上期我们学习了简单的animation动画的优化,接下来我们继续资源导入效率的优化 工程目录 首先我们来学习一下工程目录结构及用途 Asset文件夹:用来储存和重用的项目资产 Library文件夹:用来储存项目内部资产数据信息的目录 Packages文件夹…...
create-vue创建vue3项目
create-vue是Vue官方新的脚手架工具 前提条件: 已安装16.0或更高版本的Node.js (node -v查看) 创建一个Vue应用 npm init vuelatest 这一指令会帮我们安装并执行create-vue cd vue-project npm install —— 安装依赖 npm run dev...
27 基于51单片机的方向盘模拟系统
目录 一、主要功能 二、硬件资源 三、程序编程 四、实现现象 一、主要功能 基于STC89C52单片机,采用两个MPX4115压力传感器作为两路压力到位开关电路, 采用滑动变阻器连接数模转换器模拟重力加速度传感器电路; 一个按键控制LED灯的点亮与…...
HarmonyOS
UIAbility UIAbility 组件是一种包含UI的应用组件,主要用于和用户交互 设计理念:原生支持应用组件的跨端迁移和多端协同、支持多设备和多窗口的形态 UIAbility组件是系统调度的基本单位,为应用提供绘制界面的窗口。 /** 为使应用能够正常使用…...
字符串处理(二)
第1题 篮球比赛 查看测评数据信息 学校举行篮球比赛,请设计一个计分系统统计KIN、WIN两队分数,并输出分数和结果! 如果平分就输出‘GOOD’,否则输出获胜队名! 输入格式 输入数据共n1行, 第1行n…...
达梦数据库文件故障的恢复方法
目录 1、概述 1.1 概述 1.2 环境介绍 2、使用备份集的恢复方法 2.1 实验准备 2.2 误删除“用户表空间数据文件” 2.3 误删除SYSTEM.DBF 2.4 误删除ROLL.DBF 2.5 REDO日志文件 3、无备份集的恢复方法 3.1 误删除“表空间数据文件” 3.2误删除控制文件 3.3 误删除RO…...
Redis(5):哨兵
一、作用和架构 1. 作用 在介绍哨兵之前,首先从宏观角度回顾一下Redis实现高可用相关的技术。它们包括:持久化、复制、哨兵和集群,其主要作用和解决的问题是: 1)持久化:持久化是最简单的高可用方法(有时甚…...
准确--在 AlmaLinux 9.2 上快速搭建 FTP 服务器
FTP 服务器配置与验证完整步骤 以下内容是针对在 192.168.6.101 配置 FTP 服务器,端口为 59999 的完整详细操作步骤,包括配置与验证。每个步骤都附有详细注释。 配置 FTP 服务器 1. 安装 vsftpd 根据系统类型,执行以下命令安装 FTP 服务&a…...
Monitor 显示器软件开发设计入门二
基础篇--显示驱动方案输出接口介绍 写在前面:首先申明,这篇文章是写给那些初入显示器软件行业的入门者,或是对显示器没有基本知识的小白人员。如您是行业大咖大神,可以绕行,可看后期进阶文章。 上篇介绍了输入接口及相…...
MySQL 数据库学习教程一:开启数据库探索之旅
在当今数字化时代,数据已然成为企业和组织最为宝贵的资产之一。而数据库管理系统则是存储、管理和操作这些数据的核心工具。MySQL 作为一款广泛应用的开源关系型数据库管理系统,以其可靠性、高性能和易用性而备受青睐。如果你渴望踏入数据库领域…...
课程答疑微信小程序设计与实现
私信我获取源码和万字论文,制作不易,感谢点赞支持。 课程答疑微信小程序设计与实现 摘要 随着信息技术在管理上越来越深入而广泛的应用,管理信息系统的实施在技术上已逐步成熟。本文介绍了课程答疑微信小程序设计与实现的开发全过程。通过分析…...
基于yolov8、yolov5的铝材缺陷检测识别系统(含UI界面、训练好的模型、Python代码、数据集)
摘要:铝材缺陷检测在现代工业生产和质量管理中具有重要意义,不仅能帮助企业实时监控铝材质量,还为智能化生产系统提供了可靠的数据支撑。本文介绍了一款基于YOLOv8、YOLOv5等深度学习框架的铝材缺陷检测模型,该模型使用了大量包含…...
docker 僵尸进程问题
docker僵尸进程 子进程结束后,父进程没有回收该进程资源(父进程可能没有wait),子进程残留资源存放与内核中,就变为僵尸进程(zombie) 场景分析:python脚本A中执行B应用,将A部署在docker中&#…...
Web 毕设篇-适合小白、初级入门练手的 Spring Boot Web 毕业设计项目:电影院后台管理系统(前后端源码 + 数据库 sql 脚本)
🔥博客主页: 【小扳_-CSDN博客】 ❤感谢大家点赞👍收藏⭐评论✍ 文章目录 1.0 项目介绍 2.0 用户登录功能 3.0 用户管理功能 4.0 影院管理功能 5.0 电影管理功能 6.0 影厅管理功能 7.0 电影排片管理功能 8.0 用户评论管理功能 9.0 用户购票功…...
webpack5 的五大核心配置(二)
webpack主要构成部分: entry 入口output 出口loaders 转化器plugins 插件mode 模式devServer 开发服务器 webpack.config.js 配置文件基本格式 module.exports{//入口文件entry:{},//出口文件output:{},//module rules loadersmodule{};//插件plugins:[],//开发…...