上一节我们对信号evsignal_info
作了一个简单的分析, 本节就如同前面分析event
功能函数时一样的来分析signal
操作.
信号初始化
evsignal_init
函数实现对一个信号的初始化操作. 源码位置: signal.c
int
evsignal_init(struct event_base *base)
{
int i;
/*
* Our signal handler is going to write to one end of the socket
* pair to wake up our event loop. The event loop then scans for
* signals that got delivered.
*/
// 创建套接字
// 将 ev_signal_pair[2] 一对用 accept 和 connect 关联, 用于响应事件
if (evutil_socketpair(
AF_UNIX, SOCK_STREAM, 0, base->sig.ev_signal_pair) == -1) {
#ifdef WIN32
/* Make this nonfatal on win32, where sometimes people
have localhost firewalled. */
event_warn("%s: socketpair", __func__);
#else
event_err(1, "%s: socketpair", __func__);
#endif
return -1;
}
// 设置文件描述符
FD_CLOSEONEXEC(base->sig.ev_signal_pair[0]);
FD_CLOSEONEXEC(base->sig.ev_signal_pair[1]);
// 初始化 ev_signal
base->sig.sh_old = NULL;
base->sig.sh_old_max = 0;
base->sig.evsignal_caught = 0;
memset(&base->sig.evsigcaught, 0, sizeof(sig_atomic_t)*NSIG);
/* initialize the queues for all events */
for (i = 0; i < NSIG; ++i)
TAILQ_INIT(&base->sig.evsigevents[i]);
// 将写事件设置为 非阻塞
evutil_make_socket_nonblocking(base->sig.ev_signal_pair[0]);
// 注册读事件并设置为永久事件, 设置回调函数为 evsignal_cb
// evsignal_cb : 实现读一个字节. 实质表示有信号可以处理
event_set(&base->sig.ev_signal, base->sig.ev_signal_pair[1],
EV_READ | EV_PERSIST, evsignal_cb, &base->sig.ev_signal);
base->sig.ev_signal.ev_base = base;
base->sig.ev_signal.ev_flags |= EVLIST_INTERNAL; // 标志已被初始化
return 0;
}
函数中可能不太明白的
-
FD_CLOSEONEXEC
, 下面给出了源码, 其实就是设置文件描述符fork
后会自动关闭.#define FD_CLOSEONEXEC(x) do { \ if (fcntl(x, F_SETFD, 1) == -1) \ event_warn("fcntl(%d, F_SETFD)", x); \ } while (0)
-
evutil_make_socket_nonblocking
也是相似的功能, 将套接字设为非阻塞. -
唯一不太明了我猜就是在信号初始化中居然调用
evutil_socketpair
函数创建套接字, 这个问题待会结合下面的函数来说明.
信号初始化默认设置了一个事件的回调函数evsignal_cb
, 该函数的功能也很简单, 从fd(也就是ev_signal_pair[1]
)读一个字节.
/* Callback for when the signal handler write a byte to our signaling socket */
static void
evsignal_cb(int fd, short what, void *arg)
{
static char signals[1];
#ifdef WIN32
SSIZE_T n;
#else
ssize_t n;
#endif
n = recv(fd, signals, sizeof(signals), 0);
if (n == -1)
event_err(1, "%s: read", __func__);
}
evutil_socketpair 函数
该函数我们做注释, 因为都是网络编程那方面的知识, 主要关注的是fd[2]
这个参数.
int
evutil_socketpair(int family, int type, int protocol, int fd[2])
{
#ifndef WIN32
return socketpair(family, type, protocol, fd);
#else
/* This code is originally from Tor. Used with permission. */
/* This socketpair does not work when localhost is down. So
* it's really not the same thing at all. But it's close enough
* for now, and really, when localhost is down sometimes, we
* have other problems too.
*/
int listener = -1;
int connector = -1;
int acceptor = -1;
struct sockaddr_in listen_addr;
struct sockaddr_in connect_addr;
int size;
int saved_errno = -1;
if (protocol
#ifdef AF_UNIX
|| family != AF_UNIX
#endif
) {
EVUTIL_SET_SOCKET_ERROR(WSAEAFNOSUPPORT);
return -1;
}
if (!fd) {
EVUTIL_SET_SOCKET_ERROR(WSAEINVAL);
return -1;
}
listener = socket(AF_INET, type, 0);
if (listener < 0)
return -1;
memset(&listen_addr, 0, sizeof(listen_addr));
listen_addr.sin_family = AF_INET;
listen_addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
listen_addr.sin_port = 0; /* kernel chooses port. */
if (bind(listener, (struct sockaddr *) &listen_addr, sizeof (listen_addr))
== -1)
goto tidy_up_and_fail;
if (listen(listener, 1) == -1)
goto tidy_up_and_fail;
connector = socket(AF_INET, type, 0);
if (connector < 0)
goto tidy_up_and_fail;
/* We want to find out the port number to connect to. */
size = sizeof(connect_addr);
if (getsockname(listener, (struct sockaddr *) &connect_addr, &size) == -1)
goto tidy_up_and_fail;
if (size != sizeof (connect_addr))
goto abort_tidy_up_and_fail;
if (connect(connector, (struct sockaddr *) &connect_addr,
sizeof(connect_addr)) == -1)
goto tidy_up_and_fail;
size = sizeof(listen_addr);
acceptor = accept(listener, (struct sockaddr *) &listen_addr, &size);
if (acceptor < 0)
goto tidy_up_and_fail;
if (size != sizeof(listen_addr))
goto abort_tidy_up_and_fail;
EVUTIL_CLOSESOCKET(listener);// 关闭监听套接字
/* Now check we are talking to ourself by matching port and host on the
two sockets. */
if (getsockname(connector, (struct sockaddr *) &connect_addr, &size) == -1)
goto tidy_up_and_fail;
if (size != sizeof (connect_addr)
|| listen_addr.sin_family != connect_addr.sin_family
|| listen_addr.sin_addr.s_addr != connect_addr.sin_addr.s_addr
|| listen_addr.sin_port != connect_addr.sin_port)
goto abort_tidy_up_and_fail;
// 最重要的部分
fd[0] = connector;
fd[1] = acceptor;
return 0;
abort_tidy_up_and_fail:
saved_errno = WSAECONNABORTED;
tidy_up_and_fail:
if (saved_errno < 0)
saved_errno = WSAGetLastError();
if (listener != -1)
EVUTIL_CLOSESOCKET(listener);
if (connector != -1)
EVUTIL_CLOSESOCKET(connector);
if (acceptor != -1)
EVUTIL_CLOSESOCKET(acceptor);
EVUTIL_SET_SOCKET_ERROR(saved_errno);
return -1;
#endif
}
注意该函数的一个地方.
- fd[0] = connect 表示的是保存服务端的套接字(文件描述符)
- fd[1] = acceptor 表示的是保存客户端的套接字(文件描述符)
// 最重要的部分
fd[0] = connector;
fd[1] = acceptor;
socket pair
上一节在evsignal_info
结构中提到ev_signal_pair[2]
是一个socket pair.
什么是socket pair
呢?
- 就是采用了IO通信的方式一对文件描述符组合. 回到
ev_signal_pair[2]
, 这就是一对IO通信的连接,[0]
表示服务端,[1]
表示客服端.
信号机制为什么要用socket pair
呢 ?
- 当信号发生时, 注册的回调函数
evsignal_handler
会向服务端发送一字节数据并设置该信号标志和次数, 而服务端则调用统一的就绪回调函数evsignal_cb
接收一字节数据, 而libevent通过分发器(如 : epoll 中的epoll_wait) 调用evsignal_process
将信号加入就绪队列中. (是不是感觉很巧妙, 信号可以这样被加入就绪链表, 我第一次看到这里的时候也很震惊,神奇的操作) (evsignal_handler我们下节来分析) - libevent 之所以这样做的原因就是将信号, IO, 时间都交给统一的激活函数处理, 而不再是单干自己.
总结
- 清楚
evsignal_init
的实现过程 - 明白
socket pair
在信号处理中的作用