Linux系统编程之进程控制(进程创建、终止、等待及替换)

进程创建

在上一节讲解进程概念时,我们提到fork函数是从已经存在的进程中创建一个新进程。那么,系统是如何创建一个新进程的呢?这就需要我们更深入的剖析fork函数。

1.1 fork函数的返回值

调用fork创建进程时,原进程为父进程,新进程为子进程。运行 man fork后,我们可以看到如下信息:

#include <unistd.h>
pid_t fork(void);
</unistd.h>

fork函数有两个返回值,子进程中返回0,父进程返回子进程pid,如果创建失败则返回-1。

实际上,当我们调用fork后,系统内核将会做:

  • 分配新的内存块和内核数据结构(如task_struct)给子进程
  • 将父进程的部分数据结构内容拷贝至子进程
  • 添加子进程到系统进程列表中
  • fork返回,开始调度

Linux系统编程之进程控制(进程创建、终止、等待及替换)

1.2 写时拷贝

在创建进程的过程中,默认情况下,父子进程共享代码,但是数据是各自私有一份的。如果父子只需要对数据进行读取,那么大多数的数据是不需要私有的。这里有三点需要注意:

第一,为什么子进程也会从fork之后开始执行?

因为父子进程是共享代码的,在给子进程创建PCB时,子进程PCB中的大多数数据是父进程的拷贝,这里面就包括了程序计数器(PC)。由于PC中的数据是即将执行的下一条指令的地址,所以当fork返回之后,子进程会和父进程一样,都执行fork之后的代码。

第二,创建进程时,子进程需要拷贝父进程所有的数据吗?

父进程的数据有很多,但并不是所有的数据都要立马使用,因此并不是所有的数据都进行拷贝。一般情况下,只有当父进程或者子进程对某些数据进行写操作时,操作系统才会从内存中申请内存块,将新的数据拷写入申请的内存块中,并且更改页表对应的页表项,这就是 写时拷贝。原理如下图所示:

Linux系统编程之进程控制(进程创建、终止、等待及替换)

第三,为什么数据要各自私有?

这是因为进程具有独立性,每个进程的运行不能干扰彼此。

1.3 fork函数的用法及其调用失败的原因

fork函数的用法:

  • 一个父进程希望复制自己,通过条件判断,使父子进程分流同时执行不同的代码段。例如,父进程等待客户端请求,生成子 进程来处理请求。
  • 如子进程从fork返回后,调用进程替换的函数,如exec等(将会在本节4.程序替换中讲解)。

fork函数调用失败的原因:

  • 系统中进程太多
  • 实际用户的进程数超过了限制

2.进程终止

2.1 进程终止的原因

进程终止的原因有三种

  • 代码运行完毕,结果正确
  • 代码运行完毕,结果不正确
  • 代码异常终止

2.2 常见的进程退出方法

进程正常终止

1.从main函数return,这是最常见的进程退出方法。在函数设计中,0代表正确,非0代表错误。其中不同的非0的退出码对应了退出原因。

2.调用exit或者_exit

_exit函数是系统调用,执行 man _exit可以看到

#include <unistd.h>
void _exit(int status);
</unistd.h>

status 定义了进程的终止状态。父进程可以通过wait来获得子进程的status(会在3.进程等待中讲解)。

需要注意的是,

exit函数是库函数,虽然status是int,但是仅有低8位可以被父进程所用。所以_exit(-1)时,在终端执行echo $?发现返回值 是255。

#include <stdlib.h>
void exit(int status);
</stdlib.h>

从作用上来看,_exit和exit是相似的,exit是对_exit的封装,exit的执行实际上是通过调用_exit来实现的。

但是二者也有一些细微的差别,请看如下代码段:

代码1

int main()
{
 printf("Hello world");
 exit(0);
}

Linux系统编程之进程控制(进程创建、终止、等待及替换)

代码2

 #include<stdio.h>
 #include<unistd.h>
 int main()
 {
   printf("Hello world");
    _exit(0);                                                  }
</unistd.h></stdio.h>

Linux系统编程之进程控制(进程创建、终止、等待及替换)

相比于_exit函数,exit函数先要执行用户定义的清理函数,在 冲刷缓冲区,关闭所有打开的流,将所有的缓存数据写入文件后,再调用_exit。因此我们可以看到,执行exit输出了”hello World”,而执行_exit并没有输出。

那么,return和exit有什么区别呢?

在普通函数中,return是用来终止函数的,只有在main函数中才是终止进程,而exit无论在哪里,一旦调用,整个进程就会终止。

3.进程等待

3.1 为什么要有进程等待?

在讲进程概念时我们提到,当子进程退出,父进程如果不管不顾,子进程残留资源(PCB)存放于内核中,就可能会造成僵尸进程。如果该资源不能得到释放,就会导致内存泄漏。僵尸进程是不能使用 kill -9 命令清除掉的。因为 kill 命令只是用来终止进程的, 而僵尸进程已经终止。

同时,父进程派给子进程的任务完成的如何,我们是需要知道的。例如,子进程运行完成,结果对还是不对, 或者是否正常退出。

因此,就需要父进程通过进程等待的方式,回收子进程的资源。

3.2 进程等待的方法

一个进程在终止时会关闭所有文件,释放在用户空间分配的内存,但它的 PCB 还保留着,内核在其中保存了一些信息:如果是 正常终止则保存着退出状态,如果是 异常终止则保存着导致该进程终止的信号是哪个。当这个进程的父进程调用 wait 或 waitpid 获取这些信息后,才会将这个进程彻底清除掉。

一个进程的退出状态可以在 Shell 中通过运行 echo $?查看,因为 Shell 是它的父进程,当它终止时 Shell 调用 wait 或 waitpid 得到它的退出 状态同时彻底清除掉这个进程。

3.2.1 wait函数

#include<sys types.h>
#include<sys wait.h>
pid_t wait(int*status);
</sys></sys>
  • 返回值:成功返回被等待进程pid,失败返回-1。
  • status:是一个输出型参数,将wait函数内部计算的结果通过status返回给调用者,父进程从而获取子进程退出状态,如果不关心子进程的退出状态则可以将参数设置成为NULL。

这里提一下输入型参数和输出型参数的区别,输入型参数是调用者给函数传的参数,而输出型参数是是函数将内部计算结果返回给调用者,因此输出型参数往往用指针。

父进程调用 wait 函数可以回收子进程终止信息。该函数有三个功能:

  • 阻塞等待子进程退出
  • 回收子进程残留资源
  • 获取子进程结束状态(退出原因)。

当父进程调用wait得到传出参数status后,可以借助宏函数来进一步判断进程终止的具体原因:

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

WEXITSTATUS(status): 若WIFEXITED非零,说明子进程正常终止,提取子进程退出码。( 查看进程的退出码(exit 的参数))

3.2.2 waitpid函数

作用同 wait,但 waitpid可指定 pid 进程清理,可以通过非阻塞方式等待子进程退出。

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

pid:

  • pid = -1,等待任一子进程退出,此时与wait等效
  • pid > 0, 回收指定 ID 的子进程,pid为指定进程的进程号。 *如果不存在该子进程,则立即出错返回

status:

  • 同wait

option:

  • 0:阻塞模式,即父进程会阻塞在waitpid处,等到子进程退出后继续。
  • WNOHANG: 非阻塞模式,若pid指定的子进程没有结束,则waitpid函数返回0,不予以等待。若正常结束,则返回该子进程的ID。一般情况下,非阻塞模式需要搭配循环使用。

注意: 一次 wait 或 waitpid 调用只能清理一个子进程,清理多个子进程应使用循环。

返回值:

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

3.3.3 子进程的status

关于status的用法,我已经在wait函数处讲解,此处不再赘述。这里将从底层的角度剖析status的含义。

status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特位)。

Linux系统编程之进程控制(进程创建、终止、等待及替换)

我们以下一段代码为例,来展示一下非阻塞等待方式

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys wait.h>
int main()
{
   pid_t pid;

    pid = fork();
    if(pid < 0){
       printf("%s fork error\n",__FUNCTION__);
        return 1;

    }else if( pid == 0  ){ //child
     printf("child is run, pid is : %d\n",getpid());
     sleep(5);
     exit(1);
    } else{
       int status = 0;
       pid_t ret = 0;
       do
       {
       ret = waitpid(-1, &status, WNOHANG);//&#x975E;&#x963B;&#x585E;&#x5F0F;&#x7B49;&#x5F85;
       if( ret == 0  ){
       printf("child is running\n");
       }
       sleep(1);
       }while(ret == 0);

       if( WIFEXITED(status) && ret == pid  ){
          printf("wait child 5s success, child return codeis:%d.\n",WEXITSTATUS(status));
          }else{
          printf("wait child failed, return.\n");
          return 1;
          }
    }
     return 0;
}

</sys></stdlib.h></unistd.h></stdio.h>

这段代码先创建子进程,让子进程等待5s再退出,父进程每1s检查一下,5s后子进程退出,ret将变成子进程的进程号,退出循环等待。最终的运行结果如下:

Linux系统编程之进程控制(进程创建、终止、等待及替换)

4.进程替换

4.1进程替换的原理

在讲进程替换原理前,我们需要先知道什么是进程替换。在讲fork函数时我们提到,fork 创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),如果此时我们用一个新的程序替 换掉子进程的地址空间、代码段和数据,子进程将会从新程序的启动例程开始执行,这就是进程替换。

进程替换并不是创建新的进程,因为替换前后该进程的PID并未改变。

4.2 环境变量

进程替换需要用到一种exec函数,在讲exec函数族之前,我们先介绍一下环境变量的概念。

4.2.1常见的环境变量

按照惯例,环境变量字符串都是name=value 这样的形式,大多数 name 由大写字母加下划线组成,一般把name 的部分叫做环境变量,value 的部分则是环境变量的值。

环境变量定义了进程的运行环境,具有全局属性,因此设置环境变量时要加export,一些比较重要的环境变量的含义如下:

PATH

可执行文件的搜索路径。ls 命令也是一个程序,执行它不需要提供完整的路径名/bin/ls, 然而通常我们执行当前目录下的程序 a.out 却需要提供完整的路径名./a.out,这是因为 PATH 环境变量的值里面包含了 ls 命令所在的目录/bin,却不包含 a.out 所在的目录。

PATH 环境变量的值可以包含多个目录,用:号隔开。在 Shell 中用 echo 命令可以查看这个环境变量的值: echo $PATH

Linux系统编程之进程控制(进程创建、终止、等待及替换)

SHELL

当前 Shell,它的值通常是/bin/bash。

Linux系统编程之进程控制(进程创建、终止、等待及替换)

TERM

当前终端类型

Linux系统编程之进程控制(进程创建、终止、等待及替换)

HOME

当前用户主目录的路径, 很多程序需要在主目录下保存配置文件,使得每个用户在运行该程序时都有自己的一套配置。

Linux系统编程之进程控制(进程创建、终止、等待及替换)

4.2.2与环境变量相关的函数

getenv函数

获取环境变量值: char *getenv(const char *name);

成功:返回环境变量的值;失败:NULL (name 不存在)

setenv 函数

设置环境变量的值 :i nt setenv(const char *name, const char *value, int overwrite);

成功:返回0;失败: 返回-1

参数 overwrite 取值:

1:覆盖原环境变量

0:不覆盖。(该参数常用于设置新环境变量,如:HELLO = “hello”)

unsetenv 函数

删除环境变量 name 的定义: int unsetenv(const char *name);

成功:0;失败:-1

注意事项:name 不存在仍返回 0(成功)。

4.2.3 环境变量的组织形式

Linux系统编程之进程控制(进程创建、终止、等待及替换)

environ 变量是一个char * 类型,存储着系统的环境变量。*每个程序都会收到一张环境表,环境表是一个字符指针数组,每个指针指向一个以’\0’结尾的环境字符串。

4.3 exec函数族

4.3.1 exec函数族的使用

知道了环境变量的概念后,再简要介绍一下命令行参数。当我们在某个目录下输入 ls -als -l时,会有如下显示:

Linux系统编程之进程控制(进程创建、终止、等待及替换)

我们发现,同样的ls命令,由于后面所跟的字符串不同,显示了不同的结果。这里的”-a”,”-l”被称为参数。实际上,一个程序内可以通过加入参数,让相同的程序执行不同的功能。

接下来我们来介绍进程替换必不可少的函数族——exec函数族。

其实有六种以 exec 开头的函数,统称 exec 函数:

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[]);

注意:这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。 如果调用出错则返回-1 所以 exec函数只有出错的返回值而没有成功的返回值

这些函数如何使用,我们来看下面这段代码:

#include <unistd.h>
int main()
{
 char *const argv[] = {"ps", "-ef", NULL};//argv[0]&#x59CB;&#x7EC8;&#x662F;&#x7A0B;&#x5E8F;&#x540D;
 char *const envp[] = {"PATH=/bin:/usr/bin", "TERM=console", NULL};
 //execl("/bin/ps", "ps", "-ef", NULL);

 // &#x5E26;p&#x7684;&#xFF0C;&#x53EF;&#x4EE5;&#x4F7F;&#x7528;&#x73AF;&#x5883;&#x53D8;&#x91CF;PATH&#xFF0C;&#x65E0;&#x9700;&#x5199;&#x5168;&#x8DEF;&#x5F84;
 //execlp("ps", "ps", "-ef", NULL);

 // &#x5E26;e&#x7684;&#xFF0C;&#x9700;&#x8981;&#x81EA;&#x5DF1;&#x7EC4;&#x88C5;&#x73AF;&#x5883;&#x53D8;&#x91CF;
 //execle("ps", "ps", "-ef", NULL, envp);

 //execv("/bin/ps", argv);

 // &#x5E26;p&#x7684;&#xFF0C;&#x53EF;&#x4EE5;&#x4F7F;&#x7528;&#x73AF;&#x5883;&#x53D8;&#x91CF;PATH&#xFF0C;&#x65E0;&#x9700;&#x5199;&#x5168;&#x8DEF;&#x5F84;
 //execvp("ps", argv);

 // &#x5E26;e&#x7684;&#xFF0C;&#x9700;&#x8981;&#x81EA;&#x5DF1;&#x7EC4;&#x88C5;&#x73AF;&#x5883;&#x53D8;&#x91CF;
 execve("/bin/ps", argv, envp);
 exit(0);
}
</unistd.h>

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

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

  • l(list) : 表示参数采用列表,如果采用列表形式,const char *arg中的第一个参数必须是可执行程序本身,如上例中的 “ps”。
  • v(vector) : 参数用数组 ,v和l只能二选一
  • e(env) : 表示自己维护环境变量,有e参数中就需要有char *const envp[]
  • p(path) : 有p自动搜索环境变量PATH,第一个参数直接输入程序名即可,且有p一定没有e,因为有表示已经自动添加了环境变量,如果没有p则需要输入对应程序的路径

4.3.2 进程替换的应用

我们平时使用的shell读取命令和分析命令就是一个很典型的例子,如下图所示:

Linux系统编程之进程控制(进程创建、终止、等待及替换)

我们平时输入的如 ls -a等命令实际上是一个个可执行程序。当shell读取一行命令时,shell会对命令进行解析,并且shell创建一个子进程,再通过调用execve,用可执行程序替换掉子进程,当程序执行完毕并且退出后,shell读取子进程的退出信息。这样,即便会出现程序崩溃的情况,也不会影响到shell本身。

以上就是关于进程控制的内容,主要分为四个方面——进程创建,进程终止,进程等待以及进程替换。有了以上的知识,我们已经可以实现一个很简易的shell,如何实现,请读者自行思考!

Original: https://www.cnblogs.com/Grong/p/15516023.html
Author: 乌有先生ii
Title: Linux系统编程之进程控制(进程创建、终止、等待及替换)

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/585761/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • Linux tcpdump抓包命令排查

    bash;gutter:true; tcpdump命令行参数介绍:</p> <p>-A 以ASCII格式打印出所有分组,并将链路层的头最小化。 -c 在收到…

    Linux 2023年6月13日
    095
  • make及makefile简单介绍

    GUN make是一种代码维护工具。 make工具会根据makefile文件定义的规则和步骤,完成整个软件项目的代码维护工作。 一般用来简化编译工作,可以极大地提高软件开发地效率。…

    Linux 2023年6月7日
    072
  • DNS

    DNS是域名系统(Domain Name System),简单来说就是平时上网输入的URL,如 www.baidu.com 就是域名,而DNS就是将这个域名解析成IP地址,如 ww…

    Linux 2023年6月7日
    085
  • MySQL常见问题以及解决方法

    MySQL常见问题以及解决方法 数据损坏或丢失 serverid不唯一 主从节点存在复制延迟 主从节点数据不一致 数据表主键冲突 数据损坏或丢失 Master:采用MHA+semi…

    Linux 2023年6月7日
    0108
  • DDoS攻击–TCP攻击概述

    https://blog.csdn.net/qq_34777600/article/details/81945594 posted @2020-12-10 18:07 珠峰之梦 阅…

    Linux 2023年6月7日
    0103
  • U盘如何安装centos7系统?U盘安装centos7详细安装图解教程

    一般来说,无论是Windows还是linux的IOS系统镜像,我们都可以使用UltraIOS(软碟通)这款软件制作U盘启动工具,不过考虑到不少小白依然不会如何操作,所以今天考虑写一…

    Linux 2023年6月8日
    0107
  • 存入redis中的java对象都需要序列化

    存入redis中的java对象都需要实现Serializable接口 Original: https://www.cnblogs.com/toSeeMyDream/p/127795…

    Linux 2023年5月28日
    0115
  • 微信小程序大型系统架构中应用Redis缓存要点

    在大型分布式系统架构中,必须选择适合的缓存技术以应对高并发,实现系统相应的高性能,酷客多小程序经过慎重选型,选择了采用基于腾讯云服务的Redis弹性缓存技术,结合Redis官方推荐…

    Linux 2023年5月28日
    0102
  • 卷积神经网络(简单)

    1.反向传播BP 反向传播(Backpropagation)是”误差反向传播”的简称,是一种与最优化方法,用来训练人工神经网络的常见方法。 简单来说就是: …

    Linux 2023年6月6日
    0129
  • 正则表达式 9. 替换分组使用

    https://www.zybuluo.com/Zjmainstay/note/709093 替换分组使用 (9.1)给源串每个链接加上http://www.zjmainstay….

    Linux 2023年6月13日
    0101
  • Redis做Mybatis的二级缓存

    基于spring boot项目的前提下,使用redis数据库做mybatis的二级缓存。 Redis做mybatis的二级缓存 作用提升速度,保证多台服务器访问同一数据库时不会崩注…

    Linux 2023年6月7日
    0117
  • identity server4 授权成功页面跳转时遇到错误:Exception: Correlation failed. Unknown location的解决方法

    一、异常信息描述 错误信息,看到这个页面是否耳熟能详担又不知道怎么解决 ,坑死个人不偿命,,,,,,,, 二、处理方法 1、在web项目中增加类SameSiteCookiesSer…

    Linux 2023年6月13日
    0125
  • 关于 QT绝对路径依赖症的解决办法!!

    QT安装正确后,使用Qtcreator开发会非常方便; 即使重装系统后,你不用重新安装直接使用QT开发即可。 但是如果你把这个安装后的开发环境,拷贝到其它电脑或者目录后, 你会发现…

    Linux 2023年6月14日
    091
  • pycharm 设置默认换行符

    作者:Outsrkem原文链接:https://www.cnblogs.com/outsrkem/p/16488693.html本文版权归作者所有,欢迎转载,但未经作者同意必须保留…

    Linux 2023年6月6日
    0111
  • MobaXterm左侧没有文件列表,没有SCP,不显示文件夹问题处理

    一般情况是你设置的session属性问题,具体做法是右键你的session,选edit session,SSH 如下图: 选择 SFTP protocol 并勾选 Follow S…

    Linux 2023年5月27日
    0149
  • LVS负载均衡

    LVS负载均衡 LVS负载均衡 一、LVS是什么 二、LVS的作用 三、lvs的三种工作模式 1.基于NAT的LVS模式负载均衡 2.基于TUN模式的LVS负载均衡 3.LVS(D…

    Linux 2023年6月6日
    096
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球