SMP是對稱多處理器的意思。Intel為SMP特地出臺了一個MultiProcessor Specification,現(xiàn)在使用最多的就是1997年5月的1.4版。里面規(guī)范了如何設(shè)置中斷控制器,BSP如何啟動其他的AP,達到SMP。
X86多核系統(tǒng)啟動的時候,硬件會選出一個BSP專門完成系統(tǒng)的引導(dǎo)工作,因此說BIOS是一般是運行在單核狀態(tài),grub也是。這個BSP在引導(dǎo)階段要做的很重要的工作就是檢查和配置系統(tǒng)的設(shè)備(特別是磁盤相關(guān)的HBA硬件),掃描并且初始化PCI樹,初始化內(nèi)存。因此當系統(tǒng)變得非常龐大、資源很多的時候,例如高端的服務(wù)器平臺,這個初始化的時間就非常長了。
具體有多長呢?舉一個EMC DD9XXX產(chǎn)品的例子吧,在512GB RAM,12個PCIe Gen2的卡全配的情況下,系統(tǒng)從按下power button到開到grub菜單,大約需要15-20分鐘!也就是差不多1/4個小時就用來開機了。這也是為什么服務(wù)器平臺通常不建議重啟的原因,因為開關(guān)一次的代價實在是太大太大了。
上面提到的是硬件的SMP,就是系統(tǒng)當中存在同質(zhì)的多個處理器。
在Intel的平臺下,BSP在工作的時候,其他處理器(AP)處于wait startup IPI狀態(tài)。BSP在初始化了基本硬件資源之后,通過APIC首先發(fā)送INIT IPI給其他的AP,然后等待10ms;接下來發(fā)送一個STARTUP IPI,再等待200us;最后再發(fā)送一個STARTUP IPI,再等待200us。之所以會有第二個STARTUP IPI,據(jù)說是為了fix一些CPU的bug。大部分情況下,第一個STARTUP IPI就足夠了。BSP需要告知AP從哪里開始運行。
接下來說軟件。Linux在2.6就開始支持多處理器SMP。那么這里說的軟件的SMP,就是說同樣的操作系統(tǒng)內(nèi)核(Image)并發(fā)跑在多個物理處理器上。
Linux支持SMP是follow Intel的規(guī)范,包括上面提到的IPI中斷。Linux的啟動SMP的過程總結(jié)下來就是下面的過程。
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發(fā)送STARTUP IPI之后會等待AP置位cpu_initialized_mask。
AP啟動之后會把自己對應(yīng)的bit置為1,然后等待BSP設(shè)置cpu_callout_mask。
此時BSP知道該AP已經(jīng)啟動,然后設(shè)置cpu_callout_mask的相應(yīng)位,之后等待AP設(shè)置cpu_callin_mask。
AP發(fā)現(xiàn)cpu_callout_mask被設(shè)置,于是進行剩余的啟動工作。然后設(shè)置cpu_callin_mask的相應(yīng)位。
BSP檢測到cpu_callin_mask被設(shè)置之后,完成該AP的初始化,開始下一個AP的初始化操作。
這里面其實有個地方?jīng)]有詳細展開,那就是AP的啟動的entry point。這個是由BSP設(shè)置的一段代碼,由STARTUP IPI發(fā)送給AP。AP運行這段代碼,完成從實模式開始的啟動過程直到32bit保護模式/64bit長模式。這段代碼在Linux里面叫做trampoline,中文是蹦床的意思。
(其實在前一篇IoT產(chǎn)品架構(gòu)設(shè)計當中提到了一個獨創(chuàng)性的在線升級,把代碼從ROM搬移并跳轉(zhuǎn)到RAM中運行,也可以說使用了一種特殊的蹦床的技術(shù))。
關(guān)于Linux SMP的具體流程可以參考代碼。trampoline是純匯編,有興趣的也可以去看看它的原理。
剛才討論的是SMP的引導(dǎo)部分。在Linux運行過程中,有些時候也需要讓某一個或者幾個CPU完成一些特定的操作,這個時候也需要通過IPI。Linux內(nèi)核為此做了封裝,例如
/*
* 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);
具體的用法網(wǎng)上大把大把的,這里不舉例了。
但是需要注意的是,剛才提到了這些實際上是通過IPI來實現(xiàn)的,所以接收的CPU是在中斷上下文運行的特定函數(shù)(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)。
我自己實現(xiàn)了類似的操作。
總結(jié)一下,硬件和操作系統(tǒng)(Linux)都為SMP的支持做了必要的準備,不管是boot up還是run-time。
仔細想想,好像還有一個什么地方有些奇怪。為什么SMP的初始化是由Linux完成的?服務(wù)器平臺開機15-20分鐘難道就沒有什么好辦法加速?
其實,這就是問為什么BIOS不用SMP來開機,而是UP。這個我也不知道確切的答案,或許是因為復(fù)雜性,或許是因為靈活性。這些有待專家給出來吧。
下一篇講講Linux X86的中斷吧,應(yīng)該會很短小。