Eight, unified Timer event and I/O event detailed
It's easy to unify timer events and I/O events compared to signal events, why? Because the I/O multiplexing mechanism such as SELECT (), poll (), epoll_wait () allows setting a maximum wait time ^_^. So, let's take a look at how libevent is doing it. PS: In fact, a lot of event-driven software is doing this.
1. Implementation methods
The core is to set the maximum wait time for I/O multiplexing in each event loop to be the time of the top node in the small top heap of the timer (i.e. the timer event that will be sent the earliest). Of course, if the activation event queue is not empty when entering an event loop (that is, a ready event is not processed), the maximum wait time is set to 0.
The specific code is in the Event_base_loop () of the source file event.c:
/*event_ Dispatch calls this function, the core loop of the event-driven mechanism, to listen for events in this event loop and to handle the event */intevent_base_loop (struct event_base *base, int flags) {.../* The flags currently have no effect, passing all 0, if there is currently no active event, take time from the top heap, as callback epoll_wait the third parameter */if (!base->event_count_active &&!) ( Flags & Evloop_nonblock) {Timeout_next (base, &tv_p);//The maximum wait time for IO multiplexing is calculated based on the timer event} else {/* If there are still active events, it is not processed immediately. Instead, the time will be 0, let epoll_wait immediately return, follow the previous process continue to walk */evutil_timerclear (&TV);} .../* This calls the I/O multiplexed drive, which is equivalent to epoll_wait in Epoll, if this function returns, indicates that there is an event that needs to be handled waiting for this I/o ready */res = Evsel->dispatch (base, Evbase, tv_p) .../* The use of a small top heap as a cycle time is the key to fusing timed events into the I/O mechanism, where appropriate timeout events are added to the activation queue */timeout_process (base);/* Based on the number of active events (event_count_active), the function */event_process_active (base) that handles the event */if (base->event_count_active) {/*; Base->event_count_active && (Flags & evloop_once)) done = 1;} else if (Flags & evloop_nonblock) done = 1;} ......}
The Timeout_next () function calculates the wait time based on the event with the minimum timeout value in the heap and the current time
static int Timeout_next (struct event_base *base, struct timeval **tv_p) { struct timeval now; struct event *ev; struct Timeval *tv = *tv_p;<pre name= "code" class= "CPP" >
The first element of the heap has a minimum time-out value if (ev = Min_heap_top (&base->timeheap)) = = NULL) {//If there is no timed event, the wait time is set to NULL, which indicates blocking until an I/O event occurs *TV _p = NULL; return (0); }//Get current time gettime (base, &now); If the timeout time <= the current value, cannot wait, need to return immediately if (evutil_timercmp (&ev->ev_timeout, &now, <=)) {Evutil _timerclear (TV); return (0); }//Calculate wait time = Current time-minimum time-out evutil_timersub (&ev->ev_timeout, &now, TV); return (0);}
2. Basic data structure
The data structure of the management timing event in Libevent is the small top heap, the source code is located in the file Min_heap.h, the time complexity of inserting and deleting elements into the smallest heap is O (LgN), and the minimum worth of time complexity is O (1). In addition, the heap is a fully binary tree, the basic storage method is an array
Here is the typical code logic for the insertion element of the small top heap:
heap[size++]<-new; First put to the end of the array, the number of elements +1//below is the code logic of Shift_up (), constantly adjust the new upward _child = Size;while (_child>0)//Loop {_parent<-(_child-1 )/2; Calculates the parent if (Heap[_parent].key < heap[_child].key) break; Adjust the end, jump out of the loop swap (_parent, _child); Swap parent and Child}
The libevent is optimized for insert operations:
Here is the code logic for Shift_up (), which constantly adjusts the "reserved position" of new to _hole = size; _hole is the location reserved for new, but does not immediately put new on while (_hole>0)//Loop {_parent<-(_hole-1)/2;//Calculate Parent if (heap[_ Parent].key < New.key) break; Adjust the end, jump out of the loop heap[_hole] = heap[_parent]; Adjust the parent down _hole = _parent; Adjust the _hole to _parent}heap[_hole] = new; The adjustment ends and the new is inserted into the position indicated by the _hole size++; Number of elements +1
Note: The above pseudo-code from libevent source depth analysis
3. Summary
Libevent is actually using the minimal heap to manage timed events (when timing events are rare, a linked list can be used, and Redis is doing so), and then the maximum wait time for I/O multiplexing is set with the time of the most recent timed event in the minimum heap, which enables the unification of timer events and I/O events. Thus, we have all three types of events unified into the main event loop.
Note: The Epoll in libevent1.4.12 does not provide an edge trigger, but instead uses the default horizontal trigger. In addition, I would like to say that libevent1.4.12 does not support persistent timer events. I will add this feature in my simplified version of Libevent (likevent).
Ix. Choosing the best I/O multiplexing
1. Encapsulating I/O multiplexing as an event multiplexer
As we said earlier, the Libevet itself is a typical reactor pattern, and a component in reactor mode is called the event multiplexer, which is actually the encapsulation of an I/O multiplexing. So the problem is that the I/O multiplexing mechanism provided under each system is not the same, even if there are many interfaces available in the same operating system, how can I unify these I/O multiplexing mechanisms to provide a standard event multiplexer for use by other components? In Java, we can adopt interfaces; in C + +, we can take classes that contain virtual functions. Libevent shows a method of implementing a unified interface in C, a struct with a function pointer.
The key to libevent support for multiple I/O multiplexing Technologies is the structure Eventop:
struct EVENT_OP {const char *name;//io duplicate name void * (*init) (struct event_base *);//Initialize int (*add) (void *, struct event *);//Register event Int (*del) (void *, struct event *),//delete event int (*dispatch) (struct event_base *, void *, struct timeval *);//Event distribution void (*dea Lloc) (struct event_base *, void *);//Destroy Resource/* Set if we need to reinitialize the event base */int Need_reinit;};
Each I/O multiplexing mechanism must provide the implementation of these five functions to complete its own initialization, destruction, the registration of events, logoff and distribution, the following epoll as an example
static void *epoll_init (struct event_base *), static int epoll_add (void *, struct event *), static int Epoll_del (void *, str UCT event *), static int epoll_dispatch (struct event_base *, void *, struct timeval *), static void Epoll_dealloc (struct eve Nt_base *, void *), const struct EVENT_OP epollops = {"Epoll", Epoll_init,epoll_add,epoll_del,epoll_dispatch,epoll_ dealloc,1/* Need reinit */};
The implementation of Epollops and Epoll to five function interfaces is defined in the Epoll.c file, which is invisible to the outside, thus realizing the information hiding.
2, libevent How to choose the best I/O multiplexing mechanism
This can be broken down into two questions:
(1) How to know which I/O multiplexing mechanisms are available
Libevent is compiled with Autotools, and her compile script configure detects the API provided in the system when executed and generates a header file that holds the test results (for example, if the test learns that there is epoll in the system, a macro is added to the header file that holds the test results. That is, # define Have_epoll). In the same vein, we can know which I/O multiplexing mechanisms are available.
(2) How to choose the optimal I/O multiplexing mechanism
The only event multiplexer instance that will be used by Base->evbase is stored in libevent, but if there are multiple I/O multiplexing mechanisms in the system, we have multiple event multiplexer instances before initializing the base->evbase. Which of the base->evbase should be chosen to initialize it? The answer is to sort all the I/O multiplexing mechanisms by performance, and then place the corresponding event multiplexer instance into an array by performance from low to high, assigning all event multiplexer instances in the array from front to back to Base->evbase so that the last base- An event multiplexer instance implemented by the optimal I/O multiplexing mechanism is stored in the >evbase.
The following is the core code to implement this mechanism:
static const struct EVENT_OP *eventops[] = {#ifdef have_event_ports&evportops, #endif #ifdef have_select& Selectops, #endif #ifdef have_poll&pollops, #endif #ifdef have_epoll&epollops, #endif #ifdef have_working_ Kqueue&event_op kqops, #endif #ifdef have_devpoll&event_op devpollops, #endif #ifdef win32&struct event_op Win32ops, #endifNULL};const struct Event_op epollops = {"Epoll", Epoll_init,epoll_add,epoll_del,epoll_dispatch,epoll_ dealloc,1/* Need reinit */};base->evbase = null;/* Find the appropriate I/O multiplexing mechanism, as explained here, The I/O mechanism used by the Libevent library is determined at compile time in fact the relationship between Evbase and a reuse mechanism is like the relationship between classes and objects, and the reuse mechanism is the implementation of evbase */for (i = 0; Eventops[i] &&! base->evbase; i++) {/*eventops is a global structure with several I/O multiplexing mechanisms supported by different kernels */base->evsel = eventops[i];/* Note: A very important function is called here base-> Evsel->init (base), using this callback function to initialize the nested information, if using the epoll_wait multiplexing mechanism, the most important of this callback function is the Epoll_create function .... *//* The initialization in the callback function is to do some operations related to the system call, note that the return value of the callback function is a void* but this pointer is a bridge with Epoll_wait Unicom, the return is eventops[i] This struct pointer */base->evbase = Base->evsel->init (base);}
3. Summary
Libevent supports a variety of I/O multiplexing mechanisms through function pointers, as well as the application of conditional compilation in C, where we can actually change the configure generated header file to manually select which I/O multiplexing mechanism to use. Later, I will provide a simplified libevent for Linux (likevent) for your reference.
Nine, time cache and correction
1. Principle
If the system supports monotonic time, which is the time of the system from boot to present, it is not necessary to perform the correction (in the final analysis, because the user cannot change the monotonic time manually), as stated in Libevent source depth profile. Otherwise, a time check should be performed in the event loop. Why You think about it, if you add a timer event, ready for two hours after processing (perhaps the music called you get up), the result is a guy prank put system time forward for one hours, and then the computer put the music time has passed 3 hours, today's salary estimated buckle almost, haha. In this case, libevent can help you deal with it. However, it is important to note that if the guy also understands Libevent's principles (and your system does not support monotonic time), he may turn the system back on for 1 hours, this time libevent can not help you, may not sleep, the music rang. So, how does the libevent handle the system time being moved forward this situation? (Of course, if your system supports monotonic time, libevent won't be so paranoid.)
In answer to the above question, we have to answer a question is why the server generally do the time cache, need to directly from the system to take it? Efficiency. The server now requires a high level of efficiency, and then the system call is a very CPU-intensive behavior that accompanies the switch between user space and kernel space. So we cache the time in the user space, most of the time, we need to read the time cache, wait for the appropriate time to update the time cache. Libevent is in the efficiency of the consideration, also used the time buffer mechanism.
Libevent the time cache is updated when a single loop in the event loop ends. The time cache Tv_cache is placed in the base object, and there is a copy of the time cache in the base object Event_tv, and the time to follow the new copy is a single time before the next cycle begins (note: a single and next two loops are just the same). In the new time cache with the copy EVENT_TV (time cache Tv_cache is already the current time), if everything is normal, before the update event_tv, should not be event_tv<=tv_cache (because then, EVENT_TV represents the past, And Tv_cache says now, if, Event_tv>tv_cache, that time is back, a wonderful fantasy. , Libevent will understand that the system time must have been secretly modified by which guy. Therefore, libevent based on the time difference between the EVENT_TV and tv_cache to adjust the timing of each timer event time (libevent default is the system time has not been adjusted forward event_tv-tv_cache time). In this way, there is no need to worry about the system time no small partner malicious forward. However, the system time back tune how to do, libevent can only show very helpless.
2, the following is the core code of time verification
Static Voidtimeout_correct (struct event_base *base, struct timeval *tv) {struct event **pev;unsigned int size;struct Timev Al off;if (use_monotonic)//have monotonic time support, is so capricious return;/* Check if times is running backwards *//*TV <-----tv_cache*/ GetTime (base, TV);/* Original EVENT_TV should be less than Tv_cache if tv< EVENT_TV indicates that the user has adjusted the time forward, need to correct */if (EVUTIL_TIMERCMP (TV, &base- >EVENT_TV, >=)) {base->event_tv = *tv;return;} Event_debug ("%s:time is running backwards, corrected", __func__);/* Calculates the time difference */evutil_timersub (&BASE->EVENT_TV , TV, &off);/* * We can modify the key element of the node without destroying * The key, beause We apply it to all in the right order. *//////* Adjust the small top heap of timed events */pev = Base->timeheap.p;size = Base->timeheap.n;for (; size--> 0; ++pev) {struct Timeval *ev_tv = & (**pev). Ev_timeout;evutil_timersub (Ev_tv, &off, EV_TV);} /* Now remember what's the new time turned out to be. *//* update event_tv for tv_cache*/base->event_tv = *TV; }
Summary: The general server will have time to cache this mechanism, libevent such a high-performance-targeted library of course, is no exception. In addition, Libevent can find out whether the system time has been moved forward, thus adjusting the time heap.
If the system time is adjusted backwards, then libevent will not play.
Libevent Summary (bottom)