iOS 从源码解析Run Loop (七):mach msg 解析

iOS 从源码解析Run Loop (七):mach msg 解析

IOS小彩虹2021-07-12 18:21:2360A+A-

 经过前面 NSPort 内容的学习,我们大概对 port 在线程通信中的使用有一点模糊的概念了。macOS/iOS 中利用 Run Loop 实现了自动释放池、延迟回调、触摸事件、屏幕刷新等等功能,关于 Run Loop 的各种应用我们下篇再进行全面的学习。那么本篇我们学习一下 Mach。

 Run Loop 最核心的事情就是保证线程在没有消息时休眠以避免系统资源占用,有消息时能够及时唤醒。Run Loop 的这个机制完全依靠系统内核来完成,具体来说是苹果操作系统核心组件 Darwin 中的 Mach 来完成的。Mach 与 BSD、File System、Mach、Networking 共同位于 Kernel and Device Drivers 层。

 在 Mach 中,所有的东西都是通过自己的对象实现的,进程、线程和虚拟内存都被称为 “对象”,和其他架构不同, Mach 的对象间不能直接调用,只能通过消息传递的方式实现对象间的通信。“消息”(mach msg)是 Mach 中最基础的概念,消息在两个端口 (mach port) 之间传递,这就是 Mach 的 IPC (进程间通信) 的核心。

 Mach 是 Darwin 的核心,可以说是内核的核心,提供了进程间通信(IPC)、处理器调度等基础服务。在 Mach 中,进程、线程间的通信是以消息(mach msg)的方式来完成的,而消息则是在两个 mach port 之间进行传递(或者说是通过 mach port 进行消息的传递)(这也正是 Source1 之所以称之为 Port-based Source 的原因,因为它就是依靠 mach msg 发送消息到指定的 mach port 来唤醒 run loop)。

 关于 Darwin 的信息可以看 ibireme 大佬的文章,5 年前的文章放在今天依然是目前能看到的关于 run loop 最深入的文章,可能这就是大佬吧!:RunLoop 的底层实现

 (概念理解起来可能过于干涩特别是内核什么的,如果没有学习过操作系统相关的知识可能更是只识字不识意,那么下面我们从源码中找线索,从函数的使用上找线索,慢慢的理出头绪来。)

mach_msg_header_t

 Mach 消息相关的数据结构:mach_msg_base_t、mach_msg_header_t、mach_msg_body_t 定义在 <mach/message.h> 头文件中:

typedef struct{
    mach_msg_header_t       header;
    mach_msg_body_t         body;
} mach_msg_base_t;

typedef struct{
    mach_msg_bits_t       msgh_bits;
    mach_msg_size_t       msgh_size; // 消息传递数据大小
    
    // typedef __darwin_mach_port_t mach_port_t; =>
    // typedef __darwin_mach_port_name_t __darwin_mach_port_t; /* Used by mach */
    // typedef __darwin_natural_t __darwin_mach_port_name_t; /* Used by mach */
    // typedef unsigned int __darwin_natural_t;
    // mach_port_t 实际上是一个整数类型,用于标记端口。
    
    mach_port_t           msgh_remote_port;
    mach_port_t           msgh_local_port;
    
    // mach_port_name_t 是 mach 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;

#define MACH_PORT_NULL 0 /* intentional loose typing */
#define MACH_PORT_DEAD ((mach_port_name_t) ~0)

 每条消息均以 message header 开头。mach_msg_header_t 中存储了 mach msg 的基本信息,包括端口信息等。如本地端口 msgh_local_port 和远程端口 msgh_remote_port,mach msg 的传递方向在 header 中已经非常明确了。

  • msgh_remote_port 字段指定消息的目的地。它必须指定为一个有效的发送端口或有一次发送权限的端口。
  • msgh_local_port 字段指定 "reply port"。通常,此字段带有一次发送权限,接收方将使用该一次发送权限来回复消息。It may carry the values MACH_PORT_NULL, MACH_PORT_DEAD, a send-once right, or a send right.
  • msgh_voucher_port 字段指定一个 Mach 凭证端口。除 MACH_PORT_NULL 或 MACH_PORT_DEAD 之外,只能传递对内核实现的 Mach Voucher 内核对象的发送权限。
  • 消息原语(message primitives)不解释 msgh_id 字段。它通常携带指定消息格式或含义的信息。

 一条 Mach 消息实际上就是一个二进制数据包 (BLOB),其头部定义了当前端口 local_port 和目标端口 remote_port,发送和接受消息是通过同一个 API(mach_msg) 进行的,其 option 标记了消息传递的方向。

mach_msg

 首先看一下 mach_msg 函数声明:

/* * Routine: mach_msg * Purpose: * Send and/or receive a message. If the message operation * is interrupted, and the user did not request an indication * of that fact, then restart the appropriate parts of the * operation silently (trap version does not restart). */
 // 发送和/或接收消息。如果消息操作被中断,并且用户没有请求指示该事实,
 // 则静默地重新启动操作的相应部分(trap 版本不会重新启动)。
__WATCHOS_PROHIBITED __TVOS_PROHIBITED 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);

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

 run loop 的核心就是一个 mach_msg ,run loop 调用这个函数去接收消息,如果没有别人发送 port 消息过来,内核会将线程置于等待状态。例如在模拟器里跑起一个 iOS 的 App,然后在 App 静止时点击暂停,会看到主线程调用栈是停留在 mach_msg_trap 这个地方。深入理解RunLoop

 (mach_msg 函数可以设置 timeout 参数,如果在 timeout 到来之前没有读到 msg,当前线程的 run loop 会处于休眠状态。)

 消息的发送和接收统一使用 mach_msg 函数,而 mach_msg 的本质是调用了 mach_msg_trap,这相当于一个系统调用,会触发内核态与用户态的切换。

 点击 App 图标,App 启动完成后处于静止状态(一般如果没有 timer 需要一遍一遍执行的话),此时主线程的 run loop 会进入休眠状态,通过在主线程的 run loop 添加 CFRunLoopObserverRef 在回调函数中可看到主线程的 run loop 的最后活动状态是 kCFRunLoopBeforeWaiting,此时点击 Xcode 控制台底部的 Pause program execution 按钮,从 Xcode 左侧的 Debug navigator 可看到主线程的调用栈停在了 mach_msg_trap,然后在控制台输入 bt 后回车,可看到如下调用栈,看到 mach_msg_trap 是由 mach_msg 函数调用的。

(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = signal SIGSTOP
  * frame #0: 0x00007fff60c51e6e libsystem_kernel.dylib`mach_msg_trap + 10 //  mach_msg_trap
    frame #1: 0x00007fff60c521e0 libsystem_kernel.dylib`mach_msg + 60
    frame #2: 0x00007fff2038e9bc CoreFoundation`__CFRunLoopServiceMachPort + 316 //  __CFRunLoopServiceMachPort 进入休眠
    frame #3: 0x00007fff203890c5 CoreFoundation`__CFRunLoopRun + 1284 //  __CFRunLoopRun
    frame #4: 0x00007fff203886d6 CoreFoundation`CFRunLoopRunSpecific + 567 //  CFRunLoopRunSpecific
    frame #5: 0x00007fff2bededb3 GraphicsServices`GSEventRunModal + 139
    frame #6: 0x00007fff24690e0b UIKitCore`-[UIApplication _run] + 912 //  main run loop 启动
    frame #7: 0x00007fff24695cbc UIKitCore`UIApplicationMain + 101
    frame #8: 0x0000000107121d4a Simple_iOS`main(argc=1, argv=0x00007ffee8addcf8) at main.m:20:12
    frame #9: 0x00007fff202593e9 libdyld.dylib`start + 1
    frame #10: 0x00007fff202593e9 libdyld.dylib`start + 1
(lldb) 

 通过上篇 __CFRunLoopRun 函数的学习,已知 run loop 进入休眠状态时会调用 __CFRunLoopServiceMachPort 函数,该函数内部即调用了 mach_msg 相关的函数操作使得系统内核的状态发生改变:用户态切换至内核态。

mach_msg_trap

 mach_msg 内部实际上是调用了 mach_msg_trap 系统调用。陷阱(trap)是操作系统层面的基本概念,用于操作系统状态的更改,如触发内核态与用户态的切换操作。trap 通常由异常条件触发,如断点、除 0 操作、无效内存访问等。

 当 run loop 休眠时,通过 mach port 消息可以唤醒 run loop,那么从 run loop 创建开始到现在我们在代码层面的学习过程中遇到过哪些 mach port 呢?下面我们就一起回顾一下。最典型的应该当数 __CFRunLoop 中的 _wakeUpPort 和 __CFRunLoopMode 中的 _timerPort。

__CFRunLoop-_wakeUpPort

 struct __CFRunLoop 结构体的成员变量 __CFPort _wakeUpPort 应该是我们在 run loop 里见到的第一个 mach port 了,创建 run loop 对象时即会同时创建一个 mach_port_t 实例为 _wakeUpPort 赋值。_wakeUpPort 被用于在 CFRunLoopWakeUp 函数中调用 __CFSendTrivialMachMessage 函数时作为其参数来唤醒 run loop。_wakeUpPort 声明类型是 __CFPort,实际类型是 mach_port_t。

struct __CFRunLoop {
    ...
    // typedef mach_port_t __CFPort;
    __CFPort _wakeUpPort; // used for CFRunLoopWakeUp 用于 CFRunLoopWakeUp 函数
    ...
};

 在前面 NSMachPort 的学习中我们已知 +(NSPort *)portWithMachPort:(uint32_t)machPort; 函数中 machPort 参数原始即为 mach_port_t 类型。

 当为线程创建 run loop 对象时会直接对 run loop 的 _wakeUpPort 成员变量进行初始化。在 __CFRunLoopCreate 函数中初始化 _wakeUpPort。

static CFRunLoopRef __CFRunLoopCreate(pthread_t t) {
    ...
    // __CFPortAllocate 创建具有发送和接收权限的 mach_port_t
    loop->_wakeUpPort = __CFPortAllocate();
    if (CFPORT_NULL == loop->_wakeUpPort) HALT; // 创建失败的话会直接 crash
    ...
}

 在 __CFRunLoopDeallocate run loop 销毁函数中会释放 _wakeUpPort。

static void __CFRunLoopDeallocate(CFTypeRef cf) {
    ...
    // __CFPortFree 内部是调用 mach_port_destroy(mach_task_self(), rl->_wakeUpPort) 函数
    __CFPortFree(rl->_wakeUpPort);
    rl->_wakeUpPort = CFPORT_NULL;
    ...
}

 全局搜索 _wakeUpPort 看到相关的结果仅有:创建、释放、被插入到 run loop mode 的 _portSet 和 CFRunLoopWakeUp 函数唤醒 run loop 时使用,下面我们看一下以 run loop 对象的 _wakeUpPort 端口为参调用 __CFSendTrivialMachMessage 函数来唤醒 run loop 的过程。

CFRunLoopWakeUp

CFRunLoopWakeUp 函数是用来唤醒 run loop 的,唤醒的方式是以 run loop 对象的 _wakeUpPort 端口为参数调用 __CFSendTrivialMachMessage 函数

void CFRunLoopWakeUp(CFRunLoopRef rl) {
    CHECK_FOR_FORK();
    // This lock is crucial to ignorable wakeups, do not remove it.
    // 此锁对于可唤醒系统至关重要,请不要删除它。
    
    // CRRunLoop 加锁
    __CFRunLoopLock(rl);
    
    // 如果 rl 被标记为忽略唤醒的状态,则直接解锁并返回
    if (__CFRunLoopIsIgnoringWakeUps(rl)) {
        __CFRunLoopUnlock(rl);
        return;
    }
    
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
    kern_return_t ret;
    
    /* We unconditionally try to send the message, since we don't want to lose a wakeup, but the send may fail if there is already a wakeup pending, since the queue length is 1. */
    // 因为我们不想丢失唤醒,所以我们无条件地尝试发送消息,但由于队列长度为 1,因此如果已经存在唤醒等待,则发送可能会失败。
    
    // 向 rl->_wakeUpPort 端口发送消息,内部是调用了 mach_msg
    ret = __CFSendTrivialMachMessage(rl->_wakeUpPort, 0, MACH_SEND_TIMEOUT, 0);
    if (ret != MACH_MSG_SUCCESS && ret != MACH_SEND_TIMED_OUT) CRASH("*** Unable to send message to wake up port. (%d) ***", ret);
    
#elif DEPLOYMENT_TARGET_WINDOWS
    SetEvent(rl->_wakeUpPort);
#endif

    // CFRunLoop 解锁
    __CFRunLoopUnlock(rl);
}

__CFSendTrivialMachMessage

__CFSendTrivialMachMessage 函数内部主要是调用 mach_msg 函数向 port 发送消息。

static uint32_t __CFSendTrivialMachMessage(mach_port_t port, uint32_t msg_id, CFOptionFlags options, uint32_t timeout) {
    // 记录 mach_msg 函数返回结果
    kern_return_t result;
    
    // 构建 mach_msg_header_t 用于发送消息
    mach_msg_header_t header;
    
    // #define MACH_MSG_TYPE_COPY_SEND 19 // Must hold send right(s) 必须持有发送权限
    // #define MACH_MSGH_BITS(remote, local) ((remote) | ((local) << 8)) // 位操作
    header.msgh_bits = MACH_MSGH_BITS(MACH_MSG_TYPE_COPY_SEND, 0);
    
    header.msgh_size = sizeof(mach_msg_header_t);
    
    header.msgh_remote_port = port; // 远程端口
    
    header.msgh_local_port = MACH_PORT_NULL;
    header.msgh_id = msg_id; // 0 
    
    // #define MACH_SEND_TIMEOUT 0x00000010 /* timeout value applies to send */ 超时值应用于发送
    // 调用 mach_msg 函数发送消息
    result = mach_msg(&header, MACH_SEND_MSG|options, header.msgh_size, 0, MACH_PORT_NULL, timeout, MACH_PORT_NULL);
    
    if (result == MACH_SEND_TIMED_OUT) mach_msg_destroy(&header);
    
    return result;
}

 可看到 CFRunLoopWakeUp 函数的功能就是调用 mach_msg 函数向 run loop 的 _wakeUpPort 端口发送消息来唤醒 run loop。

__CFRunLoopMode-_timerPort

 在 macOS 下同时支持 dispatch_source 和 mk 构建 timer,在 iOS 下则只支持使用 mk。这里我们只关注 _timerPort。我们在 Cocoa Foundation 层会通过手动创建并添加计时器 NSTimer 到 run loop 的指定 run loop mode 下,同样在 Core Foundation 层会通过创建 CFRunLoopTimerRef 实例并把它添加到 run loop 的指定 run loop mode 下,内部实现是则是把 CFRunLoopTimerRef 实例添加到 run loop mode 的 _timers 集合中,当 _timers 集合中的计时器需要执行时则正是通过 _timerPort 来唤醒 run loop,且 run loop mode 的 _timers 集合中的所有计时器共用这一个 _timerPort。

 这里我们可以做一个验证,我们为主线程添加一个 CFRunLoopOberver 观察 main run loop 的状态变化和一个 1 秒执行一次的 NSTimer。程序运行后可看到一直如下的重复打印:(代码过于简单,这里就不贴出来了)

...
 timer 回调...
... kCFRunLoopBeforeTimers
... kCFRunLoopBeforeSources
... kCFRunLoopBeforeWaiting
... kCFRunLoopAfterWaiting
 timer 回调...
... kCFRunLoopBeforeTimers
... kCFRunLoopBeforeSources
... kCFRunLoopBeforeWaiting
... kCFRunLoopAfterWaiting
...

 计时器到了触发时间唤醒 run loop(kCFRunLoopAfterWaiting)执行计时器的回调,计时器回调执行完毕后 run loop 又进入休眠状态(kCFRunLoopBeforeWaiting)然后到达下次计时器触发时间时 run loop 再次被唤醒,如果不手动停止计时器的话则会这样一直无限重复下去。

 下面我们看一下其中唤醒 run loop 的关键 _timerPort 的创建和使用逻辑。

#if DEPLOYMENT_TARGET_MACOSX
#define USE_DISPATCH_SOURCE_FOR_TIMERS 1
#define USE_MK_TIMER_TOO 1
#else
#define USE_DISPATCH_SOURCE_FOR_TIMERS 0
#define USE_MK_TIMER_TOO 1
#endif

struct __CFRunLoopMode {
    ...
#if USE_DISPATCH_SOURCE_FOR_TIMERS
    dispatch_source_t _timerSource;
    dispatch_queue_t _queue;
    Boolean _timerFired; // set to true by the source when a timer has fired
    Boolean _dispatchTimerArmed;
#endif

#if USE_MK_TIMER_TOO
    mach_port_t _timerPort;
    Boolean _mkTimerArmed;
#endif
    ...
};

 首先是创建同样也是跟着 CFRunLoopModeRef 一起创建的。在 __CFRunLoopFindMode 函数中当创建 CFRunLoopModeRef 时会同时创建一个 mach_port_t 实例并赋值给 _timerPort。并同时会把 _timerPort 插入到 CFRunLoopModeRef 的 _portSet 中。

static CFRunLoopModeRef __CFRunLoopFindMode(CFRunLoopRef rl, CFStringRef modeName, Boolean create) {
    ...
#if USE_MK_TIMER_TOO
    // 为 _timerPort 赋值
    rlm->_timerPort = mk_timer_create();
    // 同时也把 _timerPort 插入 _portSet 集合中
    ret = __CFPortSetInsert(rlm->_timerPort, rlm->_portSet);
    // 如果插入失败则 crash
    if (KERN_SUCCESS != ret) CRASH("*** Unable to insert timer port into port set. (%d) ***", ret);
#endif
    
    // 把 rl 的 _wakeUpPort 插入到 _portSet 中
    ret = __CFPortSetInsert(rl->_wakeUpPort, rlm->_portSet);
    // 插入失败的话会 crash 
    if (KERN_SUCCESS != ret) CRASH("*** Unable to insert wake up port into port set. (%d) ***", ret);
    ...
}

 在 __CFRunLoopModeDeallocate run loop mode 的销毁函数中同样会销毁 _timerPort。

static void __CFRunLoopModeDeallocate(CFTypeRef cf) {
    ...
#if USE_MK_TIMER_TOO
    // 调用 mk_timer_destroy 函数销毁 _timerPort
    if (MACH_PORT_NULL != rlm->_timerPort) mk_timer_destroy(rlm->_timerPort);
#endif
    ...
}

 可看到 _timerPort 和 CFRunLoopModeRef 一同创建一同销毁的。

 看到 _timerPort 创建时调用了 mk_timer_create 函数,销毁时调用了 mk_timer_destroy 函数,大概这种 mk_timer 做前缀的函数还是第一次见到其中还有两个比较重要的函数:mk_timer.h

#if USE_MK_TIMER_TOO
extern mach_port_name_t mk_timer_create(void); // 创建
extern kern_return_t mk_timer_destroy(mach_port_name_t name); // 销毁
extern kern_return_t mk_timer_arm(mach_port_name_t name, AbsoluteTime expire_time); // 在指定时间发送消息
extern kern_return_t mk_timer_cancel(mach_port_name_t name, AbsoluteTime *result_time); // 取消未发送的消息
...
#endif
  • mk_timer_arm(mach_port_t, expire_time) 在 expire_time 的时候给指定了 mach_port(_timerPort) 的 mach_msg 发送消息。
  • mk_timer_cancel(mach_port_t, &result_time) 取消 mk_timer_arm 注册的消息。

 同一个 run loop mode 下的多个 timer 共享同一个 _timerPort,这是一个循环的流程:注册 timer(mk_timer_arm)—接收 timer(mach_msg)—根据多个 timer 计算离当前最近的下次 handle 时间—注册 timer(mk_timer_arm)。

 在使用 CFRunLoopAddTimer 添加 timer 时的调用堆栈是:

CFRunLoopAddTimer
__CFRepositionTimerInMode
    __CFArmNextTimerInMode
        mk_timer_arm

 mach_msg 收到 timer 事件时的调用堆栈是:

__CFRunLoopRun
__CFRunLoopDoTimers
    __CFRunLoopDoTimer
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__
__CFArmNextTimerInMode
    mk_timer_arm 

 至此 mach_msg 和一些 run loop 相关的 mach port 都看完了,mach_msg 依靠用户态和内核态的切换完成了 run loop 的休眠与唤醒,唤醒操作则是离不开向指定的 mach port 发送消息。那么 mach_msg 就看到这里吧,下篇我们开始学习系统中 run loop 的各种应用。

参考链接

参考链接:

点击这里复制本文地址 以上内容由权冠洲的博客整理呈现,请务必在转载分享时注明本文地址!如对内容有疑问,请联系我们,谢谢!

支持Ctrl+Enter提交

联系我们| 本站介绍| 留言建议 | 交换友链 | 域名展示
本站资源来自互联网收集,仅供用于学习和交流,请遵循相关法律法规,本站一切资源不代表本站立场,如有侵权、后门、不妥请联系本站删除

权冠洲的博客 © All Rights Reserved.  Copyright quanguanzhou.top All Rights Reserved
苏公网安备 32030302000848号   苏ICP备20033101号-1

联系我们