• 进程创建
  • 进程终止
  • 进程等待
  • 进程替换

进程创建

在操作系统中,使用PCB来描述进程,对应到Linux操作系统中,用于描述组织管理进程的数据结构为task_struct结构体。

在理解进程时,进程包括描述其的数据结构以及进程的代码和数据,在Linux系统中,使用"ps axj"指令可以查看当前系统中的所有进程。

Linux进程控制_进程控制

其中,PID表示系统中该进程的唯一标识符,PPID为其父进程的标识符。

程序的代码和数据原先存储在磁盘上,当期代码和数据被加载如内存,同时其PCB进入运行队列,则程序变为进程。

那么如何创建一个新的进程呢?

介绍一个系统接口

fork()函数 

Linux进程控制_进程_02

通过man指令,查找系统接口fork(),可以看到其功能是创建一个新的进程,包含在<unistd.h>的头文件中,返回值为pid_t,实际上在父进程中使用fork()函数创建一个子进程,给父进程返回子进程的PID,给子进程返回0。

创建子进程后,操作系统给子进程创建其对应的PCB,地址空间、页表等,子进程默认共享父进程的代码。需要注意的是,由于每一个进程都有自己独立的进程地址空间,所以子进程和父进程中涉及到的数据在未发生写入前共享。而在其中任意一方进行写入时,进行写时拷贝。这样做可以提高操作系统的效率,防止申请内存空间但长久不使用的情况出现。

下面的一个例子来验证地址空间的存在。

#include<iostream>
using namespace std;
#include<unistd.h>
#include<stdlib.h>
int main()
{
  pid_t id=fork();
  int tmp=0;
  if(id==0)
{
  //子进程
  cout<<"im child"<<" "<<"pid:"<<getpid()<<" "<<"ppid:"<<getppid()<<endl;
  tmp=10;
  cout<<"tmp:child"<<tmp<<"地址"<<&tmp<<endl;
}
else if(id>0)
{
  //父进程
  cout<<"im father"<<" "<<"pid:"<<getpid()<<" "<<"ppid:"<<getppid()<<endl;
  cout<<"tmp:father"<<tmp<<"地址"<<&tmp<<endl;
}
  return 0;
}

Linux进程控制_进程替换_03

通过代码运行结果可以看到,tmp变量地址相同,但值不同,如果地址是指物理空间,这种情况是不可能的,所以实际上这个地址代表的是虚拟地址,即进程的地址空间,通过页表映射到物理内存。


  • fork()函数使用场景:

父子进程同时执行不同的代码段。


  • fork()函数创建子进程也有可能失败

原因可能是:

系统中存在太多的进程。

实际用户进程超过了限制。

进程终止

进程终止有两种方式,一种是在代码内部通过return/exit()结束进程,另一种是由操作系统发送信号给进程,使进程结束。先简单了解第一种进程结束方式。

在编写程序时,main()函数是函数的入口,return 返回值表示程序的结束,当程序被编译后加载到内存执行时,其就变为了一个进程,当其执行结束时表示进程的终止。

那么我们的main()函数为什么要return 0这个值呢?也就是说,main()函数为什么要有返回值呢?因为通过返回一个值给上一级进程,上一级进程可以通过该值评判该进程的执行结果。一般情况下,0代表程序执行完毕,运行正确,非0代表运行不正确,可以通过具体的值来判断导致错误的原因。

进程退出时,可能有三种退出情况。

  • 程序执行完毕,执行结果正确,退出。
  • 程序执行完毕,执行结果不正确,退出。
  • 程序执行到中间,发生错误,例如野指针访问或者段错误等,程序崩溃。

通过程序的返回值,我们可以获取这个程序的执行结果情况。即进程的退出码

通过"echo $?"指令可以获取最近一个结束进程的退出码。

Linux进程控制_进程终止_04

在编程时,main()函数的返回值常被忽略,其值默认为0,实际上,这是不严谨的,我们可以通过设置一些判别代码来判断程序的功能是否实现,并以退出码的形式返回。

Linux进程控制_进程终止_05

系统中的命令行指令在运行时,实质上就是进程,以"ls"指令为例,可以看到列出一个不存在的文件,实际上就是"ls"指令对应的程序代码执行后,退出码为2,表示的错误是"No such file or directory"。

Linux进程控制_进程_06

通过strerror()库函数可以查看系统定义的退出码以及相关信息,可以直接使用系统定义好的退出码和对应的信息,也可以自己设计一套对应的退出码和退出信息。

情况3是程序执行到中途直接崩溃,没有执行到return退出语句。

这种情况是如何造成的呢?进程代码没有执行完,本质上是操作系统给进程发送了信号,直接杀死进程。

这种情况下退出码无意义。

Linux进程控制_进程等待_07

除了操作系统进行进程管理结束一个进程,在代码中结束/终止一个进程就是通过main()函数的return语句实现的。普通函数的return语句不代表进程退出,只代表函数的返回值。

除了main()函数中return语句表示进程终止,也可以通过库函数exit()函数实现进程终止。

void exit(int status)

Linux进程控制_进程替换_08

exit()函数是一个库函数,其声明包含在<stdlib.h>的头文件中。

与return相比,通过在main()函数尾部return一个数值结束进程,exit()在函数的任意位置调用exit()函数,都可终止进程。status是进程的退出码。

其参数status存储进程的退出信息。

需要注意的是,还存在一个系统接口_exit(int status),声明包含在<unistd.h>中。

Linux进程控制_进程替换_09

该函数也是用于结束进程的,不同的是exit()为C语言标准库提供的,而_exit()为系统接口,实质上在底层exit()函数会调用系统接口_exit(),在此之前,exit()还会进行清空缓冲区等操作。(由此可见,常说的缓冲区是由语言给我们维护的,而非操作系统)

进程等待

父进程创建子进程,并给其分配一定的任务,子进程结束之后,如果没有回收释放其资源,则子进程会在操作系统中变成导致内存泄漏的僵尸进程,所以需要父进程等待子进程结束后进行子进程资源的回收与释放,并且父进程可能也需要知道子进程执行的结果,所以产生了进程等待这一概念。

父进程通过wait()和waitpid()可以对子进程进行等待回收。

这两个接口包含在<sys/wait.h>的头文件中。

Linux进程控制_进程终止_10

pid_t wait(int* stat_loc)

参数stat_loc是一个输出型参数,通过stat_loc变量的地址作为参数传入获取子进程的退出信息,wait()函数的返回值有两种,值为-1表示子进程等待失败(例如,不存在子进程的情况),否则返回回收的子进程的pid。

pid_t wait(int* stat_loc)

参数pid表示被等待的子进程的pid,pid的值为-1时,表示等待任意一个子进程,参数stat_loc是一个输出型参数,通过stat_loc变量的地址作为参数传入获取子进程的退出信息。

参数options表示父进程等待子进程结束的方式,有阻塞式等待和非阻塞式等待两种,其值默认为0,表示阻塞式等待,如果将其值设为WNOHANG,则代表非阻塞式等待。WNOHANG是一个宏定义的值。

关于阻塞式等待和非阻塞式等待,可以简单理解为父进程在waitpid()函数执行后,如果子进程未结束,父进程是否会去执行其他的任务。还是只是继续等待。

参数stat_loc:

这两函数中都有输出型参数int* stat_loc,用于存储子进程的退出信息,但是他并不是直接存储进程退出码,而是用位图的思想,采用bit位存储子进程退出信息。

stat_loc所存储的变量为一个int类型,大小为1byte->32bits,最低八位表示操作系统发送给进程的信号,次低八位表示表示进程退出码。

可以看到,通过stat_loc对应的变量可以存储进程的退出码信息,也可以存储操作系统发送给进程的信号。

如果想查看进程的退出码以及退出状态,不需要我们对输出型参数做位运算,系统提供了接口便于我们查看退出码和退出信息。

可以通过WIFEXITED(*stat_loc)查看进程是否正常退出,若为正常退出,则其值为TRUE,反之则为FALSE。如果WIFEXITED()函数的值为真,则可以通过WEXITSTATUS(*stat_loc)获取进程退出码。这两个接口的底层均是对stat_loc整形指针对应的整型变量做位运算。

进程替换

fork()创建子进程后,子进程会和父进程共享代码和数据,但子进程有属于自己的PCB数据结构和地址空间以及地址空间对应的页表和在物理空间上的映射,只不过创建子进程时的地址空间和也变会先拷贝父进程的,当任意一方对数据进行写入时,再发生写时拷贝,实现进程之间的独立性,同时提高系统的效率和利用率。

子进程创建后,通常执行父进程的一部分的代码片段,如果想让子进程执行一个全新的程序则使用exec()系列系统接口,进行进程替换即可实现。

注意:进程替换并未创建新的进程,进程的PID未发生改变,只是进程的代码和数据发生了变化。

exec()系列函数-均可实现进程替换:

Linux进程控制_进程终止_11

函数返回值:只有在替换出错的时候才会有返回值,出错时返回值为-1。

Linux进程控制_进程_12

execl(const char* path,const char*arg,...)

path:是要被替换的程序的可执行程序的路径。

arg以及省略号参数:表示可变参数列表,传入命令行的命令以及命令行参数,最后一个参数传入NULL表示参数的结束。

函数中:

有字母'l'表示:命令以及命令行参数以列表形式传入。最后一个参数传NULL。

有字母'p'表示:程序传入路径时,不需要带全路径,系统会在环境变量PATH中查找。

有字母'v'表示:命令以及命令行参数传入时,以字符串数组形式传入。

但这些都是库接口,系统接口为execve(),这些函数底层都是调用系统接口execve()的。