HeZephyr

HeZephyr's Blog

日拱一卒无有尽,功不唐捐终入海

HeZephyr's GitHub chart

1 锁:基本思想

举个例子,假设我们的临界区如下所示,共享变量的规范更新:

1
balance = balance + 1;

当然,其他临界区也是可能的,例如向链表添加元素或对共享结构进行其他更复杂的更新,但我们现在只保留这个简单的示例。要使用锁,我们在临界区周围添加一些代码,如下所示:

线程API

1 线程创建

编写多线程程序的第一件事就是创建新线程,因此必须有某种线程创建接口。在 POSIX 中,这很容易:

1
2
3
4
5
#include <pthread.h>
int pthread_create(     pthread_t*              thread, 
                  const pthread_attr_t*         attr, 
                        void*                   (*start_routine)(void*),
                        void*                   arg);

这个函数声明有四个参数:threadattrstart_routinearg。第一个参数 thread 是指向 pthread_t 类型结构的指针;我们将使用该结构与线程交互,因此需要将其传递给 pthread_create() 以对其进行初始化。

并发和线程

到目前为止,我们已经看到了操作系统执行的基本抽象的发展。我们已经了解了如何将单个物理 CPU 转变为多个虚拟 CPU,从而实现多个程序同时运行的错觉。我们还了解了如何为每个进程创建一个大的、私有的虚拟内存;当操作系统确实在物理内存(有时是磁盘)上秘密复用地址空间时,地址空间的这种抽象使每个程序的行为就好像它拥有自己的内存一样。

完整VM系统

在结束对虚拟内存的研究之前,让我们仔细看看整个虚拟内存系统是如何组合起来的。我们已经看到了此类系统的关键要素,包括大量页表设计、与 TLB 的交互(有时甚至由操作系统本身处理),以及决定将哪些页保留在内存中、将哪些页踢出内存的策略。然而,还有许多其他特性构成了一个完整的虚拟内存系统,其中包括许多性能、功能和安全特性。这就是我们的关键所在:如何构建完整的虚拟内存系统?实现完整的虚拟内存系统需要哪些功能?它们如何提高性能、增强安全性或改进系统?

交换策略

在虚拟内存管理器中,当您拥有大量可用内存时,生活会很容易。发生页面错误时,您在空闲页面列表中找到一个空闲页面,并将其分配给发生错误的页面。嘿,操作系统,恭喜!你又这么做了。

不幸的是,当空闲内存很少时,事情就会变得更有趣。在这种情况下,这种内存压力会迫使操作系统开始调出页面,为主动使用的页面腾出空间。决定调出哪个页面(或哪些页面)封装在操作系统的替换策略中;从历史上看,这是早期虚拟内存系统做出的最重要的决定之一,因为较旧的系统几乎没有物理内存。至少,这是一套值得更多了解的有趣政策。因此我们的问题关键是:操作系统如何决定从内存中驱逐哪一页(或多页)?这一决定是由系统的替换策略做出的,该策略通常遵循一些一般原则(如下所述),但也包括某些调整以避免极端情况行为。

页面交换

到目前为止,我们假设地址空间小得不切实际,并且适合物理内存。事实上,我们一直假设每个正在运行的进程的每个地址空间都适合内存。现在,我们将放宽这些重大假设,并假设我们希望支持许多并发运行的大型地址空间。

为此,我们需要在内存层次结构中增加一个级别。到目前为止,我们假设所有页面都驻留在物理内存中。然而,为了支持大地址空间,操作系统需要一个地方来存放当前需求不大的部分地址空间。一般来说,这样的位置的特点是容量要大于内存;因此,它通常会比较慢(如果它更快,我们就会将它用作内存,不是吗?)。在现代系统中,此角色通常由硬盘驱动器担任。因此,在我们的内存层次结构中,大而慢的硬盘驱动器位于底部,内存位于上面。

高级页表

现在我们解决分页带来的第二个问题:页表太大,因此消耗太多内存。让我们从线性页表开始。您可能还记得,线性页表变得相当大。再次假设 32 位地址空间($2^{32}$字节),具有 4KB($2^{12}$ 字节)页面和 4 字节页表条目。因此,地址空间中大约有一百万个虚拟页($\frac{2^{32}}{2^{12}}$);乘以页表条目大小,您会看到页表大小为 4MB。

快表

使用分页作为支持虚拟内存的核心机制可能会导致较高的性能开销。通过将地址空间分割成小的、固定大小的单元(即),分页需要大量的映射信息。由于映射信息通常存储在物理内存中,因此分页逻辑上需要对程序生成的每个虚拟地址进行额外的内存查找。在每次取指令或显式加载或存储之前访问内存以获取地址转换信息的速度非常慢。

有时有人说,操作系统在解决大多数空间管理问题时会采用两种方法之一。第一种方法是将事物切成可变大小的块,正如我们在虚拟内存中的分段中看到的那样。不幸的是,这个解决方案有其固有的困难。特别是,当将空间划分为不同大小的块时,空间本身可能会变得碎片化,因此随着时间的推移,分配变得更具挑战性。

因此,可能值得考虑第二种方法:将空间切成固定大小的块。在虚拟内存中,我们将这种想法称为分页,它可以追溯到一个早期且重要的系统,Atlas。我们不是将进程的地址空间划分为一定数量的可变大小的逻辑段(例如代码、堆、栈),而是将其划分为固定大小的单元,每个单元称为页面。相应地,我们将物理内存视为一组固定大小的槽(称为页框);每个框都可以包含一个虚拟内存页;每个进程都需要页表来将虚拟地址转换为物理地址。。我们的挑战:

关键:如何使用页面虚拟化内存 ?

如何使用页面虚拟化内存,从而避免分段问题?有哪些基本技术?我们如何以最小的空间和时间开销使这些技术发挥良好作用?

空闲空间管理

1 假设

本讨论的大部分内容将集中于用户级内存分配库中分配器的伟大历史。

我们假设有一个基本接口,例如 malloc() 和 free() 提供的接口。具体来说,void *malloc(size t size) 采用单个参数 size,它是应用程序请求的字节数;它返回一个指向该大小(或更大)的区域的指针(没有特定类型,或者 C 语言中的 void 指针)。补充例程 void free(void *ptr) 接受一个指针并释放相应的块。注意该接口的含义:用户在释放空间时,并不告知库其大小;因此,当只提供指向内存块的指针时,库必须能够计算出内存块有多大。

0%