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() 的空实现(总会设置 errnoENOSYS 并返回 -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

我们对以上结果做出如下观察:

  1. 主线程处理信号的次数(11 次)明显高于另外一个线程(6 次)。如果尝试交换 kill 命令的两个参数的顺序,则会得到相反的结果。这说明先得到信号的线程更容易处理信号。
  2. 发送的 $10 \times 2 = 20$ 个信号只触发了 17 次信号处理器。这说明每轮连续发两个信号,发得太快,进程来不及处理,就有信号被丢掉了(标准信号是不排队的)。
  3. 第 1 轮只有线程 603935 处理了一次信号。说明第二个信号被丢弃,同上。
  4. 第 2 轮线程 603935 和 603936 先后处理了信号。
  5. 第 9 轮线程 603936 和 603935 先后处理了信号。尽管 kill 命令首先以线程 603935 为参数调用了 kill(2)(因而内核会优先考虑此线程),但却是 603936 先处理信号。这里两个线程同时被 signal_wake_up_state() 通知有信号要处理,也同时被非抢占式调度,因此 603936 也有处理信号的资格。这说明 kill() 的作用对象是整个进程,尽力、但不能保证让给定线程先处理信号。从这里就有了本文的结论。
  6. 第 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 就是进程号,那么信号会优先发给主线程(只是优先),不保证真正处理这个信号的是主线程。