<Linux>进程控制

Posted beyond->myself

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了<Linux>进程控制相关的知识,希望对你有一定的参考价值。

进程控制

文章目录

一、进程创建

1.fork函数认识

在Linux中fork函数非常的重要,它的作用是在一个已经存在的进程中创建一个新进程。新进程叫做子进程,原来的进程叫做父进程。

函数名称fork
函数功能创建子进程
头文件#include<unistd.h>
函数原型pid_t fork(void);
参数
返回值>-1:成功(其中子进程返回0,父进程返回子进程的id) =-1:失败

进程调用fork,当控制转移到内核中的fork代码后,内核要做的是:

  • 分配新内存和数据结构给子进程
  • 将父进程部分数据结构内容拷贝至子进程中
  • 添加子进程到系统进程列表中
  • fork返回,调度器开始调度

当一个进程调用了fork之后,父子进程代码是共享的,虽然他们都运行到了相同的地方,但是每个进程都可以开始自己的旅程:

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
int main()

  printf("Before:pid is: %d\\n",getpid());
  pid_t pid = fork();
  
  if(pid == -1)
  
      perror("fork()");
      exit(1);
  
  printf("after:pid is: %d,return is %d\\n",getpid(), pid);
  sleep(1);
  return 0;

结果展示:

我们可以看到,第一行输出是fork之前,只有父进程在执行,打印了before信息,fork创建子进程后,打印了两行after信息,分别由父子进程打印,注意到,进程29404打印了before的pid,而另外一个after却没有打印,这是为啥呢?

所以,fork之前父进程独立执行,fork之后,两个父子进程执行流分别执行
注意:fork之后谁先执行,完全由调度器决定。(父子都有可能先执行)

2.写时拷贝

通常,父子代码共享,父子不再写入时,数据也是共享的,当任意一方试图写入时,便以写时拷贝的方式各自一份副本(在物理内存中)。

3.fork常规用法

  • 一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。
  • 一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数

4.fork调用失败的原因

以下两种原因可能会导致fork调用失败:

  • 系统中有太多的进程
  • 实际用户的进程数超过了限制

我们写一个死循环创建子进程的程序测试我们当前操作系统最多能创建多少个进程:

注意:上面这个程序可能会导致服务器或虚拟机直接挂掉,虚拟机的话,大家只需要使用shotdown命令关机重启即可,服务器则需要去服务器控制台进行重启。

二、进程终止

1.进程退出场景

  • 代码运行完毕,结果正确
  • 代码运行完毕,结果不正确
  • 代码异常终止(vs下叫做程序崩溃)

2.进程退出码

我们先前写C/C++代码的时候,都会在入口函数main函数开始写,我们总是喜欢在结尾的时候给上一个return 0,继而引发出了如下的两个问题:

  1. return 0,给谁return?
  2. 为何是0?其它值可以吗?

下面一次解决:

  • 1、return 0,给谁return?

给父进程,具体理由在下面会有讲解。

  • 2、为何是0?其它值可以吗?

返回值代表的是进程代码跑完,结果是否正确,如果是0,则成功,非零则失败。所以我们在写一个程序的时候,如果测试结果正确,这里我们可以给上return返回值0,可如果不正确,我们return的应该是其他值以此表示结果失败,只不过我们平时都无脑return 0了,准确说是不太正确的。

此外,失败虽是用非零值表示,可也是有讲究的,结果成功都是用0表示,结果失败反倒用不同的数字来表示,以此表示失败的不同原因。所以我们把main函数的return返回值称之为进程退出码!!进程退出码表征了进程退出的信息,此信息是要给父进程去读取的。

示例:

我们可以通过如下的指令查看退出码:

echo $?

$?表示在bash中,最近一次执行完毕时,对应进程的退出码!(说的简单点就是上一条指令执行完毕后的退出码)

再比如我们平时在命令行输入的指令,诸如ls、cd……类的,其退出码均为0,表示结果正确,可是当你随便输入一条错误指令的时候,其退出码则是某一数字表示结果错误:

问:一般而言,失败的的非零值我该如何设置呢?以及默认表达的含义?

  • C语言当中的strerror函数可以通过错误码,获取该错误码在C语言当中对应的错误信息。
  • **总结:**错误码退出码可以对应不同的错误原因,方便定位问题!
  • 这里就可以提出我们退出码的意义?

它能够表示结果的正确与否,正确用0表示,因为那么多个数字,0只有一个,但是错误却有多个,用非0数字表示,错误的原因也是有多种可能的

退出码也是不能够随意乱写的,每一个退出码对应的数字,代表不同的错误,我们可以利用函数接口strerror观察有哪些错误码

#include<stdio.h>
#include<string.h>
int main()

  for(int i=0;i<150;i++)
  
    printf("%d:%s\\n",i,strerror(i));
  
  return 0;

结果展示部分错误码对应的错误原因:

3.进程退出的方式

正常终止

  • 1、main函数中return返回,代表退出进程;而非main函数中return返回表示的就是普通的函数返回/结束调用

  • 2、调用exit函数,它在程序的任意地方调用都是代表终止进程,参数是退出码,exit函数会完成一些收尾工作,例如资源的清理和释放,刷新缓冲区等

  • 3、调用_exit函数,它的作用是强制终止进程,不要进行后续收尾工作,比如刷新缓冲区(用户级别的缓冲区!)

异常终止

  • 【ctrl + c】信号终止

介绍return退出、exit函数和_exit函数
return退出
return退出是一种最为常见的一种退出进程的方法,执行return n等于执行exit(n),因为调用main函数运行时,会将main的返回值当做exit的参数。

exit函数

exit函数是标准C库中的一个库函数。

函数名称exit
函数功能正常终止一个进程
头文件#inlcude<stdlib.h>
函数原型void exit(int status)
参数status:程序退出的状态
返回值


执行并查看退出码:

在调用exit之前,还会做一些其他的工作,

  1. 执行用户通过atexit或者on_exit定义的清理函数
  2. 关闭所有打开的流,所有的缓冲数据均被写入
  3. 调用_exit函数


结果展示:
_ exit函数
_ exit也是标准C库中的一个库函数,它和_ Exit函数调用同义。

函数名称_exit
函数功能正常终止一个进程
头文件#include<unistd.h>
函数原型void _exit(int status)
参数status 定义了进程的终止状态,父进程通过wait来获取该值
返回值

_exit是强制退出进程,并不进行后续的收尾工作!

注意:status定义了进程的终止状态,父进程通过wait来获取该值,虽然status是int,但是仅有低八位可以被父进程所用。所以_exit(-1)时,在终端执行echo $?时,发现返回值是255。

_exit和exit的作用都是终止进程,但是还是有点区别的,区别如下:

结果演示:

exit_exit函数的区别:

exit函数退出进程前,exit函数会执行用户定义的清理函数、刷新缓冲区,关闭流等操作,然后再终止进程,而_exit函数会直接终止进程,不会做任何收尾工作。

三、进程等待

1.进程等待是什么?

让父进程fork之后,需要通过wait或者waitpid等待子进程退出,父进程想要知道子进程完成的任务情况如何了。

2.进程等待的必要性

  • 之前讲过,子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏问题,需要通过父进程wait,释放子进程占用的资源
  • 另外,进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程
  • 最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。父进程需要获得子进程的退出状态
  • 父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息(status),能够得知子进程的执行结果
  • 可以保证“时序问题”,那就是保证子进程先退出,父进程后退出。

3.进程等待的方法

利用系统级别的函数waitwaitpid

3.1.wait函数

函数名称wait
函数功能暂停当前进程,直至子进程结束,并取回子进程结束时的状态
头文件#include<sys/wait.h>
函数原型pid_t wait(int *status)
参数status:子进程终止状态的地址
返回值等待成功返回被等待进程pid,失败返回-1

说明:输出型参数status,获取子进程状态,不关心时可以设置为NULL

作用:等待任意子进程

我们写一段代码来看一看,fork之后我们先让子进程运行5秒,之后子进程退出,而让父进程一直在等待(调用wait函数),我们就能看到进程等待的现象。

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

int main()

	pid_t id = fork();
	if (id == 0)
	
		//child
		int cnt = 5;
		while (cnt)//5秒后子进程退出
		
			printf("child is running!,pid:%d, cnt:%d\\n", getpid(), cnt);
			cnt--;
			sleep(1);
		
		exit(0);//退出子进程
	
	printf("father wait begin!\\n");
	sleep(5);//父进程休眠5秒
	pid_t ret = wait(NULL);
	if (ret > 0)
	
		printf("father wait:%d,sucess\\n", ret);
	
	else
	
		printf("father wait failed!\\n");
	
	sleep(5);//子进程被回收之后,让父进程再运行5秒钟
	return 0;

我们可以使用以下监控脚本对进程进行实时监控:

while :; do ps axj | head -1 && ps axj | grep proc1 | grep -v grep;echo "—————————————————————————————————————————————————————————————————";sleep 1;done

运行结果如下:

这时我们可以看到,当子进程退出后,父进程读取了子进程的退出信息,子进程也就不会变成僵尸进程了。 由此得知我们可以通过wait()的方案解决回收子进程Z状态,让子进程进入X。

3.2.waitpid函数

函数名称waitpid
函数功能获取子进程结束时的状态
头文件#include<sys/wait.h>
函数原型pid_t waitpid(pid_t pid, int *status, int options)
参数pid:指定的子进程PID
status:子进程终止状态的地址
options:控制操作方式的选项
返回值1、> 0 等待子进程成功,当正常返回的时候waitpid返回收集到的子进程的进程ID;
2、 如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
3、如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;

对于三个参数进一步说明:

pid
1、pid<-1等待进程组识别码为pid绝对值的任何子进程.
2、pid=-1 等待任何子进程,相当于wait().
3、pid=0 等待进程组识别码与目前进程相同的任何子进程.
4、pid>0 等待任何子进程识别码为pid的子进程.

status
用下面的常用的两个宏:

  • WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
  • WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)

options:

  • 0:默认行为,阻塞等待(父进程什么都不做,就是等待子进程退出)
  • WNOHANG:设置等待方式为非阻塞等待

注意:当一个进程非正常退出时,说明该进程是被信号所杀,那么该进程的退出码也就没有意义了

  • 如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息。
  • 如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。
  • 如果不存在该子进程,则立即出错返回。

下面再来强调下第二个参数:status

status是一个输出型参数,通过调用该函数,从函数内部拿出来特定的数据,也就是从操作系统拿到特定数据。

子进程退出的时候会将自己的退出信息写入自己的task_struct,随后变成Z状态,随后父进程调用wait / waitpid接口,通过status把子进程的退出码拿到。

4.如何获取子进程status

4.1.如何理解status参数?

  • wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。
  • 如果传递NULL,表示不关心子进程的退出状态信息。
  • 否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。
  • status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特位)

解释上图:

  • 在status的低16比特位当中,高8位表示进程的退出状态,即退出码。进程若是被信号所杀,则低7位表示终止信号,而第8位比特位是core dump标志。

我们通过一系列位操作,就可以根据status得到进程的退出码和退出信号。

exitCode = (status >> 8) & 0xFF; //退出码
exitSignal = status & 0x7F;      //退出信号

对于此,系统中提供了两个宏来获取退出码和退出信号。

  • WIFEXITED(status):用于查看进程是否是正常退出,本质是检查是否收到信号。
  • WEXITSTATUS(status):用于获取进程的退出码。
exitNormal = WIFEXITED(status);//是否正常退出
exitCode = WEXITSTATUS(status);//获取退出码
  • 需要注意的是,当一个进程非正常退出时,说明该进程是被信号所杀,那么该进程的退出码也就没有意义了。

4.2.获取退出码和退出信号

我们通过位运算可以,根据status得到退出码和退出信号:

#include<stdio.h>
#include<string.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<unistd.h>
#include<stdlib.h>
int main()

  pid_t id=fork();
  if(id==0)
  
    //child
    int cnt=5;
    while(cnt)
    
        printf("child is running! pid:%d, cnt:%d\\n",getpid(), cnt);
        cnt--;
        sleep(1);
    
    exit(10);
  
  printf("father wait begin!\\n");
  sleep(10);
  //pid_t ret =wait(NULL);
  int status=0;
  pid_t ret=waitpid(id,&status,0);
  if(ret>0)
  
     printf("father wait:%d sucess,status wait code:%d,status exit signal:%d\\n",ret,(status>>8)&0xFF,status&0x7F);
  
  else
  
  	 printf("father wait failed!\\n");
  
  sleep(10);
  return 0;

结果如下:

status的最低7位表示进程退出时收到的信号,进程如果异常退出,是因为这个进程收到了特定的信号,我们先前kill -9 pid就是在进程异常时退出而发出的信号。下面来模拟下进程的异常退出:

#include<stdio.h>
#include<string.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<unistd.h>
#include<stdlib.h>
int main()

  pid_t id=fork();
  if(id==0)
  
    //child
    while(1)
    
        printf("child is running! pid:%d, cnt:%d\\n",getpid(), cnt);
        sleep(1);
    
    exit(10);
  
  printf("father wait begin!\\n");
  sleep(10);
  //pid_t ret =wait(NULL);
  int status=0;
  pid_t ret=waitpid(id,&status,0);
  if(ret>0)
  
     printf("father wait:%d sucess,status wait code:%d,status exit signal:%d\\n",ret,(status>>8)&0xFF,status&0x7F);
  
  else
  
  	 printf("father wait failed!\\n");
  
  sleep(10);
  return 0;

这里子进程是在无限循环的,父进程只能阻塞等待,现在我们把子进程kill掉,结果如下:

如果kil -3 pid,退出信号就是3……当进程收到信号的时候,就代表进程异常了。

综上:退出信号代表进程是否异常,退出码代表程序跑完后的结果正确与否。

问:一个进程退出的时候,父进程会拿到退出码和退出信号,那到底先看谁呢?

  • 一旦进程出现异常,只关心退出信号,退出码没有任何意义。

强调系统中的两个宏:

status:
    WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
    WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)

我们对于位操作是不是有点太过复杂和麻烦了,我们可以用上文提到的来代替位操作:

#include<stdio.h>
#include<string.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<unistd.h>
#include<stdlib.h>
int main()

	pid_t id = fork();
	if (id == 0)
	
		//child
		int cnt = 5;
		while (cnt)
		
			printf("child[%d] is running! cnt is %d\\n", getpid(), cnt);
			cnt--;
			sleep(1);
		
		exit(11);
	
	printf("father wait begin!\\n");
	sleep(10);
	//pid_t ret =wait(NULL);
	int status = 0;
	pid_t  ret = waitpid(id, &status, 0);
	if (ret>0)
	
		if (WIFEXITED(status))//没有收到任何退出信号
			//正常结束,获取对应的退出码
			printf("exit code:%d\\n", WEXITSTATUS(status));
		
		else
		
			printf("error get a signal!\\n");
		
	
	/*if (ret > 0)
	
		printf("father wait:%d sucess,status wait code:%d,status exit signal:%d\\n", ret, (status >> 8) & 0xFF, status & 0x7F);
	

	else
	
		printf("father wait failed!\\n");

	*/
	sleep(10);
	return 0;

结果展示:

3.阻塞等待和非阻塞等待

再来强调下waitpid函数中的最后一个参数options,当其值为0时,就是阻塞等待,当为WNOHANG时,就是非阻塞等待。下面展开讨论:

阻塞等待:

  • 如果子进程就是不退出(如死循环),怎么办呢?我的父进程只能阻塞等待。

  • 当我们调用某些函数的时候,因为条件不就绪(可能是任意的软硬件条件),需要我们阻塞等待,本质就是当前进程自己变成阻塞状态,当条件就绪的时候再被唤醒。

详细说明:

  • 就是一个进程在系统层面上因为要等待某种事件发生,如果这个事件并没有发生,那么当前进程的代码和数据将无法运行,此时就要进入阻塞状态,也就是将父进程的task_struct的状态由R->S,从运行队列投入到等待队列,等待子进程退出,当子进程退出了,本质就是条件就绪,那么就会逆向执行上述操作,将进程从等待队列搬到运行队列,并将状态由S->R。

举例:

  • 假设你叫李四,是个大混子,天天不学习,马上就要考数据结构了,为了能够及格,你打电话给了你班的一位学霸朋友张三来让他教我,整个过程我就是一个进程,打电话的过程就是在调用接口,张三就是所谓的OS操作系统,当电话接通了,但是张三说他在忙,于是我让张三别挂电话,我在电话这头一直等待你,此时这个等待过程中我什么也没干,只是等待张三,也就是说父进程在等待期间不做任何事情,这个过程就是阻塞等待。

非阻塞等待:

举例:

  • 此时重复上述场景,当电话接通后,张三表示还在忙,那么我直接挂电话,此时我做些自己的事情,忙了一会后又给张三打了个电话,张三还在忙,那么我又挂电话继续做自己的事情,while(1)重复循环直至张三说自己ok了。

详细说明:

  • 整个过程我依旧是用户,张三是OS操作系统,打电话就等价于调用waitpid函数,相当于是用户问操作系统子进程是否退出,当OS回应没有时,此时waitpid直接返回,此时用户不会调用wait而将自己阻塞住,此时用户在空闲时间段内做自己的事情,做一会之后再去问OS操作系统好了没,这个过程就叫做非阻塞等待。这种多次调用非阻塞接口,就是轮询检测。

总结:

1、阻塞等待:死等,就是上述的情况,父进程一直等待子进程,父进程不做任何事情。
2、非阻塞等待:我们可以不要让父进程死等,而是在等待期间,父进程去做自己的事情,等子进程退出时再来检测子进程的运行状态

代码示例:

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

int main()

	pid_t id = fork();
    assert(id != -1);
	if (id == 0)
	
		//子进程
		while (1)
		
			printf("我是子进程,我的pid:%d, 我的ppid:%d\\n", getpid(), getppid());
			sleep(3);
		
		exit(10);
	
	else if (id > 0)
	
		//父进程
		//基于非阻塞的轮询等待方案
   		int status = 0;
		while (1)
		
			pid_t ret = waitpid(id, &status, WNOHANG);// WNOHANG:非阻塞->子进程没有退出,父进程检测时候,立即返回
			if (ret > 0)
			
             //waitpid调用成功,子进程退出了
   				printf("等待成功,%d,退出信号是:%d,退出码是:%d\\n", ret, status & 0x7F, (status >> 8) & 0xFF);
				break;
			
			else if (ret == 0)
			
				//(waitpid调用成功了)等待成功了,但是子进程没有退出
             //子进程没有退出,我的waitpid没有等待失败,仅仅是检测到了子进程没有退出
   				printf("子进程完成没?还没有,那么我父进程就做其他事情啦...\\n");
				sleep(以上是关于<Linux>进程控制的主要内容,如果未能解决你的问题,请参考以下文章

linux运维 ps命令

linux运维基础篇 unit8

Linux中的进程管理

架构师成长系列|Linux运维工具—系统监控工具htop(1.1

Linux 线程(进程)数限制分析

linux云自动化运维基础知识21(selinux的初级管理)