好文值得收藏:LINUX内核–信号实现原理

好文值得收藏:LINUX内核–信号实现原理信号简介信号在最早的 UNIX 系统引入 用于进程间通信 是内核的一种软件机制 通过内核代码实现的

大家好,欢迎来到IT知识分享网。

好文值得收藏:LINUX内核--信号实现原理

目录

 信号简介

简单应用程序事例与API介绍

信号的常见系统调用

rt_sigaction

kill

sigaltstack

sigprocmask

与信号相关的内核数据结构

内核中信号发送

核心发送函数__send_signal

__send_signal的常见封装 

    force_sig_info

   do_tkill

  kill_something_info

信号传递(处理)

信号处理执行时机

获取信号信息

 切换执行到用户态信号处理函数

信号处理完成后再次回到内核态

学以致用

本文参考资料

趣味问答 


 信号简介

信号在最早的UNIX系统引入,用于进程间通信,是内核的一种软件机制,通过内核代码实现的。

内核对信号的响应机制有点像中断,信号来了后需要打断当前进程的执行,去执行信号处理函数,

执行完毕后,再恢复原来的上下文继续执行。

内核对信号是如何管理的?信号在内核中是如何响应的?当前进程执行过程中,代码执行流是如何

跳转到信号处理函数的?执行完信号处理函数后,又是如何再跳回来的?

请看下文,下文是基于X86处理器,4.18的内核为基础写的。

信号最原始的作用

信号是很短的消息,可以被发送到一个进程或者一组进程。

使用信号主要是两个目的:

  1. 让进程知道已经发生了一个特定的事件
  2. 强迫进程收到信号后,执行它自己代码中注册的信号处理程序

依靠信号机制,内核实现了如异常处理、进程状态管理,同时给用户程序提供了使用信号的支持。

所以,信号的作用说起来简单,其实在内核中的实现并不简单,属于内核的重要模块。

信号的分类

1~31为常规信号(regular signal),除了SIGUSR1和SIGUSR2之外,系统都赋予其特殊功能,如

总线错误SIGBUS、非法内存访问SIGSEGV,常规信号不具有信号缓存特性,当一个信号在处理

过程中,再来一个同样的信号,新来的信号会丢失,不会被缓存到队列。

32~64 为实时信号(real-time signal),用户可以自定义功能,具有信号缓存特性。

信号的状态

信号挂起(pending): 信号发送接口调用后,pending状态在进程(线程)的

                                  task_struct结构中更新,并把信号信息siginfo_t挂入队列。

                                  当信号处理时,从队列中dequeue队列,获取siginfo_t后,

                                  删除对应信号的pending状态位。

                                  详情见内核send_signal 、dequeue_synchronous_signal、

                                  dequeue_signal函数。

信号阻塞(blocked):当某个信号的信号处理函数正在执行且这个信号的sa_flags没有被

                                 设置为SA_NODEFER时,这时内核会设置当前信号为blocked状态,

                                 此外,用户程序可以显式的通过sigprocmask修改blocked状态。

                                  SIGSTOP和SIGKILL不能被设置blocked状态。

                                 设置阻塞某一个信号后,信号来了不会被处理,但是信号的pending

                                 状态会被设置,这个时候查看信号挂起sigpending可以查到这个信

                                  号。当阻塞状态被清除后,pengding状态的信号可以被重新处理。

                                  详情见内核send_signal 、dequeue_synchronous_signal、

                                  dequeue_signal、recalc_sigpending函数。

信号传递(delivered):也可以叫信号处理状态,当信号成功发送时,信号并不会被立即

                                   处理,只有当被发送进程处于正在运行状态且当前运行CPU出现

                                   从内核态到用户态切换时机时,信号处理过程才会被执行到。

信号忽略(ignore): 当信号被判断为ignored状态时,信号发送流程跳过设置pending状态,

                                     信号被drop掉。判断信号是否可被ignore可以查看内核函数

                                      sig_task_ignored,一般来说,当信号handler被设置为 SIG_IGN或者这

                                      个信号默认行为是ignore且handler 被设置为SIG_DFL时,这个信号会

                                      被忽略处理。还有一种特殊情况,就是init进程,只有在极少情况下才

                                      不会忽略信号。详情请看sig_task_ignored函数。

SIGKILL和SIGSTOP的特殊性

  1. 允许具有特权的用户终止、停止任何进程,除了进程0和进程1外。
  2. SIGSTOP和SIGKILL不能被设置blocked状态,详情见内核set_current_blocked函数。
  3. 当给线程组的某个线程发送SIGSTOP信号时,整个线程组都会被设置为STOP。
  4. 当给线程组的某个线程发送SIGKILL信号时,整个线程组都会被KILL。
  5. 用户态sigaction不能给SIGKILL和SIGSTOP设置自定义信号处理函数,会返回参数错误。

简单应用程序事例与API介绍

构造一个简单的应用事例,看一下Linux信号的使用:

  1. 设置SIGUSR1、SIGUSR2、SIGSEGV信号的用户态信号处理函数
  2. 其中SIGSEGV信号使用指定信号处理私有备用栈,用于后面信号处理栈空间排布的学习
  3. 设置屏蔽SIGFPE信号
  4. 当进程收到SIGUSR1信号时,通过kill函数发送信号SIGUSR2、SIGFPE到当前进程
  5. 当进程收到SIGUSR2信号时,访问非法内存地址,触发SIGSEGV信号

 测试代码如下:signal_test.c

#define _GNU_SOURCE #include <stdlib.h> #include <sys/syscall.h> #include <unistd.h> #include <stdio.h> #include <string.h> #include <sys/signal.h> #include <sys/ucontext.h> #include <errno.h> #define gettid() syscall(__NR_gettid) #define SIG_STACK_LEN (4096*8) #define SIG_STACK_POOL_SIZE (10) char sig_stack_buffer[SIG_STACK_POOL_SIZE][SIG_STACK_LEN]; int sigsegv_produce() { printf("%s\n",__func__); *(volatile char *)0x5555 = 0x33; return 0; } int siginfo_printf(siginfo_t *si) { printf("siginfo_printf:\n"); printf(" si_signo = %d\n",si->si_signo); printf(" si_errno = %d\n",si->si_errno); printf(" si_code = %d\n",si->si_code); if(si->si_code == SI_USER) { printf(" sender pid = %d\n",si->si_pid); printf(" sender uid = %d\n",si->si_uid); } /*这种情况时,si->si_addr 为异常操作的地址*/ if((si->si_signo == SIGSEGV)&&(si->si_code == SEGV_MAPERR)) { printf(" fault addr = 0x%lx\n",(long int)si->si_addr); } } static void signal_process(int sig, siginfo_t *si, void *ctx_void) { long int sp_get; printf("\nsp get = 0x%lx\n",(long int)&sp_get); printf("siginfo_t addr = 0x%lx\n",(long int)si); printf("ctx_void addr = 0x%lx\n",(long int)ctx_void); if(sig == SIGUSR1) { printf("==>recv signal SIGUSR1<==\n"); siginfo_printf(si); printf("send SIGFPE to pid %d\n",getpid()); kill(getpid(),SIGFPE); printf("send SIGUSR2 to pid %d\n",getpid()); kill(getpid(), SIGUSR2); } if(sig == SIGUSR2) { printf("==>recv signal SIGUSR2<==\n"); siginfo_printf(si); sigsegv_produce(); } if(sig == SIGSEGV) { printf("==>recv signal SIGSEGV<==\n"); siginfo_printf(si); /*如果此处不加exit,程序会进入无限循环*/ exit(1); } if(sig == SIGFPE) { printf("==>recv signal SIGFPE<==\n"); } } int set_handler(int signo, void (*handler)(int, siginfo_t *, void *), int flags) { struct sigaction sa; memset(&sa, 0, sizeof(sa)); sa.sa_sigaction = handler; sa.sa_flags = SA_RESTART|SA_SIGINFO|flags; sigemptyset(&sa.sa_mask); if (sigaction(signo, &sa, 0)) printf("sigaction failed\n"); } int set_signal_handle_onstack(int signo,void (*handler)(int, siginfo_t *, void *), int stack_id) { stack_t stack; int err; stack.ss_sp = sig_stack_buffer[stack_id]; stack.ss_size = SIG_STACK_LEN; stack.ss_flags = SS_ONSTACK; printf("set sig:%d signal stack sp = 0x%lx\n", signo, (long int)stack.ss_sp + SIG_STACK_LEN); err = sigaltstack(&stack, NULL); if (err != 0) printf("sigaltstack failed - %s\n",strerror(errno)); set_handler(signo,handler,SA_ONSTACK); return 0; } int main(int argc ,char argv) { sigset_t mask; /*设置SIGFPE 为阻塞状态*/ sigemptyset(&mask); sigaddset(&mask, SIGFPE); sigprocmask(SIG_BLOCK, &mask,NULL); set_handler(SIGUSR1,signal_process,0); set_handler(SIGUSR2,signal_process,0); /*设置SIGSEGV信号处理函数为signal_process ,且有自己的信号处理备用栈*/ set_signal_handle_onstack(SIGSEGV,signal_process,0); while(1) sleep(1); return 0; } 

执行结果打印如下:

root@intel-x86-64:/ramDisk# ./signal_test & set sig:11 signal stack sp = 0xe040 ./signal_test进程的pid号为1400,通过kill命令给进程发信号SIGUSR1(10) root@intel-x86-64:/ramDisk# kill -10 1400 ./signal_test进程收到 sp get = 0x7fffffffe620 siginfo_t addr = 0x7fffffffe770 ctx_void addr = 0x7fffffffe640 ==>recv signal SIGUSR1<== siginfo_printf: si_signo = 10 si_errno = 0 si_code = 0 0对应SI_USER,sent by kill, sigsend, raise sender pid = 1386 sender uid = 0 send SIGFPE to pid 1400 send SIGUSR2 to pid 1400 sp get = 0x7fffffffe120 siginfo_t addr = 0x7fffffffe270 ctx_void addr = 0x7fffffffe140 ==>recv signal SIGUSR2<== siginfo_printf: si_signo = 12 si_errno = 0 si_code = 0 sender pid = 1400 sender uid = 0 sigsegv_produce sp get = 0xdbe0 siginfo_t addr = 0xdd30 ctx_void addr = 0xdc00 ==>recv signal SIGSEGV<== siginfo_printf: si_signo = 11 si_errno = 0 si_code = 1 fault addr = 0x5555 

上面signal_test.c中用到的应用函数接口主要有

  1. sigaction

             函数原型为:int sigaction (int sig, const struct sigaction *act, struct sigaction *oact)

             通过这个接口设置内核对某个信号的处理,设置的信息都在struct sigaction *act中,

             获取的信息在struct sigaction *oact中。主要包括信号的处理函数sa_sigaction、

             信号处理的flag设置sa_flags、信号处理过程中屏蔽的信号集合sa_mask。

             其中sa_flags可以设置的值有:

            SA_RESTART:当系统调用被信号打断执行后,信号处理完毕后,自动重新执行系统调用

            SA_SIGINFO:信号处理函数中的siginfo_t *si有信息。

            SA_NODEFER:信号处理过程中,不屏蔽(blocked)对应的信号

            SA_ONSTACK:信号处理使用私有的备用栈

            还有一些sa_flags的值就不一一介绍了。

            用法事例请看signal_test.c中的set_handler、set_signal_handle_onstack函数

     2.  kill

              函数原型为int kill (__pid_t __pid, int __sig)

              给进程发送一个信号

    3. sigaltstack

              函数原型为int sigstack (struct sigstack *ss, struct sigstack *oss)

              设置、获取当前线程的信号处理备用栈信息,包括栈起始地址、栈size、flag。

              用法事例请看signal_test.c中的set_signal_handle_onstack函数

    4. sigprocmask

              函数原型为int sigprocmask (int how, const sigset_t *set, sigset_t *oset)

              设置、获取当前进程信号的阻塞配置

             sigset_t的本质是一个64bit的变量,一个信号对应其中的一个bit,对应bit置位,

             代表这个信号被阻塞。how的参数可以是SIG_SETMASK、SIG_BLOCK、

             SIG_UNBLOCK。

             对应的系统调用为SYSCALL_DEFINE4(rt_sigprocmask, int, how, sigset_t __user *, nset,

                   sigset_t __user *, oset, size_t, sigsetsize)

信号的常见系统调用

通过strace 跟踪上面事例程序signal_test执行的系统调用。signal_test.c中用到的信号方面的系统

调用主要有:

rt_sigaction

        用strace抓到sigaction应用函数接口抓到的系统调用接口为:

        rt_sigaction(SIGUSR1, {sa_handler=0xb73, sa_mask=[],

        sa_flags=SA_RESTORER|SA_RESTART|SA_SIGINFO,

       sa_restorer=0x7ffff7e40120}, NULL, 8)

        rt_sigaction在内核中系统调用的定义为:

        kernel/signal.c

        SYSCALL_DEFINE4(rt_sigaction, int, sig,

                        const struct sigaction __user *, act,

                        struct sigaction __user *, oact,

                        size_t, sigsetsize)

     主要做的工作有:

     (1) 使用copy_from_user 拷贝用户态act指向的struct sigaction的信息到内核态的

          struct k_sigaction new_sa变量

     (2) 把new_sa变量的内容copy到当前进程p->sighand->action[sig-1]对应的成员中。

          每一个信号对应一个action,action的类型为struct k_sigaction

     (3) 如果当前信号的handler被设置为ignore,那么清除当前进程信号共享信号pengding与进程下

          线程私有信号pengding中的对应的bit位,同时删除其对应链表上的struct sigqueue

     (4) 如果oact参数不为NULL,那么copy设置前的内核的struct sigaction信息到用户态的指针。

          SIGKILL 和SIGSTOP的信号处理函数不能通过rt_sigaction设置

kill

       kill在内核中系统调用的定义为:

       kernel/signal.c

       SYSCALL_DEFINE2(kill, pid_t, pid, int, sig)

      主要做的工作有:

      (1) 填充struct siginfo结构的信息,包括si_signo、si_code、si_pid、si_uid

      (2) 当pid大于0时,调用__kill_pgrp_info函数,向pid对应的进程发送信号

      (3) 当pid小于-1时,比如pid=-n,调用__kill_pgrp_info函数,向n所在的进程组发送信号

      (4) 当pid等于0时,向当前进程所在的进程组发送信号

      (5) 当pid等于-1时,向系统中除了进程1、进程0和当前进程之外的进程发送信号

sigaltstack

      用strace抓到sigaltstack应用函数接口抓到的系统调用接口为:

      sigaltstack({ss_sp=0x0, ss_flags=SS_ONSTACK, ss_size=32768}, NULL)

      sigaltstack应用函数接口对应系统调用:

      kernel/signal.c

     SYSCALL_DEFINE2(sigaltstack,const stack_t __user *,uss, stack_t __user *,uoss)

     主要做的工作有:

     (1) 如果uss不为空,把用户态传入的stack_t类型的数据copy到内核态的stack_t类型的变量new

          中

     (2) 设置current进程的t->sas_ss_sp、t->sas_ss_size、t->sas_ss_flags

     (3) 如果uoss不为空,返回设置前的current进程的t->sas_ss_sp、t->sas_ss_size、

           t->sas_ss_flags到uoss指向的空间。

sigprocmask

     用strace抓到sigprocmask应用函数接口抓到的系统调用接口为:

     rt_sigprocmask(SIG_BLOCK, [FPE], NULL, 8)

     sigprocmask应用函数接口对应系统调用:

     kernel/signal.c

    SYSCALL_DEFINE4(rt_sigprocmask, int, how, sigset_t __user *, nset,

                   sigset_t __user *, oset, size_t, sigsetsize)

    主要做的工作有:

    (1) copy 用户态传入的nset的信息到内核态的new_set变量

    (2) new_set变量中清除SIGKILL、SIGSTOP信号对应的bit,因为这两个信号不能设置阻塞

    (3) 调用函数sigprocmask做真正的设置:

         如果how为SIG_BLOCK,设置current进程的blocked中new_set中置位的bit位;

         如果how为SIG_UNBLOCK,清除current进程的blocked中new_set中置位的bit位;

         如果how为SIG_SETMASK,赋值new_set到current进程的blocked。

    (4) 如果oset不为null,copy 当前进程current->blocked到oset指向的用户态内存

与信号相关的内核数据结构

图解内核中与信号相关的内核数据结构直接的关系,参考了《深入理解LINUX内核》书中的

图11-1,在学习过程中,曽多次回看这个图,所以我觉得这个图不错,值得再画一次。

好文值得收藏:LINUX内核--信号实现原理

                                                  图1 信号主要内核数据结构关系图 

上图中相关数据结构就是Linux内核信号相关最关键的数据结构,下面是这些结构简单的注释,有些我也没搞懂,可供简单查阅,最细节的地方还是要看代码。

sched.h (include\linux) struct task_struct { … /* 信号描述符,用来描述共享挂起信号, 如果是给组发信号,pending状态会设置到signal->shared_pending 每个进程的资源限制数组rlim也在这个结构里 */ struct signal_struct *signal; /*信号处理描述符*/ struct sighand_struct *sighand; /* 描述线程私有挂起信号, 如果给指定线程发信号,pending状态会设置到这里的pending */ struct sigpending pending; /*被阻塞信号的掩码*/ sigset_t blocked; … } signal_types.h (include\linux) struct sigpending { struct list_head list; /*链表,用于挂接一个或多个struct sigqueue */ sigset_t signal;/*一个信号对应一个bit,从0到63,对应sig 1~64*/ }; struct sigqueue { struct list_head list; /*链表,用于挂接struct sigpending的list */ int flags; siginfo_t info;/*信号信息,包含什么信号,信号从哪来等信息*/ struct user_struct *user; }; struct k_sigaction { struct sigaction sa; #ifdef __ARCH_HAS_KA_RESTORER __sigrestore_t ka_restorer; /*4.18内核中搜到的都设置为NULL*/ #endif }; typedef void __signalfn_t(int); typedef __signalfn_t __user *__sighandler_t; typedef void __restorefn_t(void); typedef __restorefn_t __user *__sigrestore_t; /*一种类型的信号对应一个struct sigaction */ struct sigaction { unsigned int sa_flags; /*信号对应的flags,如SA_RESTART、SA_SIGINFO 、 SA_ONSTACK */ __sighandler_t sa_handler; /*信号处理函数指针*/ __sigrestore_t sa_restorer; /*当用户态信号处理函数执行完后,跳转回内核态执行的函数指针*/ sigset_t sa_mask; /* 指定这个信号的处理函数运行时,要屏蔽的信号*/ }; signal.h (arch\x86\include\asm) typedef struct { unsigned long sig[_NSIG_WORDS];/* 一个信号对应一个bit,从0到63,对应sig 1~64*/ } sigset_t; signal.h (include\linux\sched) struct sighand_struct { atomic_t count; struct k_sigaction action[_NSIG]; /*每一种信号对应一个struct k_sigaction */ spinlock_t siglock; /*在信号处理相关内核代码中,经常要获取这把锁*/ wait_queue_head_t signalfd_wqh; }; siginfo.h (include\uapi\asm-generic) typedef struct siginfo { int si_signo; /*信号的编号*/ /* 信号的发送者来源、原因 SI_USER:用户通过kill、raise发送 SI_KERNEL:一般内核信号发送函数发送的 SI_QUEUE: SI_TIMER; SI_ASYNCIO: SI_TKILL: */ int si_code; int si_errno;/*引起信号产生的出错码,在内核中搜了下,一般都设成了0*/ /* _sifields :不同的si_code的对应的更多的信息,由于不同的类型信息不同, 所以里面这是一个联合体结构 */ union { int _pad[SI_PAD_SIZE]; /* kill() */ struct { __kernel_pid_t _pid; /* sender's pid */ __kernel_uid32_t _uid; /* sender's uid */ } _kill; /* POSIX.1b timers */ struct { __kernel_timer_t _tid; /* timer id */ int _overrun; /* overrun count */ sigval_t _sigval; /* same as below */ int _sys_private; /* not to be passed to user */ } _timer; /* POSIX.1b signals */ struct { __kernel_pid_t _pid; /* sender's pid */ __kernel_uid32_t _uid; /* sender's uid */ sigval_t _sigval; } _rt; /* SIGCHLD */ struct { __kernel_pid_t _pid; /* which child */ __kernel_uid32_t _uid; /* sender's uid */ int _status; /* exit code */ __ARCH_SI_CLOCK_T _utime; __ARCH_SI_CLOCK_T _stime; } _sigchld; /* SIGILL, SIGFPE, SIGSEGV, SIGBUS, SIGTRAP, SIGEMT */ struct { void __user *_addr; /* faulting insn/memory ref. */ #ifdef __ARCH_SI_TRAPNO int _trapno; /* TRAP # which caused the signal */ #endif #ifdef __ia64__ int _imm; /* immediate value for "break" */ unsigned int _flags; /* see ia64 si_flags */ unsigned long _isr; /* isr */ #endif #define __ADDR_BND_PKEY_PAD (__alignof__(void *) < sizeof(short) ? \ sizeof(short) : __alignof__(void *)) union { /* * used when si_code=BUS_MCEERR_AR or * used when si_code=BUS_MCEERR_AO */ short _addr_lsb; /* LSB of the reported address */ /* used when si_code=SEGV_BNDERR */ struct { char _dummy_bnd[__ADDR_BND_PKEY_PAD]; void __user *_lower; void __user *_upper; } _addr_bnd; /* used when si_code=SEGV_PKUERR */ struct { char _dummy_pkey[__ADDR_BND_PKEY_PAD]; __u32 _pkey; } _addr_pkey; }; } _sigfault; /* SIGPOLL */ struct { __ARCH_SI_BAND_T _band; /* POLL_IN, POLL_OUT, POLL_MSG */ int _fd; } _sigpoll; /* SIGSYS */ struct { void __user *_call_addr; /* calling user insn */ int _syscall; /* triggering system call number */ unsigned int _arch; /* AUDIT_ARCH_* of syscall */ } _sigsys; } _sifields; } __ARCH_SI_ATTRIBUTES siginfo_t; 

内核中信号发送

核心发送函数__send_signal

内核中信号发送最终都会调到__send_signal 和send_sigqueue函数,其中send_sigqueue只被posix_timer_event函数调用到,所以我们平时常见的kill、异常信号的发送,都是经过__send_signal函数发送。

__send_signal函数在kernel/signal.c中,原型为:

static int __send_signal(int sig, struct siginfo *info, struct task_struct *t,

                            int group, int from_ancestor_ns)

参数有:

sig 指定发送的信号编号

info指定信号的信息,包括信号编号、信号来源

t指定信号发送的目标进程或线程的struct task_struct描述符

group指定这信号发给某个指定的线程还是某个线程组(进程)

from_ancestor_ns指定当前发送进程是不是属于祖先namespace

__send_signal做的主要工作就是分配一个struct sigqueue,把参数info中的信息写入struct sigqueue,再根据group的值,把struct sigqueue挂入t->signal->shared_pending或者t->pending,

的list,并且更新t->signal->shared_pendind.signal或者t->pending. signal中sig对应的bit。最后选择线程组中的一个线程设置其TIF_SIGPENDING标志,并wake_up。

内核代码复杂的地方就在于,一个函数在完成主要工作的流程中,加入了很多的判断、保护、特殊情况处理。__send_signal函数也一下。

__send_signal在主流程之外做的特殊判断流程有:

  1. 当发送的是一个stop信号时,清除进程中所有SIGCONT信号的pengding状态与待处理struct sigqueue,包括进程下各个线程的私有信号,见代码__send_signal àprepare_signal
  2. 当发送的是一个SIGCONT信号时,清除进程中所有stop系列信号的pengding状态与待处理struct sigqueue,包括进程下各个线程的私有信号,并且唤醒各个线程执行,确保线程快速进入继续运行状态。见代码__send_signal àprepare_signal
  3. 当一个信号属于ignored状态是,__send_signal直接丢弃这个信号的发送。见代码__send_signal àprepare_signalàsig_ignored
  4. 当信号是普通信号,且当前已经由同类型信号pengding时,直接丢弃这次信号发送。见代码__send_signalàlegacy_queue
  5. 当info参数的值为SEND_SIG_FORCED时,属于一种快速发信号路径,不需要挂载struct sigqueue,直接更新pending状态,一般SIGSTOP和SIGKILL可用于这种模式,需要和信号处理配合。
  6. 如果是发送给group即线程组,那么从线程组中选择一个task_struct,遵循均衡原则。见代码__send_signalàcomplete_signal
  7. 如果信号满足fatal的条件,那么可能整个线程group需要发送kill信号退出。见代码__send_signalàcomplete_signal
  8. __send_signal 函数中有一个tracepiont点:trace_signal_generate(sig, info, t, group, result),可用于做监控工具

__send_signal的常见封装 

    force_sig_info

    原型:int force_sig_info(int sig, struct siginfo *info, struct task_struct *t)

   当出现非法内存访问时由page_fault异常处理调用,

   page_fault >>do_page_fault >>__do_page_fault >>bad_area_access_error

   >> force_sig_info_fault >> force_sig_info >>specific_send_sig_info >>__send_signal

   force_sig_info调用__send_signal的提前处理:

   如果信号被设置为SIG_IGN忽略状态或者处于blocked状态,那么恢复handler为

   SIG_DFL,并清除其blocked状态

   do_tkill

   原型:static int do_tkill(pid_t tgid, pid_t pid, int sig)  kernel/signal.c

   系统调用tkill、tgkill调用的是do_kill

   do_tkillà do_send_specificà do_send_sig_infoà send_signalà__send_signal

  kill_something_info

  原型:static int kill_something_info(int sig, struct siginfo *info, pid_t pid) 

  系统调用kill调用的是kill_something_info

  kill_something_info >> __kill_pgrp_info >> group_send_sig_info >>do_send_sig_info

  >> send_signal >> __send_signal

  kill_something_info发信号的对象是整个进程或者进程组

信号传递(处理)

信号处理执行时机

信号发送后,信号并不会被立即处理,只有当信号发送的目标进程正在运行,且运行的CPU出现如中断返回、系统调用返回这样的内核态到用户态切换的时机时,pending的信号才会有机会被处理。

信号处理执行时机在内核中的函数调用关系如下:

正常系统调用路径:

entry_SYSCALL_64

  >>do_syscall_64

      >>syscall_return_slowpath

          >>prepare_exit_to_usermode

              >>exit_to_usermode_loop    判断是否有_TIF_SIGPENDING

                   >>exit_to_usermode_loop

                       >>do_signal

新进程创建返回路径:

ret_from_fork

    >> syscall_return_slowpath

        >>prepare_exit_to_usermode

            >>exit_to_usermode_loop   判断是否有_TIF_SIGPENDING

                >>exit_to_usermode_loop

                    >>do_signal

中断返回路径

retint_user

    >>prepare_exit_to_usermode

       >>exit_to_usermode_loop    判断是否有_TIF_SIGPENDING

           >>exit_to_usermode_loop

               >>do_signal

获取信号信息

获取待处理的信号信息主要在get_signal内核函数中,这个函数流程比较复杂,因为内核中的进程有多种状态,信号也有多种处理方式,组合起来就复杂了。

int get_signal(struct ksignal *ksig)

获取信号信息主要的流程就是:

  1. 扫描task中的struct sigpending中是否有挂起且不阻塞的信号
  2. 把task中的struct sigpending中list上挂载的struct sigqueue摘除,copy struct sigqueue中的信号信息到ksig对应的内存中,同时清除信号对应的pending bit位。

 

除了主要流程外,get_signal中有很多特殊的分支处理,如:

  1. 与上面核心发送函数__send_signal那一节中提到的特殊判断流程情况7对应的情况,当前进程已经被发送了fatal类型的信号,这时进程直接do_group_exit退出
  2. 当信号属于coredump信号集中的类型,且这个信号没有设置自定义的信号处理函数时,执行其默认的行为,生成coredump文件,生成coredump文件必先发送SIGKILL信号给进程,进程不运行后,再把进程的内存空间内存写入到coredump文件
  3. 当收到的信号是stop系列时,进行stop的处理流程,整个过程状态比较多,flag设置比较多,大家还是自己看代码把

 切换执行到用户态信号处理函数

   在获取到需要执行信号处理函数的信号后,调用handle_signal进行处理。

   handle_signal其实主要只做了两件事:

   1. 把进程被信号打断的上下文信息保存起来

   2. 修改当前struct pt_regs *regs指向的寄存器内容为跳转到信号处理函数的寄存器内容。

       在内核态跳转到用户态执行前,会把struct pt_regs *regs中的寄存器信息设置到寄存器里,然后执行汇编指令iretq或者sysretq,处理器就会跳转到用户态,并且执行rip寄存器中指向的代码段。

setup_rt_frame函数就是实际干上诉两件事的,

好文值得收藏:LINUX内核--信号实现原理

                                                      图2 信号处理时栈信息排布

上图是信号处理函数切换到用户态前,通过setup_rt_frame建立的栈信息排布,最上面是fpstate信息,在我的环境中size是0x280, 然后是struct rt_sigframe信息,size是0x1b8,里面主要包含信号打断前的寄存器上下文、信号的信息和从用户态信号处理函数切回到内核态的代码段地址pretcode。

在前面signal_test.c中用户态处理函数void signal_process(int sig, siginfo_t *si, void *ctx_void)的si指针与ctx_void指针,就指向图2中栈排布的地址。默认信号处理的栈共用线程的栈,但是内核支持给信号处理设置自己私有的独立栈,如前面signal_test.c中使用SA_ONSTACK机制后。对SIGSEGV信号的设置,设置后图2中栈的高低地址范围就是用户态设置的stack.ss_sp~stack.ss_size地址范围。

handle_signal最后执行了signal_setup_done函数,

这个函数主要作用是设置信号的blocked bit状态:

  1. 当信号的sa_mask被设置时,sa_mask置位bit对应的信号被设置为blocked状态,这样当信号在处理过程中时,这些信号不被接收。
  2. 当sa_flags没有设置SA_NODEFER时,当前信号被设置为blocked

做完上诉处理后,函数执行回到retint_user或者ret_from_fork或者entry_SYSCALL_64,在后面的汇编中,我们可以找到设置内核栈中struct pt_regs保存的寄存器信息到真正的寄存器,并执行INTERRUPT_RETURN或者USERGS_SYSRET64返回到用户态。具体代码可以查看

entry_64.S (arch\x86\entry)   

信号处理完成后再次回到内核态

当进入用户态指向信号处理函数时,sp指针指向的是图2中struct rt_sigframe所在的地址,struct rt_sigframe中的第一个成员是pretcode。

当用户态信号处理函数返回的时候,retq指令执行前,sp指针重新指向struct rt_sigframe所在的地址,然后执行retq指令,pop 栈中的frame->pretcode到rip寄存器,执行frame->pretcode指向的代码,回到内核态。

当调用用户态函数sigaction时,glibc自动添加SA_RESTORER 到sa_flags,并且设置sa_restorer为触发__NR_rt_sigreturn的系统调用的代码段地址。在内核信号处理__setup_rt_frame中,设置frame->pretcode为sa_restorer的值。

系统调用SYSCALL_DEFINE0(rt_sigreturn) 恢复信号处理前的寄存器上下文,恢复之前的blocked sigset_t,然后系统调用正常返回,代码执行流就会切回到最开始的地方。

学以致用

信号与进程的异常、退出、状态变化,息息相关,在懂得了信号的内核原理后,可以依靠其中的tracepoint做一个工具,监视系统中某个或某些进程的信号收发。当系统中出现奇怪的想象、问题时,从信号收发这个角度分析一下,没准对我们分析问题有所帮助。

了解Linux内核原理是一件非常划算的事,知其然并知其所以然,才能举一反三。只要你做软件开发,就可以持续受益。毕竟Linux系统不太可能被淘汰,即使linux系统不行了,这些原理代表的思想,也可以用在别的操作系统上。

本文参考资料

  1. 《深入理解LINUX内核》一书
  2.  Linux 4.18源代码

趣味问答 

可能也没那么有趣,但是我觉得学习过程中,以问答的思维方式学习,对掌握知识有好处,所以从这个角度列一些问答的问题。

1. 实时信号与普通信号的区别是啥?

 答:

(1)普通信号一个进程仅存在给定类型的一个挂起信号,同进程同类型的其他信号不被排队,只能简单丢弃。但是实时信号不同,同种类型的挂起信号可以有多个。

(2)普通信号一般都被系统赋予特殊作用。如总线错误SIGBUS、非法内存访问SIGSEGV。

 2. 进程已经设置自定义异常信号处理函数后,仍然想生成coredump文件怎么办?

 答:

       默认设置了自定义异常信号处理函数后,内核信号处理流程不会生成coredump文件,会去执行用户定义的信号处理。但是,当一个SIGSEGV信号的处理过程中,由于栈越界或者处理函数访问非法地址再次产生一个SIGSEGV信号时,内核认为这种情况是致命的问题,会杀死进程,并生成coredump文件。

  

3. 哪些进程的t->signal->flags状态是SIGNAL_UNKILLABLE?

答:

init进程,容器的init进程。

4. SIGCHLD信号什么时候产生?

答:

(1) 子进程终止时

(2)子进程收到SIGSTOP信号停止时

(3)子进程处于停止态,接收到SIGCONT后

5. 不开启信号处理备用栈时,为什么线程栈越界后,进程会退出?

答:

当线程栈越界时,触发 CPU segment fault异常,发送信号到线程,线程准备跳转到用户态执行信号处理函数时,需要在栈中建立frame,frame中包含跳转的信息,这时由于栈已经越界,在内核建立frame阶段,就会异常,导致用户态的信号处理函数根本没有机会执行。二次异常后,内核调用force_sigsegv函数,设置SIGSEGV信号为默认处理,然后杀死进程退出。

6. 在一个SIGSEGV异常处理函数中再产生一次SIGSEGV,会怎么样?

答:

进程会退出。当SIGSEGV信号处理中再次产生非法内存访问时,触发CPU异常,内核调用force_sig_info再次发送SIGSEGV信号到线程,这时SIGSEGV信号是 blocked状态,所以设置sa_handler = SIG_DFL,在进行信号处理时,执行默认的SIGSEGV信号处理流程,杀死进程,如果满足生成coredump条件,会生成coredump文件。

免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://haidsoft.com/151098.html

(0)
上一篇 2025-03-15 16:26
下一篇 2025-03-15 16:33

相关推荐

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注微信