【linux中进程相关的概念分析与总结】

在这里插入图片描述

一、计算机体系结构

开局一张图,我们对下面这张图片肯定不陌生。因为计算机、笔记本、服务器,大部分都遵守冯诺依曼体系。
在这里插入图片描述
而它们都是由硬件组成的,分为:
输入设备:如键盘、磁盘、网卡、话筒等等
输出设备:如显示器、磁盘、网卡、音响等等
存储器(内存):内存条
中央处理器(cpu):芯片
注意:
有了内存,cpu就不需要直接和外界打交道了,内存是体系结构的核心设备。
任何设备,在数据层面,基本优先和内存打交道。
cpu,在数据层面上,也直接和内存打交道。

二、操作系统

操作系统我们也不陌生,英语单词为Operator System,简写成为OS。
我们从是什么?为什么?怎么办?三个角度来分析操作系统。
操作系统是什么?
我们可以简单的认为操作系统是一款专门针对软硬件资源进行管理工作的软件。
那为什么要有操作系统呢?
因为计算机是由一个个硬件构成的,如果我们直接地和硬件进行交流,那样的成本实在是太高了,所以操作系统的功能从大方面来说可以分为:
对下:管理好软硬件资源(方式)
对上:给用户提供稳定的、高效的、安全的运行环境(目的)
操作怎么办?(怎么做)
管理:1. 决策 2. 执行 3. 先描述再组织
先描述:先描述被管理对象,如用struct结构体
再组织:将被管理对象使用特定的数据结构组织起来,如用链表或其他高效的数据结构。

三、进程的相关概念

1. 描述进程

我们要先明白一个事实:我们所有启动程序的过程,本质是在系统上创建进程。
而在系统中可能存在大量的进程,那操作系统怎么进行管理呢?那就是先描述,再组织。
所以在创建进程的时候,操作系统要为进程创建进程控制块(PCB),Linux操作系统下的PCB是: task_struct。
所以有了进程控制块,所有的进程管理任务与进程对应的程序毫无关系!!!
与进程对应的操作系统创建该进程的PCB强相关。
那tast_struct里面有什么内容呢?主要有以下几点:

1、标示符: 描述本进程的唯一标示符,用来区别其他进程。
2、状态: 任务状态,退出代码,退出信号等。(如main函数最后返回的0)
3、优先级: 相对于其他进程的优先级。
4、程序计数器: 程序中即将被执行的下一条指令的地址。
5、内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
6、上下文数据: 进程执行时处理器的寄存器中的数据。
7、I/ O状态信息: 包括显示的I/O请求,分配给进程的I/ O设备和被进程使用的文件列表。
8、记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
其他信息

2. 查看进程

好了,我们了解怎么描述进程了,那怎么查看进程呢?
我们有以下几种方式:
1、通过系统调用获取进程标示符:
getpid() 获取进程的pid
getppid()获取父进程的pid
图片如下:
在这里插入图片描述
2、指令的方式,如进程还没有结束,可以通过ps axj指令来获取
在这里插入图片描述
3、进程的信息还可以通过/proc 系统文件夹查看
在这里插入图片描述

3. 进程的创建

通过系统调用fork()创建进程。
我们先来演示一下怎么利用fork创建进程:
在这里插入图片描述
fork本质是创建进程---->则系统里多了一个进程---->则与进程相关的内核数据结构+进程的代码和数据在系统里多了一份。
1、所以默认情况下,子进程会“继承”父进程的代码和数据;
代码方面:fork之后,子进程和父进程代码是共享的!!(代码是不可修改的,代码只有一份)
数据方面:默认情况下,数据也是共享的,不过需要考虑修改的情况!
进程是具有独立性的(如QQ、微信互不干扰),所以为了维护进程的独立性,数据是通过“写时拷贝”来完成数据的独立性。
2、子进程的内核数据结构task_struct也会以父进程为模板,初始化子进程的tast_struct(PCB)。
创建子进程的意义:
我们是创建了子进程,我们创建子进程是为了什么呢?难道就是为了和父进程干一样的事情吗?
当然不是,要通过返回值不同来干不同的事情,即可以通过if else 分流来做不同的事情。
(fork之后,父子进程无法判断谁先执行)。
fork的返回值:
失败<0
成功:给父进程返回子进程的pid 给子进程返回0
展示如下:
在这里插入图片描述
如何理解fork成功有两个返回值??如何理解两个返回值的设置??
因为子进程的内核数据结构task_struct也会以父进程为模板,初始化子进程的tast_struct(PCB),
而返回值是数据,返回的时候会写入,这样也会发生写时拷贝。

4. 进程的状态

进程的状态信息在哪里?
在task_struct(PCB)中
进程状态的意义?
方便操作系统快速判断进程,完成特定的功能,比如调度,本质上是一种分类。
具体状态如下:
S/D: 当我们完成某种任务的时候,任务条件不具备,需要进程进行某种等待。
① S:休眠状态,浅度睡眠,可中断睡眠;
② D:深度睡眠 ,进程如果处理D状态不可被杀掉
③ R: 运行状态 ,不一定在占有CPU ;
④ T:暂停状态
⑤ X:死亡状态,回收进程资源==进程相关的内核数据结构 + 代码和数据
⑥ Z:僵尸状态
为什么要有僵尸状态????
因为要辨别退出死亡原因(保存进程退出的信息,在task_struct中)

注意几点:
① task_struct在运行队列种就可以叫运行状态,但在运行队列中并不一定正在执行,因为tast_struct可能在运行队列等待中。
② 不用认为进程只会等待CPU资源!!!!!!(如S/D状态可能在等待其它外部设备,如网卡)
③ 千万在不同的队列里,所处的状态是不一样的。
④ 我们把,从运行状态的task_struct,放到等待队列中,就叫挂起等待(阻塞)
从等待队列,放到运行队列,被CPU调度就叫做唤醒进程!!!

僵尸进程: 一个父进程利用fork创建子进程,如果子进程退出,而父进程没有利用wait 或者 waitpid 来获取子进程的状态信息,那么子进程的状态描述符依然保存在系统中。
孤儿进程: 一个父进程退出, 而它的一个或几个子进程仍然还在运行,那么这些子进程就会变成孤儿进程,孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集的工作。

5. 进程的优先级

我们为什么要有优先级呢?
因为资源太少,本质是分配资源的一种方式。优先权高的进程有优先执行权利。配置进程优先权对多任务环境的linux很有用,可以改善系统性能。
我们经常用ps -al 指令来查看进程的优先级:
在这里插入图片描述
它们的详细介绍如下:
1、PRI也还是比较好理解的,是进程(priority)的缩写,即进程的优先级,或者通俗点说就是程序被CPU执行的先后顺序,此值越小进程的优先级别越高。
2、那NI呢?就是我们所要说的nice值了,其表示进程可被执行的优先级的修正数值;
PRI值越小越快被执行,那么加入nice值后,将会使得PRI变为: PRI(new)=PRI(old)+nice
这样,当nice值为负值的时候,那么该程序将会优先级值将变小,即其优先级会变高,则其越快被执行。
3、所以,调整进程优先级,在Linux下,就是调整进程nice值
nice其取值范围是-20至19,一共40个级别。
nice值为什么是一个相对比较小的范围呢????
因为优先级再怎么设置,也只能是一种相对的优先级,不能出现绝对的优先级,否则会出现很严重的进程“饥饿问题”,调度器:较为均衡的让每一个进程享受到CPU资源。
4、需要注意一点的是,进程的nice值不是进程的优先级,他们不是一个概念,但是进程nice值会影响到进程的优先级变化。可以理解nice值是进程优先级的修正修正数据。

那怎么更改进程的nice值呢??
我们可以用top命令更改已存在进程的nice值:
进入top后按“r”–>输入进程PID–>输入nice值; 如下图所示:
在这里插入图片描述
并行和并发的区别:
并行:多个进程在多个CPU下分别,同时进行运行,这称之为并行。
并发: 多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发。

五、环境变量和本地变量

我们先来看看比较官方的说法:环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数。

我们先来看看这个问题:
命令,程序,工具…本质上都是一个可以执行的文件,而我们执行我们自己编写的test文件时,要带 ./test才能执行,因为这是系统帮我们确认对应的程序在哪里?
但是为何系统的命令不用带路径呢??
(因为环境变量 PATH)
我们先来查看环境变量特定的路径:echo $PATH 在这里插入图片描述
所以我们有几种方法可以让我们的程序(test) 像系统的指令一样不需要 ./test就可以执行:
第一种方法:把test拷进去命令的搜索路径中,不推荐,会污染别人的命令池。
第二种方法:把当前的路径添加到环境变量 命令的搜索路径中,
所以所谓的安装软件就是把软件拷贝到系统的环境变量特定的路径下。

第二种方法添加:export 指令
【export PATH=$PATH:/home/cjy/linux/test3_16 】(但是Xshell关掉就失效了)
在这里插入图片描述
所以我们对环境变量的更深一层的理解是:
语言上面定义环境变量本质是在内存中开辟空间(有名字)
不要去怀疑OS开辟空间的能力!!!
环境变量本质是OS在内存/磁盘文件中开辟的空间,用来保存系统相关的数据!!
如语言上 变量名+变量内容 即 a里面放10 相当于PATH里面放路径(/home/cjy/linux/test3_16)

常见的环境变量:
1、PATH:指定命令的搜索路径
2、HOME: 指定用户的主工作目录(即用户登陆到Linux系统中时,默认的目录)
3、SHELL:当前Shell,它的值通常是/bin/bash

获取环境变量的三种方式:
① 通过命令行参数:
在这里插入图片描述
②通过第三方变量environ获取:
在这里插入图片描述
③ 通过系统调用获取或设置环境变量:
在这里插入图片描述

环境变量通常是具有全局属性
在这里插入图片描述
我们再来验证环境变量通常是具有全局属性,如下图所示:
在这里插入图片描述
环境变量具有“全局属性”本质,环境变量可以被子进程继承下去,从而影响了整个“用户”系统。

环境变量和本地变量相关的命令
系统上还存在一种变量,是与本次登录有关的变量,只在本次登录有效(本地变量)。
① echo: 显示某个环境变量值
② export::设置一个新的环境变量
③ env::显示所有环境变量
④ unset: 清除环境变量
⑤ set::显示本地定义的shell变量和环境变量
它们的指令操作如下:
在这里插入图片描述

六、程序地址空间

我们在学习C语言的时候接触过程序地址空间,如下图所示:
在这里插入图片描述

但是这个程序地址空间是真正的物理内存吗?因为我们在以前学习语言的时候还没有接触过操作系统,无法真正的理解;下面我们先来看一段代码和执行结果:
在这里插入图片描述
那什么又是虚拟地址呢?怎么描述呢?
我们可以这样理解:地址空间本质是内核中一种数据结构类型,可以用结构体来描述
struct mm_struct{
//进程地址空间 }
它的划分图,如下图分析:
在这里插入图片描述
好了,我们了解这是虚拟内存了,但是问题又来了,那物理内存呢??我们在进程中最终还是要读取数据和代码的呀,怎么找到物理内存呢?
这就用到了页表和MMU了,关系如下图所示:
在这里插入图片描述
但是我们为什么要有虚拟地址空间呢?进程的数据和代码直接和物理地址打交道不香吗?要搞的这么复杂,其实这是有很多原因的,有以下几点:

1、通过添加一层软件层,有效地对进程操作内存进行风险管理(权限管理),
本质目的是为了保护物理内存以及各个进程的数据安全。
2、把内存申请和内存使用的概念在时间上划分清楚,通过虚拟地址空间,来屏蔽底层申请内存的过程;达到进程读写内存和操作系统进行内存管理操作,进行软件上的分离。
(比如你申请2000字节空间,你申请的是虚拟内存上的空间,如果你对这些空间不进行读写操作,那么操作系统就不会在物理空间上给你使用,等你进行读写操作的时候,操作系统才会在物理空间上给你开辟2000字节空间。)
3、站在CPU和应用层的角度,进程统一可以看做统一使用4GB空间,而且每一个空间区域的相对位置是比较确定的;
操作系统最终这样设计的目的,达到了一个目标:每一个进程都认为自己是独占系统资源的!(进程是具有独立性的!)
所以我们学习进程和程序地址空间过后我们提起进程,就不是简简单单的认为进程就是把程序加载到内存中那么简单了;而是要把task_struct 、mm_struct 、页表联系起来;
进程=程序+代码+数据+操作系统维护进程的相关数据结构(PCB 、虚拟地址、页表)。

然后我们也可以回答上面那个问题,为什么变量地址相同但是值却是一样呢?;
因为每一个进程都有一份虚拟地址,而子进程的创建是以父进程为模板的,所以子进程的虚拟地址和父进程的虚拟地址也是一样的;但是因为变量内容改变了,这时就会发生写时拷贝,重新建立虚拟地址和物理地址的映射关系;最终结果就是子进程和父进程的虚拟地址虽然一样,但是对应的物理内存是不一样的!!!!

再针对这个问题,我们再来重新回答我们以前学习C语言的时候一个问题,那就是:
const char* str=“hello world”;其中为什么不能*str='A’呢?为什么不能改变?
我们以前的回答是:因为“hello world”存放在字符常量区,不能改变。
但是为什么不能改变呢?今天我们学习了操作系统和进程,重新回答这个问题:
字符常量区本质上是操作系统给你的权限只有读权限,没有写权限
并且所有的只读数据一般可以只有一份,因为这样操作系统维护一份的成本是最低的。
(比如:char * str = “hello world” 和char * s=“hello world” ; 当中str和s的地址是一样的)

猜你喜欢

转载自blog.csdn.net/m0_56311933/article/details/123542327