萌妹带你学多路复用:Socket 本地 IPC 与 epoll 事件驱动机制 前言演进的前提-内核的进化之所以能从 poll 演进到 epoll本质是因为内核提供了新的基础设施红黑树、就绪链表、回调机制使得事件驱动编程模型成为可能。程序员需要主动采用新的 API将代码从人为循环遍历所有 fd 的轮询模式改写为等待内核事件通知的回调模式从而实现从 O(n) 到 O(1) 的性能跃升。事件驱动是什么程序不主动轮询检查状态而是被动等待事件通知内核资源是什么有限的硬件和软件实体内核资源以进程为容器容器之间相互隔离fd是什么续系统调用又是什么续fd和系统调用有什么关系层次接口作用系统调用接口read,write,socket,bind…用户态进入内核态的入口函数资源标识接口fd文件描述符系统调用的参数告诉内核操作哪个资源fd只是内核资源的上层暴露系统调用要么返回fd要么需要fd进程通信版本1《场景和思路一个客户端要和一个服务端通信那么就各自直接send和recv即可进程通信版本1单进程-客户端#include stdio.h #include string.h #include stdlib.h #include sys/types.h #include sys/socket.h #include sys/un.h // Unix Domain Socket 头文件 #include unistd.h #define SOCKET_PATH /tmp/demo_ipc.sock #define BUFFER_SIZE 1024 int main() { int sock_fd; struct sockaddr_un server_addr; // Unix Domain 地址结构 char buffer[BUFFER_SIZE]; int recv_len; // 第1步创建 Unix Domain 套接字 sock_fd socket(AF_UNIX, SOCK_STREAM, 0); if (sock_fd 0) { exit(1); } // 第2步设置服务器地址文件路径 memset(server_addr, 0, sizeof(server_addr)); server_addr.sun_family AF_UNIX; strncpy(server_addr.sun_path, SOCKET_PATH, sizeof(server_addr.sun_path) - 1); // 第3步连接服务器 if (connect(sock_fd, (struct sockaddr *)server_addr, sizeof(server_addr)) 0) { close(sock_fd); exit(1); } // 第4步发送数据 char *message Hello Server! This is IPC Client.; if (send(sock_fd, message, strlen(message), 0) 0) { close(sock_fd); exit(1); } // 第5步接收响应 memset(buffer, 0, BUFFER_SIZE); recv_len recv(sock_fd, buffer, BUFFER_SIZE - 1, 0);//重点recv 函数会阻塞等待 if (recv_len 0) { } else if (recv_len 0) { } else { } // 第6步关闭连接 close(sock_fd); return 0; }进程通信版本1单进程-服务端/** * Unix Domain Socket IPC 通信示例 - 服务器端 * 用于教学演示同一台机器上进程间通信 * 使用文件路径作为地址不经过网络协议栈 */ #include stdio.h #include string.h #include stdlib.h #include sys/types.h #include sys/socket.h #include sys/un.h // Unix Domain Socket 头文件 #include unistd.h #define SOCKET_PATH /tmp/demo_ipc.sock // 套接字文件路径作为通信地址 #define BUFFER_SIZE 1024 int main() { int listen_fd, conn_fd; struct sockaddr_un server_addr; // Unix Domain 地址结构 char buffer[BUFFER_SIZE]; int recv_len; // 第1步创建 Unix Domain 套接字 listen_fd socket(AF_UNIX, SOCK_STREAM, 0); if (listen_fd 0) { perror(【第一步】[服务器] socket failed); exit(1); } // 第2步设置地址结构并绑定 unlink(SOCKET_PATH); memset(server_addr, 0, sizeof(server_addr)); server_addr.sun_family AF_UNIX; strncpy(server_addr.sun_path, SOCKET_PATH, sizeof(server_addr.sun_path) - 1); if (bind(listen_fd, (struct sockaddr *)server_addr, sizeof(server_addr)) 0) { close(listen_fd); exit(1); } // 第3步开始监听 if (listen(listen_fd, 5) 0) { close(listen_fd); exit(1); } // 第4步接受客户端连接 conn_fd accept(listen_fd, NULL, NULL); if (conn_fd 0) { close(listen_fd); exit(1); } // 第5步接收数据 memset(buffer, 0, BUFFER_SIZE); recv_len recv(conn_fd, buffer, BUFFER_SIZE - 1, 0);//重点recv 函数会阻塞等待 if (recv_len 0) { } else if (recv_len 0) { } else { // 第6步发送响应 char *response Hello Client! IPC message received.; send(conn_fd, response, strlen(response), 0); } // 第7步关闭连接并清理 close(conn_fd); close(listen_fd); unlink(SOCKET_PATH); return 0; }进程通信版本1总结阻塞的方式监听fd》socket基础通信模型-单进程阻塞式1.服务端调用rece函数阻塞等待客户端send。2.客户端send后调用rece函数阻塞等待服务端send。3.可以看到对于监听fd采用的是阻塞的方式。进程通信版本2《场景和思路300个客户端要和一个服务端通信每个客户端发来的数据都不同所以服务端就需要监听连接300个连接就fork出300个子进程在这个子进程中去recv和send。可以采用双while,大层while是父进程小层while是子进程。因为是不同进程所以小while不会阻塞大while进程通信版本2多进程-客户端//假设执行了300个客户端的C代码可执行文件 #include stdio.h #include string.h #include stdlib.h #include sys/types.h #include sys/socket.h #include sys/un.h #include unistd.h #define SOCKET_PATH /tmp/demo2_multi_process.sock #define BUFFER_SIZE 1024 int main() { int sock_fd; struct sockaddr_un server_addr; char buffer[BUFFER_SIZE]; char input[BUFFER_SIZE]; sock_fd socket(AF_UNIX, SOCK_STREAM, 0); if (sock_fd 0) { exit(1); } memset(server_addr, 0, sizeof(server_addr)); server_addr.sun_family AF_UNIX; strncpy(server_addr.sun_path, SOCKET_PATH, sizeof(server_addr.sun_path) - 1); if (connect(sock_fd, (struct sockaddr *)server_addr, sizeof(server_addr)) 0) { close(sock_fd); exit(1); } while (1) { printf( ); fflush(stdout); if (fgets(input, sizeof(input), stdin) NULL) { break; } input[strcspn(input, \n)] 0; if (strcmp(input, quit) 0) { break; } if (send(sock_fd, input, strlen(input), 0) 0) { break; } memset(buffer, 0, BUFFER_SIZE); int recv_len recv(sock_fd, buffer, BUFFER_SIZE - 1, 0); if (recv_len 0) { break; } } close(sock_fd); return 0; }进程通信版本2多进程-服务端/** * 多进程模型 * */ #include stdio.h #include string.h #include stdlib.h #include sys/types.h #include sys/socket.h #include sys/un.h #include unistd.h #include signal.h #include sys/wait.h #define SOCKET_PATH /tmp/demo2_multi_process.sock #define BUFFER_SIZE 1024 // 处理僵尸进程 void sigchld_handler(int sig) { (void)sig; while (waitpid(-1, NULL, WNOHANG) 0); } int main() { int listen_fd, conn_fd; struct sockaddr_un server_addr; char buffer[BUFFER_SIZE]; pid_t pid; // 设置SIGCHLD信号处理防止僵尸进程 signal(SIGCHLD, sigchld_handler); // 第1步创建监听套接字 listen_fd socket(AF_UNIX, SOCK_STREAM, 0); if (listen_fd 0) { perror([服务器] socket failed); exit(1); } // 第2步绑定地址 unlink(SOCKET_PATH); memset(server_addr, 0, sizeof(server_addr)); server_addr.sun_family AF_UNIX; strncpy(server_addr.sun_path, SOCKET_PATH, sizeof(server_addr.sun_path) - 1); if (bind(listen_fd, (struct sockaddr *)server_addr, sizeof(server_addr)) 0) { close(listen_fd); exit(1); } // 第3步开始监听 if (listen(listen_fd, 10) 0) { close(listen_fd); exit(1); } // 第4步主循环 - 接受连接并fork子进程 while (1) { conn_fd accept(listen_fd, NULL, NULL); if (conn_fd 0) { continue; } pid fork(); if (pid 0) { close(conn_fd); continue; } if (pid 0) { // 子进程专门服务一个客户端 close(listen_fd); while (1) { memset(buffer, 0, BUFFER_SIZE); int recv_len recv(conn_fd, buffer, BUFFER_SIZE - 1, 0); if (recv_len 0) { if (recv_len 0) { } else { } break; } char response[BUFFER_SIZE]; snprintf(response, sizeof(response), [PID:%d] 收到你的消息: %s, getpid(), buffer); send(conn_fd, response, strlen(response), 0); } close(conn_fd); exit(0); } else { // 父进程继续等待新连接 close(conn_fd); } } close(listen_fd); unlink(SOCKET_PATH); return 0; }进程通信版本2 总结每个fd连接需要提前fork出子进程即使这个fd毫无动静也要持续的维持子进程占用内核资源进程通信版本3《场景和思路上一版服务端会创建很多进程但是进程毕竟是内核的大单位会占用很多资源。所以需要改成多线程但是线程还是太多了代码量也多。进程通信版本4《多路复用 select 场景和思路一个线程管理所有客户端但是fd的数量又受到了限制才1024并且对于每个fd的状态检查都是Onn为已经连接的fd数量的时间复杂度。进程通信版本5《多路复用 poll通信 场景和思路用户层代码的设计的进步时间复杂度依旧是On进程通信版本6《多路复用 epoll通信 场景和思路内核引入了基于回调的事件通知机制红黑树管理 fd、就绪链表收集事件用户层代码因此改用 epoll_wait 阻塞等待就绪通知并通过 mmap 零拷贝获取事件列表整体编程模型从主动轮询转变为被动响应epoll有两种模式LT (水平触发)缓冲区有数据就触发用recv阻塞式读完即可ET (边缘触发)状态变化时只触发一次需要用用户手动写while持续读数据读完为止在大层的while循环中执行epoll_wait来阻塞式监听就续的fd。