Linux进程概念-程序员宅基地

技术标签: 运维  linux  服务器  

目录

一、冯诺依曼体系结构

二、操作系统 (Operator System)

什么是操作系统?

系统调用接口和库函数的概念

三、进程的概念

1.进程的产生

2.进程的描述 —— PCB(Linux中为struct tast_struct)

3.查看进程

 4.进程标识符的获取

5.子进程创建的 系统调用接口 —— fork()

6.fork()为什么会有两个返回值? 

7.父子进程的运行顺序

四、进程状态 

1.Linux中的进程状态

2.僵尸状态zombie 

(1)什么是僵尸状态?

(2)为什么要存在僵尸进程状态

(3)僵尸进程的危害

3.孤儿进程 

五、进程优先级(priority)

 1.进程优先级的概念

2.查看系统的进程

 3.如何修改进程优先级

4.其他进程的概念认知 

六、环境变量(environment variables)

1.常见的环境变量

2.查看环境变量的方法 

3.和环境变量相关的命令

4.让可执行程序不带路径执行的方法(PATH)

方法1:我们吧bz.exe扔到$PATH的任意一个路径下。

 方法2:将当前路径整个放到PATH中。

5.环境变量的组织形式

6.获取环境变量的方法  

方法1:用main函数获取环境变量

方法2:通过第三方environ获取。

 方法3:通过系统调用接口getenv获得环境变量

 7.环境变量的全局性

七、进程地址空间 

1.进程地址空间的概念

2.进程地址空间的实验

3.对进程地址空间的理解

4.进程地址空间的本质

5.关于进程地址空间的几个问题 

1.问:当我们的程序,在编译时,形成的可执行程序没有加载到内存中称为进程时,在程序内部有地址吗?

 2.问:什么是挂起?

3.问:为什么要存在进程地址空间?


一、冯诺依曼体系结构

我们的计算机室友一个个的硬件组成的

输入设备有:键盘、鼠标、写板、扫描仪、摄像头、网卡、磁盘、话筒...等等

存储器就是 内存

输出设备有:显示器、音响、磁盘、网卡...等等

CPU(中央处理器)包括 运算器(进行算数运算-加减乘除 和 逻辑运算- if判断等等)和 控制器(CPU响应外部事件,协调外部就绪事件,例如,拷贝数据到内存)

:既然有输入设备和输出设备,CPU还能进行数据的处理,那为什么还要有存储器(也就是内存)呢?

:我们知道 运算速度上 : CPU&&寄存器 > 内存 > 磁盘/SSD > 光盘 > 磁带 

这里就是典型的木桶原理——计算机的速度由最慢的决定,而这里的CPU很快,外设又很慢,所以不让它们拖慢CPU速度。

总结:存储器的存在可以预先把一些数据load到内存中(让软件参与进来)

注意:所有设备(也就是外设和CPU)都只能直接与内存打交道  

二、操作系统 (Operator System)

 

什么是操作系统?

操作系统包括了 内核(进程管理、内存管理、文件管理、驱动管理)和 函数库、shell程序等其他程序 。

操作系统本质是一个基本的程序集合,也就是说,操作系统是与硬件交互,管理所有软硬件资源的软件,从而为用户程序(应用程序)提供一个良好的执行环境。

注意:关于管理

 1.管理者只要拿到被管理者的数据,就可以管理了,并不一定要直接与被管理者接触(校长(OS)、辅导员、我)

2.管理就是 先描述(类对象)、后组织(对数据结构的操作) 

 也就是说计算机管理硬件,可以这样:用struct结构体将硬件内容描述起来,再用链表或者其他高效的数据结构来管理这些struct 结构体。

系统调用接口和库函数的概念

1.在开发角度,操作系统会对外表现为一个整体,但是会暴露自己的部分接口来供上层使用,这部分由操作系统提供的接口,就叫做系统调用接口(System call)

也就说系统调用接口实质上就是系统用C语言提供的函数(其职能类似于银行柜台)

2. 系统调用接口在使用上功能比较基础,对用户的要求相对来说也比较高,因此,开发者对部分系统调用接口进行了适度的封装,从而形成了库(lib)和shell外壳程序等,从而有利于更上层用户或者开发者进行二次开发。

三、进程的概念

1.进程的产生

上面我们已经说过,操作系统管理进程的方式是先描述再组织。并且我们还提到过,在进入CPU之前,会先把数据load到内存中(——可执行程序的本质上就是代码和数据)。这里在内存中的数据,就是进程。

简单来说就是:一个可执行程序只要被加载到了内存中,就要改名为进程

 其实,我们自己启动了一个软件程序,本质上就是启动了一个进程。在Linux中,运行一条命令,例如./xxx(运行程序命令),此时,就是在系统层面创建了一个进程!

Linux系统是可以同时加载多个程序的,Linux是可能同时存在大量的进程在系统中的(OS,内存)

2.进程的描述 —— PCB(Linux中为struct tast_struct)

Linux肯定是必须要管理系统和内存中的进程的,其方式还是那句话——先描述,再组织。

a. 描述进程的方法将各个进程的属性全部封装到各个类对象中,我们称这些类对象叫做PCB(process control block进程控制块),然后再对这些类对象构成的数据结构(例如list)进行管理。

b.问:进程到底是什么呢?

答:在课本上是这样说的:程序的一个执行实例,正在执行的程序等等;在内核中有这样的观点:担当分配系统资源(CPU时间,内存)的实体;

而我们从进程的本质上来看:进程 = 对应的代码和数据 + 对应的 PCB 结构体

c.PCB的特性

那说了这么多,PCB到底是什么呢?内部到底有什么东西呢?

首先,我么需要知道的是:在不同的操作系统中,PCB的名字各不相同,例如在Linux中,PCB就是 struct task_struct {} 结构体。

总结:

1.在Linux中描述进程的结构体叫做 tast_struct 

2.task_struct 是Linux内核的一种数据结构,它会被装载到内存中并包含着进程的信息。

前面我们已经提到过,PCB可以看作是进程属性的集合,那么PCB其中就会有如下内容:

(1)标示符:描述本进程的唯一标示符,用来区别其他进程(类似于学号)

(2)状态: 任务状态,退出代码,退出信号等等

(3)优先级:相对于其它进程的优先级(优先级是来确定做的顺序,权限是来判断能否做)

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

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

(*6)上下文数据:进程执行时处理器的寄存器中的数据

(7)I/O状态信息:包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表

(8)记账信息:可能包括处理器时间总和,使用的时间总和、时间限制等(因为每个进程在CPU上处理是有时间限制的)

3.查看进程

(1)进程的信息都在 /proc 这个系统文件夹中 ,可以用ls /proc/ 的命令来查看进程。 

(2)但是上述方法,只能够笼统的查看所有进程,所以我们大部分情况下使用ps 指令和 top 指令来进行进程的查看:

ps —— 显示当前任务(进程)

ps axj——显示所有的任务(进程)

注意:我们常用下面这一串指令来根据关键词来查找进程:ps axj | head -1 && ps axj | grep xxx(PID)

(这里的&&代表:若左边的指令可以执行,那就两边指令全部执行)

top——显示系统中的所有进程

ls /proc/xxx(PID) ——显示当前所有的进程或PID对应的进程

 kill -9 PID —— 杀掉对应的进程(kill -9 是很猛的杀手)

 4.进程标识符的获取

 我们在上面的ps 、top等命令中已经知道可以通过查看进程的详细信息来获取进程的pid,那有没有办法直接在进程的代码中获取自己的pid呢?答案是肯定的。

想要获取当前进程的PID,就需要调用系统调用接口(System call)getpid 和 getppid了。

例:

#include <stdio.h>
#include <unistd.h> //getpid()......
#include <sys/types.h> //pid_t、size_t......

int main()
{
  printf("pid is %d\n",getpid());//当前进程的pid
  printf("ppid is %d\n",getppid());//父进程的pid
  return 0;
}

这并不难理解,pid我们知道,那ppid又是啥呢?父进程的pid?那父进程又是什么?

5.子进程创建的 系统调用接口 —— fork()

我们这里先初步认识一下fork的功能,在 Linux 中 man fork用手册查询一下。

(1)在fork()之后会变成了两个进程(执行流中),一个是父进程,一个是子进程。

注意:子进程只会执行fork()下面的代码!

但是这并不代表子进程中只有父进程fork()之后的代码,子进程中的代码无论是fork上面还是下面的,全部都是和父进程是一模一样的,只不过执行的起始位置是从fork()位置开始向下执行(子进程吧父进程的上下文数据中的跟执行位置有关的数据也拷贝过来了) 

(2)fork有两个返回值如果创建子进程失败会返回-1;创建成功,子进程返回0、父进程返回子进程的pid。

(3)父子进程的代码是共享的,数据是按写时拷贝的方式来拷贝的——如果要做修改,则子进程另开空间,拷贝一份;如果不用修改数据,则指向内存中的同一个数据。 

(4)但是我们使用fork创建子进程,肯定不是为了让它和父进程做同样的事情的,那该如何做到分流呢?

思路:运用第(2)点中所说的在子进程和父进程中的fork返回值不同这一特性,来进行分情况处理。

例有代码如下:

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
 int ret = fork();//创建子进程
 if(ret < 0)
 {
   perror("fork");//排查错误
   return 1;
 }
 else if(ret == 0)//fork成功,子进程返回0
 { //child
   printf("I am child : %d!, ret: %d\n", getpid(), ret);
 }
 else//fork成功,父进程返回子进程的pid
 { //father
   printf("I am father : %d!, ret: %d\n", getpid(), ret);
 }
 sleep(1);
 return 0;
}

事实上,我们通常也是用这个方法——if分流,来进行fork之后子进程和父进程工作的分配。

(5)需要注意的是,我们只要fork(),OS就会新建一个新的 task_struct ,而这个 task_struct 的内部属性,就会以父进程为模版。

同时这也带来一个疑问:当我们已经准备return的时候(例如fork要return时),我们的核心代码执行完了吗?

答:已经执行完了!

在操作系统和CPU运行某个进程,本质上就是从task_struct形成的队列中挑选一个task_struct,来执行它的代码进程调度——在task_struct的队列中选择一个进程的过程

总结:只要想到进程,就得想到它对应的task_struct

6.fork()为什么会有两个返回值? 

我们可以用推断的方法知道:

(1):因为fork开始的时候就已经产生了子进程,return这个动作,父进程和子进程都会做。

(2):返回两次,并不意味着保存了两次。

实际上:

(3)与进程的地址空间相关。return 的本质就是对 “int id = fork()”中的“id”的写入,由于发生了写时拷贝,父子进程的虚拟地址虽然相同(子进程拷贝的父进程的),但子进程会另私有一份代码和数据在物理内存中(页表映射到不同的物理地址上),也就是说父子进程的虚拟地址是相同的(我们只能看到虚拟地址),但是实际上的物理地址是不同的。这一点我会在后面详细讲解

7.父子进程的运行顺序

问:父子进程谁先运行呢?

答:不一定!因为谁先运行这是由操作系统的调度器决定的。

四、进程状态 

进程状态包括:

新建:字面义,就是新建在那,什么都不干。

 运行:进程对应的task_struct在运行队列中排队的状态就叫做运行态。

阻塞:系统中一定是存在各种资源的(不仅仅是CPU,还有许多诸如网卡、显卡、键盘等等许多设备),等待非CPU资源就绪的进程状态就叫做阻塞态

注意:等待CPU资源的排队的进程处于运行队列中 ,等待非CPU资源就绪的排队的进程就处于阻塞队列中。 

情景:当我么使用scanf函数的时候,如果我们一直不输入,会发生什么?

答:此时的进程状态就是阻塞状态,它在等待键盘输入数据就绪(等待键盘资源)。

另外,除了这些状态之外还有挂起状态:内存快要不足的时候,操作系统会将长时间不执行的进程的代码和数据(PCB不会动)换出到磁盘(SWAP),这些代码和数据被置换的进程,他们的进程状态就是挂起状态。 

1.Linux中的进程状态

 各个操作系统肯定是有差异的,在Linux中的进程有如下几种进程状态:

1.R 运行状态(running)——对应 运行:注意运行状态的进程不一定是在进行中的,它只是表明进程要么在运行中(CPU在处理),要么就是在运行队列中(等待CPU资源)

2.S睡眠状态(sleeping)——对应 阻塞:Linux中的sleep是interruptible sleep(可中断睡眠),在这个状态下的进程是可以接受到信号的,并随时可以打断。

3.D磁盘休眠状态(disk sleeping)——对应 阻塞:深度睡眠、不可被唤醒(uninterrruptible sleep),在这个状态的进程通常会等待IO的结束,但是当服务器压力过大的时候,OS会通过一定的手段,杀掉一些进程,来起到节省空间的作用。 

注意:

(1)当进程有关磁盘的读写,此时一般就会对应D状态 

(2)dd 这个命令可以显示D状态的进程有哪些。

4.T暂停状态(stopped):顾名思义,就是一个运行到一半暂停的进程的进程状态,这个状态常出现在调试过程中(打断点,运行到断点处停止),当然也可以使用 kill -19 + 对应的PID 这串命令来暂停对应进程。(也就是发送SIGSTOP信号,kill -18 +对应PID可以继续被暂停的进程)

5.X死亡状态(dead):又称为终止状态,这个状态只是个返回状态,只会存在一瞬,其作用就是告诉操作系统资源可以被回收了。 

6.Z僵尸状态(zombie):这个状态较为复杂,我们展开来讲。

2.僵尸状态zombie 

(1)什么是僵尸状态?

一个进程已经退出了,但是还不允许被OS释放、处于一个被检测的状态,这个状态就叫做僵尸状态。(PCB还在,但是代码和数据已经被回收了

而一般来说,进程的资源是由父进程或者OS回收的。因此,只要子进程退出,父进程还在运行,但是父进程没有读取子进程的状态,那子进程就会进入僵尸状态。

我们创建一个僵尸进程的例子:

#include <stdio.h>
#include <stdlib.h>
int main()
{
  pid_t id = fork();
 if(id < 0)
 {
   perror("fork");
   return 1;
 }
 else if(id > 0)
 {  //parent
   printf("parent[%d] is sleeping...\n", getpid());
   sleep(30);
 }
 else
 {
   printf("child[%d] is begin Z...\n", getpid());
   sleep(5);
   exit(EXIT_SUCCESS);
 }
 return 0;
}

 上面例子,创建了一个维持了三十秒的僵尸进程。

(2)为什么要存在僵尸进程状态

维持僵尸状态,其目的就是为了提醒父进程和OS来回收。

(3)僵尸进程的危害

我们从僵尸进程的概念的已经知道:如果父进程一直不读取子进程的状态,那(已结束的)子进程就会一直处于Z状态。僵尸状态需要一直维持下去来告诉父进程我执行的怎么样了,但是维护这个状态本身需要用数据来维护,也属于进程基本信息,所以保存在task_struct(PCB)中,会占用内存空间! 

3.孤儿进程 

比起上面所介绍的进程状态,孤儿进程很少会出现,并没有那么值得关注。

前面我们说过,子进程退出之后,会等待父进程或者OS来回收的状态就叫做僵尸状态。

那如果父进程先提前退出了呢?

答:如果父进程先退出了,那子进程就被称为“孤儿进程了”!

此时子进程必须被领养!!——一般由PID为1的“init”进程来领养,(init,系统本身)

 

为什么要被领养呢?

这个很容易回答:因为要有人来回收这个子进程的资源。

 例孤儿进程的制作:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
 pid_t id = fork();
 if(id < 0)
 {
   perror("fork");
   return 1;
 }
 else if(id == 0)
 {//child
   printf("I am child, pid : %d\n", getpid());
   sleep(10);//睡十秒,睡一半父亲嘎儿了
 }
 else
 {//parent
   printf("I am parent, pid: %d\n", getpid());
   sleep(3);
   exit(0);
 }
 return 0;
}

五、进程优先级(priority)

 1.进程优先级的概念

a.CPU资源分配的先后顺序,就是指进程的优先级。

b.优先级高的进程可以优先执行。配置进程优先级对多任务环境的Linux很有用,可以改善系统性能。在多CPU系统中还可以把进程运行到指定的CPU上,把不重要的进程安排到某个CPU,可以大大改善系统整体的性能。

简单来说进程优先级就是来确认谁应该先获得资源,谁后获得资源的数据。

为什么要有进程优先级呢?

因为CPU的资源是有限的。

需要注意的是:进程优先级和我们的成绩是一样的,它只是一串数据——需要有人来评判,正如我们的成绩有老师来评判一样,在Linux中,由调度器来评判进程优先级这串数据,如果没有评判,那么优先级这串数据就会毫无意义。

 LInux下具体的优先级的做法:

 优先级 = 老的优先级 + nice值

优先级的值越小,越早被执行(即优先级越高)

2.查看系统的进程

 进程状态后面带个“+”号,代表的是“前台状态”

ps -l 这个命令可以显示进程详细信息:

例:

其中: 

UID:代表执行者的身份 

PID:进程代号

PPID:父进程的代号

PRI:优先级(越小越高)

NI:nice值

nice就是进程可被执行的优先级的修正数值(注意nice值和优先级是两个东西!)

nice为负,则进程优先级会变高,PRI会变小,优先被执行。

因为在LInux下,调整进程优先级就是调整nice值nice取值范围[-20,19] 一共40个级别

 3.如何修改进程优先级

top  命令后输入 r键,之后输入对应的PID,再输入nice值即可修改nice值(注意有时候会需要用到sudo 前缀命令,例如nice为负时) 

默认情况下,老的优先级都是80.

4.其他进程的概念认知 

·竞争性:系统进程数目众多,而CPU资源只有少量甚至1个,所以进程之间为了高效完成任务有竞争属性,便具有了优先级。

·独立性:多进程运行时,需要独享各种资源,多进程运行之间互不干扰。

·并行:多个进程在多个CPU下分别同时运行,这称为并行

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

CPU管理进程不一定是一个一个按一定时间来的。是有时间的抢占和出让的。

并行:两个CPU,有两个进程分别在跑

并发:某个时间段,一个CPU下,各个代码都得到推进(取决于CPU内各种快速切换)。

CPU中是有着大量的寄存器的:

六、环境变量(environment variables)

基本概念:环境变量一般是指在操作系统中用来指定操作系统运行环境的一些参数。

例如:在动态链接时,我们不知道动态库在哪,但是也能链接成功,原因就是有相关环境变量帮助编译器查找。(环境变量通常具有某些特殊用途,在系统当中有全局性

再例如:我么你自己编译生成的可执行程序,要让其运行起来,我们不能直接bz.exe ,得写成./bz.exe(也就是带上路径),前面我们也了解到诸如 ll 、ls 、cd这些命令也都是文件,那为什么它们可以不以路径的形式输入而直接运行相关的命令呢?————环境变量

既然知道了不带路径直接运行的原理,那我们能不能也让bz.exe不带路径直接运行呢?

1.常见的环境变量

PATH:指定命令的搜索路径(如果我们要完成上面的问题,可见这个环境变量很关键)

上面我们说的可以让指令不带完整路径直接执行,就是靠这个环境变量

HOME:指定该用户的主工作目录(即用户登录到Linux时默认的目录)

例如我们cd ~(回到上级目录)这个指令中的~,就是对应的和HOME的关系

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

2.查看环境变量的方法 

echo $环境变量名 这个命令可以显示环境变量

例如:echo $PATH  、 echo $HOME 、echo $SHELL

3.和环境变量相关的命令

echo :显示某个环境变量(注意环境变量前面紧跟美元符号)

export:设置一个新的环境变量

env:显示所有的环境变量

unset:清除环境变量

set:显示本地定义的shell变量和环境变量

4.让可执行程序不带路径执行的方法(PATH)

 前面我们已经说了,PATH环境变量其中是大量路径,当运行命令时,会在这些路径下面查找,即指定命令的搜索路径

方法1:我们吧bz.exe扔到$PATH的任意一个路径下。

例如:我们显示出PATH中的路径 

 可以得到/usr/local/bin 、/usr/bin、/usr/local/sbin、/usr/sbin、/home/bz/.local/bin等等这些路径,随便找一个,把我们生成的可执行程序(例名为bz.exe)放到里面即可。

注意:这个方法不推荐,因为会污染命令池(万一我们乱搞的命令和官方的命令同名会很麻烦!)

 方法2:将当前路径整个放到PATH中。

我们先用pwd显示出当前的路径(例名称为 路径xxx

之后我们用export PATH = $PATH:路径xxx  这个命令来进行环境变量的修改。

此时我们再echo $PATH,我们会发现路径xxx以:(分隔号),插入到了环境变量PATH中了。

5.环境变量的组织形式

 

每个程序都会收到一张环境表(全局的),环境表是一个字符指针数组,每个指针指向一个以\0'结尾的字符串。

6.获取环境变量的方法  

问:main函数可以带参吗?可以带几个参数呢?

答:main函数可以带最多三个参数,

例:

int main(int argc,char* argv[ ],(命令行参数)char* env[ ](环境变量参数))

由此,我们可以通过如下的方法获取环境变量:

方法1:用main函数获取环境变量

思路:因为main函数的第三个参数是环境变量参数,因此可以用这样的方法获得环境变量:

int main(int argc,char* argv[ ],cahr* env[ ])
{
  int i = 0;
  for(;env[i]!=NULL;i++)
  {
     printf("%s\n",env[i]);
  }
     return 0;
}

方法2:通过第三方environ获取。

我在前面的图中已经描述了,environ是一个二级指针,指向了env[ ] 这个指针数组。

注意:

1.environ是libc中定义的一个全局变量

2.由于environ没有包含在任何头文件中,所以在使用时 要用extern声明。

#include <stdio.h>
int main(int argc, char *argv[])
{
 extern char **environ;
 int i = 0;
 for(; environ[i]; i++){
 printf("%s\n", environ[i]);
 }
 return 0;
}

 方法3:通过系统调用接口getenv获得环境变量

#include <stdio.h>
#include <stdlib.h>
int main()
{
 printf("%s\n", getenv("PATH"));
 return 0;
}

 7.环境变量的全局性

子进程的环境变量是拷贝自父进程的,默认情况下所有环境变量都会被子进程继承,这就导致了环境变量的全局性。

#include <stdio.h>
#include <stdlib.h>
int main()
{
 char * env = getenv("BAOZHENG");
 if(env){
 printf("%s\n", env);
 }
 return 0;
}

对于上面这串代码,我们直接查看,发现什么都不会显示,证明该环境变量根本不存在。

但是如果我们在Linux下运行修改环境变量的命令 : export  BAOZHENG = "nb"

再次运行该程序,我们发现显示出了对应的环境变量字符串——这证明了环境变量的全局性。

问:如果不用export,只输入 BAOZHENG = “nb,再次运行程序会发生什么?

答:什么都不会发生,只有export能修改环境变量。我们输入的这个命令修改就是个普通变量,没有意义。

七、进程地址空间 

1.进程地址空间的概念

这个图需要倒背如流,那这个玩意儿是啥呢?

我们可以用一串代码来感受一下:

#include <stdio.h>
#include <stdlib.h>

int un_g_val;//未初识化全局数据
int g_val = 1;//已初始化全局数据

int main(int argc,char* argv[ ],char* env[ ])
{
  printf("text:%p\n",main);
  printf("init;%p\n",&g_val);
  printf("uninit:%p\n",&un_g_val);

  char* p = (char*)malloc(16);//堆上申请空间

  printf("heap:%p\n",p);
  printf("stack:%p\n",&p);
  return 0;
}

需要我们注意的是:Windows和Linux进程地址和分布可能是不一样的。

 

2.进程地址空间的实验

给出如下这段代码:

#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)
 { //child,子进程肯定先跑完,也就是子进程先修改,完成之后,父进程再读取
   g_val=100;//修改g_val的值(对g_val的值进行写入)
   printf("child[%d]: %d : %p\n", getpid(), g_val, &g_val);
 }
 else
 { //parent
   sleep(3);
   printf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val);
 }
   sleep(1);
   return 0;
}

最后的到的结果:

child[3046]: 100 : 0x80497e8
parent[3045]: 0 : 0x80497e8

我们发现:父子进程的g_val的值不同,但是父子进程中的g_val的地址竟然是相同的!

因此我们可以初步得出这样的结论:

(1)变量g_val的值不同,所以父子进程的变量绝不是同一个变量

(2)变量g_val的值不同,但是地址却相同,因此这个地址绝对不是物理地址

(3)在Linux中,我们称上面所说的g_val的地址叫做虚拟地址(线性地址)

我们在C/C++中所看到的地址全部都是虚拟地址。物理地址用户是永远无法看到的,由OS管理者(也就是说OS负责把虚拟地址转为物理地址)

小插曲:malloc申请空间时,会多申请点空间来存储cookie数据,free时通过cookie知道free多少空间。 

3.对进程地址空间的理解

对于这么一个进程地址空间,在32位下,进程地址空间取值范围是0x00000000到0xffffffff

这里[0,3GB]是用户空间,[3GB,4GB]是内核空间。

那么到底该如何理解进程地址空间呢? 

(1)打个比方:

一个超级富豪,有一个亿,他有4个儿子,他对每个儿子都悄悄说了“现在好好干,以后我这个一个亿都给你一个人”,四个儿子于是都很努力的奋斗,向父亲要个百十万也会满足,久而久之就完全相信了父亲,干投资的时候总是有恃无恐,认为自己还有一个亿垫底。

上面这个例子中:

画大饼的老爹其实就是OS操作系统;

四个儿子a、b、c、d就是进程;

而老爹给儿子画的饼(一个亿)其实就对应进程地址空间;

真正的一个亿对应物理内存;

儿子脑子中的认为父亲会给自己的一个亿对应的就是我们的进程地址空间。

当然,对于进程地址空间,他作为跟着进程走(跟特定的进程关联)的数据,肯定也是一种数据(描述),那就需要管理(组织)。

(2)我们设想一下,如果直接用物理内存进行访问和读写,如果进程中出现了问题,那是不是特别不安全?

例如一个进程中出现了一个野指针指向了另一个进程的内容,那这个野指针就很有可能会干扰其他进程的正常运行(进程的独立性被破坏),当然还有内存碎片等一系列的问题。

(3)由于最初的访问物理内存的方法太过于不安全,科学家们做出了改进:

a.每个进程都有自己的PCB——在Linux中就是task_struct

b.每个进程的都有自己的进程地址空间(虚拟空间)(这是内核中的一种数据结构)

 c.进程地址空间的虚拟地址能够通过映射,映射到对应的物理内存中

那么问题来了:最终不是还是会去访问物理地址吗?万一虚拟地址还是非法呢?

答:如果虚拟地址非法,那就直接禁止映射就好了。

4.进程地址空间的本质

 进程地址空间本质上就是一种内核数据结构,例如:

小帅和小美做同桌画三八线:左边(设左边起始位置为0位置)向右50厘米归小帅,剩余70厘米归小美。则有:

struct xiaoshuai
{
  int begin = 0;
  int end = 50;
  //...
}

struct xiaomei
{
  int begin = 50;
  int end = 120;
  //...
}

同样的,进程地址空间中也有着各个区域的划分

例如:

struct addr_room
{
  int code_start;
  int code_end;
  int init_start;
  int init_end;
  int uninit_start;
  int uninit_end;
  int heap_start;
  int heap_end;
  int stack_start;
  int stack_end;
  //...
}

这里,栈和堆的向下和向上增长其实就是范围的变化,就是边界值的变化

其实,在Linux中,虚拟地址空间就是struct_mm_struct{}。

 每个进程都有自己的进程地址空间:

*地址空间mm_struct和页表(用户级)是每个进程都会私有一份的!只要保证,每一个进程的页表,映射的是物理内存的不同区域,就可以做到各个进程之间不会相互干扰,从而保证进程的独立性。 

这就可以回答我们上述实验中提到的问题了:

“为什么父子进程的值不同,地址却是一样的?”

 

(1)子进程大部分属性都是继承自父进程的,其中虚拟地址空间也是相同的,此时页表也一样,因此指向的是同一个物理地址。

(2)但是,当子进程发生修改时(即发生写入时),为了保证独立性,OS会换个页表,映射到另一个位置,进而防止改变了父进程的数据。在这整个过程中,虚拟地址是始终拷贝自父进程的不会变。

这是一种写时拷贝策略 

5.关于进程地址空间的几个问题 

1.问:当我们的程序,在编译时,形成的可执行程序没有加载到内存中称为进程时,在程序内部有地址吗?

 答:已经有地址了。可执行程序其实编译时内部已经有地址了。对于进程地址空间的规则,不仅仅是OS内部要遵守的,编译器也要遵守。编译器编译代码的时候,就已经形成了各个区域的代码区、数据区等等。。。并且,采用和Linux中一样的编址方式,每个变量,每一行代码都编了址,因此程序在编译的时候,每一个字段早已具有了一个虚拟地址。

CPU内部读的是虚拟地址 

 2.问:什么是挂起?

答:“加载”本质就是创建进程,那是不是必须立刻把所有程序的代码和数据加载到内存中,并创建内核数据结构建立映射关系呢?答案是否定的。在最极端的情况(新建状态——只有task_struct 和 mm_struct)下,甚至只有内核数据结构被创建了出来(没建立映射关系,因为内存不足)

那么就可以用分批加载(换入)和分批换出的策略,甚至,这个进程短时间不会再被执行了,例如堵塞了。——>进程的数据和代码全被换出了,就叫做挂起。

实际上页表映射的时候,可不仅仅映射的是内存,磁盘中的位置也能映射(OS找到磁盘中的位置,再做换入操作) 

3.问:为什么要存在进程地址空间?

(1).首先,进程地址空间和页表的存在,有效拦截了非法访问。凡是非法的访问或者映射,OS都会识别到,并终止这个进程(所谓的进程崩溃,本质上就是进程退出——OS杀掉了进程)——有效的保护了物理内存,因为地址空间和页表是OS创建并维护的,也就意味着凡是想使用地址空间和页表进行映射,也一定要在OS监管之下来进行访问。也便保护了物理内存中的所有合法数据,包括各个进程以及内核的相关有效数据。 

(2).因为有进程地址空间和页表的存在,我们的物理内存中可以对未来的数据进行任意位置的加载。(即,随便放到哪个位置,只要页表能够通过映射关系找到就行)

这使得物理内存的分配和进程的管理,可以做到没有联系。——内存管理模块和进程管理模块就完成了解耦。

我们在new/malloc时,本质上就是在虚拟地址空间上申请的,而不是物理空间

问:我们如果申请了物理空间,但不立马使用,搁着,是不是空间浪费了呢?

答:是的!

 因此,因为有进程地址空间的存在,所以上层申请空间,其实就是在进程地址空间上申请的,物理内存有可能一根毛都没给。而当真正对物理地址空间访问时(是由OS自动完成的,用户和进程都蒙在鼓里了——缺页中断),才会执行内存的相关算法,帮你申请内存,构建页表映射关系,然后,再进行内存访问——延迟分配的策略,来提高整机的效率(内存使用率100%)

(3).因为在物理内存中理论上可以让任意位置加载,那么物理内存中几乎所有的数据和代码都是乱序的。

但是由于页表的存在,他可以将地址空间上的虚拟地址和物理地址进行映射,所有内存分布都可以是有序的。

即:地址空间+页表的存在 可以将内存分布 有序化 (因为物理地址可能是乱的,但虚拟地址是有序的)

 由于进程要访问的物理内存中的数据和代码,可能还未加载到物理内存中,同样的,也可以让不同的进程映射到不同的物理内存,就很容易做到,进程的独立性由此得以实现。

即:进程的独立性,可以通过地址空间+页表的方式实现

总结:因为有进程地址空间的存在,每一个进程都认为自己拥有4GB的空间(32位下),并且各个区域是有序的,进而可以通过页表映射到不同的区域,来实现进程的独立性。 

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/TheBao0107/article/details/132644213

智能推荐

解决win10/win8/8.1 64位操作系统MT65xx preloader线刷驱动无法安装_mt65驱动-程序员宅基地

文章浏览阅读1.3w次。转载自 http://www.miui.com/thread-2003672-1-1.html 当手机在刷错包或者误修改删除系统文件后会出现无法开机或者是移动定制(联通合约机)版想刷标准版,这时就会用到线刷,首先就是安装线刷驱动。 在XP和win7上线刷是比较方便的,用那个驱动自动安装版,直接就可以安装好,完成线刷。不过现在也有好多机友换成了win8/8.1系统,再使用这个_mt65驱动

SonarQube简介及客户端集成_sonar的客户端区别-程序员宅基地

文章浏览阅读1k次。SonarQube是一个代码质量管理平台,可以扫描监测代码并给出质量评价及修改建议,通过插件机制支持25+中开发语言,可以很容易与gradle\maven\jenkins等工具进行集成,是非常流行的代码质量管控平台。通CheckStyle、findbugs等工具定位不同,SonarQube定位于平台,有完善的管理机制及强大的管理页面,并通过插件支持checkstyle及findbugs等既有的流..._sonar的客户端区别

元学习系列(六):神经图灵机详细分析_神经图灵机方法改进-程序员宅基地

文章浏览阅读3.4k次,点赞2次,收藏27次。神经图灵机是LSTM、GRU的改进版本,本质上依然包含一个外部记忆结构、可对记忆进行读写操作,主要针对读写操作进行了改进,或者说提出了一种新的读写操作思路。神经图灵机之所以叫这个名字是因为它通过深度学习模型模拟了图灵机,但是我觉得如果先去介绍图灵机的概念,就会搞得很混乱,所以这里主要从神经图灵机改进了LSTM的哪些方面入手进行讲解,同时,由于模型的结构比较复杂,为了让思路更清晰,这次也会分开几..._神经图灵机方法改进

【机器学习】机器学习模型迭代方法(Python)-程序员宅基地

文章浏览阅读2.8k次。一、模型迭代方法机器学习模型在实际应用的场景,通常要根据新增的数据下进行模型的迭代,常见的模型迭代方法有以下几种:1、全量数据重新训练一个模型,直接合并历史训练数据与新增的数据,模型直接离线学习全量数据,学习得到一个全新的模型。优缺点:这也是实际最为常见的模型迭代方式,通常模型效果也是最好的,但这样模型迭代比较耗时,资源耗费比较多,实时性较差,特别是在大数据场景更为困难;2、模型融合的方法,将旧模..._模型迭代

base64图片打成Zip包上传,以及服务端解压的简单实现_base64可以装换zip吗-程序员宅基地

文章浏览阅读2.3k次。1、前言上传图片一般采用异步上传的方式,但是异步上传带来不好的地方,就如果图片有改变或者删除,图片服务器端就会造成浪费。所以有时候就会和参数同步提交。笔者喜欢base64图片一起上传,但是图片过多时就会出现数据丢失等异常。因为tomcat的post请求默认是2M的长度限制。2、解决办法有两种:① 修改tomcat的servel.xml的配置文件,设置 maxPostSize=..._base64可以装换zip吗

Opencv自然场景文本识别系统(源码&教程)_opencv自然场景实时识别文字-程序员宅基地

文章浏览阅读1k次,点赞17次,收藏22次。Opencv自然场景文本识别系统(源码&教程)_opencv自然场景实时识别文字

随便推点

ESXi 快速复制虚拟机脚本_exsi6.7快速克隆centos-程序员宅基地

文章浏览阅读1.3k次。拷贝虚拟机文件时间比较长,因为虚拟机 flat 文件很大,所以要等。脚本完成后,以复制虚拟机文件夹。将以下脚本内容写入文件。_exsi6.7快速克隆centos

好友推荐—基于关系的java和spark代码实现_本关任务:使用 spark core 知识完成 " 好友推荐 " 的程序。-程序员宅基地

文章浏览阅读2k次。本文主要实现基于二度好友的推荐。数学公式参考于:http://blog.csdn.net/qq_14950717/article/details/52197565测试数据为自己随手画的关系图把图片整理成文本信息如下:a b c d e f yb c a f gc a b dd c a e h q re f h d af e a b gg h f bh e g i di j m n ..._本关任务:使用 spark core 知识完成 " 好友推荐 " 的程序。

南京大学-高级程序设计复习总结_南京大学高级程序设计-程序员宅基地

文章浏览阅读367次。南京大学高级程序设计期末复习总结,c++面向对象编程_南京大学高级程序设计

4.朴素贝叶斯分类器实现-matlab_朴素贝叶斯 matlab训练和测试输出-程序员宅基地

文章浏览阅读3.1k次,点赞2次,收藏12次。实现朴素贝叶斯分类器,并且根据李航《统计机器学习》第四章提供的数据训练与测试,结果与书中一致分别实现了朴素贝叶斯以及带有laplace平滑的朴素贝叶斯%书中例题实现朴素贝叶斯%特征1的取值集合A1=[1;2;3];%特征2的取值集合A2=[4;5;6];%S M LAValues={A1;A2};%Y的取值集合YValue=[-1;1];%数据集和T=[ 1,4,-1;..._朴素贝叶斯 matlab训练和测试输出

Markdown 文本换行_markdowntext 换行-程序员宅基地

文章浏览阅读1.6k次。Markdown 文本换行_markdowntext 换行

错误:0xC0000022 在运行 Microsoft Windows 非核心版本的计算机上,运行”slui.exe 0x2a 0xC0000022″以显示错误文本_错误: 0xc0000022 在运行 microsoft windows 非核心版本的计算机上,运行-程序员宅基地

文章浏览阅读6.7w次,点赞2次,收藏37次。win10 2016长期服务版激活错误解决方法:打开“注册表编辑器”;(Windows + R然后输入Regedit)修改SkipRearm的值为1:(在HKEY_LOCAL_MACHINE–》SOFTWARE–》Microsoft–》Windows NT–》CurrentVersion–》SoftwareProtectionPlatform里面,将SkipRearm的值修改为1)重..._错误: 0xc0000022 在运行 microsoft windows 非核心版本的计算机上,运行“slui.ex