【Linux操作系统】:进程控制

目录

一、程序地址空间

1.C/C++中的程序地址空间

2.进程地址空间

进程地址空间概念

什么是地址空间?什么是区域划分?

为啥要有地址空间?

 地址空间的补充

二、进程创建

1.fork函数

2.写时拷贝 

3.fork常规用法

4.fork调用失败的原因

三、进程终止

1.进程终止的概念

 2.进程常见退出方法

3._exit函数 

4.exit函数

四、进程等待

1.什么是进程等待

2.为啥要进程等待

3.如何进行进程等待

1.wait方法

2.waitpid方法 

 3.获取子进程status

五、进程程序替换

1.实现一个简单的进程程序替换

2.进程程序替换原理 

 3.进程程序替换的函数

4.进程程序替换函数应用场景


一、程序地址空间

1.C/C++中的程序地址空间

我们在学习C/C++中我们知道这样的空间布局图!!!

我们如何去创建和访问变量呢?

本质:起始地址 + 偏移量(其实我们的变量的类型就是偏移量)

上面的这些是内存吗?不是内存!!!

我们下面来做一个小实验!!!

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int g_val = 0;
int main()
{pid_t id = fork();if(id < 0){perror("fork");return 0;}else if(id == 0)    { //childprintf("child[%d]: %d : %p\n", getpid(), g_val, &g_val);}else{     //parentprintf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val);}sleep(1);return 0;
}

我们发现,输出出来的变量值和地址是一模一样的,很好理解呀,因为子进程按照父进程为模版,父子并没有对变量进行进行任何修改。可是将代码稍加改动。 

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int g_val = 0;
int main()
{pid_t id = fork();if(id < 0){perror("fork");return 0;}else if(id == 0)    { //childg_val = 100;printf("child[%d]: %d : %p\n", getpid(), g_val, &g_val);}else{     //parentprintf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val);}sleep(1);return 0;
}

我们发现,父子进程,输出地址是一致的,但是变量内容不一样! 

 我们可以得出下面的结论?

OS必须负责将 虚拟地址 转化成 物理地址 。 


2.进程地址空间

进程地址空间概念

上面的图就可以说明问题,同一个变量,地址相同,其实是虚拟地址相同,内容不同其实是被映射到了不同的物理地址。 

那么我们如何去理解 虚拟地址相同,但是物理地址不同呢?

  1. 父进程有自己的虚拟地址,也有自己的页表。
  2. 子进程在被创建的时候,父进程也会将页表拷贝给子进程。
  3. 子进程在更改数据(g_val = 200)时,会发生写时拷贝,物理地址改变了,但是虚拟地址没有改变。

什么是地址空间?什么是区域划分?

 我们在创建进程的时候不仅要有 pcb,也要管理地址空间(先描述,在组织),有一个 struct mm_struct 的结构体。

为啥要有地址空间?

 

我们如何去理解 存在虚拟地址空间,可以有效的进行进程内存的安全检查呢?

我提一个问题,我们 常量区的变量 为啥不能修改呢?

我们页表中除了有映射外,还有权限的限制,当进程要修改常量区的变量时,直接在页表就没有权限。

 地址空间的补充

  1. 每个进程都有自己的页表。

二、进程创建

1.fork函数

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

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

当一个进程调用fork之后,就有两个二进制代码相同的进程。而且它们都运行到相同的地方。但每个进程都将可以开始它们自己的旅程,看如下程序: 

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>int main()
{printf("Before: %d\n", getpid()); pid_t id = fork();if(id < 0){perror("fork");return 0;}else if(id == 0)    { //childprintf("child: %d \n", getpid());sleep(2);}else{     //parentprintf("parent: %d \n", getpid());sleep(2);}return 0;
}

所以,fork之前父进程独立执行,fork之后,父子两个执行流分别执行。注意,fork之后,谁先执行完全由调度器决定。 

2.写时拷贝 

通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本。具体见下图:

父子进程代码共享,数据独有:当任意一方试图写入,便以写时拷贝的方式拷贝一份副本 

3.fork常规用法

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

4.fork调用失败的原因

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

 我们写一个多进程运行的一个场景

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>#define N 10typedef void (*callback_t)();                       // 函数指针void Worker()
{int cnt = 10;while (cnt){printf("I am child process, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);sleep(1);cnt--;}
}void createSubPorcess(int n, callback_t cb)
{int i = 0;for (i = 0; i < n; i++){sleep(1);pid_t id = fork();if (id == 0){printf("create child process success: %d\n", i);// childcb();exit(0);}}
}//__StartCTR();int main()
{createSubPorcess(N, Worker);// 只有父进程走到这里sleep(100);return 0; 
}

三、进程终止

1.进程终止的概念

main 函数的返回值可以被父进程获取的,用来判断子进程的干活的情况 。

查看上一个进程的退出码 

echo $?

我们父进程就可以通过这两个数字来判断子进程的退出情况 。

代码异常终止,退出码就没有意义了!!!


 2.进程常见退出方法

3._exit函数 

  1. _exit函数 是系统调用函数。
  2. _exit函数 在终止进程的时候,不会自动刷新缓冲区。

4.exit函数

  1. exit函数 是库函数。
  2. exit函数 在终止进程的时候,会自动刷新缓冲区。

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

  1. 执行用户通过 atexit或on_exit定义的清理函数。
  2. 关闭所有打开的流,所有的缓存数据均被写入。
  3. 调用_exit。

 使用_exit系统调用函数:

使用exit系统调用函数: 

 


四、进程等待

1.什么是进程等待

通过 wait/waitpid 的方式,让父进程(一般情况)对子进程进行资源回收等待过程!!!

2.为啥要进程等待

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

3.如何进行进程等待

1.wait方法

#include <iostream>
#include <cstdlib>
#include <cstdio>#include <sys/wait.h>
#include <unistd.h>void Worker()
{int *p = NULL;int cnt = 10;while (cnt){printf("I am child process, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt--);sleep(1);}
}
const int n = 10;int main()
{pid_t id = fork();if (id == 0){// childWorker();exit(1);}else{//  fatherpid_t rid = wait(NULL);if (rid == id){printf("wait success, pid: %d, rpid: %d \n", getpid(), rid);}}return 0;
}

1、进程等待可以回收僵尸进程 

2、如果子进程没有退出,那么父进程会一直阻塞等待,直到子进程僵尸了,wait自动回收,返回了。 

看下面结果图发现当父进程调用了waitpid函数时父进程就被阻塞了,阻塞期间当子进程运行完毕父进程才执行完毕,所以只有子进程退出了父进程才会退出,那么子进程就一定不是僵尸进程。

 


2.waitpid方法 

  • 如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息。
  • 如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。
  • 如果不存在该子进程,则立即出错返回。

#include <iostream>
#include <cstdlib>
#include <cstdio>#include <sys/wait.h>
#include <unistd.h>void Worker()
{int *p = NULL;int cnt = 10;while (cnt){printf("I am child process, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt--);sleep(1);}
}
const int n = 10;int main()
{pid_t id = fork();if (id == 0){// childWorker();exit(1);}else{// sleep(10);//  fatherint status = 0;pid_t rid = waitpid(id, &status, 0);printf("wait after\n");if (rid == id){// 我们不能对status整体使用printf("wait success, pid: %d, rpid: %d, exit sig: %d, exit code: %d\n", getpid(), rid, status&0x7F, (status>>8)&0xFF);if (WIFEXITED(status)){printf("child process normal quit, exit code : %d\n", WEXITSTATUS(status));}else{printf("child process quit except!\n");}}}return 0;
}


 3.获取子进程status

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


五、进程程序替换

添加一个环境变量

 

 

1.实现一个简单的进程程序替换

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>int main()
{printf("-------------------------------------------\n");execl("/usr/bin/ls","-l","-a",NULL);printf("-------------------------------------------\n");return 0;
}

 我们查看运行结果发现,进程程序替换后还有一个printf没有执行,为啥呢?我们下面来讲解一下进程程序替换的原理。

 


2.进程程序替换原理 

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

我们就可以解释 进程程序替换后还有一个printf没有执行,为啥呢?

 


 3.进程程序替换的函数

六种进程程序替换函数

程序替换的本质

 

1、execl 进程程序替换函数

 

2、execlp 进程程序替换函数 

 

 3、execle 进程程序替换函数 

 

4、execv 进程程序替换函数  

 

 5、execvp 进程程序替换函数  

 

  6、execve 进程程序替换函数  

 

根据上面的规律我们可以总结出如下:

 

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

  • v(vector) : 参数用数组。
  • p(path) :    有p自动搜索环境变量PATH。
  • e(env) :     表示自己维护环境变量。
  • l(list) :      表示参数采用列表。
#include <unistd.h>
int main()
{char *const argv[] = {"ps", "-ef", NULL};char *const envp[] = {"PATH=/bin:/usr/bin", "TERM=console", NULL};// 带l的,表示list,表示参数采用列表execl("/bin/ps", "ps", "-ef", NULL);// 带p的,可以使用环境变量PATH,无需写全路径execlp("ps", "ps", "-ef", NULL);// 带e的,需要自己组装环境变量execle("ps", "ps", "-ef", NULL, envp);// 带v的,表示vector,表示参数采用数组execv("/bin/ps", argv);// 带p的,可以使用环境变量PATH,无需写全路径execvp("ps", argv);// 带e的,需要自己组装环境变量execve("/bin/ps", argv, envp);exit(0);
}

 

4.进程程序替换函数应用场景

例如我们执行一个python的文件 

 test.py

print("hello python")

code.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>char* const argv[] = {"ls","-l","-a",NULL
};int main()
{pid_t id = fork();if(id == 0){// childprintf("I am a child , my PID:%d\n",getpid());execl("/usr/bin/python3","python3","test.py",NULL);// execl("/usr/bin/ls","ls","-l","-a",NULL);// execlp("ls","ls","-l","-a",NULL);// execv("/usr/bin/ls",argv);//execvp("ls",argv);printf("-------------------------------------\n");exit(0);}  else {pid_t rid = waitpid(-1,NULL,0);if (rid > 0){printf("wait succes!! PID:%d\n",rid);}}return 0;
}

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

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

相关文章

Linux 常用命令 1

Tips&#xff1a;终端热键ctrl shift 放大终端窗口的字体 ctrl - 缩小终端窗口的字体 注意区分大小写 查阅命令帮助信息&#xff1a; 1&#xff09;--help command –help(两个减号) 显示command命令的帮助信息 2&#xff09;man man command 查阅command命令的使…

MyEclipse打开文件跳转到notepad打开问题

问题描述 windows系统打开README.md文件&#xff0c;每次都需要右键选择notepad打开&#xff0c;感觉很麻烦&#xff0c;然后就把README.md文件打开方式默认选择了notepad&#xff0c;这样每次双击就能打开&#xff0c;感觉很方便。 然后某天使用MyEclipse时&#xff0c;双击RE…

matlab实现神经网络检测手写数字

一、要求 1.计算sigmoid函数的梯度&#xff1b; 2&#xff0e;随机初始化网络权重&#xff1b; 3.编写网络的代价函数。 二、算法介绍 神经网络结构&#xff1a; 不正则化的神经网络的代价函数&#xff1a; 正则化&#xff1a; S型函数求导&#xff1a; 反向传播算法&…

【Linux】Linux工具学习之git

&#x1f525;博客主页&#xff1a; 小羊失眠啦. &#x1f3a5;系列专栏&#xff1a;《C语言》 《数据结构》 《C》 《Linux》 《Cpolar》 ❤️感谢大家点赞&#x1f44d;收藏⭐评论✍️ 文章目录 前言一、账号注册1.1 GitHub与Gitee 二、构建仓库三、安装git 四、配置git五、克…

详解库和程序运行过程

我最近开了几个专栏&#xff0c;诚信互三&#xff01; > |||《算法专栏》&#xff1a;&#xff1a;刷题教程来自网站《代码随想录》。||| > |||《C专栏》&#xff1a;&#xff1a;记录我学习C的经历&#xff0c;看完你一定会有收获。||| > |||《Linux专栏》&#xff1…

lvgl 窗口 windows lv_port_win_visual_studio 版本 已解决

不知道的东西&#xff0c;不知道lvgl窗口。一切从未知开始 lv_port_win_visual_studio 主分支 对应的分支 v7版本更新git submodule update --init --recursive同步 lvgl代码随后打开 visualSudio 打开.sln 文件 编译 release模式 允许 一切正常代码部分

考研数学基础差,跟宋浩?

宋浩老师的课程我大一的时候听过&#xff0c;是我大一高数的救命恩人&#xff01; 不过&#xff0c;考研的针对性很强&#xff0c;基础差听宋浩老师的课程不如直接听汤家凤老师的课程&#xff0c;因为汤家凤老师的课程是专门为考研数学设计的&#xff0c;针对性很强。 汤家凤老…

K8S之DaemonSet控制器

DaemonSet控制器 概念、原理解读、应用场景概述工作原理典型的应用场景介绍DaemonSet 与 Deployment 的区别 解读资源清单文件实践案例 概念、原理解读、应用场景 概述 DaemonSet控制器能够确保K8S集群所有的节点都分别运行一个相同的pod副本&#xff1b; 当集群中增加node节…

Django之Celery篇(一)

一、介绍 Celery是由Python开发、简单、灵活、可靠的分布式任务队列,是一个处理异步任务的框架,其本质是生产者消费者模型,生产者发送任务到消息队列,消费者负责处理任务。 Celery侧重于实时操作,但对调度支持也很好,其每天可以处理数以百万计的任务。特点: 简单:熟悉…

监控系统Prometheus--入门

文章目录 Prometheus特点易于管理监控服务的内部运行状态强大的数据模型强大的查询语言PromQL高效可扩展易于集成可视化开放性 Prometheus架构Prometheus 生态圈组件架构理解 Prometheus的安装安装Prometheus Server上传安装包解压安装包修改配置文件 prometheus.yml 安装Pushg…

Halcon 3D 平面拟合(区域采样、Z值过滤、平面拟合、平面移动)

Halcon 3D 平面拟合(区域采样、Z值过滤、平面拟合、平面移动) 链接:https://pan.baidu.com/s/1UfFyZ6y-EFq9jy0T_DTJGA 提取码:ewdi * 1.读取图片 ****************

数组三(冒泡排序、二分查找)

目录 冒泡排序算法 冒泡排序的基础算法 冒泡排序的优化算法 二分法查找 冒泡排序算法 冒泡排序是最常用的排序算法&#xff0c;在笔试中也非常常见&#xff0c;能手写出冒泡排序算法可以说是 基本的素养。 冒泡排序的基础算法 冒泡排序算法重复地走访过要排序的数列&#…

2024-03-22 问AI: 介绍一下深度学习中的 sigmoid函数

文心一言 Sigmoid函数是深度学习中常用的一种激活函数&#xff0c;主要用于将神经元的输出值压缩到0和1之间。这种特性使得sigmoid函数在二分类问题中特别有用&#xff0c;因为输出值可以被解释为属于某个类别的概率。 sigmoid函数的数学表达式为&#xff1a; σ(x) 1 / (1…

Pytorch CUDA Reflect Padding 算子实现详解

CUDA 简介 CUDA&#xff08;Compute Unified Device Architecture&#xff09;是由NVIDIA开发的一种并行计算平台和应用编程接口&#xff08;API&#xff09;&#xff0c;允许软件开发者和软件工程师使用NVIDIA的图形处理单元&#xff08;GPU&#xff09;进行通用计算。自2007…

NodeJS 集群模块: 为App创建集群实例

NodeJS 集群模块: 为App创建集群实例 目录 NodeJS 集群模块: 为App创建集群实例Node.js 集群介绍:终极扩展策略使用集群模块开始扩展 Node.js扩展 Node.js 的两种集群策略使用集群受益的 Node.js 应用程序示例没有集群的 Node.js:不可伸缩的原则集群操作:一个扩展的 Node.js…

Redis 配置与优化

目录 一、Redis 介绍 1.1、关系数据库与非关系型数据库 1&#xff09;关系型数据库 2&#xff09;非关系型数据库 3&#xff09;非关系型数据库产生背景 1.2、Redis 基础 1&#xff09;Redis 简介 2&#xff09;Redis 安装部署 3&#xff09;配置参数 1.3、Redi…

【聊一聊】三种工厂模式的创建

三种工厂模式的创建 今天终于星期五了,最近由于碰上一个需求,中间涉及Oracle改国产数据库的改造,好家伙,差点没把我忙坏了 不过今天终于有空啦!~哈哈哈 这篇本应该是上周就结束的,但是拖到今天,我们就今天进行结束 (还有一件快乐的事情,就是我遇见自己喜欢的人啦!嘻嘻) 好啦!~话…

【AI】发现一款运行成本较低的SelfHosting语言模型

【背景】 作为一个想构建局域网AI服务的屌丝,一直苦恼的自然是有限的资源下有没有对Spec要求低一点的SelfHosting的AI服务框架了。今天给大家介绍这款听起来有点希望,但是我也还没试验过,感兴趣的可以去尝试看看。 【介绍】 大模型生成式AI与别的技术不同,由于资源要求高…

Linux第83步_采用“Linux内核定时器”点灯以及相关API函数

“Linux内核定时器”是采用“系统时钟”来实现的。它不是周期性运行的&#xff0c;一旦发生超时就会自动关闭。如果想要实现周期性定时&#xff0c;那么就需要在定时处理函数中重新开启定时器。 Limux内核使用全局变量jiffies来记录“系统从启动以来的系统节拍数”&#xff0c…

路由器的端口映射能实现什么?

路由器的端口映射是一项重要的网络配置功能&#xff0c;它可以帮助实现局域网内外的设备之间的通信。通过端口映射&#xff0c;我们可以在公网上访问局域网内的设备&#xff0c;方便的进行远程访问、共享文件和资源等操作。 什么是端口映射&#xff1f; 在介绍端口映射之前&am…