进程虚拟地址空间:程序地址空间应称为 → 进程虚拟地址空间
为什么这么说,我们先来看一段代码
变量被static修饰后,编译器会把该变量编译到全局区
就变成了全局变量,作用域在被定义的函数内,生命周期跟随程序
栈向低地址生长
堆向高地址生长
- 父进程有一个全局变量val=0,创建子进程,
- 在子进程中对该val的值进行修改,并分别打印val分别在父子进程中的值和地址
输出结果:
父进程val | 值为0 ,地址0x11223344 |
---|---|
子进程val | 值为100,地址0x11223344 |
现象:父进程创建子进程,并都去打印全局变量产生的问题:
- 子进程创建成功后拷贝父进程的PCB,
它们各自的内存指针,指向各自的进程虚拟地址空间,其中的代码段和父进程的一样;现象:父子进程是两个不同的进程,打印出全局变量的地址一模一样;
- 按照对内存的理解,不同进程使用内存应在不同的物理内存上,&符号拿到地址应该不同;
- 在C/C++代码中用&符号获得的地址,都是操作系统虚拟出来的地址,并不是真正的内存条中的物理地址
离散分配:提高对内存的使用率
进程地址空间
内存是一个硬件,不能阻拦程序员访问,只能被动接收读取和写入
代码的只读特性是如何做到的呢
- C、C++中的地址都不是物理地址,都是虚拟地址
- 每一个程序在启动时,操作系统会为其虚拟一段4G的地址空间
每一个进程都认为自己独占系统中所有的资源- 虚拟地址空间中的每个区域,都要经过 页表 映射到物理空间
页表结构将虚拟地址和物理地址对应起来,每个进程都有一个页表结构- mm_struct
进程地址空间是task_struct中的一个数据结构—struct mm_struct
mm_stuct中有一个结构体,里面通过指针保存每一段区域的起始和结束地址,以及该区域的权限
程序如何变成内存
程序被编译出来后,没有被加载的时候,程序内部是有地址的
链接的时候,需要把库中的函数地址,填入调用的调用函数的地方
程序被编译出来后,没有被加载的时候,程序内部是有区域的(这里采用相对地址/逻辑地址)
程序加载到内存前后,两者地址的区别
在加载前,每个区域是采用相对地址划分的,类似结构体对齐的地址,起始地址为0
加载后,在内存开辟一段空间,这段内存空间有起始地址
最后程序在内存中,各个区域的地址都会加上 加载后的起始地址
页表结构
虚拟地址转化成物理地址三种方式:
1. 分页式:
通过虚拟地址算出页号,通过页表找到相应块号,通过块号找到相应块的起始位置,再加上页内偏移找到具体物理地址(图同分段式,只是名字不一样)
虚拟地址 = 页号 + 页内偏移
- 一个0x11223344这个虚拟地址是由两部分构成,一个是页号,一个是页内偏移;也就是说操作系统知道每一个通过虚拟地址得到对应的物理地址
操作系统在进行管理时,
- 将进程虚拟地址空间分成一页一页的小块;
- 每个小块的大小通常为4kb (4096个字节),每块大小在512字节~8kb之间;
- 并且将物理内存分成了和一页大小相同的物理块(左边叫页,右边叫块)
- 页表:第一列保存的是页号,第二列保存的是块号
- 页表结构:将虚拟地址与物理地址对应起来;页表结构有两列
虚拟地址转化成物理地址:
- 通过一个虚拟地址一定可以知道该地址对应的页号,在页表中查到对应的页号找到块号,通过块号找到物理内存;
- 一块物理内存有4096个字节,也就是说有4096个地址;所以找到块号也不能确定虚拟地址对应的物理地址,还要再加上偏移量
- 通过虚拟地址计算页号和页内偏移:
页号 = 虚拟地址 / 页的大小
页内偏移 = 虚拟地址 % 页的大小
2. 分段式:
通过虚拟地址得到段号,通过段号找到段的起始地址,段的起始地址+段内偏移得到物理地址
虚拟地址 = 段号 + 段内偏移
段表:也是两列 段号 + 段的起始地址
3. 段页式:
通过段号找到页表起始位置(也就是找到了哪个页表),通过页号对应块号,通过块号找到物理内存中某个块的起始位置,通过页内偏移在块中找到物理地址
虚拟地址=段号+页号+页内偏移
在操作系统中存在一个段表若干个页表
段表第一列是段号;第二列是页表的起始位置
页表第一列是页号;第二列是块号
其它概念:
- 进程间的运行是抢占式执行
- 并行和并发
并行:多个进程同时拥有不同的CPU进行运算----有多个CPU
并发:多个进程在同一时刻只能有一个进程有CPU进行运算----单个CPU - 独立性:一个进程被创建,该进程就是独立的,其它进程不会影响到该进程
复杂指令集:直接写一条指令
精简指令集:拆分很多模块,写成很多指令,运行一条新指令时会找一下之前是否有相同模块的指令