iOS 系统架构
从上到下分为四层:
- 用户体验层
- 应用框架层,是
App开发
会用到的 - 核心框架层。
- Darwin 层,是iOS系统的
核心
,属于内核态。
Darwin 的内核是 XNU,XNU 是在 UNIX
的基础上做了很多改进以及创新。
XNU 内核架构
- XNU 由
Mach
、BSD
、IOKit(驱动API)
组成。 - Mach 和 BSD 各自负责如图所示的系统不同的工作
Mach
Mach 是微内核
,微内核可以提高系统的模块化
程度,提供内存保护
的消息传递机制。
BSD
BSD是对 Mach再次封装
的宏内核
, 提供了更现代、更易用的内核接口
宏内核
也叫单内核,性能更高, 在高负荷状态时依然保持高效
运作。
BSD符合POSIX标准
IEEE 为了保证软件在各个 UNIX 系统上运行而制定了POSIX
标准,iOS 通过BSD
对 POSIX 的兼容而成为了类 UNIX 系统
。
比如BSD 会构建UNIX进程模型,创建POSIX兼容的线程模型pthread
。
进程
Mach
进程在Mach中表示为Task,Mach Task
是线程执行的环境和容器。
用户态通过 mach_msg_trap() 函数触发陷阱,切换至 Mach 内核态,由 Mach 里的 mach_msg() 函数完成进程间通信
。
Mach 使用 mach_msg_trap() 函数触发陷阱来处理异常消息
,
BSD
进程在BSD中表示为Process,BSD Process
扩展了 Mach Task
,增加
了进程 ID
、信号
信息等,。
BSD 在Mach异常消息
机制的基础上建立了信号处理
机制,用户态产生的信号会先被 Mach 转换成异常,BSD 将异常再转换成信号
。
线程
Mach Thread 表示一个线程,是 Mach 里的最小执行单位。Mach Thread 有自己的状态,包括机器状态、线程栈、调度优先级、调度策略、内核 Port、异常 Port。
Mach Thread 也可以扩展为 Uthread
,通过 `BSD Process`` 处理。
IOKit
IOKit 是硬件驱动程序
的运行环境,包含电源、内存、CPU 等信息。
IOKit 底层 libkern 使用 C++ 子集 Embedded C++ 编写了驱动程序基类,比如 OSObject、OSArray、OSString 等,新驱动可以继承这些基类来写。
XNU 加载 App
iOS 的可执行文件和动态库都是 Mach-O 格式,所以加载 APP 实际上就是加载 Mach-O 文件。
苹果公司已经将 xnu 开源,放到Github了, 地址是 xnu
整个 fork 进程,加载解析 Mach-O 文件的过程可以在 XNU 的源代码中查看,代码路径是 xnu/bsd/kern/kern_exec.c
,代码如下:
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 结构很大。
char *bufp = kheap_alloc(KHEAP_TEMP,
sizeof(*imgp) + sizeof(*vap) + sizeof(*origvap), Z_WAITOK | Z_ZERO);
image_params *imgp = (struct image_params *) bufp; // Mach-O参数
// 初始化 imgp 结构里的公共数据
imgp->ip_user_fname = uap->fname; // 可执行程序的文件名
imgp->ip_user_argv = uap->argp; // 参数列表
imgp->ip_user_envv = uap->envp; // 环境列表
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 (!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);
}
}
由于 Mach-O 文件很大, __mac_execve 函数会先为 Mach-O 分配一大块堆内存
imgp,接下来初始化 imgp 里的公共数据。内存处理完,通过 fork_create_child() 函数 fork 出一个新的进程和线程。新进程 fork 后,会通过 exec_activate_image() 函数解析加载 Mach-O 文件到内存 imgp 里。最后,使用 task_set_main_thread_qos() 函数设置新 fork 出进程的主线程
。
exec_mach_imgact()
通过 load_machfile() 函数加载 Mach-O 文件,根据解析 Mach-O 后得到的 load command 信息,通过映射方式加载到内存中。还会使用 activate_exec_state() 函数处理解析加载 Mach-O 后的结构信息,设置执行 App 的入口点
。
设置完入口点后会通过 load_dylinker() 函数来解析加载 dyld,然后将入口点地址改成 dyld 的入口地址。这一步完后,内核
部分就完成了 Mach-O 文件的加载。剩下的就是用户态
层 dyld
加载 App 了。
XNU加载App完整流程
-
fork 新进程;
-
为 Mach-O 分配内存;
-
解析 Mach-O;
-
读取 Mach-O 头信息;
-
遍历 load command 信息,将 Mach-O 映射到内存;
-
启动
用户态
进程dyld
, 后续流程就和内核态xnu
没有关系, dyld 流程可以参考 iOS启动优化-dyld4流程介绍。