【Linux】进程控制

2022/3/1 7:22:07

本文主要是介绍【Linux】进程控制,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

目录

  • 1. 进程创建
    • fork()函数初识
    • 写实拷贝
    • fork()常规用法
  • 2. 进程终止
    • 2.1 进程退出的场景
    • 2.2 进程常见退出方法
  • 3. 进程等待
    • 3.1 进程等待必要性
    • 3.2 进程等待的方法
      • 3.2.1 wait
      • 3.2.2 waitpid
    • 3.3 获取子进程status
    • 3.4 阻塞 与 非阻塞
  • 4. 进程替换
    • 4.1 进程替换概念
    • 4.2 替换函数
      • 4.2.1 execl
      • 4.2.2 execv
      • 4.2.3 execlp
      • 4.2.4 execvp
      • 4.2.5 execvpe
      • 4.2.6 execve
      • 4.2.7 规律总结
    • 4.3 为什么要程序替换
  • 5. shell 的模拟实现

1. 进程创建

fork()函数初识

这里之前讲过,不再赘述:
【Linux】进程入门详解## fork()函数返回值

写实拷贝

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

在这里插入图片描述
为什么不拷贝 只读数据?

所有的数据,并不是父和子都会写入,如果是只读的数据,写时拷贝不会拷贝,避免内存与系统资源的浪费

fork的时候,创建子进程的数据结构,如果还要将只读数据拷贝一份,会导致fork的效率降低。而且fork()本身就是把向系统要更多的资源,要的资源越多,fork就越容易失败。

fork()常规用法

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

2. 进程终止

2.1 进程退出的场景

  1. 代码跑完,结果正确,退出码为0
  2. 代码跑完,结果不正确,逻辑问题,但是没有导致程序崩溃,退出码 非0
  3. 代码没跑完,程序崩溃。退出码此时无意义。

2.2 进程常见退出方法

  • 正常终止(可以通过 echo $? 查看进程退出码):
  1. 从main返回
  2. 调用exit
  3. _exit
  • 异常退出
    ctrl + c,信号终止

3. 进程等待

3.1 进程等待必要性

  1. 回收僵尸进程,解决内存泄漏
  2. 需要获取进程的运行结束状态
  3. 尽量父进程要晚于子进程退出,可以规范化进行资源回收

3.2 进程等待的方法

系统提供了两个系统接口来供用户使用。


3.2.1 wait

函数声明

//需要包含的头文件
#include<sys/types.h>
#include<sys/wait.h>
//函数声明
pid_t wait(int*status);
  • 返回值:
    成功返回被等待进程pid,失败返回-1。

  • 参数:
    输出型参数,获取子进程退出状态,不关心则可以设置成为NULL。

我们写一个例子来验证一下wait函数:
在这里插入图片描述
在这里插入图片描述

同时我们使用监视脚本来对父与子进程状态进行跟踪:

//监控脚本,每一秒刷新一次进程状态
while : ; do echo "######################";ps ajx | grep proc | grep -v grep;
 echo "########################";sleep 1;done

我们看一下该程序运行时的进程转态:


在这里插入图片描述

很容易发现,在5->10秒,子进程是僵尸状态,在10秒开始时,父进程苏醒,wait函数回收子进程,所以在10到13 秒,只有父进程在运行。

3.2.2 waitpid

相比较于 wait ,waitpid像是它的plus 版本,给用户提供更加个性化的选择:

函数声明:

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

所以,对于之前的例子,我们是可以用waitpid 来代替wait的。

在这里插入图片描述

3.3 获取子进程status

wait 与 waitpid 都有一个status 参数。
该参数是一个输出型参数,由操作系统填充

已知在32位操作系统下, status 是一个整形数字,有32个bit位。

我们画一个示意图:
在这里插入图片描述
其中,次第八位 存储的是 子进程退出时的退出码,exit(n)

我们可以来验证一下:
在这里插入图片描述

运行结果:
在这里插入图片描述


这里有几个问题:

  1. 是否可以通过设置全局变量,告知父进程的退出码?
    绝对不行,写时拷贝

  2. 我们通过waitpid 拿到的status 的值,是从哪里得到的,子进程已经结束了啊?

子进程时僵尸状态,子进程的数据结构没有消失,task_struct 会被填入其退出码,所以waitpid从子进程task_struct中拿退出码。


如果进程异常终止了,那么 在 status 的最低七位存储 终止信号,而空出的那第八位,存储core dump 标志

在这里插入图片描述
所以我们在检测的时候,要先看最低七位是不是0-,如果是0,那么说明正常终止,此时我们再去查看其退出码,即次第八位。 如果非0,那么就说明 被异常终止。
在这里插入图片描述

同样,我们也可来测试一下这种情况;

我们通过写一个野指针的解引用来引发异常。
在这里插入图片描述
运行结果:
在这里插入图片描述


这里我们还有一个core dump 标志没有讲解,这是因为 这一块内容比较多,之后再介绍。

我们编写一个完整的判断逻辑:
在这里插入图片描述


但是有一个问题,我们每次想取得退出码和错误码都要按位与吗?并不是,系统提供了一堆的宏可以使用。

这里我们只介绍两个;

status:
WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)

由此 ,代码可以简化为:
在这里插入图片描述


3.4 阻塞 与 非阻塞

我们目前调用的函数,全部都是阻塞函数。调用->执行->返回->结束 。执行时 调用方一直在等待,没有做其他事情。(但执行流)

非阻塞轮询方案,更加高效。(如下图)
在这里插入图片描述
这种方案显然会有三种返回情况:

  1. 失败:下次再检测
  2. 成功:已经返回
  3. 失败:真正失败

这就对对应了waitpid 的第三个参数options:

options:

  • WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。

也就是说,如果我们设置WNOHANG参数,那么就会对进程采用非阻塞轮询方案。

在这里插入图片描述

  • 到底如何理解 进程的“等”?
    将当前的进程放入等待队列,并将进程状态设置为非R状态。
    当条件允许,OS就会唤醒进程,将其由等待队列转到运行队列,转为R状态。


4. 进程替换

4.1 进程替换概念

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

在这里插入图片描述

4.2 替换函数

Linux 提供了六组系统接口:

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 execve(const char *path, char *const argv[], char *const envp[]);

我们循序渐进,先介绍第一个函数:

4.2.1 execl

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

其中:

  • path: 你要执行那个程序
  • arg: 你要执行的命令
  • 可变参数列表,命令行怎么执行,传入什么选项,你就可以在这里直接按照顺序填写参数

我们直接实验一下:

在这里插入图片描述

在这里插入图片描述
这段代码等效于命令:

ls -a -l -i

但是,细心的同学发现,我们的最后一条语句没有打印出来。这是因为代码被替换为ls了。执行完ls后不会再回源程序了。

也就是说,exec函数,不需要考虑返回值,只要返回,一定是这个函数调用失败了。


当然,我们也可以通过父进程来创建子进程来执行程序替换,此时的程序替换是不会影响父进程的。
在这里插入图片描述
运行结果:
在这里插入图片描述

4.2.2 execv

这个函数与execl 基本相同,l 代表 list ,v代表 vector,也就说,只是传参的方式不同,如下图:
在这里插入图片描述

4.2.3 execlp

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

比较二者,多的p 表示path,指有p自动搜索环境变量PATH.
在这里插入图片描述

4.2.4 execvp

这个函数很显然了,不再赘述

4.2.5 execvpe

函数声明:

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

其中多的e 是env ,即环境变量。

我们可以传入默认或者自定义的环境变量给目标的可执行程序。
在这里插入图片描述

在这里插入图片描述

4.2.6 execve

实际上,只有execve是真正的系统调用,其它五个函数最终都调用 execve

那么 execve 也类似:
在这里插入图片描述


除了传自定义的本地变量,我们还可以传环境变量:

在这里插入图片描述
但是显然 ,此时我们是找不到MYENV的,因为它还没有写入环境变量:
在这里插入图片描述
写入后:

在这里插入图片描述

4.2.7 规律总结

这些函数原型看起来很容易混,但只要掌握了规律就很好记。

  • l(list) : 表示参数采用列表
  • v(vector) : 参数用数组
  • p(path) : 有p自动搜索环境变量PATH
  • e(env) : 表示自己维护环境变量
    在这里插入图片描述

4.3 为什么要程序替换

程序替换通常有两种应用场景:

  1. 子进程 执行父进程的一部分代码
  2. 子进程自身新的程序的需求

5. shell 的模拟实现

进程的基本内容基本掌握后,我们现在已经有能力模拟实现一下我们的命令行编辑器shell了。
在这里插入图片描述
这里我们要注意 cd 是内置命令,要单独处理。



这篇关于【Linux】进程控制的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程