33.1 能不能用 kill(1) 给特定线程发送信号呢?
用 kill
发送信号给特定线程的尝试 这篇文章提到了用 kill(1) 给特定的线程发送信号,但是标准上没有说这样是可行的。因此这篇文章探讨一下为什么 kill(1) 会具有这样的行为。以下阅读的代码都是在 2024 年 7 月 12 日 的主分支上的最新代码。
kill(1) 的实现
这里 是 kill
命令实现的关键部分,其中只调用了 kill()
系统调用。所以可以排除是 kill(1) 自己处理了发送信号给特定线程的过程。
kill(2) 在 glibc 中没有实现
kill 系统调用是需要链接 libc 的,其声明的头文件也是在 signal.h 中,因此我们需要先看 glibc 的源码。
在 glibc 源码中没有找到 kill()
对 Linux 系统调用的包装。只有一个 __kill()
的空实现(总会设置 errno
为 ENOSYS
并返回 -1)并将其声明为 kill
的弱别名(使用 weak_alias
宏)。(对于某些架构,比如 hurd,会提供 kill()
的实现,可能是这些系统上没有 kill 系统调用)。因此,kill 系统调用是直接在 Linux 内核中实现的。
Linux kernel 中对 kill(2) 的实现
tkill()
和 tgkill()
首先看下 tkill()
和 tgkill()
的实现,方便和 kill()
做下对比。在 Linux 源码中,kernel/signal.c 中有 tkill()
和 tgkill()
的定义,它们都调用了 do_tkill()
。
static int do_tkill(pid_t tgid, pid_t pid, int sig)
{
struct kernel_siginfo info;
prepare_kill_siginfo(sig, &info, PIDTYPE_PID);
return do_send_specific(tgid, pid, sig, &info);
}
这些函数中的 pid 是作为线程 id 用的,pid 的枚举定义为:
enum pid_type {
PIDTYPE_PID,
PIDTYPE_TGID,
PIDTYPE_PGID,
PIDTYPE_SID,
PIDTYPE_MAX,
};
然后,do_send_specific()
会调用 do_send_sig_info(sig, info, p, PIDTYPE_PID)
(记住这个函数,因为接下来 sys_kill()
也会最终调用它)。这里是非常明确地把信号发给特定进程(实际上是线程),不值得奇怪。
kill()
kernel/signal.c 中同时有 kill 系统调用的定义:
SYSCALL_DEFINE2(kill, pid_t, pid, int, sig)
{
struct kernel_siginfo info;
prepare_kill_siginfo(sig, &info, PIDTYPE_TGID);
return kill_something_info(sig, &info, pid);
}
其形式很像 do_tkill()
。根据 PIDTYPE_TGID
参数,kill()
的信号是发给线程组的。调用链有点长:
// [1] 本文讨论的是对特定线程发送信号,显然 pid > 0
static int kill_something_info(int sig, struct kernel_siginfo *info, pid_t pid)
{
int ret;
if (pid > 0)
return kill_proc_info(sig, info, pid); // 肯定走这一条
// ...
}
// [2] find_vpid 的返回类型是 struct pid *,而不是 pid_t
static int kill_proc_info(int sig, struct kernel_siginfo *info, pid_t pid)
{
int error;
rcu_read_lock();
error = kill_pid_info(sig, info, find_vpid(pid));
rcu_read_unlock();
return error;
}
// [3] 参数里面还是使用了 PIDTYPE_TGID,和 do_tkill 有区别
int kill_pid_info(int sig, struct kernel_siginfo *info, struct pid *pid)
{
return kill_pid_info_type(sig, info, pid, PIDTYPE_TGID);
}
// [4]
static int kill_pid_info_type(int sig, struct kernel_siginfo *info,
struct pid *pid, enum pid_type type)
{
int error = -ESRCH;
struct task_struct *p;
for (;;) {
rcu_read_lock();
p = pid_task(pid, PIDTYPE_PID);
if (p)
error = group_send_sig_info(sig, info, p, type);
rcu_read_unlock();
if (likely(!p || error != -ESRCH))
return error;
}
}
// 接下来是:
// [5] group_send_sig_info(sig, info, p, type /* PIDTYPE_TGID */)
// [6] do_send_sig_info(sig, info, p, type /* PIDTYPE_TGID */)
// [7] send_signal_locked(sig, info, p, type /* PIDTYPE_TGID */)
// [8] __send_signal_locked(sig, info, t, type, force /* 根据内部的判断逻辑决定 */)
static int __send_signal_locked(int sig, struct kernel_siginfo *info,
struct task_struct *t, enum pid_type type, bool force)
{
struct sigpending *pending;
// ...
pending = (type != PIDTYPE_PID) ? &t->signal->shared_pending : &t->pending;
// ...
out_set:
signalfd_notify(t, sig);
sigaddset(&pending->signal, sig);
/* Let multiprocess signals appear after on-going forks */
if (type > PIDTYPE_TGID) { // 前面已经给出了 pid_type 的定义
struct multiprocess_signals *delayed;
hlist_for_each_entry(delayed, &t->signal->multiprocess, node) {
sigset_t *signal = &delayed->signal;
/* Can't queue both a stop and a continue signal */
if (sig == SIGCONT)
sigdelsetmask(signal, SIG_KERNEL_STOP_MASK);
else if (sig_kernel_stop(sig))
sigdelset(signal, SIGCONT);
sigaddset(signal, sig);
}
}
complete_signal(sig, t, type);
ret:
trace_signal_generate(sig, info, t, type != PIDTYPE_PID, result);
return ret;
}
这里可以看到 __send_signal_locked()
是控制执行信号发送流程的函数,当我们给的 pid 是线程的 id 时,会选中线程的 struct task_struct *
,但其 pending 队列则是选的进程的共享队列。这可能表示信号仍然是发送给整个进程的,只是线程在处理信号的调度上有优先权。
可以看出 complete_signal()
中的 if (wants_signal(sig, p))
先检查了是否可以给传来的 struct task_struct *
发送信号,在本文的讨论中,这个 task 结构体表示一个特定线程。尤其是这句注释很醒目:Try the suggested task first (may or may not be the main thread).
// [9]
static void complete_signal(int sig, struct task_struct *p, enum pid_type type)
{
struct signal_struct *signal = p->signal;
struct task_struct *t;
/*
* Now find a thread we can wake up to take the signal off the queue.
*
* Try the suggested task first (may or may not be the main thread).
*/
if (wants_signal(sig, p)) // [10]
t = p;
else if ((type == PIDTYPE_PID) || thread_group_empty(p))
/*
* There is just one thread and it does not need to be woken.
* It will dequeue unblocked signals before it runs again.
*/
return;
else {
/*
* Otherwise try to find a suitable thread.
*/
t = signal->curr_target;
while (!wants_signal(sig, t)) {
t = next_thread(t);
if (t == signal->curr_target)
/*
* No thread needs to be woken.
* Any eligible threads will see
* the signal in the queue soon.
*/
return;
}
signal->curr_target = t;
}
// 这些逻辑是对致命信号的处理,我们不关心
if (sig_fatal(p, sig) &&
(signal->core_state || !(signal->flags & SIGNAL_GROUP_EXIT)) &&
!sigismember(&t->real_blocked, sig) &&
(sig == SIGKILL || !p->ptrace)) {
// ...
}
/*
* The signal is already in the shared-pending queue.
* Tell the chosen thread to wake up and dequeue it.
*/
signal_wake_up(t, sig == SIGKILL); // [11]
return;
}
而在 wants_signal()
的检查为:
// [10]
/*
* Test if P wants to take SIG. After we've checked all threads with this,
* it's equivalent to finding no threads not blocking SIG. Any threads not
* blocking SIG were ruled out because they are not running and already
* have pending signals. Such threads will dequeue from the shared queue
* as soon as they're available, so putting the signal on the shared queue
* will be equivalent to sending it to one such thread.
*/
static inline bool wants_signal(int sig, struct task_struct *p)
{
if (sigismember(&p->blocked, sig)) // 条件之一:不能阻塞该信号
return false;
if (p->flags & PF_EXITING)
return false;
if (sig == SIGKILL)
return true;
if (task_is_stopped_or_traced(p)) // 条件之一:不能已经停止
return false;
return task_curr(p) || !task_sigpending(p);
}
signal_wake_up()
会用 set_tsk_thread_flag(t, TIF_SIGPENDING)
设置 struct task_struct
结构的标志位,告诉线程有信号正在等待,并随后将其唤醒。
// [11] signal_wake_up 略
// [12]
void signal_wake_up_state(struct task_struct *t, unsigned int state)
{
lockdep_assert_held(&t->sighand->siglock);
set_tsk_thread_flag(t, TIF_SIGPENDING);
if (!wake_up_state(t, state | TASK_INTERRUPTIBLE))
kick_process(t);
}
解释现象
在我之前的测试中,主线程调用 pthread_exit()
退出成为僵尸,而另外一个线程仍在运行,这时向主线程发送信号就无法通过 task_is_stopped_or_traced(p)
的检查,因此内核会通知另外一个线程。
但是我之前的测试有个缺陷,即主线程在等待 thread1
,而 thread1
因为 pause()
而等待,所以它们都处于阻塞状态,kill()
只对选择的线程做了唤醒,意味着这个线程总是先处理信号。我又修改了测试代码:
#define _GNU_SOURCE
#include <pthread.h>
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
void cpu_intensive_task() {
int a = 0;
for (;;) {
++a;
}
}
// Not portable since gettid is only on Linux.
unsigned long get_thread_id(void) { return (unsigned long)gettid(); }
void *f(void *arg) {
fprintf(stderr, "Thread %lu starts\n", get_thread_id());
cpu_intensive_task();
fprintf(stderr, "Thread %lu but shouldn't reached here\n", get_thread_id());
return NULL;
}
void chld_handler(int sig) {
struct timespec t;
clock_gettime(CLOCK_MONOTONIC, &t);
// (f)printf is not async-signal-safe, but I use it for testing.
fprintf(stderr, "[%ld.%08ld] Handler thread is %lu\n", t.tv_sec, t.tv_nsec, get_thread_id());
}
int main() {
fprintf(stderr, "Main thread is %lu\n", get_thread_id());
// 因为 SIGCHLD 的默认行为是忽略,所以不设置信号处理器函数,SIGCHLD 就不会让 thread1 从 pause() 返回。
struct sigaction action;
action.sa_handler = chld_handler;
sigset_t signals_blocked;
sigfillset(&signals_blocked);
action.sa_mask = signals_blocked;
action.sa_flags = 0;
if (sigaction(SIGCHLD, &action, NULL) != 0) {
perror("sigaction");
return 1;
}
pthread_t thread1;
pthread_create(&thread1, NULL, f, NULL);
cpu_intensive_task();
}
我将两个线程的主要任务都改成无限循环的计算,导致它们永不进入阻塞状态(还是可能进入就绪状态),得到的结果也还是被发向信号的线程先处理信号。可能因为 signal_wake_up()
这个函数是先通知线程有信号需要处理,然后才将其唤醒,导致没被选择的另外一个线程根本不知道有信号要处理。
这说明我的测试方法也有缺陷,因为我一次只通知了一个线程。我之前发送信号的方法是先运行编译好的程序,得到线程的 tid,然后在命令行上运行(以下的线程 ID 会变化,因为每次测试都是启用了新的进程):
for i in {1..10}; do kill -SIGCHLD 589999 && sleep 0.5; done
后来我决定在命令行上同时对两个线程发出信号:
for i in {1..10}; do kill -SIGCHLD 603935 603936 && sleep 0.5; done
输出变成了(直到我手动终止了程序):
Main thread is 603935
Thread 603936 starts
[262391.240335037] Handler thread is 603935
[262391.741273532] Handler thread is 603935
[262391.741274174] Handler thread is 603936
[262392.242183857] Handler thread is 603935
[262392.743225834] Handler thread is 603935
[262393.244485002] Handler thread is 603935
[262393.244486029] Handler thread is 603936
[262393.745667295] Handler thread is 603936
[262393.745667013] Handler thread is 603935
[262394.246845870] Handler thread is 603935
[262394.246845874] Handler thread is 603936
[262394.748568780] Handler thread is 603935
[262394.748608692] Handler thread is 603935
[262395.249990591] Handler thread is 603935
[262395.249990578] Handler thread is 603936
[262395.751949939] Handler thread is 603935
[262395.751949944] Handler thread is 603936
^C
由于缓冲区刷新的顺序不同,需要对每一行按照时间排个序,排完顺序的结果是:
// 1
[262391.240335037] Handler thread is 603935
// 2
[262391.741273532] Handler thread is 603935
[262391.741274174] Handler thread is 603936
// 3
[262392.242183857] Handler thread is 603935
// 4
[262392.743225834] Handler thread is 603935
// 5
[262393.244485002] Handler thread is 603935
[262393.244486029] Handler thread is 603936
// 6
[262393.745667013] Handler thread is 603935
[262393.745667295] Handler thread is 603936
// 7
[262394.246845870] Handler thread is 603935
[262394.246845874] Handler thread is 603936
// 8
[262394.748568780] Handler thread is 603935
[262394.748608692] Handler thread is 603935
// 9
[262395.249990578] Handler thread is 603936
[262395.249990591] Handler thread is 603935
// 10
[262395.751949939] Handler thread is 603935
[262395.751949944] Handler thread is 603936
我们对以上结果做出如下观察:
- 主线程处理信号的次数(11 次)明显高于另外一个线程(6 次)。如果尝试交换 kill 命令的两个参数的顺序,则会得到相反的结果。这说明先得到信号的线程更容易处理信号。
- 发送的 $10 \times 2 = 20$ 个信号只触发了 17 次信号处理器。这说明每轮连续发两个信号,发得太快,进程来不及处理,就有信号被丢掉了(标准信号是不排队的)。
- 第 1 轮只有线程 603935 处理了一次信号。说明第二个信号被丢弃,同上。
- 第 2 轮线程 603935 和 603936 先后处理了信号。
- 第 9 轮线程 603936 和 603935 先后处理了信号。尽管
kill
命令首先以线程 603935 为参数调用了 kill(2)(因而内核会优先考虑此线程),但却是 603936 先处理信号。这里两个线程同时被signal_wake_up_state()
通知有信号要处理,也同时被非抢占式调度,因此 603936 也有处理信号的资格。这说明kill()
的作用对象是整个进程,尽力、但不能保证让给定线程先处理信号。从这里就有了本文的结论。 - 第 8 轮线程 603935 处理了两次信号。而第二个信号本是发给另外一个线程的,这也说明了
kill()
不能保证特定线程一定会处理信号。信号处理只发生在从内核态到用户态的转换期间,可能另外一个线程就是很不幸。
如果我们向同一个线程连续发送两次信号呢?
for i in {1..10}; do kill -SIGCHLD 609380 && kill -SIGCHLD 609380 && sleep 0.5; done
进程的输出(已排序)为:
Main thread is 609380
Thread 609381 starts
[264162.393963823] Handler thread is 609380
[264162.393964050] Handler thread is 609381
[264162.895081060] Handler thread is 609380
[264162.895087744] Handler thread is 609381
[264163.396078337] Handler thread is 609380
[264163.396088351] Handler thread is 609381
[264163.896908446] Handler thread is 609380
[264163.896919921] Handler thread is 609381
[264164.398372334] Handler thread is 609380
[264164.398379726] Handler thread is 609381
[264164.899537885] Handler thread is 609380
[264164.899547803] Handler thread is 609381
[264165.400854500] Handler thread is 609380
[264165.400863351] Handler thread is 609381
[264165.902220393] Handler thread is 609380
[264165.902241006] Handler thread is 609381
[264166.404214382] Handler thread is 609380
[264166.404235346] Handler thread is 609381
[264166.906002041] Handler thread is 609380
[264166.906025397] Handler thread is 609381
可能是因为调用了两次 kill
命令,中间有延迟,所以信号都被及时处理、没有丢弃。可以看出先是主线程处理信号,然后由另外一个线程处理信号(这是因为主线程处理信号期间屏蔽了相同信号,只有另外一个线程能处理信号)。
结论
kill
命令和 kill()
系统调用发送信号时,会将信号发给 pid 参数所在的线程组(也就是进程),同时会优先考虑 pid 参数所表示的线程。但它们不保证该线程总能处理信号:即便给定线程具备处理信号的条件,也可能由其他同样具备处理信号条件的线程先处理信号(但是触发条件比较极端)。
用 kill
发给进程信号时,如果给的 PID 就是进程号,那么信号会优先发给主线程(只是优先),不保证真正处理这个信号的是主线程。