赞
踩
目录
我们之前都学习过指针,有三十二根地址线,每一根地址线根据有无电流通过表示0和1,综合起来有二的三十二次方种可能性,这些数字将内存的每一个字节编号,加起来一共有4GB的空间,所以32位机器的内存为空间为4GB。
不过,这样的理解只在C语言层面是没问题的,但在操作系统层面还是有些问题。请看下面一段代码:
- #include<stdio.h>
- #include<unistd.h>
- #include<sys/types.h>
- int global = 100;
- int main()
- {
- pid_t id = fork();
- if (id<0)
- {
- perror("fork");
- return -1;
- }
- if(id == 0)
- {
- int n = 0;
- for (n=0; n<10; n++)
- {
- if(n == 5)
- {
- global = 200;
- printf("子进程global的值已经被修改\n");
- }
- printf("我是子进程,global的值为%d,地址为:%p\n", global, &global);
- sleep(1);
- }
- }
- else
- {
- int n = 0;
- for (n=0; n<10; n++)
- {
- printf("我是父进程,global的值为%d,地址为:%p\n", global, &global);
- sleep(1);
- }
- }
- return 0;
- }
下面是运行截图:
此时,我们发现了一个巨大的问题对于global这个整型值,同样的地址却打印出了不同的结果。如果我们的内存只是按照C语言指针的方式一一对应的话,是不可能出现同一个地址的数据是不相同的数据的。所以我们断定,C指针不是直接访问计算机的物理内存的。
所谓进程地址空间(process address space),就是从进程的视角看到的地址空间,是进程运行时所用到的虚拟地址的集合。操作系统会给每一个进程都创建一个独立的虚拟地址空间,每一个进程的地址空间都是整个内存的大小。操作系统让进程假装以为整个内存都为自己服务,就像老板给员工画的大大的饼,既然是画饼,所以在某个进程需要过大的内存时操作系统会直接拒绝。
然后,操作系统会通过页表将虚拟地址空间与物理内存建立映射,也就是说当我们得到虚拟地址时,通过页表就可以对应到物理地址。当我们需要修改虚拟地址中的数据时,操作系统会先通过页表找到对应的物理内存,然后修改物理内存中的数据。
在操作系统中,进程地址空间中的地址通常也被称为线性地址,因为它是按比特位从全0到全1依次顺序编址的;磁盘程序内部的地址通常被称为逻辑地址。而在其他地方,线性地址、虚拟地址、逻辑地址区分比较严格,但是在Linux中,三者的意思是一样的,都表示虚拟地址,大家不用过于区分。
我们在之前的C语言学习中应该都提到过栈区、堆区和常量区等内存分区,下面这张图就描述了它们的分布位置以及作用。
写时拷贝就是等到修改数据时才真正分配内存空间,这是对程序性能的优化,可以延迟甚至是避免内存拷贝,当然目的就是避免不必要的内存拷贝。
比如说,我们建立两个进程每一个都存在一个值为10的变量,操作系统并不会通过页表映射两块物理内存,而是只开辟一块物理内存,同时让两个进程的不同虚拟地址位置指向同一块物理内存。而当某一个进程修改这个变量时,操作系统又会开辟一块新的空间,存储改变后的值,页表也改变物理内存的映射关系,达到修改的目的。
这样我们就可以理解上面的代码中,子进程父进程共享物理内存,当子进程改变数据时,操作系统又另外开了一块物理空间存储修改后的值并改变页表指向,但是父子进程共享代码的特点导致各自的函数栈帧相同,对应虚拟空间中这个变量虚拟地址相同。所以导致了同一个虚拟地址却对应了不同的值。
我们之前讲到,操作系统会为每一个进程都创建一个进程地址空间,那么操作系统面对如此多的进程该如何对每个进程的地址空间进行管理呢?
在前面讲解操作系统的文章中,我们知道了操作系统管理计算机中各种对象的本质是:获取并及时更新该对象的重要数据,将相应类型的数据通过链表等数据结构管理起来,根据数据的变化管理对应对象。这也叫做:先描述,再组织。
Linux在地址空间的管理中,操作系统会为每一个进程的虚拟地址空间创建一个mm_struct结构体,然后通过管理结构体对象来间接管理进程地址空间。
Linux的task_struct源码中含有mm_struct *指针:
- struct task_struct {
- struct thread_info thread_info;
- .......
- struct mm_struct *mm, *active_mm;//可以找到mm_struct
- pid_t pid;
- pid_t tgid;
- .......
- };
进程地址空间其实也是进程属性的一种,我们可以通过进程的 task_struct 来找到进程对应的地址空间。
而且我们知道进程地址空间被划分为很多个区域,有堆区、栈区、代码段等等,那么操作系统如何对这些区域进行划分和管理呢?
答案是用通过两个表示区域边界的变量start和end来规定上界下界,从而维护一块内存区域。
Linux的mm_struct部分源码如下:
- struct mm_struct {
- ......
- //进程地址空间的大小,锁住无法换页的个数,共享文件内存映射的页数,可执行内存映射中的页数
- unsigned long total_vm, locked_vm, shared_vm, exec_vm;
- //用户态堆栈的页数,
- unsigned long stack_vm, reserved_vm, def_flags, nr_ptes;
- //维护代码段和数据段
- unsigned long start_code, end_code, start_data, end_data;
- //维护堆和栈
- unsigned long start_brk, brk, start_stack;
- //维护命令行参数,命令行参数的起始地址和最后地址,以及环境变量的起始地址和最后地址
- unsigned long arg_start, arg_end, env_start, env_end;
- ......
- };
要调整一个区域的大小,调整 mm_struct 中维护此区域 start 和 end 变量即可。
(1)保证了数据的安全性
我们为每一个进程都创建一个进程地址空间,每一次从物理内存中对数据进行增删查改都需要经过页表对应到物理内存,即使是CPU也只能老老实实地储存虚拟地址然后通过页表从物理内存中传回数据。一旦存在对内存读写的非法操作,页表就可以拒绝,大大增强了数据的安全性。
注:页表的映射并不是简单的使用一块空间映射一块空间,页表的机理十分复杂,它可以有范围的将部分空间进行映射,这也就解释了越界访问能发生的原因了。
(2)保证了进程的独立性
对于互相无关的两个进程,它们都拥有自己独立的地址空间以及页表,页表会映射到物理内存不同位置上,磁盘代码和数据加载到内存中的位置也不同,一个进程数据的改变不会影响另一个进程。
如果没有虚拟地址空间,所有进程直接读取和写入物理内存的话,进程间的相互独立难以保证,哪怕有一个野指针问题都有可能影响其他进程。
(3)进程通过虚拟地址空间的形式统一组织,也便于其他软硬件对进程进行处理
对于进程来说,由于存在虚拟地址空间,各个进程都认为自己的数据被放置在对应的区域,比如栈区、堆区和代码区等等,此时进程认为自己的数据是规律存放的,但是当虚拟地址通过页表对应到物理地址时,物理地址的分布就不一定是规律的。
由于进程地址空间保证了进程的相对独立性、进程的安全性和数据存储的规律性,所以这样的组织结构也为计算机中的CPU、磁盘和编译器等软硬件的设计减轻了负担,而且由于这种统一的组织,这些软硬件的实现模式也都遵循地址空间的这一思路,使得各种软硬件对进程的处理模式趋于统一,也就是:软硬件使用虚拟地址->通过页表对应到物理地址->在物理地址修改数据。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。