【Linux】——僵死进程、进程替换和信号概述

一、僵死进程

1、僵死进程的概念

在之前的文章中我们曾提到过僵死进程。僵死进程就是进程实体结束,被内核释放,但是PCB结构依旧保持完整;子进程结束,父进程未结束并且父进程没有获取子进程的退出状态。有了他的概念,我们首先来模拟生成一个僵死进程如下:

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <unistd.h>
#include <string.h>

int main()
{
	pid_t n = fork();
	assert(-1 != n);

	if(0 == n)
	{
		printf("child start\n");
		sleep(10);
		printf("child end\n");
	}
	else
	{
		// 保证子进程执行结束,父进程未结束
		printf("father starting\n");
		sleep(20);
		printf("father running\n");
		
	}

	exit(0);
}

因为父进程比子进程的睡眠时间要更长,所以会存在子进程比父进程先结束的僵死进程情况如下我们用ps命令来查看,在子进程child end和father over之前 这个阶段有一段的僵死进程出现:
在这里插入图片描述

2、僵死进程的解决方法

因为我们已经知道了僵死进程的产生就是因为子进程结束,父进程没有结束,并且父进程没有获取到子进程的退出状态。了解到本质过后,我们就来解决如何获取,就能处理僵死进程了。

(1)wait和waitpid函数作用
当一个进程正常或者异常终止时,内核就向其父进程发送SIGCHLD信号。因为子进程终止是一个异步事件(可以在付继成运行的任何时候发生),所以这种信号是内核享福今晨发的异步通知。父进程对此有两种选择,第一种选择就是忽略该信号,第二种选择就是提供一个该信号发生时就被调用执行的函数。而我们的wait和waitpid函数函数就是父进程选择的第二种方式。他们的原型如下:

#include<sys/wait.h>
pid_t wait(int *reval);
pid_t waitpid(pid_t pid,int *reval,int option);

其中reval记录子进程的退出状态,成功返回进程的ID,失败返回-1。调用了这两个函数其中一个过后,根据不同的情况,进程会有下面三种反应:

  1. 如果所有自己成都还在运行,则阻塞
  2. 如果一个子进程已终止,正等待父进程获取其终止状态,则取得该子进程的终止状态立即返回
  3. 如果没有任何子进程,则立即出错返回
    如果进程由于接收到SIGCHLD信号而调用wait,则可期望wait会立即返回。但是如果在任意时刻调用wait,则进程可能会阻塞。

(2)wait和waitpid函数区别

  1. 在一个子进程终止前,wait使其调用者阻塞,而waitpid有一个选项,可使调用者不阻塞。
  2. waitpid并不等待在其调用之后的第一个终止子进程,他有若干个选项,可以控制它所等待的进程。
    如果一个子进程已经终止,并且是一个僵死进程,则wait立即返回并取得孩子进程的状态,否则wait使其调用者阻塞直到一个子进程终止如调用者阻塞而且他有多个子进程,则在其一个子进程终止时,wait就立即返回。因为wait返回终止进程的进程ID,所以它总能了解哪一个进程终止了。

对于waitpid函数中pid参数的作用解释如下:
pid == -1:等待任一子进程,就这一方面而言,wait和waitpid等效
pid>0:等待其进程ID与pid相等的子进程
pid ==0: 等待其进程ID等于调用进程组ID的任一子进程
pid<-1 等待其组I等于pid绝对值的任一子进程

有了以上对wait的了解,我们就可以对本篇文章开头说提到的僵死进程有以下的解决方案了,在父进程刚开始运行时,调用wait处理僵死进程

int main()
{
	pid_t n = fork();
	assert(-1 != n);

	if(0 == n)
	{
		printf("child start:%d\n",getpid());
		sleep(10);
		printf("child end\n");
	}
	else
	{
		pid_t id = wait(NULL);
		printf("id = %d\n",id);//子进程的pid
		
		printf("father starting\n");
		sleep(20);
		printf("father over\n");

		
	}

	exit(0);
}

运行结果如下:
在这里插入图片描述
从上面的运行结果我们可以看出,Wait返回的结果就是子进程的ID,并且已经没有僵死进程的出现了。
但是上述的解放方法还是存在些许不足,在父进程中直接调用wait方法会使得父进程阻塞等待一个子进程结束。所以我们接下来给出另外一种解决方案——子进程结束会给父进程发送信号SIGCHLD,当父进程接收到SIGCHLD信号时,再调用wait方法。但是父进程哪些代码能够保证在收到信号之后执行呢。在这里的处理我们会给SIGCHLD信号绑定一个信号处理函数(此函数会在收到SIGCHLD信号后才会被调用)

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<assert.h>

#include<signal.h>

void fun(int sign)
{
	pid_t pid = wait(NULL);
	printf("fun :: pid = %d\n",pid);
}
int main()
{
	signal(SIGCHLD,fun);
	pid_t pid = fork();
	assert(pid != -1);
	if(pid == 0)
	{
		printf("child begin\n");
		sleep(5);
		printf("child over\n");
	}
	else
	{
		printf("father begin\n");
		sleep(10);
		printf("father over\n");
	}
}

运行结果如下:
在这里插入图片描述
我们会看到父进程不会阻塞运行等待子进程的结束,收到信号过后就立即结束。

二、进程替换

1、进程替换的概念

我们之前在讲fork函数创建子进程后,子进程往往要调用exec函数来执行另一个程序。当进程调用exec函数时,该进程执行的程序完全替换为新程序,而新程序则从其main函数开始执行。因为调用exec并不创建新进程,所以前后ID并未改变。exec只是用一个全新的程序替换了当前进程的正文、数据、堆和栈段。
我们可以用如下代码来更加明了的实现进程替换。其中有两个文件,一个是test.c一个是main.c
main.c实现如下:

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<assert.h>

int main()
{
	pid_t pid = fork();
	assert(pid != -1);

	if(pid == 0)
	{
		printf("i am child: my pid = %d\n",getpid());
		execl("./test","./test","hello","world",(char *)0);

		int i =0;
		for(;i<5;i++)
		{
			printf("i am child\n");
			sleep(1);
		}
	}

	else
	{
		int i = 0;
		for(;i<10;++i)
		{
			printf("i am father\n");
			sleep(1);
		}
	}
	exit(0);	
}

test.c实现如下

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<assert.h>

int main(int argc,char *argv[])
{
	printf("i am test:%d\n",getpid());
	int i =0;
	for(;i<argc;++i)
	{
		printf("argv[%d] = %s\n",i,argv[i]);
	}
	exit(0);
}

程序运行结果如下:
在这里插入图片描述
我们会发现子进程去执行.test这个程序了,但是子进程的ID没有变说明没有新的子进程产生,父进程去继续执行他的指令了。

进程替换在我们的写时拷贝技术中也有着举足轻重的地位。
操作系统为每一个进程维护一个页表,但是fork之后,子进程和父进程共用一个页表,内核将页表项中的访问权限设置为只读的,当子进程执行execl时给子进程加载新的程序,生成子进程的页表使其映射到新的物理空间中。

2、exec函数

我们有六种不同的exec函数可供使用,把他们统称为exec函数。这些函数使得进程控制原语(用fork创建新进程,exec可以执行新程序,exit处理终止,wait函数等待终止)更加完善。下图为我们几种exec函数
在这里插入图片描述
他们的返回值都是出错返回-1,成功不返回值。对于execl,第一个参数是程序的文件路径+名称;中间的arg都是程序;最后一个参数表达传参的结束。对于execv通过路径名方式调用可执行文件作为新的进程映像。execle与execl的用法相似。execve参数pathname是将要执行的程序的路径名。
上面图中的五个函数都是库函数,还有一个execve是内核系统调用,他们之间的关系如下图所示:
在这里插入图片描述

三、信号

1、信号概述

信号是操作系统预先定义好的某些特定的事件,信号可以被产生也可以被接收,产生和接收信号的主体都是进程,作用就是一个进程向另外一个进程通知某一事件的发生。信号是软件中断,很多比较重要的应用程序都需处理信号。信号提供了一种处理异步事件的方法。例如:终端用户键入中断键,则会通过信号机制停止一个程序,或及早终止管道中的下一个程序。

(1)信号的产生条件

  1. 当用户按某些终端键时,引发终端产生的信号。在终端按DELETE键或者很多系统中按ctrl+c键通常产生中断信号SIGINT.这是一个停止失去控制程序的方法。
  2. 硬件异常产生信号。例如:除数为0、无效的内存引用
  3. 进程调用kill(2)函数可将信号发送给另一个进程或进程组
  4. 用户可用kill(1)命令将信号发送给其他进程
  5. 当检测到某种软件条件已经发生,并将其通知有关进程时也产生信号。

(2)信号的类别
我们可以通过/user/include/bits/signum.h这个路径去查看信号的种类,下列图中是部分信号
在这里插入图片描述
(3)信号的响应方式
信号的相应方式有三种,如下图所示:
在这里插入图片描述

  1. 捕捉信号:为了做到这一点,要通知内核在某种信号发生时调用一个用户函数。在用户函数中,可执行用户希望对这种事件的处理。例如:如果进程创建了临时文件,那么可能要为SIGTERM信号编写一个信号捕捉函数以清除临时文件。
  2. 执行系统默认动作:下图中给出了部分信号的系统默认动作,其中大多数信号的系统默认动作是终止进程。
    在这里插入图片描述
  3. 忽略此信号:大多数信号都可使用这种方式进行处理,但是SIGKILL和SIGSTOP不能被忽略,因为他们向超级用户提供了使进程终止或停止的可靠方法。

2、信号应用

(1)修改信号的响应方式
我们可以用如下的函数修改其相应方式

typedef void(*sig_Handler,int)
sig_Handler signal(int sig_type,sig_Handler hander);

其中sig_type代表信号类型,hander是信号响应函数。
实践一:编写一个程序实现在键盘上输入ctrl+c时,当前进程输出hello world

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<assert.h>

#include<signal.h>

void fun(int sign)
{
	printf("hello world: %d\n",sign);
}

int main()
{
	signal(SIGINT,fun);

	while(1)
	{
		sleep(5);
		printf("i am main,running \n");
	}
}

代码运行结果如下:
在这里插入图片描述
注意这个结果直到下一次修改之前就一直沿用此响应方式

实践二:进程第一次接收ctrl+c发送信号打印hello world,第二次收到信号结束进程。
因为第一次收到是我们执行的fun方法的时候,第二次收到其实就是在fun方法执行过程中,所以代码实现如下:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<assert.h>

#include<signal.h>
//fun函数会在第一次收到信号时执行,他的执行肯定是在第二次收到信号之前执行的
void fun(int sign)
{
	printf("hello world\n");

	signal(SIGINT,SIG_DFL);//SIG_DFL是默认的信号处理方式
}
int main()
{
	signal(SIGINT,fun);
	
	while(1)
	{
		printf("i am running\n");
		sleep(2);
	}
	exit(0);
}

运行结果如下:
在这里插入图片描述
(2)在程序中给一个进程发送一个信号
我们可以用kill来实现,kill的原型是int kill(pid_t pid,int sigtype).。其中的两个参数前一个表示指定发给哪一个进程,后面一个参数是指定哪种信号。如果进程给自己发送信号可以使用kill(getpid(),SIGINT) 这个方式等价于rasize(SIGINT)方法。
具体代码实现如下:


#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<assert.h>

int main(int argc,char *argv[])
{
	if(argc < 2)
	{
		printf("please input process's pid. again\n");
		exit(0);
	}

	int sigtype = 15;

	if(strncmp(argv[1],"-9",2) == 0)
	{
		sigtype = 9;
	}
	if(strncmp(argv[1],"-stop",5) == 0)
	{
		sigtype = 19;
	}

	int i = 1;
	for(;i<argc;i++)
	{
		if(i == 1 && strncmp(argv[i],"-",1) == 0)
		{
			continue;
		}

		int pid = 0;
		sscanf(argv[i],"%d",&pid);
		if(kill(pid,sigtype) == -1)
		{
			perror("kill error");
		}
	}
}



发布了98 篇原创文章 · 获赞 9 · 访问量 3640

猜你喜欢

转载自blog.csdn.net/qq_43412060/article/details/105519701
今日推荐