概述
linux timer
1、数据结构
1.1 timer_list
struct timer_list {
struct hlist_node entry;
unsigned long expires;
void (*function)(struct timer_list *);
u32 flags;
#ifdef CONFIG_LOCKDEP
struct lockdep_map lockdep_map;
#endif
};
- entry:定时器保存到哈希表中的节点;
- expires:定时器到期时刻。
- function:到期时执行的回调函数。
- flags:标志位。
#define TIMER_CPUMASK 0x0003FFFF
#define TIMER_MIGRATING 0x00040000
#define TIMER_BASEMASK (TIMER_CPUMASK | TIMER_MIGRATING)
#define TIMER_DEFERRABLE 0x00080000
#define TIMER_PINNED 0x00100000
#define TIMER_IRQSAFE 0x00200000
#define TIMER_ARRAYSHIFT 22
#define TIMER_ARRAYMASK 0xFFC00000
最高 10 位记录了定时器放置到桶的编号,后面会提到一共最多只有576个桶,所以10位足够了。而最低的18位指示了该定时器绑定到了哪个CPU上,注意是一个数值,而不是位图。夹在中间的一些位到真的是一些标志位。TIMER_MIGRATING表示定时器正在从一个CPU迁移到另外一个CPU。TIMER_DEFERRABLE表示该定时器是可延迟的。TIMER_PINNED表示定时器已经绑死了当前的CPU,无论如何都不会迁移到别的CPU上。TIMER_IRQSAFE表示定时器是中断安全的,使用的时候只需要加锁,不需要关中断。
1.2 timer_base
系统中可能同时存在成千上万个定时器,如果处理不好效率会非常低下。Linux目前会将定时器按照绑定的CPU和种类(普通定时器还是可延迟定时器两种)进行区分,由timer_base结构体组织起来:
struct timer_base {
raw_spinlock_t lock;
struct timer_list *running_timer;
#ifdef CONFIG_PREEMPT_RT
spinlock_t expiry_lock;
atomic_t timer_waiters;
#endif
unsigned long clk;
unsigned long next_expiry;
unsigned int cpu;
bool is_idle;
bool must_forward_clk;
DECLARE_BITMAP(pending_map, WHEEL_SIZE);
struct hlist_head vectors[WHEEL_SIZE];
} ____cacheline_aligned;
- lock:保护timer_base的自旋锁,还同时保护vectors数组中的所有timer。
- running_timer:指向当前CPU正在处理的定时器对应的timer_list。
- clk:当前定时器所经过的 jiffies,用来判断包含的定时器是否已经到期或超时。
- next_expiry:该CPU距离超时最近的 timer的超时时间。
- cpu:所属的CPU号。
- is_idle:指示是否处于空闲模式下,在NO_HZ模式下会用到。
- must_forward_clk:指示是否需要更新当前clk的值,在NO_HZ模式下会用到。
- pending_map:一个比特位图,每个桶对应一个比特位。如果某个桶内有定时器存在,那么就将相应的比特位置1。WHEEL_SIZE=(LVL_SIZE * LVL_DEPTH)=64*(HZ>100?9:8);
- vectors:时间轮所有桶的数组,每一个元素是一个链表。
每个CPU都含有一到两个timer_base结构体变量:
static DEFINE_PER_CPU(struct timer_base, timer_bases[NR_BASES]);
其中NR_BASES定义如下:
#ifdef CONFIG_NO_HZ_COMMON
# define NR_BASES 2
# define BASE_STD 0
# define BASE_DEF 1
#else
# define NR_BASES 1
# define BASE_STD 0
# define BASE_DEF 0
#endif
所以如果内核编译选项包含 CONFIG_NO_HZ_COMMON,则每个CPU有两个timer_base结构体,下标分别是BASE_STD(Standard)和BASE_DEF(Deferrable)。如果内核编译选项没有包含CONFIG_NO_HZ_COMMON,那么每个CPU只有一个timer_base结构体,BASE_STD和BASE_DEF是同一个。
还有一个概念叫做粒度(Granularity),表示系统至少要过多少个Tick才会检查某一个级里面的所有定时器。每一级的64个桶的检查粒度是一样的,而不同级内的桶之间检查的粒度不同,级数越小,检查粒度越细。每一级粒度的Tick数由宏定义LVL_CLK_DIV的值决定:
也就是第0级内64个桶中存放的所有定时器每个Tick都会检查,第1级内64个桶中存放的所有定时器每8个Tick才会检查,第2级内64个桶中存放的所有定时器每64个Tick才会检查,以此类推。
具体将定时器放到哪一个级下面是由到期时间距离现在时间的差值,也就是距离现在还要过多长时间决定的;而要放到哪个桶里面,则单纯是由到期时间决定的。比如:
#define LVL_START(n) ((64 - 1) << (((n) - 1) * 3))
static int calc_wheel_index(unsigned long expires, unsigned long clk)
{
unsigned long delta = expires - clk;
unsigned int idx;
if (delta < LVL_START(1)) {
idx = calc_index(expires, 0);
} else if (delta < LVL_START(2)) {
idx = calc_index(expires, 1);
} else if (delta < LVL_START(3)) {
idx = calc_index(expires, 2);
} else if (delta < LVL_START(4)) {
idx = calc_index(expires, 3);
} else if (delta < LVL_START(5)) {
idx = calc_index(expires, 4);
} else if (delta < LVL_START(6)) {
idx = calc_index(expires, 5);
} else if (delta < LVL_START(7)) {
idx = calc_index(expires, 6);
} else if (LVL_DEPTH > 8 && delta < LVL_START(8)) {
idx = calc_index(expires, 7);
} else if ((long) delta < 0) {
idx = clk & LVL_MASK;
} else {
/*
* Force expire obscene large timeouts to expire at the
* capacity limit of the wheel.
*/
if (expires >= WHEEL_TIMEOUT_CUTOFF)
expires = WHEEL_TIMEOUT_MAX;
idx = calc_index(expires, LVL_DEPTH - 1);
}
return idx;
}
/***********************************************************************/
根据上述计算:
距离到期还有TICKS 桶id
[0,63): [1,64)
63 72
[64,72) 73
[72,80) 74
......
但这种算法一些桶会使用不到。。。。
2.1、add_timer/mod_timer
void add_timer(struct timer_list *timer)
{
BUG_ON(timer_pending(timer)); //若timer已经被添加则报错
mod_timer(timer, timer->expires);
}
int mod_timer(struct timer_list *timer, unsigned long expires)
{
return __mod_timer(timer, expires, 0);
}
EXPORT_SYMBOL(mod_timer);
2.2 timer_reduce
- MOD_TIMER_PENDING_ONLY 表示本次修改只针对还存在在系统内的定时器,如果定时器已经被删除了则不会再将其激活。
- MOD_TIMER_REDUCE 则表示本次修改只会将定时器的到期值减小。
// 如果我们要修改一个已经存在的定时器,比如说减小其到期时间,要使用timer_reduce函数:
int timer_reduce(struct timer_list *timer, unsigned long expires)
{
return __mod_timer(timer, expires, MOD_TIMER_REDUCE);
}
EXPORT_SYMBOL(timer_reduce);
// 第三个参数是模式,目前系统中共有两个:
#define MOD_TIMER_PENDING_ONLY 0x01
#define MOD_TIMER_REDUCE 0x02
2.3 __mod_timer
static inline int
__mod_timer(struct timer_list *timer, unsigned long expires, unsigned int options)
{
struct timer_base *base, *new_base;
unsigned int idx = UINT_MAX;
unsigned long clk = 0, flags;
int ret = 0;
BUG_ON(!timer->function);
/*
* This is a common optimization triggered by the networking code - if
* the timer is re-modified to have the same timeout or ends up in the
* same array bucket then just return:
*/
/* 定时器是否已经添加到某个链表中 */
if (timer_pending(timer)) {
/*
* The downside of this optimization is that it can result in
* larger granularity than you would get from adding a new
* timer with this expiry.
*/
long diff = timer->expires - expires;
if (!diff) //若过期时间未改变直接退出
return 1;
// 若只想减小过期时间但拟修改的过期时间更早则退出
if (options & MOD_TIMER_REDUCE && diff <= 0)
return 1;
/*
* We lock timer base and calculate the bucket index right
* here. If the timer ends up in the same bucket, then we
* just update the expiry time and avoid the whole
* dequeue/enqueue dance.
*/
/* 找到定时器对应的timer_base并上锁 */
base = lock_timer_base(timer, &flags);
/* 试着更新timer_base的clk数:时钟?那和jiffies的区别? */
forward_timer_base(base);
/* 如果是要减定时器到期时间但是传入的到期时间比定时器当前的到期时间还大则直接返回成功 */
if (timer_pending(timer) && (options & MOD_TIMER_REDUCE) &&
time_before_eq(timer->expires, expires)) {
ret = 1;
goto out_unlock;
}
clk = base->clk;
/* 计算要放置到的桶下标:根据过期时间(expires-clk)计算 */
idx = calc_wheel_index(expires, clk);
/*
* Retrieve and compare the array index of the pending
* timer. If it matches set the expiry to the new value so a
* subsequent call will exit in the expires check above.
*/
/* 如果定时器修改后桶下标不变 */
if (idx == timer_get_idx(timer)) {
/* 若选项不是REDUCE直接修改定时器到期时间 */
if (!(options & MOD_TIMER_REDUCE))
timer->expires = expires;
/* 若选项是reduce需要比较新旧到期时间再修改 */
else if (time_after(timer->expires, expires))
timer->expires = expires;
ret = 1;
goto out_unlock;
}
} else {
/* 若没有插入到链表中,则不用考虑重新插入的问题 */
base = lock_timer_base(timer, &flags);
forward_timer_base(base);
}
/* 将timer从当前链表中删除 */
ret = detach_if_pending(timer, base, false);
if (!ret && (options & MOD_TIMER_PENDING_ONLY))
goto out_unlock;
/*获得系统指定的最合适的timer_base结构体 */
new_base = get_target_base(base, timer->flags);
if (base != new_base) {
/*
* We are trying to schedule the timer on the new base.
* However we can't change timer's base while it is running,
* otherwise del_timer_sync() can't detect that the timer's
* handler yet has not finished. This also guarantees that the
* timer is serialized wrt itself.
*/
/* 若timer不是旧timer_base正在处理的定时器 */
if (likely(base->running_timer != timer)) {
/* See the comment in lock_timer_base() */
/* 设置TIMER_MIGRATING标记位 */
timer->flags |= TIMER_MIGRATING;
/* 释放迁移出的的自旋锁_base */
raw_spin_unlock(&base->lock);
base = new_base;
/* 获得迁移进的base自旋锁 */
raw_spin_lock(&base->lock);
WRITE_ONCE(timer->flags,
(timer->flags & ~TIMER_BASEMASK) | base->cpu);
/* 试着更新timer_base的clk */
forward_timer_base(base);
}
}
debug_timer_activate(timer);
/* 更新定时器的到期时间 */
timer->expires = expires;
/*若桶下标已经计算且timer_base的clk没变
*/
if (idx != UINT_MAX && clk == base->clk) {
/* 将定时器加入对应桶中 */
enqueue_timer(base, timer, idx);
trigger_dyntick_cpu(base, timer);
} else {
internal_add_timer(base, timer);
}
out_unlock:
/* 释放timer_base结构体的自旋锁并开中断 */
raw_spin_unlock_irqrestore(&base->lock, flags);
return ret;
}
可以看到该函数在获得了定时器对应的 timer_base 结构体后,都需要调用forward_timer_base 函数更新 timer_base 结构体中的clk变量:
static inline void forward_timer_base(struct timer_base *base)
{
#ifdef CONFIG_NO_HZ_COMMON
unsigned long jnow;
/*
* We only forward the base when we are idle or have just come out of
* idle (must_forward_clk logic), and have a delta between base clock
* and jiffies. In the common case, run_timers will take care of it.
*/
if (likely(!base->must_forward_clk))
return;
/* 获得当前的jiffies */
jnow = READ_ONCE(jiffies);
base->must_forward_clk = base->is_idle;
/* 如果当前jiffies和clk变量之间的差值小于2证明当前CPU没有进入空闲模式 */
if ((long)(jnow - base->clk) < 2)
return;
/*
* If the next expiry value is > jiffies, then we fast forward to
* jiffies otherwise we forward to the next expiry value.
*/
/* 最早过期定时器还没到期,则将clk更新为jiffies;否则更新为最早过期时间 */
if (time_after(base->next_expiry, jnow)) {
base->clk = jnow;
} else {
if (WARN_ON_ONCE(time_before(base->next_expiry, base->clk)))
return;
base->clk = base->next_expiry;
}
#endif
}
forward_timer_base 函数只有在内核在编译时打开 CONFIG_NO_HZ_COMMON 编译选项的时候才有实际的作用。这是因为,如果内核不支持 NO_HZ 模式的话,那 Tick 就不会中断,每次Tick到来时,clk 都会得到更新,就不需要调用 forward_timer_base 函数来补了。相反,在支持 NO_HZ模式时,CPU 如果处于空闲状态,是不会收到任何Tick的,在这段时间内对应CPU的timer_base结构体中的clk就肯定不会得到 更新,因此需要调用该函数来补。补的条件有两个,
1、必须设置了must_forward_clk(后面会看到在处理定时期到期时会关闭must_forward_clk)
2、还有就是当前的jiffies和clk中记录的已经经过的jiffies相差大于等于2(小于2基本说明还没进空闲模式)。
最后,如果下一个到期时间在现在的jiffies之后,则将clk设置为当前的jiffies;如果当前的jiffies已经超过了下一个到期时间(某些定时器已经过期了),则将clk设置为下一个到期时间,一般对于可延迟定时器会出现这种情况。
每次都要补的目的其实是为了尽量提高定时器的精度,前面已经说过了,到期时间距离clk越近,就会将其放到级别越低的桶里面,检查的Tick间隔就会越小,当然精度越高。如果长期不补clk的值,那即使到期时间只在1个Tick之后,也有可能被放到级别较大的桶内,哪怕是放到级别为1的桶中,都要每8个Tick才会被检查一次,最差情况会延迟7个Tick。
在调用 enqueue_timer 函数将定时器放到 timer_base的 某个桶中后,一般还会接着调用 trigger_dyntick_cpu 函数:
若将一个timer插到一个timer_base时但其cpu处于空闲状态,此时这个cpu的定时事件是所有定时器中最近到期的时刻。若新插入的timer定时器到期更早,那么这个新插入timer一定会超时。对于这种情况,更新timer_base下次到期时间,再调用wake_up_nohz_cpu唤醒CPU让它再检查一遍。
static void
trigger_dyntick_cpu(struct timer_base *base, struct timer_list *timer)
{
/* 如果没有切换到NO_HZ模式则直接返回 */
if (!is_timers_nohz_active())
return;
if (timer->flags & TIMER_DEFERRABLE) {
if (tick_nohz_full_cpu(base->cpu))
wake_up_nohz_cpu(base->cpu);
return;
}
/* 如果timer_base对应的CPU不是空闲的则直接返回 */
if (!base->is_idle)
return;
/* 如果定时器的到期时间晚于timer_base中的到期时间则直接返回 */
if (time_after_eq(timer->expires, base->next_expiry))
return;
if (time_before(timer->expires, base->clk)) {
base->next_expiry = base->clk;
} else {
/* 将timer_base的到期时间设置为定时器的到期时间 */
base->next_expiry = timer->expires;
}
/* 唤醒timer_base对应的CPU */
wake_up_nohz_cpu(base->cpu);
}
2.5 定时器迁移
2.6 tick到来的处理
当一个Tick到来时,无论是Tick层还是Tick模拟层最终都会调用update_process_times通知定时器层:
void update_process_times(int user_tick)
{
struct task_struct *p = current;
/* Note: this timer irq context must be accounted for as well. */
account_process_tick(p, user_tick);
/* 处理当前CPU下的所有定时器 */
run_local_timers();
rcu_sched_clock_irq(user_tick);
#ifdef CONFIG_IRQ_WORK
if (in_irq())
irq_work_tick();
#endif
scheduler_tick();
if (IS_ENABLED(CONFIG_POSIX_TIMERS))
run_posix_cpu_timers();
}
void run_local_timers(void)
{
struct timer_base *base = this_cpu_ptr(&timer_bases[BASE_STD]);
hrtimer_run_queues();
/* 如果当前jiffies小于timer_base的clk值表明还没有任何定时器到期 */
if (time_before(jiffies, base->clk)) {
if (!IS_ENABLED(CONFIG_NO_HZ_COMMON))
return;
/* CPU is awake, so check the deferrable base. */
base++;
if (time_before(jiffies, base->clk))
return;
}
/* 发起TIMER_SOFTIRQ软中断 */
raise_softirq(TIMER_SOFTIRQ);
}
先取出当前 CPU下BASE_STD 编号的 timer_base 结构体。如果当前系统的 jiffies 小于结构体中的clk变量的值,表示该结构体内包含的所有定时器都还没有到期。如果内核没有配置CONFIG_NO_HZ_COMMON 编译选项,则直接退出(没有配置NO_HZ模式,也就没有第二个timer_base结构体了)。否则继续检查BASE_DEF标号的timer_base结构体,如果全都没有到期的定时器,就没必要激活软中断继续处理了,直接退出就可以了。如果有可能有任何定时器到期的话,则激活TIMER_SOFTIRQ软中断。
TIMER_SOFTIRQ 软中断的处理函数是在 init_timers 函数里面初始化的:
void __init init_timers(void)
{
init_timer_cpus();
open_softirq(TIMER_SOFTIRQ, run_timer_softirq);
}
static __latent_entropy void run_timer_softirq(struct softirq_action *h)
{
struct timer_base *base = this_cpu_ptr(&timer_bases[BASE_STD]);
__run_timers(base);
if (IS_ENABLED(CONFIG_NO_HZ_COMMON))
__run_timers(this_cpu_ptr(&timer_bases[BASE_DEF]));
}
static inline void __run_timers(struct timer_base *base)
{
struct hlist_head heads[LVL_DEPTH];
int levels;
/* 如果当前时间早于timer_base的clk值表明没有定时器到期 */
if (!time_after_eq(jiffies, base->clk))
return;
timer_base_lock_expiry(base);
raw_spin_lock_irq(&base->lock);
/*
* timer_base::must_forward_clk must be cleared before running
* timers so that any timer functions that call mod_timer() will
* not try to forward the base. Idle tracking / clock forwarding
* logic is only used with BASE_STD timers.
*
* The must_forward_clk flag is cleared unconditionally also for
* the deferrable base. The deferrable base is not affected by idle
* tracking and never forwarded, so clearing the flag is a NOOP.
*
* The fact that the deferrable base is never forwarded can cause
* large variations in granularity for deferrable timers, but they
* can be deferred for long periods due to idle anyway.
*/
/* 在__mod_timer函数中不需要再更新timer_base的clk值 */
base->must_forward_clk = false;
while (time_after_eq(jiffies, base->clk)) {
/* 收集所有已经到期的定时器,表明确实是经过了一些 Tick,
这时候就需要一个 Tick 一个 Tick 的追查到底有多少个定时器
已经到期了,直到追到当前时间为止*/
levels = collect_expired_timers(base, heads);
base->clk++;
while (levels--)
/* 按级从高到低处理所有到期定时器 */
expire_timers(base, heads + levels);
}
raw_spin_unlock_irq(&base->lock);
timer_base_unlock_expiry(base);
}
static void expire_timers(struct timer_base *base, struct hlist_head *head)
{
/*
* This value is required only for tracing. base->clk was
* incremented directly before expire_timers was called. But expiry
* is related to the old base->clk value.
*/
unsigned long baseclk = base->clk - 1;
/* 循环访问所有超时定时器 */
while (!hlist_empty(head)) {
struct timer_list *timer;
void (*fn)(struct timer_list *);
timer = hlist_entry(head->first, struct timer_list, entry);
/* 更新timer_base的running_timer的值为当前待处理定时器 */
base->running_timer = timer;
/* 从链表中删除该定时器 */
detach_timer(timer, true);
fn = timer->function;
// 如果定时器的标志位设置了 TIMER_IRQSAFE 标志位,除了加锁和释放锁,还需要同时关闭中断和打开中断。
if (timer->flags & TIMER_IRQSAFE) {
raw_spin_unlock(&base->lock);
call_timer_fn(timer, fn, baseclk);
raw_spin_lock(&base->lock);
base->running_timer = NULL;
} else {
raw_spin_unlock_irq(&base->lock);
call_timer_fn(timer, fn, baseclk);
raw_spin_lock_irq(&base->lock);
base->running_timer = NULL;
timer_sync_wait_running(base);
}
}
}
static int collect_expired_timers(struct timer_base *base,
struct hlist_head *heads)
{
unsigned long now = READ_ONCE(jiffies);
/*
* NOHZ optimization. After a long idle sleep we need to forward the
* base to current jiffies. Avoid a loop by searching the bitfield for
* the next expiring timer.
*/
/* 如果当前jiffies和clk变量之间的差值大于2证明当前CPU已经进入过空闲模式 */
if ((long)(now - base->clk) > 2) {
/* 搜寻timer_base下最早到期定时器的时间 */
unsigned long next = __next_timer_interrupt(base);
if (time_after(next, now)) {
base->clk = now;
return 0;
}
base->clk = next;
}
/* 在正式收集之前,会检查是不是刚从空闲模式中出来。在空闲模式下,不会收到Tick,所以就会导致当前时间 jiffies 和 timer_base 的 clk 值之间差距比较大。如果是这样的话,还是像处理普通模式一样一个Tick一个Tick追就太没有效率了,因为理论上在Tick中断期间是没有要到期的定时器的。所以,可以调用 __next_timer_interrupt 函数找到最近到期定时器的到期时间,并更新 clk 的值,再去收集。 */
return __collect_expired_timers(base, heads);
}
static unsigned long __next_timer_interrupt(struct timer_base *base)
{
unsigned long clk, next, adj;
unsigned lvl, offset = 0;
next = base->clk + NEXT_TIMER_MAX_DELTA;
clk = base->clk;
/* 循环每一个级 */
for (lvl = 0; lvl < LVL_DEPTH; lvl++, offset += LVL_SIZE) {
/* 在某一级下获得下一个到期桶偏移距离 */
int pos = next_pending_bucket(base, offset, clk & LVL_MASK);
if (pos >= 0) {
/* 计算对应桶的到期时间 */
unsigned long tmp = clk + (unsigned long) pos;
tmp <<= LVL_SHIFT(lvl);
/* 找出最小的到期时间 */
if (time_before(tmp, next))
next = tmp;
}
/* 如果当前clk的最低3位不为0,则切换到下一级的时候要加1。 */
adj = clk & LVL_CLK_MASK ? 1 : 0;
/* 对clk移位切换下一级 */
clk >>= LVL_CLK_SHIFT;
clk += adj;
}
return next;
}
3、测试例子
timerTest.c
#include <linux/module.h>
#include <linux/timer.h>
#include <linux/jiffies.h>
#include <linux/init.h>
struct timer_list timer;
static void myfunc(struct timer_list *t)
{
printk(KERN_DEBUG"%s: %lu, %sn", __func__, jiffies, "hello");
mod_timer(&timer, jiffies+HZ);
}
static int __init mytimer_init(void)
{
printk(KERN_DEBUG"%s : %s : %d - ok.n", __FILE__, __func__, __LINE__);
printk(KERN_DEBUG"hello");
printk("HZ=%dn", HZ);
//初始化定时器
timer.expires = jiffies + HZ;//定时1s
timer_setup(&timer, myfunc, 0);
//注册定时器
add_timer(&timer);
return 0;
}
static void __exit mytimer_exit(void)
{
printk(KERN_DEBUG"exit..........n", HZ);
del_timer(&timer);
}
module_init(mytimer_init);
module_exit(mytimer_exit);
MODULE_LICENSE("GPL");
Makefile:
modname =timerTest
arch ?= x86
ifeq ($(arch),x86)
KERNELDIR := /lib/modules/$(shell uname -r)/build
CROSS_COMPILE :=
else
KERNELDIR := /home/linux/linux-stm32mp-5.10.61-stm32mp-r2-r0/linux-5.10.61/
CROSS_COMPILE := arm-linux-gnueabihf-
endif
CURRENTDIR := $(shell pwd)
CC := $(CROSS_COMPILE)gcc
all:
make -C $(KERNELDIR) M=$(CURRENTDIR) modules
#$(CC) test.c -o test
install:
@cp *.ko ~/nfs/rootfs/
#@cp test ~/nfs/rootfs
help:
@echo "make arch=arm|x86 modname=modules drivers source file name"
clean:
make -C $(KERNELDIR) M=$(CURRENTDIR) clean
#rm test
obj-m := $(modname).o
测试指令:
sudo rmmod timerTest # 先移除旧ko
make #重新编译
sudo insmod timerTest.ko #安装ko
dmesg|tail #查看打印
最后
以上就是活泼大象为你收集整理的linux timer浅析的全部内容,希望文章能够帮你解决linux timer浅析所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复