坐井观天的:进程 | 虚拟内存 | 虚拟地址

“ 进程、线程有什么区别?虚拟地址和物理地址有什么区别?让我们用一只青蛙的视角,来解读它们背后的秘密”
进程、线程、虚拟地址、物理地址,这些名词既熟悉也陌生!似乎无论看多少资料,都很难准确地弄清楚它们之间的差异和存在的意义 。今天我们用CPU的视角,再次会会这个老朋友,看看你是否有新的启发?
 
奇怪的内存先写一个最简单的程序 。这个程序只做两件事情:定义一个全局变量 a,并赋值为:1;然后打印出 a 的地址和数值
#include <stdio.h>#include <unistd.h>int a = 1;int main(){printf("address: %p, value: %dn", &a, a);sleep(10000000);}将上面的代码,编辑成可执行程序:p1;接着,修改一下代码:给全局变量 a 赋值为:2,再次编译出可执行程序:p2 。
好了,两个程序:p1,p2 都准备好了,让我们再以两个进程的形式,运行它们:左边的窗口进程,运行p1;右边的窗口进程,运行p2

坐井观天的:进程 | 虚拟内存 | 虚拟地址

文章插图
【坐井观天的:进程 | 虚拟内存 | 虚拟地址】 
发现问题了吗?无论是 p1 还是 p2,它们输出的 a 的地址都是一样的,但 a 的值却不一样!难道同一个内存地址里面,既能存放 1,也能存放 2?
当然不是,原来0x555555558010是虚拟地址,并不是真实的物理内存地址,进程p1的0x555555558010,跟进程p2的0x555555558010,没有任何关系!
虚拟地址,只在进程内,是有意义的;可以用来指示不同变量,所对应的不同内存地址;但一旦跳出单一进程,在进程之间比较虚拟地址,就没有任何意义了!
 
MMU是什么软件,拥有如此的魔力?能让进程p1和进程p2的内存空间完全隔离?答案不是软件,而是硬件 — 现代CPU的协处理器:MMU
MMU的工作原理,未来我们还会详细阐述,这里只说结论:无论是进程p1,还是进程p2的,它们的变量 a 的地址都是虚拟地址,看上去是同一个地址,但实际上,已经被 MMU 映射到了不同的物理地址上去了 。这就是“进程”最显著的特点:空间独立性 。
坐井观天的:进程 | 虚拟内存 | 虚拟地址

文章插图
 
举个例子,进程就像一只井底之蛙,它固执地认为:自己已经拥有整个天空;但它永远不知道:天空到底有多大 。更不知道:周围还有很多跟它有一样想法的井底之蛙,而 MMU 就是束缚这些青蛙视野的井,每一口井,就是一个:进程空间 。
坐井观天的:进程 | 虚拟内存 | 虚拟地址

文章插图
 

坐井观天的:进程 | 虚拟内存 | 虚拟地址

文章插图
 
进程 vs 线程那看来全是 MMU 惹的祸,不要 MMU 行吗?当然可以,但一旦没有井的束缚,所有的青蛙,都跳到地面上,它们都可以看到一个完整的天空,所以,没有 MMU,进程也就不存在了,进程被降级成了:线程 。
这样的例子很多,例如:在没有 MMU 的单片机 。你就只会遇到线程或者叫:task,根本没有“进程”的概念 。
在 MMU 出现之前,计算机的世界里面,只有“线程”,在 MMU 出现之后,“进程”才真正落地,因为没有 MMU,就没有办法实现:内存空间的隔离,也就根本无法实现“进程”要求的:空间独立性 。
至于“进程”中的多“线程”就很容易理解了 。就是:一堆青蛙,都放在一个井里 。而且,它们都认为自己拥有整个天空 。
坐井观天的:进程 | 虚拟内存 | 虚拟地址

文章插图
 
因为,这些“线程”都处于同一个“进程”空间中,大家可以相互访问,完全没有任何限制 。这使得用“线程”实现多任务编程,会非常便利 。
坐井观天的:进程 | 虚拟内存 | 虚拟地址

文章插图
 
但也因为这种对安全的忽视,一旦任何一个“线程”崩溃
坐井观天的:进程 | 虚拟内存 | 虚拟地址

文章插图
 
所有的“线程”都不能幸免,大家一荣俱荣,一损俱损!
坐井观天的:进程 | 虚拟内存 | 虚拟地址

文章插图
 
所以,网络服务器一般都会使用“多进程”,而很少使用“多线程” 。这样即使某一个用户的服务进程崩溃了,其他“进程”还能继续正常工作 。这样,就不会因为某个用户的访问失败,而导致其他用户也无法访问服务器 。


推荐阅读