Libev Design Ideas
After clearing the Libev code structure and the main data structure, you can follow the example of the interface into the Libev, followed by the code to understand its design ideas. Here we call struct ev_loop
the various watcher as event monitors as event loop drives.
1. Analyzing IO events in the example
Here in the previous example, we first commented out the use of timers and signal events, looking only at the IO Event Monitor to understand the most basic logic of Libev. You can combine GDB to set breakpoints step-by-step to see what the logic of the code is.
Let's start with the main step. The struct ev_loop *main_loop = ev_default_loop(0);
main logic of the first execution of a follow-up code can be followed by the function, which is that the ev_default_loop
global object pointer ev_default_loop_ptr if it is empty, that is, when no prefabricated drive is used, let him point to the global object default_loop_struct , and the name "loop" is used uniformly in this function to represent the pointer of the prefabricated drive. This is EV_P
in conjunction with the function arguments and EV_A
the notation. The pointer loop_init
is then manipulated to initialize the pre-fabricated event driver. The invocation of the function here is used to EV_A_
simplify the notation. After initialization, if the Libev supports sub-processes in the configuration, the child process Monitor is implemented through the signal monitor. Here you can not take care of him, know this code function can be. Here again Libev the function definition, will see "Ev_throw" this thing, here can not control it, he is the CPP "Try ... THROW" support, and EV_CPP(extern "C" {)
such unusual extern "C" is a coding technique. Now we take the analysis design thinking as the main. After understanding the overall, it is possible to comb their coding skills. Otherwise, looking at a piece of code can be very laborious and slow. Even sometimes these "hacker" are not necessarily beneficial.
1.1 Initialization of the drive
Let's see what happens during the initialization of the drive. The first piece of code initially determines whether the system's Clock_gettime supports Clock_realtime and clock_monotonic. The difference between these two times is that the latter will not be modified because the system time has been modified, the detailed explanation can refer to the man page. The impact of the environment variable on the drive is then judged, which is mentioned in the official manual, which is mainly affected by the default supported IO multiplexing mechanism. Then there is the assignment of a series of initial values, starting with the need to understand its effect. Can be known in the subsequent analysis process. Then, according to the system supported IO multiplexing mechanism, it is initialized operation. Here you can go to "ev_epoll.c" and "ev_select.c". The last is to judge if the system needs a signal event, then through a pipe IO event to achieve, here for the moment not to worry about him, after understanding the implementation of IO event, naturally know what he did here.
For the "ev_epoll.c" and "ev_select.c" in the nature of the same xxx_init
, just like a plug-in, followed by a format, and then can be flexibly extended. For Epoll the main is to do a epoll_create* operation (Epoll_create1 can support Epoll_cloexec).
backend_mintime = 1e-3; /* epoll does sometimes return early, this is just to avoid the worst */backend_modify = epoll_modify;backend_poll = epoll_poll;
Here can be seen as the template of the plugin, in the subsequent changes when the call Backend_modify in poll when the call Backend_poll. thus unified operation.
64; /* initial number of events receivable per poll */epoll_events = (struct epoll_event *)ev_malloc (sizeof (struct epoll_event) * epoll_eventmax)
This is seen as a specific part of each mechanism. If you are familiar with Epoll, this is needless to say.
For select (on Linux platforms)
backend_mintime = 1e-6;backend_modify = select_modify;backend_poll = select_poll;
The same as above, is equivalent to the plug-in interface
vec_ri = ev_malloc (sizeof (fd_set)); FD_ZERO ((fd_set *)vec_ri);vec_ro = ev_malloc (sizeof (fd_set));vec_wi = ev_malloc (sizeof (fd_set)); FD_ZERO ((fd_set *)vec_wi);vec_wo = ev_malloc (sizeof (fd_set));
Similarly, this is a select-specific, read-and-write Fd_set Vector,ri that is used to fit the selected part of a select return. Other such as poll, Kqueue, and Solaris ports are similar and can be read by themselves.
Initialization of the 1.2IO monitor
The above procedure executes the ev_default_loop process, and then to the back ev_init(&io_w,io_action);
, he is not a function, but a macro definition:
((ev_watcher *)(void *)(ev))->active = ((ev_watcher *)(void *)(ev))->pending = 0;ev_set_priority ((ev), 0);ev_set_cb ((ev), cb_);
Although there are two function calls here, it is well understood that the "active" in the base class described earlier indicates whether the Watcher is activated, "pending" whether the monitor is in pending state, "priority" Its priority and the callback function of the action that is executed after the trigger.
1.3 Setting trigger conditions for IO Event monitor
After the monitor is initialized, the conditions for its monitoring and monitoring are also set. Triggers the triggered action that is registered on the monitor when the condition is satisfied. ev_io_set(&io_w,STDIN_FILENO,EV_READ);
from the parameter side you can guess what he did. is to set the monitor to monitor the read events on the standard input. The call is also a macro definition:
(ev)->fd = (fd_); (ev)->events = (events_) | EV__IOFDSET;
is to set the variable FD and events, which are specific to the derived class IO monitor, to monitor the readable or writable event on which the file FD has been read. %todo: Complement the role of Ev_iofdset
1.4 Registering the IO Monitor on the event drive
Once the monitor is ready, register it on the event drive, creating a complete event-driven model. ev_io_start(main_loop,&io_w);
. This function will see a macro "Ev_frequent_check" for the first time, is the function "Ev_verify" call, then ev_verify is what? In the words of the document "This can is used to catch bugs inside Libev itself", if you look at its code, it is to detect the internal data structure of Libev, determine whether the boundary value is reasonable, unreasonable when the assert off. In the production environment, I feel that according to the disposition to treat. If you think he consumes resources (to detect a lot of things running a lot of loops), you can turn off the definition when compiling. If you need an assert, you can add options when compiling.
And then see the ev_start
call, the function is actually to give the drive loop->activecnt add loop->active to True (here Unified loop represents the global object of the prefabricated drive object default_loop_struct), They indicate, respectively, the number of monitors being monitored on the event drive and whether they are serving the monitor.
array_needsize (ANFD, anfds, anfdmax, fd + 1, array_init_zero);wlist_add (&anfds[fd].head, (WL)w);
Interested can go to see the Libev the implementation of the dynamic adjustment array. Here we mainly look at the overall logic. His work is to determine whether the array Anfds has space to add to the file descriptor FD monitoring, and if not, adjust the memory size of the array so that it is large enough to accommodate.
Here to introduce a data structure that was not introduced before, this is not context-sensitive, so it is presented here.
typedef struct{WL head; unsigned char events; /* the events watched for */unsigned char reify; /* flag set when this ANFD needs reification (ev_anfd_reify, Ev__iofdset) */unsigned char emask; /* the Epoll backend stores the actual kernel mask in here */unsign Ed char unused; unsigned int egen; /* generation counter to counter epoll bugs */} ANFD; /* here removed the judgment on Epoll and Windows iocp*/
The first thing to do here is to focus on a "head", which is a list of the wather that was previously mentioned. Here a ANFD represents the monitoring of a file descriptor, then the description of the file is readable or writable monitoring, how the monitoring action is defined, that is, through the list, the file description of the monitor to hang up, so that can be found through the file descriptor. The previous Anfds is an array of this object, and the subscript is indexed by the file descriptor FD. It has been discussed in Redis-ae that the index speed of O (1) can be reached and the footprint is reasonable.
Then the "Fd_change" and "fd_reify" are echoed. The former adds FD to an array of fdchanges, while the latter iterates through the watcher on the FD in the array and compares the Watcher in the Anfds to determine if the monitoring condition has changed, and if the change is called Backend_ Modify is the Epoll_ctl and other adjustment system to the FD monitoring. This is what this fdchanges array does, and he records the file descriptors that the watcher monitoring condition in the Anfds array may be modified and, when appropriate, modifies the system monitoring conditions by invoking the epoll_ctl of the system or other file reuse mechanisms. Here we comb through these two major physical structures:
To summarize the registration process is to set the monitoring condition IO watcher to obtain the monitoring of the file descriptor FD, find its corresponding ANFD structure in the Anfds, the Watcher is attached to the structure of the head chain. Since the monitoring conditions of the FD should be changed, the FD is recorded in the Fdchanges array, and the interface of the system is modified to monitor the condition of the FD in the subsequent steps.
1.5 Boot Event Drive
Everything is ready to start the thing drive. It is ev_run
. The logic is clear. It is
do{ xxxx; backend_poll(); xxxx}while(condition_is_ok)
Start a paragraph in the Loop and fork, prepare related this is skipped directly, to analyze the related monitoring events before going to see him. directly to /* calculate blocking time */
here. Familiar with the event model, this is still more conventional. is to obtain the most recent time from the timer heap (of course, there is no timer at the time of analysis) compared with loop->timeout_blocktime to get blocking time. If you set the io_blocktime of the drive, you will sleep Io_blocktime time before entering poll to wait for IO or other event preparation to be monitored. The blocking time that goes into Backend_poll is included in the Io_blocktime time. Then enter into the Backend_poll. For Epoll is to enter into the epoll_wait inside.
After Epoll (or select, Kqueue, etc.) returns, it will monitor the file descriptor in FD and its pending (satisfy the monitoring) condition by fd_event
making a monitoring condition whether the change is judged by the fd_event_nocheck
inside of the ANFDS[FD] The Hanging Monitor on the FD in the array is checked sequentially, if the pending condition is met, by ev_feed_event
adding the monitor to the position of pendings[pri][old_lenght+1] on Pendings[pri] in the pendings array. To introduce a new data structure, he says that the wather in pending is the monitoring condition, but there is no state to trigger the action.
typedef struct{ W w; int events; /* the pending event set for the given watcher */} ANPENDING;
This W w
should be known as the base class pointer that was previously said. Pendings is a two-dimensional array of this type. It takes the priority of watcher as the primary subscript. The number of pengding on the priority level is two subscript, and the pending value in the corresponding monitor is the result of the subscript plus one. It is defined as ANPENDING *pendings [NUMPRI]
. As with Anfds, the second dimension of a two-dimensional array ANPENDING *
is a dynamically sized array. After this operation. This series of operations can be considered as a follow-up operation of Fd_feed, xxx_reify purpose is to add pending watcher to this pengdings two-dimensional array. The following several xxx_reify are also the same, such as the analysis to that type of monitor type when the expansion. Here we use a figure to comb down the structure.
Finally, the execution of the macro in the loop EV_INVOKE_PENDING
, is actually called LOOP->INVOKE_CB, if there is no custom modification (generally not modified) is called ev_invoke_pending
. The function iterates through the two-dimensional array pendings, executing the trigger action callback function on each watcher of the pending.
This time the IO trigger process is complete.
2 summarize the design idea of Libev
In the Libev watcher to calculate the most critical data structure, the entire logic is around the watcher do the operation. Libev internally maintains a base class Ev_wathcer and a derived class ev_xxx for several specific monitors. When using, first generate an instance of a specific watcher. and set its trigger condition through the private members of the derived object. These watchers are then managed with Anfds or minimal heap. Then libev the watcher of pending through Backend_poll and time heap management. They are then added to a two-dimensional array with precedence as a one-dimensional subscript. The triggering action callback functions registered on the watcher of these pengding are invoked at the appropriate time, so that the priority model of "only-for-ordering" can be implemented in order of precedence.
The other two important monitors
The entire workflow of Libev is passed through the IO Monitor in front. The middle filter has a lot to do with other event monitors, but the overall idea is clear, as long as you look at the initialization and registration process for other types of watcher and the arrangements in Ev_run. Here we analyze the other two commonly used watcher
1. Analysis Timer Monitor
Timer in the program can do fixed-cycle tick operation, can also do a one-time timing operation. There is also a periodic event watcher similar to the timer in the Libev. Its essence is the same, just slightly different in the computational method of time, and has his own heap of event management. For timer events, we start with the ev_init in the order we said before.
1.1 Initialization of the timer monitor
Timer initialization is used ev_init(&timer_w,timer_action);
, this process is similar to the previous IO, mainly to set the base class of active, pending, priority and trigger action callback function CB.
1.2 Setting the trigger condition of the timer monitor
By ev_timer_set(&timer_w,2,0);
Setting the timer to be triggered after 2 seconds. If the third parameter is not 0 but a positive integer n greater than 0, the timer event is triggered again every n seconds after the first trigger (2 seconds).
It is defined as a macro that do { ((ev_watcher_time *)(ev))->at = (after_); (ev)->repeat = (repeat_); } while (0)
sets the "at" of the derived class timer watcher as the trigger event, and the repeating condition "repeat".
1.3 Registering the timer on the event drive
ev_timer_start(main_loop,&timer_w);
Registers the timer monitor with the event drive. It first ev_at (w) += mn_now;
gets the next time, so put in the time management heap "timers" as a weight. Then modify the status of the drive loop with the previously mentioned "Ev_start". Here again we see an array of dynamic sizes. The memory management of Libev's heap is also through this relationship. Concrete here the heap of realization, interested can be carefully read the implementation. The operation here is to put this time weight in the right place in the heap. The structure of the heap units here is:
typedef struct { ev_tstamp at; WT w;} ANHE;
The essence is a moment at which a timer watcher is hung on the list. The trigger callback function on the timer watcher is executed sequentially when the timeout occurs.
1.4 Trigger of Timer monitor
Finally look at how the timer monitor is handled in an event drive loop. Here we still leave aside the other parts, only looking for timer-related look. In the "/ calculate blocking time/" block, we see that the calculation of the blocking is first:
if (timercnt) { ev_tstamp to = ANHE_at (timers [HEAP0]) - mn_now; if (waittime > to) waittime = to;}
If there is a timer, then the minimum time of the heap overhead is obtained from the timers of the timer heap (a minimum heap). This ensures that you can come out of the backend_poll before this time. Out after timers_reify
the execution of the pengding timer will be processed.
In the top of the timers_reify
heap that takes the smallest heap in sequence, If the anhe.at on it is less than the current time, indicating that the timer watcher timed out, then it is pressed into an array, because in the actual execution of pendings two-dimensional array on the corresponding priority on the Watcher is from the tail head direction, so here first with an array of time successively saved to an intermediate array loop- The >rfeeds. The reverse call is then ev_invoke_pending
inserted into the pendings two-dimensional array. This ensures that when the trigger action of the pending event is executed, the time-dependent timer is first performed. The feed_reverse
function feed_reverse_done
is to add the timed-out timer to the Loop->rfeeds staging array and to insert the Watcher of the pending in the staging array into the pengdings array. Add the pending watcher to the Pendings array, and the subsequent operations will be the same as before. Execute the corresponding callback function back in turn.
This process also determines the value of the timer's w->repeat, if not 0, resets the timer's time and presses it into the correct position in the heap so that it will be executed after the specified time. If it is 0, then ev_timer_stop
the call closes the timer. It begins by clear_pending
placing a callback function on the watcher that is recorded in the pendings array as a dummy action that does not perform any action.
Summing up the timer is before backend_poll through the timer heap top timeout time, to ensure that the blocking time does not exceed the most recent timer time, after Backend_poll return, The watcher that gets the timeout from the timer heap is placed into the pendings two-dimensional array so that the triggering action on it can be executed on subsequent processing. Then remove the timer from the timer management heap. The last Call and ev_start
Echo ev_stop
modifies the state of the drive loop, i.e. the loop->activecnt is reduced by one. And the active zero of the Watcher is placed.
The same process is true for periodic event monitors. It's just going to be timers_reify
replaced periodics_reify
. The inside of the periodic event monitor derived class to do similar to the timer inside whether or not repeat judgment operation. Judging whether or not to re-adjust the time, or whether or not to repeat the logic, these look at the code is relatively easy to understand, here no longer repeat.
2. Analysis Signal Monitor
Analyze the part of the timer, and then look at another more commonly used signal event processing. The signal events inside the Libev are the same as the Tornado.ioloop, which is handled by a pipe IO event. Straightforward to say is to register a two-way pipe file object, and then monitor the above read event, when the corresponding signal arrives, the pipe will be written to a value of his read end of the Read event trigger, so that you can execute the corresponding registration trigger action callback function.
We are also introducing the sequence from initialize-"SET trigger condition-" Register to drive-"trigger process.
2.1 Initialization of the signal monitor
ev_init(&signal_w,signal_action);
This is the same function as above, needless to say.
2.2 Setting the trigger condition of the signal monitor
ev_signal_set(&signal_w,SIGINT);
This function sets the trigger action callback function that Libev receives the SIGINT signal which triggers the registration. Its operation is the same as above, is set the signal monitor private (EV)->signum for the mark.
2.3 Registering the signal monitor on the drive
Here we first introduce a data structure:
typedef struct{ EV_ATOMIC_T pending; EV_P; WL head;} ANSIG;static ANSIG signals [EV_NSIG - 1];
EV_ATOMIC_T pending;
Can be thought of as an atomic object, to his reading and writing is atomic. A loop that represents the event drive, and a list of watcher.
In ev_signal_start
, the signal monitoring unit is stored through the signals array. The array is similar to the Anfds array except that he is indexed with a signal value. This allows you to immediately find where the signal is located. From Linux 2.6.27, kernel provides SIGNALFD to generate a file descriptor for the signal, which can be used to manage the signal using the file reuse mechanism Epoll, select, and so on. Libev is the way to manage the signals. The code here is controlled by a macro. The logic is roughly the same.
#if EV_USE_SIGNALFD res = invoke_signalfd# if EV_USE_SIGNALFDif (res is not valied)# endif{ use evpipe to instead}
This is the framework. Its specific implementation can refer to the use of SIGNALFD and evpipe_init
implementation. The essence is to set the Read event listener for the FD by a pipe-like file descriptor FD, and when the signal is received, it is written to the FD through the callback function registered by signal, so that its read event is triggered so that it can be processed ev_init
after Backend_poll returns. Is the trigger callback function registered on the signal.
The function evpipe_init
also uses a skill that can be learned, and the same as above #if XXX if() #endif {}
, dealing with unsupported eventfd
situations. EVENTFD is a system call supported by kernel 2.6.22 to create an event object implementation, a wait/notification mechanism between processes (threads). He maintains a file descriptor that can read and write, but writes only 8byte of content. But for our use and enough, because this is mainly to get its readable state. For unsupported eventfd
cases, use the above mentioned, with the system pipe
call generated two file descriptors to do read and write objects, to complete.
2.4 Triggering of the Signal event monitor
The IO event of the pipe that sets the signal above is, depending on the mechanism used, its implementation and triggering are somewhat different. For SIGNALFD.
ev_io_init (&sigfd_w, sigfdcb, sigfd, EV_READ); /* for signalfd */ev_set_priority (&sigfd_w, EV_MAXPRI);ev_io_start (EV_A_ &sigfd_w);
That is, the SIGFDCB function is registered. The function:
sizeof (si));for (sip = si; (char *)sip < (char *)si + res; ++sip) ev_feed_signal_event (EV_A_ sip->ssi_signo);
First, the pipe content read light, so that the subsequent can be pengding on the FD. Then all the signals on the SIGNALFD Ayung ev_feed_signal_event
bar Ansig->head on each signal are ev_feed_event
added to the pendings two-dimensional array. This process is exactly the same as the IO.
For EVENTFD and pipe, it is:
ev_init (&pipe_w, pipecb); ev_set_priority (&pipe_w, EV_MAXPRI); ev_io_set (&pipe_w, evpipe [0] < 0 ? evpipe [1] : evpipe [0], EV_READ); ev_io_start (EV_A_ &pipe_w);
The Pipe_w is the loop->pipe_w of the drive itself. and set a callback function for it PIPECB:
#if ev_use_eventfd if (Evpipe [ Span class= "Hljs-number" >0] < 0) {uint64_t counter; Read (Evpipe [1], &counter, sizeof (uint64_t));} else #endif {char dummy[4]; Read ( evpipe [0], &dummy, sizeof (dummy)); }...xxx ... for (i = ev_nsig-1; i--;) if (Expect_false (signals [i].pending)) ev_feed_signal_event (EV_A_ i + 1);
This is where the above techniques are #if XXX if() #endif {}
expanded to #if XXX if() {} else #endif {}
. This is actually the same as the above operation. Subsequent operations and SIGNALFD inside the same, is to read the contents of the pipe inside, and then sequentially add watcher to the pendings array.
Other Monitors
A few of the most important monitors have been taken care of. The other I think more can see Ev_child and Ev_stat. In fact, the same as the previous three basic principles. Not to repeat. Future may be added.
Tips in the Libev
If you use Libev as a component. Official documentation is a good choice. Here is a look at the experience of the Libev process.
If you use Libev but feel that it does not provide the necessary functionality, go to the code. Perhaps Libuv has made a good example for us. LIBUV used Libev as its underlying event library. Later, the author rewritten his own set of network library LIBUV. Strictly speaking, Libev is only an event model framework and cannot be counted as a complete network library, which is why he provides so many types of events. The most important things for the network library are timers, IO, and signal events. Of course, the network also includes sockets, transceiver control and other content. So my feeling is that Libev can be a good learning object, whether it's a design idea, a variety of tips in the code, or a way to support cross-platform. Although the use of macro-wrapped relatively tight, as long as a little analysis, clear its thinking is relatively easy.
Compare the Libev with the previous redis-ae. It can be found that Libev in the design of more complete thinking, providing services are more comprehensive, but do more testing, logic complex, consume resources must be more than a simple package. From this two model you can see that the framework of the event model is:
Get a proper time and use this time to poll. Then mark the Pending file object after poll. Poll out after the timer and then unified processing pending object
Drawing a whole structure here, not very normative UML or any other academic diagram, is just a process to help understand:
At this point Libev's analysis is almost complete, mainly to understand the realization of the idea. How to achieve and from what angle to design. The results need to be tested in a production environment.
Reprint: http://my.oschina.net/u/917596/blog/177573
Event-driven model Libev (ii)