Linux 进程管理深度剖析(三):信号机制与进程间通信(IPC)实现

前言

在 Linux 进程管理体系中,信号(Signal)是最古老也最核心的异步通知机制,而进程间通信(IPC)则是多进程协作的基础设施。本文基于 Linux 6.4-rc1 内核源码,深入剖析信号的数据结构、发送路径、处理流程,以及 pipe、POSIX 消息队列、System V IPC、UNIX Domain Socket 和 futex 的内核实现。理解这些机制,是系统编程、性能调优和内核调试的必备基础。


一、信号机制:数据结构全景

1.1 sigset_t:信号集位图

信号集的底层表示是一个位图数组。x86-64 下 _NSIG = 64_NSIG_BPW = 64,因此 _NSIG_WORDS = 1,整个集合用一个 64 位整数表示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// include/linux/signal.h
static inline void sigaddset(sigset_t *set, int _sig)
{
unsigned long sig = _sig - 1;
if (_NSIG_WORDS == 1)
set->sig[0] |= 1UL << sig;
else
set->sig[sig / _NSIG_BPW] |= 1UL << (sig % _NSIG_BPW);
}

static inline int sigismember(sigset_t *set, int _sig)
{
unsigned long sig = _sig - 1;
if (_NSIG_WORDS == 1)
return 1 & (set->sig[0] >> sig);
else
return 1 & (set->sig[sig / _NSIG_BPW] >> (sig % _NSIG_BPW));
}

信号编号从 1 开始,因此位操作时先减 1。信号 1-31 是传统不可靠信号(POSIX 标准信号),32-64 是实时信号(SIGRTMIN=34, SIGRTMAX=64)。

1.2 核心数据结构

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
// include/linux/signal_types.h

// 待处理信号队列:位图 + 链表
struct sigpending {
struct list_head list; // sigqueue 链表(实时信号排队)
sigset_t signal; // 位图(标记哪些信号已投递)
};

// 单个排队信号节点
struct sigqueue {
struct list_head list;
int flags;
kernel_siginfo_t info; // 包含 si_signo/si_code/si_pid 等
struct ucounts *ucounts;
};

// 用户态信号处理器描述
struct sigaction {
__sighandler_t sa_handler; // SIG_DFL / SIG_IGN / 用户函数
unsigned long sa_flags; // SA_RESTART / SA_SIGINFO / SA_NODEFER ...
sigset_t sa_mask; // 处理期间额外屏蔽的信号
};

struct k_sigaction {
struct sigaction sa;
};

// 信号处理器数组(线程组共享)
// include/linux/sched/signal.h
struct sighand_struct {
spinlock_t siglock;
refcount_t count; // 引用计数,线程共享
wait_queue_head_t signalfd_wqh;
struct k_sigaction action[_NSIG]; // 64 个信号的处理器数组
};

sighand_struct 是线程组内所有线程共享的,当调用 sigaction() 修改某个信号的处理器时,整个线程组都受影响。

1.3 signal_struct:进程级信号状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// include/linux/sched/signal.h
struct signal_struct {
refcount_t sigcnt;
atomic_t live;
int nr_threads;
struct list_head thread_head;

wait_queue_head_t wait_chldexit; // wait4() 等待子进程退出

struct task_struct *curr_target; // 信号负载均衡目标线程

/* 进程级共享 pending 队列(发给整个线程组的信号) */
struct sigpending shared_pending;

int group_exit_code;
int group_stop_count;
unsigned int flags; // SIGNAL_GROUP_EXIT / SIGNAL_STOP_STOPPED ...
// ...
};

每个 task_struct 还拥有自己私有的 task->pending,用于接收 tgkill/tkill 等指定线程的信号。信号投递时,shared_pending 由线程组中任意一个线程处理,task->pending 只能由目标线程处理。


二、信号发送路径

2.1 kill() 到 __send_signal_locked 的调用链

1
2
3
4
5
6
7
8
kill(pid, sig)
└── sys_kill()
└── kill_something_info()
├── kill_pid_info() → 发给单进程
└── __kill_pgrp_info() → 发给进程组
└── group_send_sig_info()
└── send_signal_locked()
└── __send_signal_locked()

进程组信号发送的核心实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// kernel/signal.c:1453
int __kill_pgrp_info(int sig, struct kernel_siginfo *info, struct pid *pgrp)
{
struct task_struct *p = NULL;
int retval, success;

success = 0;
retval = -ESRCH;
do_each_pid_task(pgrp, PIDTYPE_PGID, p) {
int err = group_send_sig_info(sig, info, p, PIDTYPE_PGID);
success |= !err;
retval = err;
} while_each_pid_task(pgrp, PIDTYPE_PGID, p);
return success ? 0 : retval;
}

2.2 __send_signal_locked:信号入队核心

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
// kernel/signal.c:1078
static int __send_signal_locked(int sig, struct kernel_siginfo *info,
struct task_struct *t, enum pid_type type, bool force)
{
struct sigpending *pending;
struct sigqueue *q;
int override_rlimit;

lockdep_assert_held(&t->sighand->siglock);

if (!prepare_signal(sig, t, force))
goto ret; // 信号被忽略,直接返回

// 进程组信号进 shared_pending,线程信号进私有 pending
pending = (type != PIDTYPE_PID) ? &t->signal->shared_pending : &t->pending;

// 不可靠信号(1-31):已经在 pending 中则不重复入队
if (legacy_queue(pending, sig))
goto ret;

// 实时信号:分配 sigqueue 节点加入链表
if (sig < SIGRTMIN)
override_rlimit = (is_si_special(info) || info->si_code >= 0);
else
override_rlimit = 0;

q = __sigqueue_alloc(sig, t, GFP_ATOMIC, override_rlimit, 0);
if (q) {
list_add_tail(&q->list, &pending->list); // 加入等待链表
copy_siginfo(&q->info, info);
} else if (sig >= SIGRTMIN && info->si_code != SI_USER) {
// 实时信号队列溢出,返回 EAGAIN
return -EAGAIN;
}

out_set:
signalfd_notify(t, sig);
sigaddset(&pending->signal, sig); // 在位图中置位
complete_signal(sig, t, type); // 选择目标线程并唤醒
return 0;
}

可靠信号 vs 不可靠信号的关键差异在第 1073-1076 行的 legacy_queue()

1
2
3
4
static inline bool legacy_queue(struct sigpending *signals, int sig)
{
return (sig < SIGRTMIN) && sigismember(&signals->signal, sig);
}

信号 1-31(sig < SIGRTMIN=34)如果 pending 位图已置位,新的投递会被静默丢弃。而实时信号(34-64)不受此约束,每次都会分配新的 sigqueue 节点入链表,从而实现多次投递的可靠排队。

2.3 signal_wake_up:唤醒目标进程

1
2
3
4
5
6
7
8
9
10
11
12
// kernel/signal.c:763
void signal_wake_up_state(struct task_struct *t, unsigned int state)
{
lockdep_assert_held(&t->sighand->siglock);

// 在 thread_info 中设置 TIF_SIGPENDING 标志
set_tsk_thread_flag(t, TIF_SIGPENDING);

// 唤醒处于 TASK_INTERRUPTIBLE 或 TASK_WAKEKILL 状态的线程
if (!wake_up_state(t, state | TASK_INTERRUPTIBLE))
kick_process(t); // 如果目标在其他 CPU 上运行,发送 IPI
}

TIF_SIGPENDING 标志设置后,目标进程在下次从内核返回用户态时(系统调用返回或中断返回)会检查并处理信号。

2.4 tgkill / tkill:向指定线程发送信号

tkill(tid, sig) 绕过线程组,直接向特定 tid 发送信号,信号进入 task->pending(私有队列)而非 signal->shared_pending。这对于多线程程序中精确控制信号投递至关重要,也是 pthread_kill() 的内核实现基础。


三、信号处理流程

3.1 信号处理时机

信号不是异步立即执行的,而是在进程从内核态返回用户态的”安全点”处理:

  • 系统调用返回前syscall_exit_to_user_mode()exit_to_user_mode_loop() → 检查 TIF_SIGPENDING
  • 中断返回前:硬件中断处理完毕返回用户态时检查

x86-64 的入口点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// arch/x86/kernel/signal.c:302
void arch_do_signal_or_restart(struct pt_regs *regs)
{
struct ksignal ksig;

if (get_signal(&ksig)) {
/* 有信号需要投递,调用 handle_signal */
handle_signal(&ksig, regs);
return;
}

/* 没有信号处理器,检查是否需要重启系统调用 */
if (syscall_get_nr(current, regs) != -1) {
switch (syscall_get_error(current, regs)) {
case -ERESTARTNOINTR:
regs->ax = regs->orig_ax;
regs->ip -= 2; // 重新执行 syscall 指令
break;
// ...
}
}
restore_saved_sigmask();
}

3.2 get_signal:从 pending 队列取信号

get_signal() 是信号出队的核心函数(kernel/signal.c:2642),其工作流程:

  1. 检查 TIF_SIGPENDING 标志
  2. 调用 try_to_freeze() 处理冻结请求
  3. 调用 dequeue_signal()task->pendingsignal->shared_pending 取出最高优先级信号
  4. 检查信号处理器:若为 SIG_DFL 且是致命信号,执行默认行为(SIGKILL 直接在此终止进程)
  5. 返回 ksignal(含信号编号、info、处理器)

SIGKILLSIGSTOP 在此处被特殊处理——它们永远不会到达用户态信号处理器:

1
2
3
4
5
6
// kernel/signal.c:2709
if ((signal->flags & SIGNAL_GROUP_EXIT) || signal->group_exec_task) {
ksig->info.si_signo = signr = SIGKILL;
// 直接跳到 fatal 处理,不查 sighand->action
goto fatal;
}

3.3 handle_signal 与信号栈帧

当信号有用户态处理器时,handle_signal() 负责在用户栈上构建信号栈帧,并修改 pt_regs 使处理器返回用户态时跳转到信号处理函数:

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
// arch/x86/kernel/signal.c:224
static void handle_signal(struct ksignal *ksig, struct pt_regs *regs)
{
bool stepping, failed;
struct fpu *fpu = &current->thread.fpu;

/* 处理系统调用重启语义 */
if (syscall_get_nr(current, regs) != -1) {
switch (syscall_get_error(current, regs)) {
case -ERESTART_RESTARTBLOCK:
case -ERESTARTNOHAND:
regs->ax = -EINTR;
break;
case -ERESTARTSYS:
if (!(ksig->ka.sa.sa_flags & SA_RESTART)) {
regs->ax = -EINTR;
break;
}
fallthrough;
case -ERESTARTNOINTR:
regs->ax = regs->orig_ax;
regs->ip -= 2;
break;
}
}

failed = (setup_rt_frame(ksig, regs) < 0);
if (!failed) {
regs->flags &= ~(X86_EFLAGS_DF|X86_EFLAGS_RF|X86_EFLAGS_TF);
fpu__clear_user_states(fpu);
}
signal_setup_done(failed, ksig, stepping);
}

3.4 rt_sigframe:信号栈帧布局

setup_rt_frame() 在用户栈上压入 struct rt_sigframe

1
2
3
4
5
6
7
// arch/x86/include/asm/sigframe.h:59
struct rt_sigframe {
char __user *pretcode; // 指向 sigreturn trampoline
struct ucontext uc; // 保存的用户态上下文(含 pt_regs)
struct siginfo info; // 信号信息
/* FP/XSAVE 状态紧跟其后 */
};

struct ucontext 内嵌 struct sigcontext,后者保存了所有通用寄存器(rax/rbx/…/rsp/rip/rflags)以及 FPU 状态指针。信号处理函数执行完毕后,调用 rt_sigreturn 系统调用,内核从 uc 中恢复完整的 CPU 状态,进程无缝回到被中断的执行点。


四、管道(pipe)

4.1 struct pipe_inode_info:环形缓冲区

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// include/linux/pipe_fs_i.h:58
struct pipe_inode_info {
struct mutex mutex;
wait_queue_head_t rd_wait, wr_wait; // 读写等待队列
unsigned int head; // 写入位置(生产者指针)
unsigned int tail; // 读取位置(消费者指针)
unsigned int max_usage; // 可用槽数上限
unsigned int ring_size; // 环形数组总槽数(必须是 2 的幂)
unsigned int readers; // 当前读端 fd 引用计数
unsigned int writers; // 当前写端 fd 引用计数
struct pipe_buffer *bufs; // 环形缓冲区数组
struct user_struct *user;
// ...
};

// 每个缓冲区槽指向一个物理页
struct pipe_buffer {
struct page *page;
unsigned int offset, len;
const struct pipe_buf_operations *ops;
unsigned int flags; // PIPE_BUF_FLAG_CAN_MERGE 等
unsigned long private;
};

默认 ring_size = PIPE_DEF_BUFFERS = 16,每槽一个 4KB 页,管道默认容量 64KB。headtail 不做掩码,允许自然溢出环绕,访问时用 index & (ring_size - 1) 取模。

4.2 pipe_write:数据写入

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
// fs/pipe.c:416(关键片段)
static ssize_t pipe_write(struct kiocb *iocb, struct iov_iter *from)
{
struct pipe_inode_info *pipe = filp->private_data;
unsigned int head;

__pipe_lock(pipe);

if (!pipe->readers) {
send_sig(SIGPIPE, current, 0); // 无读端则发 SIGPIPE
ret = -EPIPE;
goto out;
}

head = pipe->head;
was_empty = pipe_empty(head, pipe->tail);

/* 尝试合并到上一个 buf(小写入优化) */
if (chars && !was_empty) {
unsigned int mask = pipe->ring_size - 1;
struct pipe_buffer *buf = &pipe->bufs[(head - 1) & mask];
if ((buf->flags & PIPE_BUF_FLAG_CAN_MERGE) &&
offset + chars <= PAGE_SIZE) {
ret = copy_page_from_iter(buf->page, offset, chars, from);
buf->len += ret;
}
}

for (;;) {
head = pipe->head;
if (!pipe_full(head, pipe->tail, pipe->max_usage)) {
struct pipe_buffer *buf = &pipe->bufs[head & mask];
/* 分配新页,写入数据,推进 head */
pipe->head = head + 1;
} else {
/* 管道满,等待读者消费 */
if (wait_event_interruptible_exclusive(pipe->wr_wait,
pipe_writable(pipe)) < 0)
break;
}
}
// ...
}

4.3 管道容量控制

1
/proc/sys/fs/pipe-max-size  # 默认 1048576(1MB),非 root 用户上限

通过 fcntl(fd, F_SETPIPE_SZ, size) 可以动态调整单个管道容量。内核将请求的 size 向上取整到 2 的幂(不超过 pipe_max_size),然后 krealloc 扩展 bufs 数组。

4.4 splice 与零拷贝

splice(2) 系统调用利用管道的 pipe_buffer 结构实现文件到 socket 的零拷贝:数据以页引用的方式在管道内传递,不需要 memcpy 到用户缓冲区。vmsplice 可以将用户态内存页”赠送”给管道(PIPE_BUF_FLAG_GIFT),配合 splice 实现用户态到 socket 的全程零拷贝传输。


五、POSIX 消息队列

5.1 基于 tmpfs 的实现

POSIX 消息队列挂载在独立的 mqueue 文件系统(基于 tmpfs)。每个消息队列对应一个 mqueue_inode_info

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// ipc/mqueue.c:134
struct mqueue_inode_info {
spinlock_t lock;
struct inode vfs_inode;
wait_queue_head_t wait_q;

struct rb_root msg_tree; // 消息按优先级存储(红黑树)
struct rb_node *msg_tree_rightmost; // 最高优先级节点(O(1) 访问)
struct posix_msg_tree_node *node_cache;
struct mq_attr attr;

struct sigevent notify; // 消息到达时的通知方式
struct pid *notify_owner;
// 等待发送 / 等待接收的任务队列
struct ext_wait_queue e_wait_q[2];

unsigned long qsize; // 队列已用内存
};

5.2 优先级队列

1
2
3
4
5
6
// ipc/mqueue.c:61
struct posix_msg_tree_node {
struct rb_node rb_node; // 插入红黑树的节点
struct list_head msg_list; // 相同优先级的消息链表
int priority;
};

mq_send() 时,先在红黑树中查找对应优先级节点,若不存在则创建新节点插入;mq_receive() 直接取 msg_tree_rightmost(最右节点即最高优先级),时间复杂度 O(log P)(P 为不同优先级数量),同优先级内 FIFO 顺序。


六、System V IPC

6.1 信号量:struct sem_array

1
2
3
4
5
6
7
8
9
10
11
12
13
// ipc/sem.c:114
struct sem_array {
struct kern_ipc_perm sem_perm; // IPC 权限(key/uid/gid/mode)
time64_t sem_ctime;
struct list_head pending_alter; // 待执行的修改操作队列
struct list_head pending_const; // 待执行的只读操作队列
struct list_head list_id; // undo 请求链表
int sem_nsems; // 信号量个数
int complex_count;
unsigned int use_global_lock;

struct sem sems[]; // 信号量值数组(柔性数组)
} __randomize_layout;

semop() 的原子性保证:操作执行前检查整个操作集合能否同时满足(”all-or-nothing”语义),不能满足则整体入 pending_alter 睡眠等待。内核还支持 SEM_UNDO 标志,进程退出时自动回滚所有 semop 操作,防止死锁。

6.2 消息队列:struct msg_queue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// ipc/msg.c:49
struct msg_queue {
struct kern_ipc_perm q_perm;
time64_t q_stime; // 最后 msgsnd 时间
time64_t q_rtime; // 最后 msgrcv 时间
unsigned long q_cbytes; // 当前队列字节数
unsigned long q_qnum; // 消息数量
unsigned long q_qbytes; // 最大字节限制
struct pid *q_lspid; // 最后发送者 PID
struct pid *q_lrpid; // 最后接收者 PID

struct list_head q_messages; // 消息链表
struct list_head q_receivers; // 等待接收的进程
struct list_head q_senders; // 等待发送的进程(队列满时阻塞)
} __randomize_layout;

msgsnd() 将消息附加到 q_messages 链表尾部;msgrcv() 支持按 msgtype 过滤接收(正数接收指定类型,负数接收类型绝对值最小的,0 接收任意最早消息)。

6.3 共享内存

shmget()/shmat() 基于 tmpfsshmem)实现。shmget() 在 shmem 文件系统上创建一个匿名文件,shmat() 调用 do_mmap() 将该文件的页面映射到进程虚拟地址空间。多个进程 shmat() 同一个 shmid,它们的虚拟地址映射到相同的物理页(零拷贝),通过共享页表实现数据直接共享。


七、UNIX Domain Socket

7.1 struct unix_sock

1
2
3
4
5
6
7
8
9
10
11
12
13
// include/net/af_unix.h:56
struct unix_sock {
struct sock sk; // 必须是第一个成员
struct unix_address *addr; // 绑定的路径名地址
struct path path; // 对应的 dentry/inode
struct mutex iolock, bindlock;
struct sock *peer; // 连接的对端 socket
struct list_head link;
atomic_long_t inflight; // 传输中的 FD 数量
spinlock_t lock;
struct socket_wq peer_wq;
struct scm_stat scm_stat;
};

7.2 unix_stream_sendmsg:内存直传

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// net/unix/af_unix.c:2160
static int unix_stream_sendmsg(struct socket *sock, struct msghdr *msg, size_t len)
{
struct sock *sk = sock->sk;
struct sock *other = unix_peer(sk); // 对端 socket

err = scm_send(sock, msg, &scm, false); // 处理辅助数据(SCM_RIGHTS 等)

while (sent < len) {
// 分配 sk_buff,将用户数据拷贝进去
skb = sock_alloc_send_pskb(sk, size - data_len, data_len, ...);
err = unix_scm_to_skb(&scm, skb, !fds_sent); // 附加文件描述符
err = skb_copy_datagram_from_iter(skb, 0, &msg->msg_iter, size);

unix_state_lock(other);
skb_queue_tail(&other->sk_receive_queue, skb); // 直接放入对端接收队列
unix_state_unlock(other);
other->sk_data_ready(other); // 唤醒对端
}
}

UNIX Domain Socket 的关键优势:数据通过 sk_buff 在内核内存中传递,完全绕过网络协议栈(无 TCP/IP 头部处理、无校验和计算、无路由查找)。与管道相比,它支持双向通信和数据报语义(SOCK_DGRAM)。

7.3 文件描述符传递(SCM_RIGHTS)

UNIX Domain Socket 最独特的能力是通过 sendmsg() + SCM_RIGHTS 辅助消息传递文件描述符。发送端将 fd 列表附加到 struct scm_fp_list,内核在 unix_scm_to_skb() 中调用 get_file() 增加文件的引用计数,将 struct file * 指针存入 skb->cb(即 UNIXCB(skb).fp)。接收端通过 recvmsg() 取出后,内核在当前进程的文件描述符表中安装新的 fd,指向同一个 struct file 对象,实现跨进程 fd 共享。这是容器运行时、Chrome 沙箱和 systemd socket activation 等系统的核心机制。


八、futex:快速用户空间互斥锁

8.1 设计哲学

futex(Fast Userspace muTEX)的核心洞察:在无竞争时完全在用户态完成,仅在竞争时陷入内核。glibc 的 pthread_mutex_lock() 底层就是 futex。

8.2 struct futex_hash_bucket

1
2
3
4
5
6
// kernel/futex/futex.h:45
struct futex_hash_bucket {
atomic_t waiters; // 该桶中等待者计数(用于快速路径优化)
spinlock_t lock;
struct plist_head chain; // 优先级排序的等待队列(用于 PI-futex)
} ____cacheline_aligned_in_smp;

内核维护一个全局哈希表 futex_queues,以 futex 变量的物理地址(对于 shared futex)或虚拟地址(对于 private futex)为 key 哈希到对应的 bucket。

8.3 FUTEX_WAIT:快速路径 vs 慢速路径

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
// kernel/futex/waitwake.c:632
int futex_wait(u32 __user *uaddr, unsigned int flags, u32 val,
ktime_t *abs_time, u32 bitset)
{
struct futex_hash_bucket *hb;
struct futex_q q = futex_q_init;

retry:
/* 慢速路径:进入内核,锁定 hash bucket */
ret = futex_wait_setup(uaddr, val, flags, &q, &hb);
/*
* futex_wait_setup 内部:
* 1. get_futex_key():计算哈希键
* 2. futex_q_lock(q):锁定 bucket,原子地增加 waiters 计数
* 3. 再次读取 *uaddr,若已不等于 val 则返回 -EWOULDBLOCK
* (防止在用户态检查和内核入队之间漏掉 wake)
*/
if (ret)
goto out;

/* 入队并调度睡眠,等待 FUTEX_WAKE */
futex_wait_queue(hb, &q, to);

if (!futex_unqueue(&q))
goto out; // 已被 wake,返回 0

if (!signal_pending(current))
goto retry; // 虚假唤醒,重试

ret = -ERESTARTSYS;
// ...
}

快速路径(用户态完成):pthread_mutex_lock()cmpxchg 原子地尝试将 futex 值从 0 改为线程 TID;成功则无需系统调用。只有 cmpxchg 失败(有竞争)时才调用 sys_futex(FUTEX_WAIT, ...)

8.4 FUTEX_WAKE:唤醒等待者

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
// kernel/futex/waitwake.c:143
int futex_wake(u32 __user *uaddr, unsigned int flags, int nr_wake, u32 bitset)
{
struct futex_hash_bucket *hb;
DEFINE_WAKE_Q(wake_q);

ret = get_futex_key(uaddr, flags & FLAGS_SHARED, &key, FUTEX_READ);

hb = futex_hash(&key);

/* 快速路径:bucket 没有等待者,直接返回 */
if (!futex_hb_waiters_pending(hb))
return ret;

spin_lock(&hb->lock);
plist_for_each_entry_safe(this, next, &hb->chain, list) {
if (futex_match(&this->key, &key)) {
futex_wake_mark(&wake_q, this); // 加入唤醒队列
if (++ret >= nr_wake)
break;
}
}
spin_unlock(&hb->lock);
wake_up_q(&wake_q); // 批量唤醒
return ret;
}

futex_hb_waiters_pending() 检查 hb->waiters 计数,若为 0 则跳过所有锁操作——这是 futex 在无竞争时保持低开销的关键优化。

8.5 pthread_mutex 的 futex 实现原理

1
2
3
4
5
6
7
8
9
10
11
12
pthread_mutex_lock():
1. cmpxchg(futex_addr, 0, tid) // 用户态原子操作,无系统调用
成功 → 获得锁,返回
失败 → 调用 sys_futex(FUTEX_WAIT, futex_addr, current_val)
内核将当前线程加入 hash bucket 的等待链表
调度出去睡眠

pthread_mutex_unlock():
1. atomic_store(futex_addr, 0) // 用户态释放
2. 如果 futex 值之前标记了 FUTEX_WAITERS:
调用 sys_futex(FUTEX_WAKE, futex_addr, 1)
内核从等待链表中取出一个线程唤醒

九、诊断方法与工具

9.1 信号诊断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 查看所有信号编号与名称
kill -l

# 追踪进程的信号收发
strace -e trace=signal -p <PID>

# 解读 /proc/PID/status 中的信号位图
cat /proc/$(pgrep mysqld)/status | grep -E "Sig(Blk|Pnd|Cgt|Ign)"
# 示例输出:
# SigPnd: 0000000000000000 # 待处理信号(线程私有)
# SigBlk: 0000000000000000 # 已屏蔽信号
# SigIgn: 0000000000001000 # 忽略的信号(bit 12 = SIGPIPE)
# SigCgt: 00000001800024fb # 有用户态处理器的信号
# 用 python 解码:python3 -c "v=0x00000001800024fb; [print(i+1) for i in range(64) if v>>i&1]"

# bpftrace 追踪信号发送
bpftrace -e 'kprobe:__send_signal_locked { printf("sig=%d pid=%d->%d\n",
arg0, pid, ((struct task_struct*)arg2)->pid); }'

9.2 IPC 资源诊断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 查看所有 System V IPC 资源(信号量/消息队列/共享内存)
ipcs -a

# 查看管道的 fd 使用情况
lsof | grep pipe | head -20

# 查看 POSIX 消息队列(需要挂载 mqueue)
ls -la /dev/mqueue/

# 追踪 futex 竞争热点
bpftrace -e 'kprobe:do_futex { @[ustack] = count(); }
interval:s:5 { print(@); clear(@); exit(); }'

# 统计 futex 系统调用耗时
perf stat -e 'syscalls:sys_enter_futex' -p <PID> -- sleep 10

9.3 /proc/PID/status 信号位图解读

信号位图是 64 位十六进制数,每个 bit 对应一个信号编号(bit N = 信号 N+1)。例如 SigIgn: 0000000000001000

1
2
0x1000 = 0001 0000 0000 0000 (binary)
bit 12 已置位 → 信号 13 (SIGPIPE) 被忽略

守护进程通常会忽略 SIGPIPESIGHUP,这在 SigIgn 中可以直接观察到。


十、各 IPC 机制对比

机制 方向 持久性 传输效率 主要用途
信号 单向通知 非持久 极低(仅编号) 异步事件通知
pipe 单向数据流 进程生命期 高(ring buffer) 父子进程数据流
UNIX Socket 双向 进程生命期 高(内存拷贝) 本机 C/S 通信、fd 传递
POSIX MQ 双向 内核持久 中(优先级排序) 有序异步消息传递
SysV 消息队列 双向 内核持久 传统 IPC
共享内存 双向 内核持久 极高(零拷贝) 大数据量共享
futex 同步原语 非持久 极高(无竞争零系统调用) 互斥锁/条件变量

总结

Linux 信号与 IPC 机制的设计体现了内核”机制与策略分离”的哲学:

  • 信号通过 sigpending 位图 + sigqueue 链表分别处理可靠性需求,借助 TIF_SIGPENDING 在内核返回用户态的安全点触发;
  • pipe 以环形 pipe_buffer 数组为核心,支持页级零拷贝(splice),默认容量 64KB 可动态调整;
  • futex 以用户态原子操作为快速路径、内核哈希等待队列为慢速路径,是现代高性能同步原语的基础;
  • UNIX Domain Socket 通过 sk_buff 在内核内完成内存传递,配合 SCM_RIGHTS 实现跨进程 fd 共享;
  • System V IPCPOSIX 消息队列 则提供了更结构化的进程间通信语义。

理解这些机制的内核实现,能够帮助我们在系统设计、性能调优和问题排查时做出更明智的技术选择。


参考源码

  • kernel/signal.c — 信号发送、处理核心
  • include/linux/signal_types.h — 信号数据结构定义
  • include/linux/sched/signal.hsighand_struct / signal_struct
  • arch/x86/kernel/signal.c — x86-64 信号帧构建
  • arch/x86/include/asm/sigframe.hstruct rt_sigframe
  • include/linux/pipe_fs_i.hpipe_inode_info / pipe_buffer
  • fs/pipe.c — 管道读写实现
  • kernel/futex/core.c — futex 哈希表
  • kernel/futex/waitwake.cfutex_wait / futex_wake
  • kernel/futex/futex.hfutex_hash_bucket
  • ipc/sem.c — System V 信号量
  • ipc/msg.c — System V 消息队列
  • ipc/mqueue.c — POSIX 消息队列
  • net/unix/af_unix.c — UNIX Domain Socket
  • include/net/af_unix.hstruct unix_sock