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产品,有更好的掌握度.

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