写了多年代码,你却不知道的程序设计的5个底层逻辑( 三 )


系统调用是执行操作系统底层的程序,linux的设计者,为了保护操作系统,将进程的执行状态用内核态和用户态分开,同一个进程中,内核和用户共享同一个地址空间,一般 4G 的虚拟地址,其中 1G 给内核态, 3G 给用户态 。在程序设计的时候我们要尽量减少用户态到内核态的切换,例如创建线程是一个系统调用,所以我们有了线程池的实现 。
从 Linux 内存管理角度理解 JVM 内存模型进程上下文
我们可以将程序理解为一段可执行的指令集合,而这个程序启动后,操作系统就会为他分配 CPU ,内存等资源,而这个正在运行的程序就是我们说的进程,进程是操作系统对处理器中运行的程序的一种抽象 。
而为进程分配的内存以及 CPU 资源就是这个进程的上下文,保存了当前执行的指令,以及变量值,而 JVM 启动后也是linux上的一个普通进程,进程的物理实体和支持进程运行的环境合称为上下文,而上下文切换就是将当前正在运行的进程换下,换一个新的进程到处理器运行,以此来让多个进程并发的执行,上下文切换可能来自操作系统调度,也有可能来自程序内部,例如读取IO的时候,会让用户代码和操作系统代码之间进行切换 。
虚拟存储
当我们同时启动多个 JVM 执行:System.out.println(new Object()); 将会打印这个对象的 hashcode ,hashcode 默认为内存地址,最后发现他们打印的都是 Java .lang.Object@4fca772d ,也就是多个进程返回的内存地址竟然是一样的 。
通过上面的例子我们可以证明,linux中每个进程有单独的地址空间,在此之前,我们先了解下 CPU 是如何访问内存的?
假设我们现在还没有虚拟地址,只有物理地址,编译器在编译程序的时候,需要将高级语言转换成机器指令,那么 CPU 访问内存的时候必须指定一个地址,这个地址如果是一个绝对的物理地址,那么程序就必须放在内存中的一个固定的地方,而且这个地址需要在编译的时候就要确认,大家应该想到这样有多坑了吧 。
如果我要同时运行两个 office word 程序,那么他们将操作同一块内存,那就乱套了,伟大的计算机前辈设计出,让 CPU 采用 段基址 + 段内偏移地址 的方式访问内存,其中段基地址在程序启动的时候确认,尽管这个段基地址还是绝对的物理地址,但终究可以同时运行多个程序了, CPU 采用这种方式访问内存,就需要段基址寄存器和段内偏移地址寄存器来存储地址,最终将两个地址相加送上地址总线 。
而内存分段,相当于每个进程都会分配一个内存段,而且这个内存段需要是一块连续的空间,主存里维护着多个内存段,当某个进程需要更多内存,并且超出物理内存的时候,就需要将某个不常用的内存段换到硬盘上,等有充足内存的时候在从硬盘加载进来,也就是 swap。每次交换都需要操作整个段的数据 。
首先连续的地址空间是很宝贵的,例如一个 50M 的内存,在内存段之间有空隙的情况下,将无法支持 5 个需要 10M 内存才能运行的程序,如何才能让段内地址不连续呢? 答案是内存分页 。

写了多年代码,你却不知道的程序设计的5个底层逻辑

文章插图
 
在保护模式下,每一个进程都有自己独立的地址空间,所以段基地址是固定的,只需要给出段内偏移地址就可以了,而这个偏移地址称为线性地址,线性地址是连续的,而内存分页将连续的线性地址和和分页后的物理地址相关联,这样逻辑上的连续线性地址可以对应不连续的物理地址 。
物理地址空间可以被多个进程共享,而这个映射关系将通过页表( page table)进行维护 。标准页的尺寸一般为 4KB ,分页后,物理内存被分成若干个 4KB 的数据页,进程申请内存的时候,可以映射为多个 4KB 大小的物理内存,而应用程序读取数据的时候会以页为最小单位,当需要和硬盘发生交换的时候也是以页为单位 。
现代计算机多采用虚拟存储技术,虚拟存储让每个进程以为自己独占整个内存空间,其实这个虚拟空间是主存和磁盘的抽象,这样的好处是,每个进程拥有一致的虚拟地址空间,简化了内存管理,进程不需要和其他进程竞争内存空间 。
因为他是独占的,也保护了各自进程不被其他进程破坏,另外,他把主存看成磁盘的一个缓存,主存中仅保存活动的程序段和数据段,当主存中不存在数据的时候发生缺页中断,然后从磁盘加载进来,当物理内存不足的时候会发生 swap 到磁盘 。页表保存了虚拟地址和物理地址的映射,页表是一个数组,每个元素为一个页的映射关系,这个映射关系可能是和主存地址,也可能和磁盘,页表存储在主存,我们将存储在高速缓冲区 cache 中的页表称为快表 TLAB。


推荐阅读