7.1 信号的概念和实现原理
信号是进程间通信的一种简单的方式,我们首先了解一下信号的概念和在操作系统中的实现原理。在操作系统内核的实现中,每个进程对应一个task_struct结构体(PCB),PCB中有一个字段记录了进程收到的信号(每一个比特代表一种信号)和信号对应的处理函数。这个和订阅者/发布者模式非常相似,我们看一下PCB中信号对应的数据结构。
struct task_struct {
// 收到的信号
long signal;
// 处理信号过程中屏蔽的信息
long blocked;
// 信号对应的处理函数
struct sigaction sigaction[32];
...
};
struct sigaction {
// 信号处理函数
void (*sa_handler)(int);
// 处理信号时屏蔽哪些信息,和PCB的block字段对应
sigset_t sa_mask;
// 一些标记,比如处理函数只执行一次,类似events模块的once
int sa_flags;
// 清除调用栈信息,glibc使用
void (*sa_restorer)(void);
};
Linux下支持多种信号,进程收到信号时,操作系统提供了默认处理,我们也可以显式注册处理信号的函数,但是有些信号会导致进程退出,这是我们无法控制的。我们来看一下在Linux下信号使用的例子。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>
void handler(int);
int main()
{
signal(SIGINT, handler);
while(1);
return(0);
}
void sighandler(int signum)
{
printf("收到信号%d", signum);
}
我们注册了一个信号对应的处理函数,然后进入while循环保证进程不会退出,这时候,如果我们给这个进程发送一个SIGINT信号(ctrl+c或者kill -2 pid)。则进程会执行对应的回调,然后输出:收到信号2。了解了信号的基本原理后,我们看一下Libuv中关于信号的设计和实现。
7.2 Libuv信号处理的设计思想
由于操作系统实现的限制,我们无法给一个信号注册多个处理函数,对于同一个信号,如果我们调用操作系统接口多次,后面的就会覆盖前面设置的值。想要实现一个信号被多个函数处理,我们只能在操作系统之上再封装一层,Libuv正是这样做的。Libuv中关于信号处理的封装和订阅者/发布者模式很相似。用户调用Libuv的接口注册信号处理函数,Libuv再向操作系统注册对应的处理函数,等待操作系统收到信号时,会触发Libuv的回调,Libuv的回调会通过管道通知事件循环收到的信号和对应的上下文,接着事件循环在Poll IO阶段就会处理收到所有信号以及对应的处理函数。整体架构如图7-1所示
图7-1
下面我们具体分析Libuv中信号处理的实现。
7.3 通信机制的实现
当进程收到信号的时候,信号处理函数需要通知Libuv事件循环,从而在事件循环中执行对应的回调,实现函数是uvsignal_loop_once_init,我们看一下uvsignal_loop_once_init的逻辑。
static int uv__signal_loop_once_init(uv_loop_t* loop) {
/*
申请一个管道用于和事件循环通信,通知事件循环是否收到信号,
并设置非阻塞标记
*/
uv__make_pipe(loop->signal_pipefd, UV__F_NONBLOCK);
/*
设置信号IO观察者的处理函数和文件描述符,
Libuv在Poll IO时,发现管道读端loop->signal_pipefd[0]可读,
则执行uv__signal_event
*/
uv__io_init(&loop->signal_io_watcher,
uv__signal_event,
loop->signal_pipefd[0]);
/*
插入Libuv的IO观察者队列,并注册感兴趣的事件为可读
*/
uv__io_start(loop, &loop->signal_io_watcher, POLLIN);
return 0;
}
uvsignal_loop_once_init首先申请一个管道,用于通知事件循环是否收到信号。然后往Libuv的IO观察者队列注册一个观察者,Libuv在Poll IO阶段会把观察者加到epoll中。IO观察者里保存了管道读端的文件描述符loop->signal_pipefd[0]和回调函数uvsignal_event。uv__signal_event是收到任意信号时的回调,它会继续根据收到的信号进行逻辑分发。执行完的架构如图7-2所示。
图7-2
7.4 信号结构体的初始化
Libuv中信号使用uv_signal_t表示。
int uv_signal_init(uv_loop_t* loop, uv_signal_t* handle) {
// 申请和Libuv的通信管道并且注册IO观察者
uv__signal_loop_once_init(loop);
uv__handle_init(loop, (uv_handle_t*) handle, UV_SIGNAL);
handle->signum = 0;
handle->caught_signals = 0;
handle->dispatched_signals = 0;
return 0;
}
上面的代码的逻辑比较简单,只是初始化uv_signal_t结构体的一些字段。
7.5 信号处理的注册
我们可以通过uv_signal_start注册一个信号处理函数。我们看看这个函数的逻辑
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;
// 注册过了,重新设置处理函数就行
if (signum == handle->signum) {
handle->signal_cb = signal_cb;
return 0;
}
// 这个handle之前已经设置了其它信号和处理函数,则先解除
if (handle->signum != 0) {
uv__signal_stop(handle);
}
// 屏蔽所有信号
uv__signal_block_and_lock(&saved_sigmask);
/*
查找注册了该信号的第一个handle,
优先返回设置了UV_SIGNAL_ONE_SHOT flag的,
见compare函数
*/
first_handle = uv__signal_first_handle(signum);
/*
1 之前没有注册过该信号的处理函数则直接设置
2 之前设置过,但是是one shot,但是现在需要
设置的规则不是one shot,需要修改。否则第
二次不会不会触发。因为一个信号只能对应一
个信号处理函数,所以,以规则宽的为准,在回调
里再根据flags判断是不是真的需要执行
3 如果注册过信号和处理函数,则直接插入红黑树就行。
*/
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;
}
上面的代码比较多,大致的逻辑如下.
1 判断是否需要向操作系统注册一个信号的处理函数。主要是调用操作系统的函数来处理的,代码如下
// 给当前进程注册信号处理函数,会覆盖之前设置的signum的处理函数
static int uv__signal_register_handler(int signum, int oneshot) {
struct sigaction sa;
memset(&sa, 0, sizeof(sa));
// 全置一,说明收到signum信号的时候,暂时屏蔽其它信号
if (sigfillset(&sa.sa_mask))
abort();
// 所有信号都由该函数处理
sa.sa_handler = uv__signal_handler;
sa.sa_flags = SA_RESTART;
// 设置了oneshot,说明信号处理函数只执行一次,然后被恢复为系统的默认处理函数
if (oneshot)
sa.sa_flags |= SA_RESETHAND;
// 注册
if (sigaction(signum, &sa, NULL))
return UV__ERR(errno);
return 0;
}
我们看到所有信号的处理函数都是uvsignal_handler,我们一会会分析uvsignal_handler的实现。
2进程注册的信号和回调是在一棵红黑树管理的,每次注册的时候会往红黑树插入一个节点。Libuv用黑红树维护信号的上下文,插入的规则是根据信号的大小和flags等信息。
RB_INSERT实现了往红黑树插入一个节点,红黑树中的节点是父节点的值比左孩子大,比右孩子小的。执行完RB_INSERT后的架构如图7-3所示。
图7-3
我们看到,当我们每次插入不同的信号的时候,Libuv会在操作系统和红黑树中修改对应的数据结构。那么如果我们插入重复的信号呢?刚才我们已经分析过,插入重复的信号时,如果在操作系统注册过,并且当前插入的信号flags是one shot,而之前是非one shot时,Libuv会调用操作系统的接口去修改配置。那么对于红黑树来说,插入重复信号会如何处理呢?从刚才RB_INSERT的代码中我们看到每次插入红黑树时,红黑树会先判断是否存在相同值的节点,如果是的话直接返回,不进行插入。这么看起来我们无法给一个信号注册多个处理函数,但其实是可以的,重点在比较大小的函数。我们看看该函数的实现。
static int uv__signal_compare(uv_signal_t* w1, uv_signal_t* w2) {
int f1;
int f2;
// 返回信号值大的
if (w1->signum < w2->signum) return -1;
if (w1->signum > w2->signum) return 1;
// 设置了UV_SIGNAL_ONE_SHOT的大
f1 = w1->flags & UV_SIGNAL_ONE_SHOT;
f2 = w2->flags & UV_SIGNAL_ONE_SHOT;
if (f1 < f2) return -1;
if (f1 > f2) return 1;
// 地址大的值就大
if (w1->loop < w2->loop) return -1;
if (w1->loop > w2->loop) return 1;
if (w1 < w2) return -1;
if (w1 > w2) return 1;
return 0;
}
我们看到Libuv比较的不仅是信号的大小,在信号一样的情况下,Libuv还会比较其它的因子,除非两个uv_signal_t指针指向的是同一个uv_signal_t结构体,否则它们是不会被认为重复的,所以红黑树中会存着信号一样的节点。假设我们按照1(flags为one shot),2(flags为非one shot),3(flags为one shot)的顺序插入红黑树,并且节点3比节点1的地址大。所形成的结构如图7-4所示。
图7-4
7.6 信号的处理
我们上一节已经分析过,不管注册什么信号,它的处理函数都是这个uvsignal_handler函数。我们自己的业务回调函数,是保存在handle里的。而Libuv维护了一棵红黑树,记录了每个handle注册的信号和回调函数,那么当任意信号到来的时候。uvsignal_handler就会被调用。下面我们看看uv__signal_handler函数。
/*
信号处理函数,signum为收到的信号,
每个子进程收到信号的时候都由该函数处理,
然后通过管道通知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;
}
// 找到该信号对应的所有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;
do {
// 通知Libuv,哪些handle需要处理该信号,
在Poll IO阶段处理
r = write(handle->loop->signal_pipefd[1],
&msg,
sizeof msg);
} while (r == -1 && errno == EINTR);
// 该handle收到信号的次数
if (r != -1)
handle->caught_signals++;
}
uv__signal_unlock();
errno = saved_errno;
}
uvsignal_handler函数会调用uvsignal_first_handle遍历红黑树,找到注册了该信号的所有handle,我们看一下uv__signal_first_handle的实现。
static uv_signal_t* uv__signal_first_handle(int signum) {
uv_signal_t lookup;
uv_signal_t* handle;
lookup.signum = signum;
lookup.flags = 0;
lookup.loop = NULL;
handle = RB_NFIND(uv__signal_tree_s,
&uv__signal_tree,
&lookup);
if (handle != NULL && handle->signum == signum)
return handle;
return NULL;
}
uv__signal_first_handle函数通过RB_NFIND实现红黑树的查找,RB_NFIND是一个宏。
#define RB_NFIND(name, x, y) name##_RB_NFIND(x, y)
我们看看name##_RB_NFIND即uv__signal_tree_s_RB_NFIND的实现
static struct uv_signal_t * uv__signal_tree_s_RB_NFIND(struct uv__signal_tree_s *head, struct uv_signal_t *elm)
{
struct uv_signal_t *tmp = RB_ROOT(head);
struct uv_signal_t *res = NULL;
int comp;
while (tmp) {
comp = cmp(elm, tmp);
/*
elm小于当前节点则往左子树找,大于则往右子树找,
等于则返回
*/
if (comp < 0) {
// 记录父节点
res = tmp;
tmp = RB_LEFT(tmp, field);
}
else if (comp > 0)
tmp = RB_RIGHT(tmp, field);
else
return (tmp);
}
return (res);
}
uvsignal_tree_s_RB_NFIND的逻辑就是根据红黑树的特点进行搜索,这里的重点是cmp函数。刚才我们已经分析过cmp的逻辑。这里会首先查找没有设置one shot标记的handle(因为它的值小),然后再查找设置了one shot的handle,一旦遇到设置了one shot的handle,则说明后面被匹配的handle也是设置了one shot标记的。每次找到一个handle,就会封装一个msg写入管道(即和Libuv通信的管道)。信号的处理就完成了。接下来在Libuv的Poll IO阶段才做真正的处理。我们知道在Poll IO阶段。epoll会检测到管道loop->signal_pipefd[0]可读,然后会执行uvsignal_event函数。我们看看这个函数的代码。
// 如果收到信号,Libuv Poll IO阶段,会执行该函数
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 {
// 读出所有的uv__signal_msg_t
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;
/*
根据收到的字节数算出有多少个uv__signal_msg_t结构体,
从而算出结束位置
*/
end=(bytes/sizeof(uv__signal_msg_t))*sizeof(uv__signal_msg_t);
// 循环处理每一个msg
for (i = 0; i < end; i += sizeof(uv__signal_msg_t)) {
msg = (uv__signal_msg_t*) (buf + i);
// 取出上下文
handle = msg->handle;
// 收到的信号和handle感兴趣的信号一致,执行回调
if (msg->signum == handle->signum) {
handle->signal_cb(handle, handle->signum);
}
// 处理信号个数,和收到的个数对应
handle->dispatched_signals++;
// 只执行一次,恢复系统默认的处理函数
if (handle->flags & UV_SIGNAL_ONE_SHOT)
uv__signal_stop(handle);
/*
处理完所有收到的信号才能关闭uv_signal_t,
见uv_close或uv__signal_close
*/
if ((handle->flags & UV_HANDLE_CLOSING) &&
(handle->caught_signals==handle->dispatched_signals))
{
uv__make_close_pending((uv_handle_t*) handle);
}
}
bytes -= end;
if (bytes) {
memmove(buf, buf + end, bytes);
continue;
}
} while (end == sizeof buf);
}
uvsignal_event函数的逻辑如下
1 读出管道里的数据,计算出msg的个数。
2 遍历收到的数据,解析出一个个msg。
3 从msg中取出上下文(handle和信号),执行上层回调。
4 如果handle设置了one shot则需要执行uvsignal_stop(我们接下来分析)。
5 如果handle设置了closing标记,则判断所有收到的信号是否已经处理完。即收到的个数和处理的个数是否一致。需要处理完所有收到的信号才能关闭uv_signal_t。
7.7 取消/关闭信号处理
当一个信号对应的handle设置了one shot标记,在收到信号并且执行完回调后,Libuv会调用uvsignal_stop关闭该handle并且从红黑树中移除该handle。另外我们也可以显式地调用uv_close(会调用uvsignal_stop)关闭或取消信号的处理。下面我们看看uv__signal_stop的实现。
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 the watcher wasn't started, this is a no-op. */
// 没有注册过信号,则不需要处理
if (handle->signum == 0)
return;
// 屏蔽所有信号
uv__signal_block_and_lock(&saved_sigmask);
// 移出红黑树
removed_handle = RB_REMOVE(uv__signal_tree_s, &uv__signal_tree, handle);
// 判断该信号是否还有对应的handle
first_handle = uv__signal_first_handle(handle->signum);
// 为空说明没有handle会处理该信号了,解除该信号的设置
if (first_handle == NULL) {
uv__signal_unregister_handler(handle->signum);
} else {
// 被处理的handle是否设置了one shot
rem_oneshot = handle->flags & UV_SIGNAL_ONE_SHOT;
/*
剩下的第一个handle是否设置了one shot,
如果是则说明该信号对应的所有剩下的handle都是one shot
*/
first_oneshot = first_handle->flags & UV_SIGNAL_ONE_SHOT;
/*
被移除的handle没有设置oneshot但是当前的第一个handle设置了
one shot,则需要修改该信号处理函数为one shot,防止收到多次信
号,执行多次回调
*/
if (first_oneshot && !rem_oneshot) {
ret = uv__signal_register_handler(handle->signum, 1);
assert(ret == 0);
}
}
uv__signal_unlock_and_unblock(&saved_sigmask);
handle->signum = 0;
uv__handle_stop(handle);
}
7.8 信号在Node.js中的使用
分析完Libuv的实现后,我们看看Node.js上层是如何使用信号的,首先我们看一下C++层关于信号模块的实现。
static void Initialize(Local<Object> target,
Local<Value> unused,
Local<Context> context,
void* priv) {
Environment* env = Environment::GetCurrent(context);
Local<FunctionTemplate> constructor = env->NewFunctionTemplate(New);
constructor->InstanceTemplate()->SetInternalFieldCount(1);
// 导出的类名
Local<String> signalString =
FIXED_ONE_BYTE_STRING(env->isolate(), "Signal");
constructor->SetClassName(signalString);
constructor->Inherit(HandleWrap::GetConstructorTemplate(env));
// 给Signal创建的对象注入两个函数
env->SetProtoMethod(constructor, "start", Start);
env->SetProtoMethod(constructor, "stop", Stop);
target->Set(env->context(), signalString,
constructor->GetFunction(env->context()).ToLocalChecked()).Check();
}
当我们在JS中new Signal的时候,首先会创建一个C++对象,然后作为入参执行New函数。
static void New(const FunctionCallbackInfo<Value>& args) {
CHECK(args.IsConstructCall());
Environment* env = Environment::GetCurrent(args);
new SignalWrap(env, args.This());
}
当我们在JS层操作Signal实例的时候,就会执行C++层对应的方法。主要的方法是注册和删除信号。
static void Start(const FunctionCallbackInfo<Value>& args) {
SignalWrap* wrap;
ASSIGN_OR_RETURN_UNWRAP(&wrap, args.Holder());
Environment* env = wrap->env();
int signum;
if (!args[0]->Int32Value(env->context()).To(&signum)) return;
int err = uv_signal_start(
&wrap->handle_,
// 信号产生时执行的回调
[](uv_signal_t* handle, int signum) {
SignalWrap* wrap = ContainerOf(&SignalWrap::handle_,
handle);
Environment* env = wrap->env();
HandleScope handle_scope(env->isolate());
Context::Scope context_scope(env->context());
Local<Value> arg = Integer::New(env->isolate(),
signum);
// 触发JS层onsignal函数
wrap->MakeCallback(env->onsignal_string(), 1, &arg);
},
signum);
if (err == 0) {
CHECK(!wrap->active_);
wrap->active_ = true;
Mutex::ScopedLock lock(handled_signals_mutex);
handled_signals[signum]++;
}
args.GetReturnValue().Set(err);
}
static void Stop(const FunctionCallbackInfo<Value>& args) {
SignalWrap* wrap;
ASSIGN_OR_RETURN_UNWRAP(&wrap, args.Holder());
if (wrap->active_) {
wrap->active_ = false;
DecreaseSignalHandlerCount(wrap->handle_.signum);
}
int err = uv_signal_stop(&wrap->handle_);
args.GetReturnValue().Set(err);
}
接着我们看在JS层如何使用。Node.js在初始化的时候,在is_main_thread.js中执行了。
process.on('newListener', startListeningIfSignal);
process.on('removeListener', stopListeningIfSignal)
newListener和removeListener事件在注册和删除事件的时候都会被触发。我们看一下这两个函数的实现
/*
{
SIGINT: 2,
...
}
*/
const { signals } = internalBinding('constants').os;
let Signal;
const signalWraps = new Map();
function isSignal(event) {
return typeof event === 'string' && signals[event] !== undefined;
}
function startListeningIfSignal(type) {
if (isSignal(type) && !signalWraps.has(type)) {
if (Signal === undefined)
Signal = internalBinding('signal_wrap').Signal;
const wrap = new Signal();
// 不影响事件循环的退出
wrap.unref();
// 挂载信号处理函数
wrap.onsignal = process.emit.bind(process, type, type);
// 通过字符拿到数字
const signum = signals[type];
// 注册信号
const err = wrap.start(signum);
if (err) {
wrap.close();
throw errnoException(err, 'uv_signal_start');
}
// 该信号已经注册,不需要往底层再注册了
signalWraps.set(type, wrap);
}
}
startListeningIfSignal函数的逻辑分为一下几个
1 判断该信号是否注册过了,如果注册过了则不需要再注册。Libuv本身支持在同一个信号上注册多个处理函数,Node.js的JS层也做了这个处理。
2 调用unref,信号的注册不应该影响事件循环的退出
3 挂载事件处理函数,当信号触发的时候,执行对应的处理函数(一个或多个)。
4 往底层注册信号并设置该信号已经注册的标记
我们再来看一下stopListeningIfSignal。
function stopListeningIfSignal(type) {
const wrap = signalWraps.get(type);
if (wrap !== undefined && process.listenerCount(type) === 0) {
wrap.close();
signalWraps.delete(type);
}
}
只有当信号被注册过并且事件处理函数个数为0,才做真正的删除。