如何理解:进程控制

server/2024/9/18 12:35:31/ 标签: linux, 进程, 进程状态, 进程控制, 进程等待, wait, waitpid

文章目录

前言:

​ 对于前面的地址空间的学习,我们现在了解到原来所谓变量的地址其实是虚拟地址,该虚拟地址是通过页表的映射关系从而找到内存中真实的物理地址!下面我们进入到关于进程的控制

进程创建:

​ 现在我们对进程就有了新的定义:进程 = 内核的相关管理数据数据结构(task_struct + mm_struct + 页表) + 代码和数据。 其中代码是父子进程共享的,数据是判断是否发生写实拷贝的。

  • fork函数的返回值

    我们之前也有过介绍,fork函数是用来创建子进程的,创建子进程成功则返回0,对于父进程的返回值则是子进程的pid,这一点虽然我上次没有细讲,但是在截图时就会发现。可以看看我之前博客——>进程理解

  • 为什么父进程返回的是子进程的pid,给子进程返回的却是0呢?

    要记住,我们在讲解进程状态的时候,对于僵尸进程我们有过介绍,父进程是会管理进程的,谈到管理永远是6个字——>”先描述,再组织“

    所以当然是为了让父进程方便对子进程进行标识,进而进行管理!

  • fork函数的常规用法

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

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

进程终止:

  • 进程终止是在做什么呢?

    首先我们应该清楚一件事情——>当加载进程是,应当是先创建PCB、页表和地址空间等,再是加载代码和数据

    所以进程终止是在:

    1. 释放曾经的代码和数据所占据的空间。
    2. 释放内核数据结构(若task_struct受僵尸状态,则演示释放)
  • 进程终止的3种情况

    • 提出问题:为什么main函数最后要返回0呢?为什么不是1或者100呢?
    #include <stdio.h>
    #include <unistd.h>
    #include <sys/types.h>int main()
    {printf("I am a process! pid: %d, ppid: %d\n", getpid(), getppid());sleep(1);return 100;
    }
    

    [!TIP]

    我们可以使用指令:echo $?

    该指令代表父进程bash获取到子进程退出码,0表示成功,非0表示失败。退出码的意义就是告诉关心方(父进程),我把任务处理的怎么了。

    image-20240814221753931

    但我要是再次执行echo $?指令,表示的就是刚刚子进程的退出码,毕竟该指令是获取最近进程的退出码。

    image-20240814221932725

    那这个退出码究竟有什么用,你想返回100或者0不是都可以吗。诶,这个时候操作系统会给我们提供个新的系统调用函数:strerror函数用来获取系统错误信息或打印用户程序错误信息,下面我们来用用这个系统调用

    [!TIP]

    #include <stdio.h>
    #include <unistd.h>
    #include <sys/types.h>
    #include <string.h>int main()
    {int errorcode = 0;for(errorcode = 0; errorcode <= 255; ++errorcode){printf("%d -> %s\n", errorcode, strerror(errorcode));}return 100;
    }
    

    image-20240814223501038

    image-20240814224450118

    我们会打印特别多的错误码,每个错误码都对应了一个描述。所以为什么我们最后都会返回0,就是因为如果全部代码都执行完毕后,那肯定是不会存在问题的,那么就返回0。如果其中有一处有错误,系统就会直接进行返回相应的错误码。

    image-20240814224639250

    这里是我随便ls一个文件夹,因为在当前目录未找到该文件夹,所以执行该指令的进程就会向bash返回相对应的错误码2。

    所以我们在输入指令的时候,本质也是OS创建子进程然后进程在执行。这点我们通过指令echo $?可以很好的证明。

    这也能很好地说明bash为什么要得到子进程的退出码,为了知道子进程退出的情况(是否成功,失败又是什么原因),当然bash只是提供信息,不会自动解决,这只是一种为用户负责的体现

    • 我们也可以实现自定义退出码

      #include <stdio.h>
      #include <unistd.h>
      #include <sys/types.h>
      #include <string.h>enum
      {ERROR_DIV = -1,NOMARL_DIV
      };int exit_code = NOMARL_DIV;int Div(int a, int b)
      {if(b == 0){exit_code = ERROR_DIV;//printf("This is error!\n");return exit_code;}exit_code = NOMARL_DIV;return a/b;
      }const char* ErrorMode(int mode)
      {switch(mode){case ERROR_DIV:return "Div zero";case NOMARL_DIV:return "Div nomarl";default:return "Unknow error!";}
      }int main()
      {int ans = Div(4, 0);printf("ans is-> %d[%s]\n", ans, ErrorMode(exit_code));ans = Div(4, 2);printf("ans is-> %d[%s]\n", ans, ErrorMode(exit_code));return 0;
      }
      

      通过判断分母是否为0,从而实现判断结果是否是正确的!

      • 退出信号

        int main()
        {int* p = NULL;*p = 3;printf("%d\n", *p);return 0;
        }
        

        对于上述代码,我们执行起来百分之百会出现报错,如果我们在VS上也写过这样的bug,那么该错误就是一个很典型的——段错误
        image-20240815120245588

        这个时候代码不会跑完,因为在执行的过程中出现了异常,就提前退出了。就像在VS写代码时,代码崩溃了,此时OS发现你的进程做了不该做的事情,OS就会杀掉进程,一旦出现了异常,退出码就没意义了。

        比如此时我使用指令echo $? 打印退出码,是会发现退出码为139,但是退出码在133后就是未知了:
        image-20240815121710938

        • 为什么会出现异常呢?

          本质上是因为进程收到了OS发给进程信号

          我们可以使用指令:kill -l
          image-20240815122015374

          段错误的出现就是对应的11号信号

          所以衡量一个进程的退出,我们(父进程bash)只需要两个数字:退出码 + 退出信号

          第一步是先确认是否异常
          若不是异常就一定是代码跑完了,看退出码就好了。

[!IMPORTANT]

所以进程终止的3种情况:

  1. 代码跑完,结果正确
  2. 代码跑完,结果不正确
  3. 代码执行时,出现了异常,提前退出了

如何终止进程

  1. main函数的return,表示进程终止(非main函数的return,代表函数结束)
  2. 代码调用exit函数(在代码任意位置调用exit函数都表示进程退出)
  3. 通过系统调用函数_exit( )
  • exit( ) VS _exit( )

    1. exit( )是库函数,_exit( )是系统调用

    2. exit函数会在进程退出的时候,重置缓冲区,而 _exit( )不会,因此我们可以猜测缓冲区是在exit那一层,也可以说明使用exit( )库函数的本质是在调用系统调用函数 _exit( ).

      #include <stdio.h>
      #include <stdlib.h>int main()
      { printf("Hello, this is a test code!");exit(0);
      }

      image-20240815123149853

      #include <stdio.h>
      #include <stdlib.h>int main()
      { printf("Hello, this is a test code!");_exit(0);
      }

      image-20240815123237707

进程等待

​ 任何进程,在退出的情况系,一般必须要被父进程进行等待!

  • 为什么父进程要等待?

    1. 进程在退出的时候,如果父进程不管不顾,退出进程会出现僵尸状态从而到时内存泄漏。所以父进程通过等待,解决子进程出现的僵尸问题,为了回收系统资源。(这是一定要考虑的)
    2. 获取子进程的退出信息,知道子进程是因为什么退出的。(可选的功能)
  • 该怎么进行等待?

    我们可以使用系统调用函数:wait( ) 和 waitpid( ) 函数

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

    pid_t wait(int* status);
    pid_t waitpid(pid_t pid, int* status, int options);

    • wait( )

      返回值:成功返回被等待进程的pid,失败返回-1.
      参数:输出型参数,获取子进程退出状态,不关心则可以设置成NULL

      #include <stdio.h>
      #include <unistd.h>
      #include <sys/types.h>
      #include <stdlib.h>
      #include <sys/wait.h>void ChildRun()
      {int cnt = 1;while(cnt <= 5){printf("%d-> I am child process, pid: %d, ppid: %d\n", cnt, getpid(), getppid());sleep(1);cnt++;}
      }int main()
      {printf("I am process!\n");pid_t id = fork();if(id == 0){// childChildRun();printf("Child quit...\n");exit(123);}// fatherpid_t rid = wait(NULL);if(rid > 0){printf("wait success, rid: %d\n", rid);}else{printf("wait failed!\n");}sleep(2);return 0;
      }
      

      image-20240815134408685

      最后是等待成功并且返回了子进程的pid

    • waitpid( )

      回收子进程资源,解决僵尸问题的同时,还能够获取子进程退出信息。

      返回值:

      • pid_t > 0:等待成功,子进程退出,并且父进程回收成功
      • pid_t < 0:等待失败
      • pid_t == 0:检测是成功的,只不过子进程还没退出,需要你下一次重复等待。

      参数:

      1. pid_t pid

        • pid == -1,等待任意一个子进程,与wait一样。
        • pid > 0,等待其进程的ID与pid相等的子进程
      2. int* status

        • 输出型参数,用来保存退出信息,保存退出码 + 退出信号,让我们知道“退出”的情况如何
      3. int options

        • 指定父进程的等待方式,为 0 则让父进程进行 阻塞 等待,非 0 则进行 非阻塞 等待。

          #include <stdio.h>
          #include <unistd.h>
          #include <sys/types.h>
          #include <stdlib.h>
          #include <sys/wait.h>
          void ChildRun()
          {int cnt = 1;while(cnt <= 5){printf("%d-> I am child process, pid: %d, ppid: %d\n", cnt, getpid(), getppid());sleep(1);cnt++;}
          }int main()
          {printf("I am process!\n");pid_t id = fork();if(id == 0){// childChildRun();printf("Child quit...\n");exit(123);}// fatherint status = 0;pid_t rid = waitpid(-1, &status, 0);if(rid > 0){printf("wait success, rid: %d\n", rid);}else{printf("wait failed!\n");}sleep(2);printf("father process quit, status: %d\n", status);return 0;
          }
          

          image-20240815142915066

          最后的运行结果status是31488,这么奇怪的数字。

          • 分析status

            我们介绍过,status是输出型参数,记录了该进程退出码 + 退出信号,但是我们应该如何利用一个值记录退出码和退出信号呢?

            [!NOTE]

            我们都知道,一个int* 变量的大小是4字节,那么也就是32个比特位,但status不能简单的当做int 来看待,可以当做位图来看,我们只研究低16比特位*

            分为两种情况:

            1. 正常终止时:高8位为退出码信息,低8位默认全部为0
            2. 被信号杀掉时(出现异常):高8位不再使用,低7位存储终止信号,中间还有一位存core dump标志位

            image-20240815144220123

            红色各段就代表着 退出码 + 退出信号,退出码拥有8个比特位,而终止信号拥有7个比特位

            所以我们上述的31488其实是两个数据的整合,但是我们也可以打印出来看看。

            我们需要将退出码先挪动到低8位,再转换为10进制,所以我们可以用位运算操作符:
            打印退出码:(status >> 8) & 0xFF
            打印退出信号:status & 0x7F
            image-20240815145027841

            printf("exit_code: %d, exit_signal: %d\n", (status >> 8)&0xFF, (status & 0x7F));
            

            最后的输出结果:
            image-20240815145620114

            因为我的子进程最后exit了123,又因为这是正常终止,所以返回退出码接收到了123.
            但如果我加一个段错误,再运行就会如下图所示:

            image-20240815150154379

            image-20240815145913895

            此时是异常退出,status就会收到退出信号非退出码

            我们也可以利用两个宏,来查出退出码:

            1. WIFEXITED(status):若位正常终止子进程返回的状态,则为真。(主要是查看子进程是否正常退出,本质上是查看signal位是否满足。
            2. WEXITSTATUS(status):若WIFEXITED非空,提取子进程的退出码。
            if(WIFEXITED(status))
            {printf("child process quit success, chid exit code: %d\n", WEXITSTATUS(status));
            }
            else
            {printf("child process quit unnormal!\n");                                     
            }
            

非阻塞等待:

​ 我们上面讲的等待过程是属于阻塞等待,还记得我们在进程状态部分讲解过阻塞态吗,还记得阻塞态是会进入等待队列的吗?如果你有疑问的话可以去看看我之前写的博客:进程的祖册、挂起和运行状态。
​ 那我们刚刚介绍waitpid( )系统调用时,也介绍了参数option是控制关于非阻塞等待的,如果参数option为0,那就是阻塞等待,父进程会等待子进程结束,再进行父进程的操作,这是阻塞等待。
非阻塞等待是父进程在等待子进程的过程中,父进程同时也在做某些操作,这就是非阻塞等待。

[!NOTE]

我们可以使用宏:WNOHANG 来表示进程进入非阻塞等待。

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
#include <sys/wait.h>void ChildRun()
{int cnt = 1;while(cnt <= 5){printf("%d-> I am child process, pid: %d, ppid: %d\n", cnt, getpid(), getppid());sleep(1);cnt++;}
}void DoOtherThing()
{printf("   I am doing my father things while child process is running!\n");
}int main()
{printf("I am a process, pid: %d, ppid: %d\n", getpid(), getppid());pid_t id = fork();if(id == 0){//childChildRun();printf("child process quit...\n");sleep(1);exit(0);}//fatherint status = 0;while(1){pid_t rid = waitpid(0, &status, WNOHANG);if(rid == 0){printf("   Just checking child process...\n");DoOtherThing();sleep(1);}else if(rid > 0){printf("wait suceess!\n");break;}else{printf("wait failed!\n");break;}}if(WIFEXITED(status)){sleep(1);printf("father process quit success!\n");printf("exit_code: %d, exit_signal: %d\n", WEXITSTATUS(status), status&0x7F);}else{printf("quit unnormal!\n");}return 0;
}

通过以上代码,我们就能实现子进程运行的同时,父进程也在运行自己的任务:
image-20240815174618876

总结:

​ 现在我们已经学会了有关进程创建的话题,接下来我们将要讨论进程替换的话题。


http://www.ppmy.cn/server/102394.html

相关文章

kylin系统永久关闭iptables

1 关闭iptables, 并且相关规则写入文件firewall.rules sudo iptables-save > /root/firewall.rules iptables -X iptables -t nat -F iptables -t nat -X iptables -t mangle -F iptables -t mangle -X iptables -P INPUT ACCEPT iptables -P FORWARD ACCEPT iptables -P …

MySQL 常用 SQL 语句大全

1. 基本查询 查询所有记录和字段 SELECT * FROM table_name; 查询特定字段 SELECT column1, column2 FROM table_name; 查询并限制结果 SELECT column1, column2 FROM table_name LIMIT 10; 条件查询 SELECT column1, column2 FROM table_name WHERE condition; 模糊匹…

守塔不能停辅助:VMOS云手机辅助玩法,2024平民必备!

在策略塔防游戏《守塔不能停》中&#xff0c;正确的战术选择和资源管理是通关的关键。而现代科技的进步为我们提供了更高效的游戏方式&#xff0c;其中最为突出的便是VMOS云手机。VMOS云手机为《守塔不能停》量身打造了专属定制版云手机&#xff0c;内置游戏安装包&#xff0c;…

\r和\n不同系统的区别

文章目录 一、\r和\n的来源1、回车和换行来源2、关于字符2.1、可显示字符2.2、不可显示字符&#xff08;控制字符&#xff09; 二、\n和\r差异1、不同操作系统中的回车换行2、影响 一、\r和\n的来源 1、回车和换行来源 在最初的电传打印机时代&#xff0c;每打完一行需要换行的…

Go 1.21 新内置函数:min、max 和 clear

max 函数 func max[T cmp.Ordered](x T, y …T) T 这是一个泛型函数&#xff0c;用于从一组值中寻找并返回 最大值&#xff0c;该函数至少要传递一个参数。在上述函数签名中&#xff0c;T 表示类型参数&#xff0c;它必须满足 cmp.Ordered 接口中定义的数据类型要求&#xff0c…

C语言—指针(1)

目录 一、内存和地址 &#xff08;1.1&#xff09;内存 &#xff08;1.2&#xff09;编址的理解 二、指针变量和地址 &#xff08;2.1&#xff09;取地址操作符&#xff08;&&#xff09; &#xff08;2.2&#xff09;指针变量和解引用操作符 &#xff08;2.2.1&…

Java知识点一——列表、表格与媒体元素

显示表格边框&#xff1a;<table border"1"></table> 因为初始的表格是没有边框的 collapse相邻的单元格共用同一条边框&#xff08;采用 collapsed-border 表格渲染模型&#xff09;。 separate默认值。每个单元格拥有独立的边框&#xff08;采用 sep…

「12月·长沙」第三届传感、测量、通信和物联网技术国际会议(SMC-IoT 2024)

第三届传感、测量、通信和物联网技术国际会议&#xff08;SMC-IoT 2024&#xff09;将于2024年11月29日-2024年12月1日召开&#xff0c;由湖南涉外经济学院主办。会议中发表的文章将会被收录, 并于见刊后提交EI核心索引。 会议旨在围绕传感、测量、通信和物联网技术等相关研究…

徐州电信托管的好处有哪些?

大部分的企业用户都在选择服务器托管这项服务&#xff0c;而电信托管是一种将网络服务器的物理资源和维护工作交给专业的电信公司管理的业务模式&#xff0c;电信托管可以帮助大部分的企业提高整体运维效率并能够保障用户数据的安全。 本文就来探索一下徐州电信托管的好处有哪些…

Django基础之MTV模型

一、Django基础 一&#xff09;Django简介 Django是一个开放源代码的Web应用框架&#xff0c;由Python写成。采用了MVC的软件设计模式&#xff0c;即模型(Model)、视图(View)和控制器(Controller)。它最初是被开发来用于管理劳伦斯出版集团旗下的一些以新闻内容为主的网站的&am…

油猴,复制local storage脚本

// UserScript // name LocalStorage Table with Copy Function // namespace http://tampermonkey.net/ // version 1.0 // description 创建一个按钮,点击按钮显示包含 localStorage的表格&#xff0c;为每个值添加复制按钮 // author Your Name // match *://xxxx.com/* // …

Kafka中查找某个topic是否包含某个字符串

要在Kafka中查找某个topic是否包含某个字符串&#xff0c;您可以通过以下几个步骤&#xff1a; 使用Kafka的命令行工具kafka-console-consumer来消费topic的消息。这个工具可以让您从某个topic读取消息并将其输出到控制台。例如&#xff0c;要从名为my_topic的topic读取消息&a…

PHP 无参数RCE总结

在这篇文章中&#xff0c;我总结了在参与CTF比赛过程中积累的关于PHP无参数远程代码执行&#xff08;RCE&#xff09;的经验。由于一直以来时间有限&#xff0c;今天终于有机会整理这些知识点。 可能用到的函数&#xff08;PHP的内置函数&#xff09; localeconv() 函数返回一…

Matplotlib库学习之scatter(模块)

Matplotlib库学习之scatter(模块) 一、简介 Matplotlib 是 Python 中一个强大的绘图库&#xff0c;其中 matplotlib.pyplot.scatter 用于创建散点图。散点图在数据可视化中广泛用于展示两个变量之间的关系。 二、语法和参数 语法 matplotlib.pyplot.scatter(x, y, sNone, c…

Java 中 String 类型的特点

在 Java 中&#xff0c;String 是一种常用且重要的数据类型&#xff0c;用于表示和处理字符序列。它有一些独特的特性和用法&#xff0c;使得它在开发中非常灵活和高效。以下是关于 String 类型的一些特点、特殊性、使用技巧以及注意事项。 1. String 的特点 1.1 不可变性 定…

Oracle笔记

一、 如何解决 sqlplus 无法使用退格键和方向键 .bashrc 中添加如下内容&#xff0c;解决 退格键 stty erase ^h 安装 rlwap 后&#xff0c;执行如下命令可解决 方向键 rlwap sqlplus 二、 都有哪些备份数据到工具 三、 谈谈 你对 oracle 中实例和数据库的理解 数据库是一…

Leetcode-552 学生出勤记录II

Leetcode-552 学生出勤记录II 1. 题目描述2. 解题思路3. 代码实现 1. 题目描述 Leetcode-552 学生出勤记录II 2. 解题思路 (1) 使用记忆化搜索来实现&#xff1b; (2) 定义f[i][j][k]为右边填写j个A, 且右边相邻位置有k个连续的L的情况下&#xff0c;向左填字母能构造多少个…

PyTorch FP16模型转换

PyTorch FP16模型转换 Q: pytorch如何将模型转换为fp16&#xff1f; A: 在 PyTorch 中&#xff0c;将模型转换为 FP16&#xff08;半精度浮点数&#xff09;可以通过几种不同的方法实现&#xff0c;最常见的方法是使用 torch.cuda.amp 模块和 torch.nn.Module.half() 方法。…

下载 kotlin compiler embeddable 巨慢的问题

参考了网上的一些说法 1.用梯子&#xff08;个人实测无作用&#xff0c;换了很多链路&#xff09; 2.官网下载对应的jar放到.gradle的缓存目录下 jar目录&#xff1a;C:\Users\你的username.gradle\caches\modules-2\files-2.1\org.jetbrains.kotlin\kotlin-compiler-embeddabl…

【Python快速入门和实践013】Python常用脚本-目标检测之按照类别数量划分数据集

一、功能介绍 这段代码实现了从给定的图像和标签文件夹中分割数据集为训练集、验证集和测试集的功能。以下是代码功能的总结&#xff1a; 创建目标文件夹结构&#xff1a; 在指定的根目录&#xff08;dataset_root&#xff09;下创建images和labels两个文件夹。在这两个文件夹下…