Linux --进程控制

news2025/6/12 16:47:30

  本文从以下五个方面来初步认识进程控制:

目录

进程创建

进程终止

进程等待

进程替换

模拟实现一个微型shell

进程创建

  在Linux系统中我们可以在一个进程使用系统调用fork()来创建子进程,创建出来的进程就是子进程,原来的进程为父进程。

  当进程调用fork以后就会进行分流,内核会分配新的内存块和内核数据结构给子进程,将父进程的部分数据结构内容拷贝到子进程,添加子进程到系统的进程列表之中。

   通常,⽗⼦代码共享,⽗⼦在不写⼊时,数据也是共享的,当任意⼀⽅试图写⼊,便以写时拷⻉的⽅式各⾃⼀份副本。因为有写时拷⻉技术的存在,所以⽗⼦进程得以彻底分离离!完成了进程独⽴性的技术保证!写时拷⻉,是⼀种延时申请技术,可以提⾼整机内存的使⽤率具体⻅下图

  fork之前⽗进程独⽴执⾏,fork之后,⽗⼦两个执⾏流分别执⾏。注意,fork之后,谁先执⾏完 全由调度器决定。fork的返回值在父子进程之中有不同的体现,父进程中会返回子进程的pid,子进程返回0,如果fork失败返回-1,这里我们用一段代码解释上面提到的一些概念。

  

  这里我们定义了一个全局变量gav,在没有fork以前我们会看到父进程的pid和gav等于100,fork以后我们通过不同的返回值能够达到不同分支完成不同任务的效果,这里能看到子进程已经将gav修改成了110,而父进程的gav没有改变,两个进程的gav地址一样是因为在各自的虚拟内存的地址,实际它们的物理地址已经发生了写时拷贝被分配了新的物理地址。

  需要注意的一点是,子进程会拷贝父进程的的代码,所以在结束if()分支后两个进程都会执行剩下的代码,而不要误解成为剩下的代码会由父进程执行。

  fork的常规用法:

  ⼀个⽗进程希望复制⾃⼰,使⽗⼦进程同时执⾏不同的代码段。例如,⽗进程等待客⼾端请求,⽣成⼦进程来处理请求。

  ⼀个进程要执⾏⼀个不同的程序。例如⼦进程从fork返回后,调⽤exec函数,这个在进程替换的时候会讲解。

进程终止

  进程终⽌的本质是释放系统资源,就是释放进程申请的相关内核数据结构和对应的数据和代码。

  进程退出有三个场景:1.代码运行完毕,结果正确。2.代码运行完毕,结果不正确。3.代码异常终止,返回异常信号。

  进程终止有三个方式:1.main()函数return  2.调用exit(),可以在代码任何地方,表示进程结束。 3._exit(),这是系统调用接口,exit()的就是底层调用这个接口。这三种方式结束进程都会有返回值,异常终止的进程只有异常信号,这些都会存在内核数据中,父进程可以等待获取。在linux命令行中我们可以使用echo $?来查看进程的退出码。注意这个退出码是程序员自己约定好的,一般来说进程成功运行返回0,其他非0返回则代表进程执行出现错误,异常信号是由系统中断进程再返回的。

  Linux Shell 中的主要退出码:

退出码 0 表⽰命令执⾏⽆误,这是完成命令的理想状态。
退出码 1 我们也可以将其解释为 “不被允许的操作”。例如在没有 sudo 权限的情况下使⽤yum;再例如除以 0 等操作也会返回错误码 1 ,对应的命令为 let a=1/0
130 SIGINT ^C )和 143 SIGTERM )等终⽌信号是⾮常典型的,它们属于128+n 信号,其中 n 代表终⽌码。
可以使⽤strerror函数来获取退出码对应的描述
  这里我使用kill -8 结束了这个进程,然后会打印一个退出信号,这是一个浮点数异常信号,信号代码为8,一般出现在除0错误中,当查看退出码的时候发现正好是128+8,即退出信号。

  当进程正常结束时就会返回我们约定好的退出码。
  另外说明一下exit()会刷新缓冲去再结束进程,而_exit会直接退出进程,不会刷新缓冲区,因为缓冲区是语言层面的,系统层面没有缓冲区。
  第一个是使用exit(),第二个使用_exit(),结果并没有打印i的值。

进程等待

  为什么要进行进程等待必要性?之前讲过,⼦进程退出,⽗进程如果不管不顾,就可能造成‘僵⼫进程’的问题,进⽽造成内存 泄漏。 另外,进程⼀旦变成僵⼫状态,那就⼑枪不⼊,“杀⼈不眨眼”的kill -9 也⽆能为⼒,因为谁也没有办法杀死⼀个已经死去的进程。最后,⽗进程派给⼦进程的任务完成的如何,我们需要知道。如⼦进程运⾏完成,结果对还是不对,或者是否正常退出。 ⽗进程通过进程等待的⽅式,回收⼦进程资源,获取⼦进程退出信息。、

  进程等待通常有两种方式,一个是pid_t wait(int* status),等待成功会返回进程Pid,失败则返回-1,status是一个输出型参数,如果不关心子进程退出状态可以将参数设为NULL。

另外一个是pid_ t waitpid(pid_t pid, int *status, int options)了

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

pid
Pid= -1 , 等待任⼀个⼦进程。与 wait 等效。
Pid> 0. 等待其进程 ID pid 相等的⼦进程。
status: 输出型参数
两个宏:WIFEXITED(status): 若为正常终⽌⼦进程返回的状态,则为真。(查看进程是
否是正常退出) WEXITSTATUS(status): 若 WIFEXITED ⾮零,提取⼦进程退出码。(查看进程的
退出码)
options: 默认为 0 ,表⽰阻塞等待,传入WNOHANG则为非阻塞等待,此时父进程可以完成自己的任务
WNOHANG: pid 指定的⼦进程没有结束,则 waitpid() 函数返回 0 ,不予以等
待。若正常结束,则返回该⼦进程的 ID

  这里我们用一个子进程提前结束但是没有及时被父进程等待回收的例子来说明wait

 pid_t id = fork();
    if(id == 0)
    {
        int cnt = 5;
        while(cnt--)
        {
            cout<<"子进程 pid:"<<getpid()<<endl;
            sleep(1);
        }
        exit(0);
    }
    else if(id>0)
    {
        sleep(10);
        pid_t rid;
        rid = wait(nullptr);
        if(rid>0)
        {
            cout<<"等待子进程成功 rid : "<<rid<<endl;
        }
        sleep(10);
    }

  当子进程结束以后父进程还需要等待5秒才会回收,此时子进程是僵尸状态,五秒以后等待成功,子进程被释放只有父进程

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

  如果传递NULL,表⽰不关⼼⼦进程的退出状态信息。
 
  否则,操作系统会根据该参数,将⼦进程的退出信息反馈给⽗进程。
  status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16
⽐特位):
  我们一般不会使用status位移以后获取退出状态,我们可以使用两个宏来获取进程结束的状态和退出码,需要注意的一点是即使异常退出例如下面代码除0错误,父进程也能成功等待子进程,因为子进程需要释放,否则就会造成内存泄漏!
异常退出时:

 
代码执行完毕退出时会返回我们约定好的退出码和进程的pid
  上面的代码使用的是阻塞等待,如果需要父进程在等待的同时执行一些其他的任务我们就需要用到 waitpid(pid,status,WNOHANG)来等待子进程。代码示例:
  这里使用一个循环来获取等待信息,当最后一个参数为WNHANG时,rid每次获取如果为0则表示子进程还在运行,当rid返回子进程的pid则代表等待成功,此时父进程可以不用阻塞在等待而完成其他的任务。
  了解了进程创建,进程终止,进程替换,我们可以利用这三点来完成一个简单的父进程一直增加数据,然后定时创建子进程进行备份数据的一个示例
vector<int> data;
void backup_data(vector<int>& data)
{
    string name =to_string( time(nullptr));//
    name += ".backup";
    FILE* fp = fopen(name.c_str(),"w");//以时间戳命名备份文件
    if(fp == nullptr)
        exit(1);
    string numdata;
    for(auto d : data)
    {
        numdata+=to_string(d);//将数据转换为string类型写入文件
        numdata+=' ';
    }
    fputs(numdata.c_str(),fp);
    fclose(fp);
    exit(0);
}
int main()
{
    int num = 0;
    while(++num)
    {
        data.push_back(num);//插入数据
        if(num %10 == 0)//每增加十次数据则进行一次备份
        {
            int status = 0;
            pid_t id = fork();
            if(id == 0)
            {
                backup_data(data);//子进程进行备份
            }
        pid_t rid = wait(&status);
        if(rid > 0 && WIFEXITED(status) && WEXITSTATUS(status) == 0)//等待子进程成功并且正常退出(退出码为0)
        {
            cout<<"备份成功!"<<endl;
        }
        else {
            cout<<"备份失败请注意!"<<endl;
        }
        }
        sleep(1);
    }
}

  运行结果: 

  

进程替换

  fork() 之后,⽗⼦各⾃执⾏⽗进程代码的⼀部分如果⼦进程就想执⾏⼀个全新的程序就需要进程的程序替换来完成这个功能。程序替换是通过特定的接⼝,加载磁盘上的⼀个全新的程序(代码和数据),加载到调⽤进程的地址空间中。

  替换原理:⽤fork创建⼦进程后执⾏的是和⽗进程相同的程序(但有可能执⾏不同的代码分⽀),⼦进程往往要调⽤⼀种exec函数以执⾏另⼀个程序。当进程调⽤⼀种exec函数时,该进程的⽤⼾空间代码和数据完全被新程序替换,从新程序的启动例程开始执⾏。调⽤exec并不创建新进程,所以调⽤exec前后该进程的id并未改变。这里用一个简单代码可以体现

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

  这些函数原型看起来很容易混,但只要掌握了规律就很好记。
  l(list) : 表⽰参数采⽤列表
  v(vector) : 参数⽤数组
  p(path) : 有p⾃动搜索环境变量PATH
  e(env) : 表⽰⾃⼰维护环境变量
  调用实例
char *const argv[] = {"ps", "-ef", NULL};
char *const envp[] = {"PATH=/bin:/usr/bin", "TERM=console", NULL};
execl("/bin/ps", "ps", "-ef", NULL);
// 带p的,可以使⽤环境变量PATH,⽆需写全路径
execlp("ps", "ps", "-ef", NULL);
// 带e的,需要⾃⼰组装环境变量
execle("ps", "ps", "-ef", NULL, envp);
execv("/bin/ps", argv);
// 带p的,可以使⽤环境变量PATH,⽆需写全路径
execvp("ps", argv);
// 带e的,需要⾃⼰组装环境变量
execve("/bin/ps", argv, envp);
  事实上,只有execve是真正的系统调⽤,其它五个函数最终都调⽤execve,所以execve在man⼿册 第2节, 其它函数在man⼿册第3节。这些函数之间的关系如下图所⽰。

模拟实现一个微型shell

  所以我们利用上诉的几个理论知识就可以手搓一个简易版的shell,我们首先要知道shell的工作原理其实就是:

  1.获取命令行 2.解析命令行 3.执行命令

  在此之前我们需要先实现shell循环打印我们的信息框 比如:[toutie40@VM-8-16-centos repos]$,这个信息框是由 [+用户名+@+主机名+当前地工作目录]

  此时就可以打印处理命令行信息了

  接下来我们需要实现地是获取命令行

  此时我们就可以获取用户输入地命令了

  然后我们需要将获取到地命令进行解析,将comman_buffer的字符分割到gargv的每一个元素中

  接着就可以正确解析命令了 

  下一步就是将我们解析的命令使用子进程调用需要的进程即可 

  然后我们就可以实现一个简易版的shell了!​​​​​

  但是​​它只能够正常调用一些系统,cd命令并没有调用成功,或者说调用了但是没有起效,这是因为调用的cd其实是更改了子进程的工作目录,并不能影响我们父进程shell的工作目录,所以这部分需要shell本身调用自身函数来实现功能的命令我们成为自建命令,那么我们可以使用chdir进行更改当前的工作目录,记得要将环境变量中的pwd一起修改,因为Linux中的cd命令也是这样实现的。

  通过检查解析后的命令是否与内建命令相同,如果相同则shell本身调用函数实现,命令行的pwd也要记得修改

  env,export等命令也是内建命令,我们在系统中export会添加加属于shell维护的env表里面,我们自己写的shell目前的环境变量表是直接继承shell进程的,我们应该也要维护自己的env表,那么这里用系统shell进程环境变量来创建我们自己的env表

  使用了自定义的环境变量表以后记得修改命令行获取pwd的使用自定义的环境变量表,替换程序也可以使用我们自定义的环境变量表

  

  至此我们就完成了一个"手搓的"shell,这个shell遵循了系统中shell的基本原理,有自己的环境变量表,能够实现一般命令和内建命令。通过这个案例我们能够知道为什么在export环境变量是一个内存级的修改,因为在每次重新启动shell以后就会重新加载环境变量,而环境变量是以参数的方式传给我们的替换程序从而实现全局变量。

总结

   函数和进程之间的相似性 exec/exit就像call/return⼀个C程序有很多函数组成。⼀个函数可以调⽤另外⼀个函数,同时传递给它⼀些参数。被调⽤的函数执⾏⼀定的操作,然后返回⼀个值。每个函数都有他的局部变量,不同的函数通过call/return系统进⾏通信。
  
  这种通过参数和返回值在拥有私有数据的函数间通信的模式是结构化程序设计的基础。Linux⿎励将这种应⽤于程序之内的模式扩展到程序之间。如下图

  ⼀个C程序可以fork/exec另⼀个程序,并传给它⼀些参数。这个被调⽤的程序执⾏⼀定的操作,然后通过exit(n)来返回值。调⽤它的进程可以通过wait(&ret)来获取exit的返回值。
  需要完整代码可以在我的gitee仓库中找到 study_code: 学习路上写的一些代码

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

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

相关文章

python执行测试用例,allure报乱码且未成功生成报告

allure执行测试用例时显示乱码&#xff1a;‘allure’ &#xfffd;&#xfffd;&#xfffd;&#xfffd;&#xfffd;ڲ&#xfffd;&#xfffd;&#xfffd;&#xfffd;ⲿ&#xfffd;&#xfffd;&#xfffd;Ҳ&#xfffd;&#xfffd;&#xfffd;ǿ&#xfffd;&am…

使用 SymPy 进行向量和矩阵的高级操作

在科学计算和工程领域&#xff0c;向量和矩阵操作是解决问题的核心技能之一。Python 的 SymPy 库提供了强大的符号计算功能&#xff0c;能够高效地处理向量和矩阵的各种操作。本文将深入探讨如何使用 SymPy 进行向量和矩阵的创建、合并以及维度拓展等操作&#xff0c;并通过具体…

html-<abbr> 缩写或首字母缩略词

定义与作用 <abbr> 标签用于表示缩写或首字母缩略词&#xff0c;它可以帮助用户更好地理解缩写的含义&#xff0c;尤其是对于那些不熟悉该缩写的用户。 title 属性的内容提供了缩写的详细说明。当用户将鼠标悬停在缩写上时&#xff0c;会显示一个提示框。 示例&#x…

零基础在实践中学习网络安全-皮卡丘靶场(第九期-Unsafe Fileupload模块)(yakit方式)

本期内容并不是很难&#xff0c;相信大家会学的很愉快&#xff0c;当然对于有后端基础的朋友来说&#xff0c;本期内容更加容易了解&#xff0c;当然没有基础的也别担心&#xff0c;本期内容会详细解释有关内容 本期用到的软件&#xff1a;yakit&#xff08;因为经过之前好多期…

Spring Cloud Gateway 中自定义验证码接口返回 404 的排查与解决

Spring Cloud Gateway 中自定义验证码接口返回 404 的排查与解决 问题背景 在一个基于 Spring Cloud Gateway WebFlux 构建的微服务项目中&#xff0c;新增了一个本地验证码接口 /code&#xff0c;使用函数式路由&#xff08;RouterFunction&#xff09;和 Hutool 的 Circle…

OPENCV形态学基础之二腐蚀

一.腐蚀的原理 (图1) 数学表达式&#xff1a;dst(x,y) erode(src(x,y)) min(x,y)src(xx,yy) 腐蚀也是图像形态学的基本功能之一&#xff0c;腐蚀跟膨胀属于反向操作&#xff0c;膨胀是把图像图像变大&#xff0c;而腐蚀就是把图像变小。腐蚀后的图像变小变暗淡。 腐蚀…

C# 求圆面积的程序(Program to find area of a circle)

给定半径r&#xff0c;求圆的面积。圆的面积应精确到小数点后5位。 例子&#xff1a; 输入&#xff1a;r 5 输出&#xff1a;78.53982 解释&#xff1a;由于面积 PI * r * r 3.14159265358979323846 * 5 * 5 78.53982&#xff0c;因为我们只保留小数点后 5 位数字。 输…

学校时钟系统,标准考场时钟系统,AI亮相2025高考,赛思时钟系统为教育公平筑起“精准防线”

2025年#高考 将在近日拉开帷幕&#xff0c;#AI 监考一度冲上热搜。当AI深度融入高考&#xff0c;#时间同步 不再是辅助功能&#xff0c;而是决定AI监考系统成败的“生命线”。 AI亮相2025高考&#xff0c;40种异常行为0.5秒精准识别 2025年高考即将拉开帷幕&#xff0c;江西、…

如何在网页里填写 PDF 表格?

有时候&#xff0c;你可能希望用户能在你的网站上填写 PDF 表单。然而&#xff0c;这件事并不简单&#xff0c;因为 PDF 并不是一种原生的网页格式。虽然浏览器可以显示 PDF 文件&#xff0c;但原生并不支持编辑或填写它们。更糟的是&#xff0c;如果你想收集表单数据&#xff…

初学 pytest 记录

安装 pip install pytest用例可以是函数也可以是类中的方法 def test_func():print()class TestAdd: # def __init__(self): 在 pytest 中不可以使用__init__方法 # self.cc 12345 pytest.mark.api def test_str(self):res add(1, 2)assert res 12def test_int(self):r…

【数据分析】R版IntelliGenes用于生物标志物发现的可解释机器学习

禁止商业或二改转载&#xff0c;仅供自学使用&#xff0c;侵权必究&#xff0c;如需截取部分内容请后台联系作者! 文章目录 介绍流程步骤1. 输入数据2. 特征选择3. 模型训练4. I-Genes 评分计算5. 输出结果 IntelliGenesR 安装包1. 特征选择2. 模型训练和评估3. I-Genes 评分计…

Mobile ALOHA全身模仿学习

一、题目 Mobile ALOHA&#xff1a;通过低成本全身远程操作学习双手移动操作 传统模仿学习&#xff08;Imitation Learning&#xff09;缺点&#xff1a;聚焦与桌面操作&#xff0c;缺乏通用任务所需的移动性和灵活性 本论文优点&#xff1a;&#xff08;1&#xff09;在ALOHA…

Springboot社区养老保险系统小程序

一、前言 随着我国经济迅速发展&#xff0c;人们对手机的需求越来越大&#xff0c;各种手机软件也都在被广泛应用&#xff0c;但是对于手机进行数据信息管理&#xff0c;对于手机的各种软件也是备受用户的喜爱&#xff0c;社区养老保险系统小程序被用户普遍使用&#xff0c;为方…

关键领域软件测试的突围之路:如何破解安全与效率的平衡难题

在数字化浪潮席卷全球的今天&#xff0c;软件系统已成为国家关键领域的核心战斗力。不同于普通商业软件&#xff0c;这些承载着国家安全使命的软件系统面临着前所未有的质量挑战——如何在确保绝对安全的前提下&#xff0c;实现高效测试与快速迭代&#xff1f;这一命题正考验着…

ABAP设计模式之---“简单设计原则(Simple Design)”

“Simple Design”&#xff08;简单设计&#xff09;是软件开发中的一个重要理念&#xff0c;倡导以最简单的方式实现软件功能&#xff0c;以确保代码清晰易懂、易维护&#xff0c;并在项目需求变化时能够快速适应。 其核心目标是避免复杂和过度设计&#xff0c;遵循“让事情保…

微软PowerBI考试 PL300-在 Power BI 中清理、转换和加载数据

微软PowerBI考试 PL300-在 Power BI 中清理、转换和加载数据 Power Query 具有大量专门帮助您清理和准备数据以供分析的功能。 您将了解如何简化复杂模型、更改数据类型、重命名对象和透视数据。 您还将了解如何分析列&#xff0c;以便知晓哪些列包含有价值的数据&#xff0c;…

USB Over IP专用硬件的5个特点

USB over IP技术通过将USB协议数据封装在标准TCP/IP网络数据包中&#xff0c;从根本上改变了USB连接。这允许客户端通过局域网或广域网远程访问和控制物理连接到服务器的USB设备&#xff08;如专用硬件设备&#xff09;&#xff0c;从而消除了直接物理连接的需要。USB over IP的…

算法笔记2

1.字符串拼接最好用StringBuilder&#xff0c;不用String 2.创建List<>类型的数组并创建内存 List arr[] new ArrayList[26]; Arrays.setAll(arr, i -> new ArrayList<>()); 3.去掉首尾空格

听写流程自动化实践,轻量级教育辅助

随着智能教育工具的发展&#xff0c;越来越多的传统学习方式正在被数字化、自动化所优化。听写作为语文、英语等学科中重要的基础训练形式&#xff0c;也迎来了更高效的解决方案。 这是一款轻量但功能强大的听写辅助工具。它是基于本地词库与可选在线语音引擎构建&#xff0c;…

SAP学习笔记 - 开发26 - 前端Fiori开发 OData V2 和 V4 的差异 (Deepseek整理)

上一章用到了V2 的概念&#xff0c;其实 Fiori当中还有 V4&#xff0c;咱们这一章来总结一下 V2 和 V4。 SAP学习笔记 - 开发25 - 前端Fiori开发 Remote OData Service(使用远端Odata服务)&#xff0c;代理中间件&#xff08;ui5-middleware-simpleproxy&#xff09;-CSDN博客…