拨云见日——基于Android的内核与系统架构源码分析(txt+pdf+epub+mobi电子书下载)


发布时间:2020-07-05 11:35:05

点击下载

作者:王森

出版社:清华大学出版社

格式: AZW3, DOCX, EPUB, MOBI, PDF, TXT

拨云见日——基于Android的内核与系统架构源码分析

拨云见日——基于Android的内核与系统架构源码分析试读:

前言

1996年,面对闪烁的DOS提示符,笔者心中产生了一个愿望——能够理解计算机每一条指令是如何工作的。

2000年,笔者明白一个道理,内核才是机器的灵魂,只有阅读源码才能与机器的灵魂对话。于是笔者开始了漫长的源码阅读之旅。

2008年,在经历了近十年内核以及驱动项目开发后,笔者发现尽管玄机重重,但是内核并非不能驾驭。在浩如烟海的Linux代码里有一个清晰的内核骨架,Linux各种文件、驱动子系统围绕该骨架接驳起来,把握住这个骨架就驾驭了Linux。

然而痛苦随之而来,以前笔者以为内核就是一切,驾驭了内核就理解了机器的一切。但是实际上,内核不是全部,要理解机器的机理还需要操作系统领域的探索,在经历了Glibc、X、GTK等痛苦且失败的探索之后,笔者发现了Android。

2011年,笔者多年的心愿总算有个了结。在经历了2年多Android源码研究之后,终于理解了Android如何将内核的强大能量释放给应用程序,如何对传统内核加以巧妙改造以满足当今以面向对象机制为基础的软件体系。这一年又是新的开始,一次难得的机会,笔者能够深入硬件开发。

2013年,这两年可谓笔者的硬件生涯。无论是原理图、PCB设计还是选择制板工厂、实施备料、贴片试产跟线,笔者都能够有幸深入一线体验硬件工程的复杂与艰辛。笔者尝试了软硬件协同设计,尽管在板级设计这一层面作用有限,但是通盘软考虑依然可以避免不少工程误区。

2014年,笔者将这些年的笔记进行整理,攒成此书,也算是个人生的小结吧。

本书其实是实现本人年少时梦想的一部分——理解计算机每一条指令是如何工作的。

然而,这只是一个梦想,操作系统如同浩如烟海的原始森林,每一条指令只是其中的一片树叶,人们不可能读完每一行代码,也没有必要这样做。任何一个优秀的操作系统都有一个精悍的架构,Android系统也不例外,本书通过分析这个架构来阐述Android机理。

笔者认为,尽管Android用户层面的代码量远大于内核部分,但是其关键架构却与内核息息相关,且其功能部分架构已经有很多优秀书籍和资料阐述。所以本书更多着墨于Android内核表现以及内核机制的Android运用。

最后需要提醒读者的是,无论多丰富的语言在源码面前也是苍白的,本书选择通过源码注释的方式来描述Android架构,建议读者结合源码来阅读本书。编者2014年4月上篇 内核Android与Linux内核的关系,就好比一辆整车和底盘发动机的关系。作为底盘和发动机的Linux内核,尽管不能直接被用户和应用开发者感知到,但是却决定了Android系统运行时最核心的机制。第1章 ARM多核处理器

计算性能是处理器演进的第一动力。然而,尽管各种架构的高性能处理器层出不穷,但真正大规模普及开来的似乎只有Intel和ARM体系。观察其中的现象不难发现如下规律。

虽然处理器性能得到大幅改善,但如果无法得到现有主流操作系统的支持,就无法大规模应用。进一步来讲,即使某种处理器得到主流操作系统的支持,但是由于其指令集的不兼容性,导致大量的应用无法运行,这种处理器也是难以普及的,安腾的失败是个很好的例子。“兼容性”是处理器演进的第一法则。现有的软件体系是计算机世界的主宰,所有不服从现在软件体系的指令集都只能被边缘化或者被淘汰。所以看到Intel和ARM不断扩展寄存器长度、增加发射单元、支持乱序、扩展总线单元等一系列手段来改进处理器架构,就是不敢废弃一条既有指令。

降低计算功耗是处理器演进的第二动力。对于服务器来说功耗就是运营成本,对于移动计算来说功耗就是生命。似乎台式机功耗要求不大,但是台式机与服务器、移动设备不仅在处理器的生产及设计上共享基础设施外,软件体系也有着相同的基础,导致整个软件体系为了照顾移动设备和服务器而收敛了扩张的步伐。这就是人们常说的“计算过剩”,其实计算永远不会过剩,只是庞大的软件体系有所收敛罢了。

频率提升是功耗的大敌,为了提高能耗比,设计频率更低而核心更多的处理器是好的方法,由此,近年来处理器的扩张由单核高频转变为多核体系。

Linux支持的多核体系的基本特点为:所有处理器拥有完全相同的指令集,所有处理器共享同一内存。如果不符合以上任何一点,操作系统内核都需要做架构及修改才能支持。若满足这两个前提,其他方面的问题,都可以通过内核和系统小规模修改来支持。如Big.little架构下互相搭配的处理器尽管核心内部实现不同,但各核心有着相同的指令集,Linux内核可以将它们当作同样的处理器来分配线程。内核在完全不加改动的情况下,也许会出现一个重量级线程被分配到了一个little的核心上,但是内核本身可以通过处理器负载均衡将其平衡到别的处理器上。当little负载较高时,big必定会上线运行,但是系统有可能会出现重负载线程独自霸占little的情形的极限情况,这时需要将调度算法稍加修改,首先记录每个处理器能力,然后监测little上单线程高负载发生的时长,若超出一定阀值则将高负载线程时间片减为零,从little摘下来,再将其挂到big处理器运行队列即可。

进一步讲,多核心之间只要基本的读写、跳转之类指令相同,其余指令集不同,Linux也可以支持的。内核只需用到基本指令即可,各核心用户空间指令可以不同。内核需做如下修改:每颗核心的调度队列记录下其核心用户态指令集特性,每个线程创建之初申明自身需要的指令集特性,内核在给线程分配核心及做负载均衡时比较两者是否匹配。用户层动态加载器也要做修改,针对当前处理器指令集特性动态加载、跳转到不同版本的动态库中,且在动态库执行过程中标记自身线程不可切换处理器。当然这种复杂情况超出了当前现状,尽管有用户态指令集不同的架构,但是运行指令集差集的线程不需要动态加载库的支持,且直接绑定到指定核心上。1.1 SMP相关基础数据结构

CPU管理的特点是自我管理,除了在启动、休眠、调频受控于CPU0的工作以外,处理器相关的绝大部分工作都由处理器自我管理。处理器是内核的执行体,又被内核控制。内核中准备了表征处理器运行状态的相应数据结构,处理器在运行时将自己的状态记录在这些结构中,而处理器也能通过别的处理器的表征结构了解其他处理器状态或发起控制。

每颗CPU都对应一个通用CPU结构,将ARM Core与device联系起来。struct cpu {/*cpu编号*/int node_id;/*对于arm smp 该值为真,表示每颗cpu都可以被关掉*/int hotpluggable;/*正如每个外设都有一个struct device表示自身一样,cpu也不例外*/struct device dev;};

处理器在内核中也被作为系统设备存在,而其相关操作以驱动形式通过处理器系统设备类——struct sysdev_class cpu_sysdev_class来识别管理处理器。在处理器拓扑初始化时,内核完成设备侧的注册。//处理器拓扑初始化static int __init topology_init(void){ …/*对于每个物理存在处理器的操作,位图cpu_possible_bits 描述了系统中的处理器,无论处理器是否online,都对应其中一位*/for_each_possible_cpu(cpu) {struct cpuinfo_arm *cpuinfo = &per_cpu(cpu_data, cpu);/*ARM SMP的架构是每个core都可以被单独关闭的*/cpuinfo->cpu.hotpluggable = 1;/*将当前cpu的struct device注册到struct sysdev_class cpu_sysdev_class*/register_cpu(&cpuinfo->cpu, cpu);}…}//注册一颗处理器的设备int _ _cpuinit register_cpu(struct cpu *cpu, int num){…//对CA9,非NUMA,为0cpu->node_id = cpu_to_node(num);cpu->sysdev.id = num;//处理器系统设备类cpu->sysdev.cls = &cpu_sysdev_class;/* 向处理器系统设备类注册该处理器,在每注册一个设备时,都会调用设备类驱动来初始化该驱动*/error = sysdev_register(&cpu->sysdev);…}

在对方驱动侧,通过向处理器系统设备类注册驱动来匹配初始化处理器,参见1.3节。

struct cpuinfo_arm对应于每颗Arm core实体,记录其基本信息,在ARM初始化时会依次初始化并注册每个struct cpuinfo_arm。struct cpuinfo_arm {struct cpu cpu;#ifdef CONFIG_SMP//当前cpu的idle线程struct task_struct *idle;unsigned int loops_per_jiffy;#endif};

CPU设备集合,每个CPU的struct device和struct sysdev_driver都会被加进来。这是联系CPU设备集合类驱动的纽带,当CPU设备或驱动注册的时候,会依次扫描这里的驱动列表或CPU设备,并进行初始化,如cpufreq_sysdev_driver。struct sysdev_class cpu_sysdev_class = {.name = "cpu",.attrs = cpu_sysdev_class_attrs,};struct cpumask:CPU状态的基本数据结构定义如下:typedef struct cpumask { DECLARE_BITMAP(bits, NR_CPUS); } cpumask_t;#define DECLARE_BITMAP(name,bits) \unsigned long name[BITS_TO_LONGS(bits)]

展开如下:typedef struct cpumask { unsigned long bits[BITS_TO_LONGS(NR_CPUS)] } cpumask_t;cpu_possible_mask位图,用来表示系统中的CPU,每颗处理器对应其中一位。cpu_online_mask位图,用来表示当前处于工作状态的CPU,每颗处理器对应其中一位。cpu_bit_bitmap:const unsigned long cpu_bit_bitmap[BITS_PER_LONG+1][BITS_TO_LONGS(NR_CPUS)]

对于双核CA9,这个数组是const unsigned long cpu_bit_bitmap[33][1]。可见这是列数为1的数组。其中列数与CPU个数和sizeof(long)有关。每颗CPU对应一行,但是最上面的一行是保留不用的。//接下来是该位图具体的定义,通过一组宏来实现const unsigned long cpu_bit_bitmap[BITS_PER_LONG+1][BITS_TO_LONGS(NR_CPUS)] = {MASK_DECLARE_8(0), MASK_DECLARE_8(8),…MASK_DECLARE_8(48), MASK_DECLARE_8(56),};

CPU0~CPU7对应于前8个数据,其宏定义为MASK_DECLARE_8(0),接下来以此为例,通过相关宏定义的层层替代,分析处理器位图结构。

第1步,展开顶层宏:#define MASK_DECLARE_8(x) MASK_DECLARE_4(x), MASK_DECLARE_4(x+4)

得到如下第二层宏数组:MASK_DECLARE_4(0), MASK_DECLARE_4(0+4)

第2步,将宏定义:#define MASK_DECLARE_4(x)MASK_DECLARE_2(x), MASK_DECLARE_2(x+2)

展开,得到第三层宏数组:MASK_DECLARE_2(0), MASK_DECLARE_2(0+2);MASK_DECLARE_2(4), MASK_DECLARE_2(4+2)

第3步,将宏定义:#define MASK_DECLARE_2(x)MASK_DECLARE_1(x), MASK_DECLARE_1(x+1)

展开,得到第四层宏数组:MASK_DECLARE_1(0), MASK_DECLARE_1(0+1);…MASK_DECLARE_1(6), MASK_DECLARE_1(6+1)

最后,再将宏定义#define MASK_DECLARE_1(x)

替换为[x+1][0] = (1UL << (x))

得到如下序列:[1][0] = (1UL << (0))[2][0] = (1UL << (1))[8][0] = (1UL << (7))

由此可见,第0行没有初始化。对于CPU0、CPU1,其MASK分别对应第1行和第2行,值分别为(1UL << (0)),(1UL << (1))。

而对于CPU8~31,则由MASK_DECLARE_8(8)、MASK_DECLARE_8(16)、MASK_DECLARE_8(24)定义了对应的MASK了,其过程与MASK_DECLARE_8(0)类似,这里不做展开。1.2 Percpu内存管理

随着处理器核心的增加,内核中系统中并发的线程也随之增加,这样对一些共享数据同时访问的几率也就增加,就避免不了使用spin_lock,而且往往处理器核心越多造成的麻烦越大。Percpu内存对这种数据无能为力,但是内核中有些数据只是处理器局部,可见,这种数据不会被别的处理器访问到,不需要加以spin_lock而直接访问。Percpu内存适用于这种数据,而实际上处理器局部数据的实际分配还是通过内核基本slab进行,而Percpu内存则是用来存放指向处理器局部数据的指针。1.2.1 内核显式定义的处理器局部数据

这是在内核代码中手工定义的变量,但是这些数据比较特殊,内核定义与引用时使用特殊的宏,链接时被集中排放在特殊的地址上,内核初始化时有着专门的指针处理。本节相关分析围绕这些方面展开。

首先分析链接文件arch/arm/kernel/vmlinux.lds.S中定义的这些特殊地址。

展开宏:PERCPU_SECTION(32)#define PERCPU_SECTION(cacheline) \.= ALIGN(PAGE_SIZE); \.data..percpu : AT(ADDR(.data..percpu) - LOAD_OFFSET) { \VMLINUX_SYMBOL(_ _per_cpu_load) = .; \PERCPU_INPUT(cacheline) \}#define PERCPU_INPUT(cacheline) \VMLINUX_SYMBOL(__per_cpu_start) = .; \*(.data..percpu..first) \.= ALIGN(PAGE_SIZE); \*(.data..percpu..page_aligned) \.= ALIGN(cacheline); \*(.data..percpu..readmostly) \.= ALIGN(cacheline); \*(.data..percpu) \*(.data..percpu..shared_aligned) \VMLINUX_SYMBOL(__per_cpu_end) = .;

该宏的作用是声明了以.data..percpu..first、.data..percpu..readmostly、.data..percpu等为名字的数据段,内核代码中如果有变量的属性指出放在该数据段的时候,链接时将其分配到对应的数据段。而且为了能够在内核里直接访问这段区域,定义两个变量:__per_cpu_start和__per_cpu_end。

在内核编码时,对处理器局部数据的定义需使用特殊的宏(DEFINE_PER_CPU)来完成。这里分析(DEFINE_PER_CPU)的实现。

层层展开该宏:#define DEFINE_PER_CPU(type, name) \DEFINE_PER_CPU_SECTION(type, name, "")#define DEFINE_PER_CPU_SECTION(type, name, sec) \__PCPU_ATTRS(sec) PER_CPU_DEF_ATTRIBUTES \__typeof__(type) name#define __PCPU_ATTRS(sec) \__percpu __attribute__((section(PER_CPU_BASE_SECTION sec))) \PER_CPU_ATTRIBUTES#define PER_CPU_BASE_SECTION ".data..percpu"

根据以上宏定义展开之:可以得到__attribute__((section(.data..percpu))) __typeof__(type) name

可见宏DEFINE_PER_CPU(type, name)的作用就是将类型为type的name变量放到.data..percpu数据段中。

同样方法可以推出:宏DECLARE_PER_CPU_READ_MOSTLY(type, name)的作用就是将类型为type的name变量放到.data..percpu..readmostly数据段中。这种数据段是cacheline对齐,可以大大提高cache的利用率,很适合频繁读取数据,不过在笔者分析的这个内核版本3.0.13中,内核中这种数据还没有大规模使用的。而且cacheline定义为32,对于不同架构可以使用时适当调整。

另外宏DECLARE_PER_CPU_FIRST(type, name)对应.data..percpu..first数据段。

宏DECLARE_PER_CPU_PAGE_ALIGNED对应.data..percpu..first”数据段。

内核初始化过程中,会为每个处理器定位链接进镜像的局部数据。//初始化是_ _per_cpu_offset的建立void_ _init setup_per_cpu_areas(void){rc = pcpu_embed_first_chunk(PERCPU_MODULE_RESERVE,PERCPU_DYNAMIC_RESERVE, PAGE_SIZE, NULL,pcpu_dfl_fc_alloc, pcpu_dfl_fc_free);delta = (unsigned long)pcpu_base_addr - (unsigned long)_ _per_cpu_start;/*设置每个cpu的_ _per_cpu_offset 指针*/for_each_possible_cpu(cpu)_ _per_cpu_offset[cpu] = delta + pcpu_unit_offsets[cpu];}

再看对内核中对percpu数据的引用:内核通过宏per_cpu对定义的处理器局部数据的引用,展开该宏:#define per_cpu(var, cpu) \(*SHIFT_PERCPU_PTR(&(var), per_cpu_offset(cpu)))#define SHIFT_PERCPU_PTR(__p, __offset) ({ \__verify_pcpu_ptr((__p)); \RELOC_HIDE((typeof(*(__p)) __kernel __force *)(__p), (__offset)); \})#define per_cpu_offset(x) (__per_cpu_offset[x])/*选用一个简单易看得RELOC_HIDE 宏定义*/# define RELOC_HIDE(ptr, off) \({ unsigned long __ptr; \__ptr = (unsigned long) (ptr); \(typeof(ptr)) (__ptr + (off)); })

可见是以当前CPU局部数据__per_cpu_offset[x]为基地址的相对寻址。1.2.2 Percpu内存管理的建立

在建立Percpu内存管理机制之前要整理出该架构下的处理器信息,包括处理器如何分组、每组对应的处理器位图、静态定义的Percpu变量占用内存区域、每颗处理器Percpu虚拟内存递进基本单位等信息。本文仅以双核CA9处理器作为分析目标。

对于处理器的分组信息,内核使用struct pcpu_group_info结构表示:struct pcpu_group_info {/*该组的处理器数目,对于双核CA9处理器,该值为2 */int nr_units;/*组内处理器数目×处理器percpu虚拟内存递进基本单位*/unsigned long base_offset;/*组内处理器对应数组,双核CA9架构下该数组长度为2*/unsigned int *cpu_map;};

整体的Percpu内存管理信息被收集在struct pcpu_alloc_info结构中:struct pcpu_alloc_info {//静态定义的percpu变量占用内存区域长度size_t static_size;/*预留区域,在percpu内存分配指定为预留区域分配时,将使用该区域*/size_t reserved_size;size_t dyn_size;/*每颗处理器的percpu虚拟内存递进基本单位*/size_t unit_size;…/*该架构下的处理器分组数目,CA9双核架构下该值为1*/int nr_groups;/*该架构下的处理器分组信息,CA9双核架构下该数组长度为1*/struct pcpu_group_info groups[];};

接下来构建静态定义的percpu变量创建percpu区域:/*该函数被void __init setup_per_cpu_areas(void)调用,对于CA9架构其参数cpu_distance_fn为NULL,参数alloc_fn为pcpu_dfl_fc_alloc*/int __init pcpu_embed_first_chunk(size_t reserved_size, size_t dyn_size,size_t atom_size,pcpu_fc_cpu_distance_fn_t cpu_distance_fn,pcpu_fc_alloc_fn_t alloc_fn,pcpu_fc_free_fn_t free_fn){void *base = (void *)ULONG_MAX;void **areas = NULL;struct pcpu_alloc_info *ai;size_t size_sum, areas_size, max_distance;int group, i, rc;/*收集整理该架构下的percpu信息,结果放在struct pcpu_alloc_info结构中*/ai = pcpu_build_alloc_info(reserved_size, dyn_size, atom_size, cpu_distance_fn);…//静态定义变量占用空间+reserved空间+动态分配空间size_sum = ai->static_size + ai->reserved_size + ai->dyn_size;…/*针对每个group操作 */for (group = 0; group < ai->nr_groups; group++) {struct pcpu_group_info *gi = &ai->groups[group];unsigned int cpu = NR_CPUS;void *ptr;…/* 为该group分配percpu内存区域。长度为处理器数目×每颗处理器的percpu递进单位。函数pcpu_dfl_fc_alloc是从bootmem里取得内存,得到的是物理内存 */ptr = alloc_fn(cpu, gi->nr_units * ai->unit_size, atom_size);…areas[group] = ptr;base = min(ptr, base);//为每颗处理器建立其percpu区域for (i = 0; i < gi->nr_units; i++, ptr += ai->unit_size) {if (gi->cpu_map[i] == NR_CPUS) {/*检查组内处理器,对于没有用到的处理器释放其percpu区域 */free_fn(ptr, ai->unit_size);continue;}/*将静态定义的percpu变量拷贝到每颗处理器percpu区域*/memcpy(ptr, __per_cpu_load, ai->static_size);/*为每颗处理器释放掉多余的空间,多余的空间是指ai->unit_size 减去静态定义变量占用空间+reserved空间+动态分配空间*/free_fn(ptr + size_sum, ai->unit_size - size_sum);}}/* 处理器架构相关的计算,对于CA9双核架构,ai->nr_groups 为1,且ai->groups[group].base_offset和max_distance这里的计算结果都为0*/max_distance = 0;for (group = 0; group < ai->nr_groups; group++) {ai->groups[group].base_offset = areas[group] - base;max_distance = max_t(size_t, max_distance,ai->groups[group].base_offset);}max_distance += ai->unit_size;…//建立可动态分配的percpu内存区域rc = pcpu_setup_first_chunk(ai, base);…}//建立可动态分配的percpu内存区域int __init pcpu_setup_first_chunk(const struct pcpu_alloc_info *ai,void *base_addr){static char cpus_buf[4096] __initdata;static int smap[PERCPU_DYNAMIC_EARLY_SLOTS] __initdata;static int dmap[PERCPU_DYNAMIC_EARLY_SLOTS] __initdata;…for (cpu = 0; cpu < nr_cpu_ids; cpu++)unit_map[cpu] = UINT_MAX;pcpu_first_unit_cpu = NR_CPUS;/*针对每一group的每一颗处理器,对于双核CA9,ai->nr_groups值为0,gi->nr_units值为2*/for (group = 0, unit = 0; group < ai->nr_groups; group++, unit += i) {const struct pcpu_group_info *gi = &ai->groups[group];//该组处理器的percpu偏移量,对于双核CA9,该值为0group_offsets[group] = gi->base_offset;//该组处理器占用的虚拟地址空间group_sizes[group] = gi->nr_units * ai->unit_size;//针对组内的每颗处理器for (i = 0; i < gi->nr_units; i++) {cpu = gi->cpu_map[i];if (cpu == NR_CPUS)continue;…unit_map[cpu] = unit + i;//计算每颗处理器的percpu虚拟空间偏移量unit_off[cpu] = gi->base_offset + i * ai->unit_size;…}}pcpu_nr_units = unit;…//记录下全局参数,留在pcpu_alloc时使用pcpu_nr_groups = ai->nr_groups;…/*构建pcpu_slot数组,不同size的chunk挂在不同pcpu_slot项目中*/pcpu_nr_slots = __pcpu_size_to_slot(pcpu_unit_size) + 2;pcpu_slot = alloc_bootmem(pcpu_nr_slots * sizeof(pcpu_slot[0]));//初始化pcpu_slot数组链头for (i = 0; i < pcpu_nr_slots; i++)INIT_LIST_HEAD(&pcpu_slot[i]);/*构建静态chunk即pcpu_reserved_chunk,该区域的物理内存以及虚拟地址都在int __init pcpu_embed_first_chunk(…)里分配了*/schunk = alloc_bootmem(pcpu_chunk_struct_size);…schunk->immutable = true;//物理内存已经分配,在这里标志bitmap_fill(schunk->populated, pcpu_unit_pages);…if (ai->reserved_size) {//reserved的空间,在指定reserved分配时使用schunk->free_size = ai->reserved_size;pcpu_reserved_chunk = schunk;//定义的静态变量的空间也算进来pcpu_reserved_chunk_limit = ai->static_size + ai->reserved_size;} else {schunk->free_size = dyn_size;dyn_size = 0; /* dynamic area covered */}schunk->contig_hint = schunk->free_size;schunk->map[schunk->map_used++] = -ai->static_size;if (schunk->free_size)schunk->map[schunk->map_used++] = schunk->free_size;/* 动态分配空间,这里构建第一个chunk,该chunk是第一次步进静态变量空间和reserved空间使用后剩下的*/if (dyn_size) {dchunk = alloc_bootmem(pcpu_chunk_struct_size);INIT_LIST_HEAD(&dchunk->list);…//记录下来分配的物理页bitmap_fill(dchunk->populated, pcpu_unit_pages);dchunk->contig_hint = dchunk->free_size = dyn_size;/*map指针更新将静态变量空间和reserved空间甩在后面*/dchunk->map[dchunk->map_used++] = -pcpu_reserved_chunk_limit;dchunk->map[dchunk->map_used++] = dchunk->free_size;}/* 把第一个chunk链接进对应的slot链表,reserved 的空间有自己单独的chunk:pcpu_reserved_chunk */pcpu_first_chunk = dchunk ?: schunk;pcpu_chunk_relocate(pcpu_first_chunk, -1);/* we're done */pcpu_base_addr = base_addr;return 0;}1.2.3 Percpu动态分配内存空间

关于Percpu动态分配内存空间有以下基本概念:(1)每颗处理器的自己Percpu动态分配内存空间都有不同的虚拟地址空间,否则同一线程在不同处理器上运行时修改页表页目录项的开销太大。(2)Chunk记录每颗处理器一次步进得到虚拟地址空间,对于一颗处理器来说一次步进长度是ai->unit_size,Percpu内存的虚拟地址以Chunk为基础。(3)Chunk中每一次配出的内存用其map[]指向。(4)Chunk根据其free的内存长度挂到Slot数组的对应链表上。(5)每次步进仅得到虚拟地址空间,在完成一次分配时才得到对应的物理内存。

本书仅考虑percpu-vm的情况。/*动态分配函数 */static void __percpu *pcpu_alloc(size_t size, size_t align, bool reserved){static int warn_limit = 10;struct pcpu_chunk *chunk;const char *err;int slot, off, new_alloc;unsigned long flags;mutex_lock(&pcpu_alloc_mutex);spin_lock_irqsave(&pcpu_lock, flags);/* 指定reserved分配,从pcpu_reserved_chunk进行,较简单不讨论*/if (reserved && pcpu_reserved_chunk) {…}restart:/* 根据需要分配内存块的大小索引slot数组找到对应链表 */for (slot = pcpu_size_to_slot(size); slot < pcpu_nr_slots; slot++) {list_for_each_entry(chunk, &pcpu_slot[slot], list) {//在该链表中进一步寻找符合尺寸要求的chunkif (size > chunk->contig_hint)continue;/*chunk用数组int*map记录每次分配的内存块,若该数组用完(该chunk仍然还有自由空间),则需要增长该int *map数组*/new_alloc = pcpu_need_to_extend(chunk);if (new_alloc) {spin_unlock_irqrestore(&pcpu_lock, flags);//扩展int *map数组if (pcpu_extend_area_map(chunk,new_alloc) < 0) {…}spin_lock_irqsave(&pcpu_lock, flags);goto restart;}/*在该chunk里分配虚拟内存空间:分割最后一段自由空间,然后重新将该chunk挂到slot数组对应链表中*/off = pcpu_alloc_area(chunk, size, align);//off大于0表示分配成功if (off >= 0)goto area_found;}}spin_unlock_irqrestore(&pcpu_lock, flags);/*创建一个新的chunk,这里进行的是虚拟地址空间的分配 */chunk = pcpu_create_chunk();…spin_lock_irqsave(&pcpu_lock, flags);//把一个全新的chunk挂到slot数组对应链表中pcpu_chunk_relocate(chunk, -1);goto restart;area_found:spin_unlock_irqrestore(&pcpu_lock, flags);/* 一次percpu内存分配成功,这里要检查该段区域对应物理页是否已经分配,否则将为该区域分配对应的物理页并作填充L1 L2页表项*/if (pcpu_populate_chunk(chunk, off, size)) {…}mutex_unlock(&pcpu_alloc_mutex);/* return address relative to base address */return __addr_to_pcpu_ptr(chunk->base_addr + off);…}1.3 CpuFreq

简单来说,CpuFreq由两方面组成,一方面是调频算法,其决定如何调频、何时调频、调频还是Up或Down处理器;一方面是具体操作由其实现如何调频。其实现为:(1)governor监测当前系统的负载情况,即为调频算法。(2)通过cpufreq_driver对Arm core 外围的时钟电路、供电电路实施操作,达到调频的作用,即为调频操作。

对于大部分CA9架构处理器,调频算法都是运行在CPU0上,通过CPU0来对其他调频、Down 或Up。1.3.1 初始化

CpuFreq初始化的工作主要有以下两个。

在底层将处理器具体操作函数注册到static struct cpufreq_driver *cpufreq_driver中,以满足处理器具体调频操作需要。SOC厂商都必须在struct cpufreq_driver里实现自己最底层的调频例程。struct cpufreq_driver {…int (*init) (struct cpufreq_policy *policy);…int (*target) (struct cpufreq_policy *policy,unsigned int target_freq,unsigned int relation);…};

其中最重要两个例程为:int (*target) (struct cpufreq_policy *policy, unsigned int target_freq, unsigned int relation);//该例程实现具体的调频操作“int (*init) (struct cpufreq_policy *policy);//该例程初始化struct cpufreq_policy

在较高级的处理器系统设备抽象层,将CpuFreq驱动static struct sysdev_driver cpufreq_sysdev_driver注册到处理器系统设备类,以匹配初始化每颗处理器的调频操作。/*SOC实现的BSP里都要通过调用int cpufreq_register_driver(struct cpufreq_driver *driver_data)向CpuFreq注册自己的struct cpufreq_driver,这将产生两个影响:将SOC相关的struct cpufreq_driver暴露给CpuFreq,governor将据此进行调频动作触发CPU设备驱动struct sysdev_driver cpufreq_sysdev_driver的注册这将导致对每一个CPU执行static int cpufreq_add_dev(struct sys_device *sys_dev)进一步触发cpufreq_driver->init对于CA9架构,该函数在具体的底层BSP中调用,参数就是SOC相关的具体调频操作*/int cpufreq_register_driver(struct cpufreq_driver *driver_data){…/*重要静态变量static struct cpufreq_driver *cpufreq_driver;记录了处理器底层相关的调频操作*/cpufreq_driver = driver_data;/*注册到struct sysdev_class cpu_sysdev_class,该函数会依次扫描处理器系统设备类中的处理器,并为其调用static struct sysdev_driver cpufreq_sysdev_driver的static int cpufreq_add_dev(…)函数,从而导致系统中调频机制的建立*/ret = sysdev_driver_register(&cpu_sysdev_class,&cpufreq_sysdev_driver);…}1.3.2 CpuFreq策略的建立

对于单颗处理器架构这里很好理解,完成处理器的struct cpufreq_policy初始化及struct cpufreq_governor的选择即可。但是对于多核处理器来说,这里隐藏着调频算法在哪颗处理器上执行的问题。/*该函数的关键是创建struct cpufreq_policy,每颗CPU初始化、wakeup的时候都会执行该函数,但是struct cpufreq_policy只在CPU0初始化时创建,后续CPU在自己的局部数据per_cpu(cpufreq_cpu_data, cpu);里将索引到该结构*/static int cpufreq_add_dev(struct sys_device *sys_dev){ …/*如果当前cpu被关闭,其不属于调频范畴*/if (cpu_is_offline(cpu))return 0;#ifdef CONFIG_SMP/*对于CA9架构,在CPU0初始化时创建struct cpufreq_policy,并且其余处理器的将共享CPU0创建的struct cpufreq_policy其余CPU初始化时直接索引到该struct cpufreq_policy,直接返回*/policy = cpufreq_cpu_get(cpu);…#endif/*CPU0执行到这里,分配struct cpufreq_policy结构*/policy = kzalloc(sizeof(struct cpufreq_policy), GFP_KERNEL);if (!policy)goto nomem_out;//分配struct cpufreq_policy结构覆盖的处理器范围if (!alloc_cpumask_var(&policy->cpus, GFP_KERNEL))goto err_free_policy;…//CPU0找到默认的GOVERNORif (!found)policy->governor = CPUFREQ_DEFAULT_GOVERNOR;/* 这里是关键,对于CA9架构多核处理器,如exynos4、imx6q,这里都把struct cpufreq_policy的cpumask_var_t cpus设置为所有的处理器。这里的原因是作为SMP的实现,CA9每个CORE的频率都必须保持一致,一次调频操作对所有的CORE都有相同的作用。这在之后的static int cpufreq_add_dev_interface(…)中,所有在线处理器的per_cpu(cpufreq_cpu_data, j)指针都指向了CPU0创建的struct cpufreq_policy。而每颗处理器的per_cpu(cpufreq_policy_cpu, j)都指向了CPU0*/ret = cpufreq_driver->init(policy);…//频率的最小最大值policy->user_policy.min = policy->min;policy->user_policy.max = policy->max;…ret = cpufreq_add_dev_interface(cpu, policy, sys_dev);…}static int cpufreq_add_dev_interface(unsigned int cpu,struct cpufreq_policy *policy,struct sys_device *sys_dev){/*体现SMP架构调频特性的操作,所有处理器协同调频步调一致*/for_each_cpu(j, policy->cpus) {if (!cpu_online(j))continue;per_cpu(cpufreq_cpu_data, j) = policy;per_cpu(cpufreq_policy_cpu, j) = policy->cpu;}…/*这里发送的CPUFREQ_GOV_START通知将导致处理器调频算法的启动*/ret = __cpufreq_set_policy(policy, &new_policy);…}1.3.3 Ondemand调频算法分析

所谓处理器调频算法struct cpufreq_governor没有准确的标准,适合的就是最好的,系统中提供了若干调频算法。本文仅选择struct cpufreq_governor cpufreq_gov_ondemand分析,这个算法的基本做法是启动一个定时器,定时检查系统负载,然后做出升降频率的决定。

首先CPUFREQ_GOV_START消息被struct cpufreq_governor cpufreq_gov_ondemand 截获:static int cpufreq_governor_dbs(struct cpufreq_policy *policy,unsigned int event){…switch (event) {//截获CPUFREQ_GOV_START消息case CPUFREQ_GOV_START:…//定时器初始化dbs_timer_init(this_dbs_info);break;…}启动定时器来检测系统负载:static inline void dbs_timer_init(struct cpu_dbs_info_s *dbs_info){…/*定时器函数static void do_dbs_timer(struct work_struct *work)是检测系统负载的重要机构*/INIT_DELAYED_WORK_DEFERRABLE(&dbs_info->work, do_dbs_timer);schedule_delayed_work_on(dbs_info->cpu, &dbs_info->work, delay);}检查系统负载来决定如何调频:static void do_dbs_timer(struct work_struct *work){…/*这里检查系统负载,无非就是累加各个处理器的idle时长,与系统设置做比较*/dbs_check_cpu(dbs_info);…//调频__cpufreq_driver_target(dbs_info->cur_policy,dbs_info->freq_lo, CPUFREQ_RELATION_H);…//准备下一次检测schedule_delayed_work_on(cpu, &dbs_info->work, delay);…}

在处理器实际实现中,有些SOC在实现中通过检查诸如总线忙碌之类的硬件参数以及处理器温度来决定调频操作。而有些系统为了节省成本省掉PMIC,调频框架并不产生实质的升降频操作。

内核中提供的调频算法一般只作为参考。根据SOC实现的不同,几乎大部分厂商的BSP都有自己的调频算法。其实现各有不同,但通常情况下有如下共性。(1)通过调整送入ARM Core的clock频率及其工作电压完成处理器的频率调整。通常:升频,先调压再调频;降频,先调频再调压。(2)某些处理器可以保持较高电压的情况下往下调频,但没有一种处理器可以在较低电压下往高调频。(3)电压的调整针对不同的PMIC进行,不同的PMIC提供不同的regulator,在drivers/regulator目录下实现不同PMIC的regulator操作。(4)调频前后,所有ONLINE会接到内核notify通知。(5)对于SMP处理器,所有的CORE都以相同的频率工作,所以每次调频都是对当前所有online的CORE进行操作。

涉及调频部分的代码是SOC厂商的敏感信息,往往开源代码中并不公开。为避免法律纠纷,这里不再分析具体厂家实现。1.4 CPU0 bootup CPU1

在SMP架构中,尽管每颗处理器运行时所属的地位都一样:一样的调度队列、一样的处理器选择策略。但是在系统引导时,这些处理器的地位是不同的。占有主导作用的是CPU0,该处理器最先被引导,为其他处理器创建运行环境之后,再boot up其他处理器。本节以双核处理器为原型分析,CPU1指的是非Booting处理器。本节分析实例为Exynos4210。

CPU1的激活时机有以下情况。(1)冷启动CPU0完成初始化后,叫醒CPU1。(2)从SUSPEND状态恢复,类似冷启动。(3)CPUFREQ监测到当前负载过高。1.4.1 CPU0侧策略和动作

在Bring up CPU1的过程中,CPU0主要有两方面的工作。(1)从CPU0跨入start_kernel那一刻起,任何一颗ONLINE的处理器在任何时刻内必然处在某一进程的Context空间内。即使调度队列为空,也必须进入Idle进程空间。所以,在Bring up其他非Booting CPU之前,CPU0需为其准备Idle进程的Context。(2)CPU0需要控制CPU1的电源、时钟等硬件开关及时序,引导CPU1进入内核入口。

当CPU0决定wakeup CPU1时,CPU0执行 cpu_up。//CPU0的执行路线int __cpuinit __cpu_up(unsigned int cpu){…// Idle指向待唤醒处理器调度队列中的线程if (!idle) {…} else {…//为即将唤醒的处理器准备一个idle线程init_idle(idle, cpu);}//把CPU1要用到的页表页目录准备好pgd = pgd_alloc(&init_mm);pmd = pmd_offset(pgd + pgd_index(PHYS_OFFSET), PHYS_OFFSET);*pmd = __pmd((PHYS_OFFSET & PGDIR_MASK) |PMD_TYPE_SECT | PMD_SECT_AP_WRITE);flush_pmd_entry(pmd);outer_clean_range(__pa(pmd), __pa(pmd + 1));//把为CPU1准备启动的参数放在secondary_data里secondary_data.stack = task_stack_page(idle) + THREAD_START_SP;secondary_data.pgdir = virt_to_phys(pgd);__cpuc_flush_dcache_area(&secondary_data, sizeof(secondary_data));outer_clean_range(__pa(&secondary_data), __pa(&secondary_data + 1));//叫醒CPU1ret = boot_secondary(cpu, idle);…return ret;}//CPU0唤醒CPU1的硬件操作int __cpuinit boot_secondary(unsigned int cpu, struct task_struct *idle){//CPU0把要叫醒的CPU写在pen_release中pen_release = cpu;__cpuc_flush_dcache_area((void *)&pen_release, sizeof(pen_release));outer_clean_range(__pa(&pen_release), __pa(&pen_release + 1));/*CPU0把CPU1的电源域打开,这时在另一侧,CPU1开始启动,CPU1跑到boot monitor中。CPU1将在 boot monitor等待*/if (!(__raw_readl(S5P_ARM_CORE1_STATUS) & S5P_CORE_LOCAL_PWR_EN)) {__raw_writel(S5P_CORE_LOCAL_PWR_EN,S5P_ARM_CORE1_CONFIGURATION);timeout = 10;/* 等待检查CPU1是否完成硬件Reset,并跑到Bootrom里相应位置*/while ((__raw_readl(S5P_ARM_CORE1_STATUS)& S5P_CORE_LOCAL_PWR_EN)!= S5P_CORE_LOCAL_PWR_EN) {if (timeout—— == 0)break;mdelay(1);}…}…/* CPU0把CPU1启动跳转地址告诉寄存器CPU1_BOOT_REG,这时CPU1还没有打开MMU,放置的是物理地址,CPU1侧将查看寄存器CPU1_BOOT_REG。CPU1不走BL1,CPU0的休眠唤醒才走BL1*/if (!__raw_readl(CPU1_BOOT_REG)) {//CPU1出了Bootrom后的地址__raw_writel(BSYM(virt_to_phys(s5pv310_secondary_startup)),CPU1_BOOT_REG);smp_cross_call(cpumask_of(cpu));}//CPU1重新设置了pen_release,CPU0这边的控制工作到此为止if (pen_release == -1)break;udelay(10);}…}1.4.2 CPU1侧执行路线

CPU1通常由自己电源域单独供电。当CPU1的电源域被打开后,CPU1直接以CPU0的当前频率起跳,然后进入处理器的片上bootrom里,在一个与CPU0约定的寄存器里查找自己的启动地址后,再从这个约定地址直接进入内核。对于CA9,这个进入的地址通常被命名为函数:xxx_secondary_startup。//非Booting 处理器入口地址ENTRY(s5pv310_secondary_startup)/*读取本CPU的ID,参见DDI0388F_cortex_a9_r2p2_trm.pdf*/Mrc p15, 0, r0, c0, c0, 5And r0, r0, #15Adr r4, 1fLdmia r4, {r5, r6}Sub r4, r4, r5Add r6, r6, r4/*查看pen_release的CPU ID是否是自己的ID*/pen:ldr r7, [r6]cmp r7, r0bne pen/* pen_release的CPU ID是自己的ID,__cpu_up()在叫自己*/b secondary_startup1: .long ..long pen_release// arch/arm/kernel/head.SENTRY(secondary_startup)…adr r4, __secondary_data/*r12被放入地址__secondary_switched*/ldmia r4, {r5, r7, r12} @ address to jump to aftersub r4, r4, r5 @ mmu has been enabledldr r4, [r7, r4] @ get secondary_data.pgdir/*执行完proc init 函数之后返回到__enable_mmu执行*/adr lr, BSYM(__enable_mmu) @ return address/*__enable_mmu执行之后会跳到r13执行。这里r13被放入__secondary_switched的地址*/mov r13, r12 @ __secondary_switched addressARM( add pc, r10, #PROCINFO_INITFUNC ) @ initialise processor@ (return control reg)THUMB( add r12, r10, #PROCINFO_INITFUNC )THUMB( mov pc, r12 )ENDPROC(secondary_startup)ENTRY(__secondary_switched)Ldr sp, [r7, #4] @ get secondary_data.stackmov fp, #0//最终执行secondary_start_kernelb secondary_start_kernelENDPROC(__secondary_switched)//__secondary_data处存放的内容.type __secondary_data, %object__secondary_data:.long ..long secondary_data.long __secondary_switched

接下来讨论一个特殊情况,即关闭CPU1失败后的再引导。这种情况发生在试图关闭CPU1,但是底层关闭操作动作失败。//CPU1关闭的最后一步void __ref cpu_die(void){…//关掉CPU1,最底层的CPU1关闭操作platform_cpu_die(cpu);/* 关闭失败,但是这时又不能直接返回Idle,需要重新走一次引导流程。正常情况下CPU1的wakeup不走这条路,只有在Die失败时才走这里*/__asm__("mov sp, %0\n"" b secondary_start_kernel":: "r" (task_stack_page(current) + THREAD_SIZE - 8));}1.5 CPU1的关闭1.5.1 关闭时机

在运行中系统根据负载或者电源策略而改变,为节省电力,在必要时关闭非Booting 处理器,而作为系统中坚持到最后的Booting处理器,在系统Suspend的时候也会关闭。本节仍以双核处理器为分析原型,分析非Booting处理器即CPU1的关闭时机。

CPU1关闭时机有以下两个情况。(1)在CpuFreq 机构监测到当前负载过低时,在某些平台实现时会关闭该处理器。内核负载检测机构会不停计算系统负载,在负载降低到一定阈值之后,将关闭CPU1。这里值得关注两个问题,首先,在某些SOC实现中硬件能够通过监控处理器状态、总线繁忙程度来判断负载情况,这时对系统负载的检测就不需要通过内核定时计算负载来完成;再者,事实上在负载变化时是先调频还是先关闭、打开处理器,每种SCO厂商采用的策略都不一样。对于核心数目较多的处理器笔者认为开关优先是较好的策略,对于双核处理器调频优先更为合理。//定时监测系统负载static void dbs_check_cpu(struct cpu_dbs_info_s *this_dbs_info){…//平均负载是否小于阈值if (avg_load < dbs_tuners_ins.down_threshold) {if (policy->cur == policy->min) {/* 当前活跃处理器较多,且hotplug_out_avg_load 小于阈值*/if (num_online_cpus() > 1 && hotplug_out_avg_load

CPU1的关闭由两方面组成:一是决策机构;二是CPU1本身。其关闭过程也分为以下阶段。(1)决策处理器上运行的决策算法决定关闭CPU1,CPU_DOWN_PREPARE类型通知被发出,导致cpu_active_bits的CPU1对应位被清除。(2)将static int __ref take_cpu_down(void *_param)操作挂到CPU1的struct cpu_stopper中,并等待CPU1完成操作。(3)CPU1上执行static int __ref take_cpu_down(void *_param),将cpu_online_bits上的CPU1对应位清除;将CPU1上中断转移走,并关闭局部时钟中断;更关键的是将系统中所有线程允许运行位图上的CPU1位清除;发出CPU_DYING通知,导致CPU1的wake_list链表上线程被加入CPU1运行队列(这时其运行位图已经去除了CPU1对应位),接下来执行static void migrate_tasks(unsigned int dead_cpu),将CPU1上线程迁移到别的处理器上。(4)CPU1继续运行,CPU1上将只剩IDLE线程可以运行。决策处理器继续往下运行等待CPU1上调度上idle线程。(5)CPU1进入idle task,在CPU1侧会清除CACHE之类的操作,然后在大部分CA9的实现中CPU1都会写入与决策处理器约定的某个寄存器,然后执行WFI。(6)决策处理器接下来与CPU1约定的寄存器被置位后,关闭CPU1电源。

接下来分析上述动作的源代码实现,首先看每个CPU的struct cpu_stopper结构。它用来处理该CPU被关掉之前的一些工作。struct cpu_stopper {spinlock_t lock;struct list_head works; /* list of pending works */struct task_struct *thread; /* stopper thread */bool enabled; /* is this stopper enabled? */};

struct cpu_stopper结构的成员变量struct task_struct*thread;执行的函数为:static int cpu_stopper_thread(void *data){不断从struct list_head works;取出work执行}

在被关掉的CPU1的cpu_stopper线程里执行的函数:static int __ref take_cpu_down(void *_param){struct take_cpu_down_param *param = _param;…//把这个CPU online位清零,并关掉这颗CPU的中断和时钟err = __cpu_disable();…//发出CPU_DYING通知cpu_notify(CPU_DYING | param->mod, param->hcpu);//把该CPU运行队列的task迁移走if (task_cpu(param->caller) == cpu)move_task_off_dead_cpu(cpu, param->caller);…//把这个CPU的idle线程调度起来sched_idle_next();return 0;}//在被关掉的CPU的idle函数中void cpu_idle(void){…#ifdef CONFIG_HOTPLUG_CPU//这颗CPU对应的online位已被清零,所以这颗CPU进入 cpu_die()if (cpu_is_offline(smp_processor_id()))cpu_die();#endif…}1.6 ARM处理器展望1.6.1 ARM架构处理器的演进

ARM公司不断推进ARM架构处理器的演进,在不同时期有着不同代表作。从彪炳史册的ARM7/9,到横空出世的Cortex A8/9,再到尚处研发状态的64位V8架构处理器。每一代的ARM处理器都有着自己的历史使命,ARM 7/9的任务是开疆拓土,实现了ARM的普及,Cortex A8/9完成了支撑智能手机推翻功能机的革命,64位架构目标是进军服务器领域。

然而这些都不是本节的重点,本节要分析的是Cortex A7/A15架构处理器。此处理器似乎仅仅是CA8/9的补充,没有明确的目标。但是Cortex A7/A15身上却蕴藏至今尚未被真正释放的能力——实现系统软件架构的革命。

1.Cortex A15

Cortex A15主要有以下3个方面的改进。(1)硬件支持虚拟化

ARM在CA15之后发布的处理器都支持硬件虚拟化,CA15扩展出了一个更高优先级的模式,将作为hypervisor的kernel和user mode都运行在这个模式,这种模式下提供了PL2的IPA到PA的转换以及VGIC。这样可以由Guest OS内核自主处理自己管辖范围内的页异常,而不必hypervisor大量的介入。笔者认为这是处理器虚拟化最关键的地方,不然大量的页异常掉到hypervisor,不仅性能受到影响,而且架构上也是畸形的。(2)40位物理地址

32位物理地址的问题以前在X86体系下出现过,现在又开始困扰ARM系统了。感谢进程间虚拟地址隔离,4G的虚拟地址仍能满足进程的需求,但是4GB的物理内存很快达到手机、平板的极限。40位物理地址为此而生,这样可以把任意一个4G的虚拟地址空间甩到1024G的物理内存的任何地方。(3)流水线的增加和Neon流水线的融合

Cortex A15像其他时代的ARM演进一样,都会扩充流水线的,改进分支预测等,但是值得一提的是NEON不再放在整数流水线的后面,当译码出NEON指令后再甩给NEON。A15里的NEON跟其他的流水线同时接受调度。

从Cortex A15的设计可看到PC级处理器的身影,更复杂的逻辑实现带来更高的性能。ARM一直以低功耗著称,而Intel的处理器则反之。是Intel的技术不如ARM?是Intel的工厂比不过Tsmc、SAMSUNG?显然都不是。原因在于:对高性能的追求,必然要用到更先进的处理器设计,更复杂的逻辑,导致在同样工艺水平下Intel处理器占用更大的芯片面积,自然功耗就上来了。当Intel忍痛阉割掉Medfiled里面的那些先进的复杂部件,自然功耗降下来了。这似乎是个围城,为了追求高性能,A15充分体现了ARM工程师的智慧,然而导致了芯片复杂度的升高,从而给ARM带来了困扰Intel的噩梦——功耗。

而反观Intel,由于其拥有独步天下的半导体工厂,在逐渐改进工艺的水平的情况下,可以使用更多复杂运算单元而保持功耗不变或下降。而ARM就玩不转了,因为ARM的投片工厂可能是台积电、三星、GlobalFoundries等大厂,但也有可能是一些工艺水平落后若干代的小厂。所以ARM的逻辑设计必须考虑到这些工厂的实现能力,一些先进复杂运算单元是无法及时在其处理器设计中使用的。所以一些大的半导体公司,干脆直接重新设计其ARM处理器,然后在使用台积电等大厂最先进的工艺来生产,这似乎起到了些较好效果比如Qualcomm的Snapdragon。但是笔者以为,这还是比不了人剑合一的Intel。

但是,天下是天下人的天下。ARM阵营有着高效、低成本的特点,不仅横行移动市场而且大有谋取PC、Server的趋势。

2.Cortex A7与Big.Little

Cortex A7与CA15共享指令集,但是CA7不能乱序执行,性能比CA8略低,CA7似乎是CA15的缩小版,其Die Size只有A15的1/4,ARM发布这个架构处理器的目的似乎是直奔功耗而来。除了可以作为一颗单独的处理器,CA7还可以与CA15搭配工作,构成big.little架构,这个架构的关键如下。(1)大部分系统运行的时候都不需要太多处理器能力,一个多核系统的大部分时间都是将其他的Core关掉的。这时Cortex A7就能满足要求,可以把耗电的A15关闭掉。但系统遇到计算压力的时候再bring up A15,以实现高性能。(2)由于A15与A7是软件完全兼容的,所以可以套用内核现有SMP支持架构而不需要大规模改动。

如果指令集相同,在不修改内核模型的情况下,不只是A15搭A7,A9搭A5也是一样可行的。

由于其小巧、省电且保持相当计算能力的特点。A7已成为低功耗及价格敏感市场的新兴力量,MTK以及几乎所有的大陆手机处理器厂商在近几年的主力产品都使用多核A7。而一些老牌嵌入式厂商也将A7作为计算中能力要求不高、且价格敏感市场的主力。1.6.2 TrustZone

TrustZone是ARM架构的安全扩展,在ARM11时首先出现,其主要思想是通过将处理器扩展出一个新的Security特权状态以完成安全性相关工作。但是尽管安全领域的市场需求强烈而迫切,TrustZone自问世以来一直未被业界广泛接纳。其原因在于:(1)TrustZone要求在Security特权状态构建一个全新的包括内核、应用在内的生态系统,而这个生态系统一直没有成长起来。(2)由于TrustZone本身的架构问题,导致在其上实现软件体系会遭遇如下问题。

① 首先,若所有的处理器核心全进入Security特权状态,势必修改当前内核模型,Non-Security状态下平滑运行的状态被打断,而所有的Core都进入Security特权状态又无必要,Security特权状态下的主要应用只有加解密算法和输入。若只有一个处理器进入Security特权状态,架构体系上又不够完美。

② 即使不考虑架构设计和运行效率,Security特权状态另外一个麻烦在于,当在Security特权状态的某个Core操作外设时,势必与Non-Security状态下的系统软件冲突,这与虚拟化中Guest访问非虚拟设备的问题一样,后面再详细介绍。

③ 另外,TrustZone机制的一个基础在于总线访问时外设依赖其当前AXI交易的安全位来做出如何响应,但是往往Security特权状态需要控制的外设IP设计并没有这个考虑。

所以,尽管TrustZone有着强烈而广泛的市场需求,但是由于其本身对现有软件体系发起的挑战,使得业界不得不另外寻找方式以解决安全问题。

其实从某种角度来看,TrustZone的Security与NON-Security模式更适合虚拟化架构,在安全模式可以使用独立的页表页目录、而且安全模式下也有对应于非安全模式下的7种模式,类似于Intel的处理器虚拟化架构的Root和Guest的关系。这样KVM运行在安全模式下还能保持内核-SVC与用户-USR的架构,非常完美。

兼容是处理器演进的第一规律,所有挑战这个规律的处理器都是找死,即使不成功的处理器设计也得兼容。为安全架构另外再设计一个模式,然后稍加修改TrustZone以实现ARM架构完美虚拟化。但是ARM没有这样做,而是选择将HYP塞在PL1的下面,这是一个尚可接受但并不完美的虚拟化架构,也许是这个规律在起作用。注意,笔者是说 “也许”。1.6.3 ARM Virtualization

ARM架构在CA15以后发布的A系列处理器都支持虚拟化扩展。在这种体系下实现虚拟化有多种方案可以选择,尽管所有的方案理论上都可行,做DEMO工程没有问题,但是如果能够产品化,笔者认为只有KVM更适合ARM虚拟化体系,其原因如下。(1)在一个没有虚拟化IO支持的硬件架构上,由于坚持使用独立的Hypervisor,嵌入式虚拟化的实现不得不允许Guest OS直接访问硬件设备。由此带来诸多隐患:多个Guest OS和Hypervisor陷入硬件访问冲突、中断分发、DMA控制的深渊。因为控制外设不是仅仅读写,还要考虑到其休眠唤醒、中断分发等情况。比如Guest OS自身的休眠对设备的影响,设备的休眠与电源域相互影响,而电源域包括多个设备必然在HyperVisor里控制,这自然导致与其他Guest OS相互影响。可见由于需要和硬件平台过度耦合,难以扩展,嵌入式虚拟化只适合硬件复杂度较小的硬件平台,或者只使用部分功能的复杂硬件平台。(2)而诸如Xen之类虚拟化方案修改Linux BSP成为Domain0,这样所有硬件访问都可以被Domain0接管,理论上是可行的。但是笔者认为这不是一个适合ARM虚拟化体系的好架构。而且Xen架构设计之初是为了适配没有硬件支持X86处理器,在有着硬件支持的处理器上这个虚拟化架构过于臃肿,使得简单问题复杂化。(3)另外,抛开架构不谈,将Linux BSP改成Domain0的工程量也不可同日而语。在CA8以后,ARM SOC体系已经不再像ARM9那么简单了。SOC构成普遍出现了GPU、VPU等多个主设备,层层交叉互联结构的总线,独立而又相互依赖的电源域、时钟域,层级的中断控制器与多核情况下的中断分发。要完美实现对所有硬件组件的控制势必有很多验证、测试工程量。(4)更进一步,还存在一个非技术因素,在于ARM SOC界的游戏规则发生改变,半导体厂商不再像以前一样公开SOC中所有硬件信息。很多硬件IP,厂商虽然开源了BSP,但是却没有任何资料描述,而诸如GPU之类的驱动只以二进制的形式发布。这使得Linux BSP改成Domain0成为非原厂而不能为的事情。

所以,笔者认为最适合ARM的虚拟化架构是KVM,其特点在于能够遵循现有以Linux BSP管理硬件的游戏规则;避免Guest OS直接访问外设以提高移植性;可以采用Frontend+Backend驱动模型,以提高其性能。

ARM KVM的实现完全符合KVM原有架构,只是在处理器相关部分与ARM 虚拟化扩展做了适配。其实现如下:(1)ARM虚拟化扩展出的Hyp模式拥有最高优先权PL2,但是这个模式下仅作为HOST与Guest切换的中转。(2)HOST Linux工作在PL0和PL1状态下,其Context被保存在Hyp的stack中。(3)Guest Linux工作在PL0和PL1状态下,其Context被保存在其虚拟处理器相关结构中。

Hyp模式下一张__kvm_hyp_vector异常表是ARM KVM架构适配的核心,搭起Host与Guest之间的桥梁。__kvm_hyp_vector:.globl __kvm_hyp_vectorW(b) hyp_resetW(b) hyp_undefW(b) hyp_svcW(b) hyp_pabtW(b) hyp_dabtW(b) hyp_hvcW(b) hyp_irqW(b) hyp_fiq

根据ARM KVM的设计,hyp_undef、hyp_svc、hyp_pabt、hyp_dabt四种异常目前被认为是不可能出现在Hyp模式的,这几种异常向量都被定义为没有意义的bad_exception。

1.HVC异常

作为两侧HVC指令异常的入口,hyp_hvc异常向量是Hyp模式下最重要的异常向量:hyp_hvc:/*HVC调用通常携带参数而来,这里先将其保存*/push {r0, r1, r2}/*HSR寄存器里记录了HVC异常的产生是否是由于PL1发生HVC异常调用,这里读取HSR寄存器,并检查是否是PL1遇到了HVC指令*/mrc p15, 4, r1, c5, c2, 0 @ HSRlsr r0, r1, #HSR_EC_SHIFT…cmp r0, #HSR_EC_HVCbne guest_trap @ Not HVC instr./*确认是PL1遇到HVC指令,接下来要通过检查VMID区分是来自Host还是Guest的调用*/mrrc p15, 6, r0, r2, c2lsr r2, r2, #16and r2, r2, #0xffcmp r2, #0/*来自Guest的 HVC调用将跳转至guest_trap*/bne guest_trap @ Guest called HVC/*来自HOST的 HVC调用走到这里*/host_switch_to_hyp:/*弹出刚才保存的HOST送下来参数*/pop {r0, r1, r2}/*保存SPSR*/push {lr}mrs lr, SPSRpush {lr}/*HOST调用kvm_call_hyp,将Hyp里的调用地址放在r0里,struct kvm_vcpu指针放在r1里,这里把跳转地址放入lr,struct kvm_vcpu指针放入r0。这里还要处理好r2、r3,以备将来之需*/mov lr, r0mov r0, r1mov r1, r2mov r2, r3…/*跳转到__kvm_vcpu_run准备恢复Guest*/blx lr @ Call the HYP function/*从Guest返回到Host的最后步骤,将从这里弹到PL1*/pop {lr}msr SPSR_csxf, lrpop {lr}/*Hyp模式遇到eret,弹到PL1*/eret/*Guest hvc异常走到这里*/guest_trap://r0存放VCPU的Contex的指针load_vcpu @ Load VCPU pointer to r0str r1, [vcpu, #VCPU_HSR]/*若从Guest而来,除了HVC调用,还可能是MMU转换异常,如对设备寄存器地址的访问*/lsr r1, r1, #HSR_EC_SHIFTcmp r1, #HSR_EC_IABTmrceq p15, 4, r2, c6, c0, 2 @ HIFARbeq 2fcmp r1, #HSR_EC_DABT/*不是转换问题,直接走返回路径*/bne 1f/*接下来读取Cp15获取必要信息,在这里展开,继续分析往Host的跳转*/…/*记录下是HVC调用*/1: mov r1, #ARM_EXCEPTION_HVC/*Host的跳转函数*/b __kvm_vcpu_return/*到这里,说明Hyp对Guest产生的异常感到绝望,最有可能是Guest 内核里的野指针,交给Guest自己了断*/4: pop {r0, r1} @ Failed translation, return to guestmcrr p15, 0, r0, r1, c7 @ PARclrexpop {r0, r1, r2}eret

接下来分析Hyp模式中对Host向Guest切换的支持。/*这个函数脉络很清晰,即把Host Context压栈,恢复Vcpu,eret*/ENTRY(__kvm_vcpu_run)…/*这是一个宏把Host Context压栈,值得注意的是这里的Context,除了通用寄存器、状态寄存器,还有CP15里的相关信息*/save_host_regs…/*恢复vcpu状态*/restore_guest_regsclrex/*弹到Guest*/eret

从Guest返回Host与从Host恢复Guest类似,只是弹向Host的操作不是在这里进行的,参见上文。/*Guest 向Host返回,来至Guest的Hvc异常*/__kvm_vcpu_return:/*保存Guest的Context*/save_guest_regs…/*恢复Host的Context,这里的关键是栈里的lr并不是Host的lr,而是Host切换到Guest之前的下一条地址,因为当时Hyp使用了blx,而不是向Guest向Host时使用的b。其实无论host、hyp、guest本身就是一个线程,至此弹掉vcpu,接着之前的栈轨迹,非常符合逻辑*/restore_host_regs…/*返回到__kvm_vcpu_run之前的下一条指令 */bx lr @ return to IOCTL

2.中断与快速中断异常

接下来再分析hyp异常表的另外两个异常处理。/*中断异常向量函数*/hyp_irq:push {r0, r1, r2}/*保存下原因,中断导致的Guest Exit */mov r1, #ARM_EXCEPTION_IRQload_vcpu @ Load VCPU pointer to r0/*继续向Host返回,与hvc异常操作一样*/b __kvm_vcpu_return.align/*快速中断异常处理,与中断异常处理无异*/hyp_fiq:b hyp_fiq

针对ARM KVM进行再深层次分析。Host Linux并没有工作在Hyp模式下,而逻辑上Hyp模式就好比一扇连接HOST世界与GUSET世界的大门,与ARM处理SE与NON-SE状态切换的手法如出一辙,都是通过异常来实现。尽管HOST与Guest都工作在PL0和PL1,但是HOST是有着优先级特权的,正是这扇大门将ROOT世界和Guest世界隔离开来。

Hyp模式下的所有工作都可以通过处理器硬件扩展来实现,这样处理器模式就可以精简掉Hyp模式,但是ARM选择了这种轻量级方式,ARM的生存哲学又一次体现在其演进道路上。第2章 异 常

异常,这个词非常形象地描述了诸如中断、MMU转换、系统调用等需要内核介入的情况与系统运行时的关系。系统的绝大部分工作时间应该是属于应用程序的,应用程序运行才是系统工作的正常情况,内核介入的时候都属于不正常状态。

操作系统设计的目的是为了运行应用,无论内核再强大,它在系统中都是应该位居幕后的、默默无闻的,应该像大地一样稳稳的支撑,而不用过多的关注。若一个内核需要时不时在系统中展露自己,那一定不是一个好的内核。

ARM处理器设计了若干种模式以便内核处理对应的情况,本章分析ARM Linux对异常处理的基本框架,并着重分析与虚拟内存管理密切相关的数据异常以及影响整个内核机理的中断异常。

传统ARM体系的7种异常类型如下。(1)复位异常。即复位,从reset异常向量重新执行。(2)数据异常。即访问数据时由于数据页面不存在或者页表项没有建立等原因引起的异常。(3)预取指令异常。即访问处理器取指令时由于代码页面不存在或者页表项没有建立等原因引起的异常。(4)快速中断请求异常。快速中断Fiq。(5)中断请求。中断IRQ。(6)软件中断异常。用于系统调用,即SWI,内核维护一张系统调用表,一旦SWI陷入内核,内核根据系统调用号索引到相应API,并依据用户态送下来的参数调用相应函数API,完成后走类似用户态中断、数据及取指异常的路径返回用户态。(7)未定义异常。2.1 异常向量表2.1.1 异常进入

根据ARM处理规范,ARM的异常向量表可以位于0X0000或者0XFFFF0000,这两个位置处理器可以任选实现。现代处理器一般都是可以通过软件来动态选择异常向量表的位置。又鉴于大部分SOC实现都是使用0XFFFF0000,因此本书认为0XFFFF0000就是ARM异常向量表地址。

在Linux内核中,ARM Linux的异常向量表位于arch/arm/kernel/entry-armv.S中。其地址范围为__vectors_start到__vectors_end,依次为复位异常、未定义指令异常、软件中断异常、取指令异常、取数据异常、保留异常、中断异常、快速中断异常。.globl __vectors_start__vectors_start:swi SYS_ERROR0b vector_und + stubs_offsetldr pc, .LCvswi + stubs_offsetb vector_pabt + stubs_offsetb vector_dabt + stubs_offsetb vector_addrexcptn + stubs_offsetb vector_irq + stubs_offsetb vector_fiq + stubs_offset.globl __vectors_end

其中每种类型的异常都会跳转到一个异常处理代码段。尽管异常种类很多,但是这些异常代码段的实现是如下形式:vector_stub XXX, XXX_MODE, 4 //调用宏vector_stub,其中XXX为某种异常/*以下又是一个跳转表,该跳转表描述了处理器在某种模式下发生该种异常的处理方式,其中最重要的是usr和svc模式,因为Linux系统下只用到这两个模式。而ARM只有7种模式(不考虑CA15的Hypervisor模式,也不考虑Trustzone的Monitor模式),但是该跳转表确定了16项,其原因在于状态寄存器的0~4位都被用来标记处理器模式了,所以为了防止某个异常的出现而搞的内核不知所措,该跳转表被定义为16项*/.long __XXX_usr @ 0 (USR_26 / USR_32).long __XXX_invalid @ 1 (FIQ_26 / FIQ_32).long __XXX_invalid @ 2 (IRQ_26 / IRQ_32).long __XXX_svc @ 3 (SVC_26 / SVC_32).long __XXX_invalid @ 4….long __XXX_invalid @ e.long __XXX_invalid @ f

异常处理宏的实现如下:.macro vector_stub, name, mode, correction=0.align 5vector_\name:.if \correctionsub lr, lr, #\correction.endif/*发生异常时arm会把cpsr保存在spsr里,所以检查spsr就能查出,异常发生前处理器的模式*/@ Save r0, lr_ (parent PC) and spsr_@ (parent CPSR)stmia sp, {r0, lr} @ save r0, lr //lr存放异常发生前pc的值,r0是swi的参数mrs lr, spsr //取出spsr的内容到lr中str lr, [sp, #8] @ save spsr/*不管是从什么模式进入异常的,在Linux内核使用SVC模式,所以将处理器的状态切换到SVC模式*/@ Prepare for SVC32 mode. IRQs remain disabled.mrs r0, cpsr //取出cpsr的内容eor r0, r0, #(\mode ^ SVC_MODE) //置为SVCmsr spsr_cxsf, r0 //写回cpsr@@ the branch table must immediately follow this code@and lr, lr, #0x0f //取出异常发生前cpsr的模式状态位mov r0, sp/*根据模式确定跳转值。当前pc值作为基址,pc的值为下一条指令地址,所以跳转的地址应该是紧跟当前这段代码的一个地址数组,即上问提到16个跳转项*/ldr lr, [pc, lr, lsl #2]movs pc, lr @ branch to handler in SVC modeENDPROC(vector_\name).endm2.1.2 异常表的构建

以上定义了内核的异常向量表和对应的处理函数,编译后被链接到内核Image,但是内核Image被加载到物理内存的地址不是使得该向量表正好能被映射到虚拟地址0xffff0000,所以内核还需要将异常向量表搬到0xffff0000,而向量表与对应的处理函数的跳转使用指令B,为相对跳转指令,指令B一共才32位长,除去B的编码,留给携带的偏移量只有32M,所以还得把处理函数处理到0xffff0000附近,且偏移量要跟链接里的相等。

1.位于arch/arm/kernel/traps.cvoid __init early_trap_init(void){//SOC使用的异常向量表基地址可认为CONFIG_VECTORS_BASE=0XFFFF0000unsigned long vectors = CONFIG_VECTORS_BASE;extern char __stubs_start[], __stubs_end[];extern char __vectors_start[], __vectors_end[];extern char __kuser_helper_start[], __kuser_helper_end[];int kuser_sz = __kuser_helper_end - __kuser_helper_start;/** Copy the vectors, stubs and kuser helpers (in entry-armv.S)* into the vector page, mapped at 0xffff0000, and ensure these* are visible to the instruction stream.*///把异常向量表移动到0XFFFF0000,__vectors_end - __vectors_start为异常向量表长度memcpy((void *)vectors, __vectors_start, __vectors_end - __vectors_start);/*把这些vector_xxx函数及.LCvswi移动到0XFFFF0000+0x200的位置, __stubs_start为vector_xxx及.LCvswi函数开始位置,__stubs_end为结束位置*/memcpy((void *)vectors + 0x200, __stubs_start, __stubs_end - __stubs_start);…//有代码移动,更新icacheflush_icache_range(vectors, vectors + PAGE_SIZE);…}

由此可见在arch/arm/kernel/entry-armv.S准备了异常向量表及其跳转地址,而实际工作时还需要再做一次移动,0XFFFF0000附近才是它们的工作地点。

2.SMP中异常的初始化

对于SMP系统,发生异常时每个处理器都要索引到自己的异常表,否则处理器是没法工作的。在CPU0的初始化函数void __init early_trap_init(void)已经建立了虚拟地址与异常表映射,接下来将其余处理器的启动过程中CPU0建立的那套页表页目录复制过来这样就可以进行异常索引了。

在CPU0启动过程中,会执行int __cpuinit __cpu_up(unsigned int cpu)。下面代码为其余CPU复制页表页目录:int __cpuinit __cpu_up(unsigned int cpu){…/*CPU0复制自己的页表页目录,这样在4.1.3节里CPU0为自己创建的异常表也为其他CPU可用了*/pgd = pgd_alloc(&init_mm);…secondary_data.pgdir = virt_to_phys(pgd);…/** Now bring the CPU into our world.*/ret = boot_secondary(cpu, idle);…}

由此可以得出结论:在一个SMP系统里,在CPU0初始化异常表之后,所有其他的CPU都共享这个异常入口,地址都一致。2.2 中断体系2.2.1 Cortex A9多核处理器的中断控制器GIC

在CA9以前,每种SOC的中断控制器是自己实现的,但是到了CA9 SMP以后,中断控制器成为ARM规范的一部分,各家的处理器都遵循ARM中断控制器GIC规范——IHI0048A_gic_architecture_spec。其中原因在于,对于非SMP的架构,中断控制器就是控制中断的功能,完成中断的记录、ack、屏蔽等功能即可,各家厂商爱怎么玩就怎么玩。但是到了SMP结构下,除了ack、记录、屏蔽之外,中断控制器还面临如下问题。(1)中断分发给哪颗处理器。(2)处理器间通信如何实现——处理器通信本质上就是一种中断。(3)处理器局部中断。

ARM提供的中断控制器GIC就是解决以上问题,根据GIC规范,中断安排如下(参见IHI0048A_gic_architecture_spec的Figure 2-1 GIC logical partitioning into Distributor and CPU interfaces)。(1)32-1019 SPI 全局中断,可指定分发到不同的处理器。(2)16-31 PPP 处理器局部中断,处理器局部可见。(3)0-15 SGI 软件产生中断,由CPU写ICDSGIR产生,可指定分发到不同的处 理器。

GIC本身分成两部分:(1)GIC的DIST部分,这部分是所有CPU公用的。(2)每颗CPU自己CPU interface部分,这部分控制寄存器地址,只能被CPU局部可见,ARM推荐各SOC厂商将该地址实现为相同位置。

以上概念在GIC描述结构struct gic_chip_data中得到体现。struct gic_chip_data {unsigned int irq_offset; //中断号的偏移量void __percpu __iomem **dist_base; //记录每颗CPU访问GIC Dist的基地址void __percpu __iomem **cpu_base; /*记录每颗CPU访问GIC CPU interface的基地址,每颗处理器看到同样的地方,但是访问的却是不同的寄存器*/…unsigned int gic_irqs;};2.2.2 MT6577的中断体系

MTK与Android是绝配。在智能机时代,MTK尽管开始走了段弯路,但是很快拨乱反正。MTK的智能机时代策略,与其功能机时代如出一辙,这与其市场定位有关。

MTK手机处理器的技术路线不追求最快,但一定会在市场需要时准时出现;不追求最强,但一定能流畅运行最新版本的Android系统与应用。

MTK的公板,从器件选型、电路板设计到Android系统实现,MTK亲力亲为,几乎做好了所有技术上的工作。MTK的参考设计总是能做到与大陆手机产业链完美匹配,着力支持手机供应链中最常用、供货最有保障器件和外设,而同一种功能器件和外设,进行多家验证,以确保整机商供应链的安全。由此,华南的IC、模具、LCD、SMT等产业链以MTK马首是瞻,总是自觉地将产品与MTK公板的兼容性作为其重要工作。

本节分析MT6577的中断体系。MT6577的Datasheet不是个公开的文档,况且该文档里也并没有清晰地阐述其中断体系的结构。但是通过U8836d公开的代码却可以完整地分析出MT6577的中断架构。MT6577的中断体系的最高层依然遵循GIC规范,可参见ARM公司release相关技术文件。MT6577中的实现可参见U8836d公开的代码中的文件://U8836D\mediatek\platform\mt6577\kernel\core\include\mach\mt6577_irq.c//16个软件中断#define NR_GIC_SGI 16//16个处理器局部中断,在mt6577中其实只使用了5个,从第27号开始#define NR_GIC_PPI 16//128个全局中断#define MT6577_NR_SPI (128)#define NR_MT6577_IRQ_LINE (NR_GIC_SGI + NR_GIC_PPI + MT6577_NR_SPI)//全局中断从第32号开始#define GIC_PRIVATE_SIGNALS 32//处理器局部中断的起始号#define GIC_PPI_OFFSET (27)//如下定义了5个处理器局部中断#define GIC_PPI_GLOBAL_TIMER (GIC_PPI_OFFSET + 0)#define GIC_PPI_LEGACY_FIQ (GIC_PPI_OFFSET + 1)#define GIC_PPI_PRIVATE_TIMER (GIC_PPI_OFFSET + 2)#define GIC_PPI_WATCHDOG_TIMER (GIC_PPI_OFFSET + 3)#define GIC_PPI_LEGACY_IRQ (GIC_PPI_OFFSET + 4)

然后从32号中断开始定义了104个全局中断。#define MT6577_L2CCINTR_IRQ_ID (GIC_PRIVATE_SIGNALS + 0)…#define MT6577_USB0_IRQ_ID (GIC_PRIVATE_SIGNALS + 8)#define MT6577_USB1_IRQ_ID (GIC_PRIVATE_SIGNALS + 9)…#define MT6577_EINT_IRQ_ID (GIC_PRIVATE_SIGNALS + 96)…#define MT6577_EINT_DIRECT7_IRQ_ID (GIC_PRIVATE_SIGNALS + 104)

以上中断体系的实现是通过GIC的配置来完成,而GIC的配置在初始化过程中完成。GIC初始化包括DIST的初始化和每颗CPU自己interface的初始化。DIST初始化由CPU0完成,然后CPU0再完成自己备份的interface初始化。在CPU1起来之后再完成CPU1的相关interface初始化。

CPU0的工作代码如下:void __init mt_init_irq(void){ …//DIST的初始化mt_gic_dist_init();//CPU0的interface初始化mt_gic_cpu_init();}static void mt_gic_dist_init(void){unsigned int i;u32 cpumask = 1 << smp_processor_id();cpumask |= cpumask << 8;cpumask |= cpumask << 16;writel(0, GIC_DIST_BASE + GIC_DIST_CTRL);/*从32号开始,设置SPI中断电平触发,N-N模式。GIC从GIC_DIST_CONFIG ——0xC00开始是中断分发寄存器ICDICFR,每个SPI对应2位*/for (i = 32; i < (MT6577_NR_SPI + 32); i += 16) {writel(0, GIC_DIST_BASE + GIC_DIST_CONFIG + i * 4 / 16);}/*所有SPI分发给CPU0。这部分代码只有CPU0才能执行到。GIC从GIC_DIST_TARGET ——0x800开始是中断分发寄存器,长度根据支持的SPI不同而不同,每个中断对应8位,哪一位置1,该中断就发到哪颗处理器。Cpumask每8位的最低位都置1,所以从32号中断开始,SPI都分发CPU0*/for (i = 32; i < (MT6577_NR_SPI + 32); i += 4) {writel(cpumask, GIC_DIST_BASE + GIC_DIST_TARGET + i * 4 / 4);}/*GIC从GIC_DIST_PRI ——0x400开始是中断优先级寄存器,长度根据支持的SPI不同而不同,每个中断对应8位,这里把所有SPI优先级都设置为相同优先级*/for (i = 32; i < NR_MT6577_IRQ_LINE; i += 4) {writel(0xA0A0A0A0, GIC_DIST_BASE + GIC_DIST_PRI + i * 4 / 4);}/*GIC从GIC_DIST_ENABLE_CLEAR ——0x180开始是中断屏蔽寄存器ICDICER,长度根据支持的SPI不同而不同,每个中断对应1位,置1屏蔽该中断*/for (i = 32; i < NR_MT6577_IRQ_LINE; i += 32) {writel(0xFFFFFFFF, GIC_DIST_BASE + GIC_DIST_ENABLE_CLEAR + i * 4 / 32);}/*把挂到Linux中断描述数组里去,每个中断一个struct irq_desc,以前是个数组,现在可选成radix树。每个中断一个struct irq_desc,记录两个中断处理的关键数据结构struct irq_chip和 irq_flow_handler_t。前者是用来对付中断控制器,在mt6577上是struct irq_chip mt_irq_chip,后者是中断处理程序,分成void handle_level_irq(unsigned int irq, struct irq_desc *desc),其用来对付电平触发和void handle_edge_irq(unsigned int irq, struct irq_desc *desc),其用来对付边沿触发*/for (i = GIC_PPI_OFFSET; i < NR_MT6577_IRQ_LINE; i++) {irq_set_chip_and_handler(i, &mt_irq_chip, handle_level_irq);set_irq_flags(i, IRQF_VALID | IRQF_PROBE);}#ifdef CONFIG_FIQ_DEBUGGERirq_set_chip_and_handler(FIQ_DBG_SGI, &mt_irq_chip, handle_level_irq);set_irq_flags(FIQ_DBG_SGI, IRQF_VALID | IRQF_PROBE);#endif/*GIC从GIC_ICDISR ——0x80开始是中断屏蔽寄存器ICDISR,长度根据支持的SPI不同而不同,每个中断对应1位,置0表示该中断是安全中断,置1表示该中断是非安全中断,这里全置1*/for (i = 32; i < NR_IRQS; i += 32){writel(0xFFFFFFFF, GIC_ICDISR + 4 * (i / 32));}/*ICDDCR位于0x,中断总开关打开*/writel(3, GIC_DIST_BASE + GIC_DIST_CTRL);}//该函数在每颗CPU被激活时得到执行static void mt_gic_cpu_init(void){int i;/*关闭PPI,打开SGI。PPI需要用到时调用void mt_enable_ppi(int irq)打开,在u8836d系统里只有local timer是PPI中断,在其初始化时将自己对应使能位打开,SGI必须全打开,SGI用来做处理器间中断的基础*/writel(0xffff0000, GIC_DIST_BASE + GIC_DIST_ENABLE_CLEAR);writel(0x0000ffff, GIC_DIST_BASE + GIC_DIST_ENABLE_SET);/* 设置PPI SGI中断优先级为0x80*/for (i = 0; i < 32; i += 4)writel(0x80808080, GIC_DIST_BASE + GIC_DIST_PRI + i * 4 / 4);/*设置PPI SGI中断优先为非secure中断*/writel(0xFFFFFFFF, GIC_ICDISR);/*操作自己的CPU INTERFACE的ICCPMR寄存器,只有优先级高于0xF0的中断才会送入本处理器*/writel(0xF0, GIC_CPU_BASE + GIC_CPU_PRIMASK);/*操作自己的CPU INTERFACE的ICCICR寄存器,做如下设置:(1)允许向本处理器送secure中断(2)允许向本处理器送非secure中断(3)把secure中断送到本处理器的FIQ中断线(4)中断抢占通过secure binary point register判断(5)一个ICCIAR的secure读,导致非secure最高优先级pending中断的ack*/writel(0x1F, GIC_CPU_BASE + GIC_CPU_CTRL);dsb();}

值得注意的是MT6577全局中断的实现又分为两种。(1)对于集成在SOC里的外设,直接对应一个全局中断,如MT6577_USB1_IRQ_ID,在其驱动初始时直接将其中断处理函数注册到内核的中断处理里数组中:request_irq(nIrq, musbfsh->isr, IRQF_TRIGGER_LOW, dev_name(dev), musbfsh)(2)对于位于SCO之外的外设,则通过MT6577_EINT_IRQ_ID 进行转发,MT6577在内核里准备了一个外设中断处理函数的数组:typedef struct{void (*eint_func[EINT_MAX_CHANNEL])(void);unsigned int eint_auto_umask[EINT_MAX_CHANNEL];} eint_func;

这些外设通过void mt65xx_eint_registration(…)注册自己的中断函数。

比如触屏控制器gt813,mediatek\custom\out\huaqin77_cu_ics2\kernel\touchpanel\ Gt813_driver.c通过该函数注册自己的中断处理函数:mt65xx_eint_registration(…, tpd_eint_interrupt_handler, 1);

当这些SOC外设中断发生以后,MT6577将在GIC的MT6577_EINT_IRQ_ID上产生中断,从而触发其中断处理函数static irqreturn_t mt65xx_eint_isr(int irq, void *dev_id)。在该函数里将检查eint_func数组,进一步触发对应的处理函数。2.2.3 Exynos4的中断体系

三星,ARM处理器界的新王者,近年来抢先实现每一代ARM处理器,而且通过手机处理器与其庞大硬件产业链的有机整合成为Apple的最有力对手。

第一次将三星与高性能手机处理器联系起来的是其S5PV210,凭借超出同级处理器一倍的L2 Cache,S5PV210成为当时跑得最快的CA8 ARM。紧接着三星开始抢跑ARM界,在CA9时期,三星已经成为ARM处理器界的第一阵营,在CA15时期三星成为第一家先进ARM架构的实现者。

三星的ARM处理器成功背后的很大原因在于三星强大的产业链整合能力。在前端,三星手机的攻城略地保证了三星处理器出货量。在后端,三星的半导体工厂有比肩Intel的能力,总是可以用更先进的工艺来降低功耗,节省成本。在市场大门开启的时候,三星的对手却要为处理器代工厂的工艺和良率伤透脑筋,为炫酷的LCD屏幕供货周期头疼,为了内存和EMMC的价格波动心惊胆战,而所有这些,三星集团的各个工厂数月之前都已经协调完毕,正按计划批量出货了。

1.exynos4的中断体系

CPU0的GIC初始化工作从void __init exynos4_init_irq(void)开始。void __init exynos4_init_irq(void){int irq;gic_bank_offset = soc_is_exynos4412() ? 0x4000 : 0x8000;/*S5P_VA_GIC_DIST是CPU0的GIC DIST基地址,S5P_VA_GIC_CPU是CPU0的GIC CPU interface 基地址*/gic_init(0, IRQ_PPI_MCT_L, S5P_VA_GIC_DIST, S5P_VA_GIC_CPU);…}void __init gic_init(unsigned int gic_nr, unsigned int irq_start,void __iomem *dist_base, void __iomem *cpu_base){struct gic_chip_data *gic;int cpu;…//GIC描述结构:struct gic_chip_datagic = &gic_data[gic_nr];//为每颗CPU分配放置DIST和CPU interface基地址的内存地址gic->dist_base = alloc_percpu(void __iomem *);gic->cpu_base = alloc_percpu(void __iomem *);…//先初始化CPU0的DIST和CPU interface基地址for_each_possible_cpu(cpu) {*per_cpu_ptr(gic->dist_base, cpu) = dist_base;*per_cpu_ptr(gic->cpu_base, cpu) = cpu_base;}gic->irq_offset = (irq_start - 1) & ~31;if (gic_nr == 0)gic_cpu_base_addr = cpu_base;//DIST的初始化gic_dist_init(gic, irq_start);//CPU0的CPU interface基地址的内存gic_cpu_init(gic);}/*所谓GIC dist的初始化,系统里只在CPU0进行,核心工作是配置GIC SPI中断(通常外围设备产生的中断)*/static void __init gic_dist_init(struct gic_chip_data *gic,unsigned int irq_start){unsigned int gic_irqs, irq_limit, i;void __iomem *base = gic_data_dist_base(gic);u32 cpumask = 1 << smp_processor_id();//cpumask:每个8位的最低位都置1cpumask |= cpumask << 8;cpumask |= cpumask << 16;writel_relaxed(0, base + GIC_DIST_CTRL);/** Find out how many interrupts are supported.* The GIC only supports up to 1020 interrupt sources.*/gic_irqs = readl_relaxed(base + GIC_DIST_CTR) & 0x1f;gic_irqs = (gic_irqs + 1) * 32;if (gic_irqs > 1020)gic_irqs = 1020;/** Set all global interrupts to be level triggered, active low.*///先将SPI设置为电平触发for (i = 32; i < gic_irqs; i += 16)writel_relaxed(0, base + GIC_DIST_CONFIG + i * 4 / 16);/** Set all global interrupts to this CPU only.*//*偏移量0x800~0x81C空间的每个BYTE指出了对应的中断分发到哪颗CPU上,这里把所有的全局中断的分发方向都指向自己,这时只有CPU0可用。以后还可以使用struct irq_chip的int (*set_affinity)(unsigned int irq,const struct cpumask *dest);函数将中断绑定到另外的CPU上*/for (i = 32; i < gic_irqs; i += 4)writel_relaxed(cpumask, base + GIC_DIST_TARGET + i * 4 / 4);/** Set priority on all global interrupts.*///0x400-0x7F8for (i = 32; i < gic_irqs; i += 4)writel_relaxed(0xa0a0a0a0, base + GIC_DIST_PRI + i * 4 / 4);/*关闭除PPI 和SGIs 之外的中断*/for (i = 32; i < gic_irqs; i += 32)writel_relaxed(0xffffffff, base + GIC_DIST_ENABLE_CLEAR + i * 4 / 32);/** Limit number of interrupts registered to the platform maximum*/irq_limit = gic->irq_offset + gic_irqs;if (WARN_ON(irq_limit > NR_IRQS))irq_limit = NR_IRQS;/** Setup the Linux IRQ subsystem.*///填充linux里generic中断分发数组for (i = irq_start; i < irq_limit; i++) {irq_set_chip_and_handler(i, &gic_chip, handle_fasteoi_irq);irq_set_chip_data(i, gic);set_irq_flags(i, IRQF_VALID | IRQF_PROBE);}writel_relaxed(1, base + GIC_DIST_CTRL);}/*CPU0的GIC cpu interface中断初始化,主要是PPI和SGI初始化,SGI是处理器间通信的重要手段*/static void __cpuinit gic_cpu_init(struct gic_chip_data *gic){void __iomem *dist_base = gic_data_dist_base(gic);void __iomem *base = gic_data_cpu_base(gic);int i;/** Deal with the banked PPI and SGI interrupts - disable all* PPI interrupts, ensure all SGI interrupts are enabled.*///值得注意的是,这里PPI中断是被屏蔽的writel_relaxed(0xffff0000, dist_base + GIC_DIST_ENABLE_CLEAR);writel_relaxed(0x0000ffff, dist_base + GIC_DIST_ENABLE_SET);/** Set priority on PPI and SGI interrupts*/for (i = 0; i < 32; i += 4)writel_relaxed(0xa0a0a0a0, dist_base + GIC_DIST_PRI + i * 4 / 4);writel_relaxed(0xf0, base + GIC_CPU_PRIMASK);writel_relaxed(1, base + GIC_CPU_CTRL);}

2.CPUX相关的GIC配置

CPUX不需要初始化GIC的SPI中断,所以CPUX相关的GIC初始化工作完成自身的GIC cpu interface中断初始化即可。值得关注是其执行流程:asmlinkage void __cpuinit secondary_start_kernel(void){…platform_secondary_init(cpu);…}void __cpuinit platform_secondary_init(unsigned int cpu){/*显然对于Exynos4x12处理器,CPUX的GIC DIST和CPU interface基地址的计算是:CPU0的对应基地址+ 前面CPU占用的长度*/void __iomem *dist_base = S5P_VA_GIC_DIST +(gic_bank_offset * cpu);void __iomem *cpu_base = S5P_VA_GIC_CPU +(gic_bank_offset * cpu);gic_secondary_init_base(0, dist_base, cpu_base);…}void __cpuinit gic_secondary_init_base(unsigned int gic_nr,void __iomem *dist_base,void __iomem *cpu_base){//先把基地址放到GIC描述结构里的数组里if (dist_base)*__this_cpu_ptr(gic_data[gic_nr].dist_base) = dist_base;if (cpu_base)*__this_cpu_ptr(gic_data[gic_nr].cpu_base) = cpu_base;//CPUX的PPI SGI中断初始化见前面的介绍gic_cpu_init(&gic_data[gic_nr]);}2.2.4 OMAP4的中断体系

尽管已经宣布退出手机市场,但是作为移动处理器领域的领袖,Ti在相当长的时间里总是抢先发布性能最强的新一代ARM处理器,而且早期还会搭配其强劲的DSP,以配合ARM CORE工作。尽管Ti在3G时代遭受专利困境,但是凭借其强大的ARM处理器设计能力在没有Modem的情况下支撑了两代:Omap3是第一款Cortex A8产品,且加入了C64+DSP,当时的性能在业界无出其右者。接着随着Neon和硬件编解码的兴起,Omap4弱化了DSP作用,但依然领导业界进入双核时代。

也许因为Omap5的功耗实在不能满足手机需求,也许无法集成Modem弱化了Ti的竞争力,也许Ti早已决心转身模拟,以后的手机处理器将不会有Ti的身影。Ti将Omap5的积累转化成其嵌入式产品keystoneII,但是嵌入式处理器的演进由于其市场需求的原因,将不会像手机处理器那样精彩。

本节介绍Omap4中断体系。

1.初始化void __init gic_init_irq(void){…//首先map出中断控制器Distributor寄存器空间gic_dist_base_addr = ioremap(OMAP44XX_GIC_DIST_BASE, SZ_4K);BUG_ON(!gic_dist_base_addr);//对Distributor的初始化gic_dist_init(0, gic_dist_base_addr, 29);/* Static mapping, never released *///map出中断控制器cpu interface寄存器空间gic_cpu_base_addr = ioremap(OMAP44XX_GIC_CPU_BASE, SZ_512);BUG_ON(!gic_cpu_base_addr);gic_cpu_init(0, gic_cpu_base_addr);/*以上2次map,实际上就是在页表中建立虚拟地址到物理地址的映射。这个动作只在主处理器初始化时进行,而在第二颗处理器初始化时并未进行map操作。其原因在于:(1)第二颗处理器初始化过程中上拷贝了主处理器的页表页目录。(2)从每颗处理器往外看,Distributor系统中就一个,其物理地址位于:#define OMAP44XX_GIC_DIST_BASE 0x48241000虽然cpu interface每个处理器都有自己的空间,但是显然在Omap4的实现中将其做成了相同的物理地址*/#define OMAP44XX_GIC_CPU_BASE 0x48240100}void __cpuinit gic_cpu_init(unsigned int gic_nr, void __iomem *base){ …gic_data[gic_nr].cpu_base = base;/*初始化CPU中断控制信息,这里没有显示的区分对哪颗CPU操作,但是由于上述的虚拟地址的拷贝关系,哪个处理器执行到这里就对哪颗处理器操作*/writel(0xf0, base + GIC_CPU_PRIMASK);writel(1, base + GIC_CPU_CTRL);}void __init gic_dist_init(unsigned int gic_nr, void __iomem *base, unsigned int irq_start){//这里最关键的是记下Distributor的虚拟地址gic_data[gic_nr].dist_base = base;}

2.第二颗CPU interface的初始化void __cpuinit platform_secondary_init(unsigned int cpu){trace_hardirqs_off();//初始化该CPU 中断控制信息gic_cpu_init(0, gic_cpu_base_addr);…spin_lock(&boot_lock);spin_unlock(&boot_lock);}

3.CPU间中断发射static inline void smp_cross_call(const struct cpumask *mask){gic_raise_softirq(mask, 1);}//mask记录了要向哪些处理器发射中断,irq记录要产生的中断号#ifdef CONFIG_SMPvoid gic_raise_softirq(const struct cpumask *mask, unsigned int irq){unsigned long map = *cpus_addr(*mask);/* this always happens on GIC0 *//*Distributor 的Software Generated Interrupt Register (ICDSGIR)用法如下:第16位——第23位:目标处理器第0 位——第4位 :中断号以下语句向mask标记的CPU发射irq号中断*/writel(map << 16 | irq, gic_data[0].dist_base + GIC_DIST_SOFTINT);}#endif2.3 中断处理

中断表面的作用是作为硬件和驱动的一部分,把中断送入处理器,激活中断handler以响应外设。但是对于内核中断的意义远非这些,对于调度器,中断是主要激励源,中断的发生意味着可能的线程唤醒,中断退出意味着可能的线程抢占。对于信号机制,若发生在用户态,中断返回意味着信号的执行。对于多核处理器中断是其处理器间交流的手段。

本书把中断在内核其他机制的分析放在其相关章节,这里仅仅分析中断本身。2.3.1 中断的基本结构

为了有一个整体的印象,先来分析中断基本结构,以内核模式发生中断为背景。首先中断异常代码识别是USER模式还是内核模式,如果是内核模式发生的中断,则跳入__irq_svc:__irq_svc:svc_entry //中断进入#ifdef CONFIG_PREEMPT //抢占维护工作//得到thread_info指针get_thread_info tsk/* preempt_count的最低8位记录着是否可以线程抢占,中断发生时可以发生中断嵌套,更高优先级中断抢占当前级别的中断。但是新唤醒的线程却不能在当前中断处理过程中抢占中断handler。因为当前中断借用当前线程的内核栈,优先级是针对线程而言的,即使新唤醒的线程优先级较高也是比这个当前线程高,并不能说明比中断本身高。再者线程抢占中断是没有意义的。若对实时性要求的确很高,可以把中断处理线程化,中断唤醒线程化的handler后即退出,让线程去抢占线程*/ldr r8, [tsk, #TI_PREEMPT] @ get preempt countadd r7, r8, #1 @ increment itstr r7, [tsk, #TI_PREEMPT]#endifirq_handler //中断分发#ifdef CONFIG_PREEMPT//抢占维护工作//本次中断退出,恢复抢占计数str r8, [tsk, #TI_PREEMPT] @ restore preempt countldr r0, [tsk, #TI_FLAGS] @ get flagsteq r8, #0 @ if preempt count!= 0movne r0, #0 @ force flags to 0//更高优先级的线程被唤醒,若当前线程被抢占tst r0, #_TIF_NEED_RESCHEDblne svc_preempt#endifldr r4, [sp, #S_PSR] @ irqs are already disabled…//中断退出svc_exit r4 @ return from exceptionUNWIND(.fnend )ENDPROC(__irq_svc)

svc_entry 是一个宏,用于SVC模式中断进入,主要是完成寄存器保护的工作:.macro svc_entry, stack_hole=0……//把内核栈的指针拉下来,为存放寄存器留出空间sub sp, sp, #(S_FRAME_SIZE + \stack_hole - 4)…//把r1-r12 这12个寄存器放进内核栈stmia sp, {r1 - r12}….endm

以上是内核态发生中断的处理,对于用户态发生中断,最大区别是在返回会检查是否有signal要处理。接下来继续看中断如何进入中断handler。

中断handler的入口也是一个宏定义:.macro irq_handler…arch_irq_handler_default9997:.endm

arch_irq_handler_default的实现位于,在文件kernel/arch/arm/kernel/entry-armv.S里包含了该文件:.macro arch_irq_handler_default//把GIC基地址取出来放到r5里get_irqnr_preamble r5, lr1: get_irqnr_and_base r0, r6, r5, lrmovne r1, sp@@ routine called with r0 = irq number, r1 = struct pt_regs *@adrne lr, BSYM(1b)/*普通中断,分发*/bne asm_do_IRQ#ifdef CONFIG_SMP//检查是否是IPI中断,IPI中断是SGI类型,中断号小于16ALT_SMP(test_for_ipi r0, r6, r5, lr)ALT_UP_B(9997f)//r1存放栈指针,跟r0一道作为参数送入do_IPImovne r1, spadrne lr, BSYM(1b)/*处理器间中断 */bne do_IPI#ifdef CONFIG_LOCAL_TIMERS//检查处理器局部时钟,29号test_for_ltirq r0, r6, r5, lrmovne r0, spadrne lr, BSYM(1b)/*tick中断*/bne do_local_timer#endif…#endif9997:.endm2.3.2 中断源识别

中断发生时,首先要确认该中断来自系统中哪个部位,这就是中断源识别。但是尽管中断控制器在CA9成为规范,但是每种架构的中断体系都不一样,所以中断源识别在每种架构下的实现也都不一样,本节以exynos4与MT6577为例分析中断源识别。

1.exynos4的中断源实现//代码位置:arch/arm/mach-exynos/include/mach/entry-macro.S.macro get_irqnr_preamble, base, tmp#ifdef CONFIG_ARCH_EXYNOS4mov \tmp, #0//读取CP15里的MPIDR寄存器mrc p15, 0, \base, c0, c0, 5// MPIDR的最低2位指示当前读取动作是哪颗CPU所为,寄存器base里就是处理器号and \base, \base, #3cmp \base, #0beq 1fldr \tmp, =gic_bank_offsetldr \tmp, [\tmp]cmp \base, #1beq 1fcmp \base, #2//CPU2addeq \tmp, \tmp, \tmp//CPU3addne \tmp, \tmp, \tmp, LSL #1#endif1: ldr \base, =gic_cpu_base_addrldr \base, [\base]#ifdef CONFIG_ARCH_EXYNOS4add \base, \base, \tmp#endif.endm

值得注意的是,该版本的代码源自SAMSUNG公开的其i9300(四核处理器版本)源码包,可见其GIC的cpu interface实现没有遵循ARM的官方推荐——每颗处理器cpu interface基地址都不一样。而对于kernel.org,仅支持exy4210(双核版本),代码中其GIC的cpu interface实现又遵循了ARM官方的推荐——每颗处理器cpu interface基地址都一样。.macro get_irqnr_preamble, base, tmp//直接取基地址,两颗处理器得到的地址都一样ldr \base, =gic_cpu_base_addrldr \base, [\base].endm.macro get_irqnr_and_base, irqnr, irqstat, base, tmp/*base是当前CPU GIC interface的基地址,基地址偏移#GIC_CPU_INTACK(0XC)位置的寄存器是Interrupt Acknowledge Register (ICCIAR)。它的12~10位记录了如果是SGI类型的中断,产生该中断的CPU号。第0~9位记录的就是当前中断号*/ldr \irqstat, [\base, #GIC_CPU_INTACK] /* bits 12-10 = src CPU, 9-0 = int # */cmp \irqnr, #29cmpcc \irqnr, \irqnrcmpne \irqnr, \tmpcmpcs \irqnr, \irqnraddne \irqnr, \irqnr, #32….endm

2.MT6577的中断源实现//取出当前处理器gic intrface的基地址.macro get_irqnr_preamble, base, tmpldr \base, =GIC_CPU_BASE.endm.macro get_irqnr_and_base, irqnr, irqstat, base, tmp//取ICCIAR到irqstatldr \irqstat, [\base, #GIC_CPU_INTACK] /* bits 12-10 = src CPU, 9-0 = int # */// NR_IRQS是mt6577支持的最高中断号16+16+128ldr \tmp, =NR_IRQS//将ICCIAR 与上0x1c00的反码,取9-0位到irqnrbic \irqnr, \irqstat, #0x1c00/* if (irqnr >= NR_IRQS) return NO_IRQ (0) */cmp \irqnr, \tmpmovcs \tmp, #0bcs BSYM(702f)/* if (irqnr >= 32) return HAVE_IRQ (1) */cmp \irqnr, #(32)movcs \tmp, #1bcs BSYM(702f)/* if (irqnr == FIQ_DBG_SGI) return HAVE_IRQ (1) */cmp \irqnr, #FIQ_DBG_SGImoveq \tmp, #1beq BSYM(702f)/* otherwise, return NO_IRQ (0) */mov \tmp, #0702:cmp \tmp, #0cmpeq \irqnr, \irqnr.endm//ipi类型的中断检测.macro test_for_ipi, irqnr, irqstat, base, tmp//取出中断号放在irqnrbic \irqnr, \irqstat, #0x1c00//用中断号减16cmp \irqnr, #16//c清零,无符号数小于,小于16号中断,为SGI,写GIC,ack该中断strcc \irqstat, [\base, #GIC_CPU_EOI]/*c置位表示无符号数大于或等于,则不属于SGI中断,若小于16号中断,该指令不执行,bne条件成立,若大于16号中断,该指令执行,bne执行条件不满足*/cmpcs \irqnr, \irqnr.endm//检测是否是lcoal timer.macro test_for_ltirq, irqnr, irqstat, base, tmp//取出中断号放在irqnrbic \irqnr, \irqstat, #0x1c00mov \tmp, #0//local timer是29号中断,比较cmp \irqnr, #29moveq \tmp, #1/*写GIC的end of interrupt寄存器,该寄存器格式要求cpuid个中断号,ack该中断,与icciar一样,把irqstat回填即可*/streq \irqstat, [\base, #GIC_CPU_EOI]//如果是29,tmp里不应该是0cmp \tmp, #0.endm//检查watchdog中断.macro test_for_wdtirq, irqnr, irqstat, base, tmp//取出中断号放在irqnrbic \irqnr, \irqstat, #0x1c00mov \tmp, #0cmp \irqnr, #30moveq \tmp, #1//写GIC的EOI,ack该中断streq \irqstat, [\base, #GIC_CPU_EOI]cmp \tmp, #0.endm2.4 数 据 异 常

数据访问异常和取指异常,都是处理器在转换数据页和代码页时遇到的,是虚拟内存机制的上层触发入口,两者工作机理类似,本节以数据异常为例展开,为下一章节虚拟内存的分析做铺垫。

首先再次审视异常向量表中数据异常相关部分,这是数据异常的内核入口:vector_stub dabt, ABT_MODE, 8/*用户态发生的异常,比如程序里访问某个变量*/.long __dabt_usr @ 0 (USR_26 / USR_32).long __dabt_invalid @ 1 (FIQ_26 / FIQ_32).long __dabt_invalid @ 2 (IRQ_26 / IRQ_32)/*内核态发生了异常。内核态正常情况下不可能发生这个异常,除非是遇到了ex_table,再者就是BUG了*/.long __dabt_svc @ 3 (SVC_26 / SVC_32)…

另一方面,内核准备了一张处理表用来处理每一种数据异常:static struct fsr_info ifsr_info[] = {{ do_bad, SIGBUS, 0, "unknown 0" },…{ do_bad, SIGSEGV, SEGV_ACCERR, "section access flag fault" },{ do_bad, SIGBUS, 0, "unknown 4" },{ do_translation_fault, SIGSEGV, SEGV_MAPERR, "section translation fault" },{ do_bad, SIGSEGV, SEGV_ACCERR, "page access flag fault"},{ do_page_fault, SIGSEGV, SEGV_MAPERR, "page translation fault"},{ do_bad, SIGBUS, 0, "external abort on non-linefetch" },{ do_bad, SIGSEGV, SEGV_ACCERR, "section domain fault"},{ do_bad, SIGBUS, 0, "unknown 10" },{ do_bad, SIGSEGV, SEGV_ACCERR, "page domain fault" },{ do_bad, SIGBUS, 0, "external abort on translation" },{ do_sect_fault, SIGSEGV, SEGV_ACCERR, "section permission fault"},{ do_bad, SIGBUS, 0, "external abort on translation" },{ do_page_fault, SIGSEGV, SEGV_ACCERR, "page permission fault"},…{ do_bad, SIGBUS, 0, "unknown 31" },};

该异常表覆盖了ARM处理器可能发生的所有的数据异常,但是因为ARM的有些机制Linux内核并没使用到,所以对于这类异常只能默认使用do_bad,这将导致系统panic。而对于Linux使用的ARM机制这些异常都有对应的处理函数。数据异常发生后,无论用户态异常还是内核态异常,都会进入到内核的数据异常处理函数中:/*该函数的参数addr记录了异常产生的虚拟地址,参数fsr记录了异常产生的原因*/asmlinkage void __exceptiondo_DataAbort(unsigned long addr, unsigned int fsr, struct pt_regs *regs){struct thread_info *thread = current_thread_info();/*根据异常产生的原因索引处理函数表*/const struct fsr_info *inf = fsr_info + fsr_fs(fsr);int ret;struct siginfo info;//调用合适的异常处理函数,如果返回非零,说明发生了不正常的异常if (!inf->fn(addr, fsr & ~FSR_LNX_PF, regs))return;info.si_signo = inf->sig;info.si_errno = 0;info.si_code = inf->code;info.si_addr = (void __user *)addr;/*如果是用户态发生的不正常异常,发信号杀死该进程;如果是内核态发生的不正常异常,panic*/arm_notify_die("", regs, &info, fsr, 0);}

接下来就到了内核的虚拟内存处理,这是一个庞大的机制,影响到系统的方方面面,将在本书的其他章节进行分析。2.5 处理器间通信

处理器间通信的基础是中断,在CA9架构下0~15号的软件触发中断SGI。在GCI初始化的时候各处理器开放了自己的0~15中断,以便接受别的处理器发给自己的中断。而GIC的ICDSGIR用来产生SGI中断,其格式如下:● Bit 0~3 SGIINTID,产生的SGI中断号。● Bit 4~14 reserved。● Bit 15安全位 0:发射安全中断 1:发射非安全中断。● Bit 16~23 目标处理器位图 每颗处理器对应一位,置1将向该处

理器发射SGI中断。● Bit 24~25 00:不考虑每个CPU interface的指示来发射中断;01

或10:根据每个CPU interface的指示来发射中断或不发射中断。

在ARM Linux系统系统系统下bit 24~25为00。//接下分析中断代码分析整理自基于开源手机u8836d(mt6577)笔记void irq_raise_softirq(const struct cpumask *mask, unsigned int irq){unsigned long map = *cpus_addr(*mask);int satt;u32 val;/*向CPU1发射 IPI_CPU_START中断,是CPU0 wakeup CPU1的最后一个动作,这里如果检测到是IPI_CPU_START中断,则其安全位被置为secure中断。这个中断假设的场景是CPU1处在WFI状态,发射该中断使其继续往下跑,但是事实上这个中断的并没起到必要的作用。当CPU0 发射这个中断时CPU1的中断被禁止的,CPU1直到跑到void __cpuinit secondary_start_kernel(void)的末尾才打开中断,这里并不是WFI,这里的IPI_CPU_START机制是没有意义的*/satt = (irq == IPI_CPU_START)? 0: (1 << 15);/*从dist偏移量0x80,记录了每个中断的secure属性,每个中断对应1位,置0表示secure中断,置1表示非secure中断,这里读取要发射中断的属性信息*/val = __raw_readl(GIC_ICDISR + 4 * (irq / 32));if (!(val & (1 << (irq % 32)))) { /* secure interrupt? *///该位为0,secure中断satt = 0;}/*将中断号、中断位、中断目标一并写入ICDSGIR*/__raw_writel((map << 16) | satt | irq, GIC_DIST_BASE + 0xf00);}

内核定义了如下几种处理器间中断:enum ipi_msg_type {IPI_CPU_START = 1,IPI_TIMER = 2,/*这是最常用的处理器间中断,被调度器使用,它用来通知某目标处理器,其运行队列被挂入若干task,目标处理器接到该中断后,把外挂队列的task active到自己的运行队列,具体参见调度器一节*/IPI_RESCHEDULE,IPI_CALL_FUNC,/*在特定目标处理器上运行某特定函数,这个也很常用,是一种重要的内核机制,在后面有详细介绍*/IPI_CALL_FUNC_SINGLE,IPI_CPU_STOP,IPI_CPU_BACKTRACE,};

以IPI_RESCHEDULE为例分析IPI发送过程:(1)函数指针:static void (*smp_cross_call)(const struct cpumask *, unsigned int);指向处理器间通信的发射函数,在系统初始化时,将该指针指向该平台的IPI实现函数,对于u8836d 自然就是void irq_raise_softirq(const struct cpumask *mask, unsigned int irq),而别的CA9 SMP架构下的实现与函数也非常相似。(2)当前处理器侧,调用void smp_send_reschedule(int cpu)函数,其中 cpu是目标处理器。(3)目标处理器侧产生SGI中断,导致函数asmlinkage void __exception_irq_entry do_IPI(int ipinr, struct pt_regs *regs)被调用。从硬件中断的产生到该函数的调用参见上文。//其中ipinr是中断号,regs与普通中断一样是存储的寄存器状态asmlinkage void __exception_irq_entry do_IPI(int ipinr, struct pt_regs *regs){/*取出CPU号*/unsigned int cpu = smp_processor_id();struct pt_regs *old_regs = set_irq_regs(regs);/*中断状态的统计,标志对于IPI中断又发生了一次*/if (ipinr >= IPI_CPU_START && ipinr < IPI_CPU_START + NR_IPI)__inc_irq_stat(cpu, ipi_irqs[ipinr - IPI_CPU_START]);switch (ipinr) {case IPI_CPU_START:/* Wake up from WFI/WFE using SGI */break;case IPI_TIMER:__raw_get_cpu_var(mt_timer_irq) = IPI_TIMER;ipi_timer();break;/*目标处理器接受到信息*/case IPI_RESCHEDULE:/*调整自己的运行队列*/scheduler_ipi();break;case IPI_CALL_FUNC:generic_smp_call_function_interrupt();break;/*目标侧IPI_CALL_FUNC_SINGLE 响应*/case IPI_CALL_FUNC_SINGLE:generic_smp_call_function_single_interrupt();break;…}…}

IPI_CALL_FUNC_SINGLE

每颗处理器都有一个struct call_single_queue队列,队列由“per_cpu(call_single_queue, cpu)”来索引,该队列上每一个节点——struct call_single_data都有一个函数指针指向某个待该处理器执行的函数。

当前处理器有需要某特定处理器执行函数的时候调用:/*cpu为目标处理器,func 指向目标处理器需要执行的函数,info为参数信息 wait指出是否等待对方处理器同步*/int smp_call_function_single(int cpu, smp_call_func_t func, void *info,int wait){struct call_single_data d = {.flags = 0,};…//取出当前cputhis_cpu = get_cpu();//如果目标处理器就是自己,就不要折腾了,执行即可if (cpu == this_cpu) {local_irq_save(flags);func(info);local_irq_restore(flags);} else {/*验证cpu号,验证该cpu是否在线 */if ((unsigned)cpu < nr_cpu_ids && cpu_online(cpu)) {struct call_single_data *data = &dif (!wait)data = &__get_cpu_var(csd_data);//将该struct call_single_data的flags置CSD_FLAG_LOCKcsd_lock(data);//设置目标处理器位,函数指针信息cpumask_set_cpu(cpu, data->cpumask);data->func = func;data->info = info;//执行generic_exec_single(cpu, data, wait);} else {err = -ENXIO; /* CPU not online */}}/*释放该处理器*/put_cpu();return err;}staticint generic_exec_single(int cpu, struct call_single_data *data, int wait){//这里的cpu是目标cpu,索引目标处理器的struct call_single_queue队列struct call_single_queue *dst = &per_cpu(call_single_queue, cpu);…//关中断,上锁,要保证这个链表操作不被打扰raw_spin_lock_irqsave(&dst->lock, flags);ipi = list_empty(&dst->list);list_add_tail(&data->list, &dst->list);//恢复中断,开锁raw_spin_unlock_irqrestore(&dst->lock, flags);//发送IPI_CALL_FUNC_SINGLEif (ipi)arch_send_call_function_single_ipi(cpu);//等待,要等待对方处理器把指定函数跑完if (wait)err = csd_lock_wait(data);return err;}/*这个等待是瞬间完成的过程,没有休眠*/static int csd_lock_wait(struct call_single_data *data){int cpu, nr_online_cpus = 0;/*检查CSD_FLAG_LOCK 标志是否被对方清楚,对方处理只有在跑完指定函数后才会清楚这个标志*/while (data->flags & CSD_FLAG_LOCK) {//不停检查在线处理器个数for_each_cpu(cpu, data->cpumask) {if (cpu_online(cpu)) {nr_online_cpus++;}}//如果等待的处理器一个都不在了就返回if (!nr_online_cpus)return -ENXIO;//等待操作cpu_relax();}return 0;}

目标CPU接到IPI_CALL_FUNC_SINGLE后调用:void generic_smp_call_function_single_interrupt(void){ //取出当前struct call_single_queue队列struct call_single_queue *q = &__get_cpu_var(call_single_queue);unsigned int data_flags;…//如果struct call_single_queue队列非空,说明有活要干while (!list_empty(&list)) {struct call_single_data *data;//依次取出待执行的函数指针data = list_entry(list.next, struct call_single_data, list);list_del(&data->list);data_flags = data->flags;//执行该函数data->func(data->info);/*下面要解锁了,对方处理器还得等待呢*/if (data_flags & CSD_FLAG_LOCK)csd_unlock(data);}}static void csd_unlock(struct call_single_data *data){ …//清楚CSD_FLAG_LOCK,释放对方处理器data->flags &= ~CSD_FLAG_LOCK;}第3章 调度与实时性3.1 Tick

Tick是内核的生命脉搏。对于Fair 调度类和RT调度类中的SCHED_RR线程,Tick的每次到来将意味调度实际的临近。Tick还意味当前User进程Signal的处理时机。对于处于Sleep状态处理器,在满足定时Timer的基础上,则需消除不必要的Tick。3.1.1 Local timer

Local timer可以叫做Ticker,是Tick之源,是处理器局部的。其产生的中断Tick只被对应处理器处理。

在ARM CA9架构下有两种方式的Local timer。(1)SPI类型的Local timer。将中断分发给对应的处理器即可,但是这样似乎有些别扭。(2)PPI类型的Local timer。只被局部处理器看到,不占用SPI号,自然和谐。

CA9早期的SOC实现主要是SPI类型的Local timer,在CA9的后期版本的SOC实现中,以PPI类型 Local timer为主流。

Local timer的具体实现与处理器相关,以Exynos4412为例。内核里每颗处理器对应的Local timer都对应一个struct mct_clock_event_device的实例:struct mct_clock_event_device mct_tick[NR_CPUS];

每颗处理器的Local timer的初始化如下:static void exynos4_mct_tick_init(struct clock_event_device *evt){//取出CPU号unsigned int cpu = smp_processor_id();//用CPU号索引对应数组mct_tick[cpu].evt = evt;/*对于每个处理器,其Local timer控制寄存器的基地址都不同,偏移量为0x100:#define EXYNOS4_MCT_L_BASE(x) (_EXYNOS4_MCT_L_BASE + (0x100 * x))而且其中断类型为PPI,由此可以推断,Exynos4412里面每颗处理器都有一个Local timer的具体实现,而且在其事件发生时仅产生PPI中断。这是较为优美的Local timer实现,比4个SPI中断好多了*/mct_tick[cpu].base = EXYNOS4_MCT_L_BASE(cpu);evt->name = mct_tick[cpu].name;evt->cpumask = cpumask_of(cpu);// set_next_event set_mode函数设置evt->set_next_event = exynos4_tick_set_next_event;evt->set_mode = exynos4_tick_set_mode;//该Local timer的能力evt->features = CLOCK_EVT_FEAT_PERIODIC | CLOCK_EVT_FEAT_ONESHOT;//优先级evt->rating = 450;…//向系统中注册该timerclockevents_register_device(evt);exynos4_mct_write(TICK_BASE_CNT, mct_tick[cpu].base + MCT_L_TCNTB_OFFSET);// mct_int_type的值是MCT_INT_PPI,不用考虑SPI中断了if (mct_int_type == MCT_INT_SPI) {if (cpu == 0) {…} else {mct_tick1_event_irq.dev_id = &mct_tick[cpu];//挂载中断setup_irq(IRQ_MCT_L1, &mct_tick1_event_irq);//中断定向irq_set_affinity(IRQ_MCT_L1, cpumask_of(1));}} else {//这里使能该PPI中断,在初始化刚完成时GIC的 PPI中断是disable的gic_enable_ppi(IRQ_PPI_MCT_L);}}3.1.2 Tick挂载

在CA9 SMP架构下,每个Core使用自己的Local timer作为自己的tick之源。

在arch/arm/kernel/smp.c中,内核为每颗处理器定义了自己的struct clock_event_device:static DEFINE_PER_CPU(struct clock_event_device, percpu_clockevent);

在每个Core初始化时,需要把自己的Tick源打开:void __cpuinit percpu_timer_setup(void){/*取得自己的CPU号*/unsigned int cpu = smp_processor_id();/*通过自己的CPU号索引自己的struct clock_event_device*/struct clock_event_device *evt = &per_cpu(percpu_clockevent, cpu);evt->cpumask = cpumask_of(cpu);evt->broadcast = smp_timer_broadcast;/*SOC架构相关的Local timer初始化,每种SOC的将自己的Local timer参数填入struct clock_event_device结构,然后调用void clockevents_register_device(…),向内核注册这个tick设备*/if(local_timer_setup(evt))broadcast_timer_setup(evt);}

接下来Core检查该struct clock_event_device是否符合自己的Tick设备标准。static int tick_check_new_device(struct clock_event_device *newdev){…cpu = smp_processor_id();/*检查该struct clock_event_device指示的cpu mask里有没有自己,要是没有自己,说明不符合,直接退出*/if (!cpumask_test_cpu(cpu, newdev->cpumask))goto out_bc;/*取出该Core当前使用的struct tick_device*/td = &per_cpu(tick_cpu_device, cpu);curdev = td->evtdev;/* 检查该struct clock_event_device是否是当前Core的Local设备*/if (!cpumask_equal(newdev->cpumask, cpumask_of(cpu))) {/*该struct clock_event_device不是当前Core的Local设备 *//*尝试将该设备的中断定向分发给当前Core,Tick需要的就是中断,如果不是Local timer,但是如果其中断能定向分发过来,也可以*/if (!irq_can_set_affinity(newdev->irq))goto out_bc;/*当前的struct clock_event_device是Local设备,就没有必要再换成非Local设备了,即使精度再高也没必要,这种Local设备在架构上更和谐 */if (curdev && cpumask_equal(curdev->cpumask, cpumask_of(cpu)))goto out_bc;}/*在新老设备中间选择 */if (curdev) {/*更喜欢CLOCK_EVT_FEAT_ONESHOT的设备,宁可在每次Tick时重新设置,也不要周期性的,ONESHOT的好处在于可以在idle时做tickless。一般的Local timer都是两者属性兼具的:CLOCK_EVT_FEAT_PERIODIC | CLOCK_EVT_FEAT_ONESHOT*/if ((curdev->features & CLOCK_EVT_FEAT_ONESHOT) &&!(newdev->features & CLOCK_EVT_FEAT_ONESHOT))goto out_bc;/*别的因素都考虑了,以优先级做评判标准 */if (curdev->rating >= newdev->rating)goto out_bc;}…/*新的struct clock_event_device已选定,下面将挂载它*/tick_setup_device(td, newdev, cpu, cpumask_of(cpu));…}/*Tick 设备的建立 */static void tick_setup_device(struct tick_device *td,struct clock_event_device *newdev, int cpu,const struct cpumask *cpumask){ktime_t next_event;/*记录当前Core的Tick中断函数*/void (*handler)(struct clock_event_device *) = NULL;if (!td->evtdev) {…/*这时Tick还没有启震起来,还不知道下一次Tick的时间,所以做周期性震荡 */td->mode = TICKDEV_MODE_PERIODIC;} else {/* Tick已经启震起来,知道下一次Tick的时间,struct tick_device的mode保持原来的方式,并记录Tick的中断handler */handler = td->evtdev->event_handler;next_event = td->evtdev->next_event;td->evtdev->event_handler = clockevents_handle_noop;}/*切换struct clock_event_device*/td->evtdev = newdev;/*如果不是Local设备,将其中断分发设置为定向到该Core */if (!cpumask_equal(newdev->cpumask, cpumask))irq_set_affinity(newdev->irq, cpumask);…/*下一步,如果是第一次挂载td->mode == TICKDEV_MODE_PERIODIC,struct clock_event_device的成员变量void (*event_handler)(…);将被设置为void tick_handle_periodic(struct clock_event_device *dev),这是该Core的Tick中断函数。如果是更新挂载,则将该struct clock_event_device的工作方式设置为单发模式:CLOCK_EVT_MODE_ONESHOT,并重新对硬件编程设置下一发的时间点*/if (td->mode == TICKDEV_MODE_PERIODIC)tick_setup_periodic(newdev, 0);elsetick_setup_oneshot(newdev, handler, next_event);}

以上均没考虑tick broadcast,在绝大多数手机里这个功能是disable。3.1.3 Tick产生

作为系统脉搏,Tick发生时,内核还会产生一些动作,如编程下一次Tick中断,更新时间,触发Timer软中断等动作,而最关键的是将Tick送入调度器。/*Tick处理的框架函数,一方面将Tick送给内核,一方面编程时钟寄存器预定下一次Tick的到来时间*/Void tick_handle_periodic(struct clock_event_device *dev){int cpu = smp_processor_id();ktime_t next;/*这里将Tick送进scheduler,Time和Timer 维护的工作也在这里*/tick_periodic(cpu);/*下面要确定下一次Tick的时间,如果该设备不支持CLOCK_EVT_MODE_ONESHOT,那么返回即可,接受周期中断*/if (dev->mode!= CLOCK_EVT_MODE_ONESHOT)return;/*对下一次Tick中断产生的时间进行硬件编程 */next = ktime_add(dev->next_event, tick_period);for (;;) {if (!clockevents_program_event(dev, next, ktime_get()))return;…}}//维护时间,并将Tick继续送给内核其他组件static void tick_periodic(int cpu){/*每个Core都有自己Timer队列,但是Time只能由一个Core来维护*/if (tick_do_timer_cpu == cpu) {…/*Time维护,记下jiffies*/do_timer(1);…}/*Timer及Tick维护*/update_process_times(user_mode(get_irq_regs()));…}//将Tick送给调度器,维护处理器自己的Timer队列、检查posix timervoid update_process_times(int user_tick){/*timer维护*/run_local_timers();/*rcu维护*/rcu_check_callbacks(cpu, user_tick);…/*把tick送进调度器*/scheduler_tick();/*posix timer维护*/run_posix_cpu_timers(p);}3.2 Fair调度类

在2.6内核的后期,引入了调度类。调度类与调度队列密不可分。调度队列是实体,每颗处理器一个,而调度类为策略。主要的调度类有 RT、Fair、Idle三种。而对于每颗处理器,所有这些调度类所管辖的线程队列都被组织在该处理器的struct rq队列中。3.2.1 Fair调度类的负载均衡

负载均衡操作的第一个任务是统计出在一个调度域最繁忙的处理器负载。这分为两步,第一步在调度域内找出最繁忙的struct sched_group,第二步在该struct sched_group中找出最繁忙的处理器。对于CA9架构,尽管每颗处理器都有自己的struct sched_domain结构,但是这些struct sched_domain的跨度范围是包含在线的所有处理器,所以,逻辑上看,这些处理器都处在一个调度域中。再者,对于CA9架构,每个struct sched_group只包含一个处理器,这些struct sched_group又被组织成一个环形链表(参见调度域构建部分)。所以在CA9架构上,最繁忙的处理器寻找可以看作遍历该struct sched_group环形链表,寻找负载最重的处理器。/*该函数找出在一个调度域内负载最高的struct sched_group。参数struct sd_lb_stats *sds用于记载统计结果。struct sched_domain *sd为当前执行负载均衡操作处理器对应struct sched_domain结构*/static inline void update_sd_lb_stats(struct sched_domain *sd, int this_cpu,enum cpu_idle_type idle, const struct cpumask *cpus,int *balance, struct sd_lb_stats *sds){struct sched_domain *child = sd->child;/*当前处理器对应的struct sched_group,该函数的主题是顺着这个struct sched_group遍历该调度域覆盖的所有struct sched_group*/struct sched_group *sg = sd->groups;//用于存放该struct sched_group负载的统计结果struct sg_lb_stats sgs;int load_idx, prefer_sibling = 0;…do {int local_group;/*struct sched_group的成员变量unsigned long cpumask[0];指出了该struct sched_group覆盖的处理器,对于CA9架构,其指出了包含处理器的对应位,在I.mx6Q架构上,其取值为1、2、4、8。这里检测要统计的struct sched_group是否就是当前处理器对应的struct sched_group*/local_group = cpumask_test_cpu(this_cpu, sched_group_cpus(sg));memset(&sgs, 0, sizeof(sgs));//提取该struct sched_group负载信息到sgs中update_sg_lb_stats(sd, sg, this_cpu, idle, load_idx,local_group, cpus, balance, &sgs);/*当前处理器对应struct sched_group无法做loadbalance的目标,无事可做返回*/if (local_group &&!(*balance))return;…if (local_group) {//统计目标处理器的负载sds->this_load = sgs.avg_load;…} else if (update_sd_pick_busiest(sd, sds, sg, &sgs, this_cpu)) {//将新统计处理出来的sgs与原有sgs中最忙碌的比较,选出繁忙处理器sds->max_load = sgs.avg_load;sds->busiest = sg;…}…//链表里的下一个struct sched_groupsg = sg->next;} while (sg!= sd->groups);}/*统计一个struct sched_group内的所有处理器负载,对于CA9架构,每个struct sched_group内置有一颗处理器*/static inline void update_sg_lb_stats(struct sched_domain *sd,struct sched_group *group, int this_cpu,enum cpu_idle_type idle, int load_idx,int local_group, const struct cpumask *cpus,int *balance, struct sg_lb_stats *sgs){…/*若需要统计的struct sched_group包含当前处理器,balance_cpu记录下该struct sched_group里第一颗处理器,对于CA9即为当前处理器*/if (local_group)balance_cpu = group_first_cpu(group);/*针对struct sched_group里的每一颗处理器操作*/for_each_cpu_and(i, sched_group_cpus(group), cpus) {struct rq *rq = cpu_rq(i);if (local_group) {/* 若是当前处理器(目标处理器)的struct sched_group,即把负载往该处理器上拉*/if (idle_cpu(i) &&!first_idle_cpu) {//Tick发生在Idle,cpu醒来试图分担别的处理器的负载first_idle_cpu = 1;balance_cpu = i;}/*取出该处理器的负载,对于Fair调度类,在线程进队出队时都是调整处理器的负载,参见相关章节*/load = target_load(i, load_idx);} else {/*从当前处理器角度看别的处理器,取出其负载*/load = source_load(i, load_idx);…}/*累计struct sched_group内所有处理器负载,对于CA9这里只做一次*/sgs->group_load += load;…}/*在schedule中若某个运行队列可运行为0,面临Idle时,出现以下情况*/if (idle!= CPU_NEWLY_IDLE && local_group) {/*这种情况要做balance的处理器为struct sched_group里第一颗处理器,否则无法做balance。这种处理器似乎不是物理处理器,应该是逻辑处理器,多颗逻辑处理器共享相同的计算单元,SMT似乎符合这种情况。但对于CA9物理处理器就是逻辑处理器,不会出现该情况*/if (balance_cpu!= this_cpu) {*balance = 0;return;}…}…}

事实上,实际情况要复杂得多,可能根本就没有符合条件的最繁忙处理器,或者当前处理器不能满足作为负载转移目标条件。这里还隐藏着另外一个影响负载的因素,即Fair调度类和Rt调度类是分别统计负载的,Fair的负载值并没有直接记录RT调度类的线程情况。但是如果某个调度队列上RT负载较重,则影响到其Fair调度类复制值的消减,这样运行RT线程的处理器上,其Fair调度类的负载值就消逝较慢,使其逐渐成为Fair调度类的busiest。

负载均衡时机为定时、idle、newly_idle、nohz_idle_balance四种情况,前三种情况的特点是目标处理器都是当前处理器。而尽管nohz_idle_balance导致的loadbalance框架与前三者相同,但其目标处理器却不是当前处理器。nohz_idle_balance基本机理是:nohz_idle在idle状态去除了禁止了tick,所以在处理器进入nohz idle之间,就把自己标志在idle_cpus_mask位图中。在别的active处理器发生tick时,将该处理器作为loadbalance目标处理器。

接下来就可以分析负载均衡的框架函数了:在选出最繁忙的处理器之后,接着就可以做负载均衡了——把最繁忙处理器上的线程拉到目标处理器上(本节提到当前处理器都是描述前三种调度时机)。/*负载均衡框架函数,前4个参数都是描述当前处理器的,最后一个参数描述负载均衡完成的结果*/static int load_balance(int this_cpu, struct rq *this_rq,struct sched_domain *sd, enum cpu_idle_type idle,int *balance){int ld_moved, all_pinned = 0, active_balance = 0;…redo:/*找出最繁忙的struct sched_group*/group = find_busiest_group(sd, this_cpu, &imbalance, idle,cpus, balance);//如果没有满足条件的struct sched_group,直接返回if (*balance == 0)goto out_balanced;/*在找到的struct sched_group里面找到繁忙的逻辑处理器,对于CA9这是一对一的关系*/busiest = find_busiest_queue(sd, group, idle, imbalance, cpus);//如果没有满足条件的逻辑处理器,直接返回if (!busiest) {schedstat_inc(sd, lb_nobusyq[idle]);goto out_balanced;}// ld_moved记录下有多少线程发生了处理器间移动ld_moved = 0;if (busiest->nr_running > 1) {all_pinned = 1;local_irq_save(flags);double_rq_lock(this_rq, busiest);/*把最繁忙处理器上的线程移动到当前处理器上,摘取出原有队列再入队的过程*/ld_moved = move_tasks(this_rq, this_cpu, busiest,imbalance, sd, idle, &all_pinned);double_rq_unlock(this_rq, busiest);local_irq_restore(flags);/*若目标处理器不是当前处理器,置位TIF_NEED_RESCHED使得目标处理器有机会运行新线程,这是nohz_idle_balance发生的情况*/if (ld_moved && this_cpu!= smp_processor_id())resched_cpu(this_cpu);/* 最繁忙处理器上的线程都是处理器绑定的,不能被转移到别的处理器上 */if (unlikely(all_pinned)) {cpumask_clear_cpu(cpu_of(busiest), cpus);if (!cpumask_empty(cpus))goto redo;goto out_balanced;}}…}3.2.2 Fair调度类的处理器选择

Fair调度类的目标是尽量让所有线程得到公平的运行时机,无论对于手机还是嵌入式系统,绝大多数线程都是属于Fair调度队列的。/*该函数作为struct sched_class fair_sched_class变量的int (*select_task_rq)(…); 函数,在当唤醒Fair调度类的线程时,为线程选择合适的处理器*/static intselect_task_rq_fair(struct task_struct *p, int sd_flag, int wake_flags){struct sched_domain *tmp, *affine_sd = NULL, *sd = NULL;int cpu = smp_processor_id();//取出待唤醒线程p之前运行的处理器int prev_cpu = task_cpu(p);int new_cpu = cpu;int want_affine = 0;int want_sd = 1;int sync = wake_flags & WF_SYNC;//在CA9的架构下该条件成立if (sd_flag & SD_BALANCE_WAKE) {//检查线程p能否运行在当前处理器上if (cpumask_test_cpu(cpu, &p->cpus_allowed))want_affine = 1;new_cpu = prev_cpu;}rcu_read_lock();for_each_domain(cpu, tmp) {…/*若线程p的先前处理器与当前处理同属于一个调度域,且线程p又能运行在当前处理器上。则affine_sd置位当前处理器对应struct sched_domain。这种情况是CA9处理器常见路径*/if (want_affine && (tmp->flags & SD_WAKE_AFFINE) &&cpumask_test_cpu(prev_cpu, sched_domain_span(tmp))) {affine_sd = tmp;want_affine = 0;}…}if (affine_sd) {…/*若当前cpu处于idle状态则选择当前cpu,否则若线程p先前cpu处于idle状态则选择其先前cpu,否则在该调度域覆盖范围内的处理器选择一个处于idle状态的运行p。若找不到idle状态处理器则直接使用prev_cpu。这是CA9架构最常见路径*/new_cpu = select_idle_sibling(p, prev_cpu);goto unlock;}/*否则在调度域覆盖处理器里选择一个最空闲的处理器来运行p*。该路径不常见,发生情况为当前处理器不在线程p的可运行处理器列表中*/while (sd) {//寻找最空闲struct sched_groupgroup = find_idlest_group(sd, p, cpu, load_idx);…//在最空闲struct sched_group中寻找最空闲处理器new_cpu = find_idlest_cpu(group, p, cpu);…}}…return new_cpu;}3.3 RT调度类3.3.1 RT调度类的基本结构

调度策略属于 SCHED_FIFO、SCHED_RR的task_struct,属于RT调度类,RT调度类的队列组织结构以优先级为核心。(1)处在运行状态的每个相同优先级的task_struct被串在同一队列。(2)内核构造一个以长为最大优先级的链表头数组,上述task_struct队列以优先级为索引挂在对应的链表头上。(3)通过一个长为最大优先级的位图表示对应优先级是否有处在运行状态的task_struct。

定义如下所示:struct rt_prio_array {//表示对应优先级是否有可运行task_structDECLARE_BITMAP(bitmap, MAX_RT_PRIO+1); /* include 1 bit for delimiter *///挂载task_struct队列的链表头struct list_head queue[MAX_RT_PRIO];};

调度类提供了入队函数的主要策略就是依据其优先级索引对应链表头:static void __enqueue_rt_entity(struct sched_rt_entity *rt_se, bool head){…//根据优先级索引链表数组struct list_head *queue = array->queue + rt_se_prio(rt_se);…//将其挂入该链表头if (head)list_add(&rt_se->run_list, queue);elselist_add_tail(&rt_se->run_list, queue);/*以优先级为索引置位运行时位图,表示当前优先级链表上有处于运行状态的task_struct*/__set_bit(rt_se_prio(rt_se), array->bitmap);//当前运行队列可运行task_struct数目增一inc_rt_tasks(rt_se, rt_rq);}/*暴露给调度器的入队函数,rq为将要加入的调度队列,p即为需要加入的task_struct*/static void enqueue_task_rt(struct rq *rq, struct task_struct *p, int flags){…//挂入调度队列enqueue_rt_entity(rt_se, flags & ENQUEUE_HEAD);/*若该task_struct不是调度队列rq的当前线程,且该task_struct可以在多个处理器上运行,则将其该队列的pushable_tasks链表,在该CPU负载严重时,该pushable_tasks链表上的task_struct将会被推送到别的处理器上*/if (!task_current(rq, p) && p->rt.nr_cpus_allowed > 1)enqueue_pushable_task(rq, p);}

RT调度类的Tick函数充分解释了SCHED_FIFO、SCHED_RR调度策略的不同。static void task_tick_rt(struct rq *rq, struct task_struct *p, int queued){…/*对于SCHED_FIFO策略的task_struct,Tick无效,这种类型的如果不是自己主动放弃CPU,不被更高优先级的SCHED_FIFO 策略task抢占,就不眠不休一直跑下去*/if (p->policy!= SCHED_RR)return;/*对于SCHED_RR调度策略的task仍有时间片的概念,Tick到来其time_slice仍然被递减*/if (——p->rt.time_slice)return;//递减到0,恢复时间片的默认值p->rt.time_slice = DEF_TIMESLICE;/*把当前SCHED_RR调度策略的task从调度队列里摘掉,并将其挂到队尾。这个操作体现了轮转。在RT里只有一个task时,即使SCHED_RR调度策略的task也能享受到FIFO的待遇*/if (p->rt.run_list.prev!= p->rt.run_list.next) {requeue_task_rt(rq, p, 0);set_tsk_need_resched(p);}}/*调度器使用的下一运行task选择函数,该函数蕴含的一个重要机制是,如果当前调度类选择不出来合适的task,则调度器会在下一个调度类里寻找,直到idle task*/static struct task_struct *_pick_next_task_rt(struct rq *rq){struct sched_rt_entity *rt_se;struct task_struct *p;struct rt_rq *rt_rq;rt_rq = &rq->rt;//如果RT调度类没有处在运行态的task,则说明没有合适的taskif (unlikely(!rt_rq->rt_nr_running))return NULL;//如果RT调度类到了带宽限制阀值,则说明没有合适的taskif (rt_rq_throttled(rt_rq))return NULL;//选取下一最高优先级的task,参考进队操作do {rt_se = pick_next_rt_entity(rq, rt_rq);…} while (rt_rq);//索引到新taskp = rt_task_of(rt_se);//在新task被选取时与RT调度队列的clock_task时钟同步p->se.exec_start = rq->clock_task;return p;}

常规应用通常是不会跑到RT调度队列中的,只有在某些特定实时应用中才能大量使用。而对于Android系统,早期的版本RT队列基本没有用到,到了后期版本进行了优化,将surfacefilnger之类的线程进行实时性改造放入了RT队列。3.3.2 Rt_Bandwidth

为了限制系统中RT调度类过度占用CPU,内核提供了rt_bandwidth机制,即设置一个百分比,当RT运行队列的task占据CPU的时间超过了这个阈值,则调度器将RT调度类视而不见,将剩下的时间分配给其他调度类的task。

尽管该机制通过全局变量int sysctl_sched_rt_runtime来控制是否开启。但是在大多数ARM处理器厂商提供的BSP里都是默认开启的,所以对于某些依靠最高优先级的FIFO实时线程来说要注意,这种情况下有些时间是不受控制的。若默认最高优先级且处于运行态的FIFO时,线程能totally控制系统将存在潜在bug。/*RT调度类的shchedule_tick里会调用该函数*/static void update_curr_rt(struct rq *rq){struct task_struct *curr = rq->curr;struct sched_rt_entity *rt_se = &curr->rt;struct rt_rq *rt_rq = rt_rq_of_se(rt_se);u64 delta_exec;if (curr->sched_class!= &rt_sched_class)return;/*在当前task被调度时及tick发生时,其se.exec_start与RQ的clock_task 同步,参见上文。而在每次tick时,RQ的clock_task 与系统时间同步。void scheduler_tick(void)-> static void update_rq_clock(struct rq *rq)-> static void update_rq_clock_task(struct rq *rq, s64 delta)。所以这里求出来的差值之和是RT调度类运行的时间长度*/delta_exec = rq->clock_task - curr->se.exec_start;…//当前task的exec_start时间与运行队列时间同步curr->se.exec_start = rq->clock_task;…//如果禁用了rt_bandwidth直接返回即可if (!rt_bandwidth_enabled())return;for_each_sched_rt_entity(rt_se) {…if (sched_rt_runtime(rt_rq)!= RUNTIME_INF) {//防抢占防中断raw_spin_lock(&rt_rq->rt_runtime_lock);//rt_time记录了RT调度类运行的时间长度rt_rq->rt_time += delta_exec;/*查看当前RT运行时间长度是否超过了rt_bandwidth机制的限制,如果超过了限制,则进入调度器*/if (sched_rt_runtime_exceeded(rt_rq))/*如果为真,当前task置为TIF_NEED_RESCHED运行状态,即使运行态的FIFO task也得交出处理器*/resched_task(curr);raw_spin_unlock(&rt_rq->rt_runtime_lock);}}}//检查否超过了rt_bandwidth机制的限制static int sched_rt_runtime_exceeded(struct rt_rq *rt_rq){//runtime取至RQ的rt_runtime成员变量,这是限制的时间点u64 runtime = sched_rt_runtime(rt_rq);…//如果实际运行时间超过了限制的运行时间,则rq的成员变量rt_throttled为真if (rt_rq->rt_time > runtime) {rt_rq->rt_throttled = 1;…}return 0;}

这之后的机制是,当前CPU进入调度器,调度器再次重选线程,当前RT调度队列被throttled,所以RT task选择返回NULL,调度器将继续尝试Fair直到Idle。这其中蕴藏着一个问题是,如果Fair里没有task,该处理器将会进入到Idle中,尽管这时RT里还有task没有运行完毕,是在所有队列中都没有task才Idle,还是不管如何都从RT里剥夺一定的运行时间,是一个值得讨论的问题。

为了控制RT调度队列的throttled与active,内核使用一个hrtimer——sched_rt_period_timer来处理这个机制。//Hrtimer时钟的处理函数static enum hrtimer_restart sched_rt_period_timer(struct hrtimer *timer){…for (;;) {//取出当前时间now = hrtimer_cb_get_time(timer);/*forward该hrtimer,rt_b->rt_period的一个周期默认为一秒,rt_b->rt_runtime为0.95秒。这些值在void init_rt_bandwidth(……)里被初始化*/overrun = hrtimer_forward(timer, now, rt_b->rt_period);if (!overrun)break;/*实际工作函数,在一个周期的边沿,检查是否重启RT调度队列,或者当前RT调度队列已无可运行task,则返回idle*/idle = do_sched_rt_period_timer(rt_b, overrun);}//是否idle来决定该Hrtime是否重新firereturn idle ? HRTIMER_NORESTART : HRTIMER_RESTART;}static int do_sched_rt_period_timer(struct rt_bandwidth *rt_b, int overrun){//idle默认为1int i, idle = 1;…//取出在线处理器位图span = sched_rt_period_mask();//只针对在线处理器操作for_each_cpu(i, span) {int enqueue = 0;struct rt_rq *rt_rq = sched_rt_period_rt_rq(rt_b, i);struct rq *rq = rq_of_rt_rq(rt_rq);raw_spin_lock(&rq->lock);//检查该运行队列是否有实际运行时间记录if (rt_rq->rt_time) {u64 runtime;raw_spin_lock(&rt_rq->rt_runtime_lock);//有实际运行时间记录且被rt_throttledif (rt_rq->rt_throttled)balance_runtime(rt_rq);//限制的时间长度runtime = rt_rq->rt_runtime;//通常overrun为0,这里rt_rq->rt_time被清零rt_rq->rt_time -= min(rt_rq->rt_time, overrun*runtime);//rt_rq->rt_time被清零,且rt_rq->rt_throttled为真if (rt_rq->rt_throttled && rt_rq->rt_time < runtime) {// rt_throttled状态被清零rt_rq->rt_throttled = 0;enqueue = 1;/*发生在RT队列throttled时,Fair里却没有处在运行状态的task*/if (rt_rq->rt_nr_running && rq->curr == rq->idle)

试读结束[说明:试读内容隐藏了图片]

下载完整电子书


相关推荐

最新文章


© 2020 txtepub下载