Linux のスリープ処理、タイマ処理の詳細を見る


UNIX  sleep(3) 使sleep()  usleep(3) ()  nanosleep(2) () 使sleep(), usleep() nanosleep() *1

 usleep()  nanosleep()  1ms http://shiroikumo.at.infoseek.co.jp/linux/time/  2.6.19 x86_64CentOS5

nanosleep()  1ms  gettimeofday(2) 
#define _POSIX_C_SOURCE 200112L

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <sys/time.h>
#include <unistd.h>

enum {
    LOOP = 10,
};

int main (int argc, char **argv)
{
    struct timeval tv[LOOP];
    struct timespec req;

    req.tv_sec  = 0;
    req.tv_nsec = 1000000; // 1ms

    for (int i = 0; i < LOOP; i++) {
        nanosleep(&req, NULL);
        gettimeofday(tv + i, NULL);
    }

    for (int i = 0; i < LOOP; i++)
        printf("%ld:%06ld\n", (long)tv[i].tv_sec, (long)tv[i].tv_usec);

    return 0;
}


% ./a.out
1200922869:179356
1200922869:183413
1200922869:187452
1200922869:191493
1200922869:195554
1200922869:199594
1200922869:203634
1200922869:207675
1200922869:211716
1200922869:215756

1 1ms  4ms  req.tv_nsec  4ms 

 usleep() CPerl  Time::HiRes 使Time::HiRes  libc  usleep()  nanosleep() 
#!/usr/local/bin/perl
use strict;
use warnings;
use Time::HiRes qw/gettimeofday usleep/;

my @tv;

for (my $i = 0; $i < 10; $i++) {
    $tv[$i] = [ gettimeofday ];
    usleep(1000); # 1ms
}

for (my $i = 0; $i < 10; $i++) {
    printf "%d:%d\n", @{$tv[$i]};
}


% perl usleep.pl
1200922887:495952
1200922887:500306
1200922887:504345
1200922887:508404
1200922887:512445
1200922887:516485
1200922887:520524
1200922887:524566
1200922887:528605
1200922887:532646

 4ms nanosleep()  usleep()  4ms 

nanosleep() の精度


 nanosleep 


glibc  sleep(3) / usleep(3)  nanosleep(2) 使

nanosleep()  (High-resolution kernel timers) 使



 2.6.19  4ms 




Linux 1 10ms  2.6.19  4ms 

 nanosleep()  2.6.20i386 

タイマ割り込み信号を発生させるハードウェア




AT HPET  PIT  IRQ 0  I/O 

HPET  High Precision Event Timer  PIT  HPET 
% dmesg | grep HPET
ACPI: HPET (v001 DELL   PE_SC3   0x00000001 DELL 0x00000001) @ 0x00000000000f293b
ACPI: HPET id: 0x1166a201 base: 0xfed00000
time.c: Using 14.318180 MHz WALL HPET GTOD HPET timer.

DELL  PowerEdge SC1435  HPET 

 HPET  Programmable Interval Timer = PIT Intel  G965 
% dmesg | grep PIT
time.c: Using 3.579545 MHz WALL PM GTOD PIT/TSC timer.

 PIT 使Linux  HPET  HPET  PIT 使

カーネルの初期化処理




 init/main.c  start_kernel() 


init_IRQ()

init_timers()

time_init()



asmlinkage void __init start_kernel(void)
{
...
    /* ハード割り込み周りの初期化。PIT の準備込み */
    init_IRQ();

    pidhash_init();

    /* 動的タイマの初期化処理 */
    init_timers();

    hrtimers_init();
    softirq_init();
    timekeeping_init();

    /* 時刻関連オブジェクトや変数の初期化。HPET の有無判定も */
    time_init();
...
    /* HPET がある場合、HPET の初期化 */
    if (late_time_init)
        late_time_init();
...
}

割り込み発生源のハードウェアの初期化

まずは割り込み発生源となる PIT/HPET の初期化処理を見ていきます。

PIT の初期化

 init_IRQ() 辿 arch/i386/kernel/i8253.c  setup_pit_timer()  PIT 
void setup_pit_timer(void)
{
    unsigned long flags;

    spin_lock_irqsave(&i8253_lock, flags);
    outb_p(0x34,PIT_MODE);      /* binary, mode 2, LSB/MSB, ch 0 */
    udelay(10);
    outb_p(LATCH & 0xff , PIT_CH0); /* LSB */
    udelay(10);
    outb(LATCH >> 8 , PIT_CH0); /* MSB */
    spin_unlock_irqrestore(&i8253_lock, flags);
}

 LATCH 


CLOCK_TICK_RATE = 82541,193,182

HZ = 1


 CLOCK_TICK_RATE HZLATCH  I/O PIT_CH0 PIT 1HZ

HZ include/asm-i386/param.h 
#ifdef __KERNEL__
# define HZ     CONFIG_HZ   /* Internal kernel timer frequency */
# define USER_HZ    100     /* .. some user interfaces are in "ticks" */
# define CLOCKS_PER_SEC     (USER_HZ)   /* like times() */
#endif

CONFIG_HZ .config 
% grep 'CONFIG_HZ=' .config
CONFIG_HZ=250

 250 12504ms 
HPET の初期化

前述の通り HPET があればタイマ割り込み発生源には HPET が使われますが、HPET の初期化はもう少し後になります。時間関連の変数やオブジェクトを初期化する time_init() の中で、HPET の有無を判定し HPET があった場合フラグを立てておいて、HPET の初期化を後で実行するようにしておきます。

arch/i386/kernel/time.c

void __init time_init(void)
{
    struct timespec ts;
#ifdef CONFIG_HPET_TIMER
    /* HPET が利用できるのであればフラグを立てて、あとで初期化 */
    if (is_hpet_capable()) {
        /*
         * HPET initialization needs to do memory-mapped io. So, let
         * us do a late initialization after mem_init().
         */
        late_time_init = hpet_time_init;
        return;
    }
#endif
    ts.tv_sec = get_cmos_time();
    ts.tv_nsec = (INITIAL_JIFFIES % HZ) * (NSEC_PER_SEC / HZ);

    do_settimeofday(&ts);

    do_time_init();
}

動的タイマ (High-resolution timers)

話は変わって、動的タイマの話。Linuxカーネル内部で時限処理を行うために"動的タイマ" という仕組みを備えています。動的タイマに任意の処理を追加した上、任意の時間を指定すると、その時間に追加した処理が起動するような仕組みです。例えばデバイスドライバで I/O のタイムアウトを実装したり、後述の nanosleep() や setitimer() などの時限系のシステムコールでもよく使われています。

次は動的タイマの初期化処理を見ていくことにします。

動的タイマを起動(遅延処理)するソフト割り込みハンドラの登録

この動的タイマですが start_kernel() の init_timers() が初期化処理に当たり、この中でタイマそのものを起動するソフト割り込みハンドラが登録されます。init_timers は kernel/timer.c に定義されいます。

void __init init_timers(void)
{
    int err = timer_cpu_notify(&timers_nb, (unsigned long)CPU_UP_PREPARE,
                (void *)(long)smp_processor_id());

    BUG_ON(err == NOTIFY_BAD);
    register_cpu_notifier(&timers_nb);

    /* ソフト割り込み TIMER_SOFTIRQ に run_timer_softirq() を登録 */
    open_softirq(TIMER_SOFTIRQ, run_timer_softirq, NULL);
}

open_softirq() はソフト割り込みハンドラをカーネルに登録する関数です。

ハードウェア割り込みとソフト割り込み

Linux  () 







init_timers()  run_timer_softirq() 

ハードウェア割り込み周辺

タイマがソフト割り込みハンドラによって起動されることはわかりました。次は、そのソフト割り込み自体を起動するハードウェア割り込みハンドラの初期化処理を見ていきたいと思います。

ハードウェア割り込みハンドラの登録

time_init() PIT  HPET  IRQ 0 arch/i386/kernel/time.c 
void __init time_init(void)
{
    struct timespec ts;
#ifdef CONFIG_HPET_TIMER
    /* HPET 判定処理 */
    if (is_hpet_capable()) {
        /*
         * HPET initialization needs to do memory-mapped io. So, let
         * us do a late initialization after mem_init().
         */
        late_time_init = hpet_time_init;
        return;
    }
#endif
    /* ハードウェアクロックから時刻を取得 */
    ts.tv_sec = get_cmos_time();
    ts.tv_nsec = (INITIAL_JIFFIES % HZ) * (NSEC_PER_SEC / HZ);

    /* 取得した時刻で時計を設定 */
    do_settimeofday(&ts);

    /* ハードウェア割り込みハンドラの登録へ */
    do_time_init();
}

do_time_init() arch/i386/mach-default/setup.c  time_init_hook() time_init_hook()  setup_irq() 
static struct irqaction irq0  = { timer_interrupt, IRQF_DISABLED, CPU_MASK_NONE, "timer", NULL, NULL};

/**
 * time_init_hook - do any specific initialisations for the system timer.
 *
 * Description:
 *  Must plug the system timer interrupt source at HZ into the IRQ listed
 *  in irq_vectors.h:TIMER_IRQ
 **/
void __init time_init_hook(void)
{
    /* IRQ 0 に irq0 構造体をセット */
    setup_irq(0, &irq0);
}

 IRQ 0  struct irqaction  irq0  timer_interrupt() IRQ0  timer_interrupt() 
ハードウェアハンドラの処理内容

  - naoya *2 

timer_interrupt() kernel/timer.c  update_process_times()  update_process_times()  run_local_timers() 
/*
 * Called from the timer interrupt handler to charge one tick to the current
 * process.  user_tick is 1 if the tick is user time, 0 for system.
 */
void update_process_times(int user_tick)
{
    struct task_struct *p = current;
    int cpu = smp_processor_id();

    /* Note: this timer irq context must be accounted for as well. */
    if (user_tick)
        account_user_time(p, jiffies_to_cputime(1));
    else
        account_system_time(p, HARDIRQ_OFFSET, jiffies_to_cputime(1));

    /* 動的タイマを走らせる */
    run_local_timers();

    if (rcu_pending(cpu))
        rcu_check_callbacks(cpu, user_tick);
    scheduler_tick();
    run_posix_cpu_timers(p);
}

run_local_timers() 
void run_local_timers(void)
{
    /* TIMER_SOFTIRQ 割り込み要求 */
    raise_softirq(TIMER_SOFTIRQ);
    softlockup_tick();
}

raise_softirq()  TIMTER_SOFTIRQ TIMER_SOFTIRQ  run_timer_softirq() raise_softirq() 

初期化処理まとめ





 PIT/HPET  4ms 

 timer_interrupt() 

timer_interrupt()  update_process()  TIMER_SOFTIRQ 

 TIMER_SOFTIRQ  run_timer_softirq() 


4ms 

nanosleep(2) の実装をみる


 nanosleep(2) sys_nanosleep() sys_nanosleep()  nanosleep  timespec 
asmlinkage long
sys_nanosleep(struct timespec __user *rqtp, struct timespec __user *rmtp)
{
    struct timespec tu;

    /* ユーザプロセスで指定された timespec 構造体をコピー */
    if (copy_from_user(&tu, rqtp, sizeof(tu)))
        return -EFAULT;

    if (!timespec_valid(&tu))
        return -EINVAL;

    /* nanosleep 処理の詳細へ移動 */
    return hrtimer_nanosleep(&tu, rmtp, HRTIMER_REL, CLOCK_MONOTONIC);
}

 hrtimer_nanosleep() ( nanosleep() )  "hrtimer"  "High-resolution timers" 
long hrtimer_nanosleep(struct timespec *rqtp, struct timespec __user *rmtp,
               const enum hrtimer_mode mode, const clockid_t clockid)
{
    struct restart_block *restart;
    struct hrtimer_sleeper t; /* スリープ用タイマオブジェクトを作成 */
    struct timespec tu;
    ktime_t rem;

    /* タイマを初期化 */
    hrtimer_init(&t.timer, clockid, mode);

    /* タイマの有効期限にユーザープロセスで指定された値をセット */
    t.timer.expires = timespec_to_ktime(*rqtp);

    /* nanosleep 処理本体を起動 */
    if (do_nanosleep(&t, mode))
        return 0;

    ...

    return -ERESTART_RESTARTBLOCK;
}

 nanosleep() 使

do_nanosleep() 
static int __sched do_nanosleep(struct hrtimer_sleeper *t, enum hrtimer_mode mode)
{
    /* 引数にカレントプロセスのプロセスディスクリプタを指定し、スリープタイマを初期化 */
    hrtimer_init_sleeper(t, current);

    do {
        /* カレントプロセスを待ち状態に変更 */
        set_current_state(TASK_INTERRUPTIBLE);
      
        /* タイマ開始。hrtimer_start() ではタイマキューにタイマをエンキュー。 */
        hrtimer_start(&t->timer, t->timer.expires, mode);

        /* カレントプロセスの実行権を手放す → コンテキストスイッチ */
        schedule();

        hrtimer_cancel(&t->timer);
        mode = HRTIMER_ABS;

    } while (t->task && !signal_pending(current));

    return t->task == NULL;
}

do_nanosleep 






 ( run_timer_softirq() 





void hrtimer_init_sleeper(struct hrtimer_sleeper *sl, struct task_struct *task)
{
    /* タイマが切れた際に起動するコールバックに hrtimer_wakeup() を登録 */
    sl->timer.function = hrtimer_wakeup;

    /* task には current が入っている */
    sl->task = task;
}

static int hrtimer_wakeup(struct hrtimer *timer)
{
    struct hrtimer_sleeper *t =
        container_of(timer, struct hrtimer_sleeper, timer);

    /* タスクを取り出す。ここでは current */
    struct task_struct *task = t->task;

    t->task = NULL;

    if (task)
        wake_up_process(task); /* 待ち状態にあるタスクを起こす */

    return HRTIMER_NORESTART;
}

 current  wake_up_process()  wake_up_process()   (TASK_RUNNING) do_nanosleep()  schedule() 

nanosleep() (OS)

nanosleep()  SIGALRM 使 setitimer() 

gettimeofday(2)


 4ms  gettimeofday(2) 調

まとめ

  • Linux のスリープ処理の詳細とタイマ処理を見て来ました
  • カーネル内部で利用される動的タイマは、タイマ割り込みを契機にソフト割り込みハンドラから起動されます
  • Linux カーネルのタイマ割り込みの周期は 4ms です
  • sleep(3), usleep(3) の実装の母体にもなっている nanosleep(2) は動的タイマとスケジューラの挙動を利用して実装されています
  • nanosleep(2) の実装の核になるカーネル内部の動的タイマの仕組みがタイマ割り込みの周期に依存している以上、4ms 以上の精度は実現できません。またスケジューラの挙動も精度に関係します。

例によって間違い等多々あるかもしれません。ツッコミ大歓迎です。

追記

コメント欄でご指摘いただきましたが、タイマ割り込み周期 4ms はデフォルト値で、HZ の値はカーネルコンパイル時に任意の値に設定可能です。id:dayflower さんありがとうございます。

*1:libc がライブラリ関数としてラップしています

*2:ユニプロセッサ環境の場合は PIT/HPET からの割り込みでプロセスアカウンティング処理が行われますが、マルチプロセッサ環境ではローカルAPIC からの割り込みがその契機になります。