一、项目概述
1.1 布景
在快手电商主播直播iOS场景,咱们遇到了比较多的反常退出状况。其间了很大一部分对错crash,内存OOM以及watchdog类型的。 为此电商客户端团队联合技能渠道稳定性团队发起了主播稳定性管理专项,进行了一系列的技能攻坚,处理了其间许多问题。
本文是沿着其间很有代表性的Mach Port超限问题展开。
而从后边的剖析的定论来看这个问题可能在业界经常出现。
1.2 问题的发现
咱们发现许多快手电商主播,包含许多大V主播在长期开播后经常出现未知原因的反常退出。
咱们的运营同学联系了主播,回捞了几份体系日志后发现是许多 Mach Port 超限导致。
别的还有一部分退出表现为溃散:
那么,mach port是什么? 为什么会产生超限问题?
为此,咱们结合XNU源代码和一些材料展开了专项的技能调研和问题攻坚。
二、Mach Port问题剖析
2.1 根底概念科普
为了搞清楚为什么会产生 mach port超限 问题。 咱们需求先了解一些根底知识。
Mach是什么
图片来自(Mac OS X And iOS Internals)
前史:
Mach作为iOS/Mac OS X 内核Darwin的一部分,是一个面向通讯的操作体系微内核。
Mach是作为传统UNIX内核的替代品出现的,因而其间的不同之处值得留心。当时的人们已逐渐感受到了前期UNIX中“全部皆文件”的笼统机制的不足,有限的扩展性使得开发者捉襟掣肘,苦不堪言。比方UNIX的管道可谓饱受争议。人们迫切需求一个类似管道的机制,答应在程序间交流不同的数据,而不仅仅是文件式的读写。或许换句话说,一套进程间通讯机制(IPC)。
鉴于此,卡耐基梅隆大学从Accent内核项目动身,尝试开发了一套根据共享内存的IPC体系。种种原因之后转向Mach项目,其设计方针大体即一个结构清晰、UNIX兼容、高度可移植的Accent。按以下几个概念作为其根底:
-
“task”即具有一组体系资源的目标,答应“线程”在其间履行。
-
“thread”是履行的根本单位,具有一个使命的上下文,并且共享使命中的资源。
-
“port”是使命间通讯的一组受保护的音讯行列;使命能够对任何port发送或接纳数据。
-
“message”是某些有类型的数据目标的调集,它们只能够发送至port – 而非某特定使命或线程。
Mach Port是什么
Port机制
Port机制在IPC中的应用该是Mach与其他传统内核不一样的当地。在UNIX下,用户进程调用内核只能经过体系调用或陷入(trap)。用户进程运用一个库安排好数据的位置,然后软件触发一个中止,内核在初始化时会为一切中止设置handler,因而程序触发中止的时分,控制权就搬运到了内核,在一些必要的检查之后即可得以进一步操作。
在Mach下,这就交给了IPC体系。与直接体系调用不同,这儿的用户进程是先向内核请求一个port的拜访答应,然后利用IPC机制向这个port发送音讯。虽说发送音讯的操作同样是体系调用,但Mach内核的作业形式有些不同——handler的作业能够交由其他进程完成。
IPC音讯传递机制的应用为线程和并发提供了很好的支持。进程之下是多个线程,线程作为IPC机制的单元,Mach得以在音讯被处理时控制线程睡眠或唤醒。这就答应体系将进程散布于多个处理器之上,音讯直接经过共享内存完成也可,必要时为其它处理器仿制一份也可。在传统内核中这很难完成:体系有必要确保不同处理器上的的不同程序不会在一同拜访同一块内存,在Mach中则要更容易的多。不同进程的内存拜访互不干涉,全部交由port通讯。
Port结构
Mach Port 是受内核保护的单向 IPC 通道、功能和称号。在 Mach 内核中,mach port 被完成成一个有限长度且被内核所保护的音讯行列,其根本操作为发送和接纳音讯。
Port 的这种笼统以及相关的操作是 mach 通讯的根底。一个端口有着与之相相关的内核办理权限,而每个 task 都有必要具有 port 的适当权限才干操作它。当一个 Mach Message 被发送至某个 task 中,只有具有接纳权限的 Mach port 才干接纳该 Message,并将其从行列中删除。
Port权限
每个 Mach Port 都有着对应 port 的权限(right),以下是 Mac OSX 所界说的部分 port right 类型:
- MACH_PORT_RIGHT_SEND:表明权限具有者能够向该端口发送信息
- MACH_PORT_RIGHT_RECEIVE:表明权限具有者能够从该端口中获取 Message
- MACH_PORT_RIGHT_SEND_ONCE:表明发送方只能发送一次 Message。不论该权限是否被毁掉,该句柄始终会发送一条音讯。
- MACH_PORT_RIGHT_PORT_SET:表明多个 port name 的调集,能够被看做是多个端口接纳权限的调集。端口集可用于一同侦听多个端口,类似于 Unix epoll 机制等等。
- MACH_PORT_RIGHT_DEAD_NAME:仅仅一个占位符。若某个端口的权限被毁掉后,则该端口的一切现有句柄的权限都将转换成 dead name(即无效权限)。dead name 机制是为了防止所接收的端口名被过早重用。
若某个端口的接纳权限被释放时,则将该端口视为被毁掉。留意接纳句柄在任何时分都只能有一个 task 所持有。
而端口权限称号(port right name)是某个 task 用来引证所持有的 port right 的特定整数值,有点类似文件描述符。
port name 和 right 的关系,类似于 Unix 中文件描述符和文件描述符权限的关系。
Port类型
Mach Port表明着目标的引证,代表了OS中各类服务、资源等笼统。在 Mach 内核中,相当多的数据结构、服务等等都用 mach port 表明;而用户也能够经过对应的 mach port 来拜访到 tasks、threads以及 memory objects等。
内核请求的接口大部分都带有类型。
而暴露给用户层的 API 是没有类型的。
内核代码中几个重要的相关类型
ipc_space
struct ipc_space {
lck_ticket_t is_lock;
os_ref_atomic_t is_bits; /* holds refs, active, growing */
ipc_entry_num_t is_table_hashed;/* count of hashed elements */
ipc_entry_num_t is_table_free; /* count of free elements */
SMR_POINTER(ipc_entry_table_t XNU_PTRAUTH_SIGNED_PTR("ipc_space.is_table")) is_table; /* an array of entries */
task_t XNU_PTRAUTH_SIGNED_PTR("ipc_space.is_task") is_task; /* associated task */
...
#if CONFIG_PROC_RESOURCE_LIMITS
ipc_entry_num_t is_table_size_soft_limit; /* resource_notify is sent when the table size hits this limit */
ipc_entry_num_t is_table_size_hard_limit; /* same as soft limit except the task is killed soon after data collection */
#endif /* CONFIG_PROC_RESOURCE_LIMITS */
};
进程用于存储ipc目标的结构
ipc_entry
struct ipc_entry {
union {
struct ipc_object *XNU_PTRAUTH_SIGNED_PTR("ipc_entry.ie_object") ie_object;
struct ipc_object *XNU_PTRAUTH_SIGNED_PTR("ipc_entry.ie_object") volatile ie_volatile_object;
};
ipc_entry_bits_t ie_bits;
...
};
#define IE_BITS_UREFS_MASK 0x0000ffff /* 16 bits of user-reference */
#define IE_BITS_UREFS(bits) ((bits) & IE_BITS_UREFS_MASK)
#define IE_BITS_TYPE_MASK 0x001f0000 /* 5 bits of capability type */
#define IE_BITS_TYPE(bits) ((bits) & IE_BITS_TYPE_MASK)
#define IE_BITS_GEN_MASK 0xff000000 /* 8 bits for generation */
#define IE_BITS_GEN(bits) ((bits) & IE_BITS_GEN_MASK)
ipc_entry有一个指向ipc_object的ie_bits指针:
低16位是ipc_entry的uref数量,最大值是0xFFFF
中心5位是ipc_entry对应port的权限。
高8位是指第几代
ipc_port
struct ipc_port {
struct ipc_object ip_object;
...
struct ipc_mqueue ip_messages;
...
mach_port_mscount_t ip_mscount;
mach_port_rights_t ip_srights;
mach_port_rights_t ip_sorights;
...
};
内核层 ipc_port 结构体,对应于单个 mach port。该结构体记载了 Mach message 行列、mach port 的接纳方和发送方 port、内核存储的相关数据等等。
回来到用户层的port name 类型为 32 位;分为两部分:
前 32 – 8 位表明 Index(在is_table中的index);后边 8 位表明 gen(第几代);
#define MACH_PORT_INDEX(name) ((name) >> 8)
#define MACH_PORT_GEN(name) (((name) & 0xff) << 24)
#define MACH_PORT_MAKE(index, gen) \
(((index) << 8) | (gen) >> 24)
ipc_object
struct ipc_object {
ipc_object_bits_t io_bits;
ipc_object_refs_t io_references;
lck_spin_t io_lock_data;
} __attribute__((aligned(8)));
#define IO_BITS_PORT_INFO 0x0000f000 /* stupid port tricks */
#define IO_BITS_KOTYPE 0x000003ff /* used by the object */
#define IO_BITS_KOBJECT 0x00000800 /* port belongs to a kobject */
#define IO_BITS_KOLABEL 0x00000400 /* The kobject has a label */
#define IO_BITS_OTYPE 0x7fff0000 /* determines a zone */
#define IO_BITS_ACTIVE 0x80000000 /* is object alive? */
#define io_active(io) (((io)->io_bits & IO_BITS_ACTIVE) != 0)
#define io_otype(io) (((io)->io_bits & IO_BITS_OTYPE) >> 16)
#define io_kotype(io) ((io)->io_bits & IO_BITS_KOTYPE)
#define io_is_kobject(io) (((io)->io_bits & IO_BITS_KOBJECT) != IKOT_NONE)
#define io_is_kolabeled(io) (((io)->io_bits & IO_BITS_KOLABEL) != 0)
办理目标活泼状况,类型
和引证计数等
Port生命周期
port和port right有别离的生命周期。
2.2 mach port为什么会超限
为什么超限
体系约束
经过阅读源码发现对 mach port 的约束有两种。在 iOS 15 体系上,苹果新增了 posix_spawnattr_set_portlimits_ext 接口来设置 port 的软约束和硬约束。(port_soft_limit, port_hard_limit)。当每次请求 mach port 时,都会检查是否超越 soft 和 hard 的阈值。超越阈值的话就会发送告诉,被看护进程 kill。
经过源码发现,这儿检测的port数其实是 space中的ipc_entry_table_count
/*
* Check if port space has exceeded its limits.
* Should be called with the space write lock held.
*/
void
ipc_space_check_limit_exceeded(ipc_space_t space)
{
size_t size = ipc_entry_table_count(is_active_table(space));
if (!is_above_soft_limit_notify(space) && space->is_table_size_soft_limit &&
((size - space->is_table_free) > space->is_table_size_soft_limit)) {
is_above_soft_limit_send_notification(space);
act_set_astproc_resource(current_thread());
} else if (!is_above_hard_limit_notify(space) && space->is_table_size_hard_limit &&
((size - space->is_table_free) > space->is_table_size_hard_limit)) {
is_above_hard_limit_send_notification(space);
act_set_astproc_resource(current_thread());
}
}
#endif /* CONFIG_PROC_RESOURCE_LIMITS */
Port走漏
经过咱们获取的用户日志来看,mach port超限被杀需求的mach port数量超越12万。而在
- 正常的APP运行过程中用于IPC通讯不需求有如此多的端口。
- mach port手动办理相关目标的引证计数,port超限被体系强杀以及溃散出现在用户长期运用的状况下。
所以估测原因是出现了mach port的走漏,即请求了port权限运用后没有释放。
问题定位过程
获取port信息
获取当时port 数量 经过调研咱们发现能够用
mach_port_names函数获取到进程的port列表 包含port name和权限type(right)信息。 其间也包含进程和线程。
mach_port_get_ref函数能够获取到当时port right对应权限的引证数量。
mach_port_kernel_object函数能够获取到port对应的类型信息,比方 IKOT_NONE IKOT_THREAD_CONTROL ,IKOT_TASK_CONTROL 等等。
mach_port_space_info函数获取到进程的port列表比对mach_port_names多了MACH_PORT_TYPE_NONE类型的ipc_entry。此外回来的数据还包含了引证数量信息。
线上监控
根据此,咱们依据mach_port_names做了线上mach port数量的监控,在页面切换的时分记载mach port的数量,并在产生OOM的时分上报上来。
经过线上的上报的数据进行剖析后,能够发现在用户进行开播操作的时分port增速较快。
线下监控
了解了 mach port 的根本原理后,咱们调研了一些开放的东西,包含苹果的 lsmp, top 等命令行东西。 这些东西都都仅仅显示具体的 port 称号,引证计数(权限)类型等,可是要想定位问题就需求这些 mach port 具体是谁在运用或许谁来请求的。因而咱们根据 instruments 模版功能完成了自研的 mach port 追寻东西,instrument 提供了 DynamicStackTracing 接口能够获取仓库信息,因而咱们尝试 hook 了 mach port 的请求和通讯接口,来追寻 mach port 的生命周期。
虽然 hook了port 请求接口 和 通讯接口,可是运用该东西后仍然发现收集的信息不全,有许多的无法获取类型的 mach port 无法盯梢到仓库。
阶段一管理
在前期没有找到port走漏真正原因的状况下,挑选经过一些手法绕过这个问题。在不影响用户体会的前提下,经过判断阈值来静默提早退出 app。
不过这种对用户体会的有损措施始终不是长久之计。
后续,咱们又从问题本身动身尝试进一步的探究。
线下debug
于是尝试在线下调试用户频频开播场景,连续进行开播关播操作时,暂停当时程序履行。发现了一个特征:
-
用户port数量快速增加的时分当时有较多的新增线程
-
且新增的port数量和新增线程数量正相关
结合之前咱们对于XNU代码的研讨,咱们了解到:线程本身也持有本身用于通讯的port。
所以咱们排查的要点放到了线程port走漏上。
经过进一步阅读代码,进行收拾。
每个线程本身都会持有port,要拜访到线程的信息,需求当时进程有对线程port有发送的权限。
经过阅读代码发现当线程创立的时分就会创立对应的port: kern/ipc_tt.c的ipc_thread_init函数
void
ipc_thread_init(
thread_t thread,
ipc_thread_init_options_t options)
{
ipc_port_t kport;
ipc_port_t pport;
ipc_kobject_alloc_options_t alloc_options = IPC_KOBJECT_ALLOC_NONE;
...
pport = ipc_kobject_alloc_port((ipc_kobject_t)thread,
IKOT_THREAD_CONTROL, alloc_options);
kport = ipc_kobject_alloc_labeled_port((ipc_kobject_t)thread,
IKOT_THREAD_CONTROL, IPC_LABEL_SUBST_THREAD, IPC_KOBJECT_ALLOC_NONE);
kport->ip_alt_port = pport;
...
thread->ith_thread_ports[THREAD_FLAVOR_CONTROL] = kport;
thread->ith_settable_self = ipc_port_make_send(kport);
...
}
-
创立了IKOT_THREAD_CONTROL类型的port
-
经过ipc_port_make_send对port增加了send权限
当线程毁掉时,kern/ipc_tt.c的ipc_thread_terminate函数
void
ipc_thread_terminate(
thread_t thread)
{
...
kport = thread->ith_thread_ports[THREAD_FLAVOR_CONTROL];
iport = thread->ith_thread_ports[THREAD_FLAVOR_INSPECT];
rdport = thread->ith_thread_ports[THREAD_FLAVOR_READ];
pport = thread->ith_self;
if (kport != IP_NULL) {
if (IP_VALID(thread->ith_settable_self)) {
ipc_port_release_send(thread->ith_settable_self);
}
...
}
...
if (pport != kport && pport != IP_NULL) {
/* this thread has immovable contorl port */
ip_lock(kport);
kport->ip_alt_port = IP_NULL;
ipc_kobject_set_atomically(kport, IKO_NULL, IKOT_NONE);
ip_unlock(kport);
ipc_port_dealloc_kernel(pport);
}
...
}
这儿会对线程port进行毁掉。
在剖析了线程生命周期内对port的操作后,将排查的要点放到可能操作线程port的函数调用上:
这儿沿着运用到thread port的API,结合XNU内核中完成的代码进行排查。
沿着会增加port引证的办法来看,发现了在 task_threads_internal办法(task_threads的内部办法) 中:
-
调用了convert_thread_to_port_pinned办法(内部调用了ipc_port_make_send)对port增加了send权限。
-
结合之前的现象:新增的port数量和新增线程数量正相关。它也做了线程的遍历增加权限。
结合上面的剖析,怀疑task_threads办法嫌疑最大,这儿看一下它的完成中操作port的代码:
ipc_port_t
convert_thread_to_port_pinned(
thread_t thread)
{
ipc_port_t port = IP_NULL;
thread_mtx_lock(thread);
if (thread->ipc_active && thread->ith_self != IP_NULL) {
port = ipc_port_make_send(thread->ith_self);
}
thread_mtx_unlock(thread);
thread_deallocate(thread);
return port;
}
static kern_return_t
task_threads_internal(
task_t task,
thread_act_array_t *threads_out,
mach_msg_type_number_t *countp,
mach_thread_flavor_t flavor)
{
...
thread_list = NULL;
...
i = 0;
queue_iterate(&task->threads, thread, thread_t, task_threads) {
assert(i < actual);
thread_reference(thread);
thread_list[i++] = thread;
}
...
switch (flavor) {
case THREAD_FLAVOR_CONTROL:
if (task == current_task()) {
for (i = 0; i < actual; ++i) {
((ipc_port_t *) thread_list)[i] = convert_thread_to_port_pinned(thread_list[i]);
}
}
...
}
return KERN_SUCCESS;
}
进行相应的调试
手动创立线程
创立后多次调用task_threads
//创立线程,线程sleep 10秒后毁掉
for (int i = 0; i < 100; i++) {
createTestThread();
//printMachPortCount();
}
//多次调用task_threads
for (int i = 0; i < 10; i++) {
kern_return_t kr;
thread_array_t thread_list;
mach_msg_type_number_t thread_count;
kr = task_threads(mach_task_self(), &thread_list, &thread_count);
vm_deallocate(mach_task_self(), (vm_offset_t)thread_list, thread_count * sizeof(thread_t));
printMachPortCount();
}
//获取port信息
- (void)printMachPortCount
{
mach_port_name_array_t rightNames;
mach_msg_type_number_t rightNamesCount = 0;
mach_port_type_array_t rightTypes;
mach_msg_type_number_t rightTypesCount = 0;
unsigned int index;
kern_return_t kr;
kern_return_t junk;
kr = mach_port_names(mach_task_self(), &rightNames, &rightNamesCount, &rightTypes, &rightTypesCount);
for (index = 0; index < rightNamesCount; index++) {
unsigned int kotype = 0;
vm_offset_t kobject = (vm_offset_t)0;
kr = mach_port_kernel_object(mach_task_self(), rightNames[index], &kotype, (unsigned *)&kobject);
mach_port_urefs_t refs;
mach_port_get_refs(mach_task_self(), rightNames[index], MACH_PORT_RIGHT_SEND, &refs);
NSLog(@"name:%d refs:%d index:%d right_type:%d kotype:%d", rightNames[index], refs, index, rightTypes[index] >> 16, kotype);
}
if (rightNames != NULL) {
junk = vm_deallocate(mach_task_self(), (vm_address_t)rightNames, rightNamesCount * sizeof(*rightNames));
}
if (rightTypes != NULL) {
junk = vm_deallocate(mach_task_self(), (vm_address_t)rightTypes, rightTypesCount * sizeof(*rightTypes));
}
}
为了便利检查,这儿筛选同一个name的线程port,能够看到随着task_threads的调用:
- refs不断增加
- 直到线程毁掉后,发现对应name的port还存在。并没有释放,可是获取到的refs为0
能够发现在这儿线程port产生了走漏
一同经过debug发现,从对应线程毁掉后开端调用mach_port_kernel_object和mach_port_get_refs都会直接回来过错15( KERN_INVALID_NAME: The name doesn’t denote a right in the task.)
其实这儿也走了一些弯路,之前在没有特别了解源码的时分经过mach_port_kernel_object和mach_port_get_refs去获取走漏port的引证和类型,取到的数据都是无类型的,导致一段时间都没有把走漏的port和IKOT_THREAD_CONTROL类型相关上。这儿其实是线程毁掉时ipc_port_dealloc_kernel清理了port的active状况。
这儿咱们替换获取refs的方式改为mach_port_space_info时,能够发现线程毁掉后ref的数量并没有削减
再经过代码测试:
每秒创立一百个线程,一个线程存活10s,十秒后port数量稳定在一个数量。
每秒创立一百个线程,一个线程存活10s 并每秒调用一次task_threads时,port数持续增长,且每次增长的数量和新增线程数相当。这个也和咱们在项目代码中看到的现象符合。
回到项目代码中,咱们看到许多高频对于task_threads的调用,首要用来做CPU的监控。运用姿势也是和上文中贴的会形成走漏的逻辑类似。
得出定论:task_threads是形成问题的重要原因。
为什么事务运用的代码会有问题
有趣的是,当咱们搜索 “iOS CPU 运用率”,出来的搜索结果里面的代码 甚至 包含 stackoverflow中的高赞答案,都有这种port走漏状况。
stackoverflow.com/questions/8…
怎么处理这个问题
参考源码,在task_threads的运用过程中有一个比较重要的留意事项,也是在官方文档中没有清晰提及的。就是对于回来的thread_list的mach port释放:调用mach_port_deallocate。 这儿是XNU test的部分代码:
static void
test_task_port(mach_port_name_t port, int type)
{
kern_return_t kr;
...
/************ TASK_THREADS ************/
thread_array_t th_list;
mach_msg_type_number_t th_cnt = 0;
kr = task_threads(port, &th_list, &th_cnt);
check_result(kr, type, POLY, INSPECT, "task_threads", victim);
/* Skip thread ports tests if task_threads() fails */
if (kr != KERN_SUCCESS) {
return;
}
...
for (unsigned int i = 0; i < th_cnt; i++) {
test_thread_port(th_list[i], type, victim); /* polymorphic */
kr = mach_port_deallocate(mach_task_self(), th_list[i]);
T_QUIET; T_EXPECT_MACH_SUCCESS(kr, "mach_port_deallocate");
}
}
在增加mach_port_deallocate调用后,mach port数也不再持续增加:
阶段二管理
在清晰问题之后,推动管理相对比较简单:就是推动修改各个事务对task_threads函数的不恰当运用。
从找到的调用来看,许多事务都会去高频的做CPU用量监控,一同调用的代码和网上搜”iOS CPU 运用率”的代码写法一样。
在连续两个版别找到task_threads调用后没有调用mach_port_deallocate后根本上线上不恰当运用的当地根本现已修正完成。
2.3 mach port超限管理收益及防劣化机制
收益
在最近几个版别推动各个事务的代码修改后。电商主播直播场景下,在最新版别对比前期版别的Mach Port超限的反常退出问题从每周几百次降到0。
防劣化
当时port形成问题对事务的影响根本上现已很小了。 可是后续还是需求做防劣化的能力。
目前防劣化分为三个思路:
- 对现有线上的问题数进行监控并进行天级的问题推送
- 对现有形成port走漏的要点函数进行静态检测
- 在调用task_threads的函数中检测是否调用了mach_port_deallocate
- 优化现有的监控能力,对走漏的port进行引证数,相关kernel目标类型进行上报
- 高频收集
- 对创立线程等行为进行hook
- 经过port_name相关后续走漏的port找到他开始的类型
以上就是本文一切内容
快手电商无线技能是公司的中心事务线, 这儿云集了各路高手, 也充满了机会与应战. 伴随着事务的高速开展, 团队也在快速扩张. 欢迎各位高手参加咱们, 一同创造世界级的电商产品。
咱们等待你的参加!hi, 我是快手电商的fx,备注我的诨名成功率更高哦,请发简历到:
hr.ec@kuaishou.com