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

好文值得收藏:LINUX内核--信号实现原理_第1张图片

目录

 信号简介

简单应用程序事例与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 
#include 
#include 
#include 
#include 
#include 
#include 
#include  
#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 = 0x55555575e040
./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 = 0x55555575dbe0
siginfo_t addr = 0x55555575dd30
ctx_void addr = 0x55555575dc00
==>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=0x555555554b73, 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=0x555555756040, 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内核--信号实现原理_第2张图片

                                                  图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内核--信号实现原理_第3张图片

                                                      图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文件。

你可能感兴趣的:(linux,linux,linux内核信号)