TCP/IP网络编程学习(9):僵尸进程及信号处理

僵尸进程及信号处理

  1. 多进程服务器:通过创建多个进程提供服务
  2. 多路复用服务器:通过捆绑并统一管理I/O对象提供服务 。
  3. 多线程服务器:通过生成与客户端等量的线程提供服务 。

进程:运行着的程序,被从磁盘加载到内存并有运行权限。

拥有 2 个运算设备的 CPU 称作双核( Daul ) CPU ,拥有 4 个运算器的 CPU称作 4 核(Quad )C PU 。也就是说, 1 个 CPU 中可能包含多个运算设备(核)。核的个数与可同时运行的进程数相同。采用多线程技术后,一个进程的线程可以在多核上运行。

进程ID

每一个进程都有一个唯一的PID。LINUX下可以通过 ps au查看

在这里插入图片描述

fork函数创建进程

pid_t fork(void*);
//成功范围ID 失败-1

fork函数将创建调用的进程副本(子进程)。也就是说 ,并非根据完全不同的程序创建进程, 而是复制正在运行的、调用fork函数的进程 。 另外,两个进程都将执行fork函数调用后的语句(准确地说是在fork函数返回后)。 但因为通过同一个进程、复制相同的内存空间 ,之后的程序流要根据 fork函数的返回值加以区分。利用 fork函数的如下特点区分程序执行流程。

父进程 : fork函数数返回子进程ID 。
子进程 : fork函数返回 0 。

此处"父进程" ( Parent Process )指原进程, 即调用fork 函数的主体,而"子进程" (Child Process ) 是通过父进程调用 fork函数复制出的进程 。 接下来讲解调用 fork函数后的程序运行流程,
在这里插入图片描述

父进程调用 fork函数的同时复制出子进程,并分别得到fork函数的返回值。但复制前,父进程将全局变量gval增加到 11, 将局部变量lval的值增加到25 , 因此在这种状态下完成进程复制。复制完成后根据fork函数的返回类型区分父子进程 。 父进程程将lval的值加 1 ,但这不会影响子进程的 lval值 。 同样, 子进程将gval的值加1也不会影响到父进程的 gval。因为fork函数调用后分成了完全不同的进程,只是二者共享同一代码而已 。

#include <stdio.h>
#include <unistd.h>
int gval=10;

int main(int argc, char *argv[])
{
	pid_t pid;
	int lval=20;
	gval++, lval+=5;
	pid=fork();		
	if(pid==0)	// if Child Process
		gval+=2, lval+=2;
	else			// if Parent Process
		gval-=2, lval-=2;
	
	if(pid==0)
		printf("Child Proc: [%d, %d] \n", gval, lval);
	else
		printf("Parent Proc: [%d, %d] \n", gval, lval);
	return 0;
}

在这里插入图片描述

进程和僵尸进程

进程销毁也和进程创建同等重要 。 如果未认真对待进程销毁,它们将变成僵尸进程困扰各位 。

僵尸进程

进程完成工作后 ( 执行完main 函数中的程序后)应被销毁,但有时这些进程将变成僵尸进程,占用系统中的重要资源 。 这种状态下的进程称作"僵尸进程",这也是给系统带来负担的原因之一。

调用fork函数产生子进程的终止方式 。
传递参数并调用exit函数 。
main函数中执行return语句并返回值 。

向 exit函数传递的参数值和main函数的return语句返回的值都会传递给操作系统 。 而操作系统不会销毁子进程,直到把这些值传递给产生该子进程的父进程。 处在这种状态下的进程就是僵尸进程 。 也就是说,将子进程变成僵尸进程的正是操作系统 。

应该向创建子进程的父进程传递子进程的 exit参数值或return语句的返回值 。 从而销毁僵尸进程。如果父进程终止,处于僵尸状态的子进程将同时销毁。

如何向父进程传递这些值呢?操作系统不会主动把这些值传递给父进程 。只有父进程主动发起请求(函数调用)时,操作系统才会传递该值 。 换言之,如果父进程未主动要求获得子进程的结束状态值,操作系统将一直保存, 并让子进程长时间处于僵尸进程状态 。

为了销毁子进程,父进程应主动请求获取子进程的返回值。

销毁僵尸进程方法1:利用wait函数

pid_t wait(int * statloc);
// 成功时退回终止的子进程 ID, 失败时返回-1。

调用此函数时如果已有子进程终止 ,那么子进程终止时传递的返回值( exit函数的参数、main函数的 return返回值)将保存到该函数的参数所指内存空间。 但函数参数指向的单元中还包含其他信息,因此需要通过下列宏进行分离 。

WIFEXITED 子进程程正常终止时返回true
WEXITSTATUS 返回子进程的返回值 。

也就是说, 向wait函数传递变量status的地址时,调用wait函数后应编写如下代码 。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int main(int argc, char *argv[])
{
	int status;
	pid_t pid=fork();
	
	if(pid==0){ 
		return 3;   //子进程1结束	
	}
	else
	{
		printf("Child PID: %d \n", pid);
		pid=fork();
		if(pid==0)
		{
			exit(7);//子进程2结束
		}
		else
		{
			printf("Child PID: %d \n", pid);
			wait(&status);
			if(WIFEXITED(status))
				printf("Child send one: %d \n", WEXITSTATUS(status));

			wait(&status);
			if(WIFEXITED(status))
				printf("Child send two: %d \n", WEXITSTATUS(status));
			sleep(30);     // Sleep 30 sec.
		}
	}
	return 0;
}

这就是通过调用 wait函数消灭僵尸进程的方法 。 调用wait函数时,如果没有己终止的子进程,那么程序将阻塞( Blocking ) 直到有子进程终止,因此需谨慎调用该函数。

使用 waitpid 函数

wait函数会引起程序阻塞,还 可以考虑调用 waitpid 函数 。 这是防止僵尸进程的第二种 方法也是防止阻塞的方法 。

pid_t waitpid(pid_t pid, int * statloc, int options);
// 成功时返回终止的子进程ID,(或0),失败时返回-1。
//PID: 等待终止的目标子进程的ID,若传递 - 1 ,则与wait函数相同,可以等待任意子进程终止。
//statloc: 与wait函数的 statloc参数具有相同含义。
//options:传递头文件sys/wait.h中声明的常量WNOHANG,即使没有终止的子进程也不会进入阻塞状态,而是返回O并退出函数 。
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

int main(int argc, char *argv[])
{
	int status;
	pid_t pid=fork();
	
	if(pid==0)
	{
		sleep(15);
		return 24;   	
	}
	else
	{
		while(!waitpid(-1, &status, WNOHANG))
		{
			sleep(3);
			puts("sleep 3sec.");
		}

		if(WIFEXITED(status))
			printf("Child send %d \n", WEXITSTATUS(status));
	}
	return 0;
}

在这里插入图片描述
父进程未阻塞。

信号处理

父进程不可能一直调用waitpid函数等待子进程终止。可以让操作系统将子进程结束的消息传递给父进程,那么当收到消息之后父进程就可以终止子进程。这就是信号处理机制。此处的"信号"是在特定事件发生时由操作系统向进程发送的消息 。

进程:"嘿,操作系统!如果我之前创建的子进程终止,就帮我调用 zombie-handier函数。 "
OS: “好的!如果你的子进程终止,我会帮你调用 zombie-handler函数,你先把该函数要执 行的语句编好!”

上述对话中进程所讲的相 当于"注册信号"过程,即进程发现自己的子进程结束时,请求操作系统调用特定函数 。该请求通过如下函数调用完成(因此称此函数为信号注册函数)。

#include <signal.h>
void (*signal(int signo, void (*func)(int)))(int);
// 为了在产生信号时调用 返回之前注册的函数指针。

/*
函数名:signal
参数: int signo, void (*func)(int)
返回类型:参数为int, 返回值为void的函数指针。
*/

调用上述函数时,第一个参数为特殊情况信息,第二个参数为特殊情况下将要调用的函数的地址值(指针)。 发生第一个参数代表的情况时,调用第二个参数所指的函数 。下面给出可以在signal函数中注册的部分特殊情况和对应的常数 。

SIGALRM: 已到通过调用alarm函数注册的时间 。
SIGINT: 输入CTRL+C 。
SIGCHLD: 子进程终止 。

例子: 子进程终止则调用 mychild函数 。
已到通过alarm函数注册的时间,请调用 timeout函数 。 "
输入CTRL +C时调用 keycontrol函数。

unsigned int alarm(unsigned int seconds);
// 返回0或以秒为单位的距SIGALRM信号发生所剩时间。
//如果调用该函数的同时向它传递一个正整型参数,相应时间后(以秒为单位)将产生SIGALRM信号。
signal(SIGCHLD,mychild)
#include <stdio.h>
#include <unistd.h>
#include <signal.h>

void timeout(int sig){//信号处理函数
	if(sig==SIGALRM)
		puts("Time out!");
	alarm(2);	//为了每隔2秒重复产生SIGALRM信号,在信号处理器中调用 alarm 函数 。
}
void keycontrol(int sig){
	if(sig==SIGINT)
		puts("CTRL+C pressed");
}

int main(int argc, char *argv[]){
	int i;
	signal(SIGALRM, timeout);//注册信号处理函数
	signal(SIGINT, keycontrol);
	alarm(2);//预约2秒后发生SIGALRM信号。

	for(i=0; i<3; i++){
		puts("wait...");
		sleep(100);
	}
	return 0;
}

也就是说,再过300秒 、 约5分钟后终止程序,这是相当长的 一段时间,但实际执行时只需不到 10秒 。

发生信号时将唤醒由于调用 sleep函数而进入阻塞状态的进程。
调用函数的主体的确是操作系统,但进程处于睡眠状态时无法调用函数。 因此产生信号时,为了调用信号处理器,将唤醒由于调用sleep函数而进入阻塞状态的进程 。 而且,进程一旦被唤醒,就不会再进入睡眠状态 。即使还未到sleep函数中规定的时间也是如此 。所以上述示例运行不到10秒就会结束,连续输入CTRL+C则有可能 1秒都不到 。

利用sigaction函数进行信号处理

sigaction函数,它类似于signal函数,而且完全可以代替后者,也更稳定 。 之所以稳定,是因为如下原因: signal 函数在 UNIX 系列的不同操作系统中可能存在区别,但 sigaction函数完全相同 "

int sigaction(int signo, const struct sigaction * act, struct sigaction *oldact);
//成功时退回 θ ,失败时退回-1
/*
slgno 与 signal 函数相同,传递信号信息 。
act 对应于第一个参数的信号处理函数 ( 信号处理器 ) 信息 。
oldact 通过此参数获取之前注册的信号处理函数指针,若不需要则传递0 。
*/

struct sigaction
{
	void (*sa_handler)(int);
	sigset_t sa_mask;//0
	int sa_flags;//0
}
#include <stdio.h>
#include <unistd.h>
#include <signal.h>

void timeout(int sig)
{
	if(sig==SIGALRM)
		puts("Time out!");
	alarm(2);	
}

int main(int argc, char *argv[])
{
	int i;
	struct sigaction act;
	act.sa_handler=timeout;
	sigemptyset(&act.sa_mask);//调用 sigemptyset函数将sa_mask成员的所有位初始化为0。
	act.sa_flags=0;
	sigaction(SIGALRM, &act, 0);
	alarm(2);
	for(i=0; i<3; i++)
	{
		puts("wait...");
		sleep(100);
	}
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值