多进程并发服务器模型,子进程结束产生僵尸进程,需要回收,僵尸残留。(PCB残留)
1、谁来回收?怎么回收? 父进程回收,当子进程退出时,内核会给父进程发送一个信号SIGCHLD。(默认忽略)只有父进程回收,但是父进程又不能回收,什么意思呢 ?我们单进程的服务器模型不能一对多的主要原因就是接收连接模块和业务处理模块二者发生冲突。我们二者分离开,分别放在两个进程中,父进程等待连接,子进程任务处理。但是现在又来了一个新的业务,回收模块,如果都让父进程去完成,而且回收是默认阻塞回收,回收模块就又会和建立连接模块发生冲突。如何解决回收与建立连接之间的冲突? 我们还是采用任务分离原则,但并非再创建一个子进程去回收僵尸进程,子进程之间是无法互相回收的。我们可以让一个父进程中有两个线程,主线程负责accept,普通线程负责回收僵尸进程。
2、信号回收方案:利用信号捕捉,捕捉函数,回收僵尸进程
我们内定普通线程回收僵尸进程,但是线程间信号捕捉共享。(其中一个线程进行捕捉,其余所有的线程都会进行捕捉) 信号是发送给进程的,普通线程看到了去执行捕捉函数正是我们想要的,但是如果主线程看到了信号去执行了捕捉函数则会导致accept被强制终断。
捕捉函数回收是内核帮助调用 signal函数工作,accept阻塞是mian函数工作。虽然是内核帮助调用捕捉函数,但是利用的还是进程资源,会将main函数中的工作暂停去进行信号捕捉。捕捉结束后,再继续mian函数。所以如果主控线程去执行捕捉函数,那么连接模块就会被终止。
如何解决signal函数与main函数之间的冲突?信号屏蔽,让主线程屏蔽SIGCHLD。
3、现在还有这样一个问题,信号捕捉设定需要执行若干语句后才能完成。如果当前普通线程中的捕捉设定还没有完成,但是已经有子进程结束了,出现僵尸了,内核发信号来,但是没有捕捉到,漏捕捉了,漏回收了,信号丢失。如何解决? 每一个线程都有屏蔽字,主线程创建普通线程,默认情况下普通线程继承主线程的屏蔽字。主线程屏蔽了SIGCHLD 创建出的普通线程也同样屏蔽SIGCHLD。这样即使信号来得早,也会被屏蔽,完成信号捕捉设定后 再解除屏蔽 。
4、代码
服务器端
#include<SOCKET_API.h>
#define SERVER_IP "192.168.27.128"
#define SERVER_PORT 8000
void sig_wait(int n)
{
//信号作为触发条件,一个信号尽可能回收多次
pid_t zpid;
while((zpid = waitpid( -1 , NULL , WNOHANG))>0){
printf("srever WaitThread tid [0x%x] Wait Success Zombie pid %d\n",(unsigned int)pthread_self(),zpid);
}
}
//回收线程的任务
void *thread_wait(void *arg)
{
//继承了主线程的屏蔽字,默认对SIGCHLD进行了屏蔽
//回收线程完成信号捕捉设定
struct sigaction act , oact;
act.sa_handler = sig_wait; //信号捕捉后的处理函数名
act.sa_flags = 0;
sigemptyset(&act.sa_mask); //临时屏蔽字初始化
sigaction(SIGCHLD , &act , &oact); //注册捕捉函数
//解除对SIGCHLD信号的屏蔽
sigprocmask(SIG_SETMASK , &act.sa_mask, NULL);
printf("server waitThread tid [0x%x] waiting ...\n",(unsigned int)pthread_self());
while(1)
sleep(1); //挂起等待信号
}
int main()
{
//1.定义初始化网络信息结构体
struct sockaddr_in serveraddr,clientaddr;
bzero(&serveraddr,sizeof(serveraddr));
//2.根据需求设置网络信息
serveraddr.sin_family = AF_INET;
serveraddr.sin_port = htons(SERVER_PORT);
inet_pton(AF_INET,SERVER_IP,&serveraddr.sin_addr.s_addr);//字符串IP转大端序IP
//3.创建sockfd
int serverfd = SOCKET(AF_INET,SOCK_STREAM,0); //创建TCP sockfd
//4.Socket 与自定义网络信息进行绑定
BIND(serverfd,(struct sockaddr*)&serveraddr,sizeof(serveraddr));
//5.开启网络链接状态监听
LISTEN(serverfd,128);
//6.服务端阻塞等待链接
int clientfd;
socklen_t addrsize = sizeof(clientaddr);
printf("Server Process Pid %d Accepting ...\n",getpid());
char ip[16];
bzero(ip,16);
pid_t pid;
sigset_t nset,oset;
sigemptyset(&nset); //初始化信号量集
sigaddset(&nset,SIGCHLD); //将SIGCHLD添加到信号量集中
sigprocmask(SIG_SETMASK,&nset,&oset); //将nset中的SIGCHLD阻塞掉,并保存当前信号屏蔽字到oset中
pthread_t tid;
pthread_create(&tid , NULL ,thread_wait , NULL);
while(1)
{
if((clientfd = ACCEPT(serverfd,(struct sockaddr*)&clientaddr,&addrsize))>0)
{
//链接成功数据客户端网络信息
printf("Server_parent Process Pid %d Accept Success ...\n",getpid());
printf("client IP[%s] PORT[%d].\n",inet_ntop(AF_INET,&clientaddr.sin_addr.s_addr,ip,16),ntohs(clientaddr.sin_port));
pid = fork();
if(pid > 0)
{
//父进程缺省
}
else if(pid ==0)
{
//服务端读取请求,处理业务,反馈响应(结果)
char buffer[1024];
bzero(buffer,sizeof(buffer));
int rsize ,wsize;
int flags;
while((rsize = RECV(clientfd ,buffer,sizeof(buffer),0))>0) //循环读取客户端请求
{
flags =0;//转换字符量
printf("serve_child pid %d recv request size = %d\n",getpid(),rsize);
while(rsize > flags)
{
buffer[flags] = toupper(buffer[flags]);
++flags;
}
wsize = SEND(clientfd ,buffer,rsize,0);
printf("server_child pid %d send response size = %d\n",getpid(),wsize);
bzero(buffer,sizeof(buffer));
}
if(rsize == 0)//客户端退出
exit(0);//结束子进程
}else
{
perror("fork call failed");
exit(-1);
}
}
}
close(serverfd);
close(clientfd);
return 0;
}
5、客户端与前一篇的客户端无异,不再赘述。
6、运行截图
1)服务器启动,等待客户端连接,等待回收子进程。
2)客户端连接并发送任务
3)服务端接收连接并处理任务
4)客户端退出,服务端子进程结束。
5)回收僵尸进程
服务端完整截图
注:需要添加新的头文件
#include<signal.h>
#include<sys/wait.h>
#include<pthread.h>
且pthread 库不是Linux系统默认的库,连接时需要使用静态库-lpthread。