Linux|内存地址空间
- 现象
- 基本概念
- 理解
- 如何理解地址空间
- 什么是划分区域?
- 地址空间的理解
- 为什么要有地址空间?
- 如何进一步理解页表和写时拷贝
- 如何理解虚拟地址
- Linux真正的进程调度方案
现象
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include<sys/types.h>
int g_val = 100;int main()
{printf("father is running, pid: %d, ppid: %d\n", getpid(), getppid());pid_t id = fork();if(id == 0){//childint cnt = 0;while(1){printf("I am child process, pid: %d, ppid: %d. g_val: %d, &g_val: %p\n", getpid(), getppid(), g_val, &g_val);sleep(1);cnt++;if(cnt == 5){g_val = 300;printf("I am child process, change %d -> %d\n", 100, 300);}}}else{//fatherwhile(1){printf("I am father process, pid: %d, ppid: %d. g_val: %d, &g_val: %p\n", getpid(), getppid(), g_val, &g_val);sleep(1);}}
}
同一个地址但是值却不一样!这是为什么呢?我们可以得出一个最基本的结论:就是我们看到的地址不是真实的物理地址
基本概念
操作系统为我们划分了4G的虚拟进程地址空间(针对32位的机器)
-
虚拟进程空间和真实的进程空间通过页表的映射联系起来
-
每一个进程都有自己的一份虚拟进程空间,子进程会基础父进程的内核数据结构
-
内存空间是用来描述代码和数据的,本质是操作内核中的一个数据结构
-
因为操作系统设计进程的时候要保证独立性,子进程的数据是独立的,子进程修改val的值的时候会发生写时拷贝
这也解释了为什么我们看到的地址一样值却不一样,因为发生了写时拷贝,虽然虚拟地址是一样的但是物理地址则不一样。
写实拷贝本质是一种按需申请,如果把父进程内存空间的数据全部拷贝一份,会浪费很多。比如命令行参数和环境变量几乎是不修改的。
理解
如何理解地址空间
什么是划分区域?
上小学的的时候,我们都会遇到这种情况,就是和同桌划分三八线,本来各占桌子的百分之五十,但是一天你突发奇想想要把区域划分大一点。这怎么用计算机描述呢?
我们可以定义一个区域的结构体来描述自己的区域大小。
另外一个结构体用来描述来个区域的信息。
struct area
{int start;int end;
}typedef areastruct desk
{area left;area right;
}typedef deskdesk.left.end += 20;
desk.right.start -= 20;
进程地址空间本质上也是一个结构体,很多属性表示end 和start的范围。
地址空间的理解
操作系统给进程画的大饼
操作系统告诉每个进程你们可以使用整个的内存哦!
为什么要有地址空间?
- 实际中的物理内存中,代码区,数据区,堆区,栈区,共享区,命令行参数,环境变量是杂乱存放的。页表和进程地址空间可以将无序变为有序。
- 对进程管理模块和内存管理模块进行解耦。我们申请开辟空间只是在进程地址空间中调end的大小,当使用时才真正的在内存中分配。这样做能提高内存的利用率
- 拦截非法请求当我们访问内存时,操作系统和硬件会在页表中寻找物理内存的位置,但是当我们越界访问时,就在页表中找不到对应的物理内存的位置,操作系统就把这次访问内存的请求拦截了下来,避免往内存中写入废旧的数据,从而影响其他内存。
如何进一步理解页表和写时拷贝
页表里面有很多标记位,比如能否读写的标记位,是否在物理内存中的标记位
-
理解进程挂起,就是把进程是否在物理内存中置为false了
-
解释 char *str =“hello word”; *str =‘H’ 在字符常量区为什么不能被修改呢? 因为页表在映射时有权限管理
OS通过识别到错误(比如父子进程对全局变量的权限都是r和w 父进程一旦创建了子进程,对全局变量的权限都变为w 此时修改就会判断为错误)来引发写时拷贝:
- 先判断数据是不是在物理内存中(看标记为是否ture)如果不在发生缺页中断,在内存中分配一个空间。这属于正常情况
- 是不是需要写实拷贝,怎么判断是否需要写实拷贝呢?通过引用计数的方式
- 如果都不是才进行异常处理,比如我们越界了,地址就不页表的虚拟地址中
如何理解虚拟地址
最开始的时候页表里的数据从那里来的?
我们知道程序里面本身就有地址
这个地址就是逻辑地址(虚拟地址)
当程序加载到物理内存的时候,就得到物理地址
根据这个虚拟到物理建立映射关系
int main()
{pid_t id = fork();if(id == 0){// childwhile(1){printf("I am child, %d, %p\n", id, &id);sleep(1);}}else if(id > 0){while(1){printf("I am father, %d, %p\n", id, &id);sleep(1);}}return 0;
}
解释这个id怎么能同时==0又!=0呢?return 的本质就是对ID进行写时拷贝
Linux真正的进程调度方案
- runqueue里的queue虽然有140个空间 但是前100个空间不用。
和我们nice -20到19 对应优先级 60~99
- queue里面的每一个元素又是一个队列,把task_struct 按优先级放入对应下标的队列中
- 因为前面有很多空间没用,我们又使用了大小为5个长整型的位图 32位 32位的查找 使用的下标。
- 实际当中有两个这样的结构 通过一个active指针和expired指针指向,active指向的队列 只出不进,expired指向的队列只进不出。
- active指向的队列为空时交换两个指针。