Linux【进程控制】总结

学习目标

首先进程控制分为四大部分:进程创建、进程退出、进程等待、进程替换;

        第一步:学习如何来创建一个进程,一般我们会使用fork函数来创建子进程,创建子进程之后,就要去探索子进程与父进程的相关联系;

        第二步:学习如何让一个进程退出,需要认识并熟练使用exit、_exit、return函数来完成进程退出,了解进程退出码,进程退出码的组成,如何获取进程退出码;

        第三步:学习如何等待一个进程和进程等待的相关函数,通过函数来获取进程退出码;

        第四步:学习如何进行进程替换,了解并熟练使用进程替换的接口exec*函数

一、进程创建

进程创建必备函数:fork函数的介绍:

在已知进程中创建一个新的进程,已知进程为父进程,新进程为子进程

fork函数声明:

pid_t fork(void);

头文件:

#include <unistd.h>

返回值:

返回值为0,表示为子进程
返回的为子进程的pid,但是此时是父进程
返回值为-1,创建子进程失败

说明:

  • fork之后,子进程与父进程共享代码,如果某一进程要修改某一段代码,会发生写时拷贝
  • 通常使用  if-else  语句让父子进程执行不同的代码
  • fork通常会让子进程去执行别的程序,也就是程序替换

提问:

1. 为什么fork之后,对代码修改会发生写时拷贝?

        首先fork之后,父子进程共享父进程的代码,当这段 被共享的数据 需要修改的时候,为了保证进程的独立性,操作系统会介入其中,发生写时拷贝,然后去修改拷贝后的数据,保证父子进程互不影响。(谁被修改,就把谁写时拷贝,其他的不变)

2. 为什么同一个变量可以有不同的值?

        (1) 首先Linux支持同一个变量名表示不同的内存,因为变量最终都是存放在内存中的,而变量名是给人看的,计算机只看二进制,所有即使同一个变量名,但是内存空间的地址不同,就可以区分;

        (2) 这里也发生了写时拷贝,上面的图是一个简单版本,下面有一个复杂版本;

        (3) 页表、虚拟空间地址都相同,但是映射过去的物理地址空间不同;

二、进程退出

       进程退出,我们就会想到之前讲过的僵尸进程,先复习一下僵尸进程,当子进程退出时,子进程会把大部分资源(代码段、数据段、页表、地址空间等)还给操作系统,唯独保留下该进程的PCB,保留PCB的原因是,PCB中记录了一个进程退出时的退出码,退出码会反馈进程退出的原因(执行完结果正确、执行完结果错、没执行完,异常退出)。

        而退出码是会被父进程读取的,这也是必须要读取的。若没有父进程读取子进程的退出码,那该子进程就会变成僵尸进程。

        而读取进程退出码的原因是,我们必须知道子进程是因为什么退出!

说到这里,我们会清楚两点:

        1. 进程退出时,必须提供进程的退出码,以供父进程读取

        2. 父进程读取子进程的退出码的方式为:进程等待

所以,我们学习进程退出,要学会下面三个方面:

        1. 进程退出的场景

        2. 进程退出的方法

        2. 进程退出码的解读

1. 进程退出的场景

        这里我们应该深有体会,有时候进程在执行一部分代码的时候就直接终止,比如野指针的使用、除0操作;又或者执行结束,但是结果不正确;最常见的就是执行结束,也得到了想要的结果。所以我们将进程退出分为以下三个场景:

1. 程序执行结束,结果正确
2. 程序执行结束,结果不正确
3. 程序没执行结束,异常终止

2. 进程退出的方法

        我们在学习C/C++时,通常会在main函数的末尾写上一个return 0,这表示main函数返回值为0,main函数返回成功。其实这就是我们程序的退出方法之一。

        下面列举了进程退出的方法汇总和逐方法讲解:

return num

必须在主函数中使用

在其他函数体使用,仅仅代表函数的结束

exit(num)

可以在任意地方调用,代表直接退出进程

支持刷新缓冲区

_exit(num)同上,但不会刷新缓冲区

ctrl + c 

kill -num pid

在命令行中使用信号杀死进程,属于异常终止

注意:

  • exit 和 _exit 可以在任意地方调用,都是用来退出进程的;
  • return 必须在主函数中使用,在其他函数体使用,仅仅代表函数的结束
  • exit 支持刷新缓冲区,_exit 不支持
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
//    printf("exit()会刷新缓冲区");
    printf("_exit()不会刷新缓冲区");
    _exit(0);
//    exit(0);
}
         这里的结果显而易见,_exit不会刷新缓冲区,也就是根本不会打印任何东西;反之exit则会打印出来;但是在每个printf语句里末尾都加一个 '\n' ,他们都会打印出来
        这是因为 ' \n '是有刷新缓冲区的作用
  •  exit在底层封装了_exit,比_exit多了个刷新缓冲区

思考:这个缓冲区是操作系统内部的吗?

        首先该缓冲区一定不是操作系统内部的,因为_exit 是一个系统调用接口,_exit被调用的时候,释放了资源,但是并没有刷新缓冲区,而exit函数封装之后才会刷新缓冲区,所以这个缓冲区一定不是操作系统的缓冲区,而是用户层的。

3. 进程退出码

       进程退出码:记录进程退出时的情况,正常或异常,正确或错误。

进程退出码的组成:

                                        进程退出码 = 退出码 + 核心转储标志 + 异常码

注意:

  • 进程退出码以位图的方式呈现,我们只关心低16位
  • 异常码为0到6比特位,表示进程出异常收到的异常编号
  • 退出码为8到15比特位,表示进程正常退出的退出码
  • 异常码 = 0,表示进程执行过程中无异常,看退出码
  • 异常码 ≠ 0,表示进程执行出异常,退出码无意义

获取进程退出码的方法:

指令:echo $?
进程等待函数wait、waitpid的输出型参数status

三、进程等待

1. 为什么要进行进程等待?

        1. 子进程退出时,父进程如果不进行进程等待,子进程会变成僵尸进程,造成内存泄漏

        2. 进程一旦变成僵尸进程,无法使用kill命令来杀死

        3. 子进程退出时,会将其退出情况存放到进程退出码中,这个退出码就放在PCB里,所以父进程进行进程等待,也可以获取子进程的退出情况

2. 进程等待必要性

回收子进程资源(必做),获取子进程退出码(选做)

3. 进程等待的方法

(1)wait函数

函数声明:

pid_t wait(int* status)

头文件:

#include<sys/types.h>
#include<sys/wait.h>

返回值:

成功:返回被等待进程的 pid ;

失败:返回  -1;

status:

输出型参数,获取子进程退出码,不获取则可以设置成为NULL

示例:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
    int id = fork(); //fork创建子进程
    if(id == 0) //子进程
    {
        printf("I am a child , pid = %d\n", getpid());
        exit(1);
    }
    int status = 0;
    if(wait(&status) == id) //等待成功,返回被等待进程pid
        printf("status = %d\n", status);
    else //失败,返回-1
        printf("wait error\n");
    return 0;
}

(2)waitpid函数

函数声明:

pid_ t waitpid(pid_t pid, int* status, int options)

头文件:

#include <sys/types.h>
#include <sys/wait.h>

返回值:

正常返回:waitpid返回等待到的子进程pid;
设置选项WNOHANG:而调用中waitpid发现没有已退出的子进程可收集,则返回0;
调用中出错:则返回-1

status:

输出型参数,依旧是获取子进程的退出码,不获取可设置为NULL。

查看进程是否是正常退出:

WIFEXITED(status)

若为正常终止子进程返回的状态,则为真

异常退出为假,可查看进程异常退出信号

查看进程的退出码:

WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。

options:

当options传入 WNOHANG 时 (一个宏,值为1)

代表当pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。

若正常结束,则返回该子进程pid

options传入的整型值,不是1,就会一直等待子进程的退出,父进程会阻塞等待;

为WNOHANG,则不会等待,父进程会执行后面的代码,是非阻塞等待

示例1:使用WIFEXITED和WEXITSTATUS获取进程退出情况

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
    int id = fork(); //fork创建子进程
    if(id == 0) //子进程
    {
        printf("I am a child, pid = %d\n", getpid());
        exit(11); //子进程退出
    }
    int status = 0;
    waitpid(id, &status, 0);
    if(WIFEXITED(status))  //当进程正常退出,返回真
        printf("exit code = %d\n", WEXITSTATUS(status)); //打印进程的退出码
    else   //进程异常退出
        printf("exit signal = %d\n", WIFEXITED(status)); //打印进程的异常退出信号
    return 0;
}

示例2: 阻塞等待

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
    int id = fork(); //fork创建子进程

    if(id == 0) //子进程
    {
        int count = 5;
        while(count--)
        {
            printf("I am a child, pid = %d\n", getpid());
            sleep(1);
        }
        exit(11); //子进程退出
    }
    int status = 0;
    while(1)
    {
        int rid = waitpid(id, &status, 0);
        if(rid == 0)
            printf("child is running\n");
        else
        {
            printf("child exit success\n");
            return 0;
        }
        sleep(1);
    }
}

示例3:非阻塞等待

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
    int id = fork(); //fork创建子进程

    if(id == 0) //子进程
    {
        int count = 5;
        while(count--)
        {
            printf("I am a child, pid = %d\n", getpid());
            sleep(1);
        }
        exit(11); //子进程退出
    }
    int status = 0;
    while(1)
    {
        int rid = waitpid(id, &status, WNOHANG);
        if(rid == 0)
            printf("child is running\n");
        else
        {
            printf("child exit success\n");
            return 0;
        }
        sleep(1);
    }
}

四、进程替换

        进程替换就是将现在正在执行的进程换去执行另一个程序的代码

1. 进程替换的函数

(1)库函数

#include <unistd.h>
int execl(const char* path, const char* arg, ...);
int execlp(const char* file, const char* arg, ...);
int execle(const char* path, const char* arg, ...,char* const envp[]);
int execv(const char* path, char* const argv[]);
int execvp(const char* file, char* const argv[]);
int execvpe(const char* file, char* const argv[]s, char* const envp[])

(2)系统调用函数

#include <unistd.h>
int execve(const char *path, char *const argv[], char *const envp[]);


名词解释:

path        传程序的绝对路径
file传程序的文件名
arg

可变参数列表,依次传使用规则传入字符串

但结尾必须为NULL

argv

将上面的可变参数放在指针数组里,传数组名

数组最后一个元素必须为NULL

envp

将自己写的环境变量放在指针数组里,传数组名

数组最后一个元素必须为NULL

函数返回值:

这些函数如果调用成功,则替换后的程序从启动代码开始执行,不再返回;
如果调用出错则返回  -1;
所以exec函数只有出错的返回值而没有成功的返回值

快速记忆exec*函数的命名:

l -> list : 表示参数采用列表
v -> vector : 参数用数组
p -> path : 有p自动搜索环境变量PATH,只需要传程序名,自动搜索路径
e -> env : 表示需要自己传环境变量

2. 进程替换后的注意事项

  • 进程替换不会产生新的进程,进程pid不变
  • 进程成功替换后,不会执行exec*函数后面的代码,因为被替换掉了
  • exec*函数只有失败返回值,没有成功返回值
  • 不论什么语言,只要是一个可执行程序,我们就可以替换
  • 进程替换,只是把被替换的程序的代码段和数据段覆盖到替换之前到程序上其他内核数据结构(PCB、页表、虚拟地址空间)不变,但是进程地址空间和页表会重新初始化,这也就解释了为什么没有产生新进程这里需要注意的是如果我们设置了允许使用的系统资源的函数,或许会导致进程替换不完整,比如设置了允许使用的地址空间的大小,则会导致新程序的库加载不完全,代码加载不完整等问题

3. exec函数示例

#include <unistd.h>
int main()
{
    char *const argv[] = {"ls", "-l", NULL};
    char *const envp[] = {"PATH=/usr/bin", NULL};

//    execl("/usr/bin/ls", "ls", "-l", NULL);

//    execlp("ls", "ls", "-l", NULL);

//    execle("ls", "ls", "-l", NULL, envp);

//    execv("/usr/bin/ls", argv);

//    execvp("ls", argv);

//    execve("/usr/bin/ls", argv, envp);

    return 0;
}

4. 进程替换的应用场景

通常会创建子进程,让子进程进行进程替换,利用该特性,我们可以实现一个自己的shell,通过main函数的可变参数列表来完成。

  • 27
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

终将向阳而生

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值