【Linux】进程控制基础知识

news2024/11/28 3:35:05

目录

一,fack回顾

二,进程终止

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

2.进程终止,常见的方式

1.main函数的,return + 返回码

2. exit()函数

三,进程等待 

1. 回收进程方法

(1. wait方法

补充理解:僵尸进程与内存泄露区别

(2.waitpid函数

a,参数pid 

b, 参数status

c, 参数options

四,进程替换

1,概念与原理 

2,进程替换方法

3. 尝试fork + execl函数

其他函数补充:

4. 如何利用execl函数运行其他可执行程序


 

一,fack回顾 

在linux中fork函数时非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。
#include <unistd.h>
pid_t fork(void);
返回值:自进程中返回0,父进程返回子进程id,出错返回-1
进程调用fork,当控制转移到内核中的fork代码后,内核做:
  1. 分配新的内存块和内核数据结构给子进程;
  2. 将父进程部分数据结构内容拷贝至子进程;
  3. 添加子进程到系统进程列表当中;
  4. fork返回,开始调度器调度。

由于前面已经出现了fork的理解,这里不做详解。 

前面地址空间章节(【Linux】地址空间概念_花果山~~程序猿的博客-CSDN博客)我们已经初步了解了在创建子进程时,系统会采用写时拷贝这项决策,我们可以反向思考,如果系统直接拷贝一份给子进程呢?我们知道进程 = 内核数据结构 + 代码&数据,进程一旦被创建,代码是处于只读的状态,但数据可以转为可写,这样我们为啥不拷贝一份数据给子进程?因为系统是不知道那些数据是要被使用的,所以拷贝一份不怎么被使用的数据会导致内存利用率下降。

所以关于为何OS选择写时拷贝技术,对父子进程进行分离?

1.用的时候,给子进程分配,是一种高效的内存表现。

2.系统在执行代码时,无法的知道那些内存会被访问。

数据修改前后: 

结论:在计算机系统中,当父进程创建子进程时,子进程会继承父进程的代码和数据。初始时,这些代码和数据的权限是只读的。当子进程需要修改这些代码和数据时,会进行写时拷贝操作,即将需要修改的部分数据的权限从只读变为可写。

二,进程终止

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

进程终止,操作系统释放进程所申请的内核数据机构,代码和数据,其本质上就是内存释放。

2.进程终止,常见的方式

1.main函数的,return + 返回码

我们在编写一个C/C++程序时,运行起来,会有以下情况:

 (a,代码跑完,结果正确;

   (b,代码跑完,结果不正确;

我们在编写main函数时,往往都会返回一个0(不总是0),这是进程退出码,是提供给上一级父进程的,如果返回的不是0就代表结果不正确;反之,正常退出,则返回0;

补充: 获取上一次进程退出码指令 echo  $?

那main()函数返回值有什么意义呢?

当我们在shell脚本中调用一个程序时,可以通过检查该程序的main函数返回码来确定程序是否成功执行。如果返回码为0,则表示程序执行成功;而如果返回码为其他非零值,则表示程序执行失败或出现错误。通过这种方式,我们可以根据main函数的返回码来进行后续的处理(意味着得程序跑完,才会有后续处理),例如输出相应的提示信息或进行错误处理(比如:strerror()函数的错误原因)。(来自chatgpt)

 (c,代码未跑完,程序奔溃。(这里在信号部分讲解)

2. exit()函数

exit在代码任何地方调用,都会终止进程。这里补充一个系统层面的接口_exit()

接下来我们来实验一下两者的区别:

行缓冲区,如果我们不添加换行符,打印数据会先存放到缓存区,在进程结束后刷新到显示器。

    int main()
  6 {
  7   cout << "lisan";
  8   sleep(3);
  9   exit(11); // _exit(11);                                     
 10   return 0;                    
 11 }                              
 12      

尝试两个函数,_exit()函数在进程退出时不会打印lisan,下面是原因示意图。

由于_exit()直接终止程序,所以缓冲区的数据没有被刷新出。那这里我们会想缓冲区在那里呢?我们知道_exit()是操作系统的接口,exit()是库函数,因此我们可以大概猜到管理缓冲区的程序在操作系统之上


三,进程等待 

为什么需要进程等待?父进程需要拿到一个数据,创建子进程,等待子进程返回数据,父进程才能进入下一步操作。以及,子进程退出,如果父进程提前退出,子进程则变成僵尸进程,造成内存泄露。

总之;

  1. 之前讲过,子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。
  2. 另外,进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。
  3. 最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。
  4. 父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息。

执行下面程序:

        int main()
  6 {
 12     pid_t pd = fork();
 14     if (pd < 0)
 15     {
 16       // 程序失败
 17       perror("fork ");
 18     }else if(pd == 0)
 19     {
 20       // 子进程
 21       int a = 5;
 22       while(a--)                                                   
 23       {
 24       printf("是子进程:getpid:%d,getppid:%d\n",getpid(), getppid()    );
 25       sleep(1);
 26       }      
 27     }else{
 28       // 父进程
 29       while(1)
 30       {
 31       printf("是父进程:getpid:%d,getppid:%d\n",getpid(), getppid()    );
 32       sleep(1);
 33       }   
 34     }
 35 }

 

那我们如何接收进程呢?(虽然父进程提前结束,子进程会被操作系统领养,回收,这种思路:是一种编程思路,我们以后会学习到)

1. 回收进程方法

(1. wait方法

#include<sys/types.h>
#include<sys/wait.h>
pid_t   wait(int*  status);
返回值: 成功返回被等待进程pid ,失败返回 -1
参数: 输出型参数,获取子进程退出状态, 不关心则可以设置成为 NULL

补充理解:僵尸进程与内存泄露区别

我们知道子进程一旦进入僵尸状态,其代码和数据虽然可以被释放,但其PCB(task_struct)的内核数据结构会被保留,如果操作系统一直不回收那么也属于内存泄露;在我们编写的应用程序中,我们通过new,malloc向堆区申请的内存,需要我们在使用完后进行释放,否则会造成内存泄露。理解:

这两种泄露前者是操作系统级别的,后者是进程中,后者进程退出,系统回收内存,不存在内存泄露;前者操作系统不处理僵尸进程的PCB是永远回收不了这些内存的。

(2.waitpid函数

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

a,参数pid 

Pid=-1,  等待任一个子进程。与 wait 等效。
Pid>0. 等待其进程 ID pid 相等的子进程。

 补充一点关于status的知识,我们知道其是用来记录子进程返回码的,同时我们也知道程序运行结束会有三种情况:

那怎么从status上表达这不同情况?
 

b, 参数status

wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。
如果传递NULL,表示不关心子进程的退出状态信息。否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特位——小端机器)
32位,我们学习前15位

 

 所以我们怎么取得退出状态??

(status >> 8) & 0xff    //  0xff ->  0000 0000....1111  1111保留最后8个比特位

这是进程正常结束,那进程异常结束呢?我们知道进程异常退出,其实是系统杀掉了进程,系统向进程发送杀掉信号。进程一但异常退出,那么其进程返回码就失去了意义。

所以如何获取这个信号呢?

(status >> 7) & 0x7F   // 0000...  111 1111 保留最后7个比特位,(注意:如果status如果已经进行位向右移,这次的位运算是会在上次的基础上向右移)

进程异常结束,不都是进程内部代码问题,也有可能是外部原因,比如:kill -9  杀死进程,错误消息就是9

但是,这个还得知道status的组成,然后需要进行位运算,这个了解还行,但使用长期下来不方便,因此,为了使用方便提供了如下:

常用获取进程退出情况(推荐) 

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

补充:

c, 参数options

option默认为0,表示子进程运行时,父进程为阻塞等待; WNOHANG 参数,是一个宏定义,表示父进程为非阻塞状态。(WNOHANG理解:HANG是一种专业的术语,如果一个进程卡死,这个进程要么在阻塞队列中,要么等地被调度,所以称作这个进程HANG住了。所以NOHANG就是非阻塞等待)

Linux由C语言编写,wait本质上是系统中的一个函数,我们通过一个伪代码来理解:

 那非阻塞等待,难道是不等待子进程?本质上,非阻塞等待是基于非阻塞调用的轮询方案,说人话是,我找张三帮忙,张三说在忙,我先做我的事,然后每过一分钟给他打个电话,查看他是事是否做完。

四,进程替换

1,概念与原理 

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

2,进程替换方法

方法:通过execl函数

我们问问man

今天我们来学习最简单的execl。

int execl (const char* path, const char*  arg, ...)   // 路径 , 命令行上该怎么写就怎么写

path : 目标程序的地址+路径

arg:   函数参数

...  : 的意思是可变参数列表,注意点:参数列表必须以NULL结尾,这表示参数提取结束。

下面是例子:

 从上面的观察中发现:

1. 进程替换后,"进程结束"没有打印,这一点可以佐证,一旦execl函数调用成功,原来进程的代码和数据全部被替换为新进程。

2. 如果execl调用失败,继续原来进程,不过这时可以直接终止进程。

3. 尝试fork + execl函数

看下面代码:

  1 #include <iostream>
  2 #include <unistd.h>
  3 #include <sys/types.h>
  4 #include <sys/wait.h>
  5 using namespace std;
  6 
  7 int main()
  8 {
  9    pid_t pd = fork();
 10    if (pd == 0)
 11    {
 12     // 子进程
 13      cout << "子进程开始, pid:" << getpid() <<  endl;
 14      execl("/usr/bin/ls", "ls","-l", "-a", "--color=auto", NULL);
 15      exit(-1);
 16 
 17    }else if (pd)
 18    {
 19      // 父进程
 20      int status = 100;
 21      cout << "父进程开始" << endl;
 22      pid_t ret = waitpid(-1, &status, 0);                               
 23      if (ret)
 24      {
 25        cout << "子进程退出,打印子进程退出码:" << WEXITSTATUS(status) <<    endl;
 26      }else 
 27      {
 28        cout << "子进程未退出" << endl;
 29      }
 30 
 32    }
 33    else 
 34    {
 35      cout << "创建子进程失败" <<endl;
 36    }
 37   return 0;
 38 }

结果:

问:为什么要创建子进程来替换呢?

答:为了实现父进程读取数据,分析数据,然后指派子进程去完成某项任务的思想。

问:父子进程代码共享,数据写时拷贝?那execl函数替换进程了呢?代码是否会进行写时拷贝?

答:会,因为如果父子进程共享,在调用execl函数时,会对代码进行写时拷贝,否则父进程会受到影响。

其他函数补充:

进程替换函数其实还是有挺多接口的,如下:

1. execv 函数,使用一图流如下:

2. execlp函数,

3. execvp函数,这个就挺容易用的,可以这么理解,指令方式以Vector存储,并且“P”省略文件路径,自动搜索环境变量。

4. execle函数,"e"表示的则是环境变量的意思,通过传递环境变量给新程序,可以在新程序中使用这些环境变量的值。例如,可以通过设置环境变量来影响新程序的行为,或者传递一些需要在新程序中使用的配置信息。

下面是一个示例,展示了如何使用execle函数传递环境变量:

#include <unistd.h>

int main() {
    char *envp[] = {"MYVAR=Hello", "OTHERVAR=World", NULL};
    execle("/path/to/program", "/path/to/program", NULL, envp);
    return 0;
}

在上面的示例中,我们定义了两个环境变量MYVAROTHERVAR,并将它们传递给新程序。新程序可以使用getenv函数来获取这些环境变量的值。

需要注意的是,使用execle函数时,必须传递完整的环境变量数组,包括系统默认的环境变量。如果只想传递自定义的环境变量,可以使用execve函数(这是真正的系统调用,其他exec**函数都只是封装),并将environ变量作为参数传递给它。(来自chatgpt)

命名总结:

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

4. 如何利用execl函数运行其他可执行程序

诺,下面是我在Test程序上调用mypro程序。 

makefile: 可以做到一次编译多个文件。

最右侧的图,运用到了命令行参数,可参考本博客中命令行参数部分【Linux】进程基础概念【下篇】-CSDN博客 

到这里我们,我们可以理解exec***函数的功能——底层加载器的接口

结语

   本小节就到这里了,感谢小伙伴的浏览,如果有什么建议,欢迎在评论区评论,如果给小伙伴带来一些收获请留下你的小赞,你的点赞和关注将会成为博主创作的动力

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1052622.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

cola 架构简单记录

cola 是来自张建飞&#xff08;Frank&#xff09;的偏实现的技术架构&#xff0c;里面的业务身份和扩展点也被MEAF引用&#xff0c;cola本身由java 实现、但其实可以是一种企业通用的技术架构。 业务身份来源 https://blog.csdn.net/significantfrank/article/details/8578556…

Jasypt 实现自定义加解密

如下文章已经讲解了&#xff0c; Jasypt 是什么&#xff0c;怎么集成 Jasypt&#xff0c;怎么使用 Jasypt。 Jasypt 开源加密库使用教程_jasyptstringencryptor-CSDN博客Jasypt 加密框架概述1、Jasypt Spring Boot 为 spring boot 应用程序中的属性源提供加密支持&#xff0c;…

Linux开发工具之文本编译器vim

●IDE例子 Linux编辑器-vim使用 vi/vim的区别简单点来说&#xff0c;它们都是多模式编辑器&#xff0c;不同的是vim是vi的升级版本&#xff0c;它不仅兼容vi的所有指令&#xff0c;而且还有一些新的特性在里面。例如语法加亮&#xff0c;可视化操作不仅可以在终端运行&#xff…

Windows电脑显示部分功能被组织控制

目录 问题描述 解决方法 总结 问题描述 如果你的电脑出现以上情况&#xff0c;建议你使用我这种方法&#xff08;万不得已&#xff09; 解决方法 原因就是因为当时你的电脑在激活的时候是选择了组织性激活的&#xff0c;所以才会不管怎么搞&#xff0c;都无法摆脱组织的控…

发布以太坊测试网络中的第一笔交易

1.安装以太坊钱包 要想发送发布以太坊测试网络中的第一笔交易&#xff0c;首先需要创建一个管理账户的钱包&#xff0c;这个钱包可以理解为管理私钥的容器&#xff0c;具体按照步骤为&#xff1a;打开Chrome浏览器应用商店搜索MetaMask&#xff0c;选择对应的钱包添加至Chrome…

提升API文档编写效率,Dash for Mac是你的不二之选

在编写和开发API文档的过程中&#xff0c;你是否经常遇到查找困难、管理混乱、效率低下等问题&#xff1f;这些都是让人头疼的问题&#xff0c;但现在有了Dash for Mac&#xff0c;一切都将变得简单而高效。 Dash for Mac是一款专为API文档编写和管理设计的工具&#xff0c;它…

SpringBoot 中使用JPA

最近忙里偷闲&#xff0c;想写一点关于JPA的东西&#xff0c;另外也加深下对JPA的理解&#xff0c;才有了此篇博文。 一、JPA JPA &#xff08;Java Persistence API&#xff09;Java持久化API&#xff0c;是一套Sun公司Java官方制定的ORM 规范&#xff08;sun公司并没有实现…

【Java】对象内存图多个对象同一内存地址

目录 学生类 单个对象内存图 多个对象指向同一个内存地址 学生类 Student.java如下&#xff1a; package com.面向对象;public class Student {String name;int age;public void work() {System.out.println("开始敲代码...");} }StudentDemo.java如下&#xff…

MAC地址震荡,STP震荡,OSPF路由协议震荡

目录 一.MAC地址震荡 原因 二层环路 三层环路 避免三层环路的方法 1.最小化路由汇总 2.null0路由 二.STP震荡 原因 三.路由协议震荡 OSPF路由协议震荡 BGP路由震荡 一.MAC地址震荡 原因 MAC地址表震荡也称为MAC地址漂移&#xff0c;是由网络环路造成的&#xff1…

【数据结构篇】堆

文章目录 堆前言基本介绍认识堆堆的特点堆的分类堆的操作堆的常见应用 堆的实现JDK 自带的堆手动实现堆 堆 前言 本文主要是对堆的一个简单介绍&#xff0c;如果你是刚学数据结构的话&#xff0c;十分推荐看这篇文章&#xff0c;通过本文你将对堆这个数据结构有一个大致的了解…

No164.精选前端面试题,享受每天的挑战和学习

🤍 前端开发工程师(主业)、技术博主(副业)、已过CET6 🍨 阿珊和她的猫_CSDN个人主页 🕠 牛客高级专题作者、在牛客打造高质量专栏《前端面试必备》 🍚 蓝桥云课签约作者、已在蓝桥云课上架的前后端实战课程《Vue.js 和 Egg.js 开发企业级健康管理项目》、《带你从入…

怒刷LeetCode的第20天(Java版)

目录 第一题 题目来源 题目内容 解决方法 方法一&#xff1a;回溯算法 方法二&#xff1a;permute方法 方法三&#xff1a;交换法 第二题 题目来源 题目内容 解决方法 方法一&#xff1a;回溯算法 方法二&#xff1a;递归和交换 方法三&#xff1a;二维列表 第三…

NLP 04(GRU)

一、GRU GRU (Gated Recurrent Unit)也称门控循环单元结构,它也是传统RNN的变体,同LSTM一样能够有效捕捉长序列之间的语义关联&#xff0c; 缓解梯度消失或爆炸现象&#xff0c;同时它的结构和计算要比LSTM更简单,它的核心结构可以分为两个部分去解析: 更新门、重置门 GRU的内…

苹果iPhone手机使用草柴返利APP查询领取淘宝天猫京东优惠券如何取消关闭粘贴商品链接时的弹窗提示?

使用苹果手机在淘宝或京东复制商品链接&#xff0c;到草柴APP粘贴时总是弹窗提示&#xff0c;如何关闭苹果手机粘贴弹窗的提示&#xff1f; 苹果手机如何关闭粘贴弹窗提示&#xff1f; 1、在草柴APP内&#xff0c;点击底部「我的」接着点击「系统设置」进入&#xff1b; 2、进…

【数据结构和算法】--N叉树中,返回某些目标节点到根节点的所有路径

目录 一、前言二、具体实现及拓展2.1、递归-目标节点到根节点的路径数据2.2、list转换为tree结构2.3、tree转换为list结构 一、前言 这么多年工作经历中&#xff0c;“数据结构和算法”真的是超重要&#xff0c;工作中很多业务都能抽象成某种数据结构问题。下面是项目中遇到的…

MDK报错:Undefined symbol assert_failed报错解决策略

MDK报错&#xff1a;Undefined symbol assert_failed报错解决策略 &#x1f3af;&#x1fa95;在全网搜索相关MDK编译报错:Error: L6218E: Undefined symbol assert_param (referred from xxx.o). ✨有些问题看似很简单&#xff0c;可能产生的问题是由于不经意的细节原因导致。…

16.PWM输入捕获示例程序(输入捕获模式测频率PWMI模式测频率和占空比)

目录 输入捕获相关库函数 输入捕获模式测频率 PWMI模式测频率和占空比 两个代码的接线图都一样&#xff0c;如下 测量信号的输入引脚是PA6&#xff0c;信号从PA6进来&#xff0c;待测的PWM信号也是STM32自己生成的&#xff0c;输出引脚是PA0。 需要配置电路连接图示如下&…

Grafana 开源了一款 eBPF 采集器 Beyla

eBPF 的发展如火如荼&#xff0c;在可观测性领域大放异彩&#xff0c;Grafana 近期也发布了一款 eBPF 采集器&#xff0c;可以采集服务的 RED 指标&#xff0c;本文做一个尝鲜介绍&#xff0c;让读者有个大概了解。 eBPF 基础介绍可以参考我之前的文章《eBPF Hello world》。理…

TrustRadius 评论:为什么 Splashtop 优于 LogMeIn

在当今日益数字化的格局中&#xff0c;远程访问和远程支持工具不仅方便而且至关重要。无论对于居家办公人员&#xff0c;还是对于提供远程支持的 IT 专家&#xff0c;能够安全高效地访问远程系统已成为以技术为导向的日常生活的主要内容。 Splashtop 和 LogMeIn 是远程领域的两…

FFmpeg 命令:从入门到精通 | 查看帮助文档

FFmpeg 命令&#xff1a;从入门到精通 | 查看帮助文档 FFmpeg 命令&#xff1a;从入门到精通 | 查看帮助文档FFmpeg 库模块ffmpeg 命令查看帮助文档ffplay 命令查看帮助文档ffprobe 命令查看帮助文档注意事项 FFmpeg 命令&#xff1a;从入门到精通 | 查看帮助文档 FFmpeg 库模…