Libevent: 集成signal处理到事件处理主循环中

集成signal处理到事件中

Tips: 关联signal到event base的api主要在evsignal.h

Note: 以下封装的系统I\O mutiplexing具体实现均以epoll为例子

集成signal处理的核心信息在evsignal_info结构体中

struct evsignal_info {
	struct event ev_signal;               // "读socket"事件,用于关联signal到事件框架中
	int ev_signal_pair[2];                // socket对,用于关联signal到事件框架中
	int ev_signal_added;                  // 标识是否已注册"读socket到base"中.
	volatile sig_atomic_t evsignal_caught;// 标识base中信号发生, 0无1有
	struct event_list evsigevents[NSIG];  // evsigevents[sig]表示注册到sig信号的事件集合
	sig_atomic_t evsigcaught[NSIG];       // evsigcaught[sig]表示记录信号sig的触发次数           
#ifdef HAVE_SIGACTION
	struct sigaction **sh_old;            // sh_old数组,记录原有的信号处理的函数指针
#else                                     // 当signal事件从base注销后,需要恢复原有的signal handler
	ev_sighandler_t **sh_old;             
#endif
	int sh_old_max;                       // sh_old的容量大小
};

信号机制的加载

eventop::init中,通过evsignal_init调用evutil_socketpair创建socket pair.

evsignal_init 职责: 初始化struct evsignal_info:

  1. 创建socket对
  2. 初始化ev_singal(封装读端的event)
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.
	 */
	if (evutil_socketpair(
		    AF_UNIX, SOCK_STREAM, 0, base->sig.ev_signal_pair) == -1) {
		return -1;
	}
    
	FD_CLOSEONEXEC(base->sig.ev_signal_pair[0]); //写端
	FD_CLOSEONEXEC(base->sig.ev_signal_pair[1]); //读端
	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]);
        evutil_make_socket_nonblocking(base->sig.ev_signal_pair[1]);
	
	// 初始化socket pair读事件, 
	// 实际上直到base被注册signal事件才将读socket注册到base中
	// 详见evsig_add()
	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;
}

Note: evsignal_cb 几乎什么也没做


懒惰加载——注册读端

eventop::add(以epoll_add为例)只有观察到用户注册signal event到base时,才将读端event关联到base中.

static int
epoll_add(void *arg, struct event *ev)
{
	struct epollop *epollop = arg;
	struct epoll_event epev = {0, {0}};
	struct evepoll *evep;
	int fd, op, events;

	if (ev->ev_events & EV_SIGNAL)
		return (evsignal_add(ev));

	/*......*/

	return (0);
}
int
evsignal_add(struct event *ev)
{
	int evsignal;
	struct event_base *base = ev->ev_base;
	struct evsignal_info *sig = &ev->ev_base->sig;
    
	// 事件不能即使I/O事件又是signal事件(check valid)
	if (ev->ev_events & (EV_READ|EV_WRITE))
		event_errx(1, "%s: EV_SIGNAL incompatible use", __func__);

    // 检测signal事件的ev_fd合法性(必须是系统支持的信号类型范围)
	// evsignal相当于信号的类型的数值signo
	evsignal = EVENT_SIGNAL(ev);
	assert(evsignal >= 0 && evsignal < NSIG);

	// 首次添加该信号事件
	if (TAILQ_EMPTY(&sig->evsigevents[evsignal])) {
		if (_evsignal_set_handler(
			    base, evsignal, evsignal_handler) == -1)
			return (-1);

		/* catch signals if they happen quickly */
		evsignal_base = base;
        
		// 首次注册"读socket"事件到base中(懒惰加载)
		if (!sig->ev_signal_added) {
			if (event_add(&sig->ev_signal, NULL))
				return (-1);
			sig->ev_signal_added = 1;
		}
	}

	/* multiple events may listen to the same signal */
	TAILQ_INSERT_TAIL(&sig->evsigevents[evsignal], ev, ev_signal_next);

	return (0);
}

socket pair运作机制

通过socket pair关联signal event处理。
一个做读端一个做写端。eventop::dispatch接受到信号中断后,在signal handler(evsignal_handler)中往写端写入数据从而唤醒读端event.

当signal被接受时,不是立刻调用相应的signal handler,而是仅仅通过evsignal_handler告知base,这个signal接收了几次,再通过唤醒读端event告知base并通过evsignal_process将触发的信号事件加入到激活链表中, 最后在事件主循环中对signal事件和I\O事件集中统一处理。

思路脉络:

return
dispatch
signal handler
text change
restore
evsignal_handler
epoll_wait
evsignal_process
event_base_loop

evsignal_handler的职责:

  1. 记录信号产生的标识
  2. 登记被触发信号的触发次数
  3. 重新注册信号
/**
 * 用于注册到signal(2)的handler
 * 
 * 职责:
 * 1. 当系统处理信号中断时,维护base中的signal事件触发相关信息
 * 2. 通过"写socket"唤醒注册到base中的"读socket"处理信号事件
 */
static void
evsignal_handler(int sig)
{
	int save_errno = errno;

	if (evsignal_base == NULL) {
		event_warn(
			"%s: received signal %d, but have no base configured",
			__func__, sig);
		return;
	}
    
	// sig触发次数自增
	evsignal_base->sig.evsigcaught[sig]++;
	// 标记base中有signal事件被触发
	evsignal_base->sig.evsignal_caught = 1;

    // 若系统不支持sigaction函数,libevent将做什么处理?
#ifndef HAVE_SIGACTION
    // 重新注册信号
	signal(sig, evsignal_handler);
#endif

	/* Wake up our notification mechanism */
	// 向"写socket"写入数据唤醒"读socket"
	send(evsignal_base->sig.ev_signal_pair[0], "a", 1, 0);
	errno = save_errno;
}

在libevent接受信号后(即控制回到程序段eventop::dispatch)
1.系统I/O检测到读端事件的到达,从阻塞中恢复
2.随后evsignal_process处理信息事件。

evsignal_process的职责:
将所有触发的信号放入激活链表中

/**
 * 遍历所有信号类型的触发次数
 * 针对被触发的信号,遍历注册该信号的事件链表
 * 并将相关事件添加到激活链表中
 */
void
evsignal_process(struct event_base *base)
{
	struct evsignal_info *sig = &base->sig;
	struct event *ev, *next_ev;
	sig_atomic_t ncalls;
	int i;
	
	// 重置信号触发标识
	base->sig.evsignal_caught = 0;
	for (i = 1; i < NSIG; ++i) {
		ncalls = sig->evsigcaught[i];
		if (ncalls == 0)
			continue;
		
		// 为什么不直接置0而是减ncals呢?
		// 猜测由于signal到达时机不明确,
		// 可能导致竟态问题
		sig->evsigcaught[i] -= ncalls;

        // 遍历注册了该信号的事件链表
		for (ev = TAILQ_FIRST(&sig->evsigevents[i]);
		    ev != NULL; ev = next_ev) {
			next_ev = TAILQ_NEXT(ev, ev_signal_next);
			// 非持久事件直接删除
			if (!(ev->ev_events & EV_PERSIST))
				event_del(ev);
			// 添加信号事件到激活链表
			event_active(ev, EV_SIGNAL, ncalls);
		}

	}
}

Q: eventop::dispatch已被信号中断,通过写端唤醒读端还有必要的吗?
A: 有必要。eventop::dispatch会自动重启(设置了SA_RESTART)时,就需要“读socket"即时唤醒。这其实与可移植性有关,系统的信号处理语义不同系统中的实现不明确,libevent对此做了调整,能重启被中断的系统调用尽量重启。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在TCP协议,由于数据传输的不可靠性,数据可能会被拆分成多个小的数据包进行传输,或者多个数据包会被合并成一个大的数据包进行传输,这就是所谓的TCP粘包问题。 libevent是一个高性能事件驱动库,可以用来处理网络通信,包括TCP粘包问题。下面介绍一下libevent处理TCP粘包的方法: 1. 设置TCP_NODELAY选项 在TCP连接建立时,可以设置TCP_NODELAY选项为1,表示禁止Nagle算法,即禁止将数据合并成一个大的数据包进行传输。这样可以避免TCP粘包问题的发生。设置方式如下: ```c int flag = 1; setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, (char *)&flag, sizeof(flag)); ``` 2. 使用分隔符 在数据传输时,可以在每个数据包的末尾添加一个分隔符,表示数据包的结束。这样接收端就可以根据分隔符来分割数据包,避免TCP粘包问题。常用的分隔符有"\r\n"和"\0"。设置方式如下: ```c #define DELIMITER "\r\n" char buf[MAX_BUF_SIZE]; int len = recv(fd, buf, MAX_BUF_SIZE, 0); if (len > 0) { buf[len] = '\0'; char *p = strstr(buf, DELIMITER); if (p != NULL) { *p = '\0'; // 处理一个完整的数据包 } } ``` 3. 使用固定长度的数据包 在数据传输时,可以将每个数据包的长度固定为一个固定的值,这样接收端就可以根据固定长度来分割数据包,避免TCP粘包问题。设置方式如下: ```c #define PACKET_SIZE 1024 char buf[MAX_BUF_SIZE]; int len = recv(fd, buf, MAX_BUF_SIZE, 0); if (len > 0) { while (len >= PACKET_SIZE) { // 处理一个完整的数据包 len -= PACKET_SIZE; } } ``` 4. 使用消息头 在数据传输时,可以在每个数据包的开头添加一个消息头,消息头包含了数据包的长度信息,这样接收端就可以根据消息头的长度信息来分割数据包,避免TCP粘包问题。设置方式如下: ```c #define HEADER_LENGTH 4 char buf[MAX_BUF_SIZE]; int len = recv(fd, buf, MAX_BUF_SIZE, 0); if (len > 0) { while (len >= HEADER_LENGTH) { int packet_len = *(int *)buf; // 读取消息头的长度信息 if (len >= HEADER_LENGTH + packet_len) { // 处理一个完整的数据包 len -= HEADER_LENGTH + packet_len; } else { break; } } } ``` 以上是libevent处理TCP粘包问题的几种方法,可以根据具体情况选择合适的方法。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值