第20章 Linux芯片级移植及底层驱动之SMP多核启动以及CPU热插拔驱动

20.4 SMP多核启动以及CPU热插拔驱动

    在Linux系统中,对于多核的ARM芯片,在Bootrom代码中,每个CPU都会识别自身ID,如果ID是0,则引导Bootloader和Linux内核执行,如果ID不是0,则Bootrom一般在上电时将自身置于WFI(WaitFortInerrupt)或者WFE(WaitForEvent)状态,并等待CPU0给其发CPU核间中断或事件(一般通过SEV指令)以唤醒它。一个典型的多核Linux启动过程如图20.6所示。

第20章 Linux芯片级移植及底层驱动之SMP多核启动以及CPU热插拔驱动_第1张图片

图20.6 一个典型的多核Linux启动过程

    被CPU0唤醒的CPUn可以在运行过程中进行热插拔,如运行如下命令即可卸载CPU1,并且将CPU1上的任务全部迁移到其他CPU中:

    echo 0 > /sys/devices/system/cpu/cpu1/online

    同理,运行如下命令可以再次启动CPU1

    echo 1 > /sys/devices/system/cpu/cpu1/online

    之后CPU1会主动参与系统中各个CPU之间要运行任务的负载均衡工作。

    CPU0唤醒其他CPU的动作在内核中被封装为一个smp_operations的结构体,对于ARM而言,定义于

arch/arm/include/asm/smp.h中。该结构体的成员函数如代码清单20.8所示。

    代码清单20.8 smp_operations结构体

struct smp_operations {
#ifdef CONFIG_SMP
        /*
         * Setup the set of possible CPUs (via set_cpu_possible)
         */
        void (*smp_init_cpus)(void);
        /*
         * Initialize cpu_possible map, and enable coherency
         */
        void (*smp_prepare_cpus)(unsigned int max_cpus);


        /*
         * Perform platform specific initialisation of the specified CPU.
         */
        void (*smp_secondary_init)(unsigned int cpu);
        /*
         * Boot a secondary CPU, and assign it the specified idle task.
         * This also gives us the initial stack to use for this CPU.
         */
        int  (*smp_boot_secondary)(unsigned int cpu, struct task_struct *idle);
#ifdef CONFIG_HOTPLUG_CPU
        int  (*cpu_kill)(unsigned int cpu);
        void (*cpu_die)(unsigned int cpu);
        bool  (*cpu_can_disable)(unsigned int cpu);
        int  (*cpu_disable)(unsigned int cpu);
#endif
#endif

};

    从arch/arm/mach-vexpress/v2m.c中看到VEXPRESS电路板用到的smp_ops()为vexpress_smp_ops:

DT_MACHINE_START(VEXPRESS_DT, "ARM-Versatile Express")
        .dt_compat = v2m_dt_match,
        .smp = smp_ops(vexpress_smp_ops),
        .map_io = v2m_dt_map_io,
        …

MACHINE_END

通过arch/arm/mach-vexpress/platsmp.c的实现代码可以看出,smp_operations的

成员函数smp_init_cpus(),即vexpress_smp_init_cpus()调用的ct_ca9x4_init_cpu_map()会探测SoC内CPU核的个数,并通过set_cpu_possible()设置这些CPU可见。

    而smp_operations的成员函数smp_prepare_cpus(),即vexpress_smp_prepare_cpus()则会通过
v2m_flags_set(virt_to_phys(versatile_secondary_startup))设置其他CPU的启动地址为

versatile_secondary_startup,如代码清单20.9所示。

代码清单20.9 在smp_prepare_cpus()中设置CPU1...n的启动地址

static void __init vexpress_smp_prepare_cpus(unsigned int max_cpus)
{
              …

             /*
               * Write the address of secondary startup into the
               * system-wide flags register. The boot monitor waits
               * until it receives a soft interrupt, and then the
               * secondary CPU branches to this address.
               */
            v2m_flags_set(virt_to_phys(versatile_secondary_startup));

}

    这部分的具体实现方法是与SoC相关的,由芯片的设计以及芯片内部的Bootrom决定。对于VEXPRESS来讲,设置方法如下:

void __init v2m_flags_set(u32 data)
{
        writel(~0, v2m_sysreg_base + V2M_SYS_FLAGSCLR);
        writel(data, v2m_sysreg_base + V2M_SYS_FLAGSSET);

}

    即填充v2m_sysreg_base+V2M_SYS_FLAGSCLR标记清除寄存器为0xFFFFFFFF,将CPU1...n初始启动执行的指令地址填入v2m_sysreg_base+V2M_SYS_FLAGSSET寄存器。这两个地址由芯片实现时内部的Bootrom程序设定的。填入CPU1...n的起始地址都通过virt_to_phys()转化为物理地址,因为此时CPU1...n的MMU尚未开启。

    比较关键的是smp_operations的成员函数smp_boot_secondary(),对于本例为

versatile_boot_secondary(),它完成CPU的最终唤醒工作,如代码清单20.10所示。

代码清单20.10 CPU0通过中断唤醒其他CPU

static void write_pen_release(int val)
{
        pen_release = val;
        smp_wmb();
        sync_cache_w(&pen_release);

}

int versatile_boot_secondary(unsigned int cpu, struct task_struct *idle)
{
        unsigned long timeout;
        ...
        /*
         * This is really belt and braces; we hold unintended secondary
         * CPUs in the holding pen until we ’ re ready for them. However,
         * since we haven ’ t sent them a soft interrupt, they shouldn ’ t
         * be there.
         */
        write_pen_release(cpu_logical_map(cpu));//将pen_release变量设置为要唤醒的CPU核的CPU号
cpu_logical_map(cpu),

        /*
         * Send the secondary CPU a soft interrupt, thereby causing
         * the boot monitor to read the system wide flags register,
         * and branch to the address found there.
         */
        arch_send_wakeup_ipi_mask(cpumask_of(cpu));//给要唤醒的CPU发IPI中断

        timeout = jiffies + (1 * HZ);
        while (time_before(jiffies, timeout)) {
                 smp_rmb();
                 if (pen_release == -1)
                         break;

                 udelay(10);
        }
        ...
        return pen_release != -1 -ENOSYS : 0;

}

versatile_secondary_startup实现于arch/arm/plat-versatile/headsmp.S中,是汇编,如代码清单20.11所示。

代码清单20.11 被唤醒CPU的执行入口

1ENTRY(versatile_secondary_startup)
2 mrc p15, 0, r0, c0, c0, 5
3 and r0, r0, #15
4 adr r4, 1f
5 ldmia r4, {r5, r6}
6 sub r4, r4, r5
7 add r6, r6, r4
8pen: ldr r7, [r6]
9 cmp r7, r0
10 bne pen
11
12 /*
13 * we ’ ve been released from the holding pen: secondary_stack
14 * should now contain the SVC stack for this core
15 */
16 b secondary_startup
17
18 .align
191: .long .
20 .long pen_release

21ENDPROC(versatile_secondary_startup)

分析:

    上述代码第8~10行的循环是等待pen_release变量成为CPU0设置的cpu_logical_map(cpu),一般直接
就成立了。第16行则调用内核通用的secondary_startup()函数,经过一系列的初始化(如MMU等),最
终新的被唤醒的CPU将调用smp_operations的smp_secondary_init()成员函数,对于本例为

versatile_secondary_init(),如代码清单20.12所示。

代码清单20.12 被唤醒的CPU恢复pen_release()

void versatile_secondary_init(unsigned int cpu)
{
         /*
          * let the primary processor know we ’ re out of the
          * pen, then head off into the C entry point
          */
         write_pen_release(-1);
         /*
         * Synchronise with the boot thread.
         */
         spin_lock(&boot_lock);
         spin_unlock(&boot_lock);

}

    上述代码第会将pen_release写为-1,于是CPU0还在执行的代码清单20.10里versatile_boot_secondary()函数中的如下循环就退出了:

    while (time_before(jiffies, timeout)) {
            smp_rmb();
            if (pen_release == -1)
                break;
            udelay(10);

    }

    这样CPU0就知道目标CPU已经被正确地唤醒,此后CPU0和新唤醒的其他CPU各自运行。整个系统在运行过程中会进行实时进程和正常进程的动态负载均衡。

    图20.7描述上文提到的vexpress_smp_prepare_cpus()、versatile_boot_secondary()、

write_pen_release()、versatile_secondary_startup()、versatile_secondary_init()等函数的执行顺序。

    第20章 Linux芯片级移植及底层驱动之SMP多核启动以及CPU热插拔驱动_第2张图片

图20.7 CPU0唤醒其他CPU过程

CPU热插拔的实现是与芯片相关的,对于VEXPRESS,实现了smp_operations的cpu_die()成员函数,即vexpress_cpu_die()。它会在进行CPUn的拔除操作时将CPUn投入低功耗的WFI状态,相关代码位于arch/arm/mach-vexpress/hotplug.c中,如代码清单20.13所示。

代码清单20.13 smp_operations的cpu_die()成员函数

static inline void platform_do_lowpower(unsigned int cpu, int *spurious)
{
/*
* there is no power-control hardware on this platform, so all
* we can do is put the core into WFI; this is safe as the calling
* code will have already disabled interrupts
*/
for (;;) {
wfi();
if (pen_release == cpu_logical_map(cpu)) {
/*
* OK, proper wakeup, we're done
*/
break;
}
/*
* Getting here, means that we have come out of WFI without
* having been woken up - this shouldn't happen
*
* Just note it happening - when we're woken, we can report
* its occurrence.
*/
(*spurious)++;
}
}

/*
 * platform-specific code to shutdown a CPU
 *
 * Called with IRQs disabled
 */
void vexpress_cpu_die(unsigned int cpu)
{
int spurious = 0;

/*
* we're ready for shutdown now, so do it
*/
cpu_enter_lowpower();
platform_do_lowpower(cpu, &spurious);

/*
* bring this CPU back into the world of cache
* coherency, and then restore interrupts
*/
cpu_leave_lowpower();

if (spurious)
pr_warn("CPU%u: %u spurious wakeup calls\n", cpu, spurious);
}

分析:

        CPUn睡眠于wfi(),之后再次在线的时候,又会因为CPU0给它发出的IPI而从wfi()函数返回继续执行,醒来时CPUn也判断“pen_release==cpu_logical_map(cpu)”是否成立,以确定该次醒来确实是由
CPU0唤醒的一次正常醒来。

你可能感兴趣的:(Linux驱动开发)