一、进程创建
前面我们学习进程概念的时候,已经知道了,创建进程,我们可以使用fork函数进行创建。其是在当前的进程下创建一个新的进程,新进程为子进程,原进程为父进程。
其给父进程返回子进程的pid,给子进程返回0,出错返回-1。
进程调用fork后,当控制转移到内核中的fork代码后,内核做下面几个工作:
1、分配新的内存块和内核数据结构给子进程
2、将父进程部分数据结构内容拷贝到子进程中
3、添加子进程到系统进程列表当中
对于fork的使用和现象我们前面已经讲解的很清楚了。
二、进程终止
进程终止的本质是释放系统资源,也就是释放掉进程申请的相关内核数据和对应的代码。
那么我们的进程中终止一共有几种情况呢?
一共有三种情况:
1、代码跑完,运行结果是正确的
2、代码跑完,运行结果有误
3、代码都没跑完,运行异常
首先我们看前面两种:
我们是如何判断我们的代码跑完是正确的还是错误的呢?
我们一开始学习语言的时候,在main函数也有返回值,其返回值为int类型,然后我们在main函数的末尾都会返回一个0,然后遇到一种输入错误呀啥的,都会返回1的。
其返回0,说明我们的运行是正确的。
要是返回非0的数那么说明运行错误。
然后就是代码都没走完,就发生异常,这个是如何判断的呢?
异常退出,说明我们的代码都没有运行到return,所以退出码是没有意思的,其实我们的进程管理中,还有一个是用来记录进程异常退出的码。一个是信号,信号就是异常的时候反馈的。
运行结果状态其用两个整型变量表示:
int exit_code。int exit------signal。
那么我们如何查看我们的进程退出码呢?
1、查看进程退出码
在我们的Shell中我们可以使用echo $?查看我们的上一个进程的退出码。

可以看到我们的程序运行错误的退出码是1。
2、error
这个函数我们前面也有使用过,这个是我们对于一些异常情况进行处理的函数,其通常会返回一个特殊值,然后将这个错误编码存放在一个全局变量errno中,如果我们要查看我们拿到的错误编号,那么我们要先安装moreutils,然后执行errno -l查看。
errno是一个整数,其每一个数据就代表一种特定的错误类型
其使用要包含头文件error.h
其返回值有两种情况:
返回0,说明没有问题
返回非0,就说明发生了错误

3、exit和_exit
exit是C++标准库中提供的一个函数,其是用来正常终止一个进程的。
其功能如下:
终止当前的进程,然后将状态码返回给父进程,其函数样子为:void exit(int status)。
注意:虽然我们的status是int类型,但是其只有8位bit位给其使用。'
exit和_exit的区别:
exit是C++标准库中提供的函数,_exit是系统调用,实际上exit的底层还是调用的_exit。
exit其执行退出进程的时候会将缓冲区进行I\O,刷新I\O缓冲区,然后我们的_exit的话是不会对缓冲区进行刷新的,直接就终止我们的进程。
下面是我们常见的信号状态和对应 的编号:
我们可以使用kill -l来查看:

三、进程等待
前面我们提到,进程状态,会有僵尸状态,这是因为我们的父进程先结束,然后子进程运行结束,父进程没有进行回收,获取子进程的退出信息。
所以父进程通过进程等待的方式,回收子进程的资源,获取子进程退出信息
我们在父进程中使用wait()函数或者waitpid()函数进行阻塞等待。
1、wait()
函数作用:
阻塞等待:如果子进程不退出,那么父进程会一直卡在wait这个函数中,直到接收到子进程的退出信息。
其是等待的任意一个子进程,不能指定等待某一个子进程。
其原型如下:
wait(int *status),这个指针可以穿空,就是只需要回收到子进程即可,不关心其运行状态。
其返回值类型为:pid_t,其实就是一个整型,如果等待成功,那么就返回等待到的子进程的pid,如果等待失败,那么就返回-1。
下面我们演示一下:


2、waitpid()
这个函数的功能和wait是一样的,不过其支持我们去指定子进程回收,其使用一次也是回收一个子进程。
函数原型如下:
waitpid(pid_t pid,int *status,int options);
第一个参数pid
传入的是-1的话,那么父进程就会等待任意一个子进程。
传入的为>0的值的话,那么其就会等待pid为这个数的子进程。
第二个参数,我们暂时将其设置为NULL
第三个参数,我们暂时将其设置为0。
下面我们演示其效果:


下面我们来具体看看其参数:
3、status
这个参数是一个int*类型的参数,其是用来接收被回收的子进程的退出状态的。
如果我们需要知道的话,那么可以创建一个int*类型的数据,获取其退出的状态。
然后要注意的是,其返回的话,是包含了退出信号和退出码的。
我们的退出信号和退出码就是其前16bit位。

那么我们要是想看子进程的退出码,那么我们只需要将status的数据右移8个bit位。
然后终止信号就在前八个bit位置,然后如果其是被信号所杀的话,那么会有一个bit位被用来做为core dump标志。所以其是7个bit位置。


然后我们的Linux下定义了两个宏来获取我们进程退出的状态的:

4、阻塞等待和非阻塞等待
阻塞等待就是我们的父进程等待子进程回收,在这个等待的期间,我们的父进程啥都不做。
非阻塞等待就是我们的父进程在等待回收子进程的时候,我们的父进程还可以去干别的事情。
选择阻塞等待还是非阻塞等待,就是我们的waitpid函数的第三个参数。
如果我们传入的是0,那么就是阻塞等待,如果传入的是WNOHANG,那么就使用的是非阻塞等待。
如下:

运行结果如下:

可以看到在等待子进程的回收中,我们的父进程也还在运行。
四、进程替换
我们前面创建了一个子进程后,父子各自执行父进程的一部分代码,如果我们的子进程想要执行一个全新的进程呢?
那么就需要用到我们的进程替换了,进程替换是不会创建新的进程的。
可以理解其是直接在这个进程内,将其后面的代码覆盖了。
我们要使用到exec系列的函数:

其替换原理如上。

这是我们的进程替换的函数,那么其使用要包含unistd头文件。
那么我们下面来对其一一讲解:
1、execl()
函数原型:
int execl(const char*path,const char *arg,.....);
第一个参数,要传入的是我们要替换的文件的绝对路径或者相对路径
第二个参数,传入我们的执行方式,命令行咋使用的,那么我们这边就怎么调用
最后一个参数,一定要传一个NULL
返回值:调用成功的话,就没有返回值,如果失败返回-1;
2、execlp()
函数原型:
int execlp(const char *file,const char*agr,...);
第一个参数:文件的绝对路径或者相对路径,然后如果这个路径在我们的PATH环境变量中的话, 那么我们就可以直接写文件名。
第二个参数:和execl是一样的,命令行咋写的,这里就咋写。
最后一个参数就必须是NULL。
3、execv()
函数原型:
int execv(const char*path,char*const argv[]);
第一个参数:文件的绝对路径或者相对路径
第二个参数:参数数组,argv[0]一般是我们要执行的文件名字或者命令,注意的是其最后一个元 素必须是NULL。
4、execvp()
函数原型:
int execvp(const char*file,char *const argv[]);
第一个参数:文件的绝对路径或者相对路径,如果在环境变量PATH中有的话,那么可以只传文件 名字
第二个参数:参数数组,和上面的execv一样。
5、execle()
函数原型
int execle(const char*pathname,chonst char * arg0,.....,NULL,char *const envp[]);
第一个参数:必须是可执行文件的路径
第二个参数:新程序的命令行参数列表,注意最后一个参数一定要为NULL
第三个参数:新的环境变量,替换当前进程的环境变量。