最近的状态嘚吧嘚
最近工作有些忙碌,不停追赶着迭代,修复问题填坑…已经不记得上次自己写文章是什么时候了,导致之前的账号都忘了,好在一直有做笔记的习惯。知识在于积累和反复,无论多么成熟的开发人员都有遗漏或者淡忘一些时间久不用或者不在意的东西。
回顾下iOS系统架构
iOS 系统是基于 ARM 架构的,大致可以分为四层:
1.最上层是用户体验层,主要是提供用户界面。这一层包含了 SpringBoard用户GUI管理界面、Spotlight、Accessibility辅助功能。
2.第二层是应用框架层,是开发者会用到的,这一层包含了开发框架 Cocoa Touch。
3.第三层是核心框架层,是系统核心功能的框架层。这一层包含了各种图形和媒体核心框架、Metal 等。
4.第四层是 Darwin 层,是操作系统的核心,属于操作系统的内核态。这一层包含了系统内核 XNU、驱动等。
用户体验层、应用框架层和核心框架层,属于用户态,是上层 App 的活动空间。Darwin 是用户态的下层支撑,是 iOS 系统的核心。
Darwin 的内核是 XNU,而 XNU 是在 UNIX 的基础上做了很多改进以及创新。了解 XNU 的内部是怎么样的,将有助于我们解决系统层面的问题。
Darwin 历史
Darwin: (部分开源)基于乔布斯的:OpenStep
OpenStep:及其前身NextStep则是衍生与加州大学伯克利分校所发布的Berkeley Software Distribution(BSD).
Darwin内核XNU: 结合了BSD 与 Mach,以及苹果自己的一些科技研发出来的。
早期的 BSD 是 UNIX 衍生出的操作系统,现在 BSD 是类 UNIX 操作系统的统称。XNU 的 BSD 来源于 FreeBSD 内核,经过深度定制而成。IEEE 为了保证软件可以在各个 UNIX 系统上运行而制定了 POSIX 标准,iOS 也是通过 BSD 对 POSIX 的兼容而成为了类 UNIX 系统。
iOS 6 后,为了增强系统安全,BSD 实行了 ASLR(Address Space Layout Randomization,地址空间布局随机化)。随着 iPhone 硬件升级,为了更好地利用多核,BSD 加入了工作队列,以支持多核多线程处理,这也是 GCD 能更高效工作的基础。 BSD 还从 TrustdBSD 引入了 MAC 框架以增强权限 entitlement 机制的安全。
Darwin主要组件:
BSD
Mach : 最底层为(BSD和I/O kit提供服务
I/O Kit : 面向对象的设备驱动框架
Platform Expert
libkern
libs
XNU内核是一个混合内核,它的核心是一个叫Mach的微内核,Mach中也是消息传递机制,但是它使用的是指针形式传递,因为大部分服务都在XNU内核中,所以Mach没有昂贵的复制操作,只用指针就可以完成消息传递。
微内核可以提高系统的模块化程度,提供内存保护的消息传递机制;
宏内核也可以叫单内核,在出现高负荷状态时依然能够让系统保持高效运作。
Mach 是对内核运作方式的一次探索创新。Mach 提出了“微内核”的概念——将系统内核的部分任务交给用户层进程处理。(Mach 可以认为是微内核的 BSD 系统)
XNU(“X is Not UNIX”)
Mach负责 XNU 比较底层的任务。如:
-抢占式多任务,包括内核线程(Mac OS X用内核线程实现POSIX线程)
-内存保护
-虚拟内存管理
-进程间通信
-中断管理
-实时支持
-内核调试支持
-控制台I/O
下面详细聊下 XNU 的架构,看看它的内部到底都包含了了哪些
其中,Mach是作为 UNIX 内核的替代,主要解决 UNIX 一切皆文件导致抽象机制不足的问题,为现代操作系统做了进一步的抽象工作。 Mach 负责操作系统最基本的工作,包括进程和线程抽象、处理器调度、进程间通信、消息机制、虚拟内存管理、内存保护等。
进程对应到 Mach 是 Mach Task,Mach Task 可以看做是线程执行环境的抽象,包含虚拟地址空间、IPC 空间、处理器资源、调度控制、线程容器。
进程在 BSD 里是由 BSD Process 处理,BSD Process 扩展了 Mach Task,增加了进程 ID、信号信息等,BSD Process 里面包含了扩展 Mach Thread 结构的 Uthread。
Mach 的模块包括进程和线程都是对象,对象之间不能直接调用,只能通过 Mach Msg 进行通信,也就是 mach_msg() 函数。在用户态的那三层中,也就是在用户体验层、应用框架层和核心框架层中,你可以通过 mach_msg_trap() 函数触发陷阱,从而切至 Mach,由 Mach 里的 mach_msg() 函数完成实际通信,具体实现可以参看 NSHipster 的这篇文章“Inter-Process Communication”。
每个 Mach Thread 表示一个线程,是 Mach 里的最小执行单位。Mach Thread 有自己的状态,包括机器状态、线程栈、调度优先级(有 128 个,数字越大表示优先级越高)、调度策略、内核 Port、异常 Port。
Mach Thread 既可以由 Mach Task 处理,也可以扩展为 Uthread,通过 BSD Process 处理。这是因为 XNU 采用的是微内核 Mach 和 宏内核 BSD 的混合内核,具备微内核和宏内核的优点。
Mach 是微内核,可以将操作系统的核心独立在进程上运行,不过,内核层和用户态各层之间切换上下文和进程间消息传递都会降低性能。为了提高性能,苹果深度定制了 BSD 宏内核,使其和 Mach 混合使用。
宏内核 BSD 是对 Mach 封装,提供进程管理、安全、网络、驱动、内存、文件系统(HFS+)、网络文件系统(NFS)、虚拟文件系统(VFS)、POSIX(Portable Operating System Interface of UNIX,可移植操作系统接口)兼容。
BSD 提供了更现代、更易用的内核接口,以及 POSIX 的兼容,比如通过扩展 Mach Task 进程结构为 BSD Process。对于 Mach 使用 mach_msg_trap() 函数触发陷阱来处理异常消息,BSD 则在异常消息机制的基础上建立了信号处理机制,用户态产生的信号会先被 Mach 转换成异常,BSD 将异常再转换成信号。对于进程和线程,BSD 会构建 UNIX 进程模型,创建 POSIX 兼容的线程模型 pthread。
除了微内核 Mach 和宏内核 BSD 外,XNU 还有 IOKit。IOKit 是硬件驱动程序的运行环境,包含电源、内存、CPU 等信息。IOKit 底层 libkern 使用 C++ 子集 Embedded C++ 编写了驱动程序基类,比如 OSObject、OSArray、OSString 等,新驱动可以继承这些基类来写。
了解了 XNU 后,接下来,我再跟你聊聊 XNU 怎么加载 App 的?
XNU 怎么加载 App?
iOS 的可执行文件和动态库都是 Mach-O 格式,所以加载 APP 实际上就是加载 Mach-O 文件。
Mach-O header 信息结构代码如下:
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; //保留字段
};
如上面代码所示,包含了表示是 64 位还是 32 位的 magic、CPU 类型 cputype、CPU 子类型 cpusubtype、文件类型 filetype、描述文件在虚拟内存中逻辑结构和布局的 load commands 数量和大小等文件信息。
其中,文件类型 filetype 表示了当前 Mach-O 属于哪种类型。Mach-O 包括以下几种类型。
OBJECT,指的是 .o 文件或者 .a 文件;
EXECUTE,指的是 IPA 拆包后的文件;
DYLIB,指的是 .dylib 或 .framework 文件;
DSYM,指的是保存有符号信息用于分析闪退信息的文件(平时诊断crash信息会用到)。
整个 fork 进程,加载解析 Mach-O 文件的过程可以在 XNU 的源代码中查看,代码路径是 darwin-xnu/bsd/kern/kern_exec.c,kern_exec.c,相关代码在 __mac_execve 函数里DYLINKER,指的是动态链接器;
加载 Mach-O 文件,内核会 fork 进程,并对进程进行一些基本设置,比如为进程分配虚拟内存、为进程创建主线程、代码签名等。用户态 dyld 会对 Mach-O 文件做库加载和符号解析。
int __mac_execve(proc_t p, struct __mac_execve_args *uap, int32_t *retval)
{
// 字段设置
...
int is_64 = IS_64BIT_PROCESS(p);
struct vfs_context context;
struct uthread *uthread; // 线程
task_t new_task = NULL; // Mach Task
...
context.vc_thread = current_thread();
context.vc_ucred = kauth_cred_proc_ref(p);
// 分配大块内存,不用堆栈是因为 Mach-O 结构很大。
MALLOC(bufp, char *, (sizeof(*imgp) + sizeof(*vap) + sizeof(*origvap)), M_TEMP, M_WAITOK | M_ZERO);
imgp = (struct image_params *) bufp;
// 初始化 imgp 结构里的公共数据
...
uthread = get_bsdthread_info(current_thread());
if (uthread->uu_flag & UT_VFORK) {
imgp->ip_flags |= IMGPF_VFORK_EXEC;
in_vfexec = TRUE;
} else {
// 程序如果是启动态,就需要 fork 新进程
imgp->ip_flags |= IMGPF_EXEC;
// fork 进程
imgp->ip_new_thread = fork_create_child(current_task(),
NULL, p, FALSE, p->p_flag & P_LP64, TRUE);
// 异常处理
...
new_task = get_threadtask(imgp->ip_new_thread);
context.vc_thread = imgp->ip_new_thread;
}
// 加载解析 Mach-O
error = exec_activate_image(imgp);
if (imgp->ip_new_thread != NULL) {
new_task = get_threadtask(imgp->ip_new_thread);
}
if (!error && !in_vfexec) {
p = proc_exec_switch_task(p, current_task(), new_task, imgp->ip_new_thread);
should_release_proc_ref = TRUE;
}
kauth_cred_unref(&context.vc_ucred);
if (!error) {
task_bank_init(get_threadtask(imgp->ip_new_thread));
proc_transend(p, 0);
thread_affinity_exec(current_thread());
// 继承进程处理
if (!in_vfexec) {
proc_inherit_task_role(get_threadtask(imgp->ip_new_thread), current_task());
}
// 设置进程的主线程
thread_t main_thread = imgp->ip_new_thread;
task_set_main_thread_qos(new_task, main_thread);
}
...
}
小结
总体来说,XNU 加载就是为 Mach-O 创建一个新进程,建立虚拟内存空间,解析 Mach-O 文件,最后映射到内存空间。流程可以概括为:
fork 新进程;
为 Mach-O 分配内存;
解析 Mach-O;
读取 Mach-O 头信息;
遍历 load command 信息,将 Mach-O 映射到内存;
启动 dyld。