信号(signal),又称为软中断信号,用于通知进程发生了异步事件,它是Linux系统响应某些条件而产生的一个事件,它是在软件层次上对中断机制的一种模拟,是一种异步通信方式,在原理上,一个进程收到一个信号与处理器收到一个中断请求可以说是一样的。
信号是进程间通信机制中唯一的异步通信机制,一个进程不必通过任何操作来等待信号的到达,事实上,进程也不知道信号到底什么时候到达。正如我们所了解的中断服务函数一样,在中断发生的时候,就会进入中断服务函数中去处理,同样的,当进程接收到一个信号的时候,也会相应地采取一些行动。我们可以使用术语"生成(raise)"表示一个信号的产生,使用术语"捕获(catch)"表示进程接收到一个信号。
在Linux系统中,信号可能是由于系统中某些错误而产生,也可以是某个进程主动生成的一个信号。由于某些错误条件而生成的信号:如内存段冲突、浮点处理器错误或非法指令等,它们由shell和终端处理器生成并且引起中断。由进程主动生成的信号可以作为在进程间传递通知或修改行为的一种方式,它可以明确地由一个进程发送给另一个进程,当进程捕获了这个信号就会按照程序进行相应并且去处理它。无论何种情况,它们的编程接口都是相同的,信号可以被生成、捕获、响应或忽略。进程之间可以互相发送信号,内核也可以因为内部事件而给进程发送信号,通知进程发生了某个事件。
信号值 | 名称 | 描述 | 默认处理 |
---|---|---|---|
1 | SIGHUP | 控制终端被关闭时产生。 | 终止 |
2 | SIGINT | 程序终止(interrupt)信号,在用户键入INTR字符(通常是Ctrl + C)时发出,用于通知前台进程组终止进程。 | 终止 |
3 | SIGQUIT | SIGQUIT 和SIGINT类似,但由QUIT字符(通常是Ctrl + \)来控制,进程在因收到SIGQUIT退出时会产生core文件,在这个意义上类似于一个程序错误信号。 | 终止并产生转储文件(core文件) |
4 | SIGILL | CPU检测到某进程执行了非法指令时产生,通常是因为可执行文件本身出现错误, 或者试图执行数据段、堆栈溢出时也有可能产生这个信号。 | 终止并产生转储文件(core文件) |
5 | SIGTRAP | 由断点指令或其它trap指令产生,由debugger使用。 | 终止并产生转储文件(core文件) |
6 | SIGABRT | 调用系统函数 abort()时产生。 | 终止并产生转储文件(core文件) |
7 | SIGBUS | 总线错误时产生。一般是非法地址,包括内存地址对齐(alignment)出错。比如访问一个四个字长的整数,但其地址不是4的倍数。它与SIGSEGV的区别在于后者是由于对合法存储地址的非法访问触发的(如访问不属于自己存储空间或只读存储空间)。 | 终止并产生转储文件(core文件) |
8 | SIGFPE | 处理器出现致命的算术运算错误时产生,不仅包括浮点运算错误,还包括溢出及除数为0等其它所有的算术的错误。 | 终止并产生转储文件(core文件) |
9 | SIGKILL | 系统杀戮信号。用来立即结束程序的运行,本信号不能被阻塞、处理和忽略。如果管理员发现某个进程终止不了,可尝试发送这个信号将进程杀死。 | 终止 |
10 | SIGUSR1 | 用户自定义信号。 | 终止 |
11 | SIGSEGV | 访问非法内存时产生,进程试图访问未分配给自己的内存,或试图往没有写权限的内存地址写数据。 | 终止 |
12 | SIGUSR2 | 用户自定义信号。 | 终止 |
13 | SIGPIPE | 这个信号通常在进程间通信产生,比如采用FIFO(管道)通信的两个进程,读管道没打开或者意外终止就往管道写,写进程会收到SIGPIPE信号。此外用Socket通信的两个进程,写进程在写Socket的时候,读进程已经终止,也会产生这个信号。 | 终止 |
14 | SIGALRM | 定时器到期信号,计算的是实际的时间或时钟时间,alarm函数使用该信号。 | 终止 |
15 | SIGTERM | 程序结束(terminate)信号,与SIGKILL不同的是该信号可以被阻塞和处理。通常用来要求程序自己正常退出,shell命令kill缺省产生这个信号,如果进程终止不了,才会尝试SIGKILL。 | 终止 |
16 | SIGSTKFLT | 已废弃。 | 终止 |
17 | SIGCHLD | 子进程暂停或终止时产生,父进程将收到这个信号,如果父进程没有处理这个信号,也没有等待(wait)子进程,子进程虽然终止,但是还会在内核进程表中占有表项,这时的子进程称为僵尸进程,这种情况我们应该避免。父进程默认是忽略SIGCHILD信号的,我们可以捕捉它,做成异步等待它派生的子进程终止,或者父进程先终止,这时子进程的终止自动由init进程来接管。 | 忽略 |
18 | SIGCONT | 系统恢复运行信号,让一个停止(stopped)的进程继续执行,本信号不能被阻塞,可以用一个handler来让程序在由stopped状态变为继续执行时完成特定的工作 | 恢复运行 |
19 | SIGSTOP | 系统暂停信号,停止进程的执行。注意它和terminate以及interrupt的区别:该进程还未结束,只是暂停执行,本信号不能被阻塞,处理或忽略。 | 暂停 |
20 | SIGTSTP | 由控制终端发起的暂停信号,停止进程的运行,但该信号可以被处理和忽略,比如用户键入SUSP字符时(通常是Ctrl+Z)发出这个信号。 | 暂停 |
21 | SIGTTIN | 后台进程发起输入请求时控制终端产生该信号。 | 暂停 |
22 | SIGTTOU | 后台进程发起输出请求时控制终端产生该信号。 | 暂停 |
23 | SIGURG | 套接字上出现紧急数据时产生。 | 忽略 |
24 | SIGXCPU | 处理器占用时间超出限制值时产生。 | 终止并产生转储文件(core文件) |
25 | SIGXFSZ | 文件尺寸超出限制值时产生。 | 终止并产生转储文件(core文件) |
26 | SIGVTALRM | 由虚拟定时器产生的虚拟时钟信号,类似于SIGALRM,但是计算的是该进程占用的CPU时间。 | 终止 |
27 | SIGPROF | 类似于SIGALRM / SIGVTALRM,但包括该进程用的CPU时间以及系统调用的时间。 | 终止 |
28 | SIGWINCH | 窗口大小改变时发出。 | 忽略 |
29 | SIGIO | 文件描述符准备就绪, 可以开始进行输入/输出操作。 | 终止 |
30 | SIGPWR | 启动失败时产生。 | 终止 |
31 | SIGUNUSED | 非法的系统调用。 | 终止并产生转储文件(core文件) |
对于表格有几点需要注意的地方:
生成信号的事件一般可以归为3大类:程序错误、外部事件以及显式请求。例如零作除数、非法存储访问等,这种情况通常是由硬件而不是由Linux内核检测到的,但由内核向发生此错误的那个进程发送相应的信号;例如当用户在终端按下某些键时产生终端生成的信号,当进程超越了CPU或文件大小的限制时,内核会生成一个信号通知进程;例如使用kill()函数允许进程发送任何信号给其他进程或进程组。
信号的生成既可以是同步的,也可以是异步的。同步信号大多数是程序执行过程中出现了某个错误而产生的,由进程显式请求生成的给自己的信号也是同步的。
异步信号是接收进程可控制之外的事件所生成的信号,这类信号一般是进程无法控制的,只能被动接收,因为进程也不知道这个信号会何时发生,只能在发生的时候去处理它。一般外部事件总是异步地生成信号,异步信号可在进程运行中的任意时刻产生,进程无法预期信号到达的时刻,它所能做的只是告诉Linux内核假如有信号生成时应当采取什么行动(这相当于注册信号对应的处理)。
无论是同步还是异步信号,当信号发生时,我们可以告诉Linux内核采取如下3种动作中的任意一种:
因为libuv是一个跨平台的框架,它的底层处理可以在Windows、也可以在linux,所以libuv信号的实现也是视平台而定的,在这里我们只讲解linux平台下的处理,当然对应的Windows也是差不多的。
信号是有生命周期的,可以把信号当做一个handle,那么libuv的信号就是signal handle
,如果创建了signal handle
实例并且start了,那么当signal handle
指定的信号发生时,将进入对应的回调函数去处理该信号,这与linux的信号处理是差不多的,只不过libuv在系统的处理之上进行抽象,形成与平台无关的处理,仅此而已。
关于libuv的signal handle有几个点要知悉:
以编程方式调用raise()或abort()触发的信号不会被libuv检测到;所以这些信号不会对应的回调函数。
SIGKILL和SIGSTOP是不可能被捕捉到的。
通过libuv处理SIGBUS、SIGFPE、SIGILL或SIGSEGV会导致未定义的行为。
libuv的信号与平台的信号基本上是一样的,也就是说信号可以从系统中其他进程发出。
libuv的信号依赖管道进行通信。
uv_signal_t
是 thread handle 的数据类型,通过它可以定义一个 thread handle 的实例。
typedef struct uv_signal_s uv_signal_t;
在libuv/include/uv.h
文件中存在以下的定义,它继承了UV_HANDLE_FIELDS
相关的字段,因此它属于handle,同时还定义了signal的回调函数signal_cb,以及记录触发的信号值signum,当然还有一个UV_SIGNAL_PRIVATE_FIELDS,其实就是定义了红黑树的数据结构与记录触发信号的次数与处理信号的次数。
struct uv_signal_s {
UV_HANDLE_FIELDS
uv_signal_cb signal_cb;
int signum;
UV_SIGNAL_PRIVATE_FIELDS
};
#define UV_SIGNAL_PRIVATE_FIELDS \
/* 红黑树的节点 */ \
struct { \
struct uv_signal_s* rbe_left; \
struct uv_signal_s* rbe_right; \
struct uv_signal_s* rbe_parent; \
int rbe_color; \
} tree_entry; \
/* 分别记录了触发信号的次数与处理信号的次数 */ \
unsigned int caught_signals; \
unsigned int dispatched_signals;
typedef void (*uv_signal_cb)(uv_signal_t* handle, int signum);
int uv_signal_init(uv_loop_t* loop, uv_signal_t* handle);
初始化信号句柄,将signal handle
绑定到指定的loop事件循环中。
具体的初始化操作过程是:libuv申请一个管道,用于其他进程(libuv进程或fork出来的进程)和libuv进程通信。然后往libuv的io观察者队列注册一个观察者,这其实就是观察这个管道是否可读,libuv在轮询I/O的阶段会把观察者加到epoll
中。io观察者里保存了管道读端的文件描述符loop->signal_pipefd[0]和回调函数uv__signal_event。
int uv_signal_init(uv_loop_t* loop, uv_signal_t* handle) {
int err;
/* 初始化loop,它只会被初始化一次 */
err = uv__signal_loop_once_init(loop);
if (err)
return err;
/* 初始化handle的类型,并且插入loop的handle队列,因为所有的handle都会被放到该队列管理 */
uv__handle_init(loop, (uv_handle_t*) handle, UV_SIGNAL);
handle->signum = 0;
handle->caught_signals = 0;
handle->dispatched_signals = 0;
return 0;
}
/* 申请和libuv的通信管道并且注册io观察者 */
static int uv__signal_loop_once_init(uv_loop_t* loop) {
int err;
/* 如果已经初始化则返回 */
if (loop->signal_pipefd[0] != -1)
return 0;
/* 申请两个管道,用于其他进程和libuv主进程通信,并设置非阻塞标记 */
err = uv__make_pipe(loop->signal_pipefd, UV__F_NONBLOCK);
if (err)
return err;
/* 置信号io观察者的处理函数和文件描述符,libuv在循环I/O的时候,
如果发现管道读端loop->signal_pipefd[0]可读,则执行对应的回调函数uv__signal_event */
uv__io_init(&loop->signal_io_watcher,
uv__signal_event,
loop->signal_pipefd[0]);
/* 插入libuv的signal io观察者队列,当管道可读的时候,执行uv__signal_event */
uv__io_start(loop, &loop->signal_io_watcher, POLLIN);
return 0;
}
启动signal handle
,并函数注册信号和对应的处理函数,并且设置信号句柄处于活跃状态。
int uv_signal_start(uv_signal_t* handle,
uv_signal_cb signal_cb,
int signum);
int uv_signal_start(uv_signal_t* handle, uv_signal_cb signal_cb, int signum) {
return uv__signal_start(handle, signal_cb, signum, 0);
}
static int uv__signal_start(uv_signal_t* handle,
uv_signal_cb signal_cb,
int signum,
int oneshot) {
sigset_t saved_sigmask;
int err;
uv_signal_t* first_handle;
assert(!uv__is_closing(handle));
/* 如果用户提供的signum == 0,则返回错误。 */
if (signum == 0)
return UV_EINVAL;
/* 这个信号已经注册过了,重新设置回调处理函数就行。 */
if (signum == handle->signum) {
handle->signal_cb = signal_cb;
return 0;
}
/* 如果信号处理程序已经处于活动状态,请先停止它。 */
if (handle->signum != 0) {
uv__signal_stop(handle);
}
/* 暂时屏蔽所有信号 */
uv__signal_block_and_lock(&saved_sigmask);
/* 如果此时没有用于该信号的活动信号监视程序(在任何循环中),则给进程注册一个信号和信号处理函数。主要是调用操作系统的sigaction()函数来处理的。 */
first_handle = uv__signal_first_handle(signum);
if (first_handle == NULL ||
(!oneshot && (first_handle->flags & UV_SIGNAL_ONE_SHOT))) {
err = uv__signal_register_handler(signum, oneshot);
if (err) {
/* 注册信号失败 */
uv__signal_unlock_and_unblock(&saved_sigmask);
return err;
}
}
handle->signum = signum;
/* 设置UV_SIGNAL_ONE_SHOT标记,表示libuv只响应一次信号 */
if (oneshot)
handle->flags |= UV_SIGNAL_ONE_SHOT;
/* 插入红黑树 */
RB_INSERT(uv__signal_tree_s, &uv__signal_tree, handle);
/* 接触屏蔽信号 */
uv__signal_unlock_and_unblock(&saved_sigmask);
handle->signal_cb = signal_cb;
/* 设置handle的标志UV_HANDLE_ACTIVE,表示处于活跃状态 */
uv__handle_start(handle);
return 0;
}
libuv只响应一次信号,在响应一次后恢复系统默认的信号处理。
int uv_signal_start_oneshot(uv_signal_t* handle,
uv_signal_cb signal_cb,
int signum) {
return uv__signal_start(handle, signal_cb, signum, 1);
}
停止signal handle,将信号句柄设置为非活跃状态,事件循环中不在对它进行轮询。
int uv_signal_stop(uv_signal_t* handle);
int uv_signal_stop(uv_signal_t* handle) {
assert(!uv__is_closing(handle));
uv__signal_stop(handle);
return 0;
}
static void uv__signal_stop(uv_signal_t* handle) {
uv_signal_t* removed_handle;
sigset_t saved_sigmask;
uv_signal_t* first_handle;
int rem_oneshot;
int first_oneshot;
int ret;
/* 如果没有启动观察程序,则该操作无效。 */
if (handle->signum == 0)
return;
/* 暂时屏蔽所有信号 */
uv__signal_block_and_lock(&saved_sigmask);
/* 从红黑树取出信号节点 */
removed_handle = RB_REMOVE(uv__signal_tree_s, &uv__signal_tree, handle);
assert(removed_handle == handle);
(void) removed_handle;
/* 检查是否还有其他活动的信号监视程序正在观察此信号。如果没有了则注销信号处理程序。*/
first_handle = uv__signal_first_handle(handle->signum);
if (first_handle == NULL) {
/* 注销信号,还是依赖系统的函数sigaction() */
uv__signal_unregister_handler(handle->signum);
} else {
rem_oneshot = handle->flags & UV_SIGNAL_ONE_SHOT;
first_oneshot = first_handle->flags & UV_SIGNAL_ONE_SHOT;
if (first_oneshot && !rem_oneshot) {
ret = uv__signal_register_handler(handle->signum, 1);
assert(ret == 0);
(void)ret;
}
}
/* 解除屏蔽所有信号 */
uv__signal_unlock_and_unblock(&saved_sigmask);
handle->signum = 0;
uv__handle_stop(handle);
}
libuv的信号分为两个部分,一个部分是用于通知,另一部分才是真正的处理,当系统有信号到达后,libuv会通过管道通知到libuv的事件循环中,然后在事件循环中处理信号,这里的事件循环其实是一个笼统的概念,具体的处理是在poll io阶段,即I/O轮询阶段,因为在等待信号的过程中,它可能会进入阻塞状态。
在libuv的处理中,无论有什么信号到来,它都通过uv__signal_handler()函数去处理信号。为什么呢,因为我们在注册的时候是通过uv__signal_register_handler()函数进行注册的,而这个函数中就将对应的回调处理设置为uv__signal_handler()函数。
static int uv__signal_register_handler(int signum, int oneshot) {
/* When this function is called, the signal lock must be held. */
struct sigaction sa;
/* XXX use a separate signal stack? */
memset(&sa, 0, sizeof(sa));
if (sigfillset(&sa.sa_mask))
abort();
/* 注册回调函数uv__signal_handler */
sa.sa_handler = uv__signal_handler;
sa.sa_flags = SA_RESTART;
if (oneshot)
sa.sa_flags |= SA_RESETHAND;
/* XXX save old action so we can restore it later on? */
if (sigaction(signum, &sa, NULL))
return UV__ERR(errno);
return 0;
}
接下来看看uv__signal_handler()
函数的处理过程,该函数遍历红黑树,找到注册了该信号的handle,然后封装一个msg写入管道(即libuv的通信管道)。信号的通知处理就完成了。我们看看这个函数的代码。:
static void uv__signal_handler(int signum) {
uv__signal_msg_t msg;
uv_signal_t* handle;
int saved_errno;
saved_errno = errno;
memset(&msg, 0, sizeof msg);
if (uv__signal_lock()) {
errno = saved_errno;
return;
}
/* 获取signal handle */
for (handle = uv__signal_first_handle(signum);
handle != NULL && handle->signum == signum;
handle = RB_NEXT(uv__signal_tree_s, &uv__signal_tree, handle)) {
int r;
msg.signum = signum;
msg.handle = handle;
/* 往signal_pipefd管道写入数据,就是通知libuv,拿些signal handle需要处理信号,这是在事件循环中处理的 */
do {
r = write(handle->loop->signal_pipefd[1], &msg, sizeof msg);
} while (r == -1 && errno == EINTR);
assert(r == sizeof msg ||
(r == -1 && (errno == EAGAIN || errno == EWOULDBLOCK)));
/* 记录该signal handle收到信号的次数 */
if (r != -1)
handle->caught_signals++;
}
uv__signal_unlock();
errno = saved_errno;
}
在信号通知完成后,事件循环中管道读取数据段有消息到达,此时事件循环将接收到消息,接下来在libuv的poll io
阶段才做真正的处理。从uv__io_init()
函数的处理过程得知,它把管道的读取端loop->signal_pipefd[0]
看作是一个io观察者,在poll io阶段,epoll会检测到管道loop->signal_pipefd[0]
是否可读,如果可读,然后会执行uv__signal_event()
函数。在这个uv__signal_event()
函数中,libuv将从管道读取刚才写入的一个个msg,从msg中取出对应的handle,然后执行里面保存的回调函数:
static void uv__signal_event(uv_loop_t* loop,
uv__io_t* w,
unsigned int events) {
uv__signal_msg_t* msg;
uv_signal_t* handle;
char buf[sizeof(uv__signal_msg_t) * 32];
size_t bytes, end, i;
int r;
bytes = 0;
end = 0;
/* 读取管道里的消息,处理所有的信号消息 */
do {
r = read(loop->signal_pipefd[0], buf + bytes, sizeof(buf) - bytes);
if (r == -1 && errno == EINTR)
continue;
if (r == -1 && (errno == EAGAIN || errno == EWOULDBLOCK)) {
if (bytes > 0)
continue;
return;
}
/* Other errors really should never happen. */
if (r == -1)
abort();
bytes += r;
end = (bytes / sizeof(uv__signal_msg_t)) * sizeof(uv__signal_msg_t);
for (i = 0; i < end; i += sizeof(uv__signal_msg_t)) {
msg = (uv__signal_msg_t*) (buf + i);
handle = msg->handle;
/* 如果收到的信号与预期的信号是一致的,则执行回调函数 */
if (msg->signum == handle->signum) {
assert(!(handle->flags & UV_HANDLE_CLOSING));
/* signal 回调函数 */
handle->signal_cb(handle, handle->signum);
}
/* 记录处理的信号个数 */
handle->dispatched_signals++;
/* 只响应一次,需要回复系统默认的处理函数 */
if (handle->flags & UV_SIGNAL_ONE_SHOT)
uv__signal_stop(handle);
}
bytes -= end;
if (bytes) {
memmove(buf, buf + end, bytes);
continue;
}
} while (end == sizeof buf);
}
我们从example来讲解相关的函数使用吧,本次实验主要是是创建两个线程,其中一个线程等待SIGUSR1信号,另一个线程发送SIGUSR1信号,在处理完信号后退出。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <uv.h>
void signal_handler(uv_signal_t *handle, int signum)
{
printf("signal received: %d\n", signum);
uv_signal_stop(handle);
}
void thread1_entry(void *userp)
{
sleep(2);
kill(0, SIGUSR1);
}
void thread2_entry(void *userp)
{
uv_signal_t signal;
uv_signal_init(uv_default_loop(), &signal);
uv_signal_start(&signal, signal_handler, SIGUSR1);
uv_run(uv_default_loop(), UV_RUN_DEFAULT);
}
int main()
{
uv_thread_t thread1, thread2;
uv_thread_create(&thread1, thread1_entry, NULL);
uv_thread_create(&thread2, thread2_entry, NULL);
uv_thread_join(&thread1);
uv_thread_join(&thread2);
return 0;
}