iOS随笔之系统架构与App启动过程

最近的状态嘚吧嘚

最近工作有些忙碌,不停追赶着迭代,修复问题填坑…已经不记得上次自己写文章是什么时候了,导致之前的账号都忘了,好在一直有做笔记的习惯。知识在于积累和反复,无论多么成熟的开发人员都有遗漏或者淡忘一些时间久不用或者不在意的东西。


回顾下iOS系统架构

iOS 系统是基于 ARM 架构的,大致可以分为四层: 

1.最上层是用户体验层,主要是提供用户界面。这一层包含了 SpringBoard用户GUI管理界面、Spotlight、Accessibility辅助功能。

2.第二层是应用框架层,是开发者会用到的,这一层包含了开发框架 Cocoa Touch。

3.第三层是核心框架层,是系统核心功能的框架层。这一层包含了各种图形和媒体核心框架、Metal 等。

4.第四层是 Darwin 层,是操作系统的核心,属于操作系统的内核态。这一层包含了系统内核 XNU、驱动等。

iOS 系统架构极

用户体验层、应用框架层和核心框架层,属于用户态,是上层 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 的架构,看看它的内部到底都包含了了哪些

Darwin

其中,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。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容