Linux【进程控制】

发布于:2022-12-19 ⋅ 阅读:(2875) ⋅ 点赞:(3)

目录

一、创建进程

简单使用一下fork 

问:fork创建子进程,操作系统都做了什么?

写时拷贝

fork常规的用法

fork失败的原因

二、进程终止

1.进程终止时,操作系统做了什么?

2.进程终止的常见方式

获取最近一次进程退出的退出码 

strerror

代码没跑完,程序崩溃的情况 

3.用代码,如何终止一个进程

①main函数中的return语句

②exit退出进程

③_exit

库函数vs系统接口

三、进程等待

为什么要进行进程等待?

如何等待和等待是什么? 

1.wait-基本验证,回收僵尸进程的问题

wait的返回值

2.waitpid-获取子进程退出结果的问题

父进程获取子进程的status

进程异常退出,或者崩溃的本质

1.除以0来生成一个异常信号

2.野指针错误

3.死循环

进程的细节问题

WIFEXITED(status)和WEXITSTATUS(status)

waitpid是阻塞式调用,但是如果我们想要让父进程也做一些别的事情呢?(options WNOHANG)

阻塞等待和非阻塞等待

waitpid详解

非阻塞等待的表现

练习:让我们的父进程在闲着的时候运行别的代码的测试

四、进程替换

1.是什么?概念+原理

①概念

②原理

2.怎么办?操作

1.不创建子进程

为什么没有打印最后那一句话?

如果我们写一个根本就不存在命令呢?

2.创建子进程 [只是用最简单的exec函数]

1.execl/execv

2.execlp

3.execvp 

4.execle

1.如何执行其他我自己写的C、C++二进制程序

如何使用Makefile一次性创建两个可执行程序

2.如何执行其他语言的程序

如何给我们的程序传入环境变量 

5.execvpe 

编写一个简单的shell

命名理解

3.为什么?总结


一、创建进程

在linux中fork函数时非常重要的函数,它从已存在进程中创建一个新进程。

新进程为子进程,而原进程为父进程

简单使用一下fork 

 使用项目自动化构建工具创建我们的项目

这是写在myproc.c中的代码 

int main()
{
    printf("我是父进程\n");

    pid_t id = fork();
    if(id < 0)
    {
        printf("创建子进程失败\n");
        return 1;
    }
    else if(id == 0)
    {
        // 子进程
        while(1)
        {
            printf("我是子进程: pid: %d, ppid: %d\n", getpid(), getppid());
            sleep(1);
        }
    }
    else{
        // 父进程
        while(1)
        {
            printf("我是父进程: pid: %d, ppid: %d\n", getpid(), getppid());
            sleep(1);
        }
    }
    return 0;
}

 

  在我们程序执行的时候再打开一个窗口

ps axj |grep myproc

将pid和ppid都打印出来 

 ps ajx |head -1 && ps axj | grep myproc

#include <unistd.h>
pid_t fork(void);
返回值:自进程中返回0,父进程返回子进程id,出错返回-1

问:fork创建子进程,操作系统都做了什么?

fork创建子进程,是不是系统里多了一个进程?是的!

进程=内核数据结构+进程代码和数据

 内核数据结构是从操作系统来的,进程代码和数据是从磁盘来的,也就是你的C/C++程序加载之后的结果!

进程调用fork,当控制转移到内核中的fork代码后,内核做:
分配新的内存块和内核数据结构给子进程(开辟空间)
将父进程部分数据结构内容拷贝至子进程(赋值和初始化,子进程的相关数据是以父进程为模板的)
添加子进程到系统进程列表当中
fork返回,开始调度器调度 

创建子进程,给子进程分配对应的内核结构,必须子进程自己独有了,因为进程具有独立性!

理论上,子进程也要有自己的代码和数据!

可是一般而言,我们没有加载的过程,也就是说,子进程没有自己的代码和数据!

所以子进程只能使用父进程的代码和数据

代码:都是不可以被写的,只能被读取,所以父子共享,没有问题!

数据:可能被修改的,所以必须分开!

对于数据而言:

1.创建进程的时候,就直接拷贝分离。

        但是可能拷贝子进程根本就不会用到的数据空间,及时用到了,也可能只是读取!

int main()
{
    const char *str = "aaa";
    const char *str1 = "aaa";
    printf("%p\n", str);
    printf("%p\n", str1);
    return 0;
}

 

 我们发现这两个字符串的地址其实是一样的。

编译器在编译程序的时候,尚且知道节省空间。因为在编译的之后这个“aaa”是不可以被修改的,所以为了节省空间,所以只保存一份。

所以我们的子进程也是一样,创建子进程,不需要将不会被访问的,或者只会读取的数据拷贝一份。

但是,还是必须要拷贝的(因为数据的独立性),什么样的数据值得拷贝呢?

将来会被父或子进程写入的数据!!

1.一般而言,即便是操作系统,也无法提前知道,哪些空间可能会被写入。 

2.即便是提前拷贝了,你会立马使用吗?

所以操作系统采用了写时拷贝技术,来进行将父子进程的数据进行分离

操作系统为何要使用写时拷贝的技术,对父子进程进行分离 

1.用的时候,再给你分配,是高效使用内存的一种表现

2.在代码执行前操作系统无法预知那些空间会被访问

string的深拷贝和浅拷贝的问题

浅拷贝就是两个指针指向同一块空间,如果其中一个指针修改了这块空间的数据,另一个指针访问的时候会发现这块空间已经被修改过了

深拷贝就是两个指针指向两块不同的空间,这两块空间的内容是一样的。

所以string的深拷贝和浅拷贝的问题其实和我们上面的写时拷贝的问题有点相似之处

 fork之后,父子进程代码共享(是after共享的,还是所有的?)

所有的代码都是共享的!

1.我们的代码在汇编之后会有很多行代码,而且诶行代码加载到内存之后,都会有对应的地址

2.因为进程随时可能被中断,(可能并没有被执行完),下次回来,还必须从之前的位置继续执行(不是从最开始的位置哦!),就要求CPU记随时记录下当前进程执行的位置。所以CPU内有对应的寄存器数据,用来记录当前进程的执行位置!(EIP,也就是PC指针(point code),我们在此将其称为程序计数器)

(PC指针中永远记录的是当前正在执行的代码的下一行代码的地址!)

CPU其实只会取指令,分析指令,执行指令,而eip就是相当于是CPU的小秘书的职责,eip指向哪里,我们的CPU就执行哪里的代码

寄存器在CPU内,只有一份,寄存器内的数据,是可以有多份的,也就是进程的上下文数据!创建进程时候,要不要给子进程?

虽然父子进程各自调度,各自会修改EIP,但是已经不重要了,因为子进程已经认为自己的EIP起始值,就是fork之后的代码!!

CPU如何认识指令?

通过指令集。比方说你想和外国人沟通,你可能就需要会英语。CPU也是一样,需要会一些简单的指令集,也就像是我们所学的英语一样。

写时拷贝

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

写时拷贝本质上值一种延迟申请,只有当真正要用到的时候才会分配资源。

 ​​​​​​​

 因为有写时拷贝技术的存在,所以父子进程得以彻底分开!

完成了进程独立性的技术保证!(写时拷贝的好处)

写时拷贝,是一种延时申请技术,可以提高整机内存的使用率 !!

fork常规的用法

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

一般而言,父子进程在实际使用的时候上面时候与我们生活中也是一样。比方说一个父亲希望子女和子女继承自己的事业,也就是第一种情况,又或者是希望子女去试一试别的事业,也就是第二种情况

fork失败的原因

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

进程创建,本质上是会消耗内存的。如果消耗的内存超出了系统的资源上限,系统就会封杀进程。并且一个普通用户能创建的进程个数其实是有上限的,系统不会让你创建太多的进程

二、进程终止

1.进程终止时,操作系统做了什么?

当然要释放进程申请的相关内核数据结构和对应的数据和代码

释放掉的本质就是释放系统资源(最主要的就是内存)。

2.进程终止的常见方式

①代码运行完毕,结果正确
②代码运行完毕,结果不正确(代码当中的逻辑有问题,没有达到预期的效果)
③代码异常终止(代码中存在野指针,或者越界之类的等等的行为,导致了程序崩溃)

这里我们先重点研究前两种。我们以main函数的返回值作为我们研究的切入点。

1.我们发现我们在写c/c++的时候我们的main函数的返回值只一个整数

2.我们的main函数总是会return一个0

那么main函数返回值的意义是什么?或者是说main函数return 0的意义是什么,为什么总是0?return 1 2 或者别的值不可以吗?

使用项目自动化构建工具Makefile

 再在myproc.c文件中写入下面的代码

#include <stdio.h>
int main()
{
  printf("hello world\n");
  return 0;                                                                       
}  

 这个代码很简单,就是会打印出一个hello world,但是这个return 0到底是什么意思呢?

这个return 0其实并不是总是0,可以是其他值。

一般来使我们的main函数的返回值可以叫做进程的退出码

这个进程的退出码是什么意思呢?

这里我们将我们的代码修改一下,将其return变成10,然后打印其进程和父进程的pid

 

最终给我们看淡ppid是7832,这个7832其实就是我们的bash,也就是我们的命令行解释器。

这个10就是我们的进程退出码,,表示我们的进程是正确还是不正确返回的,如果退出码为0,代码退出的结果是正确的。如果退出的结果码非0,就代表运行的结果是不正确

获取最近一次进程退出的退出码 

echo $?

这时,我们就获取到了之前的退出码10。

main函数的返回码是返回给上一级进程(比方说是父进程),用来评判该进程的执行结果用的,可以忽略。 

我是先写的代码,我怎么知道这个运行结果正不正确呢?

不要忘了,main函数实际上是一个函数,你自己写的代码实际上也是可以封装成一个函数。是可以传回对应的返回值来判断是否成功完成了当前的工作的。比方说求1-100的和,我们就可以通过返回值的形式来判断程序运行的结果是否正确,当然如果不关心的话,返回0也没有关系

 比方说上面的代码,就会通过ret,也就是返回值来判断我们当前的进程有没有成功运行

 我们查看到返回值是0,就是成功运行

总结:程序结果跑完运行正确还是不正确,你关心返回值的话,可以通过对返回值的判断来知道我们的程序运行结果是否正确。

但是对于main函数的返回值其实还有更加深刻的考量 

比方说,我们去参加一场考试,正常来说,我们的考试结果会有三种结果,第一种就是我们将考试考完了,考试成绩通过,第二种情况是我们考试考完了,但是考试成绩不通过,第三种情况就是我们考试中工作比被抓住了,我们的考试提前终止了,我们的考试成绩作废了。我们先看前两种情况。

我们拿着我们的考试100分的成绩告诉家长,家长一般不回去关心你为什么考了100分。但是你拿着你的20分的试卷告诉家长,他们一定会质问你怎么才考这么一点分。

也就是说一旦程序运行的结果是正确的,没有人会关心结果为什么是正确的。只有当程序运行的结果是不正确的时候,别人才会去关心这个运行结果为什么是不正确的。

非零的返回值有无数个,不同的非零值就可以表示不同的错误原因。我们不妨用1表示考试前没睡好,2表示考试时候拉肚子,3表示考试之前没有好好复习。

所以在我们的程序给出运行结果之后,结果不正确是,方便定位错误原因的细节

所以退出码的意义除了判断程序是否正确运行之外,还能够方便判断程序运行错误的原因!

strerror

但是对于我们,一名程序员,可能并不清楚退出码1,2,3,4之类的是什么意思,所以我们需要将我们的错误码挥着退出码转换为字符(具体出错提示)的方案。 

这时,我们就可以使用strerror

编写我们的测试代码 

 

 运行我们的程序

这些系统提供的退出码在133的时候大概就结束了 

这就代码着我们可以通过这些对应的退出码来返回对应的错误信息

我们自己可以使用这些退出码和含义,但是,如果你想自己定义,也可以自己设计一套退出方案!

这里我们打开一个不存在文件,它范围No such file or directory

我们发现这就是我们退出码为2的退出码信息 

 

 

 但是我们输入kill -9 111111111,显然这个11111111是一个不存在的进程,所以会报错。

 但是我们打印出返回码,我们所得到的却是1,这根我们之前的标号对应不上

 这是因为的kill命令其实是有自定义的退出码的。

我们也可以使用这些退出码,比方说打开文件失败,直接return 1,也就是operation not permitted

在Linux下成功跑出来的代码,返回值都是0

代码没跑完,程序崩溃的情况 

前三条代码运行出来了,但是后面的代码没有运行出来 

 

 程序崩溃的时候,退出码没有意义!一般而言退出码对应的return语句,没有被执行!就像是你考试作弊了,那么这个成绩都没有任何意义了。

我们此时更想要知道程序为什么会崩溃。

3.用代码,如何终止一个进程

①main函数中的return语句

 

 return语句,就是终止进程的!return 退出码就可以终止一个进程。

只有main函数内的return语句才能够表示进程退出

②exit退出进程

我们发现exit可以成功退出并且返回其退出的数据

那如果我们在函数中也写入exit呢?

这里我们惊奇地发现我们的退出码变成了222,也就是在我们的sum函数中的那个exit的退出值

 所以exit在任何地方被调用,都代表着直接终止进程

③_exit

将我们之前的exit全部都换成_exit 

我们发现运行的结果跟我们之前的exit是一样的。

这里我们输入下面的测试代码

 

由于我们的you can see me后面并没有\n也就是并没有将我们的缓冲区内的字符马上刷新到我们的呢屏幕上,所以我们的you can see me 会在等待三秒过后才将我们的内容打印到屏幕上。

 也就是说我们的exit是会在退出之前将我们的缓冲区的内容刷新到我们的屏幕上的。

我们修改一下exit为_exit,再次测试我们的程序

我们发现我们缓冲区中的内容并没有被打印出来!

 

说明_exit是一个系统调用,直接终止掉这个进程,并不去管这个缓冲区中的内容是否已经刷新到屏幕上的问题,但是我们的exit会在关闭进程之前执行用户定义的清理函数,冲刷缓冲,关闭流等概念,最后再退出进程。

 我们更推荐使用exit

库函数vs系统接口

  exit()是C语言提供给我们的,是一个库函数,而_exit()属于系统接口,但是exit()在底层其实调用的是_exit()。

printf -\n数据是保存在“缓冲区”中的

请问这个缓冲区在哪里,指的是什么?缓冲区是由谁维护的?

通过上面的实验验证,我们可以知道缓冲区一定不在操作系统内部!!!

因为如果是操作系统维护的,,缓冲区_exit()也能将缓冲区中的内容刷新到屏幕上!

所以是c语言的标准库帮助我们维护的。

(初始窥见缓冲区)

三、进程等待

为什么要进行进程等待?

1.子进程退出,父进程不管子进程,子进程就要处于僵尸状态--导致内存泄漏

(另外,进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。)

2.父进程创建了子进程,是要让子进程办事儿的,那么子进程把任务完成的怎么样,父进程需要关心吗?如果需要,如何得知?如果不需要,该怎么处理呢?

子进程把任务完成总共有三种结果:1.代码跑完,结果正确2.代码跑完,结果不正确3.代码没有跑完,程序崩溃了。

那么父进程如何知道子进程完成任务的结果呢?就是通过等待来完成。

父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息

如何等待和等待是什么? 

1.wait-基本验证,回收僵尸进程的问题

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

int main()
{
    pid_t id = fork();
    if(id < 0)
    {
        perror("fork");
        exit(1); //标识进程运行完毕,结果不正确
    }
    else if(id == 0)
    {
        //子进程
        int cnt = 5;
        while(cnt)
        {
            printf("cnt: %d, 我是子进程, pid: %d, ppid : %d\n", cnt, getpid(), getppid());
            sleep(1);
            cnt--;

        }
        //h直接终止了子进程
        exit(0);
    }
    else
    {
        //父进程
        while(1)
        {
            printf("我是父进程, pid: %d, ppid: %d\n", getpid(), getppid());
            sleep(1);
        }
    }
}

重新打开一个终端,输入下面的代码,监视我们刚刚的进程

while :; do ps ajx | head -1 &&ps ajx| grep myproc|grep -v grep; sleep 1;echo "-------------------------"; done

 

 

 这时,我们不妨使用wait接口

我们使用man 2 wait来查看一下wait的用法

 调用一个进程,直到这个进程的状态发生变化,也就是父进程等待这个进程的死亡。

wait的返回值

成功了就返回子进程的pid,失败了就返回-1.

创建我们的测试代码

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
//从这两个导入的库可以看出这可能是一个系统接口
#include <sys/types.h>
#include <sys/wait.h>


int main()
{
    pid_t id = fork();
    if(id < 0)
    {
        perror("fork");
        exit(1); //标识进程运行完毕,结果不正确
    }
    else if(id == 0)
    {
        //子进程
        int cnt = 5;
        while(cnt)
        {
            printf("cnt: %d, 我是子进程, pid: %d, ppid : %d\n", cnt, getpid(), getppid());
            sleep(1);
            cnt--;

        }
        //直接终止了子进程
        exit(0);
    }
    else
    {
        //父进程
        printf("我是父进程, pid: %d, ppid: %d\n", getpid(), getppid());
        pid_t ret=wait(NULL);//阻塞式地等待!说白了,就是让父进程处于一种阻塞状态
        if(ret > 0)
        {
            // 0x7F -> 0000.000 111 1111
            printf("等待子进程成功, ret: %d",ret);
            sleep(1);
        }
        printf("ok\n");
    }
}

 我们看到在子进程运行期间,父进程一直等待着子进程的死亡。

为了让我们的测试结果更加明显一点,我们不妨将父进程sleep的时间调整为7秒

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
//从这两个导入的库可以看出这可能是一个系统接口
#include <sys/types.h>
#include <sys/wait.h>


int main()
{
    pid_t id = fork();
    if(id < 0)
    {
        perror("fork");
        exit(1); //标识进程运行完毕,结果不正确
    }
    else if(id == 0)
    {
        //子进程
        int cnt = 5;
        while(cnt)
        {
            printf("cnt: %d, 我是子进程, pid: %d, ppid : %d\n", cnt, getpid(), getppid());
            sleep(1);
            cnt--;

        }
        //直接终止了子进程
        exit(0);
    }
    else
    {
        //父进程
        printf("我是父进程, pid: %d, ppid: %d\n", getpid(), getppid());
        sleep(7);
        pid_t ret=wait(NULL);//阻塞式地等待!说白了,就是让父进程处于一种阻塞状态
        if(ret > 0)
        {
            // 0x7F -> 0000.000 111 1111
            printf("等待子进程成功, ret: %d",ret);
            sleep(1);
        }
        printf("ok\n");
    }
}

也就是说子进程跑5秒,父进程跑7秒,大概有两秒钟的时间内,子进程处于僵尸状态

从下面的测试结果汇总,我们看到最终结果5513也就是我们的子进程的pid等待成功,然后等待期间也是大概有两秒子进程处于僵尸状态

 

2.waitpid-获取子进程退出结果的问题

pid_ t waitpid(pid_t pid, int *status, int options);
返回值:
        当正常返回的时候waitpid返回收集到的子进程的进程ID;
        如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
        如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;

        返回值>0表示等待子进程成功,小于零表示等待自己成失败
参数:
pid:Pid=-1,等待任一个子进程。与wait等效。

         Pid>0,等待其进程ID与pid相等的子进程。
status:

WIFEXITED(status): 若为正常终止子进程返回的状态,则为。(查看进程是否是正常退出)
WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
options:
        WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结        束,则返回该子进程的ID。 (默认为0,表示阻塞等待)

waitpid(pid,nul,0)等价于wait(NULL)

输入测试代码

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
//从这两个导入的库可以看出这可能是一个系统接口
#include <sys/types.h>
#include <sys/wait.h>


int main()
{
    pid_t id = fork();
    if(id < 0)
    {
        perror("fork");
        exit(1); //标识进程运行完毕,结果不正确
    }
    else if(id == 0)
    {
        //子进程
        int cnt = 5;
        while(cnt)
        {
            printf("cnt: %d, 我是子进程, pid: %d, ppid : %d\n", cnt, getpid(), getppid());
            sleep(1);
            cnt--;

        }
        //h直接终止了子进程
        exit(0);
    }
    else
    {
        //父进程
        printf("我是父进程, pid: %d, ppid: %d\n", getpid(), getppid());
        //阻塞式的等待
        pid_t ret=waitpid(id,NULL,0);
        if(ret > 0)
        {
            printf("等待子进程成功, ret: %d",ret);
            sleep(1);
        }
    }
}

  

 

父进程获取子进程的status

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
//从这两个导入的库可以看出这可能是一个系统接口
#include <sys/types.h>
#include <sys/wait.h>


int main()
{
    pid_t id = fork();
    if(id < 0)
    {
        perror("fork");
        exit(1); //标识进程运行完毕,结果不正确
    }
    else if(id == 0)
    {
        //子进程
        int cnt = 5;
        while(cnt)
        {
            printf("cnt: %d, 我是子进程, pid: %d, ppid : %d\n", cnt, getpid(), getppid());
            sleep(1);
            cnt--;

        }
        //设置退出码为105
        exit(105);
    }
    else
    {
        //父进程
        printf("我是父进程, pid: %d, ppid: %d\n", getpid(), getppid());
        int status=0;
        //阻塞式的等待
        pid_t ret=waitpid(id,&status,0);
        if(ret > 0)
        {
            printf("等待子进程成功, ret: %d,status: %d",ret,status);
            sleep(1);
        }
    }
}

但是我们观察到我们的status并不是我们之前父进程传给子进程的105,而是26880 !!

 

 所以父进程并没有成功拿到我们子进程的退出结果。

这里我们需要解释一下status的构成

虽然我们的status在函数参数上是一个整数,再参数上将我们整型变量的地址传递给我们的status,waitpid是我们操作系统的接口,所以它最终就可以把这个值拿出来,给我们的操作系统读取,但是status并不是按照整数来整体使用的

子进程运行完毕是有三种结果的①代码跑完,结果正确②代码跑完,结果不正确③代码没有跑完,程序崩溃,

所以我们的status并不是按照我们整数的形式来整体使用的!,而是按照比特位的方式,将32个比特位进行划分,我们只学习低16位。

那么我们如果想要得到子进程的退出结果,我们应该如何去得到呢?

我们再对我们的代码进行一点修改


int main()
{
    pid_t id = fork();
    if(id < 0)
    {
        perror("fork");
        exit(1); //标识进程运行完毕,结果不正确
    }
    else if(id == 0)
    {
        //子进程
        int cnt = 5;
        while(cnt)
        {
            printf("cnt: %d, 我是子进程, pid: %d, ppid : %d\n", cnt, getpid(), getppid());
            sleep(1);
            cnt--;

        }
        //设置退出码为105
        exit(105);
    }
    else
    {
        //父进程
        printf("我是父进程, pid: %d, ppid: %d\n", getpid(), getppid());
        int status=0;
        //阻塞式的等待
        pid_t ret=waitpid(id,&status,0);
        if(ret > 0)
        {
            //因为我们需要获取退出码status的次低八位,所以我们需要将status右移8位,
            //然后再按位与上0xFF,相当于是0000 ……0000 1111 1111,也就是取出了当前的最低八位,
            //也就是取出了源数据的次低八位
            printf("等待子进程成功, ret: %d,子进程退出码: %d",ret,(ret,status>>8)&0xFF);
            sleep(1);
        }
    }
}

这里我们成功查看到了105,也就是我们子进程的返回值。 

进程异常退出,或者崩溃的本质

只要程序跑起来,它就是进程,和我们的语言的特性没有任何关系,而是属于操作系统的范畴。所以进程异常退出的本质是操作系统杀掉了我们的进程!

操作系统是如何杀掉我的进程的?本质上是通过发送信号的方式!

所以其实是操作系统给我们的进程发信号,将我们的进程杀掉的。每一个信号都是有编号的。(上面的编号看起来有64个,但是气势上是没有32和33号的)

前面31个是普通信号。

所以如果我们的进程如果因为野指针退出了,我们的进程一定会收到上面的前31个信号中的某一个,这31个信号我们就用status的低7个信号的比特位来表示

之后我们用jdb调试崩溃程序的信号的时候,还会使用到core dump标志

注意,信号从1-31,是没有0号信号的!!

接下来我们编写代码查看我们上面的进程的信号,也就是status的低7位的数据

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
//从这两个导入的库可以看出这可能是一个系统接口
#include <sys/types.h>
#include <sys/wait.h>


int main()
{
    pid_t id = fork();
    if(id < 0)
    {
        perror("fork");
        exit(1); //标识进程运行完毕,结果不正确
    }
    else if(id == 0)
    {
        //子进程
        int cnt = 5;
        while(cnt)
        {
            printf("cnt: %d, 我是子进程, pid: %d, ppid : %d\n", cnt, getpid(), getppid());
            sleep(1);
            cnt--;

        }
        //设置退出码为105
        exit(105);
    }
    else
    {
        //父进程
        printf("我是父进程, pid: %d, ppid: %d\n", getpid(), getppid());
        int status=0;
        //阻塞式的等待
        pid_t ret=waitpid(id,&status,0);
        if(ret > 0)
        {
            //0x7F ->0000 000 0111 1111
            //因为我们需要获取退出码status的次低八位,所以我们需要将status右移8位,
            //然后再按位与上0xFF,相当于是0000 ……0000 1111 1111,也就是取出了当前的最低八位,
            //也就是取出了源数据的次低八位
            //status右移8位并不会影响status本身

            printf("等待子进程成功, ret: %d,子进程收到的信号编号:%d ,子进程退出码: %d",ret,status&0x7F,(ret,status>>8)&0xFF);
            sleep(1);
        }
    }
}

这里咱们的信号编号是0,代表我们的程序是正常退出的,运行期间没有错误。 

 

我们不妨制造一个异常信号查看一下会发生什么情况

1.除以0来生成一个异常信号

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
//从这两个导入的库可以看出这可能是一个系统接口
#include <sys/types.h>
#include <sys/wait.h>


int main()
{
    pid_t id = fork();
    if(id < 0)
    {
        perror("fork");
        exit(1); //标识进程运行完毕,结果不正确
    }
    else if(id == 0)
    {
        //子进程
        int cnt = 5;
        while(cnt)
        {
            printf("cnt: %d, 我是子进程, pid: %d, ppid : %d\n", cnt, getpid(), getppid());
            sleep(1);
            cnt--;
            //产生一个异常信号
            int a=10;
            a/=0;
        }
        //设置退出码为105
        exit(105);
    }
    else
    {
        //父进程
        printf("我是父进程, pid: %d, ppid: %d\n", getpid(), getppid());
        int status=0;
        //阻塞式的等待
        pid_t ret=waitpid(id,&status,0);
        if(ret > 0)
        {
            //0x7F ->0000 000 0111 1111
            //因为我们需要获取退出码status的次低八位,所以我们需要将status右移8位,
            //然后再按位与上0xFF,相当于是0000 ……0000 1111 1111,也就是取出了当前的最低八位,
            //也就是取出了源数据的次低八位
            //status右移8位并不会影响status本身

            printf("等待子进程成功, ret: %d,子进程收到的信号编号:%d ,子进程退出码: %d",ret,status&0x7F,(ret,status>>8)&0xFF);
            sleep(1);
        }
    }
}

2.野指针错误

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
//从这两个导入的库可以看出这可能是一个系统接口
#include <sys/types.h>
#include <sys/wait.h>


int main()
{
    pid_t id = fork();
    if(id < 0)
    {
        perror("fork");
        exit(1); //标识进程运行完毕,结果不正确
    }
    else if(id == 0)
    {
        //子进程
        int cnt = 5;
        while(cnt)
        {
            printf("cnt: %d, 我是子进程, pid: %d, ppid : %d\n", cnt, getpid(), getppid());
            sleep(1);
            cnt--;
            //产生一个异常信号
            int *p=NULL;
            *p=100;
        }
        //设置退出码为105
        exit(105);
    }
    else
    {
        //父进程
        printf("我是父进程, pid: %d, ppid: %d\n", getpid(), getppid());
        int status=0;
        //阻塞式的等待
        pid_t ret=waitpid(id,&status,0);
        if(ret > 0)
        {
            //0x7F ->0000 000 0111 1111
            //因为我们需要获取退出码status的次低八位,所以我们需要将status右移8位,
            //然后再按位与上0xFF,相当于是0000 ……0000 1111 1111,也就是取出了当前的最低八位,
            //也就是取出了源数据的次低八位
            //status右移8位并不会影响status本身

            printf("等待子进程成功, ret: %d,子进程收到的信号编号:%d ,子进程退出码: %d",ret,status&0x7F,(ret,status>>8)&0xFF);
            sleep(1);
        }
    }
}

3.死循环

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
//从这两个导入的库可以看出这可能是一个系统接口
#include <sys/types.h>
#include <sys/wait.h>


int main()
{
    pid_t id = fork();
    if(id < 0)
    {
        perror("fork");
        exit(1); //标识进程运行完毕,结果不正确
    }
    else if(id == 0)
    {
        //子进程
        int cnt = 5;
        //死循环
        while(cnt)
        {
            printf("cnt: %d, 我是子进程, pid: %d, ppid : %d\n", cnt, getpid(), getppid());
            sleep(1);
        }
        //设置退出码为105
        exit(105);
    }
    else
    {
        //父进程
        printf("我是父进程, pid: %d, ppid: %d\n", getpid(), getppid());
        int status=0;
        //阻塞式的等待
        pid_t ret=waitpid(id,&status,0);
        if(ret > 0)
        {
            //0x7F ->0000 000 0111 1111
            //因为我们需要获取退出码status的次低八位,所以我们需要将status右移8位,
            //然后再按位与上0xFF,相当于是0000 ……0000 1111 1111,也就是取出了当前的最低八位,
            //也就是取出了源数据的次低八位
            //status右移8位并不会影响status本身

            printf("等待子进程成功, ret: %d,子进程收到的信号编号:%d ,子进程退出码: %d",ret,status&0x7F,(ret,status>>8)&0xFF);
            sleep(1);
        }
    }
}

当我们子进程运行起来的时候,在通过另外一个终端将我们的子进程杀掉,就能够看到死循环的返回信号。

总结:程序异常不光光是内部代码有问题,也可能是外力直接杀掉(子进程代码跑完了吗?不确定,所以子进程的退出码没有任何意义)

进程的细节问题

1.父进程通过wait/waitpid可以拿到子进程的退出结果,为什么要用wait/waitpid函数呢??直接全局变量不行吗?

 我们不妨使用代码直接验证一下

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
//从这两个导入的库可以看出这可能是一个系统接口
#include <sys/types.h>
#include <sys/wait.h>

int code=0;
int main()
{
    pid_t id = fork();
    if(id < 0)
    {
        perror("fork");
        exit(1); //标识进程运行完毕,结果不正确
    }
    else if(id == 0)
    {
        //子进程
        int cnt = 5;
        //死循环
        while(cnt)
        {
            printf("cnt: %d, 我是子进程, pid: %d, ppid : %d\n", cnt, getpid(), getppid());
            sleep(1);
        }
        //设置退出码为105
        code=15;
        exit(15);
    }
    else
    {
        //父进程
        printf("我是父进程, pid: %d, ppid: %d\n", getpid(), getppid());
        int status=0;
        //阻塞式的等待
        pid_t ret=waitpid(id,&status,0);
        if(ret > 0)
        {
            //0x7F ->0000 000 0111 1111
            //因为我们需要获取退出码status的次低八位,所以我们需要将status右移8位,
            //然后再按位与上0xFF,相当于是0000 ……0000 1111 1111,也就是取出了当前的最低八位,
            //也就是取出了源数据的次低八位
            //status右移8位并不会影响status本身

            printf("等待子进程成功, ret: %d,子进程收到的信号编号:%d ,子进程退出码: %d",ret,status&0x7F,(ret,status>>8)&0xFF);
            printf("code: %d\n",code);
            sleep(1);
        }
    }
}

从我们的测试结果来看,我们的父进程根本拿不到这个全局变量的值 

 进程具有独立性,那么数据就要发生写时拷贝,父进程无法拿到,更何况,信号呢?

2.既然进程是具有独立性的,进程退出码,不也是进程的数据吗?父进程又凭什么拿到呢??wait/waitpid究竟干了什么呢?

僵尸进程:至少要保留该进程的pcb信息!task_struct里面保留了任何进程退出时的退出结果信息!!

wait/waitpid本质上是读取子进程的task_struct结构,也就是说一个进程在退出的时候,会将自身的一些信号参数(退出码和退出信号)写入自身的pcb中,等待别的进程来读取这些信号。

3.wait/waitpid有这个权限吗?task_struct是内核的数据结构呀

当然有这个权限呀,因为wait/waitpid其实就是系统调用,不就是操作系统。

所以父进程没有权利直接去获得子进程的相关信息,但是父进程可以通过系统调用去获取子进程的退出结果

4.子进程退出了,变成了僵尸进程,反正会被系统领养,那么父进程为什么还要等待子进程的推出码呢?是不是多此一举呢?

可以,这也是一种编程方式。但是这样的话,我们的父进程就拿不到子进程的运行状态。但是如果我们就是不关心子进程的运行状态呢?但是万一我们的父进程一直不退出,就是比方说部署在服务器上的软件,一直都在运行,那么这个僵尸子进程就会一直占用系统资源。

5.内存泄漏的问题,如果一个进程退出了,那么这个内存泄漏的问题还在不在?(比方说我们的进程new/malloc开辟了一块空间,但是没有释放,这块空间会不会随着进程的退出而自动被回收?)

如果曾经在语言层面上new/malloc出来的空间,没有释放,在进程退出的时候是会将其空间释放的。比防说我们关闭掉一个软件,我们看到我们的内存剩余空间就变大了很多。

6.那么子进程的僵尸状态退出了,所占用的资源会不会被回收呢?

我们的new/malloc仅仅是语言层面的内存泄漏,而子进程的pcb结构是由操作系统维护的,是属于操作系统层面的内存泄漏,是不会被自动回收的!!

(服务器上的软件一般都是要跑好几年的,千万不能发生语言或者系统层面的内存泄漏!)

WIFEXITED(status)和WEXITSTATUS(status)

pid_ t waitpid(pid_t pid, int *status, int options);
返回值:
当正常返回的时候waitpid返回收集到的子进程的进程ID;
如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;
参数:
pid:
        Pid=-1,等待任一个子进程。与wait等效。
        Pid>0.等待其进程ID与pid相等的子进程。
status:
        WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(这个宏查看进程是否是正常退出)
        WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码,用这个宏来帮助我们提取退出码)
options:
        WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。

编写测试代码测试这里的WIFEXITED(status)

waitpid的第一个参数

id>0代表的是等待指定进程
id==0;todo
id==-1等待任意一个子进程的退出
id==-1等待任意一个子进程退出,等价于wait();

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
int main()
{
   pid_t id=fork();
   if(id==0)
   {
      int cnt=5;
      while(cnt)
      {
         printf("我是子进程:%d\n",cnt);
         sleep(1);
      }
      exit(11);
  }
  else{
      //父进程
      int status=0;
            //只有子进程推出的时候,父进程才会waitpid函数,进行返回,父进程还或者!!
            //waitpid/wait!可以在目前的情况下,让进程退出具有一定的顺序性
            //将来可以让父进程进行更多的收尾工作
            //id可以被设置成三种值
            //id>0代表的是等待指定进程
            //id==0;todo
            //id==-1等待任意一个子进程的退出
            //id==-1等待任意一个子进程退出,等价于wait();
      pid_t result=waitpid(id,&status,0);//默认是在阻塞状态等待子进程退出
      if(result>0)
      {
//           printf("父进程等待成功,退出码:%d退出信号%d\n",(status>>8)&0xFF,status &0x7F);
            //系统已经提供给我们对应的宏来提取退出码
            if(WIFEXITED(status))
            {
                //子进程是正常退出的
                printf("子进程执行完毕,子进程的退出码:%d\n", WEXITSTATUS(status));
            }
            else
            {
                //查看一下异常退出的标记码是什么
                printf("子进程异常退出:%d\n", WIFEXITED(status));
            }
      }
    }
 }

 

 

waitpid是阻塞式调用,但是如果我们想要让父进程也做一些别的事情呢?(options WNOHANG)

 如果我们的就是想让父进程在子进程的等待期间也运行一些工作呢?

第三个参数options默认为0,也就是阻塞等待

我们可以将option的属性设置为WNOHANG:叫做非阻塞等待!

Linux是用C语言写的->通过系统调用接口->OS自己提供的接口->就是C语言函数->系统提供的一半是大写的标记位WNOHANG,其实就是宏定义

因为在代码中定义的一些参数,时间长了,也记不清到底应该传什么参数,比方说wait(x,y,l) ,但是我们将这些参数定义成宏就会好辨识一点wait(x,y,WNOHANG)

这里的WNOHANG也就是这样的

假如一个进程长时间没有反应,或者窗口没办法拖拽,或者下载文件突然变成了0kb/s,就是一个程序卡住了,就称为程序(夯住了),在系统层面上,不就是这个进程没有被CPU调度吗?

没有被CPU调度的原因非常多,比方说我们的CPU非常忙,在切换进程的时候比较慢,这个时间间隔被你感知到了,你就会觉得这个电脑变卡了。假设我们的CPU更忙了,那个一个进程切换出来再切换进去的时间特别长,就称为这个进程夯住了。(这个进程要么是在阻塞队列中,要么是在等待被调度)。

这里父进程等待子进程就是被夯住了。

我们想让父进程在等子进程的时候运行一些别的工作,就是不被“夯”住,也就是我们上面定义的宏W NO HANG

阻塞等待和非阻塞等待

阻塞等待的意思,将进程的状态R改成D,然后PCB进入到阻塞队列中。底层都是数据结构的调度。(进程阻塞,本质是进程阻塞在系统函数的内部!阻塞等待一般都是在内核中阻塞,等待被唤醒)(一个进程被阻塞了,往往伴随着进程的切换,也就是我们的软件好像卡住了)

我们之前调用的scanf(),cin,也就是从键盘上读数据,但是要是我们键盘上并没有输入,那么我们的进程就会被挂起(在我们操作系统的层面上的系统调用接口并没有传入数据),也就是我们的程序在代码中间部分被切换出去了。(这个scanf和cin必定封装系统调用)

非阻塞等待的意思就是,我们在等待子进程的时候,发现子进程并没有就绪(父进程检测子进程的退出状态,发现子进程并没有退出)。

也就是我们的父进程通过调用waitpid来进行等待,如果子进程没有退出,我们waitpid这个系统调用,立马返回。

case1:

假设你是一个学渣,但是你有一个学霸朋友张三,笔记做得非常好,知识点非常清晰。快要考试了,你给张三打电话,说你请张三吃饭,问张三能不能帮帮忙将笔记借给你,顺便把考试重点划一划。张三说这时他还有事要忙能不能等一下。你说电话别挂,就一直连着,等你忙完了就回我一声,咱一起去吃饭。

这里张三没有忙完,张三不挂电话,我一直将电话放在耳朵旁边,等待张三的回应,其他什么事情都没有做,你整整等了2和小时,花光了话费。

张三没有好,不挂电话,就是阻塞调用


case2:

一个月之后你的考试通过了,但是你又有另一门考试,你再次给张三打电话,问张三能不能把张三的重点笔记借给你。但是张三说还是得等他一会儿。这一次你说:“行,那我等一会儿再打过去。”你开始做自己的事情,先开始复习。每过一段时间打电话给张三,问张三有没有好。

这里每一次打电话的过程,就是非阻塞调用(基于非阻塞调用的轮询检测方案!)。不会因为打一次电话没有就绪,就干扰我自身的工作。

这里你就是用户打电话就是系统调用张三就是操作系统

waitpid详解

下面是一段伪代码

假设父进程调用下面的这个函数,也就是调用了waitpid

waitpid(child_id,status,flags);
{
    //下面是内核中waitpid的实现,属于操作系统的
    //检测子进程退出状态,检查你的task_struct中子进程的运行信息
    if(status ==退出){
        return child_pid;
    }
    else if(status ==没退出)
    {
        //==0也就是我们的参数处于默认状态
        if(flags==0)//挂起父进程,拿到父进程的pcb(father_pcb),放入等待队列中
        //进程阻塞,本质是进程阻塞在系统函数的内部!
        //后面的代码不执行了
        //当条件满足的时候,父进程被唤醒,从哪里唤醒?
        //1.从waitpid重新调用,还是从if的后面?
        //当父进程被切换走的时候,系统的EIP寄存器的指针是指向上面的if的位置的,所以从if的位置继续执行
        else if(flag==WNOHANG)
        {
            //不阻塞进程
            return 0
            //这里return 0之后,我们的父进程不会被挂起,而是继续做自己的工作
        }
        
        return 0;
    }
    else
    {
        //出错了等其他原因
        return -1;
    }    

}

阻塞vs非阻塞

我们未来编写的代码的内容,大多都是网络代码,大部分都是IO类别,不断面临阻塞和非阻塞的接口。


非阻塞等待的表现

测试代码

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

int main()
{
    pid_t id=fork();
    if(id==0)
    {
        int cnt=5;
        while(cnt)
        {
            printf("我是子进程:%d\n",cnt--);
            sleep(1);
        }
        exit(11);
    }
    else{
        int quit=0;
        //只要不退出
        while(!quit)
        {
            int status=0;
            pid_t res=waitpid(-1,&status,WNOHANG);//以非阻塞方式等待
            //基本的判定
            if(res>0)
            {
                //等待成功 && 子进程退出
                //你给张三打电话,张三说他做好了,你俩直接去吃饭了
                printf("等待子进程退出成功,退出码:%d\n", WEXITSTATUS(status));
                quit=1;
            }
            else if(res==0)
            {
                //等待成功 && 但子进程并未退出
                //你给张三的电话打成功了,但是张三还没好
                printf("子进程还在运行中,暂时还没有退出,父进程可以再等一等,父进程可以再处理一下其他事情??\n");
                //quit状态不变,一直进行检测
                //等待一下,不然刷太快了
                sleep(1);
            }
            else
            {
                //等待失败
                //你给张三打电话,但是电话号码记错了,没打成功
                printf("wait失败\n");
                quit=1;
            }
        }
    }
}

练习:让我们的父进程在闲着的时候运行别的代码的测试

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

typedef  void (*hander_t)();//函数指针类型
std::vector<hander_t> handers;//定义了一个容器,容器中能保存一个一个函数指针类型,也就是函数指针数组

void func_one()
{
    printf("这是一个临时任务1\n");
}
void func_two()
{
    printf("这是一个临时任务2\n");
}

//设置对应的方法回调
//我们就将这两个函数保存在我们的方法中
//以后想让父进程闲了指定别的任务,就将任务的往Load中注册,就可以让父进程执行对应的方法喽!
void Load()
{
    handers.push_back(func_one);
    handers.push_back(func_two);
}
int main()
{
    pid_t id=fork();
    if(id==0)
    {
        int cnt=5;
        while(cnt)
        {
            printf("我是子进程:%d\n",cnt--);
            sleep(1);
        }
        exit(11);
    }
    else{
        int quit=0;
        //只要不退出
        while(!quit)
        {
            int status=0;
            pid_t res=waitpid(-1,&status,WNOHANG);//以非阻塞方式等待
            //基本的判定
            if(res>0)
            {
                //等待成功 && 子进程退出
                //你给张三打电话,张三说他做好了,你俩直接去吃饭了
                printf("等待子进程退出成功,退出码:%d\n", WEXITSTATUS(status));
                quit=1;
            }
            else if(res==0)
            {
                //等待成功 && 但子进程并未退出
                //你给张三的电话打成功了,但是张三还没好
                printf("子进程还在运行中,暂时还没有退出,父进程可以再等一等,父进程可以再处理一下其他事情??\n");
                if(handers.empty())
                {
                    Load();
                }
                for(auto iter :handers)
                {
                    //执行处理其他任务
                    iter();
                }
                //quit状态不变,一直进行检测
                //等待一下,不然刷太快了
                sleep(1);
            }
            else
            {
                //等待失败
                //你给张三打电话,但是电话号码记错了,没打成功
                printf("wait失败\n");
                quit=1;
            }
        }
    }
}

四、进程替换

1.是什么?概念+原理

①概念

fork()之后,父子各自执行代码的一部分,父子代码共享,数据写时拷贝各自一份!

如果子进程就想要执行一个全新的程序呢?(不进行父进程分配的那一段小代码呢?)

子进程也想有自己的代码!

这里我们就需要用到进程的程序替换来完成这个功能。

程序替换:是通过特定的接口,加载磁盘上的一个全新的程序(代码和数据),加载到调用进程的上下文(地址空间)中!从而达到让子进程运行其他程序的目的

回忆一下fork的用法

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

②原理

用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。

问:进程替换有没有创建新的子进程?

没有!!进程是内核数据结构+代码+数据!!

 一个运行起来的程序就叫做进程,这里我们的进程替换仅仅是重新加载了一段代码进来,并没有生成新的PCB等等。

如何理解所谓的将程序放入内存中?

加载!所谓的exec*系列的函数,本质就是如何加载程序的函数!属于系统调用。

2.怎么办?操作

man 3 exec

 我们关注到第一个用法

int execl(const char *path,const char *args,...);

找到程序:path:路径+目标文件名(你想要替换哪一个程序)

上面的...:是可变参数列表,这个参数可以传入多个不定个数参数!最后一个参数,必须是NULL,表示参数传递结束。

我们在命令行上怎么执行的,我们这里的参数就一个一个怎么填写!

a.基本演示1.不创建子进程2.创建子进程 [只是用最简单的exec函数]--见见猪猪跑

1.不创建子进程

测试代码

我们这里不妨用ls程序来替换掉我们下面运行的程序

#include <iostream>
#include <vector>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>
int main()
{
    printf("当前进程的开始代码\n");
    execl("/user/bin/ls","ls",NULL);
    printf("当前进程的结束代码\n");
}

#include <vector>
#include <stdio.h>
#include <unistd.h>


int main()
{
    printf("当前进程的开始代码!\n");

    execl("/usr/bin/ls", "ls", "-l", "-a", "-i", NULL);
    printf("当前进程的结束代码!\n");
    return 0;
}

 ​​​​​​​

#include <vector>
#include <stdio.h>
#include <unistd.h>


int main()
{
    printf("当前进程的开始代码!\n");
    execl("/usr/bin/top", "top", NULL);
    printf("当前进程的结束代码!\n");
    return 0;
}

#include <vector>
#include <stdio.h>
#include <unistd.h>


int main()
{
    printf("当前进程的开始代码!\n");
    execl("/usr/bin/ls", "ls","--color=auto","-l",NULL);
    printf("当前进程的结束代码!\n");
    return 0;
}

 

为什么没有打印最后那一句话?

Execl是程序替换,调用该函数成功之后,就会将当前进程所有的代码和数据都进行替换!包括已经执行的和没有执行的!

所以,一旦调用成功,后序的所有的代码都不再会执行!

第一行的打印因为已经打印出来的,但是其实代码已经被换掉了

如果我们写一个根本就不存在命令呢?

#include <iostream>
#include <vector>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>
int main()
{
    printf("当前进程的开始代码\n");
//    execl("/user/bin/ls","ls",NULL);
//    execl("/user/bin/ls","ls","-l",NULL);
//    execl("/user/bin/ls","ls","-l","-a","-i",NULL);
//    execl("/user/bin/top",NULL);
//    execl("/user/bin/ls","ls","--color=auto","-l",NULL);
    execl("/user/bin/topppppppp",NULL);
    printf("当前进程的结束代码\n");
}

这里我们的topppppppp 函数根本就不存在,所以替换失败的话,就会继续执行原有的代码。也就是最后一句打印的代码也会被打印出来。

execl加载重新映射失败的时候就直接返回了-1,为什么调用成功就没有返回值呢?

因为调用成功了,就会将其后面的代码全部都替换掉了,根本就没有返回值。execl根本就不需要进行返回值判定!!

#include <iostream>
#include <vector>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>
int main()
{
    printf("当前进程的开始代码\n");
//    execl("/user/bin/ls","ls",NULL);
    execl("/user/bin/ls","ls","-l",NULL);
    exit(1);
//    execl("/user/bin/ls","ls","-l","-a","-i",NULL);
//    execl("/user/bin/top",NULL);
//    execl("/user/bin/ls","ls","--color=auto","-l",NULL);
//    execl("/user/bin/topppppppp",NULL);
    printf("当前进程的结束代码\n");
}
//查看退出码
echo $?

2.创建子进程 [只是用最简单的exec函数]

为什么我要创建子进程?? 如果不创建,那么我们替换的进程只能是父进程,如果创建了,替换的进程就是子进程,而不影响父进程(为什么?)

进程替换替换的是子进程的代码和数据,不影响父进程的代码和数据,因为进程具有独立性。

如果替换的代码是子进程,那么父进程就可以专注于创建子进程解析任务和派发任务。

父进程就像是一个包工头一样,拦活,子进程就是工人,负责具体的工作。


加载新程序之前,父子的数据和代码的关系?代码共享,数据写时拷贝。

当子进程 加载新程序的时候,不就是一种“写入”吗?代码要不要写时拷贝?要不要将父子的代码分离?

代码必须分离!!不然子进程替换了代码之后,就会影响父进程,这就不符合进程之间的独立性原则!所以需要进行写时拷贝。这样的话父子进程在代码和数据上就彻底分开了,虽然曾经并不冲突

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

int main()
{
//    为什么我要创建子进程??
//    如果不创建,那么我们替换的进程只能是父进程,如果创建了,替换的进程就是子进程,而不影响父进程(为什么?)
//    while(1)
//    {
        //下面是一段伪代码
        //1.显示一个提示行:root@localhost#
        //2.获取用户输入的字符串,fgets,scanf. ->ls -a -l
        //3.对字符串进行解析
        //4.下面的代码
        pid_t id=fork();
        if(id==0)
        {
            //子进程
            printf("子进程开始运行,pid:%d\n",getpid());
            sleep(3);
            execl("user/bin/ls","ls","-a","-l",NULL);

            exit(1);
        }
        else
        {

            //父进程
            printf("父进程开始运行,pid:%d\n",getpid());
            int status=0;
            pid_t id= waitpid(-1,&status,0);//阻塞等待,一定是子进程先运行完毕,然后父进程获取之后,才退出
            if(id>0)
            {
                //打印退出码
                printf("wait success,exit code: %d\n", WEXITSTATUS(status));
            }
        }
//    }
    return 0;
}

b.详细展开其他函数用法!

1.execl/execv

execl已经在上面演示过

//execl后面的l是list的意思
int execl(const char *path, const char *arg, ...);
//char *const argv[]是一个指针数组
int execv(const char *path, char *const argv[]);

这两个参数并没有本质区别,只是在传参的方式上有一点不同 

execv的v是vector的意思,表示参数用数组一个一个传入

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

#define NUM 16
int main()
{
        pid_t id=fork();
        if(id==0)
        {
            //子进程
            printf("子进程开始运行,pid:%d\n",getpid());
            sleep(3);
            char *const _argv[NUM]={
                    (char*)"ls",
                    (char*)"-a",
                    NULL
            };

            execv("/user/bin/ls",_argv);
            exit(1);
        }
        else
        {

            //父进程
            printf("父进程开始运行,pid:%d\n",getpid());
            int status=0;
            pid_t id= waitpid(-1,&status,0);//阻塞等待,一定是子进程先运行完毕,然后父进程获取之后,才退出
            if(id>0)
            {
                //打印退出码
                printf("wait success,exit code: %d\n", WEXITSTATUS(status));
            }
        }
    return 0;
}

2.execlp

int execlp(const char *file, const char *arg, ...);

如果想要执行一个程序,必须先找到程序!带路径,不带路径能找到程序吗??

答案是使用PATH,所以我们上面的execlp中的p就是说明这个函数会自己在环境变量PATH中进行查找,你不用告诉我命令在哪里。

execlp中的l代表着参数采用列表的形式

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

#define NUM 16
int main()
{

        pid_t id=fork();
        if(id==0)
        {
            //子进程
            printf("子进程开始运行,pid:%d\n",getpid());
            sleep(3);

            //最后一个参数必须要是NULL,代表传参结束
            //实际上是这些命令行参数一个一个传递给main函数
            execlp("ls","ls","-a","-l",NULL);
            exit(1);
        }
        else
        {

            //父进程
            printf("父进程开始运行,pid:%d\n",getpid());
            int status=0;
            pid_t id= waitpid(-1,&status,0);//阻塞等待,一定是子进程先运行完毕,然后父进程获取之后,才退出
            if(id>0)
            {
                //打印退出码
                printf("wait success,exit code: %d\n", WEXITSTATUS(status));
            }
        }
    return 0;
}

 ​​​​​​​

 

3.execvp 

int execvp(const char *file, char *const argv[]);

 execvp这里v代表的是参数要用vector的形式一个一个传入,p代表的是会从PATH路径中寻找,不用再传入全路径了

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

#define NUM 16
int main()
{
        pid_t id=fork();
        if(id==0)
        {
            //子进程
            printf("子进程开始运行,pid:%d\n",getpid());
            sleep(3);
            char *const _argv[NUM]={
                    (char*)"ls",
                    (char*)"-a",
                    NULL
            };

            //最后一个参数必须要是NULL,代表传参结束
            //实际上是这些命令行参数一个一个传递给main函数
            execvp("ls",_argv);
            exit(1);
        }
        else
        {

            //父进程
            printf("父进程开始运行,pid:%d\n",getpid());
            int status=0;
            pid_t id= waitpid(-1,&status,0);//阻塞等待,一定是子进程先运行完毕,然后父进程获取之后,才退出
            if(id>0)
            {
                //打印退出码
                printf("wait success,exit code: %d\n", WEXITSTATUS(status));
            }
        }
    return 0;
}

4.execle

int execle(const char *path, const char *arg,..., char * const envp[]);

这里的l代表的就是将参数一个一个传递进去,e代表的就是环境变量(这个参数会自己维护环境变量),这个函数没有带p,就说明这个函数在运行的时候需要带全路径 

上面的char * const envp[]这个参数是用来传入环境变量(可以用于向目标进程传递环境变量!)

1.如何执行其他我自己写的C、C++二进制程序

如何使用Makefile一次性创建两个可执行程序

Makefile在从上到下扫描的时候,会先查看我们的第一行的依赖关系,我们需要在Makefile的第一行添加.PHONY:all,(声明我们这个工程需要生成exec和mycmd两个不同的程序)然后第二行all:exec mycmd也就是添加依赖关系,生成all这个工程需要生成exec和mycmd这两个程序的生成,同时下面也要写上mycmd是如何编写的,最后再clean也就是清理的时候还要将我们的mycmd也一起清除掉。

下面是使用绝对路径的 

下面的这个是写在exec.cc中的代码

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

#define NUM 16
const char *myfile="/home/zhuyuan/test_2022_9_21/mycmd";
int main()
{
        pid_t id=fork();
        if(id==0)
        {
            //子进程
            printf("子进程开始运行,pid:%d\n",getpid());
            sleep(3);
            char *const _argv[NUM]={
                    (char*)"ls",
                    (char*)"-a",
                    (char*)"-l",
                    (char*)"-a",
                    NULL
            };
//这里我们调用我们上面指定路径下的mycmd程序,并且传入的参数为-a
//也就是预期会打印出hello a!
            execl(myfile,"mycmd","-a",NULL);
            exit(1);
        }
        else
        {

            //父进程
            printf("父进程开始运行,pid:%d\n",getpid());
            int status=0;
            pid_t id= waitpid(-1,&status,0);//阻塞等待,一定是子进程先运行完毕,然后父进程获取之后,才退出
            if(id>0)
            {
                //打印退出码
                printf("wait success,exit code: %d\n", WEXITSTATUS(status));
            }
        }
    return 0;
}

下面这个是写在mycmd.cc中的代码 

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(int argc,char *argv[])
{
//如果传入的参数个数并不是两个,就直接报错了
    if(argc !=2)
    {
        printf("can not execute\n");
        exit(1);
    }
//比较传入的字符串,如果是-a就打印hello a!
//下面的代码以此类推
    if(strcmp(argv[1],"-a")==0)
    {
        printf("hello a!\n");

    }
    else if((strcmp(argv[1],"-b")==0))
    {
        printf("hello b!\n");
    }
    else if(strcmp(argv[1],"-c")==0)
    {
        printf("hello a!\n");

    }
    else{
        printf("exit\n");
    }
    return 0;
}

我们看到我们成功调用了mycmd程序 

 下面是使用相对路径的,另外的那个mycmd.cc程序不变,就将exec.cc代码改成下面的

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

#define NUM 16
//使用相对路径来找到我们想要调用的程序
const char *myfile="./mycmd";
int main()
{
        pid_t id=fork();
        if(id==0)
        {
            //子进程
            printf("子进程开始运行,pid:%d\n",getpid());
            sleep(3);
            char *const _argv[NUM]={
                    (char*)"ls",
                    (char*)"-a",
                    (char*)"-l",
                    (char*)"-a",
                    NULL
            };

            execl(myfile,"mycmd","-a",NULL);
            exit(1);
        }
        else
        {

            //父进程
            printf("父进程开始运行,pid:%d\n",getpid());
            int status=0;
            pid_t id= waitpid(-1,&status,0);//阻塞等待,一定是子进程先运行完毕,然后父进程获取之后,才退出
            if(id>0)
            {
                //打印退出码
                printf("wait success,exit code: %d\n", WEXITSTATUS(status));
            }
        }
    return 0;
}

 

2.如何执行其他语言的程序

创建一个Python文件

 写入Python测试代码

 ​​​​​​​

  运行Python文件

创建一个bash文件

 写入shell代码

运行shell程序

 实际上C++是纯正的编译型语言,Python、shell、Java都是有对应的解释器的。解释器也就是一个程序,执行的时候就直接用解释器执行脚本就可以了,不用进行编译脚本中的内容都是作为参数的形式给我们的Python或者是bash来读取到,然后再自己的程序内部去执行。也就是解释对应的脚本,然后取执行对应的功能

调用我们的Python程序,我们的Python文件就是上面编写的打印hello Python的文件

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#define NUM 16
int main()
{
        pid_t id=fork();
        if(id==0)
        {
            //子进程
            printf("子进程开始运行,pid:%d\n",getpid());
            sleep(3);
            char *const _argv[NUM]={
                    (char*)"ls",
                    (char*)"-a",
                    (char*)"-l",
                    (char*)"-a",
                    NULL
            };
//由于我们调用的execlp,所以会自动从系统路径中查找对应的程序
//所以第一个参数就传入我们要查找的程序,第二个参数选择你想怎么执行,第三个参数是你想要执行的脚本,
//最后一个参数亿NULL结尾
            execlp("python","python","test.py",NULL);
            exit(1);
        }
        else
        {

            //父进程
            printf("父进程开始运行,pid:%d\n",getpid());
            int status=0;
            pid_t id= waitpid(-1,&status,0);//阻塞等待,一定是子进程先运行完毕,然后父进程获取之后,才退出
            if(id>0)
            {
                //打印退出码
                printf("wait success,exit code: %d\n", WEXITSTATUS(status));
            }
        }
    return 0;
}


调用bash文件


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

#define NUM 16
const char *myfile="./";
int main()
{
        pid_t id=fork();
        if(id==0)
        {
            //子进程
            printf("子进程开始运行,pid:%d\n",getpid());
            sleep(3);
            char *const _argv[NUM]={
                    (char*)"ls",
                    (char*)"-a",
                    (char*)"-l",
                    (char*)"-a",
                    NULL
            };
            execlp("bash","bash","test.sh",NULL);
            exit(1);
        }
        else
        {

            //父进程
            printf("父进程开始运行,pid:%d\n",getpid());
            int status=0;
            pid_t id= waitpid(-1,&status,0);//阻塞等待,一定是子进程先运行完毕,然后父进程获取之后,才退出
            if(id>0)
            {
                //打印退出码
                printf("wait success,exit code: %d\n", WEXITSTATUS(status));
            }
        }
    return 0;
}

 想要运行Python的话,还可以给Python给予可执行权限,然后./test.py

chmod +x test.py

这样可以直接./来运行我们的Python脚本,因为在系统层面上,它会自动寻找Python的解释器,然后执行Python的脚本 

 也就是等价于我们下面这样编写我们的代码


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

#define NUM 16
const char *myfile="./";
int main()
{
        pid_t id=fork();
        if(id==0)
        {
            //子进程
            printf("子进程开始运行,pid:%d\n",getpid());
            sleep(3);
            char *const _argv[NUM]={
                    (char*)"ls",
                    (char*)"-a",
                    (char*)"-l",
                    (char*)"-a",
                    NULL
            };
            execlp("./test.py","test.py",NULL);
            exit(1);
        }
        else
        {

            //父进程
            printf("父进程开始运行,pid:%d\n",getpid());
            int status=0;
            pid_t id= waitpid(-1,&status,0);//阻塞等待,一定是子进程先运行完毕,然后父进程获取之后,才退出
            if(id>0)
            {
                //打印退出码
                printf("wait success,exit code: %d\n", WEXITSTATUS(status));
            }
        }
    return 0;
}

所以我们的exec*: 功能其实就是加载器的底层接口,理论上可以执行任何程序

如何给我们的程序传入环境变量 

因为我们没有传环境变量,所以这个环境变量获取到的就是null

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

#define NUM 16
const char *myfile="./mycmd";
int main()
{
        pid_t id=fork();
        if(id==0)
        {
            //子进程
            printf("子进程开始运行,pid:%d\n",getpid());
            sleep(3);
            char *const _argv[NUM]={
                    (char*)"ls",
                    (char*)"-a",
                    (char*)"-l",
                    (char*)"-a",
                    NULL
            };
//调用上面的设定好的路径来调用我们的mycmd程序
            execl(myfile,"mycmd","-a",NULL,);
            exit(1);
        }
        else
        {

            //父进程
            printf("父进程开始运行,pid:%d\n",getpid());
            int status=0;
            pid_t id= waitpid(-1,&status,0);//阻塞等待,一定是子进程先运行完毕,然后父进程获取之后,才退出
            if(id>0)
            {
                //打印退出码
                printf("wait success,exit code: %d\n", WEXITSTATUS(status));
            }
        }
    return 0;
}

对mycmd.cc代码的修改

// ./mycmd -a/-b/-c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(int argc,char *argv[])
{
    if(argc !=2)
    {
        printf("can not execute\n");
        exit(1);
    }

    //hello这个环境变量并不存在
    printf("获取环境变量:%s\n",getenv("hello"));
    if(strcmp(argv[1],"-a")==0)
    {
        printf("hello a!\n");

    }
    else if((strcmp(argv[1],"-b")==0))
    {
        printf("hello b!\n");
    }
    else if(strcmp(argv[1],"-c")==0)
    {
        printf("hello a!\n");

    }
    else{
        printf("exit\n");
    }
    return 0;
}

 ​​​​​​​

环境变量的传递

下面我们在我们的exec.cc中传入环境变量,然后再子程序中打印环境变量

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

#define NUM 16
const char *myfile="./mycmd";
int main()
{
        char *_env[NUM]={
                (char*)"hello=8877666",
                NULL
        };
        pid_t id=fork();
        if(id==0)
        {
            //子进程
            printf("子进程开始运行,pid:%d\n",getpid());
            sleep(3);
            char *const _argv[NUM]={
                    (char*)"ls",
                    (char*)"-a",
                    (char*)"-l",
                    (char*)"-a",
                    NULL
            };
//运行我们的myfile路径下的mycmd程序,传入参数-a,并且传入环境变量_env
            execle(myfile,"mycmd","-a",NULL,_env);
            exit(1);
        }
        else
        {

            //父进程
            printf("父进程开始运行,pid:%d\n",getpid());
            int status=0;
            pid_t id= waitpid(-1,&status,0);//阻塞等待,一定是子进程先运行完毕,然后父进程获取之后,才退出
            if(id>0)
            {
                //打印退出码
                printf("wait success,exit code: %d\n", WEXITSTATUS(status));
            }
        }
//    }
    return 0;
}


mycmd文件并没有被修改,还是上面那一个程序 

// ./mycmd -a/-b/-c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(int argc,char *argv[])
{
    if(argc !=2)
    {
        printf("can not execute\n");
        exit(1);
    }
    //hello这个环境变量并不存在
    printf("获取环境变量:%s\n",getenv("hello"));
    if(strcmp(argv[1],"-a")==0)
    {
        printf("hello a!\n");

    }
    else if((strcmp(argv[1],"-b")==0))
    {
        printf("hello b!\n");
    }
    else if(strcmp(argv[1],"-c")==0)
    {
        printf("hello a!\n");

    }
    else{
        printf("exit\n");
    }
    return 0;
}

 

 环境变量具有全局属性,可以被子进程继承下去

5.execvpe 

int execvpe(const char *file, char *const argv[],char *const envp[]);

这个接口参考上面其它接口的用法

man execve 

但是上面的集合函数其实都不是我们的系统直接提供给我们的

只有上面这个execve才是系统提供给我们的,(你必须要传入全路径(没有带P),参数需要一个一个地传入。)

只有这个才是真正的系统调用,其余的几个底层都是调用这个系统提供给我们的接口,都是系统提供的基本封装,都是为了满足上层不同的调用场景

(下面的六个全部都是系统提供的封装(C语言提供的封装))

 

编写一个简单的shell

https://blog.csdn.net/weixin_62684026/article/details/126993865?csdn_share_tail=%7B%22type%22%3A%22blog%22%2C%22rType%22%3A%22article%22%2C%22rId%22%3A%22126993865%22%2C%22source%22%3A%22weixin_62684026%22%7D

命名理解

这些函数原型看起来很容易混,但只要掌握了规律就很好记。
l(list) : 表示参数采用列表
v(vector) : 参数用数组
p(path) : 有p自动搜索环境变量PATH
e(env) : 表示自己维护环境变量

3.为什么?总结

为什么要替换呢?

一定和应用场景有关,我们有时候必须让子进程执行新的程序。比方说我们上面写的shell的简单代码。


网站公告

今日签到

点亮在社区的每一天
去签到