如何确保信号处理程序永远不会屈服于同一进程组中的线程?
How to ensure a signal handler never yields to a thread within the same process group?
这是一个元问题,因为我认为我有一个适合我的解决方案,但它有其自身的缺点和优点。我需要做一件相当常见的事情,在一个线程上捕获 SIGSEGV
(没有专门的崩溃处理线程),转储一些调试信息并退出。
这里的问题是,在崩溃时,我的应用程序 运行s llvm-symbolizer
需要一段时间(相对而言)并导致收益(由于 clone + execve
或超过线程的时间量,我已经看到后者在使用 libLLVM
在进程中自己进行符号化时发生。这样做的原因是为了获得一个堆栈跟踪,其中包含 demangled 符号和 line/file 信息(存储在单独的 DWP 文件中)。出于显而易见的原因,我不希望在我的 SIGSEGV
处理程序中发生 yield,因为我打算在它执行后终止应用程序(线程组)并且从不从信号处理程序 return。
我不太熟悉 Linux 信号处理和 glibc 的包装器在它们周围施展魔法,不过,我知道基本的陷阱,但没有太多关于信号处理细节的信息,比如是否同步信号处理程序在调度方面获得任何类型的特殊优先级。
集思广益,我有一些想法和缺点:
pthread_kill(<every other thread>, SIGSTOP)
- 多线程很麻烦,与信号处理程序交互,这似乎会产生意想不到的副作用。还需要拦截来自其他库的线程创建以跟踪线程列表并增加每个系统调用的抢占机会。一旦它们停止指向系统调用 exit
存根或完全使用 SIGKILL
. ,甚至可能会更改它们的上下文
- 用作所有线程取消点的全局标志(有点像
pthread_cancel/pthread_testcancel
)。更安全,但需要大量维护,并且在大型代码库中,除了轻微的性能开销之外,它可能是地狱般的。全局标志也可能导致错误级联,因为程序已经处于不可预测的状态,所以让任何其他线程 运行 已经不是很好了。
- "Abusing" 我当前选择的调度程序,我的实现是答案之一。切换到
FIFO
调度策略并提高优先级,因此成为该组中唯一 运行 可用线程。
- 核心转储不是一个选项,因为这里的目标是首先避免它们。除了符号器之外,我更希望不需要辅助程序。
环境是典型的基于 glibc
的 Linux (4.4) 分布 NPTL
。
我知道崩溃处理程序现在相当普遍,所以我相信 none 我选择的方式非常棒,特别是考虑到我从未见过调度程序 "hack" 曾经以这种方式使用.因此,有没有人有比调度程序更清洁、风险更低的更好替代方案 "hack",我是否遗漏了关于信号的一般想法中的任何重要点?
编辑: 看来我并没有真正考虑这个等式中的 MP(根据评论),事实上其他线程仍然 运行nable in一个 MP 情况,并且可以愉快地继续 运行ning 与 FIFO
线程一起在不同的处理器上。但是,我可以将进程的亲和性更改为仅在与崩溃线程相同的核心上执行,这基本上会有效地在调度边界处冻结所有其他线程。但是,这仍然使 "FIFO thread yielding due to blocking IO" 场景处于打开状态。
似乎 FIFO + SIGSTOP
选项是最好的选项,但我想知道是否有任何其他技巧可以使线程无法调度,而不是使用 SIGSTOP
。从文档来看,似乎不可能将线程的 CPU 亲和力设置为零(使其处于技术上 运行 可用的边缘状态,除了没有处理器可用于 运行在)。
这是我能想到的最好的解决方案(为简洁起见省略了部分,但它显示了原理),我的基本假设是在这种情况下进程 运行s 作为 root。如果事情变得非常糟糕并且需要特权(如果我正确理解man(7) sched
页面),这种方法可能导致资源匮乏我运行信号处理程序的一部分在[=12=下导致抢占] 守卫并尽快退出范围。这不是严格的 C++ 相关,因为同样可以用 C 或任何其他本地语言完成。
void spl_get(spl_t& O)
{
os_assert(syscall(__NR_sched_getattr,
0, &O, sizeof(spl_t), 0) == 0);
}
void spl_set(spl_t& N)
{
os_assert(syscall(__NR_sched_setattr,
0, &N, 0) == 0);
}
void splx(uint32_t PRI, spl_t& O) {
spl_t PL = {0};
PL.size = sizeof(PL);
PL.sched_policy = SCHED_FIFO;
PL.sched_priority = PRI;
spl_set(PL, O);
}
class OSSplHigh {
os::spl_t OldPrioLevel;
public:
OSSplHigh() {
os::splx(2, OldPrioLevel);
}
~OSSplHigh() {
os::spl_set(OldPrioLevel);
}
};
处理程序本身使用 sigaltstack
和 sigaction
非常简单,但我不会在任何线程上阻塞 SIGSEGV
。同样奇怪的是 syscalls sched_setattr and sched_getattr 或者结构定义没有通过 glibc 公开,这与文档相反。
后期编辑:最好的解决方案是向所有线程发送SIGSTOP
(通过链接器的--wrap
选项拦截pthread_create
)以保持所有 运行ning 线程的分类帐,感谢您在评论中的建议。
upon crash, my application runs llvm-symbolizer
这很可能会导致死锁。我找不到任何关于 llvm-symbolizer 是异步信号安全的声明。它可能会调用 malloc
,如果崩溃也发生在 malloc
内(例如,由于其他地方的堆损坏),肯定会死锁。
Switching to FIFO scheduling policy and raising priority therefore becoming the only runnable thread in that group.
我相信你错了:一个 SCHED_FIFO
线程将 运行 只要它是 运行nable(即不发出任何阻塞的系统调用)。如果线程确实发出这样的调用(它必须:例如 open
单独的 .dwp
文件),它将阻塞并且其他线程将变为 运行nable.
TL;DR:没有简单的方法可以实现您想要的,而且似乎也没有必要:当崩溃的线程完成其业务时,其他线程继续 运行ning 有什么关系?
这是一个元问题,因为我认为我有一个适合我的解决方案,但它有其自身的缺点和优点。我需要做一件相当常见的事情,在一个线程上捕获 SIGSEGV
(没有专门的崩溃处理线程),转储一些调试信息并退出。
这里的问题是,在崩溃时,我的应用程序 运行s llvm-symbolizer
需要一段时间(相对而言)并导致收益(由于 clone + execve
或超过线程的时间量,我已经看到后者在使用 libLLVM
在进程中自己进行符号化时发生。这样做的原因是为了获得一个堆栈跟踪,其中包含 demangled 符号和 line/file 信息(存储在单独的 DWP 文件中)。出于显而易见的原因,我不希望在我的 SIGSEGV
处理程序中发生 yield,因为我打算在它执行后终止应用程序(线程组)并且从不从信号处理程序 return。
我不太熟悉 Linux 信号处理和 glibc 的包装器在它们周围施展魔法,不过,我知道基本的陷阱,但没有太多关于信号处理细节的信息,比如是否同步信号处理程序在调度方面获得任何类型的特殊优先级。
集思广益,我有一些想法和缺点:
pthread_kill(<every other thread>, SIGSTOP)
- 多线程很麻烦,与信号处理程序交互,这似乎会产生意想不到的副作用。还需要拦截来自其他库的线程创建以跟踪线程列表并增加每个系统调用的抢占机会。一旦它们停止指向系统调用exit
存根或完全使用SIGKILL
. ,甚至可能会更改它们的上下文
- 用作所有线程取消点的全局标志(有点像
pthread_cancel/pthread_testcancel
)。更安全,但需要大量维护,并且在大型代码库中,除了轻微的性能开销之外,它可能是地狱般的。全局标志也可能导致错误级联,因为程序已经处于不可预测的状态,所以让任何其他线程 运行 已经不是很好了。 - "Abusing" 我当前选择的调度程序,我的实现是答案之一。切换到
FIFO
调度策略并提高优先级,因此成为该组中唯一 运行 可用线程。 - 核心转储不是一个选项,因为这里的目标是首先避免它们。除了符号器之外,我更希望不需要辅助程序。
环境是典型的基于 glibc
的 Linux (4.4) 分布 NPTL
。
我知道崩溃处理程序现在相当普遍,所以我相信 none 我选择的方式非常棒,特别是考虑到我从未见过调度程序 "hack" 曾经以这种方式使用.因此,有没有人有比调度程序更清洁、风险更低的更好替代方案 "hack",我是否遗漏了关于信号的一般想法中的任何重要点?
编辑: 看来我并没有真正考虑这个等式中的 MP(根据评论),事实上其他线程仍然 运行nable in一个 MP 情况,并且可以愉快地继续 运行ning 与 FIFO
线程一起在不同的处理器上。但是,我可以将进程的亲和性更改为仅在与崩溃线程相同的核心上执行,这基本上会有效地在调度边界处冻结所有其他线程。但是,这仍然使 "FIFO thread yielding due to blocking IO" 场景处于打开状态。
似乎 FIFO + SIGSTOP
选项是最好的选项,但我想知道是否有任何其他技巧可以使线程无法调度,而不是使用 SIGSTOP
。从文档来看,似乎不可能将线程的 CPU 亲和力设置为零(使其处于技术上 运行 可用的边缘状态,除了没有处理器可用于 运行在)。
这是我能想到的最好的解决方案(为简洁起见省略了部分,但它显示了原理),我的基本假设是在这种情况下进程 运行s 作为 root。如果事情变得非常糟糕并且需要特权(如果我正确理解man(7) sched
页面),这种方法可能导致资源匮乏我运行信号处理程序的一部分在[=12=下导致抢占] 守卫并尽快退出范围。这不是严格的 C++ 相关,因为同样可以用 C 或任何其他本地语言完成。
void spl_get(spl_t& O)
{
os_assert(syscall(__NR_sched_getattr,
0, &O, sizeof(spl_t), 0) == 0);
}
void spl_set(spl_t& N)
{
os_assert(syscall(__NR_sched_setattr,
0, &N, 0) == 0);
}
void splx(uint32_t PRI, spl_t& O) {
spl_t PL = {0};
PL.size = sizeof(PL);
PL.sched_policy = SCHED_FIFO;
PL.sched_priority = PRI;
spl_set(PL, O);
}
class OSSplHigh {
os::spl_t OldPrioLevel;
public:
OSSplHigh() {
os::splx(2, OldPrioLevel);
}
~OSSplHigh() {
os::spl_set(OldPrioLevel);
}
};
处理程序本身使用 sigaltstack
和 sigaction
非常简单,但我不会在任何线程上阻塞 SIGSEGV
。同样奇怪的是 syscalls sched_setattr and sched_getattr 或者结构定义没有通过 glibc 公开,这与文档相反。
后期编辑:最好的解决方案是向所有线程发送SIGSTOP
(通过链接器的--wrap
选项拦截pthread_create
)以保持所有 运行ning 线程的分类帐,感谢您在评论中的建议。
upon crash, my application runs llvm-symbolizer
这很可能会导致死锁。我找不到任何关于 llvm-symbolizer 是异步信号安全的声明。它可能会调用 malloc
,如果崩溃也发生在 malloc
内(例如,由于其他地方的堆损坏),肯定会死锁。
Switching to FIFO scheduling policy and raising priority therefore becoming the only runnable thread in that group.
我相信你错了:一个 SCHED_FIFO
线程将 运行 只要它是 运行nable(即不发出任何阻塞的系统调用)。如果线程确实发出这样的调用(它必须:例如 open
单独的 .dwp
文件),它将阻塞并且其他线程将变为 运行nable.
TL;DR:没有简单的方法可以实现您想要的,而且似乎也没有必要:当崩溃的线程完成其业务时,其他线程继续 运行ning 有什么关系?