Overview
The timer is a basic component. Whether it is user space program development or kernel space program development, a timer is often needed as the basic component, the implementation of the timer is also different. This article discusses various implementation methods of the timer at the application layer and kernel layer in Linux, the advantages and disadvantages of various implementation methods and the appropriate environment are analyzed.
First, a basic model is provided. The implementation of the timer requires the following actions. This is also a basic model for evaluating the implementation of various timers [1]:
Starttimer (interval, timerid, expiryaction)
Register a timer instance that executes expiryaction after interval. timerid is returned to distinguish other timer instances in the timer system.
Stoptimer (timerid)
Locate the registered timer instance based on the timerid and execute stop.
Pertickbookkeeping ()
In a tick, the most important action of the timer system is to check whether a timer instance has expired in the timer system. Note that the tick here actually implies a granularity concept.
Expiryprocessing ()
After the timer instance expires, execute the pre-registered expiryaction action.
The above describes the basic timer model, but there are two kinds of timers for the actual use of the basic behavior:
Single-shot Timer
This timer is executed only once from registration to termination.
Repeating Timer
The timer automatically restarts after each termination. In essence, it can be considered that the repeating timer is a single-shot timer registered in the timer system after the single-shot timer is terminated. Therefore, it is not very complex to support repeating timer based on single-shot timer.
Timer Based on linked list and signal (in kernel 2.4)
In the 2.4 kernel, POSIX timer [2] is not supported. To support multiple timers in the process environment, you can only implement the timer by yourself. Fortunately, setitimer (2) is provided in Linux). It is an interval timer, but if you want to support multiple timers in the process environment, you have to manage all the timers by yourself. Setitimer (2) is defined as follows:
Listing 1. setitimer prototype
#include <sys/time.h> int setitimer(int which, const struct itimerval *new_value,struct itimerval *old_value); |
Setitimer can automatically start itself after it expires. Therefore, it is very easy to use it to solve the single-shot timer and repeating timer problems. This function can work in three modes:
Itimer_real decreases in real time and sends sigalrm signals after expiration.
Itimer_virtual only declines when the process is executed in the user space and sends the sigvtalrm signal after the expiration.
When the itimer_prof process is executed in the user space and the kernel serves the process (for example, if a system call is completed), it will decrease, when used with itimer_virtual, it can measure the time consumption of the application in the kernel space and user space, and send a sigprof signal after expiration.
The timer value is defined by the following structure:
List 2. setitimer timer value definition
struct itimerval { struct timeval it_interval; /* next value */ struct timeval it_value; /* current value */ }; struct timeval { long tv_sec; /* seconds */ long tv_usec; /* microseconds */ }; |
Setitimer () uses new_value to set a specific timer. If old_value is not empty, it returns the previous value of the which interval timer. The timer decreases from it_value to zero, generates a signal, and reset it to it_interval. If it_interval is zero, the timer stops. At any time, the timer stops as long as it_value is set to zero.
Because setitimer () does not support multiple timers in the same process at the same time, if you need to support multiple scheduled instances at the same time, you need the implementer to manage all instances. Using setitimer () and linked list, you can construct a timer that supports multiple timer instances in the process environment. In the general implementation of pertickbookkeeping, the elapse value of each timer is increased, until the value increments to the initial set interval, the timer expires.
A linked list-based timer can be defined:
Listing 3. linked list-based timer Definition
typedef int timer_id; /** * The type of callback function to be called by timer scheduler when a timer * has expired. * * @param id The timer id. * @param user_data The user data. * $param len The length of user data. */ typedef int timer_expiry(timer_id id, void *user_data, int len); /** * The type of the timer */ struct timer { LIST_ENTRY(timer) entries;/**< list entry */ timer_id id; /**< timer id */ int interval; /**< timer interval(second) */ int elapse; /**< 0 -> interval */ timer_expiry *cb; /**< call if expiry */ void *user_data; /**< callback arg */ int len; /**< user_data length */ }; |
The time interval of the timer is expressed as interval, while the elapse Increments at pertickbookkeeping () until interval indicates that the timer is stopped. At this time, the callback function CB is called to execute related behaviors, user_data and Len are parameters that can be passed to the callback function.
All timer instances are managed by linked list:
List 4. Timer linked list
/** * The timer list */ struct timer_list { LIST_HEAD(listheader, timer) header; /**< list header */ int num; /**< timer entry number */ int max_num; /**< max entry number */ void (*old_sigfunc)(int); /**< save previous signal handler */ void (*new_sigfunc)(int); /**< our signal handler */ struct itimerval ovalue; /**< old timer value */ struct itimerval value; /**< our internal timer value */ }; |
Here, the implementation of the linked list uses a group of Macros in the BSD style about the linked list to avoid re-engineering the wheel. In this structure, old_sigfunc is used to save the system's processing function for sigalrm in the init_timer initial timer linked list. It is used to restore the previous processing function when the timer system is destory. The purpose of ovalue is similar to this.
Listing 5. Create a timer linked list and destroy
/** * Create a timer list. * * @param count The maximum number of timer entries to be supported initially. * * @return 0 means ok, the other means fail. */ int init_timer(int count) { int ret = 0; if(count <=0 || count > MAX_TIMER_NUM) { printf("the timer max number MUST less than %d.\n", MAX_TIMER_NUM); return -1; } memset(&timer_list, 0, sizeof(struct timer_list)); LIST_INIT(&timer_list.header); timer_list.max_num = count; /* Register our internal signal handler and store old signal handler */ if ((timer_list.old_sigfunc = signal(SIGALRM, sig_func)) == SIG_ERR) { return -1; } timer_list.new_sigfunc = sig_func; /*Setting our interval timer for driver our mutil-timer and store old timer value*/ timer_list.value.it_value.tv_sec = TIMER_START; timer_list.value.it_value.tv_usec = 0; timer_list.value.it_interval.tv_sec = TIMER_TICK; timer_list.value.it_interval.tv_usec = 0; ret = setitimer(ITIMER_REAL, &timer_list.value, &timer_list.ovalue); return ret; } /** * Destroy the timer list. * * @return 0 means ok, the other means fail. */ int destroy_timer(void) { struct timer *node = NULL; if ((signal(SIGALRM, timer_list.old_sigfunc)) == SIG_ERR) { return -1; } if((setitimer(ITIMER_REAL, &timer_list.ovalue, &timer_list.value)) < 0) { return -1; } while (!LIST_EMPTY(&timer_list.header)) { /* Delete. */ node = LIST_FIRST(&timer_list.header); LIST_REMOVE(node, entries); /* Free node */ printf("Remove id %d\n", node->id); free(node->user_data); free(node); } memset(&timer_list, 0, sizeof(struct timer_list)); return 0; } |
The operation of adding a timer is very simple. In essence, it is just the insertion of a linked list:
Listing 6. Add a timer to the timer linked list
/** * Add a timer to timer list. * * @param interval The timer interval(second). * @param cb When cb!= NULL and timer expiry, call it. * @param user_data Callback's param. * @param len The length of the user_data. * * @return The timer ID, if == INVALID_TIMER_ID, add timer fail. */ timer_id add_timer(int interval, timer_expiry *cb, void *user_data, int len) { struct timer *node = NULL; if (cb == NULL || interval <= 0) { return INVALID_TIMER_ID; } if(timer_list.num < timer_list.max_num) { timer_list.num++; } else { return INVALID_TIMER_ID; } if((node = malloc(sizeof(struct timer))) == NULL) { return INVALID_TIMER_ID; } if(user_data != NULL || len != 0) { node->user_data = malloc(len); memcpy(node->user_data, user_data, len); node->len = len; } node->cb = cb; node->interval = interval; node->elapse = 0; node->id = timer_list.num; LIST_INSERT_HEAD(&timer_list.header, node, entries); return node->id; } |
The registered signal processing function is used to drive the timer system:
Listing 7. Signal Processing Function-driven Timer
/* Tick Bookkeeping */ static void sig_func(int signo) { struct timer *node = timer_list.header.lh_first; for ( ; node != NULL; node = node->entries.le_next) { node->elapse++; if(node->elapse >= node->interval) { node->elapse = 0; node->cb(node->id, node->user_data, node->len); } } } |
When receiving the sigalrm signal, it executes the auto-increment operation of each timer elapse in the timer linked list and compares it with interval. If it is equal, it indicates that the registered timer has timed out, in this case, the registered callback function is called.
The above implementation has many optimizations: consider another way of thinking, in the timer system, convert the Relative Interval of maintenance to absolute time, so that each pertickbookkeeping, you only need to compare the current time with the absolute time of the timer to know whether the timer has expired. This method changes incremental operations to comparison operations. In addition, the preceding implementation method is inefficient. When starttimer, stoptimer, and pertickbookkeeping are executed, the algorithm complexity is O (1), O (N), O (N ), you can make a simple improvement on the above implementation, in starttimer
When a timer instance is added, the linked list is sorted. This improves the complexity of the algorithms when starttimer, stoptimer, and pertickbookkeeping are executed ), O (1), O (1 ). The improved timer system is shown in Figure 1:
Figure 1. Sort linked list-based Timer
Implementation Based on Kernel timer 2.6 (POSIX real-time timer)
Linux has started to support the timer defined by POSIX timer [2] Since 2.6. It mainly consists of the following interfaces:
Listing 8. POSIX timer Interface
#include <signal.h> #include <time.h> int timer_create(clockid_t clockid, struct sigevent *evp, timer_t *timerid); int timer_settime(timer_t timerid, int flags, const struct itimerspec *new_value, struct itimerspec * old_value); int timer_gettime(timer_t timerid, struct itimerspec *curr_value); int timer_getoverrun(timer_t timerid); int timer_delete(timer_t timerid); |
This interface is used to provide better real-time support for the operating system. You must specify-LRT when linking.
Timer_create (2): Creates a timer.
Timer_settime (2): start or stop a timer.
Timer_gettime (2): returns the remaining time value from the next expiration date and the time interval defined by the timer. The reason for this interface is that if the user defines a 1 ms timer, the system load may be very heavy at that time, resulting in the timer to time out after 10 ms. In this case, overrun = 9 ms.
Timer_getoverrun (2): returns the threshold value of the last time the timer expires.
Timer_delete (2): Stop and delete a timer.
The most important interface above is timer_create (2). clockid indicates the clock type to be used. In POSIX, clock_realtime must be implemented. The EVP parameter specifies the method in which the caller is notified after the scheduled expiration. The struct is defined as follows:
Listing 9. Signal and event definitions in the POSIX timer Interface
union sigval { int sival_int; void *sival_ptr; }; struct sigevent { int sigev_notify; /* Notification method */ int sigev_signo; /* Timer expiration signal */ union sigval sigev_value; /* Value accompanying signal or passed to thread function */ void (*sigev_notify_function) (union sigval); /* Function used for thread notifications (SIGEV_THREAD) */ void *sigev_notify_attributes; /* Attributes for notification thread (SIGEV_THREAD) */ pid_t sigev_notify_thread_id; /* ID of thread to signal (SIGEV_THREAD_ID) */ }; |
Sigev_notify indicates the notification method:
Sigev_none
When the timer expires, no asynchronous notification is sent, but the running Progress of the timer can be monitored using timer_gettime (2.
Sigev_signal
When the timer expires, the signal specified by sigev_signo is sent.
Sigev_thread
When the timer expires, a new thread starts with sigev_policy_function. This function uses sigev_value as its parameter. When sigev_policy_attributes is not empty, the attributes of this thread are formulated. Note that due to the special nature of threads on Linux, this function is actually implemented by glibc along with the kernel.
Sigev_thread_id (Linux-specific)
It is only recommended for implementing the thread library.
If EVP is null, the function is equivalent to: sigev_notify = sigev_signal, sigev_signo = sigvtalrm, sigev_value.sival_int = timer ID.
Since the POSIX timer [2] interface supports multiple timer instances in a process at the same time, the preceding pertickbookkeeping action based on setitimer () and linked list is maintained by the Linux kernel, this greatly reduces the timer implementation burden. Since the POSIX timer [2] interface has more control capabilities when the timer expires, you can use real-time signals to avoid signal loss and specify the sigev_value.sival_int value as the timer ID, in this way, you can manage multiple timers together. Note that POSIX Timer
[2] APIs are meaningful only in the process environment (Fork (2) and exec (2) also need special treatment) and are not suitable for multithreading environments. Similarly, Linux provides related timer Interfaces Based on file descriptors:
Listing 10. file descriptor-based timer interface provided by Linux
#include <sys/timerfd.h> int timerfd_create(int clockid, int flags); int timerfd_settime(int fd, int flags, const struct itimerspec *new_value, struct itimerspec *old_value); int timerfd_gettime(int fd, struct itimerspec *curr_value); |
In this way, based on the file descriptor, this interface can support asynchronous interfaces such as select (2) and Poll (2), making the implementation and use of the timer more convenient. More importantly, supports the semantics of multiple processes such as fork (2) and exec (2). Therefore, they can be used in multi-threaded environments. They are more flexible than POSIX timer [2, the root cause is that the timer management is unified under one of the basic Unix/Linux philosophy-"Everything is a file.
Minimum heap implementation Timer
The minimum heap refers to the heap that satisfies that each node except the root node is not smaller than its parent node. In this way, the minimum value in the heap is stored in the root node. In the subtree with a node as the root, the values of each node are not smaller than those of the Child root node. Example of a minimum heap:
Figure 2. Minimum heap
A minimum heap generally supports the following operations:
Insert (timerheap, timer): inserts a value in the heap and maintains the minimum heap nature. Specifically, the timer is inserted into the timer heap. According to the insert algorithm analysis of the minimum heap, the time complexity of this operation is O (lgn ).
Minimum (timerheap): gets the medium and minimum values of the smallest heap. In the timer system, it is the most likely to terminate the timer in the returned timer heap. Because it is the smallest heap, you only need to return the heap root. The algorithm complexity is O (1 ).
Extractmin (timerheap): After the timer expires, it executes the relevant action. Its algorithm complexity is O (1 ).
The minimum heap is essentially a minimum priority queue (min-priority queue ). Timing can be used as an application of the minimum priority queue. This priority queue converts the timer time interval value to an absolute time for processing. The extractmin operation is performed in all the waiting timers, find the timer that times out first. At any time, a new timer instance can be added to the timer queue through the insert operation.
In the Basic Library pjlib of the pjsip project, there is a timer based on the minimum heap implementation. It mainly provides the following interfaces:
Listing 10. Minimum heap-based timer interface provided by pjlib
/** * Create a timer heap. */ PJ_DECL(pj_status_t) pj_timer_heap_create( pj_pool_t *pool, pj_size_t count, pj_timer_heap_t **ht); /** * Destroy the timer heap. */ PJ_DECL(void) pj_timer_heap_destroy( pj_timer_heap_t *ht ); /** * Initialize a timer entry. Application should call this function at least * once before scheduling the entry to the timer heap, to properly initialize * the timer entry. */ PJ_DECL(pj_timer_entry*) pj_timer_entry_init( pj_timer_entry *entry, int id, void *user_data, pj_timer_heap_callback *cb ); /** * Schedule a timer entry which will expire AFTER the specified delay. */ PJ_DECL(pj_status_t) pj_timer_heap_schedule( pj_timer_heap_t *ht, pj_timer_entry *entry, const pj_time_val *delay); /** * Cancel a previously registered timer. */ PJ_DECL(int) pj_timer_heap_cancel( pj_timer_heap_t *ht, pj_timer_entry *entry); /** * Poll the timer heap, check for expired timers and call the callback for * each of the expired timers. */ PJ_DECL(unsigned) pj_timer_heap_poll( pj_timer_heap_t *ht, pj_time_val *next_delay); |
The timer in pjlib implements heap by using arrays internally, which makes the use of memory space more compact; its implementation can also increase the maximum number of timers when the number of timers exceeds the preset maximum number. The pjlib/src/pjlib-test/Timer. c file is a unit test of pjlib. Compared with the implementation based on the linked list method, the time complexity is obviously lower, so that more timer instances can be supported.
Timer implemented based on the timing-wheel Method
The time wheel (timing-wheel) algorithm is similar to a revolver that rotates at a constant speed. The Pin of a gun hits the gun. If there is a bullet in the gun, the gun will be shot; correspondingly, for pertickbookkeeping, the most essential task is to increase the clock in units of tick. If any timer expires, the corresponding expiryprocessing is called. Set a cycle to N tick units. The current time is to point to the element I (I> = 0 and I <= n-1) After s cycles ), the current time tc can be expressed as: TC = S * n
+ I; if a timer with the time interval Ti is inserted at this time, it is set to be placed into the element N (next), then n = (TC + Ti) MOD n = (S * n + I + Ti) mod n = (I + Ti) mod n. If the N value is large enough, it is clear that when starttimer, stoptimer, and pertickbookkeeping, the algorithm complexity is O (1), O (1), O (1) respectively ). In [5], a timer for implementing a simple timer wheel is provided. 3 is a simple time wheel Timer:
Figure 3. Simple time Wheel
If the range of the timer to be supported is very large, the above implementation method cannot meet this requirement. This will consume a considerable amount of memory. Assuming that the timer range to be expressed is 0-2 ^ 3-1ticks, a simple time wheel requires 2 ^ 32 element space, the memory space is very large. It may reduce the timer precision, so that each tick represents a longer time, but the price is that the timer precision will be greatly reduced. Now the question is, can only the unique granularity be used to measure the timer granularity? Think about the water meters that we often encounter in daily life, such as 4:
Figure 4. Water Meter
In the above water meter, in order to represent the measurement range, it is divided into different units, such as 1000,100, 10, and so on. Similarly, it indicates a 32 bits range, it does not need an array of 2 ^ 32 elements. In fact, the Linux kernel divides the timer into five groups. The granularity of each group is expressed as: 1 jiffies, 256 jiffies, 256*64 jiffies, 256*64*64 * jiffies, 256*64*64*64 jiffies. The number of buckets in each group is, 64, 64, and 64, in 256 + 64 + 64 + 64 + 64 = 512 buckets, the range is
2 ^ 32. With this implementation, the driver kernel timer mechanism can also be understood through the water meter example, just like the water meter, each granularity has a pointer pointing to the current time, the time increments with a fixed tick, and the current time pointer increments sequentially. If you find that the position of the current pointer can be determined as a registered timer, the registered callback function is triggered. The Linux kernel timer is essentially a single-shot timer. If you want to become a repeating timer, you can register yourself again in the registered callback function. Kernel timer such as 5:
Figure 5. Linux time Wheel
Conclusion
From the above analysis, we can see the complexity of various timer implementation algorithms:
Table 1. complexity of timer Algorithm Implementation
Implementation Method |
Starttimer |
Stoptimer |
Pertickbookkeeping |
Linked list-based |
O (1) |
O (N) |
O (N) |
Sort linked list |
O (N) |
O (1) |
O (1) |
Minimum heap |
O (lgn) |
O (1) |
O (1) |
Based on Time Wheel |
O (1) |
O (1) |
O (1) |
If you need a timer that can be used in the thread environment, you may need to be very careful with the signal problem for the linked list-based timer, while the POSIX timer [2] interface timer, only process semantics is available. If you want to use N in a multi-threaded environment, you can use the timerfd_create (2) interface provided by Linux. If you need to support a very large number of timers, you can consider using the method based on the minimum heap and time wheel.
Http://www.ibm.com/developerworks/cn/linux/l-cn-timers/#ibm-pcon