一、早期搭建原因与后续放弃原因
1.1 早期搭建原因
- 硬件适配与稳定性保障:早期服务器开发受限于单核 CPU 与有限内存,进程池架构通过预创建固定进程,规避频繁进程操作开销,利用进程资源隔离特性,防止单业务异常拖垮整个系统。
- 技术成熟与高效处理:多进程编程结合epoll事件驱动模型已趋成熟,凭借epoll对活跃事件的精准处理,服务器在高并发场景下实现资源高效利用。
- 性能优化策略:无锁化调度与动态负载均衡,有效化解多进程资源竞争,均衡任务分配,提升服务整体处理性能。
1.2 后续放弃原因
- 资源管理局限:业务扩张与用户增长时,固定进程池难以适配动态负载。高并发下请求排队导致响应延迟,且进程独占内存,数量过多易造成资源浪费,灵活性不足。
- 切换开销较大:进程池虽减少创建销毁频率,但上下文切换仍有开销。多核时代,线程作为轻量级单元,切换开销远低于进程,更适合高并发短周期任务,削弱多进程架构优势。
- 新技术的冲击:Node.js、Golang 等语言框架自带高效异步 I/O 与协程模型,开发效率和性能俱佳;Nginx 事件驱动架构成行业标杆,传统多进程服务器逐渐被替代。
二、主函数核心职责概述
主函数作为整个服务器的入口,承担着初始化、资源调度和事件循环的核心职责。下面我们逐行解析代码逻辑,揭示各模块如何协同工作构建高并发服务。
2.1 完整main函数代码展示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70
| #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/socket.h> #include <sys/epoll.h> #include <unistd.h> #include <arpa/inet.h> #define ARGS_CHECK(argc, val) \ if(argc != val) { \ fprintf(stderr, "Usage: %s <ip> <port> <worker_num>\n", argv[0]); \ exit(EXIT_FAILURE); \ } #define FREE 0 #define BUSY 1 typedef struct { pid_t pid; int status; int pipefd; } workerData_t; // 假设存在的函数声明 int initTcp(const char *ip, const char *port); void makeWorker(int num, workerData_t *arr); void epollAdd(int epfd, int fd); void sendfd(int pipefd, int fd); int main(int argc, char *argv[]) { ARGS_CHECK(argc,4); int workerNum = atoi(argv[3]); workerData_t *workerArr = (workerData_t *)calloc(workerNum,sizeof(workerData_t)); makeWorker(workerNum,workerArr); int sockfd = initTcp(argv[1],argv[2]); int epfd = epoll_create(1); epollAdd(epfd,sockfd); for(int i = 0; i < workerNum; ++i){ epollAdd(epfd,workerArr[i].pipefd); } struct epoll_event readySet[1024]; while(1){ int readyNum = epoll_wait(epfd,readySet,1024,-1); for(int i = 0; i < readyNum; ++i){ // 处理客户端连接事件 if(readySet[i].data.fd == sockfd){ int netfd = accept(sockfd,NULL,NULL); printf("1 client connect, netfd = %d\n", netfd); // 负载均衡调度 for(int j = 0; j < workerNum; ++j){ if(workerArr[j].status == FREE){ sendfd(workerArr[j].pipefd, netfd); workerArr[j].status = BUSY; break; } } close(netfd); // 传递后关闭主进程引用 } // 处理工人进程完成事件 else{ for(int j = 0; j < workerNum; ++j){ if(readySet[i].data.fd == workerArr[j].pipefd){ pid_t pid; read(readySet[i].data.fd, &pid, sizeof(pid)); printf("worker %d is finished!\n", pid); workerArr[j].status = FREE; break; } } } } } free(workerArr); return 0; }
|
2.2 参数解析与初始化阶段
1 2 3
| ARGS_CHECK(argc,4); int workerNum = atoi(argv[3]); workerData_t *workerArr = (workerData_t *)calloc(workerNum,sizeof(workerData_t));
|
参数合法性校验:ARGS_CHECK宏确保命令行参数数量为 4(程序名 + IP + 端口 + 工作进程数),避免因参数错误导致的运行异常。
工作进程数量配置:通过atoi将字符串参数转为整数,确定工人进程池规模。这一数值直接影响服务器并发处理能力,需根据硬件配置调整。
进程管理数组初始化:calloc函数分配内存并清零,workerArr数组用于存储每个工人进程的 PID、状态和通信管道描述符,是主进程调度的关键数据结构。
2.3 工人进程池创建
1
| makeWorker(workerNum,workerArr);
|
- 调用makeWorker函数批量创建工人进程,通过循环执行socketpair+fork实现:
- 每个工人进程与主进程通过 Unix 域套接字建立私有通信管道
- 子进程进入死循环等待任务,父进程记录进程元数据到workerArr
- 初始状态均设为FREE,表示可接收新任务
2.4 TCP 服务器初始化
1
| int sockfd = initTcp(argv[1],argv[2]);
|
socket创建流式套接字(SOCK_STREAM)
setsockopt设置 SO_REUSEADDR 选项,允许端口复用
bind将套接字绑定到指定 IP 和端口(注意字节序转换)
listen开启监听,backlog 设为 50(半连接队列长度)
2.5 epoll 事件驱动引擎搭建
1 2 3 4 5
| int epfd = epoll_create(1); epollAdd(epfd,sockfd); for(int i = 0; i < workerNum; ++i){ epollAdd(epfd,workerArr[i].pipefd); }
|
2.6 核心事件循环
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| struct epoll_event readySet[1024]; while(1){ int readyNum = epoll_wait(epfd,readySet,1024,-1); for(int i = 0; i < readyNum; ++i){ // 处理客户端连接事件 if(readySet[i].data.fd == sockfd){ int netfd = accept(sockfd,NULL,NULL); printf("1 client connect, netfd = %d\n", netfd); // 负载均衡调度 for(int j = 0; j < workerNum; ++j){ if(workerArr[j].status == FREE){ sendfd(workerArr[j].pipefd, netfd); workerArr[j].status = BUSY; break; } } close(netfd); // 传递后关闭主进程引用 } // 处理工人进程完成事件 else{ for(int j = 0; j < workerNum; ++j){ if(readySet[i].data.fd == workerArr[j].pipefd){ pid_t pid; read(readySet[i].data.fd, &pid, sizeof(pid)); printf("worker %d is finished!\n", pid); workerArr[j].status = FREE; break; } } } } }
|
- accept获取客户端连接描述符netfd
- 遍历进程池找到空闲工人(FREE 状态)
- 通过sendfd传递文件描述符,利用 SCM_RIGHTS 机制实现跨进程资源共享
- 及时关闭主进程中的netfd,避免文件描述符泄漏
- 从管道读取工人进程 PID,确认任务完成
- 将对应进程状态重置为 FREE,使其可接收新任务
- 整个过程通过数组索引快速定位目标进程,时间复杂度为 O (n)
三、main 函数整体流程总结
在整个main函数中,从程序启动开始,首先进行严格的参数检查,确保输入正确,为后续流程奠定基础。接着初始化工作进程池和 TCP 服务器,搭建起服务的基本架构。随后创建并配置epoll事件驱动引擎,让服务器能够高效地监听各类事件。
进入核心事件循环后,main函数持续通过epoll_wait等待事件发生。一旦有事件就绪,便根据事件类型进行针对性处理:连接事件发生时,将新连接合理分配给空闲的工作进程;工作进程完成任务后,及时将其状态重置为空闲,以便接收新任务。整个过程循环往复,实现服务器的持续稳定运行。
四、关键设计亮点
- 无锁化调度:主进程单线程处理事件循环,通过串行化操作避免进程状态竞争
- 资源隔离:每个工人进程独立处理业务,崩溃不影响整体服务
- 动态负载均衡:基于状态的调度策略确保任务均匀分配
- 事件驱动效率:epoll 机制使主进程仅处理活跃事件,CPU 利用率高
通过main函数的流程控制,各模块被有机串联:initTcp建立通信基础,makeWorker构建处理能力,epoll实现高效事件监控,sendfd/recvfd解决跨进程通信难题,共同构成一个完整的高并发服务架构。