Linux进程概念

文章目录

    • 简述进程基本概念
    • 描述进程- PCB
      • task_struct--PCB的一种
    • 查看进程
      • 通过系统目录查看
      • 通过ps命令查看
      • 通过系统调用获取进程的PID和PPID
      • fork函数创建子进程
      • fork函数的返回值
    • Linux进程状态
      • 浅谈四种状态
      • 如果频繁挂起状态 磁盘内部空间满了怎么办?
      • 运行状态—R
      • 可中断睡眠—S
      • 深度睡眠状态-D
      • 暂停状态-T
      • 暂停状态和休眠状态的区别
      • 僵尸状态- Z
      • 僵尸进程的危害
      • 孤儿进程
      • 进程优先级
      • 查看系统进程
      • PRI与NI
      • 通过top命令来更改进程的nice值
      • 通过renic命令修改进程的nice值
      • 四个重要概念】
    • 环境变量
      • 基本概念
      • 常见环境变量
      • 环境变量相关命令
      • 测试PATH
      • 环境变量的组织方式
      • 通过代码获取环境变量
      • 通过系统调用来获取环境变量
      • 所有环境变量通常是具有全局属性的
      • Linux下的全局变量和局部变量
    • 程序地址空间
    • 进程地址空间
      • 历史上计算机的程序空间
      • 现代计算机的程序空间
      • 地址空间是什么?
        • 程序编译连接形成一个可执行程序时,程序内部有地址么?
    • 为什么要存在进程地址空间?
    • 再谈新建状态,挂起状态

简述进程基本概念

当我们的代码进行编译链接后便会生成一个可执行程序,这个可执行程序本质是一个文件,是保存在磁盘上的。
当我们双击点这个可执行程序将其运行起来,本质上是将这个内存加载到内存中了。因为只有将数据和代码加载到内存中,CPU才能对齐进行语句执行,而一旦将这个程序加载到内存中,此时OS会为加载到内存中的数据和代码,b并创建创建一个进程控制块(PCB)——PCB控制块记录着当前进程的各种属性信息(还有进程地址空间,页表等内容)等数据结构

总之:进程可以简述成
进程 = 相关内核数据结构 + 对应的数据和代码。

描述进程- PCB

ps ajx 显示系统当中存在存在的进程。
Linux进程概念_第1张图片
操作系统是如何管理大量进程的?
简单来说就6个字:先描述,再组织。
操作系统作为管理者是不直接和被管理者(进程)直接沟通,当一个进程出现时,操作系统立马对其进行描述,之后转换为对进程的管理本质上是对这些描述信息的管理。
这些进程信息被放在一个的进程控制块的数据结构中,可以理解为进程属性的集合,我们将其称为PCB。
面对大量进程时,操作系统分别对其描述,形成了一个个进程控制块,并将这些进程控制块(PCB)以双链表的形式组织起来。
在这里插入图片描述
这样一来,操作系统只要具有这个双链表的头指针,就可以对这个双链表进行增,删,查,改操作。操作系统对描述信息的管理又转换为对这些数据结构的管理。

比如:创建一个进程,就是先将该进程的代码和数据加载到内存当中,操作系统对其描述形成PCB,并将这个PCB插入到双链表中,退出一个程序实际上是将双链表对应的pcb进行删除,操作系统再将对应的内存进行释放。

总的来说:操作系统对内存的管理实际上就是对相应的PCB形成的数据结构管理,例如:增,删,查,改等操作。

task_struct–PCB的一种

task_struct是PCB的一种类型,它是一种包含这进程信息的结构体。
task_struct内容分类:
标识符:描述进程的唯一标识符,用来区分进程。

**状态:**任务状态,推出代码,退出信号等

优先级: 相对于其他进程的优先级。

程序计数器(pc): 程序中即将被执行的下一条指令的地址。

内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针。

其他信息

查看进程

通过系统目录查看

ls proc(系统文件夹)
proc为系统文件夹,ls proc可以查看该文件夹的大量进程信息。
Linux进程概念_第2张图片
左边的蓝色数据其实是某一进程的PID,对应文件夹中记录着对应进程信息
ls /proc/PID 查看对应进程信息
例如:查看PID为1的进程信息
Linux进程概念_第3张图片

通过ps命令查看

ps ajx 查看所有进程信息
Linux进程概念_第4张图片

ps axj | head - 1 显示进程列表标头
grep -v grep 让grep的进程信息消失。
ps axj | head -1 && ps ajx | grep 要查找的文件名 | grep -v grep

Linux进程概念_第5张图片
在创建子进程的同时还会创建副进程bash,而bash的子进程就是帮助该进程运行的

ps axj | grep bash
查看还有bash的所有子进程.

Linux进程概念_第6张图片

通过系统调用获取进程的PID和PPID

通过fork函数,来获取进程的PID和PPID
Linux进程概念_第7张图片
通过ps命令查看,系统调用fork函数所获取的PID和PPID与我们查看的相同。
Linux进程概念_第8张图片

fork函数创建子进程

fork函数,它的功能就是创建一个子进程:
Linux进程概念_第9张图片
运行结果如下:
由图可知,第一行为子进程运行的,第二行的的PPID却是子进程的PID,由此可见第二行所显示的进程为子进程的副进程!
Linux进程概念_第10张图片
此时我们就有疑问了,父进程的代码和数据是从内存中获取的,那么子进程的代码和数据又是从哪获取的呢?
代码如下:
Linux进程概念_第11张图片
运行结果如下:

Linux进程概念_第12张图片
第一行和第二行的PID和PPID相同,可见它们是同一个进程。第三行代码代表的进程很明显是副进程。而第一行代码fork函数使用之前就有。
所以使用fork函数创建子进程,fork函数被调用之前的代码被父进程执行,fork之后的代码可以被父子进程同时执行。它们的代码虽然共享,但是它们的数据所占的内存空间是不同的,但是大体上相似的(由父进程拷贝而来)

注意:子进程创建之后父子进程谁先进行由操作系统的调度器所决定的!

fork函数的返回值

fork函数调失败 :返回 -1
fork函数调用成功:给父进程返回子进程的PID,给子进程返回0.
运行代码如图:
在这里插入图片描述
运行结果如下:
fork函数创建子进程后,父进程就会执行else if里面的代码,子进程就会执行if(id ==0)的代码。
Linux进程概念_第13张图片

Linux进程状态

一个进程从创建到消失整个时期,有时会在运行队列中运行,有时会等待外设设备的回应而等待进入休眠状态,进程随着活动的不同也是变化的,而进程状态就是描述多种状态的综合。
Linux进程概念_第14张图片

浅谈四种状态

新建:刚创建PCB,还没有进入运行队列的状态。

运行:task_struct结构体在运行队列中等待CPU资源,就叫做运行态。

阻塞:等待非CPU资源就绪(外设设备)的状态就做阻塞状态。
Linux进程概念_第15张图片

比如等待键盘输入的状态:
在这里插入图片描述

**挂起状态:**当内存不足的时候,OS通过置换进程的代码和数据到磁盘进程的状态就叫做挂起。
将长时间不执行的进程代码和数据(此时进程只有pcb)换出到磁盘。(磁盘swap分区)

如果频繁挂起状态 磁盘内部空间满了怎么办?

首先,当内存不足的时候,OS会将进程的代码和数据切换到磁盘中的swap分区,但是肯定不是很频繁的,因为频繁的换入换出会极大影响操作系统的运行效率。
当磁盘swap分区满了,就说明者操作系统面临着极大的内存压力,就只能宕机了。

运行状态—R

一个进程处于运行状态,并不意味着进程一定处于运行当中,运行状态表明一个进程要么在运行中,要么在运行状态等待里。也就是说,可以存在多个R状态的进程.
注意:所有处于运行状态,即可被调度的进程,都被放到运行队列中,当操作系统需要切换进程运行时,就直接在运行队列中选取进程运行。

可中断睡眠—S

一个进程处于可中断睡眠状态,意味着该进程正在等待某件事情的完成,,处于可中断睡眠的进程随时可以被唤醒,也可以杀掉。
例如:
我们正在不断循环输出一句代码如图:
Linux进程概念_第16张图片
可中断睡眠状态是可以被杀死的!
如图:
正在处于可中断睡眠状态的进程被 kill - 9 PID(目标进程PID)指令杀死。
在这里插入图片描述

运行结果和查看运行状态打如下:
我们明明是在运行程序,应该处于运行态,但是我们利用ps命令查的状态却是可中断睡眠S状态,为什么会+早就这样的结果?
在这里插入图片描述
因为在冯诺依曼体系中显示器属于外设,PCB在运行队列的时间远远小于在阻塞队列的时间(相比较而言),并且外设的效率较慢,所以我们所查的状态为S状态(浅度睡眠状态)。

深度睡眠状态-D

一个进程处于深度睡眠状态,表示该进程不会被杀掉,即便是当操作系统压力过大也不行,也不可被唤醒,只有当该进程得到外设设备的反馈才可以被杀死.

例如: 当一进程要求对磁盘进行写入操作,在磁盘的进行写入期间,该进程就在等待外设设备—磁盘的反馈,类似于阻塞状态,此时进程状态为深度状态D,在得到磁盘的反馈之前是不会被操作系统杀死的,以免数据丢失。(除非拔电源!)

暂停状态-T

kill -l 可以展现所有kill指示命令。
Linux进程概念_第17张图片
在Linux中,我们可以采用kill -9 目标进程PID 来使进程暂停。
Linux进程概念_第18张图片
kill -18 进程PID 让暂停的进程重新执行。
在这里插入图片描述

暂停状态和休眠状态的区别

休眠:阻塞状态,就是等待某中资源就绪。
暂停:只是单纯地把代码停住了。

例如:我们在使用gdb时在第7行处打了断点,实际gdb进程给当前进程在对应地断点上发送了kill -19命令使我的进程暂停,但是当前进程还是处于运行状态,他并没有和休眠状态一样等待某种资源,只是把这个进程停住了。

gdb调试时打上断点.
Linux进程概念_第19张图片
此时该进程打处于打暂停状态,t。Linux进程概念_第20张图片

僵尸状态- Z

一个进程已经退出,但是还是不被OS释放,而是处于一个被检测的状态。(一般由父进程或者操作系统调查的)
当一个程序在运行时把子进程退出,但是父进程没有读取到子进程到子进程退出时的返回代码时就会产生僵尸进程。

例如: fork函数创建父子子进程后打印5次后会推出,而父进程则会不断循环打印信息,5次之后,子进程推出了,父进程还在运行,但是父进程并没有读取子进程的退出信息,此时子进程就陷入僵尸状态。


```cpp
在这里插入代码片
```#include <stdio.h>
#include 
#include 
int main()
{
	printf("I am running...\n");
	pid_t id = fork();
	if(id == 0){ //child
		int count = 5;
		while(count){
			printf("I am child...PID:%d, PPID:%d, count:%d\n", getpid(), getppid(), count);
			sleep(1);
			count--;
		}
		printf("child quit...\n");
		exit(1);
	}
	else if(id > 0){ //father
		while(1){
			printf("I am father...PID:%d, PPID:%d\n", getpid(), getppid());
			sleep(1);
		}
	}
	else{ //fork error
	}
	return 0;
} 

再通过脚本指令检测该进程的变化:

while :; do ps axj | head -1 && ps axj | grep proc | grep -v grep;sleep 1;done

五次打印之后,子进程退出,父进程还在运行,子进程的状态就变味僵尸状态Z。
Linux进程概念_第21张图片

僵尸进程的危害

1:僵尸进程的必须一直维持下去,因为僵尸基础进程要告诉父进程的退出信息,如果父进程一直没有读取到僵尸进程的退出信息,那么子就将会一直维持在僵尸状态。

2:僵尸进程维护推出状态本身就是要用数据维护,也属于进程基本信息,及保存在PCB中,Z状态不退出,则操作系统需要PCB一直维护。

3:一个父进程创造了很多进程,但是一直处于僵尸状态而不能回收,此时就会造成内存资源的浪费,因为PCB也是一种数据结构,也要占用内存。

4:僵尸进程申请的资源无法进行回收,僵尸进程越多,实际可用的资源就越少,这就会造成内存泄漏等问题。

孤儿进程

在Linux进程中大多数是父子关系,如果子进程先退出,父进程没有接受子进程退出的指令,那么子进程就为僵尸进程。如果父进程先退出,子进程没有父进程进行内存资源的回收,那么子进程就为孤儿进程。

如图以下代码,子进程一直处于死循环状态,父进程在打印五次代码后就先退出。

#include 
#include 
#include 
int main()
{
	printf("I am running...\n");
	pid_t id = fork();
	if(id == 0){ //child
		int count = 5;
		while(1){
			printf("I am child...PID:%d, PPID:%d\n", getpid(), getppid(), count);
			sleep(1);
		}
	}
	else if(id > 0){ //father
		int count = 5;
		while(count){
			printf("I am father...PID:%d, PPID:%d, count:%d\n", getpid(), getppid(), count);
			sleep(1);
			count--;
		}
		printf("father quit...\n");
		exit(0);
	}
	else{ //fork error
	}
	return 0;
} 

经历五秒之后,子进程的PPID为1,说明当父进程退出后子进程已经被操作系统领养了。
Linux进程概念_第22张图片

进程优先级

什么是优先级?
优先级实际上就是获取某种资源的先后顺序,而进程优先级就是指cpu资源分配的先后顺序。

优先级存在的原因?
CPU的资源是有限的,当有多个进程时,cpu需要进程优先级来确定进程获取CPU资源的先后顺序来提高操作系统的整体效能。

查看系统进程

p -l 查看系统进程
在这里插入图片描述

UID:代表执行者的身份
PID: 代表这个进程的代号
PPID:代表在这个进程是由哪个进程衍生呢而来,即父进程的代号
PRI: 代表这个进程可被执行的优先级,其值越小越早被执行。
NI: 代表这个进程的nice值。

PRI与NI

NI代表的时nice值,表示其可被执行的优先级的修正数值。

PRI越小越快被执行,有了nice值后,PRI = PRI + nice;

NI的取值范围打为 -20到19。

通过top命令来更改进程的nice值

top指令进入页面,查看CPU被调用情况。
Linux进程概念_第23张图片
进入top页面后按r后输入你想改变进程的PID
例如 :我要改变优先级的目标进程为ps进程
Linux进程概念_第24张图片
输入你要改变的nice值:
比如当前输入的nice的值为1;
Linux进程概念_第25张图片
查看被调用的目标进程PID,发现PRI值已经发生改变,即为默认值80 + N;
当前ps进程的PRI值正好为80+1;
在这里插入图片描述

通过renic命令修改进程的nice值

输入renice 修改的nice值 目标进程PID

例如:我要修改的目标进程打为txt进程,修改的nice为-1
Linux进程概念_第26张图片
之后利用命令指令ps -al查看进程,发现txt进程的nice值为-1,该进程PRI进程值就变为79;
Linux进程概念_第27张图片
**注意:**当我们要修改的nice值为负数时,及提高该进程的优先级需要在renice指令前加上sudo命令。

四个重要概念】

竞争性:系统进程数目众多,而cpu资源只有少量,甚至只有1个,所以进程之间是有竞争属性的。为了高效完成任务,更合理地竞争相关资源,便有了优先级。

独立性:多进程运行,需要独享各中资源,多进程运行期间互不干扰。

并行多个进程多个CPU下分别,同时进行进行,称之为并行。
Linux进程概念_第28张图片

并发多个进程一个cpu下采用进程切换的方式,在一段时间内,让多个进程都得以推进,称之为并发。

时间片:给每个进程一个固定的时间享受cpu资源,当该进程时间到了,便会切换到其他进程。

抢占:当更高优先级进程到来之时抢占正在被cpu运行的优先级较低的进程。

出让:进程让出cpu资源

例如:
当cpu执行进程1到了超出时间片时,cpu会切换运行队列中的下一个进程继续运行,在这段时间内一个cpu得以让多个进程都得以得到推进,这样称之为并发。
Linux进程概念_第29张图片
切换:
**上下文数据:**寄存器中的临时数据。
我们知道当进程A暂时被切换的时候,需要进程A顺便带走自己的上下文数据!就是为了下次切换到A进程的时候能够按照之前的逻辑继续往后运行。
Linux进程概念_第30张图片

环境变量

基本概念

环境变量一般指在操作系统中用来指定操作系统运行环境的一些参数。

例如,我们编写的C/C++代码,在各个目标文件进行链接的时候,从来不知道我们所连接的东京库在哪里,但是照样可以连接成功,生成可执行程序,原因就是有相关环境变量帮助编译器进行查找

常见环境变量

PATH: 指定命令搜索路径

HOME: 指定用户的主工作目录(及用户登录到Linux系统中的默认所处目录)

SHELL: 当前Shell,它的值通常是/bin/bash

环境变量相关命令

echo $NAME NAME为带查看环境变量名
env 查看系统中的环境变量
set 查看系统中的所有变量
unset 清楚环境变量

测试PATH

例如:当要运行我们所写的程序时是要带上路径的,
在这里插入图片描述

但是:我们在Linux下输入命令行不用带上路径是因为这些命令的路径在环境变量所维护的搜索路径中,当我们使用ls命令时系统就会通过环境变量从左到右依次在各个路劲当中查找。
在这里插入图片描述
我们可以将我们的可执行程序的路径加入到PATH环境变量中之后,我们就可以不带路径直接写程序名就可以了。

export PATH=$PATH:运行程序目录
Linux进程概念_第31张图片

总结:系统运行一个命令必须要获取它的路径,系统命令的路径在环境变量中的搜索路径中,而我们的运行程序的路径不在PATH环境变量搜索路径中,要运行必须带上路径。

测试HOME:
任何一个用户在运行系统登录时都有自己的主工作目录(家目录),环境变量当中即保存着该用户的主工作目录
对于普通用户:
在这里插入图片描述
对于超级用户:
在这里插入图片描述

环境变量的组织方式

在系统中,环境变量的组织方式如下:
Linux进程概念_第32张图片
每个程序都会收到一张环境变量表,环境表是一个字符指针数组,每个指针指向一个以\0为结尾的环境字符串,最后一个字符指针为空。

通过代码获取环境变量

当我们调用main函数时实际给main函数传递了三个参数。

一:main函数的第三个参数接受的实际就是环境变量表,调用main函数时编译器通过第三个参数来获取系统的坏境变量
1:例如:我们通过命令行来输出第三个参数获取的环境变量。
Linux进程概念_第33张图片
运行结果如下:
成功打印出各个环境变量的值。
Linux进程概念_第34张图片
2:除了使用main函数的第三参数获取环境变量之外,我们还可以通过第三方全局变量environ获取。(本质上通过main()函数的第三个参数获取方式相同)
Linux进程概念_第35张图片
运行结果如下:
Linux进程概念_第36张图片
注意: libc中定义的全局变量environ指向环境变量表,environ没有包含在任何头文件中,所以在使用时要用extern进行声明。

二:main函数的前两个参数中,第二个参数是一个指向指针数组的地址,第一个参数是代表该指针数组个数的整数,代表数组的大小。
例如:
让我们通过代码来看看该指针数组的值:
Linux进程概念_第37张图片
运行结果如下:
可以看到该argv指向的指针数组存储着命令行参数的地址,而argv整数代表指针数组的个数。
在这里插入图片描述
三:命令行参数的意义
让我们看看测试代码:
Linux进程概念_第38张图片
运行结果如下:
我们发现该程序能根据不同的选项输出不同的结果
Linux进程概念_第39张图片
**总结:**命令行参数的意义就是让同一个程序,通过多个选项的方式来实现的不同的子功能,这些命令行参数是在当我们进行命令行调用的时候由当前bash(父进程)先拿到后由子进程继承的。

通过系统调用来获取环境变量

getenv函数可以根据所给的环境变量名,在系统所给的环境变量表中搜索,并返回一个指向相应指针的字符串地址。

例如:我们使用getenv函数获取环境变量PATH的值。
Linux进程概念_第40张图片
运行结果如下:
使用getenv函数成获取环境变量PATH的值:
在这里插入图片描述

所有环境变量通常是具有全局属性的

我们知道在命令行中运行命令时它的父进程都可以叫作当前bash。
例如:
我们运行一个进程查看该进程的PID和PPID
Linux进程概念_第41张图片
再打通过ps axj | head -1 && ps axj | grep PPID(父进程)查看:
可知当前bash的PID就是我们运行程序的父进程。
在这里插入图片描述
任何一条命令在命令行启动的时候,所有环境变量都是继承当前bash(父进程)
例如:
我们查找了一个不存在的环境变量class
Linux进程概念_第42张图片
运行结果:
该环境变量是不存在的。
在这里插入图片描述
我们向当前bash(父进程)中导出环境变量class后再运行时发现该进程有了环境变量。
Linux进程概念_第43张图片
总结: 所有父进程的环境变量都会被子进程继承,当命令行打运行的时候,会创建一个子进程,再次运行的时候该子进程就会继承class这个环境变量。所以每个子进程都会继承当前bash(父进程)的环境变量,即环境变量具有全局属性,所有子进程都会继承。

Linux下的全局变量和局部变量

在命令行中一般可以定义两种变量,一种为局部变量,一种为全局变量(环境变量)。
例如:我们在当前bash(父进程)临时定义了一个hello的局部变量,可以利用set | grep hello 命令查询。
在这里插入图片描述
但是我们运行程序创建子进程的过程中却无法从当前bash(父进程)继承。
Linux进程概念_第44张图片

在这里插入图片描述
当我们利用export命令在当前bash导出环境变量hello时,运行程序发现该进程可以继承当前bash的环境变量。
Linux进程概念_第45张图片
**总结:**对于在命令行中(当前bash)定义的局部变量不能被子进程继承,对于export定义的全局变量可以被子进程继承。

程序地址空间

以下为虚拟内存空间分布图:
Linux进程概念_第46张图片

在Linux操作系统中我们通过打印各种区域的变量地址来进行验证。
Linux进程概念_第47张图片
运行结果如下:我们发现各种区域变量的地址与分布图是相同的。
Linux进程概念_第48张图片
我们来看看一段奇怪的代码:
我们使用fork函数创建一个子进程,父进程执行else 语句休眠3秒,子进程先执行代码else if(id == 0),修改变量 g_val 为100后,父进程再进行继续执行以下代码。
Linux进程概念_第49张图片
运行结果如下:
我们发现父子进程的输出地址是一样的,但是变量内容却不一样.
在这里插入图片描述
总结:
1:变量内容不一样,所以父子进程绝对输出的变量绝对不是同一个变量,但是两个变量的输出地址确实一样的,说明这两个输出地址绝对不是物理地址。
2:在Linux地址下,这种地址叫做虚拟地址。我们在C/C++所看到的地址,全部都是虚拟地址。

进程地址空间

其实对于我们之前所说“程序地址空间”是不准确的,准确应该来说是“进程地址空间”。

历史上计算机的程序空间

原来操作系统是直接访问物理内存,将可执行程序加载导物理程序,操作系统在调度时会从众多进程中通过数据结构的方式打选择一个进程让CPU执行。
但是如果进程一的地址为野指针(地址是任意的)的话,打cpu有可能会去访问进程三的物理内存,进而有可能将进程三的内存修改或者读取,这样做法特别不安全,还会违反了进程的独立性。
Linux进程概念_第50张图片

现代计算机的程序空间

为了保持进程的独立性,现代计算机提出了以下方式。
当我们的进程被cpu访问时,cpu根据虚拟地址,然后通过映射找到特定的物理内存进行访问。当虚拟地址为非法地址时(如野指针), cpu就无法通过虚拟地址空间映射为物理内存,所以就保护了物理内存。
Linux进程概念_第51张图片

地址空间是什么?

地址空间是一种内核数据结构,它里面包含着各个区域的划分,在Linux当中进程地址空间具体由结构体 struct mm_struct 实现。在结构题mm_struct中,记录这各个特定的区域的划分,分别为start和and。在程序的执行过程中,进栈出栈时也会发生各个区域范围变化,本质上就是对stat和end标记值 ± 特定的范围即可。
Linux进程概念_第52张图片
进程地空间和页表(用户级)每个进程都私有一份,只要保证每一个进程的页表映射的是物理内存的不同区域,就能做到进程之间不会相互干扰,从而保证进程的独立性。

一:例如
我们曾经谈过的代码,如当子进程修改全局变量g_val之后,父进程再继续执行,此时输出值是不同的,输出地址确实相同的。这是为什么呢?
Linux进程概念_第53张图片
父子进程被创建,子进程就会继承父进程的相关属性,其中包括了地址空间和页表。当子进程尝试修改去全局变量g_val时,为了保持进程的独立性,操作系统会将原来的内存空间重新拷贝一份,然后将子进程的的页表进行修改,映射到操作系统新开辟的物理空间中。完成更改之后,父子进程的地址空间虽然相同,但是通过页表映射到的物理内存是不同的区域,所以我们看到的值是不同的,但是虚拟地址没有改变,即输出的地址也就是相同的。

Linux进程概念_第54张图片
例如二:
我们曾经用过fork函数,使用fork函数之后会有两个返回值,且这两个返回值的内容是不同的。这是为什么呢?

return 的本质:就是将id进行写入,写入之后父子进程都会执行相同的if else语句进行判断,其中父子进程也是拿着各自的ID。父子进程return 写入ID时发生了写时拷贝,所以父子进程其实存在于不同的物理内存,有着属于自己变量的物理空间。 只不过在用户层上,用同一个地址(虚拟地址)标识了。

程序编译连接形成一个可执行程序时,程序内部有地址么?

1:地址空间不仅是操作系统内部要遵守的,其实编译也要遵守。即编译器编译代码的时候,就已经给我们形成各个区域,代码区,字符常量区,初始化数区,未初始化数据区等,并且采用和Linux内核中一样的编址方式,给每一个变量,每一行代码都进行了编址,所以程序在编译的时候,每一个字段早已有了一个虚拟地址。
2:每次调用函数时,每一个程序都会保留着调用函数的地址(虚拟地址),当程序加载到物理内存中(数据和代码的拷贝),操作系统在物理内存中会给每一个函数定义一个物理内存地址。
总结:在编译完毕时,将各种范围代码程序的start和end会对应填充到进程地址空间的各种代码区。可执行程序每一个变量,每一个函数都有地址(虚拟地址),加载时将每一个变量的虚拟地址填充到页表的左侧,将程序加载到物理内存中操作系统所分配的物理地址加载到页表的右侧建立映射关系。所以cpu能够根据变量名(本质也是虚拟地址),找到特定区域的虚拟地址,根据映射关系找到对应的物理内存进行访问。
Linux进程概念_第55张图片

为什么要存在进程地址空间?

1:凡是非法的访问,或者是映射,操作系统都会识别,并且终止该进程。
防止虚拟地址为野指针,干扰其他进程的物理内存。这有效保护了物理内存的所有合法数据,包括各个进程,以及内核相关的有效的数据。

2:
(1): 在我们的物理内存中,可以打实现对程序是数据代码进行任意位置的加载,物理内存的分配和进程的管理没有任何关系。这样内存管理模块和基础南横管理模块就完了解耦合。
Linux进程概念_第56张图片
(2): 我们在C,C++语言上用new,malloc申请空间的时候,本质上是在虚拟内存中申请的。因为,有进程地址空间的存在,当用户上层申请空间时,其实是在进程地址空间上申请的,此时物理内存空间并不会为我们申请的任何内存,也不会构建页表映射关系。 只有当我们真正对物理内存进行访问的时候,才会执行申请内存的相关算法,构建页表映射关系。
总2(1)(2):
因为有了进程地址空间,操作系统可以采取延迟分配内存的策略,只有当我们访问物理内存才会分配,以防止申请内存后却不尽快用的情况。来提高整机的使用效率,(此时申请和访问呢物理内存的时间间隙,操作系统可以分配给其他物理空间),在这个过程中,用户,包括基础进程是零感知的。

因为有了进程地址空间,进程所要访问的物理内存中的代码和数据,可能并没有出现在物理内存中,同样的,也可以让不同的进程映射到不同的物理内存中,进程的独立性,由进程地址空间 + 页表的方式体现。并且各个基础进程也不知道其他进程的存在,这是独立性的第二个体现。

3: 因为物理内存理论上是可以任意加载,在物理内存中几乎所有的数据和代码都是乱序的,但是因为有页表的存在,它可以将地址进程地址空间上的虚拟地址和物理地址进行映射,那么在进程的视角来说,所有的内存分布都是有序的(如代码区,已初始化区,未初始化区…等等),进程地址空间和页表的存在可以让进程的内存有序化。

总概:因为进程地址空间的存在,每个进程都认为自己独占内存,并且虚拟内存中各个区域都是有序的,进程通过页表映射到不同的区域,进行物理内存访问,每个进程都不知道其他进程的存在,进而实现进程的独立性!

再谈新建状态,挂起状态

1:新建状态: 在极端条件下,一个进程只有内核结构(地址空间,页表)创建了,此时进程却不会被cpu通过页表映射关系访问物理内存中的代码和数据,程序的代码和数据也没有加载到内存中,此时的状态为新建状态。

2: 挂起状态: 我们知道,当某个进程在等待外设设备的回应的状态叫做阻塞状态。当内存不足时,如果长时间不会再被cpu执行,操作系统会将进程的数据和代码换出物理内存,而存入磁盘的swap分区,当时此时页表的右侧记录着该进程磁盘的位置代码或数据,当cpu通过进程空间执行该代码的时候,能够通过页表映射到swap分区,此时操作系统会将磁盘的代码和数据换入到物理内存中进行访问。

Linux进程概念_第57张图片

你可能感兴趣的:(Linux,linux,服务器,unix)