Linux signal 图文详解(二)信号发送

目录

一、信号发送简介

二、实现原理

三、代码实现


(代码:linux 6.3.1,架构:arm64)

One look is worth a thousand words. ------ Tess Flanders

一、信号发送简介

Linux中,信号(signal)是进程间通信的一种重要机制,用于通知进程、线程、进程组发生了某种事件。用户态程序可以通过多种方式向其他进程发送信号,以下是常见的几种方法:

1)kill 系统调用

kill 可以向指定的进程、进程组发送特定信号,函数原型如下:

cpp 复制代码
#include <signal.h>

/*
 * - pid:目标进程 ID,特殊值含义:
 *     pid > 0:发送信号给 ID 为 pid 的进程
 *     pid = 0:发送信号给与当前进程同组的所有进程
 *     pid = -1:发送信号给所有有权限发送的进程(除了 init 进程)
 *     pid < -1:发送信号给进程组 ID 为 |pid| 的所有进程
 * - sig:要发送的信号编号(如 SIGTERM、SIGKILL 等)
 */
int kill(pid_t pid, int sig);

2)raise() 函数

raise()函数用于向当前进程发送信号,是kill(getpid(), sig)的简单封装,函数原型如下:

cpp 复制代码
#include <signal.h>

int raise(int sig);

3)pthread_kill() 函数

多线程环境下,pthread_kill() 用于向进程内指定的线程发送信号,函数原型如下:

cpp 复制代码
#include <signal.h>
#include <pthread.h>

int pthread_kill(pthread_t thread, int sig);

4)alarm() 函数

alarm() 用于在指定秒数后,向当前进程发送SIGALRM信号,函数原型如下:

cpp 复制代码
#include <unistd.h>

unsigned int alarm(unsigned int seconds);

由此可见,信号的发送对象既可以是一个线程、也可以是一个进程、进程组,对于发送给指定线程的信号,信号的处理就是指定线程负责完成;

而针对发送给进程的信号,最终处理该信号的可能是进程中任意一个有能力处理的线程,具体由谁处理,是由内核发送信号的时候,就已经决定好了的!

二、实现原理

上一节介绍的各种信号发送的用户态接口,这些接口在内核中,最终对应的 "信号发送" 函数实际上都是同一个:send_signal_locked

以下就是信号发送最核心代码的完整逻辑:

信号发送的处理流程主要分两块内容:

1)检查信号的类型、信号是否被目标task屏蔽、信号是否为legacy信号(主要涉及 步骤1 ~ 步骤6);

2)将信号设置到对应的私有、共享信号pending队列中,并给选取到的task置位TIF_SIGPENDING代表有信号需要处理,然后唤醒该task(若该任务处于挂起状态)去处理信号(主要涉及 步骤7 ~ 步骤10)。

这里需要注意的是: 信号的唯一处理时机是内核态返回用户态前夕,task的挂起只可能是在内核态,因此这里唤醒任务后,任务也是从内核态挂起的地方继续运行,直到其内核态处理完毕,并走到返回用户态前夕时,就会去处理pending的信号了!

一些特殊的信号处理:

1)SIGCONT、SIGSTOP类型信号

对于这类信号的处理,是在信号发送时,就直接在内核态中进行处理了(具体见步骤2、步骤3)

2)Fatal信号

对于fatal类型的信号,是需要整个进程下的所有线程都去立刻处理该信号的,因此会将进程中的所有线程都置位TIF_SIGPENDING、并设置相应信号pending位,然后唤醒所有线程去立刻处理Fatal信号。

三、代码实现

1)所有类型的信号发送在内核中的实现

cpp 复制代码
/* 发送信号给进程、进程组 */
SYSCALL_DEFINE2(kill, pid_t, pid, int, sig)
    kill_something_info(sig, &info, pid)
        if (pid > 0)
            /* 发送给进程 */
            kill_proc_info
                kill_pid_info
                    group_send_sig_info
                        do_send_sig_info
                            send_signal_locked
        if (pid != -1) {
            /* 发送给进程组 */
            __kill_pgrp_info
                do_each_pid_task(pgrp, PIDTYPE_PGID, p) {
                    group_send_sig_info
                        do_send_sig_info
                            send_signal_locked
                }
        } else {
            /* 发送给所有进程 */
            for_each_process(p) {
                group_send_sig_info(sig, info, p,PIDTYPE_MAX)
                    do_send_sig_info
                        send_signal_locked
            }
        }
        

/* 发送信号给指定线程 */
SYSCALL_DEFINE2(tkill, pid_t, pid, int, sig)
    do_tkill
        do_send_specific
            do_send_sig_info
                send_signal_locked

2)send_signal_locked 函数实现

cpp 复制代码
send_signal_locked(int sig, struct kernel_siginfo *info, struct task_struct *t, enum pid_type type) {
	bool force = false
	
	/* Check whether signal must be sent ? */
	...
	
	return __send_signal_locked(sig, info, t, type, force) {
		struct sigpending *pending
		struct sigqueue *q
		
        /* 1) 预处理信号, 若是STOP/CONTINUE类型的信号则先做一些处理, 若目标进程被置位SIGNAL_GROUP_EXIT则无需发送信号, 
         *    最后检查信号是否被目标进程ignore
         *
         * 处理影响进程级别的 stop/continue信号,与其他信号的action不同,如果是这两个信号的话,对应的sigaction会立即执行,
 		 * 并且不论blocking, ignoring, handling.
		 * This does the actual continuing for SIGCONT, but not the actual stopping for stop signals.
         * The process stop is done as a signal action for SIG_DFL.
         */
		res = prepare_signal(sig, struct task_struct *p = t, force) {
			struct signal_struct *signal = p->signal
            
            /* 1.1) 若目标进程的signal->flags被置位SIGNAL_GROUP_EXIT, 说明其需要走退出流程, 无需再给它发送信号了, 信号发送提前结束 */
            if (signal->flags & SIGNAL_GROUP_EXIT) {
                if (signal->core_state)
                    return sig == SIGKILL
                return false
            } else if (sig_kernel_stop(sig)) {
                /* 1.2) 若是STOP类型的信号, 则清空所有pending位图(进程共享/线程私有)中的SIGCONT信号位 */
                siginitset(&flush, sigmask(SIGCONT))
                flush_sigqueue_mask(&flush, &signal->shared_pending)
                for_each_thread(p, t)
                    flush_sigqueue_mask(&flush, &t->pending)
            } else if (sig == SIGCONT) {
                /* 1.3) 若是CONTINUE类型的信号, 则清空所有pending位图中的STOP类型信号位, 并唤醒所有线程 */
                siginitset(&flush, SIG_KERNEL_STOP_MASK)
                flush_sigqueue_mask(&flush, &signal->shared_pending)
                for_each_thread(p, t) {
                    flush_sigqueue_mask(&flush, &t->pending)
                    task_clear_jobctl_pending(t, JOBCTL_STOP_PENDING)
                    if (likely(!(t->ptrace & PT_SEIZED))) {
                        t->jobctl &= ~JOBCTL_STOPPED
                        wake_up_state(t, __TASK_STOPPED)
                    } else
                        ptrace_trap_notify(t)
                }
            }
            
            /* 1.4) 检查发送的信号是否被目标进程忽略(如: 其handler为SIG_IGN等) */
            is_ignored = sig_ignored(p, sig, force) {
                if (sigismember(&t->blocked, sig) || sigismember(&t->real_blocked, sig))
                    return false
                
                if (t->ptrace && sig != SIGKILL)
                    return false
                
                return sig_task_ignored(t, sig, force) {
                    void __user *handler = sig_handler(t, sig)
                        return t->sighand->action[sig - 1].sa.sa_handler
                        
                    return sig_handler_ignored(handler, sig) {
                        /* Is it explicitly or implicitly ignored? */
                        return handler == SIG_IGN || (handler == SIG_DFL && sig_kernel_ignore(sig))
                    }//sig_handler_ignored
                }//sig_task_ignored
            }//sig_ignored
            
            return !is_ignored
		}//prepare_signal
		
        if(!res)
            goto ret

        /* 2) 根据信号发送类型, 判断信号是发送给进程还是线程的, 获取对应的pending信号队列 */
        pending = (type != PIDTYPE_PID) ? &t->signal->shared_pending : &t->pending
    
        /* 3) 若是legacy类型信号, 且信号已经被设置到pending队列中, 则提前结束信号发送 */
        is_iegacy = legacy_queue(pending, sig) {
            return (sig < SIGRTMIN) && sigismember(&signals->signal, sig)   // SIGRTMIN: 32
        }//legacy_queue
        if (is_iegacy)
               goto ret
        
        /* 4) 若是给内核线程发送一个SIGKILL信号, 则跳过siginfo allocation过程 */
        if ((sig == SIGKILL) || (t->flags & PF_KTHREAD))
            goto out_set
    
        /* 5) 若当前没有超过实时信号的MAX, 则分配一个sigqueue对象 */
        q = __sigqueue_alloc(sig, t, GFP_ATOMIC, override_rlimit, 0) {
            struct sigqueue *q = NULL
           
            rcu_read_lock()
            struct ucounts *ucounts = task_ucounts(t)
            sigpending = inc_rlimit_get_ucounts(ucounts, UCOUNT_RLIMIT_SIGPENDING)
            rcu_read_unlock()
            if (!sigpending)
                return NULL
            if (override_rlimit || likely(sigpending <= task_rlimit(t, RLIMIT_SIGPENDING))) {
                q = kmem_cache_alloc(sigqueue_cachep, gfp_flags)
            }
            return q
        }//__sigqueue_alloc
    
        if (q) {
            /* 6) 将sigqueue对象添加到对应的pending队列中, 并设置sigqueue->info字段内容 */
            list_add_tail(&q->list, &pending->list)
            switch ((unsigned long) info) {
                case (unsigned long) SEND_SIG_NOINFO:
                    clear_siginfo(&q->info)
                    q->info.si_signo = sig
                    q->info.si_errno = 0
                    q->info.si_code = SI_USER
                    q->info.si_pid = task_tgid_nr_ns(current, task_active_pid_ns(t))
                    q->info.si_uid = from_kuid_munged(task_cred_xxx(t, user_ns), current_uid())
                    break
                case (unsigned long) SEND_SIG_PRIV:
                    clear_siginfo(&q->info)
                    q->info.si_signo = sig
                    q->info.si_errno = 0
                    q->info.si_code = SI_KERNEL
                    q->info.si_pid = 0
                    q->info.si_uid = 0
                    break
                default:
                    copy_siginfo(&q->info, info)
                    break
            }
        }//if (q)
    
    out_set:
        signalfd_notify(t, sig) {
            if (unlikely(waitqueue_active(&tsk->sighand->signalfd_wqh)))
                wake_up(&tsk->sighand->signalfd_wqh)
        }
        
        /* 7) 将信号添加到对应的pending位图中 */
        sigaddset(&pending->signal, sig)
        
        /* 8) 在目标进程中搜寻并唤醒一个可以处理该信号的线程, 同时置位TIF_SIGPENDING, 若是致命信号则将所有线程都置位SIGKILL, 并全部唤醒 */
        complete_signal(sig, struct task_struct *p = t, type) {
            struct signal_struct *signal = p->signal
            struct task_struct *t
            
            /* 8.1) 判断目标任务是否能够处理该信号 */
            ret = wants_signal(sig, p) {
                if (sigismember(&p->blocked, sig))  /* 目标任务屏蔽了该信号 */
                    return false
                if (p->flags & PF_EXITING)          /* 目标任务正在执行退出流程 */
                    return false
                if (sig == SIGKILL)                 /* 信号为SIGKILL */
                    return true
                if (task_is_stopped_or_traced(p))
                    return false
                return task_curr(p) || !task_sigpending(p)
            }//wants_signal
            
            /* 8.2) 若目标任务不能处理该信号, 则搜寻进程中其他线程是否有能够处理该信号的 */
            if (ret) {
                t = p
            } else if ((type == PIDTYPE_PID) || thread_group_empty(p)) {
                return
            } else {
                t = signal->curr_target
                while (!wants_signal(sig, t)) {
                    t = next_thread(t)
                    if (t == signal->curr_target)
                        return
                }
                signal->curr_target = t
            }
            
            /* 8.3) 若信号是致命信号, 则将进程中所有线程都置位SIGKILL信号, 并将所有线程唤醒 */
            if (sig_fatal(p, sig) && (signal->core_state || !(signal->flags & SIGNAL_GROUP_EXIT)) &&
                !sigismember(&t->real_blocked, sig) && (sig == SIGKILL || !p->ptrace))
            {
                if (!sig_kernel_coredump(sig)) {
                    /* 给进程signal置位SIGNAL_GROUP_EXIT, 代表进程需要走退出流程 */
                    signal->flags = SIGNAL_GROUP_EXIT
                    signal->group_exit_code = sig
                    signal->group_stop_count = 0
                    
                    t = p
                    
                    /* 将所有线程都置位SIGKILL pending信号位并唤醒它们 */
                    do {
                        task_clear_jobctl_pending(t, JOBCTL_PENDING_MASK)
                        sigaddset(&t->pending.signal, SIGKILL)
                        signal_wake_up(t, 1)
                    } while_each_thread(p, t)
                    return
                }
            }
            
            /* 8.4) 唤醒处理该信号的任务, 并为其置位TIF_SIGPENDING */
            signal_wake_up(t, fatal = sig == SIGKILL) {
                if (fatal && !(t->jobctl & JOBCTL_PTRACE_FROZEN)) {
                    t->jobctl &= ~(JOBCTL_STOPPED | JOBCTL_TRACED)
                    state = TASK_WAKEKILL | __TASK_TRACED
                }
                signal_wake_up_state(t, state) {
                    set_tsk_thread_flag(t, TIF_SIGPENDING)
                    
                    ### 若wake_up_state返回0, 代表目标task已经在其他CPU上运行, 此时调用kick_process发送核间中断, 使其在中断返回前夕相应信号
                    if (!wake_up_state(t, state | TASK_INTERRUPTIBLE))
                        kick_process(struct task_struct *p = t) {
                            preempt_disable()
                            cpu = task_cpu(p)
                            if ((cpu != smp_processor_id()) && task_curr(p))
                                smp_send_reschedule(cpu)
                            preempt_enable()
                        }
                }//signal_wake_up_state
            }//signal_wake_up
            return
        }//complete_signal
    ret:
        return ret
	}//__send_signal_locked
}
相关推荐
東雪蓮☆6 小时前
Shell 编程 —— 正则表达式与文本处理实战
linux·运维·服务器
叁仟叁佰6 小时前
Shell脚本编程:函数、数组与正则表达式详解
运维·服务器·网络·chrome·正则表达式
Gary Studio7 小时前
Linux-驱动积累
linux
lin张8 小时前
函数、数组与 grep + 正则表达式的 Linux Shell 编程进阶指南
linux·运维
苏三福9 小时前
交叉编译linux-arm32位程序
linux·运维·服务器
Lin南舟10 小时前
掌握正则表达式与文本处理:提升 Shell 编程效率的关键技巧
linux·正则表达式
青瓦梦滋10 小时前
Linux基本工具(yum、vim、gcc、Makefile、git、gdb)
linux·运维·服务器·c++
梅坞茶坊11 小时前
Centos安装unoconv文档转换工具并在PHP中使用phpword替换word模板中的变量后,使用unoconv将word转换成pdf
linux·服务器·centos
2004v200411 小时前
交叉编译 手动安装 libzip 库 移植ARM 需要 zlib的
linux·运维·arm开发