Linux 延时实现

独家号 Linux内核剖析 作者 Linux内核那些事

很多时候我们需要在程序中等待一定的时间再运行, 例如我们需要每隔10秒去执行某个任务, 这就需要系统提供接口来等待指定的时间再运行. Linux系统中提供了sleep(nsec)系统调用来等待nsec.

严格来说sleep()并不是Linux提供的系统调用, 而是Glibc库实现的. Glibcsysdeps/posix/sleep.c文件中可以找到具体的实现:

unsigned int

DEFUN(sleep, (seconds), unsigned int seconds)

{

......

act.sa_handler = sleep_handler;

act.sa_flags = 0;

if (sigemptyset (&act.sa_mask) < 0 ||

sigaction (SIGALRM, &act, &oact) < 0)

return seconds;

before = time ((time_t *) NULL);

remaining = alarm (seconds);

......

sigsuspend (&oset);

after = time ((time_t *) NULL);

(void) sigaction (SIGALRM, &oact, (struct sigaction *) NULL);

......

}

代码去掉了很多无关重要的细节, 加粗的代码是实现的主要部分, 流程大概如下:

1) 调用alarm(nsec)系统调用设置一个定时器, nsec秒后内核会发送SIGALRM信号给进程.

2) 调用sigsuspend()系统调用等待信号的发生. Sigsuspend()系统调用会阻塞当前进程, 直到当前进程收到信号才会被唤醒.

调用alarm()系统调用会进入到内核态, 并调用sys_alarm()函数, sys_alarm()函数会调用_setitimer()函数来, _setitimer()函数最终会调用add_timer()函数把当前进程添加到定时器队列中, add_timer()代码如下:

void add_timer(struct timer_list * timer)

{

......

p = &timer_head;


save_flags(flags);

cli();

do {

p = p->next;

} while (timer->expires > p->expires);

timer->next = p;

timer->prev = p->prev;

p->prev = timer;

timer->prev->next = timer;

restore_flags(flags);

}

从代码实现可以看到, 就是把当前进程添加到timer_head队列中, timer_head就是定时器队列.

把当前进程添加到定时器队列后, 内核会在什么时候发送SIGALRM信号给进程呢? 答案就是: 时钟中断.

时钟中断: LinuxOS时钟的物理产生原因是可编程定时/计数器产生的输出脉冲,这个脉冲送入CPU,就可以引发一个中断请求信号,我们就把它叫做时钟中断。

简单的说, 时钟中断就是硬件定时产生的一个电信号, 然后发送给CPU, CPU接收到这个电信号就会做一些动作: 调用中断处理函数. Linux的时钟中断处理函数是do_timer(), do_timer()会调用mark_bh(TIMER_BH)来开启定时器的下半部处理. 定时器下半部会调用timer_bh()函数来处理, 然后timer_bh()函数调用run_timer_list()函数来处理定时器. run_timer_list()代码如下:

static inline void run_timer_list(void)

{

struct timer_list * timer;

cli();

while ((timer = timer_head.next)

!= &timer_head && timer->expires <= jiffies)

{

void (*fn)(unsigned long) = timer->function;

unsigned long data = timer->data;

timer->next->prev = timer->prev;

timer->prev->next = timer->next;

timer->next = timer->prev = NULL;

sti();

fn(data);

cli();

}

sti();

}

从代码可以知道, run_timer_list()函数就是遍历timer_head队列, 如果其中的定时器到期了, 那就调用定时器的回调函数(timerfunction字段).

而对于alarm定时器的回调函数是it_real_fn(). it_real_fn()函数代码如下:

void it_real_fn(unsigned long __data)

{

struct task_struct * p = (struct task_struct *)__data;

unsigned long interval;

send_sig(SIGALRM, p, 1);

interval = p->it_real_incr;

if (interval) {

unsigned long timeout = jiffies + interval;

if (timeout < interval)

timeout = ULONG_MAX;

p->real_timer.expires = timeout;

add_timer(&p->real_timer);

}

}

上面加粗的代码就是用来发送SIGALRM信号给进程.

从上面的分析可以知道, 当进程调用sleep()函数时会调用alarm()系统调用设置一个定时器, 然后调用sigsuspend()等待信号的发生. 当定时器到期时, 内核通过send_sig()函数发送SIGALARM信号给进程, 进程收到SIGALARM信号后会被唤醒.

send_sig()函数代码如下:

int send_sig(unsigned long sig,struct task_struct * p,int priv)

{

......

if (sig == SIGSTOP

|| sig == SIGTSTP

|| sig == SIGTTIN || sig == SIGTTOU)

p->signal &= ~(1<<(SIGCONT-1));

generate(sig,p);

return 0;

}

static inline void generate(unsigned long sig, struct task_struct * p)

{

......

p->signal |= mask;

if (p->state == TASK_INTERRUPTIBLE && (p->signal & ~p->blocked))

wake_up_process(p);

}

从上面的代码可以看到, send_sig()函数把信号保存到进程的signal字段中,然后 调用wake_up_process()函数唤醒进程(加粗的代码).