SMP是对称多处理器的意思。Intel为SMP特地出台了一个MultiProcessor Specification,现在使用最多的就是1997年5月的1.4版。里面规范了如何设置中断控制器,BSP如何启动其他的AP,达到SMP。
X86多核系统启动的时候,硬件会选出一个BSP专门完成系统的引导工作,因此说BIOS是一般是运行在单核状态,grub也是。这个BSP在引导阶段要做的很重要的工作就是检查和配置系统的设备(特别是磁盘相关的HBA硬件),扫描并且初始化PCI树,初始化内存。因此当系统变得非常庞大、资源很多的时候,例如高端的服务器平台,这个初始化的时间就非常长了。
具体有多长呢?举一个EMC DD9XXX产品的例子吧,在512GB RAM,12个PCIe Gen2的卡全配的情况下,系统从按下power button到开到grub菜单,大约需要15-20分钟!也就是差不多1/4个小时就用来开机了。这也是为什么服务器平台通常不建议重启的原因,因为开关一次的代价实在是太大太大了。
上面提到的是硬件的SMP,就是系统当中存在同质的多个处理器。
在Intel的平台下,BSP在工作的时候,其他处理器(AP)处于wait startup IPI状态。BSP在初始化了基本硬件资源之后,通过APIC首先发送INIT IPI给其他的AP,然后等待10ms;接下来发送一个STARTUP IPI,再等待200us;最后再发送一个STARTUP IPI,再等待200us。之所以会有第二个STARTUP IPI,据说是为了fix一些CPU的bug。大部分情况下,第一个STARTUP IPI就足够了。BSP需要告知AP从哪里开始运行。
接下来说软件。Linux在2.6就开始支持多处理器SMP。那么这里说的软件的SMP,就是说同样的操作系统内核(Image)并发跑在多个物理处理器上。
Linux支持SMP是follow Intel的规范,包括上面提到的IPI中断。Linux的启动SMP的过程总结下来就是下面的过程。
do_boot_cpu() [ setup start_secondary() as entry point ]
|-> wakeup_xxxx_via_init_nmi() [ runs start_secondary in APs ]
|-> cpu_init() [ initializes AP ]
|-> wait_for_master_cpu() [ set xxx_initialized_mask, then pends on xxx_callout_mask ]
|-> BSP set xxx_callout_mask, to continue AP's cpu_init(), pends on xxx_callin_mask ]
也就是说这里定义了三个bitmap,其中每一位代表一个逻辑CPU(HT,或者core)。
BSP发送STARTUP IPI之后会等待AP置位cpu_initialized_mask。
AP启动之后会把自己对应的bit置为1,然后等待BSP设置cpu_callout_mask。
此时BSP知道该AP已经启动,然后设置cpu_callout_mask的相应位,之后等待AP设置cpu_callin_mask。
AP发现cpu_callout_mask被设置,于是进行剩余的启动工作。然后设置cpu_callin_mask的相应位。
BSP检测到cpu_callin_mask被设置之后,完成该AP的初始化,开始下一个AP的初始化操作。
这里面其实有个地方没有详细展开,那就是AP的启动的entry point。这个是由BSP设置的一段代码,由STARTUP IPI发送给AP。AP运行这段代码,完成从实模式开始的启动过程直到32bit保护模式/64bit长模式。这段代码在Linux里面叫做trampoline,中文是蹦床的意思。
(其实在前一篇IoT产品架构设计当中提到了一个独创性的在线升级,把代码从ROM搬移并跳转到RAM中运行,也可以说使用了一种特殊的蹦床的技术)。
关于Linux SMP的具体流程可以参考代码。trampoline是纯汇编,有兴趣的也可以去看看它的原理。
刚才讨论的是SMP的引导部分。在Linux运行过程中,有些时候也需要让某一个或者几个CPU完成一些特定的操作,这个时候也需要通过IPI。Linux内核为此做了封装,例如
/*
* Call a function on all other processors
*/
void smp_call_function(smp_call_func_t func, void *info, int wait);
void smp_call_function_many(const struct cpumask *mask,
smp_call_func_t func, void *info, bool wait);
int smp_call_function_any(const struct cpumask *mask,
smp_call_func_t func, void *info, int wait);
具体的用法网上大把大把的,这里不举例了。
但是需要注意的是,刚才提到了这些实际上是通过IPI来实现的,所以接收的CPU是在中断上下文运行的特定函数(Linux-4.4.30, Ubuntu-16.04 X86_64 server),例如。
void print_cpu_id(void * cpuid)
{
int cpu=smp_processor_id();
printk("Called: myid %d\n",cpu);
printk("Called: myid %d\n",cpu);
printk("Called: in_int=%d, in_irq=%d, in_softirq=%d\n",
in_interrupt(), in_irq(), in_softirq());
return;
}
static int __init hello_world_init(void)
{
int cpu=0;
flag=0;
printk("hello_world_init\n");
cpu=smp_processor_id();
printk("Caller: myid is %d\n",cpu);
smp_call_function(print_cpu_id, &cpu, 0);
。。。
}
[447099.229034] Called: myid 0
[447099.229037] Called: in_int=65536, in_irq=65536, in_softirq=0
因此需要记住中断上下文里的一些使用禁忌。
如果不能避免这样的禁忌,那就需要用别的方法来做,例如可以参考Linux中断处理的路子(top half和bottom half,softirq, tasklet, workqueue)。
我自己实现了类似的操作。
总结一下,硬件和操作系统(Linux)都为SMP的支持做了必要的准备,不管是boot up还是run-time。
仔细想想,好像还有一个什么地方有些奇怪。为什么SMP的初始化是由Linux完成的?服务器平台开机15-20分钟难道就没有什么好办法加速?
其实,这就是问为什么BIOS不用SMP来开机,而是UP。这个我也不知道确切的答案,或许是因为复杂性,或许是因为灵活性。这些有待专家给出来吧。
下一篇讲讲Linux X86的中断吧,应该会很短小。