libevent源码分析之关于notify

之前学习过程中遇到过要notify主线程的情况, 不过并未理会选择直接跳过, 现在拿来细讲.
其实简单的说, notify就是告知主线程你的内部发生了变化,现在最好停止等待, 重新调用IO复用函数


比如在event_add ---> event_add_internal中
static inline int
event_add_internal(struct event *ev, const struct timeval *tv,
int tv_is_absolute)
{
        ... ...


	int notify = 0;				//默认为不需要通知


        ... ...


	if ((ev->ev_events & (EV_READ|EV_WRITE|EV_SIGNAL)) && !(ev->ev_flags & (EVLIST_INSERTED|EVLIST_ACTIVE)))


                ... ...
                //根据evmap_***_add函数的内容,我们可以看出,若返回-1则表示出错, 返回0表示成功但不发生任何事情, 返回1表示成功但需要提醒主线程要在IO复用中添加要关注的文件描述符
		if (res == 1) {
			/* evmap says we need to notify the main thread. */
			notify = 1;
			res = 0;
		}
	}


	if (res != -1 && tv != NULL) {
          
                ... ...


		/*
		 * we already reserved memory above for the case where we
		 * are not replacing an existing timeout.
		 */	
		if (ev->ev_flags & EVLIST_TIMEOUT) {
			/* XXX I believe this is needless. */
			//判断此事件是否为堆顶元素,如果是的话,就需要提醒主线程
                        //还未涉及这里可以暂时不管. 也可以进行猜测,可能在select的定时部分设置的是小顶堆顶部的时间?这样删除它就需要notify主线程了
                        //但是官方注释说没必要? 那就是最小的元素已经被取出来了?... 
			if (min_heap_elt_is_top(ev))
				notify = 1;


			event_queue_remove(base, ev, EVLIST_TIMEOUT);
		}
     
                ... ...


		} else {
			/* See if the earliest timeout is now earlier than it
			 * was before: if so, we will need to tell the main
			 * thread to wake up earlier than it would
			 * otherwise. */
                        //将新插入的定时器与之前最小的定时器相比较, 如果新的更小,那要提醒主线程IO复用缩短wake up的时间
			if (min_heap_elt_is_top(ev))
				notify = 1;
		}
	}


	/* if we are not in the right thread, we need to wake up the loop */
	if (res != -1 && notify && EVBASE_NEED_NOTIFY(base))
                //可见,是通过此函数通知提醒主线程的
		evthread_notify_base(base);
}


看了上面函数中需要notify base的地方,我们可能就发现了其中notify发生的前提, 猜测应该一般是在需要修改event_base内容的时候才要notify的
就拿这个添加要监听的描述符的函数来说,在我们得知某描述符的可读或可写尚未监听且需要监听时, 立马就令notify为1
在定时器发生变化时, 也立即令notify为1
通过观察这个函数, 我们模模糊糊的感觉到了notify的作用, 那么它到底是传输什么数据的呢? 是如何传输的呢?

想要知道这些, 需要一步步来观察
首先, 需要先看看event_base中关于notify的部分:
struct event_base {
        ... ...


	/* Notify main thread to wake up break, etc. */
	/** True if the base already has a pending notify, and we don't need
	 * to add any more. */
	int is_notify_pending;
	/** A socketpair used by some th_notify functions to wake up the main
	 * thread. */
	evutil_socket_t th_notify_fd[2];
	/** An event used by some th_notify functions to wake up the main
	 * thread. */
	struct event th_notify;
	/** A function used to wake up the main thread from another thread. */
	int (*th_notify_fn)(struct event_base *base);


};


从以上的定义看来, 对一个event_base, 同一时间只可以有一个可以提醒base的notify,还发现了一个管道, 一个与notify相关的事件, 是不是觉得很熟悉呢? 
对的, 这里与信号的处理方式类似, 只是信号的发生比较突然, 系统会调用信号捕捉函数, 从而将信号的值通过管道发送给后台的IO复用, IO复用判断信号值来调用不同信号注册的不同回调函数. 对于notity, 则需要在需要的地方向管道发送数据, IO复用知道有notify来后会调用其回调函数进行相关处理
至于最后一个函数指针, 并不是IO复用得知管道一端可读后就调用的回调函数, 此函数是包装的一个公用函数,会被很多地方用到, 目的向IO复用监听的管道一端发送数据,唤醒主线程。

既然如此, 想要知道libevent是如何使一个base具有被notify的功能, 肯定是去找base的初始化部分了
event_base_new ---> event_base_new_with_config ---> evthread_make_base_notifiable


//在event_base_new_with_config中:
r = evthread_make_base_notifiable(base);
if (r<0) {
	event_warnx("%s: Unable to make base notifiable.", __func__);
	event_base_free(base);
	return NULL;
}

//在event.c中
static void
evthread_notify_drain_default(evutil_socket_t fd, short what, void *arg)
{
	unsigned char buf[1024];
	struct event_base *base = arg;
#ifdef WIN32
	while (recv(fd, (char*)buf, sizeof(buf), 0) > 0)
		;
#else
	while (read(fd, (char*)buf, sizeof(buf)) > 0)
		;
#endif


	EVBASE_ACQUIRE_LOCK(base, th_base_lock);
	base->is_notify_pending = 0;
	EVBASE_RELEASE_LOCK(base, th_base_lock);
}


static int
evthread_notify_base_default(struct event_base *base)
{
	char buf[1];
	int r;
	buf[0] = (char) 0;
#ifdef WIN32
	r = send(base->th_notify_fd[1], buf, 1, 0);
#else
	r = write(base->th_notify_fd[1], buf, 1);
#endif
	return (r < 0 && errno != EAGAIN) ? -1 : 0;
}


int
evthread_make_base_notifiable(struct event_base *base)
{
        //此函数目的是从notify的管道一端读
	void (*cb)(evutil_socket_t, short, void *) = evthread_notify_drain_default;
        //此函数目的是从notify的管道一端写
	int (*notify)(struct event_base *) = evthread_notify_base_default;


	/* XXXX grab the lock here? */
	if (!base)
		return -1;


        //初始化为-1, 大于0表示这个事件注册过了
	if (base->th_notify_fd[0] >= 0)
		return 0;


        ... ...


        //几种用来传输notify的方式, 上一种是 eventfd, 下一种是win32上的方法
        //这里选择pipe来看目的是理解起来更清晰
#if defined(_EVENT_HAVE_PIPE)
	if (base->th_notify_fd[0] < 0) {
                //有的后台IO复用不一定支持文件描述符的, 所以如果不支持, 就不能使用pipe了
		if ((base->evsel->features & EV_FEATURE_FDS)) {
			if (pipe(base->th_notify_fd) < 0) {
				event_warn("%s: pipe", __func__);
			} else {
				evutil_make_socket_closeonexec(base->th_notify_fd[0]);
				evutil_make_socket_closeonexec(base->th_notify_fd[1]);
			}
		}
	}
#endif


        ... ...


        //以上申请的两端都具备了closeonexec的属性
        //使得读端是非阻塞的
	evutil_make_socket_nonblocking(base->th_notify_fd[0]);


	base->th_notify_fn = notify;


	/*
	  Making the second socket nonblocking is a bit subtle, given that we
	  ignore any EAGAIN returns when writing to it, and you don't usally
	  do that for a nonblocking socket. But if the kernel gives us EAGAIN,
	  then there's no need to add any more data to the buffer, since
	  the main thread is already either about to wake up and drain it,
	  or woken up and in the process of draining it.
	*/


        //****************************************************************************************************************************
        //这里是我疑惑的地方, 下面evthread_notify_base函数中表明, 如果有一个事件notify, 其余就不需要再notify了
        //不会有填满的时候, 可上面的注释却说填满了也不要管
        //虽然目的一样, 但却矛盾了, 让我茫然...
	if (base->th_notify_fd[1] > 0)
		evutil_make_socket_nonblocking(base->th_notify_fd[1]);


        //这就是我们要注册的事件了
	/* prepare an event that we can use for wakeup */
	event_assign(&base->th_notify, base, base->th_notify_fd[0],
				 EV_READ|EV_PERSIST, cb, base);


	/* we need to mark this as internal event */
	base->th_notify.ev_flags |= EVLIST_INTERNAL;
	event_priority_set(&base->th_notify, 0);


        //添加到IO复用中去,用以唤醒主线程
	return event_add(&base->th_notify, NULL);
}


好了, 在观察了这几个函数后, 来做一个简略的总结.
(在event_base_loop中)当我们dispatch一个base后, base一马当先获取锁, 之后调用IO复用的dispatch. (在select.c中的evsel->dispatch中), 真正调用IO复用前释放锁.进入休眠状态等待某个或某几个描述符可用. (在event->add中)某一时刻, 某线程要往base中添加一个需要监听的描述符, 此刻主线程处于select的休眠状态. 该线程获取锁, (在event_add_internal)在添加完毕后向主线程监听的notify管道端口传输数据试图唤醒主线程,此时select醒来,但无法获取锁从而阻塞,而此线程完成后释放锁. 此刻假设主线程除了notify端口之外尚且没有任何描述符有动静,主线程在之前线程释放锁后立刻得到锁, 使用修改后的参数重新启动IO复用函数.



关于调用IO复用函数前后对锁的操作可见下面两个简略函数:
int
event_base_loop(struct event_base *base, int flags)
{
        ... ...


       /* Grab the lock.  We will release it inside evsel.dispatch, and again
	 * as we invoke user callbacks. */
       /获取锁
	EVBASE_ACQUIRE_LOCK(base, th_base_lock);


        ... ...
 
	while (!done) {
		... ...


		res = evsel->dispatch(base, tv_p);


                ... ...
	}


done:
        //整个base结束后的动作
	EVBASE_RELEASE_LOCK(base, th_base_lock);


}

//就是上面函数中的 evsel->dispatch(base, tv_p);
static int
select_dispatch(struct event_base *base, struct timeval *tv)
{
        ... ...


    //若支持多线程, 就必须要使用锁控制住核心函数select。
	EVBASE_RELEASE_LOCK(base, th_base_lock);


	res = select(nfds, sop->event_readset_out,
	    sop->event_writeset_out, NULL, tv);


	EVBASE_ACQUIRE_LOCK(base, th_base_lock);
 
        ... ... 


}


可以清楚的看到, 在开始循环之前, 会提前获得锁, 而在进入等待IO可读或可写期间打开锁, 在有描述符可读或可写后继续锁住.
从整体上来看,我们可以在任何时候添加事件。但是, 往细节观察就发现,已经把添加、删除event的操作限制在了IO复用的休眠期间. 
这样做就是因为多线程导致的. 如果没有锁,base的dispatch线程(主线程)会增删base的数据成员内容, 而其他线程也要修改base的数据成员内容, 岂不是乱了?所以主线程在非睡眠期间加锁. 然而在IO复用睡眠期间, 则允许其他线程增删修改base内容继而notify主线程重启IO复用

先观察在event_add_internal中调用的evthread_notify_base(base)

static int
evthread_notify_base(struct event_base *base)
{
	EVENT_BASE_ASSERT_LOCKED(base);
        //至少要提供提醒用的函数吧?
	if (!base->th_notify_fn)
		return -1;
        //都已经
	if (base->is_notify_pending)
		return 0;
	base->is_notify_pending = 1;
        //base->th_notify_fn之前也提到过, 目的是向管道一端write
	return base->th_notify_fn(base);
}

之后IO复用通过注册事件的回调函数读取notify管道发来的数据.一系列过程上面也解释的十分清楚了.
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值