一、简介
阅读XNU源码,从中我们可以学到什么?我们可以理解线程和线程容器,可以理解Mach是如何管理虚拟内存,可以看到IPC和Mach中很重要的消息机制。
iOS的内核是Darwin
Darwin的内核是XNU
XNU由Mach、BSD、IOKit组成
- 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消息传递步骤
- 分配mach_port端口
kern_return_t kret = mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &serverPort);
- 通过Task赋予权限
kret = mach_port_insert_right(mach_task_self(), serverPort, serverPort, MACH_MSG_TYPE_MAKE_SEND);
- 建立接收端口的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);
}
- 发送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语言方法一样。
- MIG是一个工具,可以通过定义文件生成Client-Server形式基于mach IPC的RPC代码(这里mach IPC指使用mach port传递mach msg)
- 典型的IPC代码需要实现:数据准备、发送、接收、解包、消息复用;MIG自动生成完成这些事情的代码
- MIG生成的代码,调用mach ports的各种API
- MIG定义文件的扩展名.defs
- 下面列举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
- 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…