Android 筆記-Linux Kernel SMP (Symmetric Multi-Processors) 開機流程解析 Part(3) Linux 多核心啟動流程從rest_init到ker

  承襲之前的內容,本文會先把resr_init到kernel_init的流程做一個說明. 並針對Secondary處理器Booting,CPU Idle省電機制與CPU Up/Down HotPlug機製做介紹. 而屬於核心 Kthreadd機制,會放到下次的文章中.

 

            Linux Kernel針對CPU Idle與對應的省電機制,已經有滿不錯的框架,只要處理器平臺開發者,根據自己的處理器架構,Power Group與對應的周邊適配進來,就可以達到省電最佳化的效益. 尤其,在多核心SMP架構上,Idle的省電機制在Linux Kernel中待改善的部分還非常多,這也是因為每個ARM SMP架構與周邊都會因為平臺提供者的設計差異,而不容易標準化(畢竟這不是一家處理器業者獨大的市場). 要達到SMP多核心最佳的省電效益,就必須在基於對核心流程有清楚掌握下,針對要優化的平臺來做調整,讓系統可以有更多的機會關閉對應的周邊供電與Clock,纔有機會達到最佳化的省電效益.

 

            Android除了TV產品外,多數的應用都是屬於移動裝置,而這些裝置對於省電需求,是非常殷切的. 以目前的平板電腦來說,不同的系統廠商採用同一個雙核心的晶片,最終所設計的產品,在待機時間上,兩者就可以有數倍的差距(mmm,商品名稱就別提了..@_@),其中很大的差異就是對於所採用平臺的掌握度是不是足夠高,以及對於系統軟體的投注程度.

 

            由於筆者時間受限,本系列文章會分次刊登,還請見諒.

 

 

 

rest_init

            這函式是Kernel初始化的最後一棒,負責的Bootstrap處理器,也會在這之後進入CPU Idle狀態,並讓系統行程Scheduling正常運作,也是我們瞭解Kernel時,值得清楚掌握的部份.

 

 

rest_init 流程

說明

rcu_scheduler_starting

實作在檔案kernel/rcutree.c中,

啟動Read-Copy Update,會呼叫num_online_cpus確認目前只有bootstrap處理器在運作,以及呼叫nr_context_switches確認在啟動RCU前,沒有進行過Contex-Switch,最後就是設定rcu_scheduler_active=1啟動RCU機制.

RCU在多核心架構下,不同的行程要讀取同一筆資料內容/結構,可以提供高效率的同步與正確性.

在這之後就可以使用 rcu_read_lock/rcu_read_unlock了.

產生Kernel Thread

kernel_init

Kernel Thread函式 kernel_init實作在檔案init/main.c中,

init Task PID=1,是核心第一個產生的Task.

產生後,會停在函式呼叫wait_for_completion中,等待kthreadd_done Signal,以便往後繼續執行下去.

 

產生Kernel Thread

kthreadd

Kernel Thread函式 kthreadd實作在檔案kernel/kthread.c中,

kthreadd Task PID=2,是核心第二個產生的Task.

 

 

find_task_by_pid_ns

實作在檔案kernel/pid.c中,

呼叫函式find_task_by_pid_ns,並帶入參數 kthreadd的PID 2與PID NameSpace (struct pid_namespace init_pid_ns)取回PID 2的Task Struct.

complete

實作在檔案kernel/sched.c中,

會送出kthreadd_done Signal,讓 kernel_init(也就是 init task)可以往後繼續執行.

init_idle_bootup_task

實作在檔案kernel/sched.c中,

設定目前啟動的Task為IDLE Task. (idle->sched_class = &idle_sched_class), 而struct sched_class idle_sched_class的宣告在檔案kernel/sched_idletask.c中.

在Linux下IDLE Task並不佔PID(也可以把它當作是PID 0),每個處理器都會有這樣的IDLE Task,用來在沒有行程排成時,讓處理器掉入執行的.而最基礎的省電機制,也可透過IDLE Task來進行. (包括讓系統可以關閉必要的周邊電源與Clock Gating).

schedule();

實作在檔案kernel/sched.c中,

//preempt_enable_no_resched/preempt_disable();

啟動Linux Kernel Process的排成Context-Switch機制.

cpu_idle();

實作在檔案arch/arm/kernel/process.c中,

這是處理器IDLE Task的主函式,我們會在稍後進一步說明,走到這,屬於Booting到IDLE Task的流程就算是初步結束了.

 

 

 

 

 

           

 

 

kernel_init

 

            實作在檔案 init/main.c中,這是Linux Kernel產生的第一個Tasks,也是User Mode起點init Task的產生者,所有User Space的初始化工作,包括Shell與相關的Script執行,都必須仰賴init Task,簡要說明如下

 

 

kernel_init
初始化函式的流程

說明

wait_for_completion

實作在檔案kernel/sched.c中,

會呼叫函式wait_for_completion,等待Kernel Thread kthreadd (PID=2)產生完畢.

init can run on any cpu

set_cpus_allowed_ptr

實作在檔案kernel/sched.c中,

透過這函式可以設定CPU bitmask,限定Task只能在特定的處理器上運作.而在init中,會設定為cpu_all_mask (= to_cpumask(cpu_all_bits)),

也就是 init Task可以在所有處理器上運作.

cad_pid = task_pid(current);  

呼叫task_pid (以inline實作在檔案include/linux/sched.h中),設定目前current的init Task PID給cad_pid (也就是要用來接收”ctrl-alt-del” Reboot Signal

的Process ID, 如果設定C_A_D=1,就表示可以處理來自”ctrl-alt-del”的動作.).

最後會呼叫函式ctrl_alt_del (in kernel/sys.c),並確認C_A_D是否為1,以便執行kernel_restart流程.

smp_prepare_cpus

實作在檔案arch/arm/kernel/smp.c中,

呼叫函式smp_prepare_cpus時,會以全域變數setup_max_cpus為函式參數max_cpus,用以表示在編譯核心時,設定支援的最大CPU數量.

首先,會透過函式num_possible_cpus(=cpumask_weight(cpu_possible_mask) ,in include/linux/cpumask.h)取得目前系統存在的處理器數量,並呼叫

函式smp_store_cpu_info,把透過calibrate_delay計算的loops_per_jiffy存到目前處理器的cpu_info (宣告為struct cpuinfo_arm *)中.

而Kernel中的全域變數setup_max_cpus初始值為NR_CPUS (參考檔案include/linux/threads.h 會以CONFIG_NR_CPUS 為值),如果編譯時設定的處理器

個數大於運作時取得的處理器個數(if (max_cpus > ncores) ),會以運作時偵測到的處理器個數為主.

呼叫函式percpu_timer_setup,設定目前處理器的Local Timer.

呼叫函式platform_smp_prepare_cpus (in arch/arm/mach-tegra/platsmp.c),依據max_cpus結果,透過函式set_cpu_present (in kernel/cpu.c),設定這些

處理器為present true.

do_pre_smp_initcalls

實作在檔案init/main.c中,

會透過函式do_one_initcall,執行介於Symbol  __initcall_start與__early_initcall_end之間的函式呼叫,

如下為arch/arm/kernel/vmlinux.lds中的Symbol區間內容,

__initcall_start = .; *(.initcallearly.init) __early_initcall_end = .;

以筆者編譯的結果來說,會執行有透過early_initcall 註冊的函式,例如

spawn_ksoftirqd  (in kernel/softirq.c),

init_workqueues (in kernel/workqueue.c),

init_call_single_data (in kernel/smp.c),

cpu_stop_init (in kernel/stop_machine.c),

….etc

smp_init

實作在檔案kernel/smp.c中,

這函式主要是由Bootstrap處理器,進行Active多核心架構下其它的處理器.

如果發生Online的處理器個數(from num_online_cpus)超過在覈心編譯時,所設定的最大處理器個數 setup_max_cpus (from NR_CPUS),就會終止流程.

如果該處理器目前屬於Present (也就是存在系統中),但尚未是Online的狀態,就會呼叫函式cpu_up(in kernel/cpu.c)來啟動該處理器.

 

sched_init_smp

實作在檔案kernel/sched.c中,

 

1,呼叫get_online_cpus,如果目前CPU Hotplug Active Write行程是自己,就直接返回.反之就把 cpu_hotplug.refcount加1 (表示多一個Reader)

2,取得Mutex Lock “sched_domains_mutex”

3,呼叫arch_init_sched_domains,設定scheduler domains與groups,參考Linux Documentation/scheduler/sched-domains.txt文件,一個Scheduling

 Domain會包含一個或多個CPU Groups,排程的Load-Balance就會根據Domain中的Groups來做調整.

4,釋放Mutex Lock “sched_domains_mutex”

5,呼叫put_online_cpus,如果目前CPU Hotplug Active Writer行程是自己,就直接返回.反之就把 cpu_hotplug.refcount減1,如果 cpu_hotplug.refcount減到為0,

表示沒有其他Reader,此時如果有CPU Hotplug Active Writer行程在等待,就會透過wake_up_process喚醒該行程,以便讓等待中的Writer可以被執行下去.

(也可以參考_cpu_up中對於函式cpu_hotplug_begin的說明).

6,註冊CPU Notifier cpuset_cpu_active/cpuset_cpu_inactive/update_runtime

7,呼叫set_cpus_allowed_ptr,透過這函式可以設定CPU bitmask,限定Task只能在特定的處理器上運作.在這會用參數”non_isolated_cpus”,也就是會把init指

定給non-isolated CPU. Linux Kernel可以在啟動時,透過Boot Parameters “isolcpus=“指定CPU編號或是範圍,讓這些處理器不被包含在Linux Kernel SMP

 balancing/scheduling算法內,可以在啟動後指派給特定的Task運作.而不在 “isolcpus=“ 指定範圍內的處理器就算是non-isolated CPU.

8,呼叫sched_init_granularity,透過函式update_sysctl,讓

sysctl_sched_min_granularity=normalized_sysctl_sched_min_granularity,sysctl_sched_latency=normalized_sysctl_sched_latency,

sysctl_sched_wakeup_granularity=normalized_sysctl_sched_wakeup_granularity.

do_basic_setup

實作在檔案init/main.c中,

1,呼叫usermodehelper_init (in kernel/kmod.c),產生khelper workqueue.

2,呼叫init_tmpfs (in mm/shmem.c),對VFS註冊Temp FileSystem.

3,呼叫driver_init (in drivers/base/init.c),初始化Linux Kernel Driver System Model.

4,呼叫init_irq_proc(in kernel/irq/proc.c),初始化 “/proc/irq”與其下的File Nodes.

5,呼叫do_ctors (in init/main.c),執行位於Symbol __ctors_start 到 __ctors_end間屬於Section  “.ctors” 的Constructor函式.

6,透過函式do_initcalls,執行介於Symbol  __early_initcall_end與__initcall_end之間的函式呼叫,

如下為arch/arm/kernel/vmlinux.lds中的Symbol區間內容,

__early_initcall_end = .; *(.initcall0.init) *(.initcall0s.init) *(.initcall1.init) *(.initcall1s.init) *(.initcall2.init) *(.initcall2s.init) *(.initcall3.init) *(.initcall3s.init) *

(.initcall4.init) *(.initcall4s.init) *(.initcall5.init) *(.initcall5s.init) *(.initcallrootfs.init) *(.initcall6.init) *(.initcall6s.init) *(.initcall7.init) *(.initcall7s.init)

 __initcall_end = .;

以筆者編譯的結果來說,會執行有透過__initcall 註冊的函式,例如

(0….added by pure_initcall....)

init_atomic64_lock

(1….added by core_initcall....)

ptrace_break_init

consistent_init

v6_userpage_init

alloc_frozen_cpus

sysctl_init

ksysfs_init

init_jiffies_clocksource

pm_init

init_zero_pfn

fsnotify_init

filelock_init

init_script_binfmt

nit_elf_binfmt

random32_init

(2….added by postcore_initcall....)

tegra_gpio_init

tegra_dma_init

bdi_class_init

tty_class_init

vtconsole_class_init

wakeup_sources_debugfs_init

(3….added by arch_initcall....)

customize_machine

exceptions_init

(4….added by subsys_initcall....)

topology_init4

param_sysfs_init4

default_bdi_init4

init_bio4

fsnotify_notification_init4

blk_settings_init4

blk_ioc_init4

blk_softirq_init4

blk_iopoll_setup4

genhd_device_init4

misc_init4

serio_init4

input_init4

hwmon_init4

(5….added by fs_initcall....)

proc_cpu_init5

dma_debug_do_init5

alignment_init5

clocksource_done_booting5

init_pipe_fs5

eventpoll_init5

anon_inode_init5

blk_scsi_ioctl_init5

chr_dev_init5

firmware_class_init5

(rootfs….added by rootfs_initcall....)

default_rootfs

(6….added by device_initcall....)

timer_init_sysfs6

register_pmu_driver6

proc_execdomains_init6

ioresources_init6

uid_cache_init6

init_posix_timers6

init_posix_cpu_timers6

nsproxy_cache_init6

timekeeping_init_ops6

init_clocksource_sysfs6

init_timer_list_procfs6

futex_init6

kallsyms_init6

user_namespaces_init6

pid_namespaces_init6

utsname_sysctl_init6

init_per_zone_wmark_min6

kswapd_init6

setup_vmstat6

mm_sysfs_init6

proc_vmalloc_init6

procswaps_init6

slab_proc_init6

slab_sysfs_init6

fcntl_init6

proc_filesystems_init6

fsnotify_mark_init6

dnotify_init6

inotify_user_setup6

aio_setup6

proc_locks_init6

proc_cmdline_init6

proc_consoles_init6

proc_cpuinfo_init6

proc_devices_init6

proc_interrupts_init6

proc_loadavg_init6

proc_meminfo_init6

proc_stat_init6

proc_uptime_init6

proc_version_init6

proc_softirqs_init6

proc_kmsg_init6

proc_page_init6

init_devpts_fs6

init_ramfs_fs6

proc_genhd_init6

bsg_init6

noop_init6

deadline_init6

cfq_init6

percpu_counter_startup6

pty_init6

rand_initialize6

topology_sysfs_init6

fusb300_udc_init6

init6

serport_init6

mousedev_init6

atkbd_init6

psmouse_init6

hid_init6

(7….added by late_initcall....)

init_oops_id7

printk_late_init7

pm_qos_power_init7

max_swapfiles_check7

random32_reseed7

seqgen_init7

sys_open

透過sys_open以Read/Write模式開啟Console "/dev/console",

由於系統在此時沒有任何檔案開啟,這個返回一定是 "0". (也就是對應到stdin).

sys_dup(0)

實作在檔案fs/fcntl.c中,

宣告為"SYSCALL_DEFINE1(dup, unsigned int, fildes)",

在這會連續執行兩次sys_dup,複製兩個sys_open開啟/dev/console所產生的檔案描述0 (也就是會多生出兩個1與2),只是都對應到"/dev/console",我們在

System V streams下的Standard Stream一般而言會有如下的對應

0:Standard input (stdin)

1:Standard output (stdout)

2:Standard error (stderr)

(為方便大家參考,附上Wiki URL http://en.wikipedia.org/wiki/Standard_streams )                              

 

ramdisk_execute_command與prepare_namespace

 

1,如果ramdisk_execute_command為0,就設定ramdisk_execute_command = "/init"

2,如果sys_access確認檔案ramdisk_execute_command 失敗,就把ramdisk_execute_command 設定為0,然後呼叫prepare_namespace去mount root

 FileSystem.

init_post

實作在檔案kernel/main.c中

1,呼叫async_synchronize_full (in kernel/async.c),用以同步所有非同步函式呼叫的執行,在這函式中會等待List async_running與async_pending都清空後,

才會返回. Asynchronously called functions主要設計用來加速Linux Kernel開機的效率,避免在開機流程中等待硬體反應延遲,影響到開機完成的時間.

2,呼叫free_initmem (in arch/arm/mm/init.c),釋放Linux Kernel介於__init_begin到 __init_end屬於 init Section的程式碼與資料.並會把Page個數加到變數

totalram_pages中,作為後續Linux Kernel在配置記憶體時可以使用的Pages. (在這也可把TCM範圍(__tcm_start到__tcm_end)釋放加入到

總Page中,但TCM比外部記憶體有效率,適合多媒體,中斷,...etc等對效能要求高的執行需求,放到總Page中,成為可供一般目的配

置的記憶體範圍,mmm,是有點浪費的.)

3,設定system_state 為 SYSTEM_RUNNING,與設定init Task的”SIGNAL_UNKILLABLE” Signal Bit. (

4,產生第一個User Space行程.

  4.a,如果ramdisk_execute_command不為0,就執行該命令成為init User Process.

  4.b,如果execute_command不為0,就執行該命令成為init User Process.

  4.c,如果上述都不成立,就依序執行如下指令

 

run_init_process("/sbin/init");

run_init_process("/etc/init");

run_init_process("/bin/init");

run_init_process("/bin/sh");

也就是說會試著從/sbin/init, /etc/init, /bin/init 與 /bin/sh依序執行嘗試執行第一個 init User Process.

5,如果都找不到可以執行的 init Process,就會進入Kernel Panic.如下所示

panic("No init found.  Try passing init= option to kernel. "

              "See Linux Documentation/init.txt for guidance.");

 

 

有關IDLE Task, init Task 與 kthreadd Task的啟動流程與關係,如下圖所示

 

 

 

CPU Mask

 

            Linux Kernel針對多核心的需求,提供了對應處理器狀態的CPU Mask Bitmap,參考kernel/cpu.c中的實作,

 

const DECLARE_BITMAP(cpu_all_bits, NR_CPUS) = CPU_BITS_ALL;

static DECLARE_BITMAP(cpu_possible_bits, CONFIG_NR_CPUS) __read_mostly;

static DECLARE_BITMAP(cpu_online_bits, CONFIG_NR_CPUS) __read_mostly;

static DECLARE_BITMAP(cpu_present_bits, CONFIG_NR_CPUS) __read_mostly;

static DECLARE_BITMAP(cpu_active_bits, CONFIG_NR_CPUS) __read_mostly;  

 

            有關CPU狀態的CPU Mask會包含以下五種屬性

            (也可以參考Linux Documentation/cpu-hotplug.txt文件)

 

            1,cpu_all_bits: 用以表示目前透過NR_CPUS設定的處理器Bits,例如系統中有4個處理器此值初始化為0x0000000f,如果有兩個處理器此值初始化為0x00000003.

            2,cpu_possible_bits:這表示系統實際Run-Time時,存在的處理器個數(cpu_all_bits是編譯時期指定的最大處理器個數),在系統初始化的過程中,會根據偵測到的處理器數量透過函式set_cpu_possible設定哪些Bits要為1. (如果有兩個處理器就是0x00000003). (set_cpu_possible會再透過 cpumask_set_cpu或  cpumask_clear_cpu設定對應Bit的值).

            3,cpu_online_bits:用以表示哪個處理器目前是Online(也就是可以正常使用並參與排程的與處理中斷).當處理器進行 CPU Up/Down時,就會更改Online Bits的狀態.

            4,cpu_present_bits: 用以表示目前系統中Present的處理器個數,且並非所有屬於Present的處理器,目前都是Online可供系統排程與處理中斷的. 通常Present跟Possible的處理器個數會是相等的.在有支援處理器熱插拔的環境下,Possible跟Present的關係為 ”cpu_possible_map = cpu_present_map + additional_cpus”,也就是說Present是指系統非外部額外加入的處理器數量,而Possible則會包含Present與額外插入的處理器數量.

            5,cpu_active_bits:用以表示目前有哪些處於Online,且為Active的處理器可供scheduler 依據domains/groups進行排程配置.

 

           

            相關巨集的定義,可以在include/linux/types.h中,看到如下宣告

 

#define DECLARE_BITMAP(name,bits) \

        unsigned long name[BITS_TO_LONGS(bits)]

 

            與在include/linux/bitmap.h中,如下的宣告

 

#define BITMAP_LAST_WORD_MASK(nbits)                                    \

(                                                                       \

        ((nbits) % BITS_PER_LONG) ?                                     \

                (1UL<<((nbits) % BITS_PER_LONG))-1 : ~0UL               \

)

            與在include/linux/cpumask.h中,如下的宣告

 

#define CPU_MASK_LAST_WORD BITMAP_LAST_WORD_MASK(NR_CPUS)

 

#if NR_CPUS <= BITS_PER_LONG

#define CPU_BITS_ALL                                            \

{                                                               \

        [BITS_TO_LONGS(NR_CPUS)-1] = CPU_MASK_LAST_WORD \

}

                                                                               

#else /* NR_CPUS > BITS_PER_LONG */

                                                                               

#define CPU_BITS_ALL                                            \

{                                                               \

        [0 ... BITS_TO_LONGS(NR_CPUS)-2] = ~0UL,                \

        [BITS_TO_LONGS(NR_CPUS)-1] = CPU_MASK_LAST_WORD         \

}

#endif /* NR_CPUS > BITS_PER_LONG */

 

            以筆者所在的環境來說,NR_CPUS設定為4,則 cpu_all_bits為一個unsigned long (也就是4bytes)初始值為0x0000000f,其它cpu_possible_bits,cpu_online_bits,cpu_present_bits與cpu_active_bits長度為為一個unsigned long 初始值為0. NR_CPUS會在Config Linux Kernel被設定,用來表示目前所編譯的核心最大支援的處理器個數.

        

 

多核心架構下,跟CPU個數與狀態有關的基礎函式

 

            全域變數setup_max_cpus  (=NR_CPUS)可取得編譯時設定的處理器個數,函式smp_processor_id 可取得目前的處理器ID.

 

 

            取得目前系統在對應狀況(Online,Possible,Present或Active)的處理器個數

#define num_online_cpus()       cpumask_weight(cpu_online_mask)

#define num_possible_cpus()     cpumask_weight(cpu_possible_mask)

#define num_present_cpus()      cpumask_weight(cpu_present_mask)

#define num_active_cpus()       cpumask_weight(cpu_active_mask)

 

            確認對應處理器目前的狀態 (Online,Possible,Present或Active).

#define cpu_online(cpu)         cpumask_test_cpu((cpu), cpu_online_mask)

#define cpu_possible(cpu)       cpumask_test_cpu((cpu), cpu_possible_mask)

#define cpu_present(cpu)        cpumask_test_cpu((cpu), cpu_present_mask)

#define cpu_active(cpu)         cpumask_test_cpu((cpu), cpu_active_mask)

 

 

initcall

 

            Linux Kernel有提供initcall機制,讓每個核心模塊的設計者,可以在系統init初始的不同階段,執行對應的初始化動作.參考檔案include/linux/init.h,Linux Kernel有提供__define_initcall巨集,用以根據輸入的Level產生對應的 initcall Section,如下所示  

 

#define __define_initcall(level,fn,id) \

        static initcall_t __initcall_##fn##id __used \

        __attribute__((__section__(".initcall" level ".init"))) = fn

 

 

            如果使用者希望在初始SMP前(也就是呼叫smp_init前), 有要執行的函式,就可以透過early_initcall (會把指定的函式放到 Level early中,也就是Section .initcallearly.init),如此在執行到函式do_pre_smp_initcalls時,就會執行Section .initcallearly.init裡被註冊的函式呼叫,如下所示

 

#define early_initcall(fn)              __define_initcall("early",fn,early)

 

            以筆者所編譯的ARM Tegra環境來說,在init執行到尾聲時,會呼叫do_initcalls,進行執行使用者模式init前的最後核心裝置與檔案系統...等的initcall呼叫,在筆者的環境中主要會產生以下的Level “0”,“1”,”2”,”3”,”4”,”5”,”rootfs”,”6”與 ”7”,並且會對應到如下的Section “.initcall0.init”,”.initcall1.init”,”.initcall2.init”,”.initcall3.init”,”.initcall4.init”,”.initcall5.init”,”.initcallrootfs.init”,”.initcall6.init”, 與”.initcall7.init” .

 

            對應的宣告如下所示

 

#define pure_initcall(fn)               __define_initcall("0",fn,0)

#define core_initcall(fn)               __define_initcall("1",fn,1)

#define postcore_initcall(fn)           __define_initcall("2",fn,2)

#define arch_initcall(fn)               __define_initcall("3",fn,3)

#define subsys_initcall(fn)             __define_initcall("4",fn,4)

#define fs_initcall(fn)                 __define_initcall("5",fn,5)

#define rootfs_initcall(fn)             __define_initcall("rootfs",fn,rootfs)

#define device_initcall(fn)             __define_initcall("6",fn,6)

#define late_initcall(fn)               __define_initcall("7",fn,7)

 

            在實際的程式碼中,可以看到例如在檔案kernel/softirq.c中,函式static __init int spawn_ksoftirqd(void),會執行 early_initcall(spawn_ksoftirqd) 把函式spawn_ksoftirqd放到Section “.initcallearly.init”.

 

            或像是在檔案arch/arm/kernel/setup.c中,函式static int __init topology_init(void) 後會宣告subsys_initcall(topology_init)把函式 topology_init放到Section ”.initcall4.init”. ( topology_init會把系統中每個處理器 cpuinfo->cpu.hotpluggable設定為1,並呼叫register_cpu )

 

            在檔案kernel/posix-timers.c中,函式static __init int init_posix_timers(void)會執行__initcall(init_posix_timers)把函式 init_posix_timers放到 Section ”.initcall6.init” (實際上__initcall 會宣告為device_initcall).

 

            以及在檔案drivers/char/random.c中,函式static __init int seqgen_init(void)回執行late_initcall(seqgen_init)把函式 seqgen_init放到Section “.initcall7.init”.

 

 

與平臺相關的函式boot_secondary與Secondary處理器啟動流程

 

            要了解不同處理器平臺(例如Nvidia Tegra (in arch/arm/mach-tegra/platsmp.c), TI OMAP (in arch/arm/mach-omap2/omap-smp.c)或Qualcomm MSM (in arch/arm/mach-msm/platsmp.c)對多核心啟動的實作差異,從這個函式就可以揭露出概略的框架,例如

 

 

處理器

說明

NVIDIA Tegra

參考arch/arm/mach-tegra/platsmp.c中的實作,

1,透過virt_to_phys(tegra_secondary_startup)取得函式tegra_secondary_startup的Physical Adress

2,讀取並暫存HW Register EVP_CPU_RESET_VECTOR 的值,並把把函式tegra_secondary_startup的Physical Address寫入HW Register EVP_CPU_RESET_VECTOR 

3,致能Secondary處理器的Clock

4,執行smp_wmb (=barrier),透過Memory Barrier執行ARM dmb sy指令(Data Memory Barrier)與Flush Cache,讓處理器重新去外部記憶體讀取資料到Register與Cache中.

5,設定HW Register TEGRA_FLOW_CTRL_BASE的值(Unhalt the Online CPU),讓處理器可以開始運作 (透過 Flag Register去執行函式tegra_secondary_startup與後續流程)

6,HW Register EVP_CPU_RESET_VECTOR的值,如果跟原本寫入的不同 (也就是說不等於函式tegra_secondary_startup的Physical Address),就表示Secondary處理器順利被啟動了.

7,還原原本 HW Register EVP_CPU_RESET_VECTOR的值.

 

至此,就完成Tegra雙核心架構下,由Bootstrap處理器把SMP的Secondary處理器帶起來的動作.

 

 

 

參考資訊:

#define EVP_CPU_RESET_VECTOR \

        (IO_ADDRESS(TEGRA_EXCEPTION_VECTORS_BASE) + 0x100)

TEGRA_EXCEPTION_VECTORS_BASE =0x6000F000

Qualcomm MSM

參考 arch/arm/mach-msm/platsmp.c中的實作,

1,呼叫prepare_cold_cpu,透過函式scm_set_boot_addr利用Qualcomm的SCM(Secure Channel Manager)介面,把函式msm_secondary_startup的Physical Address傳給Online處理器.

2,把全域變數pen_release設定為Secondary處理器CPU ID(沒特別的意義,只是要在後面利用確保這個值不為-1來確認Secondary處理器是不是被啟動了).

3,Flush DCache 與全域變數pen_release的在Layer2 Cache中的內容.

4,呼叫smp_cross_call,觸發Software Interrupt給Secondary處理器,讓Secondary處理器開始啟動流程.(透過 Flag Register去執行函式msm_secondary_startup與後續流程)

5,接下來會確任全域變數pen_release是否為-1,若是就表示Secondary處理器順利被啟動了.

TI OMAP

參考 arch/arm/mach-omap2/omap-smp.c中的實作,

1, 雙核心的OMAP Secondary處理器,會在啟動後進入函式omap_secondary_startup中(in arch/arm/mach-omap2/omap-headsmp.S),並透過smc (Secure Monitor Call)讀取AuxCoreBoot0的內容. (TI OMAP有支援 TrustZone Security Extensions,所以可以使用SMC指令,進入Secure Mode.)

2,直到Bootstrap處理器透過omap_modify_auxcoreboot0(0x200, 0xfffffdff);更新AuxCoreBoot0內容.讓函式omap_secondary_startup離開hold的迴圈.

3,Bootstrap處理器會透過smp_cross_call(cpumask_of(cpu), 1)觸發IPI (Inter-Processor Interrupt)給Secondary處理器.

4,此時Secondary處理器往後進入函式secondary_startup中執行後續流程.

 

            以上,筆者把目前常見的Dual-Core平臺,如何把Secondary處理器帶起的機製做一個簡要說明,我們可以看到每一個方案的作法都有些許的差異,但其實主要還是在於Reset/Halt機制或是透過 Flag Register/Loop(ex,WFI-Loop)的作法. 在平臺的實現上,我們可以根據自己的處理器方案,設計適合啟動流程. ㄟ.....當然,並不是每個平臺都會有TrustZone的 (Linux Kernel也是在沒有Enable這個Feature下執行),所以,SMC的機制,也就不是每個平臺實現時,需要考慮的.

 

 

cpu_up與cpu_down

 

            Linux Kernel支援CPU hotplug機制,並可透過全域變數cpu_hotplug_disabled決定處理器Hot Plug機制的致能與否.

 

            參考檔案 kernel/cpu.c,如果全域變數cpu_hotplug_disabled被設定為1,cpu_up與cpu_down機制就會失效(返回-EBUSY). 在實作上,可以呼叫函式enable_nonboot_cpus致能CPU Up/Down機制,或呼叫函式disable_nonboot_cpus關閉CPU Up/Down機制.

 

            CPU Up呼叫流程為

            cpu_up (in kernel/cpu.c) -> _cpu_up (in kernel/cpu.c) → __cpu_up (in arch/arm/kernel/smp.c)

 

            如下簡述這三個流程的動作.

 

CPU UP 流程

說明

1, “cpu_up”
(in kernel/cpu.c)

1,呼叫cpu_possible確認目前要Online的處理器是否可被啟用.

2,呼叫cpu_maps_update_begin 設定Mutex Lock “cpu_add_remove_lock”

3,確認cpu_hotplug_disabled是否有被設定 (也就是不允許動態的CPU Online動作)

4,呼叫 _cpu_up(cpu, 0)

5,呼叫cpu_maps_update_done解開Mutex Lock “cpu_add_remove_lock”

2, “_cpu_up” 
(in kernel/cpu.c)

1,如果該CPU已經Online或是非Present的狀態,就返回錯誤

2,呼叫cpu_hotplug_begin,

   2.a,設定Active Write為目前的Process (cpu_hotplug.active_writer = current),取得 Mutex Lock “cpu_hotplug.lock”.

   2.b,如果 cpu_hotplug.refcount為0,表示目前沒有其它Reader,因此,可以結束函式cpu_hotplug_begin,讓_cpu_up後續工作繼續

   2.c,反之,如果cpu_hotplug.不為0,就會把行程設定為TASK_UNINTERRUPTIBLE,並解開Mutex Lock “cpu_hotplug.lock”,觸發排程,讓其它的Reader把工作結束 
    (必須讓Write取得Mutex Lock “cpu_hotplug.lock”以及cpu_hotplug.refcount為0,才能讓函式cpu_hotplug_begin結束).

3,呼叫__cpu_notify,透過函式__raw_notifier_call_chain,通知CPU Chain中的處理器,目前正在進行Online動作的處理器狀態為”CPU_UP_PREPARE”.

4,呼叫 __cpu_up(cpu)

5,呼叫cpu_notify,透過函式__raw_notifier_call_chain,通知CPU Chain中的處理器,目前完成Online動作的處理器狀態為”CPU_ONLINE”.

6,呼叫cpu_hotplug_done,設定Active Write為NULL (cpu_hotplug.active_writer = NULL),解開Mutex Lock “cpu_hotplug.lock”.

3,  “__cpu_up”

(in arch/arm/kernel/smp.c)

1,呼叫per_cpu取得要Online處理器的cpuinfo_arm結構 (in arch/arm/include/asm/cpu.h),如下所示

struct cpuinfo_arm {

        struct cpu      cpu;

#ifdef CONFIG_SMP

        struct task_struct *idle; //會指向目前這個處理器的IDLE Task.

        unsigned int    loops_per_jiffy;

#endif

};

 

2,如果目前處理器沒有指定Idle Task,就透過函式fork_idle (in kernel/fork.c)產生Idle Task後指定給這個處理器. 函式fork_idle會呼叫copy_process複製一個PID = init_struct_pid的行程,並執行函式init_idle_pids與init_idle把這Idle Task指定給要Online的處理器.

3,反之,如果這處理器已經有Idle Task,就呼叫函式init_idle (in kernel/sched.c)指定Idle Task給目前進行Online的處理器.

4,呼叫函式pgd_alloc (in arch/arm/mm/pgd.c),會以1MB TLB Settings (約需要16kbytes 記憶體),產生TLB Level 1的Page Table.

5,如果PHYS_OFFSET != PAGE_OFFSET (PHYS_OFFSET為Kernel Image在實體記憶體中的Offset,PAGE_OFFSET為Kernel Image在虛擬記憶體中的Offset,一般而言為0x8000),就會透過函式identity_mapping_add (in arch/arm/mm/idmap.c),把Linux Kernel Image 程式區段 (_stext 到_etext)與資料區段(_sdata到_edata)的記憶體分頁以1MB TLB與AP (Access Permission)為PMD_SECT_AP_WRITE  (1 << 10) 屬性設定到TLB分頁中.有關記憶體分頁的屬性與對應的Bits如下圖所示,以現有程式碼的配置來說,對Linux Kernel 的程式與資料區段是特權等級Privileged permissions為Read/Write,而一般應用程式User permissions為No Access.

 

 


至此就完成對新增處理器的初步MMU記憶體分頁與安全性的配置動作.

6,設定要Online處理器的Stack (secondary_data.stack)與Page Table(secondary_data.pgdir)位址到全域變數struct secondary_data.

7,呼叫函式__cpuc_flush_dcache_area進行 DCache Flush (範圍是全域變數struct secondary_data在記憶體的起點與大小)

8,呼叫函式outer_clean_range把Clean L2 Cache (範圍是全域變數struct secondary_data在記憶體的起點與大小)

9,透過函式boot_secondary (in arch/arm/mach-tegra/platsmp.c)帶起Online處理器.

10,secondary_data.stack = NULL;與secondary_data.pgdir = 0;

11, 如果PHYS_OFFSET != PAGE_OFFSET,就會透過函式identity_mapping_del把之前配置的Linux Kernel Image 程式區段 (_stext 到_etext)與資料區段(_sdata到_edata)記憶體分頁刪除.

12, 呼叫函式pgd_free釋放Page Table.

 

            執行完畢__cpu_up,就完成了新增處理器的流程.

 

            

 

            CPU Down呼叫流程為

            cpu_down (in kernel/cpu.c) -> _cpu_down (in kernel/cpu.c) → __cpu_die (in arch/arm/kernel/smp.c)

 

            如下簡述這三個流程的動作.

 

CPU DOWN流程

說明

1,”cpu_down”
 (in kernel/cpu.c)

1,呼叫cpu_maps_update_begin 設定Mutex Lock “cpu_add_remove_lock”

2,確認cpu_hotplug_disabled是否有被設定

3,呼叫 _cpu_down(cpu, 0)

4,呼叫cpu_maps_update_done解開Mutex Lock “cpu_add_remove_lock”

2,”_cpu_down”
(in kernel/cpu.c)

1,呼叫函式num_online_cpus,確認如果目前Online的處理器只有一個,會直接返回錯誤 (mmmm,如果這個也Down下去就沒有處理器可用了...)

2,呼叫函式cpu_online,如果該CPU並非Online狀態,就返回錯誤

3,呼叫cpu_hotplug_begin,取得 Mutex Lock “cpu_hotplug.lock”.

4,呼叫__cpu_notify,透過函式__raw_notifier_call_chain,通知CPU Chain中的處理器,目前正在進行Online動作的處理器狀態為”CPU_DOWN_PREPARE”.

5,呼叫函式__stop_machine (in )

   5.a,透過函式set_state設定struct stop_machine_data smdata 中的state為STOPMACHINE_PREPARE

   5.b,透過函式stop_cpus,停止指定的處理器. 會在關閉Preemption的狀態下,透過cpu_stop_queue_work,讓要被停止的處理器執行函式take_cpu_down.

    5.c,由於是由要被停止的處理器執行函式take_cpu_down,在這函式的實作中,會呼叫__cpu_disable(in arch/arm/kernel/smp.c),把自己Offline,Migrate IRQ給其他處理器,停止Local Timer,Flush Cache/TLB,透過cpumask_clear_cpu把自己從Memory Management CPU Mask中移除,最後透過cpu_notify通知自己處於CPU_DYING.

     5.d,進入Callback函式migration_call中 (in kernel/sched.c),並透過函式migrate_tasks把要Offline處理器的Tasks轉移到其它處理器上. (在這之後,處理器就只剩下Idle Task.).

6,透過BUG_ON(cpu_online(cpu)),確認要停止的處理器,是否已經處於Offline的狀態. (若還是在Online狀態就會導致Kernel Panic)

7,呼叫函式idle_cpu (in kernel/shced.c),確認要Offline處理器是否正在執行idle task.(前面的migrate_tasks已經把要Offline處理器的所有Tasks都轉到其它處理器上了).若該處理器不是正在執行Idle Task,就會呼叫 cpu_relax (對應的實作為ARM的Memory Barrier),直到確認要Offline的處理器是處於Idle Task中.

8,呼叫 __cpu_die(cpu)

9,呼叫cpu_notify_nofail,通知完成Offline動作的處理器狀態為”CPU_DEAD”.

10,呼叫check_for_tasks,確認目前是否還有Tasks在被停止的處理器上,若有就會Printk出警告訊息...(ㄟ....就算有在這階段也來不及做啥了......@_@)

11,呼叫cpu_hotplug_done,設定Active Write為NULL,解開Mutex Lock “cpu_hotplug.lock”.

3,”__cpu_die”

(in arch/arm/kernel/smp.c)

1,會執行函式wait_for_completion_timeout,等待函式cpu_die 透過函式 complete 設定“Completion”給 cpu_died物件,如果cpu_died物件有設定完成或是TimeOut就會繼續往後執行.有關cpu_die函式的執行,是在處理器初始化到最後時,會透過函式rest_init呼叫到函式cpu_idle (in arch/arm/kernel/process.c)中,由cpu_idle執行處理器的IDLE流程. 而在cpu_idle中,在有支援CPU HotPlug的組態下,會去確認處理器是否被Offiline,若是就會執行 cpu_die,如下所示

#ifdef CONFIG_HOTPLUG_CPU

                        if (cpu_is_offline(smp_processor_id()))

                                cpu_die();

#endif

2,呼叫platform_cpu_kill (in arch/arm/mach-tegra/hotplug.c),以Tegra方案來說,這函式為空函式.

3,而CPU Idle Task在執行cpu_die後,就會進入函式platform_cpu_die (in arch/arm/mach-tegra/hotplug.c),並透過platform_do_lowpower,讓處理器處於WFI Low Power的狀態,等待下一次的喚醒.

4,若處理器重新被喚醒,就會執行函式secondary_start_kernel (in arch/arm/kernel/smp.c),重新執行初始化流程.

 

 

            執行完畢__cpu_die,就完成了卸載處理器的流程.

 

 


cpu_idle

 

            實作在檔案arch/arm/kernel/process.c中,當處理器沒有其它Task執行時,就會在這函式中運作.

            Linux Kernel支援 Tickless System (設定路徑在Kernel Features  --->Tickless System (Dynamic Ticks)),如果有選擇這能力,會在.config中設定CONFIG_NO_HZ=y. 當處理器執行IDLE Task時,可以停止系統的Scheduling Tick,也就是說進入pm_idle時,可以減少Timer中斷觸發(例如HZ=100,每秒就有一百次Tick Timer中斷觸發),減少系統醒來的機會.

 

            有關CPU Idle的說明,也可以參考Linux Kernel文件”Documentation/scheduler/sched-arch.txt”,如下簡述 CPU Idle的行為

 

            1,Enable FIQ.

            2,進入 while(1) 的 Loop

            2-1,呼叫tick_nohz_stop_sched_tick,停止Scheduling Tick,包括會透過get_next_timer_interrupt取得下一次Timer中斷,跟現在的時間計算Delta delta_jiffies,如果delta_jiffies過小(=1),也就是說下個Timer中斷很快就會被觸發,就會直接結束這函式. (間隔過短,關閉Scheduling Tick所產生的效益就降低了.),由於系統時間所依賴的jiffies就是透過Scheduling Tick更新,因此在這函式中也會針對暫停Scheduling Tick引起的時間差,預備修正的機制.

            2-2,進入while (!need_resched()) Loop  ,執行need_resched可以知道是否需要重新排程,或是要準備進入IDLE的省電機制中. Idle 流程不會Set或Clear 函式need_resched所參考的TIF_NEED_RESCHED,當Idle流程觸發排程時,才會Clear該狀態

            2-2-1,如果目前的處理器已經被Offline,則呼叫cpu_die

            2-2-2,Disable IRQ

            2-2-3,如果hlt_counter不為0,就會Enable IRQ,執行cpu_relax. 如果有呼叫platform_suspend_ops中的 end 函式指標,就會呼叫enable_hlt,讓hlt_counter不為0,反之如果呼叫begin函式指標,就會透過disable_hlt,讓htl_counter減為0.  (並非所有ARM方案都有設定Platform Suspend Operation的機制,例如TI OMAP有,而Nvidia Tegra則無).

            2-2-4,若hlt_counter為0,就會進入每個平臺差異化的pm_idle實作,在這個函式指標中,會依據每個平臺的實作不同,在系統初始化時,設定給不同的Power Management函式.例如:omap2_pm_idle,omap3_pm_idle或s5p64x0_idle,在這就可以根據每個平臺的差異,優化跟平臺有關的Power Saving機制,包括關閉PMU Power Group(LDO)或是停止PLL..等,讓系統可以進入更深度的省電模式. 執行完畢pm_idle後,就會Enable IRQ,往後繼續執行.

            2-3,呼叫tick_nohz_restart_sched_tick,恢復Scheduling Tick,並更新jiffies

            2-4,執行preempt_enable_no_resched,致能Preemptive排程 (Call dec_preempt_count())

            2-5,進行Kernel Scheduling

            2-6,執行preempt_disable,關閉Preemptive排程  (Call inc_preempt_count()),在cpu_idle中,除了要觸發排程外,多數的情況下Preemptive排程會被關閉,直到要重新觸發Linux Kernel排程才會致能Preemptive機制.

 

           

            基於上述的說明,筆者把CPU Idle主要的行為,用如下的圖示表示

 



結語

 

            希望透過本文的介紹,各位可以對Linux Kernel與Idle機制有清楚的藍圖,進而在ARM MPCore產品,有更好的掌握度.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章