0%

整体上还算不错,对内核,Mach和BSD的介绍,包括一些函数和调用等,以及对异常处理的描述,对于理解PLC这个崩溃日志生成的库还是很有帮助的。同时一些内核线程、用户线程的概念也算是填补部分知识空白。引导过程虽然讲的很详细,不过兴趣不大。

名词解释

ASLR:Address Space Layout Randomization 地址空间布局随机化

XNU:X is Not UNIX

POSIX:Portable Operating System Interface 可移植操作系统接口

XPC:高级IPC框架,实现进程间特权的分离

ELF:Executable and Library Format ,Unix下的可执行文件格式

__LINKEDIT:由dyld使用,这个区包含了字符串表、符号表以及其他数据

pthread:就是BSD层实现的POSIX规范的线程,Mach层则是mach_thread

DFU:设备固件更新模式,这个模式用于更新iOS镜像

HFS:Hierarchical File System 层次文件系统

FAT:File Allocation Table 文件分配表

NFS:Network File System网络文件系统

VFS:Virtual File System虚拟文件系统

LR寄存器:保存了当前函数的返回地址

PC寄存器:指令指针,程序计数器,PC寄存器中的内容,是下一条要取来执行的指令的地址

ABI:Application Binary Interface 应用程序二进制接口

第一部分 第一到三章 高级用户指南

1.iOS系统的GUI是springboard,这是大家熟知的触屏应用加载器。

2.Darwin是操作系统的类UNIX核心,本身由内核,XNU和运行时组成。

3.XNU实际上是由两种技术混合在一起的:Mach和BSD,此外还添加了一些其他的组件,主要是IOKit。

4.在Mac OS中,“bundle”这个词实际上描述的是两种不同的术语:第一种是本节中讨论的目录结构;第二种是共享库目标的一种文件目标格式,共享库由进程显示的加载(普通的库是隐式加载的)。这个词有时候也表示一个插件。

5.OS X是在Mach内核的基础上构建的,而Mach是NeXTSTEP的遗产。BSD层是对Mach内核的包装,但是Mach系统调用仍然可以在用户态访问。

6.内核XNU是Darwin的核心,也是整个OS X的核心。XNU本身由以下几个组件构成:Mach微内核,BSD层,libKern,IOKit。

7.Mach微内核仅能处理操作系统最基本的职责:进程和线程抽象,虚拟内存管理,任务调度,进程间通信和消息传递机制。

8.BSD层建立在Mach层之上,BSD层提供了更高层次的抽象,其中包括:UNIX进程模型,POSIX线程模型(Pthread)及其相关的同步原语,UNIX用户和组,网络协议栈(BSD Socket API),文件系统访问,设备访问(通过/dev目录访问)。

9.OS X提供了一个系统级的通知机制。这是分布式IPC的一种形式,进程可以通过这种机制广播或监听事件。通知机制的核心部分在于notifyd(8) 守护程序,这个守护程序是在系统引导时启动的,这是Darwin的通知服务器。还有一个守护程序 distnoetd(8) 的作用是分布式通知服务器。notify(8) 默认使用Mach消息并注册Mach端口 com.apple.system.notification_center,能够处理大部分通知。notify(8)还有一个有意思的特点,这个API允许通过Mach消息传递文件描述符。

10.苹果通过代码签名,沙盒以及entitlement授权文件来保证iOS和OS X的安全。

11.沙盒机制也有一个专用的守护程序usr/libexec/sandboxd,这个程序运行在用户态,提供了跟踪功能以及内核扩展所需要的辅助服务,这个守护程序是根据需要启动的。

第四章 进程线程和Mach-O

1.尽管同一个可执行程序可以并发的启动多个实例,但是每一个实例都有一个不同的PID。

2.子进程返回的整数由其父进程收集。进程将要返回的值传递给exit(2) 系统调用(或者从main()函数返回)。

3.一个进程内的所有线程都共享虚拟内存空间,文件描述符和各种句柄。

4.进程的生命周期开始于SIDL状态,在这个状态中,进程仍被定义为”正在初始化“,不会响应任何信号,也不会进行任何操作。

5.睡眠的进程也会被信号唤醒。通过一个特殊的信号(TSTOP或TOSTOP)可以使一个进程停止执行。这相当于”冻结“了进程(即同时挂起这个进程的所有线程),将这个进程置于”深度睡眠“的状态。恢复这个进程的唯一方法就是发送另一个信号(CONT),这个信号可以将进程切换回可运行状态,使得进程中的每一个线程都可以被重新调度了。

6.终止一个进程会同时终止其所有线程。但是在进程完成终止之前,会短暂的处于僵尸(zombie)状态。每一个进程在安息之前都会有很短暂的时间处于这个状态。僵尸进程都是绝对死亡的进程,僵尸进程只是进程的空壳,所有的资源都被释放了,但是仍然占用着PID。

7.pid_suspend”冰冻“一个进程,pid_resume”解冻“一个进程。冰冻背后的挂起操作是在更为底层的Mach任务的层次实现的,而不是进程的层次实现的,所以这个睡眠更深。iOS的springboard大量使用了这些调用,比如用户按下Home键时。

8.iOS还添加了一个私有的系统调用 pid_shutdown_sockets,这在OS X是没有的。这个系统调用可以在进程之外关闭这个进程所有的套接字。只有springboard使用了这个调用,多半是挂起进程时使用的。

9.信号指发送给程序的异步通知,其中不包括数据(或只包括非常少量的数据)。信号是操作系统发送给进程的,用于表示发生了某种条件,而这种条件通常是因为某类硬件错误或程序异常而产生的。除了SIGKILL之外,进程可以通过一些系统调用屏蔽或处理以下的所有错误。

10.通用二进制(胖二进制)格式只不过是其支持的各种架构的二进制文件的打包文件。也就是说,这种格式的文件包含一个非常简单的文件头,文件头后面依次拷贝了每一种支持架构的二进制文件。lipo这个工具可以提取、删除或替换通用二进制文件中制定架构的二进制代码,因此可以用于对通用二进制文件进行”瘦身“。这个工具还可以显示胖二进制文件头的详细信息。

11.调用一个二进制文件时,Mach加载器会首先解析胖二进制文件头,确定其中可用的架构,然后只加载最适合的架构的代码,因此不相关架构的代码不会占用任何内存。

12.Mach-O格式具有一个固定的文件头,文件头一开始是一个魔数值,加载器可以通过这个魔数值快速判断这个二进制文件用于32位还是64位,在魔数值之后跟着的是CPU类型及子类型字段。

13.在iOS中,没有暴露sysct接口,堆和栈都默认不可执行。

14.Mach-O文件头的主要功能在于加载命令load command。otool工具可用于分析Mach-O文件。

15.加载过程在内核的部分负责新进程的基本设置——分配虚拟内存,创建主线程,以及处理任何可能的代码签名/加密的工作。然而对于动态链接的可执行文件(大部分可执行文件都是动态链接的)来说,真正的库加载和符号解析的工作都是通过LC_LOAD_DYLINKER命令指定的动态链接器在用户态完成的。控制权会转交给链接器。

16._PAGEZERO段(空指针陷阱)、_TEXT段(程序代码)、_DATA段(程序数据)和_LINKEDIT(链接器使用的符号和其他表)段提供了LC_SEGMENT命令。段有时候也可以进一步分解为区(section)。

17.当所有的库都完成加载之后,dyld的工作也完成了,之后由LC_UNIXTHREAD命令负责启动二进制程序的主线程(因此主线程总是在可执行文件中,而不会在其他二进制文件中例如库文件)。

18.LC_THREAD用于核心转储文件,Mach-O核心转储文件实际上是一组LC_SEGMENT命令的集合,这些命令负责建立起进程的内存镜像。

19.LC_MAIN命令的作用是设置程序主线程的入口点地址和栈大小。

20.Mach-O二进制文件有一个重要特性就是可以进行数字签名。LC_CODE_SIGNATURE包含了Mach-O二进制文件的代码签名,如果这个签名和代码本身不匹配(或者在iOS上这条命令不存在),那么内核会立即给进程发送一个SIGKILL信号,将进程杀死,没有商量的余地。

21.dyld是一个用户态的进程,dyld不属于内核的一部分,而是作为一个开源的项目由苹果单独维护的。

22.可执行文件很少是独立的,除了极少数的一些静态链接的可执行文件,大部分可执行文件都是动态链接的。

23.内核加载器执行的设置工作包括根据段的描述初始化进程地址空间以及执行其他命令。然而,仅有非常少量的进程只需要内核加载器就可以完成加载,而OS X上几乎所有的程序都是动态链接的。也就是说,Mach-O镜像中有很多”空洞“——即对外部的库和符号的引用——这些空洞要在程序启动时填补。这项工作就需要由动态链接器来完成。这个过程有时候也称为符号绑定(binding)。

24.动态链接器是内核执行LC_DYLINKER 加载命令时启动的。通常情况下,使用的是 /usr/lib/dyld 作为动态链接器,不过这条加载命令可以指定任何程序作为参数。链接器接管刚创建的进程的控制权,因为内核将进程的入口点设置为链接器的入口点。链接器索要完成的工作,就是查找进程中所有的符号和库的依赖关系,然后解决这些关系。这个过程必须递归的完成,因为通常情况下库还会依赖于其他的库。

25.通过otool -L命令可以显示库的依赖关系。

26.LC_LOAD_DYLIB命令告诉链接器在哪里可以找到这些符号。链接器要加载每一个指定的库,并且搜寻匹配的符号。被链接的库有一个符号表,符号表将符号名称和地址关联起来。符号表在Mach-O目标文件中的地址可以通过LC_SYMTAB加载命令指定的symoff找到。

27.libSystem库是系统上所有二进制代码的绝对先决条件,不论是C、C++还是Objective-C的程序。这是因为这个库是对底层系统调用和内核服务的接口,如果没有这些接口就什么事也干不了。

28.共享库缓存指的是一些库经过预先链接,然后保存在磁盘上的一个文件中。iOS中大部分常用的库都被缓存了。这个概念有点类似安卓的prelink-map,在prelink-map中的库被提前链接到地址空间中的固定偏移处。

29.为了节省加载的时间,iOS的dyld采用了一个共享库链接缓存,苹果从iOS3.0开始将所有的基础库都移到了这个缓存中。

30.在OS X中,dyld共享库缓存保存在/private/var/db/dyld目录下。在iOS中,共享库缓存可以在/System/Library/Caches/com.apple.dyld 中找到。这个缓存是一个单独的文件,即dyld_shared_cache_armv7。共享库缓存都会增长到非常庞大,OS X包含整整200多个文件,而iOS包含500多个文件,大小约为200M。

问题:共享库缓存到底是一个文件还是多个文件的组合?如果APP使用的第三方的动态库,是APP启动的时候操作系统才去加载,还是手机开机后自动加载这个共享库缓存目录下的文件?

31.OS X的dyld支持两级名称空间。这个特性是10.1引入的,指的是符号名称还包含其所在库的信息。这种方法更具有优势,因为允许两个不同的库导出相同的符号——而这在其他UNIX中会产生链接错误。有时候又需要禁用这种行为,通过将DYLD_FORCE_FLAT_NAMESPACE 环境变量设置为非零的值即可禁用。

32.函数拦截是传统ld没有而dyld有的特性。DYLD_INTERPOSE宏定义允许一个库将其库函数实现替换为另一个函数的实现。——这也是追踪函数调用和修改函数实现的原理

33.用户态的一个优点在于虚拟内存的隔离。进程独享一个私有的地址空间。

34.和所有标准的C语言程序一样,OS X中的可执行文件也有一个标准的入口点,默认名称为”main“。不过除了三个标准的参数——argc、argv和envp——之外,Mach-O程序还接受第四个参数:名为”apple“的char**。在Snow Leopard系统之前,”apple“参数只包含一个字符串——程序的完整路径,即启动这个程序所用的execve()系统调用传入的第一个参数。dyld在进程加载的过程中使用了这个参数。从Lion系统开始,”apple“参数被扩展为一个完整的向量,其中包括两个新加入的参数。

35.Cocoa应用程序的入口也是标准的C main(),不过常见做法是将main实现为NSApplicationMain()的包装,然后通过其进入Objective-C的编程模型。

36.重写内存最常用的方法是采用缓冲区溢出(即利用未经保护的内存复制操作越过栈上数组的边界),将函数的返回地址重写为自己的指针。不仅如此,黑客还有更具创意的技术,例如破坏pringtf()格式化字符串以及基于堆的缓冲区溢出等。

37.ASLR:进程每一次启动时,地址空间都会被简单的随机化——只是偏移,而不是搅乱。实现方法是通过内核将Mach-O的段平移某个随机系数。

38.在64位模式下,由于内存空间巨大,所以也就可以遵循其他操作系统采用的模型了,即将内核的地址空间映射到每一个进程的地址空间中。这是64位地址空间和传统的OS X模型的不同之处,传统的OS X内存模型中内核有自己的地址空间,但是新的地址空间允许更快速的用户态/内核态切换(共享CR3寄存器,CR3寄存器是包含了页表指针的控制寄存器)。

39.在内核层面,既没有用户堆也没有栈存在,所有的内存相关操作都要归约为页面。

40.尽管栈在传统上一直是用来保存自动变量的,但是在某些情况下,程序员也可以选择使用栈来动态分配内存,方法是使用鲜为人知的alloca()。如果发生了栈溢出,alloca()会返回NULL,进程会收到SIGSEGV信号。

41.iOS中VM压力释放机制依赖于Jetsam,Jetsam是一种类似于Linux的Out-Of-Memory killer的机制。

42.OS X有一个来自于Mach的独特之处在于,交换空间不是直接在内核层次管理的,而是由一个专用的用户进程dynamic_pager()处理所有的交换请求。这个进程在引导时由launchd通过一个属性列表文件cpm.apple.dynamic_pager.plist启动。

43.线程,作为最大化利用进程时间片的方法应运而生:通过使用多个线程,程序的执行可以分割为表面看上去并发执行的子任务。

44.进程中线程的抢占开销比多任务系统对进程抢占的开销要小。因此从这个角度看,大部分操作系统开始将调度策略从进程转换到线程是有意义的。

45.多处理器更是特别适合线程,因为多个处理器核心共享同样的cache和RAM——这为多线程之间的共享虚拟内存提供了基础。

46.GCD自己维护了一个底层的线程库实现,以支持并发和异步的执行模型,减轻开发者处理并发问题的负担,以及减少类似于死锁之类的潜在错误。GCD的另一个优势是能够自动的随着逻辑处理器的个数而扩展。 ——YYDispatchQueuePool可以根据设备的物理核心数量来创建对应的线程数量。

第五章 进程跟踪和调试

1.OS X中的调试工具首先要介绍的就是 DTrace。DTrace是一个重要的调试平台,移植自Sun的Solaris。DTrace中的D指的是D语言,这是一门完整的跟踪语言,通过这个语言可以创建专用的跟踪器,或称为探测器。D语言脚本被编译后由内核执行。

2.iOS中根本就没有提供DTrace。Linux上的ptrace提供了完整的进程跟踪和调试能力,因此称为了Linux下strace,gdb的基础。

3.DTrace神器的调试功能来源于能在内核中执行探测器的能力。DTrace的用户态部分由/usr/lib/dtrace.dylib 负责,Instruments和脚本解释器/usr/sbin/dtrace都使用了这个库。这是编译D脚本的运行时系统。然而,对于大部分有用的脚本来说,实际的执行都在内核态。DTrace通过一个特殊的字符设备(/dev/device)和内核组件进行通信。

4.sysctl机制在之前的章节中已经讨论过了,sysctl提供 一些显示进程统计数据的变量。sysctl获得进程ID列表的机制非常重要(事实上,ps和top指令都是通过这个机制获得进程列表的)。

5.除了DTrace和Instruments之外,在OS X中海油一些工具可以获得系统或进程状态的快照:system_profiler,sysdiagnose,allmemory,stackshot,stack_snapshot系统调用。

6.allmemory工具的作用是捕获用户进程的所有内存使用情况。运行时,这个工具遍历系统中的每一个进程,然后将其内存映射导出至/tmp/allmemoryfiles(可以通过-o指定其他文件)。获得了所有进程内存快照之后,allmemory会显示每一个进程的汇总统计数据,还会显示框架的内存使用。

7.stack_snapshot这个系统调用可以捕获指定进程中所有线程的状态。

8.sc_usage工具显示每一个进程的系统调用信息。fs_usage可以显示系统调用,但是显示的是与文件、套接字和目录相关的应用,这个工具可以显示系统范围内的跟踪(除非调用时提供了PID或命令参数)。

9.latency工具显示中断和调度的延迟值。这个工具展示落在阈值内的上下文切换和中断处理程序计数,这两个阈值分别可以通过-st和-it参数设置。

10.XNU包含一个称谓kdebug的内建内核跟踪设施。kdebug利用内核缓冲器来记录日志,而内核缓冲器的空间极为有限。

11.在UNIX中,崩溃和一个信号有关。崩溃的真正原因来自于内核,内核发现进程无法继续执行时,生成这个信号作为最后的补救办法。

12.当一个进程崩溃时,可以选择是否生成核心存储文件。iOS和OS X都没有选择创建巨大的核心转存文件,而是包含了一个CrashReporter,当进程异常终止时自动触发Crash Reporter,生成详细的崩溃日志。这个机制在进程消亡之前进行快速简单的分析,并且在崩溃日志中记录重要的内容。

13.有没有可能在一个应用程序崩溃时自动运行另一个应用程序?在iOS和OS X中,将异常端口绑定至BSD进程底层的Mach任务的机制则能实现这一点。

14.spindump和sample命令的采样方法都是类似的——挂起进程,记录栈跟踪(spindump使用之前描述的stack_snapshot系统调用),然后恢复进程。采样间隔通常大约为10毫秒,整个采样过程通常持续10秒,这两个值是可以配置的。

15.应用程序崩溃的主要原因就是缓冲区溢出(既包含栈也包含堆)和堆内存的破坏。

16.libgmalloc.dylib这个库可以截获并调试内存分配,这个强大的库的工作原理是截获LibSystem中的分配函数。

17.任何读/写操作如果越过了缓冲区的尾部就会导致读写操作越过页边界,从而导致一个未处理的页错误,使得进程收到总线错误的信号(SIGBUS)而崩溃。

18.标准的UNIX命令ps可以显示进程列表。UNIX的top命令是一个获得当前系统运行状况的关键工具,在OS X和iOS上都可用,而且相比标准版本有所修改。这些修改都和底层的Mach架构有关,使得top既能显示UNIX的信息(来自XNU的BSD层),也能显示Mach的信息。

19.有时候需要查看某个进程在使用哪些文件,或某个文件正在被哪些进程使用。lsof()和fuser()这两个工具就分别能够实现以上两个功能。lsof()是对之前描述的fs_usage的补充,因为后者只能看到新打开文件的操作,而看不到已经打开的文件。lsof()能显示一个进程所有文件描述符(包括套接字)的映射。换句话说,fs_usage可以持续运行,lsof产生的是一个快照。

20.fuser()提供的是反向的映射——从文件到拥有这个文件的进程。这个工具的主要作用是诊断文件锁定或者“文件被占用”的问题。

21.尽管XNU在用户态提供了完整的POSIX API,展示了UNIX兼容的人格,但是底层的实现却主要依靠Mach的基本原语。

22.随着苹果转向LLVM-gcc转换,还引入了LLDB作为GDB的替代品。LLDB的语法基本上和GDB类似,但是调试功能大有增强。

第六章 引导过程 第七章 贯穿始终——launchd

1.固件(firmware)可以看作是一种软件,这种软件因为被写入了芯片,所以是“固化”的。固件代码本身可以保存在只读存储器(ROM)中,也可以保存在电可擦除只读存储器EEPROM中

2.BIOS是一个固定的程序,而且通常都是封闭的。EFI是一套接口。EFI更像是一个运行时环境,规范了一组应用程序编程接口,基于EFI的程序可以利用这些接口实现功能。

3.苹果的EFI实现的另一个重要特性是Boot Camp。Boot Camp是苹果双重引导的解决方案,这个解决方案再Mac硬件上运行非苹果的操作系统(主要是Windows)。

4.一旦苹果关闭了某个iOS版本的时间窗口,将安全服务器配置为拒绝这个版本的签名时,就不可能降级固件了。

5.OS X和iOS中,用户环境始于launchd。launchd作为系统中的第一个用户态进程,负责直接或间接的启动系统中的其他进程。launchd仍然属于Darwin的范畴。

6.launchd是由内核直接启动的。负责加载BSD子系统的主内核线程创建一个线程来执行bdsinit_task。这个线程获得PID 1,并且临时命令为“init”,这是一项来源于BSD的遗产。bsdinit_task然后调用load_init_program(),这个函数调用execve()系统调用(在内核空间中执行)执行守护程序。

7.系统范围的launchd(PID 1)是不可能终止的。事实上,这个launchd是系统上唯一不朽的进程。当系统关闭的时候,launchd也是最后一个退出的进程。

8.launchd的核心职责是根据预定的安排或实际的需要加载其他应用程序或作业。launchd区分两种类型的后台作业:

①守护程序(daemon)和传统的UNIX概念一样,是后台服务,通常和用户没有交互。守护程序由系统自动启动,不考虑是否有用户登录进系统。

②代码程序(agent)是一类特殊的守护程序,只有在用户登录的时候才启动。和守护程序的不同之处在于,代理程序可以和用户交互,有的代理程序还有GUI。

③iOS不支持用户登录的概念,因此只有LaunchDaemon。

④守护程序和代理程序都是通过自己的属性列表文件(.plist)声明的。

9.launchd是用户态出现的第一个进程。当系统还在启动初期的时候,launchd是系统上唯一的进程(尽管这个状态很短暂)。这意味着系统启动和功能的方方面面都和launchd直接或间接相关。

10.launchd的第一个,也是主要的职责就是init守护程序的职责。init的职责是派生出各种各样的后台守护程序,设置好系统,然后转入后台,确保这些守护程序都活着。如果有程序死亡了,launchd要负责重新派生出新的守护程序。

11.传统意义上,init是用户态的第一个进程,从init fork出其他进程(转而可能fork出更多进程),init设置的资源限制会被所有后代继承。而launchd(新时代意义上的用户态第一个进程)还会设置Mach异常端口,内核内部通过异常端口处理异常情况并生成信号。

12.UNIX传统上包含两个守护程序——atd和crond——来定时作业,即在指定时间运行指定的命令。第一个守护程序atd负责一次运行的作业,第二个守护程序crond提供了重复执行作业的支持。

13.inetd/xinetd的用途是启动网络服务。这个守护程序的职责是绑定一些端口(UDP端口或TCP端口),当有连接请求到达的时候,根据需要启动相应的服务程序,并且将服务程序的输入输出描述符(stdin,stderr,stdout)连接到对应的套接字。

14.Mach的IPC服务依赖于“端口”的概念(有点类似TCP和UDP端口的概念),端口是通信的端点。

15.launchd还整合了iOS的Jetsam机制,Jetsam机制可以强制施行虚拟内存使用率的限制,这项特性在没有交换空间的iOS中特别重要。

16.ReportCrash守护程序是默认的崩溃处理程序,截获所有应用程序的崩溃。通过设置作业的Mach异常端口,在发生崩溃的时候自动运行。

17.iOS中amfid守护程序,可以阻止一切无签名无entitlement的代码在iOS中运行。

18.apsd(ApplePushService.framework)守护程序则是苹果推送服务的守护程序,以mobile用户运行。

19.atc守护程序则用于空中流量限制。crash_mover守护程序用于将崩溃日志移动到 /var/Mobile/Library/Logs 目录。

20.mobile_obliterator守护程序则用于远程擦除设备。

21.lockdownd就像是用户态的狱警,是所有越狱者的头号敌人。lockdownd由launchd启动,负责处理设备激活、备份、崩溃报告、设备同步以及其他服务。

22.OS X下的图形shell环境是Finder,iOS使用的择时SpringBoard。

23.和Finder不太一样,SpringBoard几乎全部靠自己完成所有的工作,在CoreServices目录下也只有少量几个可加载的bundle。

lockbundle提供了锁屏时的功能。NowPlayingArtLockScreen.lockbundle负责提供当音乐播放器正在运行且屏幕锁定的时候的锁屏画面,PictureFramePlugin负责显示用户照片库中的图片。iPhone还有一个名为VoiceMemosLockScreen的bundle,负责显示语音信息和未接电话指示器。

24.SpringBoard是一个多线程的应用程序,线程数远多于Finder。如果SpringBoard超过几分钟没有响应,那么看门狗会重启系统。

25.SpringBoard注册的端口中,最重要的是PurpleSystemEventPort,这个端口通过GSEvent消息的方式处理UI事件。SpringBoard中的主线程调用GSEventRun(),GSEventRun()是一个处理UI消息的CFRunloop。其他线程都类似的运行循环,处理SpringBoard中的其他Mach端口。

26.XPC是Lion和iOS5新引入的轻量级进程间通信原语。libxpc.dylib提供了各种各样的C语言层次的XPC原语。默认情况下XPC的消息是异步发送的,应答也是异步的,通过reply_sync函数可以阻塞知道收到应答消息。XPC是通过Mach消息机制实现的。

第二部分 内核

  • 第八章 内核架构

1.内核既是一个操作系统,也是一个调度器,还是一个仲裁器,内核同时也提供安全服务。

2.内核从架构上分为巨内核,微内核和混合内核。

3.巨内核采取的方式是将所有的内核功能——不论是基础功能还是高级功能——全部放在一个地址空间中。在这种架构的内核中,线程调度和内存管理,以及文件系统、安全管理、甚至设备驱动全都在一起。

所有的内核功能都实现在同一个地址空间中吗。为了进一步优化,巨内核不进将所有的工呢过都组织在同一个地址空间中们还将这个地址空间映射到每一个进程的内存中。

在巨内核架构中,从用户态到内核态的切换非常搞笑,基本上就是一次线程切换的开销。这是因为内核的你内存页面映射在所有进程的地址空间中,也就是说,除了硬件强制的内核态和用户态之间的隔离外,两者之间其实没有任何分别。所有的进程,不论所有者或功能,都包含一份内核内存的拷贝,就好像包含共享库的拷贝一样。此外,这些拷贝(同样类似于共享库)都映射到了同一组物理页面,而且是常驻内存的物理页面。

4.XNU的核心组件Mach是一个微内核系统。

一个微内核值包含最核心的内核功能,代码量也最精简。内黑只负责完成最最关键的部分——通常包括任务调度和内存管理,其他的功能都交给外部服务程序(通常是用户态)完成。

微内核有几个巨内核没有的有点:正确性,稳定性和健壮性,灵活性(移植)。

尽管微内核架构有着种种优势,但是却有一个致命的缺点——性能。微内核的消息传递在底层需要通过内存复制操作以及数次上下文的切换来实现,而这些操作对计算速度的影响都不小。

5.混合内核试图结合两种内核的好处。内核最核心部分支持底层服务,包括调度、进程间通信和虚拟内存,是自包含的,这一部分就像微内核一样。所有其他的服务都实现在这个核心之外,但是也在内核态中,而且和这个核心在同一个内存空间中。

这种内核不强制要求消息传递。其他组件可以调用这个“内部核心”的服务,但是这个“内部核心”不能调用外部的组件。

6.从技术上说,XNU是一个混合内核。Windows内核也被认为是一个混合型的内核,但是两者差别巨大。Windows更接近巨内核,所以死亡蓝屏概率高,而XNU更接近于微内核。

7.XNU的Mach最早是一个真正的微内核,现在Mach的原语仍然是围绕着消息传递的基础构建的。然而,消息通常是以指针的形式传递的,因此没有昂贵的复制操作。这是因为大部分服务现在都在同一个地址空间中执行(因为也会被归为巨内核)。类似的,建立在Mach之上的BSD层一直都是一个巨内核,而且这个子系统也在同一个地址空间中。

8.32位的OS X应用程序可以享用完整的没有内核预留的地址空间——内核有自己的地址空间。然而在64位的OS X中,苹果却顺从了,就像其他巨内核的系统一样,内核空间和用户空间是共享的。在iOS中也是如此。

9.内核是一个受信任的系统组件。内核的功能和应用程序的功能之间需要有一种严格的分离,否则应用程序的崩溃会使整个系统本科鬼。这种分离需要由硬件强制支持,因为基于软件的强制实施不但会产生很大的开销,也不可靠。区分内核态和用户态非常重要,因此这个功能是由硬件提供的。

10.用户态和内核态的切换有两种类型:

①自愿转换,比如系统调用;

②非自愿转换,当发生异常、中断或处理器陷阱的时候,代码的执行会被挂起,并且保留发生错误时候的完整状态。控制权被转交给预定义的内核态错误处理程序或中断服务程序。

11.一共有三种类型的异常:

①错误(fault):指令遇到一个可以纠正的异常,并且处理器可以重新启动这条出现异常的指令。

②陷阱(trap):类似于错误,但是错误处理完成后返回发生陷阱指令之后的那条指令。

③中止(abort):不可重启指令。

12.BSD系统调用,可以通过current_task获得当前BSD进程的数据结构。

第九章 内核引导和内核崩溃

1.苹果只开源了针对OS X编译的XNU版本,iOS的版本则是闭源的。

2.和Linux内核类似,Linux可以针对特定架构编译,Mach也能。

3.如果要在一堆源码文件中查找某个特定的函数名、变量名或其他符号,grep是一个不错的工具,grep可以接受任何正则表达式,并且在.h和.c文件中寻找匹配。

4.vstart是i386/x64架构下的“官方”的内核初始化函数,标志着从汇编代码到C语言代码的转换。

5.除了虚拟内存之外,kernel_bootstrap还初始化Mach的一些关键抽象:

IPC——进程间通信是Mach构建的根基,IPC要求一些重要的资源,例如内存、同步对象和Mach接口生成器;

时钟clock——通过时钟抽象实现闹铃;

账本——账本是Mach系统的记账工具;

任务task——任务是Mach的容器,类似BSD的进程;

线程thread——线程是实际的执行单元。任务只不过是一个资源容器,真正被调度和执行的是线程。

6.关于异常处理

第十章 Mach原语:一切以消息为媒介

1.XNU的核心是苹果从NeXTSTEP带来的Mach微内核。尽管Mach核心被BSD层包装起来了,而且主要的内核接口是标准的POSIX系统调用,但是这个Mach核心具有一组独特的API和原语。

消息传递原语:讨论消息和端口,这是Mach IPC的基础。

同步原语:锁和信号量是两种内核对象,这些对象用于确保并发执行的安全。

2.Mach采用的是极简主义的概念。Mach和其他操作系统不同,其他操作系统提供了用户态进程实现所基于的完整模型,而Mach只提供了一个极简的模型,操作系统本身可以在这个模型的基础上实现。

在Mach中,所有的东西都是通过自己的对象实现的。进程(在Mach中称为任务)、线程和虚拟内存都是对象,所有对象都有自己的属性。

Mach的独特之处在于选择了通过消息传递的方式实现对象和对象之间的通信。Mach对象不能直接调用另一个对象,而是必须传递消息。源对象发送一条消息,然后这条消息被加入到目标对象的队列中等待处理。类似的,消息处理中可能会产生一个应答,这个应答通过另一条消息被发送回源对象。消息是以FIFO的方式可靠传输的(如果消息被发送出去,那么一定能被收到)。

3.Mach的首要设计目标也是最重要的目标就是要将所有功能移出内核,并且放在用户态中,将内核保持在极简的状态。

4.Mach的设计有一个非常强大的优点——在设计中考虑了多处理。从理论上说,Mach可以轻松扩展成计算机集群使用的操作系统。

5.Mach中最基本的概念是消息,消息在两个端点或端口之间传递。任何两个端口之间都可以传递消息——不论是同一台机器上的端口还是远程主机的端口。Mach消息的设计考虑了参数串行化、对齐、填充和字节顺序的问题,这些问题都被消息实现隐藏了。

6.Mach消息的发送和接收都是通过同一个API函数mach_msg()进行的,这个函数在用户态和内核态都有实现。

7.Mach消息原本是为真正的微内核架构而设计的,也就是说,mach_msg()函数必须在发送者和接受者之间复制消息所在的内存。但是XNU通过单一内核的方式来“作弊”:所有的内核组件都共享同一个地址空间,因此消息传递时只需要传递消息的指针就可以了,从而省去了昂贵的内存复制操作。

8.为了实现消息的发送和接收,mach_msg()函数调用了一个Mach陷阱(trap)。Mach陷阱就是Mach中跟系统调用等同的概念,在用户态调用mach_msg_trap()会引发陷阱机制,切换到内核态,在内核态中,内核实现的mach_msg()会完成实际的工作。

9.消息在端口之间传递。消息从某个端口发送到另一个端口。每个端口都可以接收来自任意发送者的消息,但是只能有一个指定接收者。向一个端口发送消息时实际上是将消息放在一个队列中,直到消息能被接收者处理。

10.所有的Mach原生对象都是通过对对应的端口访问的。

11.端口和权限也可以从一个实体传递到另一个实体。实际上,通过复杂消息将端口从一个任务传递到另一个任务并不罕见。这是IPC设计中的一个非常强大的特性,有一点类似于主流UNIX的domain socket,允许在进程之间传递文件描述符。

12.Mach的消息传递模型是远程过程调用(RPC)的一种实现。

13.IPC所需要的基本原语:消息、发送和接收消息的端口,以及确保安全并发的信号量和锁。

14.每一个Mach任务(Mach任务是一个对应于进程的高层次抽象)包含一个指针指向自己的IPC名称空间,在名称空间中保存了自己的端口。此外,任务也可以获得系统范围内的端口。

15.同步机制的根本是排他访问的能力:当别人在使用一个资源时,排除其他人对这个资源的访问。因此最基本的同步原语是互斥对象,也称为互斥体。互斥体只不过是内核内存中的普通变量,通常是机器字大小的证书,但是有一个特殊要求——硬件必须对这些变量进行原子操作。“原子”的意思就是说,对互斥体的操作决不允许被打断——即使是硬件中断也不能打断。在SMP系统上,对物理互斥还有一个要求,就是要求硬件实现某种内存屏障。

16.Mach的锁依赖两个层次组合而成:硬件相关层——依赖于硬件的特殊性质,并且通过特定的汇编指令实现原子性和互斥性;硬件无关层——通过统一的API包装硬件特定的调用,这些API使得Mach之上的层完全不用关心实现的细节。

17.互斥体有一个最大的缺点,就是一次只能有一个线程持有锁。读写锁就是这个问题的解决方案,读写锁能够区分读访问和写访问,多个读者可以同时持有锁,而一次只能有一个写者。当一个写者持有锁时,所有其他线程都被阻塞。 ——pthread_rwlock_t就是POSIX层的API在iOS下的读写锁,区分pthread_rwlock_rdlock读和pthread_rwlock_wrlock写的加锁,解锁统一用pthread_rwlock_unlock。

18.阻塞一个线程意味着放弃线程的时间片,把处理器让给调度器认为下一个要执行的线程。当锁可用时,调度器会得到通知,然后根据自己的判断将线程从等待队列中取出并重新调度。

19.Mach提供了信号量,信号量是泛化的互斥体。互斥体的值只能是0和1,而信号量的取值可以达到某个正数,即允许并发持有信号量的持有者的个数。信号量可以在用户态使用,而互斥体只能在内核态使用。

20.在XNU上,POSIX信号量的底层实现是通过Mach信号量实现的。

21.信号量可以转换为端口,也可以由端口转换而来。

22.锁集就是锁的数组,通过给定的锁ID可以访问锁。锁也可以传递给其他线程。交出一个锁会阻塞交出锁的线程,并唤醒接受锁的线程。

23.锁集的有趣之处在于允许锁的传递。锁的传递指的是将锁从一个任务传递给另一个任务的过程。Mach在调度中也使用了传递的概念,允许一个线程放弃处理器但是指定哪一个线程接替运行。

24.Mach提供了一组异常丰富的API调用用于查询机器信息,所有这些调用都要求获得主机端口才能工作。

主机API最重要的一个方面就是能提供其他方式几乎无法获得的信息。Mach API是获得内核模块信息,内存映射表信息以及其他POSIX(BSD层)无法获得的信息的最直接方法。

25.所有的用户都可以通过mach_host_self()获得主机端口,但是只有特权用户才能通过调用host_get_host_priv_port()获得特权端口。

26.host_set_exception_ports()获得/设置或交换主机层次的异常处理程序。

27.一个或多个processor_t对象可以分组为处理器集,或称为pset。pset中的处理器通过两个队列进行维护:一个是active_queue,保存当前正在执行线程的处理器;另一个是idle_queue,用于保存当前空闲的处理器。

第十一章 Mach调度

1.和所有现代的操作系统一样,内核调度的对象是线程,而不是进程。Mach使用了比进程更轻量级的概念:任务task。

2.线程定义了Mach中最小的执行单元。一个或多个线程包含在一个任务中。

3.Mach将任务定义为线程的容器,因此资源是在任务这个层次处理的。线程只能(通过端口)访问包含这个线程的任务中分配的资源和内存。

4.任务task是一种容器对象,虚拟内存空间和其他资源都是通过这个容器对象管理的。这些资源包括设备和其他句柄。资源进一步被抽象为端口。因此资源的共享实际上相当于允许对对应端口进行访问。

5.严格的说,Mach的任务并不是其他操作系统中所谓的进程,因为Mach作为一个微内核的操作系统,并没有提供“进程”的逻辑,而只提供了最基本的实现。不过在BSD的模型中,这两个概念有1:1的简单映射,每一个BSD进程(也就是OS X进程)都在底层关联了一个Mach任务对象。实现这种映射的防范是指定一个透明的指针bsd_info,Mach对bsd_info完全无知。

6.任务是没有生命的,任务存在的目的就是要成为一个或多个线程的容器。大部分针对任务的操作实际上就是遍历给定任务中的所有线程,并对这些线程进行对应的线程操作。

7.在任何时刻,内核都必须能够蝴蝶当前任务和当前线程的句柄。内核分别通过current_task()和current_thread()函数完成这两个任务。

8.thread_suspend/thread_resume表示挂起/恢复线程,会递增/递减挂起计数器。线程只有在其suspend计数器和所在的任务的suspend计数器都为0时才能执行。

9.当调用pthread_create()时,底层会转而调用Mach的API调用thread_create(),并且使用mach_task_self()作为第一个参数。

10.由于每一个处理器核心在同一时刻只能运行一个线程,所以内核必须具有抢占一个线程的执行,将处理器让给另一个线程的能力,从而实现上下文切换。

11.由于Mach具有处理器集的抽象,所以从某种角度说,Mach比Linux和Windows更擅长管理多核处理器:Mach可以将同一个CPU的多个核心放在同一个pset中管理,并且通过不同的pset管理不同的CPU。

12.上下文切换是暂停某个线程的执行,并且将其寄存器状态记录在某个预定义的内存位置中。当一个线程被抢占时,CPU寄存器中会加载另一个线程保存的线程状态,从而恢复那个线程的执行。

13.一个线程在CPU上可以执行任意长时间。执行指的是这样一个事实:CPU寄存器中填满了线程的状态,因此CPU执行该线程函数的代码。这个执行过程一直持续,直到①线程终止;②线程自愿放弃CPU;③外部中断打断了线程执行(时间片用完或更高优先级的线程被唤醒)。

14.每一个操作系统都提供了一个优先级的范围:Windows有32个优先级,Linux有140个优先级,而Mach有128个优先级。

内核线程的最低优先级为80,比用户态线程的优先级要高,可以保证内核以及系统维护管理的线程能够抢占用户态的线程。

通过 ps -l 命令可以查看优先级

15.Mach会针对每一个线程的CPU利用率和整体系统负载动态吊证每一个线程的优先级。因此线程会在自己的优先级范围中“漂移”,如果耗CPU太多则降低优先级,如果不能得到足够的CPU资源则提升优先级。

16.在使用多核、SMP或超线程的现代架构中,还可以设置某个线程和一个或多个指定CPU的亲缘性。这种亲缘性对于线程和系统来说都是有好处的,因为当线程回到同一个CPU上执行时,线程的数据可能还留在CPU的缓存中,从而提升性能。用Mach的说法,线程对CPU的亲缘性的意思就是绑定。

17.Mach含有的特殊特性:

①控制权转交(handoff)允许一个线程主动放弃CPU,但不是将CPU放弃给任何其他线程,而是降CPU转交给自己选择的某个特定的线程。

②使用续体可以使线程不用管理自己的栈,线程可以丢弃自己的栈,系统恢复线程执行时不需要恢复线程的栈。

③异步软件陷阱AST是软件对底层硬件陷阱机制的补充完善。通过使用AST,内核可以响应需要得到关注的带外(out-of-band)事件,例如调度事件。

18.Mach对yield做了改进,允许选择将CPU转交给谁。控制权转交并不是对调度器的强制要求,调度器还可以选择将控制权转交给其他线程(例如,如果指定的线程处于不可运行的状态)。作为控制权转交的结果,当前线程剩下的时间片也会被转交给新调度的线程。

19.如果线程要进行控制权转交而不是简单的yield操作,那么需要调用thread_switch(),Mach将thread_switch()导出为一个陷阱,因此也可以从用户态调用这个函数。

20.上下文切换采用的是每一个线程都有自己独有栈的模型,当线程自愿请求一次上下文切换时可以选择指定一个续体。如果指定了续体,那么当线程恢复执行时,系统会以续体作为入口点重新加载线程,并创建新的栈,之前的状态都不会得到保留。这样可以明显加快上下文切换的速度,因为不需要保存和加载寄存器(此外还能显著地节省内核栈的空间,内核栈本身很小,只有4个页面,即16kb)。续体中的线程只需要4-5kb的空间来保存线程状态,将16kb中的其他空间节省下来用作其他用途。使用续体不需要保存完整的寄存器状态和线程栈,只需要保存续体以及参数。

21.续体是缓解上下文切换开销的简单有效的机制,主要由Mach的内核线程使用。内核线程特别喜欢使用续体,Mach的内核线程是通过续体启动的。

22.Mach支持两种不同模式的抢占——显示抢占和隐式抢占,续体模式只能用于显示抢占。

23.系统中的线程可能会被两种方式抢占:一种是显示的抢占,即线程放弃CPU的控制权或进入阻塞的操作;另一种是隐式的抢占,这种抢占是由中断引起的。显示抢占有时候也被认为是同步的,因为这种抢占是事先可以预知的。而由于中断不可预测的本质,所以隐式的抢占是异步的。

24.发生显示抢占的原因可能是等待某个资源,等待某个IO,或睡眠一定的时间。当用户态的线程调用阻塞的系统调用(例如read(),select()和sleep())时会发生显示抢占。

25.thread_invoke()函数负责执行上下文切换并负责处理续体。

26.显示抢占本身是有局限性的,将放弃CPU的选择权交给运行的线程是极为不可靠的。线程会陷入费时的处理操作中,甚至会进入死循环。

27.Mac OS X是一个抢占式的多任务系统。简单的说,Mach具有随时抢占一个线程的权利,不论这个线程是否准备好了抢占。和显示的抢占不同,这种隐式的抢占对于线程来说是不可见的。线程可以对这种抢占完全不知情,线程的状态会被透明的保存并恢复。大部分线程不会受到太大的影响,因为大部分线程都是IO密集型的。但是对于CPU密集型的线程来说,这种抢占可能会造成一些问题,特别是要求时间关键的性能时(例如视频和音频解码都是这种类型的任务)。

28.Mach是一个分时系统,而不是一个实时系统。

29.THREAD_AFFINITY_POLICY策略定义了线程的L2缓存亲缘性,这些线程共享同一块缓存。这意味着这些线程很可能运行在同一个CPU上,不论这个CPU有多少核心(毕竟同一个CPU上所有核心都共享同一块L2缓存)。

30.异步软件陷阱AST就是为了支持隐士抢占的。AST是人工引发的非硬件触发的陷阱。AST是内核操作的关键部分,而且是调度时间的底层机制,也是BSD信号的实现基础。

31.ast_taken函数(内核陷阱中和内核线程终止时也可以调用)负责处理除了内核idle线程之外的所有线程的AST。否则,这个函数会检查AST_BSD,这原本是对Mach的一个临时修改,使其能够处理BSD事件(例如信号),但是被永久的保留了。如果设置了AST_BSD,则调用bsd_ast处理信号。

32.Mach的线程调度算法高度可扩展,而且允许更换用于线程调度的算法。不过通常情况下,只启用了一个调度器,即传统调度器。调度器大量使用了AST机制。

33.对于要提供抢占式多任务的系统来说,必须有某种机制允许调度器能够首先得到CPU的控制权,从而抢占当前正在执行的线程,然后才能执行调度算法,并且通过调度算法决定当前的线程可以继续恢复执行还是要抢夺其CPU给更重要的线程使用。

34.为了能够从当前运行的线程抢夺CPU,现在的操作系统都利用了现有的硬件中断机制。由于中断的特点是强迫CPU在发生中断时“放下手中所有的任务”,并longjmp跳转到中断处理程序(也称为中断服务例程ISR)执行,因此可以通过中断机制在发生中断时运行调度器。但是问题是,中断是异步的。

35.内核可以配置时钟使其在给定数目的周期之后产生一个中断。这个中断源通常称为定时器中断。

36.解决方案是采用另一种不同的模型:无tick内核。在这种模型中,每一次定时中断发生时,定时器都会被重新设置为调度器认为需要下一次中断的时刻。这意味着在每一次定时器中断时,中断处理程序都要(非常快)扫描还没超期的截止时间线的列表。相比大量不必要的中断,每一次定时器中断中多做的这些处理工作还是值得的,而且通过只跟踪那些最紧急的截止时间线可以将这些处理工作的开销降到最低。

37.在添加非严格的定时器事件是会加上一个所谓的“宽限slop”值,通过宽限值可以合并一些定时器事件,从而增加这些定时器事件同时超时的概率(从而减少了定时器中断的总数)。

38.Mach只提供了一个异常处理机制用于处理所有类型的异常——包括用户定义的异常、平台无关的异常、以及平台特定的异常。

39.在Mach中,异常是通过内核中的基础设施——消息传递机制——处理的。异常由出错的线程或任务(通过msg_send())抛出,然后由一个处理程序(通过msg_recv())捕捉。处理程序可以处理异常,也可以清除异常,可以决定终止线程。

40.Mach的异常处理程序在不同的上下文中运行异常处理程序,出错的线程向预先指定好的异常端口发送消息,然后等待应答。每一个任务都可以注册一个异常端口,这个异常端口会对同一个任务中的所有线程起效。单个的线程还可以通过 thread_set_exception_prots 注册自己的异常端口。通常情况下,任务和线程的异常端口都是NULL,也就是说异常不会被处理。

41.发生异常时,首先尝试将异常抛给线程的异常端口,然后尝试抛给任务的异常端口,最后再抛给主机的异常端口(即主机注册的默认端口)。如果没有一个端口返回 KERN_SUCCESS,那么整个任务被终止。Mach不提供异常处理逻辑——只提供传递异常通知的框架。

42.exception_triage()负责主要的异常处理逻辑,这个逻辑在两种架构上的Mach消息层面都是一样的。这个函数尝试根据前文描述的方式——线程、任务、最终到达主机——利用exception_deliver()投递异常。

43.每一个线程或任务对象,以及主机本身,都有一个异常端口数组,这个数组中的端口通常初始化为IP_NULL。通过xxx_set_exception_ports()调用可以设置这些异常端口,其中的xxx为thread、task或host。

44.[mach]_exception_raise 用于EXCEPTION_DEFAULT,[mach]_exception_state_raise 用于EXCEPTION_STATE。[PLCrashReporter里有相关的使用] 这些函数最终通过调用ux_exception将异常转换为响应的UNIX信号,并且通过threadsignal将信号投递到出错的线程。

45.OS X的最重要的特性之一崩溃报告器(crash reporter)就是利用异常端口的机制实现的。launchd注册了异常端口,然后将所有子进程都应用同样的异常端口,因为异常端口是随着进程fork集成的。launchd将ReportCrash设置为MachExceptionHandler。通过这种方式,当一个launchd作业发生异常时,崩溃报告器会自动根据需要启动。调试器也可以利用异常端口捕捉异常并且在发生错误时中断。

46.Mach异常处理会先于UNIX异常处理发生。

47.使用mach_msg在异常端口上创建一个活动监视者。异常处理可以由同一个程序中的另一个线程来完成,不过更有意思的做法是在另一个程序中实现异常处理的部分。

48.Mach是XNU的微内核核心。XNU暴露给用户的主要接口:BSD层。BSD层使用了Mach作为底层的原语和抽象,向应用程序暴露出流行的POSIX API,使得OS X能够和很多其他的UNIX实现兼容。

常见的架构无关的Mach异常

EXC_BAD_ACCESS 内存访问异常,代码包含发生内存访问异常的地址。

EXC_BAD_INSTRUCTION 指令异常,非法或未定义的指令或操作数。

EXC_BREAKPOINT 和跟踪、断点相关的异常

EXC_SYSCALL 系统调用

EXC_CRASH 异常的进程退出

第十二章 虚拟内存

1.Mach和所有内核一样,代码中有很大一部分都在负责高效的管理虚拟内存。

2.vm_map:表示任务地址空间内的一个或多个虚拟内存区域。每一个区域都由一个独立的条目vm_map_entry表示,这些条目由一个双向链表vm_map_links维护。

vm_map_entry: 每一个vm_map_entry 都表示了虚拟内存中一块连续的区域。每一个这样的区域都可以通过指定的访问保护权限进行保护(和虚拟内存页面采用同样的权限,即r/w/x权限)。vm_map_entry 通常指向一个vm_object,但是也可以指向一个嵌套的vm_map,即子映射。

vm_object: 用于将vm_map_entry 和实际支撑的内存关联起来。这个数据结构包含一个 vm_page 的链表。

vm_page:vm_page真正表示了vm_object或部分vm_object。vm_page可以有多种状态:驻留内存、交换出、加密、干净和脏等。

3.每一个Mach任务都有一个自己的虚拟内存空间,任务的struct task 中的map字段保存的就是这个虚拟内存空间。

4.purgeable的对象在内存低的情况下可能会丢失,即直接释放,而不是提交到后备存储。

5.Mach的API比POSIX提供的等同API要强大得多,主要是因为Mach API允许一个任务入侵到另一个任务的地址空间。为了访问其他任务的地址空间,要求有相应的权限(具体来说就是任务的端口)。除了这点要求之外,这些调用几乎是无所不能的。事实上,在OS X中很多进程入侵和线程注入技术都依赖这些Mach调用,而不是依赖BSD提供的调用。

6.vmmap()的例子可以很容易扩展得更具有入侵性,比如可以将进程内存映射导出到磁盘,甚至可以写入内存映射。

7.pmap可以嵌套(即包含其他pmap)。这是一个非常常见的技术,共享内存严重依赖这项技术——包括隐式的共享内存(共享库)和显式的共享内存(mmap())。

8.Mach Zone的概念相当于Linux的内存缓存和Windows的Pool。Zone是一种内存区域,用于快速分配和释放频繁使用的固定大小的对象。Zone的API是内核内部使用的,在用户态不可使用。内核中的Zone和malloc的zone完全不同,后者是C运行时库的一部分,在用户态使用,而且具有很好的文档。

BSD内核zone 直接构建与Mach的zone之上。

9.如果系统内存不足,zone可能会进行垃圾回收。垃圾回收是一个两趟的过程,系统首先扫描所有的zone(跳过标记为不可回收的zone),检查这些zone的空闲列表,判断哪些对象是可以回收的。在第二趟中,将这些对象转换为页面:和非空闲对象共享了一个页面的对象不能被释放,只有页面全部空闲的对象才能被释放。最后,当判定好了可以释放的页面之后,通过kmem_free()释放。

10.所有的内核分配(除了连续物理内存的分配)的路径最终都会到达一个函数,那就是kernel_memory_allocate()。

11.进程的内存需求早晚会超过可用的RAM,系统必须有一种方法能将不活动的页面备份起来并且从RAM中删除,腾出更多的RAM给活动的页面使用,至少暂时能够满足活动页面的需求。在其他的操作系统中,这个工作是由专门的内核线程完成的。例如,Linux中的pdflush和wswapd内核线程。在Mach中,这些专用的任务成为分页器,分页器可以是内核线程,甚至可以是外部的用户态服务程序。

这里提到的分页器及基金实现了各自负责的内存对象的分页操作。这些分页器不会控制系统的分页侧路。分页策略是由vm_pageour守护线程负责的,而vm_pageout守护线程是kernel_bootstrap_thread()完成所有任务之后最后变成的。

12.Mach通过Universal Page List(统一页列表)这个数据结构来维护页的信息,这个列表和分页器的具体实现无关。UPL是连接虚拟地址和实际的物理页面的纽带,有一点类似于Windows的Memory Descriptor List和IOKit的IOMemoryDescriptor。

13.Vnode分页器负责支持文件的内存映射。当内存映射了文件,这些文件的内容需要从文件系统中读取。当内存映射的文件在内存中“脏”了,那么这些脏的页面需要写回文件系统。解密的页面永远不会被标记为脏,因此永远都不会被换出到磁盘上(如果可以从交换文件中提取到明文,那么整个加密就没有意义了)。

14.通过DYLD_INSERT_LIBRARIES强制注入一个库,然后直接从任务中读取内存。这也是为什么尽管App Store的二进制文件被加密,但是iOS应用程序的破解依然很繁荣的原因。

15.pageout守护程序其实不是一个真正的守护程序,而是一个线程。vm_pageout永不返回。初始化之后会派生出两个线程:外部的iothread,和一个垃圾回收线程(其实还有第三个线程,内部的iothread,是默认分页器注册时创建的)。设置完成后,vm_pageout()最终调用vm_pageout_continue(),这个函数周期性的唤醒并执行vm_pageout_scan()。

16.BSD层的Jetsam机制类似于Linux的Low Memory Killer。

17.在iOS中,物理内存非常紧缺而且没有交换空间,这个宏调用了vm_check_memorystatus(),而后者负责唤醒kernel_memorystatus线程,这属于Jetsam机制的一部分。

18.vm_fault()函数调用vm_page_fault()处理实际发生错误(缺页中断)的页面,并且从后备存储中将这个页面取回。实现方法是:查找vm_page对应的vm_object,然后从中获得分页器的端口,分页器的data_request函数负责从后备存储中读入要换入的页面。如果需要的话,换入操作还会对页面进行解密(如果页面在加密的交换文件中),并且验证代码签名。

19.非法访问:访问一个没有映射到进程地址空间(即任务的vm_map)的地址。解引用一个野指针时通常会发生这种错误。发生这种错误时进程会收到SIGSEGV信号。

20.页面保护错误:访问一个映射的地址,但是页面的保护掩码拒绝请求的访问。通常引发这个错误的原因包括跳转到数据段或试图写入(或读取)一个不允许写入(或不允许读取)的页面。发生这种错误时进程会收到SIGBUS信号。

21.dynamic_pager()是一个用户态的守护程序,负责维护系统交换文件,默认情况下交换文件在/private/var/vm/swapfile目录下。内核的default_pager分页器需要在用户态的干预下调整或修改交换条件时,从内核态调用这个守护程序。

第十三章 BSD层

1.Mach只是一个微内核。尽管Mach的部分应用程序编程接口(API)也暴露给了用户态,但是开发者主要使用的还是更为流行的POSIX API,而这一套API是通过Mach之上的BSD层实现的。

2.ptrace()属于进程控制相关的调用。

3.在Mach提供的这些原语之上还需要建立一个层次提供像文件、设备、用户和组等重要从抽象。Mach最早选择的这个层次就是BSD,而且延续在XNU中了。

4.BSD采用了两个原语,并且组织成立UNIX世界上著名的进程和线程的概念。

5.BSD的进程可以唯一的映射到Mach任务,但是包含的信息比Mach任务提供的基本调度和统计信息要丰富。其中最值得注意的是,BSD进程包含了文件描述符和信号处理程序的数据。进程还支持复杂的谱系,将进程和其父进程、兄弟进程和子进程连接起来。

6.进程就是容器,二进制代码的实际执行单元是线程。

7.用户态的线程始于对pthread_create的调用。这个函数做的工作并不多,因为主要工作是由bsdthread_create()系统调用完成的,bsdthread_create()只不过是对Mach线程创建的复杂包装。真正的线程创建是由底层的Mach层完成的。bsdthread_create()负责的工作是设置线程栈(如果指定了自定义栈),设置(机器相关的)线程状态,以及设置自定义调度参数(如果提供了的话)等。

8.在UNIX中,进程不能被创建出来,只能通过fork()系统调用复制出来。如果fork()操作失败,fork()只会在调用的进程中返回-1。

9.子进程是父进程的完整复制,除了一下几个重要的例外:

①文件描述符,尽管数值和指向的文件都是一样,但只是原始描述符的副本。这意味着后续对这些描述符修改的调用(例如lseek()和close())只会影响创建这些描述符的进程。

②资源限制,子进程会继承资源限制,但是资源利用率都设置为0。

③子进程的内存映像看上去是子进程私有的,但是事实上子进程和父进程使用的是内存中相通的物理页面。虚拟内存的私有性是通过设置页面的写时复制标志位来保证的,因此不论是哪个进程试图写入页面时都会引发页错误,从而创建页面的副本,并且重新建立映射。

10.vfork()的进程没有对应的Mach任务和线程。只有在接下来调用了execve()之后才会创建任务和线程。事实上,除了有一个execve()跟在后面之外,vfork()没有存在的意义,因为这个系统调用最初的设计就是为了这个目的。子进程的 task_t 和 thread_t (可以分别通过mach_task_self()和mach_thread_self()获得)完全就是父进程的 task_t 和 thread_t ,vm_map也是如此,只有以后调用 execve()载入一个Mach-O镜像才能最终真正创建一个Mach任务和进程。

11.如果将一个进程比作是一个人体,那么在进程中执行的二进制程序就是这个人体的大脑。只是通过fork()新创建出来的进程也没有多大作用,除非执行镜像通过exec()替换为另一个可执行程序。因此,进程创建的核心在于二进制文件的加载和执行。

12.execsw镜像加载器。进程执行镜像的流程太长了,这里不做记录,还是直接看书吧,

13.所有的镜像加载的路径要么终止在一个错误上,要么最终完成加载Mach镜像。

14.XNU中的Mach-O加载逻辑基本上和NeXT在1988年发明这个格式时差不多。经过这么多年苹果对这个过程做了一些修改,其中主要针对代码解密部分进行修改,但是Mach-O文件格式基础基本没什么变化。

苹果将这个修改封装在 exec_mach_imgact()中了,这是Mach二进制文件注册的处理程序。这个函数首先读取Mach文件头,然后解析其架构(32位或64位)和标志位。这个函数拒绝接受Dylib和Bundle文件——这些文件是由用户态的dyld动态链接器负责的。然后再应用posix_spawn()中的参数(如果有的话)。之后,对二进制进行评估以确保满足当前架构的需求。

处理Mach-O加载的主要函数是load_machfile()。load_machfile()函数负责设置内存映射,这个映射最终会加载各种 LC_SEGMENT 命令加载的数据。

Load_machfile()的核心在于parse_machfile。这个函数负责实际解析加载命令的繁杂工作。

其中,load_code_signature 也是load_machfile()函数里众多流程中的一个步骤,也就是验证代码签名。

经过三趟扫描后,在dlp变量中有一个保存的动态链接器命令,将动态链接器加载到新的映射中,可能要根据ASLR偏移进行调整。load_dylinker()函数会递归的调用parse_machfile()。

如果load_machfile()成功返回了,exec_mach_imgact会继续完成后续的工作。具体操作如下:

①通过调用vm_map_set_user_write_limit设置ulimit-m;

②设置代码签名的标志:

​ CS_HARD:拒绝加载无效页;

​ CS_KILL:如果有任何无效页则杀掉进程;

​ CS_EXEC_*:和上面两个标志位一样,只不过来自execve()。

③设置新的进程名称等等。

需要注意的是,这里并没有强制任何事情:真正的代码签名实施是在Mach的VM页错误处理程序中,通过调用 cs_invalid_page 来强制实施策略。

15.Mach提供了丰富的跟踪机制,其中最重要的就是DTrace。另一个机制ptrace(),这个机制在OS X和iOS(故意的)上只有部分功能有效。

16.BSD和其他UNIX系统提供了一个名为ptrace()的一站式系统调用,这个调用支持进程跟踪和调试。这个系统调用对于调试和逆向工程来说非常有用,例如在Linux中,gdb,系统调用跟踪(strace)和库函数调用跟踪(ltrace)就是用了这个系统调用。

17.在Linux中,ptrace的真正实力在于能够读写其他进程的内存,而XNU的ptrace实现则忽略了这些选项。不过Mach的API也能实现类似的功能。

18.挂起一个进程相当于停止一个进程的执行无限长时间,知道这个进程被恢复。冷冻和解冻的决定权通常都在iOS的加载器SpringBoard手中。

19.Mach已经通过异常机制提供了底层的陷阱处理,而BSD则在异常机制之上构建了信号处理机制。硬件产生的信号被Mach层捕捉,然后转换为对应的UNIX信号。为了维护一个统一的机制,操作系统和用户产生的信号首先被转换为Mach异常,然后再转换为信号。

20.当一个BSD进程(也是用户态进程)被bsdinit_task()函数启动时,这个函数还调用了 ux_handler_init()函数,这个函数设置了一个名为 ux_handler 的Mach内核线程。只有在 ux_handler_init()函数返回之后,bsdinit_task() 才能够注册使用 ux_exception_port。

通过调用 host_set_exception_ports()函数,bsdinit_task() 将所有的Mach异常消息都重定向到 ux_exception_port,这个端口由 ux_handler() 线程持有。由于所有后创建的用户态进程都是PID 1的后台,所以这些进程都会自动继承这个异常端口,相当于 ux_handler() 线程要负责处理系统上 UNIX 进程产生的每一个Mach异常。

ux_handler()函数非常简单,这个函数在进入时首先设置好 ux_handler_port,然后进入一个无限的Mach消息循环。消息循环接收Mach异常消息,然后调用mach_exc_server()处理异常。

mach_exc_server会调用mach_exception_raise(),然后会被mach_catch_exception_raise()捕获,信号处理逻辑就在这里。

21.硬件产生的信号始于处理器陷阱。处理器陷阱是平台相关的。ux_exception负责将陷阱转换为信号。

如果信号不是由硬件产生的,那么这个信号来源于两个API调用:kill()或pthread_kill()。这两个函数分别向进程和线程发送信号。

第十四章 BSD的高级功能

1.虚拟内存管理是在Mach层进行的,Mach控制了分页器,并且向用户态导出了各种vm_和mach_vm_消息接口。而用户态的开发者大部分都只知道标准的POSIX调用,因此需要对这些Mach调用进行封装。类似的,BSD层也使用了自己的内存管理函数。

2.OS X和iOS实现了一个低内存情形的处理机制,成为Jetsam,或者称为Memorystatus。这个机制有点类似于Linux的“out-of-memory”杀手,最初的用途就是杀掉消耗太多内存的进程。Jetsam的名字来源于杀掉消耗内存最多的进程并且抛弃这些进程占用的内存页面的过程。

3.Memorystaus维护了两个列表:一个是快照列表,这个列表保存了系统中所有进程的状态以及消耗的内存页面数;还有一个优先级列表,保存了要杀掉的备选进程。

4.用户态也可以通过pid_suspend()和pid_resume()控制进程的休眠。

5.ASLR:Address Space Layout Randomization 内核地址空间布局随机化。

ASLR对内核代码的影响非常小:代码不再使用固定地址,而是转变为使用相对地址,相对地址是针对程序的当前位置确定的。

6.工作队列是OS X中开发的一项机制,作用是为应用程序提供多线程并扩展到多处理器支持。工作队列是GCD的基础机制。

7.overcommit位表示这个队列可以创建新的线程。通常情况下不建议使用这个策略,因为线程数多于CPU数会降低程序的运行速度。

GCD仅通过dispatch_get_global_queue调用可以接受的一个标志(DISPATCH_QUEUE_OVERCOMMIT)来支持overcommit,但是苹果的文档掩盖了这个事实,宣称这个标志位必须为0。

GCD和libdispatch在工作队列不存在或被禁用时也能工作,在这种情况下,GCD和libdispatch会退而使用线程池模型。

补充:

_dispatch_get_root_queue 会获取一个全局队列,它有两个参数,分别表示优先级和是否支持 overcommit。一共有四个优先级,LOW、DEFAULT、HIGH 和 BACKGROUND,因此共有 8 个全局队列。带有 overcommit 的队列表示每当有任务提交时,系统都会新开一个线程处理,这样就不会造成某个线程过载(overcommit)。——参考自https://bestswifter.com/deep-gcd/

阅读过 GCD 源码的同学会知道,所有默认创建的 GCD queue 都有一个优先级,但其实每个优先级对应两个 queue,比如一个是 default-priority, 那么另一个就是 default-priority-overcommit。dispatch_async 的时候,会首先将任务丢进 default-priority 队列,如果队列满了,就转而丢进 default-priority-overcommit。

在 Mac 系统里,GCD 允许 overcommit,意味着每次 dispatch_async 都会创建一个新线程,即使 over commit 了,这些过量的线程会根据优先级来竞争 CPU 资源。

而在 iOS 系统里,GCD 会控制 overcommit,如果某个优先级队列 over commit 里,那么排在后面的任务就会处于等待状态。移动设备 CPU 资源比较紧张,这种设计合乎常理。

所以如果在 iOS 里创建过多的 serial queue,那么后面提交的任务可能就会一直处于等待状态。这也是为什么我们需要严格控制 queue 的数量和层级关系,最好是 App 当中每个子系统只能分配固定数量和优先级的 queue,从而避免 thread explosion 导致的代码无法及时执行问题。

——参考自https://zhuanlan.zhihu.com/p/37463055

8.MAC:Mandatory Access Control 强制访问控制。

9.iOS的安全机制比OS X的安全机制严格得多。OS X中代码签名是可选的,而iOS会通过kill-9杀掉任何代码签名不正确的进程。

iOS中“坏警察”是由AMFL扮演的,AMFL在用户态也有一个守护程序:/usr/libexec/amfid。这个守护程序是由launchd启动的。也注册了一个主机特殊端口。

第十五/十六章 文件系统

1.ACL:Access Control List 访问控制列表。OS X允许通过chmod()设置和修改ACL。通过 ls -e 可以显示访问控制列表。 ——比如著名的chomd 777就是把文件改为可读可写可执行的状态

2.Unix要求维护3个时间戳:创建事件、修改时间和访问时间。

3.FIFO是UNIX对“具名管道”的实现。通过pipe()系统调用可以创建匿名管道,但是匿名管道不能在无关的进程间共享。

4.底层的文件系统可以是基于表的(例如FAT),也可以基于B树的(例如NTFS和HFS+)。

5.每一个操作系统都有一个自己的原生文件系统。DOS的原生文件系统是FAT,Windows的原生文件系统是NTFS,HFS+是OS X的原生文件系统。

6.内核不能执行压缩操作,而且也没有提供对外部压缩的支持:在内核层只支持解压缩。

7.HFS+使用的是UTF_16编码——双字节Unicode。HFS+也是大小写不敏感的文件系统。OS X 默认使用HFS+,iOS使用启动了大小写敏感的HFSX。

8.日志是磁盘中一块特殊的区域,用户看不见这个区域,文件系统在向磁盘提交事务之前会将事务记录在这个区域中。如果修改事务被成功提交,那么这些事务就会从日志中删除。但是如果发生了崩溃,文件系统可以快速恢复到一致的状态——要么重放日志(即提交所有记录的事务),要么回滚日志(如果包含未完成的事务)。

日志并不是解决数据丢失的灵丹妙药。然而,日志可以显著的减少系统崩溃导致文件系统无法使用的情况。

9.HFS+有一项很有意思很特别的特性是能够动态适应频繁访问的文件。HFS+为每一个文件维护一个热度值。HFS+能够在工作时进行碎片整理工作。

10.B树是一些文件系统构建的基础,例如NTFS(Windows)、Ext4(Linux)和苹果的HFS及HFS+。

11.任何文件系统最基本的概念就是用于保存和取得文件的机制。需要满足的需求包括:搜索、插入、更新和随机访问。

大部分文件系统都采用了基于树的方案。根据树形结构的设计,上述的要求都能满足,而且还很自然的提供了层次结构,这是平坦的表示结构无法提供的。

12.B数可以看成是二叉树的扩展,相似的地方在于都采用了树形结构,而不同的地方在于B树的节点可以有任意数据的子节点——定义为m——而不只是两个子节点。这种结构可以帮助限制树的深度,从log2(N)(典型的二叉树搜索时间复杂度),到最优情况的logm(N)以及最坏情况的logm/2(N)。

13.和所有的树一样,B树由节点组成,但是和其他树不一样的地方在于,B树的节点可以有具体的子类型,或称为kind。不同的节点类型可以保存不同的数据,但是所有类型的节点都来源于一个基本类型(可以看成是一个基类)。

为了遍历所有的记录,在节点尾部向头部方向依次保存了指向每一条记录的指针,其中也包含节点中包含的任何空闲空间使用的空记录的指针。

14.HFS+的B树总是有一个固定的深度。也就是说,所有的叶子节点都在同一层上。

15.当HFS+挂载时启动了日志功能,那么还会启用一个日志文件。

其他部分 网络协议栈/内核扩展模块/IOKit

1.一般的Cocoa开发者并不需要了解套接字相关的知识。因为CoreFoundation通过CFNetwork提供了封装了套接字的CFSocket和CFStream,此外还提供了一些协议的封装,例如CFFTP、CFHTTP等。尽管如此,BSD套接字是XNU中所有网络组件的核心(实际上也是所有现代操作系统的核心)。

2.模块化设计是可扩展性之母。

3.代码签名,如今已经被大多数系统采纳为标准。Windows是一个典型实例,只允许加载具有合法数字签名的驱动程序。在控制权转交给模块入口点之前,内核会验证代码签名,代码签名保存在附加的证书中。证书必须通过私钥签名,内核已知公钥,内核也可以通过一个信任链获得这样一个秘钥。

早在iBoot阶段,未经苹果签名的代码就不能被加载。

4.预链接是苹果在OS X和iOS中使用的方法。引导加载器不按照先加载内核,再以一定顺序加载kext的方式进行加载,而是加载一个kernelcache文件。——好处是加载的速度快得多,并且kernelcache可以添加签名,甚至还可以加密,一旦加载了kernelcache,就可以禁止所有kext加载,这样可以阻断代码进入iOS内核的合法通道。

5.IOKit有一个顶层的抽象基类是OSObject。

6.IOKit提供了一个工作循环(work loop)模型,有一点类似于Objective-C的runloop(或Mach的消息循环)。简而言之,工作循环是一个不断处理事件的消息处理循环。通过使用工作循环可以极大的简化并发问题,而且通常可以避免对锁的使用,锁是会影响性能的。

7.IOKit采用了NeXT的runloop模型,用户态开发者应该会想到CFRunLoop。IOKit版本的runloop成为IOWorkloop,基本思想是一样的:提供一个单线程且线程安全的机制处理所有类型的事件,如果不采用这种机制则是异步的。工作循环的访问被一个互斥体保护,因此不需要考虑可重入的问题以及线程安全的问题。不过要注意的是,不能保证工作循环确实是一个线程。也就是说,工作循环的迭代可能会运行在系统中另一个线程的上下文中。

8.上下文切换:是另一种类型的控制权转移,上下文切换指的是将当前正在执行的线程的上下文切换为另一个线程的上下文。

9.ARM和Intel处理器都在处理器层次提供了线程的支持。事实上,这也是为什么现代操作系统都不调度进程,而是调度线程的原因。

10.在现代操作系统中,实现并发的先决条件是具有能够提供安全锁定机制的能力,通过这种方式能够同步共享资源的访问。同步机制通常都依赖于硬件的支持,因此在ARM和Intel架构上的实现是不同的。Mach底层的hw_lock_lock()函数的实现时候一个很好的示例。从内核的角度看,这个函数提供的方法总是一直的:快速的自旋锁。

11.原子操作是和锁非常接近的功能。院子操作是一类保证了原子性(atomicity,几不可中断性)的操作。原子操作常作为锁操作的底层机制(因为必须以原子的方式访问锁),而且经常可以替代锁的使用(当要保护的对象为机器字大小时)。

原子不一定意味着单周期,原子的意思只是说CPU保证在访问的过程中不会被打断。

12.为了能够最优化的利用内部组件(例如ALU,FPU和加载/存储组件),现代的CPU会采用乱序的方式执行指令。然而在某些情况下,乱序执行可能会在程序中引入bug。在这种情况下,可以使用屏障(barrier)指令来确保程序执行到某个点时所有访问操作都完成了。——比如iOS的内存屏障

逐步分析

Glide.with(……)

Glide.with() 有下面几种实现方式。

1
2
3
4
5
1. Glide.with(Context context)
2. Glide.with(Activity activity)
3. Glide.with(FragmentActivity activity)
4. Glide.with(android.app.Fragment fragment)
5. Glide.with(View view)

所以的方法实现也是很类似,都是调用同一个方法

1
2
3
public static RequestManager with(Fragment fragment) {
return getRetriever(fragment.getActivity()).get(fragment);
}

再看一下 getRetriever() 方法

1
2
3
4
5
6
private static RequestManagerRetriever getRetriever(@Nullable Context context) {
……
省略一些判空检查
——
return Glide.get(context).getRequestManagerRetriever();
}

其中 Glide.get(context) 主要用来初始化 Glide 的全局单利对象,以及一些配置。

getRequestManagerRetriever() 则是返回 Glide 对象的 requestManagerRetriever 对象。

然后看一下 requestManagerRetriever.get() 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public RequestManager get(Context context) {
if (context == null) {
throw new IllegalArgumentException("You cannot start a load on a null Context");
} else if (Util.isOnMainThread() && !(context instanceof Application)) {
if (context instanceof FragmentActivity) {
return get((FragmentActivity) context);
} else if (context instanceof Activity) {
return get((Activity) context);
} else if (context instanceof ContextWrapper) {
return get(((ContextWrapper) context).getBaseContext());
}
}

return getApplicationManager(context);
}

get() 方法会根据传入的 context 对象和当前线程,创建不同的 RequestManager 实例

1
2
3
4
5
1. 非 UI 线程,返回 applicationManager 对象,能感知 Application 生命周期。
2. UI 线程,如果 context 是 Activity 、FragmentActivity
则会创建一个能感知对应 Activity 的 RequestManager。
3. UI 线程,如果 Context 是 Fragment、android.support.v4.app.Fragment
则会创建一个能感知对应 Fragment 生命周期 的 RequestManager。

这里反复提到了一个 感知生命 xx 周期,也是 Glide 的一个特性。

1
2
3
Glide 在加载资源的时候,如果是在 Activity、Fragment 这一类有生命周期的组件上进行。
当 Activity、Fragment 等组件进入不可见,或者已经销毁的时候,Glide 会停止加载资源。
Application 的生命周期贯穿整个应用,所以 applicationManager 只有在应用程序关闭的时候终止加载。

所以尽量不要在非 UI 线程使用 Glide 加载图片,尽量使用 Activity、Fragment 等带有生命周期的组件配合 Glide 使用。

Glide 如何获得 Activity、Fragment 生命周期回调

在 各种 requestManagerRetriever.get() 方法中如果传入的是带有生命周期的组件,并且在 UI 线程,会执行以下几个方法端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// FragmentActivity
assertNotDestroyed(activity);
FragmentManager fm = activity.getSupportFragmentManager();
return supportFragmentGet(activity, fm, null /*parentHint*/);

//android.support.v4.app.Fragment
FragmentManager fm = fragment.getChildFragmentManager();
return supportFragmentGet(fragment.getActivity(), fm, fragment);

//Activity
assertNotDestroyed(activity);
android.app.FragmentManager fm = activity.getFragmentManager();
return fragmentGet(activity, fm, null /*parentHint*/);

//android.app.Fragment
android.app.FragmentManager fm = fragment.getChildFragmentManager();
return fragmentGet(fragment.getActivity(), fm, fragment);
  1. 如果是 Activity ,先获取 FragmentManager ,如果是 Fragment 则先获取 ChildFragmentManager。
  2. 如果是 support 包下面的 Activity 、Fragment 调用 supportFragmentGet,否则调用 fragmentGet。

fragmentGet() 和 supportFragmentGet() 方法大致类似,选取一个分析一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private RequestManager fragmentGet(Context context, android.app.FragmentManager fm,
android.app.Fragment parentHint) {
RequestManagerFragment current = getRequestManagerFragment(fm, parentHint);
RequestManager requestManager = current.getRequestManager();
if (requestManager == null) {
// TODO(b/27524013): Factor out this Glide.get() call.
Glide glide = Glide.get(context);
requestManager =
factory.build(
glide, current.getGlideLifecycle(), current.getRequestManagerTreeNode(), context);
current.setRequestManager(requestManager);
}
return requestManager;
}

上面这段代码做了两个功能

1
2
1. 创建一个 RequestManagerFragment。
2. 创建一个 RequestManager。

先看一下 getRequestManagerFragment

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
RequestManagerFragment getRequestManagerFragment(
final android.app.FragmentManager fm, android.app.Fragment parentHint) {
RequestManagerFragment current = (RequestManagerFragment) fm.findFragmentByTag(FRAGMENT_TAG);
if (current == null) {
current = pendingRequestManagerFragments.get(fm);
if (current == null) {
current = new RequestManagerFragment();
current.setParentFragmentHint(parentHint);
pendingRequestManagerFragments.put(fm, current);
fm.beginTransaction().add(current, FRAGMENT_TAG).commitAllowingStateLoss();
handler.obtainMessage(ID_REMOVE_FRAGMENT_MANAGER, fm).sendToTarget();
}
}
return current;
}

这是是 Glide 设计中比较一个巧妙的地方

1
2
创建一个透明的 RequestManagerFragment 加入到FragmentManager 之中
通过添加的这个 Fragment 感知 Activity 、Fragment 的生命周期。

在 RequestManagerFragment 中可以看到以下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Override
public void onStart() {
super.onStart();
lifecycle.onStart();
}

@Override
public void onStop() {
super.onStop();
lifecycle.onStop();
}

@Override
public void onDestroy() {
super.onDestroy();
lifecycle.onDestroy();
unregisterFragmentWithRoot();
}

可以通过 RequestManagerFragment 把 Activity 的生命周期通过 lifecycle 传递给在 lifecycle 注册的 LifecycleListener。

RequestManager.load(url)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public RequestBuilder<Drawable> load(@Nullable Object model) {
return asDrawable().load(model);
}

public RequestBuilder<Drawable> asDrawable() {
return as(Drawable.class);
}

public <ResourceType> RequestBuilder<ResourceType> as(Class<ResourceType> resourceClass) {
return new RequestBuilder<>(glide, this, resourceClass, context);
}

public RequestBuilder<TranscodeType> load(@Nullable Object model) {
return loadGeneric(model);
}

private RequestBuilder<TranscodeType> loadGeneric(@Nullable Object model) {
this.model = model;
isModelSet = true;
return this;
}

以上就是 RequestBuilder.load(url) 的相关代码,发现并没有什么特殊之处。 只是创建了一个 RequestBuilder 。

RequestBuilder.into(view)

into() 方法调用起来十分方便,只要传递一个 ImageView ,Glide 就会自动下载图片,并且显示到 ImageView 上。这看似十分简单的一步,也是 Glide 最负责的调用。

1
2
3
4
5
6
7
8
9
10
public Target<TranscodeType> into(ImageView view) {

RequestOptions requestOptions = this.requestOptions;
……

return into(
glideContext.buildImageViewTarget(view, transcodeClass),
/*targetListener=*/ null,
requestOptions);
}

跟踪一下 glideContext.buildImageViewTarget(view, transcodeClass) 会发现这里返回的是一个DrawableImageViewTarget
into(ImageView view) 把 requestOptions 和 DrawableImageViewTarget 传入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private <Y extends Target<TranscodeType>> Y into(
@NonNull Y target,
@Nullable RequestListener<TranscodeType> targetListener,
RequestOptions options) {
……
Request request = buildRequest(target, targetListener, options);

Request previous = target.getRequest();
……
requestManager.clear(target);
target.setRequest(request);
requestManager.track(target, request);

return target;
}

下一步跟踪到 requestManager.track(target, request)

1
2
3
4
5
6
7
8
9
10
11
12
13
 void track(Target<?> target, Request request) {
targetTracker.track(target);
requestTracker.runRequest(request);
}

public void runRequest(Request request) {
requests.add(request);
if (!isPaused) {
request.begin();
} else {
pendingRequests.add(request);
}
}

isPaused 变量

1
2
3
4
5
1. 如果此时 GlideRequests 的 Lifecycle 为 ApplicationLifecycle,只要应用存活
isPaused 为 false ,直接执行 request.begin()
2. 如果 GlideRequests 的 Lifecycle 是观测 Fragment 或者 Activity
isPaused 为true ,不会立即执行 request.begin()
当 Fragment 或者 Activity 显示到前台时通过遍历 requests 数组执行 request.begin()

所以执行网络请求下载图片的操作在 request.begin() 之中。
回到 into(ImageView view) 方法的

1
Request request = buildRequest(target, targetListener, options)

经过层层包裹我们可以找到一下路线,发现最后返回的是 SingleRequest

1
2
3
buildRequest >> buildRequestRecursive >> buildThumbnailRequestRecursive

>> obtainRequest >> SingleRequest

创建 SingleRequest 的过程比较复杂,牵扯到缩略图、错误处理之类的逻辑,大致都是上面那条路径。

然后就看一下 SingleRequest.begin()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Override
public void begin() {
……
省略一些其他分支
……
status = Status.WAITING_FOR_SIZE;
if (Util.isValidDimensions(overrideWidth, overrideHeight)) {
onSizeReady(overrideWidth, overrideHeight);
} else {
target.getSize(this);
}
……
……
}

begin() 方法很长,我们剔除了一些异常处理,直接看最核心的方法。

如果我给 Glide 设置了 override() 则直接调用 onSizeReady(overrideWidth, overrideHeight)

否则会调用 target.getSize(this) 让 ImageView 计算自己的尺寸。

1
glideContext.buildImageViewTarget(view, transcodeClass) 创建一个 DrawableImageViewTarget

Glide流程

Glide网络下载流程

总结

  • 缓存
    • 活动资源 (Active Resources)
    • 内存缓存 (Memory Cache)
    • 资源类型(Resource Disk Cache)
    • 原始数据 (Data Disk Cache)
    • 网络缓存

问:

阅读全文 »

https

SSL 的握手协议非常有效的让客户和服务器之间完成相互之间的身份认证,其主要过程如下:

1.客户端向服务器请求HTTPS连接:
客户端向服务器传送客户端 SSL 协议的版本号,加密算法的种类,
产生的随机数,以及其他服务器和客户端之间通讯所需要的各种信息。

2.服务器确认并返回证书。
服务器向客户端传送 SSL 协议的版本号,加密算法的种类,
随机数以及其他相关信息,同时服务器还将向客户端传送自己的证书。

3.客户端验证服务器发来的证书。
客户端利用服务器传过来的信息验证服务器的合法性,服务器的合法性包括:
证书是否过期,
发行服务器证书的 CA 是否可靠(CA 认证机构颁发证书),
发行者证书的公钥能否正确解开服务器证书的“发行者的数字签名”,
服务器证书上的域名是否和服务器的实际域名相匹配。
如果合法性验证没有通过,通讯将断开;
如果合法性验证通过,将继续进行第4步。
// 关于验证 CA 证书 及可能的中间人攻击:
[
https://www.jianshu.com/p/76540830537f
]

4.信息验证通过,客户端生成随机密钥A,用公钥加密后发给服务器。
从第3步验证过的证书里面可以拿到服务器的公钥,客户端生成的随机密钥就使用这个公钥来加密.
加密之后,只有拥有该服务器(持有私钥)才能解密出来,保证安全。

5.服务器用私钥解密出随机密钥A,以后通信就用这个随机密钥A来对通信进行加密。
至此, 客户端和服务端的握手完成, 即可以开始进行加密传输了。

#须知:
这个握手过程并没有将验证客户端身份的逻辑加进去。
因为在大多数的情况下,HTTPS只是验证服务器的身份而已。
如果要验证客户端的身份,需要客户端拥有证书,在握手时发送证书,而这个证书是需要成本的。

前言

Flutter已经面世这么长时间了,不抽空学习一下,实在是对不起自己。也是由于前天面试被问到Flutter的一些东西,一脸懵逼,真不是一个合格的iOS开发猿!!!

分享一下学习经验和心得,纪录一下学习过程的疑问。

官网学习,手敲练习demo源码

学习之前我有几个问题问自己

  • 为什么大家都说Flutter比RN和Weex流畅?
  • 和RN、Weex实现原理对比, Flutter的实现原理是什么?
  • 怎么和Native通讯?
阅读全文 »

LLDB官方文档地址

LLDB调试器

LLVM源代码树,并在lldb 子目录中找到源代码:

git clone https://github.com/llvm/llvm-project.git
请注意,LLDB通常使用CMake和Ninja从top-of-trunk构建。另外它构建:

  • 在macOS上生成Xcode项目
  • 在Linux和FreeBSD上使用clang和libstdc ++ / libc ++
  • 在NetBSD上使用GCC / clang和libstdc ++ / libc ++
  • 在Windows上生成VS 2017或更高版本的项目

先说大话:

  • 构建库以包含在IDE,命令行工具和其他分析工具中
  • 高性能和高效的内存使用
  • 可扩展:Python可编写脚本并使用插件架构
  • 在有意义的地方重用现有的编译器技术
  • 出色的多线程调试支持
  • 对C,Objective-C和C ++的大力支持
  • 可重定向以支持多个平台
  • 为调试器研究和其他创新提供基础

LDB支持各种基本调试功能,如读取DWARF,支持步骤,下一步,完成,回溯等。一些更感兴趣的位是:

  • 用于可移植性和可扩展性的插件架构:
    • 可执行文件格式的目标文件解析器。目前支持包括Mach-O(32位和64位)和ELF(32位)。
    • 对象容器解析器,用于提取文件中包含的对象文件。支持目前包括通用Mach-O文件和BSD档案。
    • 调试符号文件解析器以从对象文件中逐步提取调试信息。支持目前包括DWARF和Mach-O符号表。
    • 符号供应商插件从可执行对象的各种不同源收集数据。
    • 每个体系结构的反汇编插件。支持目前包括用于i386,x86-64,ARM / Thumb和PPC64le的LLVM反汇编程序
    • 调试器插件实现调试所需的主机和目标特定功能。
  • SWIG生成的脚本桥接允许Python访问和控制调试器库的公共API。
  • 远程协议服务器debugserver在i386和x86-64上实现macOS调试。
  • 命令行调试器 - lldb可执行文件本身。
  • 库的框架API。

编译器集成的好处

LLDB目前将调试信息转换为clang类型,以便它可以利用clang编译器基础结构。这允许LLDB在表达式中支持最新的C,C ++,Objective-C和Objective-C ++语言特性和运行时,而无需重新实现任何此功能。它还利用编译器在对表达式进行函数调用时,在反汇编指令和提取指令细节等时处理所有ABI细节。

主要好处包括:

  • 最新的C,C ++,Objective-C语言支持
  • 可以声明局部变量和类型的多行表达式
  • 支持时使用JIT表达式
  • 当不能使用JIT时,评估表达式中间表示(IR)

使用

命令结构

<noun> <verb> [-options [option-value]] [argument [argument...]]

选项可以放在命令行的任何位置,但是如果参数以“ - ”开头,那么你必须告诉lldb你已经完成了当前命令的选项,方法是添加一个选项终止:“ - ”所以例如如果你想启动一个进程并给“进程启动”命令“-stop-at-entry”选项,但是你想要启动你要启动的进程并使用参数“-program_arg value”,你会输入:

  • (lldb) process launch –stop-at-entry – -program_arg value

  • 要在LLDB中设置相同的文件和换行符,您可以输入以下任一项:
    (lldb) breakpoint set –file foo.c –line 12
    (lldb) breakpoint set -f foo.c -l 12

  • 您可以多次使用-name选项在一组函数上创建断点。
    (lldb) breakpoint set –name foo
    (lldb) breakpoint set -n foo
    (lldb) breakpoint set –name foo –name bar

  • 要在名为foo的所有C ++方法上设置断点,您可以输入以下任一项:
    (lldb) breakpoint set –method foo
    (lldb) breakpoint set -M foo

  • 设置一个名为alignLeftEdges的断点Objective-C选择器:
    (lldb) breakpoint set –selector alignLeftEdges:
    (lldb) breakpoint set -S alignLeftEdges:

  • 您可以使用“-shlib <path>”(简称“-s <path>”)将任何断点限制为特定的可执行映像:
    (lldb) breakpoint set –shlib foo.dylib –name foo
    (lldb) breakpoint set -s foo.dylib -n foo

  • lldb命令解释器对命令名进行最短的唯一字符串匹配,因此以下两个命令都将执行相同的命令:
    (lldb) breakpoint set -n “-[SKTGraphicView alignLeftEdges:]”
    (lldb) br s -n “-[SKTGraphicView alignLeftEdges:]”

设置观察点

除断点外,您还可以使用help watchpoint查看监视点操作的所有命令。例如,我们可能会执行以下操作来查看名为“global”的变量进行写入操作,但只有在条件“(global == 5)”为真时才会停止:

(lldb) watch set var global
(lldb) watch modify -c ‘(global==5)’
(lldb) watch list
(lldb) about to write to ‘global’…

启动或附加到您的程序

  • 要在lldb中启动程序,我们使用“process launch”命令或其内置别名之一:
    (lldb) process launch
    (lldb) run
    (lldb) r

  • 您还可以按进程ID或进程名称附加到进程。当按名称附加到进程时,lldb还支持“-waitfor”选项,该选项等待显示该名称的下一个进程,并附加到该进程
    (lldb) process attach –pid 123
    (lldb) process attach –name Sketch
    (lldb) process attach –name Sketch –waitfor

  • 启动或附加到进程后,您的进程可能会在某处停止:

    1
    2
    3
    4
    5
    (lldb) process attach -p 12345
    Process 46915 Attaching
    Process 46915 Stopped
    1 of 3 threads stopped with reasons:
    * thread #1: tid = 0x2c03, 0x00007fff85cac76a, where = libSystem.B.dylib`__getdirentries64 + 10, stop reason = signal = SIGSTOP, queue = com.apple.main-thread

控制你的程序

  • 启动后,我们可以继续,直到我们达到断点。进程控制的原始命令都存在于“thread”命令下:
    (lldb) thread continue
    Resuming thread 0x2c03 in process 46915
    Resuming process 46915
    (lldb)

  • 步进命令
    (lldb) thread step-in // The same as gdb’s “step” or “s”
    (lldb) thread step-over // The same as gdb’s “next” or “n”
    (lldb) thread step-out // The same as gdb’s “finish” or “f”

  • 逐步指令版本:
    (lldb) thread step-inst // The same as gdb’s “stepi” / “si”
    (lldb) thread step-over-inst // The same as gdb’s “nexti” / “ni”

  • 最后,lldb运行直到行或帧退出步进模式:
    (lldb) thread until 100

检查堆栈帧状态

  • 检查框架参数和局部变量的最方便方法是使用“frame variable”命令:
    (lldb) frame variable

不想写了,用到的时候,写一些用到的东西吧,学习的话,大家去看官方文档吧!!!

iOS App程序崩溃的抓取与分析

备用: xun开源代码地址

前言

  1. cpu无法执行,除以0,无权限内存地址(pagezero-4g)无效
    • pagezero,拦截空指针的访问。 隔绝32位操作系统
  2. 被系统强杀
    • oom
    • ANR
    • 资源异常
    • 死锁
    • 非法应用签名
    • 后台超时
    • 内存紧张
    • 设备过热
  3. oc异常
    • 数组越界
    • C++异常
    • 断言
  4. 中断
    • 外部中断(IO)
    • 异常中断
    • 系统调用

具体处理

  1. Mach异常捕获方式
    • mach_port_allocate 创建一场端口
    • mach_port_insert_right 申请 set_exception_ports 的权限
    • xxx_set_exception_ports 设置异常端口
    • 循环等待异常消息
  2. Unix 信号方式 signal(SIGSEGV, signalHandler)
    • 除了OC层面的异常捕捉之外,很多内存错误、访问错误的地址产生的crash则需要利用unix标准的signal机制,注册SIGABRT, SIGBUS, SIGSEGV等信号发生时的处理函数。该函数中我们可以输出栈信息,版本信息等其他一切我们所想要的。
  3. Mach异常 + Unix信号方式
    • 某个NSException导致程序Crash的,只有拿到这个NSException,获取它的reason,name,callStackSymbols信息才能确定出问题的程序位置。
    • 方法很简单,可通过注册NSUncaughtExceptionHandler捕获异常信息
      1
      2
      3
      4
      5
      static void my_uncaught_exception_handler (NSException *exception) {
      //这里可以取到 NSException 信息
      }
      NSSetUncaughtExceptionHandler(&my_uncaught_exception_handler);

多个 Crash 日志收集服务共存

  • 拒绝传递 UncaughtExceptionHandler
  • 开发测试阶段,可以利用 fishhook 框架去hookNSSetUncaughtExceptionHandler方法,这样就可以清晰的看到handler的传递流程断在哪里,快速定位污染环境者。
  • Mach异常端口换出+信号处理Handler覆盖
  • 影响系统崩溃日志准确性
    • 若程序因NSException而Crash,系统日志中的Last Exception Backtrace信息是完整准确的,不会受应用层的胡来而影响,可作为排查问题的参考线索。

概述

Mach: 微内核,负责操作系统中基本职责:进程和线程抽象、虚拟内存管理、任务调度、进程间通信和消息传递机制。

大家熟知的NSSetUncaughtExceptionHandler() + signal() / sigaction()的方式收集Crash,但是stack overflow并不能被此方法扑捉到;

通常我们所说的异常,一般是有处理器陷阱引发的,通用的Mach异常处理程序exception_triage(),负责将异常转换成Mach 消息。exception_triage()通过调用exception_deliver()尝试把异常投递到thread、task最后是host。首先尝试将异常抛给thread端口,然后尝试抛给task端口,最后再抛给host端口(默认端口),如果没有一个端口返回KERN_SUCCESS,那么任务就会被终止。

代码太多,不再贴了,自行下载源码,找到 osfmk/kern/exception.c 文件。 里面有exception_triage()和exception_deliver()函数。关于Exception的相关定义,可以在osfmk/mach/exception_types.h里面查看。下面只贴伪代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62

// 位于 osfmk/kern/exception.c
kern_return_t
xception_triage(
exception_type_t exception,
mach_exception_data_t code,
mach_msg_type_number_t codeCnt
)
{
thread_t thread;
task_t task;
host_priv_t host_priv;
lck_mtx_t *mutex;
kern_return_t kr = KERN_FAILURE;
assert(exception != EXC_RPC_ALERT);
if (panic_on_exception_triage) {
panic("called exception_triage when it was forbidden by the boot environment");
}
thread = current_thread();

// ================ 分别尝试把异常投递到thread、task最后是host。================
/*
* Try to raise the exception at the activation level.
*/
mutex = &thread->mutex;
if (KERN_SUCCESS == check_exc_receiver_dependency(exception, thread->exc_actions, mutex))
{
kr = exception_deliver(thread, exception, code, codeCnt, thread->exc_actions, mutex);
if (kr == KERN_SUCCESS || kr == MACH_RCV_PORT_DIED)
goto out;
}
/*
* Maybe the task level will handle it.
*/
task = current_task();
mutex = &task->lock;
if (KERN_SUCCESS == check_exc_receiver_dependency(exception, task->exc_actions, mutex))
{
kr = exception_deliver(thread, exception, code, codeCnt, task->exc_actions, mutex);
if (kr == KERN_SUCCESS || kr == MACH_RCV_PORT_DIED)
goto out;
}

/*
* How about at the host level?
*/
host_priv = host_priv_self();
mutex = &host_priv->lock;

if (KERN_SUCCESS == check_exc_receiver_dependency(exception, host_priv->exc_actions, mutex))
{
kr = exception_deliver(thread, exception, code, codeCnt, host_priv->exc_actions, mutex);
if (kr == KERN_SUCCESS || kr == MACH_RCV_PORT_DIED)
goto out;
}

out:
if ((exception != EXC_CRASH) && (exception != EXC_RESOURCE) &&
(exception != EXC_GUARD) && (exception != EXC_CORPSE_NOTIFY))
thread_exception_return();
return kr;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111

// 位于 osfmk/kern/exception.c
kern_return_t
exception_deliver(
thread_t thread,
exception_type_t exception,
mach_exception_data_t code,
mach_msg_type_number_t codeCnt,
struct exception_action *excp,
lck_mtx_t *mutex)
{
... // 省略部分代码

switch (behavior) {
case EXCEPTION_STATE: {
mach_msg_type_number_t state_cnt;
thread_state_data_t state;

c_thr_exc_raise_state++;
state_cnt = _MachineStateCount[flavor];
kr = thread_getstatus(thread, flavor,
(thread_state_t)state,
&state_cnt);
if (kr == KERN_SUCCESS) {
if (code64) {
kr = mach_exception_raise_state(exc_port,
exception,
code,
codeCnt,
&flavor,
state, state_cnt,
state, &state_cnt);
} else {
kr = exception_raise_state(exc_port, exception,
small_code,
codeCnt,
&flavor,
state, state_cnt,
state, &state_cnt);
}
if (kr == MACH_MSG_SUCCESS && exception != EXC_CORPSE_NOTIFY)
kr = thread_setstatus(thread, flavor,
(thread_state_t)state,
state_cnt);
}
return kr;
}

case EXCEPTION_DEFAULT:
c_thr_exc_raise++;
if (code64) {
kr = mach_exception_raise(exc_port,
retrieve_thread_self_fast(thread),
retrieve_task_self_fast(thread->task),
exception,
code,
codeCnt);
} else {
kr = exception_raise(exc_port,
retrieve_thread_self_fast(thread),
retrieve_task_self_fast(thread->task),
exception,
small_code,
codeCnt);
}
return kr;

case EXCEPTION_STATE_IDENTITY: {
mach_msg_type_number_t state_cnt;
thread_state_data_t state;

c_thr_exc_raise_state_id++;
state_cnt = _MachineStateCount[flavor];
kr = thread_getstatus(thread, flavor,
(thread_state_t)state,
&state_cnt);
if (kr == KERN_SUCCESS) {
if (code64) {
kr = mach_exception_raise_state_identity(exc_port,
retrieve_thread_self_fast(thread),
retrieve_task_self_fast(thread->task),
exception,
code,
codeCnt,
&flavor,
state, state_cnt,
state, &state_cnt);
} else {
kr = exception_raise_state_identity(exc_port,
retrieve_thread_self_fast(thread),
retrieve_task_self_fast(thread->task),
exception,
small_code,
codeCnt,
&flavor,
state, state_cnt,
state, &state_cnt);
}
if (kr == MACH_MSG_SUCCESS && exception != EXC_CORPSE_NOTIFY)
kr = thread_setstatus(thread, flavor,
(thread_state_t)state,
state_cnt);
}
return kr;
}

default:
panic ("bad exception behavior!");
return KERN_FAILURE;
}/* switch */
}

当第一个BSD进程调用bsdinit_task()函数(源码位于bsd/kern/bsd_init.c)启动时,这函数还调用了ux_handler_init()函数(位于bsd/uxkern/ux_exception.c)设置了一个Mach内核线程跑ux_handler()的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

// 位于bsd/kern/bsd_init.c
/* Called with kernel funnel held */
void
bsdinit_task(void)
{
// ...省略

ux_handler_init(); // 初始化handler

// 设置port
thread = current_thread();
(void) host_set_exception_ports(host_priv_self(),
EXC_MASK_ALL & ~(EXC_MASK_RPC_ALERT),//pilotfish (shark) needs this port
(mach_port_t) ux_exception_port,
EXCEPTION_DEFAULT| MACH_EXCEPTION_CODES,
0);

ut = (uthread_t)get_bsdthread_info(thread);

bsd_init_task = get_threadtask(thread);
init_task_failure_data[0] = 0;

#if CONFIG_MACF
mac_cred_label_associate_user(p->p_ucred);
mac_task_label_update_cred (p->p_ucred, (struct task *) p->task);
#endif
load_init_program(p);
lock_trace = 1;
}

每一个thread、task及host自身都有一个异常端口数组,通过调用xxx_set_exception_ports()(xxx为thread、task或host)可以设置这些异常端口。 xxx_set_exception_ports()第四个参数为exception_behavior_t behavior,这将会使用到与行为相匹配的实现(exc.defs 或 mach_exc.defs)。
各种行为都在host层被catch_[mach]_exception_xxx处理,64位的对应的是有mach函数(可在/bsd/uxkern/ux_exception.c查看)。
这些函数通过调用ux_exception()将异常转换为对应的UNIX信号,并通过threadsignal()将信号投递到出错线程。

所以,如果异常是栈溢出,那么signal是SIGSEGV而不是SIGBUS;如果进程退出了或者线程/进程未准备好处理signal,所注册的signal()是无法接收信号的。

把Mach exception 和 UNIX signal 的转换制表后,如下

exception type signal
EXC_BAD_ACCESS 1、SIGSEGV (KERN_INVALID_ADDRESS)
2、SIGBUS(其它)
EXC_BAD_INSTRUCTION SIGILL
EXC_ARITHMETIC SIGFPE
EXC_EMULATION SIGEMT
EXC_SOFTWARE 1、SIGSYS(EXC_UNIX_BAD_SYSCALL)
2、SIGPIPE(EXC_UNIX_BAD_PIPE)
3、SIGABRT(EXC_UNIX_ABORT)
4、SIGKILL(EXC_SOFT_SIGNAL)
EXC_BREAKPOINT SIGTRAP ()

实战

在Mach中,异常是通过内核中的主要设施-消息传递机制-进行处理的。一个异常与一条消息并无差别,由出错的线程或任务(通过 msg_send())发送,并通过一个处理程(通过 msg_recv())接收。
由于Mach的异常以消息机制处理而不是通过函数调用,exception messages可以被转发到先前注册的Mach exception处理程序。这意味着你可以插入一个exception处理程序,而不干扰现有的无论是调试器或Apple’s crash reporter。可以使用mach_msg() // flag MACH_SEND_MSG发送原始消息到以前注册的处理程序的Mach端口,将消息转发到一个现有的处理程序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
void catchMACHExceptions() {

kern_return_t rc = 0;
exception_mask_t excMask = EXC_MASK_BAD_ACCESS |
EXC_MASK_BAD_INSTRUCTION |
EXC_MASK_ARITHMETIC |
EXC_MASK_SOFTWARE |
EXC_MASK_BREAKPOINT;

rc = mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &myExceptionPort);
if (rc != KERN_SUCCESS) {
fprintf(stderr, "------->Fail to allocate exception port\\\\\\\\n");
return;
}

rc = mach_port_insert_right(mach_task_self(), myExceptionPort, myExceptionPort, MACH_MSG_TYPE_MAKE_SEND);
if (rc != KERN_SUCCESS) {
fprintf(stderr, "-------->Fail to insert right");
return;
}

rc = thread_set_exception_ports(mach_thread_self(), excMask, myExceptionPort, EXCEPTION_DEFAULT, MACHINE_THREAD_STATE);
if (rc != KERN_SUCCESS) {
fprintf(stderr, "-------->Fail to set exception\\\\\\\\n");
return;
}

// at the end of catchMachExceptions, spawn the exception handling thread
pthread_t thread;
pthread_create(&thread, NULL, exc_handler, NULL);
} // end catchMACHExceptions

static void *exc_handler(void *ignored) {
// Exception handler – runs a message loop. Refactored into a standalone function
// so as to allow easy insertion into a thread (can be in same program or different)
mach_msg_return_t rc;
fprintf(stderr, "Exc handler listening\\\\\\\\n");
// The exception message, straight from mach/exc.defs (following MIG processing) // copied here for ease of reference.
typedef struct {
mach_msg_header_t Head;
/* start of the kernel processed data */
mach_msg_body_t msgh_body;
mach_msg_port_descriptor_t thread;
mach_msg_port_descriptor_t task;
/* end of the kernel processed data */
NDR_record_t NDR;
exception_type_t exception;
mach_msg_type_number_t codeCnt;
integer_t code[2];
int flavor;
mach_msg_type_number_t old_stateCnt;
natural_t old_state[144];
} Request;

Request exc;

struct rep_msg {
mach_msg_header_t Head;
NDR_record_t NDR;
kern_return_t RetCode;
} rep_msg;


for(;;) {
// Message Loop: Block indefinitely until we get a message, which has to be
// 这里会阻塞,直到接收到exception message,或者线程被中断。
// an exception message (nothing else arrives on an exception port)
rc = mach_msg( &exc.Head,
MACH_RCV_MSG|MACH_RCV_LARGE,
0,
sizeof(Request),
myExceptionPort, // Remember this was global – that's why.
MACH_MSG_TIMEOUT_NONE,
MACH_PORT_NULL);

if(rc != MACH_MSG_SUCCESS) {
/*... */
break ;
};

// Normally we would call exc_server or other. In this example, however, we wish
// to demonstrate the message contents:

printf("Got message %d. Exception : %d Flavor: %d. Code %lld/%lld. State count is %d\\\\\\\\n" ,
exc.Head.msgh_id, exc.exception, exc.flavor,
exc.code[0], exc.code[1], // can also print as 64-bit quantity
exc.old_stateCnt);

rep_msg.Head = exc.Head;
rep_msg.NDR = exc.NDR;
rep_msg.RetCode = KERN_FAILURE;

kern_return_t result;
if (rc == MACH_MSG_SUCCESS) {
result = mach_msg(&rep_msg.Head,
MACH_SEND_MSG,
sizeof (rep_msg),
0,
MACH_PORT_NULL,
MACH_MSG_TIMEOUT_NONE,
MACH_PORT_NULL);
}
}

return NULL;

} // end exc_handler

接下来,我们测试一下。

1
2
3
4
- (void)test
{
[self test];
}

结果如下:

1
2
3
Exc handler listening
Got message 2401. Exception : 1 Flavor: 0. Code 2/1486065656. State count is 8
(lldb)

我们可以查看mach/exception_types.h 对exception type的定义

Exception: 1, 即为EXC_BAD_ACCESS
code: 2, 即KERN_PROTECTION_FAILURE

而ux_exception() 函数告诉我们Code与signal是怎么转换的。
也就是 Exception = EXC_BAD_ACCESS, code = 2 对应的是SIGBUS信号,又因为为是stack overflow,信号应该是SIGSEGV。
那么结这个exception就是:

Exception Type: EXC_BAD_ACCESS (SIGSEGV)
Exception Subtype: KERN_PROTECTION_FAILURE

再试试其他的:

1
2
int *pi = (int*)0x00001111;
*pi = 17;

Exc handler listening
Got message 2401. Exception : 1 Flavor: 0. Code 1/4369. State count is 8(lldb)

Exception Type: EXC_BAD_ACCESS (SIGSEGV)
Exception Subtype: KERN_INVALID_ADDRESS

最后

除了上面硬件产生的信号,另外还有软件产生的信号。软件产生的信号来自kill()、pthread_kill()两个函数的调用,大概过程是这样的:kill()/pthread_kill() –> … –> psignal_internal() –> act_set_astbsd()。最终也会调用act_set_astbsd()发送信号到目标线程。这意味着Mach exception流程是不会走的。
另外,在abort()源码注释着:<rdar://problem/7397932> abort() should call pthread_kill to deliver a signal to the aborting thread , 它是这样调用的(void)pthread_kill(pthread_self(), SIGABRT);。Apple’s Crash Reporter 把SIGABRT的Mach exception type记为EXC_CRASH,不同与上面转变表。

Exception Type: EXC_CRASH (SIGABRT)
Exception Codes: 0x0000000000000000, 0x0000000000000000

所以尽管Mach exception handle 比 UNIX signal handle 更有优势,但我们还是须要注册signal handle用于处理EXC_SOFTWARE/EXC_CRASH。

阅读全文 »

Mach

OS X内核的基本服务和基本类型都基于Mach 3.0。苹果对Mach进行了修改和扩展,以更好地满足OS X的功能和性能目标。

Mach 3.0最初被认为是一个简单、可扩展的通信微内核。它能够作为一个独立的内核运行,而其他传统的操作系统服务(如I/O、文件系统和网络堆栈)作为用户模式服务器运行。

然而,在OS X中,Mach与其他内核组件链接到一个内核地址空间中。这主要是为了性能;在链接组件之间进行直接调用要比在单独任务之间发送消息执行远程过程调用(RPC)快得多。这种模块化的结构导致了一个比单一内核所允许的更健壮和可扩展的系统,而没有纯微内核的性能损失。

因此,在OS X中,Mach主要不是客户机和服务器之间的通信中心。相反,它的价值由抽象、可扩展性和灵活性组成。特别地,Mach提供了

  • 基于对象的api,使用通信通道(例如端口)作为对象引用
  • 高度并行执行,包括预先调度的线程和对SMP的支持
  • 一个灵活的调度框架,支持实时使用
  • 一组完整的IPC原语,包括消息传递、RPC、同步和通知
  • 支持大型虚拟地址空间、共享内存区域和由持久性存储支持的内存对象
  • 经过验证的可扩展性和可移植性,例如跨指令集体系结构和分布式环境
  • 安全与资源管理作为设计的基本原则;所有资源都是虚拟化的

Mach内核抽象化

Mach提供了一组被设计成既简单又强大的抽象。这些是主要的内核抽象:

  • 任务(Tasks)。资源所有权单位;每个任务由一个虚拟地址空间、一个端口名称空间和一个或多个线程组成。(类似于流程。)
  • 线程(Threads)。任务中CPU执行的单位。
  • 地址空间(Address space)。与内存管理器一起,Mach实现了稀疏虚拟地址空间和共享内存的概念。
  • 内存对象(Memory objects)。内存管理的内部单元。内存对象包括命名项和区域;它们表示可能映射到地址空间的持久数据。
  • 端口(Ports)。安全、简单的通信通道,只能通过发送和接收功能(称为端口权限)访问。
  • IPC。消息队列、远程过程调用、通知、信号量和锁集。
  • 时间(Time)。时钟、定时器和等待。

在陷阱级别,大多数Mach抽象的接口由表示这些对象的内核端口之间消息组成。陷阱(trap)级别的接口(例如mach_msg_overwrite_trap)和消息格式在正常使用中由Mach接口生成器(MIG)抽象出来。MIG用于编译基于消息的api的过程接口,基于这些api的描述。(MIG参看下面总结部分)

任务和线程

OS X进程和POSIX线程(pthreads)分别在Mach任务和线程之上实现。线程是任务中的控制流点。存在一个任务来为其包含的线程提供资源。这种分割是为了提供并行性和资源共享。

一个线程

  • 是任务中的控制流点。
  • 可以访问包含任务的所有元素。
  • 与其他线程(可能)并行执行,甚至是同一任务中的线程。
  • 具有最小的状态信息,以降低开销。

一个任务

  • 是系统资源的集合。这些资源(地址空间除外)由端口引用。如果对端口被分配了权限,那么这些资源可以与其他任务共享。
  • 提供一个大的、可能稀疏的地址空间,由虚拟地址引用。这个空间的一部分可以通过继承或外部内存管理共享。
  • 包含一些线程。

请注意,任务没有自己的线程执行指令的生命周期。当说“task Y做X”时,真正的意思是“task Y中包含的线程做X”。

  1. 任务是一个相当昂贵的实体。它的存在是资源的集合。任务中的所有线程都共享所有内容。如果没有显式的操作(尽管操作通常很简单),两个任务就不能共享任何内容,而且一些资源(例如端口接收权限)根本不能在两个任务之间共享。

  2. 线程是一个相当轻量级的实体。它的创建成本相当低,操作开销也很低。这是真的,因为一个线程只有很少的状态信息(主要是它的寄存器状态)。它所拥有的任务承担着资源管理的重担。在多处理器计算机上,任务中的多个线程可以并行执行。即使并行性不是目标,多线程也有一个优势,即每个线程都可以使用同步编程风格,而不是试图使用单个线程进行异步编程来提供多个服务。

  3. 线程是基本的计算实体。一个线程只属于一个任务,这个任务定义了它的虚拟地址空间。影响地址空间的结构或引用任何资源以外的地址空间,线程必须执行一个特殊的陷阱指令 引起内核代表线程执行操作或发送消息代理代表线程。通常,这些陷阱操作与包含线程的任务相关的资源。内核可以发出请求来操作这些实体:创建它们、删除它们并影响它们的状态。

  4. Mach为线程调度策略提供了一个灵活的框架。OS X的早期版本同时支持分时和固定优先级策略。提高和降低分时线程的优先级,以平衡它与其他分时线程之间的资源消耗。

  5. 固定优先级的线程执行一定的时间量,然后放在具有相同优先级的线程队列的末尾。将固定优先级线程的量子级别设置为无穷大,可以让线程一直运行,直到阻塞,或者直到被优先级更高的线程抢占为止。高优先级实时线程通常是固定优先级的。

  6. OS X还为实时性能提供了时间约束调度。这个调度允许您指定线程必须在一定时间内获得一定的时间量。

Mach调度在Mach调度和线程接口中有进一步的描述。

端口、端口权限、端口集和端口名称空间

除了任务的虚拟地址空间之外,所有其他Mach资源都是通过称为端口的间接级别访问的。端口是请求服务的客户机和提供服务的服务器之间单向通信通道的端点。如果要向此类服务请求提供应答,则必须使用第二个端口。这类似于UNIX中的(单向)管道。

在大多数情况下,由端口访问的资源(即由端口命名的资源)被称为对象。大多数由端口命名的对象都有一个接收方和(可能的)多个发送方。也就是说,对于典型对象(如消息队列),只有一个接收端口,至少有一个发送端口。

对象提供的服务由接收发送到对象的请求的管理器决定。因此,内核是与内核提供的对象关联的端口的接收方,而与任务提供的对象关联的端口的接收方是提供这些对象的任务。

对于命名任务提供的对象的端口,可以将该端口的请求接收方更改为不同的任务,例如通过在消息中将该端口传递给该任务。一个任务可能有多个引用其支持的资源的端口。就此而言,任何给定的实体都可以有多个表示它的端口,每个端口表示不同的允许操作集。例如,许多对象都有一个名称端口和一个控制端口(有时称为特权端口)。对控制端口的访问允许操作对象;对name端口的访问简单地为对象命名,这样您就可以获得关于它的信息,或者对它执行其他非特权操作。

任务具有以特定方式访问端口的权限(发送、接收、发送一次);这些被称为port rights。端口只能通过右值访问。端口通常用于授予客户机对Mach内对象的访问权。有权发送到对象的IPC端口表示有权以规定的方式操作对象。因此,端口所有权是Mach内部的基本安全机制拥有访问对象的权利就是拥有访问或操作该对象的能力

端口权限可以通过IPC在任务之间复制和移动。这样做实际上是将功能传递给某个对象或服务器。

一种类型的对象引用一个端口是一个端口组。顾名思义,端口设置一组端口的权利时,可以当作一个单独的单元接收消息或事件的任何成员集。港口集允许一个线程等待的消息和事件源,例如在工作循环。

传统上,在Mach中,由端口表示的通信通道总是消息队列。然而,OS X支持其他类型的通信通道,这些新的IPC对象类型也由端口和端口权限表示。有关消息和其他IPC类型的详细信息,请参阅下面进程间通信(Interprocess Communication, IPC)一节。

端口和端口权限没有允许直接操作任意端口或权限的系统范围名称。只有当任务在其端口名称空间中具有端口权时,才可以由任务操作端口。端口权由端口名称指定,一个整数索引进入一个32位端口名称空间。每个任务都与它关联一个端口名称空间。

当另一个任务显式地将它们插入它的名称空间时,当它们在消息中接收到权限时,任务通过创建返回对象权限的对象获得端口权限,并通过Mach调用特定的特殊端口(mach_thread_self、mach_task_self和mach_reply_port)获得端口权限。

内存管理

与大多数现代操作系统一样,Mach提供对大型、稀疏的虚拟地址空间的寻址。运行时访问是通过虚拟地址进行的,这些虚拟地址可能与初始访问时物理内存中的位置不对应。Mach负责获取一个请求的虚拟地址,并在物理内存中为它分配一个相应的位置。它通过请求分页来实现这一点。

当内存对象映射到虚拟地址空间的范围时,将用数据填充虚拟地址空间的范围。地址空间中的所有数据最终都是通过内存对象提供的。Mach在物理内存中建立页面时,向内存对象(分页器)的所有者询问页面的内容,并在回收页面之前将可能修改过的数据返回给分页器。OS X包含两个内置分页器——默认分页器和vnode分页器(default pager and the vnode pager)。

默认的分页器处理非持久性内存,称为匿名内存。匿名内存是零初始化的,并且只在任务执行期间存在。vnode分页器将文件映射到内存对象。Mach向内存对象导出一个接口,允许用户模式任务提供内存对象的内容。这个接口称为外部内存管理接口(EMMI)。

内存管理子系统导出称为命名项或命名内存项的虚拟内存句柄。与大多数内核资源一样,这些资源由端口表示。拥有一个命名的内存条目句柄,允许所有者映射底层虚拟内存对象,或者将映射底层对象的权利传递给其他人。在两个不同的任务中映射命名项会在两个任务之间生成共享内存窗口,从而为建立共享内存提供了一种灵活的方法。

从OS X v10.1开始,EMMI系统得到了增强,以支持“无端口”EMMI。在传统的EMMI中,为每个内存区域创建两个Mach端口,同样也为每个缓存的vnode创建两个端口。在最初的实现中,无端口的EMMI用直接内存引用(基本上是指针)替换了这一点。在将来的版本中,端口将用于与内核外部的分页器通信,同时使用直接引用与驻留在内核空间中的分页器通信。这些更改的最终结果是,早期版本的无端口EMMI不支持运行在内核空间之外的分页器。这种支持有望在未来的版本中恢复。

虚拟内存空间的地址范围也可以通过直接分配(使用vm_allocation)来填充。底层虚拟内存对象是匿名的,并由默认分页程序支持。地址空间的共享范围也可以通过继承设置。创建新任务时,将从父任务中克隆它们。这种克隆也属于底层内存地址空间。根据与映射相关的属性,对象的映射部分可以作为副本继承,也可以作为共享,或者根本不可以。Mach采用一种称为“写中复制”的延迟复制形式,以优化任务创建时继承副本的性能。

不是直接复制范围,而是通过受保护的共享来实现写时复制优化。这两个任务共享要复制的内存,但具有只读访问。当任何一个任务试图修改范围的一部分时,该部分将被复制。这种对内存副本的延迟计算是一种重要的优化,它允许在几个方面进行简化,尤其是消息传递api。

Mach还提供了另一种形式的共享,通过导出指定的区域。命名区域是命名条目的一种形式,但是它不是由虚拟内存对象支持的,而是由虚拟映射片段支持的。这个片段可能包含到许多虚拟内存对象的映射。它可以映射到其他虚拟映射,提供了一种方法,不仅可以继承一组虚拟内存对象,还可以继承它们现有的映射关系。该特性在任务设置中提供了重要的优化,例如在共享用于共享库的地址空间的复杂区域时。

进程间通信(IPC)

任务之间的通信是Mach哲学的一个重要元素。Mach支持客户机/服务器系统结构,其中任务(客户机)通过通过通信通道发送的消息请求其他任务(服务器)来访问服务。

Mach中这些通信通道的端点称为端口,而端口权限表示使用该通道的权限。Mach提供的IPC形式包括

  • 消息队列
  • 信号量
  • 通知
  • 锁集
  • 远程过程调用(rpc)

由端口表示的IPC对象的类型决定了该端口上允许的操作,以及数据传输的方式(和是否)。

重要提示:OS X中的IPC设施处于过渡状态。在系统的早期版本中,并不是所有这些IPC类型都可以实现。

对于端口的原始操作,有两种基本不同的Mach api, mach_ipc家族和mach_msg家族。在合理的范围内,这两个系列都可以用于任何IPC对象;然而,在新代码中,mach_ipc调用是首选的。mach_ipc调用在适当的地方维护状态信息,以便支持事务的概念。mach_msg调用支持遗留代码,但不推荐使用;他们是无状态的。

IPC事务和事件调度

当线程调用mach_ipc_dispatch,它反复处理事件设置的注册端口。这些事件可以从RPC参数块对象(如客户调用的结果),一个锁对象被(由于其他线程释放锁),通知或信号量被发布或消息来自一个传统的消息队列。

这些事件通过mach_msg_dispatch的调用来处理。有些事件暗示调用生命周期内存在事务。在锁的情况下,状态是锁的所有权。当callout返回时,锁被释放。在远程过程调用的情况下,状态是客户机的标识、参数块和应答端口。当callout返回时,将发送应答。

当callout返回时,事务(如果有的话)就完成了,线程等待下一个事件。mach_ipc_dispatch工具旨在支持工作循环。

消息队列

最初,Mach中进程间通信的唯一样式是消息队列。只有一个任务可以持有表示消息队列的端口的接收权。这个任务允许从端口队列接收(读取)消息。多个任务可以拥有向队列发送(写)消息的端口的权限。

任务通过构建包含一组数据元素的数据结构与另一个任务通信,然后在其拥有发送权限的端口上执行消息发送操作。稍后,具有该端口接收权限的任务将执行消息接收操作。

一条消息可包括以下部分或全部:

  • 纯数据
  • 存储范围的副本
  • 端口的权利
  • 内核隐式属性,如发送方的安全令牌

消息传输是一个异步操作。消息逻辑上被复制到接收任务中,可能使用写时复制优化。接收任务中的多个线程可以尝试从给定端口接收消息,但是只有一个线程可以接收任何给定的消息。

信号量

信号量IPC对象支持等待、post和post所有操作。这些是计数信号量,如果在该信号量的等待队列中当前没有线程在等待,则保存(计数)post。post all操作将唤醒所有当前等待的线程。

通知

与信号量一样,通知对象也支持post和wait操作,但添加了一个state字段。状态是一个固定大小、固定格式的字段,在创建通知对象时定义。每个post更新状态字段;每个post都覆盖一个状态。

锁是提供对临界区互斥访问的对象。锁的主要接口是面向事务的(参见IPC事务和事件调度)。在事务期间,线程持有锁。当它从事务返回时,锁被释放。

远程过程调用(RPC)对象

顾名思义,RPC对象旨在促进和优化远程过程调用。RPC对象的主要接口是面向事务的(参见IPC事务和事件调度)

当创建RPC对象时,定义一组参数块格式。当客户机发出RPC(对象上的发送)时,它会导致以预定义格式之一的消息创建并在对象上排队,然后最终传递给服务器(接收方)。当服务器从事务返回时,将回复返回给发送方。Mach试图通过使用客户机的资源执行服务器来优化事务;这称为线程迁移

时间管理

Mach中时间的传统抽象是时钟,它提供了一组基于mach_timspec_t的异步报警服务。有一个或多个时钟对象,每个对象定义一个以纳秒为单位的单调递增的时间值。实时时钟是内置的,是最重要的,但是系统中可能还有其他时钟用于其他时间概念。时钟支持获取当前时间、给定时间段的睡眠、设置闹钟(在给定时间发送的通知)等操作。

mach_timespec_t API在OS x中是不推荐的。更新的、首选的API基于计时器对象,而计时器对象又使用AbsoluteTime作为基本数据类型。AbsoluteTime是一种依赖于机器的类型,通常基于平台本机时基。提供了一些例程来将绝对时间值转换为其他数据类型,或者从其他数据类型转换为绝对时间值,比如纳秒。计时器对象支持异步、无漂移通知、取消和提前警报。它们比时钟效率更高,分辨率更高。

我们总结一下

Mach 、端口、Mach消息部分

  • 在Mach中所有东西(Task、线程、虚拟内存等)都是对象
  • 对象与对象之间通信只能(任务虚拟地址空间)通过端口收发(一个接收方,可以多个发送方)消息。

Mach做以下几件事儿:

  • “控制点”或执行单元的管理。
  • 线程或线程组(Task)的资源分配。
  • 虚拟内存的分配和管理。
  • 底层物理资源–即CPU、内存和任何物理设备的分配。

Mach消息结构体

  • 最基本的包含两部分:消息头、消息体。可选的消息尾
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    typedef	struct 
    {
    mach_msg_bits_t msgh_bits;//标志位
    mach_msg_size_t msgh_size;//大小
    mach_port_t msgh_remote_port;//目标端口(发送:接受方,接收:发送方)
    mach_port_t msgh_local_port; //源端口(发送:发送方,接收:接收方)
    mach_port_name_t msgh_voucher_port;
    mach_msg_id_t msgh_id;
    } mach_msg_header_t; //消息头
    typedef struct
    {
    mach_msg_size_t msgh_descriptor_count;
    } mach_msg_body_t;//消息体
    typedef struct
    {
    mach_msg_header_t header;
    mach_msg_body_t body;
    } mach_msg_base_t; //基本消息
    typedef unsigned int mach_msg_trailer_type_t;//消息尾的类型
    typedef struct
    {
    mach_msg_trailer_type_t msgh_trailer_type;
    mach_msg_trailer_size_t msgh_trailer_size;
    } mach_msg_trailer_t; //消息尾
  • 复杂一点的,将消息头的标志位mach_msg_bits_t设置为MACH_MSGH_BITS_COMPLEX,就表示复杂消息。此时消息体里面指定了描述符的个数,接下来就是一个接着一个的描述符:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    typedef struct
    {
    uint64_t address;//数据的大小
    boolean_t deallocate: 8;//发送之后是否接触分配
    mach_msg_copy_options_t copy: 8;//复制指令
    unsigned int pad1: 8;
    mach_msg_descriptor_type_t type: 8;
    mach_msg_size_t size;//数据的大小
    } mach_msg_ool_descriptor64_t;

消息收发

1
2
3
4
5
6
7
8
extern mach_msg_return_t	mach_msg(
mach_msg_header_t *msg,
mach_msg_option_t option,
mach_msg_size_t send_size,
mach_msg_size_t rcv_size,
mach_port_name_t rcv_name,
mach_msg_timeout_t timeout,
mach_port_name_t notify);

步骤如下:
发送消息:

  • 调用current_space()获取当前的IPC空间。
  • 调用current_map()获取虚拟空间
  • 消息大小正确性检查
  • 计算要分配的消息大小
  • 通过ipc_kmsg_alloc分配消息
  • 复制消息
  • 复制消息关联的端口权限,然后通过ipc_kmsg_copyin将所有的out-of-line数据的内存复制到当前虚拟空间。(如果不复制权限可能导致无法访问数据)
  • 调用ipc_kmsg_send()发送消息
    • 获得msgh_remote_port引用并锁定端口
    • 调用ipc_mqueue_send(),将消息直接复制到端口的ipc_messages队列中并唤醒等待的线程。

接收消息

  • 调用current_space()获取当前的IPC空间。
  • 调用current_map()获取虚拟空间
  • 调用ipc_mqueue_copyin()获取IPC队列。
  • 调用ipc_mqueue_receive()从队列中取出消息
  • 执行

端口

端口实际上就是一个整型的标识符,是如下结构(在osfmk/ipc/ipc_port.h中定义)的一个句柄:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
struct ipc_port {
/*
* Initial sub-structure in common with ipc_pset
* First element is an ipc_object second is a
* message queue
*/
struct ipc_object ip_object;
struct ipc_mqueue ip_messages;
natural_t ip_sprequests:1, /* send-possible requests outstanding */
ip_spimportant:1, /* ... at least one is importance donating */
ip_impdonation:1, /* port supports importance donation */
ip_tempowner:1, /* dont give donations to current receiver */
ip_guarded:1, /* port guarded (use context value as guard) */
ip_strict_guard:1, /* Strict guarding; Prevents user manipulation of context values directly */
ip_reserved:2,
ip_impcount:24; /* number of importance donations in nested queue */
union {
struct ipc_space *receiver;
struct ipc_port *destination;
ipc_port_timestamp_t timestamp;
} data;
union {
ipc_kobject_t kobject;
ipc_importance_task_t imp_task;
uintptr_t alias;
} kdata;

struct ipc_port *ip_nsrequest;
struct ipc_port *ip_pdrequest;
struct ipc_port_request *ip_requests;
struct ipc_kmsg *ip_premsg;
mach_vm_address_t ip_context;
mach_port_mscount_t ip_mscount;
mach_port_rights_t ip_srights;
mach_port_rights_t ip_sorights;
#if MACH_ASSERT
#define IP_NSPARES 4
#define IP_CALLSTACK_MAX 16
/* queue_chain_t ip_port_links;*//* all allocated ports */
thread_t ip_thread; /* who made me? thread context */
unsigned long ip_timetrack; /* give an idea of "when" created */
uintptr_t ip_callstack[IP_CALLSTACK_MAX]; /* stack trace */
unsigned long ip_spares[IP_NSPARES]; /* for debugging */
#endif /* MACH_ASSERT */
} __attribute__((__packed__));

Mach接口生成器 MIG

Mach消息传递模型是远程调用(Remote Procedure Call,RPC)的一种现实(类似Thrift)。在/usr/include/mach目录下可以看到一些.defs文件,这些文件包含了Mach子系统(一组操作)的定义。操作类型如下:

IPC

  • Mach的每个Task都包含一个指针,这个指针指向一个IPC命名空间,这个IPC命名空间了包含了Task的端口,当然Task还可以获取系统范围的端口,例如:主机端口、特权端口(可以重启机器等)等。
  • 在用户态下,消息传递都是通过mach_msg()函数实现的,这个函数会触发一个mach陷阱mach_msg_trap(),接下来mach_msg_trap()又会调用mach_msg_overwrite_trap(),它会通过MACH_SEND_MSG和MACH_RCV_MSG来判断是发送操作,还是接收操作。
  • 期中内核态中还可以通过mach_msg_receive()和mach_msg_send()来收发数据。

主机、时钟、处理器、处理器集

主机对象 Host

主机就是一组“特殊”端口的集合,以及一组异常处理程序的集合,同时定义了一个锁用于保护异常处理的并发访问。结构如下:

1
2
3
4
5
struct	host {
decl_lck_mtx_data(,lock) /* lock to protect exceptions */
ipc_port_t special[HOST_MAX_SPECIAL_PORT + 1];
struct exception_action exc_actions[EXC_TYPES_COUNT];
};

时钟对象(Clock)

Mach内核提供了一个简单的“时钟”对象(在osfmk/kern/clock.h中定义)的抽象,这个对象用于计时和闹铃,期中最重要的内部API是clock_deadline_for_periodic_event(),调度器通过它设置了一个重复发生的通知–从而保证了多任务引擎的运转。

处理器对象(Processer)

在多核架构中每一个核心都可以看做是一个CPU,处理器被分配给处理器集,处理器是CPU的简单抽象,被Mach用于一些基本的操作,比如:启动和关闭一个CPU,向CPU分发要执行的线程。结构的定义(在osfmk/kern/processor.h)如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
struct processor {
queue_chain_t processor_queue;/* idle/active queue link,
* MUST remain the first element */
int state; /* See below */
boolean_t is_SMT;
boolean_t is_recommended;
struct thread
*active_thread, /* thread running on processor */
*next_thread, /* next thread when dispatched */
*idle_thread; /* this processor's idle thread. */
processor_set_t processor_set; /* assigned set */
int current_pri; /* priority of current thread */
sched_mode_t current_thmode; /* sched mode of current thread */
sfi_class_id_t current_sfi_class; /* SFI class of current thread */
int cpu_id; /* platform numeric id */
timer_call_data_t quantum_timer; /* timer for quantum expiration */
uint64_t quantum_end; /* time when current quantum ends */
uint64_t last_dispatch; /* time of last dispatch */
uint64_t deadline; /* current deadline */
boolean_t first_timeslice; /* has the quantum expired since context switch */
#if defined(CONFIG_SCHED_TRADITIONAL) || defined(CONFIG_SCHED_MULTIQ)
struct run_queue runq; /* runq for this processor */
#endif
#if defined(CONFIG_SCHED_TRADITIONAL)
int runq_bound_count; /* # of threads bound to this processor */
#endif
#if defined(CONFIG_SCHED_GRRR)
struct grrr_run_queue grrr_runq; /* Group Ratio Round-Robin runq */
#endif
processor_t processor_primary; /* pointer to primary processor for
* secondary SMT processors, or a pointer
* to ourselves for primaries or non-SMT */
processor_t processor_secondary;
struct ipc_port * processor_self; /* port for operations */
processor_t processor_list; /* all existing processors */
processor_data_t processor_data; /* per-processor data */
};

其中最重要的是runq,这是分发到这个处理器的线程队列。

处理器集

处理器集就是一个或多个processor_t的分组,也被称为pset。pset通常维护三个队列:

active_queue:用于保存当前正在执行线程的CPU。
idle_queue:用于保存当前空闲的CPU(例如:正在执行idle_thread)。
pset_runq:保存了在这个集合中的所有CPU上执行的线程。
processor_set的定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
struct processor_set {
queue_head_t active_queue; /* active processors */
queue_head_t idle_queue; /* idle processors */
queue_head_t idle_secondary_queue; /* idle secondary processors */
int online_processor_count;
int cpu_set_low, cpu_set_hi;
int cpu_set_count;
#if __SMP__
decl_simple_lock_data(,sched_lock) /* lock for above */
#endif
#if defined(CONFIG_SCHED_TRADITIONAL) || defined(CONFIG_SCHED_MULTIQ)
struct run_queue pset_runq; /* runq for this processor set */
#endif
#if defined(CONFIG_SCHED_TRADITIONAL)
int pset_runq_bound_count;
/* # of threads in runq bound to any processor in pset */
#endif
/* CPUs that have been sent an unacknowledged remote AST for scheduling purposes */
uint64_t pending_AST_cpu_mask;
#if defined(CONFIG_SCHED_DEFERRED_AST)
/*
* A seperate mask, for ASTs that we may be able to cancel. This is dependent on
* some level of support for requesting an AST on a processor, and then quashing
* that request later.
*
* The purpose of this field (and the associated codepaths) is to infer when we
* no longer need a processor that is DISPATCHING to come up, and to prevent it
* from coming out of IDLE if possible. This should serve to decrease the number
* of spurious ASTs in the system, and let processors spend longer periods in
* IDLE.
*\/
uint64_t pending_deferred_AST_cpu_mask;
#endif
struct ipc_port * pset_self; /* port for operations */
struct ipc_port * pset_name_self; /* port for information */
processor_set_t pset_list; /* chain of associated psets */
pset_node_t node;
};

XNU 怎么加载 App?

iOS 的可执行文件和动态库都是 Mach-O 格式,所以加载 APP 实际上就是加载 Mach-O 文件。
Mach-O header 信息结构代码如下:

1
2
3
4
5
6
7
8
9
10
struct mach_header_64 {
uint32_t magic; //64位还是32位
cpu_type_t cputype; //CPU 类型,比如 arm 或 X86
cpu_subtype_t cpusubtype; //CPU 子类型,比如 armv8
uint32_t filetype; // 文件类型
uint32_t ncmds; // load commands 的数量
uint32_t sizeofcmds; // load commands 大小
uint32_t flags; // 标签
uint32_t reserved; // 保留字段
}

其中,文件类型 filetype 表示了当前 Mach-O 属于哪种类型。Mach-O 包括以下几种类型。

  • OBJECT,指的是 .o 文件或者 .a 文件;
  • EXECUTE,指的是IPA 拆包后的文件;
  • DYLIB,指的是 .dylib 或 .framework 文件;
  • DYLINKER,指的是动态链接器;
  • DSYM,指的是保存有符号信息用于分析闪退信息的文件。

加载 Mach-O 文件,内核会 fork 进程,并对进程进行一些基本设置,比如为进程分配虚拟内存、为进程创 建主线程、代码签名等。用户态 dyld 会对 Mach-O 文件做库加载和符号解析。

官方文档地址

OS X简介

OS X为Macintosh用户和开发人员社区提供了许多好处。这些优点包括改进的可靠性和性能、增强的网络特性、基于对象的系统编程接口以及对行业标准的更多支持。

在创建OS X的过程中,苹果彻底改造了Mac OS核心操作系统。形成OS X的基础是内核。下图说明了OS X体系结构。

内核为OS x提供了许多增强功能,包括抢占、内存保护、增强的性能、改进的网络设施、对Macintosh(扩展版和标准版)和非Macintosh (UFS、ISO 9660等等)文件系统的支持、面向对象的api等等。其中两个特性,抢占和内存保护,导致了更健壮的环境。

在Mac OS 9中,应用程序协作共享处理器时间。类似地,所有应用程序之间共享计算机的内存。Mac OS 9是一个协作的多任务环境。如果一个应用程序不合作,那么所有进程的响应性都会受到影响。另一方面,实时应用程序(如多媒体)需要保证具有可预测的、时间关键的行为。

相比之下,OS X是一个先发制人的多任务环境。在OS X中,内核提供了强制合作,调度进程来共享时间(抢占)。这支持需要实时行为的应用程序。

在OS X中,进程通常不共享内存。相反,内核为每个进程分配自己的地址空间,控制对这些地址空间的访问。此控件确保任何应用程序都不能无意中访问或修改另一个应用程序的内存(保护)。规模不是问题;由于OS X包含了虚拟内存系统,每个应用程序都可以访问自己的4 GB地址空间。

从整体上看,所有应用程序都在用户空间中运行,但这并不意味着它们共享内存。用户空间只是所有用户级应用程序的组合地址空间的术语。内核本身有它自己的地址空间,称为内核空间。在OS X中,没有应用程序可以直接修改系统软件(内核)的内存。

虽然默认情况下用户进程不像Mac OS 9那样共享内存,但应用程序之间的通信(甚至内存共享)仍然是可能的。例如,内核提供了一组丰富的原语,允许进程之间共享一些信息。这些基本类型包括共享库、框架和POSIX共享内存。Mach消息传递提供了另一种方法,将内存从一个进程传递到另一个进程。然而,与Mac OS 9不同的是,如果没有程序员的显式操作,内存共享就无法实现。

达尔文操作系统

OS X内核是一个开源项目。内核以及OS X的其他核心部分统称为Darwin。Darwin是一个完整的操作系统,基于与OS x相同的许多技术。但是,Darwin不包括Apple专有的图形或应用程序层,比如Quartz、QuickTime、Cocoa、Carbon或OpenGL。

下图显示了Darwin和OS X之间的关系,它们都构建在相同的内核之上,但是OS X添加了核心服务、应用程序服务和QuickTime,以及经典的Carbon、Cocoa和Java (JDK)应用程序环境。Darwin和OS X都包含BSD命令行应用程序环境;然而,在OS X中,不需要使用环境,因此它对用户是隐藏的,除非用户选择访问它。

Darwin技术基于BSD、Mach 3.0和苹果技术。最重要的是,Darwin技术是开源技术,这意味着开发人员可以完全访问源代码。实际上,OS X第三方开发人员可以成为Darwin核心系统软件开发团队的一部分。开发人员还可以看到苹果在核心操作系统中是如何工作的,并在自己的产品中采用(或调整)代码。有关详细信息,请参阅Apple公共源许可证(APSL)。

因为相同的软件构成了OS X和Darwin的核心,所以开发人员可以创建在OS X和Darwin上运行的底层软件,而几乎不需要进行任何更改。唯一的区别可能是软件与应用程序环境的交互方式。

达尔文是基于许多来源的成熟技术。这项技术的很大一部分来自FreeBSD,这是4.4BSD的一个版本,提供了先进的网络、性能、安全和兼容性特性。该系统软件的其他部分,如Mach,基于苹果MkLinux项目、OS X服务器以及NeXT收购的技术。大部分代码与平台无关。所有核心操作系统代码都以源代码的形式提供。

选择核心技术有几个原因。Mach提供了一组干净的抽象,用于处理内存管理、进程间(和处理器间)通信(IPC)和其他低级操作系统功能。在当今快速变化的硬件环境中,这在操作系统和底层硬件之间提供了一个有用的隔离层。

BSD是一个精心设计的成熟操作系统,具有许多功能。事实上,当今大多数商业UNIX和类UNIX操作系统都包含大量BSD代码。BSD还提供了一组行业标准api。

新技术,如I/O工具包和网络内核扩展(NKEs),都是由苹果公司设计和设计的,以利用先进的功能,如面向对象编程模型提供的功能。OS X将这些新技术与经过时间检验的行业标准结合起来,创建了一个稳定、可靠、灵活和可扩展的操作系统。

系统架构

Darwin和OS X的基础层由几个体系结构组件组成,如下图所示。这些组件合在一起构成内核环境。

重要提示:注意OS X使用术语内核的方式与您可能期望的有所不同。

在传统的操作系统术语中,内核是一个很小的软件核心,它只提供实现附加操作系统服务所需的最小设施。——摘自《4.4 BSD操作系统的设计与实现》,McKusick, Bostic, Karels和Quarterman, 1996。

类似地,在传统的基于Mach的操作系统中,内核指的是Mach微内核,忽略了额外的底层代码,没有这些底层代码,Mach几乎做不了什么。

然而,在OS X中,内核环境包含的内容比Mach内核本身多得多。OS X内核环境包括Mach内核、BSD、I/O工具包、文件系统和网络组件。这些通常统称为内核。下面几节将简要描述这些组件。有关进一步的详细信息,请参阅具体的组成部分章节或参考书目中列出的参考资料。

因为OS X包含三个基本组件(Mach、BSD和I/O工具包),所以对于某些关键操作,常常有多达三个api。一般来说,所选择的API应该与正在使用它的内核部分匹配,而内核部分又由您的代码试图执行的操作决定。本章的其余部分将介绍Mach、BSD和I/O工具包,并概述这些组件提供的功能。

Mach

Mach管理CPU使用情况和内存等处理器资源,处理调度,提供内存保护,并为其他操作系统层提供以消息为中心的基础设施。

参看我的另外一篇Mach概览一章

Mach组件提供

  • 无类型进程间通信(IPC)
  • 远程过程调用(RPC)
  • 对称多处理(SMP)的调度程序支持
  • 对实时服务的支持
  • 虚拟内存支持
  • 支持寻呼机
  • 模块化的体系结构

有关Mach的一般信息可以在Mach概览一章中找到。有关调度的信息可以在Mach调度和线程接口一章中找到。有关VM系统的信息可以在内存和虚拟内存中找到。

BSD

在Mach层之上,BSD层提供“OS personality”api和服务。BSD层基于BSD内核,主要是FreeBSD。BSD组件提供

  • 文件系统
  • 联网(硬件设备级除外)
  • UNIX的安全模型
  • 系统调用的支持
  • BSD过程模型,包括过程id和信号
  • FreeBSD内核api
  • 许多POSIX api
  • 对pthreads (POSIX线程)的内核支持

BSD组件在BSD概述一章中有更详细的描述。

网络

OS X网络利用BSD的高级网络功能来提供对现代特性的支持,比如网络地址转换(NAT)和防火墙。网络组件提供

  • 4.4BSD TCP/IP栈和套接字api
  • 同时支持IP和DDP (AppleTalk传输)
  • multihoming
  • 路由
  • 多播支持
  • 服务器调优
  • 信息包过滤
  • Mac OS经典支持(通过过滤器)

有关网络的更多信息可以在网络体系结构一章中找到。

文件系统

OS X支持多种类型的文件系统,包括HFS、HFS+、UFS、NFS、ISO 9660等。默认的文件系统类型是HFS+;OS X引导(和“根”)来自HFS+、UFS、ISO、NFS和UDF。OS X文件系统的高级功能包括一个增强的虚拟文件系统(VFS)设计。VFS提供了分层的体系结构(文件系统是可堆叠的)。文件系统组件提供

  • utf - 8 (Unicode)支持
  • 与以前版本的Mac OS相比,性能有所提高。

更多信息可以在章节文件系统概述中找到。

I / O设备

I/O工具包为简化驱动程序开发提供了一个框架,支持多种类型的设备。I/O工具包提供了一个面向对象的I/O体系结构,该体系结构在c++的一个受限子集中实现。I/O工具包框架是模块化和可扩展的。I/O工具包组件提供

  • 真正的即插即用
  • 动态设备管理
  • 动态(按需)加载驱动程序
  • 桌面系统和便携式设备的电源管理
  • 多处理器能力

I/O工具包在I/O工具包概述一章中有更详细的描述。

内核扩展

OS X提供了一种内核扩展机制,作为一种允许将代码片段动态加载到内核空间的方法,而不需要重新编译。这些代码通常称为插件plug-ins,或者在OS X内核环境中称为kernel extensionsKEXTs

因为KEXTs同时提供模块化和动态可加载性,所以对于需要访问不导出到用户空间的接口的任何相对自包含的服务,KEXTs都是一个自然的选择。内核环境的许多组件都支持这种扩展机制,尽管它们以不同的方式支持。

例如,一些新的网络特性涉及到使用网络内核扩展(NKEs)。这些将在网络体系结构一章讨论。

动态添加新文件系统实现的能力基于VFS KEXTs, I/O工具包中的设备驱动程序和设备族使用KEXTs实现。对于编写驱动程序或编写代码以支持新的卷格式或网络协议的开发人员来说,KEXTs使开发变得更加容易。在内核扩展概述一章中将更详细地讨论KEXTs。