多进程并发服务器
使用多进程并发服务器时要考虑以下几点:
父进程最大文件描述个数(父进程中需要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_join
pthread_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模型。