Switch to High Resolution Mode

2012年4月21日 没有评论

继续:
此时 global and local clock event device 已经注册完毕,系统现在就开始响应中断了,当内核从RTC 取出时间后,内核就应该自己更新时间了,所以中断处理程序的很重要的一部分工作就是计时。而在 timer_interrupt 函数中:

global_clock_event->event_handler(global_clock_event);

实际上执行的就是 tick_handle_periodic()

Note:
当系统启动时是默认工作在 low resolution mode 中,时钟源为 PIT,当系统中存在高精度的时钟源时,时钟源就应该切换到高精度的上(比如 tsc,hpet),在系统初始化末期 clocksource_done_booting() ,kernel 会选择 the best clocksource 然后完成切换操作,在大多数情况下都是从 PIT 切换到 HPET,details see 时钟源注册 .此时高精度的时钟源已经存在,在正常情况下系统就应该将 clock event device 切换到高分辨率模式(在 boot parameter “highres=off” 没有设置的情况下).那么究竟在何时内核回检查并且将时钟事件设备切换到高分辨率模式呢?那就是在每次的时钟中断处理程序中。

 

void tick_handle_periodic(struct clock_event_device *dev)
{
        int cpu = smp_processor_id();
        ktime_t next;

        tick_periodic(cpu);

        if (dev->mode != CLOCK_EVT_MODE_ONESHOT)
                return;

        // 此时直接返回 时钟事件设备的 mode 应该为  CLOCK_EVT_MODE_UNUSED
        // 若为 one-shot 模式 ,则需要编程设置下一个时钟事件

        /*
         * Setup the next period for devices, which do not have
         * periodic mode:
         */
        next = ktime_add(dev->next_event, tick_period);
        for (;;) {
                if (!clockevents_program_event(dev, next, ktime_get()))
                        return;
                /*
                 * Have to be careful here. If we're in oneshot mode,
                 * before we call tick_periodic() in a loop, we need
                 * to be sure we're using a real hardware clocksource.
                 * Otherwise we could get trapped in an infinite
                 * loop, as the tick_periodic() increments jiffies,
                 * when then will increment time, posibly causing
                 * the loop to trigger again and again.
                 */
                if (timekeeping_valid_for_hres())
                        tick_periodic(cpu);
                next = ktime_add(next, tick_period);
        }
}

 

static void tick_periodic(int cpu)
{
        if (tick_do_timer_cpu == cpu) {
                // 如果当前 cpu 是 在 tick_setup_device () 中设置的 cpu,一般情况下为 CPU0,值允许一个 CPU 负责更新时间.
                write_seqlock(&xtime_lock);

                /* Keep track of the next tick event */
                tick_next_period = ktime_add(tick_next_period, tick_period);

                do_timer(1);
                // 更新 wall time 和 jiffies

                write_sequnlock(&xtime_lock);
        }

        update_process_times(user_mode(get_irq_regs()));
        profile_tick(CPU_PROFILING);
}

 


此时我们关注 update_process_times()
update_process_times()

 -> run_local_timers()
     -> raise_softirq(TIMER_SOFTIRQ) // 激活软中断下半部

软中断下半部处理函数在 init_timers () 中注册

run_timer_softirq()                                              
 -> hrtimer_run_pending()                                                                  
      -> hrtimer_switch_to_hres()                                                          
           -> tick_init_highres()                                                          
                -> tick_switch_to_oneshot()                                              
                     -> tick_broadcast_switch_to_oneshot()                              
                         -> tick_broadcast_setup_oneshot()

 

void hrtimer_run_pending(void)
{
        if (hrtimer_hres_active())
                return;
        // 如果此时已经是高精度模式则无需转换

        /*
         * This _is_ ugly: We have to check in the softirq context,
         * whether we can switch to highres and / or nohz mode. The
         * clocksource switch happens in the timer interrupt with
         * xtime_lock held. Notification from there only sets the
         * check bit in the tick_oneshot code, otherwise we might
         * deadlock vs. xtime_lock.
         */
        if (tick_check_oneshot_change(!hrtimer_is_hres_enabled()))
                hrtimer_switch_to_hres();
        // 检查系统中是否存在适用于高分辨率定时器的时钟事件设备
}

static inline int hrtimer_is_hres_enabled(void)
{
        return hrtimer_hres_enabled;
        // 一般情况下 为 1
}

 

int tick_check_oneshot_change(int allow_nohz)
{
        // allow_nohz=0

        struct tick_sched *ts = &__get_cpu_var(tick_cpu_sched);

        if (!test_and_clear_bit(0, &ts->check_clocks))
                return 0;

        if (ts->nohz_mode != NOHZ_MODE_INACTIVE)
                return 0;

        if (!timekeeping_valid_for_hres() || !tick_is_oneshot_available())
                return 0;

        if (!allow_nohz)
                return 1;
        // 一般情况下执行到此,返回 1

        tick_nohz_switch_to_nohz();
        return 0;
}

 

/**
 * timekeeping_valid_for_hres - Check if timekeeping is suitable for hres
 */
int timekeeping_valid_for_hres(void)
{
        unsigned long seq;
        int ret;

        do {
                seq = read_seqbegin(&xtime_lock);

                ret = timekeeper.clock->flags & CLOCK_SOURCE_VALID_FOR_HRES;

                // 当时钟源为高精度已经由 pit 切换到 hpet,此时 timekeeper 中使用的 clock source 是 hpet,而 clocksource_hpet 中的 flag 已经在注册时钟源时在 clocksource_enqueue_watchdog 被设置,所以此时 ret = 1,表示可以切换

        } while (read_seqretry(&xtime_lock, seq));

        return ret;
}


Note:
在系统没有切换到 high resolution clock source 时,在每次时钟中断处理程序中也都会检查时钟时间设备是否能够切换到 high resolution mode.但是每当执行到 tick_check_oneshot_change()-> timekeeping_valid_for_hres() 时,由于 timekeeper 当前使用的是 clocksource_jiffies,PIT 并没有 CLOCK_SOURCE_VALID_FOR_HRES flag 所以每次 tick_check_oneshot_change() 函数都会返回 0,所以不会执行切换操作。但是一旦系统切换时钟源到某个高分辨率时钟源(tsc,hpet)上,就会执行真正切换操作!!

好继续 hrtimer_switch_to_hres() 执行时钟事件设备的注册。

hrtimer_switch_to_hres()
  -> tick_init_highres ()

int tick_init_highres(void)
{
        return tick_switch_to_oneshot(hrtimer_interrupt);
}

 


Note:
此函数应该被执行的次数就是 CPU 的个数

int tick_switch_to_oneshot(void (*handler)(struct clock_event_device *))
{
        struct tick_device *td = &__get_cpu_var(tick_cpu_device);
        // 还是获得那个 tick_device

        struct clock_event_device *dev = td->evtdev;
        // tick device 上的时钟时间设备 此时假设是 CVPU0,那 cpu0 的时钟事件设备此时就是 Lapic

        if (!dev || !(dev->features & CLOCK_EVT_FEAT_ONESHOT) ||
                    !tick_device_is_functional(dev)) {

                printk(KERN_INFO "Clockevents: "
                       "could not switch to one-shot mode:");
                if (!dev) {
                        printk(" no tick device\n");
                } else {
                        if (!tick_device_is_functional(dev))
                                printk(" %s is not functional.\n", dev->name);
                        else
                                printk(" %s does not support one-shot mode.\n",
                                       dev->name);
                }
                return -EINVAL;
        }

        td->mode = TICKDEV_MODE_ONESHOT;
        dev->event_handler = handler;
        // 设置 event_handler 为 hrtimer_interrupt 就是这里啦

        clockevents_set_mode(dev, CLOCK_EVT_MODE_ONESHOT);
        // 设置为 oneshot 模式

        tick_broadcast_switch_to_oneshot();
        // Select oneshot operating mode for the broadcast device

        return 0;
}

 

void tick_broadcast_switch_to_oneshot(void)
{
        struct clock_event_device *bc;
        unsigned long flags;

        spin_lock_irqsave(&tick_broadcast_lock, flags);

        tick_broadcast_device.mode = TICKDEV_MODE_ONESHOT;
        // 设置广播的时钟时间设备的模式
        // tick_broadcast_device 不陌生吧,在 tick_check_broadcast_device() 中被设置,此时就是 HPET

        bc = tick_broadcast_device.evtdev;
        // bc 一般情况下为 &hpet_clockevent

        if (bc)
                tick_broadcast_setup_oneshot(bc);
        // 设置广播设备

        spin_unlock_irqrestore(&tick_broadcast_lock, flags);
}

 


Note:
此函数只会被执行一次

void tick_broadcast_setup_oneshot(struct clock_event_device *bc)
{
        /* Set it up only once ! */
        if (bc->event_handler != tick_handle_oneshot_broadcast) {
                // 此时 bc->event_handler 应该为 clockevents_handle_noop

                int was_periodic = bc->mode == CLOCK_EVT_MODE_PERIODIC;
                int cpu = smp_processor_id();

                bc->event_handler = tick_handle_oneshot_broadcast;
                // 设置 hpet_clockevent 的 event_handler 为 tick_handle_oneshot_broadcast

                clockevents_set_mode(bc, CLOCK_EVT_MODE_ONESHOT);

                /* Take the do_timer update */
                tick_do_timer_cpu = cpu;
                // 设置负责 do_timer 的 CPU.

                /*
                 * We must be careful here. There might be other CPUs
                 * waiting for periodic broadcast. We need to set the
                 * oneshot_mask bits for those and program the
                 * broadcast device to fire.
                 */
                cpumask_copy(to_cpumask(tmpmask), tick_get_broadcast_mask());
                cpumask_clear_cpu(cpu, to_cpumask(tmpmask));
                cpumask_or(tick_get_broadcast_oneshot_mask(),
                           tick_get_broadcast_oneshot_mask(),
                           to_cpumask(tmpmask));

                if (was_periodic && !cpumask_empty(to_cpumask(tmpmask))) {
                        tick_broadcast_init_next_event(to_cpumask(tmpmask),
                                                       tick_next_period);
                        tick_broadcast_set_event(tick_next_period, 1);
                } else
                        bc->next_event.tv64 = KTIME_MAX;
        }
}

 


好的,总结一下:
在完成从低分辨率到高分辨率时钟事件设备的切换过程中,hrtimer_switch_to_hres()被调用的次数正好为 CPU 的个数,将每个CPU 的 local apic clock event device 的 event_handler 设置为 hrtimer_interrupt;而在 tick_broadcast_setup_oneshot() 函数中将 global clock event device 的 event_handler 设置为tick_handle_oneshot_broadcast,tick_broadcast_setup_oneshot() 只会被某个 CPU 执行,不一定是 CPU0,但是只会执行一次,至此完成切换操作。之后在每个 CPU 相应时钟中断时还会去检查是否需要切换到高分辨率模式(连写这块代码的人都说这块有点 ugly),但是在 hrtimer_run_pending()中会首先检查如果已经切换到高分辨率则直接返回。

cat /proc/timer_list | grep "event_handler"
 event_handler:  tick_handle_oneshot_broadcast
 event_handler:  hrtimer_interrupt
 event_handler:  hrtimer_interrupt
 event_handler:  hrtimer_interrupt
 event_handler:  hrtimer_interrupt

 


说明一下,不同的 boot parameter 相应的 event_handler 也会不一样,比如:

boot parameter "highres=off"
   event_handler:  tick_handle_oneshot_broadcast
   event_handler:  tick_nohz_handler
   event_handler:  tick_nohz_handler
   event_handler:  tick_nohz_handler
   event_handler:  tick_nohz_handler

boot parameter "nohz=off highres=off"
   (1)
   event_handler:  clockevents_handle_noop
   event_handler:  tick_handle_periodic
   event_handler:  tick_handle_periodic
   event_handler:  tick_handle_periodic
   event_handler:  tick_handle_periodic
   (2)
   event_handler:  tick_handle_periodic_broadcast
   event_handler:  tick_handle_periodic
   event_handler:  tick_handle_periodic
   event_handler:  tick_handle_periodic
   event_handler:  tick_handle_periodic
分类: kernel, timer 标签: , ,

实习

2012年4月1日 4 条评论

听着陈奕迅的歌,看着地上的旅行袋和那几张泛黄的火车票,回头看这一年走过的路,百感交集…… 如今就要离去,心底有一丝伤感,一丝向往,还有一丝惆怅……

今天是我在公司的最后一天,从去年六月到现,时间在不长也不短,但却很值得回味……

首先感谢 eguan 和 adam8157 平时的帮助,从你们身上学习到了很多。以前在学校每当我折腾 Linux 或者看内核代码时我就不停问自己我这么付出究竟是为了什么?看那些破玩意究竟有什么用?直到后来来到了 Redhat,让我明白了一切…… 在这里真的很锻炼人,也许其他都是假的,但这一点绝对是真的。在这里你可以用任何语言写程序只要你会,C/shell/python。有时需要根据 testcase 的需要简单的可能 shell 或 python 就搞定,稍微复杂的需要用 C,甚至有时还需写 kernel Module。总之会接触到各种系统函数各种用法。同时通过看 patch 促使你有一种阅读代码的欲望,当你弄懂了内核的实现时那种感觉真的很美妙仿佛被打通了任督二脉。工作在这样一个 opensource 中真是一件美妙的事情。

哎,接下来还得回到那个让我上了一辈子火的学校,参加那个破 JB 考试,还得弄那个破 JB 毕设。我只求能顺利毕业,我做学生已经够了。

有时感觉自己就像一直蜗牛,背着重重的壳,蹒跚前行,却始终找不到属于自己的那片天 ……

分类: 生活 标签:

本地时钟事件设备的注册

2012年3月18日 没有评论

话说上一回合已经完成了Global事件设备(hpet)的注册,现在来看下Local时钟事件的注册,就以 CPU0 为例.

在 kernel 初始化的末期,内核线程 1 中(还没有 up 其他 cpu),在 log 中出现了类似“CPU0: Intel(R) Xeon(TM) CPU 3.20GHz stepping 04”,发生了这样一个故事……

局部时钟事件设备的注册

kernel_init()
  -> smp_prepare_cpus(setup_max_cpus)
     native_smp_prepare_cpus(64)
       -> x86_init.timers.setup_percpu_clockev()
          setup_boot_APIC_clock()
            -> setup_APIC_timer()
                 -> clockevents_register_device(levt)

 

static void __cpuinit setup_APIC_timer(void)
{
        struct clock_event_device *levt = &__get_cpu_var(lapic_events);

        if (cpu_has(&current_cpu_data, X86_FEATURE_ARAT)) {
                lapic_clockevent.features &= ~CLOCK_EVT_FEAT_C3STOP;
                /* Make LAPIC timer preferrable over percpu HPET */
                lapic_clockevent.rating = 150;
        }

        memcpy(levt, &lapic_clockevent, sizeof(*levt));
        levt->cpumask = cpumask_of(smp_processor_id());

        clockevents_register_device(levt);
        // 注册 CPU0 Local Clock Event Device
}

 

 +----------------------------------------------------------------------------------+
 |      /*                                                                          |
 |       * The local apic timer can be used for any function which is CPU local.    |
 |       */
                                                                        |
 |      static struct clock_event_device lapic_clockevent = {                       |
 |              .name           = "lapic",                                          |
 |              .features       = CLOCK_EVT_FEAT_PERIODIC | CLOCK_EVT_FEAT_ONESHOT  |
 |                              | CLOCK_EVT_FEAT_C3STOP | CLOCK_EVT_FEAT_DUMMY,     |
 |              .shift          = 32,                                               |
 |              .set_mode       = lapic_timer_setup,                                |
 |              .set_next_event = lapic_next_event,                                 |
 |              .broadcast      = lapic_timer_broadcast,                            |
 |              .rating         = 100,                                              |
 |              .irq            = -1,                                               |
 |      };                                                                          |
 |                                                                                  |
 +----------------------------------------------------------------------------------+

 


故事从这里就又开始了

void clockevents_register_device(struct clock_event_device *dev)
{
        // dev=&lapic_clockevent

        unsigned long flags;

        BUG_ON(dev->mode != CLOCK_EVT_MODE_UNUSED);
        BUG_ON(!dev->cpumask);
        // 此时设备必须绑定到某个 CPU 上,此时还是 cpu0

        spin_lock_irqsave(&clockevents_lock, flags);

        list_add(&dev->list, &clockevent_devices);
        // 加入到 clockevent_devices 链表中,跟时钟源的注册很像,只要注意以下插入链表的方式就好

        clockevents_do_notify(CLOCK_EVT_NOTIFY_ADD, dev);

        clockevents_notify_released();
        // 当 Global Clock Event Device 注册完 hpet 和 cpu0 注册 lapic 之后会被调用.

        spin_unlock_irqrestore(&clockevents_lock, flags);
}

        ......

static int tick_check_new_device(struct clock_event_device *newdev)
{
        ......

        cpu = smp_processor_id();
        // 还是 CPU0
        if (!cpumask_test_cpu(cpu, newdev->cpumask))
                goto out_bc;
        // cpumask:            cpumask to indicate for which CPUs this device works

        td = &per_cpu(tick_cpu_device, cpu);
        // 还是 CPU0 的那个 struct tick_device 实例

        curdev = td->evtdev;
        // 注意此时 curdev 并不是 NULL 而是指向 hpet_clockevent

        ......

        if (curdev) {
                // curdev = &hpet_clockevent
                // newdev = &lapic_clockevent

                /*
                 * Prefer one shot capable devices !
                 */
                if ((curdev->features & CLOCK_EVT_FEAT_ONESHOT) &&
                    !(newdev->features & CLOCK_EVT_FEAT_ONESHOT))
                        goto out_bc;
                /*
                 * Check the rating
                 */
                if (curdev->rating >= newdev->rating)
                        goto out_bc;

                // lapic rating = 100,hpet rating = 50 

                // 此处需要说明一下:一个 CPU 的一个 tick_device 只能对应一个 clock event device ,但是此时由于 CPU0 的 tick_device 上已经有了用作 global event device 的 hpet,所以此时就应该确定一下要选哪个根据 clcok event device 是否支持 one-shot mode 和 rating 值,此时顺序执行。
        }

        /*
         * Replace the eventually existing device by the new
         * device. If the current device is the broadcast device, do
         * not give it back to the clockevents layer !
         */
        if (tick_is_broadcast_device(curdev)) {
                clockevents_shutdown(curdev);
                curdev = NULL;
        }

        clockevents_exchange_device(curdev, newdev);

        tick_setup_device(td, newdev, cpu, cpumask_of(cpu));
        // 设置 lapic_clockevent 的 event_handler 为 tick_handle_periodic

        ......        

        return NOTIFY_STOP;

out_bc:
        /*
         * Can the new device be used as a broadcast device ?
         */
        if (tick_check_broadcast_device(newdev))
                ret = NOTIFY_STOP;

        spin_unlock_irqrestore(&tick_device_lock, flags);

        return ret;
}

 

static void tick_setup_device(struct tick_device *td,
                              struct clock_event_device *newdev, int cpu,
                              const struct cpumask *cpumask)
{
        ......

        // 回到前文留下伏笔的 else
        else {
                handler = td->evtdev->event_handler;
                // handler 应该为 tick_handle_periodic

                next_event = td->evtdev->next_event;
                td->evtdev->event_handler = clockevents_handle_noop;
                // 设置 hpet_clockevent 的 event_handler 为 clockevents_handle_noop.
        }

        ......
}

 

void clockevents_exchange_device(struct clock_event_device *old,
                                 struct clock_event_device *new)
{
        // old = hpet
        // new = lapic

        unsigned long flags;

        local_irq_save(flags);
        /*
         * Caller releases a clock event device. We queue it into the
         * released list and do a notify add later.
         */
        if (old) {
                clockevents_set_mode(old, CLOCK_EVT_MODE_UNUSED);
                // 设置为 UNUSED mode
                list_del(&old->list);
                // 从原来的 clockevent_devices 中删除
                list_add(&old->list, &clockevents_released);
                // 加到 clockevents_released 链表中
        }

        if (new) {
                BUG_ON(new->mode != CLOCK_EVT_MODE_UNUSED);
                clockevents_shutdown(new);
        }
        local_irq_restore(flags);
}

 


好了,现在回到了 clockevents_register_device() 中的 clockevents_notify_released() 函数。

static void clockevents_notify_released(void)
{
        struct clock_event_device *dev;

        while (!list_empty(&clockevents_released)) {
                // 为空 list_empty 返回 1,如果 clockevents_released 链表不为空则进入循环
                // 现在链表中的设备为 hpet

                dev = list_entry(clockevents_released.next,
                                 struct clock_event_device, list);
                // dev 指向 hpet_clockevent  

                list_del(&dev->list);
                list_add(&dev->list, &clockevent_devices);
                // 再次加入到 clockevent_devices 链表
                clockevents_do_notify(CLOCK_EVT_NOTIFY_ADD, dev);
                // 再次设置 event_handler
        }
        // 在注册 global clock_event device 时因为 clockevents_released 链表为空,根本就没有进入循环体,但是现在不一样了。
}

 


故事从这又 TMD 的开始了……
现在 CPU0 的 tick_device 上的 clock_event device 是 local 的 (lapic_clockevent),现在就是要处理全局的~

static int tick_check_new_device(struct clock_event_device *newdev)
{
        ......

        cpu = smp_processor_id();
        // 还是 CPU0
        if (!cpumask_test_cpu(cpu, newdev->cpumask))
                goto out_bc;
        // cpumask:            cpumask to indicate for which CPUs this device works

        td = &per_cpu(tick_cpu_device, cpu);
        // 还是 CPU0 的那个 struct tick_device 实例

        curdev = td->evtdev;
        // 注意此时 curdev 并不是 NULL 而是指向 lapic_clockevent

        ......

        if (curdev) {
                // curdev = &lapic_clockevent
                // newdev = &hpet_clockevent

                /*
                 * Prefer one shot capable devices !
                 */
                if ((curdev->features & CLOCK_EVT_FEAT_ONESHOT) &&
                    !(newdev->features & CLOCK_EVT_FEAT_ONESHOT))
                        goto out_bc;
                /*
                 * Check the rating
                 */
                if (curdev->rating >= newdev->rating)
                        goto out_bc;
                        // 没办法只能去检查 hpet_clockevent 能否用作 broadcast device,因为它的 rating 值小于已经注册上的 lapic_clockevent

                // lapic rating = 100,hpet rating = 50 

        }

        ......

out_bc:
        /*
         * Can the new device be used as a broadcast device ?
         */
        if (tick_check_broadcast_device(newdev))
                ret = NOTIFY_STOP;

        spin_unlock_irqrestore(&tick_device_lock, flags);

        return ret;
}

 

int tick_check_broadcast_device(struct clock_event_device *dev)
{
        if ((tick_broadcast_device.evtdev &&
             tick_broadcast_device.evtdev->rating >= dev->rating) ||
             (dev->features & CLOCK_EVT_FEAT_C3STOP))
                return 0;

        clockevents_exchange_device(NULL, dev);

        tick_broadcast_device.evtdev = dev;
        // tick_broadcast_device 终于出现了,设置为 hpet

        if (!cpumask_empty(tick_get_broadcast_mask()))
                tick_broadcast_start_periodic(dev);
                // 设置周期模式,此函数在当前 context 中并没有被执行,也就是说 hpet_clockevent 的 event_handler 应该是 "clockevents_handle_noop"
        return 1;
}

 


好现现在说说现在所处的situation,Global 以及 CPU0 的 LAPIC 都已经注册完毕,假设系统 4-core,现在处于 kernel 初始化末期线程1中,马上要进行的是启动其他额外 CPU(1-3),并且注册相应的 LAPIC clock_event device.

kernel_init()
  -> smp_init()

static void __init smp_init(void)
{
        unsigned int cpu;

        /* FIXME: This should be done in userspace --RR */
        for_each_present_cpu(cpu) {
                if (num_online_cpus() >= setup_max_cpus)
                        break;
                if (!cpu_online(cpu))
                        cpu_up(cpu);
        }
        // 启动其他 CPU 调用 start_secondary()

        /* Any cleanup work */
        printk(KERN_INFO "Brought up %ld CPUs\n", (long)num_online_cpus());
        smp_cpus_done(setup_max_cpus);

        ......
}

 

局部时钟事件设备的注册

cpu_up()
  ...
  -> _cpu_up()
     smp_ops.cpu_up(cpu)
       -> native_cpu_up()
            -> do_boot_cpu()
                 -> wakeup_secondary_cpu_via_init()
                      -> startup_ipi_hook()
                         ......
                         maybe (这块没太看懂怎么 invoke 的 start_secondary)
                         start_secondary()
                           -> setup_secondary_APIC_clock()
                                -> setup_APIC_timer()
                                     -> clockevents_register_device(levt)

 

其余的 CPU Lapic 注册过程和 CPU0 的一样也不复杂,最终都把相关 event_handler 初始化为 “tick_handle_periodic”
可是 …… 但是 …… 还是那句话,,故事远没有结束这才是一个开始 ……

分类: kernel, timer 标签: , ,

全局时钟事件设备的注册

2012年3月13日 没有评论

全局时钟事件设备(Global Clock Event Device):HPET/PIT

* 主要负责提供周期时钟,更新 jiffies

* 全局时钟的角色由一个明选择的局部时钟承担,每个 cpu 都有 local apic,而 global clock 有一个特定的 cpu 承担,全局的时钟事件设备虽然附属于某一个特定的 CPU 上,但是完成的是系统相关的工作,例如完成系统的 tick 更新,说白了就是某个 cpu 一个人接俩活~

* 结构 struct clock_event_device

局部时钟事件设备(Local Clock Event Device):lapic
* 每个 CPU 都有一个局部时钟,用作进程统计,最主要的实现了高分辨率定时器(只能工作在提供了lapic 的系统上)
* 主要完成统计运行在当前 CPU 上的进程的统计,以及设置 Local CPU 下一次中断
* 结构 struct clock_event_device

时钟设备(tick device)

struct tick_device {
        struct clock_event_device *evtdev;
        enum tick_device_mode mode;
};    

enum tick_device_mode {
        TICKDEV_MODE_PERIODIC,
        TICKDEV_MODE_ONESHOT,
};

tick device 只是 clock_event_device 的一包装器,增加了而外的字段用于指定设备的运行模式(周期或者单触发)。

全局 tick device

static struct tick_device tick_broadcast_device;

tick_broadcast_device 很重要,后面会说到~

 


查看当前系统的 Global Clock Event Device 以及 Local Clock Event Device

cat /proc/timer_list

查看 tick_device

cat /proc/timer_list | grep "Clock Event Device"
Clock Event Device: hpet
Clock Event Device: lapic
Clock Event Device: lapic
Clock Event Device: lapic
Clock Event Device: lapic

查看 event_handler

cat /proc/timer_list | grep "event_handler"
 event_handler:  tick_handle_oneshot_broadcast
 event_handler:  hrtimer_interrupt
 event_handler:  hrtimer_interrupt
 event_handler:  hrtimer_interrupt
 event_handler:  hrtimer_interrupt

从以上信息可以得出 Global Clock Event Device 是 hpet,event_hanler 为 tick_handle_oneshot_broadcast,Local Clock Event Device 是 lapic,event_hanler 为 hrtimer_interrupt,当前系统使用的是高分辨率的 Timer.

event_handler 是什么呢就是中断到来所要执行的函数。(在中断处理程序中被调用)
大概是这个这样的
Global

static irqreturn_t timer_interrupt(int irq, void *dev_id)
{
         ......

         global_clock_event->event_handler(global_clock_event);
         // tick_handle_oneshot_broadcast

         ......
}

Local

void __irq_entry smp_apic_timer_interrupt(struct pt_regs *regs)
{
         ......

         local_apic_timer_interrupt();

         ......
}

static void local_apic_timer_interrupt(void)
{
        int cpu = smp_processor_id();
        struct clock_event_device *evt = &per_cpu(lapic_events, cpu);

        ......

        evt->event_handler(evt);
        // hrtimer_interrupt
}

那 GLobal 与 Local 的 event_handler 是经过怎样的过程而最终得到的呢,下面就以此主线来分析~有关一些基本的概念大家可以参考 《Professional Linux Kernel Architecture》Chapter 15

我觉得要彻底弄懂一个东西就得看源代码,不看是怎么实现的就是看再多书也没用,这是亲身感受~ ok,Let us Go.

 

全局时钟事件设备的注册

start_kernel()
  -> if (late_time_init)
           late_time_init()
           x86_late_time_init()
             -> hpet_time_init()
                  -> hpet_enable()
                       -> hpet_legacy_clockevent_register()

 

static void hpet_legacy_clockevent_register(void)
{
        /* Start HPET legacy interrupts */
        hpet_enable_legacy_int();

        hpet_clockevent.mult = div_sc((unsigned long) FSEC_PER_NSEC,
                                      hpet_period, hpet_clockevent.shift);
        /* Calculate the min / max delta */
        hpet_clockevent.max_delta_ns = clockevent_delta2ns(0x7FFFFFFF,
                                                           &hpet_clockevent);
        /* 5 usec minimum reprogramming delta. */
        hpet_clockevent.min_delta_ns = 5000;

        /*
         * Start hpet with the boot cpu mask and make it
         * global after the IO_APIC has been initialized.
         */
        hpet_clockevent.cpumask = cpumask_of(smp_processor_id());
        // 当前 cpu 肯定是 cpu0
        clockevents_register_device(&hpet_clockevent);
        // 开始注册
        global_clock_event = &hpet_clockevent;
        // Global clock event
        printk(KERN_DEBUG "hpet clockevent registered\n");
}
static struct clock_event_device hpet_clockevent = {
        .name           = "hpet",
        .features       = CLOCK_EVT_FEAT_PERIODIC | CLOCK_EVT_FEAT_ONESHOT,
        .set_mode       = hpet_legacy_set_mode,
        .set_next_event = hpet_legacy_next_event,
        .shift          = 32,
        .irq            = 0,
        .rating         = 50,
};

 

故事从这就开始了 ……

void clockevents_register_device(struct clock_event_device *dev)
{
        // dev=&hpet_clockevent

        unsigned long flags;

        BUG_ON(dev->mode != CLOCK_EVT_MODE_UNUSED);
        BUG_ON(!dev->cpumask);
        // 此时设备必须绑定到某个 CPU 上,此时是 cpu0

        spin_lock_irqsave(&clockevents_lock, flags);

        list_add(&dev->list, &clockevent_devices);
        // 加入到 clockevent_devices 链表中,跟时钟源的注册很像,只要注意以下插入链表的方式就好

        clockevents_do_notify(CLOCK_EVT_NOTIFY_ADD, dev);

        clockevents_notify_released();
        // 当 Global Clock Event Device 注册完 hpet 和 cpu0 注册 lapic 之后会被调用.

        spin_unlock_irqrestore(&clockevents_lock, flags);
}

static void clockevents_do_notify(unsigned long reason, void *dev)
{
        // dev 为时钟事件设备 (hpet)
        raw_notifier_call_chain(&clockevents_chain, reason, dev);
}

// clockevents_chain 已经在 tick_init 函数中初始化
// clockevents_chain->head = &tick_notifier

struct raw_notifier_head {
                struct notifier_block *head;
};

int raw_notifier_call_chain(struct raw_notifier_head *nh,
                                unsigned long val, void *v)
{
                return __raw_notifier_call_chain(nh, val, v, -1, NULL);
}

int __raw_notifier_call_chain(struct raw_notifier_head *nh,
                              unsigned long val, void *v,
                              int nr_to_call, int *nr_calls)
{
        return notifier_call_chain(&nh->head, val, v, nr_to_call, nr_calls);
}

static int __kprobes notifier_call_chain(struct notifier_block **nl,
                                        unsigned long val, void *v,
                                        int nr_to_call, int *nr_calls)
{
        int ret = NOTIFY_DONE;
        struct notifier_block *nb, *next_nb;

        nb = rcu_dereference(*nl);
        // nb 指向 notifier_block

        while (nb && nr_to_call) {
                // nr_to_call = -1

                next_nb = rcu_dereference(nb->next);

#ifdef CONFIG_DEBUG_NOTIFIERS
                if (unlikely(!func_ptr_is_kernel_text(nb->notifier_call))) {
                        WARN(1, "Invalid notifier called!");
                        nb = next_nb;
                        continue;
                }
#endif
                ret = nb->notifier_call(nb, val, v);
                // 调用 tick_notify 函数 val = CLOCK_EVT_NOTIFY_ADD
                // 返回 ret = NOTIFY_STOP

                if (nr_calls)
                        (*nr_calls)++;
                // nr_calls = NULL;

                if ((ret & NOTIFY_STOP_MASK) == NOTIFY_STOP_MASK)
                        break;
                nb = next_nb;
                nr_to_call--;
        }
        return ret;
}

#define NOTIFY_STOP_MASK        0x8000          /* Don't call further */
#define NOTIFY_DONE             0x0000          /* Don't care */
#define NOTIFY_OK               0x0001          /* Suits me */
#define NOTIFY_STOP             (NOTIFY_OK|NOTIFY_STOP_MASK) 

static int tick_notify(struct notifier_block *nb, unsigned long reason,
                               void *dev)
{
        switch (reason) {

        case CLOCK_EVT_NOTIFY_ADD:
                return tick_check_new_device(dev);
                // 这里

        case CLOCK_EVT_NOTIFY_BROADCAST_ON:
        case CLOCK_EVT_NOTIFY_BROADCAST_OFF:
        case CLOCK_EVT_NOTIFY_BROADCAST_FORCE:
                tick_broadcast_on_off(reason, dev);
                break;

        case CLOCK_EVT_NOTIFY_BROADCAST_ENTER:
        case CLOCK_EVT_NOTIFY_BROADCAST_EXIT:
                tick_broadcast_oneshot_control(reason);
                break;

        case CLOCK_EVT_NOTIFY_CPU_DYING:
                tick_handover_do_timer(dev);
                break;

        case CLOCK_EVT_NOTIFY_CPU_DEAD:
                tick_shutdown_broadcast_oneshot(dev);
                tick_shutdown_broadcast(dev);
                tick_shutdown(dev);
                break;

        case CLOCK_EVT_NOTIFY_SUSPEND:
                tick_suspend();
                tick_suspend_broadcast();
                break;

        case CLOCK_EVT_NOTIFY_RESUME:
                tick_resume();
                break;

        default:
                break;
        }

        return NOTIFY_OK;
}

 


接下来的两个函数很重要,当时看的时候丈二和尚摸不着头脑,但是经过各种 debug kernel,各种 brew,终于看出了点门道~

static int tick_check_new_device(struct clock_event_device *newdev)
{
        // newdev = &hpet_clockevent;

        struct clock_event_device *curdev;
        struct tick_device *td;
        int cpu, ret = NOTIFY_OK;
        unsigned long flags;

        spin_lock_irqsave(&tick_device_lock, flags);

        cpu = smp_processor_id();
        if (!cpumask_test_cpu(cpu, newdev->cpumask))
                goto out_bc;
        // cpumask:cpumask to indicate for which CPUs this device works

        td = &per_cpu(tick_cpu_device, cpu);
        // td 时钟设备,tick_cpu_device 是一个每CPU链表,包含了系统中每个CPU对应的struct tick_device 实例.关于每 CPU 变量这里就不说了,因为也不是一两句话能说明白的。

        curdev = td->evtdev;
        // 此时由于刚注册时钟设备上没有时钟事件设备,所以 curdev 为 NULL,而之后发生的可完全不一样那时 curdev 不为空,到时候再说

        /* cpu local device ? */
        if (!cpumask_equal(newdev->cpumask, cpumask_of(cpu))) {

                /*
                 * If the cpu affinity of the device interrupt can not
                 * be set, ignore it.
                 */
                if (!irq_can_set_affinity(newdev->irq))
                        goto out_bc;

                /*
                 * If we have a cpu local device already, do not replace it
                 * by a non cpu local device
                 */
                if (curdev && cpumask_equal(curdev->cpumask, cpumask_of(cpu)))
                        goto out_bc;
        }
        // 这几行代码没太看懂貌似根本就没有被执行?

        /*
         * If we have an active device, then check the rating and the oneshot
         * feature.
         */
        if (curdev) {
                // 2.curdev = hpet
                // 2.newdev = lapic

                // 3.curdev = lapic
                // 3.newdev = hpet

                /*
                 * Prefer one shot capable devices !
                 */
                if ((curdev->features & CLOCK_EVT_FEAT_ONESHOT) &&
                    !(newdev->features & CLOCK_EVT_FEAT_ONESHOT))
                        goto out_bc;
                /*
                 * Check the rating
                 */
                if (curdev->rating >= newdev->rating)
                        goto out_bc;

                // lapic rating = 100,hpet rating = 50
        }
        // curdev = NULL ,ignore this "if",这块是个关键点

        /*
         * Replace the eventually existing device by the new
         * device. If the current device is the broadcast device, do
         * not give it back to the clockevents layer !
         */
        if (tick_is_broadcast_device(curdev)) {
                clockevents_shutdown(curdev);
                curdev = NULL;
        }

        clockevents_exchange_device(curdev, newdev);

        tick_setup_device(td, newdev, cpu, cpumask_of(cpu));
        // 建立 clock event device

        if (newdev->features & CLOCK_EVT_FEAT_ONESHOT)
                tick_oneshot_notify();
                // here

        spin_unlock_irqrestore(&tick_device_lock, flags);
        return NOTIFY_STOP;

out_bc:
        /*
         * Can the new device be used as a broadcast device ?
         */
        if (tick_check_broadcast_device(newdev))
                ret = NOTIFY_STOP;

        spin_unlock_irqrestore(&tick_device_lock, flags);

        return ret;
}

 

int tick_is_broadcast_device(struct clock_event_device *dev)
{
        return (dev && tick_broadcast_device.evtdev == dev);
}

void clockevents_exchange_device(struct clock_event_device *old,
                                 struct clock_event_device *new)
{
        // <condition 1>
        //old = NULL
        //new = &hpet_clockevent 

        // <condition 2>
        // old = hpet
        // new = lapic

        unsigned long flags;

        local_irq_save(flags);
        /*
         * Caller releases a clock event device. We queue it into the
         * released list and do a notify add later.
         */
        if (old) {
                clockevents_set_mode(old, CLOCK_EVT_MODE_UNUSED);
                list_del(&old->list);
                list_add(&old->list, &clockevents_released);
                // 加到 clockevents_released 链表中
        }

        if (new) {
                BUG_ON(new->mode != CLOCK_EVT_MODE_UNUSED);
                clockevents_shutdown(new);
        }
        local_irq_restore(flags);
        // 此时我们考虑 condition 1
}

 

static void tick_setup_device(struct tick_device *td,
                              struct clock_event_device *newdev, int cpu,
                              const struct cpumask *cpumask)
{
        ktime_t next_event;
        void (*handler)(struct clock_vent_device *) = NULL;

        /*
         * First device setup ?
         */
        if (!td->evtdev) {
        // 此时钟设备没有相关的时钟事件设备

                /*
                 * If no cpu took the do_timer update, assign it to
                 * this cpu:
                 */
                if (tick_do_timer_cpu == TICK_DO_TIMER_BOOT) {
                        // 如果没有选定时钟设备来承担全局时钟设备的角色,那么将选择当前设备来承担此职责

                        tick_do_timer_cpu = cpu;
                        // 设置为当前设备所属处理器编号
                        tick_next_period = ktime_get();

                        tick_period = ktime_set(0, NSEC_PER_SEC / HZ);
                        // 时钟周期,纳秒
                        //HZ = 1000
                }    

                /*
                 * Startup in periodic mode first.
                 */
                td->mode = TICKDEV_MODE_PERIODIC;
                // 设备运行模式 --> 周期模式

        } else {
                // 关于这个 else ,我们某天会来到这里,现在 ignore it.
                handler = td->evtdev->event_handler;
                next_event = td->evtdev->next_event;
                td->evtdev->event_handler = clockevents_handle_noop;
        }    

        td->evtdev = newdev;
        //为时钟设备指定事件设备

        /*
         * When the device is not per cpu, pin the interrupt to the
         * current cpu:
         */
        if (!cpumask_equal(newdev->cpumask, cpumask))
                irq_set_affinity(newdev->irq, cpumask);

        /*
         * When global broadcasting is active, check if the current
         * device is registered as a placeholder for broadcast mode.
         * This allows us to handle this x86 misfeature in a generic
         * way.
         */
        // check whether enable the broadcast mode,如果系统处于省电模式,而局部时钟停止工作,则会使用广播机制
        if (tick_device_uses_broadcast(newdev, cpu))
                return;

        if (td->mode == TICKDEV_MODE_PERIODIC)
                tick_setup_periodic(newdev, 0);
                // 周期模式 invoke this ......
        else
                tick_setup_oneshot(newdev, handler, next_event);
                // 单触发模式
}

 

void tick_setup_periodic(struct clock_event_device *dev, int broadcast)
{
        tick_set_periodic_handler(dev, broadcast);
        // broadcast = 0

        /* Broadcast setup ? */
        if (!tick_device_is_functional(dev))
                return;

        if ((dev->features & CLOCK_EVT_FEAT_PERIODIC) &&
            !tick_broadcast_oneshot_active()) {
                clockevents_set_mode(dev, CLOCK_EVT_MODE_PERIODIC);
                // here
                // 设置成周期模式
        } else {
                unsigned long seq;
                ktime_t next;

                do {
                        seq = read_seqbegin(&xtime_lock);
                        next = tick_next_period;
                } while (read_seqretry(&xtime_lock, seq));

                clockevents_set_mode(dev, CLOCK_EVT_MODE_ONESHOT);

                for (;;) {
                        if (!clockevents_program_event(dev, next, ktime_get()))
                                return;
                        next = ktime_add(next, tick_period);
                }
        }
}

void tick_set_periodic_handler(struct clock_event_device *dev, int broadcast)
{
        if (!broadcast)
                dev->event_handler = tick_handle_periodic;
                // here
        else
                dev->event_handler = tick_handle_periodic_broadcast;
}

static inline int tick_device_is_functional(struct clock_event_device *dev)
{
        return !(dev->features & CLOCK_EVT_FEAT_DUMMY);
}

此时 Global event_handler 的注册就接近尾声了,event_handler = tick_handle_periodic,不对啊应该是 “tick_handle_oneshot_broadcast”啊,是啊,莫急,故事远没有结束这才是一个开始 ……

分类: kernel, timer 标签: , ,

故宫

2012年2月19日 没有评论

在北京快一年了,一直没有机会游览一下北京的风光,在实习即将结束之际游览一下也不枉来一次北京。

故宫一直是我想去的地方,因为它承载着很多。当游览完后有点小遗憾,说实话故宫里其实没有什么了,只剩了一个躯壳,剩下了别人拿不走的东西,感觉那些建筑承受着时间的侵蚀,独自伫立在风中,很孤单,很孤单 …… 真正我感兴趣的想看的东西根本不在这里 ……

发几张图吧:

分类: 生活 标签:

时钟源的注册

2012年1月20日 没有评论

时钟源是啥?它与时钟中断源的有啥区别?

时钟中断源:以某个固定频率向系统发送中断请求的设备,系统执行相应中断处理程序,负责更新 jiffies 等等一系列重要的工作,是系统的脉搏。
可能的时钟中断源:PIT,HPET,Local-APIC 等
查看时钟中断源:

cat /proc/timer_list | grep “Clock Event Device:”
Clock Event Device: hpet
Clock Event Device: lapic
Clock Event Device: lapic

表示系统中 global 时钟中断源是 HPET,系统中有两个 logical cpu,每个 CPU 的 local apic 也同样是时钟中断源。(关于 /proc/timer_list 的其他内容后面的文章中会有介绍)。

时钟源:不具备向系统发送中断请求的功能,但能提供更高的时间精度。简单的说就是一个提供一定精度的计时设备。gettimeofday 就是通过 timekeeper 根据当前使用的时钟源获取时间(单位微妙级)。
可能的时钟源 PIT,HPET,TSC,ACPI_PM 等
查看时钟源:

RHEL-5:
# dmesg | grep “time.c” (详细内容请查看 time_init_gtod 函数)

RHEL-6:
# cat /sys/devices/system/clocksource/clocksource0/current_clocksource
hpet
# cat /sys/devices/system/clocksource/clocksource0/available_clocksource
hpet acpi_pm

Change the current clocksource
# echo “acpi_pm” > /sys/devices/system/clocksource/clocksource0/current_clocksource

 


下面进入正题,介绍一下时钟源的注册(个人理解,欢迎指正)。

1) hpet_time_init()
    -> hpet_enable()
         -> hpet_clocksource_register()
              -> clocksource_register(&clocksource_hpet)
2) tsc_init ()
    -> init_tsc_clocksource()
         -> clocksource_register(&clocksource_tsc)

在系统初始化结束前会调用:
3) core_initcall(init_jiffies_clocksource)
    -> init_jiffies_clocksource()
        -> clocksource_register(&clocksource_jiffies)
maybe ...
4) fs_initcall(init_acpi_pm_clocksource)
    -> init_acpi_pm_clocksource
        -> clocksource_register(&clocksource_acpi_pm)

 


以 clocksource_register(&clocksource_tsc) 为例:

int clocksource_register(struct clocksource *cs)
{
        /* calculate max idle time permitted for this clocksource */
        cs->max_idle_ns = clocksource_max_deferment(cs);

        mutex_lock(&clocksource_mutex);
        // 互斥锁用来保护 clocksource_list 链表
        clocksource_enqueue(cs);
        // 将当前时钟源加入到 clocksource_list 链表中

        clocksource_select();
        // 选择时钟源
        clocksource_enqueue_watchdog(cs);
        mutex_unlock(&clocksource_mutex);
        return 0;
}

 

static struct clocksource clocksource_tsc = {
        .name                   = "tsc",
        .rating                 = 300,
        .read                   = read_tsc,
        .resume                 = resume_tsc,
        .mask                   = CLOCKSOURCE_MASK(64),
        .shift                  = 22,
        .flags                  = CLOCK_SOURCE_IS_CONTINUOUS |
                                  CLOCK_SOURCE_MUST_VERIFY,
#ifdef CONFIG_X86_64
        .vread                  = vread_tsc,
#endif
};

 

static void clocksource_enqueue(struct clocksource *cs)
{
        struct list_head *entry = &clocksource_list;
        // static LIST_HEAD(clocksource_list);
        // 初始化为空 head prev 都指向自己

        struct clocksource *tmp;

        list_for_each_entry(tmp, &clocksource_list, list)
                /* Keep track of the place, where to insert */
                if (tmp->rating >= cs->rating)
                        entry = &tmp->list;
                //rating 值越大代表精度越高,放在链表前
        list_add(&cs->list, entry);
        // 现在假设当前注册的是 TSC ,因为在此之前已经注册了 hpet,tsc rating 大于 hpet 所以 tsc 应该仅靠 clocksource_list 链表头部
        // 插入到双向循环链表中 hpet --> tsc --> head(clocksource_list)
}

 

static void clocksource_select(void)
{
        struct clocksource *best, *cs;

        if (!finished_booting || list_empty(&clocksource_list))
                return;
        // 此时 finished_booting = 0,所以 closksource_select 函数直接返回。在系统初始化快结束时调用 clocksource_done_booting() 会设置finished_booting = 1,紧接着调用 clocksource_select() 进行真正的时钟源选择。

        ......

}

 

到现在为止,时钟源基本上都已经注册好了,来张图(HEAD 为 clocksource_list):

 

最终调用:
fs_initcall(clocksource_done_booting)

 

static int __init clocksource_done_booting(void)
{
        finished_booting = 1;
        // 表示初始化完成

        /*
         * Run the watchdog first to eliminate unstable clock sources
         */
        clocksource_watchdog_kthread(NULL);

        mutex_lock(&clocksource_mutex);

        clocksource_select();
        // 再次调用,选择最好的时钟源

        mutex_unlock(&clocksource_mutex);
        return 0;
}

 

static void clocksource_select(void)
{
        struct clocksource *best, *cs;

        if (!finished_booting || list_empty(&clocksource_list))
                return;

        /* First clocksource on the list has the best rating. */
        best = list_first_entry(&clocksource_list, struct clocksource, list);
        // 如上图中的链表中第一个就是最好的时钟源

        /* Check for the override clocksource. */
        list_for_each_entry(cs, &clocksource_list, list) {
                if (strcmp(cs->name, override_name) != 0)
                        continue;
                /*
                 * Check to make sure we don't switch to a non-highres
                 * capable clocksource if the tick code is in oneshot
                 * mode (highres or nohz)
                 */
                if (!(cs->flags & CLOCK_SOURCE_VALID_FOR_HRES) &&
                    tick_oneshot_mode_active()) {
                        /* Override clocksource cannot be used. */
                        printk(KERN_WARNING "Override clocksource %s is not "
                               "HRT compatible. Cannot switch while in "
                               "HRT/NOHZ mode\n", cs->name);
                        override_name[0] = 0;
                } else
                        /* Override clocksource can be used. */
                        best = cs;
                break;
        }
        if (curr_clocksource != best) {
                printk(KERN_INFO "Switching to clocksource %s\n", best->name);
                // Switching to clocksource tsc log from dmesg .

                curr_clocksource = best;
                // 保存当前使用的时钟源

                timekeeping_notify(curr_clocksource);
                // Install a new clocksource
        }
}

 

void timekeeping_notify(struct clocksource *clock)
{
        if (timekeeper.clock == clock)
                return;
        // clock = &clocksource_tsc .
        // 此时 clock = &clocksource_jiffies . timekeeping_init() 中初始化.

        stop_machine(change_clocksource, clock, NULL);
        tick_clock_notify();
}

 


感觉接下来执行的函数应该为 change_clocksource,这中间的代码没有太细看始终不太明白怎么执行到这个函数的(知道的同学麻烦告诉一下~~),感觉这期间应该做一些基本的工作毕竟时钟源要切换了!

static int change_clocksource(void *data)
{
        struct clocksource *new, *old;

        new = (struct clocksource *) data;
        // new = $clocksource_tsc .

        timekeeping_forward_now();
        // 更新时间

        if (!new->enable || new->enable(new) == 0) {
                // new->enable == NULL

                old = timekeeper.clock;
                timekeeper_setup_internals(new);
                // 设置新的时钟源 tsc --> timekeeper.

                if (old->disable)
                        old->disable(old);
        }
        return 0;
}

 

static void timekeeping_forward_now(void)
{
        cycle_t cycle_now, cycle_delta;
        struct clocksource *clock;
        s64 nsec;

        clock = timekeeper.clock;
        // clock = &clocksource_jiffies 

        cycle_now = clock->read(clock);
        // 返回 jiffies

        cycle_delta = (cycle_now - clock->cycle_last) & clock->mask;
        // 经过的时间(jiffies 数)

        clock->cycle_last = cycle_now;

        nsec = clocksource_cyc2ns(cycle_delta, timekeeper.mult,
                                  timekeeper.shift);
        // 转换成 纳秒 表示再切换时钟源之前最后一次更新 xtime

        /* If arch requires, add in gettimeoffset() */
        nsec += arch_gettimeoffset();
        // 空函数

        timespec_add_ns(&xtime, nsec);
        // 加到 xtime 中去 

        nsec = clocksource_cyc2ns(cycle_delta, clock->mult, clock->shift);
        timespec_add_ns(&raw_time, nsec);
}

 

static void timekeeper_setup_internals(struct clocksource *clock)
{
        cycle_t interval;
        u64 tmp;

        timekeeper.clock = clock;
        // 设置时钟源 time_keeper gettimeofday 会通过 time_keeper layer 获取 wall time

        clock->cycle_last = clock->read(clock);
        // 读取时钟源获取当前值

        /* Do the ns -> cycle conversion first, using original mult */
        tmp = NTP_INTERVAL_LENGTH;
        tmp <<= clock->shift;
        tmp += clock->mult/2;
        do_div(tmp, clock->mult);
        if (tmp == 0)
                tmp = 1;

        interval = (cycle_t) tmp;
        timekeeper.cycle_interval = interval;

        /* Go back from cycles -> shifted ns */
        timekeeper.xtime_interval = (u64) interval * clock->mult;
        timekeeper.raw_interval =
                ((u64) interval * clock->mult) >> clock->shift;

        timekeeper.xtime_nsec = 0;
        timekeeper.shift = clock->shift;

        timekeeper.ntp_error = 0;
        timekeeper.ntp_error_shift = NTP_SCALE_SHIFT - clock->shift;

        /*
         * The timekeeper keeps its own mult values for the currently
         * active clocksource. These value will be adjusted via NTP
         * to counteract clock drifting.
         */
        timekeeper.mult = clock->mult;
}

 

void tick_clock_notify(void)
{
        int cpu;

        for_each_possible_cpu(cpu)
                set_bit(0, &per_cpu(tick_cpu_sched, cpu).check_clocks);
}

 


时钟源的注册就说到这里,接下来就是时钟事件设备啦!

分类: kernel, timer 标签: , ,

系统调用 gettimeofday 分析

2012年1月5日 没有评论

背景:
因为需要所以最近总会写一些 timer 相关的程序,既然是和 timer 相关的那少不了调用 gettimeofday system call。这个函数很有名,在某个版本的内核中创建多个线程 invoke gettimeofday,此时 kernel timer 会 backwards。带着各种疑问我决定阅读 timer 相关内核代码。个人认为 timer 部分不算太好理解,涉及到东西很多(中断,进程调度,CFS, 还有那可恶的 SMP 等等等等),毕竟它是系统的心跳,虽然以前接触点 timer 相关的东西但是我也不太确定能把这部分代码理解透彻。总之 do my best !很喜欢一句歌词:出发了不要问那路在哪 ……

gettimeofday 就是获取 wall time。

用户态调用 gettimefoday,int 0X80 —> 软中断 -> IDT 表中断描述符 -> 段选择子 -> GDT 表段描述符 … 执行中断处理程序 -> 最终执行 sys_gettimeofday.(中间还有什么特权级切换啊,等等不再赘述)

sys_gettimeofday 是内核的入口函数可是我费了很大力气才找到,大概是这个样子:

SYSCALL_DEFINE2(gettimeofday, struct timeval __user *, tv,
                struct timezone __user *, tz)
{
        // tv 和 tz 都是来自用户空间,tv 指向的结构保存获得的当前秒数和微妙数,tz 通常那为 NULL
        if (likely(tv != NULL)) {
                struct timeval ktv;
                do_gettimeofday(&ktv);
                if (copy_to_user(tv, &ktv, sizeof(ktv)))
                        return -EFAULT;
        }
        if (unlikely(tz != NULL)) {
                if (copy_to_user(tz, &sys_tz, sizeof(sys_tz)))
                        return -EFAULT;
        }
        return 0;
}

为什么非得弄个宏 SYSCALL_DEFINE2,直接 sys_gettimeofday 不行吗? 搞得人家很不爽!

 


核心就是下面的函数:

void do_gettimeofday(struct timeval *tv)
{
        struct timespec now;

        getnstimeofday(&now);
        // now 中返回当前时间 秒数以及纳秒数

        tv->tv_sec = now.tv_sec;
        // 秒数
        tv->tv_usec = now.tv_nsec/1000;
        // now.tv_nsec 不足一秒的纳秒数 转换为微秒数.
}

 

CONFIG_GENERIC_TIME=y
// rhel6 中被定义,本文基于 2.6.32 以上版本内核

 

struct timeval {
        __kernel_time_t         tv_sec;         /* seconds */
        __kernel_suseconds_t    tv_usec;        /* microseconds */
};

 

void getnstimeofday(struct timespec *ts)
{
        unsigned long seq;
        s64 nsecs;

        WARN_ON(timekeeping_suspended);

        do {
                seq = read_seqbegin(&xtime_lock);
                // 加读锁,如果此时已经加上的写锁,则自旋在这里

                *ts = xtime;
                // 此时很有可能更新 xtime.(时钟中断处理程序)

                nsecs = timekeeping_get_ns();

                /* If arch requires, add in gettimeoffset() */
                nsecs += arch_gettimeoffset();
                // NULL function ?

        } while (read_seqretry(&xtime_lock, seq));
        // 顺序锁,判断是否重读. 因为此时可能发生时钟中断更新 xtime .这样读出来的值就不准确需要重读.

        timespec_add_ns(ts, nsecs);
        // add nsecs 到 struct timespec 结构.
}

 

这里有三种情况:
1 循环只执行一遍,准确获得当前时间
2 顺利加上读锁,但是与此同时时钟中断到来需要更新 xtime 加上写锁(有更高的优先级),然后 read_seqretry 返回 1 表示时间已经不准却需要重新读取,再次循环读取。以此类推~
3 读锁阻塞,因为此时已经加上了写锁(更新 xtime),等到写锁释放后便可读取.

 

struct timespec {
        time_t  tv_sec;         /* seconds */
        long    tv_nsec;        /* nanoseconds */
};

 

struct timespec xtime __attribute__ ((aligned (16)));

 

__cacheline_aligned_in_smp DEFINE_SEQLOCK(xtime_lock);

#define DEFINE_SEQLOCK(x) \
                seqlock_t x = __SEQLOCK_UNLOCKED(x)                                     

typedef struct {
        unsigned sequence;
        spinlock_t lock;
} seqlock_t;
//记录了写着进程访问临界资源的过程 初始化为 0 写时 +1,解除写锁时再 +1 ,该直为奇数时处于写>锁定态
//读直接访问不需要加锁

#define __SEQLOCK_UNLOCKED(lockname) \
                 { 0, __SPIN_LOCK_UNLOCKED(lockname) }

# define __SPIN_LOCK_UNLOCKED(lockname) \
        (spinlock_t)    {       .raw_lock = __RAW_SPIN_LOCK_UNLOCKED,   \
                                SPIN_DEP_MAP_INIT(lockname) }

 

static __always_inline unsigned read_seqbegin(const seqlock_t *sl)
{
        unsigned ret;

repeat:
        ret = sl->sequence;
        smp_rmb();
        if (unlikely(ret & 1)) {
                // 如果当前值为奇数,表示已经加上写锁则等待直到写锁释放 ......
                cpu_relax();
                goto repeat;
        }

        return ret;
}

static __always_inline int read_seqretry(const seqlock_t *sl, unsigned start)
{
        smp_rmb();

        return (sl->sequence != start);
        // unlikely -> 如果此时读取的值与最开始读取的值不同则表示在这期间加了写锁更新了 xtime,返回 1
        // likely   -> 如果相等返回 0
}

 


下个比较重要的函数:

static inline s64 timekeeping_get_ns(void)
{
        cycle_t cycle_now, cycle_delta;
        struct clocksource *clock;

        clock = timekeeper.clock;
        // 时钟源 PIT/HPET/TSC
        // timekeeper 是什么 ? 在哪里初始化?先卖个关子,后面的文章会继续介绍。简单说下,timekeeper 是 gettimeofday 等等系统调用的接口,timekeeper 下面是 clocksource 

        cycle_now = clock->read(clock);
        // 读取时钟周期的当前计数值 假设为 TSC --> read_tsc()

        /* calculate the delta since the last update_wall_time: */
        cycle_delta = (cycle_now - clock->cycle_last) & clock->mask;
        // clock->cycle_last 应该由时钟中断处理程序负责更新.

        /* return delta convert to nanoseconds using ntp adjusted mult. */
        return clocksource_cyc2ns(cycle_delta, timekeeper.mult,
                                  timekeeper.shift);
        //转换成纳秒
}

timekeeper.mult timekeeper.shift 细节之后会介绍

 

static inline s64 clocksource_cyc2ns(cycle_t cycles, u32 mult, u32 shift)
{
                return ((u64) cycles * mult) >> shift;
}

 

static __always_inline void timespec_add_ns(struct timespec *a, u64 ns)
{
                a->tv_sec += __iter_div_u64_rem(a->tv_nsec + ns, NSEC_PER_SEC, &ns);
                // 返回 秒数 一般情况下返回 0
                a->tv_nsec = ns;
                // 不足 1s 的纳秒数 (此值会一直叠加)
}

 


最后一个函数:

static __always_inline u32
__iter_div_u64_rem(u64 dividend, u32 divisor, u64 *remainder)
{
        // dividend = a->tv_nsec + ns
        u32 ret = 0;

        while (dividend >= divisor) {
                // 如果 纳秒大于一秒 NSEC_PER_SEC 

                /* The following asm() prevents the compiler from
                   optimising this loop into a modulo operation.  */
                asm("" : "+rm"(dividend));

                dividend -= divisor;
                // 减去一秒

                ret++;
                // 秒数加1
        }

        *remainder = dividend;

        return ret;
}

 


总结:
不知不觉中,gettimeofday 已经介绍完了,但是这里面还有几个细节没有讲述,比如 时钟源,时钟源的注册 timekeeping 等等.后面的文章中会陆续介绍,说实话 gettimeofday 此函数实现不算很难,我觉得难理解的在后面,就当它是个开胃小菜吧!

分类: kernel, timer 标签: , ,

一年又一年

2012年1月1日 没有评论

转眼间,2011年又过去了,这一刻,回头看见自己这一路的风景,百感交集……

 


这一年恐怕我这辈子都难以忘怀,因为对我来说实在太痛苦了,真的太痛苦了…… 在那段时间我甚至祈求上天早点结束我的生命,因为每活一分钟痛苦就会增加一分。直到现在我仍然不清楚我是否真的挺过来了,不过我真得想永远忘记它,永远!同时感谢在我最困难的时候帮助过我的人,真心的谢谢他们。

 

大学
大四了,要结束了,其实已经结束了,确切的说一开始就结束了。

我的大学生活大部分时间是在图书馆中度过的,每天面对着黑色的终端,对我而言仿佛整个世界都是黑色的,犹如在黑夜中一般。我始终努力在黑夜中找到属于自己的光明,但是我始终没有找到,现在仍然在黑夜中蹒跚前行 ……

呐喊、彷徨、沉沦、朝花夕拾,是大部分大学生四年的真实写照。但是对我来说只有呐喊和彷徨,因为始终处于一种茫然的状态,不知道付出这么多何时才能收获。

总之我的大学生活很不精彩,病了,一个人扛;烦了,一个人藏;痛了,一个人挡 …… 除了同学间的情谊其他的没什么值得回忆的。

 

Intern
这一年对我来说唯一的好事就是实习了。来到了一家还说得过去的公司实习,真的挺高兴的。记得那是一个艳阳高照的下午,在食堂吃饭的我突然接到电话,说要马上要进行电话面试,当时既激动又忐忑,惶恐中找到了一个人际稀少的楼梯口前坐了下来 ……

就这样揣着梦想只身一人来到了帝都 …… 作为一个还没毕业的菜鸟,工作中一直是保持着虚心学习态度。实习过程中遇到了很多牛人同时也学习到了很多,和一群热爱 Linux 的人工作在一起真是一件幸福的事!我不知道未来会是怎样,我也尽量不去想,因为往往计划的东西最终都没有按计划执行,一切随缘吧,我所能做的就是把当前的事做好。一万年太久,只争朝夕!

 

2012
世界末日?如果要真是世界末日我还蛮期待的,因为只有那个时候才能实现真正的众生平等!那个时候再也不会为就业和房价等问题发愁 …… 那个时候才叫一个洒脱!

不知道怎么回事以前能看书看一下午的我现在每当拿起书脑子中就思绪万千根本就看不进去,或许是因为寂寞吧。我的意志总被寂寞吞噬 …… 每当下班时我是又高兴又害怕,高兴的是忙了一天终于可以休息休息了,害怕的是回去能干什么呢?看墙?看窗?还是看风景?一个对下班没有期待的人下了班能干吗?

人生路漫漫,失意何其多?有时候确实挺无奈的,但无奈又有什么用呢?还不得昂起头继续前进!生活就像是被强奸,既然无力反抗倒不如默默享受。

新的一年即将来临,祝愿自己在新的一年中毕业顺利,工作顺利,活的快乐、充实、洒脱!BTW:如果可能的话,我非常渴望找到人生中的另一半,我知道这几乎没有可能,但是我真的很想,真的很想吗!

分类: 生活 标签:

迁移完毕

2011年11月29日 没有评论

终于迁移完毕,啊,好累啊!

接下来会把之前看过的内存(buddy + slab),PCI,USB(EHCI,UHCI,hub,usb-storage),等其他文章贴上来,因为之前看的时候已经总结好了,只需 (Ctrl + v) 就好,英明吧!!
在看这些代码时遇到了各种困难,各种 google,大部分问题得以解决就是因为参考了前辈们的博文,感谢他们的分享精神,真心的谢谢他们!所以我想把我的理解或者说愚见和大家分享一下,当然不一定对(大牛请绕过),总之互相探讨吧,希望对大家有帮助!

分类: 生活 标签:

ioremap_nocache 函数分析(二)

2011年11月29日 没有评论

非连续映射地址空间

static struct vm_struct *__get_vm_area_node(unsigned long size, unsigned long flags,
                                             unsigned long start, unsigned long end,
                                             int node, gfp_t gfp_mask)
 {
         struct vm_struct **p, *tmp, *area;

         unsigned long align = 1;

         unsigned long addr;

         BUG_ON(in_interrupt());

         if (flags & VM_IOREMAP) {

                 int bit = fls(size);

                 if (bit > IOREMAP_MAX_ORDER)
                         bit = IOREMAP_MAX_ORDER;
                 else if (bit < PAGE_SHIFT)
                         bit = PAGE_SHIFT;

                 align = 1ul << bit;
         }

         addr = ALIGN(start, align);

         size = PAGE_ALIGN(size);

         if (unlikely(!size))
                 return NULL;

         area = kmalloc_node(sizeof(*area), gfp_mask & GFP_LEVEL_MASK, node);
         分配虚拟页面结构

         if (unlikely(!area))
                 return NULL;

         /*
          * We always allocate a guard page.
          */

         size += PAGE_SIZE;
         作为隔离带

         write_lock(&vmlist_lock);
         查找之前的 struct vmlist 链表(有序链表从小到大) 查找到一个合理的虚拟地址空间

         for (p = &vmlist; (tmp = *p) != NULL ;p = &tmp->next) {
                 if ((unsigned long)tmp->addr < addr) {
                         if((unsigned long)tmp->addr + tmp->size >= addr)
                                 addr = ALIGN(tmp->size +
                                              (unsigned long)tmp->addr, align);
                         continue;
                 }
                 if ((size + addr) < addr)
                         goto out;

                 回绕
                 if (size + addr <= (unsigned long)tmp->addr)
                         goto found;

                 addr = ALIGN(tmp->size + (unsigned long)tmp->addr, align);

                 if (addr > end - size)
                         goto out;

         }

 found:

         area->next = *p;
         *p 可能为 NULL

         *p = area;
         新申请的 struct vm_struct 加入到 vmlist 链表中

         area->flags = flags;

         area->addr = (void *)addr;
         VMALLOC_START (线性地址)

         area->size = size;

         area->pages = NULL;

         area->nr_pages = 0;

         area->phys_addr = 0;

         write_unlock(&vmlist_lock);

         return area;

 out:

         write_unlock(&vmlist_lock);

         kfree(area);

         if (printk_ratelimit())
                 printk(KERN_WARNING "allocation failed: out of vmalloc space - use vmalloc=<size> to increase size.\n");

         return NULL;

}

注意:此时我们假设没有打开PAE!

(页目录)

int ioremap_page_range(unsigned long addr,
                       unsigned long end, unsigned long phys_addr, pgprot_t prot)
{

        pgd_t *pgd;

        unsigned long start;

        unsigned long next;

        int err;

        BUG_ON(addr >= end);

        start = addr;
        线性地址

        phys_addr -= addr;

        pgd = pgd_offset_k(addr);
        求出线性地址 addr 在页目录表中的地址
     
        #define pgd_offset_k(address)     pgd_offset(&init_mm, address)

        #define pgd_offset(mm, address)   ((mm)->pgd+pgd_index(address))

        #define pgd_index(address)    (((address) >> PGDIR_SHIFT) & (PTRS_PER_PGD-1))

        页目录表中的偏移

        #define PGDIR_SHIFT     22

        #define PTRS_PER_PGD    1024

        do {

                next = pgd_addr_end(addr, end);
                一般情况下返回 end(线性地址末端)

                err = ioremap_pud_range(pgd, addr, next, phys_addr+addr, prot);

                if (err)
                        break;

        } while (pgd++, addr = next, addr != end);

        flush_cache_vmap(start, end);

        return err;
        返回 0

}
#define pgd_addr_end(addr, end)                                         \
({                                                                      \
        unsigned long __boundary = ((addr) + PGDIR_SIZE) & PGDIR_MASK;  \
                                                                        \
        (__boundary - 1 < (end) - 1)? __boundary: (end);                \
})

PGDIR_SIZE = 1<<22

PGDIR_MASK = 3FFFFF

addr 与 end 之间大小不能超过4Mb,因为一个页目录项最多表示4Mb 内存

(页上层目录)

static inline int ioremap_pud_range(pgd_t *pgd, unsigned long addr,
                unsigned long end, unsigned long phys_addr, pgprot_t prot)

{
        参数:
        pgd 线性地址 addr 所表示的页目录地址
        addr 线性地址
        end 线性地址末端
        phys_addr =  EHCI 总线地址
        prot 标志

 

        pud_t *pud;

        unsigned long next;

        phys_addr -= addr;

        pud = pud_alloc(&init_mm, pgd, addr);
        返回的是pgd 页目录地址

        if (!pud)
                return -ENOMEM;

        do {
                next = pud_addr_end(addr, end);

                #define pud_addr_end(addr, end)                 (end)

                if (ioremap_pmd_range(pud, addr, next, phys_addr + addr, prot))
                        return -ENOMEM;

        } while (pud++, addr = next, addr != end);

        return 0;

}
static inline pud_t *pud_alloc(struct mm_struct *mm, pgd_t *pgd, unsigned long address)
{

        return (unlikely(pgd_none(*pgd)) && __pud_alloc(mm, pgd, address))?
                NULL: pud_offset(pgd, address);
}

static inline pud_t * pud_offset(pgd_t * pgd, unsigned long address)
{

        return (pud_t *)pgd;

}

(页中间目录)

static inline int ioremap_pmd_range(pud_t *pud, unsigned long addr,
                unsigned long end, unsigned long phys_addr, pgprot_t prot)

{
        pud 为pgd

        pmd_t *pmd;

        unsigned long next;

        phys_addr -= addr;

        pmd = pmd_alloc(&init_mm, pud, addr);
        pmd 还是为 pgd
 
        if (!pmd)
                return -ENOMEM;

        do {

                next = pmd_addr_end(addr, end);

                #define pmd_addr_end(addr, end)                 (end)

                if (ioremap_pte_range(pmd, addr, next, phys_addr + addr, prot))
                        return -ENOMEM;

        } while (pmd++, addr = next, addr != end);

        return 0;

}

static inline pmd_t *pmd_alloc(struct mm_struct *mm, pud_t *pud, unsigned long address)
{

        return (unlikely(pud_none(*pud)) && __pmd_alloc(mm, pud, address))?
                NULL: pmd_offset(pud, address);
}

static inline pmd_t * pmd_offset(pud_t * pud, unsigned long address)
{

        return (pmd_t *)pud;

}

(页表) 开始干正经事了

static int ioremap_pte_range(pmd_t *pmd, unsigned long addr,
                unsigned long end, unsigned long phys_addr, pgprot_t prot)
{

        pte_t *pte;

        unsigned long pfn;

        pfn = phys_addr >> PAGE_SHIFT;
        EHCI 控制器总线地址的页框号

        pte = pte_alloc_kernel(pmd, addr);
        创建页表(如果不存在)
        pte 为线性地址addr 在页表中的地址
 
        if (!pte)
                return -ENOMEM;

        do {

                BUG_ON(!pte_none(*pte));

                set_pte_at(&init_mm, addr, pte, pfn_pte(pfn, prot));

                #define pfn_pte(pfn, prot)      __pte(((pfn) << PAGE_SHIFT) | pgprot_val(prot))

                很明显最后一个参数为 EHCI 的总线地址加上标志位(设置到页表中的地址为总线地址)              

                设置页表

                pfn++;

        } while (pte++, addr += PAGE_SIZE, addr != end);

        此时假设 EHCI映射到内存的 I/O MEM大小为 1024Kb,此处会循环设置

        return 0;

}
#define pte_alloc_kernel(pmd, address)                  \
        ((unlikely(!pmd_present(*(pmd))) && __pte_alloc_kernel(pmd, address))? \
                NULL: pte_offset_kernel(pmd, address))
int __pte_alloc_kernel(pmd_t *pmd, unsigned long address)
{

        pte_t *new = pte_alloc_one_kernel(&init_mm, address);
        申请一页页框作为页表

        if (!new)
                return -ENOMEM;

        spin_lock(&init_mm.page_table_lock);

        if (pmd_present(*pmd))          /* Another has populated it */
                pte_free_kernel(new);
        else
                pmd_populate_kernel(&init_mm, pmd, new);
                设置到页目录表中去(将页表地址填入到页目录中)

        spin_unlock(&init_mm.page_table_lock);

        return 0;

}
pte_t *pte_alloc_one_kernel(struct mm_struct *mm, unsigned long address)
{

        return (pte_t *)__get_free_page(GFP_KERNEL|__GFP_REPEAT|__GFP_ZERO);

}
#define pte_offset_kernel(dir, address) \
       ((pte_t *) pmd_page_vaddr(*(dir)) +  pte_index(address))

线性地址 addr 在页表中的地址

#define pmd_page_vaddr(pmd) \
                 ((unsigned long) __va(pmd_val(pmd) & PAGE_MASK))


#define pte_index(address) \
                (((address) >> PAGE_SHIFT) & (PTRS_PER_PTE - 1))

取线性地址的中间10位

#define PTRS_PER_PTE    1024

调用 ioremap_nocache() 函数之后,返回一个线性地址,此时 CPU 可以访问设备的内存(已经将其映射到了线性地址空间中了),此时 CPU 可以使用访问内存的指令访问设备的内存空间(host bridge 判断访问物理内存还是设备中的内存),此时我们就可以像访问内存一样来访问设备的内存(寄存器)!

现在我们就可以使用 readl() 或 writel() 函数读取或写入 EHCI 中 Capability Registers Operational Registers,此时我们就可以对EHCI 编程了!

注意:
PCI 设备的 I/O 寄存器 与 配置寄存器 的区别哦!

只提一点,后续的文章会有一部分专门介绍 PCI
对于任何一个PCI 设备 其内部
都有 I/O 寄存器 —-> 由cpu 直接访问 (writel or readl)
PCI 配置寄存器 —-> host bridge 直接访问 (pci_read_config_word)

真相在此:

分类: kernel, PCI 标签: , ,