Archive for操作系统

CLR 2.0 Memory Model

预备知识

任何支持多线程的系统都需要一个规范来描述多线程交互时如何精确的访问内存数据状态,我们称其为内存模型(Memory Model)。最简单的模型就是图1所展示的序列一致性内存模型(Sequential Consistency Memory Model)。在这个模型中,内存独立于任何使用它们的进程(线程)。内存通过内存控制器连接到每个线程,而内存控制器则会反馈每个线程的读写请求。来自单线程的读写请求会精确的按照线程的指定次序到达内存,然而它们也可能会和其他线程的读写请求按照一个未指定的方式交错进行。

fig01

图1 Sequential Consistency Memory Model

在图1所示的例子中,Value变量需要初始化,而Inited标志则用来表明其是否已被初始化。Value和Inited首先都被设置为0。线程1首先初始化Value(Value被设置成5),然后设置Inited为1表明Value已被初始化了。线程2把这些值读进寄存器Rv和Ri。在一个依赖于顺序的(sequential)程序中,不可能会有当Ri为1(说明Value已被初始化了)时,Rv还是0(Value未初始化)的情况发生。这归因于Sequential Consistency Memory Model。图2中的表格枚举了序列一致性内存所允许的所有6种合法排列,从中我们可以看出Ri和Rv最终的值是什么。没有一种情况当Ri非零时,Rv为零。

fig02

图2 Possible Permutations of Memory Requests

如前面的例子所示,序列一致性是很容易想到的一种模型。依赖于顺序的程序的一些概念都可以在其上应用。坦白说,它也很自然的在单处理器机器上获得实现。大多数程序员都对这种内存模型相当熟悉。不幸的是,对于真正的多处理器机器,通过内存硬件来高效的实现这种模型,非常受限制。并且,没有商业多处理器机器符合这种模型。

典型的多处理器机器内存系统看起来很像图3。每一个处理器都有一个相关的非常快速的小型缓存来记忆最近访问的数据。除此之外,所有从处理器到内存的写操作都会有一个缓冲区,以便处理器可以在数据刷到以及缓存前可以继续下一个指令。我们可以在图3看到每个处理器至少会有一级或更多级的缓存。这些缓存每一级都会比前一级容量更大,但是速度更慢。最终数据会到达内存,这样数据就可以被其他处理器共享。这种架构会加速处理器,而且也意味着不再会有单一内存视图。现在一个处理器会在各级缓存针对特定内存位置存储一个值,同时其他处理器也缓存了相同内存位置的数据,但是这个数据可能是过期的老的数据。这就可能导致处理器的缓存内容不一致。

fig03

图3 Realistic Memory System

上述情况肯定不符合需要。如果我们拿它来用于线程间通信铁定引发问题。但是如果我们刷新每一个线程的缓存数据来作为通信机制的一部分,系统就会可靠了。这样的系统是高效的,因为保持缓存同步的开销只会在线程使用内存通信时才会产生,这只占所有内存访问的很小的一部分。

图3所示的就是真实的内存系统,它并不符合序列一致性内存模型。因此我们就要创建一种内存模型来解决这样的内存系统所带来的问题。我们要做的就是让处理器互相“看到”所有移动内存存取。

fig04

图4 Initial Memory State

举个例子,考虑下Inited-Value示例运行在如图4所示的多处理器机器上会发生什么。注意,这里的缓存是简化的。在这个版本的系统里,主内存内的两个变量初始值都是0。处理器1的缓存现在是空的,而处理器2则已经读取Value的值(Inited还没读)到了它的本地缓存。如果我们以前读取过程序的其他数据,而Value又紧挨着这个数据的话,就有可能发生上述情况。

fig05

图5 Incoherent Caches

如果两个处理器均运行该程序的话,状态变化就如图5所示。不要考虑图5了,联系图4,现在我们把在主内存的数据Value设置成5,Inited则设置1。然而,由于缓存的原因,处理器2的内容不是我们所期望看到的。当处理器2读取Inited时,它没有在缓存中发现Inited的值,然后它看到其在主内存的值(Inited = 1)。当处理器2读取Value时,它在缓存中发现其值为过期的0。这其实是处理器2在早期读取的Value。这跟运行在序列一致性内存模型上的下列代码有着相同的行为:

CacheTemp = Inited
Rv = Value
Ri = CacheTemp

写操作跟读操作类似。

这种技术有三个优点。首先,它相对简单,不需要精确的详细描述硬件细节。其次,它可以让源代码回退到一个很简单易懂的风格。缓存读取可以比其真正需要时才读取更早的把值载入到一个临时变量。缓存写入则比写入最终位置要晚。最后,它可以应用于编译器优化中。

很明显,这种任意移动内存访问的能力会导致混乱。所以所有的实际内存模型有以下三个基本原则:

  1. 当线程隔离运行时,行为不会改变。它的意思是,指定线程到指定位置的读或写操作不会通过相同的线程到相同位置的写操作。
  2. 读操作在获得锁时,数据不能移动。
  3. 写操作在释放锁时,数据不能移动。

这就是锁协议实际的意义。这个协议确保了当持有相关锁时,所有的线程共享,读/写内存访问。

有了这三个原则就使得所有遵循锁协议的程序在任何内存模型都有相同的行为。这是一个极有价值的属性。没有这些关于编译器或内存系统能重新排序读写的必要思考,我们要写一个正确的并发程序将会非常困难。

遵循锁协议的程序不必思考这些。一旦你不想遵循锁协议,那么就必须详细说明和考虑硬件或编译器的读写变换。

较宽松的模型:ECMA

你现在已经明白序列一致性内存模型非常受限,因为它不允许任何交互的读写操作。在Section 12, Partition I of the .NET Framework ECMA standard描述了一个较宽松的模型。这个模型把内存访问操作划分成原始内存访问和那些特别标记为“volatile”的访问。Volatile内存访问不能创建,删除或移动。原始内存访问不止受限于三个基本原则,同时也要遵循以下两个原则:

  1. 在volatile读前,读操作和写操作都不能移动数据。
  2. 在volatile写后,读操作和写操作都不能移动数据。

这给了编译器和硬件相当的优化自由。它们只需要关心通过锁和Volatile访问的边界格式。对于没有使用锁或Volatile的程序片段,可以做任意的合法优化。这也意味着内存系统只需要在锁或Volatile访问时做相关的昂贵缓存失效,然后刷新即可。这个模型非常高效,但是需要程序在使用Low-Lock技术时,遵循锁协议或显式标记volatile。

健壮模型 1: x86 行为

不幸的是,正确遵循锁协议的程序还有更多例外。当设计基于x86架构的多处理器系统时,设计者需要一个内存模型来使得大多数程序可以正常工作,同时也允许硬件合理有效。规范需要单处理器写操作相对其他写操作保持有序,而读操作则不受限。

不幸的是,如果读操作不受限,写操作有序的保证等于什么也不做。因此,x86架构没有提供比ECMA模型更强的保证。

然而,我相信,x86实际实现的内存模型和文档上描述的有些微不同。因为在我的试验中正确预测行为这个模型从未失败,而且它跟公开已知的硬件如何工作完全一致,但是却不是官方规范。新的处理器可能会打破该规范。在这个模型中,除了三个基本内存模型规则,这些规则也起作用:

  1. A write can only move later in time.
  2. A write cannot move past another write from the same thread.
  3. A write cannot move past a read from the same thread to the same location.
  4. A read can only move by going later in time to stay after a write to keep from breaking rule 3 as that write moves later in time.

这个模型对于带有写缓冲队列和窥探读操作的系统很有效。写数据到内存时,不是立即写到内存,而是按序放入队列里。这些写操作或许会延迟,但是却保持了有序。高效率的读操作不会移动数据,除非允许窥探写操作队列。每一个读操作都会窥探写缓冲区来查看是否处理器最近写进了要读取的值,如果发现就会使用写缓冲区的值。因为逻辑上讲写缓冲区的写操作只在实体在刷到主内存时才会发生,高效率的读取操作也会延迟到那时才会获取其值。规则4特别允许了这种行为。

健壮模型 2: .NET Framework 2.0

尽管.NET Framework的ECMA模型规范并不是那么健壮,但是运行于x86机器上的.NET Framework 1.x运行时的实现模型却跟x86模型非常接近(源于JIT或JIT编译器优化)。在版本2.0时,这个模型在Intel IA-64处理器上却遇到了问题。那些依赖于x86实现的客户程序在类似IA-64平台上运行时会有问题。结果就导致了.NET Framework 2.0运行时内存模型,它的规则如下:

  1. All the rules that are contained in the ECMA model, in particular the three fundamental memory model rules as well as the ECMA rules for volatile.
  2. Reads and writes cannot be introduced.
  3. A read can only be removed if it is adjacent to another read to the same location from the same thread. A write can only be removed if it is adjacent to another write to the same location from the same thread. Rule 5 can be used to make reads or writes adjacent before applying this rule.
  4. Writes cannot move past other writes from the same thread.
  5. Reads can only move earlier in time, but never past a write to the same memory location from the same thread.

同x86模型一样的是写操作被严格限制了,不一样的则是读操作可以移动数据和可以被消除。因为重新在内存获取值和在low-lock代码内存中可以被改变,所以有了规则2.最后的规则似乎是多余的,但是如果允许通过写到相同位置就会改变要读取的值,这也就改变了序列行为。如果读取的值真正被使用了,这就会发生。这个规则更有技术性,而且它被特别添加进来是为了使得通用的延迟初始化模式在该模型中合法。

Lock-Free

对于编写lock-free代码来说,上面的描述并没有详细的涉及,下面我们来描述一下其规则:

  1. Data dependence among loads and stores is never violated.
  2. All stores have release semantics, i.e. no load or store may move after one.
  3. All volatile loads are acquire, i.e. no load or store may move before one.
  4. No loads and stores may ever cross a full-barrier (e.g. Thread.MemoryBarrier, lock acquire, Interlocked.Exchange, Interlocked.CompareExchange, etc.).
  5. Loads and stores to the heap may never be introduced.
  6. Loads and stores may only be deleted when coalescing adjacent loads and stores from/to the same location.

注意从定义来看,非易失性加载不需要请求拥有任何一种与其关联的内存屏障。所以加载可以被自由重新排序,写操作也可以在其后移动数据(由于规则2)。在这个模型里,如果你真的需要规则4所提供的完全内存屏障的话,就可以防止紧跟在易失性加载后重新排序。没有屏障,指令就会重新排序。

参考:

Understand the Impact of Low-Lock Techniques in Multithreaded Apps

Joe Duffy’s Weblog

评论

[翻译]Singularity项目概览 - 2.Singularity

Singularity是一个新的操作系统,它被用来开发成为更加可信赖系统和应用软件的基础平台。Singularity利用现代编程语言和工具的进步来创建一种环境,使得软件更容易被正确构建,使得程序行为更容易被验证,也能容忍运行时失败。

Singularity很关键的一方面是基于软件隔离进程(SIP)的扩展模型。SIP封装了程序或系统的一部分,并且提供了信息隐藏,失败隔离和强壮的接口。SIP贯穿于整个操作系统和应用程序软件中。我们相信,构建于这个抽象概念上的系统将会导致更加可信赖的软件平台。

SIP实际上就是Singularity操作系统的进程。所有内核之外的代码都运行在SIP中。SIP在许多方面都不同于常规操作系统的进程概念:

  • SIP是封闭的对象空间,而不是地址空间。两个Singularity进程不能同时访问一个对象。进程间通信则互斥转移数据的所有权。
  • SIP是封闭的代码空间。进程不能动态加载或生成代码。
  • SIP并不依赖内存管理硬件来达到隔离的目的。多个SIP均在同一物理或虚拟地址空间中。
  • SIP间通信是通过双向作用的,强类型的,高阶次序的管道来完成的。管道不仅说明了双方通信协议,还指出了传输的值和双方都是经过验证的。
  • 创建SIP开销并不大,而且在SIP间通信也是低负载。低开销便会使得把SIP作为良好细粒度和扩展的机制可行。
  • 我们通过操作系统来创建和结束SIP,这样在终止SIP时,SIP的资源也能被有效收回。
  • SIP独立运行,即使有不同的数据布局,运行时系统和垃圾收集。

SIP不止可以用来封装应用程序扩展,还可以用来作为保护和扩展的单独机制,来替代常规的进程和动态代码加载双重机制。如此一来,Singularity就只需要一个错误恢复模型,一个通信机制,一个安全策略和一个编程模型,而不是当前系统的层层多余机制和政策。Singularity的一个很重要的实验就是使用SIP来构造整个操作系统以及证明这个系统比常规系统更加可信赖。

Singularity内核几乎全部由安全代码组成。其余的部分则运行在SIP中,只由经过验证的安全代码组成,包括所有的设备驱动,系统进程和应用程序。在所有非信任代码必须验证其安全的同时,部分Singularity内核和运行时系统(叫做trusted base,即信任基础)则没有验证其安全。语言安全性保护了来自于非信任代码的trusted base。

SIP的完整依赖于语言安全性和泛系统不变量,即进程不能引用另外一个进程的对象空间。

确保代码安全性是很明显的要点。就眼前来说,Singularity依赖于源代码和中间代码的编译器验证。未来,类型化汇编语言(Typed Assembly Language,即TAL)将允许Singularity来验证编译代码的安全性。TAL要求可运行的程序提供它类型安全性的凭据(这个对于安全语言来说,会通过编译器自动产生)。对于简单审核几千行代码来验证凭据是否正确,以及运行指令是否适合是一件非常简单的任务。那么这种端到端的验证策略便可以从Singularity的trusted base来剔除编译器。这个验证程序必须被仔细设计,实现和检验,但这些任务是可行的。这是由于它的尺寸和简易性。

独立于内存的不变量(the memory independence invariant,即禁止跨对象空间的指针)担当了几个用途。首先,通过隐藏实现的具体细节和预防指针指向已终结的进程,它增强了进程的数据抽象和失败隔离。其次,通过允许进程拥有不同的运行时系统和垃圾收集器,它减轻了运行无关的实现限制。再次,通过明确内存的进程所有权,它阐明了资源计数和回收。最后,通过消除操作多类型指针和地址空间的需要,它简化了内核接口。

该架构的主要难点在于通过消息传递的通信与直接共享数据相比缺乏灵活性。Singularity现在正通过有效的消息系统,编程语言扩展(基于管道的简洁明确的通信)和验证工具致力于解决这个问题。

评论

[翻译]Singularity项目概览 - 1.介绍

软件运行平台发展演变已经超过40年了。该平台是一个巨大的代码集合,包括了操作系统,编程语言,编译器,代码库,运行时系统,中间件等。此外,还包括运行程序的硬件。一方面,该平台在金融和实际应用上取得了巨大成功。它形成了价值1790亿美元软件工业的基础,并且促使了一大批的革命性创新,比如Internet。另一方面,该平台以及运行在其上的软件越来越缺乏健壮性,可靠性和大多数用户(以及开发人员)所期望的安全性。

部分问题是我们目前的平台并没有演变到远远超越1960-1970年代的计算机体系结构,操作系统和编程语言。那时的计算环境非常不同于现在的周遭环境。那时计算机的运行速度和存储容量十分有限。只有一小部分技术学者和无恶意用户在使用它,并且只有相当少的机器组网或连接到物理设备。虽然这些特征现在没有一样保留下来,但是与那时的计算机和使用方式相比,现代计算机体系结构,操作系统,编程语言并没有出现根本性变化。

Singularity是微软研究院的一个研究项目。它起始于这样的问题:如果从头开始设计一个以可信赖为首要目标的软件平台,而不是大家普遍认可的性能目标,那么它应该是什么样子?Singularity正是用来回答该问题的。为了产生更加健壮和可信赖的软件平台,我们以高级编程语言和工具为基础,开发了一个新系统架构和操作系统(即Singularity)。尽管可信赖性非常难以衡量,Singularity还是展示了新技术架构的可行性,并且将会导致建造更加健壮和可信赖的系统。

随着硬件的发展,通常会驱动系统和应用程序发生根本性变化。而软件的发展则很少会创建根本性改善的机会。然而,软件也在演变,而且正是它的变化使得重新思考旧有的设定和习惯成为可能,甚至是必要。高级编程语言,运行时系统和程序分析工具构造的体系结构和系统比那些现存的系统更加健壮和可信赖。原因如下:

  • 富于表达,安全的编程语言,比如Java和C#。类型安全性可以保证值或对象总被正确解释和操作。内存安全性则保证程序只在合法存活的对象边界上引用内存。
  • 优化编译器和高性能运行时系统会生成安全代码,而且这些安全代码的速度可以与非安全代码相媲美。不同于普通的JIT编译器,我们的编译器会执行全局优化来缓解因为安全而带来的额外开销。系统中的垃圾收集器也会修正额外存储开销,且媲美于那些显式的存储单元分配。
  • 验证技术可以确保编译器,编译过的代码以及运行时系统的端到端类型安全。类型化中间语言和汇编语言可以验证系统组件的正确操作,并且保证语言的安全性。而语言安全性则是系统正确性的基础。
  • 合理的驱动规范缺陷检测工具可以保证系统许多方面的正确性。这些工具能够检测到错误的发生(伴随错误的确定),而且当某个缺陷被消除时也能可靠地显示出来,也不会去找寻电路上的缺陷集。尽管如此,这些工具却很容易扩展,适于检查程序或特定库是否被正确使用。

基于以上高级技术的语言和工具完全可以被用在检测和预防编程错误上。探究系统架构机制的变化,反过来或许会在预防和减少软件缺陷的终极目标上更进一步。

评论

[翻译]Singularity

原文链接 - http://msdn.microsoft.com/msdnmag/issues/06/06/EndBracket/

当发明C和C++语言时,计算机速度相当慢,存储器容量有限且昂贵,编译器也很简易。所以那时一门实际应用的语言只能是对汇编语言的修饰而已。然而,随着时代变更,这却限制了现代软件发展。今天,程序性能已经很少受限于原生处理器的速度。相反,延迟内存,磁盘,网络,数据库等经常对性能有着绝对性作用。此外,软件的可信任性(即软件可靠性,实用性,安全性的综合)已经成为软件开发的主要挑战。

通过预防(或者至少是检测)多数通用编程错误,安全的编程语言可以增加软件的可信任性。安全性包括两部分:类型和存储。类型安全是说程序不应当把类型A的对象视为无关类型来对待(C/C++语言提供了这个“特性”)。存储安全指的是程序不应当通过对象边界或构造对象指针来引用。比如,把某个随机整数转换成指针。

不必怀疑你所认为的:“嗯…,你那种调调我以前就听过了,但是安全的语言对我的应用程序来说太慢了,很吃内存的”。事实上,或许你更应该关注所使用的编程语言或操作系统的实现,而不是语言安全性。

为了探究这些问题,微软研究院的队伍已经构建了一个新的系统来研究权衡取舍在安全语言中广泛使用的用法,以及证明它们并不会导致大的性能下降。

Singularity是一个新操作系统。它几乎完全用C#写成,只运行可验证的安全程序。Singularity的内核和运行时库只是系统的一部分。此外,还包含不安全代码。大部分代码都是用安全的C#写成的。

Singularity通过重新改造代码执行环境达到了良好的性能。在现存的系统中,安全代码相对于那些生活在系统上流社会的服务来说,它是个奇怪的外来客。与此相反,Singularity架构了一个与现存系统性能相媲美的,每个人都安全的单一世界。

很关键的一个起始点就是Singularity的进程。它一开始是空的,然后按需增加特性。现代语言运行时往往伴随着巨大的库和富于表现的动态语言特性,比如反射。这就是丰富的代价。像代码访问安全或反射等特性会导致高开销,即使从来都不使用。

Singularity应用程序详细说明了它需要哪个库,而且Bartok编译器会与代码结合,通过调用进程“tree shaking”(该进程删除了未使用的类,方法,甚至字段)消除了不必要的功能。结果,在Singularity中一个简单的C#“Hello world”进程占用的内存,比起与之相当的运行在大多数Unix或Windows系统上的C/C++程序还要小。此外,Bartok编译器还把微软中间语言(MSIL)转换成高度优化过的x86代码。它还通过中间过程优化来消除多余的运行时安全测试,减少语言安全性的消耗。

极其有效的中间过程优化是非常有可能的,因为Singularity进程是封闭的。系统不允许在进程开始运行之后继续加载代码。这是个引人注目的变化,因为动态代码加载是个非常流行,但又有问题的插件机制。允许插件程序访问程序内部导致了很严重的安全性和可靠性问题(你可知道大约85%的Windows蓝屏是由第三方插件和设备驱动引起的)。动态加载会破坏在编译器或缺陷检测工具中的程序分析工作,因为或许会看不到所有的代码运行。为了保证安全,必须保守分析,预先排除许多优化,削减缺陷检测精度。

在Singularity中的插件程序运行在它们自己的进程空间中,并且用仔细验证过的通信管道来通信。这种架构是可行的,因为创建Singularity进程和进程间通信并不昂贵。这是缘于Singularity进程依赖于语言安全性,而不是虚拟内存硬件来强制隔离。

重新思考程序运行环境会极大减少现代安全编程语言所带来的负作用。Singularity是个重新设计的系统,这样程序员便不再为了速度而牺牲安全性。

评论