序
在开发的日常中,经常会遇到一些极端偶现的Bug,有些Bug很难以复现,所以一般的解决计划是接入PLCrashReporter这些第三方的溃散核算东西,从保存的溃散文件中读取相应的溃散信息。那么这些溃散核算东西又是依据什么原理运作的呢?我对此产生了很大的兴趣,所以对此做了一些调研,以下是我的成果:
Task & Thread & Process
在谈到运用溃散之前,首要需求知道的是,iOS操作体系的内核是XNU,它是一个混合内核,而这个混合内核的中心便是Mach这个微内核。
Process
操作体系被规划作为一个渠道,而运用运转在这个渠道之上。**每一个运转中的运用的实例都是一个进程(process)。**当然,一般情况下咱们描述的是用户角度的进程。和许多使命的体系一样,一个可履行程序的一个实例便是一个进程,UNIX也是依据这个概念创立的。而每一个实例都经过一个独有的Process ID来标识(PID),即使是同一个可履行程序的不同实例,也是有不同的PID的。而许多进程进一步或许成为进程组,一般经过向一个Group发送信息,用户能够控制多个进程。一个进程能够经过调用setpgrp(2)
来参加进程组。
而在BSD这一层,BSD Process则更为详细一些,包括了内部的多个线程,以及对应的Mach Task等等。
Task
首要要说到的便是Mach中的Task这个概念,Mach Task是体系资源的集合,每一个Task都包括了一个虚拟的地址空间(分配内存),一个端口权限名称空间,还有一个或许几个线程。在Mach内核中,Task是体系分配资源的基本单位。它和咱们熟悉的进程的概念是十分相识的,可是Mach Task和Process是有差异的,比较而言Mach Task要供给更少的功用。在Process中,有信号、组、文件描述符等等。而Mach Task用于资源的分配和同享,它是资源的容器。
因为Mach是XNU这个混合内核中的微内核,所以Mach中的Mach Task是无法供给其他操作体系中的“进程”中的逻辑的,Mach Task仅仅供给了最重要的一些基础的完成,作为资源的容器。
而在BSD层中,BSD的process(其实也便是iOS的进程)和Mach Task是一一对应的。
Thread
理论上,Thread是CPU调度的基本单位。iOS中的进程和POSIX 线程(pthread)是别离依据Mach task和Mach thread的顶层完成。一个线程是相当轻量级的实体,创立一个新线程和操作一个线程的开销是十分低的。
Mach threads是在内核中被完成的,Mach thread是最基本的核算实体,它归于且仅归于一个Mach task,这个Mach task界说了线程的虚拟地址内存空间。值得一提的是POSIX线程模型是除Windows之外,一切的操作体系都支撑的一套标准的线程API,而iOS和OS X比其他体系都要愈加支撑pthread
。
Mach Task是没有自己的生命周期的,因为它并不会去履行使命,只要线程才会履行指令。当它说“task Y does X”的时分,这其实意味着“包括在task Y中的一个线程履行了X操作”。
疑问
因为Task是XNU的微内核Mach独有的,这个就和咱们熟知的进程,线程等等会有一些差异,所以这儿就提出了几个问题
1、Task和进程到底是什么联系?
首要要清晰的是task和进程是一一对应的联系,从springborad翻开的每一个进程,其实在内核里都有一个task与之对应。Task仅仅进程资源的容器,并不具有一般进程应该拥有的功用。
2、进程和线程到底是什么差异?
线程是资源调度的最小单位。
进程是资源分配的最小单位,而在OS X以及iOS体系中,每一个进程对应的仅有资源容器便是Task。
反常的简述
运用一般运转在用户态的,可是当运用需求去自动运用体系调用,或许说在被迫遇到一些反常或许中断的时分,运用都会有用户态进入到内核态,这个时分相当于体系收回了运用的运转权限,它要在内核态中去做一些特殊的处理。(system calls, exceptions, and interrupts)
而**接下来咱们要说的反常(Exception),它就会运用由用户态进入到内核态。**这儿就学习了腾讯Bugly的一张图来表明这种联系:
可是在iOS中一切的反常都会使得运用从用户态进入到内核态吗?
反常的分类
在所遇到的场景中,反常基本只要一种产生的原因,那便是工程师写的代码呈现了问题,然后导致了反常的产生,引起了程序的溃散。而产生的反常成果能够分类为两类:一种是硬件反常,一种是软件反常。
比方咱们做了一个除0操作,这在CPU履行指令的时分呈现指令反常,这便是一个hardware-generated 反常,再比方咱们写Objective-C业务的进程中,给一个不存在的目标发送了音讯,在Runtime时会抛出反常,这便是software-generated 反常。当然了假如不做处理他们都会导致程序的溃散,而假如要做处理,那就需求知道怎么去捕获这些反常。
这儿再重复一下:**尽管都是咱们写的软件过错,可是形成的反常成果却或许是硬件反常,亦或是软件反常,**而只要硬件反常才会产生上述的用户态到内核态的转化。
Mach Exception
Mach Exception的传递
在上面咱们说到了硬件反常,硬件反常会产生用户态→内核态的转化,那么有哪些反常归于硬件反常呢?
- 企图拜访不存在的内存
- 企图拜访违反地址空间保护的内存
- 因为不合法或未界说的操作代码或操作数而无法履行指令
- 产生算术过错,例如被零除、上溢、或许下溢
- ……
以上这些都归于硬件反常,可是这些硬件反常和咱们说到的Mach Exception有什么联系呢?
Mach内核供给了一个依据IPC的反常处理东西,其间反常被转化为message。当反常产生的时分,一条包括反常的mach message,例如反常类型、产生反常的线程等等,都会被发送到一个反常端口。而线程(thread),使命(task),主机(host)都会维护一组反常端口,当Mach Exception机制传递反常音讯的时分,它会按照thread → task → host
的次序传递反常音讯(这三者便是线程,进程,和体系的递进联系),假如这三个等级都没有处理反常成功,也便是收到KERN_SUCCESS
成果,那么内核就会停止该进程。在/osfmk/kern/exception.c
的源码中会经过exception_trige()
办法来进行上述音讯传递的流程,此办法内部调用exception_deliver()
往对应等级的反常端口发送信息:
// 源码地址:https://opensource.apple.com/source/xnu/xnu-2050.24.15/osfmk/kern/exception.c
void exception_trige(
exception_type_t exception,
mach_excpetion_data_t code,
mach_msg_type_number_t codeCnt) {
...
kern_return_t kr;
...
// 1、Try to raise the exception at the activation level.
// 线程等级
thread = current_thread()
mutex = &thread->mutex;
excp = &thread->exc_actions[exception];
kr = exception_deliver(thread, esception, code, codeCnt, excp, mutex);
if (kr == KERN_SUCCESS || kr == MACH_RCV_PORT_DIED) {
goto out;
}
....
// 2、Maybe the task level will handle it.
// 进程等级
task = current_task();
mutex = &task->lock;
excp = &task->exc_actions[exception];
kr = exception_deliver(thread, exception, code, codeCnt, excp, mutex);
if (kr == KERN_SUCCESS || kr == MACH_RCV_PORT_DIED) {
goto out;
}
...
// 3、How about at the host level?
// 主机等级
host_priv = host_priv_self();
mutex = &host_priv->lock;
excp = &host_priv->exc_actions[exception];
kr = exception_deliver(thread, exception, code, codeCnt, excp, mutex);
if (kr == KERN_SUCCESS || kr == MACH_RCV_PORT_DIED) {
goto out;
}
// 在MAC中还有一步,那便是假如这儿启动了KDB,那么就运用KDB调试反常。
/*
* 4、Nobody handled it, terminate the task.
*/
(void) task_terminate(task);
.....
out:
if ((exception != EXC_CRASH) && (exception != EXC_RESOURCE))
thread_exception_return();
return;
}
怎么处理Mach Exception?
已然反常产生了,那么反常就需求得到处理。反常处理程序是反常音讯的承受者,它运转在自己的线程,尽管说它能够和产生反常的线程在同一个task中(也便是同一个进程中),可是它一般运转在其他的task中,比方说一个debugger。假如一个线程想处理这个task的反常音讯,那么就需求调用task_set_exception_ports()
来注册这个task的反常端口。这样的话,只要这个进程呈现了硬件反常终究都会转化为Mach Exception Mesaage并传递给注册的端口,然后被反常处理程序承受到,处理接纳到的反常音讯。以下是反常code对应详细的原因:
Exception | Notes |
---|---|
EXC_BAD_ACCESS | 无法拜访内存 |
EXC_BAD_INSTRUCTION | 不合法或许未界说的指令或许操作数 |
EXC_ARITHMETIC | 算术反常(例如被零除) |
EXC_EMULATION | 遇到仿真支撑指令 |
EXC_SOFTWARE | 软件生成的反常(比方浮点数核算的反常) |
EXC_BREAKPOINT | 盯梢或许断点(比方Xcode的断点,就会产生反常) |
EXC_SYSCALL | Unix体系调用 |
EXC_MACH_SYSCALL | Mach体系调用 |
EXC_RPC_ALERT | RPC警告 |
当然,并不是一切的反常引发的Exception都是咱们所说的反常,这其间有的是体系调用,或许断点如EXC_SYSCALL
,所以设置反常端口的时分,就需求去考虑到这一点,如下方的myExceptionMask
局部变量存储了需求捕获的几种反常类型:
exception_mask_t myExceptionMask;
myExceptionMask = EXC_MASK_BAD_ACCESS | /* Memory access fail */
EXC_MASK_BAD_INSTRUCTION | /* Illegal instruction */
EXC_MASK_ARITHMETIC | /* Arithmetic exception (eg, divide by zero) */
EXC_MASK_SOFTWARE | /* Software exception (eg, as triggered by x86's bound instruction) */
EXC_MASK_BREAKPOINT | /* Trace or breakpoint */
EXC_MASK_CRASH;
// 注意:这儿必须要运用THREAD_STATE_NONE和plcrash结构中运用的坚持一致
//
rc = task_set_exception_ports(mach_task_self(),
myExceptionMask,
myexceptionPort,
(EXCEPTION_DEFAULT | MACH_EXCEPTION_CODES),
THREAD_STATE_NONE);
这儿得着重强调一下端口设置办法的参数:
kern_return_t task_set_exception_ports
(
task_t task,
exception_mask_t exception_mask,
mach_port_t new_port,
exception_behavior_t behavior,
thread_state_flavor_t new_flavor
);
在这之中xx_set_exception_ports()
的behavior
参数指定来产生反常时发送的反常音讯的类型。
behavior | Notes |
---|---|
EXCEPTION_DEFAULT | catch_exception_raise音讯:包括线程标识 |
EXCEPTION_STATE | catch_exception_raise_state: 包括线程状况 |
EXCEPTION_STATE_IDENTITY | catch_exception_raise_state_identity: 包括线程标识和状况 |
flavour
参数指定要与反常音讯一起发送的线程状况的类型,假如不需求,能够运用THREAD_STATE_NONE
。可是要注意的是,无论线程状况是否在反常音讯中被发送,反常处理程序都能够运用thread_get_state()
和thread_set_state()
别离查询和设置犯错线程的状况。
而默许情况下,线程等级的反常端口都被设置为null端口,而task等级的反常端口,会在fork()
期间被继承,一般也是null 端口(fock其实指的是从内核fock出一个进程)。所以这个时分,压力就来到了Host的反常端口(也便是机器级的反常端口),这儿产生了什么呢?
接下来,咱们详细看一看假如一款Mac运用当线程中产生反常时,假如咱们不做任何处理,会产生什么?(Apple自己的exception handler的处理流程)
1、内核会将过错线程挂起,而且发送一条音讯给适合的反常端口。
2、过错线程坚持挂起状况,等待音讯回复。
3、exception_deliver()
办法向线程的反常端口发送音讯,未得到成功回复。
4、exception_deliver()
办法向task的反常端口发送音讯,未得到成功回复。
5、exception_deliver()
办法向host的反常端口发送音讯。
3、具有接纳反常端口权限的任意task中的反常处理线程将取出该音讯(在Mac上一般是KDB调试程序)
4、反常处理程序调用exc_server
办法来处理该音讯。
5、exc_server
依据端口设置的 behavior
参数来挑选调用什么办法来获取相应的线程信息:catch_exception_raise()、catch_exception_raise_state()、catch_exception_raise_state_identity()
,便是三个函数之一
6、假如上述函数处理后回来KERN_SUCCESS
,那么exc_server()
准备回来音讯发送到内核,使得线程从反常点持续履行。假如反常不是致命的,而且经过该函数修复了问题,那么修复线程的状况能够使得线程持续。
7、假如上述函数处理后回来的不是KERN_SUCCESS
,那么内核将停止该task。
这也便是为什么在Mac上假如Xcode溃散之后,Mac上会呈现Xcode溃散的陈述界面,一起体系会将Xcode封闭。
假如咱们自己捕获处理之后,能否直接将调用办法
exc_server
将音讯持续往后转发呢?答案是否定的,因为在iOS中exc_server
并不是一个public的API,所以根本无法运用。那么咱们捕获反常之后怎么转发给其他的端口呢?这个后面进行描述。
上述进程的详细处理流程如下图:
实际上在体系启动的时分,Host反常端口对应的反常处理程序就现已初始化好了,一起,Unix的反常处理也是在这儿初始化,它会将Mach反常转化为Unix signals。在体系启动时,内核的BSD层经过bsdinit_task()
办法[源码在:bsd/kern/bsd_ init.c中]
来进行初始化的:
//源码地址:https://opensource.apple.com/source/xnu/xnu-7195.81.3/bsd/kern/bsd_init.c.auto.html
void
bsdinit_task(void)
{
proc_t p = current_proc();
process_name("init", p);
/* Set up exception-to-signal reflection */
ux_handler_setup();
}
然后bsdinit_task()
它会调用ux_handler_init
(在最新的xnu-7195.81.3中为ux_handler_setup
)办法来进行设置反常监听端口:
/// 源码地址:https://opensource.apple.com/source/xnu/xnu-7195.81.3/osfmk/kern/ux_handler.c.auto.html
/*
* setup is called late in BSD initialization from initproc's context
* so the MAC hook goo inside host_set_exception_ports will be able to
* set up labels without falling over.
*/
void
ux_handler_setup(void)
{
ipc_port_t ux_handler_send_right = ipc_port_make_send(ux_handler_port);
if (!IP_VALID(ux_handler_send_right)) {
panic("Couldn't allocate send right for ux_handler_port!\n");
}
kern_return_t kr = KERN_SUCCESS;
/*
* Consumes 1 send right.
*
* Instruments uses the RPC_ALERT port, so don't register for that.
*/
kr = host_set_exception_ports(host_priv_self(),
EXC_MASK_ALL & ~(EXC_MASK_RPC_ALERT),
ux_handler_send_right,
EXCEPTION_DEFAULT | MACH_EXCEPTION_CODES,
0);
if (kr != KERN_SUCCESS) {
panic("host_set_exception_ports failed to set ux_handler! %d", kr);
}
}
这儿host_set_exception_ports
办法注册host等级的ux_exception_port
反常端口,当这个端口承受到反常信息之后,反常处理线程会调用**handle_ux_exception
** 办法,这个办法会调用ux_exception
将mach信息转化为signal信号,随后会将转化的unix signal投递到过错线程:threadsignal(thread, ux_signal, code, TRUE);
详细的转化办法如下:
/*
* Translate Mach exceptions to UNIX signals.
*
* ux_exception translates a mach exception, code and subcode to
* a signal. Calls machine_exception (machine dependent)
* to attempt translation first.
*/
static int
ux_exception(int exception,
mach_exception_code_t code,
mach_exception_subcode_t subcode)
{
int machine_signal = 0;
/* Try machine-dependent translation first. */
if ((machine_signal = machine_exception(exception, code, subcode)) != 0) {
return machine_signal;
}
switch (exception) {
case EXC_BAD_ACCESS:
if (code == KERN_INVALID_ADDRESS) {
return SIGSEGV;
} else {
return SIGBUS;
}
case EXC_BAD_INSTRUCTION:
return SIGILL;
case EXC_ARITHMETIC:
return SIGFPE;
case EXC_EMULATION:
return SIGEMT;
case EXC_SOFTWARE:
switch (code) {
case EXC_UNIX_BAD_SYSCALL:
return SIGSYS;
case EXC_UNIX_BAD_PIPE:
return SIGPIPE;
case EXC_UNIX_ABORT:
return SIGABRT;
case EXC_SOFT_SIGNAL:
return SIGKILL;
}
break;
case EXC_BREAKPOINT:
return SIGTRAP;
}
return 0;
}
Unix Signal
Mach现已供给了底层的反常机制,可是依据Mach exception,Apple在内核的BSD层上也建立了一套信号处理体系。这是为什么呢?原因很简单,其实便是为了兼容Unix体系。而依据Linux的安卓也是兼容Unix的,所以安卓的反常也是抛出的Signal。当然这儿得说明,在现代的Unix体系中,Mach反常仅仅导致信号生成的一类事情,还有许多其他的事情或许也会导致信号的生成,比方:显式的调用kill(2)或许killpg(2)、子线程的状况变化等等。
信号机制的完成只要是两个重要的阶段:信号生成和信号传递。信号生成是确保信号被生成的事情,而信号传递是对信号处理的调用,即相关信号动作的履行。而每一个信号都有一个默许动作,在Mac OS X上能够是以下事情:
1、停止反常进程
2、Dump core停止反常进程
3、暂停进程
4、假如进程停止,持续进程;否则疏忽
5、疏忽信号
当然这些都是信号的默许处理办法,咱们能够运用自界说的处理程序来重写信号的默许处理办法,详细来说能够运用sigaction
来自界说,详细的代码实例咱们在后续的捕获信号的demo中有描述。
Mach Exception转化为Signal
Mach反常假如没有在其他当地(thread,task)得到处理,那么它会在ux_exception()
中将其转化为对应的Unix Signal信号,以下是两者之间的转化:
Mach Exception | Unix Signal | 原因 |
---|---|---|
EXC_BAD_INSTRUCTION | SIGILL | 不合法指令,比方除0操作,数组越界,强制解包可选形等等 |
EXC_BAD_ACCESS | SIGSEVG、SIGBUS | SIGSEVG、SIGBUS两者都是过错内存拜访,可是两者之间是有差异的:SIGBUS(总线过错)是内存映射有效,可是不允许被拜访; SIGSEVG(段地址过错)是内存地址映射都失效 |
EXC_ARIHMETIC | SIGFPE | 运算过错,比方浮点数运算反常 |
EXC_EMULATION | SIGEMT | hardware emulation 硬件仿真指令 |
EXC_BREAKPOINT | SIGTRAP | trace、breakpoint等等,比方说运用Xcode的断点 |
EXC_SOFTWARE | SIGABRT、SIGPIPE、SIGSYS、SIGKILL | 软件过错,其间SIGABRT最为常见。 |
Mach反常转化为了Signal信号并不代表Mach反常没有被处理过。有或许存在线程级或许task级的反常处理程序,它将承受反常音讯并处理,处理完毕之后将反常音讯转发给ux_exception()
这也将导致终究反常转化为Signal。
软件反常转化为Signal
除了上述引发CPU Trap的反常之外,还有一类反常是软件反常,这一类反常并不会让进程进入内核态,所以它也并不会转化为Mach Exception,而是会直接转化为Unix Signal。而由Objective-C产生的反常便是软件反常这一类,它将直接转换为Signal信号,比方给目标发送未完成的音讯,数组索引越界直接引发SIGABRT信号,作为对比Swift的数组反常会导致CPU Trap,转化为EXC_BAD_INSTRUCTION反常音讯。
那为什么Objective-C反常仅仅软件反常,而不会触发CPU Trap?
因为Objective-C写的代码都是依据Runtime运转的,所以反常产生之后,直接会被Runtime处理转化为Unix Signal,一起,关于这类反常,咱们能够直接运用**NSSetUncaughtExceptionHandler
** 设置处理办法,即使咱们设置了处理办法,OC反常依旧会被转发为信号,一起值得说明的是注册Signal的处理程序运转于的线程,以及**NSSetUncaughtExceptionHandler
** 的处理程序运转于的线程,便是反常产生的线程,也便是哪个线程犯错了,由哪个线程来处理。
Mach Exception和Unix Signal的差异
Mach Exception的处理机制中反常处理程序能够在自己创立的处理线程中运转,而该线程和犯错的线程甚至能够不在一个task中,即能够不在一个进程中,因而反常处理不需求过错线程的资源来运转,这样能够在需求的时分直接获得过错线程的反常上下文,而Unix Signal的处理无法运转在其他的线程,只能在过错线程上处理,所以Mach反常处理机制的优势是很明显的,比方说debugging场景,咱们平常打断点的时分,其实程序运转到这儿的时分会给Xcode这个task中的注册反常端口发EXC_BREAKPOINT音讯,而Xcode收到之后,就会暂停在断点处,在处理完之后(比方点击跳过断点),将发送音讯回来到Xcode,Xcode也将持续跑下去。
这也是Mach Exception处理机制的优势,它能够在多线程的环境中很好的运转,而信号机制只能在犯错线程中运转。而其实Mach反常处理程序能够以更细粒度的办法来运转,因为每一种Mach反常音讯都能够有自己的处理程序,甚至是每一个线程,每一个Task单独处理,可是要说明的是,线程级的反常处理程序一般适用于过错处理,而Task级的反常处理程序一般适用于调试。
那么Unix Signal的优势是什么呢?便是全!无论是硬件反常还是软件反常都会被转化为Signal。
在《Mac OS X and iOS Internals To the Apple Core》这本书中说到:为了统一反常处理机制,一切的用户自身产生的反常并不会直接转化为Unix信号,而是会先下沉到内核中转化为Mach Exception,然后再走Mach反常的处理流程,终究在host层转化为UnixSignal信号。
可是我是不同意这个观念的,因为在我注册的Task等级的反常处理程序中并不会捕获Objective-C产生的反常(如数组越界),它是直接转化为SIGABRT的。而软件反常产生的Signal,实际上都是由以下两个API:kill(2)或许pthread_kill(2)之一生成的反常信号,而我这两个办法的源码中并没有看到下沉到内核中的代码,而是直接转化为Signal并投递反常信号。流程如下图所示,其间psignal()
办法以及psignal_internal()
办法的源码都在[/bsd/kern/kern_sig.c]文件中。
反常的捕获
捕获反常的办法
说了这么多反常是什么,反常怎样分类,那么接下来咱们详细来说说咱们怎么捕获反常,可是再聊怎么捕获之前,且考虑一下,咱们应该选用哪种办法来捕获呢?从上述可知Mach Exception反常处理机制只能捕获硬件反常,而Unix反常处理机制都能捕获,所以大略有两种办法能够挑选:
1、Unix Signal
2、Mach Exception and Unix Signal
微软有一个十分著名的溃散核算结构**PLCrashReport ,**这个结构也是供给了两种核算溃散的计划:
typedef NS_ENUM(NSUInteger, PLCrashReporterSignalHandlerType) {
PLCrashReporterSignalHandlerTypeBSD = 0, /// 一种是BSD层,也便是Unix Signal办法
PLCrashReporterSignalHandlerTypeMach = 1 /// 一种是Mach层,也便是Mach Exception办法
}
关于第二种计划,假如看网上许多文章,都说说到到PLCrashReport这个库中说:
We still need to use signal handlers to catch SIGABRT in-process. The kernel sends an EXC_CRASH mach exception to denote SIGABRT termination. In that case, catching the Mach exception in-process leads to process deadlock in an uninterruptable wait. Thus, we fall back on BSD signal handlers for SIGABRT, and do not register for EXC_CRASH.
意思便是说,假如不捕获SIGABRT 信号,那么Mach Exception接到EXC_CRASH音讯会产生进程的死锁,可是我不认可这个观念,原因如下:
1、在我自己测验Demo的进程中,发现需求捕获SIGABRT 信号的原因是软件反常并不会下沉到Mach内核转化为Signal,而是会直接发出SIGABRT 信号,所以需求捕获。
2、即使我在task的task_set_exception_ports
办法中设置了需求捕获EXC_CRASH反常,当反常产生时也不会呈现死锁的情况。
3、假如看BSD层中将Mach反常转化为Signal的源码中ux_exception
办法的详细完成,会发现根本就不会处理EXC_CRASH的情况,正如上述列表中的Mach Exception和Unix Signal的对应联系
所以我的结论是捕获SIGABRT信号,仅仅因为软件反常并不会形成Mach Exception,而是直接会被转化SIGABRT信号,并向过错线程投递。也便是说:只选用Mach Exception无法捕获软件反常,所以需求额外捕获SIGABRT信号。 那么详细来说怎么捕获呢?
捕获反常的实践——Unix Signal
// 1、首要是确认注册哪些信号
+ (void)signalRegister {
ryRegisterSignal(SIGABRT);
ryRegisterSignal(SIGBUS);
ryRegisterSignal(SIGFPE);
ryRegisterSignal(SIGILL);
ryRegisterSignal(SIGPIPE);
ryRegisterSignal(SIGSEGV);
ryRegisterSignal(SIGSYS);
ryRegisterSignal(SIGTRAP);
}
// 2、实际的注册办法:将信号和action关联,此处我的处理办法为rySignalHandler
static void ryRegisterSignal(int signal) {
struct sigaction action;
action.sa_sigaction = rySignalHandler;
action.sa_flags = SA_NODEFER | SA_SIGINFO;
sigemptyset(&action.sa_mask);
sigaction(signal, &action, 0);
}
// 3、完成详细的反常处理程序
static void rySignalHandler(int signal, siginfo_t* info, void* context) {
NSMutableString *mstr = [[NSMutableString alloc] init];
[mstr appendString:@"Signal Exception:\n"];
[mstr appendString:[NSString stringWithFormat:@"Signal %@ was raised. \n", signalName(signal)]];
// 因为注册了信号溃散回调办法,体系回来调用
for (NSUInteger index = 0; index < NSThread.callStackSymbols.count; index ++) {
NSString *str = [NSThread.callStackSymbols objectAtIndex:index];
[mstr appendString:[str stringByAppendingString:@"\n"]];
}
[mstr appendString:@"threadInfo: \n"];
[mstr appendString:[[NSThread currentThread] description]];
NSString *path = [NSString stringWithFormat:@"%@/Library/signal.txt",NSHomeDirectory()];
[mstr writeToFile:path atomically:true encoding:NSUTF8StringEncoding error:nil];
exit(-1);
}
上面的流程很简单,我会在收到Signal信号之后,由过错线程来履行反常处理程序,履行完毕之后,运用exit(-1)
强制退出。
问题一:假如仅仅履行一个写入文件的操作之后不退出即不履行exit(-1)
会产生什么?
它将会导致该犯错线程履行完写入文件的操作之后,持续履行的时分仍然呈现反常,仍然会抛出信号,然后又会抛给该线程处理反常,所以变成了一个死循环,导致一直在将过错信息写入文件。
问题二:假如不想运用exit(-1)
又想正常作业,应该怎么做呢?
// 1、首要取消掉一切绑定的action
// 2、然后处理完之后运用raise(signal) 将信号发给进程做默许处理
static void rySignalHandler(int signal, siginfo_t* info, void* context) {
[Signal unRegisterSignal];
...
raise(signal);
}
static int monitored_signals[] = {SIGABRT, SIGBUS, SIGFPE, SIGILL, SIGPIPE, SIGSEGV, SIGSYS, SIGTRAP};
static int monitored_signals_count = (sizeof(monitored_signals) / sizeof(monitored_signals[0]));
+ (void)unRegisterSignal {
for (int i = 0; i < monitored_signals_count; i++) {
struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sa.sa_handler = SIG_DFL;
sigemptyset(&sa.sa_mask);
sigaction(monitored_signals[i], &sa, NULL);
}
}
上述计划其实是仿照的PLCrashReport
结构中的写法,主张阅读相关源码。
问题三:假如过错线程是子线程,然后Signal投递到子线程处理,这个时分影响主线程吗?
不影响,因为Signal反常处理程序在过错线程运转,这个和主线程无关,当然,假如过错线程是主线程,那就另当别论了。
捕获反常的实践——Mach Exception + Unix Signal
相对而言运用Mach Exception的反常处理机制要稍微杂乱一些,Unix Signal的捕获上述现已说到了,接下来便是Mach Exception反常的捕获了。
+ (void)setupMachHandler {
kern_return_t rc;
// 1、分配端口
rc = mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &myexceptionPort);
if (rc != KERN_SUCCESS) {
NSLog(@"声明反常端口没有成功");
}
// 2、增加mach_send的权限
rc = mach_port_insert_right(mach_task_self(), myexceptionPort, myexceptionPort, MACH_MSG_TYPE_MAKE_SEND);
if (rc != KERN_SUCCESS) {
NSLog(@"增加权限失利");
}
exception_mask_t myExceptionMask;
// 3、设置需求承受哪些反常信息
myExceptionMask = EXC_MASK_BAD_ACCESS | /* Memory access fail */
EXC_MASK_BAD_INSTRUCTION | /* Illegal instruction */
EXC_MASK_ARITHMETIC | /* Arithmetic exception (eg, divide by zero) */
EXC_MASK_SOFTWARE | /* Software exception (eg, as triggered by x86's bound instruction) */
EXC_MASK_BREAKPOINT | /* Trace or breakpoint */
EXC_MASK_CRASH;
// 4、task_set_exception_ports设置task等级的反常端口
rc = task_set_exception_ports(mach_task_self(),
myExceptionMask,
myexceptionPort,
(EXCEPTION_DEFAULT | MACH_EXCEPTION_CODES),
THREAD_STATE_NONE);
// 5、初始化反常处理线程,并设置反常处理办法。
pthread_t thread;
pthread_create(&thread, NULL, exc_handler, NULL);
}
// 6、反常处理程序
// 相似RunLoop的思路,运用一个while-true循环来确保线程不会退出,一起运用mach_msg来一直接纳音讯
static void* exc_handler(void *ignored) {
mach_msg_return_t rc;
// 自界说一个音讯体
typedef struct {
mach_msg_header_t Head; /* start of the kernel processed data */
mach_msg_body_t msgh_body;
mach_msg_port_descriptor_t thread;
mach_msg_port_descriptor_t task; /* end of the kernel processed data */
NDR_record_t NDR;
exception_type_t exception;
mach_msg_type_number_t codeCnt;
integer_t code[2];
int flavor;
mach_msg_type_number_t old_stateCnt;
natural_t old_state[144];
kern_return_t retcode;
} Request;
Request exc;
exc.Head.msgh_size = 1024;
exc.Head.msgh_local_port = myexceptionPort;
while (true) {
rc = mach_msg(&exc.Head,
MACH_RCV_MSG | MACH_RCV_LARGE,
0,
exc.Head.msgh_size,
exc.Head.msgh_local_port, // 这是一个全局的变量
MACH_MSG_TIMEOUT_NONE,
MACH_PORT_NULL);
if (rc != MACH_MSG_SUCCESS) {
NSLog(@"没有成功承受到溃散信息");
break;
}
// 将反常写入文件(当然, 你也能够做自己的自界说操作)
break;
}
exit(-1);
}
代码很简单理解,收到反常之后就会履行相应的处理代码,处理完反常之后履行exit(-1)
退出运用。仍然是问自己几个问题:
问题一:不做exit(-1)操作会产生什么,反常会不停写入吗?
不然,因为这儿接纳到反常音讯之后,就没有对外转发了,只会停留在task这一级,可是因为反常线程没有得到康复,所以表现出来的状况便是反常线程堵塞。
问题二:不做exit(-1),反常线程是子线程,会对主线程有影响吗?
不会,它只会堵塞反常线程,对主线程没有影响。换言之,UI事情正常响应。
问题三:Mach Exception收到音讯处理之后就不会向外转发了,那假如想转发呢?
能够向原端口回复你的处理成果,这就会由体系默许向上转发,终究转化为Unix信号。
static void* exc_handler(void *ignored) {
mach_msg_return_t rc;
// 自界说一个音讯体
typedef struct {
mach_msg_header_t Head; /* start of the kernel processed data */
mach_msg_body_t msgh_body;
mach_msg_port_descriptor_t thread;
mach_msg_port_descriptor_t task; /* end of the kernel processed data */
NDR_record_t NDR;
exception_type_t exception;
mach_msg_type_number_t codeCnt;
integer_t code[2];
int flavor;
mach_msg_type_number_t old_stateCnt;
natural_t old_state[144];
kern_return_t retcode;
} Request;
....
// 处理完音讯之后,咱们回复处理成果
Request reply;
memset(&reply, 0, sizeof(reply));
reply.Head.msgh_bits = MACH_MSGH_BITS(MACH_MSGH_BITS_REMOTE(exc.Head.msgh_bits), 0);
reply.Head.msgh_local_port = MACH_PORT_NULL;
reply.Head.msgh_remote_port = exc.Head.msgh_remote_port;
reply.Head.msgh_size = sizeof(reply);
reply.NDR = NDR_record;
reply.retcode = KERN_SUCCESS;
/*
* Mach uses reply id offsets of 100. This is rather arbitrary, and in theory could be changed
* in a future iOS release (although, it has stayed constant for nearly 24 years, so it seems unlikely
* to change now). See the top-level file warning regarding use on iOS.
*
* On Mac OS X, the reply_id offset may be considered implicitly defined due to mach_exc.defs and
* exc.defs being public.
*/
reply.Head.msgh_id = exc.Head.msgh_id + 100;
mach_msg(&reply.Head,
MACH_SEND_MSG,
reply.Head.msgh_size,
0,
MACH_PORT_NULL,
MACH_MSG_TIMEOUT_NONE,
MACH_PORT_NULL);
return NULL;
}
参阅
- 《Mac OS X and iOS Internals To the Apple Core》
- Mac OS X Internals: A Systems Approach 第九章
- kernel源码
- Android 渠道 Native 代码的溃散捕获机制及完成
- PLCrashReporter