Android筆記-Linux KernelSMP(Symmetric Multi-Processors)開機流程解析Part(3) Linux多核心啟動流程從rest_init到kernel_init與CPU Idle/HotPlug機制

hlchou@mail2000.com.tw

byloda.

Loda's Blog

App BizOrz

Android/Linux Source Code Tags
App BizOrz
BizOrz.COM
BizOrz Blog

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

Linux Kernel針對CPU Idle與對應的省電機制,已經有滿不錯的框架,只要處理器平台開發者,根據自己的處理器架構,PowerGroup與對應的周邊適配進來,就可以達到省電最佳化的效益.尤其,在多核心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中,

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

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

產生Kernel Thread

kthreadd

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

kthreaddTask 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可以在啟動時,透過BootParameters “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 URLhttp://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,產生第一個UserSpace行程.

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針對多核心的需求,提供了對應處理器狀態的CPUMask Bitmap,參考kernel/cpu.c中的實作,

constDECLARE_BITMAP(cpu_all_bits, NR_CPUS) = CPU_BITS_ALL;

staticDECLARE_BITMAP(cpu_possible_bits, CONFIG_NR_CPUS) __read_mostly;

staticDECLARE_BITMAP(cpu_online_bits, CONFIG_NR_CPUS) __read_mostly;

staticDECLARE_BITMAP(cpu_present_bits, CONFIG_NR_CPUS) __read_mostly;

staticDECLARE_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) \

unsignedlong 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) \

staticinitcall_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,HWRegister 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,FlushDCache與全域變數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指令,進入SecureMode.)

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,設定ActiveWrite為目前的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 cpucpu;

#ifdef CONFIG_SMP

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

unsigned intloops_per_jiffy;

#endif

};

2,如果目前處理器沒有指定IdleTask,就透過函式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,反之,如果這處理器已經有IdleTask,就呼叫函式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轉移到其它處理器上. (在這之後,處理器就只剩下IdleTask.).

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,而CPUIdle 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 (設定路徑在KernelFeatures--->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,EnableFIQ.

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中斷很快就會被觸發,就會直接結束這函式. (間隔過短,關閉SchedulingTick所產生的效益就降低了.),由於系統時間所依賴的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,DisableIRQ

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,進行KernelScheduling

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

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



結語

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

更多相关文章

  1. android往文件中保存和读取数据
  2. Android(安卓)7.1.2(Android(安卓)N) Activity启动流程分析
  3. 你百度不到的android坑 imageview.setimageDrawable和setbackgro
  4. Android上实现zlib解压缩的方法 Inflater用法
  5. Android之service探究
  6. Android(安卓)Activity生命週期简介
  7. Android各个版本API的区别
  8. Android(安卓)5.1 open data flow 数据开启流程
  9. 在android中创建bitmap避免内存不足的方法

随机推荐

  1. Android中RelativeLayout各个属性,例如:and
  2. Android Studio 1.0 苹果电脑安装配置
  3. Android工具箱之组织你的代码文件
  4. Android 应用程序基础知识(1)
  5. AndroidDevTools收集整理Android开发所需
  6. Android布局之xml设置
  7. Android中application的theme不生效的bug
  8. android android:gravity 不起作用
  9. Android 动态加载(五) - 借尸还魂之代理Act
  10. 第一天Android校内实习笔记--回顾