*OS Internals 卷一 读书笔记-第八章 Threads and GCD

97 阅读38分钟

第八章 Parts of the Process:Threads and GCD

UI Applications, CLI utilties和system daemons都是进程,Darwin作为POSIX兼容系统,使用一组标准system calls来处理。这一章开头提供了进程的概览, 松散地分类了管理它们的system calls ——-lifecycle、identifiers、credentials和signals。同时这个模型非POSIX的扩展——也就是, coalitions (一种grouping形式)和personae (一种credentials形式)会讨论到

但进程只是外壳也就是容器,其内部可调试的实体是线程。因此讨论不可避免地转移到了线程,尤其是用户态的POSIX线程库API。虽然这一章深度讨论了线程,但假定读者是熟悉这些API和编程模型的。我们讨论的是标准的实现和扩展而不是API本身(除了一些非POSIX的扩展)

Darwin通过GCD(Grand Central Dispatcher)及其block和queue模型提供了相比线程再高层的抽象。 GCD的APIs文档极多(in dispatch(3)和its various apropos(1) pages),同样地假定用户对这些有先验的概念。这一章会先解释BSD kqueue (2)和 kevent (2) APIs (是GCD的基础),然后深入GCD的复杂实现,聚焦在block,queue和dispatch source背后的对象。如果不是一些特定和未文档化的system calls,GCD不会如此高效,所以这些都会重点介绍。最终会介绍一些debug方法

Processes

对系统来说,进程是资源——虚拟内存和描述符——容器,也是维护执行统计数据的容器。

需要注意的是进程不是可运行实体,一个进程对应一个运行中的执行文件实例,但不是实际的执行代码。执行文件中的可执行实体是线程。进程技术上只是一到多个线程的容器,并提供所有线程共享的虚拟内存映射image,描述符descriptors和ports。因此当一个人refer to a process as "executing"时,正确的terminology是"at least one of the threads of the process pid is executing"

进程属性 - 像进程credentials, label, scheduling priority等 - 也适用于apply to它的线程。有些像优先级可能在单独在每线程的维度更改。由于历史原因(和管理多线程的难度),PID仍在处理进程中的线程时使用。像ps(1), top(1)和作者的process explorer工具,展示了进程统计数据,但并不是在线程粒度,除非显式要求(e.g. with ps -M or procexp threads)

这里有ps命令展示进程属性和统计数据的图示

Darwin系统自标准模型有一定变化。为了维持POSIX兼容和UNIX接口,很多命令行工具和BSD system calls管理、维护并收集进程的统计数据。而如本书后面描述的,Darwin's BSD-layer进程其实是底层Mach层task的投影。实际的基础对象是Mach tasks,而调度器管理的调度的也是task的线程 POSIX system calls的很大一部分是用来处理进程的,我们接下来讨论这些calls,分类上先从生命周期开始

Process Lifecycle

POSIX/BSD模型定义了进程状态机,这些状态由ps(1)在其BSD mode 'STAT'下展示 (q.v. Output 8-1) and shown in Figure 8-2:

image.png 需要注意的是遵从前述说法,进程在running状态时它的至少一条线程是在执行中,进程stop或者sleep时其所有线程都停止了。zombie只适用于主线程,此时所有线程都已经终止了。

Table 8-3: Process-lifecycle system calls provided by XNU

#Syscall nameFunction
1exitTerminate self,returning exit status
2forkClone process,including memory,descriptors,etc
7/400wait4[_noncancel]Wait for child process to terminate
173/416waitid[_noncancel]Wait for child process to change state(or terminate)
66vforkClone process,skipping memory and descriptors
244posix_spawnSpawn a process(equivalent to fork and immediate exec)
59execveExecute a binary in current process
380mac_execveExecute a binary in current process, with MAC label

POSIX只允许进程创建进程(例外PID 1,它是直接从内核孕育的)。进程创建要经历两个过程首先调用fork()或者vfork(),会创建其调用者的拷贝,这个system call因此如果成功会回调两次:一次在调用者的空间,返回复制进程的PID,另一次是在被复制出来的进程,返回0。除了返回值和PID,两个进程会完全一样,包括线程和打开的文件描述符 两个一样的进程很少是有用处的,所以下一个阶段通常包括(通常是被复制出的进程)调用execve(2)(通过它的一个包装方法exec[lv][pe])。这个system call的作用是将一个新执行文件加载进调用者的地址空间,抹去进程中的当前image。这个操作如此普遍,以致vfork()调用引入了"quick fork",可以不做任何拷贝。 POSIX曾扩展出更现代的posix_spawn(2) system call,其将fork(2)和execve(2)调用合而为一,更高效。此外posix_spawn接受Table 8-4中的flags,用以修改spawned进程创建参数(阴影rows代表私有参数):

image.png

Process Identifiers and Grouping

Table 8-6: Process ID and Process group system calls provided by XNU

#Syscall nameFunction
20getpid(2)Get own process identifier
151getpgid(2)Get process group identifier of a pid
81getpgrp(2)Get own process group identifier
39getppid(2)Get parent process identifier
82setpgrp/setpgid(2)Set process group identifier of self or a pid
147setsid(2)Set session group identifier (starts a session) for self

Coalitions (Darwin 14+)

进程组是UNIX标准feature,但Darwin 14引入了一种叫做coalitions的新grouping机制。Coalitions让进程可以group起来,但方式更高高级:加入一个coalition允许进程pool资源(也就是coalitions的COALITION_TYPE_RESOURCE),共享一个ledger* 或者面对Jetsam (第9章会介绍,COALITION_TYPE_JETSAM)。进程也能自愿成为coalition中的某个角色:Leader (1), XPC service (2) or Extension (3)

* - Ledgers are a Mach object used for process accounting, which was re-introduced by Apple into XNU in Darwin 12. It is discussed in greater detail (along with Mach task and thread internals) in Volume II.

image.png 可以用Process Explorer或者内置的taskinfo(1)来查看coalitions

Credentials

进程代表执行它们的用户执行操作。 为此,他们需要持有凭证,以便系统识别他们的身份。 POSIX 将用户标识符 (uids) 和组标识符 (gids) 识别为凭据。 每个进程可以拥有一个主 uid 和一个主 gid,但可以属于多个组。 如果二进制文件标记有适当的 setuid/setgid 位,则该进程还可能请求更改有效effective用户和组标识符。

Table 8-8:Credential calls provided by XNU

#Syscall nameFunction
24/25get[e]uid(2)get [effective] user ID
79/80/243[get/set]groups(2)get/set group access list
327issetugid(2)is current process tainted by uid or gid changes
182/183sete[g/u]id(2)set effective group/user ID
181setgid(2)set group ID
126/127setre[u/g]id(2)set real and effective group/user IDs
23setuid(2)set user ID
285/286[set/get]tid(2)set per-thread identity override
311settid_with_pid(assume, pid)assume/revert identity of/from another process given by pid
287/288[set/get]sgroupsset/get supplementary groups list(UNSUPPORTED)
289/290[set/get]wgroupsget/set whiteout groups list(UNSUPPORTED)

Personae (*OS)

Darwin 16引入了一种新的system call - persona(#494)。a persona提供了进程或线程(在创建时)可以采用的一组备用凭据 - 类似于 UNIX setuid(2)/setgid(2) 系统调用,但提供了更具体和原子的凭据credential更改。 接受a persona意味着uid,gid和group的一次性变更。

Personae定义为struct kpersona_info in XNU's bsd/sys/persona.h,目前只在*OS中使用,目前只被keybagd使用,其在加载或者卸载a syncdomain的时候调用libxpc.dylib的launch_[create/destroy]_persona

Signals

UNIX提供了信号作为打断进程的方法。发送信号可以通过kill (2) system call,或者kill (1)命令行(其是system call的简单封装)。两种方式都需要指定pid和信号类型。Table 8-11展示了Darwin支持的信号:

image.png

Pthreads

所有UN*X支持POSIX thread标准,称为pthreads,通过-lpthread*链接上。Darwin支持pthread所有API,在pthread中有详细文档化。 Darwin的pthread实现在/usr/lib/system/libsystem_pthread.dylib中。因为它在libSystem.B.dylib中被re-exported,所以没有必要显示链接它,所以可以假定其一直存在。这个库也是开源的(libpthread项目),且编译时指定了__DARWIN_UNIX03以确保完全的POSIX兼容

Non-POSIX extensions

Darwin在POSIX标准基础上添加了很多非标准的扩展,扩展都会带上"_np"后缀

image.png Apple意在用这些扩展作为与底层Mach thread ports的胶水,与GCD整合(libdispatch.dylib)并容纳Quality of Service这个新模型。

Initialization

pthread的初始化器 __pthread_init()会在libSystem的初始化过程中被调用,随即会为用户态pthread线程管理进行内部状态的初始化。 image.png libpthread初始化期间,__bsdthread_register(pthread_bsdthread_init())会被调用。这个未文档化的system call (#366)用来在用户态向内核注册thread函数地址for newly created threads。这里涉及到两个函数地址 - thread_start(一个最终会执行pthread_start的trampoline,pthread_start会执行用户提供的线程入口地址)和wqthread (workqueues使用,后面会讨论)

bsdthread_register是专门为pthread支持设计的。其一旦返回, system call会返回一组内核支持的threading capabilities ("features"),并且会缓存以供未来使用。pthread demands support for QoS classes (PTHREAD_FEATURE_QOS_DEFAULT and . . MAINTENANCE),XNU目前返回这些features:

image.png

Thread lifecycle

pthread_create(3)负责创建pthread管理的新线程,但其在Darwin中的实现大部分是offloaded给XNU's bsdthread_create。这个未文档化的system call(#360) takes the user mode start_routine和arg, 一个线程栈(通常由内核根据thread_attr_setstacksize(3)分配,但可以被调用者通过pthread_attr_setstackaddr(3)设置)和一些flags。flags是pthread_create()根据pthread_attr值填充的,或者默认值(Table 8-17, later)

线程由pthread_exit(3)销毁,其内部会调用pthread_terminate。这个函数会移除所有线程数据并移除线程管理数据结构, signaling a semaphore for thread_join(3) (创建为joinable的线程),也会释放Mach thread ports。这里也有一个未文档化system call - bsdthread_terminate(3) (#361)- 处理内核侧,释放内核使用的数据结构

相比于thread-specific signalling (使用Table 8-12中的system calls),更偏好的在同一进程中从一条线程终止线程的方法是pthread_cancel(3)。如果目标线程thread_setcancelstate(3)返回值是PTHREAD_CANCEL_ENABLE且线程执行走到了一个取消点(或者通过用户态pthread_testcancel(3),或者system call 在内核态),线程就会被标记为canceled。控制流会走向pthread_exit(3),以及目标线程的优雅终结。

本书第12章和卷2会讨论到,BSD线程 - 支撑POSIX threads - 自己其实是由Mach threads支撑起来的。到Darwin 14时,XNU's BSD thread的大部分支持已经移到了pthread.kext,pthread project的一部分,幸运的是仍是开源的。

Figure 8-16展示了线程生命周期in the context of the three layers:

image.png

Introspection

Darwin pthread允许introspection。与其它库需要重新构建以获取introspection能力不同,MacOS 10.9 and iOS 7的发行版上就有这个能力(虽然<pthread/introspection.h>警告不要在发布的代码中使用)

Introspection需要使用pthread_introspection_hook_install()安装回调函数即可使用。

What's in a pthread_t

Pthreads将pthread_t定义为void *,以对客户封装内部实现 - pthread_t作为对象句柄在函数中传递,且客户不需要依赖其具体实现

image.png image.png

Thread Specific Data (TSD)

所有线程都能“免费”得到一个私有栈,凭借私有的栈指针。但栈指针并不足以成为线程自己在栈帧的切换中携带自己的特定上下文。基于此,pthread提供了pthread_[get/set]specific(2),用以向"slots"中写入和获取任意的共享值,且线程之间共享相同的索引值,当然对每个线程来说都是私有的。也就是listing-18中的void *tsd[]数组

image.png

Synchronization objects

Thread Local Variables

第6章中介绍Mach-O objects有对线程本地变量的内置支持。可以通过__thread关键字定义。这些情况下,Mach-O 头的flag中有MH_HAS_TLV_DESCRIPTORS(0x800000)且DATA段中有两个sections: __DATA.__thread_vars和__DATA.__thread_bss。Listing 8-22有展示一个简单的程序例子,由otool -t -V(for×86_64)和jtool-a(for ARM64):

image.png

Thread Priorities and QoS Classes

很长一段时间,开发者并不能控制线程调度,除了经典的UN*X nice(1)/renice(8)方式,以及getpriority/setpriority(2) system calls。而10.9/7开始,Apple引入了一种完全新的概念到其调度器中 - 那就是线程级别的Quality of Service (QoS)。Table 8-23显示了当前定义的QoS classes:

image.png Apple在Energy Efficiency Guide for Mac Apps" guide[2]中有详述,且允许通过NSOperation和Foundation.framework的其它对象设置。 为一个线程设置QoS class可能影响这些点:

  • Priority: 最明显的效果是修改线程优先级, 会改变调度器对这个线程的行为。高优先级的线程会受到偏好的对待。完整的线程调度和优先级的讨论留给卷2,而在这里只要知道XNU使用一个O(1)的调度器(有些类似于Linux在2.6.23之前使用的)就足够了,其是基于一个0..127 priorities数组。数组尾部的一半(0.63)是给用户态线程使用的。UNIX nice(1)值提供值+20到-20的修改,这些值会从线程基本优先级中减去。通过这种方式,正nice值实际会降低优先级,而负值会增加优先级。默认的QoS优先级是31,在用户态光谱的中间位置,而interactive QoS class是48,在最上面的quarter
  • I/O Throughput:系统提供几个I/O等级,而线程请求可能会被节流
  • CPU Throughput:XNU并不是一个实时内核,但它的调度非常有弹性且允许线程在一段给定的时间内要求CPU给予到一个最少的指令周期数
  • Timer Latency:对于Darwin 13,定时器合并提供了整体的电力利用效率(减少了wakeups),但增加了timer的延迟。高优先级线程可以覆盖这个策略
  • NetworkTraffic:线程QoS可以传递到socket层级的QoS,能给线程提供更大的带宽分配 libpthread提供了几个非POSIX APIs以控制线程QoS。这些APIs定义在<pthread/qos.h>中,展示在Table 8-13中。大多数线程并不直接被QoS影响,除非资源变得稀缺导致发生了资源争夺。在这些情况下,更高的QoS classes 会分配更多资源(正比于它们的class)。一个例外是低优先级的QoS classes,通常目标是尽可能节省更多的能量(减少发热)

Process, Task & Thread Policies

XNU允许在进程、任务或者线程级设置特定策略。这些策略可能会对进程行为产生戏剧性的效果,首要地会影响调度决策。

Process Policies

未文档化的process_policy system call (#323) 如下定义listing procpol:

image.png 这个system call作为一个多策略与子策略的复用器,这些策略可以用在进程级别,但也同时对特定线程起作用。目前有7个定义的策略,如Table 8-25所示:

image.png 每个策略都有其自己的子策略,感兴趣的读者可以查看bsd/sys/process_policy.h查看细节。这个头文件并未导出到用户态,但其中的值很容易复制粘贴。策略实现和执行在卷2中会详述 虽然未文档化,system call在libsystem_kerner中有一个导出的包装器,这个wrapper是双下划线的且并未预期直接使用。相反,很多高层级wrapper存在,fixing the scopeaction参数并执行更多特定的调用。proc_set_cpumon_params中可以看到一个例子:

image.png

procrlimit control (#446)

jopolicysys (#323)

Task Policies

task_policy_[get/set]和task_policy image.png task_policy主要用途是改变task role

image.png

Thread Policies

Mach提供了高级的线程调度策略。这些策略允许用户态调用者指定一组调度参数,这样调度器在处理线程时可以参考。API是thread_act MIG subsystem (q.v. I/12)的一部分。现在过时的thread_policy MIG call提供了3个基本的策略 - POLICY_TIMESHARE, and .. __RR (Round-Robin) or ..FIFO,类似于Linux现在提供的。更现代的thread_policy[set/get]能力更超越了这个简单的三分法,且提供了更高级的策略,如Table 8-30所示。置灰行代表私有,未导出到用户态头文件

image.png

THREAD_[STANDARD/EXTENDED1_POLICY
THREAD_TIME_CONSTRAINT_POLICY
THREAD_PRECEDENCE_POLICY
THREAD_AFFINITY_POLICY
THREAD_BACKGROUND_POLICY (*OS)

Thread level QoS

Darwin 13极大扩展了策略标准集,通过引入线程级的Quality of Service (QoS)。QoS是一组来自网络,限制网络带宽和保证吞吐和时延 的众所周知的概念。然而Darwin是首个将这些应用到线程中的操作系统,将CPU时间当做一种带宽资源

一种特殊的策略,THREAD_QOS_POLICY_OVERRIDE允许正常的QoS层被暂时超越。这个策略#defined PRIVATE, 所以用户态并不可用,虽然可以通过专用的bsdthread_ctlsystem call (稍后会讨论)覆盖。

当override的时候,一个特殊的struct thread_qos_override structure会被分配(从专用的thread qos override zone)。这是一个24字节的structure,定义在XNU's osfmk/kern/thread.h:

image.png override_resource使用一个指针指向资源。override resource类型定义在 /osfmk/mach/thread_policy.h (THREAD_QOS_OVERRIDE_TYPE_PTHREAD_(MUTEX/RWLOCK]或者..TYPE_DISPATCH_ASYNCHRONOUS_OVERRIDE)。更多细节QoS enforcement可在II/9中找到

bsdthread ctl

Additional Pthread-specific system calls

psynch

Ulocks (Darwin 17+)

Darwin 17引入了一个新的os_unfair_lock对象和相关APIs,libplatform.dylib。这个锁类型取代了OSSpinLock,且并不自旋。其"unfairness"在于锁的拥有者可以解锁并在其它线程有机会之前尝试重新锁住,即有潜在的可能性会starving them。当然也有一个严格的要求就是只有锁的线程才可以是在完成后解锁的那一个。

内部os_unfair_lock_lock/unlock都是由一个锁地址上的os_atomic_cmpxchg2o指令实现的。如果锁已经在使用中,那么必须走一条slow path 。两条slow paths都是使用一个内核ulock对象实现的,通过两个专用的system calls:

image.png

operation 参数由几个bitfields组成,定义在sys/ulock.h中:实际的操作在least significant字节, 有两个操作: UL_COMPARE_AND_WAIT(1),这个操作是模仿OSSpinlock,另一个操作是OS_UNFAIR_LOCK(2)。下一个字节包含__ulock_wake()的flag: ULF_WAKE_THREAD(0x200),代表一个特定的线程,或者ULF_WAKE_ALL (Ox200),代表是广播。下一个字节包含__ulock_wait()的flag:ULF_WAIT_CANCEL_POINT(0x20000),代表这是一个安全的pthread cancelation point,或者ULF_WAIT_WORKQ_DATA_CONTENTION(0x20000)代表workqueues不应该在这个线程阻塞时创建新线程。最后一个字节包含通用flag,目前只定义了一个ULF_NO_ERRNO (1000000) 到Darwin 18,内核ulock进一步由一个turnstile增强了,turnstile是一个可以优化锁传递并防止优先级反转的同步对象。ulock对象和turnstiles的底层实现在卷2第9章会详述

Interlude: kevents and kqueues

现在把焦点从threads转向另一个事情 - kqueue kevent APIs。这包括4个system calls,是从BSD引入Darwin的,且提供了一个简单而有效的事件通知机制。

kernel event queue由kqueue(2)(#363)或者guarded_kqueue_np(#443)创建的,不带参数。The kernel event queue是一个文件描述符,但必须随时准备被kevent/64/_qos*(#363,#369或者#374)调用,指定flags和监听的事件changelist。事件列表是作为一个对应的struct kevent[/64_s/_qos_s]传递,可以通过EV_SET[/64/_QOS](类似于由select(2) FD_SE宏做的preparations)prepared。EV_SET宏可以帮忙迅速初始化对应的structures,但遗憾的是不向后兼容,因为它们的field顺序已经任意改变,如Figure 8-36所示:

* - kevent_qos is #defined PRIVATE and is therefore not readily visible in the exported <sys/event.h>; It is nonetheless usable with the right declarations.

image.png 一旦changelist加载,所有后续不带changelist的kevent(2)调用,可能反而会指定一个eventlist数组,和timeout,并且会阻塞直到任意一个要求的事件发生。当它返回时,它会指明eventlist数组中事件的数量。Apple偶尔会添加filter类型,而8-37显示了目前在XNU-4903中定义的各种各样的filter类型:

image.png 读取的时候,内核event structure's fflag域设置一个对应指定的事件类型的NOTE_ flags,Table 8-37所示。 Darwin 17添加了一个新system call, kevent_id(#375)。它为workloops提供了"dynamic queues"。伴随的新filter类型是EVFILT_WORKLOOP,用来追踪workloop events。proc_info syscall(#336 第15章会讨论)可以用来获取kqueue信息。 到了这里,你可能会好奇这里的插曲(interlude)到底是什么 - 而且kqueues对线程有多重要呢。可以证明的是queues对Apple偏好的线程环境 - 也就是 Grand Central Dispatcher是有帮助的。

Grand Central Dispatcher

Mac OS 10.6 (Snow Leopard)'s最有趣的特性就是引入了称为"Grand Central Dispatcher"的新机制, GCD呈现了一种全新的并发编程模式,一种不基于线程而是基于queue的方式。Apple在*Concurrency Programming Guide[3]*中文档化了GCD概念,也包括对应的API。大量Apple自己的代码从此开始了重构,将基于线程的传统线程替代为使用GCD。

GCD提供了一种旧设计模式(线程池)的新实践。与其为每个任务创建一个线程,GCD使用一个现存的专用线程集合。当任务work可用时, GCD选择一个空闲线程并将work对象分配给它。与已知的模式相反,GCD维护的线程池对调用者是完全不可见的,调用者只需要提供work对象也就是block给一组queues。GCD俯瞰视角描述如Figure 8-38所示:

image.png GCD是一个复杂的framework,需要用户态和内核态组件的相互配合。在用户态,libdispatch.dylib提供客户使用的API和对象。libdispatch.dylib与libpthread.dylib紧密配合,libpthread.dylib导出非POSIX扩展直接供libdispatch.dylib使用。通过这些扩展可以使用到内核态功能, 通过这些非POSIX system calls。与libpthread的整合并不意外,正如人们对线程抽象库的期望一样。

libdispatch是开源的,由Apple开发,其源代码在Darwin 17上已经升级到了913。它的接口由libSystem重新导出。虽然源代码经历了常规delay(i.e.通常their MacOS version发布之后libdispatch的源代码通常还没发布出来),libdispatch的新代码经常作为swift开源代码的一部分而"leak",因为libdispatch是其依赖库。 libdispatch也移植到了BSD(BSD也正是其基础库),Linux甚至Android(!),但其最主要的受众还是Darwin

Blocks

Blocks是GCD体系的基础。block是一块自包含的代码,与函数一样,有参数和返回值。与函数不同的是它能捕获作用域内的变量。Blocks因此具备了所有被调用运行的状态,所以可以在任何线程执行(理论上也可以在任何进程)。因为blocks是自包含且state-isolated所以作为callback和并发执行任务尤其合适。Blocks也能在栈和堆上在程序运行时创建。Darwin 18时,内核代码也支持了blocks。Apple在"Blocks Programming Topics"中文档化了blocks,其底层实现的详细解释可以在clang文档中找到。

读者很可能熟悉block语法,其与函数声明类似,但以插入符^作为block字面量的指标符。Blocks是由C支持的,所以自然也由其派生语言i.e. C++和Objective-C*所支持。自从Darwin 10引入以来,几乎所有Apple的代码都转而使用block。

* - Other languages have their own variants of blocks, known as closures (in Java and Swift), and lambdas.

在编译器层级,block实际是不存在的。实际上block语法其实是底层实现的语法糖,底层其实是创建了一个os_object并封装了block方法体。这个对象如Figure8-39所示:

image.png block拆分成两个结构,一个实现结构和一个描述符。其实现是os_object派生类,起头是一个isa域,指向block's class - an NSConcreteGlobalBlock (对于__DATA.__const中的block), NSConcreteMallocBlock (heap上的block),或者NSConcreteStackBlock(栈上的block)。也有flags,它决定block descriptor中的域。实现中最重要的域是函数指针, - 如果block捕获了作用域内的变量,它们是直接跟随在implementation后面的。

block descriptor由implementation struct指向,拥有可变大小,依赖于实现中指定的flags。描述符的最小字节数是16(也就是Figure 8-39中头两行),但实现flags通常包含BLOCK_HAS_SIGNATURE,这种情况下descriptor指向一个Objective-C style signature。signature决定了arguments需要的空间和类型。如果block内部引用了__block类型变量,描述符也会包含一个copy helper指针(block创建时复制变量值)和一个dispose helper指针(释放时销毁copy的值)。通常这些都是对 libsystem_blocks.dylib的_Block_object_assign()和.._dispose()的一层薄薄的封装,取block变量域和类型域中的值(比如,3代表对象, 7代表block变量)。

因此Blocks本质上是boxed functions。编译器通常在运行中创建block实现(i.e. in code),或者是与描述符一起预置在__DATA.__const中。这两种情况下,对象结构都是一样的,可以很容易识别。jtool(j) dump__DATA.__const 如Output 8-40所示:

image.png 匿名boxed functions实际是怎么命名的呢 - 来自于它们的dispatching function,并冠以block invoke的后缀。Apple并不总是strip掉符号名(尤其是在MacOS二进制中),而且就算在*OS二进制中剥离了符号的函数也会complain(i.e. oslog()或者syslog (3)),它们经常暴露自己的函数名 - 这可以直接推断出使用它的函数名

对于它们的可用性和(有些人觉得舒服的)语法, blocks的实现使用了相当的代码。它是大量模板和自动生成的代码,做二进制逆向时很容易识别。下面的实现显示了block背后的代码

实验代码略去 The code behind blocks

Queues

Dispatch驱动的编程通过dispatch queues取代了经典的pthread线程模型。dispatch队列是命名对象,blocks执行顺序为FIFO。队列支持两种模式 - DISPATCH_QUEUE_SERIAL and ..._CONCURRENT。串行模式保证block严格的顺序 - 也就是前一个block执行完成才执行下一个。这样可以省略同步机制避免死锁。并发模式仍按block进入队列的顺序执行,但允许并行执行。因此为避免竞争条件,blocks要么满足Bernstein的条件*(block输入输出完全无依赖)完全可并行化,要么使用dispatch barriers。

image.png

Queues也提供一种方法dispatch_suspend()和dispatch_resume(),且初始化后必须dispatch_resume()以调度blocks。Darwin 16添加了一个dispatch_activate(),还有_INACTIVE flavors 到队列的类型中。虽然不太常见,但队列可以使用表Table 8-23中的QoS classes创建, 但大多情况下,可以使用几个内置队列:

image.png

Apple提供了Table 8-44中未置灰行的队列访问权限,overcommit队列代表必须为调度执行的block创建新线程(就算这代表着会有超过可用核数的线程数),这个特性通过目前开放权限且文档化的API是不能获取的。可以通过dispatch_get_global_queue(3),并指定未文档化的flag DISPATCH_QUEUE_OVERCOMMIT,其值为2。当然也可以创建新队列并使用 dispatch_queue_attr_make_with_overcommit。

Apple隐藏overcommit功能的原因是 - GCD透明地处理线程,用系统认为合适的方式创建或者join线程,而对进程没有做保证。可能会有很多队列关联很多线程。因为队列和线程之间的映射是完全未定义的,同一线程可能为多个队列服务,或者一个队列可能由多个线程服务。系统决定最佳的线程策略,同时考虑CPU可用核数。开发者不需要知道具体的实现,只需要知道blocks是FIFO执行的。

提交block给队列时提供了额外层级的控制:可以使用dispatch_sync或者dispatch_async,调用者可以等待block结束或者继续后台运行。Blocks也可以放入调度groups,可以be waited on (使用dispatch_group_wait())或者使用completion notifications。也可以使用dispatch_semaphore_create (3) and friends代替groups,避免使用单例。

Dispatch queues很好地整合进了CFRunLoops (第10章会讨论)。 "Pure" GCD程序(i.e.也就是不存在CFRunLoop或者其它UI constructs),预期调用dispatch_main。这个函数只能从主线程调用,因为要确保根队列已经初始化,然后才能调用pthread_exit() - 否则可能导致退出主线程hangs on pthread_tsd_cleanup,因为主队列的libdispatch TSD被清理了。如果pthread_exit()实际上返回,这将导致蓄意崩溃。

Queue Attributes

调度队列支持attributes,且dispatch_queue_create(3)的第三个参数类型是dispatch_queue_attr。manual page将其定义为"attr参数指定要创建的队列类型,必须是DISPATCH_QUEUE_SERIAL或者DISPATCH_QUEUE CONCURRENT"。然而这只是个可选事实,因为背后的选项目前相当多。

DISPATCH_QUEUE_SERIAL常量是NULL, and ..CONCURRENT是一个指向预定义的dispatch_queue_attr_t structure的指针,它是在libdispatch src/queue_internal.h中定义的:

image.png

为保持其不透明性,attr定义并没有公开导出,但很多dqa_ 域可以通过各自的dispatch_queue_attr_make_with_field。dqa_concurrent_bit影响队列选择使用哪个vtable。dqa_overcommit将队列标记为overcommitted(served by more threads than optimal)

Queue maintenance

调用dispatch_main(),作为预期主线程最后的调用,内部调用dispatch_root_queues_init()来初始化root 队列 (如Table 8-44所示)。这个函数(实际也从其它地方调用,其实是个单例dispatch_once())调用_dispatch_root_queues_init_workq,提供了GCD的"heavy lifting",要求内核创建对应的kernel work queues。Work queues是内核管理的线程,其用户态可以由两个未文档化的system calls其中之一管理:

  • workq_open (void)(#367): 建立处理work queues的内核状态,包括workqueue的管理线程
  • workq_kernreturn(options, item, arg2,arg3)(#368): 允许用户态引导direct内核根据options管理work queues, which are:
    • WQOPS_QUEUE_NEWSPISUPP: 请求代表new service provider interface在用户态被支持,并进一步在arg2中指定了在dispatch queue structure中的偏移(否则其对内核来说是opaque的),arg3代表是否支持kevents(presently ignored).
    • WQOPS_QUEUE_REQTHREADS: 请求代表要求内核开启arg2 数量的线程
    • WQOPS_SET_EVENT_MANAGER_PRIORITY: 请求 workqueue event manager thread 拥有arg2指定的优先级
    • WOOPS_THREAD_[KEVENT/WORKLOOP/]_RETURN: 代表workqueue thread, kevent handler或者workloop thread已经完成并返回给内核。返回的线程会hang here,直到有更多队列items需要处理
    • WOOPS_SHOULD_NARROW: 请求检查arg2中指定的优先级是否可用,或者队列是否需要narrow(由于system constraints)

这几个__workq_kernreturn的选项已经在libdispatch中导出了wrapper(through several _pthread_workqueue_* exports)。因此libdispatch使用WOOPS_QUEUE_REQTHREADS值来为root queues创建workqueue handlers,通过调用libpthread的pthread_workqueue_addthreads[_np]

回忆一下,bsdthread_register(system call #366,早些在Pthread初始化中讨论过)使用了两个用户态地址作为线程的入口点。第一个是"normal" entry point - thread_start,第二个是start_wqthread - 它是由libdispatch通过 libpthread的GCD-specific exports导出之一 设置的(pthread_workqueue_setdispatch* functions)。取决于不同环境,其可能由libdispatch设置为几个worker threads中的一个。

Dispatching blocks

Blocks是由dispatch管理的workqueue线程执行的。它们对线程是完全不可见的,只有libdispatch能够控制或者影响其行为,通过调用workq_kernreturn()。处理指定队列的blocks执行的过程叫做draining the queue

在初始化之后的任意时间点,都会有一些workqueue线程。当空闲的时候,workqueue线程会阻塞在_workq_kernreturn(#368)上,阻塞使用的选项是WQOPS_THREAD_[KEVENT/WORKLOOP/]_RETURN options。空闲的workqueue线程因此总是有相同的栈,所以可以通过栈快照获取(第15章有介绍),可以通过procexp pid threads查看:

image.png 当有可用work时,workq_kernreturn syscall会在一个libdispatch例程_dispatch_worker_thread## (one of several variants)中选取work并返回调用者。然后现在的栈trace就熟悉了,如Output 8-47:

image.png

在同步执行的场景,block可能直接在调用线程中执行。比如block调度到主线程执行,在调用dispatch_main()之前。对于异步执行,workqueue线程会更偏爱。如果workqueue可用线程不足,libdispatch可能请求更多线程。这也就是overcommitting起作用的场景

Continuations

GCD的一个关键概念是continuations。这个术语代表一种无状态的function,可以被立即恢复resumed,也不需要栈空间。Continuations允许关联一个上下文,但这个上下文可以与一个CPU关联,而不是一个线程 - 所以它的最佳选择是存储在per-CPU cache中,极大地提升性能

Continuations从NeXTSTEP开始就存在于Darwin中甚至更早:它们是首先由Draves et al.[7]提出的,作为一个引入Mach 3.0内核的新特性。然而Continuations 只存在于内核态(直到今天,在卷II中有讨论) - 且GCD标志着它们首次在用户态使用(虽然不同于内核实现)

Continuations让dispatch sources能够"fire"并触发他们关联的event handlers

Listing 8-48: A dispatch continuation(libdispatch-913)

// If dc_flags is less than 0x1000, then the object is a continuation.
// Otherwise, the object has a private layout and memory management rules. The
// layout until after 'do_next' must align with normal objects.
#if __LP64__
#define DISPATCH_CONTINUATION_HEADER(x) \
	union { \
		const void *do_vtable; \
		uintptr_t dc_flags; \
	}; \
	union { \
		pthread_priority_t dc_priority; \
		int dc_cache_cnt; \
		uintptr_t dc_pad; \
	}; \
	struct dispatch_##x##_s *volatile do_next; \
	struct voucher_s *dc_voucher; \
	dispatch_function_t dc_func; \
	void *dc_ctxt; \
	void *dc_data; \
	void *dc_other

continuation对象持有执行executing function(dc_func)和其context(dc_ctxt),context通常持有block对象。Continuation的处理通常从dispatch_continuation_pop()开始,这会leads to导致continuation function开始执行。让block开始执行的函数是dispatch_client_callout(或者其变体)

Dispatch objects

libdispatch使用几种类型的对象,queues是其中之一。用户可以使用dispatch_object_create函数来创建这些对象,同时只有有限访问能力来获取和设置这些对象的属性,这些对象的内部structure保持不可知。Table 8-49展示了Darwin 17支持的objects:

Table 8-49:Dispatch objects

ObjectUsed for
blockOperation blocks
dataOpaque data blobs
groupGroup of blocks
ioAsynchronous I/O requests
machMach ports
mach_msgMach messages
pthread_root_queueQueue,but with a pthread thread pool,rather than workqueues
queueQueue(for handling blocks)
semaphoreSynchronization primitive
sourceEvent source

Output 8-50显示使用gcdump工具*展示的dispatch objects:

* - gcdump is still in development, as it heavily depends on the version of libdispatch due to its parsing of internal structures which change in between library versions.

image.png

object structures是由C实现的,用等同于类结构的面向对象的方式设计的。其是通过所有类型中都包括相同structures(as "super classes")来实现的,并使用了大量的宏。

所有dispatch对象来自dispatch_object_s,这意味着每个结构的first entry都是一个DISPATCH_OBJECT_HEADER。这是一个扩展为公共structure header的宏。这个公共header自身使用了另一个宏, OS_OBJECT_HEADER,一个16-byte header,由一个Objective-C style "isa" - a pointer to a vtable (= method function pointers),和两个reference counts:一个是对象本身持有的对别的对象的引用计数,一个是对象本身被持有的计数。这个头在DISPATCH_OBJECT_HEADER的_as_os_obj字段中也有引用,这个字段故意被设计为0长度的数组(0长度的数组字段通常用于可变长结构体,方便快速引用,相比于使用指针字段,少一次访问即可读取到内容),以用于快速引用。这个小技巧在_os_mpsc_queue_s(Multi Producer Single Consumer)上有重repeated用。Listing 8-51显示了所有dispatch_queue_s定义中使用到的宏

Listing 8-51:The MacOS used to create a dispatch_queue_s(from libdispatch-913)

//libdispatch_913:src/object_private.h

#define _OS_OBJECT_HEADER(isa, ref_cnt, xref_cnt) \
        isa; /* must be pointer-sized */ \
        int volatile ref_cnt; \
        int volatile xref_cnt

//libdispatch_913:src/object_internal.h

typedef struct _os_object_s {
	_OS_OBJECT_HEADER(
	const _os_object_vtable_s *os_obj_isa,
	os_obj_ref_cnt,
	os_obj_xref_cnt);
} _os_object_s;

#define OS_OBJECT_STRUCT_HEADER(x) \
	_OS_OBJECT_HEADER(\
	const void *_objc_isa, \
	do_ref_cnt, \
	do_xref_cnt); \
	const struct x##_vtable_s *do_vtable

#define _DISPATCH_OBJECT_HEADER(x) \
	struct _os_object_s _as_os_obj[0]; \
	OS_OBJECT_STRUCT_HEADER(dispatch_##x); \
	struct dispatch_##x##_s *volatile do_next; \
	struct dispatch_queue_s *do_targetq; \
	void *do_ctxt; \
	void *do_finalizer

struct dispatch_object_s {
	_DISPATCH_OBJECT_HEADER(object);
};

#define _OS_MPSC_QUEUE_FIELDS(ns, __state_field__) \
	DISPATCH_UNION_LE(uint64_t volatile __state_field__, \
			dispatch_lock __state_field__##_lock, \
			uint32_t __state_field__##_bits \
	) DISPATCH_ATOMIC64_ALIGN; \
	struct dispatch_object_s *volatile ns##_items_head; \
	unsigned long ns##_serialnum; \
	const char *ns##_label; \
	struct dispatch_object_s *volatile ns##_items_tail; \
	dispatch_priority_t ns##_priority; \
	int volatile ns##_sref_cnt

//libdispatch_913:src/queue_internal.h

#define _DISPATCH_QUEUE_HEADER(x) \
	struct os_mpsc_queue_s _as_oq[0]; \
	DISPATCH_OBJECT_HEADER(x); \
	_OS_MPSC_QUEUE_FIELDS(dq, dq_state); \
	uint32_t dq_side_suspend_cnt; \
	dispatch_unfair_lock_s dq_sidelock; \
	union { \
		dispatch_queue_t dq_specific_q; \
		struct dispatch_source_refs_s *ds_refs; \
		struct dispatch_timer_source_refs_s *ds_timer_refs; \
		struct dispatch_mach_recv_refs_s *dm_recv_refs; \
	}; \
	DISPATCH_UNION_LE(uint32_t volatile dq_atomic_flags, \
		const uint16_t dq_width, \
		const uint16_t __dq_opaque \
	); \
	DISPATCH_INTROSPECTION_QUEUE_HEADER
	/* LP64: 32bit hole */

struct dispatch_queue_s {
	_DISPATCH_QUEUE_HEADER(queue);
	DISPATCH_QUEUE_CACHELINE_PADDING; // for static queues only
} DISPATCH_ATOMIC64_ALIGN;

Dispatch Sources

GCD定义了Dispatch Source作为异步响应系统事件的方法。每种支持的系统事件都有对应的dispatch source类型,每个source都负责收集事件详情并创建一个对应的block对象(或者函数),block对象会提交给目标dispatch queue。调用者使用dispatch_source_create(3)指定一种source type,一个handle,一个flags的掩码,和目标队列。Table 8-52展示了定义在libdispatch-1008中的source类型:

Table 8-52: The dispatch sources supported in Darwin 19's libdispatch-1173

Source TypeAbstractsTriggered on
data_addCoalesces data with ADDManual trigger
data_orCoalesces data with ORManual trigger
data_replaceReplaces dataManual trigger
intervalPeriodic executionRegular interval,aligned with others
mach_recvMach port receive rightIncoming message
mach_sendMach port send rightMessage can be sent
memorypressureMemory pressureMemory pressure notification
memorystatusMemory status(deprecated in favor of memorypressure)
nw_channelNetwork ChannelsFlow notifications
procProcess lifecycle notifications by PIDExit,fork,exec and signal
readFile descriptor,open for readData available for read
signalUN*X signalSignal received
sockSocket lifecycle[DIS]CONNECTED,CONNRESET,TIMEOUT...
timerTimerOne time interval + leeway timers
vfsVirtual Filesystem SwitchFilesystem notification
vmVirtual memory events(deprecated in favor of memorypressure)
vnodeVnodeFile descriptor, open for O_EVTONLY
writeFile descriptor, open for writeBuffer available for write

调用者也可以指定事件在目标队列上的处理block或者函数,使用dispatch_source_set_event_handler[_f]。处理block和函数可以调用dispatch_source_get_[mask/handle],或者调用dispatch_source_get_data以从source获取与事件关联的数据(使用__block修饰符声明以在handler内部可见)。dispatch source是在暂停状态创建的,但一旦设置了事件handler,那么source可以被dispatch_resume激活。Sources也可以同样地被暂停和取消。也可以通过dispatch_source_set_[registration/cancel]_handler()设置进一步的回调

Implementation

Dispatch sources将其底层的实现kevents向其调用都隐藏了。与其它dispatch object一样,source是通过使用宏定义的,这些宏的扩展提供了在Listing 8-53中展示的对象实现:

image.png Dispatch sources也允许自定义的sources。用户定义的sources不依赖于queue(3)机制,但依赖于用户手动触发。

Output 8-54显示了dispatch sources,使用gcdump工具展示:

image.png

此处有一个实验 debugging dispatch objects

Apple 尽力使dispatch对象尽可能不透明,并保留根据需要更改其内部结构的权利。 然而,有了调试器,检查对象就很容易而且有时很有用。 考虑以下示例程序: Listing 8-55:A sample program illustrating inspecting a dispatch queue

#include <dispatch/dispatch/h>
int main(int argc, char argv**)
{
    dispatch_queue_t dq = disaptch_queue_create("com.newosxbook.sample",//const char *label
                          DISPATCH_QUEUE_SERIAL);// dispatch_queue_attr_t
    printf("Queue: %p\n", dq);
    dispatch_main();
}

在 printf () 之后的任何时间中断将允许检查队列,为方便起见,该示例打印其地址。 dispatch_queue_t 的打印值是一个指向dispatch_queue_s 的指针。 跟随指针并转储其内容将给出

Output 8-55-b:Inspecting object memory

image.png

po 命令通常在dumping转储对象方面非常有效,但事实证明它对指针及其关联对象毫无用处。 然而,跟随对象内部的指针,可以快速显示函数指针和常量:

Output 8-55-c: Inspecting object memory

image.png

将指针拼凑在一起可以快速揭示对象的整个结构(在本例中为调度队列)。 这样做会产生:

Output 8-55-d: Inspecting object memory

image.png

如果将输出与清单 8-51 相关联,就很容易找出哪些 do_ 字段与哪些指针相关。 这很有用,特别是在 Apple 更新了 libdispatch 但尚未发布源代码的窗口期中 - 在beta测试期内以及主要操作系统发布后的几个月内。 但这仍然留下了一些可能无法解释的非指针字段(如果版本releases之间的结构发生变化)。

幸运的是,更好的方法是利用 libdispatch 的调试函数 - 尽管在 libdispatch 中定义为本地符号,但调试器仍然可以轻松看到这些函数。 输出 8-55-e 显示了如何转储调度队列dispatch queue的详细信息,使用dispatch_queue_debug。 请记住该函数需要一个缓冲区和一个长度,您首先需要分配它:

Output 8-55-e: Using dispatch_queue_debug

image.png

Introspection

libpthread和libdispatch都有introspection hooks。Introspection要求DISPATCH_INTROSPECTION是#defined,这并不是默认build配置,但可以在rebuild源码时设置, 或者更简单来说直接让dyld使用introspection开启的版本,即使用DYLD_LIBRARY_PATH指向/usr/lib/system/introspection.

使用introspection很轻易就可以追踪dispatch queue lifecycle events。感兴趣的进程可以调用dispatch_introspection_hooks_install并提供一个dispatch_introspection_hooks_t,这是一个指向回调函数structure的指针,如Listing 8-56所示:

Listing 8-56: libdispatch introspection hooks(from libdispatch-703's private/introspection_private.h

typedef struct dispatch_introspection_hooks_s {
	dispatch_introspection_hook_queue_create_t queue_create;
	dispatch_introspection_hook_queue_dispose_t queue_dispose;
	dispatch_introspection_hook_queue_item_enqueue_t queue_item_enqueue;
	dispatch_introspection_hook_queue_item_dequeue_t queue_item_dequeue;
	dispatch_introspection_hook_queue_item_complete_t queue_item_complete;
	void *_reserved[5];
} dispatch_introspection_hooks_s;
typedef dispatch_introspection_hooks_s *dispatch_introspection_hooks_t;

不像libpthread的introspection,libdispatch的introspection提供了很多其它功能,但符号都没有导出(意味着它们的符号必须在debugger中解析或者调用)。

kdebug codes

同大多数Apple子系统一样, libdispatch使用了kdebug facility(第15章会讨论)来追踪大多数生命周期行为。 libdispatch有自己的kdebug code (DBG_LIBDISPATCH 值为 0x2E),subcodes是VOUCHER(1),PERF(2) 和MACH_MSG。而是它们都没有列举在/us/share/misc/trace.codes。Table 8-57总结了重要的codes:

Table 8-57: kdebug codes emitted by libdispatch

CodeEmitted by
0x2e020004dispatch_queue_legacy_set_target_queue
0x2e020008dispatch_queue_set_target_queue
0x2e02000cdispatch_source_set_handler
0x2e020010__dispatch_queue_compute_priority_and_wlh
0x2e020014__dispatch_queue_class_wakeup_with_override_slow:
0x2e020018__dispatch_source_refs_register
0x2e030004_dispatch_mach[_reply]_merge_msg
0x2bdc0008_dispatch_main()(DBG_ARIADNE)

Review Questions

  1. What is the fundamental difference between a posix thread and a dispatch worker thread?
  2. What trait of kqueues makes them suitable for providing the dispatch source abstraction?
  3. Why should thread identifiers not be visible from outside the process scope? What operating system violates this? And what is one advantage of out-of-process thread visibilty?
  4. What is the main advantage of using blocks, rather than directly threads?
  5. Why does a dispatch source "inherit" from a dispatch queue?

References

  1. Ian Beer (Project Zero) - "MacOS/OS userspace entitlement checking is racy" - bugs.chromium.org/p/project-z…
  2. developer.apple.com/library/con…
  3. Apple Developer - Concurrency Programming Guide - developer.apple.com/library/...….
  4. Apple Developer - libdispatch reference - developer.apple.com/reference/d…
  5. Apple Developer - Blocks Programming Topics- developer.apple.com/library/con…
  6. Clang 6 Documentation - "Block Implementation Specification" - clang.llvm.org/docs/Block-…
  7. Draves et. Al - "Continuations" - zoo.cs.yale.edu/classes/cs4…

上述pdf现在的真实地址其实是 www.cse.iitd.ac.in/~sbansal/os…