【Linux】进程控制

一、创建子进程

创建子进程需要用fork函数,以下是fork函数的声明及相关描述:

fork函数的返回值:

调用成功:给父进程返回子进程的pid,给子进程返回0。

调用失败:创建子进程失败,向父进程返回-1。

一般情况下,fork函数不会调用失败,而失败的情况有以下两种:

1.系统中有太多的进程

2.OS会限制一个用户所能创建的进程数量,避免OS自己都挂掉

int main()
{
    pid_t id=fork();
    //子进程
    if(id==0)
    {
        cout<<"我是子进程,fork的返回值是:"<<id<<"地址是:"<<&id<<endl;
        exit(0);
    }
    //父进程
    cout<<"我是父进程,fork的返回值是:"<<id <<"地址是:"<<&id<<endl;

    return 0;
}

运行截图:

为什么同一个fork函数却会给父子进程针对同一个变量但返回的却是两个不同的值?

fork创建子进程之后,子进程和父进程共享代码和数据,只有某个进程单独对数据进行修改的时候,才会发生写时拷贝,即另外开辟一块内存来存储改变后的数据,而原数据仍保留一份(即fork的返回值在内存中存在两份,一份属于父进程,一份属于子进程,这两个值不相同),这也是os为了避免内存浪费或者不高效行为的一种机制。

至于为什么父子进程fork返回值的地址却是一样的,这个在会在后面进程虚拟地址部分具体讲解。

而不只是数据能做修改,当程序替换时,也能修改写时拷贝代码。


二、进程终止

进程终止的情况分类:

1.进程正常执行完成后退出:创建子进程的目的就是为了帮用户完成任务,但计算结果到底是否正确就需要我们自行判断,可以根据退出码来帮助判断结果是否正确。

2.进程执行中异常导致进程终止

进程异常退出是进程在运行过程中被意外终止,从而导致进程本来应该继续执行的任务无法完成。即该完成的任务没有完成,因为异常导致进程提前终止。

#include <iostream>
#include <unistd.h>
#include <string.h>

using namespace std;

int main()
{

    for(int i=0;i<255;i++)
    {
        cout<<"退出码:"<<i<<":"<<strerror(i)<<endl;
    }

   return 0;
}

运行截图:

C语言strerror对进程退出码进行了解释,但进程的退出码和相关的描述可以自定义,不一定是按照C语言规定的来设置的。

在命令行使用echo $?会显示最近一次执行的进程的退出码

进程退出的方式:

1.main函数return使进程退出

2.exit和_exit都是使进程退出,但是exit会刷新缓冲区,执行清理函数等,而_exit是直接退出,不做其他任何操作。从此处也能看出缓冲区不存在内核,而存在用户层。


三、进程等待

进程等待:父进程等待子进程退出,如果父进程对子进程不管不顾,那么子进程就变为僵尸进程,kill -9命令也无法将僵尸进程杀死,因为它已经死亡了,而父进程同时也要知道子进程完成任务的情况,所以也要接收子进程的退出码及信号。

总结:

1.避免内存泄漏

2.获取子进程执行的结果

只要接收到信号,那就说明进程是异常退出的,此时的退出码就失去了意义;

没有接收到信号,那么进程就是正常运行的,此时的退出码可以用来评判结果是否正确。

进程等待的方法有两种:

1.wait(int* status

返回值:成功返回被等待进程的pid,失败返回-1。

参数:status:输出型参数

获取子进程退出状态,不关心可以设置为NULL。即不需要获取子进程的退出码和信号

2. waitpid(pid_t pid, int *status, int options)

参数:pid:如果pid>0,表示等待指定的进程。

                   如果pid=-1,表示等待任意一个子进程,和wait是相同效果。

           status:同wait函数中的status,获取进程的退出码和信号的。

           options:waitpid函数执行的选项,例如,将其设置为WNOHANG,可进行非阻塞轮询。

一般都是等待成功,除非是等待一个pid不存在的进程。

将status看作一个位图:不要当成一个完整的整数,而是其中特定的比特位来分别表示信号和退出码

前十六个比特位不用,低八位表示退出状态即退出码,倒数七位表示信号。

获取退出码和信号的方式(status右移八位,但status本身不变)

父进程如何获取子进程的退出信息?

子进程的pcb中有信号和退出码,父进程调用waitpid通过pid找到子进程的pcb,再将其中退出码和信号写进waitpid函数中的status,父进程就可以获取子进程的退出码和信号了。

父进程在wait的时候,如果子进程没有退出,那么父进程在干嘛?

父进程处于阻塞等待的状态,那么父进程就不在运行队列当中,而是在子进程的队列当中,即子进程的pcb中含有一个父进程的指针,当os检测到子进程结束后,通过该指针找到父进程,再执行父进程的操作。

如果不想让父进程一直等待子进程退出,而是在等待子进程退出的过程中去做一些其他事情,那么就可以采用非阻塞轮询(将waitpid中的option设置为WNOHANG)的方式,即时不时检测一下子进程是否退出,如果没有退出就继续做自己的事情,如果退出了那就回收,如果出错了,也能及时得到反馈。

非阻塞轮询的返回值有三种:

1.子进程还在运行当中没有退出

2.子进程运行过程中出错

3.子进程正常运行后退出

WNOHANG:父进程采用非阻塞轮询的方式。


四、程序替换

创建子进程的目的是为了让子进程帮我执行特定的任务

1.让子进程执行父进程的一部分代码

2.如果子进程指向一个全新的程序代码,就叫做程序替换。

程序替换函数:

以execl函数为例来理解这些函数的用法:

#include <iostream>
#include <unistd.h>
#include <string.h>

using namespace std;

int main()
{
    pid_t id=fork();
    if(id==0)
    {
        execl("/bin/ls","ls","-l","-a",NULL);
        exit(0);
    }

    
   return 0;
}

参数:

path:代表执行的程序所在的路径,在一系列程序替换函数中,函数名中不带p的替换函数,都要传入执行程序的路径。

args:代表如何执行程序,在命令行上输入的指令都要传入进来,而且必须用NULL结尾。

函数名中带l:在传参处必须带上完整的指令,并且还要以NULL结尾。

函数名中带p:不需要传入程序的路径,只需要传入程序的名称,调用时,会自动去环境变量对应的路径下去寻找改程序。

函数名中带v:可以将执行程序的指令放在一个vetcor容器里面,在传参时传入该vector,不需要在传参处带上所有指令。

函数名中带e:可以传入环境变量,可以是系统自带的环境变量,也可以是自己自定义的环境变量。但注意,自定义环境变量是覆盖式传入,自己定义的环境变量会覆盖系统的环境变量,如果不想覆盖式传入,可以在传入系统环境变量的基础上,追加自己的环境变量。

而上面的一系列函数都是对execve函数的封装,只有该函数是真正的系统调用。

追加环境变量代码:

#include <iostream>
#include <unistd.h>
#include <string.h>

using namespace std;
extern char** environ; //系统的环境变量

int main()
{
    pid_t id=fork(); //创建子进程
      
    //让子进程去完成任务
    if(id==0)
    {
        putenv("MYENY=youcanseeme"); // 追加自定义环境变量到系统的环境变量表当中
        execle("../myotherproc/test1","test1",NULL,environ); //执行另一个目录的可执行程序
        exit(0);
    }
    
   return 0;
}

在shell命令行当中直接使用export指令导入自定义环境变量,也可以做到传递环境变量。因为我们在bash中执行test程序,test变成bash的子进程,而test因为创建子进程并且程序替换去调用test1,那么test1就成了test的子进程,而子进程是继承父进程的环境变量表的,所以在shell命令行导入的环境变量,最终也能被test1这个“孙子进程”所接受到。

程序替换可以做到调用其他语言写的可执行程序,只要这个程序可以转化为Linux操作系统下进程的程序,都可以被替换执行。

程序替换实质上OS先创建了进程相关的数据结构(进程创建时,先有进程数据结构,再将代码和数据加载到内存),然后把进程的数据段和代码段用存在于磁盘上的程序进行了替换,把磁盘上的程序加载到了内存,进而执行替换后的程序,所以excel函数也可以被称为加载器。

所以如果一个程序中有execl函数,那么os先创建进程数据结构,然后通过execl对代码和数据进行替换。

注意:

1.程序替换是整体替换,不能局部替换。

2.程序替换只影响调用程序替换的进程,因为进程具有独立性。

3.因为父进程和子进程共享代码和数据,即页表映射的是同一块物理内存,而当子进程进行程序替换时,加载了新的代码,那么此时会发生写时拷贝,即写时拷贝也能发生在代码区。

关于execl函数的返回值问题:

execl如果没有替换程序成功就会有返回值,因为如果一旦替换成功了,就算接收了返回值,那么这个返回值在execl之后的任何一处都没有作用,因为后面的代码都被替换了,所以程序替换成功之后就不会有返回值的存在,而一旦有返回值,就是替换失败,并且会执行execl后面的代码,所以不用对返回值进行判断,execl就是替换新的程序,后面紧跟的就是没有替换成功的操作。

  • 4
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值