<audio id="audio" title="64 | 知识串讲：用一个创业故事串起操作系统原理（三）" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/19/07/19cc71d1d2659f99de7309a143b8bc07.mp3"></audio>

上一节我们说到，周瑜和张昭商定了调用schedule的时机。尽管项目越来越多，但是也井井有条。可是我们也说了，不管你的事情做得有多好，项目保密问题都是要解决的重要问题。怎么解决呢？今天我们就来看一看。

## 保密需封闭开发，空间小巧妙安排

慢慢地，小马发现，项目接的多了之后，CPU小伙伴的任务调度问题解决了之后，会议室的使用经常陷入混乱。不同的项目使用会议室的时候，经常冲突，一个项目组没用完，另一个项目组就在那里等着，十分耽误开发效率。

小马说：“要不咱们的项目别用会议室封闭开发了，原来总是说封闭开发，就是为了隔离，保密。这对于公司声誉来说很重要，但是能不能通过签订保密协议的方式来，干嘛非得封闭开发呢？”

周瑜说：“马哥，以我在大公司管理项目的经验来看，您还是想简单了。”

“你看，每次你接一个项目，总要写成项目执行计划书，CPU小伙伴们才能执行吧，项目计划书中的一行一行指令运行过程中，免不了要产生一些数据。这些数据要保存在一个地方，这个地方就是会议室（内存）。会议室（内存）被分成一块一块儿的，都编好了号。例如3F-10，就是三楼十号会议室。这个地址是实实在在的地址，通过这个地址我们就能够定位到物理内存的位置。”

“现在问题来了，写项目执行计划书的时候，里面的指令使用的地址是否可以使用物理地址呢？当然不行了，项目执行计划书，都是事先写好的，可以多次运行的。如果里面有个指令是，要把用户输入的数字保存在内存中，那就会有问题。”

“会产生什么问题呢？我举个例子你就明白了。如果我们使用那个实实在在的地址，3F-10，打开三个相同的程序，都执行到某一步。比方说，打开了三个计算器，用户在这三个程序的界面上分别输入了10、100、1000。如果内存中的这个位置只能保存一个数，那应该保存哪个呢？这不就冲突了吗？”

“如果不用这个实实在在的地址，那应该怎么办呢？那就必须用封闭开发的办法。

每个项目的物理地址对于进程不可见，谁也不能直接访问这个物理地址。操作系统会给进程分配一个虚拟地址。所有进程看到的这个地址都是一样的，里面的内存都是从0开始编号。

在程序里面，指令写入的地址是虚拟地址。例如，位置为10M的内存区域，操作系统会提供一种机制，将不同进程的虚拟地址和内存的物理地址映射起来。

当程序要访问虚拟地址的时候，由内核的数据结构进行转换，转换成不同的物理地址，这样不同的进程运行的时候，写入的是不同的物理地址，就不会冲突了。”

小马想想，对啊，这是个好办法，咱们得规划一套会议室管理系统（内存管理）。根据刚才的分析，这个系统应该包含以下三个部分：

第一，物理内存的管理，相当于会议室管理员管理会议室；

第二，虚拟地址的管理，也即在项目组的视角，会议室的虚拟地址应该如何组织；

第三，虚拟地址和物理地址如何映射的问题，也即会议室管理员如果管理映射表。

我们先来盘点一下物理内存的情况。

<img src="https://static001.geekbang.org/resource/image/8f/49/8f158f58dda94ec04b26200073e15449.jpeg" alt="">

不同的园区工位的安排和会议室的布局各不相同。

第一种情况是，CPU小伙伴们坐在一起，会议室在楼层的另一面，大家到会议室里面去都要通过统一的过道，优点简单，缺点是通道会成为瓶颈。

第二种情况是，会议室分成多个节点，离散地分布在CPU小伙伴周围。有的小伙伴离这个会议室近一些，有的小伙伴离另外一些会议室近一些。这样做的优点是，如果CPU小伙伴干活总是能够去离他最近的会议室，则速度非常快，但是一旦离他最近的会议室被占用了，他只能去其他会议室，这样就比较远了。

现在的园区基本都设计成第二种样子，也即会议室（内存）要分节点，每个节点用struct pglist_data表示。

每个节点里面再分区域，用于区分内存不同部分的不同用法。ZONE_NORMAL是最常用的区域。ZONE_MOVABLE是可移动区域。我们通过将物理内存划分为，可移动分配区域和不可移动分配区域，来避免内存碎片。每个区域用struct zone表示，也放在一个数组里面。

每个区域里面再分页。默认的大小为4KB。这就相当于每个会议室的最小单位。

如果有项目要使用会议室，应该如何分配呢？不能任何项目来了，咱都给他整个会议室。会议室也是可以再分割的，例如在中间拼起一堵墙，这样一个会议室就可以分成两个，继续分，可以再分成四个1/4大小的会议室，直到不能再分，我们就能得到一页的大小。

物理页面分配的时候，也可以采取这样的思路，我们称为伙伴系统。

空闲页放在struct free_area里面，每一页用struct page表示。

把所有的空闲页分组为11个页块链表，每个块链表分别包含很多个大小的页块，有1、2、4、8、16、32、64、128、256、512和1024个连续页的页块。最大可以申请1024个连续页，对应4MB大小的连续内存。每个页块的第一个页的物理地址是该页块大小的整数倍。

<img src="https://static001.geekbang.org/resource/image/3f/4f/3fa8123990e5ae2c86859f70a8351f4f.jpeg" alt="">

例如，要请求一个128个页的页块时，我们要先检查128个页的页块链表是否有空闲块。如果没有，则查256个页的页块链表；如果有空闲块的话，则将256个页的页块分成两份，一份使用，一份插入128个页的页块链表中。如果还是没有，就查512个页的页块链表；如果有的话，就分裂为128、128、256三个页块，一个128的使用，剩余两个插入对应页块链表。

把物理页面分成一块一块大小相同的页，这样带来的另一个好处是，当有的内存页面长时间不用了，可以暂时写到硬盘上，我们称为换出。一旦需要的时候，再加载进来，就叫作换入。这样可以扩大可用物理内存的大小，提高物理内存的利用率。在内核里面，也即张昭的管理下，有一个进程kswapd，可以根据物理页面的使用情况，对页面进行换入换出。

小马觉得这种方式太好了，如此高效地使用会议室，公司不用租用多少会议室，就能解决当前的项目问题了。

## 会议室排列有序，分视角各有洞天

周瑜说，“你先别急，这还仅仅是会议室物理地址的管理，每一个项目组能够看到的虚拟地址，咱还没规划呢！这个规划不好，执行项目还是会有问题的。”

每个项目组能看到的虚拟地址怎么规划呢？我们要给项目组这样一种感觉，从项目组的角度，也即从虚的角度来看，这一大片连续的内存空间都是他们的了。

如果是32位，有2^32 = 4G的内存空间都是他们的，不管内存是不是真的有4G。如果是64位，在x86_64下面，其实只使用了48位，那也挺恐怖的。48位地址长度也就是对应了256TB的地址空间。

小马说：“我都没怎么见过256T的硬盘，别说是内存了。”

周瑜接着说：“现在，一个项目组觉得，会议室可比世界首富房子还大。虽然是虚拟的，下面尽情地去排列咱们要放的东西吧！请记住，现在我们是站在一个进程的角度，去看这个虚拟的空间，不用管其他进程。”

首先，这么大的虚拟空间一切二，一部分用来放内核的东西，称为内核空间；一部分用来放进程的东西，称为用户空间。用户空间在下，在低地址，我们假设是0号到29号会议室；内核空间在上，在高地址，我们假设是30号到39号会议室。这两部分空间的分界线，因为32位和64位的不同而不同，我们这里不深究。

对于普通进程来说，内核空间的那部分，虽然虚拟地址在那里，但是不能访问。这就像作为普通员工，你明明知道财务办公室在这个30号会议室门里面，但是门上挂着“闲人免进”，你只能在自己的用户空间里面折腾。

<img src="https://static001.geekbang.org/resource/image/af/83/afa4beefd380effefb0e54a8d9345c83.jpeg" alt="">

我们从最低位开始排起，先是Text Segment、Data Segment和BSS Segment。Text Segment是存放二进制可执行代码的位置，Data Segment存放静态常量，BSS Segment存放未初始化的静态变量。这些都是在项目执行计划书里面有的。

接下来是堆段。堆是往高地址增长的，是用来动态分配内存的区域，malloc就是在这里面分配的。

接下来的区域是Memory Mapping Segment。这块地址可以用来把文件映射进内存用的，如果二进制的执行文件依赖于某个动态链接库，就是在这个区域里面将so文件映射到了内存中。

再下面就是栈地址段了，主线程的函数调用的函数栈就是用这里的。

如果普通进程还想进一步访问内核空间，是没办法的，只能眼巴巴地看着。如果需要进行更高权限的工作，就需要调用系统调用，进入内核。

一旦进入了内核，就换了一副视角。刚才是普通进程的视角，觉着整个空间是它独占的，没有其他进程存在。当然另一个进程也这样认为，因为它们互相看不到对方。这也就是说，不同进程的0号到29号会议室放的东西都不一样。

但是，到了内核里面，无论是从哪个进程进来的，看到的是同一个内核空间，看到的是同一个进程列表。虽然内核栈是各用各的，但是如果想知道的话，还是能够知道每个进程的内核栈在哪里的。所以，如果要访问一些公共的数据结构，需要进行锁保护。也就是说，不同的进程进入到内核后，进入的30号到39号会议室是同一批会议室。

<img src="https://static001.geekbang.org/resource/image/4e/9d/4ed91c744220d8b4298237d2ab2eda9d.jpeg" alt="">

内核的代码访问内核的数据结构，大部分的情况下都是使用虚拟地址的。虽然内核代码权限很大，但是能够使用的虚拟地址范围也只能在内核空间，也即内核代码访问内核数据结构，只能用30号到39号这些编号，不能用0到29号，因为这些是被进程空间占用的。而且，进程有很多个。你现在在内核，但是你不知道当前指的0号是哪个进程的0号。

在内核里面也会有内核的代码，同样有Text Segment、Data Segment和BSS Segment，内核代码也是ELF格式的。

不过有了这个规定以后，项目执行计划书要写入数据的时候，就需要符合里面的规定了，数据不能随便乱放了。

小马说，“没问题，这个作为项目章程，每一个新员工来了都培训。”

## 管理系统全搞定，至此生存无问题

周瑜接着说：“物理会议室和虚拟空间都分成大小相同的页，我们还得有一个会议室管理系统，将两者关联起来，这样项目组申请会议室的时候，也有个系统可以统一的管理，要不然会议室还不得老冲突呀。”

对于虚拟内存的访问，也是有一个地址的，我们需要找到一种策略，实现从虚拟地址到物理地址的转换。

为了能够定位和访问每个页，需要有个页表，保存每个页的起始地址，再加上在页内的偏移量，组成线性地址，就能对于内存中的每个位置进行访问了。

<img src="https://static001.geekbang.org/resource/image/ab/40/abbcafe962d93fac976aa26b7fcb7440.jpg" alt="">

虚拟地址分为两部分，页号和页内偏移。页号作为页表的索引，页表包含物理页每页所在物理内存的基地址。这个基地址与页内偏移的组合就形成了物理内存地址。

下面的图，举了一个简单的页表的例子，虚拟内存中的页通过页表映射对应到物理内存中的页。

<img src="https://static001.geekbang.org/resource/image/83/c3/83a5de160088a2e23e7c1a76c013efc3.jpg" alt="">

32位环境下，虚拟地址空间共4GB。如果分成4KB一个页，那就是1M个页。每个页表项需要4个字节来存储，那么整个4GB空间的映射就需要4MB的内存来存储映射表。如果每个进程都有自己的映射表，100个进程就需要400MB的内存。对于内核来讲，有点大了 。

页表中所有页表项必须提前建好，并且要求是连续的。如果不连续，就没有办法通过虚拟地址里面的页号找到对应的页表项了。

那怎么办呢？我们可以试着将页表再分页，4G的空间需要4M的页表来存储映射。我们把这4M分成1K（1024）个4K，每个4K又能放在一页里面，这样1K个4K就是1K个页，这1K个页也需要一个表进行管理，我们称为页目录表，这个页目录表里面有1K项，每项4个字节，页目录表大小也是4K。

页目录有1K项，用10位就可以表示访问页目录的哪一项。这一项其实对应的是一整页的页表项，也即4K的页表项。每个页表项也是4个字节，因而一整页的页表项是1k个。再用10位就可以表示访问页表项的哪一项，页表项中的一项对应的就是一个页，是存放数据的页，这个页的大小是4K，用12位可以定位这个页内的任何一个位置。

这样加起来正好32位，也就是用前10位定位到页目录表中的一项。将这一项对应的页表取出来共1k项，再用中间10位定位到页表中的一项，将这一项对应的存放数据的页取出来，再用最后12位定位到页中的具体位置访问数据。

<img src="https://static001.geekbang.org/resource/image/b6/b8/b6960eb0a7eea008d33f8e0c4facc8b8.jpg" alt="">

你可能会问，如果这样的话，映射4GB地址空间就需要4MB+4KB的内存，这样不是更大了吗？ 当然如果页是满的，当时是更大了，但是，我们往往不会为一个进程分配那么多内存。

比如说，上面图中，我们假设只给这个进程分配了一个数据页。如果只使用页表，也需要完整的1M个页表项共4M的内存，但是如果使用了页目录，页目录需要1K个全部分配，占用内存4K，但是里面只有一项使用了。到了页表项，只需要分配能够管理那个数据页的页表项页就可以了，也就是说，最多4K，这样内存就节省多了。

当然对于64位的系统，两级肯定不够了，就变成了四级目录，分别是全局页目录项PGD（Page Global Directory）、上层页目录项PUD（Page Upper Directory）、中间页目录项PMD（Page Middle Directory）和页表项PTE（Page Table Entry）。

<img src="https://static001.geekbang.org/resource/image/42/0b/42eff3e7574ac8ce2501210e25cd2c0b.jpg" alt="">

设计完毕会议室管理系统，再加上前面的项目管理系统，对于一家外包公司来讲，无论接什么样的项目都能轻松搞定了。我们常把CPU和内存合称为计算。至此，计算的问题就算搞定了。解决了这两大问题，一家外包公司的生存问题，就算解决了。

小马总算是可以松一口气了，他和周瑜、张昭好好地搓了一顿，喝得昏天黑地。周瑜和张昭纷纷感慨，幸亏当年跟了马哥，今日才有出头之日。

生存问题虽然解决了，马哥可非池中之物，接下来要解决的就是发展问题，马哥能想出什么办法进一步壮大企业呢？欲知后事，且听下回分解。
