读书笔记之基于龙芯的linux内核探索解析

1.1龙芯处理器简介
中央处理器(cpu)分为复杂指令集计算机(cisc)和精简指令集计算机(risc),cisc具有指令集复杂而庞大,指令字不等长,寻址方式复杂,计算指令操作数可以是内存等特征,典型代表有x86.risc具有指令集精简而高效,指令字等长,寻址方式简明,计算指令操作必须是寄存器等特征,典型代表有arm,mips,power.两者各有其优劣.
龙芯cpu属于无互锁流水阶段微型计算机(mips)家族,是risc精简指令集体系结构的一种.产品以32位和64位单核及多核为主,主要面向网络安全,高端嵌入式,个人电脑,服务器和高性能计算机等应用.
龙芯3a2000,3a3000与mips64 r2兼容,注意这些mips版本(mips1,2,3,5,r1,r2等)指的是体系结构(指令集isa),并不是cpu设计.龙芯处理器在指令集以外的部分的许多设计跟r4000比较相似(比如时钟源,cache,mmu,fpu的设计等),因此在本书的解析中许多代码都是使用和r4000(代码缩写r4k)相同的版本.
在交叉开关上面,所谓主设备,就是主动发起访问请求的主控方;所谓从设备,就是被动接受访问请求并给出响应的受控方.龙芯三号的两级交叉开关都采用读写分离的数据通道,数据通道宽度为128位,工作在与处理器核相同的频率,用以提供高速的片上数据传输.
逻辑上,一个龙芯三号处理器包括主处理器,协处理器0(系统控制协处理器,负责一些跟特权级以及内存管理单元有关的功能,至少32个),协处理器1(浮点运算FPU),协处理器2(多媒体指令),一级cache,sfb(位于寄存器和一级cache之间,只有数据访问才会经过sfb)和tlb等多个组成部分.
龙芯处理器核有32个通用寄存器,在64位模式下,gpr字长均为64位;在32位兼容模式下,gpr只有低32位可用,所谓通用寄存器,就是可以用作任意用途.mips处理器有三种常用的abi,o32(只能用32位操作数指令和32位GPR),n32(可以用64位操作数和64位gpr),64(o代表old,n代表new),除了以上数据格式区别,三种ABI 32个通用寄存器的使用约定不一样.
指令集
访存指令分为加载指令和存储指令,将内存内容读到寄存器,将寄存器写入内存.遵循"操作类型-操作位宽-后缀",后缀u表示加载的数是无符号整数,会对高位进行0扩展而不是符号扩展,例如lhu加载一个16位无符号整数到寄存器,对高位进行0拓展,但是lui,li/dli,la/dla不符合上述规律.lui表示加载高半字立即数,即加载一个无符号16位立即数并左移16位,li是宏指令,加载一个任意32位立即数到寄存器,la宏指令,加载一个符号(变量名或函数名)的32位地址到寄存器.
计算指令,遵循"操作位宽-计算类型-后缀",操作位宽有d的表示64位操作数,无d的表示32位.无后缀的表示两个源操作数都来自寄存器,后缀I,表示一个源操作数来自寄存器,而另一个源操作数是立即数;后缀U的表示无符号操作数(实际含义是溢出时不产生异常);后缀IU则是上面两个的组合;两个标准字长的操作数做乘法往往会产生双倍字长的结果,比如32位操作数乘以32位操作数的结果可能是64位,因此,mips在32个通用寄存器之外专门设置了HI和LO寄存器,分别保存乘法结果的高位字和低位字.MFHI/MTHI/MFLO/MTLO用于在通用寄存器和Hi/Lo两个辅助寄存器之间传递数据,MF 指的是move from,MT指的是move to.逻辑指令,AND OR XOR NOR ANDI ORI XORI,逻辑与,逻辑或,逻辑异或,逻辑或非,无i的表示两个源操作数都来自寄存器,有i的表示一个源操作数来自寄存器,另一个源操作数是立即数.带D前缀的指令是64位操作数,无V后后缀的是固定位移(位移的位数由立即数给出),有后缀v的是可变移位.跳转指令,JR,J,JALR,B,BAL,BEQ,BEQAL…前缀J的指令为绝对跳转(无条件),跳转目标地址是相对pc所在地址段的256MB边界偏移;前缀B的是相对跳转(有条件,也叫分支指令),跳转目标地址是相对pc的偏移,j类跳转指令里面,无后缀R表示目标地址为立即数,有后缀r的表示跳转目标为寄存器的值.特殊指令:SYNC内存屏障,SYSCALL系统调用,ERET异常返回,BREAK断点,EI开中断,DI关中断,NOP空操作,WAIT暂停等待.
龙芯电脑基本结构
龙芯电脑还可以使用南北桥合一(合在一起称为桥片)的LS2H LS7A;南桥芯片内部包含各种低速控制器,这些低速控制器本身是pcie设备;linux内核中,直接通过内部总线连接的设备叫平台设备,也叫平台总线.pcie总线规范,pcie设备运行时是可探测的.然而平台设备运行时却不可探测,传统上这类设备只能在内核里静态声明,因而影响可移植性.为了解决这个问题,现在比较常用的方法是设备树(所有平台设备的描述).集成在bios里,以传参的方式传递给linux内核.cpu和dma之间的一致性,指的是空间一致性(coherency)(多个cache副本之间的一致性,由cpu硬件负责),而不是时序一致性(consistancy)(多个处理器之间访存操作的顺序问题,通常需要软件和硬件协同解决).
linux内核简介
包含linux内核,基础运行环境(运行时库如glibc),编译环境(如gcc),外壳程序(shell,命令行解释器)和图形操作界面(gui)的完整操作系统套件被称为GNU/LINUX.宏内核的设计风格是"凡是可以在内核里实现的都在内核实现",宏内核的优点是内核内部的各种互操作都可以通过系统调用实现,因此性能较好,缺点是体积较大且理论上健壮性不好(因为内部耦合性太高);微内核,常见的实现是gnu hurb,其设计风格是"凡是可以不在内核里实现的都不在内核实现",因此很多功能子系统被设计成了一种服务.
理清主脉络
代码vs注释,程序流程vs变量声明,功能语句vs调试语句,正常流程vs异常流程,常见路径vs罕见路径.
1.4 如何开发健壮性内核
内核最重要的是稳定性,甚至比功能和性能更重要,而稳定性的根本来源就是内核代码的健壮性.1.采用规范的代码风格,2.合理地生成补丁系列,3,谨慎地对待自主创新.
1.4.1内核代码风格
规范的代码风格具有良好的可读性和可维护性.
命名,缩进(switch case不需要缩进),行长,括号与空格,注释格式(单行/* xxxx / 多行 / *xxx 回车 * yyyy 回车 */)
1.4.2合理生成补丁
原则:1.git记录必须全程有机可回溯  2.补丁质量与代码质量同样重要  3.日志信息与代码质量同样重要  4.每次提交必须是一个完整的功能单元.
按逻辑单位(功能)分解.
标准:按照顺序以增量方式逐个应用补丁系列中的补丁时,每应用一个补丁,都必须保证内核能够顺利构建,顺利运行;如果补丁系列涉及增加kconfig中的配置项,通常需要在配置项所涉及的代码功能已经全部加入以后再增加配置项本身.
1.4.3谨慎对待创新
保持自由开放,人人为我,我为人人;集众人之智,采众家之长,消化吸收再创新;站在巨人的肩膀上,站得更高才能看得更远.
你面临一个全新问题的概率非常小,需要你深入理解并尽量复用现有的内核设计框架,然后参照相似的功能模块去增加新功能或者拓充已有的功能.
加强知识积累,并在融会贯通的基础上自主创新;知识累积的过程中,建议参与开源社区互动;
第二章 内核启动解析
内核的三大基本功能:异常与中断(异常/中断)处理,内存管理,进程管理.还有文件系统,设备驱动,网络协议;
pmon是传统bios,昆仑固件是基于uefi规范实现的bios
2.1 内核源代码目录结构
linux内核中,比"核"更通用的说法是逻辑cpu,一个逻辑cpu就是一个执行任务的最小单元.在单核处理器中,逻辑cpu就是物理cpu;在多核单线程处理器中,逻辑cpu就是核;在单核多线程或者多核多线程处理器中,逻辑cpu就是线程;
2.2内核启动过程:主核视角
主核的执行入口(pc寄存器的初始值)是编译内核时决定的,运行时由bios或者bootloader传递给内核,内核入口kernel_entry(严格来说,只是非压缩版原始内核的执行入口点,非压缩版本vmlinux,将vmlinux压缩以后再加上一个新的elf头就得到一个压缩版本vmlinuz),如果启动的是压缩版本内核,在解压前真正的执行入口是arch/mips/boot/compressed/head.S中的start标号,压缩版内核在start标号处开始执行的时候会通过decompress_kernel进行自解压,解压内容释放到内存里形成一个原始内核,解压完毕后,执行流程跳转到原始内核的kernel_entry入口继续执行;
剩余部分参照linux内核启动专题.
3.0 异常与中断解析
异常/中断指的是发生了正常的执行流程被打断的事件.来自于cpu内部的叫异常,来自于外部设备的,叫中断;在mips处理器的分类中,中断被归结为异常的一种,一般来说,异常是同步事件,通常会立即处理;中断是异步事件,通常会在一定程度上延迟处理.
出错只是许多异常的一部分,另外一些种类的异常并不意味着出错,而是为了请求系统服务或者调试需要或者特意设计,比如应用程序执行特权级,系统调用本身就是一种异常;中断是为了cpu和外设能够并行工作而引入的一种机制;异常还有一个重要作用,就是让cpu不仅能够顺序执行,还可以交错执行,有了交错执行,cpu才能具有并发特征(并发并不意味着并行,因为并发包括交错与并行,交错在宏观上并行但微观上串行,而并行是宏观和微观都并行),而有了并发,单核cpu上才有多任务操作系统的可能.
Linux内核里,上下文用于描述当前的软硬件状态和环境.用户进程,内核线程和异常处理时cpu处于进程上下文;
硬中断上下文:不可被硬中断,不可被软中断,不可睡眠(自愿调度),不可抢占(强制调度);
软中断上下文:可以被硬中断,不可被软中断,不可睡眠(自愿调度),不可抢占(强制调度);
进程上下文:可以被硬中断,可以被软中断,可以睡眠(自愿调度),是否可以抢占(强制调度)取决于有没有关抢占;
不可屏蔽中断上下文的性质根硬中断类似,可以被认为特殊的硬中断上下文.中断上下文和不可抢占进程上下文统称为原子上下文.内核提供了一系列宏来判定当前上下文.in_nmi是否处于不可屏蔽中断上下文;in_irq是否处于硬中断上下文;in_softirq是否处于软中断上下文;in_interrupt是否处于中断上下文(不可屏蔽中断/硬中断/软中断上下文);in_atomic是否处于原子上下文(中断/不可抢占进程上下文);in_task是否处于进程上下文;
龙芯不支持硬件层面的中断优先级;
do_irq处理过程,计算机处于可屏蔽中断上下文(包括硬中断上下文,软中断上下文),边界是irq_enter和irq_exit,前半部分是硬中断上下文,其边界是local_irq_disable和local_irq_enable,后半部分(即do_softirq)为软中断上下文,边界为local_bh_disable和local_bh_enable;
3.1 寄存器操作
异常总是意味着上下文切换;
保存/恢复寄存器上下文包括大部分通用寄存器和少数协处理器0寄存器的内容,linux内核提供了一系列宏来完成这些操作:
SAVE_AT 将at寄存器保存到内核栈;
SAVE_TEMP 将t系列寄存器保存到内核栈;
SAVE_STATIC 将s系列寄存器保存到内核栈;
SAVE_SOME 将zero,gp,sp,ra,a系列,v系列以及cp0的status,cause,epc寄存器保存到内核栈;
SAVE_ALL,相当于上面的综合;
….
save_*和restore_并不是完全对称的,不对称主要体现于sp寄存器(栈指针),原因如下:
1.进程在用户态和内核态使用不一样的栈;
2.从用户态切换到内核态时,需要将除k0/k1以外的所有寄存器保存到内核栈;
3.保存寄存器之前,sp指向的是用户栈,因此首要的事情是切换sp;
4.切换sp主要是由get_saved_sp完成的,与set_saved_sp相对应,用于获取当前进程在当前cpu上的内核栈指针,即kernelsp[]数组的对应元素;
5.切换并保存sp的大致过程:将旧sp值(用户栈指针)放到k0;通过get_saved_sp将内核栈指针装入k1;将k1的值减去PT_SIZE(一个完整寄存器上下文的大小)后装入sp;此时新sp已经指向内核栈,于是将k0(用户栈指针)保存到寄存器上下文中sp的位置;
6.内核态切换回用户态时,只需要简单恢复寄存器上下文中的sp就可以了.
如果是在内核态发生异常,则旧sp已经指向内核栈,不需要切换.
异常处理时发生的上下文切换只涉及一个进程,是同一个进程在"正常执行流"与"异常执行流"之间的切换(正常执行流处于用户态或内核态,异常执行流一定处于内核态),进程切换是从一个进程的执行流切换到另一个进程的执行流,属于涉及多个进程的水平切换.
3.2 异常处理解析
硬复位,软复位和nmi;
3.2.1 复位异常和NMI
复位异常包括硬复位和软复位.硬复位异常在冷启动时发生,也就是说硬复位就是第一次开机上电.软复位异常在热启动时发生,也就是在开机状态下执行重复操作将触发软复位.nmi表示发生了非常严重的错误,通过cpu上专门的nmi引脚进行触发,因此不可屏蔽.
上面三种异常具有相同的入口地址0xffff ffff bfc0 0000,因此均由bios第一时间处理.bios通过协处理器0中的status寄存器区分这三种异常,异常发生时,硬件会自动设置status寄存器的bev,sr,nmi位;相应的位值为硬复位1 0 0,软复位1 1 0,nmi 1 0 1;(sr 1 表示有软复位例外发生; nmi位是否发生 NMI 例外。注意,软件不能把这位由 0 写为 1);
龙芯对于硬复位和软复位采用了相同的处理方法,就是bios正常启动流程,nmi则直接跳转到内核进行处理.内核的nmi处理入口except_vec_nmi(定义在./arch/mips/kernel/genex.S),函数基本上就是直接跳转到nmi_handler,设置bev,erl清除,同时设置exl位,通过SAVE_ALL保存寄存器上下文,把sp寄存器写入a0;
2k上nmi没有设置,3a上设置了,mips_nmi_setup(文件./arch/mips/loongson/common/init.c)->memcpy(base, &except_vec_nmi, 0x80);->except_vec_nmi->nmi_handler->nmi_exception_handler,nmi通常意味着发生了严重错误,因此一般情况下通知块函数就是执行关机或者重启操作;
3.22 缓存错误异常
mips的四个异常:向量0是tlb重填异常,向量1是xtlb重填异常,向量2是高速缓存错误异常,向量3是其他通用异常.
高速缓存(即cache)错误异常的入口是except_vec2_generic,定义在arch/mips/mm/cex-gen.S中;
LEAF(except_vec2_generic)
.set noreorder
.set noat
.set mips0
/

* This is a very bad place to be. Our cache error
* detection has triggered. If we have write-back data
* in the cache, we may not be able to recover. As a
* first-order desperate measure, turn off KSEG0 cacheing.
/
mfc0 k0,CP0_CONFIG //$16
li k1,~CONF_CM_CMASK //7
and k0,k0,k1
ori k0,k0,CONF_CM_UNCACHED //2,手册中提及Kseg0 一致性算法,2 – Uncached,3 – Cacheable,配置kseg0的cache一致性;
mtc0 k0,CP0_CONFIG
/
Give it a few cycles to sink in… */
nop
nop
nop

j   cache_parity_error
nop
END(except_vec2_generic)
cache错误一旦发生,意味着cache已经不可用,接下来的所有的取指令和内存访问都必须以非缓存的方式进行;内核本身放置在kseg0段,起始此段是否缓存是可以配置的,配置方式就是config0寄存器的最低三位,决定kseg0的cache属性,2表示非缓存,3表示缓存,7表示写合并(wc)或者非缓存加速(uca);
except_vec2_generic的第一步就是设置kseg0非缓存,之后跳转到cache_parity_error(通过errorepc,cacheerr等寄存器获取必要的信息,打印出来,之后调用panic,进入死机状态);虽然设置了kseg0非缓冲访问,但并不意味着内核能够在全局无cache的情况下继续工作,因为除了内核代码和全局数据外,各种动态生成的代码和数据一般不通过kseg0访问,因此对于cache错误异常,实际上软件无能为力.(龙芯三号处理器核对cache错误实现了硬件自纠错功能,所以一般不会出现cache错误).

3.2.3 tlb/xtlb异常
cpu的内存管理单元叫mmu,而mmu包括tlb和一系列cp0寄存器.tlb负责从虚拟地址到物理地址的转换,是内存中页表的一个子集.虚拟页号,物理页号pfn,页内偏移两个是相等的;
多级页表,页目录pgd,页表pte,pud页上位目录;
tlb是一个表;tlb表项是成对组织的,地址相邻的两个页面被放置在一个表项里面,其虚拟页号的高位叫2,asid约等于进程id,asid+2基本可以保证唯一性;
2 asid pagemask g pfn-0 flags-0(vdc) pfn-1 flags-1(vdc)
pagemask表示页面的大小,龙芯可以设置的最小页面是4kB,最大页面是16MB(从龙芯3a2000开始最大页面支持1GB),flags-0/1表示页面的属性标志,其中,c是cache属性,d表示可写位,v是有效位;
龙芯的每个cpu都有64项全相连的vtlb,全相连代表任意一个虚拟地址可以通过tlb中的任意一项来映射;v(variable)代表页面大小可变,3a2000开始每个核在vtlb的基础上另外引入了1024项8路组相连的ftlb(龙芯3a4000的ftlb进一步增加到2048项),8路组相连意味着对于1个特定的虚拟地址,只有8个候选的tlb可以用来映射,f(fixed)代表页面大小固定;
根tlb相关的cp0寄存器主要有pagemask(标识页面大小),entryhi(存放着虚拟页号2和asid),这两个寄存器用作tlb查询的输入,entrylo0和entrylo1包括两个物理页号pfn-0和pfn-1,以及两个物理页的v,d,c属性,用作tlb查询的输出;Index和random主要用于写tlb项,前者用于索引写(tlbwi指令),后者用于随机写(tlbwr);
当tlb里面没有合适的条目可以翻译当前虚拟地址的时候,就会发生tlb异常,进而访问内存中的页表.tlb异常总共有四种:tlb/xtlb重填异常(意味着tlb中没有对应项),tlb加载无效异常(意味着读请求,tlb中有对应项,但对应项无效),tlb存储无效异常(意味着写请求,tlb中有对应项,但对应项无效),tlb修改异常(意味着写请求,tlb中有对应项,对应项也有效,但只读).tlb/xtlb重填异常有专门的入口向量(0和1);
tlb重填异常用于32位模式,xtlb重填异常用于64位模式,龙芯三号使用64位内核,因此使用xtlb重填,在不引起歧义的情况下,使用tlb/xtlb重填异常统一称为tlb重填(tlb refill);
(二)内核微汇编器原理
tlb异常的处理函数不是静态编写的,而是在启动的过程中用内核自带的微汇编器生成的动态代码.这样可以保证在使用一个内核的情况下,在不同平台上根据cpu特征来生成最优化的代码,龙芯的tlb采用了mips r4000兼容的设计,build_r4000_tlb_load_handler,build_r4000_tlb_store_handler,build_r4000_tlb_modify_handler,build_r4000_tlb_refill_handler,龙芯3a2000/3000/4000提供了加速页表访问的lddir/ldpte等指令,因此tlb重填异常常用专门的build_loongson3_tlb_refill_handler来生成;
微汇编器原理(uasm);
mips指令为定长32位,其格式主要分为三种类型:立即数型(i型),跳转型(j型),寄存器型®;
微汇编器用的主要数据结构如下:
struct insn {
enum opcode opcode; //指令操作码索引(微汇编器内部使用,不是真实的操作码);
u32 match; //指令里面明确的编码部分,一般情况下是真实的指令操作码和function域;
enum fields fields; //描述该指令除指令码以外有哪些域,
};
/* Handle labels. */
struct uasm_label {
u32 addr;
int lab;
};
/
Handle relocations. */
struct uasm_reloc {
u32 addr;
unsigned int type;
int lab;
};
微汇编器用到的主要函数和宏:
1.uasm_i_lnsnName(buf,…),生成指令的函数,lnsnNanme是指令的名字,如加法指令addu,buf是存放指令的缓冲区;所有的指令生成函数最终都会通过调用build_insn来生成指令,而build_insn则可能通过build_rs生成rs域,通过build_rt生成rt域,rt生成rt域,re生成ra域,func生成function域;
2.uasm_l_LablelName标号生成函数;
3.uasm_il_lnsnName(buf,relocs,…,label_id),标号跳转指令生成函数;
4.uasm_copy_handler代码动态拷贝函数;
current_cpu_data.vmbits是虚拟地址空间的长度,对于龙芯3号就是48(龙芯使用48位虚拟地址空间),
虚拟地址的分解:
bits_per_long(64位)
current_cpu_data.vmbits(48bit)
保留17位 pgd(11bit) pmd(11bit) pte(11bit) offset(14bit)
current_cpu_data.vmbits- |
pgdir_shift- |
pmd_shift- |
page_shift- |
内核处理虚拟地址超过47位的地址
XUSEG,XSSEG,XKPHYS,XKSEG(包括CKSEG)的地址前缀分别是00,01,10,11,通过最高位的值便可区分两种情况,一种是PGD范围之外的用户态虚地址(非法XUSEG地址)或者管理态地址(XSSEG地址),一种是vmalloc/vmap得到的内核态虚地址(CKSEG2/XKSEG);
龙芯只支持4kB和4kB的4倍递增值,即4KB,16KB,64KB等,龙芯3a2000之前的处理器最大支持到16MB,3a2000及更新的处理器最大支持1GB;
build_tlb_refill_handler→check_for_high_segbits
除了tlb重填异常外,所有的tlb异常处理都有一个共同特点:包括一个快速路径和慢速路径,在快速路径下,与给定虚拟地址对应的tlb项要么无效,要么有效无权,但对应的页表项是存在并且有效有权的,因此异常处理函数访问相应的页表项并将其填充到tlb中;在慢速路径下,页表里也不存在有效对应项(或有效无权),因此需要通过tlb_do_page_fault_0(读操作触发的异常)或者tlb_do_page_fault_1(写操作触发的异常)标号处的代码调用do_page_fault进行缺页异常处理,建立有效表项后再进行tlb重填;
3.2.4 其他通用异常
通用异常统一的入口except_vec3_generic,定义在arch/mips/kernel/genex.S中,
/

  • General exception vector for all other CPUs.

  • Be careful when changing this, it has to be at most 128 bytes

  • to fit into space reserved for the exception handler.
    */
    NESTED(except_vec3_generic, 0, sp)
    .set push
    .set noat
    //For LSVZ, Use at least 8 nop for workaround
    #ifdef CONFIG_KVM_GUEST_LS3A3000
    nop
    nop
    nop
    nop
    nop
    nop
    nop
    nop
    #endif
    #if R5432_CP0_INTERRUPT_WAR
    mfc0 k0, CP0_INDEX
    #endif
    mfc0 k1, CP0_CAUSE
    andi k1, k1, 0x7c  //cause寄存器的第2~6位表示了异常的种类,
    #ifdef CONFIG_64BIT
    dsll k1, k1, 1 //对于32位内核 exception_handlers数组每一项为4个字节,对于64位内核8字节,k1寄存器中的异常编码要先逻辑左移一位,才能用于查找异常处理函数;
    #endif
    PTR_L k0, exception_handlers(k1)
    jr k0
    .set pop
    END(except_vec3_generic)

    exception_handlers数组是第二层次的异常向量,32个表项
    set_except_vector(0, using_rollback_handler() ? rollback_handle_int: handle_int);中断,3a2000开始使用 rollback_handle_int;
    set_except_vector(1, handle_tlbm);tlb修改异常;
    set_except_vector(2, handle_tlbl); tlb无效异常(读)
    set_except_vector(3, handle_tlbs); tlb无效异常(写)
    set_except_vector(4, handle_adel); 地址错误异常(读)
    set_except_vector(5, handle_ades); 地址错误异常 (写)
    set_except_vector(6, handle_ibe); 总线错误异常(指令)
    set_except_vector(7, handle_dbe); 总线错误异常 (数据)
    set_except_vector(8, handle_sys); 系统调用异常
    set_except_vector(9, handle_bp); 断点异常
    set_except_vector(10, handle_ri); 保存指令异常(龙芯三号使用handle_ri_rdhwr_tlbp);
    set_except_vector(11, handle_cpu); 协处理器不可用异常
    set_except_vector(12, handle_ov); 算术溢出异常
    set_except_vector(13, handle_tr); 陷阱异常
    set_except_vector(14, handle_msa_fpe); msa向量浮点异常

    set_except_vector(16, handle_ftlb); ftlb异常

    set_except_vector(19, tlb_do_page_fault_0); tlb抗读异常
    set_except_vector(20, tlb_do_page_fault_0);
    set_except_vector(21, handle_msa); msa异常
    22-25龙芯不支持
    set_except_vector(22, handle_mdmx); mdmx向量模块异常

    set_except_vector(24, handle_mcheck); 机器检查异常
    set_except_vector(26, handle_dsp); dsp模块异常

异常代码生成的宏定义如下:
arch/mips/kernel/genex.S
.macro BUILD_HANDLER exception handler clear verbose ext
.align 5
NESTED(handle
\exception, PT_SIZE, sp)
.set noat
SAVE_ALL //保存寄存器上下文,然后sti开中断,并进入内核态,将sp的值当作第一个参数调用do
\handler异常处理函数,然后调用ret_from_exception完成异常处理的返回;sp当参数的作用是它恰好指向刚才保存的寄存器上下文(struct pt_regs),可以给核心异常处理函数提供必要的信息;
FEXPORT(handle_\exception\ext)
BUILD_clear\clear
.set at
BUILD\verbose \exception
move a0, sp
PTR_LA ra, ret_from_exception
j do
\handler
END(handle
\exception)
.endm
如:BUILD_HANDLER cpu cpu sti silent
协处理器不可用异常核心处理函数
arch/mips/kernel/traps.c
asmlinkage void do_cpu(struct pt_regs *regs)
{
enum ctx_state prev_state;
unsigned int __user *epc;
unsigned long old_epc, old31;
void __user *fault_addr;
unsigned int opcode;
unsigned long fcr31;
unsigned int cpid;
int status, err;
unsigned long __maybe_unused flags;
int sig;

prev_state = exception_enter();
die_if_kernel("do_cpu invoked from kernel context!", regs);

cpid = (regs->cp0_cause >> CAUSEB_CE) & 3;

switch (cpid) {
case 0:
    epc = (unsigned int __user *)exception_epc(regs);
    old_epc = regs->cp0_epc;
    old31 = regs->regs[31];
    opcode = 0;
    status = -1;

    if (unlikely(compute_return_epc(regs) < 0))
        break;

    if (get_isa16_mode(regs->cp0_epc)) {
        unsigned short mmop[2] = { 0 };

        if (unlikely(get_user(mmop[0], epc) < 0))
            status = SIGSEGV;
        if (unlikely(get_user(mmop[1], epc) < 0))
            status = SIGSEGV;
        opcode = (mmop[0] << 16) | mmop[1];

        if (status < 0)
            status = simulate_rdhwr_mm(regs, opcode);
    } else {
        if (unlikely(get_user(opcode, epc) < 0))
            status = SIGSEGV;

        if (!cpu_has_llsc && status < 0)
            status = simulate_llsc(regs, opcode);

        if (status < 0)
            status = simulate_rdhwr_normal(regs, opcode);
    }

    if (status < 0)
        status = SIGILL;

    if (unlikely(status > 0)) {
        regs->cp0_epc = old_epc;    /* Undo skip-over.  */
        regs->regs[31] = old31;
       force_sig(status, current);
    }

    break;

case 3:
    /*
     * Old (MIPS I and MIPS II) processors will set this code
     * for COP1X opcode instructions that replaced the original
     * COP3 space.  We don't limit COP1 space instructions in
     * the emulator according to the CPU ISA, so we want to
     * treat COP1X instructions consistently regardless of which
     * code the CPU chose.  Therefore we redirect this trap to
     * the FP emulator too.
     *
     * Then some newer FPU-less processors use this code
     * erroneously too, so they are covered by this choice
     * as well.
     */
    if (raw_cpu_has_fpu) {
        force_sig(SIGKILL, current);
        break;
    }
    /* Fall through.  */

case 1:
    err = enable_restore_fp_context(0);

    if (raw_cpu_has_fpu && !err)
        break;
    sig = fpu_emulator_cop1Handler(regs, ¤t->thread.fpu, 0,
                    &fault_addr);


    /*
     * We can't allow the emulated instruction to leave
     * andy enabled Cause bits set in $fcr31
     */
    fcr31 = mask_fcr31_x(current->thread.fpu.fcr31);
    current->thread.fpu.fcr31 &= ~fcr31;

    /* Send a signal if required.  */
    if (!process_fpemu_return(sig, fault_addr, fcr31) && !err)
        mt_ase_fp_affinity();

    break;
case 2:
    raw_notifier_call_chain(&cu2_chain, CU2_EXCEPTION, regs);
    break;
}

exception_exit(prev_state);

}
该函数首先通过cause寄存器取出协处理器的编号,根据mips规范,只有协处理器2允许在内核发生"协处理器不可用"异常,因此如果在内核态发生其他其他协处理器异常,则调用die进入死机状态;
龙芯三号只有cp0,cp1,cp2共三个协处理器,忽略协处理器3的处理分支;
通常情况下,在用户态执行cp0特权指令或者访问cp0寄存器是不允许的,属于越权;
(二)系统调用;
系统调用是用户态应用程序请求内核为其服务的一种机制,是用户态访问特权资源的合法入口,mips提供了syscall指令用于实现系统调用,glibc是Linux上最常用的基础运行时库,系统调用大多是通过glibc中的函数发起的;
下面以创建新进程的fork为例,在glibc-2.28中,库函数fork是__libc_fork()的别名,定义在sysdeps/npt/fork.c中,而__libc_fork将会调用arch_fork,mips处理器上的arch_fork会被展开成INLINE_SYSCALL_CALL,即通过sys_clone系统调用来实现fork功能(早期的fork->sys_fork,而新版的glibc使用sys_clone系统调用);
系统调用表;
将sys_fork的系统调用号_NR_fork加载到v0寄存器,然后用syscall指令触发系统调用异常;
内核系统调用的总入口是handle_sys,在内核中有四种定义,32位内核的032系统调用入口定义在./arch/mips/kernel/scall32-o32.S,64位内核的o32系统调用入口定义在./arch/mips/kernel/scall64-o32.S,64位内核的n32系统调用入口定义./arch/mips/kernel/scall64-n32.S,64位内核的n64系统调用入口./arch/mips/kernel/scall64-64.S,关注64位内核的n64 abi;
2.6.29内核以后,引入了"系统调用包装器",系统调用函数通常不像普通函数一样直接定义,而是专门有一系列的包装器宏;如:SYSCALL_DEFINE0
3.3中断处理解析
3.3.1中断处理的入口
中断处理的入口在第二层次的异常向量表里,其中断处理函数是handle_int或rollback_handle_int;
arch/mips/kernel/genex.S
.macro BUILD_ROLLBACK_PROLOGUE handler
FEXPORT(rollback_\handler) //此函数的前面部分用于检测中断发生时是否已经执行了wait指令(通过比较epc寄存器高位部分和__r4k_wait的地址来判断),如果已经执行了,保持epc不变,跳转到\handler(此处的\handler展开即为handler_int),如果尚未执行,则通过ori和xori指令清零epc的低五位,即8条指令的空间,清零低五位相当于把epc设置成__r4k_wait,于是,handler_int返回以后,要么回到__r4k_wait的第一条指令,要么回到_r4k_wait的最后一条指令,如果是前者,将会有机会再次检测是否有任务需要调度,如果是后者,直接退出空闲等待状态,总之,不会耽误重要的工作.
.set push
.set noat
MFC0 k0, CP0_EPC
PTR_LA k1, __r4k_wait
ori k0, 0x1f /* 32 byte rollback region */
xori k0, 0x1f
bne k0, k1, 9f //此处只考虑了0号进程执行r4k_wait的过程,未考虑普通进程上下文时产生中断,如果是普通进程,则执行bne k0,k1,\handler,k0,k1必定不相等,因为epc中的地址不可能在r4k_wait内,因此会直接执行handler_int
MTC0 k0, CP0_EPC
9:
.set pop
.endm

.align  5

BUILD_ROLLBACK_PROLOGUE handle_int
NESTED(handle_int, PT_SIZE, sp)
#ifdef CONFIG_TRACE_IRQFLAGS
/*
* Check to see if the interrupted code has just disabled
* interrupts and ignore this interrupt for now if so.
*
* local_irq_disable() disables interrupts and then calls
* trace_hardirqs_off() to track the state. If an interrupt is taken
* after interrupts are disabled but before the state is updated
* it will appear to restore_all that it is incorrectly returning with
* interrupts disabled
*/
.set push
.set noat
mfc0 k0, CP0_STATUS
#if defined(CONFIG_CPU_R3000) || defined(CONFIG_CPU_TX39XX)
and k0, ST0_IEP
bnez k0, 1f

mfc0    k0, CP0_EPC
.set    noreorder
j   k0
rfe

#else
and k0, ST0_IE
bnez k0, 1f

eret

#endif
1:
.set pop
#endif
SAVE_ALL //保存寄存器上下文
CLI //关中断
TRACE_IRQS_OFF //jal trace_hardirqs_off,

LONG_L  s0, TI_REGS($28)	//$28即gp寄存器,保存着当前进程的thread_info,ti_regs是thread_info中regs成员的偏移;
LONG_S  sp, TI_REGS($28)
PTR_LA  ra, ret_from_irq		
PTR_LA  v0, plat_irq_dispatch	//调用核心中断的处理函数
jr  v0

#ifdef CONFIG_CPU_MICROMIPS
nop
#endif
END(handle_int)

.align  5   /* 32 byte rollback region */

LEAF(__r4k_wait)
.set push
.set noreorder
/* start of rollback region /
LONG_L t0, TI_FLAGS($28) //从当前进程的thread_info取出进程标志变量到t0,
nop
andi t0, _TIF_NEED_RESCHED //判断有没有置位,如果置位了说明进程有调度
bnez t0, 1f //置位了,跳转到1处,此函数返回;
nop
nop //没有置位,执行wait进入空闲等待状态(停止执行指令流,进入节能状态,直到被中断唤醒再继续执行指令流);__r4k_wait函数的执行不是原子的,是可以被中断的,总共八条指令,占用32字节
nop
#ifdef CONFIG_CPU_MICROMIPS
nop
nop
nop
nop
#endif
.set mips3
wait
/
end of rollback region (the region size must be power of two) */
1:
jr ra
nop
.set pop
END(__r4k_wait)
3.3.2中断处理的分派
第一,二级分派
第一级分派函数就是handler_int所调用的plat_irq_dispatch;
函数所在文件arch/mips/loongson2/irq.c;
asmlinkage void plat_irq_dispatch(void)
{
unsigned int cp0_cause;
unsigned int cp0_status;
unsigned int cp0_cause_saved;
cp0_cause_saved = read_c0_cause() & ST0_IM ; //取出$13 cause寄存器的8~15的中断信号的ip位(指出等待的中断,该位保持不变,直到中断撤销,最低两位是软中断位,可由软件设置与清除)标识产生了中断信号的ip位,
cp0_status = read_c0_status(); //$12状态寄存器,8位的中断屏蔽(im)域控制8个中断条件的使能,中断被触发之前必须被使能,satus寄存器中的中断屏蔽域和cause寄存器的中断待定域对应的位都应该被置位;控制每一个外部、内部和软件中断的使能。如果中断被使能,将允许它触发,同时 Cause 寄存器的中断 pending 字段相应的位被置位;状态寄存器中的ip位标识当前被启用的ip位,因此pending变量标识了需要进一步处理的中断源;
cp0_cause = cp0_cause_saved & cp0_status;

if (cp0_cause & STATUSF_IP7) //1<<15;  7+8 ip7用于内部时钟中断,ip2在3a上用于cpu串口中断,
    ip7_dispatch();

#ifdef CONFIG_SMP
else if (cp0_cause & STATUSF_IP6) //1<<14; 6+8 ip6用于处理核间中断,但ipi包括不同的子类型;ip3在3a上连接的是ht1控制器(或者sys int0)和级联的外部中断控制器(i8259或者apic),包括了所有来自外部设备的中断,因此处理器间中断和外部中断设备中断需要再次进行分派;每一个核都有一组ipi寄存器,但都是全局可访问的,既可以访问自己的也可以访问其他的,其中mailbox在启动多核时传递初始入口,堆栈指针,全局指针等参数;直接按地址访问上述ipi寄存器的方式叫做mmio方式,龙芯3a4000引入了crs方式(control status register),如CSR_IPI_Status寄存器,在csr方式中,状态,使能,置位,清零寄存器与传统mmio方式一一对应,但只能操作本核,跨核发送ipi和传递消息必须通过CSR_IPI_Send寄存器,mips定义了四种标准的ipi类型;Ipi中断由一个核发往另一个核,SMP_RESCHEDULE_YOURSELF 0x01要求目标cpu调用一个函数;SMP_ICACHE_FLUSH要求目标cpu刷新一次指令cache;SMP_ASK_C0COUNT询问0号核的count寄存器值,理论上SMP_CALL_FUNCTION就可以实现任意ipi功能,但是设计成专门的ipi,在性能上会更有优势;龙芯处理器的外部中断路由是在内核初始化的时候确定(都路由到了0号核),不允许运行时修改;为了实现中断负载均衡,可以通过ipi来完成外部中断的软件转发,因此我们需要一个irq->IPI_OFFSET映射表;32个ipi位域里,第0~3位已经被linux内核定义,第4位开始都用做irq转发的IPI_OFFSET,因此代码中IPI_IRQ_OFFSET为4(32-4=28,因此最多可转发28中irq);ipi是cpu和cpu之间的互动;
ip6_dispatch();

#endif
else if (cp0_cause & STATUSF_IP5) //1<<13,5+8;
ip5_dispatch();
else
ip4_dispatch();

}
asmlinkage void ip7_dispatch(void)
{
do_IRQ(MIPS_CPU_IRQ_BASE + 7); //0;
}

#ifdef CONFIG_SMP
asmlinkage void ip6_dispatch(void)
{
ls64_ipi_interrupt(NULL);
}
#endif

void ls64_ipi_interrupt(struct pt_regs regs)
{
int i, cpu = smp_processor_id();
unsigned long base = IPI_BASE_OF(cpu_logical_map(cpu)); //1fe1 + i
100;
unsigned int action, c0count;

/* Load the ipi register to figure out what we're supposed to do */
action = ls64_conf_read32((void *)(base + IPI_OFF_STATUS)); //+0x1000,0 /1号处理器核的 IPI_Status 寄存器

/* Clear the ipi register to clear the interrupt */
ls64_conf_write32((u32)action,(void *)(base + IPI_OFF_CLEAR)); //0/1 号处理器核的 IPI_Clear 寄存器

if (action & SMP_RESCHEDULE_YOURSELF) { //0x1
    scheduler_ipi();
}

if (action & SMP_CALL_FUNCTION) { //
    smp_call_function_interrupt();
}

if (action & SMP_ASK_C0COUNT) {
    BUG_ON(cpu != 0);
    c0count = read_c0_count();
    c0count = c0count ? c0count : 1;
    for (i = 1; i < num_possible_cpus(); i++)
        core0_c0count[i] = c0count;
    __sync();
}

}
ipi发送和处理函数
arch/mips/loongson2/smp.c
/*

  • Simple enough, just poke the appropriate ipi register
    */
    static void loongson_send_ipi_single(int cpu, unsigned int action) //往单个目标cpu发送ipi,参数是目标cpu的标号cpu,ipi的类型action
    {
    unsigned long base = IPI_BASE_OF(cpu_logical_map(cpu));
    ls64_conf_write32((u32)action, (void *)(base + IPI_OFF_SET));
    }
    static void loongson_send_ipi_mask(const struct cpumask *mask, unsigned int action) //往多个核发送ipi,mask是目标cpu编号组成的掩码;
    {
    unsigned int i;
    for_each_cpu(i, mask){
    unsigned long base = IPI_BASE_OF(cpu_logical_map(i));
    ls64_conf_write32((u32)action, (void *)(base + IPI_OFF_SET));
    }
    }

/* ip7 take perf/timer /
/
ip6 take smp /
/
ip5 take off-chip msi irq /
/
ip4 take on-chip irq */
void ls64_ipi_interrupt(struct pt_regs *regs) //ipi处理函数
{
int i, cpu = smp_processor_id();
unsigned long base = IPI_BASE_OF(cpu_logical_map(cpu));
unsigned int action, c0count;

/* Load the ipi register to figure out what we're supposed to do */
action = ls64_conf_read32((void *)(base + IPI_OFF_STATUS));  //通过ipi_status寄存器读取所有ipi中断状态并保存到action;

/* Clear the ipi register to clear the interrupt */
ls64_conf_write32((u32)action,(void *)(base + IPI_OFF_CLEAR)); //清楚所有的中断状态;

if (action & SMP_RESCHEDULE_YOURSELF) {
    scheduler_ipi(); //触发一次SCHED_SOFTIRQ的软中断,导致目标cpu重新调度;
}

if (action & SMP_CALL_FUNCTION) {
    smp_call_function_interrupt(); //执行源cpu所请求的函数;SMP_ICACHE_FLUSH类型不做任何处理的原因是龙芯由硬件维护cache一致性;
}

if (action & SMP_ASK_C0COUNT) { //该类型将导致0号核通过read_c0_count读取count寄存器的值并写入core0_c0count[]数组;
    BUG_ON(cpu != 0);
    c0count = read_c0_count();
    c0count = c0count ? c0count : 1;
    for (i = 1; i < num_possible_cpus(); i++)
        core0_c0count[i] = c0count;
    __sync();
}
//3a的话此部分还有中断转发的处理,通过IPI_OFFSET->IRQ映射表(即loongson_ipi_pos2irq[]数组)来获取irqs中各个被转发的irq编号,再调用do_irq逐个处理这些irq;

}
(三)第三级分派--外部设备中断
外部设备的中断也需要进行第三级分派,而第三级分派是一个函数指针loongson_pch->irq_dispatch,在使用不同芯片组的机型中有不同的定义;
2h芯片组:三级分派函数,ls2h_irq_dispatch
7a芯片组:第三级分派函数为legacy版的ls7a_irq_dispatch和msi版本的ls7a_msi_irq_dispatch;
由于硬件平台的限制,龙芯的外部中断只能使用静态路由,而通常的做法是把所有的外部中断全部路由到0号核,但是会导致0号核非常繁忙,中断处理效率低下甚至丢失中断,中断负载就是用于解决此问题的,均匀中断转发到每一个核,提高处理效率;
并不是每一个irq都会进行中断负载均衡,只有高速设备才会作此处理,如可配置的pci/pcie设备以及pata磁盘器,loongson_ipi_irq2pos记录了irq→IPI_OFFSET的映射关系,取值为-1的irq就是本地处理的irq;
进行平衡的irq,不一定能够被转发出去,因为一个irq能被哪些cpu核处理是受该irq的irq_data结构中affinity变量控制的,如irq_data::common::affinity所指定的核处于非活动状态(如核被关闭或者正在关闭过程中),那就不能转发;
第三级分派最终的核心是do_irq,其核心步骤是调用generic_handle_irq,也就是第四级分派;
(四)第四级分配
文件:kernel/irq/irqdesc.c
/**

  • generic_handle_irq - Invoke the handler for a particular irq
  • @irq: The irq number to handle

*/
int generic_handle_irq(unsigned int irq)
{
struct irq_desc *desc = irq_to_desc(irq);

if (!desc)
    return -EINVAL;
generic_handle_irq_desc(irq, desc);
return 0;

}
EXPORT_SYMBOL_GPL(generic_handle_irq);
/*

  • Architectures call this to let the generic IRQ layer
  • handle an interrupt. If the descriptor is attached to an
  • irqchip-style controller then we call the ->handle_irq() handler,
  • and it calls __do_IRQ() if it’s attached to an irqtype-style controller.
    */
    static inline void generic_handle_irq_desc(unsigned int irq, struct irq_desc *desc)
    {
    desc->handle_irq(irq, desc);
    }

irqreturn_t
__handle_irq_event_percpu(struct irq_desc *desc, struct irqaction *action,
unsigned int *flags)
{
irqreturn_t retval = IRQ_NONE; //意味着这不是我的中断;
unsigned int irq = desc->irq_data.irq;

do {
    irqreturn_t res;
   
    trace_irq_handler_entry(irq, action);
    res = action->handler(irq, action->dev_id);
    trace_irq_handler_exit(irq, action, res);

    if (WARN_ONCE(!irqs_disabled(),"irq %u handler %pF enabled interrupts\n",
              irq, action->handler))
        local_irq_disable();
        
    switch (res) {
    case IRQ_WAKE_THREAD: //意味着这是我的中断,并且需要唤醒中断线程(当前irqaction中的thread成员)
        /*
         * Catch drivers which return WAKE_THREAD but
         * did not set up a thread function
         */
        if (unlikely(!action->thread_fn)) {
            warn_no_thread(irq, action);
            break;
        }

        irq_wake_thread(desc, action);

        /* Fall through to add to randomness */ 
    case IRQ_HANDLED: //意味着这是我的中断并且已经处理完完毕;
        *flags |= action->flags;
        break;

    default:
        break;
    }

    retval |= res;
    action = action->next;
} while (action);

return retval;

}
IRQ_WAKE_THREAD用于支持线程化中断,这是2.6.30版本开始引入的新特性,因为中断处理函数irqaction::handler通常需要关中断执行,而关中断上下文是一种非常影响效率的,应当尽可能避免;线程化中断本质上是将irqaction::handler一分为二:非常紧迫并且需要关中断的操作依旧在irqaction::handler中执行;其他比较耗时并且不需要关中断的操作放到irqaction::thread_fn()去执行;后者在进程上下文(内核线程)中执行,相当于对关中断区间进行了最小化;如果irqaction::handler返回IRQ_WAKE_THREAD,则中断线程irqaction::thread将被唤醒,用于执行irqaction::thread_fn;
request_irq用于注册非线程化isr,特定的irq中断号和特定的中断处理函数handler进行绑定,相当于给irq注册了一个irqaction::handler;
request_threaded_irq用于注册线程化isr,将特定的irq中断号和特定的中断处理函数handler以及thread_fn进行绑定,相当于给irq注册了一个irqaction::handler和irqaction::thread_fn;setup_irq用于关联一个irq和一个已经设置好isr的irqaction;
时钟中断为例,缺省mips时钟事件源的初始化函数为r4k_clockevent_init, setup_irq(cd->irq, &c0_compare_irqaction);
大多数的mips处理器的时钟中断和性能计数器中断共享一个irq,但从Mips r2开始可以通过cp0的cause寄存器进行区分,如果第30位(CAUSEF_TI)置位,就是时钟中断;如果第26位置位(CAUSEF_PCI)置位,就是性能计数器中断;mips时钟中断是一个每cpu中断,也就是每个逻辑cpu上都有自己的本地时钟源设备(count/compare寄存器),都会产生本地的时钟中断;
irqreturn_t c0_compare_interrupt(int irq, void *data)
{
const int r2 = cpu_has_mips_r2; //用局部变量r2标识cpu是否支持mips r2
struct clock_event_device cd;
int cpu = smp_processor_id();
#if defined(CONFIG_CPU_LOONGSON3)
#if defined(CONFIG_OPROFILE)
if(!(read_c0_cause() & (1<<30))) return IRQ_NONE;
#endif
loongson3_cache_stall_unlock(cpu, irq);
#endif
/

* Suckage alert:
* Before R2 of the architecture there was no way to see if a
* performance counter interrupt was pending, so we have to run
* the performance counter interrupt handler anyway.
*/
if (handle_perf_irq(r2)) //此函数处理性能计数器中断,如果处理成功,直接返回;否则开始真正的mips时钟中断;
goto out;

/*
 * The same applies to performance counter interrupts.  But with the
 * above we now know that the reason we got here must be a timer
 * interrupt.  Being the paranoiacs we are we check anyway.
 */
if (!r2 || (read_c0_cause() & (1 << 30))) {在mips r2之前cpu上会无条件处理时钟中断,而在mips r2或者更新的cpu上,需要看cause寄存器上的causef_ti是否置位,下面是处理时钟中断的流程;
    /* Clear Count/Compare Interrupt */
    write_c0_compare(read_c0_compare()); //往compare寄存器写入旧值来清中断,
    cd = &per_cpu(mips_clockevent_device, cpu); //获取本cpu的时钟源设备

#ifdef CONFIG_CEVT_GIC
if (!gic_present)
#endif
cd->event_handler(cd); //时钟源设备上的函数指针,根据配置不同而不同,龙芯使用的是hrtimer_interrupt;
}

out:
return IRQ_HANDLED;
}

内存管理:

linux内核中调用伙伴系统的主要api函数接口:alloc_page,alloc_pages分别用于分配单独的一个页帧和连续的多个页帧;__get_free_page,__get_free_pages
linux内核有非抢占,自愿抢占,完全抢占和实时抢占这四种抢占调度方式,非抢占内核只允许在用户态发生抢占,内核态不允许抢占;自愿抢占内核是白名单机制,在内核某些抢占点上可以抢占;完全抢占是黑名单机制,除非显示禁用抢占,否则内核态总是可以抢占;实时抢占在内核态的任意时间点都可以抢占;might_sleep_if就是一个抢占点,
页帧释放解析
void __free_pages(struct page *page, unsigned int order)
{
if (put_page_testzero(page)) {
if (order == 0)
free_hot_cold_page(page, false);
else
__free_pages_ok(page, order);
}
}

EXPORT_SYMBOL(__free_pages);

void free_pages(unsigned long addr, unsigned int order)
{
if (addr != 0) {
VM_BUG_ON(!virt_addr_valid((void *)addr));
__free_pages(virt_to_page((void *)addr), order);
}
}
4.3 内核内存对象管理
为了解决内核自身使用小块内存的碎片问题,linux引入了基于对象的内存管理(或者叫做内存管理,memory area management),就是slab系统算法;一个对象(内存区)是一个具有任意大小的以字节为单位的连续内存块.对象可以拥有特定的内部数据结构(因而具有特定的大小),也可以是大小符合几何分布的无结构内存块.尽量减少直接调用伙伴系统的api,因而对硬件高速缓存更加友好.
广义的slab是一个通用概念,泛指内核中的内存对象管理系统,其具有实现有经典的slab,适用于嵌入式的slob和适用于大规模系统的slub这三种;组织结构分三级:快速缓存cache(纯软的概念),slab,对象object;
4.3.1 数据结构与api
kmem_cache
mips虚拟地址空间,32和64位地址空间分布
FIXADDR_TOP
固定映射区
FIXADDR_START
PKMAP_END
持久映射区
PKMAP_BASE 0xFE00 0000
隔离带 (约两个页大小,用于捕获内存越界访问)
VMALLOC_END
非连续内存区
VMALLOC_START 0xC000 0000
以上是32位地址空间
FIXADDR_TOP
伪临时映射区
FIXADDR_START
MODULE_END
CKSEG2(模块区)
MODULE_START 0xFFFF FFFF C000 0000
CKSEG1
0xFFFF FFFF A000 0000
CKSEG0
0xFFFF FFFF 8000 0000
XKSEG(非连续内存区)
VMALLOC_END

VMALLOC_START 0xC000 0000 0000 0000
以上是64位地址空间
64位系统的持久内核映射和临时内核映射都是线性的;
4.4.1 持久内核映射
一种用于建立一个页帧到内核虚拟地址的长期映射,它可能引发睡眠,因此只能在进程上下文中.页帧是预先从伙伴系统里分配出来的,但分配的时候可能没有拿到虚拟地址,因此需要通过映射/解映射来建立或解除虚拟地址到物理地址的关联;

https://www.cnblogs.com/ralap7/p/9184773.html
虚拟地址空间不等于虚拟内存。虚拟地址空间是一个空间,不是真正存在的,只是通过CPU的寻址虚拟出来的一个范围。而虚拟内存是实实在在的硬盘的空间
每个进程根据虚拟地址从低到高都有代码段,数据段,堆,mmap映射区,栈和特殊区段(包括命令参数,环境变量,vdso段等),
tlb命中= 2匹配 && (全局匹配 || asid匹配);
内存描述符:
struct mm_struct init_mm = {
.mm_rb = RB_ROOT,
.pgd = swapper_pg_dir,
.mm_users = ATOMIC_INIT(2),
.mm_count = ATOMIC_INIT(1),
.mmap_sem = __RWSEM_INITIALIZER(init_mm.mmap_sem),
.page_table_lock = __SPIN_LOCK_UNLOCKED(init_mm.page_table_lock),
.mmlist = LIST_HEAD_INIT(init_mm.mmlist),
INIT_MM_CONTEXT(init_mm)
};
进程描述符:
struct vm_area_struct {
/* The first cache line has the info for VMA tree walking. */

unsigned long vm_start;     /* Our start address within vm_mm. */
unsigned long vm_end;       /* The first byte after our end address
                   within vm_mm. */

/* linked list of VM areas per task, sorted by address */
struct vm_area_struct *vm_next, *vm_prev;

struct rb_node vm_rb;

/*
 * Largest free memory gap in bytes to the left of this VMA.
 * Either between this VMA and vma->vm_prev, or between one of the
 * VMAs below us in the VMA rbtree and its ->vm_prev. This helps
 * get_unmapped_area find a free area of the right size.
 */
unsigned long rb_subtree_gap;

/* Second cache line starts here. */

struct mm_struct *vm_mm;    /* The address space we belong to. */
pgprot_t vm_page_prot;      /* Access permissions of this VMA. */
unsigned long vm_flags;     /* Flags, see mm.h. */

/*
 * For areas with an address space and backing store,
 * linkage into the address_space->i_mmap interval tree, or
 * linkage of vma in the address_space->i_mmap_nonlinear list.
 */
union {
    struct {
        struct rb_node rb;
        unsigned long rb_subtree_last;
    } linear;
    struct list_head nonlinear;
} shared;
/*
 * A file's MAP_PRIVATE vma can be in both i_mmap tree and anon_vma
 * list, after a COW of one of the file pages.  A MAP_SHARED vma
 * can only be in the i_mmap tree.  An anonymous MAP_PRIVATE, stack
 * or brk vma (with NULL file) can only be in an anon_vma list.
 */
struct list_head anon_vma_chain; /* Serialized by mmap_sem &
                  * page_table_lock */
struct anon_vma *anon_vma;  /* Serialized by page_table_lock */

/* Function pointers to deal with this struct. */
const struct vm_operations_struct *vm_ops;

/* Information about our backing store: */
unsigned long vm_pgoff;     /* Offset (within vm_file) in PAGE_SIZE
                   units, *not* PAGE_CACHE_SIZE */
struct file * vm_file;      /* File we map to (can be NULL). */
void * vm_private_data;     /* was vm_pte (shared mem) */

#ifndef CONFIG_MMU
struct vm_region vm_region; / NOMMU mapping region */
#endif
#ifdef CONFIG_NUMA
struct mempolicy vm_policy; / NUMA policy for the VMA */
#endif

/* reserved for Red Hat */
RH_KABI_USE(1, struct vm_userfaultfd_ctx vm_userfaultfd_ctx)
RH_KABI_USE(2, unsigned long vm_flags2) /* Flags, see mm.h. */
RH_KABI_RESERVE(3)
RH_KABI_RESERVE(4)

};
4.5.2内存映射
建立映射
arch/mips/kernel/syscall.c
SYSCALL_DEFINE6(mips_mmap, unsigned long, addr, unsigned long, len,
unsigned long, prot, unsigned long, flags, unsigned long,
fd, off_t, offset)
{
unsigned long result;

result = -EINVAL;
if (offset & ~PAGE_MASK)
    goto out;

result = sys_mmap_pgoff(addr, len, prot, flags, fd, offset >> PAGE_SHIFT);

out:
return result;
}
SYSCALL_DEFINE6(mips_mmap2, unsigned long, addr, unsigned long, len,
unsigned long, prot, unsigned long, flags, unsigned long, fd,
unsigned long, pgoff)
{
if (pgoff & (~PAGE_MASK >> 12))
return -EINVAL;

return sys_mmap_pgoff(addr, len, prot, flags, fd, pgoff >> (PAGE_SHIFT-12));

}
参数,映射虚拟地址,映射区长度,页保护权限,映射标志,映射文件描述符,文件内偏移,返回值是映射成功产生的虚拟地址或映射失败的错误码;上面两个函数的区别是偏移量的单位,前者是字节,后者是4kB,在于为32位应用程序提供大文件的映射能力;
4.5.3 堆区管理
使用malloc/free分配和释放堆内存,通过brk/sbrk来直接修改堆区vma大小;
4.5.4 缺页异常处理
tlb_do_page_fault,
4.6.2 内存回收
内存回收分直接回收和周期性回收两种:前者在内存分配函数得不到满足时直接触发,后者是内核线程kswapd的周期性扫描和评估;
4.6.3 巨页机制
巨页的主要原理是在多级页表中去掉最低一级,直接用次一级来当做页表项(把pmd当pte用);
第五章 进程管理

你可能感兴趣的:(读书笔记)