多进程并发服务器
使用多进程并发服务器时要考虑以下几点:
-
父进程最大文件描述个数(父进程中需要close关闭accept返回的新文件描述符)
-
系统内创建进程个数(与内存大小相关)
-
进程创建过多是否降低整体服务性能(进程调度)
server
#include <stdio.h>
#include <sys/socket.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include "wrap.h"
void free_process(int sig)
{pid_t pid;while(1){//如果设置了选项 WNOHANG,而调用中 //waitpid() 发现没有已退出的子进程可等待,则返回 0;pid = waitpid(-1,NULL,WNOHANG);if(pid <=0 )// pid<=0 表示无待回收子进程{break;}else{printf("child pid =%d\n",pid);}}}
int main(int argc, char *argv[])
{sigset_t set;sigemptyset(&set);// //将set集合置空/* 1) 子进程终止时* 2) 子进程接收到SIGSTOP信号停止时* 3) 子进程处在停止态,接受到SIGCONT后唤醒时*/sigaddset(&set,SIGCHLD);//将SIGCHLD信号加入到set集合//信号阻塞集用来描述哪些信号递送到该进程的时候被阻塞//(在信号发生时记住它,直到进程准备好时再将信号通知进程)防止fork()前子进程退出导致信号丢失sigprocmask(SIG_BLOCK,&set,NULL);//创建套接字,绑定int lfd = tcp4bind(8008,NULL);//监Listen(lfd,128);//提取//回射struct sockaddr_in cliaddr;socklen_t len = sizeof(cliaddr);while(1){//进程分工://子进程:处理客户端请求(cfd)//父进程:继续监听新连接(lfd)//关键资源管理://子进程关闭lfd:避免文件描述符泄漏//父进程关闭cfd:减少引用计数,客户端断开时子进程可正常退出char ip[16]="";//提取连接,int cfd = Accept(lfd,(struct sockaddr *)&cliaddr,&len);printf("\nnew client ip=%s port=%d\n",inet_ntop(AF_INET,&cliaddr.sin_addr.s_addr,ip,16),ntohs(cliaddr.sin_port));//fork创建子进程pid_t pid;pid = fork();if(pid < 0){perror("");exit(0);}else if(pid == 0)//子进程{// 关闭监听套接字(子进程不需要)close(lfd);while(1){char buf[1024]="";int n = read(cfd,buf,sizeof(buf));if(n < 0){perror("");close(cfd);exit(0);}else if(n == 0)//对方关闭j{printf("\nclient close\n");close(cfd);exit(0);}else{struct sockaddr_in remote_addr;socklen_t len = sizeof(remote_addr);getpeername(cfd, (struct sockaddr *)&remote_addr, &len);int remote_port = ntohs(remote_addr.sin_port);printf("客户端%d:%s",remote_port,buf);write(cfd,buf,n);// exit(0); }}}else//父进程{// 关闭客户端套接字(父进程不需要)close(cfd);//回收//注册信号回调struct sigaction act;act.sa_flags =0;act.sa_handler = free_process;//free_process函数指针传给act结构体sigemptyset(&act.sa_mask);//把信号阻赛集清空sigaction(SIGCHLD,&act,NULL);//遇到SIGCHLD信号调用free_process函数sigprocmask(SIG_UNBLOCK,&set,NULL);//:从信号阻塞集合中删除 set 信号集/*整个代码意思就是在注册回调函数之前先把SIGCHLD信号阻塞,回调函数注册好了* 之后,在从set阻塞信号集中删除SIGCHLD信号,使得sigaction函数能够捕捉到SIGCHLD信号,从而调用free_process函数回收进程资源*/}}//关闭return 0;
}
在 Linux 系统中,当父进程通过 fork() 创建子进程时,子进程会复制父进程的文件描述符表,包括 socket 描述符。这种复制并非创建新的 socket 资源,而是共享相同的底层文件表项(即内核中的打开文件结构)。
文件描述符的复制机制
- 共享相同内核对象:
子进程复制的文件描述符与父进程指向同一个内核文件表项(包括 socket 结构)。
例如:父进程的 socket 描述符fd=3,子进程的fd=3也指向同一个 socket 对象。 - 引用计数增加:
每复制一次描述符,内核中该文件表项的引用计数会+1。只有所有进程关闭描述符后(引用计数归零),内核才会释放资源。
父子进程对描述符的操作独立性
- 修改互不影响:
父子进程可独立操作复制的描述符(如read/write/close),但操作会影响同一个 socket 资源:- 读写共享缓冲区:一方发送数据,另一方会收到;一方关闭连接,另一方会感知(如
read返回 0)。 - 关闭操作独立:子进程关闭
fd不影响父进程的fd,但会减少引用计数。
- 读写共享缓冲区:一方发送数据,另一方会收到;一方关闭连接,另一方会感知(如
- 典型多进程服务器模型:
- 父进程:关闭客户端连接描述符(
close(cfd)),仅保留监听套接字。 - 子进程:关闭监听描述符(
close(lfd)),仅保留客户端连接描述符。
- 父进程:关闭客户端连接描述符(
client
客户端使用:
nc 127.0.0.1 8008
模拟客户端链接服务器
结果显示如下:

多线程并发服务器
在使用线程模型开发服务器时需考虑以下问题:
-
调整进程内最大文件描述符上限
-
线程如有共享数据,考虑线程同步
-
服务于客户端线程退出时,退出处理。(退出值,分离态)
-
系统负载,随着链接客户端增加,导致其它线程不能及时得到CPU
server
#include <stdio.h>
#include <pthread.h>
#include <string.h>
#include <arpa/inet.h>
#include <ctype.h>
#include <unistd.h>
#include <fcntl.h> #include "wrap.h"// 作用:封装客户端连接信息
// 设计意图:避免线程参数传递时出现竞争(主线程的accept循环与线程启动存在时间差)
// 动态内存分配:每个连接独立分配malloc,确保线程安全(避免共用栈变量)
typedef struct c_info
{int cfd;struct sockaddr_in cliaddr;}CINFO;void* client_fun(void *arg);
int main(int argc, char *argv[])
{if(argc < 2){printf("argc < 2??? \n ./a.out 8000 \n");return 0;}//PTHREAD_CREATE_DETACHED:线程自动回收资源,无需主线程调用 pthread_joinpthread_attr_t attr;pthread_attr_init(&attr);pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED);short port = atoi(argv[1]);int lfd = tcp4bind(port,NULL);//创建套接字 绑定 Listen(lfd,128);struct sockaddr_in cliaddr;socklen_t len = sizeof(cliaddr);CINFO *info;while(1){// Accept():阻塞等待新连接,返回客户端socket (cfd)// 动态创建CINFO对象存储客户端信息// pthread_create:启动线程处理连接,传入info指针int cfd = Accept(lfd,(struct sockaddr *)&cliaddr,&len);char ip[16]="";pthread_t pthid;info = malloc(sizeof(CINFO));info->cfd = cfd;info->cliaddr= cliaddr;pthread_create(&pthid,&attr,client_fun,info);}return 0;
}void* client_fun(void *arg)
{CINFO *info = (CINFO *)arg;char ip[16]="";printf("new client ip=%s port=%d\n",inet_ntop(AF_INET,&(info->cliaddr.sin_addr.s_addr),ip,16),ntohs(info->cliaddr.sin_port));while(1){char buf[1024]="";int count=0;count = read(info->cfd,buf,sizeof(buf));if(count < 0){perror("");break;}else if(count == 0){printf("client close\n");break;}else{printf("%s\n", buf);write(info->cfd,buf,count);}}close(info->cfd);free(info);
}
client
客户端使用:
nc 127.0.0.1 8008
模拟客户端链接服务器
结果显示如下:

总结:
- 优势:
- 高并发:线程切换开销远小于进程(约1/10~1/100)1
- 资源共享:线程共享进程内存,数据传递高效(对比进程需IPC)
- 风险:
- 线程数过多时,上下文切换开销增大(需线程池优化)
- 共享资源需同步(本例无共享数据,故未用锁)
两者之间对比
1. 资源开销对比
-
进程高并发
- 创建/销毁开销大:每个进程需独立分配内存、文件描述符等资源,系统调用(如
fork())耗时长。 - 内存占用高:多进程独立内存空间导致总内存消耗线性增长,例如10个进程可能占用10倍于单进程的内存。
- 上下文切换慢:切换进程需保存/恢复完整的地址空间、页表等,开销约为线程的 10~100倍。
- 创建/销毁开销大:每个进程需独立分配内存、文件描述符等资源,系统调用(如
-
线程高并发
- 轻量级创建:线程仅需分配栈和寄存器,共享进程资源(如内存句柄),创建速度比进程快 5~10倍。
- 内存共享:多个线程共享同一进程的内存,内存占用远低于多进程(如100线程共享同一堆)。
- 切换开销低:仅需保存寄存器、栈指针等少量状态,切换速度更快。
2. 隔离性与稳定性
-
进程高并发
- 强隔离性:进程崩溃不影响其他进程(如Apache的prefork模式)。
- 安全性高:独立地址空间避免内存越界污染(适合沙箱环境)。
-
线程高并发
- 弱隔离性:单线程崩溃(如段错误)可能导致整个进程终止(所有线程退出)。
- 共享资源风险:线程间共享变量需严格同步,否则易引发数据竞争(如未加锁的全局计数器)。
3. 通信效率
-
进程高并发
- IPC机制复杂:需管道、消息队列、共享内存等,数据需内核态拷贝(如
memcpy),延迟高。 - 共享内存例外:虽高效但需手动同步(如信号量),编程复杂度高。
- IPC机制复杂:需管道、消息队列、共享内存等,数据需内核态拷贝(如
-
线程高并发
- 直接共享内存:通过全局变量即可通信(如生产者-消费者模型),速度比IPC快 10~100倍。
- 同步成本:需锁(互斥锁、条件变量)避免竞态,不当使用易导致死锁
相比之下,这两种方式在高并发服务器场景中各具优势,因而催生了多路I/O转接服务器(select、poll、epoll)的设计理念。其核心思想是将I/O事件的轮询工作交给内核,应用程序仅在事件就绪时被唤醒处理,从而避免为每个连接创建独立线程/进程的开销。
下一篇文章将详细讲解select、poll和epoll三种I/O模型。