Linux内核中的信号机制 从用户层到内核层

信号机制是类UNIX系统中的一种重要的进程间通信手段之一。我们经常使用信号来向一个进程发送一个简短的消息。例如:假设我们启动一个进程通过socket读取远程主机发送过来的网络数据包,此时由于网络因素当前主机还没有收到相应的数据,当前进程被设置为可中断等待状态(TASK_INTERRUPTIBLE),此时我们已经失去耐心,想提前结束这个进程,于是可以通过kill命令想这个进程发送KILL信号,内核会唤醒该进程,执行它的信号处理函数,KILL信号的默认处理是退出该进程。当然并不是一定要进程处于TASK_INTERRUPTIBLE状态时才能够处理信号。

另外应用程序可以通过signal()等函数来为一个信号设置默认处理函数。例如当用户按下CTRL+C时,shell将会发出SIGINT信号,SIGINT的默认处理函数是执行进程的退出代码,但是下面的例子把SIGINT的响应函数设置为int_handler。

#include <signal.h>
#include <stdio.h>

void int_handler(int signum)
{
 printf("\nSIGINT signal handler.\n");
 printf("exit.\n");
 exit(-1);
}

int main()
{
 signal(SIGINT, int_handler);
 printf("int_handler set for SIGINT\n");

 while(1)
 {
  printf("go to sleep.\n");
  sleep(60);
 }

 return 0;
}

信号的分发和处理是在内核态进行的,当从上面的例子中可以看书,信号的处理函数可能是在用户态,在这种情况下,内核需要内核态构建一个临时的用户态环境,然后调用用户态的信号处理函数。

信号只是一个数字,数字为0-31表示不同的信号,如下表所示。

编号

信号名

默认动作

说明

1

SIGHUP

进程终止

终端断开连接

2

SIGINT

进程终止

用户在键盘上按下CTRL+C

3

SIGQUIT

进程意外结束(Dump)

用户在键盘上按下CTRL+\

4

SIGILL

进程意外结束(Dump)

遇到非法指令

5

SIGTRAP

进程意外结束(Dump)

遇到断电,用于调试

6

SIGABRT/SIGIOT

进程意外结束(Dump)

 

7

SIGBUS

进程意外结束(Dump)

总线错误

8

SIGFPE

进程意外结束(Dump)

浮点异常

9

SIGKILL

进程终止

其他进程发送SIGKILL将导致目标进程终止

10

SIGUSR1

进程终止

应用程序可自定义使用

11

SIGSEGV

进程意外结束(Dump)

非法的内存访问

12

SIGUSR2

进程终止

应用程序可自定义使用

13

SIGPIPE

进程终止

管道读取端已经关闭,写入端进程会收到该信号

14

SIGALRM

进程终止

定时器到时

15

SIGTERM

进程终止

发送该信号使目标进程终止

16

SIGSTKFLT

进程终止

堆线错误

17

SIGCHLD

忽略

子进程退出时会向父进程发送该信号

18

SIGCONT

忽略

进程继续执行

19

SIGSTOP

进程暂停

发送该信号会使目标进程进入TASK_STOPPED状态

20

SIGTSTP

进程暂停

在终端上按下CTRL+Z

21

SIGTTIN

进程暂停

后台进程从控制终端读取数据

22

SIGTTOU

进程暂停

后台进程从控制终端读取数据

23

SIGURG

忽略

socket收到设置紧急指针标志的网络数据包

24

SIGXCPU

进程意外结束(Dump)

进程使用CPU已经超过限制

25

SIGXFSZ

进程意外结束(Dump)

进程使用CPU已经超过限制

26

SIGVTALRM

进程终止

进程虚拟定时器到期

27

SIGPROF

进程终止

进程Profile定时器到期

28

SIGMNCH

忽略

进程终端窗口大小改变

29

SIGIO

进程暂停

用于异步IO

29

SIGPOLL

进程暂停

用于异步IO

30

SIGPWR

进程暂停

电源失效

31

SIGUNUSED

进程暂停

保留未使用

注意在上标中的默认动作是指,在没有任何程序为相应的信号设置信号处理函数的情况下,内核接收到该信号的默认处理方式,但在实际中,有可能不是这样的。另外,在这里,进程终止一般是指进程通过do_exit()退出,进程意外结束(Dump)则表示进程遇到了一个异常。默认情况下,内核会根据进程当时的内存情况,在进程的当前目录中生成一个Core Dump文件,以后用户可以通过这个文件分析进程异常的原因。这个工作主要通过do_coredump()来完成。

由于早期只有31个信号,内核仅仅使用一个32位的变量signal来表示进程接收到的信号,因此如果要向一个进程发送一个信号,就把signal的第n位设置为1,这非常类似中断请求寄存器SRCPND寄存器,同时,还有一个blocked的变量,用来屏蔽信号,这类似中断屏蔽寄存器INTMSK。这样做的好处是可以“很快”判断出一个进程收到了哪些信号,如果采用链表或者数组,则需要扫描整个队列,但这也带来了新的问题,如果向一个进程发送了SIGINT信号,在这个信号处理之前,再次发送SIGINT,当这个进程开始处理信号时,它只知道收到了SIGINT信号,而无法判断出有几个SIGINT需要处理。此后加入了信号队列,把收到的信号保存在这个队列中,就可以很好的解决这个问题了。但是为了兼容的目的,仍能保留了旧的信号处理方式,因此1-32还是按原有的方式进行处理,而33-64则使用新的机制,为了区别对待,编号为33-64的信号又称为实时信号。需要注意的是:这里的“实时”和实时操作系统中的“实时”没有任何联系,实时信号在处理速度上并不会比普通信号快,它们之间的区别就是:普通信号会对多次的同一个信号进行“合并”处理,而实时信号会一一处理。因此我们这里仅讨论普通信号。

实时信号引入了信号队列,为了处理上的方便,普通信号也使用了信号队列,仅仅从数字上无法区分实时信号和普通信号。由于在linux中,进程对象和线程对象都是task_struct,因此需要区别对待线程的信号和进程的信号。在上图中,Private Signal Queue是线程(在linux中称为轻权进程)信号队列,而Shared Signal Queue是进程(在linux中被称为进程组)信号队列。对于进程信号,则由进程组中的每一个线程共享。例如,在上图中,pending和shared_pending分别是Private Signal Queue和Shared Signal Queue其类型都是sigpending(/include/linux/signal.h),定义如下:

struct sigpending {
 struct list_head list;
 sigset_t signal;
};

list用于连接信号队列,signal是一个位图,每一位表示一个对应的信号,用于指示信号队列中有哪些信号等待处理,其类型为sigset_t(include/asm-arm/signal.h),其定义如下:

#define _NSIG  64
#define _NSIG_BPW 32
#define _NSIG_WORDS (_NSIG / _NSIG_BPW)


typedef struct {
 unsigned long sig[_NSIG_WORDS];
} sigset_t;

sigset_t是一个数组,总共有64位,对应64个信号位图(32个普通信号和32个实时信号)。在以后的信号发送的分析中,我们会看到,对于普通信号,只需要把sigset_t中对应的位置1就可以了,而对于实时信号,还需要把相关信息添加到list的信号队列,信号队列类型为sigqueue(include/linux/signal.h),定义如下:

/*
 * Real Time signals may be queued.
 */

struct sigqueue {
 struct list_head list;
 spinlock_t *lock;
 int flags;
 siginfo_t info;
 struct user_struct *user;
};

sigqueue中的list是队列链表指针,info为这个信号的相关信息,其定义如下(include/asm-generic/siginfo.h):

typedef struct siginfo {
 int si_signo;
 int si_errno;
 int si_code;

 union {
  int _pad[SI_PAD_SIZE];

  /* kill() */
  struct {
   pid_t _pid;  /* sender's pid */
   __ARCH_SI_UID_T _uid; /* sender's uid */
  } _kill;

  /* POSIX.1b timers */
  struct {
   timer_t _tid;  /* timer id */
   int _overrun;  /* overrun count */
   char _pad[sizeof( __ARCH_SI_UID_T) - sizeof(int)];
   sigval_t _sigval; /* same as below */
   int _sys_private;      /* not to be passed to user */
  } _timer;

  /* POSIX.1b signals */
  struct {
   pid_t _pid;  /* sender's pid */
   __ARCH_SI_UID_T _uid; /* sender's uid */
   sigval_t _sigval;
  } _rt;

  /* SIGCHLD */
  struct {
   pid_t _pid;  /* which child */
   __ARCH_SI_UID_T _uid; /* sender's uid */
   int _status;  /* exit code */
   clock_t _utime;
   clock_t _stime;
  } _sigchld;

  /* SIGILL, SIGFPE, SIGSEGV, SIGBUS */
  struct {
   void __user *_addr; /* faulting insn/memory ref. */
#ifdef __ARCH_SI_TRAPNO
   int _trapno; /* TRAP # which caused the signal */
#endif
  } _sigfault;

  /* SIGPOLL */
  struct {
   __ARCH_SI_BAND_T _band; /* POLL_IN, POLL_OUT, POLL_MSG */
   int _fd;
  } _sigpoll;
 } _sifields;
} siginfo_t;

上图中的sighand保存信号的处理函数指针,其作用类似于中断向量表,类型为sighand_struct(include/linux/sched.h),定义为:

struct sighand_struct {
 atomic_t  count;
 struct k_sigaction action[_NSIG];
 spinlock_t  siglock;
};

_NSIG定义在asm-arm/signal.h中,为64,数组action,对应64个信号处理函数的相关信息,烈性为k_sigaction,在arm平台上,

struct k_sigaction {
 struct sigaction sa;
};

sigantion(include/asm-arm/signal.h)的定义如下:

struct sigaction {
 __sighandler_t sa_handler;
 unsigned long sa_flags;
 __sigrestore_t sa_restorer;
 sigset_t sa_mask;  /* mask last for extensibility */
};

sa_handler就是信号处理函数指针。另外在task_struct结构中还有一个blocked可以用来屏蔽信号。明白了上面的主要数据结构的作用之后,很容易想到信号的处理主要有以下几方面。

设置信号回调函数:内核吧函数的相关信息保存到对应的sigantion结构中。

信号的发送:通过相关系统调用吧一个指定的信号发送到目标进程,如果该信号没有被屏蔽,就把信号的相关信息添加到信号队列中,如果有必要就唤醒目标进程。

信号响应:进程被唤醒后,根据信号队列中的信息,调用信号回调函数。

我们再来看一下信号回调函数,sa_handler的类型是__sihandler_t(include/asm-generic/singal.h),其定义为:

typedef void __signalfn_t(int);
typedef __signalfn_t __user *__sighandler_t;

应用程序发送信号时,主要通过kill进行。注意:不要被“kill”迷惑,它并不是发送SIGKILL信号专用函数。这个函数主要通过系统调用sys_kill()进入内核,它接收两个参数:

第一个参数为目标进程id,kill()可以向进程(或进程组),线程(轻权线程)发送信号,因此pid有以下几种情况:

pid>0:目标进程(可能是轻权进程)由pid指定。

pid=0:信号被发送到当前进程组中的每一个进程。

pid=-1:信号被发送到任何一个进程,init进程(PID=1)和以及当前进程无法发送信号的进程除外。

pid<-1:信号被发送到目标进程组,其id由参数中的pid的绝对值指定。

第二个参数为需要发送的信号。

由于sys_kill处理的情况比较多,分析起来比较复杂,我们从tkill()函数入手,这个函数把信号发送到由参数指定pid指定的线程(轻权进程)中。tkill的内核入口是sys_tkill(kernel/signal.c),其定义如下:

/*
 *  Send a signal to only one task, even if it's a CLONE_THREAD task.
 */
asmlinkage long
sys_tkill(int pid, int sig)
{
 struct siginfo info;
 int error;
 struct task_struct *p;

 /* This is only valid for single tasks */
 if (pid <= 0)//对参数pid进行检查
  return -EINVAL;

 info.si_signo = sig; //根据参数初始化一个siginfo结构
 info.si_errno = 0;
 info.si_code = SI_TKILL;
 info.si_pid = current->tgid;
 info.si_uid = current->uid;

 read_lock(&tasklist_lock);
 p = find_task_by_pid(pid);//获取由pid指定的线程的task_struct结构
 error = -ESRCH;
 if (p) {
  error = check_kill_permission(sig, &info, p);//权限检查
  /*
  * The null signal is a permissions and process existence
  * probe.  No signal is actually delivered.
  */
  if (!error && sig && p->sighand) {
   spin_lock_irq(&p->sighand->siglock);
   handle_stop_signal(sig, p);
   //对某些特殊信号进程处理,例如当收到SIGSTOP时,需要把信号队列中的SIGCONT全部删除
   error = specific_send_sig_info(sig, &info, p);//把信号加入到信号队列
   spin_unlock_irq(&p->sighand->siglock);
  }
 }
 read_unlock(&tasklist_lock);
 return error;
}

sys_tkill函数主要是通过pecific_send_sig_info()函数实现的,下面我们看一下pecific_send_sig_info()(kernel/signal.c)的定义:

static int
specific_send_sig_info(int sig, struct siginfo *info, struct task_struct *t)
{
 int ret = 0;

 if (!irqs_disabled())
  BUG();
 assert_spin_locked(&t->sighand->siglock);

 if (((unsigned long)info > 2) && (info->si_code == SI_TIMER))
  /*
  * Set up a return to indicate that we dropped the signal.
  */
  ret = info->si_sys_private;
 /*信号被忽略*/
 /* Short-circuit ignored signals.  */
 if (sig_ignored(t, sig))
  goto out;

 /* Support queueing exactly one non-rt signal, so that we
    can get more detailed information about the cause of
    the signal. */
 if (LEGACY_QUEUE(&t->pending, sig))
  goto out;

 ret = send_signal(sig, info, t, &t->pending);//实际的发送工作
 if (!ret && !sigismember(&t->blocked, sig))
  signal_wake_up(t, sig == SIGKILL);
out:
 return ret;
}

首先调用sig_ignored检查信号是否被忽略,然后检查发送的信号是不是普通信号,如果是普通信号,就需要根据信号位图来检查当前信号队列中是否已经存在该信号,如果已经存在,对于普通信号不需要做任何处理。然后调用send_signal来完成实际的发送工作,send_signal()是信号发送的重点,除sys_tkill之外的函数,最终都是通过send_signal()来完成信号的发送工作的。

这里注意到想send_signal()传递的参数时t->pending,也就是连接Private Signal Queue的那条链。最后,如果发送成功就调用signal_wake_up()来唤醒目标进程,这样可以保证该进程进入就绪状态,从而有机会被调度执行信号处理函数。

现在我们来看看send_signal()(kernel/signal.c)函数,这个函数的主要工作就是分配并初始化一个sigqueue结构,然后把它添加到信号队列中。

static int send_signal(int sig, struct siginfo *info, struct task_struct *t,
   struct sigpending *signals)
{
 struct sigqueue * q = NULL;
 int ret = 0;

 /*
  * fast-pathed signals for kernel-internal things like SIGSTOP
  * or SIGKILL.
  */
 if ((unsigned long)info == 2)
  goto out_set;

 /* Real-time signals must be queued if sent by sigqueue, or
    some other real-time mechanism.  It is implementation
    defined whether kill() does so.  We attempt to do so, on
    the principle of least surprise, but since kill is not
    allowed to fail with EAGAIN when low on memory we just
    make sure at least one signal gets delivered and don't
    pass on the info struct.  */

 q = __sigqueue_alloc(t, GFP_ATOMIC, (sig < SIGRTMIN &&
          ((unsigned long) info < 2 ||
          info->si_code >= 0)));//分配sigqueue结构
 if (q) {//如果成功分配到sigqueue结构,就把它添加到队列中,并对其初始化
  list_add_tail(&q->list, &signals->list);
  switch ((unsigned long) info) {
  case 0:
   q->info.si_signo = sig;
   q->info.si_errno = 0;
   q->info.si_code = SI_USER;
   q->info.si_pid = current->pid;
   q->info.si_uid = current->uid;
   break;
  case 1:
   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);//拷贝sigqueue结构
   break;
  }
 } else {
  if (sig >= SIGRTMIN && info && (unsigned long)info != 1
    && info->si_code != SI_USER)
  /*
  * Queue overflow, abort.  We may abort if the signal was rt
  * and sent by user using something other than kill().
  */
   return -EAGAIN;
  if (((unsigned long)info > 1) && (info->si_code == SI_TIMER))
   /*
    * Set up a return to indicate that we dropped
    * the signal.
    */
   ret = info->si_sys_private;
 }

out_set:
 sigaddset(&signals->signal, sig);//设置信号位图
 return ret;
}

从上面的分析可以看出,我们看到信号被添加到信号队列之后,会调用signal_wake_up()唤醒这个进程,signal_wake_up()(kernel/signal.c)的定义如下:

/*
 * Tell a process that it has a new active signal..
 *
 * NOTE! we rely on the previous spin_lock to
 * lock interrupts for us! We can only be called with
 * "siglock" held, and the local interrupt must
 * have been disabled when that got acquired!
 *
 * No need to set need_resched since signal event passing
 * goes through ->blocked
 */
void signal_wake_up(struct task_struct *t, int resume)
{
 unsigned int mask;

 set_tsk_thread_flag(t, TIF_SIGPENDING);//为进程设置TIF_SIGPENDING标志

 /*
  * For SIGKILL, we want to wake it up in the stopped/traced case.
  * We don't check t->state here because there is a race with it
  * executing another processor and just now entering stopped state.
  * By using wake_up_state, we ensure the process will wake up and
  * handle its death signal.
  */
 mask = TASK_INTERRUPTIBLE;
 if (resume)
  mask |= TASK_STOPPED | TASK_TRACED;
 if (!wake_up_state(t, mask))
  kick_process(t);
}

signal_wake_up()首先为进程设置TIF_SIGPENDING标志,说明该进程有延迟的信号要等待处理。然后再调用wake_up_state()唤醒目标进程,如果目标进程在其他的CPU上运行,wake_up_state()将返回0,此时调用kick_process()向该CPU发送一个处理器间中断。当中断返回前戏,会为当前进程处理延迟的信号。

此后当该进程被调度时,在进程返回用户空间前,会调用do_notify_resume()处理该进程的信号。

当进程被调度时,会调用do_notify_resume()来处理信号队列中的信号。信号处理主要就是调用sighand_struct结构中对应的信号处理函数。do_notify_resume()(arch/arm/kernel/signal.c)函数的定义如下:

asmlinkage void
do_notify_resume(struct pt_regs *regs, unsigned int thread_flags, int syscall)
{
 if (thread_flags & _TIF_SIGPENDING)
  do_signal(&current->blocked, regs, syscall);
}

_TIF_SIGPENDING标志是在signal_wake_up()函数中设置的,检查该标志后,接下来就调用do_signal()函数,我们来看看do_signal()(arch/arm/kernel/signal.c)的具体定义:

/*
 * Note that 'init' is a special process: it doesn't get signals it doesn't
 * want to handle. Thus you cannot kill init even with a SIGKILL even by
 * mistake.
 *
 * Note that we go through the signals twice: once to check the signals that
 * the kernel can handle, and then we build all the user-level signal handling
 * stack-frames in one go after that.
 */
static int do_signal(sigset_t *oldset, struct pt_regs *regs, int syscall)
{
 struct k_sigaction ka;
 siginfo_t info;
 int signr;

 /*
  * We want the common case to go fast, which
  * is why we may in certain cases get here from
  * kernel mode. Just return without doing anything
  * if so.
  */
 if (!user_mode(regs))//regs保存的是进入内核态之前的寄存器现场,必须为用户模式,否则直接返回
  return 0;

 if (try_to_freeze())
  goto no_signal;

 if (current->ptrace & PT_SINGLESTEP)
  ptrace_cancel_bpt(current);//和调试相关,我们在后面的文章中会具体分析

 signr = get_signal_to_deliver(&info, &ka, regs, NULL);//取出等待处理的信号
 if (signr > 0) {
  handle_signal(signr, &ka, &info, oldset, regs, syscall);//处理信号
  if (current->ptrace & PT_SINGLESTEP)
   ptrace_set_bpt(current);
  return 1;
 }

 no_signal:
 /*
  * No signal to deliver to the process - restart the syscall.
  */
 if (syscall) {
  if (regs->ARM_r0 == -ERESTART_RESTARTBLOCK) {
   if (thumb_mode(regs)) {
    regs->ARM_r7 = __NR_restart_syscall;
    regs->ARM_pc -= 2;
   } else {
    u32 __user *usp;

    regs->ARM_sp -= 12;
    usp = (u32 __user *)regs->ARM_sp;

    put_user(regs->ARM_pc, &usp[0]);
    /* swi __NR_restart_syscall */
    put_user(0xef000000 | __NR_restart_syscall, &usp[1]);
    /* ldr pc, [sp], #12 */
    put_user(0xe49df00c, &usp[2]);

    flush_icache_range((unsigned long)usp,
        (unsigned long)(usp + 3));

    regs->ARM_pc = regs->ARM_sp + 4;
   }
  }
  if (regs->ARM_r0 == -ERESTARTNOHAND ||
      regs->ARM_r0 == -ERESTARTSYS ||
      regs->ARM_r0 == -ERESTARTNOINTR) {
   restart_syscall(regs);
  }
 }
 if (current->ptrace & PT_SINGLESTEP)
  ptrace_set_bpt(current);
 return 0;
}

执行do_signal()函数时,进程一定处于内核空间,通常进程只有通过中断或者系统调用才能进入内核空间,regs保存着系统调用或者中断时的现场。user_mode()根据regs中的cpsr寄存器来判断是中断现场环境还是用户态环境。如果不是用户态环境,就不对信号进行任何处理,直接从do_signal()函数返回。

如果user_mode()函数发现regs的现场是内核态,那就意味着这不是一次系统调用的返回,也不是一次普通的中断返回,而是一次嵌套中断返回(或者在系统调用过程中发生了中断)。此时大概的执行路径应该是这样的:假设进场现在运行在用户态,此时发生一次中断,进场进入内核态(此时user_mode(regs)返回1,意味着中断现场是用户态。),此后在中断返回前,发生了一个更高优先级的中断,于是CPU开始执行高优先级的处理函数(此时user_mode(regs)返回0,意味着中断现场是在内核态)。当高优先级中断处理结束后,在它返回时,是不应该处理信号的,因为信号的优先级比中断的优先级低。在这种情况下,对信号的处理将会延迟到低优先级中断处理结束之后。相对于Windows内核来说,尽管linux内核中没有一组显式的操作函数来实现这一系列的优先级管理方案,但是linux内核和Windows内核都使用了同样的机制,优先级关系为:高优先级中断->低优先级中断->软中断(类似Windows内中的DPC)->信号(类似Windows内核中的APC)->进程运行。

如果user_mode(regs)返回1,接下来会执行(中间略去一下和本文关系不大的代码)get_signal_to_deliver(),这个函数从当前进程的信号队列(保存Private Signal Queue和Shared Signal Queue)取出等待处理的信号(调用dequeue_signal()函数),然后根据信号定位到对应的signal_struct结构,如果信号的处理函数sa_handler为SIG_IGN,就忽略该信号,继续取下一个信号;如果信号的处理函数sa_handler为SIG_DFL,意味着按照信号默认的处理方式对待就可以了(例如直接调用do_coredump()等)。

如果get_signal_to_deliver()函数返回值大于0,说明这个信号的处理函数是在用户态空间(通过signal()和sigaction()等函数设置的自定义信号处理函数。),将调用handle_signal()函数进行处理。handle_signal()函数的定义如下:

/*
 * OK, we're invoking a handler
 */ 
static void
handle_signal(unsigned long sig, struct k_sigaction *ka,
      siginfo_t *info, sigset_t *oldset,
      struct pt_regs * regs, int syscall)
{
 struct thread_info *thread = current_thread_info();
 struct task_struct *tsk = current;
 int usig = sig;
 int ret;

 /*
  * If we were from a system call, check for system call restarting...
  */
 if (syscall) {
  switch (regs->ARM_r0) {
  case -ERESTART_RESTARTBLOCK:
  case -ERESTARTNOHAND:
   regs->ARM_r0 = -EINTR;
   break;
  case -ERESTARTSYS:
   if (!(ka->sa.sa_flags & SA_RESTART)) {
    regs->ARM_r0 = -EINTR;
    break;
   }
   /* fallthrough */
  case -ERESTARTNOINTR:
   restart_syscall(regs);
  }
 }

 /*
  * translate the signal
  */
 if (usig < 32 && thread->exec_domain && thread->exec_domain->signal_invmap)
  usig = thread->exec_domain->signal_invmap[usig];

 /*
  * Set up the stack frame//设置栈帧
  */
 if (ka->sa.sa_flags & SA_SIGINFO)
  ret = setup_rt_frame(usig, ka, info, oldset, regs);
 else
  ret = setup_frame(usig, ka, oldset, regs);

 /*
  * Check that the resulting registers are actually sane.
  */
 ret |= !valid_user_regs(regs);

 /*
  * Block the signal if we were unsuccessful.
  */
 if (ret != 0) {
  spin_lock_irq(&tsk->sighand->siglock);
  sigorsets(&tsk->blocked, &tsk->blocked,
    &ka->sa.sa_mask);
  if (!(ka->sa.sa_flags & SA_NODEFER))
   sigaddset(&tsk->blocked, sig);
  recalc_sigpending();
  spin_unlock_irq(&tsk->sighand->siglock);
 }

 if (ret == 0)
  return;

 force_sigsegv(sig, tsk);
}

在这样情况下,进程当前处于内核态,而信号处理函数却处于用户态,为此必须在进程的用户态构造一个临时的堆栈环境(因为进程的信号处理函数在进行函数调用以及使用局部变量时需要使用堆栈。),然后进入用户态执行信号处理函数,最后再返回内核态继续执行。在这个过程中,有以下几个问题需要解决:

1.临时的用户态堆栈在哪里呢?这个很好解决,因为可以直接使用进程现有的用户态堆栈,这里要保证的是:使用结束后这个堆栈必须和使用前是一模一样的。

2.临时堆栈解决后,需要确定的是通过什么方法来保证返回到用户态后,进程执行的是信号的处理函数。我们知道在进入内核态时,内核态堆栈中保存了一个中断现场,也就是一个pt_regs结构,中断返回地址就保存在pt_regts中的pc中,因此我们这里只要把当前进程的pt_regs中pc设置为sa_handler,然后返回到用户态就开始从sa_handler处开始执行了。

unsigned long handler = (unsigned long)ka->sa.sa_handler;
regs->ARM_pc = handler;

3.当信号的用户态处理函数执行结束时,需要再次进入内核态,还原用户态堆栈,并且修改pt_regs中的pc,保证将来能够按照正常的方式返回用户态。我们知道进程要主动进入内核态只有通过系统调用,出发异常等方法,为此内核专门提供了一个系统调用sys_sigreturn()(还有一个sys_rt_sigreturn()),但是如何调用sys_sigreturn()呢?强制安装信号处理函数最后必须调用一个sigreturn()不是一个好办法,因为不了解内核的程序员会对这个限制感到不解,为此程序员常常忘记在它们的信号处理函数的末尾调用sigreturn(),如果真是这样,arm-linux-gcc也检测不出这个错误。为此,内核修改regs的ARM_lr值,:

regs->ARM_lr = retcode; 

当用户态信号处理函数运行结束时,会从lr取出返回地址,因此内核在构建临时regs时,会把上面这段代码的入口地址保存在lr,这样当信号处理完成后,就会顺利的通过系统调用sys_sigreturn()进入内核。

4.当通过构造的sys_sigreturn()返回到内核态之后,内核需要顺利的返回到用户态执行原来的代码(信号处理前应该返回的用户空间状态),但是此时进入内核空间的pt_regs上下文是通过sys_sigreturn()构造的,而最初的内核堆栈中的pt_regs上下文在第一次返回用户空间执行信号处理函数时,就已经被“销毁”了(内核态堆栈的pt_regs上下文在中断返回后就不复存在了)。而现在必须通过最初的pt_regs上下文返回用户态,为此,在构建临时堆栈环境时,内核会把最初的pt_regs上下文备份到临时堆栈中(位于用户态堆栈),当通过系统调用sys_sigreturn()再次进入内核时,内核从用户态空间还原出原始的pt_regs。最后正常返回。

通过上面的讨论,我们知道在这个迂回的处理过程中,关键在于用户态的临时堆栈环境的建立,这是一个sigframe结构:

/*
 * Do a signal return; undo the signal stack.  These are aligned to 64-bit.
 */
struct sigframe {
 struct sigcontext sc;//保存一组寄存器上下文
 unsigned long extramask[_NSIG_WORDS-1];
 unsigned long retcode;//保存返回地址
 struct aux_sigframe aux __attribute__((aligned(8)));
};

struct rt_sigframe {
 struct siginfo __user *pinfo;
 void __user *puc;
 struct siginfo info;
 struct ucontext uc;
 unsigned long retcode;
 struct aux_sigframe aux __attribute__((aligned(8)));
};

其中的sigcontext的作用类似于pt_regs用于保存相关寄存器上下文,原始的pt_regs的相关信息就备份在这里。而整个sigframe结构是通过get_sigframe()函数在进程用户态空间分配的,get_sigframe()定义如下:

static inline void __user *
get_sigframe(struct k_sigaction *ka, struct pt_regs *regs, int framesize)
{
 unsigned long sp = regs->ARM_sp;
 void __user *frame;

 /*
  * This is the X/Open sanctioned signal stack switching.
  */
 if ((ka->sa.sa_flags & SA_ONSTACK) && !sas_ss_flags(sp))
  sp = current->sas_ss_sp + current->sas_ss_size;

 /*
  * ATPCS B01 mandates 8-byte alignment
  */
 frame = (void __user *)((sp - framesize) & ~7);

 /*
  * Check that we can actually write to the signal frame.
  */
 if (!access_ok(VERIFY_WRITE, frame, framesize))
  frame = NULL;

 return frame;
}

get_sigframe()通过用户态空间堆栈的sp-sizeof(struct sigframe)在用户态堆栈的顶部分配了一篇存储空间,将来使用完成后,再通过sp+sizeof(struct sigframe)还原。

通过上面的讨论,我们再回到do_signal()中来,如果有用户态的信号处理函数,do_signal()会调用handle_signal(),handle_signal()将调用setup_frame()或者setup_rt_frame()来完成实际的工作,这里我们以setup_frame()为例进行进一步讨论。

static int
setup_frame(int usig, struct k_sigaction *ka, sigset_t *set, struct pt_regs *regs)
{
 //在用户态堆栈上分配一个sigframe结构
 struct sigframe __user *frame = get_sigframe(ka, regs, sizeof(*frame));
 int err = 0;

 if (!frame)
  return 1;

 //把相关信息从内核态备份到用户态堆栈的sigframe结构中
 err |= setup_sigcontext(&frame->sc, &frame->aux, regs, set->sig[0]);

 if (_NSIG_WORDS > 1) {
  err |= __copy_to_user(frame->extramask, &set->sig[1],
          sizeof(frame->extramask));
 }

 if (err == 0)
  err = setup_return(regs, ka, &frame->retcode, frame, usig);

 return err;
}

setup_return()设置返回地址,其定义如下:

static int
setup_return(struct pt_regs *regs, struct k_sigaction *ka,
      unsigned long __user *rc, void __user *frame, int usig)
{
 unsigned long handler = (unsigned long)ka->sa.sa_handler;
 unsigned long retcode;
 int thumb = 0;
 unsigned long cpsr = regs->ARM_cpsr & ~PSR_f;

 /*
  * Maybe we need to deliver a 32-bit signal to a 26-bit task.
  */
 if (ka->sa.sa_flags & SA_THIRTYTWO)
  cpsr = (cpsr & ~MODE_MASK) | USR_MODE;

#ifdef CONFIG_ARM_THUMB
 if (elf_hwcap & HWCAP_THUMB) {
  /*
  * The LSB of the handler determines if we're going to
  * be using THUMB or ARM mode for this signal handler.
  */
  thumb = handler & 1;

  if (thumb)
   cpsr |= PSR_T_BIT;
  else
   cpsr &= ~PSR_T_BIT;
 }
#endif
<SPAN style="WHITE-SPACE: pre"> </SPAN>//这里的retcode就是保存手工构造的sigreturn()代码
 if (ka->sa.sa_flags & SA_RESTORER) {
  retcode = (unsigned long)ka->sa.sa_restorer;
 } else {
  unsigned int idx = thumb;

  if (ka->sa.sa_flags & SA_SIGINFO)
   idx += 2;

  if (__put_user(sigreturn_codes[idx], rc))
   return 1;

  if (cpsr & MODE32_BIT) {
   /*
    * 32-bit code can use the new high-page
    * signal return code support.
    */
   retcode = KERN_SIGRETURN_CODE + (idx << 2) + thumb;
  } else {
   /*
    * Ensure that the instruction cache sees
    * the return code written onto the stack.
    */
   flush_icache_range((unsigned long)rc,
        (unsigned long)(rc + 1));

   retcode = ((unsigned long)rc) + thumb;
  }
 }

 regs->ARM_r0 = usig;
 regs->ARM_sp = (unsigned long)frame;//堆栈
 regs->ARM_lr = retcode;//返回地址,当用户态信号处理函数结束时,就会把这个地址作为返回地址
 regs->ARM_pc = handler;//信号处理函数
 regs->ARM_cpsr = cpsr;

 return 0;
}

当setup_frame()返回后,一切准备就绪,因此可以从内核态返回了,这样就顺利过渡到用户态的信号处理函数。当这个函数处理结束后,会通过retcode再次进入内核态,现在我们看看retcode是如何处理的,下面的代码选自glibc(2.3.2):

 #include <sysdep.h>

/* If no SA_RESTORER function was specified by the application we use
  one of these.  This avoids the need for the kernel to synthesise a return
  instruction on the stack, which would involve expensive cache flushes. */

ENTRY(__default_sa_restorer)
 swi SYS_ify(sigreturn)

#ifdef __NR_rt_sigreturn

ENTRY(__default_rt_sa_restorer)
 swi SYS_ify(rt_sigreturn)

#define SYS_ify(syscall_name) (__NR_##syscall_name)

下面具体看看sys_sigreturn()的定义:

asmlinkage int sys_sigreturn(struct pt_regs *regs)
{
 struct sigframe __user *frame;
 sigset_t set;

 /* Always make any pending restarted system calls return -EINTR */
 current_thread_info()->restart_block.fn = do_no_restart_syscall;

 /*
  * Since we stacked the signal on a 64-bit boundary,
  * then 'sp' should be word aligned here.  If it's
  * not, then the user is trying to mess with us.
  */
 if (regs->ARM_sp & 7)
  goto badframe;

 frame = (struct sigframe __user *)regs->ARM_sp;

 if (!access_ok(VERIFY_READ, frame, sizeof (*frame)))
  goto badframe;
 if (__get_user(set.sig[0], &frame->sc.oldmask)
    || (_NSIG_WORDS > 1
        && __copy_from_user(&set.sig[1], &frame->extramask,
        sizeof(frame->extramask))))
  goto badframe;

 sigdelsetmask(&set, ~_BLOCKABLE);
 spin_lock_irq(&current->sighand->siglock);
 current->blocked = set;
 recalc_sigpending();
 spin_unlock_irq(&currentt->sighand->siglock);

 if (restore_sigcontext(regs, &frame->sc, &frame->aux))
  goto badframe;

 /* Send SIGTRAP if we're single-stepping */
 if (current->ptrace & PT_SINGLESTEP) {
  ptrace_cancel_bpt(current);
  send_sig(SIGTRAP, current, 1);
 }

 return regs->ARM_r0;

badframe:
 force_sig(SIGSEGV, current);
 return 0;
}

这个函数主要调用restore_sigcontext()根据用户态态堆栈上的sigframe备份还原pt_regs。

static int
restore_sigcontext(struct pt_regs *regs, struct sigcontext __user *sc,
    struct aux_sigframe __user *aux)
{
 int err = 0;

 __get_user_error(regs->ARM_r0, &sc->arm_r0, err);
 __get_user_error(regs->ARM_r1, &sc->arm_r1, err);
 __get_user_error(regs->ARM_r2, &sc->arm_r2, err);
 __get_user_error(regs->ARM_r3, &sc->arm_r3, err);
 __get_user_error(regs->ARM_r4, &sc->arm_r4, err);
 __get_user_error(regs->ARM_r5, &sc->arm_r5, err);
 __get_user_error(regs->ARM_r6, &sc->arm_r6, err);
 __get_user_error(regs->ARM_r7, &sc->arm_r7, err);
 __get_user_error(regs->ARM_r8, &sc->arm_r8, err);
 __get_user_error(regs->ARM_r9, &sc->arm_r9, err);
 __get_user_error(regs->ARM_r10, &sc->arm_r10, err);
 __get_user_error(regs->ARM_fp, &sc->arm_fp, err);
 __get_user_error(regs->ARM_ip, &sc->arm_ip, err);
 __get_user_error(regs->ARM_sp, &sc->arm_sp, err);
 __get_user_error(regs->ARM_lr, &sc->arm_lr, err);
 __get_user_error(regs->ARM_pc, &sc->arm_pc, err);
 __get_user_error(regs->ARM_cpsr, &sc->arm_cpsr, err);

 err |= !valid_user_regs(regs);

#ifdef CONFIG_IWMMXT
 if (err == 0 && test_thread_flag(TIF_USING_IWMMXT))
  err |= restore_iwmmxt_context(&aux->iwmmxt);
#endif
#ifdef CONFIG_VFP
// if (err == 0)
//  err |= vfp_restore_state(&aux->vfp);
#endif

 return err;
}

restore_sigcontext()很简单,当它返回后,通过sys_sigreturn()在内核态堆栈建立的pt_regs已经变为调用信号处理函数之前的pt_regs。这样,sys_sigreturn()返回用户态时,就顺利地过渡到处理之前的状态了。

1.简介

如果进程要处理某一信号,那么要在进程中注册该信号。注册信号主要用来确定信号值及进程针对该信号值的动作之间的映射关系,即进程将要处理哪个进程和该信号被传递给进程时,将执行何种操作。主要有两个函数实现信号的注册:signal()和sigaction()。

2.signal()

signal()的函数原型如下:

void (*signal(int signum, void (*handler)(int)))(int);

在使用该调用的进程中加入以下头文件:

#include <signal.h>

上述声明格式比较复杂,如果不清楚如何使用,也可以通过下面这种类型定义的格式来使用(POSIX的定义):

typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

但这种格式在不同的系统中有不同的类型定义,所以要使用这种格式,最好还是参考一下手册。在调用中,参数signum指出要设置处理方法的信号。第二个参数handler是一个处理函数,或者是

  • SIG_IGN:忽略参数signum所指的信号。
  • SIG_DFL:恢复参数signum所指信号的处理方法为默认值。

传递给信号处理例程的整数参数是信号值,这样可以使得一个信号处理例程处理多个信号。系统调用signal()返回值是指定信号signum前一次的处理例程或者错误时返回错误代码SIG_ERR。

signal()通过系统调用sys_signal()为一个指定的信号设置用户态处理函数。sys_signal()定义如下:

/*
 * For backwards compatibility.  Functionality superseded by sigaction.
 */
asmlinkage unsigned long
sys_signal(int sig, __sighandler_t handler)
{
 struct k_sigaction new_sa, old_sa;
 int ret;

 new_sa.sa.sa_handler = handler;
 new_sa.sa.sa_flags = SA_ONESHOT | SA_NOMASK;

 ret = do_sigaction(sig, &new_sa, &old_sa);

 return ret ? ret : (unsigned long)old_sa.sa.sa_handler;
}

__sighandler_t的定义如下:

typedef void __signalfn_t(int);
typedef __signalfn_t __user *__sighandler_t;

信号由sys_signal()的第一个参数指定,信号处理函数的地址由第二个参数指定。sys_signal()根据这两个参数设置一个k_sigaction结构,然后调用do_sigaction(),该函数的定义我们会在后面具体讲解。

2.sigaction()

sigaction()的函数原型如下:

sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

sigaction()对应的系统调用为do_sigaction(),下面我们具体讲解do_sigaction()函数,其定义如下:

2.1do_sigaction()

int
do_sigaction(int sig, const struct k_sigaction *act, struct k_sigaction *oact)
{
 struct k_sigaction *k;

 if (!valid_signal(sig) || sig < 1 || (act && sig_kernel_only(sig)))
  return -EINVAL;

 k = &currentt->sighand->action[sig-1];

 spin_lock_irq(&currentt->sighand->siglock);
 if (signal_pending(current)) {
  /*
  * If there might be a fatal signal pending on multiple
  * threads, make sure we take it before changing the action.
  */
  spin_unlock_irq(&currentt->sighand->siglock);
  return -ERESTARTNOINTR;
 }

 if (oact)//把原来的k_sigaction保存到oact结构中,这里是对整个数据结构进行复制
  *oact = *k;

 if (act) {
  /*
  * POSIX 3.3.1.3:
  *  "Setting a signal action to SIG_IGN for a signal that is
  *  pending shall cause the pending signal to be discarded,
  *  whether or not it is blocked."
  *
  *  "Setting a signal action to SIG_DFL for a signal that is
  *  pending and whose default action is to ignore the signal
  *  (for example, SIGCHLD), shall cause the pending signal to
  *  be discarded, whether or not it is blocked"
  */
  if (act->sa.sa_handler == SIG_IGN ||
      (act->sa.sa_handler == SIG_DFL &&
      sig_kernel_ignore(sig))) {
   /*
    * This is a fairly rare case, so we only take the
    * tasklist_lock once we're sure we'll need it.
    * Now we must do this little unlock and relock
    * dance to maintain the lock hierarchy.
    */
   struct task_struct *t = current;
   spin_unlock_irq(&t->sighand->siglock);
   read_lock(&tasklist_lock);
   spin_lock_irq(&t->sighand->siglock);
   *k = *act; //把新的k_sigaction结构复制到进程的sighand->action中

   sigdelsetmask(&k->sa.sa_mask,
          sigmask(SIGKILL) | sigmask(SIGSTOP));
   rm_from_queue(sigmask(sig), &t->signal->shared_pending);
   do {
    rm_from_queue(sigmask(sig), &t->pending);
    recalc_sigpending_tsk(t);
    t = next_thread(t);
   } while (t != current);
   spin_unlock_irq(&current->sighand->siglock);
   read_unlock(&tasklist_lock);
   return 0;
  }

  *k = *act; //把新的k_sigaction结构复制到进程的sighand->action中
  sigdelsetmask(&k->sa.sa_mask,
        sigmask(SIGKILL) | sigmask(SIGSTOP));
 }

 spin_unlock_irq(&currentt->sighand->siglock);
 return 0;
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值