一、虚拟地址

之前提到过父子进程的数据是各自私有一份的,所以子进程中数据的变化不会影响到父进程。我们理所当然的会认为俩进程的数据该保存在不同的地方才对,可是我们却发现他们的地址竟然是相同的,这该怎么理解?
其实我们打印出来的地址并非真实的物理地址,而是虚拟地址(线性地址)。
操作系统在给每个进程分配系统的物理内存时,不会让进程知道其他进程占用内存、当前物理内存还剩余多少的情况,而是让每个进程都以为进程自己独占整个系统物理内存。进程之间彼此不知道,不关心对方存在,从而实现一定程度的隔离。
所以进程并不能直接访问到物理内存,也拿不到真正的物理地址,而是通过内核组织起的虚拟地址空间间接访问的。
task_struck 中存在一个 mm 指针,指向一个 mm_struct,这个结构体就是内核为这个进程创建的管理虚拟地址的。
进程虚拟地址空间,本质上是一个内核数据结构对象(类似PCB),记录着系统给进程划分区域的虚拟地址。
二、页表
系统对应还有一张页表,记录着与虚拟地址对应的物理地址。
当进程要访问一个变量时,进程会去找变量的虚拟地址,系统会用页表找到对应的真实物理地址进行访问。
mm_struct + 页表,就是 Linux 系统的虚拟内存管理方案。
在子进程被创建时,不仅 task_struct 被继承一份,mm_struct 也被完全继承一份,页表也被完全继承。
当父或子进程要修改一个变量值时,比如子进程修改 a 的值,系统会先重新分配一块物理内存给子进程的 a,把 a 虚拟地址对应的物理地址替换为新物理地址,然后再修改。这是OS的写时拷贝机制。
fork 后的 id 之所以值不同,也是同样的道理,虽然是同一变量名同一虚拟地址,但被操作系统映射到了不同的物理地址,拿到的值自然也就不同。
现在我们就可以认为,进程 = 内核数据结构(task_struct、mm_struct、页表)+ 代码和数据
进程的独立性,就是内核数据结构各自一份,代码和数据也是独立的(写时拷贝)。

页表中有记录着对物理地址的权限的标志位,比如代码区的权限是只读的,那么当进程拿着虚拟地址想要修改代码区的内容时,OS先查到页表中确实有对应条目,再发现对应的物理地址是只读的,就会拒绝进程访问,或者直接杀掉进程。

我们知道 C 中常量字符串是不可以被修改的,但这段代码可以被编译器编过的,而一运行就会出错,是为什么呢?
这是因为这个错误和编译器无关,是发生在系统层面的。常量字符串在代码区,系统查页表时发现进程没有修改权限,于是直接把进程结束掉了。
所以我们要在这样的 char *str 前加 const,这其实是告诉编译器,编程时不要让人改这个字符串,改了要给人报错。
isexists 标志位记录着当前物理地址对应的数据是否存在。当这个数据不存在(还没有被加载进内存、已经被清理等)时,OS 会将这部分数据重新唤入内存,并更新物理地址。
电脑的内存并不大,之所以能跑几十上百个 G 的程序,是因为程序并不是一运行就把全部代码和数据加载进内存的,而是分批加载的。
虚拟地址空间 + 页表,目的是:
1.保护物理内存
2.实现进程管理和内存管理的解耦合
3.让进程以统一的视角看待物理内存
所谓野指针,就是页表中不存在相对应的映射或者没有权限。
全局变量、字符常量,之所以存在全局性,在程序运行时一直有效,是因为他们被存储在已 / 未初始化变量区,在地址空间中,随着进程一直都存在,他们的虚拟地址,也能一直被大家看到。
命令行参数和环境变量会被传递给进程,他们在地址空间中在栈区上边,会继承自父进程。