【Linux篇】进程控制

📌 个人主页: 孙同学_
🔧 文章专栏:Liunx
💡 关注我,分享经验,助你少走弯路!
在这里插入图片描述

1. 进程创建

1.1 fork函数

linuxfork函数是非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。

#include <unistd.h>
pid_t fork(void);
返回值:⾃进程中返回0,⽗进程返回⼦进程id,出错返回-1

进程调用fork,当控制转移到内核中的fork代码后,内核做:

  • 分配新的内存块和内核数据结构给子进程
  • 将父进程部分数据结构内容拷贝至子进程
  • 添加子进程到系统进程列表当中
  • fork返回,开始调度器调度
    在这里插入图片描述
    当一个进程调用fork之后,就有两个二进制代码相同的进程。而且它们都运行到相同的地方。但每个进程都将可以开始它们自己的旅程,看如下程序。
int main( void )
{pid_t pid;printf("Before: pid is %d\n", getpid());if ( (pid=fork()) == -1 )perror("fork()"),exit(1);printf("After:pid is %d, fork return %d\n", getpid(), pid);sleep(1);return 0;
} 
运⾏结果:
[root@localhost linux]# ./a.out
Before: pid is 43676
After:pid is 43676, fork return 43677
After:pid is 43677, fork return 0

这里看到了三行输出,一行before,两行after。进程43676先打印before消息,然后它有打印after。另一个after消息有43677打印的。注意到进程43677没有打印before,为什么呢?如下图所示
在这里插入图片描述
所以,fork之前父进程独立执行,fork之后,父子两个执行流分别执行。
注意fork之后,谁先执行完全由调度器决定。

1.2 写时拷贝

通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷备的方式各自一份副本。具体见下图:
在这里插入图片描述
上图显示父进程代码段在自己的页表中是只读的,包括我们以前定义的字符常量区,代码是不可写的,可是数据段为什么也是只读的?起始在我们的父进程还没创建子进程前,代码段是只读的没问题,但是数据段对应的映射关系,可能有一百个一千个映射地址,这些映射地址的权限实际上是读写的,但一旦创建了子进程,操作系统就会把数据段的权限也改成只读的。 然后后面的父子进程,比如说子进程尝试对它的数据进行写入,当它写入时,操作系统就会发现你要访问的数据,第一,数据是合法的,因为虚拟地址物理地址都有,而且它发现访问的区域是数据段,如果是代码段肯定在start_code,end_code这个区间里面,如果是数据段肯定在start_data,start_end这个区间里面,发现你是数据段,而且页表的映射关系是正确的,但是发现数据段怎么是只读的,所以这时候操作系统就会出错,这种出错不是真的错了,是操作系统检测到一个用户对一个只读的区域进行写入,但操作系统经过检查发现它是数据段,而且是子进程,这时候操作系统就会触发写时拷贝。写时拷贝是通过设置页表的权限,让页表让操作系统出错的行为。让操作系统知道我们正在访问一个只读的区域,进而在错误的驱使之下让操作系统完成对应的写时拷贝这样的任务。

因为有写时拷贝技术的存在,所以父子进程得以彻底分离离!完成了进程独立性的技术保证!
写时拷贝,是一种延时申请技术,可以提高整机内存的使用率

为什么要写时拷贝?

  1. 减少子进程的创建时间
  2. 减少内存浪费

1.3 fork调用失败的原因

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

2. 进程终止

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

2.1 进程退出场景

  1. 代码运行完毕,结果正确
  2. 代码运行完毕,结果不正确
  3. 代码异常终止(退出码无意义)

2.2 进程常见退出方式

正常终止(可以通过 echo $? 查看最近进程的退出码):

  1. main返回(main函数结束表示进程结束,其他函数只表示自己函数调用完成)
  2. 调用exit(status)(任何地方调用exit表示进程结束,并返回给父进程bash子进程的退出码)
  3. _exit:终止一个调用进程(相当于谁调用它,它把谁“弄死”)
2.2.1 exit函数
#include <unistd.h>
void exit(int status);

exit最后也会调用_exit,但在调用_exit之前,还做了其他工作:

  1. 执行用户通过atexiton_exit定义的清理函数。
  2. 关闭所有打开的流,所有的缓存数据均被写入
  3. 调用_exit
    在这里插入图片描述
2.2.2 _exit函数
#include <unistd.h>
void _exit(int status);
参数:status 定义了进程的终⽌状态,⽗进程通过wait来获取该值

说明:虽然statusint,但是仅有低8位可以被父进程所用。所以_exit(-1)时,在终端执行$?发现返回值是255。

2.2.3 exit和_exit的区别:

exit是c语言提供的,_exit是系统提供的
进程如果exit退出的时候,exit()会进行缓冲区的刷新
进程如果exit退出的时候,_exit不会进行缓冲区的刷新

库函数和系统调用是上下层的关系,库函数没有进程终止能力,只能调用系统调用,操作系统给它提供的进程终止的接口它才能终止进程,所以exit的底层封装了_exit,所以我们之前谈论的缓冲区一定不在操作系统的内部,而是库缓冲区(c语言提供的缓冲区)

异常退出

  • ctrl + c,信号终止
2.2.4 退出码

退出码在Linux中通常用来表示命令执行后的结果,0表示成功,非0表示不同的错误类型
Linux Shell 中的主要退出码:

退出码说明
0成功(命令正常执行)
1一般性错误(如参数错误、文件未找到、权限不足等)
2Shell 内置命令误用(如语法错误、未找到命令等)
126权限问题(命令不可执行,如缺少执行权限)
127命令未找到(Shell 找不到指定命令)
130进程被 Ctrl+C 终止(SIGINT 信号)
141进程被 SIGHUP 信号终止(如终端关闭)

3. 进程等待

3.1 进程等待必要性

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

3.2 进程等待的方法

3.2.1 wait方法

如果等待子进程,子进程没有退出,父进程就会阻塞在wait调用处(相当于scanf)
在这里插入图片描述

在这里插入图片描述

#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int* status);
返回值:成功返回被等待进程pid,失败返回-1。
参数:输出型参数,获取子进程退出状态,不关⼼则可以设置成为NULL
3.2.2 waitpid方法
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)((status>>8)&0xFF): 若WIFEXITED⾮零,提取子进程退出码。(查看进程的退出码)options:默认为0,表⽰阻塞等待WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等
待。若正常结束,则返回该子进程的ID。
  • 如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息。
  • 如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。
  • 如果不存在该子进程,则立即出错返回。
    在这里插入图片描述
3.2.3 获取子进程status
  • waitwaitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。
  • 如果传递NULL,表示不关心子进程的退出状态信息。
  • 否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。
  • status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特位):
    在这里插入图片描述
3.2.4 阻塞与非阻塞等待
  • 进程的阻塞等待方式:
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
int main(void)
{pid_t pid;if ((pid = fork()) == -1)perror("fork"), exit(1);if (pid == 0) {sleep(20);exit(10);}else {int st;int ret = wait(&st);if (ret > 0 && (st & 0X7F) == 0) { // 正常退出 printf("child exit code:%d\n", (st >> 8) & 0XFF);}else if (ret > 0) { // 异常退出 printf("sig code : %d\n", st & 0X7F);}}
}
测试结果:
# ./a.out #等20秒退出
child exit code : 10
# ./a.out #在其他终端kill掉
sig code : 9
  • 进程的非阻塞等待方式:
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>
#include <vector>
typedef void (*handler_t)(); // 函数指针类型 
std::vector<handler_t> handlers; // 函数指针数组 
void fun_one() {printf("这是⼀个临时任务1\n");
}
void fun_two() {printf("这是⼀个临时任务2\n");
}
void Load() {handlers.push_back(fun_one);handlers.push_back(fun_two);
}
void handler() {if (handlers.empty())Load();for (auto iter : handlers)iter();
}
int main() {pid_t pid;pid = fork();if (pid < 0) {printf("%s fork error\n", __FUNCTION__);return 1;}else if (pid == 0) { // childprintf("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); // ⾮阻塞式等待 if (ret == 0) {printf("child is running\n");}handler();} while (ret == 0);if (WIFEXITED(status) && ret == pid) {printf("wait child 5s success, child return code is :%d.\n",WEXITSTATUS(status));}else {printf("wait child failed, return.\n");return 1;}}return 0;
}

4. 进程替换

我们先来看一段代码:
在这里插入图片描述
在这里插入图片描述
上面这种现象就叫做程序替换,也就是我自己的程序把系统当中的指令跑起来了。在程序替换的时候,并没有创建新的进程,只是把当前进程的代码和数据用新的进程的代码和数据覆盖式的进行替换。

  1. 问题一:“为什么我的程序运行结束了”,这段话没有在显示器上打印出来?
    答案是一旦程序替换成功就去执行新代码了,原始代码的后半部分已经不存在了
  • 有没有办法让后面的代码能继续执行?
    答案是有的,创建一个子进程,让子进程去做替换工作,让父进程继续执行后面的代码。
    在这里插入图片描述
    效果演示:
    在这里插入图片描述
    📌Tips:程序替换也能替换我们自己写的程序,就相当于一种加载器,可以加载各种程序,包括编译型的解释型的,程序替换本质上不会创建新的进程
    为什么不会影响父进程呢?
    a.进程具有独立性 b.数据和代码发生写时拷贝
  1. execl的返回值
    在这里插入图片描述
    execl函数只有失败返回值,没有成功返回值
    💦结论exec*系列的函数,不用做返回值判定,只要返回,就是失败

4.1 替换原理

在这里插入图片描述

4.2 替换函数

  1. int execl(const char *path,const char *arg,...),第一个参数表示程序+路径名,第二个参数有个口诀:命令行怎么写,我们就怎么传(当然传-al也是可以的),而把参数一个一个的传进来我们称之为list,类似于以链表的形式传给它,所以execl这个l就是llist的意思,execl函数的最后一个参数必须以NULL结尾,表明参数传递完成在这里插入图片描述

  2. int execlp(const char *file,cont char *arg,...),execlp当中的p表示PATH,所以第一个参数只需传要执行的程序名就行了,因为execlp会自动的在环境变量(PATH)里查找对应的命令,所以execlp一般执行系统级的命令。后面参数的传递和上面的相同
    在这里插入图片描述

  3. int execv(const char *path,char *const argv[]),首先它没有带p所以它的参数是path,所以同上上,这里的v就相当于vector,所以第二个参数就以数组的形式呈现了,所以就必须提供一个命令行参数表,就是指针数组,就是把ls -a -l整体放在数组里,一次性传递,这个表也必须以NULL结尾。所以我们以前执行的所有命令行参数都是父进程通过execv传给子进程的
    在这里插入图片描述

  4. int execvp(const char *path,char *const argv[]),有p所以不用带路径
    在这里插入图片描述

  5. int execvpe(const *file,char *const argv[],char *const envp[])这里的v表示以数组的方式传进来,p表示不用带路径,e表示环境变量,如果非要传递环境变量列表,要求:被替换的子进程使用全新的Env列表(自己写的)
    在这里插入图片描述
    若要以新增的方式传递环境列表呢?
    putenv表示哪个进程调用它,就在谁的环境变量表里新增一个环境变量(B是A的子进程,C是B的子进程,如果B在它的环境列表里导入了一个新的环境变量,A的环境列表里看不到,而C的环境列表里能看到)
    ➁如果我们就行用execvpe的方式呢?environ
    在这里插入图片描述
    表示把新增的环境变量添加到环境变量表里面去,然后把环境变量表的起始地址传给execvpe

  6. int execle(const *path,const *arg,...,char * const envp[])
    在这里插入图片描述

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

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

上面这些函数都是对系统调用进行了语言型的封装,最后都要调用系统调用execve,为什么要做语言封装呢?因为程序替换时要面对各种各样上层替换的场景。所以execve在man手册第2节
在这里插入图片描述
下图exec函数簇一个完整的例子:
在这里插入图片描述


👍 如果对你有帮助,欢迎:

  • 点赞 ⭐️
  • 收藏 📌
  • 关注 🔔

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

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

相关文章

HyperAD:学习弱监督音视频暴力检测在双曲空间中的方法

文章目录 速览摘要1. 引言2. 相关工作弱监督暴力检测双曲空间中的神经网络 3. 预备知识双曲几何切空间&#xff08;Tangent Space&#xff09;指数映射与对数映射&#xff08;Exponential and Logarithmic Maps&#xff09;3.1 双曲图卷积网络&#xff08;Hyperbolic Graph Con…

动态规划(6.不同路径II)

题目链接&#xff1a;63. 不同路径 II - 力扣&#xff08;LeetCode&#xff09; 解法&#xff1a; 本题为不同路径的变型&#xff0c;只不过有些地方有「障碍物」&#xff0c;只要在「状态转移」上稍加修改就可解决。 状态表示&#xff1a; 对于这种Γ路径类」的问题&#xf…

深度洞察:DeepSeek 驱动金融行业智能化转型变革

该文章为软件测评&#xff0c;不是广告&#xff01;&#xff01;&#xff01;&#xff01; 目录 一.金融行业的智能化转型浪潮​ 二.DeepSeek的核心技术剖析 1.DeepSeek 模型的金融智慧​ 2.实时联网搜索&#xff1a;把握金融市场脉搏​ 3.RAG 能力&#xff1a;铸就精准金…

蓝桥杯备考----》暴力枚举---金盏花

这道题&#xff0c;一共12位&#xff0c;给了后六位&#xff0c;我们只要枚举前六位就行了&#xff0c;当然如果是10的12次方的话&#xff0c;必须要开long long才可以存下&#xff0c;这点我们不要忘了 然后题目中又告诉了没有前导0&#xff0c;我们可以从100000开始枚举&…

RAG各类方法python源码解读与实践:利用Jupyter对RAG技术综合评测【3万字长文】

检索增强生成&#xff08;RAG &#xff09;是一种结合信息检索与生成模型的混合方法。它通过引入外部知识来提升语言模型的性能&#xff0c;从而提高回答的准确性和事实正确性。为了简单易学&#xff0c;不使用LangChain框架或FAISS向量数据库&#xff0c;而是利用Jupyter Note…

Python列表2

print("—————————— 列表的相关操作 ————————————")lst.append(x)在列表lst最后增加一个元素 lst.insert(index,x)在列表中第index位置增加一个元素 lst.clear()清除列表lst中所有元素 lst.pop(index)将列表lst中第index位置的元素取出&#xf…

华为OD机试-IPv4地址转换成整数(Java 2024 B卷 100分)

题目描述 存在一种虚拟 IPv4 地址 Q,由 4 小节组成,每节的范围为 0~255,以 # 号间隔。虚拟 IPv4 地址可以转换为一个 32 位的整数。例如: 128#0#255#255 转换为 32 位整数的结果为 2147549183(0x8000FFFF)1#0#0#0 转换为 32 位整数的结果为 16777216(0x01000000)现以字…

C语言复习笔记--数组

今天继续来浅浅推进一下C语言的复习,这次是数组的复习,话不多说,正文开始. 数组的概念 数组是⼀组相同类型元素的集合,一种自定义类型.数组中元素个数不能为0.数组分为⼀维数组和多维数组&#xff0c;多维数组⼀般⽐较多⻅的是⼆维数组. 下面从一维数组说起. 一维数组的创建和…

Canal 解析与 Spring Boot 整合实战

一、Canal 简介 1.1 Canal 是什么&#xff1f; Canal 是阿里巴巴开源的一款基于 MySQL 数据库增量日志解析&#xff08;Binlog&#xff09;中间件&#xff0c;它模拟 MySQL 的从机&#xff08;Slave&#xff09;行为&#xff0c;监听 MySQL 主机的二进制日志&#xff08;Binl…

《论语别裁》第01章 学而(31) 诗的人生

不过这句话研究起来有一个问题&#xff0c;是诗的问题。我们知道中国文化&#xff0c;在文学的境界上&#xff0c;有一个演变发展的程序&#xff0c;大体的情形&#xff0c;是所谓汉文、唐诗、宋词、元曲、明小说&#xff0c;到了清朝&#xff0c;我认为是对联&#xff0c;尤其…

笔记本运行边缘计算

笔记本电脑可以用来运行PCDN&#xff08;Peer-to-Peer Content Delivery Network&#xff09;服务。实际上&#xff0c;如果你有闲置的笔记本电脑&#xff0c;并且它具备一定的硬件条件和网络环境&#xff0c;那么它可以成为一个不错的PCDN节点。 运行PCDN的基本要求 硬件需求…

暗光增强技术研究进展与产品落地综合分析(2023-2025)

一、引言 暗光增强技术作为计算机视觉与移动影像领域的核心研究方向之一,近年来在算法创新、硬件适配及产品落地方面取得了显著进展。本文从技术研究与产业应用两个维度,系统梳理近三年(2023-2025)该领域的关键突破,并对比分析主流手机厂商的影像技术优劣势。 二、暗光增…

多维array和多维视图std::mdspan

多维数组 这个特性用于访问多维数组&#xff0c;之前C operator[] 只支持访问单个下标&#xff0c;无法访问多维数组。 因此要访问多维数组&#xff0c;以前的方式是&#xff1a; 重载operator()&#xff0c;于是能够以m(1, 2) 来访问第1 行第2 个元素。但这种方式容易和函数…

Python标准库之os模块常用方法

一、os模块简介 os模块是Python标准库中与操作系统交互的一个重要模块。它提供了非常丰富的方法来处理文件、目录以及与操作系统相关的操作&#xff0c;让我们可以编写跨平台的代码&#xff0c;无论是在Windows、Linux还是macOS系统上都能运行。 二、文件和目录操作 获取当前…

利用AI让数据可视化

1. 从问卷星上下载一份答题结果。 序号用户ID提交答卷时间所用时间来源来源详情来自IP总分1、《中华人民共和国电子商务法》正式实施的时间是&#xff08;&#xff09;。2、&#xff08;&#xff09;可以判断企业在行业中所处的地位。3、&#xff08;&#xff09;是指店铺内有…

K8S学习之基础三十五:k8s之Prometheus部署模式

Prometheus 有多种部署模式&#xff0c;适用于不同的场景和需求。以下是几种常见的部署模式&#xff1a; 1. 单节点部署 这是最简单的部署模式&#xff0c;适用于小型环境或测试环境。 特点&#xff1a; 单个 Prometheus 实例负责所有的数据采集、存储和查询。配置简单&…

【第14节】windows sdk编程:进程与线程介绍

目录 一、进程与线程概述 1.1 进程查看 1.2 何为进程 1.3 进程的创建 1.4 进程创建实例 1.5 线程查看 1.6 何为线程 1.7 线程的创建 1.8 线程函数 1.9 线程实例 二、内核对象 2.1 何为内核对象 2.2 内核对象的公共特点 2.3 内核对象句柄 2.4 内核对象的跨进程访…

Python简单爬虫实践案例

学习目标 能够知道Web开发流程 能够掌握FastAPI实现访问多个指定网页 知道通过requests模块爬取图片 知道通过requests模块爬取GDP数据 能够用pyecharts实现饼图 能够知道logging日志的使用 一、基于FastAPI之Web站点开发 1、基于FastAPI搭建Web服务器 # 导入FastAPI模…

uniapp工程中解析markdown文件

在uniapp中如何导入markdown文件&#xff0c;同时在页面中解析成html&#xff0c;请参考以下配置&#xff1a; 1. 安装以下3个依赖包 npm install marked highlight.js vite-plugin-markdown 2. 创建vite.config.js配置文件 // vite.config.js import { defineConfig } fro…

sass介绍

1、Sass简介 Sass 是一种 CSS 的预编译语言。它提供了 变量&#xff08;variables&#xff09;、嵌套&#xff08;nested rules&#xff09;、 混合&#xff08;mixins&#xff09;、 函数&#xff08;functions&#xff09;等功能&#xff0c;并且完全兼容 CSS 语法。Sass 能…