[转载] 快手主播稳定性 - Mach Port 超限问题治理

[转载] 快手主播稳定性 - Mach Port 超限问题治理

原文地址

本文介绍了快手客户端团队处理的 Mach Port 超限导致异常退出的问题。介绍了从发现到解决的全过程,并且给出了监控和防劣化方案。这个问题对快手稳定性尤其是电商主播稳定性影响很大且难以察觉,同时可能在业界广泛出现。

项目概述

1.1 背景

在快手电商主播直播iOS场景,我们遇到了比较多的异常退出情况。其中了很大一部分是 crash,内存 OOM 以及 watchdog 类型的。为此电商客户端团队联合主站技术平台稳定性团队发起了主播稳定性治理专项,进行了一系列的技术攻坚,解决了其中很多问题。

本文是沿着其中很有代表性的Mach Port超限问题展开。

而从后面的分析的结论来看这个问题可能在业内经常出现。

1.2 问题的发现

我们发现很多快手电商主播,包括很多大V主播在长时间开播后经常出现未知原因的异常退出。

我们的运营同学联系了主播,回捞了几份系统日志后发现是很多 Mach Port 超限导致。

descript

另外还有一部分退出表现为崩溃:

descript

那么,mach port是什么?为什么会发生超限问题?

为此,我们结合XNU源代码和一些资料展开了专项的技术调研和问题攻坚。

Mach Port问题分析

2.1 基础概念科普

为了搞清楚为什么会发生 mach port超限 问题。我们需要先了解一些基础知识。

Mach是什么

descript

历史:

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等。

descript

内核申请的接口大部分都带有类型。
而暴露给用户层的 API 是没有类型的。

内核代码中几个重要的相关类型

descript

ipc_space

1
2
3
4
5
6
7
8
9
10
11
12
13
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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

1
2
3
4
5
6
7
8
9
10
11
12

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(第几代);

1
2
3
4
5

#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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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有分别的生命周期。

descript

descript

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/*
* 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的时候上报上来。

descript

descript

通过线上的上报的数据进行分析后,可以发现在用户进行开播操作的时候port增速较快。

线下监控

了解了 mach port 的基本原理后,我们调研了一些开放的工具,包括苹果的 lsmp, top 等命令行工具。这些工具都都只是显示具体的 port 名称,引用计数(权限)类型等,但是要想定位问题就需要这些 mach port 具体是谁在使用或者谁来申请的。因此我们基于 instruments 模版功能实现了自研的 mach port 追踪工具,instrument 提供了 DynamicStackTracing 接口可以获取堆栈信息,因此我们尝试 hook 了 mach port 的申请和通信接口,来追踪 mach port 的生命周期。

descript

虽然 hook了port 申请接口 和 通讯接口,但是使用该工具后依然发现采集的信息不全,有大量的无法获取类型的 mach port 无法跟踪到堆栈。

阶段一治理

在早期没有找到port泄漏真正原因的情况下,选择通过一些手段绕过这个问题。在不影响用户体验的前提下,通过判断阈值来静默提前退出 app。

不过这种对用户体验的有损措施始终不是长久之计。

后续,我们又从问题本身出发尝试进一步的探索。

线下debug

于是尝试在线下调试用户频繁开播场景,连续进行开播关播操作时,暂停当前程序执行。发现了一个特征:

  • 用户port数量快速增加的时候当前有较多的新增线程

  • 且新增的port数量和新增线程数量正相关

结合之前我们对于XNU代码的研究,我们了解到:线程本身也持有自身用于通讯的port。

所以我们排查的重点放到了线程port泄漏上。

通过进一步阅读代码,进行整理。
每个线程自身都会持有port,要访问到线程的信息,需要当前进程有对线程port有发送的权限。

descript

通过阅读代码发现当线程创建的时候就会创建对应的port:kern/ipc_tt.cipc_thread_init函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
//创建线程,线程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

descript

可以发现在这里线程port发生了泄漏

同时通过debug发现,从对应线程销毁后开始调用mach_port_kernel_object和mach_port_get_refs都会直接返回错误15(KERN_INVALID_NAME: The name doesn’t denote a right in the task.)

descript

其实这里也走了一些弯路,之前在没有特别熟悉源码的时候通过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的数量并没有减少

descript

再通过代码测试:

每秒创建一百个线程,一个线程存活10s,十秒后port数量稳定在一个数量。

descript

每秒创建一百个线程,一个线程存活10s
并每秒调用一次task_threads时,port数持续增长,且每次增长的数量和新增线程数相当。这个也和我们在项目代码中看到的现象吻合。

descript

回到项目代码中,我们看到很多高频对于task_threads的调用,主要用来做CPU的监控。使用姿势也是和上文中贴的会造成泄漏的逻辑类似。

得出结论:task_threads是造成问题的重要原因。

为什么业务使用的代码会有问题

有趣的是,当我们**搜索 “iOS CPU 使用率”**,出来的搜索结果里面的代码 甚至
包括 stackoverflow中的高赞答案,都有这种port泄漏情况。

descript

如何解决这个问题

参考源码,在task_threads的使用过程中有一个比较重要的注意事项,也是在官方文档中没有明确提及的。就是对于返回的thread_list的mach port释放:调用mach_port_deallocate。这里是XNU test的部分代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

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数也不再持续增加:

descript

阶段二治理

在明确问题之后,推进治理相对比较简单:就是推进修改各个业务对task_threads函数的不恰当使用。

从找到的调用来看,很多业务都会去高频的做CPU用量监控,同时调用的代码和网上搜”iOS
CPU 使用率”的代码写法一样。

在连续两个版本找到task_threads调用后没有调用mach_port_deallocate后基本上线上不恰当使用的地方基本已经修复完成。

2.3 mach port超限治理收益及防劣化机制

收益

在最近几个版本推进各个业务的代码修改后。电商主播直播场景下,在最新版本对比早期版本的Mach Port超限的异常退出问题从每周几百次降到0

防劣化

当前port造成问题对业务的影响基本上已经很小了。但是后续还是需要做防劣化的能力。

目前防劣化分为三个思路:

  1. 对现有线上的问题数进行监控并进行天级的问题推送

  2. 对现有造成port泄漏的重点函数进行静态检测

    1. 在调用task_threads的函数中检测是否调用了mach_port_deallocate
  3. 优化现有的监控能力,对泄漏的port进行引用数,关联kernel对象类型进行上报

    1. 高频采集

    2. 对创建线程等行为进行hook

    3. 通过port_name关联后续泄漏的port找到他最初的类型

-------------本文结束感谢您的阅读-------------

欢迎关注我的其它发布渠道