Skip to main content

libuv 句柄解读-信号(signal)

解读

信号(signal),又称为软中断信号,用于通知进程发生了异步事件,它是 Linux 系统响应某些条件而产生的一个事件,它是在软件层次上对中断机制的一种模拟,是一种异步通信方式,在原理上,一个进程收到一个信号与处理器收到一个中断请求可以说是一样的。

信号是进程间通信机制中唯一的异步通信机制,一个进程不必通过任何操作来等待信号的到达,事实上,进程也不知道信号到底什么时候到达。正如我们所了解的中断服务函数一样,在中断发生的时候,就会进入中断服务函数中去处理,同样的,当进程接收到一个信号的时候,也会相应地采取一些行动。我们可以使用术语 "生成(raise)" 表示一个信号的产生,使用术语 "捕获(catch)" 表示进程接收到一个信号。

在 Linux 系统中,信号可能是由于系统中某些错误而产生,也可以是某个进程主动生成的一个信号。由于某些错误条件而生成的信号:如内存段冲突、浮点处理器错误或非法指令等,它们由 shell 和终端处理器生成并且引起中断。由进程主动生成的信号可以作为在进程间传递通知或修改行为的一种方式,它可以明确地由一个进程发送给另一个进程,当进程捕获了这个信号就会按照程序进行相应并且去处理它。无论何种情况,它们的编程接口都是相同的,信号可以被生成、捕获、响应或忽略。进程之间可以互相发送信号,内核也可以因为内部事件而给进程发送信号,通知进程发生了某个事件。

信号值名称描述默认处理
1SIGHUP控制终端被关闭时产生。终止
2SIGINT程序终止 (interrupt) 信号,在用户键入 INTR 字符(通常是 Ctrl + C)时发出,用于通知前台进程组终止进程。终止
3SIGQUITSIGQUIT 和 SIGINT 类似,但由 QUIT 字符(通常是 Ctrl + \)来控制,进程在因收到 SIGQUIT 退出时会产生 core 文件,在这个意义上类似于一个程序错误信号。终止并产生转储文件(core 文件)
4SIGILLCPU 检测到某进程执行了非法指令时产生,通常是因为可执行文件本身出现错误, 或者试图执行数据段、堆栈溢出时也有可能产生这个信号。终止并产生转储文件(core 文件)
5SIGTRAP由断点指令或其它 trap 指令产生,由 debugger 使用。终止并产生转储文件(core 文件)
6SIGABRT调用系统函数 abort() 时产生。终止并产生转储文件(core 文件)
7SIGBUS总线错误时产生。一般是非法地址,包括内存地址对齐(alignment)出错。比如访问一个四个字长的整数,但其地址不是 4 的倍数。它与 SIGSEGV 的区别在于后者是由于对合法存储地址的非法访问触发的(如访问不属于自己存储空间或只读存储空间)。终止并产生转储文件(core 文件)
8SIGFPE处理器出现致命的算术运算错误时产生,不仅包括浮点运算错误,还包括溢出及除数为 0 等其它所有的算术的错误。终止并产生转储文件(core 文件)
9SIGKILL系统杀戮信号。用来立即结束程序的运行,本信号不能被阻塞、处理和忽略。如果管理员发现某个进程终止不了,可尝试发送这个信号将进程杀死。终止
10SIGUSR1用户自定义信号。终止
11SIGSEGV访问非法内存时产生,进程试图访问未分配给自己的内存,或试图往没有写权限的内存地址写数据。终止
12SIGUSR2用户自定义信号。终止
13SIGPIPE这个信号通常在进程间通信产生,比如采用 FIFO(管道)通信的两个进程,读管道没打开或者意外终止就往管道写,写进程会收到 SIGPIPE 信号。此外用 Socket 通信的两个进程,写进程在写 Socket 的时候,读进程已经终止,也会产生这个信号。终止
14SIGALRM定时器到期信号,计算的是实际的时间或时钟时间,alarm 函数使用该信号。终止
15SIGTERM程序结束(terminate)信号,与 SIGKILL 不同的是该信号可以被阻塞和处理。通常用来要求程序自己正常退出,shell 命令 kill 缺省产生这个信号,如果进程终止不了,才会尝试 SIGKILL。终止
16SIGSTKFLT已废弃。终止
17SIGCHLD子进程暂停或终止时产生,父进程将收到这个信号,如果父进程没有处理这个信号,也没有等待(wait)子进程,子进程虽然终止,但是还会在内核进程表中占有表项,这时的子进程称为僵尸进程,这种情况我们应该避免。父进程默认是忽略 SIGCHILD 信号的,我们可以捕捉它,做成异步等待它派生的子进程终止,或者父进程先终止,这时子进程的终止自动由 init 进程来接管。忽略
18SIGCONT系统恢复运行信号,让一个停止(stopped)的进程继续执行,本信号不能被阻塞,可以用一个 handler 来让程序在由 stopped 状态变为继续执行时完成特定的工作恢复运行
19SIGSTOP系统暂停信号,停止进程的执行。注意它和 terminate 以及 interrupt 的区别:该进程还未结束,只是暂停执行,本信号不能被阻塞,处理或忽略。暂停
20SIGTSTP由控制终端发起的暂停信号,停止进程的运行,但该信号可以被处理和忽略,比如用户键入 SUSP 字符时(通常是 Ctrl+Z)发出这个信号。暂停
21SIGTTIN后台进程发起输入请求时控制终端产生该信号。暂停
22SIGTTOU后台进程发起输出请求时控制终端产生该信号。暂停
23SIGURG套接字上出现紧急数据时产生。忽略
24SIGXCPU处理器占用时间超出限制值时产生。终止并产生转储文件(core 文件)
25SIGXFSZ文件尺寸超出限制值时产生。终止并产生转储文件(core 文件)
26SIGVTALRM由虚拟定时器产生的虚拟时钟信号,类似于 SIGALRM,但是计算的是该进程占用的 CPU 时间。终止
27SIGPROF类似于 SIGALRM / SIGVTALRM,但包括该进程用的 CPU 时间以及系统调用的时间。终止
28SIGWINCH窗口大小改变时发出。忽略
29SIGIO文件描述符准备就绪,可以开始进行输入输出操作。终止
30SIGPWR启动失败时产生。终止
31SIGUNUSED非法的系统调用。终止并产生转储文件(core 文件)

对于表格有几点需要注意的地方:

  1. 信号的 “值” 在 x86、PowerPC 和 ARM 平台下是有效的,但是别的平台的信号值也许跟这个表的不一致。
  2. “描述” 中注明的一些情况发生时会产生相应的信号,但并不是说该信号的产生就一定发生了这个事件。事实上,任何进程都可以使用 kill() 函数来产生任何信号。
  3. 信号 SIGKILL 和 SIGSTOP 是两个特殊的信号,他们不能被忽略、阻塞或捕捉,只能按缺省动作来响应。
  4. 一般而言,信号的响应处理过程如下:如果该信号被阻塞,那么将该信号挂起,不对其做任何处理,等到解除对其阻塞为止。如果该信号被捕获,那么进一步判断捕获的类型,如果设置了响应函数,那么执行该响应函数;如果设置为忽略,那么直接丢弃该信号。最后才执行信号的默认处理。

生成信号的事件一般可以归为 3 大类:程序错误、外部事件以及显式请求。例如零作除数、非法存储访问等,这种情况通常是由硬件而不是由 Linux 内核检测到的,但由内核向发生此错误的那个进程发送相应的信号;例如当用户在终端按下某些键时产生终端生成的信号,当进程超越了 CPU 或文件大小的限制时,内核会生成一个信号通知进程;例如使用 kill() 函数允许进程发送任何信号给其他进程或进程组。

信号的生成既可以是同步的,也可以是异步的。同步信号大多数是程序执行过程中出现了某个错误而产生的,由进程显式请求生成的给自己的信号也是同步的。

异步信号是接收进程可控制之外的事件所生成的信号,这类信号一般是进程无法控制的,只能被动接收,因为进程也不知道这个信号会何时发生,只能在发生的时候去处理它。一般外部事件总是异步地生成信号,异步信号可在进程运行中的任意时刻产生,进程无法预期信号到达的时刻,它所能做的只是告诉 Linux 内核假如有信号生成时应当采取什么行动(这相当于注册信号对应的处理)。

无论是同步还是异步信号,当信号发生时,我们可以告诉 Linux 内核采取如下 3 种动作中的任意一种:

  • 忽略信号。大部分信号都可以被忽略,但有两个除外:SIGSTOP 和 SIGKILL 绝不会被忽略。不能忽略这两个信号的原因是为了给超级用户提供杀掉或停止任何进程的一种手段。此外,尽管其他信号都可以被忽略,但其中有一些却不宜忽略。例如,若忽略硬件例外(非法指令)信号,则会导致进程的行为不确定。
  • 捕获信号。这种处理是要告诉 Linux 内核,当信号出现时调用专门提供的一个函数。这个函数称为信号处理函数,它专门对产生信号的事件作出处理。
  • 让信号默认动作起作用。系统为每种信号规定了一个默认动作,这个动作由 Linux 内核来完成,有以下几种可能的默认动作:
    1. 终止进程并且生成内存转储文件,即写出进程的地址空间内容和寄存器上下文至进程当前目录下名为 cone 的文件中;
    2. 终止终止进程但不生成 core 文件。
    3. 忽略信号。
    4. 暂停进程。
    5. 若进程是暂停暂停,恢复进程,否则将忽略信号。

因为 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);

  • uv_signal_t:传入了触发信号的句柄。
  • signum:传入触发信号的值,这个值可能跟系统的值不一样,不过无所谓。

uv_signal_init()

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;


err = uv__signal_loop_once_init(loop);
if (err)
return err;


uv__handle_init(loop| (uv_handle_t*) handle| UV_SIGNAL);
handle->signum = 0;
handle->caught_signals = 0;
handle->dispatched_signals = 0;

return 0;
}


static int uv__signal_loop_once_init(uv_loop_t* loop) {
int err;


if (loop->signal_pipefd[0] != -1)
return 0;


err = uv__make_pipe(loop->signal_pipefd| UV__F_NONBLOCK);
if (err)
return err;


uv__io_init(&loop->signal_io_watcher|
uv__signal_event|
loop->signal_pipefd[0]);


uv__io_start(loop| &loop->signal_io_watcher| POLLIN);

return 0;
}

uv_signal_start()

启动signal handle,并函数注册信号和对应的处理函数,并且设置信号句柄处于活跃状态。

int uv_signal_start(uv_signal_t* handle|
uv_signal_cb signal_cb|
int signum);
  • handle:信号句柄。
  • signal_cb:信号的回调函数。
  • 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));


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);


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;


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;


uv__handle_start(handle);

return 0;
}

uv_signal_start_oneshot()

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);
}

uv_signal_stop()

停止 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) {


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) {

struct sigaction sa;


memset(&sa| 0| sizeof(sa));
if (sigfillset(&sa.sa_mask))
abort();


sa.sa_handler = uv__signal_handler;
sa.sa_flags = SA_RESTART;

if (oneshot)
sa.sa_flags |= SA_RESETHAND;


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;
}


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;


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)));


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;
}


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));


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;
}

参考