【Linux | 网络编程】02 IO多路复用

【Linux | 网络编程】02 IO多路复用

4 IO多路复用

4.1 IO模型

传统I/O(输入/输出)是相对于内存而言的:输入即文件到内存,输出即内存到文件。

socket通信中,每个socket的文件描述符fd对应于内核中的一块缓冲区(读缓冲区+写缓冲区)。这里的I/O则是对缓冲区的操作。

常见的几种IO模型:

  • 阻塞I/O(Blocking I/O)
    • 等待与拷贝阶段全程阻塞,进程挂起,不占用 CPU;
    • 例如:accept()read()recv()默认都是阻塞 I/O;
    • 优点:代码简单;
    • 缺点:一个进程只能处理一个连接,高并发下会卡死(比如 100 个客户端连接,只能串行处理)。
  • 非阻塞I/O(Non-blocking I/O)
    • 等待数据就绪阶段非阻塞:每次调用read()会立即返回,没数据就返回-1,并设置 errno=EAGAIN/EWOULDBLOCK;进程需要轮询调用read(),直到数据就绪;
    • 把套接字设置为非阻塞模式(O_NONBLOCK);
    • 优点:一个进程能处理多个连接;
    • 缺点:轮询会占用大量CPU资源(空等也在跑循环),效率低。
  • I/O多路复用(I/O Multiplexing)
    • 进程通过epoll等函数监听多个套接字,只有当至少一个套接字数据就绪时,函数才返回;本质是 “阻塞在多路复用函数上”,而非阻塞在单个 read()/write() 上;
    • 最常用的高并发模型,例如selectpollepoll
    • 优点:一个进程能高效处理上万连接(高并发核心方案),CPU利用率高;
    • 缺点:代码比阻塞I/O复杂,需要理解事件驱动逻辑。
  • 信号驱动I/O(Signal-driven I/O)
    • 数据就绪时,内核给进程发SIGIO信号,进程在信号处理函数中调用read()拷贝数据;等待阶段非阻塞,拷贝阶段阻塞;
    • 通过信号SIGIO实现:先注册信号处理函数,然后进程继续执行;
    • 优点:无需轮询,等待阶段不占用CPU;
    • 缺点:信号处理逻辑复杂,高并发下信号会 “扎堆”,难以处理,实际中很少用。
  • 异步I/O(Asynchronous I/O)
    • 等待与拷贝阶段全程非阻塞:进程调用aio_read()后直接返回,内核完成 “等待数据+拷贝到用户缓冲区”后,通过回调/信号通知进程;进程全程不用参与I/O操作,直到内核通知操作完成。
    • 最理想的 I/O 模型,例如aio_readaio_write
    • 优点:完全非阻塞,CPU利用率最高;
    • 缺点:Linux下支持不够完善,代码复杂度高。

4.2 select

4.2.1 select函数

select函数允许程序同时监视多个文件描述符,检测它们的状态变化(例如,数据可读或可写),从而高效地管理多个I/O操作而不需为每个操作创建独立线程或进程。这种能力尤其在网络通信和服务器编程中提高了并发性能。select还支持非阻塞I/O,允许程序在等待I/O事件的同时执行其他任务,有效利用CPU资源。

基本原型如下:

intselect(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds,structtimeval*timeout);

参数:

  • nfds:监控的文件描述符集合中最大文件描述符的值加1。在使用select函数时,必须确保这个参数正确设置,以便函数能监视所有相关的文件描述符。
  • readfds, writefds, exceptfds:这三个参数分别代表读、写和异常监视的文件描述符集合。它们使用fd_set类型表示,这是一种通过位图来管理文件描述符的数据结构。无需监视则传NULL。以下是对fd_set操作的常用宏定义:
    • FD_SET(fd, &set):将文件描述符fd添加到集合set中;
    • FD_CLR(fd, &set):从集合set中移除文件描述符fd;
    • FD_ISSET(fd, &set):检查文件描述符fd是否已被加入集合set;
    • FD_ZERO(&set):清空集合set中的所有文件描述符。
    • 当timeout为NULL时,select会无限等待,直到至少有一个文件描述符就绪。
    • 当timeout设置为0时(即tv_sec和tv_usec都为0),select会立即返回,用于轮询。
    • 设置具体的时间,select将等待直到该时间过去或者有文件描述符就绪。

timeout:这是一个指向timeval结构的指针,该结构用于设定select等待I/O事件的超时时间。结构定义如下:

structtimeval{long tv_sec;// secondslong tv_usec;// microseconds};

timeout的设定有三种情况:

返回值:

  • 大于0:表示就绪的文件描述符数量,即有多少文件描述符已经准备好进行I/O操作。
  • 等于0:表示超时,没有文件描述符在指定时间内就绪。
  • 小于0:发生错误。返回-1,并设置errno。

4.2.2 调用流程

  1. 始化文件描述符集合:使用fd_set类型的集合来监控不同的I/O操作(读、写、异常),操作这些集合可使用相关宏。
  2. 调用select函数:传入文件描述符集合和超时时间,允许在超时或有描述符就绪时返回,避免无限等待。
  3. 阻塞与等待I/O事件:select阻塞程序执行,直至至少一个文件描述符就绪或超时。
  4. 检查就绪的文件描述符:当select返回后,检查各文件描述符集合的状态,确定哪些文件描述符准备好进行读、写或异常处理。
  5. 循环监控:如果继续监控是必要的,重置文件描述符集合并重新调用select,这支持持续监控多个I/O源。

4.2.3 select实现实时对话

server服务端:

#include<func.h>#defineCONNS_MAX1024intmain(int argc,char*argv[]){signal(SIGPIPE, SIG_IGN);ARGC_CHECK(argc,3);int ret =0;int listenfd =socket(AF_INET, SOCK_STREAM,0);ERROR_CHECK(listenfd,-1,"socket");printf("listenfd : %d.\n", listenfd);int on =1; ret =setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR,&on,sizeof(int));ERROR_CHECK(ret,-1,"setsockopt");structsockaddr_in serveraddr;memset(&serveraddr,0,sizeof(serveraddr)); serveraddr.sin_family = AF_INET; serveraddr.sin_port =htons(atoi(argv[2])); serveraddr.sin_addr.s_addr =inet_addr(argv[1]); ret =bind(listenfd,(conststructsockaddr*)&serveraddr,sizeof(serveraddr));ERROR_CHECK(ret,-1,"bind"); ret =listen(listenfd,10);ERROR_CHECK(ret,-1,"listen");printf("server start listening!\n"); fd_set rdset;FD_ZERO(&rdset);char buff[1000]={0};int maxfd = listenfd;int conns[CONNS_MAX]={0};while(1){FD_ZERO(&rdset);FD_SET(listenfd,&rdset);for(int i =0; i < CONNS_MAX;++i){if(conns[i]!=0){FD_SET(conns[i],&rdset);}}select(maxfd +1,&rdset,NULL,NULL,NULL);if(FD_ISSET(listenfd,&rdset)){structsockaddr_in clientaddr;memset(&clientaddr,0,sizeof(clientaddr));socklen_t len =sizeof(clientaddr);int peerfd =accept(listenfd,(structsockaddr*)&clientaddr,&len);printf("%s:%d has connected, peerfd: %d.\n",inet_ntoa(clientaddr.sin_addr),ntohs(clientaddr.sin_port), peerfd);for(int i =0; i < CONNS_MAX;++i){if(conns[i]==0){ conns[i]= peerfd;break;}}if(maxfd < peerfd){ maxfd = peerfd;}}for(int i =0; i < CONNS_MAX;++i){if(conns[i]!=0){if(FD_ISSET(conns[i],&rdset)){memset(buff,0,sizeof(buff)); ret =recv(conns[i], buff,sizeof(buff),0);if(ret ==0){close(conns[i]); conns[i]=0;continue;}printf("recv msg %d: %s\n", conns[i], buff);send(conns[i], buff,strlen(buff),0);}}}}printf("byebye.\n");close(listenfd);return0;}

client客户端:

#include<func.h>intmain(int argc,char*argv[]){ARGC_CHECK(argc,3);int ret =0;int clientfd =socket(AF_INET, SOCK_STREAM,0);ERROR_CHECK(clientfd,-1,"socket");structsockaddr_in serveraddr;memset(&serveraddr,0,sizeof(serveraddr)); serveraddr.sin_family = AF_INET; serveraddr.sin_port =htons(atoi(argv[2])); serveraddr.sin_addr.s_addr =inet_addr(argv[1]); ret =connect(clientfd,(conststructsockaddr*)&serveraddr,sizeof(serveraddr));ERROR_CHECK(ret,-1,"connect");printf("connection success!\n"); fd_set rdset;FD_ZERO(&rdset);char buff[1000]={0};while(1){FD_ZERO(&rdset);FD_SET(clientfd,&rdset);FD_SET(STDIN_FILENO,&rdset);select(clientfd +1,&rdset,NULL,NULL,NULL);if(FD_ISSET(STDIN_FILENO,&rdset)){memset(buff,0,sizeof(buff)); ret =read(STDIN_FILENO, buff,sizeof(buff));if(ret ==0){break;}send(clientfd, buff,strlen(buff)-1,0);printf("send successfully!\n");}if(FD_ISSET(clientfd,&rdset)){memset(buff,0,sizeof(buff)); ret =recv(clientfd, buff,sizeof(buff),0);if(ret ==0){break;}printf("recv msg: %s\n", buff);}}printf("byebye.\n");return0;}

4.2.4 select的限制

  • 支持能够监听的文件描述符数量有限(32位系统为1024,64位系统为2048);
  • fd_set位图不能重用,每次循环前都需要进行重置;
  • 每次调用select时,都需要把fd位图从用户态拷贝到内核态,且都需要在内核遍历传递进来的所有fd,这个开销在fd很多时会很大;
  • 用户态必须遍历所有已经建立连接的fd,查找到真正发生了事件的fd采取进行相应处理,效率不高。

4.3 epoll

4.3.1 epoll基本原理

epoll可以监听多个设备的就绪状态,让进程或者线程只在有事件发生之后再执行真正的读写操作。epoll可以在内核态空间当中维持两个数据结构:监听事件集合和就绪事件队列。监听事件集合用来存储所有需要关注的设备(即文件描述符)和对应操作(比如读、写、挂起和异常等等),当监听的设备有事件产生时,硬件会采用中断等方式通知操作系统,操作系统会将就绪事件拷贝到就绪事件队列中,并且找到阻塞在epoll_wait的线程让其就绪。监听事件集合通常是一个红黑树,就绪事件队列是一个线性表。

epoll基本原理

select相比,epoll的优势如下:

  • 除了水平触发,还支持边缘触发;
  • 监听事件集合容量很大,有多少内存就能放下多少文件描述符;
  • 监听事件集合常驻内核态,调用epoll_wait函数不会修改监听性质,不需要每次将集合从用户态拷贝到内核态;
  • 监听事件和就绪事件的状态分为两个数据结构存储,当epoll_wait就绪之后,用户可以直接遍历就绪事件队列,而不需要在所有事件当中进行轮询。

有了这些优势之后, epoll逐渐取代了select的市场地位,尤其是在管理巨大量连接的高并发场景中, epoll的性能要远超select

4.3.2 核心函数

4.3.2.1 epoll_create1()

epoll_create1()/epoll_create()函数用来创建一个epoll实例:

#include<sys/epoll.h>intepoll_create(int size);intepoll_create1(int flags);// flag 一般直接指定为0// 返回值 成功 --> 大于0(一个文件描述符epfd), 失败 --> -1(设置errno)

这个epoll实例中就包含监听事件集合和就绪设备集合。

4.3.2.2 epoll_ctl()

epoll_ctl用于调整监听事件集合,可以向其中添加、修改或删除相关文件描述符:

intepoll_ctl(int epfd,int op,int fd,structepoll_event*event);// epfd 指定epoll的实例// op 操作类型,可以是:// EPOLL_CTL_ADD 将文件描述符fd注册到epfd中// EPOLL_CTL_MOD 修改已注册文件描述符fd的事件类型// EPOLL_CTL_DEL 从epfd中删除文件描述符fd// fd 要操作的目标文件描述符// event 一个指向epoll_event结构体的指针,用于指定fd感兴趣的事件类型// 返回值 成功 --> 返回0(一个文件描述符epfd), 失败 --> -1(设置errno)

epoll_event结构体定义如下:

structepoll_event{ __uint32_t events;// 要监听的事件epoll_data_t data;};typedefunion epoll_data {void*ptr;int fd;// 关注的文件描述符uint32_t u32;uint64_t u64;}epoll_data_t;

联合体:n个字段共享同一片地址空间,同一时刻只能表示一个字段。

events常用的事件类型有:

  • EPOLLIN:文件描述符可读(有数据到达);
  • EPOLLOUT:文件描述符可写(可以发送数据);
  • EPOLLET:边缘触发 (Edge Triggered)模式,是epoll最强大的特性之一;
  • EPOLLLT:水平触发 (Level Triggered)模式(默认);
  • EPOLLERR:文件描述符发生错误;
  • EPOLLHUP:文件描述符被挂断(对端关闭连接);
  • EPOLLONESHOT:一次性事件,事件触发后自动取消监听,需要重新MOD才能再次监听。
4.3.2.3 epoll_wait()

epoll_wait用于使线程陷入阻塞,直到监听的设备就绪或者超时:

intepoll_wait(int epfd,structepoll_event*events,int maxevents,int timeout);// epfd 指定epoll的实例// events 用户态空间的struct epoll_event数组首地址// maxevents 数组的长度j// timeout 超时事件,单位是毫秒;设置为-1表示无限等待// 返回值 大于0 --> 就绪的文件描述符的数量// 等于0 --> epoll_wait超时 // 小于0 --> 发生了错误,返回-1,设置errno

epoll_wait是一个阻塞式函数。

4.3.3 epoll与select

  1. 对于文件描述符的监听
    • select每次执行之前都需要重新进行设置 ;
    • epoll只需要监听一次,后续不再需要再次监听。
  2. 底层实现
    • select底层是采用数组(位图),其监听的文件描述符是有上限的;
    • epoll底层实现采用的是红黑树+就绪链表,其监听的文件描述符没有上限,一般情况下与内存有关 。
  3. 内核态轮询机制
    • select每一次轮询时,都要对监听的所有fd都去询问一次是否就绪;
    • epoll具有回调机制,一旦监听的某一个文件描述符上发生了事件,会主动通知epoll进行处理,尤其是针对于大并发的请求,效率远远高于select。
  4. 用户态轮询
    • select返回之后需要对已经建立好的连接轮询,确认其是否在已经就绪的fd_set中;
    • epoll_wait返回之后,相应的就绪文件描述符已经拿到了,不再需要轮询所有已经建立好的连接。

4.3.4 epoll实现实时对话

server服务端:

#include<func.h>#defineCONNS_MAX1024intmain(int argc,char*argv[]){signal(SIGPIPE, SIG_IGN);ARGC_CHECK(argc,3);int ret =0;int listenfd =socket(AF_INET, SOCK_STREAM,0);ERROR_CHECK(listenfd,-1,"socket");printf("listenfd : %d.\n", listenfd);int on =1; ret =setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR,&on,sizeof(int));ERROR_CHECK(ret,-1,"setsockopt");structsockaddr_in serveraddr;memset(&serveraddr,0,sizeof(serveraddr)); serveraddr.sin_family = AF_INET; serveraddr.sin_port =htons(atoi(argv[2])); serveraddr.sin_addr.s_addr =inet_addr(argv[1]); ret =bind(listenfd,(conststructsockaddr*)&serveraddr,sizeof(serveraddr));ERROR_CHECK(ret,-1,"bind"); ret =listen(listenfd,10);ERROR_CHECK(ret,-1,"listen");printf("server start listening!\n");int epfd =epoll_create1(0);ERROR_CHECK(epfd,-1,"epoll_create1");structepoll_event ev; ev.events = EPOLLIN; ev.data.fd = listenfd; ret =epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd,&ev);ERROR_CHECK(ret,-1,"epoll_ctl");structepoll_event*pEventArr =(structepoll_event*)calloc(CONNS_MAX,sizeof(structepoll_event));while(1){int nready =epoll_wait(epfd, pEventArr, CONNS_MAX,-1);if(nready ==-1&& errno == EINTR){continue;}elseif(nready ==0){printf("timeout!\n");}elseif(nready ==-1){break;}else{for(int i =0; i < nready;++i){int fd = pEventArr[i].data.fd;if(fd == listenfd){structsockaddr_in clientaddr;memset(&clientaddr,0,sizeof(clientaddr));socklen_t len =sizeof(clientaddr);int peerfd =accept(listenfd,(structsockaddr*)&clientaddr,&len);printf("%s:%d has connected, peerfd: %d.\n",inet_ntoa(clientaddr.sin_addr),ntohs(clientaddr.sin_port), peerfd);structepoll_event ev; ev.events = EPOLLIN; ev.data.fd = peerfd;epoll_ctl(epfd, EPOLL_CTL_ADD, peerfd,&ev);}else{if(pEventArr[i].events == EPOLLIN){char buff[1024]={0}; ret =recv(fd, buff,sizeof(buff), MSG_DONTWAIT);if(ret ==0){ ev.data.fd = fd; ret =epoll_ctl(epfd, EPOLL_CTL_DEL, fd,&ev);ERROR_CHECK(ret,-1,"epoll_ctl");close(fd);printf("byebye %d.\n", fd);continue;}printf("recv msg %d: %s\n", fd, buff);send(fd, buff,strlen(buff),0);}}}}}close(listenfd);close(epfd);return0;}

client客户端可以使用select实现。

4.3.5 非阻塞操作

之前所了解的读(read)操作都是阻塞的,即调用该函数时,如果硬件没有将数据拷贝到内核缓冲区当中时,线程会主动陷入阻塞。为此,操作系统为用户提供非阻塞版本的read:当读取内核缓冲区数据时,如果没有数据, read会直接返回-1,并将errno的数值设置为EAGAIN。非阻塞操作通常会配合循环一起使用以实现一种同步非阻塞的IO模型。

实现非阻塞有两种方式:

将某一个文件描述符设置为非阻塞,使用fcntl函数可以将已打开文件增加一个非阻塞选项:

intfcntl(int fd,int cmd,.../* arg */);//cmd F_GETFL 获取状态作为返回值//cmd F_SETFL arg STATUS 将文件状态设置为新状态

示例:

int flags =fcntl(peerfd, F_GETFL,0);// 读取peerfd当前的文件状态标志 flags |= O_NONBLOCK;// 给标志添加 O_NONBLOCK 属性fcntl(peerfd, F_SETFL, flags);

直接对read/recv函数的第四个参数进行设置MSG_DONTWAIT,只作用于这一次调用。

recv(fd, buff,sizeof(buff), MSG_DONTWAIT);send(fd, buff,strlen(buff), MSG_DONTWAIT);

4.3.6 两种工作模式

4.3.6.1 水平触发(LT)

只要缓冲区有数据可读(或可写),epoll_wait就会一直就绪,直到数据读完(或写完)。

  • 优点: 简单,不容易丢事件。即使只读了一部分数据,下次epoll_wait依然会通知,直到缓冲区清空。
  • 缺点:如果没有一次性处理完所有数据,可能会被重复通知,导致不必要的系统调用。

select/epoll默认情况下都是采用水平触发。

4.3.6.2 边缘触发(ET)

只有当文件描述符状态发生变化时(例如从不可读变为可读,或者有新数据到来),epoll_wait才会通知一次。如果不去接收数据,之后epoll不会再通知进行处理。

  • 优点:效率更高。只通知一次,避免重复通知,减少系统调用次数,适合处理大量并发连接。
  • 缺点:要求程序一次性处理完所有数据。如果只读了一部分,下次epoll_wait不会再通知你,剩下的数据就会滞留在内核缓冲区,导致数据丢失或逻辑错误。

采用边缘触发之后,消息处理的时机变得不确定了,因此设置消息处理的条件。此时可以将recv函数的第四个参数设置为MSG_PEEK,只是去窥探一下内核接收缓冲区,并将数据拷贝到用户态缓冲区(当第四个参数flags值为0时,将内核接收缓冲区中的数据拷贝到了用户态,同时还移走了内核中的数据)。

epoll的ET模式几乎总是和非阻塞 I/O 结合使用。当epoll_wait在ET模式下通知某个文件描述符可读时,应该在一个循环中尽可能多地读取所有可用数据,直到read()返回EAGAINEWOULDBLOCK(表示当前没有更多数据可读了)。这样做是为了避免数据在内核缓冲区堆积,导致程序无法感知到未处理的数据。例如:

while(1){int nready =epoll_wait(epfd, pEventArr, CONNS_MAX,-1);if(nready ==-1&& errno == EINTR){continue;}elseif(nready ==0){printf("timeout!\n");}elseif(nready ==-1){break;}else{for(int i =0; i < nready;++i){int fd = pEventArr[i].data.fd;if(fd == listenfd){structsockaddr_in clientaddr;memset(&clientaddr,0,sizeof(clientaddr));socklen_t len =sizeof(clientaddr);int peerfd =accept(listenfd,(structsockaddr*)&clientaddr,&len);printf("%s:%d has connected, peerfd: %d.\n",inet_ntoa(clientaddr.sin_addr),ntohs(clientaddr.sin_port), peerfd);int flag =fcntl(peerfd, F_GETFL,0); flag |= O_NONBLOCK;fcntl(peerfd, F_SETFL, flag);structepoll_event ev; ev.events = EPOLLIN | EPOLLET; ev.data.fd = peerfd;epoll_ctl(epfd, EPOLL_CTL_ADD, peerfd,&ev);}else{if(pEventArr[i].events == EPOLLIN){char buff[1024]={0}; ret =recv(fd, buff,sizeof(buff), MSG_PEEK);if(ret ==0){ ev.data.fd = fd; ret =epoll_ctl(epfd, EPOLL_CTL_DEL, fd,&ev);ERROR_CHECK(ret,-1,"epoll_ctl");close(fd);printf("byebye %d.\n", fd);continue;}printf("ret: %d\n", ret);printf("recv msg %d: %s\n", fd, buff);if(ret >15){// 当内核缓冲区数据长度大于15字节时才会进行处理消息的操作// 此处为移走内核中数据recv(fd, buff, ret,0);send(fd, buff,strlen(buff),0);}}}}}}

4.3.7 epoll实现聊天室

server服务端:

#include<func.h>#defineCONNS_MAX1024int clientfds[CONNS_MAX]={0};intmain(int argc,char*argv[]){signal(SIGPIPE, SIG_IGN);ARGC_CHECK(argc,3);int ret =0;memset(clientfds,0,sizeof(clientfds));int listenfd =socket(AF_INET, SOCK_STREAM,0);ERROR_CHECK(listenfd,-1,"socket");printf("listenfd : %d.\n", listenfd);int on =1; ret =setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR,&on,sizeof(int));ERROR_CHECK(ret,-1,"setsockopt");structsockaddr_in serveraddr;memset(&serveraddr,0,sizeof(serveraddr)); serveraddr.sin_family = AF_INET; serveraddr.sin_port =htons(atoi(argv[2])); serveraddr.sin_addr.s_addr =inet_addr(argv[1]); ret =bind(listenfd,(conststructsockaddr*)&serveraddr,sizeof(serveraddr));ERROR_CHECK(ret,-1,"bind"); ret =listen(listenfd,10);ERROR_CHECK(ret,-1,"listen");printf("server start listening!\n");int epfd =epoll_create1(0);ERROR_CHECK(epfd,-1,"epoll_create1");structepoll_event ev; ev.events = EPOLLIN; ev.data.fd = listenfd; ret =epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd,&ev);ERROR_CHECK(ret,-1,"epoll_ctl");structepoll_event*pEventArr =(structepoll_event*)calloc(CONNS_MAX,sizeof(structepoll_event));while(1){int nready =epoll_wait(epfd, pEventArr, CONNS_MAX,-1);if(nready ==-1&& errno == EINTR){continue;}elseif(nready ==0){printf("timeout!\n");}elseif(nready ==-1){break;}else{// 新连接到来for(int i =0; i < nready;++i){int fd = pEventArr[i].data.fd;if(fd == listenfd){structsockaddr_in clientaddr;memset(&clientaddr,0,sizeof(clientaddr));socklen_t len =sizeof(clientaddr);int peerfd =accept(listenfd,(structsockaddr*)&clientaddr,&len);printf("%s:%d has connected, peerfd: %d.\n",inet_ntoa(clientaddr.sin_addr),ntohs(clientaddr.sin_port), peerfd);int flag =fcntl(peerfd, F_GETFL,0); flag |= O_NONBLOCK;fcntl(peerfd, F_SETFL, flag);structepoll_event ev; ev.events = EPOLLIN | EPOLLET; ev.data.fd = peerfd;epoll_ctl(epfd, EPOLL_CTL_ADD, peerfd,&ev);for(int i =0; i < CONNS_MAX;++i){if(clientfds[i]==0){ clientfds[i]= peerfd;break;}}char welcome_msg[1024]={0};snprintf(welcome_msg,sizeof(welcome_msg),"[system] %s:%d (fd: %d) joined the chat!\n",inet_ntoa(clientaddr.sin_addr),ntohs(clientaddr.sin_port), peerfd);for(int i =0; i < CONNS_MAX;++i){if(clientfds[i]!=0&& clientfds[i]!= peerfd){send(clientfds[i], welcome_msg,strlen(welcome_msg),0);}}}else{if(pEventArr[i].events == EPOLLIN){char buff[1024]={0};ssize_t recv_len;while((recv_len =recv(fd, buff,sizeof(buff)-1,0))>0){// 正常处理客户端消息 buff[recv_len]='\0';printf("recv msg %d: %s\n", fd, buff);char send_buff[2048]={0};snprintf(send_buff,sizeof(send_buff),"[client %d]: %s", fd, buff);for(int i =0; i < CONNS_MAX;++i){if(clientfds[i]!=0&& clientfds[i]!= fd){send(clientfds[i], send_buff,strlen(send_buff),0);}}memset(buff,0,sizeof(buff));}if(recv_len ==0){// 客户端断开连接 ev.data.fd = fd; ret =epoll_ctl(epfd, EPOLL_CTL_DEL, fd,&ev);ERROR_CHECK(ret,-1,"epoll_ctl");char leave_msg[1024]={0};snprintf(leave_msg,sizeof(leave_msg),"[system] client(fd: %d) left the chat!\n", fd);for(int i =0; i < CONNS_MAX;++i){if(clientfds[i]!=0&& clientfds[i]!= fd){send(clientfds[i], leave_msg,strlen(leave_msg),0);}}for(int i =0; i < CONNS_MAX;++i){if(clientfds[i]== fd){ clientfds[i]=0;break;}}close(fd);printf("byebye %d.\n", fd);continue;}}}}}}close(listenfd);close(epfd);return0;}

Read more

ARM Linux 驱动开发篇---Linux 设备树简介-- Ubuntu20.04

ARM Linux 驱动开发篇---Linux 设备树简介-- Ubuntu20.04

🎬 渡水无言:个人主页渡水无言 ❄专栏传送门: 《linux专栏》   《嵌入式linux驱动开发》 ⭐️流水不争先,争的是滔滔不绝  📚博主简介:第二十届中国研究生电子设计竞赛全国二等奖 |国家奖学金 | 省级三好学生 | 省级优秀毕业生获得者 | ZEEKLOG新星杯TOP18 | 半导纵横专栏博主 | 211在读研究生 在这里主要分享自己学习的linux嵌入式领域知识;有分享错误或者不足的地方欢迎大佬指导,也欢迎各位大佬互相三连 目录 前言 一、什么是设备树? 二、DTS、DTB 和 DTC 三、DTS编译规则 四、DTB 文件最终如何被内核使用? 总结 前言 在传统驱动中,GPIO址、中断号、时钟参数等硬件信息都硬编码在代码里,换一块开发板就要改一次驱动;而设备树通过.dts文件统一描述所有硬件资源,驱动只需通过标准 API获取资源,实现 “一次编写、多板适配”。如今设备树已经成为 Linux 驱动开发的核心规范,是每一位嵌入式

By Ne0inhk
【Linux】从版本控制到代码调试:Git 入门与 GDB 调试器学习指南

【Linux】从版本控制到代码调试:Git 入门与 GDB 调试器学习指南

前言  作为 Linux 开发的 “左膀右臂”,Git 管版本、gdb 调程序 —— 前者搞定代码的迭代与协同,后者专治程序里的各种 “疑难杂症”。这篇博客就从 Git 的核心概念、基础操作,讲到 gdb 的调试指令,把这俩工具的高频用法讲透,帮你把开发效率直接拉满。 目录 一、Git 版本控制器 1.1 什么是 Git? 【问题】:什么是分布式? 【问题】:什么是集中式? 1.2 Git 核心概念  1.3 GitHub 与 Gitee 二、Git 基础操作 1. Git安装 2. 远程仓库与本地仓库联动(以

By Ne0inhk
连接管理模块和服务器模块

连接管理模块和服务器模块

1. 封装连接管理类 向用户提供一个用于实现网络通信的 Connection 对象,从其内部可创建出粒度更轻的Channel 对象,用于与客户端进行网络通信。 1. 成员信息: * 连接关联的信道管理句柄(实现信道的增删查) * 连接关联的实际用于通信的 muduo::net::Connection 连接 * protobuf 协议处理的句柄(ProtobufCodec 对象) * 消费者管理句柄 * 虚拟机句柄 * 异步工作线程池句柄 2. 连接操作: * 提供创建 Channel 信道的操作 * 提供删除 Channel 信道的操作 3. 连接管理: * 连接的增删查 为什么需要这些成员和操作? 1. 信道管理句柄:因为AMQP协议允许在一个连接上创建多个信道,每个信道可以独立进行操作(如声明队列、发布消息等)。所以连接管理模块需要能够管理这些信道,包括创建、删除和查找。 2. muduo::net::Connection:这是实际进行网络通信的对象,

By Ne0inhk