iOS - XNU Overview

1,340 阅读8分钟

一、简介

阅读XNU源码,从中我们可以学到什么?我们可以理解线程和线程容器,可以理解Mach是如何管理虚拟内存,可以看到IPC和Mach中很重要的消息机制。

iOS的内核是Darwin

Darwin的内核是XNU

XNU由Mach、BSD、IOKit组成

XNU是一种混合式核心Hybrid Kernel,将宏内核微内核两者的特性兼收并蓄,以期同时拥有两种内核的优点

  1. 比如在微内核中提高操作系统模块化程度以及让操作系统更多的部分接受内存保护消息传递机制

  2. 宏内核在高负荷下表现的高性能

  • Mach是微内核,只负责最基本的职责
  • BSD是基于Unix开发的
  • IOKit负责硬件信息

Mach的核心概念是IPC

主要基于mach_port,mach_msg消息传递机制

二、Mach

微内核负责进程和线程抽象、虚拟内存管理、任务管理以及进程间通信和消息传递机制。

Mach微内核的主要抽象

  • Tasks
  • Threads
  • Address space
  • Memory objects
  • Ports
  • IPC
  • Time

任务和线程

任务,最小的资源持有单位

线程,最小的调度单位

为什么要划分进程?不可能一台机器上只跑一个程序吧。

为什么有了进程,还需要划分线程?进程切换的开销太大了,那么粒度再小一些、轻量一些,不要独立持有资源了,只要调度就可以,所以就有了线程。

但是在XNU中其实没有Process的概念,因为太精简了,所以用Task做为是线程的容器。

在简单iOS开发中,一个APP是一个进程(即一个Task),在APP内部我们可以通过各种方式进行Thread调度,至于线程调度就不细说了,大家都懂。

我们通过代码来看看,进程线程模型可以用来做什么吧。


+ (float)cpuUsage {

  kern_return_t kr;

  task_info_data_t tinfo;

  mach_msg_type_number_t task_info_count;

   

  task_info_count = TASK_INFO_MAX;

  kr = task_info(mach_task_self(), TASK_BASIC_INFO, (task_info_t) tinfo, &task_info_count);

  if (kr != KERN_SUCCESS) {

    return -1;

  }

   

  thread_array_t thread_list;

  mach_msg_type_number_t thread_count;

  kr = task_threads(mach_task_self(), &thread_list, &thread_count);

  if (kr != KERN_SUCCESS) {

    return -1;

  }

   

  float tot_cpu = 0;

   

  for (int j = 0; j < thread_count; j++) {

    thread_info_data_t thinfo;

    mach_msg_type_number_t thread_info_count = THREAD_INFO_MAX;

    kr = thread_info(thread_list[j], THREAD_BASIC_INFO, (thread_info_t)thinfo, &thread_info_count);

    if (kr != KERN_SUCCESS) {

      return -1;

    }

    thread_basic_info_t basic_info_th = (thread_basic_info_t)thinfo;

    if (!(basic_info_th->flags & TH_FLAGS_IDLE)) {

      tot_cpu = tot_cpu + basic_info_th->cpu_usage / (float) TH_USAGE_SCALE * 100.0;

    }

  }

   

  kr = vm_deallocate(mach_task_self(), (vm_address_t) thread_list, thread_count * sizeof(thread_t));

  return tot_cpu;

}

先说一下kernel层的交互礼仪吧,这个礼仪就是kern_return_t result,我们在使用跟内核交互的方法的时候,都需要判断result是否等于KERN_SUCCESS,跟内核通信失败的话就不用处理逻辑了。

这段代码的主流程其实比较清晰

  • 获取self_task(进程)
  • 获取进程中的所有线程
  • 遍历线程
  • 从线程的thread_info中获取cpu相关信息
  • 把所有CPU使用的数据叠加和计算一下
  • 得到整体CPU的使用率,任务完成

再来看一段代码加深理解一下。


+ (long)currentMemoryUsage {

  task_vm_info_data_t vmInfo;

  mach_msg_type_number_t count = TASK_VM_INFO_COUNT;

  kern_return_t result = task_info(mach_task_self(), TASK_VM_INFO, (task_info_t) &vmInfo, &count);

  if (result != KERN_SUCCESS) return 0;

  return (long)(vmInfo.phys_footprint);

}

这里是从Task获取了内存占用相关的信息。

所以为什么内存占用相关的信息只需要从Task获取,但是CPU相关的信息需要遍历Thread获取?

总结一下,Task是Threads的容器,Task是独立持有资源的单位,Task也有获取自身信息的函数和结构体,task_info等。每个Thread是最小调度单位,不过资源是在Task中大家共享的,Thread也有自身相关信息相关的信息,thread_info,thread_state,thread_basic_info等。

虚拟内存管理

操作系统中有2大抽象,其一是CPU指令集,其二就是虚拟内存。我管你是什么CPU,只要你支持这个指令集,我就按指令集写我的程序。同理,我管你是内存还是磁盘,中间又是怎么映射,怎么通过MMU处理,巴拉巴拉,我只要管理我的虚拟内存就可以。而且一个进程对应一段独有的虚拟内存,完全不用担心内存写写删删,影响了其他进程的内存区域。

我们在上面的实例代码中有没有看到这么一句代码?


kr = vm_deallocate(mach_task_self(), (vm_address_t) thread_list, thread_count * sizeof(thread_t));

这是释放一段虚拟内存区域的代码。

太复杂了,完全看不懂。没关系,我们至少知道了mach里面的代码有很多vm处理相关的文件。 还看到了很多vm创建销毁的方法。 总结,的确mach的代码对虚拟内存进行了管理的。

内存还是太复杂了一些,这边不做详细阐述了,比如resident_size跟phys_footprint的区别,比如clean/dirty memory,比如page in/out,比如swap in/out,还有compressor,zoneMapSize等等。

Mach Port

Mach的核心概念是IPC,而IPC主要是基于Port实现。

Mach Port的概念和Socket有些类似,都是用来发送和接收消息。

也与UNIX单向管道类似,是由内核管理的消息队列。有多个发送方和一个接收方。

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

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

Mach Port组成

  • Port 端口
  • Port权限,Task信息是系统资源的集合,也可以说是资源的所有权。这些Task允许您访问Port(发送,接收,发送一次),称为Port权限。(也就是说,Port权限是Mach的基本安全机制。)

    • 发送权限,不受限制地将数据插入到特定的消息队列中
  • 一次发送权限,将单个消息数据插入到特定的消息队列中
  • 接收权限,不受限制地从特定消息队列中提取数据
  • Port Set,一组有权限的端口,在接收来自某个成员的消息或事件时,可以将其视为单个单元。
  • Port Set权限
  • Message mach_msg的消息结构

Mach Port消息传递步骤

  1. 分配mach_port端口
kern_return_t kret = mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &serverPort);
  1. 通过Task赋予权限
kret = mach_port_insert_right(mach_task_self(), serverPort, serverPort, MACH_MSG_TYPE_MAKE_SEND);
  1. 建立接收端口的Handler
mach_msg_header_t header;

header.msgh_bits = 0;

header.msgh_size = sizeof(mach_msg_header_t) * 2;

header.msgh_remote_port = MACH_PORT_NULL;

header.msgh_local_port = mach_port;

header.msgh_id = 0;

mach_msg_return_t msgReturn;

while (true) {

msgReturn = mach_msg(&header, MACH_RCV_MSG | MACH_RCV_LARGE, 0, header.msgh_size, header.msgh_local_port, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL);

if (msgReturn != MACH_MSG_SUCCESS && msgReturn != MACH_RCV_TOO_LARGE) {

NSLog(@"error");

}

mach_msg_id_t msg_id = header.msgh_id;

mach_port_t remote_port = header.msgh_remote_port;

mach_port_t local_port = header.msgh_local_port;

NSLog(@"Receive a mach message:[%d], remote_port: %d, local_port: %d, exception code: %d", msg_id, remote_port, local_port);

}
  1. 发送mach_message
kern_return_t kret;

mach_msg_header_t header;

header.msgh_bits = MACH_MSGH_BITS(MACH_MSG_TYPE_COPY_SEND, 0);

header.msgh_size = sizeof(mach_msg_header_t);

header.msgh_remote_port = mach_port;

header.msgh_local_port = MACH_PORT_NULL;

header.msgh_id = 100;

NSLog(@"Send a mach message: [%d]", header.msgh_id);

kret = mach_msg(&header, MACH_SEND_MSG, header.msgh_size, 0, MACH_PORT_NULL, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL);

可能大家都有一个疑问,为什么这边很多数据类型都是通过typedef mach_port_t得到的,说明在内核进程之间的调用关系都是通过mach_port实现的。

IPC

Mach支持Client/Server结构,其中Client Task通过在消息通道向其他Server Task发送消息请求来访问服务。这些通信通道在Mach中的端点就是Port,端口的权限表示使用通道的权限。

Mach提供的IPC形式包括

  • Message queues
  • Semaphores
  • Notifications
  • Lock sets
  • Remote procedure calls(RPCs)

通过Mach Port就是走Message queues的形式,其他几种形式不再阐述。

MIG

Mach Interface Generator是一种接口定义语言编译器。通过接口定义,把详细传递包装成对对象方法的调用。从而隐藏了IPC和Port的细节,方便作为RPC调用和使用,就像调用C语言方法一样。

  1. MIG是一个工具,可以通过定义文件生成Client-Server形式基于mach IPC的RPC代码(这里mach IPC指使用mach port传递mach msg)
  2. 典型的IPC代码需要实现:数据准备、发送、接收、解包、消息复用;MIG自动生成完成这些事情的代码
  3. MIG生成的代码,调用mach ports的各种API
  4. MIG定义文件的扩展名.defs
  5. 下面列举MIG实现的一些功能:
  • clock_priv, clock_reply, exc, host_notify_reply, host_priv, host_security, ledger, mach_exc, mach_host, mach_vm, map, memory_object_default, memory_object_name, notify, processor, processor_set, prof, security, semaphore, sync, task_access, thread_act, vm_map
  1. MIG生成的代码,丰富了接口,提供了更多( BSD层没有的)功能,非常有用:
  • 比如host相关的API可以获取硬件信息
  • mach_vm和vm_map相关API提供了虚拟内存操作接口
  • thread_act相关API提供了thread操作接口

时间管理

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

mach_timespec_tAPI 在 OS X 中已被弃用。较新的首选 API 基于计时器对象,后者把AbsoluteTime用作基本数据类型。AbsoluteTime是一种依赖于机器的类型,通常基于平台原生时基。提供了例程来将AbsoluteTime值与其他数据类型(例如纳秒)相互转换。计时器对象支持异步、无漂移通知、取消和过早警报。它们比时钟更有效并且允许更高的分辨率。

引用

xnu源码: github.com/apple/darwi…

Mach Overview: developer.apple.com/library/arc…