Blog

conforming与non-conforming代码段

Content #

  1. conforming类型强迫使用低权限或相等权限(CPL>=DPL)来运行,进入conforming段运行不改变当前运行的CPL值(无论是通过直接调用还是gate调用)。
  2. nonconforming类型限制用户使用低权限来运行(进入高权限代码需要通过gate符调用)。

conforming段: 用于不重要、无须保护的代码。 non-conforming段:用于需要保护的代码和数据。

在某些场合下,使用conforming段会比使用non-conforming段灵活:某个库routine并不涉及重要的数据和使用系统资源,它能在任意权限下运行:

  1. 使用conforming段将DPL设为0级权限,在3级权限下可以直接调用(CPL>DPL),在0级权限下,依然可以使用直接调用(CPL==DPL)。
  2. 使用non-conforming段将DPL设为3级权限,在3级权限下可以直接调用(CPL==DPL),而在其他级别无法直接调用,例如在0级不能直接调用3级权限的代码(CPL!=DPL)。将DPL设为0级权限,在3级权限下可以使用gate符进行调用,在0级权限下也可以使用gate符进行调用。

相比之下,non-conforming段的执行权限需要被定义为0级,通过gate符进行调用,显得不如conforming段灵活。并且conforming段定义在3级权限,不会改变调用者的CPL值。对于不重要的库routine来说,使用conforming段会更适合些。

Viewpoint #

From #

保护模式的三种权限类型

Content #

在保护模式的权限检查中使用了3种权限类型。

  1. CPL(Current Privilege Level)当前的权限级别,它指示当前运行的代码在哪个权限级别里,CPL的值存放在 CS寄存器Selector域的RPL,CS.Selector.RPL与SS寄存器的Selctor.RPL总是相等的,因此:SS.Selector.RPL也是CPL。
  2. DPL(Descriptor Privilege Level) DPL存放在Descriptor(包括Segment Descriptor和Gate Descriptor)里的 DPL域,它指示访问这些segment所需要的权限级别。Gate描述符的DPL值指示访问Gate的权限,并不代表由Gate所引用的Segment的权限。
  3. RPL(Requested Privilege Level) RPL存放在访问者所使用Selector的Bit 0和Bit 1位,它指示着发起访问的访问者使用什么样的权限对目标进行访问。因此,访问者可以使用不同的权限对不同的目标进行访问。当Selector被加载到段寄存器时,CS.RPL和SS.RPL 代表着当前的CPL值。

值得注意的是,数字越大权限越低,假如遇到以下的情况(在CPL与DPL的比较中): CPL>DPL 表示当前运行的权限级别不足,不能对segment或gate进行访问,只有在 CPL<=DPL的情况下才能对segment或gate进行访问。

Viewpoint #

From #

IO多路复用模型

I/O 多路复用(I/O Multiplexing) #

I/O多路复用模型下,应用线程与内核之间的交互行为模式如下图:

在这种模型下,应用线程首先将需要进行 I/O 操作的 Socket,都添加到多路复用函数中(这里以 select 为例),然后阻塞,等待 select 系统调用返回。当内核发现有数据到达时,对应的 Socket 具备了通信条件,这时 select 函数返回。然后用户线程会针对这个 Socket 再次发起网络 I/O 请求,比如一个 read 操作。由于数据已就绪,这次网络 I/O 操作将得到预期的操作结果。

相比于阻塞模型一个线程只能处理一个 Socket 的低效,I/O 多路复用模型中,一个应用线程可以同时处理多个 Socket。同时,I/O 多路复用模型由内核实现可读 / 可写事件的通知,避免了非阻塞模型中轮询,带来的 CPU 计算资源浪费的问题。

目前,主流网络服务器采用的都是“I/O 多路复用”模型,有的也结合了多线程。不过,I/O 多路复用模型在支持更多连接、提升 I/O 操作效率的同时,也给使用者带来了不小的复杂度,以至于后面出现了许多高性能的 I/O 多路复用框架,比如:libevent、libev、libuv等,以帮助开发者简化开发复杂性,降低心智负担。

Viewpoint #

From #

36|打稳根基:怎么实现一个TCP服务器?(上)

非阻塞IO模型

Content #

非阻塞 I/O 模型下,应用线程与内核之间的交互行为模式是这样的:

和阻塞 I/O 模型正相反,在非阻塞模型下,当用户空间线程向操作系统内核发起 I/O 请求后,内核会执行这个 I/O 操作,如果这个时候数据尚未就绪,就会立即将“未就绪”的状态以错误码形式(比如:EAGAIN/EWOULDBLOCK),返回给这次 I/O 系统调用的发起者。而后者就会根据系统调用的返回状态来决定下一步该怎么做。

在非阻塞模型下,位于用户空间的 I/O 请求发起者通常会通过轮询的方式,去一次次发起 I/O 请求,直到读到所需的数据为止。不过,这样的轮询是对 CPU 计算资源的极大浪费,因此,非阻塞 I/O 模型单独应用于实际生产的比例并不高。

Viewpoint #

From #

36|打稳根基:怎么实现一个TCP服务器?(上)

阻塞IO模型

Content #

阻塞 I/O 是最常用的模型,这个模型下应用线程与内核之间的交互行为模式是这样的:

在阻塞 I/O 模型下,当用户空间应用线程,向操作系统内核发起 I/O 请求后(一般为操作系统提供的 I/O 系列系统调用),内核会尝试执行这个 I/O 操作,并等所有数据就绪后,将数据从内核空间拷贝到用户空间,最后系统调用从内核空间返回。而在这个期间内,用户空间应用线程将阻塞在这个 I/O 系统调用上,无法进行后续处理,只能等待。

因此,在这样的模型下,一个线程仅能处理一个网络连接上的数据通信。即便连接上没有数据,线程也只能阻塞在对 Socket 的读操作上(以等待对端的数据)。虽然这个模型对应用整体来说是低效的,但对开发人员来说,这个模型却是最容易实现和使用的,所以,各大平台在默认情况下都将 Socket 设置为阻塞的。

Viewpoint #

From #

36|打稳根基:怎么实现一个TCP服务器?(上)

mheap结构

Content #

mheap 在 Go 的运行时里边是只有一个实例的全局变量。维护 Go 的整个虚拟内存布局的 heapArena 的二维数组,就存放在 mheap 中。mheap 结构对应于 TCMalloc 中的 Page heap 结构。mheap 的主要结构如下:

type mheap struct {
    lock  mutex

    arenas [1 << arenaL1Bits]*[1 << arenaL2Bits]*heapArena
    central [numSpanClasses]struct {
        mcentral mcentral
        pad      [cpu.CacheLinePadSize - unsafe.Sizeof(mcentral{})%cpu.CacheLinePadSize]byte
    }
}

var mheap_ mheap

mheap 中存放了 68×2 个不同 spanClass 的 mcentral 数组,分别区分了 scan 队列以及 noscan 队列。

Viewpoint #

From #

24 | GC实例:Python和Go的内存管理机制是怎样的?

参看 Go虚拟内存布局

条件轮询(Mutex and Cond)

条件变量 #

sync.Cond是传统的条件变量原语概念在 Go 语言中的实现。我们可以把一个条件变量理解为一个容器,这个容器中存放着一个或一组等待着某个条件成立的 Goroutine。当条件成立后,这些处于等待状态的 Goroutine 将得到通知,并被唤醒继续进行后续的工作。这与百米飞人大战赛场上,各位运动员等待裁判员的发令枪声的情形十分类似。

条件变量是同步原语的一种,如果没有条件变量,开发人员可能需要在 Goroutine 中通过连续轮询的方式,检查某条件是否为真,这种连续轮询非常消耗资源,因为 Goroutine 在这个过程中是处于活动状态的,但它的工作又没有进展。

这里我们先看一个用sync.Mutex 实现对条件轮询等待的例子:

type signal struct{}

var ready bool

func worker(i int) {
  fmt.Printf("worker %d: is working...\n", i)
  time.Sleep(1 * time.Second)
  fmt.Printf("worker %d: works done\n", i)
}

func spawnGroup(f func(i int), num int, mu *sync.Mutex) <-chan signal {
  c := make(chan signal)
  var wg sync.WaitGroup

  for i := 0; i < num; i++ {
    wg.Add(1)
    go func(i int) {
      for {
        mu.Lock()
        if !ready {
          mu.Unlock()
          time.Sleep(100 * time.Millisecond)
          continue
        }
        mu.Unlock()
        fmt.Printf("worker %d: start to work...\n", i)
        f(i)
        wg.Done()
        return
      }
    }(i + 1)
  }

  go func() {
    wg.Wait()
    c <- signal(struct{}{})
  }()
  return c
}

func main() {
  fmt.Println("start a group of workers...")
  mu := &sync.Mutex{}
  c := spawnGroup(worker, 5, mu)

  time.Sleep(5 * time.Second) // 模拟ready前的准备工作
  fmt.Println("the group of workers start to work...")

  mu.Lock()
  ready = true
  mu.Unlock()

  <-c
  fmt.Println("the group of workers work done!")
}

轮询的方式开销大,轮询间隔设置的不同,条件检查的及时性也会受到影响。

...

条件变量的Wait方法主要做了哪四件事情

Content #

Go语言中条件变量的Wait方法主要做了哪四件事情?

  1. 把调用它的 goroutine(也就是当前的 goroutine)加入到当前条件变量的通知队列中。
  2. 解锁当前的条件变量基于的那个互斥锁。
  3. 让当前的 goroutine 处于等待状态,等到通知到来时再决定是否唤醒它。此时,这个 goroutine 就会阻塞在调用这个Wait方法的那行代码上。
  4. 如果通知到来并且决定唤醒这个 goroutine,那么就在唤醒它之后重新锁定当前条件变量基于的互斥锁。自此之后,当前的 goroutine 就会继续执行后面的代码了。

Viewpoint #

From #

两个偏爱使用sync的场景

Content #

一般情况下,优先使用 CSP 并发模型进行并发程序设计。但是在下面一些场景中,我们依然需要 sync 包提供的低级同步原语。

  1. 需要高性能的临界区(critical section)同步机制场景。

在 Go 中,channel 并发原语也可以用于对数据对象访问的同步,我们可以把 channel 看成是一种高级的同步原语,它自身的实现也是建构在低级同步原语之上的。也正因为如此,channel 自身的性能与低级同步原语相比要略微逊色,开销要更大。

  1. 在不想转移结构体对象所有权,但又要保证结构体内部状态数据的同步访问的场景。

基于 channel 的并发设计,有一个特点:在 Goroutine 间通过 channel 转移数据对象的所有权。所以,只有拥有数据对象所有权(从 channel 接收到该数据)的 Goroutine 才可以对该数据对象进行状态变更。

如果你的设计中没有转移结构体对象所有权,但又要保证结构体内部状态数据在多个 Goroutine 之间同步访问,那么你可以使用 sync 包提供的低级同步原语来实现,比如最常用的sync.Mutex。

Viewpoint #

From #

34|并发:如何使用共享变量?