Jan Fan     About     Archive     Feed     English Blog

Unix Signal 的实现

在终端(Terminal)上工作的时候,我们常常一个Ctrl+C就终结了当前程序的运行。 在这背后,就是Unix Signal搞的鬼。

这篇文章就来讲讲Unix Signal和它的Linux源码实现。

What is Signals

Signal分为两种,同步和异步

可以看出,Signal的来源有两大类,一是来源于硬件的中断或指令执行异常,二则是来源于用户进程的请求,如kill()alarm()等。

Signal常常被拿来和“硬件中断”(Hardware Interrupt)做比较,因为它们有一个共同点——被某个事件触发之后,自动去执行注册的“中断服务程序”ISR(Interrupt Service Routine)或用户自定义handler程序。

UNIX guru W. Richard Stevens aptly describes signals as software interrupts.

不同之处在于,Signal的设计其实是在Kernel层面上对硬件\CPU层面的Interrupts\traps的进一步扩展,它赋予了Userland层面更多的信息来源和更大的自由度。 除了通过直接的\即时的系统调用来与应用层底下的Kernel或HW打交道,如申请内存\读写I/O等;还可以通过间接的\事件触发回调(callback)的方式来满足应用层的需求。 更多的比较可以参见这里

+--------+
|Process |
+--------+
    | callback
   +--+
   |OS|
   +--+
    |
   +--+
   |HW|
   +--+

这个设计还额外给IPC带来了一个福利——反正已经注册了进程A的回调函数,Kernel可以在需要的时候调用,不用白不用,除了让硬件事件来触发,给进程B触发也是顺手的功夫,多挖个洞——开个kill()函数发信息就是了。

+--------+ +--------+
|Process1| |Process2|
+--------+ +--------+
 kill \      / callback
       \+--+/
        |OS|
        +--+

Implementation of Unix Signals

首先要说明一些术语和概念。

We say that a signal is delivered to a process when the action for a signal is taken. During the time between the generation of a signal and its delivery, the signal is said to be pending. A process has the option of blocking the delivery of a signal.

Signal可以看作是进程和内核间的一种交流方式,那么它们俩是怎么通信的呢?

Signals can be viewed as a mean of communication between the OS kernel and OS processes.

Signal delivery is done when the process returns from the kernel to user mode.

在OS里面,每个进程是一个独立的结构体,轮流地让Kernel装载\切换自己家的资源,霸占CPU。 这就很明显了,Kernel如果想使点坏,让进程做点别的事情(比如,让它执行之前注册的回调callback函数),那么就在装载执行这个进程的时候动动手脚就可以了。

struct task_struct {
    
/* signal handlers */
    struct signal_struct *signal;
    struct sighand_struct *sighand;
    
    sigset_t blocked, real_blocked;
    sigset_t saved_sigmask; /* restored if set_restore_sigmask() was used */
    struct sigpending pending;
    
    unsigned long sas_ss_sp;
    size_t sas_ss_size;
    int (*notifier)(void *priv);
    void *notifier_data;
    sigset_t *notifier_mask;
    struct callback_head *task_works;
    
    struct audit_context *audit_context;
    
};

从上面Linux的进程\任务task结构体代码里可以看到,每个进程有专门记录与Signal相关的部分(field)。

当Signal的触发事件发生时,Kernel就跑到这个进程里偷偷修改这部分的数据(比如增加一个等待的Signal或阻塞的Signal,清空一些低优先级的Signals)。 当下一次轮到这个进程执行的时候,在Kernel将执行权切换给进程的之前:

  1. 先查询进程有没有在等待的Signal数据;
  2. 如果有,好,提出来,并让进程先执行Signal对应的handler程序。
  3. 直到清空Signal的集合,才真正恢复进程正常程序流的执行。

这就是Signal传递的主要的基本流程了。 下面我们来看看一些具体的细节代码。

Signals & Handlers

让我们看看非常关键的signal.hsignal.c源文件。

Singal排队等待执行的位置,可以看出sigset_t中的一个bit表示一个signal。

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

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

每个Signal对应的handler的定义,就是一函数指针。

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

struct k_sigaction {
	struct sigaction sa;
#ifdef __ARCH_HAS_KA_RESTORER
	__sigrestore_t ka_restorer;
#endif
};

struct sigaction {
#ifndef __ARCH_HAS_IRIX_SIGACTION
	__sighandler_t	sa_handler;
	unsigned long	sa_flags;
#else
	unsigned int	sa_flags;
	__sighandler_t	sa_handler;
#endif
#ifdef __ARCH_HAS_SA_RESTORER
	__sigrestore_t sa_restorer;
#endif
	sigset_t	sa_mask;	/* mask last for extensibility */
};

/* Type of a signal handler.  */
#if defined(__LP64__)
/* function pointers on 64-bit parisc are pointers to little structs and the
 * compiler doesn't support code which changes or tests the address of
 * the function in the little struct.  This is really ugly -PB
 */
typedef char __user *__sighandler_t;
#else
typedef void __signalfn_t(int);
typedef __signalfn_t __user *__sighandler_t;
#endif

发送一个signal,就是将task的pending成员里相应的位置为1。

static int __send_signal(int sig, struct siginfo *info, struct task_struct *t,
			int group, int from_ancestor_ns)
{
	
	pending = group ? &t->signal->shared_pending : &t->pending;
	
	sigaddset(&pending->signal, sig);
	
}

最后再将进程唤醒,提取并清空等待的signal,并执行相应的handler函数。

static void complete_signal(int sig, struct task_struct *p, int group)
{
	
	/*
	 * The signal is already in the shared-pending queue.
	 * Tell the chosen thread to wake up and dequeue it.
	 */
	signal_wake_up(t, sig == SIGKILL);
	
}

以上只是精简版的流程,方便大家理解,很多细节按下不表。

其它零散细节的补充

Signal的触发来源

Signals are classic examples of asynchronous events. Numerous conditions can generate a signal:

Default Handler

每个Signal有自己的代码标志符,也就是它的名字。

Every signal has a unique signal name, an abbreviation that begins with SIG (SIGINT for interrupt signal, for example) and a corresponding signal number. Signal names are all defined by positive integer constants (the signal number) in the header <signal.h>.

每个Signal都有自己默认的处理函数,在代码中有预先设定,但用户也可以自定义(除了这两个信号不能自定义SIGKILLSIGSTOP),在Linux的源码中也有注释说明。

The disposition of the signal, or the action associated with a signal. There are four possible default dispositions:

  1. ignore
  2. terminate
  3. coredump
  4. stop
/*
 * The possible effects an unblocked signal set to SIG_DFL can have are:
 * 
 *   ignore	- Nothing Happens
 *   terminate	- kill the process, i.e. all threads in the group,
 * 		  similar to exit_group.  The group leader (only) reports
 *		  WIFSIGNALED status to its parent.
 *   coredump	- write a core dump file describing all threads using
 *		  the same mm and then kill all those threads
 *   stop 	- stop all the threads in the group, i.e. TASK_STOPPED state
 *
 * SIGKILL and SIGSTOP cannot be caught, blocked, or ignored.
 */

libc Functions

下面贴上一些常见的Signal控制函数接口。

#include <signal.h>

// void (*signal(int signo, void (*func)(int)))(int);
typedef void (*sig_t) (int);
sig_t signal(int sig, sig_t func);

// This function supersedes the signal function from earlier releases of the UNIX System.
int sigaction(int signo, const struct sigaction *restrict act,
struct sigaction *restrict oact); 

// A process can examine its signal mask, change its signal mask, or perform both operations in one step by calling this function.
int sigprocmask(int how, const sigset_t *restrict set, sigset_t *restrict oset);

// This function returns the set of signals that are blocked from delivery and currently pending for the calling process.
int sigpending(sigset_t *set); 

Signal作为进程通信时常用的函数。

#include <signal.h>

int kill(pid_t pid, int signo); 
int raise(int signo);
#include <unistd.h>

// When the timer expires, the SIGALRM signal is generated.
unsigned int alarm(unsigned int seconds);

// The pause function suspends the calling process until a signal is caught.
int pause(void); 
#include <stdlib.h>

// This function sends the SIGABRT signal to the caller.
void abort(void);

Sginal的IPC对通信进程是有权限要求的。

A process needs permission to send a signal to another process.

  • The superuser can send a signal to any process.
  • For other users, the basic rule is that the real or effective user ID of the sender has to equal the real or effective user ID of the receiver.

最后

Linux源码繁杂,也没有较好的文档可以借鉴,如果有错误的地方欢迎各位同行指出,不盛感谢!

主要参考资料

Comments

多说 Disqus