linux-mips启动分析 (1)
系统加电起动后,MIPS 处理器默认的程序入口是0xBFC00000,此地址在无缓存的KSEG1的地址区域内,对应的物理地址是 0x1FC00000,即CPU从0x1FC00000开始取第一条指令,这个地址在硬件上已经确定为FLASH的位置,Bootloader将 Linux 内核映像拷贝到 RAM 中某个空闲地址处,然后一般有个内存移动操作,目的地址在 arch/mips/Makefile 内指定:
load-$(CONFIG_MIPS_PB1550) += 0xFFFFFFFF80100000,
则最终bootloader定会将内核移到物理地址 0x00100000 处。
上面Makefile 里指定的的 load 地址,最后会被编译系统写入到 arch/mips/kernel/vmlinux.lds 中:
OUTPUT_ARCH(mips)
ENTRY(kernel_entry)
jiffies = jiffies_64;
SECTIONS
{
. = 0xFFFFFFFF80100000;
/* read-only */
_text = .; /* Text and read-only data */
.text : {
*(.text)
...
这个文件最终会以参数 -Xlinker --script -Xlinker vmlinux.lds 的形式传给 gcc,并最终传给链接器 ld 来控制其行为。
ld 会将 .text 节的地址链接到 0xFFFFFFFF80100000 处。
关于内核 ELF 文件的入口地址(Entry point),即 bootloader 移动完内核后,直接跳转到的地址,由ld 写入 ELF的头中,其会依次用下面的方法尝试设置入口点,当遇到成功时则停止:
a. 命令行选项 -e entry
b. 脚本中的 ENTRY(symbol)
c. 如果有定义 start 符号,则使用start符号(symbol)
d. 如果存在 .text 节,则使用第一个字节的地址。
e. 地址0
注意到上面的 ld script 中,用 ENTRY 宏设置了内核的 entry point 是 kernel_entry,因此内核取得控制权后执行的第一条指令是在 kernel_entry 处。
*********************************************
linux 内核启动的第一个阶段是从 /arch/mips/kernel/head.s文件开始的。
而此处正是内核入口函数kernel_entry(),该函数定义在 /arch/mips/kernel/head.s文件里。
kernel_entry()函数是体系结构相关的汇编语言,它首先初始化内核堆栈段,来为创建系统中的第一个进程进行准备,
接着用一段循环将内核映像的未初始化数据段(bss段,在_edata和_end之间)清零,
最后跳转到 /init/main.c 中的 start_kernel()初始化硬件平台相关的代码。
*********************************************
NESTED(kernel_entry, 16, sp) # kernel entry point
声明函数 kernel_entry,函数的堆栈为 16 byte,返回地址保存在 $sp 寄存器中。
-----------------------------
声明函数入口
#define NESTED(symbol, framesize, rpc) \
.globl symbol; \
.align 2; \
.type symbol,@function; \
.ent symbol,0; \
symbol: .frame sp, framesize, rpc
汇编伪指令 frame 用来声明堆栈布局。
它有三个参数:
1)第一个参数 framereg:声明用于访问局部堆栈的寄存器,一般为 $sp。
2)第二个参数 framesize:申明该函数已分配堆栈的大小,应该符合 $sp + framesize = 原来的 $sp。
3)第三个参数 returnreg:这个寄存器用来保存返回地址。
----------------------------
kernel_entry_setup # cpu specific setup
----------------------------
这个宏一般为空的,在 include/asm-mips/mach-generic/kernel-entry-init.h 文件中定义。
某些MIPS CPU需要额外的设置一些控制寄
存器,和具体的平台相关,一般为空宏;某些多核MIPS,启动时所
有的core的入口一起指向 kernel_entry,然后在该宏里分叉,
boot core 继续往下,其它的则不停的判断循环,直到boot core 唤醒之
----------------------------
setup_c0_status_pri
设置 cp0_status 寄存器
----------------------------
.macro setup_c0_status_pri
#ifdef CONFIG_64BIT
setup_c0_status ST0_KX 0
#else
setup_c0_status 0 0
#endif
.endm
----------------------------
ARC64_TWIDDLE_PC
除非 CONFIG_ARC64,否则为空操作
-----------------------------
#ifdef CONFIG_MIPS_MT_SMTC
mtc0 zero, CP0_TCCONTEXT__bss_start
mfc0 t0, CP0_STATUS
ori t0, t0, 0xff1f
xori t0, t0, 0x001e
mtc0 t0, CP0_STATUS
#endif /* CONFIG_MIPS_MT_SMTC */
宏定义 CONFIG_MIPS_MT_SMTC 是使用多核的 SMTC Linux 时定义的。一般情况下不考虑。
MIPS已经开发出 SMP Linux的改进版,叫做SMTC(线程上下文对称多处理) Linux。
SMTC Linux能理解轻量级 TC 的概念,并能因此减少某些与SMP Linux相关的开销。
----------------------------
PTR_LA t0, __bss_start # clear .bss
LONG_S zero, (t0)
PTR_LA t1, __bss_stop - LONGSIZE
1:
PTR_ADDIU t0, LONGSIZE
LONG_S zero, (t0)
bne t0, t1, 1b
清除 BSS 段,清 0。
变量 __bss_start 和 __bss_stop 在连接文件arch/mips/kernel/vmlinux.lds 中定义。
--------------------------------
LONG_S a0, fw_arg0 # firmware arguments
LONG_S a1, fw_arg1
LONG_S a2, fw_arg2
LONG_S a3, fw_arg3
把 bootloader 传递给内核的启动参数保存在 fw_arg0,fw_arg1,fw_arg2,fw_arg3 变量中。
变量 fw_arg0 为内核参数的个数,其余分别为字符串指针,为 *** = XXXX 的格式。
----------------------------------
MTC0 zero, CP0_CONTEXT # clear context register
清除 CP0 的 context register,这个寄存器用来保存页表的起始地址。
----------------------------------
PTR_LA $28, init_thread_union
初始化 $gp 寄存器,这个寄存器的地址指向一个 union,
THREAD_SIZE 大小,最低处是一个thread_info 结构
---------------------------------
PTR_LI sp, _THREAD_SIZE - 32
PTR_ADDU sp, $28
设置 $sp 寄存器,堆栈指针。 $sp = (init_thread_union 的地址) + _THREAD_SIZE - 32
的得出 $sp 指向这个 union 结构的结尾地址 - 32 字节地址。
-----------------------------------
set_saved_sp sp, t0, t1
把 这个 CPU 核的堆栈地址 $sp 保存到 kernelsp[NR_CPUS] 数组。
---------------------------------
如果定义了 CONFIG_SMP 宏,即多 CPU 核。
.macro set_saved_sp stackp temp temp2
#ifdef CONFIG_MIPS_MT_SMTC
mfc0 \temp, CP0_TCBIND
#else
MFC0 \temp, CP0_CONTEXT
#endif
LONG_SRL \temp, PTEBASE_SHIFT
LONG_S \stackp, kernelsp(\temp)
.endm
如果没有定义 CONFIG_SMP 宏,单 CPU 核。
.macro set_saved_sp stackp temp temp2
LONG_S \stackp, kernelsp
.endm
变量 kernelsp 的定义,在 arch/mips/kernel/setup.c 文件中。
unsigned long kernelsp[NR_CPUS];
把 这个 CPU 核的堆栈地址 $sp 保存到 kernelsp[NR_CPUS] 数组。
---------------------------------
PTR_SUBU sp, 4 * SZREG # init stack pointer
---------------------------------
j start_kernel
END(kernel_entry)
最后跳转到 /arch/mips/kernel/main.c 中的 start_kernel()初始化硬件平台相关的代码。
----------------------------------
******************************************
这个 init_thread_union 变量在 arch/mips/kernel/init_task.c 文件中定义。
union thread_union init_thread_union
__attribute__((__section__(".data.init_task"),
__aligned__(THREAD_SIZE))) =
{ INIT_THREAD_INFO(init_task) };
******************************************
问题:
1)这个 init_thread_union 结构体指针是怎么初始化的?
******************************************
linux-mips启动分析(2)
linux 内核启动的第一个阶段是从 /arch/mips/kernel/head.s文件开始的。
而此处正是内核入口函数kernel_entry(),该函数定义在 /arch/mips/kernel/head.s文件里。
kernel_entry()函数是体系结构相关的汇编语言,它首先初始化内核堆栈段,来为创建系统中的第一个进程进行准备, 接着用一段循环将内核映像的未初始化数据段(bss段,在_edata和_end之间)清零, 最后跳转到 /arch/mips/kernel/main.c 中的 start_kernel()初始化硬件平台相关的代码。
下面讲述 start_kernel() 函数。
********************************************
asmlinkage void __init start_kernel(void)
{
---------------------------------
char * command_line;
extern struct kernel_param __start___param[], __stop___param[];
定义了核的参数数据结构
---------------------------------
smp_setup_processor_id();
设置 SMP 多核的 CPU 核的 ID 号,单核不进行任何操作,我们不关心。
---------------------------------
unwind_init();
在 MIPS 体系结构中,这个函数是个空函数(可能调用setup_arch,配置核的相关函数)
---------------------------------
lockdep_init();
初始化核依赖关系哈希表。
---------------------------------
local_irq_disable();
关闭当前 CPU 核的中断
---------------------------------
early_boot_irqs_off();
通过一个静态全局变量 early_boot_irqs_enabled 来帮助我们调试代码,
通过这个标记可以帮助我们知道是否在“early bootup code”,
也可以通过这个标志警告是否有无效的中断打开。
和 early_boot_irqs_on() 函数配置使用,参考下面。
---------------------------------
early_init_irq_lock_class();
每一个中断都有一个 IRQ 描述符 ( struct irq_desc )来进行描述。
这个函数的主要作用是设置所有的 IRQ 描述符 ( struct irq_desc )的锁是统一的锁,
还是每一个 IRQ 描述符 ( struct irq_desc )都有一个小锁。
参考《linux-mips启动分析(2-1)》。
---------------------------------
lock_kernel();
获取大内核锁,这种大内核锁锁定整个内核。
--------------------------------
tick_init();
如果没有定义 CONFIG_GENERIC_CLOCKEVENTS 宏定义,则这个函数为空函数,
如果定义了这个宏,这执行初始化 tick 控制功能,注册 clockevents 的框架。
---------------------------------
boot_cpu_init();
对于 CPU 核的系统来说,设置第一个 CPU 核为活跃 CPU 核。
对于单 CPU 核系统来说,设置 CPU 核为活跃 CPU 核。
参考《linux-mips启动分析(2-1)》。
---------------------------------
page_address_init();
当定义了 CONFIG_HIGHMEM 宏,并且没有定义 WANT_PAGE_VIRTUAL 宏时,非空函数。
其他情况为空函数。
参考《linux-mips启动分析(2-1)》。
---------------------------------
printk(KERN_NOTICE);
printk(linux_banner);
输出打印版本信息。
---------------------------------
setup_arch(&command_line);
每种体系结构都有自己的 setup_arch() 函数,这些是体系结构相关的。
如何确定编译那个体系结构的 setup_arch() 函数呢?
主要由 linux 源码树顶层 Makefile 中 ARCH 变量来决定的。
例如: MIPS 体系结构的。
SUBARCH := mips
ARCH ?= $(SUBARCH)
---------------------------------
setup_command_line(command_line);
保存未改变的 comand_line 到字符数组 static_command_line[] 中。
保存 boot_command_line 到字符数组 saved_command_line[] 中。
参考《linux-mips启动分析(2-1)》。
---------------------------------
unwind_setup();
空函数。
---------------------------------
setup_per_cpu_areas();
如果没有定义 CONFIG_SMP 宏,则这个函数为空函数。
如果定义了 CONFIG_SMP 宏,
则这个 setup_per_cpu_areas() 函数给每个CPU分配内存,并拷贝 .data.percpu 段的数据。
参考《linux-mips启动分析(2-1)》。
---------------------------------
如果没有定义 CONFIG_SMP 宏,则这个函数为空函数。
如果定义了 CONFIG_SMP 宏,这个函数
smp_prepare_boot_cpu();
---------------------------------
sched_init();
核心进程调度器初始化,调度器的初始化优先于任何中断的建立(包括 timer 中断)。
并且初始化进程 0 ,即 idle 进程,但是并没有设置 idle 进程的 NEED_RESCHED 标志,
以完成内核剩余的启动部分。
---------------------------------
preempt_disable();
进制内核的抢占。使当前进程的 struct thread_info 结构 preempt_count 成员的值增加 1。
参考《linux-mips启动分析(2-1)》。
---------------------------------
建立各个节点的管理区的 zonelist,便于分配内存的 fallback 使用。
这个链表的作用: 这个链表是为了在一个分配不能够满足时可以考察下一个管理区来设置了。
在考察结束时,分配将从 ZONE_HIGHMEM 回退到 ZONE_NORMAL,
在分配时从 ZONE_NORMAL 退回到 ZONE_DMA 就不会回退了。
build_all_zonelists();
---------------------------------
参考《linux-mips启动分析(2-1)》。
page_alloc_init();
---------------------------------
在 MIPS 体系结构下,这个函数已经在 arch_mem_init() 函数中调用了一次。
这个函数的具体分析详细分析,请看《linux-mips启动分析(4)》。
所以这个函数直接返回。
parse_early_param();
---------------------------------
打印 linux 启动命令行参数。
printk(KERN_NOTICE "Kernel command line: %s\n", boot_command_line);
---------------------------------
这个函数在 《linux-mips启动分析(4)》文件有分析,可以参考。
这个函数的意思对 linux 启动命令行参数进行再分析和处理。
这两个变量 __start___param 和 __stop___param 在
链接脚本 arch/mips/kernel/vmlinux.lds 中定义。
最后一个参数为,当不能够识别 linux 启动命令行参数时,调用的函数。
parse_args("Booting kernel", static_command_line, __start___param,
__stop___param - __start___param, &unknown_bootoption);
---------------------------------
检查中断是否已经打开了,如果已将打开了,关闭中断。
if (!irqs_disabled()) {
local_irq_disable();
}
---------------------------------
sort_main_extable();
这个函数对内核建立的异常处理调用函数表( exception table )
根据异常的向量号进行堆排序。
---------------------------------
设置 CPU 的异常处理函数,TLB 重填,cache 出错,还有通用异常处理表的初始化。
trap_init();
---------------------------------
初始化 RCU 机制,这个步骤必须比本地 timer 的初始化早。
rcu_init();
---------------------------------
用来初始化中断处理硬件相关的寄存器和 中断描述符数组 irq_desc[] 数组,
每个中断号都有一个对应的中断描述符。
参考《linux-mips启动分析(11)》。
init_IRQ();
--------------------------------
系统在初始化阶段动态的分配了 4 个 hashtable,并把它们的地址存入 pid_hash[] 数组。
便于从 PID 查找 进程描述符地址。
参考《linux-mips启动分析(12)》。
pidhash_init();
---------------------------------
1)初始化本 CPU 上的定时器(timer)相关的数据结构
2)向 cpu_chain 通知链注册元素 timers_nb,该元素的回调函数用于初始化指定 CPU 上的定时器相关的数据结构。
3) 初始化时钟的软中断处理函数
参考《linux-mips启动分析(13)》。
init_timers();
---------------------------------
这个 hrtimers_init() 函数对高精度时钟进行初始化。
参考《linux-mips启动分析(14)》。
hrtimers_init();
---------------------------------
softirq_init();
---------------------------------
调用 timekeeping_init() 函数来初始化系统计时器。
参考《linux-mips启动分析(16)》。
timekeeping_init();
---------------------------------
调用 time_init() 函数,初始化系统时钟源,
对 MIPS 体系结构的 clocksource_mips 时钟源进行配置,和注册。
参考《linux-mips启动分析(17》。
time_init();
---------------------------------
这个函数对内核的 profile 功能进行初始化,这是一个内核调式工具,
通过这个可以发现内核在内核态的什么地方花费时间最多,
即发现内核的“hot spot”——执行最频繁的内核代码。
参考《linux-mips启动分析(18)。
profile_init();
---------------------------------
检测是否已经打开了中断。
如果已经打开了中断,则打印提示信息,报告一个 bug 。
if (!irqs_disabled())
printk(打印提示信息,报告一个 bug );
通过一个静态全局变量 early_boot_irqs_enabled 来帮助我们调试代码,
通过这个标记可以帮助我们知道是否在“early bootup code”,
也可以通过这个标志警告是否有无效的中断打开。
和 early_boot_irqs_off () 函数配置使用,参考上面。
early_boot_irqs_on();
使能本地 CPU 的中断。
local_irq_enable();
====================================================
总结:上面是早期启动阶段,在上面的执行过程中,中断是关闭的。
# 输出Linux版本信息(printk(linux_banner))
# 设置与体系结构相关的环境(setup_arch())
# 页表结构初始化(paging_init())
# 设置系统自陷入口(trap_init())
#初始化系统IRQ(init_IRQ())
# 核心进程调度器初始化(sched_init())
# 时间、定时器初始化(包括读取CMOS时钟、估测主频、初始化定时器中断等,time_init())
# 提取并分析核心启动参数(从环境变量中读取参数,设置相应标志位等待处理,(parse_options())
======================================================
console_init();
这个 console_init() 函数初始化内核显示终端,在这里仅仅进行一些初级的初始化。
参考《linux-mips启动分析(19)》。
---------------------------------
如果这个 panic_later 字符串已经设置了,则停止系统的启动。
if (panic_later)
panic(panic_later, panic_param);
---------------------------------
如果没有定义 LOCKDEP 这个宏,则这个 lockdep_info() 函数就为空函数。
lockdep_info();
---------------------------------
如果没有定义 CONFIG_DEBUG_LOCKING_API_SELFTESTS 这个宏,
则这个 locking_selftest() 函数为空函数。
locking_selftest();
---------------------------------
#ifdef CONFIG_BLK_DEV_INITRD
if (initrd_start && !initrd_below_start_ok
&& initrd_start < min_low_pfn << PAGE_SHIFT) {
initrd_start = 0;
}
#endif
---------------------------------
vfs_caches_init_early();
---------------------------------
cpuset_init_early();
---------------------------------
mem_init();
---------------------------------
kmem_cache_init();
---------------------------------
setup_per_cpu_pageset();
---------------------------------
numa_policy_init();
---------------------------------
if (late_time_init)
late_time_init();
---------------------------------
calibrate_delay();
---------------------------------
pidmap_init();
---------------------------------
pgtable_cache_init();
---------------------------------
prio_tree_init();
---------------------------------
anon_vma_init();
---------------------------------
fork_init(num_physpages);
---------------------------------
}
*******************************************
问题:
1)核依赖关系哈希表是如何使用的?什么功能?
2)在变量 __per_cpu_end 和 __per_cpu_start 之间保存了什么数据?
linux-mips启动分析 (2-1)
这个文件讲解了 start_kernel() 函数中调用的一些小函数,可以作为参考。
有下列函数:
early_init_irq_lock_class()
boot_cpu_init()
page_address_init()
setup_command_line()
setup_per_cpu_areas()
preempt_disable()
**********************************************
如果没有定义宏 CONFIG_TRACE_IRQFLAGS 和 CONFIG_GENERIC_HARDIRQS,
函数 early_init_irq_lock_class() 就是空的;
#if defined(CONFIG_TRACE_IRQFLAGS) && defined(CONFIG_GENERIC_HARDIRQS)
extern void early_init_irq_lock_class(void);
#else
static inline void early_init_irq_lock_class(void)
{
}
#endif
如果没有定义宏 CONFIG_TRACE_IRQFLAGS 和 CONFIG_GENERIC_HARDIRQS,
则 early_init_irq_lock_class() 函数在 kernel/irq/handle.c 文件中,
进行了定义;
static struct lock_class_key irq_desc_lock_class;
void early_init_irq_lock_class(void)
{
int i;
for (i = 0; i < NR_IRQS; i++)
lockdep_set_class(&irq_desc[i].lock, &irq_desc_lock_class);
}
每一个中断都有一个 IRQ 描述符 ( struct irq_desc )来进行描述。
这个函数的主要作用是设置所有的 IRQ 描述符 ( struct irq_desc )的锁是统一的锁,
还是每一个 IRQ 描述符 ( struct irq_desc )都有一个小锁。
********************************************
这个 boot_cpu_init() 函数设置 CPU 位图信息。
参考《linux的CPU信息管理》。
----------------------------------------
static void __init boot_cpu_init(void)
{
int cpu = smp_processor_id();
获取当前 CPU 的 ID 号。
cpu_set(cpu, cpu_online_map);
cpu_set(cpu, cpu_present_map);
cpu_set(cpu, cpu_possible_map);
}
*****************************************
page_address_init();
当定义了 CONFIG_HIGHMEM 宏,并且没有定义 WANT_PAGE_VIRTUAL 宏时,非空函数。
其他情况 page_address_init() 函数为空函数。
static struct page_address_map page_address_maps[LAST_PKMAP];
void __init page_address_init(void)
{
int i;
INIT_LIST_HEAD(&page_address_pool);
for (i = 0; i < ARRAY_SIZE(page_address_maps); i++)
list_add(&page_address_maps[i].list, &page_address_pool);
for (i = 0; i < ARRAY_SIZE(page_address_htable); i++) {
INIT_LIST_HEAD(&page_address_htable[i].lh);
spin_lock_init(&page_address_htable[i].lock);
}
spin_lock_init(&pool_lock);
}
把结构体 struct page_address_map 类型的数组 page_address_maps 加入链表 page_address_pool 上。
********************************************
字符数组 command_line[] 和 boot_command_line[]在 arch_mem_init() 函数中赋值,
为命令行启动参数字符串组合。参考《linux-mips启动分析(4)》。
这两个字符指针 saved_command_line 和 static_command_line 在 main.c 文件中定义。
----------------------------------------
static void __init setup_command_line(char *command_line)
{
使用引导内存分配器分配内存 command_line 的保存空间,并拷贝到这个空间中。
saved_command_line = alloc_bootmem(strlen (boot_command_line)+1);
static_command_line = alloc_bootmem(strlen (command_line)+1);
strcpy (saved_command_line, boot_command_line);
strcpy (static_command_line, command_line);
}
******************************************
如果没有定义 CONFIG_SMP 宏,则这个函数为空函数。
如果定义了 CONFIG_SMP 宏,
则这个 setup_per_cpu_areas() 函数给每个CPU分配 pre-cpu 结构内存,
并拷贝 .data.percpu 段的数据。
参考《每CPU变量的数据组织和访问》。
----------------------------------------
static void __init setup_per_cpu_areas(void)
{
unsigned long size, i;
char *ptr;
取得 CPU 核的数目。
unsigned long nr_possible_cpus = num_possible_cpus();
为每个 CPU 分配 size 大小的空间。
size = ALIGN(PERCPU_ENOUGH_ROOM, PAGE_SIZE);
ptr = alloc_bootmem_pages(size * nr_possible_cpus);
for_each_possible_cpu(i) {
__per_cpu_offset[i] = ptr - __per_cpu_start;
memcpy(ptr, __per_cpu_start, __per_cpu_end - __per_cpu_start);
ptr += size;
}
}
----------------------------------------
变量 __per_cpu_end 和 __per_cpu_start 在链接
文件 arch/mips/kernel/vmlinux.lds 中定义。
在该函数中,为每个CPU分配一段专有数据区,并将.data.percpu中的数据拷贝到其中,
每个CPU各有一份。由于数据从__per_cpu_start处转移到各CPU自己的专有数据区中了,
因此存取其中的变量就不能再用原先的值了,比如存取per_cpu__runqueues
就不能再用per_cpu__runqueues了,需要做一个偏移量的调整,
即需要加上各CPU自己的专有数据区首地址相对于__per_cpu_start的偏移量。
在这里也就是__per_cpu_offset[i],其中CPU i的专有数据区相对于
__per_cpu_start的偏移量为__per_cpu_offset[i]。
这样,就可以方便地计算专有数据区中各变量的新地址,比如对于per_cpu_runqueues,
其新地址即变成per_cpu_runqueues + __per_cpu_offset[i]。
----------------------------------------
定义 per_cpu 数据时使用这个 DEFINE_PER_CPU(type, name) 宏定义
它的定义如下:
如果定义了 CONFIG_SMP:
#define DEFINE_PER_CPU(type, name) \
__attribute__((__section__(".data.percpu"))) __typeof__(type) per_cpu__##name
访问 per_cpu 数据的实现:
#define per_cpu(var, cpu) (*({ \
extern int simple_identifier_##var(void); \
RELOC_HIDE(&per_cpu__##var, __per_cpu_offset[cpu]); }))
#define __get_cpu_var(var) per_cpu(var, smp_processor_id())
#define __raw_get_cpu_var(var) per_cpu(var, raw_smp_processor_id())
如果没有定义 CONFIG_SMP:
#define DEFINE_PER_CPU(type, name) \
__typeof__(type) per_cpu__##name
访问 per_cpu 数据的实现:
#define per_cpu(var, cpu) (*((void)(cpu), &per_cpu__##var))
#define __get_cpu_var(var) per_cpu__##var
#define __raw_get_cpu_var(var) per_cpu__##var
*****************************************
如果没有定义 CONFIG_PREEMPT 这个宏,这个 preempt_disable()函数为空函数。
如果定义了这个 CONFIG_PREEMPT 这个宏。
----------------------------------------
这个 preempt_disable()函数在 include/linux/preempt.h 文件中实现。
#define preempt_disable() \
do { \
inc_preempt_count(); \
barrier(); \
} while (0)
主要由宏 inc_preempt_count() 来完成工作。
#define inc_preempt_count() add_preempt_count(1)
# define add_preempt_count(val) do { preempt_count() += (val); } while (0)
#define preempt_count() (current_thread_info()->preempt_count)
----------------------------------------
由以上可以看出这个 preempt_disable()函数主要工作是使当前进程
的 struct thread_info 结构 preempt_count 成员的值增加 1。
****************************************
void __init page_alloc_init(void)
{
hotcpu_notifier(page_alloc_cpu_notify, 0);
}
---------------------------------
这个 hotcpu_notifier()宏函数在 include/linux/cpu.h 文件中实现的,
这个函数有两个版本,通过 CONFIG_HOTPLUG_CPU 宏定义来进行控制。
我们的系统不支持 CPU 的热插拔,所以没有定义这个 CONFIG_HOTPLUG_CPU 宏。
所以它的实现如下所示:
#define hotcpu_notifier(fn, pri) do { (void)(fn); } while (0)
它的第一个参数是一个函数指针,这个函数的意思是直接调用这个函数指针就行了。
不过经过测试,好像也没有调用 page_alloc_cpu_notify() 函数。
******************************************
问题:
1)这个 page_alloc_init() 函数好像没有执行任何的操作,不知道对不对?
linux-mips启动分析(3)
在 start_kernel() 函数中调用了 setup_arch() 函数。
每种体系结构都有自己的 setup_arch() 函数,这些是体系结构相关的。
如何确定编译那个体系结构的 setup_arch() 函数呢?
主要由 linux 源码树顶层 Makefile 中 ARCH 变量来决定的。
例如: MIPS 体系结构的。
SUBARCH := mips
ARCH ?= $(SUBARCH)
下面我们分析一下 MIPS 体系结构的 setup_arch() 函数。
从《linux-mips启动分析(3)》到《linux-mips启动分析(5)》文件中 一直在讲述 setup_arch() 函数。
********************************************
位于 /arch/mips/kernel/setup.c 文件中。
参数 cmdline_p 为字符的指针的指针,没有赋值。
可能为了以后把内核启动参数保存到这个指针指向的字符串中。
void __init setup_arch(char **cmdline_p)
{
cpu_probe();
调用函数cpu_probe(),该函数通过MIPS CPU的PRID寄存器来确定CPU类型,
从而确定使用的指令集和其他一些CPU参数,如TLB等
prom_init();
prom_init() 函数是和硬件相关的,做一些低层的初始化,接受引导装载程序传给内核的参数,
确定 mips_machgroup,mips_machtype 这两个变量,这两个变量分别对应着相应的芯片组合开发板;
打印 cpu_probe() 函数检测到的 CPU 的 Processor ID。
如果有浮点处理器,也打印浮点处理器的 Processor ID。
cpu_report();
应用程序通过终端接口设备使用特定的接口规程与终端进行交互,与操作系统内核本身交互的终端称为控制台,
它可以是内核本身的内部显示终端,也可以是通过串口连接的外部哑终端。
由于大多数情况下控制台都是内核显示终端,因此内核显示终端也常常直接称为控制台。
内核终端对用户来说具有若干个虚拟终端子设备,它们共享同一物理终端,
但同一时刻只能有一个虚拟终端操作硬件屏幕。
宏 CONFIG_VT 的意思是否支持虚拟终端。
当配置了宏 CONFIG_VGA_CONSOLE 时为内核本身的内部显示终端。
当配置了宏 CONFIG_DUMMY_CONSOLE 时为通过串口连接的外部哑终端。
用变量 conswitchp 来进行指定。
#if defined(CONFIG_VT)
#if defined(CONFIG_VGA_CONSOLE)
conswitchp = &vga_con;
#elif defined(CONFIG_DUMMY_CONSOLE)
conswitchp = &dummy_con;
#endif
#endif
对内存进行初始化。
arch_mem_init(cmdline_p);
这个函数遍历每一个内存空间范围(物理地址),在资源管理器中进行资源申请,
并对内核代码和数据段进行资源申请。
resource_init();
#ifdef CONFIG_SMP
plat_smp_setup();
#endif
}
*****************************************
第一函数:
cpu_probe ( )函数的部分源码如下:
==========================================================
这个 cpu_data[] 数组定义在 arch/mips/kernel/setup.c 文件中。
定义如下所示:
struct cpuinfo_mips cpu_data[NR_CPUS] __read_mostly;
它的类型为 struct cpuinfo_mips 结构体:
struct cpuinfo_mips {
unsigned long udelay_val;
unsigned long asid_cache;
unsigned long options;
unsigned long ases;
unsigned int processor_id;
unsigned int fpu_id;
unsigned int cputype;
int isa_level;
int tlbsize;
struct cache_desc icache; /* Primary I-cache */
struct cache_desc dcache; /* Primary D or combined I/D cache */
struct cache_desc scache; /* Secondary cache */
struct cache_desc tcache; /* Tertiary/split secondary cache */
void *data; /* Additional data */
} __attribute__((aligned(SMP_CACHE_BYTES)));
----------------------------------------
static inline void cpu_probe(void) {
struct cpuinfo_mips *c = ¤t_cpu_data;
----------------------------------------
#define current_cpu_data cpu_data[smp_processor_id()]
smp_processor_id() 函数获得当前 CPU 的 ID 号。
所以 cpuinfo_mips 变量为当前 CPU 的数据结构指针。
下面对这个 CPU 的数据结构进行填充。
----------------------------------------
c->processor_id = read_c0_prid();
----------------------------------------
获取CP0_CONFIG寄存器的值,根据CP0控制寄存器PRID来确定CPU的类型
+------------------+----------------+----------------+----------------+
|Company Options| Company ID | Processor ID | Revision |
+------------------+----------------+----------------+----------------+
31 24 23 16 15 8 7 0
这个寄存器的 [23:16]位表明 CPU 的公司 ID 。
----------------------------------------
switch (c->processor_id & 0xff0000){
根据 PRID 的 [23:16]位来选择 CPU 的公司 ID。
case PRID_COMP_CLXRISC: /* CLXRISC Implementation. */
decode_configs(c);
switch (c->processor_id & 0xff000000) {
case PRID_IMP_CLXRISC:
对 CPU 的功能和特性进行描述。
c->options |= MIPS_CPU_MCHECK;
c->cputype = CPU_CLXRISC;
c->isa_level = MIPS_CPU_ISA_M32R1;
c->tlbsize = 32;
break;
default:
c->cputype = CPU_UNKNOWN;
break;
}
break;
}
----------------------------------------
根据 CPU 的特性进行检测 CPU 是否支持浮点运算单元,取得浮点运算单元的 ID 号。
并检测 CPU 是否支持 3D 图像运算,如果支持表明 CPU 支持。
if (c->options & MIPS_CPU_FPU) {
c->fpu_id = cpu_get_fpu_id();
if (c->isa_level == MIPS_CPU_ISA_M32R1 ||
c->isa_level == MIPS_CPU_ISA_M32R2 ||
c->isa_level == MIPS_CPU_ISA_M64R1 ||
c->isa_level == MIPS_CPU_ISA_M64R2) {
if (c->fpu_id & MIPS_FPIR_3D)
c->ases |= MIPS_ASE_MIPS3D;
}
}
}
从中可以看出,cpu_probe()通过 CPU 的CP0控制寄存器 PRID 来对 CPU 功能和特性进行描述,
这些特性在后面用来决定调用相应的异常处理和内存管理程序。
----------------------------------------
这个函数的 linux 内核移植相关部分:
1)在 include/asm-mips/cpu.h 中添加 CPU 公司的 ID。
2)修改添加 cpu_probe()函数中关于 CPU 公司 ID 的处理。
********************************************
第二函数:
prom_init() 函数的部分源码如下:
----------------------------------------
void __init prom_init(void)
{
unsigned long memsize;
prom_argc = (int) fw_arg0;
prom_argv = (char **) fw_arg1;
prom_envp = (char **) fw_arg2;
这三个 fw_arg0、fw_arg1、fw_arg2 变量的赋值,参考 《linux-mips启动分析(1).txt》,
在 /arch/mips/kernel/head.s 文件中初始化的。
mips_machgroup = MACH_GROUP_CLXRISC;
mips_machtype = 0;
初始化 mips_machgroup,mips_machtype 这两个变量,这两个变量分别对应着相应的芯片组合开发板;
把内核启动参数拷贝到 arcs_cmdline[]字符数组中。
prom_init_cmdline();
memsize = 128;
if (memsize < 0x1000)
memsize *= 1024*1024;
在 结构体变量 boot_mem_map 中赋值指定的内存范围(物理地址)映像图。
add_memory_region(0, memsize, BOOT_MEM_RAM);
add_memory_region(0x28000000, memsize, BOOT_MEM_RAM);
}
----------------------------------------
这个函数的 linux 内核移植相关部分:
这个函数是和具体的硬件相关的,做一些底层的操作,移植 linux 内核时,需要自己手动写整个函数。
----------------------------------------
prom_init() 函数调用 prom_init_cmdline() 函数,
void prom_init_cmdline(void)
{
char *cp;
int actr;
actr = 1; /* Always ignore argv[0] */
变量 arcs_cmdline 为定义的字符数组,默认为字符串 CONFIG_CMDLINE。
cp = &(arcs_cmdline[0]);
while(actr < prom_argc) {
strcpy(cp, prom_argv[actr]);
cp += strlen(prom_argv[actr]);
*cp++ = ' ';
actr++;
}
if (cp != &(arcs_cmdline[0])) /* get rid of trailing space */
--cp;
*cp = '\0';
}
----------------------------------------
如果内核启动参数为
mem=64M console=tty0 console=ttyS0,115200n8 ip=192.168.4.46:::::eth0:off root=/dev/nfs
nfsroot=192.168.4.55:/nfsroot/root-vw/ rw
prom_argv[]数组字符串如下所示。
prom_argv[1] len = 7. prom_argv[1]=mem=64M.
prom_argv[2] len = 12. prom_argv[2]=console=tty0.
prom_argv[3] len = 22. prom_argv[3]=console=ttyS0,115200n8.
prom_argv[4] len = 28. prom_argv[4]=ip=192.168.4.46:::::eth0:off.
prom_argv[5] len = 13. prom_argv[5]=root=/dev/nfs.
prom_argv[6] len = 38. prom_argv[6]=nfsroot=192.168.4.55:/nfsroot/root-vw/.
prom_argv[7] len = 2. prom_argv[7]=rw.
----------------------------------------
add_memory_region()函数在 arch/mips/kernel/setup.c 文件中定义。
void __init add_memory_region(phys_t start, phys_t size, long type)
{
结构体变量 boot_mem_map 在 在 arch/mips/kernel/setup.c 文件中定义。
这个变量保存了命令行或者编译时指定的内存范围(物理地址)映像图。
int x = boot_mem_map.nr_map;
struct boot_mem_map_entry *prev = boot_mem_map.map + x - 1;
如果添加的内存范围和已经有的内存范围有重合,进行合并。
if (x && prev->addr + prev->size == start && prev->type == type) {
prev->size += size;
return;
}
如果已经添加了最大量的内存范围,报错。
if (x == BOOT_MEM_MAP_MAX) {
printk("Ooops! Too many entries in the memory map!\n");
return;
}
对内存范围(物理地址)映像图进行描述赋值。
boot_mem_map.map[x].addr = start;
boot_mem_map.map[x].size = size;
boot_mem_map.map[x].type = type;
boot_mem_map.nr_map++;
}
*****************************************
第三函数:
arch_mem_init () 函数主要对内存进行的初始化,内容比较复杂,
在《linux-mips启动分析(4) 》文件对对内存系统进行比较细致的分析。
*****************************************
第四函数:
resource_init() 函数申请系统资源,具体分析如下:
这个函数遍历每一个内存空间范围(物理地址),在资源管理器中进行资源申请,
并对内核代码和数据段进行资源申请。
参考《linux-mips启动分析(5)》。
********************************************
第五函数:
plat_smp_setup() 函数,如果没有定义 CONFIG_SMP 宏定义,则不执行这个函数。
********************************************
问题:
1)plat_smp_setup() 函数的功能?它执行了什么操作?
linux-mips启动分析(4)
在 setup_arch() 函数中调用了 arch_mem_init() 函数。
参数 cmdline_p 为字符的指针的指针,是从 start_kernel() 函数传给这个 setup_arch() 函数的。
在 setup_arch() 函数没有对它进行处理。
可能为了以后把内核启动参数保存到这个指针指向的字符串中。
******************************************
位于 arch/mips/kernel/setup.c 文件中。
static void __init arch_mem_init(char **cmdline_p)
{
这个函数是平台具体相关的,移植内核需要自己手动编写。
对于开发板的 CPU 和 board 的初始化都是在这个函数中进行的。
plat_mem_setup();
这个函数打印输出内存范围(物理地址)映像图。
printk("Determined physical RAM map:\n");
print_memory_map();
把字符数组 arcs_cmdline[] 拷贝到字符数组 command_line[] 和 boot_command_line[]中。
字符数组 arcs_cmdline[] 在 prom_init_cmdline() 函数中进行的赋值。
为命令行启动参数字符串组合。参考《linux-mips启动分析(3).txt》。
strlcpy(command_line, arcs_cmdline, sizeof(command_line));
strlcpy(boot_command_line, command_line, COMMAND_LINE_SIZE);
把字符数组 command_line[] 的地址赋值给函数的参数。
返回给上级的函数,实际上这个指针返回到了 start_kernel() 函数。
*cmdline_p = command_line;
parse_early_param();
这个 usermem 变量在 early_parse_mem()函数进行修改。
参考下面的分析。
if (usermem) {
printk("User-defined physical RAM map:\n");
print_memory_map();
}
bootmem_init();
sparse_init();
paging_init();
}
*****************************************
第一函数:
plat_mem_setup() 函数的分析:
这个函数是移植相关的,移植内核需要自己手动编写。
对于开发板的 CPU 和 board 的初始化都是在这个函数中进行的。
========================================================
在我们公司的开发板上代码如下:
void __init plat_mem_setup(void)
{
主要是在这个函数中进行的 CPU 和 board 的初始化。
主要包括 SOC 各个功能模块的电源管理和时钟设置;
设置中断优先级;
设置 EMC 外部存储控制器的初始化;
设置 PCI 控制器的初始化,等等。
clx_soc_setup();
这三个变量 _machine_restart, _machine_halt,pm_power_off 是函数指针,
在 arch/mips/kernel/reset.c 文件中定义并使用的。
_machine_restart = clx_restart; 定义自己开发板的重启函数。
_machine_halt = clx_halt; 定义自己开发板的系统立即停止函数。
pm_power_off = clx_power_off; 定义自己开发板的断电停止函数。
这个变量 board_time_init 也是一个函数指针,
在 arch/mips/kernel/time.c 文件中定义并使用。
为系统的 timer 系统初始化函数,在 time_init() 函数被调用。
board_time_init = cq8401_time_init;
设置 mips_io_port_base 这个变量的值。
在 MIPS 体系结构下, I/O 端口是由 memory 来映射的,并没有特殊的地址空间,
所以可以使用普通的 Load/Store 指令来读取。
这个 mips_io_port_base 变量是一个虚拟地址,所有的端口都以这个地址为起始地址映射的。
set_io_port_base(0);
对 PCI 的 IO 和 MEM 资源进行初始化。
资源管理器的根资源 I/O 或者 MEM 分别不同。
I/O 的根资源为 ioport_resource。
MEM 的根资源为 iomem_resource。
ioport_resource.start = 0x00000000;
ioport_resource.end = 0xffffffff;
iomem_resource.start = 0x00000000;
iomem_resource.end = 0xffffffff;
定定义 CONFIG_BLK_DEV_INITRD 决定是否是否使用了 ROMDISK 根文件系统。
如果定义了取得 ROMDISK 根文件系统的起始和结束地址。
#ifdef CONFIG_BLK_DEV_INITRD
extern char __initramfs_start[], __initramfs_end[];
ROOT_DEV = MKDEV(RAMDISK_MAJOR, 0);
initrd_start = (unsigned long)&__initramfs_start;
initrd_end = (unsigned long)&__initramfs_end;
#endif
#ifdef CONFIG_PC_KEYB
kbd_ops = &std_kbd_ops;
#endif
设置 NOR flash MTD 分区和 MTD 设备的注册。
#ifdef CONFIG_MTD
physmap_set_partitions(cq8401_mtd_parts, number_partitions);
#endif
初始化串口。
clx_serial_setup();
设置开发板的板级初始化,移植相关,这个函数需要根据具体的设置进行操作。
clx_board_setup();
}
*****************************************
第二函数:
print_memory_map() 函数的分析:
这个函数打印输出内存范围(物理地址)映像图,这个地址范围在 prom_init()函数
中调用 add_memory_region(),使用结构体变量 boot_mem_map 保存了命令行
或者编译时指定的内存范围(物理地址)映像图。
参考《linux-mips启动分析(3) 》。
=====================================================
打印输出信息:
Determined physical RAM map:
memory: 04000000 @ 00000000 (usable)
********************************************
第三函数:
parse_early_param() 函数分析:
这个函数主要内容还是在 parse_args() 这个函数中完成的。
这个 parse_args() 函数在 kernel/params.c 文件中实现的。
这个 parse_args() 函数是对启动命令行参数进行分析,并根据参数名称把值保存到
结构体 struct kernel_param 类型的数组中。
======================================================
void __init parse_early_param(void)
{
static __initdata char tmp_cmdline[COMMAND_LINE_SIZE];
strlcpy(tmp_cmdline, boot_command_line, COMMAND_LINE_SIZE);
因为没有设置 struct kernel_param 结构体的指针,
所以这个和直接调用 do_early_param(har *param, char *val) 函数是一样的。
parse_args("early options", tmp_cmdline, NULL, 0, do_early_param);
}
这个函数直接把内核启动命令行参数拷贝到临时的命令行参数字符数组中,
再对内核启动命令行参数进行语法上的分析。
主要内容还是在 parse_args() 这个函数中完成的。
********************************************
这个 parse_args() 函数在 kernel/params.c 文件中实现的。
这个 parse_args() 函数有五个参数:
第一个参数:此次操作的名称,好像没有用处,只是打印信息使用。
第二个参数:内核启动命令行参数字符串。
第三个参数:为 struct kernel_param 结构体的变量数组,这个结构体变量用于保存内核启动命令行参数。
第四个参数:为 struct kernel_param 结构体的变量数组的成员个数。
第五个参数:是一个函数指针,当内核启动命令行参数字符串的参数名称
在 struct kernel_param 结构体的变量数组中没有找到对应的名称时,
调用这个函数,当找到时,赋值对应的 struct kernel_param 结构体的变量。
参数为 1) 参数名称; 2)参数值。
----------------------------------------
int parse_args(const char *name,
char *args,
struct kernel_param *params,
unsigned num,
int (*unknown)(char *param, char *val))
{
char *param, *val;
跳过命令行的前导空白符。
while (*args == ' ')
args++;
while (*args) {
int ret;
int irq_was_disabled;
这个 next_arg() 函数是取得命令行中的下一个命令行参数的字符串。
args = next_arg(args, ¶m, &val);
查看中断是否关闭,如果 irqs_disabled 返回 0,中断没有关闭。
irq_was_disabled = irqs_disabled();
对参数进行处理,看下面。
ret = parse_one(param, val, params, num, unknown);
if (irq_was_disabled && !irqs_disabled()) {
printk(打印警告信息);
}
switch (ret) {
错误处理,等等。
}
}
return 0;
}
----------------------------------------
static int parse_one(char *param,
char *val,
struct kernel_param *params,
unsigned num_params,
int (*handle_unknown)(char *param, char *val))
{
unsigned int i;
从 struct kernel_param 结构体的变量数组,这个结构体变量用于保存内核启动命令行参数,
中找到对应的参数名称,并根据 linux 启动命令行的设置来设置这个参数的值。
for (i = 0; i < num_params; i++) {
if (parameq(param, params[i].name)) {
DEBUGP("They are equal! Calling %p\n",params[i].set);
return params[i].set(val, ¶ms[i]);
}
}
如果没有在 struct kernel_param 结构体的变量数组中找到传入的 linux 启动命令行参数,
就调用默认的处理函数。
if (handle_unknown) {
return handle_unknown(param, val);
}
return -ENOENT;
}
----------------------------------------
static int __init do_early_param(char *param, char *val)
{
struct obs_kernel_param *p;
参考《Linux启动参数及实现》和 《启动时内核参数解析》。
变量 __setup_start 和 __setup_end 在连接脚本中进行定义的。
这两个变量决定了 init.setup 段的起始和结束地址。
启动参数(函数指针)被封装到 obs_kernel_param 结构中,
所有的内核启动参数形成内核映像 .init.setup 段中的一
个 obs_kernel_param 数组。
如何在内核启动参数中进行了这个某个参数,则对调用这个参数的支持函数。
for (p = __setup_start; p < __setup_end; p++) {
如果这个命令行参数有 early 处理函数,则调用这个 setup_func() 函数。
if (p->early && strcmp(param, p->str) == 0) {
if (p->setup_func(val) != 0)
printk(KERN_WARNING "Malformed early option '%s'\n", param);
}
}
return 0;
}
********************************************
第四函数:
这个 early_parse_mem() 函数比较特别,并不是在这里进行的调用,
而是在 do_early_param() 函数执行时进行了调用。
因为在内核中定义了 early_param("mem", early_parse_mem)。
当内核启动参数中定义了 mem 的大小时就会调用。
=========================================================
这个函数的意思是当
static int __init early_parse_mem(char *p)
{
unsigned long start, size;
如果用户在内核启动命令行内设置了 mem 的大小,取消以前设置的内存范围(物理地址)映像图。
以内核启动参数为高优先级。
if (usermem == 0) {
boot_mem_map.nr_map = 0;
usermem = 1;
}
默认起始地址为 0。
start = 0;
size = memparse(p, &p);
如果命令行指定了内存的起始地址,从命令行取得内存的起始地址。
if (*p == '@')
start = memparse(p + 1, &p);
调用 add_memory_region(),使用结构体变量 boot_mem_map 保存了命令行指定的内存范围(物理地址)映像图。
add_memory_region(start, size, BOOT_MEM_RAM);
return 0;
}
----------------------------------------
代码运行到这里打印:
User-defined physical RAM map:
memory: 04000000 @ 00000000 (usable)
*****************************************
第五函数:
bootmem_init()函数对引导内存分配器进行初始化。
这个函数太大了,包含的内容也比较多,所以在在《linux-mips启动分析(4-1).txt》文件。
********************************************
第六函数:
sparse_init()函数。
在 linux 内核中可以选择三种内部的 memory 管理模型: FLATMEM、SPARSEMEM 和 DISCONTIGMEM。
一般情况下选择 FLATMEM 内存管理模型肯定是正常的,它支持所有的系统。
如果系统为非一致性的内存管理 NUMA 和支持内存热插拔的,可以选择其他两种。
这种 DISCONTIGMEM 内存管理模型相比于 FLATMEM 内存管理模提供了一些扩展的功能,
在有些系统中,内存有许多的空洞,这个中模型提供了更有效的的处理这些 hole 的方法。
然而,事实上尽管有许多的空洞,这些地址空间仍然是巨大的 flat 地址空间。
所以也可以退化为 FLATMEM 内存管理模型。
在许多的 NUMA 系统中,需要配置这种的内存管理模型。
这中 SPARSEMEM 内存管理模型可以支持内存的热插拔的,如果你不确定,可以不选择这个配置选项。
在我们的系统中,选择了 FLATMEM 内存管理模型。
没有定义这个配置选项 CONFIG_SPARSEMEM 宏定义。
所以这个 sparse_init()函数是一个空函数。
********************************************
第七函数:
paging_init()函数初始化内核空间的页表,建立页表项。
初始化内存节点和各个管理区和 mem_map[] 数组。
参考《linux-mips启动分析(4-2)》。
********************************************
问题:
1)内核都定义了哪些内核启动参数?都有哪些参数调用了参数的初始化?
2)在 IO 和 MEM 的端口操作中, 这个 mips_io_port_base 变量如何使用的?
linux-mips启动分析(4-1)
第五函数:
bootmem_init()函数对引导内存分配器进行初始化。
=========================================================
static void __init bootmem_init(void)
{
unsigned long reserved_end;
unsigned long mapstart = ~0UL;
unsigned long bootmap_size;
int i;
取得内核映像的末尾地址(物理地址),如果没有定义 CONFIG_BLK_DEV_INITRD 这个宏,
这个 init_initrd() 函数为空函数,如果定义了返回文件系统的映像的结束地址。
reserved_end = max(init_initrd(), PFN_UP(__pa_symbol(&_end)));
对变量 min_low_pfn 和 max_low_pfn 进行初始化。
变量 min_low_pfn 表示系统可用的最小的 PFN(page frame num)。
变量 max_low_pfn 表示低端内存可用的最大的 PFN(page frame num)。
min_low_pfn = ~0UL;
max_low_pfn = 0;
下面的循环对每一个内存区域(物理地址)进行检测,
对变量 min_low_pfn 和 max_low_pfn 根据硬件进行赋值。
对变量 mapstart 进行赋值,这个变量还是等于内核映像的结束地址(为什么不直接赋值呢?)。
for (i = 0; i < boot_mem_map.nr_map; i++) {
unsigned long start, end;
if (boot_mem_map.map[i].type != BOOT_MEM_RAM)
continue;
start = PFN_UP(boot_mem_map.map[i].addr);
end = PFN_DOWN(boot_mem_map.map[i].addr
+ boot_mem_map.map[i].size);
if (end > max_low_pfn)
max_low_pfn = end;
if (start < min_low_pfn)
min_low_pfn = start;
if (end <= reserved_end)
continue;
if (start >= mapstart)
continue;
mapstart = max(reserved_end, start);
}
进行检测。
if (min_low_pfn >= max_low_pfn)
panic("Incorrect memory mapping !!!");
if (min_low_pfn > ARCH_PFN_OFFSET) {
printk(KERN_INFO
"Wasting %lu bytes for tracking %lu unused pages\n",
(min_low_pfn - ARCH_PFN_OFFSET) * sizeof(struct page),
min_low_pfn - ARCH_PFN_OFFSET);
} else if (min_low_pfn < ARCH_PFN_OFFSET) {
printk(KERN_INFO
"%lu free pages won't be used\n",
ARCH_PFN_OFFSET - min_low_pfn);
}
使变量 min_low_pfn == 每个体系结构的最低物理地址。
在 MIPS 体系结构下 ARCH_PFN_OFFSET == 0。
min_low_pfn = ARCH_PFN_OFFSET;
如果定义支持高端内存,在这里确定高端内存的范围,这个 HIGHMEM_START 宏定义,决定了
高端内存地址的起始范围,为移植相关,移植 linux 内核时需要注意。
if (max_low_pfn > PFN_DOWN(HIGHMEM_START)) {
#ifdef CONFIG_HIGHMEM
highstart_pfn = PFN_DOWN(HIGHMEM_START);
highend_pfn = max_low_pfn;
#endif
max_low_pfn = PFN_DOWN(HIGHMEM_START);
}
初始化低端内存引导内存分配器。分析见下面。
这个函数使用 mapstart 以后,第一个可用的 PFN 来初始化 min_low_pfn 和 max_low_pfn
之间的内存,并把节点加入 pgdat_list 节点链表。
bootmap_size = init_bootmem_node(NODE_DATA(0), mapstart,
min_low_pfn, max_low_pfn);
把所有低端内存中的有效 RAM 页面在引导内存分配器中进行分配。
for (i = 0; i < boot_mem_map.nr_map; i++)
{
unsigned long start, end, size;
if (boot_mem_map.map[i].type != BOOT_MEM_RAM)
continue;
start = PFN_UP(boot_mem_map.map[i].addr);
end = PFN_DOWN(boot_mem_map.map[i].addr
+ boot_mem_map.map[i].size);
跳过高端内存(如果包含高端内存 max_low_pfn = = PFN_DOWN(HIGHMEM_START))。
if (start >= max_low_pfn)
continue;
对内核映像所占据的 RAM 空间进行保留。
if (start < reserved_end)
start = reserved_end;
跳过高端内存(如果包含高端内存 max_low_pfn = = PFN_DOWN(HIGHMEM_START))。
if (end > max_low_pfn)
end = max_low_pfn;
进行错误检测。
if (end <= start)
continue;
计算得出这段 RAM 空间的 物理页面总数。
size = end - start;
在节点的位图中表目 size 个页面为空闲的。
free_bootmem(PFN_PHYS(start), size << PAGE_SHIFT);
如果没有定义这个 CONFIG_HAVE_MEMORY_PRESENT 宏定义,这个函数为空函数。
这个函数主要???????????????????????????????
?????????????????????????????????????
memory_present(0, start, end);
}
把节点的位图所占用的空间设置为保留(已占用),防止释放掉。
reserve_bootmem(PFN_PHYS(mapstart), bootmap_size);
如果没有定义 CONFIG_BLK_DEV_INITRD 这个宏,这个 init_initrd(已占用) 函数为空函数,
如果定义了设置 INITRD 根文件系统所占用的内存为为保留,防止释放掉。
finalize_initrd();
}
----------------------------------------
这个函数注意点:
1)这个 HIGHMEM_START 宏定义了高端内存的起始地址,为移植相关,移植 linux 内核时需要注意。
*****************************************
第一个函数:
init_bootmem_node() 函数初始化引导内存分配器。
第一个参数:为 NODE_DATA(0),为第 0 个节点的指针,在 UMA 的机器上,
只有一个静态的节点为 contig_page_data 表示。
第二个参数:为 mapstart, 即第一个可以使用的 PFN。
第三个参数:为 min_low_pfn 表示系统可用的最小的 PFN(page frame num)。
第四个参数:为 max_low_pfn 表示低端内存可用的最大的 PFN(page frame num)。
========================================================
第一个参数为 NODE_DATA(0),在 include/linux/mmzone.h 文件中定义。
如果定义了这个 CONFIG_NEED_MULTIPLE_NODES 宏,必须使用自己定义的。
默认为系统定义静态内存节点 contig_page_data。
#define NODE_DATA(nid) (&contig_page_data)
这个静态内存节点 contig_page_data,在 mm/page_alloc.c 中进行的定义。
static bootmem_data_t contig_bootmem_data;
struct pglist_data contig_page_data = { .bdata = &contig_bootmem_data };
----------------------------------------
typedef struct bootmem_data {
unsigned long node_boot_start;
unsigned long node_low_pfn;
void *node_bootmem_map;
unsigned long last_offset;
unsigned long last_pos;
unsigned long last_success;
struct list_head list;
} bootmem_data_t;
这个 bootmem_data_t 类型用于引导内存分配器分配内存时使用。
这个 node_bootmem_map 成员是物理内存的映射位图,表明了这个节点的内存分配情况,
包含内存节点的 holes 的情况。
这个位图为 1 表示这个页面未分配,为 0 ,表示这个页面已经分配或者不能够使用。
这个 node_boot_start 成员变量表示这个节点的起始物理地址。
这个 node_low_pfn 成员变量表示这个节点的结束 PFN 。
----------------------------------------
unsigned long __init init_bootmem_node(pg_data_t *pgdat, unsigned long freepfn,
unsigned long startpfn, unsigned long endpfn)
{
return init_bootmem_core(pgdat, freepfn, startpfn, endpfn);
}
--------------------------------------
static unsigned long __init init_bootmem_core(pg_data_t *pgdat,
unsigned long mapstart, unsigned long start, unsigned long end)
{
取得内存映射结构体。这个结构体用于引导内存分配器分配内存时使用。
bootmem_data_t *bdata = pgdat->bdata;
unsigned long mapsize;
这个 node_bootmem_map 成员变量包含已分配和空闲页面的位图的地址,
在这里设置为 mapstart 地址,即内核结束后的第一个 页面的起始地址。
bdata->node_bootmem_map = phys_to_virt(PFN_PHYS(mapstart));
这个 node_boot_start 成员变量表示这个节点的起始物理地址。
bdata->node_boot_start = PFN_PHYS(start);
这个 node_low_pfn 成员变量表示这个节点的结束 PFN 。
bdata->node_low_pfn = end;
把这个节点加入 pgdat_list 节点链表。
link_bootmem(bdata);
计算得出位图的大小,以一位代表一个页面。
mapsize = get_mapsize(bdata);
设置保留所有的页面,为 1 ,在 setup_arch()函数中再详细的进行表明有效的 RAM 。
memset(bdata->node_bootmem_map, 0xff, mapsize);
return mapsize;
}
****************************************
第六函数:
sparse_init()函数。
在 linux 内核中可以选择三种内部的 memory 管理模型: FLATMEM、SPARSEMEM 和 DISCONTIGMEM。
一般情况下选择 FLATMEM 内存管理模型肯定是正常的,它支持所有的系统。
如果系统为非一致性的内存管理 NUMA 和支持内存热插拔的,可以选择其他两种。
这种 DISCONTIGMEM 内存管理模型相比于 FLATMEM 内存管理模提供了一些扩展的功能,
在有些系统中,内存有许多的空洞,这个中模型提供了更有效的的处理这些 hole 的方法。
然而,事实上尽管有许多的空洞,这些地址空间仍然是巨大的 flat 地址空间。
所以也可以退化为 FLATMEM 内存管理模型。
在许多的 NUMA 系统中,需要配置这种的内存管理模型。
这中 SPARSEMEM 内存管理模型可以支持内存的热插拔的,如果你不确定,可以不选择这个配置选项。
在我们的系统中,选择了 FLATMEM 内存管理模型。
没有定义这个配置选项 CONFIG_SPARSEMEM 宏定义。
所以这个 sparse_init()函数是一个空函数。
****************************************
问题:
1)这个 memory_present()函数的作用?
2)三种内存管理模型 FLATMEM、SPARSEMEM 和 DISCONTIGMEM 的具体区别?
linux-mips启动分析(4-2)
第七函数:
paging_init()函数进行内核页表初始化。
这个函数在 arch/mips/mm/init.c 文件中定义实现的。
=====================================================
void __init paging_init(void)
{
unsigned long zones_size[MAX_NR_ZONES] = { 0, };
因为定义这个 CONFIG_FLATMEM 宏,所以不定义下面的变量。
#ifndef CONFIG_FLATMEM
unsigned long zholes_size[MAX_NR_ZONES] = { 0, };
unsigned long i, j, pfn;
#endif
----------------------------------------
这个函数利用 swapper_pg_dir 设立一个静态页表作为 PGD。
pagetable_init();
----------------------------------------
#ifdef CONFIG_HIGHMEM
这个函数取得高端内存首地址对应的 PTE 页表的起始页表项,并进行保护。
kmap_init();
#endif
----------------------------------------
如果编译时没有设置 CONFIG_MIPS_MT_SMTC 宏定义,下面这个函数为空函数。
kmap_coherent_init();
----------------------------------------
在内存中每个节点分为 三个管理区,
ZONE_DMA 表示低端范围的内存, ISA 设备要用到它。
ZONE_NORMAL 由内核直接映射的线性地址空间。
ZONE_HIGHMEM 内核不能够直接映射的地址空间。
现在 ISA 设备很少,一般不定义 CONFIG_ZONE_DMA 这个宏。
下面的代码设置内存节点的各个管理区的大小。
#ifdef CONFIG_ZONE_DMA
if (min_low_pfn < MAX_DMA_PFN && MAX_DMA_PFN <= max_low_pfn) {
zones_size[ZONE_DMA] = MAX_DMA_PFN - min_low_pfn;
zones_size[ZONE_NORMAL] = max_low_pfn - MAX_DMA_PFN;
} else if (max_low_pfn < MAX_DMA_PFN)
zones_size[ZONE_DMA] = max_low_pfn - min_low_pfn;
else
#endif
zones_size[ZONE_NORMAL] = max_low_pfn - min_low_pfn;
----------------------------------------
#ifdef CONFIG_HIGHMEM
设置高端内存管理区的大小。
zones_size[ZONE_HIGHMEM] = highend_pfn - highstart_pfn;
这个 cpu_has_dc_aliases 变量是一个宏定义,这个宏定义了数据高速缓存
数据重影的问题,参考《MIPS-cache重影问题的产生》。
如果 CPU 有数据高速缓存的数据重影问题,则不支持高端缓存。
if (cpu_has_dc_aliases && zones_size[ZONE_HIGHMEM]) {
printk("This processor doesn't support highmem." );
zones_size[ZONE_HIGHMEM] = 0;
}
#endif
----------------------------------------
下面代码初始化各个管理区,建立各个管理区的内核页表。
对于 UMA 系统,调用 free_area_init() 函数,
对于 NUMA 系统,调用 free_area_init_node() 函数。
#ifdef CONFIG_FLATMEM
初始化各个管理区和 mem_map[] 数组。
free_area_init(zones_size);
#else
当为非 FLATMEM 类型内存管理模型时,执行。
pfn = min_low_pfn;
统计内存地址空间中的漏洞。
for (i = 0; i < MAX_NR_ZONES; i++)
for (j = 0; j < zones_size[i]; j++, pfn++)
if (!page_is_ram(pfn))
zholes_size[i]++;
free_area_init_node(0, NODE_DATA(0), zones_size, 0, zholes_size);
#endif
}
----------------------------------------
这个 free_area_init() 函数和 free_area_init_node() 函数其实基本相同,
都是通过调用 free_area_init_core() 函数来完成的。
********************************************
这个函数 pagetable_init() 利用 swapper_pg_dir 设立一个静态页表作为 PGD。
这个函数建立内核空间的页表。
void __init pagetable_init(void)
{
unsigned long vaddr;
pgd_t *pgd_base;
#ifdef CONFIG_HIGHMEM
pgd_t *pgd;
pud_t *pud;
pmd_t *pmd;
pte_t *pte;
#endif
----------------------------------------
初始化 pgd_t 类型的数组 swapper_pg_dir,这个数组中每一项指向
一个页面,这个页面包含一个 pte_t 类型的书作。
这个函数初始化,这个 pgd_t 类型的数组 swapper_pg_dir 数组中
每一项指向一个无效的 pte_t 类型的数组。
pgd_init((unsigned long)swapper_pg_dir);
再在刚才的基础上初始化 2 G 空间的 pgd_t 项。
pgd_init((unsigned long)swapper_pg_dir + sizeof(pgd_t) * USER_PTRS_PER_PGD);
上面对内核空间的 4G 页表初始化为无效的。
----------------------------------------
pgd_base = swapper_pg_dir;
固定虚拟地址映射区域,固定映射的线性地址可以映射任何物理地址,比指针更有效。
固定虚拟地址空间被认为从 FIXADDR_TOP 开始,并在虚拟地址空间前面结束。
这个 __fix_to_virt() 将一个下标作为参数。
__end_of_fixed_addresses 是上一个固定虚拟地址空间用到的下标。
这一行返回固定虚拟地址空间起始地址的 PMD 虚拟地址。
vaddr = __fix_to_virt(__end_of_fixed_addresses - 1) & PMD_MASK;
建立从 vaddr 地址对应的 PGD 表项开始的,到 PTRS_PER_PGD 结束的页目录项。
如果 PGD 表项无效,建立有效的 PGD 表项,即建立二级页表。
fixrange_init(vaddr, 0, pgd_base);
----------------------------------------
#ifdef CONFIG_HIGHMEM
这个 PKMAP_BASE 宏定义了 kmap 地址空间的大小。
vaddr = PKMAP_BASE;
建立从 vaddr 地址对应的 PGD 表项开始的,到 kmap 地址空间结束的页目录项。
如果 PGD 表项无效,建立有效的 PGD 表项,即建立二级页表。
fixrange_init(vaddr, vaddr + PAGE_SIZE*LAST_PKMAP, pgd_base);
pgd = swapper_pg_dir + __pgd_offset(vaddr);
pud = pud_offset(pgd, vaddr);
pmd = pmd_offset(pud, vaddr);
pte = pte_offset_kernel(pmd, vaddr);
最后取得 kmap 空间的二级页表基地址。
以后可能要用到。
pkmap_page_table = pte;
#endif
}
CONFIG_MIPS_MT_SMTC
********************************************
void pgd_init(unsigned long page)
{
unsigned long *p = (unsigned long *) page;
int i;
这个 USER_PTRS_PER_PGD 定义在 include/asm-mips/pgtable-32.h 文件中,
#define USER_PTRS_PER_PGD (0x80000000UL/PGDIR_SIZE)
为 2G 空间需要占用多少 pgd_t 数组项。
对 2G 空间占用的 pgd_t 数组项进行配置为无效的 pte_t 数组地址。
for (i = 0; i < USER_PTRS_PER_PGD; i+=8) {
p[i + 0] = (unsigneswapper_pg_dird long) invalid_pte_table;
p[i + 1] = (unsigned long) invalid_pte_table;
p[i + 2] = (unsigned long) invalid_pte_table;
p[i + 3] = (unsigned long) invalid_pte_table;
p[i + 4] = (unsigned long) invalid_pte_table;
p[i + 5] = (unsigned long) invalid_pte_table;
p[i + 6] = (unsigned long) invalid_pte_table;
p[i + 7] = (unsigned long) invalid_pte_table;
}
}
*******************************************
这个函数只有定义了这个 CONFIG_HIGHMEM 宏或者这个 CONFIG_MIPS_MT_SMTC 宏
才有效,否则为空函数。
void __init fixrange_init(unsigned long start, unsigned long end,
pgd_t *pgd_base)
{
#if defined(CONFIG_HIGHMEM) || defined(CONFIG_MIPS_MT_SMTC)
pgd_t *pgd;
pud_t *pud;
pmd_t *pmd;
pte_t *pte;
int i, j, k;
unsigned long vaddr;
vaddr = start;
取得对应于 start 的 PGD 内部索引。
i = __pgd_offset(vaddr);
取得对应于 start 的 PUD 内部索引。
j = __pud_offset(vaddr);
取得对应于 start 的 PMD 内部索引。
k = __pmd_offset(vaddr);
pgd = pgd_base + i;
在这种模式下,
PTRS_PER_PGD = 1024
PTRS_PER_PUD = 1
PTRS_PER_PMD = 1,
即只有两层页表,没有中间层。
所以可以直接使 pud, pmd 指向 PGD 页表,而不用检测 PUD 和 PMD 页表项是否
有具体的页面。而是通过指向 pgd 表项的指针,检测 PGD 表项是否有效。
这段代码的意思是检测 从 start 地址开始的 PGD 表项是否有效有效,如果无效,
而给这个表项分配页面。建立第二级页表。
一直循环到达 end ,如果 end== 0,将继续循环直到 PGD 的末端。
for ( ; (i < PTRS_PER_PGD) && (vaddr != end); pgd++, i++) {
强制把 pgd 的地址付给 pud,即使 pud 这个指针指向 pgd 数组中的某一项。
pud = (pud_t *)pgd;
for ( ; (j < PTRS_PER_PUD) && (vaddr != end); pud++, j++) {
强制把 pud 的地址付给 pmd,即使 pmd 这个指针指向 pud 数组中的某一项。
实际还是指向 pgd 数组中的某一项。
pmd = (pmd_t *)pud;
for (; (k < PTRS_PER_PMD) && (vaddr != end); pmd++, k++) {
如果 pmd 指针指向的页表项,为无效项。
if (pmd_none(*pmd)) {
为 PGD 分配一个页面。
pte = (pte_t *) alloc_bootmem_low_pages(PAGE_SIZE);
设置 PGD 指针指向这个页面。
set_pmd(pmd, __pmd((unsigned long)pte));
if (pte != pte_offset_kernel(pmd, 0))
BUG();
}
vaddr += PMD_SIZE;
}
k = 0;
}
j = 0;
}
#endif
}
******************************************
在 pagetable_init() 函数中对 kmap 地址空间建立的二级页表,
并使变量 pkmap_page_table 等于 kmap 地址空间二级页表的基地址。
这个 kmap_init() 函数,仅在编译时配置了 CONFIG_HIGHMEM 的情况下使用,
它负责获取 kmap 地址空间的首地址,引用 kmap 地址空间的 PTE 页表(二级页表),
并对它进行保护。
这意味着使用 kmap() 函数映射高端内存时可以,不需要检查 PGD 页目录表。
----------------------------------------
pte_t *kmap_pte;
pgprot_t kmap_prot;
static void __init kmap_init(void)
{
unsigned long kmap_vstart;
取得 kmap 地址空间的首地址。
kmap_vstart = __fix_to_virt(FIX_KMAP_BEGIN);
遍历页表取得 kmap 地址空间的首地址对应的 PTE 页表的首个页表项。
kmap_pte = kmap_get_fixmap_pte(kmap_vstart);
设置 页表项的保护项。
kmap_prot = PAGE_KERNEL;
}
*****************************************
问题:
1)MIPS-cache重影问题的产生和解决办法?
2)这个 cpu_has_dc_aliases 变量(实际为宏定义)的使用,它的大小的确定?
linux-mips启动分析(4-3)
这个文件主要讲述管理区的初始化。
初始化各个管理区,建立各个管理区的内核页表。
对于 UMA 系统,调用 free_area_init() 函数,
对于 NUMA 系统,调用 free_area_init_node() 函数。
这个 free_area_init() 函数和 free_area_init_node() 函数其实基本相同,
也是通过调用 free_area_init_node() 函数来完成的。就是传入的参数不同。
free_area_init_node()函数各个参数的意思:
第一个参数:将要初始化的节点标识。
第二个参数:节点 struct pglist_data 结构的指针。
第三个参数:各个管理区大小的数组指针。
第四个参数:将要初始化的节点的可是 PFN(物理页面震号)。
第五个参数:将要初始化的节点内存空间的漏洞。
*****************************************
|--------------------|
| |
| | KSEG2 段,这段地址通过 MMU 来转换成物理地址。为1G空间。
| |
|--------------------|
| | KSEG1 段,这段地址映射到物理地址空间的 512MB 的低内存段,
|--------------------| 为不经过 cache 存取的。
| | KSEG1 段,这段地址映射到物理地址空间的 512MB 的低内存段,
|--------------------| 经过 cache 存取的。
| |
| |
| | KUSEG 段,这段地址空间为用户地址空间。
| | 它的操作是有特定的 CPU 来定义的。
| |
| |
| |
|--------------------|
void __init free_area_init(unsigned long *zones_size)
{
对于 MIPS 的 UMA 系统来说,只有一个静态的节点 contig_page_data ,
这个宏 PAGE_OFFSET 定义了,内核空间和用户空间的分界,
在 MIPS32 的体系结构中,我们可以通过基本的地址空间看出,
PAGE_OFFSET 等于 0x80000000。
内核用户空间可以从这里看出为 从 0x80000000 到 0xFFFFFFFF 的 2G 空间。
free_area_init_node(0, NODE_DATA(0), zones_size,
__pa(PAGE_OFFSET) >> PAGE_SHIFT, NULL);
}
----------------------------------------
void __meminit free_area_init_node(int nid, struct pglist_data *pgdat,
unsigned long *zones_size, unsigned long node_start_pfn,
unsigned long *zholes_size)
{
pgdat->node_id = nid;
记录节点的起始物理祯号(PFN)。
pgdat->node_start_pfn = node_start_pfn;
这个函数,取得这个节点各个管理区所包含的实际页面数,和内存空间漏洞的页面数。
calculate_node_totalpages(pgdat, zones_size, zholes_size);
为全局的 mem_map[] 数组分配空间。
alloc_node_mem_map(pgdat);
free_area_init_core(pgdat, zones_size, zholes_size);
}
********************************************
这个 calculate_node_totalpages() 函数主要工作是通过两个函数完成的,
zone_spanned_pages_in_node() 函数和 zone_absent_pages_in_node()函数
这个 zone_spanned_pages_in_node() 函数返回节点一个管理区的跨度所包含的页面数,
包含内存空间的漏洞。而要取得这个节点管理区所包含的实际页面数,需要减去这个管理区内存空间的漏洞
所包含的页面数。
这个 zone_absent_pages_in_node()函数返回节点一个管理区所包含的内存空间漏洞页面数。
在这个 calculate_node_totalpages() 函数内取得了这个节点各个管理区的跨度所包含的页面数,
并赋值给这个节点结构体 struct pglist_data 的 node_spanned_pages 成员。
把这个管理区所包含的实际页面数赋值给这个节点结构体 struct pglist_data 的 node_present_pages 成员。
static void __meminit calculate_node_totalpages(struct pglist_data *pgdat,
unsigned long *zones_size, unsigned long *zholes_size)
{
unsigned long realtotalpages, totalpages = 0;
enum zone_type i;
for (i = 0; i < MAX_NR_ZONES; i++)
totalpages += zone_spanned_pages_in_node(pgdat->node_id, i, zones_size);
pgdat->node_spanned_pages = totalpages;
realtotalpages = totalpages;
for (i = 0; i < MAX_NR_ZONES; i++)
realtotalpages -= zone_absent_pages_in_node(pgdat->node_id, i, zholes_size);
pgdat->node_present_pages = realtotalpages;
}
*****************************************
static void __init_refok alloc_node_mem_map(struct pglist_data *pgdat)
{
跳过空的内存节点。
if (!pgdat->node_spanned_pages)
return;
#ifdef CONFIG_FLAT_NODE_MEM_MAP
如果这个节点的 mem_map 数组不存在,
if (!pgdat->node_mem_map) {
unsigned long size, start, end;
struct page *map;
节点结构体 struct pglist_data 的成员 node_start_pfn 表示节点的物理地址起始页面祯号(PFN)。
这表示 start 的地址必须对齐 (PAGE_SIZE * MAX_ORDER_NR_PAGES) ,为什么?
取得开始这个节点的物理地址起始页面祯号(PFN)。
start = pgdat->node_start_pfn & ~(MAX_ORDER_NR_PAGES - 1);
取得开始这个节点的物理地址结束页面祯号(PFN)。
这个 pgdat->node_spanned_pages 成员在 calculate_node_totalpages() 函数赋值。
为包含内存漏洞的总的页面数目。
end = pgdat->node_start_pfn + pgdat->node_spanned_pages;
end = ALIGN(end, MAX_ORDER_NR_PAGES);
取得这个节点的 mem_map[] 数组的大小,即内存页面的数目乘以页面结构体的大小。
size = (end - start) * sizeof(struct page);
这个 alloc_remap() 函数,为空函数,只返回 NULL 指针。
map = alloc_remap(pgdat->node_id, size);
if (!map)
从这个节点上为 mem_map[] 数组分配 size 数量的字节,这个分配将对齐 CPU 的一级高速缓存。
为从 静态的节点 contig_page_data 节点的第一物理页面开始分配。
map = alloc_bootmem_node(pgdat, size);
pgdat->node_mem_map = map + (pgdat->node_start_pfn - start);
}
----------------------------------------
#ifndef CONFIG_NEED_MULTIPLE_NODES
if (pgdat == NODE_DATA(0)) {
mem_map = NODE_DATA(0)->node_mem_map;
#ifdef CONFIG_ARCH_POPULATES_NODE_MAP
if (page_to_pfn(mem_map) != pgdat->node_start_pfn)
mem_map -= pgdat->node_start_pfn;
#endif /* CONFIG_ARCH_POPULATES_NODE_MAP */
}
#endif
----------------------------------------
这段代码主要是为全局变量 struct page *mem_map 指针赋值。
----------------------------------------
#endif /* CONFIG_FLAT_NODE_MEM_MAP */
}
****************************************
这个 free_area_init_core() 函数为节点的核心初始化函数。
这个函数初始化全局的 mem_map[] 数组。
----------------------------------------
在 mm/page_alloc.c 文件头部定义了三个全局变量。
unsigned long __meminitdata nr_kernel_pages;
unsigned long __meminitdata nr_all_pages;
static unsigned long __meminitdata dma_reserve;
----------------------------------------
static void __meminit free_area_init_core(struct pglist_data *pgdat,
unsigned long *zones_size, unsigned long *zholes_size)
{
enum zone_type j;
int nid = pgdat->node_id;
unsigned long zone_start_pfn = pgdat->node_start_pfn;
int ret;
如果没有定义这个 CONFIG_MEMORY_HOTPLUG 宏定义,即不支持内存的热插拔。
这个 pgdat_resize_init() 函数为空函数。
pgdat_resize_init(pgdat);
默认该节点的管理区数据为 0 ,即没有管理区。
pgdat->nr_zones = 0;
初始化该节点的页面 swap 等待列表,在内核中,,每个节点都有一个 kswapdN 的交换线程,
每个节点都有自己的等待队列。
init_waitqueue_head(&pgdat->kswapd_wait);
pgdat->kswapd_max_order = 0;
for (j = 0; j < MAX_NR_ZONES; j++) {
struct zone *zone = pgdat->node_zones + j;
unsigned long size, realsize, memmap_pages;
这个 zone_spanned_pages_in_node() 函数返回节点一个管理区的跨度所包含的页面数,
包含内存空间的漏洞。而要取得这个节点管理区所包含的实际页面数,需要减去这个管理区内存空间的漏洞
所包含的页面数。
size = zone_spanned_pages_in_node(nid, j, zones_size);
这个 zone_absent_pages_in_node()函数返回节点一个管理区所包含的内存空间漏洞页面数。
realsize = size - zone_absent_pages_in_node(nid, j, zholes_size);
减去这个管理区的 memmap[] 数组,占用的空间,取得实际可用的内存空间大小。
memmap_pages = (size * sizeof(struct page)) >> PAGE_SHIFT;
if (realsize >= memmap_pages) {
realsize -= memmap_pages;
} else
printk(KERN_WARNING “打印错误提示。”);
当需要保留 DMA 空间时,保留 DMA 空间。
if (j == 0 && realsize > dma_reserve) {
realsize -= dma_reserve;
printk(KERN_DEBUG " 打印提示信息");
}
对于非高端内存累加计算到 nr_kernel_pages 变量。
对于所有内存累加计算到 nr_all_pages 变量。
if (!is_highmem_idx(j))
nr_kernel_pages += realsize;
nr_all_pages += realsize;
--------------------------------------
初始化各个管理区的结构体。
spanned_pages :这个管理区包含内存漏洞的,总得页面数。
present_pages :这个管理区不包含内存漏洞的,页面数目。
node :管理区所属的节点号。
zone_pgdat : 指向节点的指针。
lock : 并行访问该管理区的自旋锁。
lru_lock :管理区的 LRU 链表的自旋锁。
active_list : 管理区的活动链表。每个管理区一个。
inactive_list : 管理区的非活动链表。每个管理区一个。
nr_scan_active : active_list 链表上上的数目。
nr_scan_inactive : inactive_list 链表上上的数目。
vm_stat :
reclaim_in_progress :
wait_table_hash_nr_entries: 等待队列的个数。
wait_table_bits : 等待队列哈希表的大小。
wait_table : 等待队列哈希表。
zone_start_pfn : 管理区的起始物理内存页面祯号。
--------------------------------------
zone->spanned_pages = size;
zone->present_pages = realsize;
#ifdef CONFIG_NUMA
zone->node = nid;
zone->min_unmapped_pages = (realsize*sysctl_min_unmapped_ratio)
/ 100;
zone->min_slab_pages = (realsize * sysctl_min_slab_ratio) / 100;
#endif
zone->name = zone_names[j];
spin_lock_init(&zone->lock);
spin_lock_init(&zone->lru_lock);
zone_seqlock_init(zone);
zone->zone_pgdat = pgdat;
zone->prev_priority = DEF_PRIORITY;
zone_pcp_init(zone);
INIT_LIST_HEAD(&zone->active_list);
INIT_LIST_HEAD(&zone->inactive_list);
zone->nr_scan_active = 0;
zone->nr_scan_inactive = 0;
对 vm_stat 成员清 0。
zap_zone_vm_stats(zone);
atomic_set(&zone->reclaim_in_progress, 0);
if (!size)
continue;
ret = init_currently_empty_zone(zone, zone_start_pfn, size, MEMMAP_EARLY);
BUG_ON(ret);
zone_start_pfn += size;
----------------------------------------
} 这个是 for 循环的结束。
}
*****************************************
__meminit int init_currently_empty_zone(struct zone *zone,
unsigned long zone_start_pfn,
unsigned long size,
enum memmap_context context)
{
struct pglist_data *pgdat = zone->zone_pgdat;
int ret;
初始化管理区的等待队列哈希表和等待队列头。
ret = zone_wait_table_init(zone, size);
if (ret)
return ret;
pgdat->nr_zones = zone_idx(zone) + 1;
赋值该管理区的起始物理内存页面祯号。
zone->zone_start_pfn = zone_start_pfn;
这个 memmap_init() 函数初始化 mem_map[] 数值。
memmap_init(size, pgdat->node_id, zone_idx(zone), zone_start_pfn);
初始化管理区的空闲块链表。
zone_init_free_lists(pgdat, zone, zone->spanned_pages);
return 0;
}
****************************************
这个 zone_wait_table_init() 函数初始化等待队列哈希表和等待队列头。
----------------------------------------
static noinline __init_refok
int zone_wait_table_init(struct zone *zone, unsigned long zone_size_pages)
{
int i;
struct pglist_data *pgdat = zone->zone_pgdat;
size_t alloc_size;
计算有几个等待队列,最多有 4096 个等待队列。
zone->wait_table_hash_nr_entries = wait_table_hash_nr_entries(zone_size_pages);
计算等待队列哈希表的大小。
zone->wait_table_bits = wait_table_bits(zone->wait_table_hash_nr_entries);
alloc_size = zone->wait_table_hash_nr_entries * sizeof(wait_queue_head_t);
分配哈希表空间。
if (system_state == SYSTEM_BOOTING) {
zone->wait_table = (wait_queue_head_t *) alloc_bootmem_node(pgdat, alloc_size);
} else {
zone->wait_table = (wait_queue_head_t *)vmalloc(alloc_size);
}
if (!zone->wait_table)
return -ENOMEM;
初始化各个等待队列。
for(i = 0; i < zone->wait_table_hash_nr_entries; ++i)
init_waitqueue_head(zone->wait_table + i);
return 0;
}
********************************************
这个 memmap_init() 函数初始化 mem_map[] 数值。
设置这个管理区页面为空闲状态,引用计数为 1。初始化 LRU 链表头。
并设置 page->flags 标志,标记页面属于那个节点,那个管理区,那个 section。
#ifndef __HAVE_ARCH_MEMMAP_INIT
#define memmap_init(size, nid, zone, start_pfn) \
memmap_init_zone((size), (nid), (zone), (start_pfn), MEMMAP_EARLY)
#endif
移植相关:如果没有定义这个 __HAVE_ARCH_MEMMAP_INIT 宏,则使用公共的 memmap_init() 函数。
一般是否定义这个宏,由特定的体系结构决定。
----------------------------------------
void __meminit memmap_init_zone(unsigned long size, int nid, unsigned long zone,
unsigned long start_pfn, enum memmap_context context)
{
struct page *page;
unsigned long end_pfn = start_pfn + size;
unsigned long pfn;
for (pfn = start_pfn; pfn < end_pfn; pfn++) {
if (context == MEMMAP_EARLY) {
如果没有定义 CONFIG_SPARSEMEM 这个宏,这个 early_pfn_valid() 、
和 early_pfn_in_nid() 函数一直返回 TRUE。
if (!early_pfn_valid(pfn))
continue;
if (!early_pfn_in_nid(pfn, nid))
continue;
}
取得 mem_map[] 数组中的 struct page 结构体指针。
page = pfn_to_page(pfn);
设置 page->flags 标志,标记页面属于那个节点,那个管理区,那个 section。
set_page_links(page, zone, nid, pfn);
设置页面被应用的数目,初始化为 1,当为 0 时,页面被释放。
init_page_count(page);
设置 _mapcount 成员为 -1。
reset_page_mapcount(page);
设置这个页面为空闲状态。
SetPageReserved(page);
初始化 LRU 链表的头。
INIT_LIST_HEAD(&page->lru);
#ifdef WANT_PAGE_VIRTUAL
这个 set_page_address() 和 page_address_init() 函数一样,参考《linux-mips启动分析(2)》。
当定义了 CONFIG_HIGHMEM 宏,并且没有定义 WANT_PAGE_VIRTUAL 宏时,非空函数。
其他情况为空函数。
if (!is_highmem_idx(zone))
set_page_address(page, __va(pfn << PAGE_SHIFT));
#endif
}
}
--------------------------------------
static inline void set_page_links(struct page *page, enum zone_type zone,
unsigned long node, unsigned long pfn)
{
set_page_zone(page, zone);
set_page_node(page, node);
set_page_section(page, pfn_to_section_nr(pfn));
}
***************************************
这个函数是初始化管理区的空闲块链表。
每个管理区有一个 struct free_area free_area[MAX_ORDER],保存着 zone 中的空闲块。
struct free_area {
struct list_head free_list;
unsigned long nr_free;
};
----------------------------------------
void zone_init_free_lists(struct pglist_data *pgdat, struct zone *zone,
unsigned long size)
{
int order;
for (order = 0; order < MAX_ORDER ; order++) {
INIT_LIST_HEAD(&zone->free_area[order].free_list);
zone->free_area[order].nr_free = 0;
}
}
*****************************************
问题:
1)在这个 alloc_node_mem_map() 函数中,分配 mem_map 数组时,为什么 start 要
和 (PAGE_SIZE * MAX_ORDER_NR_PAGES) 地址必须对齐 ?
2) MAX_ORDER_NR_PAGES 宏定义的意义和大小的确定?
3)有 alloc_bootmem_node()函数返回的地址为虚拟地址,为什么直接赋值给 pgdat->node_mem_map ?
4)为什么要加上 (pgdat->node_start_pfn - start),在赋值给 pgdat->node_mem_map ?
********************************************
参考:
mem_map:一个Struct page数组,对应系统中所有的物理内存页。
而每一个zone结构里都有一个zone_mem_map 域指向这个zone的第一个page 在mem_map的位置,
还有一个域size代表这个区的大小,即总共有多少页。
每一个zone都有自己的buddy system,由下面的zone结构就可以看出。
空闲块是根据其大小做的保存,特别强调的是struct free_area free_area[MAX_ORDER];
保存着zone中的空闲块。数组中的每一个元素都有个双链表结构。比如说 free_area中第 K 个元素
保存着大小为 2 的k次方大小的块的链表结构。数组中保存的是表头结构,即指向第一个2的k次方大小块的
第一个页面。那块的剩余的页面怎么办?不用管,因为都是按块来操作的,只需要知道块的第一个页面即可,
最后一个页面就是第一个页面加上2的k次方。同属于一个链表的块与块之间由每一个块的第一个
页面 的 struct page 中的 list_head lru 来相互链接。
#ifndef CONFIG_FORCE_MAX_ZONEORDER
#define MAX_ORDER 11
#else
#define MAX_ORDER CONFIG_FORCE_MAX_ZONEORDER
#endif
#define MAX_ORDER_NR_PAGES (1 << (MAX_ORDER - 1))
struct free_area {
struct list_head free_list;
unsigned long nr_free;
};
************************************
linux-mips启动分析(4-4)
这个文件主要讲述引导内存分配器的内存分配函数。
主要 API 函数:
alloc_bootmem_node()
alloc_bootmem_pages_node()
alloc_bootmem_low_pages_node()
*****************************************
总结:
在第一次分配 mem_map[] 数组时, 参数为静态节点 contig_page_data ,
分配空间大小为 size ,对齐方式为 SMP_CACHE_BYTES。
从 ZONE_NORMAL 处的第一页开始分配。
*****************************************
这个 alloc_bootmem_node() 函数为引导内存分配器的内存分配 API。
这个 __alloc_bootmem_core() 函数为引导内存分配器的核心内存分配函数。
这个 alloc_bootmem_node() 函数在指定的节点上开始分配内存,从 ZONE_NORMAL 处的
第一页开始分配 size 数量的字节,这个分配将对齐 CPU 的一级高速缓存,充分利用硬件的高速缓存。
----------------------------------------
#define alloc_bootmem_node(pgdat, x) \
__alloc_bootmem_node(pgdat, x, SMP_CACHE_BYTES, __pa(MAX_DMA_ADDRESS))
这个宏 SMP_CACHE_BYTES 定义决定了分配内存的对齐字节,在这里对齐 CPU 的一级高速缓存。
这个 MAX_DMA_ADDRESS 宏定义决定了从什么地方开始分配内存,在这里
从 ZONE_NORMAL 处的第一页开始分配。
********************************************
void * __init __alloc_bootmem_node(pg_data_t *pgdat, unsigned long size,
unsigned long align, unsigned long goal)
{
void *ptr;
ptr = __alloc_bootmem_core(pgdat->bdata, size, align, goal, 0);
if (ptr)
return ptr;
return __alloc_bootmem(size, align, goal);
}
*****************************************
这个 __alloc_bootmem_core() 函数为引导内存分配器的核心内存分配函数分析,这个函数是不可重载的。
参数如下:
1)要分配结构体的启动内存。
2)请求分配的大小。
3)分配内存的对齐方式,它必须是 2 的冥数。
4)分配内存的开始地址。
5)分配内存的最大的结束地址,如果 == 0,可以任意,如果不为 0,则不能超过这个地址。
这个函数分为四部分:
1)参数检测,保证参数的正确性。
2)以 goal 参数为基础计算并扫描开始地址。
3)检测本次分配是否可以使用上次分配使用的页面,以节省内存。
4)在引导内存分配器的“位图”中,标记页面已分配,并将页面内存清 0。
void * __init
__alloc_bootmem_core(struct bootmem_data *bdata, unsigned long size,
unsigned long align, unsigned long goal, unsigned long limit)
{
unsigned long offset, remaining_size, areasize, preferred;
unsigned long i, start = 0, incr, eidx, end_pfn;
void *ret;
---------------------------------------
检查 size 参数,不能为 0。
if (!size) {
BUG();
}
检查 align 参数,如果对齐不是 2 的幂数,则 BUG()。
BUG_ON(align & (align-1));
如果这个节点的起始地址,大于 limit 并且 limit 不为 0,则返回报错。
if (limit && bdata->node_boot_start >= limit)
return NULL;
如果节点没有页面位图,则报错,返回 NULL。
if (!bdata->node_bootmem_map)
return NULL;
把这个节点(低端内存)的结束 PFN。
end_pfn = bdata->node_low_pfn;
取得 limit 地址所对应的物理页面祯号。
limit = PFN_DOWN(limit);
if (limit && end_pfn > limit)
end_pfn = limit;
eidx = end_pfn - PFN_DOWN(bdata->node_boot_start);
对齐方式默认为 0 。
offset = 0;
如果指定了分配内存的对齐方好,计算对齐方式。
if (align && (bdata->node_boot_start & (align - 1UL)) != 0)
offset = align - (bdata->node_boot_start & (align - 1UL));
offset = PFN_DOWN(offset);
----------------------------------------
如果设置了分配空间的目标地址,目标地址在这个节点的空间内。
if (goal && goal >= bdata->node_boot_start && PFN_DOWN(goal) < end_pfn) {
开始的适当偏移是 goal 减去该节点可寻址的内存起点。
preferred = goal - bdata->node_boot_start;
考虑到上次分配的地址。
if (bdata->last_success >= preferred)
if (!limit || (limit && limit > bdata->last_success))
preferred = bdata->last_success;
} else
preferred = 0;
上面这段代码确定了 preferred 这个变量的值,这个值确定了内存分配的地址,
下面设置了这个地址对齐设置。
preferred = PFN_DOWN(ALIGN(preferred, align)) + offset;
对要分配的内存空间大小进行调整,进行整页分配。
areasize = (size + PAGE_SIZE-1) / PAGE_SIZE;
这个 incr 变量是要跳过的页面数,如果要求对齐方式多于一页,则他们满足对齐请求。
incr = align >> PAGE_SHIFT ? : 1;
---------------------------------------
restart_scan:
for (i = preferred; i < eidx; i += incr) {
unsigned long j;
i = find_next_zero_bit(bdata->node_bootmem_map, eidx, i);
i = ALIGN(i, incr);
if (i >= eidx)
break;
if (test_bit(i, bdata->node_bootmem_map))
continue;
for (j = i + 1; j < i + areasize; ++j) {
if (j >= eidx)
goto fail_block;
if (test_bit(j, bdata->node_bootmem_map))
goto fail_block;
}
start = i;
goto found;
fail_block:
i = ALIGN(j, incr);
}
如果不能够从 goal 以开始分配足够的空间,直接从最低端的对齐地址,重新进行扫描。
if (preferred > offset) {
preferred = offset;
goto restart_scan;
}
return NULL;
----------------------------------------
这段代码在页面 preferred 和这个节点的结束(或者 limit)之间扫描地址空间,
当需要对齐大于 1 页时,步长以 incr 为准。
----------------------------------------
found:
记录这次分配的起始地址。
bdata->last_success = PFN_PHYS(start);
BUG_ON(start >= eidx);
bdata->last_offset 表示最后一次分配所在页面的偏移,如果为 0, 则表示该页全部使用。
bdata->last_pos 表示最后一次分配时的物理页面祯数。
如果这次空间分配可以和上面的分配进行合并。
if (align < PAGE_SIZE && bdata->last_offset && bdata->last_pos+1 == start) {
offset = ALIGN(bdata->last_offset, align);
BUG_ON(offset > PAGE_SIZE);
上次分配之后,这个页面还有多少内存空间(经过对齐的)。
remaining_size = PAGE_SIZE - offset;
如果要分配的空间大小,小于这个页面还没有分配的空间大小。
if (size < remaining_size) {
areasize = 0;
/* last_pos unchanged */
bdata->last_offset = offset + size;
ret = phys_to_virt(bdata->last_pos * PAGE_SIZE +
offset +
bdata->node_boot_start);
} else {
这个页面的空间不满足这次的空间分配,分配下一页。
remaining_size = size - remaining_size;
areasize = (remaining_size + PAGE_SIZE-1) / PAGE_SIZE;
ret = phys_to_virt(bdata->last_pos * PAGE_SIZE +
offset +
bdata->node_boot_start);
bdata->last_pos = start + areasize - 1;
bdata->last_offset = remaining_size;
}
bdata->last_offset &= ~PAGE_MASK;
} else {
如果这个内存空间分配,不可以和上次的空间分配进行合并。
bdata->last_pos = start + areasize - 1;
bdata->last_offset = size & ~PAGE_MASK;
ret = phys_to_virt(start * PAGE_SIZE + bdata->node_boot_start);
}
----------------------------------------
这段代码主要测试,这次的内存空间地址分配是否能够和上次的地址空间分配进行合并。
并对 bdata->last_offset 和 bdata->last_pos 进行更新。
bdata->last_offset 表示最后一次分配所在页面的偏移,如果为 0, 则表示该页全部使用。
bdata->last_pos 表示最后一次分配时的物理页面祯数。
----------------------------------------
for (i = start; i < start + areasize; i++)
if (unlikely(test_and_set_bit(i, bdata->node_bootmem_map)))
BUG();
memset(ret, 0, size);
----------------------------------------
把这次分配的页面在引导内存分配器的位图上表为 1 ,并将其内容清 0。
----------------------------------------
return ret;
}
******************************************
问题:
1)在这个 alloc_node_mem_map() 函数中,分配 mem_map 数组时,为什么 start 要
和 (PAGE_SIZE * MAX_ORDER_NR_PAGES) 地址必须对齐 ?
2) MAX_ORDER_NR_PAGES 宏定义的意义和大小的确定?
3)在 __alloc_bootmem_core() 函数中 BUG_ON(align & (align-1)) 的意思?
********************************************
参考:
mem_map:一个Struct page数组,对应系统中所有的物理内存页。
而每一个zone结构里都有一个zone_mem_map域指向这个zone的第一个page 在mem_map的位置,
还有一个域size代表这个区的大小,即总共有多少页。
每一个zone都有自己的buddy system,由下面的zone结构就可以看出。
空闲块是根据其大小做的保存,特别强调的是struct free_area free_area[MAX_ORDER];
保存着zone中的空闲块。数组中的每一个元素都有个双链表结构。比如说 free_area中第 K 个元素
保存着大小为2的k次方大小的块的链表结构。数组中保存的是表头结构,即指向第一个2的k次方大小块的
第一个页面。那块的剩余的页面怎么办?不用管,因为都是按块来操作的,只需要知道块的第一个页面即可,
最后一个页面就是第一个页面加上2的k次方。同属于一个链表的块与块之间由每一个块的第一个
页面 的 struct page 中的 list_head lru 来相互链接。
#ifndef CONFIG_FORCE_MAX_ZONEORDER
#define MAX_ORDER 11
#else
#define MAX_ORDER CONFIG_FORCE_MAX_ZONEORDER
#endif
#define MAX_ORDER_NR_PAGES (1 << (MAX_ORDER - 1))
struct free_area {
struct list_head free_list;
unsigned long nr_free;
};
linux-mips启动分析(4-5)
在 mem_init() 函数中调用 free_all_bootmem() 函数,取得内存启动分配器的空闲页面数目。
在我们的系统中,为 UMA 内存模型,只有一个内存节点,为静态内存节点 contig_page_data 。
这个 free_all_bootmem() 函数销毁引导内存分配器。
==============================================================
unsigned long __init free_all_bootmem(void)
{
return free_all_bootmem_core(NODE_DATA(0));
}
********************************************
这个 free_all_bootmem_core() 函数为销毁引导内存分配器的核心函数。
它分为如下的步骤:
1) 对该节未分配的页面
清除 struct page 结构的 PG_reserved 标志。
设置 struct page 结构引用技术为 1。
调用 __free_pages() 函数构建伙伴分配的空闲页面链表。
2) 释放用于位图的页面,并将其释放给伙伴分配器。
---------------------------------------
static unsigned long __init free_all_bootmem_core(pg_data_t *pgdat)
{
struct page *page;
unsigned long pfn;
取得内存映射结构体。这个结构体用于引导内存分配器分配内存时使用。
这个 bootmem_data_t 类型参考《linux-mips启动分析(4-1)》。
bootmem_data_t *bdata = pgdat->bdata;
unsigned long i, count, total = 0;
unsigned long idx;
unsigned long *map;
int gofast = 0;
BUG_ON(!bdata->node_bootmem_map);
count = 0;
这个 node_boot_start 成员变量表示这个节点的起始物理地址。
取得这个内存节点的起始物理页面祯号(PFN)。
pfn = PFN_DOWN(bdata->node_boot_start);
这个 node_low_pfn 成员变量表示这个节点的结束 PFN 。
idx = bdata->node_low_pfn - pfn;
这个 node_bootmem_map 是这个内存节点页面映射位图。
map = bdata->node_bootmem_map;
由于 node_bootmem_map 是 long 类型的,当第二个条件成立时,
也就是指 node_boot_start ,这个内存节点的起始地址,是 LOG2(BITS_PER_LONG) 个页面对齐的。
也就是当这个节点内存的起始地址为 0 ,或者 LOG2(BITS_PER_LONG) 个页面对齐时, gofast = 1。
if (bdata->node_boot_start == 0 ||
ffs(bdata->node_boot_start)-PAGE_SHIFT>ffs(BITS_PER_LONG))
gofast = 1;
for (i = 0; i < idx; ) {
取得内存页面 i 所在的页面映射位图占用的 long 字。
unsigned long v = ~map[i / BITS_PER_LONG];
如果 gofast 等于 1 ,并且这个 long 字的位全部为 1 ,
表明这个 long 字表示未分配的页面,可以快速的设置。
if (gofast && v == ~0UL) {
int order;
这个 page 变量为这个 long 字的起始的第一个 struct page 的指针。
这个 struct page mem_map[] 数组是在 free_area_init_core()函数中初始化的。
参考《linux-mips启动分析(4-3)》。
page = pfn_to_page(pfn);
使用 count 变量进行统计所有的未分配页面的数量。
count += BITS_PER_LONG;
这个 order 变量表示内存空间的为 2 的 order 次页面数目。
order = ffs(BITS_PER_LONG) - 1;
__free_pages_bootmem(page, order);
i += BITS_PER_LONG;
page += BITS_PER_LONG;
当这个 long 字的位并不全部为 1 时,也不全部为 0 时。
} else if (v) {
unsigned long m;
这个 page 变量为这个 long 字的起始的第一个 struct page 的指针。
page = pfn_to_page(pfn);
循环测试,当 v 的某一位为 1 时,表明这个页面存在并且没有分配。
for (m = 1; m && i < idx; m<<=1, page++, i++) {
if (v & m) {
count++;
__free_pages_bootmem(page, 0);
}
}
当这个 long 字的位不全部为 0 时。
} else {
i += BITS_PER_LONG;
}
这个 pfn 变量,用于 pfn_to_page(pfn) 取得每一个 long 字的起始的第一个 struct page 的指针。
pfn += BITS_PER_LONG;
}
使用 total 变量计数所有可以分配的页面总数。
total += count;
page = virt_to_page(bdata->node_bootmem_map);
count = 0;
取得位图所占用的内存页面数量。
idx = (get_mapsize(bdata) + PAGE_SIZE-1) >> PAGE_SHIFT;
循环遍历位图所占用的页面,进行释放。
for (i = 0; i < idx; i++, page++) {
__free_pages_bootmem(page, 0);
count++;
}
使用 total 变量计数所有可以分配的页面总数。
total += count;
设置引导内存分配器的位图等于 NULL。
bdata->node_bootmem_map = NULL;
返回空闲内存页面的总数。
return total;
}
****************************************
这个 __free_pages_bootmem() 函数建立伙伴分配器的空闲页面的空闲页面链表。
---------------------------------------
void fastcall __init __free_pages_bootmem(struct page *page, unsigned int order)
{
这个 order 表示
如果等于 0 ,则清除 struct page 结构体的 PG_reserved 标志。引用计数设为 1。
if (order == 0) {
__ClearPageReserved(page);
set_page_count(page, 0);
set_page_refcounted(page);
这个 __free_page() 函数建立伙伴分配器的空闲链表。
__free_page(page);
} else {
int loop;
prefetchw(page);
循环清除 page 结构体的 PG_reserved 标志。引用计数设为 0 。
for (loop = 0; loop < BITS_PER_LONG; loop++) {
struct page *p = &page[loop];
if (loop + 1 < BITS_PER_LONG)
prefetchw(p + 1);
__ClearPageReserved(p);
set_page_count(p, 0);
}
设置 struct page 结构体的引用计数为 1 。
set_page_refcounted(page);
这个 __free_pages() 函数建立伙伴分配器的空闲链表。
__free_pages(page, order);
}
}
****************************************
这个 __free_page() 或者 __free_pages() 函数的
实现参考《linux-mips启动分析(21-1)》。
---------------------------------------
#define __free_page(page) __free_pages((page), 0)
---------------------------------------
fastcall void __free_pages(struct page *page, unsigned int order)
{
这个 put_page_testzero() 函数,把 page 的计数原子性的减 1 ,
并测试是否为 0 ,如果为 0 ,返回 true,否则返回 false。
if (put_page_testzero(page)) {
if (order == 0)
free_hot_page(page);
else
__free_pages_ok(page, order);
}
}
*****************************************
问题:
1) 在 __free_pages_bootmem() 函数中,当 order 不等于 0 时,
为什么只设置一个 page 的引用计数为 1 ,其他的为 0 ?
linux-mips启动分析(5)
在 setup_arch() 函数中调用了 resource_init() 函数。
这个函数遍历每一个内存空间范围(物理地址),在资源管理器中进行资源申请,
并对内核代码和数据段进行资源申请。
==============================================================
static void __init resource_init(void)
{
int i;
这个宏 UNCAC_BASE 和 IO_BASE 是在 include/asm-mips/page.h 文件中定义的。
表示不经过 cache 的内存的起始地址和 IO mem 的起始地址。
if (UNCAC_BASE != IO_BASE)
return;
对将要申请的资源进行赋值。(_text,_etext 等由链接文件中得出。)
变量 code_resource 和 data_resource 是静态定义的。
code_resource.start = __pa_symbol(&_text);
code_resource.end = __pa_symbol(&_etext) - 1;
data_resource.start = __pa_symbol(&_etext);
data_resource.end = __pa_symbol(&_edata) - 1;
for (i = 0; i < boot_mem_map.nr_map; i++) {
struct resource *res;
unsigned long start, end;
start = boot_mem_map.map[i].addr;
end = boot_mem_map.map[i].addr + boot_mem_map.map[i].size - 1;
if (start >= HIGHMEM_START)
continue;
if (end >= HIGHMEM_START)
end = HIGHMEM_START - 1;
动态分配 struct resource 结构,每个内存空间范围(物理地址)对应一个 struct resource 结构。
res = alloc_bootmem(sizeof(struct resource));
switch (boot_mem_map.map[i].type) {
case BOOT_MEM_RAM:
case BOOT_MEM_ROM_DATA:
res->name = "System RAM";
break;
case BOOT_MEM_RESERVED:
default:
res->name = "reserved";
} switch 选择结束。
res->start = start;
res->end = end;
res->flags = IORESOURCE_MEM | IORESOURCE_BUSY;
这个 request_resource() 在资源管理器上申请资源。
把每一个内存空间范围(物理地址)都在根资源 iomem_resource 中申请。
根资源 iomem_resource 是在 plat_mem_setup() 函数中进行的赋值。
request_resource(&iomem_resource, res);
因为不知道内核代码和数据方在哪个内存空间范围(物理地址)中,所以都进行申请,由资源管理器来测试。
request_resource(res, &code_resource);
request_resource(res, &data_resource);
} 循环结束。
}
******************************************
进行 I/O 或者 MEM 申请都是由 request_resource() 来完成的。
资源管理器的根资源 I/O 或者 MEM 分别不同。
I/O 的根资源为 ioport_resource。
MEM 的根资源为 iomem_resource。
这两种资源在 plat_mem_setup() 函数中进行的赋值。
这个 request_resource() 函数的参数为:
root 表示上一级的资源结构。 new 表示新添加的资源。
这个函数主要功能还是有 __request_resource() 函数来实现的。
==============================================================
这个 resource_lock 锁定所有的资源链表,在申请或者移出资源时,需要锁定这个锁。
在 kernel/resource.c 文件进行定义。
static DEFINE_RWLOCK(resource_lock);
int request_resource(struct resource *root, struct resource *new)
{
struct resource *conflict;
write_lock(&resource_lock);
conflict = __request_resource(root, new);
write_unlock(&resource_lock);
return conflict ? -EBUSY : 0;
}
*****************************************
资源的组织方式,在 struct resource 结构体中有三个指针,
->parent 指向父资源的指针。
->child 指向子资源的指针。
->sibling 指向兄弟资源的指针。
--------------------- / parent指针
| iomem_resource | -------------------
--------------------- \ |
/\ | ->child 指向子资源的指针 |
parent指针 | \/ |
---------------------- sibling 指针 ------------------- sibling 指针
| new_resource_1 | ---------------------------| new_resource_2 | --------
------------------- -------------------
/\ | ->child 指向子资源的指针
parent指针| \/
------------------- sibling 指针 ------------------- sibling 指针
| new_resource_3 | ---------------------------| new_resource_4 | --------
------------------- -------------------
| ->child 指向子资源的指针
\/
各个兄弟资源组成一个链表,从 root->child 到依次的 sibling 资源,
它们的各个资源的 I/O 或者 MEM 空间也是依次的从低到高的。
例如 new_resource_1 的 start = 0x00000000, end = 0x000000FF。
new_resource_2 的 start 必须大于 0x000000FF。
new_resource_3 和 new_resource_4 的空间必须在从
start = 0x00000000, end = 0x000000FF 之间。
===============================================================
static struct resource * __request_resource(struct resource *root, struct resource *new)
{
resource_size_t start = new->start;
resource_size_t end = new->end;
struct resource *tmp, **p;
首先进行参数检测,如果错误则返回 root 指针。
if (end < start)
return root;
if (start < root->start)
return root;
if (end > root->end)
return root;
取得指向 root->child 指针的指针。
p = &root->child;
for (;;) {
取得 root->child 指针 == tmp 。
tmp = *p;
如果 root->child 指针是 NULL 指针。或者新资源空间块在 root->child 资源块之下。
if (!tmp || tmp->start > end) {
new->sibling = tmp;
*p = new;
new->parent = root;
return NULL;
}
取得 root->child 的兄弟资源指针。
p = &tmp->sibling;
如果新资源和 root->child 资源没有冲突,则继续循环。
if (tmp->end < start)
continue;
返回有冲突的资源指针。
return tmp;
}
}
linux-mips启动分析(6)
在 start_kernel() 函数中调用了 sched_init() 函数。
这个函数进行核心进程调度器初始化,调度器的初始化优先于任何中断
的建立(包括 timer 中断)。
这个函数函数的主要工作是初始化各个 CPU 的运行队列。
并且初始化进程 0 ,即 idle 进程,但是并没有设置 idle 进程的 NEED_RESCHED 标志,
以完成内核剩余的启动部分。
下面对进程调度器进行初始化函数 sched_init() 函数进行分析。
================================================================
void __init sched_init(void)
{
int i, j, k;
int highest_cpu = 0;
for_each_possible_cpu(i) {
struct prio_array *array;
struct rq *rq;
这个 cpu_rq(i) 函数取得索引为 n 的 CPU 的运行队列的地址。
rq = cpu_rq(i);
锁定这个运行队列保护进程链表的自选锁。
spin_lock_init(&rq->lock);
设置锁的依赖关系。
lockdep_set_class(&rq->lock, &rq->rq_lock_key);
rq->nr_running = 0;
这个 active 指针表示指向活动进程链表的指针。
这个 expired 指针表示指向已过时进程链表的指针。
rq->active = rq->arrays;
rq->expired = rq->arrays + 1;
这个 best_expired_prio 成员表示过期进程中静态优先级最高的进程。
rq->best_expired_prio = MAX_PRIO;
#ifdef CONFIG_SMP
如果定义了 CONFIG_SMP 这个宏定义,则执行下面的。
这些成员的主要意义是保持各个 CPU 运行链表的负载平衡使用。
rq->sd = NULL;
for (j = 1; j < 3; j++)
rq->cpu_load[j] = 0;
rq->active_balance = 0;
rq->push_cpu = 0;
rq->cpu = i;
rq->migration_thread = NULL;
INIT_LIST_HEAD(&rq->migration_queue);
#endif
atomic_set(&rq->nr_iowait, 0);
这个 array 成员是一个包含两个 prio_array_t 结构的数组。
每个数据结构表示一个进程的集合,包含 140 个双向链表头,
每个链表表示一个可能的进程优先级,一个优先级位图和一个计数器。
这个位图中置 1, 表示这个优先级的链表中有进程。
这个计数器表示这个进程组合中共有多少进程。
for (j = 0; j < 2; j++) {
array = rq->arrays + j;
for (k = 0; k < MAX_PRIO; k++) {
INIT_LIST_HEAD(array->queue + k);
__clear_bit(k, array->bitmap);
}
把 MAX_PRIO = 140 为置 1, 定义查找 bitmap 的终止符。
__set_bit(MAX_PRIO, array->bitmap);
}
highest_cpu = i;
}
设置进程 0 的 load_weight 成员。
set_load_weight(&init_task);
#ifdef CONFIG_SMP
设置 CPU 的总数目。
nr_cpu_ids = highest_cpu + 1;
调用 open_softirq()函数开启使用软中断向量 SCHED_SOFTIRQ 。
设置它的软中断处理函数指针为 run_rebalance_domains 函数。
open_softirq(SCHED_SOFTIRQ, run_rebalance_domains, NULL);
#endif
#ifdef CONFIG_RT_MUTEXES
初始化每个 mutex 的最高优先级的任务的队列头和队列锁。
plist_head_init(&init_task.pi_waiters, &init_task.pi_lock);
#endif
原子性增加 init_mm 的引用计数,初始化为 1, 当这个成员为 0 时,则释放对这段空间。
atomic_inc(&init_mm.mm_count);
如果 进程 0 (idle进程)也进行缓慢的 MMU 转换,则这个函数不为空函数,
否则是空函数,在 MIPS 体系结构下,为空函数。
enter_lazy_tlb(&init_mm, current);
初始化指定 CPU (当前 CPU)的 idle 进程。
这个 idle 进程只有当这个 CPU 的可运行进程队列为空时,才会被调度。
init_idle(current, smp_processor_id());
}
******************************************
这个 for_each_possible_cpu(i) 循环的意思可以在 include/linux/cpumask.h 文件中
看到,实际是查找这个 cpu_possible_map 的信息。
参考《linux的CPU信息管理》。
这个函数是取的位图 cpu_possible_map 的置位的位置,返回给变量 i。
******************************************
第一个函数: set_load_weight(&init_task);
参数:init_task 是进程 0 的进程描述符。
它是在 arch/mips/kernel/init_task.c 文件中定义的,通过 INIT_TASK 宏静态初始化。
这个函数根据进程的实时优先级或者静态优先级设置进程的 load_weight 成员,
这个成员是为了各个 CPU 的运行队列均衡。
----------------------------------------
static void set_load_weight(struct task_struct *p)
{
这个 has_rt_policy() 判断进程是否是实时进程,
即它的调度策略不是 SCHED_NORMAL 和 SCHED_BATCH 策略。
if (has_rt_policy(p)) {
#ifdef CONFIG_SMP
这个 task_rq() 宏定义是冲进程 p 取得进程 p 所属的运行队列指针。
if (p == task_rq(p)->migration_thread)
运行队列 struct runquere 的成员 migration_thread 表示迁移线程的进程描述符指针。
这个进程赋值各个 CPU 的运载平衡。
p->load_weight = 0;
else
#endif
p->load_weight = RTPRIO_TO_LOAD_WEIGHT(p->rt_priority);
} else
p->load_weight = PRIO_TO_LOAD_WEIGHT(p->static_prio);
}
******************************************
第二个函数:open_softirq(SCHED_SOFTIRQ, run_rebalance_domains, NULL);
这个函数open_softirq()用于开启一个指定的软中断向量 nr,
也即适当地初始化软中断向量nr所对应的软中断描述符softirq_vec[nr]。
在这里开启使用软中断向量 SCHED_SOFTIRQ 。
设置它的软中断处理函数指针为 run_rebalance_domains 函数。
在这里这个软中断处理函数 run_rebalance_domains() 函数的作用以后在讲解。
----------------------------------------
void open_softirq(int nr, void (*action)(struct softirq_action*), void *data)
{
softirq_vec[nr].data = data;
softirq_vec[nr].action = action;
}
******************************************
第三个函数: plist_head_init(&init_task.pi_waiters, &init_task.pi_lock);
这个函数初始化 struct task_struct 结构体的 struct plist_head 结构成员。
这个 struct task_struct 结构体的 struct plist_head pi_waiters 成员,
是按优先级排序的,它其实是两个双链表的组合,目的是为了在遍历优先级时减少处理重复优
先级所浪费的时间;最后,优先级是按从高至低排序的。
这个成员表示:进程拥有的每个 mutex 的最高优先级的任务的队列,
这个任务的 PI 优先级应等于 TOP。
----------------------------------------
static inline void
plist_head_init(struct plist_head *head, spinlock_t *lock)
{
初始化两个链表的头。
INIT_LIST_HEAD(&head->prio_list);
INIT_LIST_HEAD(&head->node_list);
#ifdef CONFIG_DEBUG_PI_LIST
head->lock = lock;
#endif
}
******************************************
第四个函数:init_idle(current, smp_processor_id());
这个函数在这里初始化当前 CPU 的 idle 进程。
这个函数没有设置这个 idle 进程的 NEED_RESCHED 标志,以完成内核的启动。
----------------------------------------
void __cpuinit init_idle(struct task_struct *idle, int cpu)
{
struct rq *rq = cpu_rq(cpu);
unsigned long flags;
idle->timestamp = sched_clock();
idle->sleep_avg = 0;
idle->array = NULL;
idle->prio = idle->normal_prio = MAX_PRIO;
idle->state = TASK_RUNNING;
idle->cpus_allowed = cpumask_of_cpu(cpu);
set_task_cpu(idle, cpu);
spin_lock_irqsave(&rq->lock, flags);
rq->curr = rq->idle = idle;
#if defined(CONFIG_SMP) && defined(__ARCH_WANT_UNLOCKED_CTXSW)
idle->oncpu = 1;
#endif
spin_unlock_irqrestore(&rq->lock, flags);
#if defined(CONFIG_PREEMPT) && !defined(CONFIG_PREEMPT_BKL)
task_thread_info(idle)->preempt_count = (idle->lock_depth >= 0);
#else
task_thread_info(idle)->preempt_count = 0;
#endif
}
----------------------------------------
task_struct 结构成员:
timestamp:
sleep_avg: 平均睡眠时间
array : struct prio_array 指针。
prio :
normal_prio:
state : 进程的状态,运行,睡眠、或停止。
cpus_allowed : 设置进程可以使用的 CPU 。
oncpu :
******************************************
问题:
1)锁的依赖关系如何设置?
2)task_struct 结构成员的含义?
******************************************
题外话:
文件 arch/mips/kernel/init_task.c 中对进程 0 需要的一些数据结构进行了定义,并通过
一些宏静态初始化。
static struct fs_struct init_fs = INIT_FS;
static struct files_struct init_files = INIT_FILES;
static struct signal_struct init_signals = INIT_SIGNALS(init_signals);
static struct sighand_struct init_sighand = INIT_SIGHAND(init_sighand);
struct mm_struct init_mm = INIT_MM(init_mm);
初始化进程 0 的线程信息和堆栈。
union thread_union init_thread_union
__attribute__((__section__(".data.init_task"),
__aligned__(THREAD_SIZE))) =
{ INIT_THREAD_INFO(init_task) };
初始化进程 0 进程描述符。
struct task_struct init_task = INIT_TASK(init_task);
linux-mips启动分析(7)
在 start_kernel() 函数中调用了 build_all_zonelists() 函数。
这个函数主要是构建系统所有节点的管理区链表。
这个链表的作用: 这个链表是为了在一个分配不能够满足时可以考察下一个管理区来设置了。
在考察结束时,分配将从 ZONE_HIGHMEM 回退到 ZONE_NORMAL,
在分配时从 ZONE_NORMAL 退回到 ZONE_DMA 就不会回退了。
==============================================================
void __meminit build_all_zonelists(void)
{
在 start_kernel 函数中调用 system_state = SYSTEM_BOOTING。
if (system_state == SYSTEM_BOOTING) {
__build_all_zonelists(NULL);
这个函数如果没有定义 CONFIG_CPUSETS 宏,就是空函数。
这个特性是主要是用于NUMA架构和拥有大量逻辑CPU的SMP架构,开
启这一特性会浪费一些内存,对一般人的桌面环境没有任何帮助。
cpuset_init_current_mems_allowed();
} else {
这个函数我们暂时不关心。
stop_machine_run(__build_all_zonelists, NULL, NR_CPUS);
}
统计所有的管理区可以用于分配的内存空间的大小。
vm_total_pages = nr_free_pagecache_pages();
printk( 打印节点数目和总共页面数目 );
}
******************************************
这个 __build_all_zonelists() 函数遍历每个内存节点,
为每个内存节点的每个管理区建立 zonelist 链表(其实是数组)。
---------------------------------
static int __meminit __build_all_zonelists(void *dummy)
{
int nid;
循环遍历每个内存节点。
for_each_online_node(nid) {
为节点的每个管理区建立 zonelist 链表(其实是数组)。
build_zonelists(NODE_DATA(nid));
把节点的每个管理区的 zonelist 的 zlcache_ptr 指针置为 NULL。
build_zonelist_cache(NODE_DATA(nid));
}
return 0;
}
******************************************
遍历每个节点是通过 for_each_online_node() 宏来实现的。
这个宏定义在 include/linux/nodemask.h 文件。
---------------------------------
#define for_each_online_node(node) for_each_node_mask((node), node_online_map)
---------------------------------
#if MAX_NUMNODES > 1
#define for_each_node_mask(node, mask) \
for ((node) = first_node(mask); \
(node) < MAX_NUMNODES; \
(node) = next_node((node), (mask)))
#else /* MAX_NUMNODES == 1 */
#define for_each_node_mask(node, mask) \
if (!nodes_empty(mask)) \
for ((node) = 0; (node) < 1; (node)++)
#endif /* MAX_NUMNODES */
---------------------------------
可以使用的内存节点位图 node_online_map 来记录节点的存在。
这个内存节点位图是在 mm/page_alloc.c 文件中定义的。
nodemask_t node_online_map __read_mostly = { { [0] = 1UL } };
EXPORT_SYMBOL(node_online_map);
nodemask_t node_possible_map __read_mostly = NODE_MASK_ALL;
EXPORT_SYMBOL(node_possible_map);
******************************************
这个 build_zonelists() 函数根据配置的不同也不同。
我们没有配置 CONFIG_NUMA 宏定义,为一致性内存模型。
如果有大于 1 个的内存节点时,也需要把其他的内存节点的管理区也加入这个 zonelist 链表。
不过为了降低某个特定的节点的压力,所以加入这个 zonelist 链表的管理区必须属于这个节点 N 的
下一个节点 (N + 1)的,如此形成一个环形。
例如:要为节点 2 的 ZONE_NORMAL 管理区建立它的 zonelist 链表。
首先加入这个 zonelist 链表的管理区是节点 2 的管理区,
其次加入这个 zonelist 链表的管理区是节点 3 的管理区,一直到 MAX_NUMNODES -1 的节点。
最后又从 0 节点开始加入这个 zonelist 链表,知道小于 2 的 节点结束。
---------------------------------
static void __meminit build_zonelists(pg_data_t *pgdat)
{
int node, local_node;
enum zone_type i,j;
取得内存节点号。
local_node = pgdat->node_id;
for (i = 0; i < MAX_NR_ZONES; i++) {
struct zonelist *zonelist;
每一个节点都有一个 zonelist 数组。
这个节点的每个管理区分配对应了一个 zonelist 。
zonelist = pgdat->node_zonelists + i;
构建这个节点的 zone[i] 的管理区链表。
j = build_zonelists_node(pgdat, zonelist, 0, i);
下面这两个循环的意思是如果有其他的内存节点,就把其他的内存节点的管理区也加入这个链表。
不过为了降低某个特定的节点的压力,所以加入这个链表的管理区必须属于这个节点 N 的
下一个节点 (N + 1)的,如此形成一个环形。
for (node = local_node + 1; node < MAX_NUMNODES; node++) {
if (!node_online(node))
continue;
j = build_zonelists_node(NODE_DATA(node), zonelist, j, i);
}
for (node = 0; node < local_node; node++) {
if (!node_online(node))
continue;
j = build_zonelists_node(NODE_DATA(node), zonelist, j, i);
}
这个管理区链表的最后一个为 NULL 指针。
zonelist->zones[j] = NULL;
}
}
---------------------------------
struct zonelist {
struct zonelist_cache *zlcache_ptr; // NULL or &zlcache
struct zone *zones[MAX_ZONES_PER_ZONELIST + 1]; // NULL delimited
#ifdef CONFIG_NUMA
struct zonelist_cache zlcache; // optional ...
#endif
};
所有的内存分配操作都是基于 zonelist 的,一个 zonelist 就是一系列的 zone 的列表,
链表的第一个 zone 是分配的首要目标 zone ,其余的 zone 都是 ballback 使用的,
他们的优先级依次降低。
如果 zlcache_ptr 指针不是 NULL,它所指向的就是 zlcache 的地址。
---------------------------------
这个函数被 build_zonelists() 函数调用。
参数 zone_type 的类型定义如下所示。
enum zone_type {
#ifdef CONFIG_ZONE_DMA
ZONE_DMA,
#endif
#ifdef CONFIG_ZONE_DMA32
ZONE_DMA32,
#endif
ZONE_NORMAL,
#ifdef CONFIG_HIGHMEM
ZONE_HIGHMEM,
#endif
MAX_NR_ZONES
};
因此当我们传入的参数 zone_type = ZONE_HIGHMEM 时,
那么参数 zonelist 的成员 zone[] 数组所形成的链表就是
这个节点各个低一级的管理区的指针,
zonelist->zones[ZONE_HIGHMEM ]= pgdat->node_zones + ZONE_HIGHMEM;
zonelist->zones[ZONE_NORMA ]= pgdat->node_zones + ZONE_NORMA;
zonelist->zones[ZONE_DMA32 ]= pgdat->node_zones + ZONE_DMA32;
zonelist->zones[ZONE_DMA ]= pgdat->node_zones + ZONE_DMA;
---------------------------------
static int __meminit build_zonelists_node(pg_data_t *pgdat,
struct zonelist *zonelist, int nr_zones, enum zone_type zone_type)
{
struct zone *zone;
检测参数。
BUG_ON(zone_type >= MAX_NR_ZONES);
zone_type++;
do {
zone_type--;
取得管理区的指针。
zone = pgdat->node_zones + zone_type;
这个 populated_zone() 函数是检测 zone 指针所指向的管理区所实际存在的页面数量是否不为 0 。
if (populated_zone(zone)) {
zonelist->zones[nr_zones++] = zone;
check_highest_zone(zone_type);
}
} while (zone_type);
返回 zonelist 的 成员 zone[] 数组共有几个成员。
return nr_zones;
}
******************************************
这个 build_zonelist_cache() 函数把特定节点的
每个管理区的 zonelist 的 zlcache_ptr 指针置为 NULL。
---------------------------------
static void __meminit build_zonelist_cache(pg_data_t *pgdat)
{
int i;
for (i = 0; i < MAX_NR_ZONES; i++)
pgdat->node_zonelists[i].zlcache_ptr = NULL;
}
******************************************
这个 nr_free_pagecache_pages() 函数主要统计所有的管理区可以用于分配的内存空间的大小。
---------------------------------
unsigned int nr_free_pagecache_pages(void)
{
return nr_free_zone_pages(gfp_zone(GFP_HIGHUSER));
}
这个函数的参数 gfp_zone(GFP_HIGHUSER) 等价于 ZONE_HIGHMEM 。
********************************************
这个 present_pages 成员表示这个管理区实际的页面数量。
这个 pages_high 是和 pages_low、pages_min 配合使用的,
这三个极值用于跟踪一个管理区的分配内存的压力,
当这个管理区的空闲页面的数量小于 pages_low 值时,就会唤醒 kswapd 内核线程,来释放页面。
当 kswapd 内核线程释放的内存页面大于 pages_high 时,这个 kswapd 内核线程就会停止运行。
---------------------------------
static unsigned int nr_free_zone_pages(int offset)
{
/* Just pick one node, since fallback list is circular */
pg_data_t *pgdat = NODE_DATA(numa_node_id());
unsigned int sum = 0;
struct zonelist *zonelist = pgdat->node_zonelists + offset;
struct zone **zonep = zonelist->zones;
struct zone *zone;
for (zone = *zonep++; zone; zone = *zonep++) {
unsigned long size = zone->present_pages;
unsigned long high = zone->pages_high;
if (size > high)
sum += size - high;
}
return sum;
}
******************************************
问题:
1)遍历每个内存节点函数是通过位图 node_online_map 实现的,但是这个位图在什么地方初始化的?
2) 这个 stop_machine_run() 函数的意思?
linux-mips启动分析(8)
在 start_kernel 函数中调用 sort_main_extable() 函数。
这个函数对内核建立的异常表( exception table )进行重新排序。
这个函数对 exception_table_entry[] 数组根据异常的向量号进行堆排序。
=============================================================
void __init sort_main_extable(void)
{
sort_extable(__start___ex_table, __stop___ex_table);
}
这个函数直接调用 sort_extable() 函数,这个 sort_extable() 函数的两个参数,
这个两个变量 __start___ex_table 和 __stop___ex_table 在
链接脚本文件 arch/mips/kernel/vmlinux.lds 中定义。
******************************************
由于 MIPS 体系结构没有定义自己的 sort_extable() 函数,即没有定义
这个 ARCH_HAS_SORT_EXTABLE 宏,所以 MIPS 体系结构使用 linux 共有
的 sort_extable() 函数和 cmp_ex () 函数。
----------------------------------------
这个 exception_table_entry 结构体如下。
struct exception_table_entry
{
unsigned long insn;
unsigned long nextinsn;
} ;
----------------------------------------
void sort_extable(struct exception_table_entry *start,
struct exception_table_entry *finish)
{
sort(start, finish - start, sizeof(struct exception_table_entry),
cmp_ex, NULL);
}
----------------------------------------
static int cmp_ex(const void *a, const void *b)
{
const struct exception_table_entry *x = a, *y = b;
if (x->insn > y->insn)
return 1;
if (x->insn < y->insn)
return -1;
return 0;
}
******************************************
这个 sort()函数定义在 lib/sort.c 文件中。
这个 sort 函数对数组进行堆排序。
参数:
base : 是数组的基地址
num: 是数组所占有的空间大小,以字节为单位
size: 是数组成员的大小,以字节为单位
cmd: 是一个函数指针,对两个成员进行比较
swap: 是成员交换函数。
----------------------------------------
void sort(void *base, size_t num, size_t size,
int (*cmp)(const void *, const void *),
void (*swap)(void *, void *, int size))
{
int i = (num/2 - 1) * size, n = num * size, c, r;
if (!swap)
swap = (size == 4 ? u32_swap : generic_swap);
构建堆。
for ( ; i >= 0; i -= size) {
for (r = i; r * 2 + size < n; r = c) {
c = r * 2 + size;
if (c < n - size && cmp(base + c, base + c + size) < 0)
c += size;
if (cmp(base + r, base + c) >= 0)
break;
swap(base + r, base + c, size);
}
}
/* sort */
for (i = n - size; i >= 0; i -= size) {
swap(base, base + i, size);
for (r = 0; r * 2 + size < i; r = c) {
c = r * 2 + size;
if (c < i - size && cmp(base + c, base + c + size) < 0)
c += size;
if (cmp(base + r, base + c) >= 0)
break;
swap(base + r, base + c, size);
}
}
}
******************************************
参考:
堆排序的设计思路是:
定义堆为一个键值序列 { k1 ,k2 ,…,kn },
它具有如下特性:ki < = k2i,ki < = k2i + 1 (i = 1 ,2 ,…, [ n/ 2 ]) 。
(这个思想是利用二叉树的特性,节点 i 的子节点为 2 * i 和 2 * i + 1 )。
对一组待排序的键值,首先把它们按堆的定义排列成一个序列 ( 称为初建堆),这就找到了最小键值。
然后将最小的键值取出,用剩下的键值再重新建堆,便得到次小键值。
如此反复进行,直到把全部键值排好序为止。
******************************************
问题:
1)这段代码没有仔细阅读,为什么要重新排序呢,而不是本身已经排好序列了?
linux-mips启动分析(9)
在 start_kernle() 函数中调用了 trap_init() 函数。
这个函数
==========================================================
void __init trap_init(void)
{
extern char except_vec3_generic, except_vec3_r4000;
extern char except_vec4;
unsigned long i;
在我们的系统中没有定义这两个变量 == 0。
if (cpu_has_veic || cpu_has_vint)
ebase = (unsigned long) alloc_bootmem_low_pages (0x200 + VECTORSPACING*64);
else
ebase = CAC_BASE;
这个 CAC_BASE 表明支持 CACHE 的 base 地址,由于是 32 的 CPU ,所以等于 0x80000000。
这个 ebase 变量表示异常入口点的基地址。
一般情况下不配置 CONFIG_CPU_MIPSR2_SRSS 这个宏,所以这个函数为空函数。
mips_srs_init();
初始化 CUP 的 TLB 和 cache ,以及它们的异常处理程序。
per_cpu_trap_init();
设置通用异常入口(ebase+0x180)的处理程序为 except_vec3_generic 。
set_handler(0x180, &except_vec3_generic, 0x80);
---------------------------------
在通用异常处理程序中,读取 CAUSE 寄存器的 ExcCode 域的值,这个预在发生异常时,
由 CPU 自动设置,使软件也可以知道异常的原因。
通用异常处理程序使用 ExcCode 域的值,来索引异常处理表 exception_handlers[]。
这个通用异常处理表其实是一个 unsigned long exception_handlers[32] 数组,表示
相应异常的处理程序地址,下面的代码进行初始化为保留的异常处理。
---------------------------------
for (i = 0; i <= 31; i++)
set_except_vector(i, handle_reserved);
---------------------------------
如果 CPU 有 EJTAG,设置 EJTAG 的异常处理程序。
这个可以在 probe_cpu() 函数中自动检测,并设置 cpu_data 的 option 成员。
也可以在 include/asm-mips/mach-XXXX/cpu-feature-overrides.h
头文件中设置 。
移植相关,移植时需要配置。
---------------------------------
if (cpu_has_ejtag && board_ejtag_handler_setup)
board_ejtag_handler_setup ();
设置 CPU 是否有 watch (内存的访问检测点)异常,如果有进行设置。
移植相关,移植时需要配置。
if (cpu_has_watch)
set_except_vector(23, handle_watch);
---------------------------------
初始化中断处理器,如果有外部中断控制器或者支持中断向量模式,
或者除法异常时 (cpu_has_divec) ,需要进行特殊的操作。
下面的我们的 CPU 不执行,不支持。
---------------------------------
if (cpu_has_veic || cpu_has_vint) {
int nvec = cpu_has_veic ? 64 : 8;
for (i = 0; i < nvec; i++)
set_vi_handler(i, NULL);
}else if (cpu_has_divec)
set_handler(0x200, &except_vec4, 0x8);
设置 CPU 的 cache 的奇偶检测位。
parity_protection_init();
由于 Data Bus Errors / Instruction Bus Errors 异常是由外部硬件通知的,
所以这两种异常需要有板级的特殊处理程序。
if (board_be_init)
board_be_init();
根据 ExcCode 域的值对通用异常处理表进行初始化,注册各个通用异常处理程序。
可以查看 CACUS 寄存器 ExcCode 域的值所对应的异常种类。
set_except_vector(0, handle_int); 中断的处理程序
set_except_vector(1, handle_tlbm);
set_except_vector(2, handle_tlbl);
set_except_vector(3, handle_tlbs);
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, rdhwr_noopt ? handle_ri :
(cpu_has_vtag_icache ?
handle_ri_rdhwr_vivt : handle_ri_rdhwr));
set_except_vector(11, handle_cpu);
set_except_vector(12, handle_ov);
set_except_vector(13, handle_tr);
if (current_cpu_data.cputype == CPU_R6000 ||
current_cpu_data.cputype == CPU_R6000A) {
如果 CPU 的类型是 CPU_R6000 或者 CPU_R6000A,就
注册一些专用的通用异常处理程序。
}
如果 CPU 支持不可屏蔽中断,设置不可屏蔽中断的处理函数。
移植相关,移植时需要配置。
if (board_nmi_handler_setup)
board_nmi_handler_setup();
if (cpu_has_fpu && !cpu_has_nofpuex)
set_except_vector(15, handle_fpe);
set_except_vector(22, handle_mdmx);
if (cpu_has_mcheck)
set_except_vector(24, handle_mcheck);
if (cpu_has_mipsmt)
set_except_vector(25, handle_mt);
set_except_vector(26, handle_dsp);
--------------------------------------
选择特殊的通用异常处理程序,并拷贝到通用异常入口(ebase+0x180)。
--------------------------------------
if (cpu_has_vce)
memcpy((void *)(CAC_BASE + 0x180), &except_vec3_r4000, 0x100);
else if (cpu_has_4kex)
memcpy((void *)(CAC_BASE + 0x180), &except_vec3_generic, 0x80);
else
memcpy((void *)(CAC_BASE + 0x080), &except_vec3_generic, 0x80);
设置浮点处理器的保存和加载程序指针。
signal_init();
这个 flush_icache_range()是一个函数指针,在 r4k_cache_init() 函数中赋值。
参考《linux-mips启动分析(9-1)》。
把 ebase 地址到 ebase + 0x400 地址的指令装入 指令 cache 中去。
flush_icache_range(ebase, ebase + 0x400);
把 TLB 修改、加载和读取异常的处理代码加载到指令缓存中。
flush_tlb_handlers();
}
******************************************
这个 cpu_has_veic 变量是在 include/asm-mips/cpu-features.h 文件中定义的,
这个 CONFIG_CPU_MIPSR2_IRQ_EI 宏定义,表明是是使用了一个外部中断控制器来快速的
分发中断,如果不知道是否是使用了外部中断控制器,就不应该设置这个宏定义。
---------------------------------
#if defined(CONFIG_CPU_MIPSR2_IRQ_EI) && !defined(cpu_has_veic)
# define cpu_has_veic (cpu_data[0].options & MIPS_CPU_VEIC)
#elif !defined(cpu_has_veic)
# define cpu_has_veic 0
#endif
******************************************
这个 cpu_has_vint 变量是在 include/asm-mips/cpu-features.h 文件中定义的,
这个 CONFIG_CPU_MIPSR2_IRQ_VI 宏定义,表明 CPU 是否支持 Vectored interrupt mode。
这种模式能够使中断分发反应更加快速。这种模式的代码兼容非 Vectored interrupt mode。
所以如果一个 CPU 不支持这种模式,也可以选择上这个选项。
---------------------------------
#if defined(CONFIG_CPU_MIPSR2_IRQ_VI) && !defined(cpu_has_vint)
# define cpu_has_vint (cpu_data[0].options & MIPS_CPU_VINT)
#elif !defined(cpu_has_vint)
# define cpu_has_vint 0
#endif
******************************************
这个 CONFIG_CPU_MIPSR2_SRS 宏配置的帮助信息:
Allow the kernel to use shadow register sets for fast interrupts.
Interrupt handlers must be specially written to use shadow sets.
Say N unless you know that shadow register set upport is needed.
******************************************
把 addr 起始地址的代码(异常处理程序)拷贝到 (ebase + offset) 地址(异常处理入口点)。
---------------------------------
void __init set_handler (unsigned long offset, void *addr, unsigned long size)
{
memcpy((void *)(ebase + offset), addr, size);
flush_icache_range(ebase + offset, ebase + offset + size);
}
******************************************
这个 signal_init() 函数被 trap_init() 函数所调用。
这个函数初始化 save_fp_context 和 restore_fp_context 这两个函数指针。
这两个变量 save_fp_context 和 restore_fp_context 是两个函数指针。
这个两个函数用来保存和重新加载协处理器浮点处理器的上下文。
asmlinkage int (*save_fp_context)(struct sigcontext __user *sc);
asmlinkage int (*restore_fp_context)(struct sigcontext __user *sc);
----------------------------------------
static inline void signal_init(void)
{
if (cpu_has_fpu) {
save_fp_context = _save_fp_context;
restore_fp_context = _restore_fp_context;
} else {
save_fp_context = fpu_emulator_save_context;
restore_fp_context = fpu_emulator_restore_context;
}
}
******************************************
这个 flush_icache_range()是一个函数指针,在 r4k_cache_init() 函数中赋值。
参考《linux-mips启动分析(9-1)》。
在 r4k_cache_init()赋值 flush_icache_range = r4k_flush_icache_range 。
这个函数的主要作用是刷新 指定指令 cache 的范围。
-----------------------------------------
static void r4k_flush_icache_range(unsigned long start, unsigned long end)
{
struct flush_icache_range_args args;
args.start = start;
args.end = end;
r4k_on_each_cpu(local_r4k_flush_icache_range, &args, 1, 1);
instruction_hazard();
}
******************************************
这个 flush_tlb_handlers() 函数被 trap_init() 函数所调用。
这个函数把 TLB 修改、加载和读取异常的处理代码加载到指令缓存中。
-----------------------------------------
void __init flush_tlb_handlers(void)
{
把 TLB 读取异常的处理代码加载到指令缓存中。
flush_icache_range((unsigned long)handle_tlbl,
(unsigned long)handle_tlbl + sizeof(handle_tlbl));
把 TLB 加载异常的处理代码加载到指令缓存中。
flush_icache_range((unsigned long)handle_tlbs,
(unsigned long)handle_tlbs + sizeof(handle_tlbs));
把 TLB 修改异常的处理代码加载到指令缓存中。
flush_icache_range((unsigned long)handle_tlbm,
(unsigned long)handle_tlbm + sizeof(handle_tlbm));
}
*****************************************
问题:
1)通用异常处理程序 except_vec3_generic() 没有看?
2)这个 flush_icache_range() 函数的实现?
linux-mips启动分析(9-1)
这个 per_cpu_trap_init() 函数被 trap_init() 函数调用。
这个函数根据 cpu_probe() 函数检测的 CPU 特性或者移植时的配置,对寄存器进行设置。
---------------------------------
在这个函数中多次使用到了 CONFIG_MIPS_MT_SMTC 这个宏定义。
宏定义 CONFIG_MIPS_MT_SMTC 是使用多核的 SMTC Linux 时定义的。
一般情况下不考虑。MIPS已经开发出 SMP Linux的改进版,
叫做 SMTC (线程上下文对称多处理) Linux。
SMTC Linux能理解轻量级 TC 的概念,并能因此减少某些与SMP Linux相关的开销。
所以把这个 CONFIG_MIPS_MT_SMTC 宏关闭的代码去掉了。
---------------------------------
由于 CPU 是 32 位的,没有定义 CONFIG_64BIT 这个宏,把这个宏关闭的代码也去掉了。
---------------------------------
在移植时需要选择 MIPS 体系结构的版本, Release 1 or Release 2。(移植相关)
config CPU_MIPSR1
bool
default y if CPU_MIPS32_R1 || CPU_MIPS64_R1
config CPU_MIPSR2
bool
default y if CPU_MIPS32_R2 || CPU_MIPS64_R2
我们的 CPU 为 Release 1,所以把 CPU_MIPSR2 宏关闭的代码去掉了。
=============================================================
void __init per_cpu_trap_init(void)
{
取得当前 CPU 的逻辑号。
unsigned int cpu = smp_processor_id();
unsigned int status_set = ST0_CU0;
---------------------------------
这个 current_cpu_data 变量是由宏定义的:
如下所示:
#define current_cpu_data cpu_data[smp_processor_id()]
这个 cpu_data[]的数据是在 cpu_probe() 函数中填充的。
参考《linux-mips启动分析(3)》文件中的 cpu_probe() 函数讲解。
---------------------------------
检测当前 CPU 的 ISA 等级,如果等于 MIPS_CPU_ISA_IV,则修改状态寄存器进行配置。
if (current_cpu_data.isa_level == MIPS_CPU_ISA_IV)
status_set |= ST0_XX;
change_c0_status(ST0_CU|ST0_MX|ST0_RE|ST0_FR|ST0_BEV|ST0_TS|ST0_KX|ST0_SX|ST0_UX, status_set);
---------------------------------
这个 cpu_has_dsp 变量是在 cpu_probe() 函数中检测到的,
如: #define cpu_has_dsp (cpu_data[0].ases & MIPS_ASE_DSP)
也可以在移植内核时,定义这个宏,为 0 或者 1 ,移植相关。
if (cpu_has_dsp)
set_c0_status(ST0_MX);
---------------------------------
从上面的代码分析,得知这两个变量 cpu_has_veic, cpu_has_vint 都等于 0。
if (cpu_has_veic || cpu_has_vint) {
write_c0_ebase (ebase);
change_c0_intctl (0x3e0, VECTORSPACING);
}
---------------------------------
这个 cpu_has_divec 变量是在 cpu_probe() 函数中检测到的,
如: #define cpu_has_divec (cpu_data[0].options & MIPS_CPU_DIVEC)
也可以在移植内核时,定义这个宏,为 0 或者 1 ,移植相关。
if (cpu_has_divec) {
if (cpu_has_mipsmt) {
unsigned int vpflags = dvpe();
set_c0_cause(CAUSEF_IV);
evpe(vpflags);
} else
set_c0_cause(CAUSEF_IV);
}
---------------------------------
这个 cpu_has_mips_r2 宏变量的定义与否,参考上面的解释。
在我们的 CPU 上没有定义 MIPSR2 这个宏,而是 MIPSR1 版本的。
这两个变量在 arch/mips/kernel/time.c 文件中定义。
if (cpu_has_mips_r2) {
cp0_compare_irq = (read_c0_intctl () >> 29) & 7;
cp0_perfcount_irq = (read_c0_intctl () >> 26) & 7;
if (cp0_perfcount_irq == cp0_compare_irq)
cp0_perfcount_irq = -1;
} else {
cp0_compare_irq = CP0_LEGACY_COMPARE_IRQ;
cp0_perfcount_irq = -1;
}
---------------------------------
SAID 是地址空间标识的意思,这个值用来让操作系统识别当前进程的地址空间。
cpu_data[cpu].asid_cache = ASID_FIRST_VERSION;
---------------------------------
设置 TLB 异常需要提前设置的寄存器和变量。参考下面。
TLBMISS_HANDLER_SETUP();
---------------------------------
初始化当前进程 ,swapper 进程的,进程地址空间指针。
atomic_inc(&init_mm.mm_count);
current->active_mm = &init_mm;
BUG_ON(current->mm);
enter_lazy_tlb(&init_mm, current);
---------------------------------
这个函数没有看完,在前面是检测 cache 的 cache linesize,cache 的路数和组数。
cpu_cache_init();
---------------------------------
初始化 TLB 的各个条目,并自动构建 TLB (重添、修改、读取、写入)的异常处理函数。
tlb_init();
---------------------------------
}
*****************************************
#define TLBMISS_HANDLER_SETUP_PGD(pgd) \
pgd_current[smp_processor_id()] = (unsigned long)(pgd)
#define TLBMISS_HANDLER_SETUP() \
write_c0_context((unsigned long) smp_processor_id() << 25); \
TLBMISS_HANDLER_SETUP_PGD(swapper_pg_dir)
这个 TLBMISS_HANDLER_SETUP() 宏函数,设置 CP0 的 CONTEXT 寄存器。
这个寄存器主要为为了在 TLB 异常发生后,加速异常的处理过程,把常驻内存的页表和 VPN 以某种格式
打包一下,形成一个可以立即使用的指向内存页表的指针值。
第一步设置 CP0 的 CONTEXT 寄存器为( CPU 的逻辑号左移 25 位)。
第二步调用 TLBMISS_HANDLER_SETUP_PGD() 宏函数,
设置每 CPU 变量 pgd_current[]数组, 当前 CPU 的页目录基地址。
******************************************
这个 cpu_cache_init() 函数被 per_cpu_trap_init() 函数所调用。
这个函数根据 cpu_probe() 函数检测的 CPU 信息,或者配置的 CPU 信息,来选择 cache 的初始化函数。
我们的 CPU 选择的是 r4k_cache_init() 函数。
---------------------------------
void __init cpu_cache_init(void)
{
if (cpu_has_4k_cache) {
extern void __weak r4k_cache_init(void);
r4k_cache_init();
return;
}
}
*****************************************
这个 r4k_cache_init() 函数定义在 arch/mips/mm/c-r4k.c 文件。
void __init r4k_cache_init(void)
{
extern void build_clear_page(void);
extern void build_copy_page(void);
extern char except_vec2_generic;
struct cpuinfo_mips *c = ¤t_cpu_data;
设置 cache 错误的处理入口点 ( KSEG1ADDR(ebase+0x100)),
处理程序为 except_vec2_generic。
set_uncached_handler (0x100, &except_vec2_generic, 0x80);
---------------------------------
检测一级和二级 cache 的 cache_size, ways 和 line_size。
由于我们的 CPU 没有二级 cache ,所以 setup_scache() 函数只是返回了。
probe_pcache();
setup_scache();
---------------------------------
根据数据 cache linesize,设置 r4k_blast_dcache_page 函数指针。
r4k_blast_dcache_page_setup();
---------------------------------
根据数据 cache linesize,设置 r4k_blast_dcache_page_indexed 函数指针。
r4k_blast_dcache_page_indexed_setup();
---------------------------------
根据数据 cache linesize,设置 r4k_blast_dcache 函数指针。
r4k_blast_dcache_setup();
---------------------------------
根据指令 cache linesize,设置 r4k_blast_icache_page 函数指针。
r4k_blast_icache_page_setup();
---------------------------------
根据指令 cache linesize,设置 r4k_blast_icache_page_indexed 函数指针。
r4k_blast_icache_page_indexed_setup();
---------------------------------
根据指令 cache linesize,设置 r4k_blast_icache 函数指针。
r4k_blast_icache_setup();
---------------------------------
r4k_blast_scache_page_setup();
r4k_blast_scache_page_indexed_setup();
r4k_blast_scache_setup();
---------------------------------
设置数据 cache 的对齐掩码。
if (c->dcache.linesz)
shm_align_mask = max_t( unsigned long,
c->dcache.sets * c->dcache.linesz - 1,
PAGE_SIZE - 1);
else
shm_align_mask = PAGE_SIZE-1;
---------------------------------
对 cache 的一些操作函数指针,进行赋值。
flush_cache_all = r4k_flush_cache_all;
__flush_cache_all = r4k___flush_cache_all;
flush_cache_mm = r4k_flush_cache_mm;
flush_cache_page = r4k_flush_cache_page;
flush_cache_range = r4k_flush_cache_range;
flush_cache_sigtramp = r4k_flush_cache_sigtramp;
flush_icache_all = r4k_flush_icache_all;
flush_data_cache_page = r4k_flush_data_cache_page;
flush_icache_range = r4k_flush_icache_range;
#ifdef CONFIG_DMA_NONCOHERENT
_dma_cache_wback_inv = r4k_dma_cache_wback_inv;
_dma_cache_wback = r4k_dma_cache_wback_inv;
_dma_cache_inv = r4k_dma_cache_inv;
#endif
---------------------------------
这个些函数不知道有什么作用,看着好象是通过软件构建模拟指令?
build_clear_page();
build_copy_page();
local_r4k___flush_cache_all(NULL);
coherency_setup();
---------------------------------
}
*****************************************
static void __init probe_pcache(void)
{
struct cpuinfo_mips *c = ¤t_cpu_data;
unsigned int config = read_c0_config();
取得 CPU 标识寄存器。
unsigned int prid = read_c0_prid();
unsigned long config1;
unsigned int lsize;
根据自己 CPU 的类型来选择处理的过程,cputype 字段在 cpu_probe() 函数中赋值。
switch (c->cputype) {
case CPU_CLXRISC:
---------------------------------
初始 CP0 的 config1 寄存器的值。
config1 = read_c0_config1();
取得 config1 寄存器的 [22:24]位的值,为指令cache的每一路有几组。
config1 = (config1 >> 22) & 0x07;
if (config1 == 0x07)
config1 = 10;
else
config1 = config1 + 11;
config1 += 2;
icache_size = (1 << config1);
c->icache.linesz = 32;
c->icache.ways = 4;
c->icache.waybit = __ffs(icache_size / c->icache.ways);
对指令 cache 的 way 、 linesize 进行设置。
---------------------------------
config1 = read_c0_config1();
config1 = (config1 >> 13) & 0x07;
if (config1 == 0x07)
config1 = 10;
else
config1 = config1 + 11;
config1 += 2;
dcache_size = (1 << config1);
c->dcache.linesz = 32;
c->dcache.ways = 4;
c->dcache.waybit = __ffs(dcache_size / c->dcache.ways);
对数据 cache 的 way 、 linesize 进行设置。
---------------------------------
c->dcache.flags = 0;
c->options |= MIPS_CPU_PREFETCH;
---------------------------------
break;
}
c->icache.waysize = icache_size / c->icache.ways;
c->dcache.waysize = dcache_size / c->dcache.ways;
c->icache.sets = c->icache.linesz ?
icache_size / (c->icache.linesz * c->icache.ways) : 0;
c->dcache.sets = c->dcache.linesz ?
dcache_size / (c->dcache.linesz * c->dcache.ways) : 0;
}
---------------------------------
移植相关函数,如果这个函数不支持自己 CPU 的类型,需要添加,自己 CPU 类型的处理过程。
如我们的处理过程为 case CPU_CLXRISC,就是自己添加的。
******************************************
这个 tlb_init() 函数被 per_cpu_trap_init() 函数所调用。
---------------------------------
void __init tlb_init(void)
{
取得 CP0 的 config0 寄存器的值。
unsigned int config = read_c0_config();
取得 TLB 的条目数。
probe_tlb(config);
设置 CP0 的 PageMask 寄存器的 MASK 标志位,用来确定 1 条 TLB 可以映射的地址空间的大小。
write_c0_pagemask(PM_DEFAULT_MASK);
设置 CP0 的 Wired 寄存器,用来确定固定的 TLB 条目的数目==0 。
write_c0_wired(0);
我们的 CPU 上没有这个寄存器。
write_c0_framemask(0);
temp_tlb_entry = current_cpu_data.tlbsize - 1;
初始化 TLB 的每个条目。为进程 0 的虚拟地址对应物理地址 0。
local_flush_tlb_all();
---------------------------------
这个变量 ntlb 一个内核启动参数设置的,它的定义如下:
__setup("ntlb=", set_ntlb);
这个参数的处理函数为 static int __init set_ntlb(char *str);
这个 set_ntlb()主要是根据内核的启动命令行参数设置变量 ntlb 的值。
如果没有设置这个参数,则这个变量 ntlb 的值 默认 == 0。
参考《Linux启动参数及实现》和 《启动时内核参数解析》。
下面我们看看设置了这个命令行启动参数的时候,发生什么?
---------------------------------
if (ntlb) {
检测设置的 TLB 参数的值是个有效值,大于 1,并且小于 TLB 总的条目数目。
if (ntlb > 1 && ntlb <= current_cpu_data.tlbsize) {
int wired = current_cpu_data.tlbsize - ntlb;
主要是设置 ntlb 个固定的 TLB 条目数目。
write_c0_wired(wired);
write_c0_index(wired-1);
printk ("Restricting TLB to %d entries\n", ntlb);
} else
printk("Ignoring invalid argument ntlb=%d\n", ntlb);
}
build_tlb_refill_handler();
}
---------------------------------
static void __init probe_tlb(unsigned long config)
{
struct cpuinfo_mips *c = ¤t_cpu_data;
unsigned int reg;
如果这个 CPU 不是 MIPS32 或者 MIPS64 的兼容 CPU,就不支持 config1 寄存器,
所以我们直接返回。
if ((c->processor_id & 0xff0000) == PRID_COMP_LEGACY)
return;
读 CP0 的 config1 寄存器。
reg = read_c0_config1();
如果 CP0 的 config0 寄存器的 [7:9]为 == 0,则表示这个 CPU 没有 TLB。
if (!((config >> 7) & 3))
panic("No TLB present");
协处理器 CP0 的 config1 寄存器的 [25:30]表示 TLB 的条目数。
c->tlbsize = ((reg >> 25) & 0x3f) + 1;
}
---------------------------------
void local_flush_tlb_all(void)
{
unsigned long flags;
unsigned long old_ctx;
int entry;
进入临界区,关闭中断,保存中断标志。
ENTER_CRITICAL(flags);
取得 EntryHi 寄存器的值。
在 TLB 异常中, EntryHi 寄存器会装入引发 TLB 异常的程序地址的 VPN。
old_ctx = read_c0_entryhi();
把 EntryLo0, EntryLo1 寄存器清 0。
write_c0_entrylo0(0);
write_c0_entrylo1(0);
读取 Wired 寄存器,取得固定的 TLB 条目的数目。
entry = read_c0_wired();
对每一个 TLB 条目,进行初始化。
必须保证 TLB 条目的 VFN 不同,如果相同,就会出现错误。
while (entry < current_cpu_data.tlbsize) {
设置 EntryHi 寄存器,即程序地址的 VPN。 ASID 进程号为 0。
write_c0_entryhi(UNIQUE_ENTRYHI(entry));
设置 TLB 的 index 寄存器。
write_c0_index(entry);
如果是 MIPSR2 的 CPU 需要这个步骤,来保证 TLB 的填写正确,
为了 兼容 MIPSR2 的 CPU,所以 MIPSR1 的内核也需要做这个步骤。
mtc0_tlbw_hazard();
把 index 寄存器选择的 TLB 条目,写入 EntryHi、 EntryLo0, EntryLo1 寄存器的值。
tlb_write_indexed();
entry++;
}
如果是 MIPSR2 的 CPU 需要这个步骤,来保证 TLB 的填写正确,
为了 兼容 MIPSR2 的 CPU,所以 MIPSR1 的内核也需要做这个步骤。
tlbw_use_hazard();
把 EntryHi 寄存器原来的值写入 EntryHi 寄存器。
write_c0_entryhi(old_ctx);
出临界区,打开中断,恢复中断标志位。
EXIT_CRITICAL(flags);
}
---------------------------------
void __init build_tlb_refill_handler(void)
{
设置标志变量,检测是否是第一次执行。
static int run_once = 0;
根据 CPU 的类型,选择执行。
switch (current_cpu_data.cputype) {
default:
动态构建 TLB refill 异常处理程序,并拷贝到 ebase 变量指定的地址。
这个 ebase 变量的赋值,参考《linux-mips启动分析(9)》,在 trap_init()函数中赋值的。
当发生 TLB refill 异常时从 ebase 取指令(实际是 0x80000000 地址)。
build_r4000_tlb_refill_handler();
如果不是第一次执行则直接退出。
if (!run_once) {
动态自己构建 TLB modify 异常、 TLB load 异常 和 TLB store 异常处理程序。
build_r4000_tlb_load_handler();
build_r4000_tlb_store_handler();
build_r4000_tlb_modify_handler();
run_once++;
}
}
}
******************************************
这两个函数 mtc0_tlbw_hazard() 和 tlbw_use_hazard() 的使用。
如果是 MIPSR2 的 CPU 需要这个步骤,来保证 TLB 的填写正确,
为了 兼容 MIPSR2 的 CPU,所以 MIPSR1 的内核也需要做这个步骤。
******************************************
问题:
1)这两个 cp0_compare_irq 和 cp0_perfcount_irq 变量的意义?
2)struct cpuinfo_mips 结构体中这个 asid_cache 成员的意义?
3)这个 TLBMISS_HANDLER_SETUP() 宏函数,为什么这么设置 CONTEXT 寄存器?
4)这个 shm_align_mask 掩码的使用?
5)这个 cpu_has_4k_cache 宏定义的实现和赋值?
6)这个 build_clear_page() 函数的作用?
7)这个 r4k_cache_init() 函数最后有几个函数没有看完?
8)这四个函数
build_r4000_tlb_load_handler();
build_r4000_tlb_store_handler();
build_r4000_tlb_modify_handler();
build_r4000_tlb_refill_handler()
函数如何构建 TLB 异常处理指令的?
9)下面四个函数
build_clear_page();
build_copy_page();
local_r4k___flush_cache_all(NULL);
coherency_setup();
的作用?
10) cache 错误的处理程序 except_vec2_generic() 没有读?
linux-mips启动分析(10)
这个 rcu_init() 函数被 start_kernel() 函数调用。
这个 rcu_init() 函数初始化 RCU 机制,这个步骤必须比本地 timer 的初始化早。
并在 cpu_chain 通知链表上注册了一个通知结构体,由 CPU 管理子系统检测到 CPU_UP_PREPARE、
CPU_UP_PREPARE_FROZEN、CPU_DEAD、CPU_DEAD_FROZEN 事件时同时 RCU 管理子系统。
参考《linux通知链表机制》和《Linux RCU 机制详解》。
******************************************
在 rcu_init() 函数中使用到了 struct notifier_block 类型的变量 rcu_nb,
这个变量定义如下:
static struct notifier_block __cpuinitdata rcu_nb = {
.notifier_call = rcu_cpu_notify,
};
这个通知结构体的回调函数为 rcu_cpu_notify()。
-----------------------------------------
void __init rcu_init(void)
{
通知 RCU 管理子系统,当前 CPU 已经准备工作了。
rcu_cpu_notify(&rcu_nb, CPU_UP_PREPARE, (void *)(long)smp_processor_id());
register_cpu_notifier(&rcu_nb);
}
******************************************
下面是 通知结构体的回调函数为 rcu_cpu_notify()。
当 CPU 管理子系统检测到 CPU_UP_PREPARE、 CPU_UP_PREPARE_FROZEN、CPU_DEAD、
CPU_DEAD_FROZEN 事件时通知 RCU 管理子系统。
-----------------------------------------
static int __cpuinit rcu_cpu_notify(struct notifier_block *self,
unsigned long action, void *hcpu)
{
long cpu = (long)hcpu;
switch (action) {
case CPU_UP_PREPARE:
case CPU_UP_PREPARE_FROZEN:
rcu_online_cpu(cpu);
break;
case CPU_DEAD:
case CPU_DEAD_FROZEN:
rcu_offline_cpu(cpu);
break;
default:
break;
}
return NOTIFY_OK;
}
******************************************
每一个 CPU 维护两个数据结构 rcu_data,rcu_bh_data,它们用于保存回调函数,
函数 call_rcu() 和函数 call_rcu_bh() 用户注册回调函数,
前者把回调函数注册到 rcu_data,而后者则把回调函数注册到r cu_bh_data,
在每一个数据结构上,回调函数被组成一个链表,先注册的排在前头,后注册的排在末尾。
----------------------------------------
static void __devinit rcu_online_cpu(int cpu)
{
struct rcu_data *rdp = &per_cpu(rcu_data, cpu);
struct rcu_data *bh_rdp = &per_cpu(rcu_bh_data, cpu);
rcu_init_percpu_data(cpu, &rcu_ctrlblk, rdp);CT_TARGET_VENDOR
rcu_init_percpu_data(cpu, &rcu_bh_ctrlblk, bh_rdp);
初始化 RCU_SOFTIRQ 类型的软中断,软中断处理函数为 rcu_process_callbacks()。
tasklet_init(&per_cpu(rcu_tasklet, cpu), rcu_process_callbacks, 0UL);
}
-----------------------------------------
参考《linux-mips启动分析(2-1)》文件 setup_per_cpu_areas() 函数的实现。
参考《每CPU变量的数据组织和访问》。
这个函数使用每 CPU 变量 rcu_data 和 rcu_bh_data ,它们的定义如下所示:
DEFINE_PER_CPU(struct rcu_data, rcu_data) = { 0L };
DEFINE_PER_CPU(struct rcu_data, rcu_bh_data) = { 0L };
-----------------------------------------
调用这个函数的第二个参数是一个全局变量 rcu_ctlblk 和 rcu_bh_ctrlblk,定义如下:
static struct rcu_ctrlblk rcu_ctrlblk = {
.cur = -300,
.completed = -300,
.lock = __SPIN_LOCK_UNLOCKED(&rcu_ctrlblk.lock),
.cpumask = CPU_MASK_NONE,
};
static struct rcu_ctrlblk rcu_bh_ctrlblk = {
.cur = -300,
.completed = -300,
.lock = __SPIN_LOCK_UNLOCKED(&rcu_bh_ctrlblk.lock),
.cpumask = CPU_MASK_NONE,
};
-----------------------------------------
在 linux 内核中有一个总的 RCU 控制块,它的结构如下所示:
struct rcu_ctrlblk {
long cur; 是指当前等待 grace period 的 RCU 的回调函数的批号。
long completed; 是指已经完成的上次的 RCU 回调函数的批号。
int next_pending; 指在 CPU 上是否还有 call_rcu()新添加的回调函数。
int signaled;
spinlock_t lock ____cacheline_internodealigned_in_smp;
cpumask_t cpumask;
} ____cacheline_internodealigned_in_smp;
-----------------------------------------
每个 CPU 有一个 per_cpu 数据,它的类型如下所示:
struct rcu_data {
下面为 quiescent state 的处理成员
long quiescbatch; 当前等待的 quiescent state 号
int passed_quiesc; 由 rcu_qsctr_inc() 函数进行设置,
表示当前 CPU 已经经过了一次 quiescent state。
int qs_pending; 表示当前是否在等待 quiescent state 。
下面为回调函数的批号的处理成员
long batch; 表示当前 CPU 等待的 grace period 的 RCU 的回调函数的批号。
struct rcu_head *nxtlist; 当调用 call_rcu()时在 nextlist 链表注册新的回调函数
struct rcu_head **nxttail; 指向 nextlist 链表最后一个节点 next 指针的地址
long qlen;
struct rcu_head *curlist; 是指当前等待 grace period 的 RCU 的回调函数的链表。
struct rcu_head **curtail;
struct rcu_head *donelist; 是指已经等待到 grace period 的 RCU 的回调函数的链表。
struct rcu_head **donetail;
long blimit;
int cpu;
struct rcu_head barrier;
};
每次调用 call_rcu() 函数注册的回调函数,都会链入到 taillist 链表;
当前等待 grace period 完成的函数都会链入到rdp->curlist上.
到等待的 grace period 已经到来,就会将curlist上的链表移到donelist上.
当一个grace period过了之后,就会将taillist上的数据移到rdp->curlist上.之后加册的回调函数又会将其加到rdp->taillist上.
-----------------------------------------
每个 CPU 有一个 RCU_SOFTIRQ 类型的软中断,软中断处理函数为 rcu_process_callbacks()。
-----------------------------------------
static void rcu_init_percpu_data(int cpu, struct rcu_ctrlblk *rcp,
struct rcu_data *rdp)
{
memset(rdp, 0, sizeof(*rdp));
初始化了三个链表,分别是 curlist , nextlist和 donelist。
rdp->curtail = &rdp->curlist;
rdp->nxttail = &rdp->nxtlist;
rdp->donetail = &rdp->donelist;
这个 quiescbatch 成员表示 rcu_data 已经完成的 grace period 序号,
(在代码中也被称为了batch),
将 quiescbatch = completed,表示不需要等待 grace period.
rdp->quiescbatch = rcp->cCT_TARGET_VENDORompleted;
rdp->qs_pending = 0;
rdp->cpu = cpu;
rdp->blimit = blimit;
}
******************************************
这个 rcu_offline_cpu() 函数是一个可选函数,
当 没有配置 CONFIG_HOTPLUG_CPU 这个选项时,这个rcu_offline_cpu() 函数是空函数。
-----------------------------------------
static void rcu_offline_cpu(int cpu)
{
struct rcu_data *this_rdp = &get_cpu_var(rcu_data);
struct rcu_data *this_bh_rdp = &get_cpu_var(rcu_bh_data);
__rcu_offline_cpu(this_rdp, &rcu_ctrlblk,
&per_cpu(rcu_data, cpu));
__rcu_offline_cpu(this_bh_rdp, &rcu_bh_ctrlblk,
&per_cpu(rcu_bh_data, cpu));
是否掉 per_cpu 数据 rcu_data 和 rcu_bh_data。
put_cpu_var(rcu_data);
put_cpu_var(rcu_bh_data);
释放相应 CPU 的 RCU_SOFTIRQ 软中断。
tasklet_kill_immediate(&per_cpu(rcu_taskleubuntu:/opt/my-crosstool/cross-ngt, cpu), cpu);
}
******************************************
在 CPU 管理的通知链表 cpu_chain 注册通知结构 rcu_nb。
static struct notifier_block __cpuinitdata rcu_nb = {
.notifier_call = rcu_cpu_notify,
};
这个通知结构的回调函数为 rcu_cpu_notify() ,在上面也讲解过。
-----------------------------------------
这个 Raw 类型的通知链表 cpu_chain 是在 kernel/cpu.c 文件中定义的。
static __cpuinitdata RAW_NOTIFIER_HEAD(cpu_chain);
int __cpuinit register_cpu_notifier(struct notifier_block *nb)
{
int ret;
锁定 cpu_add_remove_lock 互斥锁。
mutex_lock(&cpu_add_remove_lock);
在 Raw 类型的通知链表 cpu_chain 上注册通知结构 nb。
ret = raw_notifier_chain_register(&cpu_chain, nb);
释放 cpu_add_remove_lock 互斥锁。
mutex_unlock(&cpu_add_remove_lock);
return ret;
}
-----------------------------------------
int raw_notifier_chain_register(struct raw_notifier_head *nh,
struct notifier_block *n)
{
return notifier_chain_register(&nh->head, n);
}
******************************************
最后,总结一下,RCU 机制在 linux 中的实现。
一) 首先讲一下, RCU 机制使用的数据结构。
1) 总控制单元
在 linux 中有两种 RCU 锁, 一种是和软中断相关的,一种是和软中断不相关的,
每种锁有一个总的控制单元,就有两种数据结构:
static struct rcu_ctrlblk rcu_ctrlblk = {
.cur = -300,
.completed = -300,
.lock = __SPIN_LOCK_UNLOCKED(&rcu_ctrlblk.lock),
.cpumask = CPU_MASK_NONE,
};
static struct rcu_ctrlblk rcu_bh_ctrlblk = {
.cur = -300,
.completed = -300,
.lock = __SPIN_LOCK_UNLOCKED(&rcu_bh_ctrlblk.lock),
.cpumask = CPU_MASK_NONE,
};
2) per_cpu 数据
这种 RCU 机制需要统计每个 CPU 是否经过了静止状态(quiescent state)。
所以每个 CPU 也就需要一个数据结构,来统计自己是否经过了静止状态(quiescent state)。
就有了 per_cpu 数据(在 include/linux/rcupdate.h 文件定义):
DECLARE_PER_CPU(struct rcu_data, rcu_data);
DECLARE_PER_CPU(struct rcu_data, rcu_bh_data);
二) RCU 的操作流程
1) 读者操作
对于读者,RCU 仅需要使内核抢占失效,因此获得读锁和释放读锁分别定义为:
#define rcu_read_lock() preempt_disable()
#define rcu_read_unlock() preempt_enable()
而和软中断相关的 RCU 锁,在进行临界区时,禁止了软中断饥和禁止抢占,使用下面的函数:
#define rcu_read_lock_bh() local_bh_disable()
#define rcu_read_unlock_bh() local_bh_enable()
2) 写者的操作
在写者访问数据时首先拷贝一个副本,然后对副本进行修改,最后调用 call_rcu() 函数
把一个回调函数注册在当前 CPU 的 rcu_data 或者 rcu_bh_data 的 nextlist 链表注册新的回调函数。
这个 RCU 等待一个 grace period 周期,如果在当前 CPU 上执行这个 回调函数。
3) 在进程切换时
在每一次进程切换的时候,都会调用rcu_qsctr_inc().如下代码片段如示:
asmlinkage void __sched schedule(void)
{
......
rcu_qsctr_inc(cpu);
......
}
Rcu_qsctr_inc()代码如下:
static inline void rcu_qsctr_inc(int cpu)
{
struct rcu_data *rdp = &per_cpu(rcu_data, cpu);
rdp->passed_quiesc = 1;
}
该函数将对应 CPU 上的 rcu_data 的 passed_quiesc 成员设为了 1 。
这个过程就标识该 CPU 经过了一次静止状态( quiescent state)。
4) 在 timer 中断时
每个 CPU 都会在每次时钟中断时,周期性的检测在总控制单元 rcu_ctrlblk 和 rcu_bh_ctrlblk 中是否有
RCU 的回调函数需要处理。如果没有则跳过,如果有则执行下面的操作,
本地 CPU 是否经过了一个静止状态( quiescent state)。
也检测本地 CPU 上的 RCU 回调函数经过了静止状态( quiescent state),
如果经过了则调用 RCU 回调函数的处理函数 rcu_do_batch() 函数。
三) 数据结构分析
这种 RCU 机制,要求写者在访问它时首先拷贝一个副本,然后对副本进行修改,最后使用一个
回调(callback)机制在适当的时机把指向原来数据的指针重新指向新的被修改的数据。
这个时机就是所有引用该数据的 CPU 都退出对共享数据的操作。
即要求写者必须要等它之前的读者全部都退出之后才能释放之前的资源。
这里规定了是那个 CPU 修改的资源,必须有那个 CPU 释放资源的副本。
所以每一个 CPU 都提供了一个链表 nxtlist ,供写者在这里注册回调函数,
struct rcu_head *nxtlist; 当调用 call_rcu()时在 nextlist 链表注册新的回调函数
struct rcu_head **nxttail; 指向 nextlist 链表最后一个节点 next 指针的地址
每一个 CPU 都提供了一个链表 curlist ,表示当前正在等待 静止状态(quiescent state)的链表。
struct rcu_head *curlist; 是指当前等待 grace period 的 RCU 的回调函数的链表。
struct rcu_head **curtail;
每一个 CPU 都提供了一个链表 doenlist ,表示已经等待到 grace period 的可以释放资源副本的链表。
struct rcu_head *donelist; 是指已经等待到 grace period 的 RCU 的回调函数的链表。
struct rcu_head **donetail;
在 RCU 机制中,最困难的是判断这个 RCU 已经经过了一次 静止状态(quiescent state)。
读者在调用 rcu_read_lock() 的时候要禁止抢占,所以如果读者仍然在这个临界区内,就不会发生上下文切换。
因此,我们只需要判断如有的 CPU 都经过了一次上下文切换,就说明所有读者已经退出了。
而 和软中断相关的 RCU 锁,读者调用 rcu_read_lock_bh() 在进入读者临界区时,禁止了软中断饥和禁止抢占,
基于同样的原因,如果所有 CPU 的都经过了一次上下文切换或者软中断,说明 所有读者已经退出了。
******************************************
问题:
1)《Linux RCU 机制详解》没有看懂,需要再看一遍?
linux-mips启动分析(11)
在 start_kernel() 函数中调用 init_IRQ() 函数,用来初始化
中断处理硬件相关的寄存器和 中断描述符数组 irq_desc[] 数组,
每个中断号都有一个对应的中断描述符。
移植相关,移植 linux 内核需要自己写 arch_init_irq() 函数的实现。
=============================================================
void __init init_IRQ(void)
{
移植相关,移植 linux 内核需要自己写 arch_init_irq() 函数的实现。
arch_init_irq();
下面的内容先不看了,以后再看和 KBDG 相关的内容。
#ifdef CONFIG_KGDB
if (kgdb_flag) {
printk("Wait for gdb client connection ...\n");
set_debug_traps();
breakpoint();
}
#endif
}
******************************************
void __init arch_init_irq(void)
{
unsigned long cp0_status;
设置 CP0 状态寄存器和中断相关的位。
清除 ERL 位,并屏蔽除了 HW0 外的所有中断, HW0(IP2)连接到中断控制器的中断申请。
这个 CP0 状态寄存器 IM[8:15]的其他位,没有使用。
如果有另外一个中断控制器,可能使用这个 IM[8:15]的其他位。
cp0_status = read_c0_status();
cp0_status &= ~0x4;
cp0_status |= 0x00000400;
set_c0_status(cp0_status & 0xffff04ff);
设置 irq_desc[] 数组。
soc_init_irq();
}
********************************************
/**
* struct irq_chip - hardware interrupt chip descriptor
*
* @name: name for /proc/interrupts
* @startup: start up the interrupt (defaults to ->enable if NULL)
* @shutdown: shut down the interrupt (defaults to ->disable if NULL)
* @enable: enable the interrupt (defaults to chip->unmask if NULL)
* @disable: disable the interrupt (defaults to chip->mask if NULL)
* @ack: start of a new interrupt
* @mask: mask an interrupt source
* @mask_ack: ack and mask an interrupt source
* @unmask: unmask an interrupt source
* @eoi: end of interrupt - chip level
* @end: end of interrupt - flow level
* @set_affinity: set the CPU affinity on SMP machines
* @retrigger: resend an IRQ to the CPU
* @set_type: set the flow type (IRQ_TYPE_LEVEL/etc.) of an IRQ
* @set_wake: enable/disable power-management wake-on of an IRQ
*
* @release: release function solely used by UML
* @typename: obsoleted by name, kept as migration helper
*/
struct irq_chip {
const char *name;
unsigned int (*startup)(unsigned int irq);
void (*shutdown)(unsigned int irq);
void (*enable)(unsigned int irq);
void (*disable)(unsigned int irq);
void (*ack)(unsigned int irq);
void (*mask)(unsigned int irq);
void (*mask_ack)(unsigned int irq);
void (*unmask)(unsigned int irq);
void (*eoi)(unsigned int irq);
void (*end)(unsigned int irq);
void (*set_affinity)(unsigned int irq, cpumask_t dest);
int (*retrigger)(unsigned int irq);
int (*set_type)(unsigned int irq, unsigned int flow_type);
int (*set_wake)(unsigned int irq, unsigned int on);
/* Currently used only by UML, might disappear one day.*/
#ifdef CONFIG_IRQ_RELEASE_METHOD
void (*release)(unsigned int irq, void *dev_id);
#endif
/*
* For compatibility, ->typename is copied into ->name.
* Will disappear.
*/
const char *typename;
};
linux 使用 struct irq_chip 结构体过定义一组接口方法,将不同的中断硬件行为统一进行了封装。
一方面底层中断硬件驱动只要根据自身功能部分或全部实现这组方法,向上提供满足需要的中断处理功能;
另一方面负责中断处理流程的 handle_irq,通过使用 chip 的方法与硬件交互。
----------------------------------------
struct irq_desc {
irq_flow_handler_t handle_irq; /*高级中断事件控制*/
struct irq_chip chip; /*低级别硬件中断*/
struct msi_desc msi_desc; /*可屏蔽软中断描述符*/
void * handler_data;
void * chip_data;
struct irqaction * action; /* IRQ 服务程序链表 */
unsigned int status; /* IRQ 状态 */
unsigned int depth; /* 中断禁止次数,也称为禁止深度*/
unsigned int wake_depth; /* nested wake enables */
unsigned int irq_count; /* 中断发生次数*/
unsigned int irqs_unhandled; /*未处理中断的计数,这个的计数是在HZ/10内未处理的中断数*/
unsigned long last_unhandled; /* 未处理中断计数的计时,他的值是上一次发生未处理中断时的jiffies值 */
spinlock_t lock; /*用于串行访问中断描述符数组的自旋锁*/
#ifdef CONFIG_SMP
cpumask_t affinity;
unsigned int cpu; /*SMP中cup的索引号,用于平衡调度*/
#endif
#if defined(CONFIG_GENERIC_PENDING_IRQ) || defined(CONFIG_IRQBALANCE)
cpumask_t pending_mask;
#endif
#ifdef CONFIG_PROC_FS
struct proc_dir_entry *dir; /* 在/proc/irq/中对应的文件 */
#endif
const char * name; /*要在interrupts中显示的名字*/
} ____cacheline_internodealigned_in_smp;
这是在SMP中使用的一个数据结构,当然在单cpu(更准确的说应该是单核cpu中)有一些数据项是没有使用的。
这其中比较重要的数据项是 action 和 status。
一个是真正要执行服务程序的一个链表,另一个是当前irq的状态。
----------------------------------------
这个函数初始化 irq_desc[] 数组,每个中断号都有一个对应的中断描述符,这些
描述符组织在一起形成 irq_desc[] 数组。
struct irq_desc irq_desc[NR_IRQS] __cacheline_aligned_in_smp = {
[0 ... NR_IRQS-1] = {
.status = IRQ_DISABLED,
.chip = &no_irq_chip,
.handle_irq = handle_bad_irq,
.depth = 1,
.lock = __SPIN_LOCK_UNLOCKED(irq_desc->lock),
#ifdef CONFIG_SMP
.affinity = CPU_MASK_ALL
#endif
}
} ;
这个 NR_IRQS 宏是中断描述符的数量,默认是 128 个。
#define NR_IRQS 128
----------------------------------------
void __init soc_init_irq(void)
{
int i;
关闭从 0 到 31 的中断,并设置从 0 到 32 的中断的底层硬件为中断控制器。
for (i=0;i<32;i++) {
disable_intc_irq(i);
irq_desc[i].chip= &intc_irq_type;
}
设置从 32 到 37 的中断描述符的 chip 指针为 DMA 类型的中断操作。
for (i=0;i
irq_desc[IRQ_DMA_0+i].chip = &dma_irq_type;
使能中断控制器中 DMA 中断控制位。
enable_intc_irq(IRQ_DMAC);
设置从 40 到 43 的中断描述符的 chip 指针为 PCIC 类型的中断操作。
for (i=0;i irq_desc[IRQ_PCIC_0+i].chip = &pcic_irq_type;
使能中断控制器中的 PCIC 的的中断控制位。
enable_intc_irq(IRQ_PCIC);
设置从 48 到 117 的中断描述符的 chip 指针为 GPIO 类型的中断操作。
for (i=0;i irq_desc[IRQ_GPIO_0+i].status = IRQ_DISABLED;
irq_desc[IRQ_GPIO_0+i].action = NULL;
irq_desc[IRQ_GPIO_0+i].depth = 1;
irq_desc[IRQ_GPIO_0+i].chip = &gpio_irq_type;
}
使能中断控制器中的 GPIO 的中断控制位。
enable_intc_irq(IRQ_GPIO0);
enable_intc_irq(IRQ_GPIO1);
enable_intc_irq(IRQ_GPIO2);
}
******************************************
在 soc_init_irq() 函数中,最主要的操作是对中断描述符的 chip 指针赋值。
其中共有四种类型的中断底层操作,intc_irq_type、dma_irq_type,
pcic_irq_type 和 gpio_irq_type 。
----------------------------------------
static struct irq_chip intc_irq_type = {
.name = "INTC",
.startup = startup_intc_irq,
.shutdown = shutdown_intc_irq,
.enable = enable_intc_irq,
.disable = disable_intc_irq,
.ack = mask_and_ack_intc,
.mask = disable_intc_irq,
.mask_ack = mask_and_ack_intc,
.unmask = enable_intc_irq,
.end = end_intc_irq,
};
----------------------------------------
static struct irq_chip dma_irq_type = {
.name = "DMA",
.startup = startup_dma_irq,
.shutdown = shutdown_dma_irq,
.enable = enable_dma_irq,
.disable = disable_dma_irq,
.ack = mask_and_ack_dma_irq,
.mask = disable_dma_irq,
.mask_ack = mask_and_ack_dma_irq,
.unmask = enable_dma_irq,
.end = end_dma_irq,
};
----------------------------------------
static struct irq_chip pcic_irq_type = {
.name = "PCIC",
.startup = startup_pcic_irq,
.shutdown = shutdown_pcic_irq,
.enable = enable_pcic_irq,
.disable = disable_pcic_irq,
.ack = mask_and_ack_pcic_irq,
.mask = disable_pcic_irq,
.mask_ack = mask_and_ack_pcic_irq,
.unmask = enable_pcic_irq,
.end = end_pcic_irq,
};
----------------------------------------
static struct irq_chip gpio_irq_type = {
.name = "GPIO",
.startup = startup_gpio_irq,
.shutdown = shutdown_gpio_irq,
.enable = enable_gpio_irq,
.disable = disable_gpio_irq,
.ack = mask_and_ack_gpio_irq,
.mask = disable_gpio_irq,
.mask_ack = mask_and_ack_gpio_irq,
.unmask = enable_gpio_irq,
.end = end_gpio_irq,
};
******************************************
问题:
1) 中断描述符中的 chip 指针中的函数的调用?
linux-mips启动分析(12)
在 start_kernel() 函数中调用 pidhash_init() 函数。
在 linux 内核中,有很多情况下需要从进程的 PID 导出对应的进程描述符指针。
为了这个操作,系统在初始化阶段动态的分配了 4 个 hashtable,并把它们的地址
存入 pid_hash[] 数组。
下面是四种散列表和进程描述符中相关字段:
----------------------------------------
Hash table type | Field name | Description
----------------------------------------
PIDTYPE_PID | pid | 进程的 PID
PIDTYPE_TGID | tgid | 线程组的领头进程的 PID
PIDTYPE_PGID | pgid | 进程组的领头进程的 PID
PIDTYPE_SID | session | 会话组的领头进程的 PID
----------------------------------------
这个 pidhash_init() 函数为全局变量 pidhash_shift 和 pid_hash 进行赋值。
这两个变量是在 kernel/pid.c 文件中定义的。
static int pidhash_shift;
static struct hlist_head *pid_hash;
----------------------------------------
散列表讲解:
struct hlist_head {
struct hlist_node *first;
};
struct hlist_node {
struct hlist_node *next, **pprev;
};
散列表的表头存放在 hlist_head 数据结构中,这个结构仅仅是指向第一个节点的指针,
如果链表为空,这个指针为 NULL。
这个散列表的每一个节点的类型为 struct hlist_node 数据结构,
每个节点有两个指针,其中 next 指针指向下一个节点,
另一个指针 pprev 指针指向前一个节点的 next 指针字段(它是个指针的指针,指向前一个节点
的 next 字段,而前一个节点的 next 字段又指向这个节点)。
因为不是循环链表,所以第一个节点的 pprev 指针和 最后一个节点的 next 指针为 NULL 指针。
==============================================================
void __init pidhash_init(void)
{
int i, pidhash_size;
这个 nr_kernel_pages 变量在 free_area_init_core() 函数中赋值,
参考《linux-mips启动分析(4-3)》,表示不包含高端内存的系统内存共有的内存页面数。
通过下面的公式计算出不包含高端内存的系统内存共有 多少 MB。
unsigned long megabytes = nr_kernel_pages >> (20 - PAGE_SHIFT);
下面步骤计算出 pid_hash 表的表项数目。这个 hashtable 的长度依赖于不包含高端内存
的系统内存的大小。
这个 pidhash_shift 的范围是 从 4 到 12 。hashtable 的长度从 16 到 4096 项。
pidhash_shift = max(4, fls(megabytes * 4));
pidhash_shift = min(12, pidhash_shift);
pidhash_size = 1 << pidhash_shift;
为 PID hash 表分配空间。
pid_hash = alloc_bootmem(pidhash_size * sizeof(*(pid_hash)));
if (!pid_hash)
panic("Could not alloc pidhash!\n");
初始化 PID 哈希表的头节点。
for (i = 0; i < pidhash_size; i++)
INIT_HLIST_HEAD(&pid_hash[i]);
}
******************************************
问题:
1)应该是四个 PID hash 表,为什么只分配了一个?
linux-mips启动分析(13)
在 star_kernel() 函数中调用 init_timers() 函数。
这个函数完成如下功能:
1)初始化本 CPU 上的定时器(timer)相关的数据结构
2)向 cpu_chain 通知链注册元素 timers_nb,该元素的回调函数用于初始化指定 CPU 上的定时器相关的数据结构。
3) 初始化时钟的软中断处理函数
参考《Linux内部的时钟处理机制全面剖析》。
参考《深入理解 linux 内核》。
参考《linux通知链表机制》。
================================================================
void __init init_timers(void)
{
初始当前 CPU 的定时器链表。
int err = timer_cpu_notify(&timers_nb, (unsigned long)CPU_UP_PREPARE,
(void *)(long)smp_processor_id());
如果没有定义 CONFIG_TIMER_STATS 这个宏,则这个 init_timer_stats() 函数为空函数。
init_timer_stats();
BUG_ON(err == NOTIFY_BAD);
在 CPU 管理的通知链表 cpu_chain 注册通知结构 timers_nb。
register_cpu_notifier(&timers_nb);
注册时钟软中断 TIMER_SOFTIRQ,它的处理函数为 run_timer_softirq()。
这个处理函数如下所示。
open_softirq(TIMER_SOFTIRQ, run_timer_softirq, NULL);
}
******************************************
这个函数是 注册到 CPU 子系统的通知链表的回调函数。
这个函数主要是在新的 CPU 开始工作或者停止工作(支持热插拔)时,通知 timer 管理器,
使 timer 管理器管理器知道,并进行相应的处理。
----------------------------------------
static int __cpuinit timer_cpu_notify(struct notifier_block *self,
unsigned long action, void *hcpu)
{
long cpu = (long)hcpu;
switch(action) {
case CPU_UP_PREPARE:
case CPU_UP_PREPARE_FROZEN:
if (init_timers_cpu(cpu) < 0)
return NOTIFY_BAD;
break;
#ifdef CONFIG_HOTPLUG_CPU
case CPU_DEAD:
case CPU_DEAD_FROZEN:
migrate_timers(cpu);
break;
#endif
default:
break;
}
return NOTIFY_OK;
}
******************************************
注册到通知链表的回调函数支持两类事件( UP 和 DEAD 类型的事件),
下面为 支持 CPU_UP_PREPARE 和 CPU_UP_PREPARE_FROZEN 类型事件的函数。
----------------------------------------
static int __devinit init_timers_cpu(int cpu)
{
int j;
tvec_base_t *base;
定义静态局部变量 tvec_base_done[] 数组,表示是否已经初始化了由索引号表示的 CPU。
static char __devinitdata tvec_base_done[NR_CPUS];
检测 CPU 的初始化是否已经做过,如果没有做过,继续,如果已经做过,则跳过。
if (!tvec_base_done[cpu]) {
定义静态全局变量,表示是否是第一个启动的 CPU 的初始化。
static char boot_done;
如果不是启动 CPU,则走这个路径。
if (boot_done) {
为每个 CPU 分配 struct tvec_t_base_s 结构的空间。
base = kmalloc_node(sizeof(*base), GFP_KERNEL,
cpu_to_node(cpu));
if (!base)
return -ENOMEM;
if (tbase_get_deferrable(base)) {
WARN_ON(1);
kfree(base);
return -ENOMEM;
}
把这个结构体空间清 0。
memset(base, 0, sizeof(*base));
设置 per_cpu 变量中的指针指向分配的空间。
per_cpu(tvec_bases, cpu) = base;
} else {
这个路径是在 boot 阶段,启动的第一个 CPU 的路径。
设置标志已经启动过了。
由于在启动阶段 per_cpu 数据没有准备好,所以使用静态定义的结构。
这是应为 per_cpu 数据是一个指针,尽管 per_cpu 中为指针预留了空间,
但是指针指向的空间,没有进行分配,都是执行 boot_tvec_bases 的。
boot_done = 1;
base = &boot_tvec_bases;
}
设置 CPU 已经设置过了。
tvec_base_done[cpu] = 1;
} else {
base = per_cpu(tvec_bases, cpu);
}
spin_lock_init(&base->lock);
lockdep_set_class(&base->lock, base_lock_keys + cpu);
初始化 tvec_t_base_s 结构中每个链表的头节点。
for (j = 0; j < TVN_SIZE; j++) {
INIT_LIST_HEAD(base->tv5.vec + j);
INIT_LIST_HEAD(base->tv4.vec + j);
INIT_LIST_HEAD(base->tv3.vec + j);
INIT_LIST_HEAD(base->tv2.vec + j);
}
for (j = 0; j < TVR_SIZE; j++)
INIT_LIST_HEAD(base->tv1.vec + j);
base->timer_jiffies = jiffies;
return 0;
}
----------------------------------------
在这个 init_timers_cpu() 函数中,使用了全局变量 boot_tvec_bases。
它的定义如下所示:
typedef struct tvec_s {
struct list_head vec[TVN_SIZE];
} tvec_t;
typedef struct tvec_root_s {
struct list_head vec[TVR_SIZE];
} tvec_root_t;
struct tvec_t_base_s {
spinlock_t lock;
struct timer_list *running_timer;
unsigned long timer_jiffies;
tvec_root_t tv1;
tvec_t tv2;
tvec_t tv3;
tvec_t tv4;
tvec_t tv5;
} ____cacheline_aligned;
typedef struct tvec_t_base_s tvec_base_t;
tvec_base_t boot_tvec_bases;
----------------------------------------
lock spinlock_t 用于同步操作
----------------------------------------
running_timer struct timer_list * 正在处理的定时器
----------------------------------------
timer_jiffies unsigned long 当前正在处理的定时器到期时间
----------------------------------------
tv1 struct tvec_root 保存了到期时间从 timer_jiffies 到(2^8 -1)
之间(包括边缘值)的所有定时器
----------------------------------------
tv2 struct tvec 保存了到期时间从 timer_jiffies +(2^8)到
timer_jiffies + (2^14-1)之间(包括边缘值)的 所有定时器
----------------------------------------
tv3 struct tvec 保存了到期时间从 timer_jiffies +(2^14)到
timer_jiffies +(2^20-1)之间(包括边缘值)的所有定时器
----------------------------------------
tv4 struct tvec 保存了到期时间从 timer_jiffies +(2^20)到
timer_jiffies + (2^26-1)之间(包括边缘值)的所有定时器
----------------------------------------
tv5 struct tvec 保存了到期时间从 timer_jiffies +(2^16)到
timer_jiffies +(2^32-1)之间(包括边缘值)的所有定时器
******************************************
下面为 支持 CPU_DEAD 和 CPU_DEAD_FROZEN 类型事件的函数。
当没有配置这个宏 CONFIG_HOTPLUG_CPU 定义时,即系统不支持 CPU 热插拔时,
这个 migrate_timers() 为空函数;
当配置了这个宏定义时,这个函数如下所示。
----------------------------------------
static void __devinit migrate_timers(int cpu)
{
tvec_base_t *old_base;
tvec_base_t *new_base;
int i;
首先检查这个 CPU 是否在工作,如果仍然在工作,则发出警告信息。
BUG_ON(cpu_online(cpu));
这个 tvec_bases 是一个 per_cpu 变量,是 tvec_base_t 的指针。
这个 per_cpu() 函数取得这个 cpu 的私有变量,即tvec_base_t 的指针
old_base = per_cpu(tvec_bases, cpu);
这个 get_cpu_var()函数取得当前 cpu 的tvec_base_t 的指针 。
new_base = get_cpu_var(tvec_bases);
禁止当前 cpu 的中断,同样也禁止了内核抢占。
local_irq_disable();
根据需求锁定这两个自旋锁 new_base->lock, old_base->lock 。
double_spin_lock(&new_base->lock, &old_base->lock, smp_processor_id() < cpu);
如果参数 cpu 上有正在发生的定时器,则发出 bug 信息。
BUG_ON(old_base->running_timer);
把参数 cpu 上注册的 tv1 定时器链表上的定时器迁移到当前 cpu 的定时器链表上。
for (i = 0; i < TVR_SIZE; i++)
migrate_timer_list(new_base, old_base->tv1.vec + i);
把参数 cpu 上注册的 tv2、 tv3、tv4、tv5 定时器链表上的定时器迁移到当前 cpu 的定时器链表上。
for (i = 0; i < TVN_SIZE; i++) {
migrate_timer_list(new_base, old_base->tv2.vec + i);
migrate_timer_list(new_base, old_base->tv3.vec + i);
migrate_timer_list(new_base, old_base->tv4.vec + i);
migrate_timer_list(new_base, old_base->tv5.vec + i);
}
根据需求为这两个自旋锁 new_base->lock, old_base->lock 解锁。
double_spin_unlock(&new_base->lock, &old_base->lock, smp_processor_id() < cpu);
恢复本地 cpu 的中断。
local_irq_enable();
put_cpu_var(tvec_bases);
}
----------------------------------------
static void migrate_timer_list(tvec_base_t *new_base, struct list_head *head)
{
struct timer_list *timer;
如果定时器链表为 空链表,则直接退出。
while (!list_empty(head)) {
timer = list_first_entry(head, struct timer_list, entry);
detach_timer(timer, 0);
timer_set_base(timer, new_base);
internal_add_timer(new_base, timer);
}
}
********************************************
static inline void double_spin_lock(spinlock_t *l1, spinlock_t *l2,
bool l1_first)
__acquires(l1)
__acquires(l2)
{
使用标志 l1_first 判断先锁定这两个中的那个锁,以防止发生死锁现象。
if (l1_first) {
spin_lock(l1);
spin_lock(l2);
} else {
spin_lock(l2);
spin_lock(l1);
}
}
----------------------------------------
# define __acquires(x) __attribute__((context(x,0,1)))
********************************************
如果没没有定义这个 CONFIG_TIMER_STATS 宏,这个 init_timer_stats() 函数就是个空函数。
这个 CONFIG_TIMER_STATS 宏的作用是是否在 /proc 文件系统中生成 timer_stats 文件,
这个文件允许你查看Linux内核里使用定时器的常规事件一些信息。
通过查看这个文件,你可以看到那些常规事件使用定时器的次数最多,使用的频率是多少。
更详细的信息可以参考内核源码树下面的 Documentation/filesystems/proc.txt 文件。
如果定义了如下所示:
这个 init_timer_stats() 函数对于每个 CPU 来说初始化了 timer_stat 的自旋锁。
----------------------------------------
void __init init_timer_stats(void)
{
int cpu;
for_each_possible_cpu(cpu)
spin_lock_init(&per_cpu(lookup_lock, cpu));
}
*******************************************
在 CPU 管理的通知链表 cpu_chain 注册通知结构 timers_nb。
static struct notifier_block __cpuinitdata timers_nb = {
.notifier_call = timer_cpu_notify,
};
这个通知结构的回调函数为 timer_cpu_notify() ,在上面也讲解过。
-----------------------------------------
这个 Raw 类型的通知链表 cpu_chain 是在 kernel/cpu.c 文件中定义的。
static __cpuinitdata RAW_NOTIFIER_HEAD(cpu_chain);
int __cpuinit register_cpu_notifier(struct notifier_block *nb)
{
int ret;
锁定 cpu_add_remove_lock 互斥锁。
mutex_lock(&cpu_add_remove_lock);
在 Raw 类型的通知链表 cpu_chain 上注册通知结构 nb。
ret = raw_notifier_chain_register(&cpu_chain, nb);
释放 cpu_add_remove_lock 互斥锁。
mutex_unlock(&cpu_add_remove_lock);
return ret;
}
******************************************
这个函数 run_timer_softirq() 是 TIMER_SOFTIRQ 的处理函数。
这个函数对当前 CPU 到期的定时器进行处理。
参考《深入理解 linux 内核》第六章。
-----------------------------------------
static void run_timer_softirq(struct softirq_action *h)
{
首先获得到本地 CPU 的定时器链表的 base 地址。
tvec_base_t *base = __get_cpu_var(tvec_bases);
这个函数和高精度时钟定时器有关,检测高精度时钟定时器是否 active,如果是 active 的就替换到
hres tick机制。参考《linux高精度时钟分析》。
hrtimer_run_queues();
检测如果 jiffies大于等于 timer_jiffies ,说明可能已经有软件时钟到期了,
此时就要进行软件时钟的处理,调用函数 __run_timers() 函数 进行处理。
如果 jiffies 小于 timer_jiffies ,表明没有软件时钟到期,则不用对软件时钟进行处理。函数返回。
if (time_after_eq(jiffies, base->timer_jiffies))
__run_timers(base);
}
******************************************
在 TIMER_SOFTIRQ 软中断的处理函数调用 __run_timers() 对到期的定时器进行处理。
-----------------------------------------
static inline void __run_timers(tvec_base_t *base)
{
struct timer_list *timer;
spin_lock_irq(&base->lock);
while (time_after_eq(jiffies, base->timer_jiffies)) {
struct list_head work_list;
struct list_head *head = &work_list;
int index = base->timer_jiffies & TVR_MASK;
if (!index &&
(!cascade(base, &base->tv2, INDEX(0))) &&
(!cascade(base, &base->tv3, INDEX(1))) &&
!cascade(base, &base->tv4, INDEX(2)))
cascade(base, &base->tv5, INDEX(3));
++base->timer_jiffies;
list_replace_init(base->tv1.vec + index, &work_list);
while (!list_empty(head)) {
void (*fn)(unsigned long);
unsigned long data;
timer = list_first_entry(head, struct timer_list,entry);
fn = timer->function;
data = timer->data;
timer_stats_account_timer(timer);
set_running_timer(base, timer);
detach_timer(timer, 1);
spin_unlock_irq(&base->lock);
{
int preempt_count = preempt_count();
fn(data);
if (preempt_count != preempt_count()) {
printk(KERN_WARNING "huh, entered %p "
"with preempt_count %08x, exited"
" with %08x?\n",
fn, preempt_count,
preempt_count());
BUG();
}
}
spin_lock_irq(&base->lock);
}
}
set_running_timer(base, NULL);
spin_unlock_irq(&base->lock);
}
-----------------------------------------
1. 获得 base 的同步锁
2. 如果 jiffies 大于等于 timer_jiffies (当前正要处理的软件时钟的到期时间,
说明可能有软件时钟到期了),就一直运行3~7,否则跳转至8
3. 计算得到 tv1 的索引,该索引指明当前到期的软件时钟所在 tv1 中的链表(结构参见3.2节),代码:
int index = base->timer_jiffies & TVR_MASK;
1. 调用 cascade 函数对软件时钟进行必要的调整(稍后会介绍调整的过程)
2. 使得 timer_jiffies 的数值增加1
3. 取出相应的软件时钟链表
4. 遍历该链表,对每个元素进行如下操作
* 设置当前软件时钟为 base 中正在运行的软件时钟(即保存当前软件时钟到 base-> running_timer 成员中)
* 将当前软件时钟从链表中删除,即卸载该软件时钟
* 释放锁,执行软件时钟处理程序
* 再次获得锁
1. 设置当前 base 中不存在正在运行的软件时钟
2. 释放锁
********************************************
问题:
1)在 init_timers_cpu() 函数中,为什么不直接使用 per_cpu() 函数定义的静态数组
而是动态分配呢?
在源码注释中,解释由于内存分配器没有工作,所以启动时使用静态分配的,但是在 RCU_init() 函数
中已经使用了 per_cpu() 函数了?
其实已经在 setup_per_cpu_areas() 函数为 per_cpu 数据分配了空间。
这是应为 per_cpu 数据是一个指针,尽管 per_cpu 中为指针预留了空间,
但是指针指向的空间,没有进行分配,都是执行 boot_tvec_bases 的。
参考《每CPU变量的数据组织和访问》。
2)这个 __acquires(x) 宏定义的意义?
3)与 timer_stat 相关的代码没有看?
linux-mips启动分析(14)
在 star_kernel() 函数中调用 hrtimers_init() 函数。
这个 hrtimers_init() 函数对高精度时钟进行初始化。
如果没有定义 CONFIG_HIGH_RES_TIMERS 宏,则不支持高精度事件定时器。
需要需要对高精度事件定时器的支持,需要打开这个 CONFIG_HIGH_RES_TIMERS 宏开关。
============================================================
static struct notifier_block __cpuinitdata hrtimers_nb = {
.notifier_call = hrtimer_cpu_notify,
};
----------------------------------------
void __init hrtimers_init(void)
{
初始当前 CPU 的高精度时钟。
hrtimer_cpu_notify(&hrtimers_nb, (unsigned long)CPU_UP_PREPARE,
(void *)(long)smp_processor_id());
在 CPU 管理子系统通知链表 cpu_chain 上注册通知结构 hrtimers_nb。
register_cpu_notifier(&hrtimers_nb);
#ifdef CONFIG_HIGH_RES_TIMERS
如果打开了高精度事件定时器开关,则初始化 HRTIMER_SOFTIRQ 软中断,
软中断处理函数为 run_hrtimer_softirq()。
open_softirq(HRTIMER_SOFTIRQ, run_hrtimer_softirq, NULL);
#endif
}
******************************************
这个函数是 注册到 CPU 子系统的通知链表的回调函数。
这个函数主要在新的 CPU 开始工作或者停止工作(支持热插拔)时,通知 hrtimer 管理器,
使 hrtimer 管理器管理器知道,并进行相应的处理。
可以支持 CPU_UP_PREPARE、CPU_UP_PREPARE_FROZEN、
CPU_DEAD 和 CPU_DEAD_FROZEN 类型的事件。
----------------------------------------
static int __cpuinit hrtimer_cpu_notify(struct notifier_block *self,
unsigned long action, void *hcpu)
{
long cpu = (long)hcpu;
switch (action) {
case CPU_UP_PREPARE:
case CPU_UP_PREPARE_FROZEN:
init_hrtimers_cpu(cpu);
break;
#ifdef CONFIG_HOTPLUG_CPU
case CPU_DEAD:
case CPU_DEAD_FROZEN:
clockevents_notify(CLOCK_EVT_NOTIFY_CPU_DEAD, &cpu);
migrate_hrtimers(cpu);
break;
#endif
default:
break;
}
return NOTIFY_OK;
}
******************************************
当 CPU 子系统发生 CPU_UP_PREPARE、CPU_UP_PREPARE_FROZEN 事件时,
下面的 init_hrtimers_cpu() 函数被 CPU 子系统通知链表的回调函数所调用。
----------------------------------------
这个 per_cpu 变量定义在 kernel/hrtimer.c 文件。
DEFINE_PER_CPU(struct hrtimer_cpu_base, hrtimer_bases) =
{
.clock_base =
{
{
.index = CLOCK_REALTIME,
.get_time = &ktime_get_real,
.resolution = KTIME_LOW_RES,
},
{
.index = CLOCK_MONOTONIC,
.get_time = &ktime_get,
.resolution = KTIME_LOW_RES,
},
}
};
为 struct hrtimer_cpu_base 结构体类型,每个 CPU 一个。
----------------------------------------
static void __devinit init_hrtimers_cpu(int cpu)
{
取得参数 cpu 的 per_cpu 数据 hrtimer_bases。
struct hrtimer_cpu_base *cpu_base = &per_cpu(hrtimer_bases, cpu);
int i;
初始化自旋锁。
spin_lock_init(&cpu_base->lock);
初始化锁的依赖关系。
lockdep_set_class(&cpu_base->lock, &cpu_base->lock_key);
for (i = 0; i < HRTIMER_MAX_CLOCK_BASES; i++)
cpu_base->clock_base[i].cpu_base = cpu_base;
如果没有定义 CONFIG_HIGH_RES_TIMERS 宏,则下面为空函数。
hrtimer_init_hres(cpu_base);
}
******************************************
问题:
1)对高精度事件定时器的处理需要更深入理解?
2)资料:Documentation/hrtimers/ 目录下文档?
linux-mips启动分析(15)
在 star_kernel() 函数中调用 init_timers() 函数。
这个函数初始化高优先级和低优先级的 tasklet 软中断处理函数。
初始化高优先级 tasklet 软中断处理函数 tasklet_action() 函数。
初始化低优先级的 tasklet 软中断处理函数 tasklet_hi_action() 函数。
============================================================
void __init softirq_init(void)
{
open_softirq(TASKLET_SOFTIRQ, tasklet_action, NULL);
open_softirq(HI_SOFTIRQ, tasklet_hi_action, NULL);
}
*******************************************
在 linux 系统中可以定义 32 个软中断,每一个软中断都是由 struct softirq_action 类型的结构体表示的,
所以在系统中定义了一个 softirq_vec[32] 数组,用来管理软中断。
struct softirq_action
{
void (*action)(struct softirq_action *);
void *data;
};
static struct softirq_action softirq_vec[32] __cacheline_aligned_in_smp;
----------------------------------------
这个 open_softirq() 函数初始化相应软中断的 struct softirq_action 类型的结构体。
----------------------------------------
void open_softirq(int nr, void (*action)(struct softirq_action*), void *data)
{
softirq_vec[nr].data = data;
softirq_vec[nr].action = action;
}
linux-mips启动分析(16)
在 start_kernel() 函数中调用 timekeeping_init() 函数来初始化系统计时器。
==============================================================
void __init timekeeping_init(void)
{
unsigned long flags;
unsigned long sec = read_persistent_clock();
使用顺序锁 xtime_lock 来锁定来包含与定时相关的内核变量。
锁定顺序锁 xtime_lock ,并禁止中断,保存中断标志。
write_seqlock_irqsave(&xtime_lock, flags);
清除 NTP(网络校时协议)接口的变量。
ntp_clear();
这个 clock 是静态全局变量,是 current clocksource 的指针。
clock = clocksource_get_next();
计算每次时钟中断(时钟嘀嗒)给 xtime 变量 tv_nsec 成员增加的纳秒值。
clocksource_calculate_interval(clock, NTP_INTERVAL_LENGTH);
读取上一次周期时钟嘀嗒值。
clock->cycle_last = clocksource_read(clock);
初始化 wall time 的值。
xtime.tv_sec = sec;
xtime.tv_nsec = 0;
把 xtime 上的时间调整成 struct timespec 格式,并保存到 wall_to_monotonic 变量中。
set_normalized_timespec(&wall_to_monotonic, -xtime.tv_sec, -xtime.tv_nsec);
释放顺序锁 xtime_lock ,恢复保存的中断标志。
write_sequnlock_irqrestore(&xtime_lock, flags);
}
******************************************
这个 ntp_clear() 函数清除 NTP(网络校时协议)接口的变量。
这些变量在 adjtimex() 系统调用时,能够使用到。
----------------------------------------
void ntp_clear(void)
{
time_adjust = 0;
time_status |= STA_UNSYNC;
time_maxerror = NTP_PHASE_LIMIT;
time_esterror = NTP_PHASE_LIMIT;
ntp_update_frequency();
tick_length = tick_length_base;
time_offset = 0;
}
*******************************************
这个 tick_usec 变量表示 一个时钟嘀嗒经过的多少微秒。
(tick_usec * NSEC_PER_USEC )表示一个时钟嘀嗒经过的多少纳秒。
(tick_usec * NSEC_PER_USEC * USER_HZ)表示 USER_HZ 个时钟嘀嗒经过的多少纳秒。
这个函数 ntp_update_frequency() 使用 tick_usec 计算 base tick 的长度,
这个 tick_length_base 变量。
----------------------------------------
static void ntp_update_frequency(void)
{
u64 second_length = (u64)(tick_usec * NSEC_PER_USEC * USER_HZ)
<< TICK_LENGTH_SHIFT;
加上这个 CLOCK_TICK_ADJUST 主要是为了防止 tick_length_base 不能够被 HZ 整除。
second_length += (s64)CLOCK_TICK_ADJUST << TICK_LENGTH_SHIFT;
second_length += (s64)time_freq << (TICK_LENGTH_SHIFT - SHIFT_NSEC);
tick_length_base = second_length;
这个 do_div() 是一个宏,这个宏的主要作用是把 64 位的 second_length 除以 HZ ,并赋值给
这个 second_length 变量,应为这个 do_div() 是一个宏,所以 这个 second_length 变量
可以不是指针。
do_div(second_length, HZ);
这个 tick_nsec 表示一个时钟嘀嗒经过的多少纳秒。
tick_nsec = second_length >> TICK_LENGTH_SHIFT;
把 tick_length_base 变量,除以 NTP_INTERVAL_FREQ,并把结果赋值给 tick_length_base 变量。
do_div(tick_length_base, NTP_INTERVAL_FREQ);
}
*******************************************
这个函数取得时钟源,时钟源在 arch/mips/kernel/time.c 文件中的 time_init() 函数
调用 init_mips_clocksource() 函数注册的时钟源。
参考《linux时钟管理机制》。
----------------------------------------
struct clocksource *clocksource_get_next(void)
{
unsigned long flags;
spin_lock_irqsave(&clocksource_lock, flags);
检测是否有时钟源,并且已经启动完毕,如果是,则使用注册的时钟源。
if (next_clocksource && finished_booting) {
curr_clocksource = next_clocksource;
next_clocksource = NULL;
}
spin_unlock_irqrestore(&clocksource_lock, flags);
如果不是,则使用默认的时钟源。
return curr_clocksource;
}
----------------------------------------
这 curr_clocksource 和 next_clocksource 变量都是静态全局变量。
在 kernel/time/clocksource.c 文件中定义。
static struct clocksource *curr_clocksource = &clocksource_jiffies;
static struct clocksource *next_clocksource;
*******************************************
在 timekeeping_init() 函数中,主要用到的新的数据结构为 struct clocksource,
下面对这个结构的成员进行一下讲解。
struct clocksource {
char *name; 时钟的名称
struct list_head list; 时钟注册链表
int rating;
cycle_t (*read)(void); 读取精确的单调时间计数的接口,
cycle_t mask;
u32 mult;
u32 shift;
unsigned long flags;
cycle_t (*vread)(void);
void (*resume)(void);
cycle_t cycle_interval;
u64 xtime_interval;
cycle_t cycle_last ____cacheline_aligned_in_smp;
u64 xtime_nsec;
s64 error;
#ifdef CONFIG_CLOCKSOURCE_WATCHDOG
/* Watchdog related data, used by the framework */
struct list_head wd_list;
cycle_t wd_last;
#endif
} ;
----------------------------------------
这个 struct clocksource 是一个硬件时钟的抽象化,提供了硬件时钟的底层操作。
最重要的成员是 read() 函数指针, cycle_last 和 cycle_interval 成员,
分别定义了读取硬件时钟寄存器当前计数值接口, 保存上一次周期计数值和每个tick周期间隔值。
这个结构的数组,都是以计数值(cycle) 为单位, 而不是 nsec,sec 和 jiffies 等。
read()是整个 kernel 读取精确的单调时间计数的接口,内核根据这个函数返回的值,
用来计算 jiffies 和 xtime。
*******************************************
问题:
1) NTP(网络校时协议)接口变量的使用?
2)这个 tick_length_base 变量的意义?
3)这个 clocksource_calculate_interval() 函数的意义?
linux-mips启动分析(17)
在 start_kernle() 函数中调用 time_init() 函数,初始化系统时钟源,
对 MIPS 体系结构的 clocksource_mips 时钟源进行配置,和注册。
这段代码是体系结构相关的,对于 MIPS 体系结构的位于 arch/mips/kernel/time.c 文件中。
=============================================================
void __init time_init(void)
{
这个 board_time_init 变量是一个函数指针,在 plat_mem_setup() 函数进行的赋值。
参考《linux-mips启动分析(4)》。
if (board_time_init)
board_time_init();
----------------------------------------
这个 rtc_mips_set_mmss 变量也是一个函数指针,如果没有设置,
则设置为 rtc_mips_set_time() 函数。
定义如下所示(在 arch/mips/kernel/time.c 文件中):
unsigned long (*rtc_mips_get_time)(void) = null_rtc_get_time;
int (*rtc_mips_set_time)(unsigned long) = null_rtc_set_time;
int (*rtc_mips_set_mmss)(unsigned long);
----------------------------------------
if (!rtc_mips_set_mmss)
rtc_mips_set_mmss = rtc_mips_set_time;
xtime.tv_sec = rtc_mips_get_time();
xtime.tv_nsec = 0;
把 xtime 中的时间转换成 timespec 格式,保存到 wall_to_monotonic 变量中。
set_normalized_timespec(&wall_to_monotonic, -xtime.tv_sec, -xtime.tv_nsec);
设置时钟源的 truct clocksource clocksource_mips 的 read 函数指针。
if (!cpu_has_counter && !clocksource_mips.read)
clocksource_mips.read = null_hpt_read;
else if (!mips_hpt_frequency && !mips_timer_state) {
if (!clocksource_mips.read)
clocksource_mips.read = c0_hpt_read;
} else {
if (!clocksource_mips.read) {
clocksource_mips.read = c0_hpt_read;
if (!mips_timer_state) {
mips_timer_ack = c0_timer_ack;
cycles_per_jiffy = (mips_hpt_frequency + HZ / 2) / HZ;
c0_hpt_timer_init();
}
}
if (!mips_hpt_frequency)
mips_hpt_frequency = calibrate_hpt();
打印 timer 的计算时钟频率。
printk("Using %u.%03u MHz high precision timer.\n";
}
if (!mips_timer_ack)
mips_timer_ack = null_timer_ack;
这个 plat_timer_setup() 函数是体系结构相关的,初始化硬件时钟。
参数是时钟中断 irqaction 描述符。
plat_timer_setup(&timer_irqaction);
init_mips_clocksource();
}
******************************************
这个 board_time_init 变量是一个函数指针,在 plat_mem_setup() 函数进行的赋值。
参考《linux-mips启动分析(4)》。
board_time_init = cq8401_time_init;
所以实际调用的为 cq8401_time_init() 函数,这个函数是移植相关的,
在移植内核时,写的代码。
----------------------------------------
unsigned int mips_hpt_frequency;
int (*mips_timer_state)(void);
void (*mips_timer_ack)(void);
struct clocksource clocksource_mips = {
.name = "MIPS",
.mask = CLOCKSOURCE_MASK(32),
.flags = CLOCK_SOURCE_IS_CONTINUOUS,
};
这些变量都是在 arch/mips/kernel/time.c 文件中定义的。
----------------------------------------
void __init cq8401_time_init(void)
{
mips_hpt_frequency = OST_CLK;
mips_timer_state = clxsoc_timer_state;
mips_timer_ack = taurus_timer_ack;
clocksource_mips.read = clxsoc_hpt_read;
}
******************************************
这个 plat_timer_setup() 函数是体系结构相关的,对硬件时钟进行初始化。
----------------------------------------
void __init plat_timer_setup(struct irqaction *irq)
{
初始化全局 jiffies 变量为 0 。
jiffies = 0;
取得 1 个 时钟嘀嗒,需要多少个 OST 时钟周期。
latch = (OST_CLK + (HZ>>1)) / HZ;
注册时钟中断,这个 setup_irq()函数为 request_irq() 的内部函数, 参考下面讲解。
setup_irq(IRQ_OST0, irq);
__ost_disable_channel(0);
设置 1 个时钟嘀嗒需要的 OST 时钟周期。
__ost_set_reload(0, latch);
__ost_set_count(0, latch);
使能中断,并选择 OST 的时钟源。
#if USE_EXTAL_CLK
__ost_set_mode(0, OST_TCSR_UIE | OST_TCSR_CKS_EXTAL);
#else
__ost_set_mode(0, OST_TCSR_UIE | OST_TCSR_CKS_PCLK_4);
#endif
等待 OST 设置好,后开始工作。
while(__ost_is_busy(0))
/* nothing */ ;
__ost_enable_channel(0);
}
******************************************
这个 setup_irq() 是注册中断处理的内部函数, request_irq() 函数就是由这个函数构成的。
这个函数由两个参数,一个是中断号,另一个为 irqaction 描述符(对中断进行处理)。
时钟中断的 irqaction 描述符定义在 arch/mips/cq8401/common/time.c 文件中,如下所示:
static struct irqaction timer_irqaction = {
.handler = timer_interrupt,
.flags = IRQF_DISABLED | IRQF_PERCPU,
.name = "timer",
};
----------------------------------------
int setup_irq(unsigned int irq, struct irqaction *new)
{
在 linux 内核中是由 irq 描述符数组 irq_desc[] 进行的管理。
由中断号取得 时钟中断所使用的 irq 描述符。
struct irq_desc *desc = irq_desc + irq;
struct irqaction *old, **p;
const char *old_name = NULL;
unsigned long flags;
int shared = 0;
进行参数检测。
if (irq >= NR_IRQS)
return -EINVAL;
if (desc->chip == &no_irq_chip)
return -ENOSYS;
如果中断的参数设置了 IRQF_SAMPLE_RANDOM 标志,对内核熵池有贡献。
if (new->flags & IRQF_SAMPLE_RANDOM) {
rand_initialize_irq(irq);
}
锁定当前 irq 描述符的自旋锁,并保存中断标志。
spin_lock_irqsave(&desc->lock, flags);
p = &desc->action;
old = *p;
检测这个 irq 号上是否已经注册了中断,如果 old 不等于 NULL,则已经注册了中断。
if (old) {
当已经注册了中断时,检测原来的中断处理函数,和新的中断处理函数是否支持中断共享。
if (!((old->flags & new->flags) & IRQF_SHARED) ||
((old->flags ^ new->flags) & IRQF_TRIGGER_MASK)) {
old_name = old->name;
goto mismatch;
}
#if defined(CONFIG_IRQ_PER_CPU)
当已经注册了中断时,所有的中断必须符合这个 IRQF_PERCPU 标志,否则出错。
if ((old->flags & IRQF_PERCPU) !=
(new->flags & IRQF_PERCPU))
goto mismatch;
#endif
当已经注册了中断时,并符合上面的条件时,把中断处理描述符加入 action 链表的最后。
do {
p = &old->next;
old = *p;
} while (old);
shared = 1;
}
*p = new;
检测新注册的中断处理函数的标志,设置中断描述符的标志。
这个标志表示这个中断处理函数不能够被中断平衡所影响。
if (new->flags & IRQF_NOBALANCING)
desc->status |= IRQ_NO_BALANCING;
如果 irq 号上没有注册中断,则执行下面的操作,
如果已经注册了中断,则 shared = 1 跳过这个条件判断。
if (!shared) {
检测中断的底层操作 enable、disable、startup、shutdown 等函数是否存在,
如果不存在使用默认的函数。
irq_chip_set_defaults(desc->chip);
#if defined(CONFIG_IRQ_PER_CPU)
这个 IRQF_PERCPU 标志表示这个中断是所有 CPU 共享的。
if (new->flags & IRQF_PERCPU)
desc->status |= IRQ_PER_CPU;
#endif
检测是否设置了中断触发模式。
if (new->flags & IRQF_TRIGGER_MASK) {
if (desc->chip && desc->chip->set_type)
desc->chip->set_type(irq, new->flags & IRQF_TRIGGER_MASK);
else
printk( 打印警告信息) ;
} else
compat_irq_chip_set_default_handler(desc);
清除 irq 描述符的 IRQ_AUTODETECT 、IRQ_WAITING 和 IRQ_INPROGRESS 标志位。
desc->status &= ~(IRQ_AUTODETECT | IRQ_WAITING | IRQ_INPROGRESS);
检测这个 irq 号是否在注册时自动使能的,是的话则执行下面。
if (!(desc->status & IRQ_NOAUTOEN)) {
这个 depth 表示中断关闭的深度, 0 表示中断使能。
desc->depth = 0;
desc->status &= ~IRQ_DISABLED;
if (desc->chip->startup)
desc->chip->startup(irq);
else
desc->chip->enable(irq);
} else
表示中断现在是关闭的。
desc->depth = 1;
}
这个 irq_count 表示中断发生次数,这个 irqs_unhandled 表示中断相应次数。
desc->irq_count = 0;
desc->irqs_unhandled = 0;
解锁,恢复中断标识。
spin_unlock_irqrestore(&desc->lock, flags);
new->irq = irq;
在 proc 文件系统中进行注册,在此不进行详细讲解。
register_irq_proc(irq);
new->dir = NULL;
这个也和 proc 文件系统有关,在此也不进行详细讲解。
register_handler_proc(irq, new);
return 0;
mismatch:
#ifdef CONFIG_DEBUG_SHIRQ
if (!(new->flags & IRQF_PROBE_SHARED)) {
printk(KERN_ERR "IRQ handler type mismatch for IRQ %d\n", irq);
if (old_name)
printk(KERN_ERR "current handler: %s\n", old_name);
dump_stack();
}
#endif
spin_unlock_irqrestore(&desc->lock, flags);
return -EBUSY;
}
******************************************
这个 init_mips_clocksource() 函数被 time_init() 函数所调用,
这个函数主要是注册时钟源,这个时钟源是硬件时钟的抽象。
----------------------------------------
static void __init init_mips_clocksource(void)
{
u64 temp;
u32 shift;
进行检测,这个 mips_hpt_frequency 表示硬件时钟的计数频率。
这个 clocksource_mips.read 为读出硬件时钟的计数接口函数。
if (!mips_hpt_frequency || clocksource_mips.read == null_hpt_read)
return;
计算时钟源的等级,这个等级主要是硬件时钟的计数频率有关,频率越高,则等级越高,时钟精度越高。
clocksource_mips.rating = 200 + mips_hpt_frequency / 10000000;
这个 NSEC_PER_SEC 表示每秒钟的纳秒数,
for (shift = 32; shift > 0; shift--) {
这个 temp 表示 2 的 shift 次方秒钟的纳秒数。
temp = (u64) NSEC_PER_SEC << shift;
这个 do_div() 把 temp 除以 mips_hpt_frequency ,并把结构给 temp。
这是一个宏,所以不使用 temp 的指针。
除以 mips_hpt_frequency,相当于乘以硬件时钟 1 周期的时间。
do_div(temp, mips_hpt_frequency);
if ((temp >> 32) == 0)
break;
}
clocksource_mips.shift = shift;
clocksource_mips.mult = (u32)temp;
注册时钟源 clocksource_mips。
参考《 linux时钟管理机制 》。
clocksource_register(&clocksource_mips);
}
*******************************************
问题:
1)在中断注册中的 register_irq_proc() 和 register_handler_proc() 函数的处理过程。
2)这个时钟源的 rating 等级有什么作用?
答:这个等级主要是硬件时钟的计数频率有关,频率越高,则等级越高,时钟精度越高。
3)这个时钟源结构的 shift 和 mult 的作用?
linux-mips启动分析(18)
在 start_kernel() 函数调用 profile_init() 函数,这个函数对内核的 profile 功能
进行初始化,这是一个内核调式工具,通过这个可以发现内核在内核态的什么地方花费时间最多,
即发现内核的“hot spot”——执行最频繁的内核代码。
这个 profile_init() 分配一段内存,用来存放 profile 信息,每条指令都有一个计数器。
========================================================
内核代码监控使用的全局变量:
static atomic_t *prof_buffer;
static unsigned long prof_len, prof_shift;
int prof_on __read_mostly;
EXPORT_SYMBOL_GPL(prof_on);
----------------------------------------
void __init profile_init(void)
{
这个 prof_on 是一个全局变量,默认为 0 ,表示了 profile 的类型,
这个 prof_on 变量在 profile_setup() 函数进行赋值。
if (!prof_on)
return;
这两个变量 _stext 和 _etext 在链接文件 arch/mips/kernel/vmlinux.lds 中定义。
表示代码段的开始地址可结束地址。
通过 prof_shift 来决定分配 profile 空间的大小。
prof_len = (_etext - _stext) >> prof_shift;
分配 profile 空间。
prof_buffer = alloc_bootmem(prof_len*sizeof(atomic_t));
}
******************************************
这个 profile_setup() 函数比较特别,并不是在这里进行的调用,
而是在 do_early_param() 函数执行时进行了调用。
参考《linux-mips启动分析(4)》。
因为内核定义了 __setup("profile=", profile_setup);
把这个 profile_setup() 函数 ,链接入 .init.setup 段,
在 do_early_param() 函数中,判断内核的启动参数命令行,如果有
“profile=” 字符串,则调用 profile_setup() 函数。
参考《Linux启动参数及实现》和 《启动时内核参数解析》
----------------------------------------
这个函数主要对 prof_on 和 prof_shift 变量进行赋值。
定义了 prof_on 全局变量,表示 profiling 的类型,有如下四种:
#define CPU_PROFILING 1
#define SCHED_PROFILING 2
#define SLEEP_PROFILING 3
#define KVM_PROFILING 4
这个 prof_shift 变量,是静态全局变量,表示 prefile 时,监控的内核代码的精度。
也就是几条指令使用一个计数器。
----------------------------------------
static int __init profile_setup(char * str)
{
static char __initdata schedstr[] = "schedule";
static char __initdata sleepstr[] = "sleep";
static char __initdata kvmstr[] = "kvm";
int par;
当命令行启动参数为 profile=schedule,[n] (n 为一个数字) 时。
if (!strncmp(str, sleepstr, strlen(sleepstr))) {
prof_on = SLEEP_PROFILING;
if (str[strlen(sleepstr)] == ',')
str += strlen(sleepstr) + 1;
取得 schedule,[n] 中 n 数值的大小,并设置 prof_shift 变量。
if (get_option(&str, &par))
prof_shift = par;
当命令行启动参数为 profile=sleep,[n] (n 为一个数字) 时。
} else if (!strncmp(str, schedstr, strlen(schedstr))) {
prof_on = SCHED_PROFILING;
if (str[strlen(schedstr)] == ',')
str += strlen(schedstr) + 1;
取得 sleep,[n] 中 n 数值的大小,并设置 prof_shift 变量。
if (get_option(&str, &par))
prof_shift = par;
当命令行启动参数为 profile=kvm,[n] (n 为一个数字) 时。
} else if (!strncmp(str, kvmstr, strlen(kvmstr))) {
prof_on = KVM_PROFILING;
if (str[strlen(kvmstr)] == ',')
str += strlen(kvmstr) + 1;
取得 kvm,[n] 中 n 数值的大小,并设置 prof_shift 变量。
if (get_option(&str, &par))
prof_shift = par;
当命令行启动参数为 profile=[n] (n 为一个数字) 时。
} else if (get_option(&str, &par)) {
prof_shift = par;
prof_on = CPU_PROFILING;
}
return 1;
}
*******************************************
参考:
内核版本2.6.18-RC7
profile只是内核的一个调试性能的工具,这个可以通过menuconfig中的Instrumentation Support->profile打开。
1. 如何使用profile:
首先确认内核支持profile,然后在内核启动时加入以下参数:profile=1或者其它参数, 新的内核支持profile=schedule 1
2. 内核启动后会创建/proc/profile文件,这个文件可以通过readprofile读取,
如readprofile -m /proc/kallsyms | sort -nr > ~/cur_profile.log,
或者readprofile -r -m /proc/kallsyms |sort -nr,
或者readprofile -r && sleep 1 && readprofile -m /proc/kallsyms |sort -nr >~/cur_profile.log
3. 读取/proc/profile可获得哪些内容?
根据启动配置profile=?的不同,获取的内容不同:
如果配置成profile=? 可以获得每个函数执行次数,用来调试函数性能很有用
如果设置成profile=schedule ?可以获得每个函数调用schedule的次数,用来调试schedule很有用
profile的实现:
在内核中创建一个/proc/profile接口,在系统启动时用profile_init()分配好存放profile信息的内存,每条指令都有一个计数器。
如果设置的是profile=? 统计每条指令执行的次数。在时钟中断中调用 profile_tick(CPU_PROFILING, regs),
将当前指令regs->eip的计数值+1。这个统计有点不准,因为一个jiffies之间,可能执行很多函数,
而统计的只是恰好发生时钟中断时的那个函数。但取样点多了,这些信息还是能说明问题。
如果设置的是profile=schedule ? 统计每个指令调用schedule()的次数,
在schedule()中调用profile_hit(SCHED_PROFILING, __builtin_return_address(0));
其实真正调用schedule的指令只有有限的几个,但这些信息可以获得调度点的精确信息。
profile_hit()的作用是将当前指令的计数值加1
profile_tick()是在每个时钟tick的时候将响应的指令计数值加1
time_hook 一般被其它profile工具,如oprofile用来在每次中断发生时,添加自己的处理函数。
profile信息其实包括任务的所有统计信息,所以可以用profile_event_register()在任务退出或者用户空间内存释放时,挂载自己的回调函数,以统计这些信息。
profile信息的统计在smp和up下不同,即profile_hit的实现不同,smp的实现中有一个PerCPU cache,
这可避免多个CPU在profile统计时效率低下问题。具体可以察看源代码kernel/profile.c, 可以看看driver/oprofile的实现。
linux-mips启动分析(19)
在 start_kernel() 函数中调用 console_init() 函数,这个 console_init() 函数
初始化内核显示终端,在这里仅仅进行一些初级的初始化。
应用程序通过终端接口设备使用特定的接口规程与终端进行交互,与操作系统内核本身交互的终端称为控制台,
它可以是内核本身的内部显示终端,也可以是通过串口连接的外部哑终端。
由于大多数情况下控制台都是内核显示终端,因此内核显示终端也常常直接称为控制台。
内核终端对用户来说具有若干个虚拟终端子设备,它们共享同一物理终端,
但同一时刻只能有一个虚拟终端操作硬件屏幕。
宏 CONFIG_VT 的意思是否支持虚拟终端。
当配置了宏 CONFIG_VGA_CONSOLE 时为内核本身的内部显示终端。
当配置了宏 CONFIG_DUMMY_CONSOLE 时为通过串口连接的外部哑终端。
内核终端的主设备号为 4( TTY_MAJOR) ,1 至 63 号子设备表示不同的虚拟终端,
0 号子设备代表当前活动的虚拟终端, 内核终端的物理显示设备接口用 struct consw 结构描述,
在配置了VGA文本控制台时 ( CONFIG_VGA_CONSOLE),
系统的缺省显示设备指针 conswitchp 指向 vga_con 设备。
在配置串口连接的外部哑终端文本控制台时 ( CONFIG_DUMMY_CONSOLE),
系统的缺省显示设备指针 conswitchp 指向 dummy_con 设备。
参考《linux-mips启动分析(3)》。
=============================================================
void __init console_init(void)
{
这个 initcall_t 类型是一个函数指针的类型。
initcall_t *call;
(void) tty_register_ldisc(N_TTY, &tty_ldisc_N_TTY);
这个 __con_initcall_start 变量在链接文件中定义,
表示了 .con_initcall.init 段的其实地址。
call = __con_initcall_start;
依次调用 .con_initcall.init 段的函数。
通过分析在这个 .con_initcall.init 段有下面的三个函数。
con_init()
serial8250_console_init()
early_uart_console_init()
while (call < __con_initcall_end) {
(*call)();
call++;
}
}
******************************************
这个 tty_register_ldisc() 函数,初始化一个 tty_ldiscs[] 数字中的一个成员。
static DEFINE_SPINLOCK(tty_ldisc_lock);
static DECLARE_WAIT_QUEUE_HEAD(tty_ldisc_wait);
static struct tty_ldisc tty_ldiscs[NR_LDISCS];
使用这个 tty_ldisc_lock 自旋锁来保护 tty_ldiscs[] 数组。
这个 tty_ldiscs[] 数组表示了控制终端的线路规范和 TTY 例程之间的接口。
用来在文件接口与输入设备和输出设备之间进行调控。
---------------------------------------
在 linux 系统中,打开终端的文件操作表为 tty_fops,
终端的打开结构用 tty_struct 结构描述
文件结构的 private_data 指针指向打开的终端结构,
如果当前进程的终端指针 ( current->tty ) 指向某个打开的终端结构,
则表示该进程与此终端相关联,tty_driver 结构描述终端的输出设备,
终端线路规程 tty_ldiscs[NR_LDISCS] 用来在文件接口与输入设备和输出设备之间进行调控。
TTY规程 ( N_TTY ) 用于连接终端输入驱动设备和终端显示驱动设备。
结构名中的 “ldisc” 是 “Line Discipline” 的缩写,表示“链录规则”的意思。
这个结构体中,不但有供上层调用的 open、read、write 等等,还有供下层调用的函数指针
receive_buf、receive_room 以及 write_wakeup 等等。
使用不同的 ldisc 结构体,表示不同的链路规则。
----------------------------------------
int tty_register_ldisc(int disc, struct tty_ldisc *new_ldisc)
{
unsigned long flags;
int ret = 0;
if (disc < N_TTY || disc >= NR_LDISCS)
return -EINVAL;
spin_lock_irqsave(&tty_ldisc_lock, flags);
结构体整体赋值,这个 struct tty_ldisc 结构体的成员赋值为 tty_ldisc_N_TTY 的成员值。
tty_ldiscs[disc] = *new_ldisc;
tty_ldiscs[disc].num = disc;
tty_ldiscs[disc].flags |= LDISC_FLAG_DEFINED;
tty_ldiscs[disc].refcount = 0;
spin_unlock_irqrestore(&tty_ldisc_lock, flags);
return ret;
}
*******************************************
这个 con_init() 函数位于内核镜像的 .con_initcall.init 段,被 console_init() 函数
所调用。
这个函数在 drivers/char/vt.c 文件中定义。
这个函数初始化 console 中断。
----------------------------------------
static int __init con_init(void)
{
const char *display_desc = NULL;
struct vc_data *vc;
unsigned int currcons = 0, i;
取得信号量,这个信号量用来保护 console 子系统,和 console_drivers 链表。
acquire_console_sem();
这个 conswitchp 变量是一个 struct consw 结构的全局变量。
表示 内核终端的物理显示设备接口的结构描述。
这个 conswitchp 变量在 setup_arch() 函数中进行赋值为 vga_con 或者 dummy_con。
参考《linux-mips启动分析(3)》。
在这里假设为 conswitchp == dummy_con。
这个 dummy_con 变量在 drivers/video/console/dummycon.c 文件中定义。
if (conswitchp)
display_desc = conswitchp->con_startup();
这个 display_desc 指针为控制台的描述,如果为 NULL,则内核没有配置控制台。
if (!display_desc) {
fg_console = 0;
release_console_sem();
return 0;
}
这个 registered_con_driver[] 数组表示已经注册了的 console 驱动数组。
这个一个 struct con_driver 结构全局变量。
在 registered_con_driver[] 数组中注册一个 console 驱动。
for (i = 0; i < MAX_NR_CON_DRIVER; i++) {
struct con_driver *con_driver = ®istered_con_driver[i];
if (con_driver->con == NULL) {
con_driver->con = conswitchp;
con_driver->desc = display_desc;
con_driver->flag = CON_DRIVER_FLAG_INIT;
con_driver->first = 0;
con_driver->last = MAX_NR_CONSOLES - 1;
break;
}
}
这个 MAX_NR_CONSOLES 等于 63,表示不同的虚拟终端。
这个 con_driver_map[] 数组为 struct consw 结构体的指针数组。
for (i = 0; i < MAX_NR_CONSOLES; i++)
con_driver_map[i] = conswitchp;
这个 blankinterval 变量表示刷新的间隔时间。
这个 blank_state 变量表示刷新控制的状态。
这个 console_timer 为刷新操作的定时器。
if (blankinterval) {
blank_state = blank_normal_wait;
mod_timer(&console_timer, jiffies + blankinterval);
}
这个 vc_cons[] 数组为 struct vc 结构的数组,这个数组对控制台的显示进行控制。
分配虚拟控制台的参数结构空间。并对虚拟控制台的参数进行初始化。
这个 MIN_NR_CONSOLES 为 1, 表示建立一个虚拟终端。
这个 struct vc_data 实际是一个虚拟缓冲区。
for (currcons = 0; currcons < MIN_NR_CONSOLES; currcons++) {
vc_cons[currcons].d = vc = alloc_bootmem(sizeof(struct vc_data));
初始化虚拟控制台的工作结构体。这个工作结构体的处理函数为 vc_SAK() 函数。
INIT_WORK(&vc_cons[currcons].SAK_work, vc_SAK);
visual_init(vc, currcons, 1);
vc->vc_screenbuf = (unsigned short *)alloc_bootmem(vc->vc_screenbuf_size);
vc->vc_kmalloced = 0;
vc_init(vc, vc->vc_rows, vc->vc_cols, currcons || !vc->vc_sw->con_save_screen);
}
currcons = fg_console = 0;
用以识别当前的前景控制台
master_display_fg = vc = vc_cons[currcons].d;
设置终端的起始显示点。
set_origin(vc);
保存硬件屏幕到终端屏幕缓冲区。
save_screen(vc);
定位光标。
gotoxy(vc, vc->vc_x, vc->vc_y);
清除光标到屏幕尾部的字符。
csi_J(vc, 0);
使用终端屏幕缓冲区重新绘制屏幕。
update_screen(vc);
printable = 1;
释放信号量。
release_console_sem();
#ifdef CONFIG_VT_CONSOLE
将内核终端注册到内核控制台。
register_console(&vt_console_driver);
#endif
return 0;
}
----------------------------------------
这个 vt_console_driver 在 drivers/char/vt.c 中定义。
static struct console vt_console_driver = {
.name = "tty",
.write = vt_console_print,
.device = vt_console_device,
.unblank = unblank_screen,
.flags = CON_PRINTBUFFER,
.index = -1,
};
----------------------------------------
在 con_init() 函数中调用 conswitchp->con_startup() 函数,
实际为 dummy_con 结构的 .con_startup = dummycon_startup() 函数。
static const char *dummycon_startup(void)
{
return "dummy device";
}
返回一个字符串的地址起始地址。
******************************************
这个 serial8250_console_init() 函数位于内核镜像的 .con_initcall.init 段,
被 console_init() 函数所调用。
这个函数在 drivers/serial/8250.c 文件中定义。
----------------------------------------
static int __init serial8250_console_init(void)
{
初始化 8250 串口。
serial8250_isa_init_ports();
将 8250 终端注册到内核控制台。
register_console(&serial8250_console);
return 0;
}
******************************************
这个 early_uart_console_init() 函数位于内核镜像的 .con_initcall.init 段,
被 console_init() 函数所调用。
这个函数在 drivers/serial/8250_early.c 文件中定义。
----------------------------------------
static int __init early_uart_console_init(void)
{
if (!early_uart_registered) {
将串口控制台注册到内核控制台。
register_console(&early_uart_console);
early_uart_registered = 1;
}
return 0;
}
******************************************
通过分析上面的代码发现在这三个函数当中,
con_init()
serial8250_console_init()
early_uart_console_init()
主要是调用 register_console()函数, 在内核控制台注册终端驱动。
register_console确实是把console结构体加入到console_drivers,需要注意的是,
console_drivers是一个队列,那么也就意味着可以在该队列中有多个console。
如果console_drivers中注册有多个设备,那么究竟是将printk的信息输出到哪个呢?
事实上,printk要判断消息级别是否小于console_loglevel,如果小于的话,就发送
到 console_drivers 中的所有设备中去。
到这里,我们已经基本清晰了, register_console() 函数的功用,注册到
console_drivers 中的的 console, 仅仅是为了显示 printk() 的信息,
而不是传统意义所指的控制台—在众多终端中唯一可以进入单用户模式进行系统维护的某个终端设备。
----------------------------------------
void register_console(struct console *console)
{
int i;
unsigned long flags;
struct console *bootconsole = NULL;
这个 console_drivers 变量是一个 struct console 指针类型的全局变量。
在 struct console 结构体中,有一个 next 成员,指向下一个 console 。
如果这个 console_drivers 链表不是 NULL 的。
if (console_drivers) {
而要注册的 console 为 boot 阶段的 console ,则直接返回,而不注册。
if (console->flags & CON_BOOT)
return;
如果已经注册了 boot 阶段的 console ,则取得这个 console 的指针。
在注册 first real console 时,这个 boot 阶段的 console 会被注销掉。
if (console_drivers->flags & CON_BOOT)
bootconsole = console_drivers;
}
如果已经注册了 boot 阶段的 console,或者已经注册了 console ,或者 ******?
if (preferred_console < 0 || bootconsole || !console_drivers)
preferred_console = selected_console;
这个 preferred_console 和 selected_console 变量在 console_setup() 函数中设置。
if (preferred_console < 0) {
if (console->index < 0)
console->index = 0;
如果这个需要注册的 console 的 setup 函数成员,可以执行,则设置这个 console 可以注册。
if (console->setup == NULL || console->setup(console, NULL) == 0) {
console->flags |= CON_ENABLED | CON_CONSDEV;
preferred_console = 0;
}
}
这个循环是在 console_driver 中
for (i = 0; i < MAX_CMDLINECONSOLES && console_cmdline[i].name[0]; i++) {
如果这个要注册的 console 的名称和内核启动参数命令行 console 名称不同,查看下一个。
if (strcmp(console_cmdline[i].name, console->name) != 0)
continue;
如果要注册的 console 的 index 和内核启动参数命令行的 console 的 index 不相同,查看下一个。
if (console->index >= 0 && console->index != console_cmdline[i].index)
continue;
设置要注册的 console 的 index 值。
if (console->index < 0)
console->index = console_cmdline[i].index;
如果这个 console 的 setup 成员函数不能够正确执行,则跳出循环。
if (console->setup && console->setup(console, console_cmdline[i].options) != 0)
break;
设置要注册的 console 的标志。
console->flags |= CON_ENABLED;
console->index = console_cmdline[i].index;
这个 selected_console 变量在 console_setup() 函数中设置。
if (i == selected_console) {
console->flags |= CON_CONSDEV;
preferred_console = selected_console;
}
break;
}
如果要注册 console 的 flags 没有设置使能标志,则这个 console 就不执行注册。
通过上面的分析,可以看出,只有当 第一次调用 register_console() 函数,或者
这个 console 的名称和内核启动命令行参数相同,才会注册。
if (!(console->flags & CON_ENABLED))
return;
如果已经注册了 boot 阶段的 console ,则取得这个 console 的指针。
在注册 first real console 时,这个 boot 阶段的 console 会被注销掉。
if (bootconsole) {
unregister_console(bootconsole);
console->flags &= ~CON_PRINTBUFFER;
}
取得 这个 console_drivers 的信号量。
acquire_console_sem();
如果这个 console 的 flags 设置了 CON_CONSDEV 标志,或者这个 console_drivers 链表为空。
把这个 console 加入这个 console_drivers 链表的最前面。
这个console_drivers 链表的第一个节点,为当前选择的输出 console ,是当前控制台。
是通过 selected_console 全局变量,来控制的。
否则加入这个 console_drivers 链表的第二个节点。
从上面可以看出只有在第一次调用 register_console() 函数,或者
情况下个 console 的 flags 设置才能够设置 CON_CONSDEV 标志。
if ((console->flags & CON_CONSDEV) || console_drivers == NULL) {
console->next = console_drivers;
console_drivers = console;
if (console->next)
console->next->flags &= ~CON_CONSDEV;
} else {
console->next = console_drivers->next;
console_drivers->next = console;
}
if (console->flags & CON_PRINTBUFFER) {
spin_lock_irqsave(&logbuf_lock, flags);
con_start = log_start;
spin_unlock_irqrestore(&logbuf_lock, flags);
}
释放 这个 console_drivers 的信号量。
release_console_sem();
}
******************************************
在 register_console() 函数中,使用到了 console_cmdline[] 数组,
这个数组在内核命令行的处理函数中设置。
----------------------------------------
在 printk.c 里面定义了
__setup("console=", console_setup);
当内核命令行的启动参数中含有 "console=" 字符时,就调用 console_setup() 函数。
这个 console_setup() 就记录下来 console 驱动的 name,以及一些选项参数
到 console_cmdline[] 数组中(如波特率), 设置 preferred_console 参数。
----------------------------------------
static int __init console_setup(char *str)
{
char name[sizeof(console_cmdline[0].name)];
char *s, *options;
int idx;
取得 console 的名称,存入 name[] 数组中。
if (str[0] >= '0' && str[0] <= '9') {
strcpy(name, "ttyS");
strncpy(name + 4, str, sizeof(name) - 5);
} else {
strncpy(name, str, sizeof(name) - 1);
}
name[sizeof(name) - 1] = 0;
取得这个 console 的参数,比如串口的波特率等等。
if ((options = strchr(str, ',')) != NULL)
*(options++) = 0;
#ifdef __sparc__
if (!strcmp(str, "ttya"))
strcpy(name, "ttyS0");
if (!strcmp(str, "ttyb"))
strcpy(name, "ttyS1");
#endif
取得 console 的号码,如 ttyS0,ttyS1 中的 0 或者 1。
for (s = name; *s; s++)
if ((*s >= '0' && *s <= '9') || *s == ',')
break;
idx = simple_strtoul(s, NULL, 10);
*s = 0;
add_preferred_console(name, idx, options);
return 1;
}
----------------------------------------
int __init add_preferred_console(char *name, int idx, char *options)
{
struct console_cmdline *c;
int i;
循环 这个 console_cmdline[] 数组,查看是否这个 console 已经注册上。
for(i = 0; i < MAX_CMDLINECONSOLES && console_cmdline[i].name[0]; i++)
if (strcmp(console_cmdline[i].name, name) == 0 &&
console_cmdline[i].index == idx) {
selected_console = i;
return 0;
}
没有空闲的 slot 了。
if (i == MAX_CMDLINECONSOLES)
return -E2BIG;
设置 selected_console 变量。在 register_console() 函数中使用到了。
selected_console = i;
对 console_cmdline[i] 成员中的各项进行设置。
c = &console_cmdline[i];
memcpy(c->name, name, sizeof(c->name));
c->name[sizeof(c->name) - 1] = 0;
c->options = options;
c->index = idx;
return 0;
}
******************************************
问题:
1)这个 console_drivers 链表的第一个节点有什么不同?
2)这个 con_start、log_start 变量的作用?
3)这个 selected_console 和 preferred_console 的作用?
linux-mips启动分析(20)
在 start_kernel()函数中调用 vfs_caches_init_early() 函数,
再由 vfs_caches_init_early() 调用 dcache_init_early()
和 inode_init_early() 函数,完成虚拟文件系统的初始化。
初始化 dentry 和 inode 缓冲队列的 hash 表。
===============================================================
void __init vfs_caches_init_early(void)
{
对目录项哈希表进行早期的初始化。
dcache_init_early();
对文件节点哈希表进行早期初始化。
inode_init_early();
}
******************************************
这个 hashdist 变量在 mm/page_alloc.c 文件中定义。参考下面分析。
这个 dentry_hashtable 变量是一个 hashtable 的头。
在 fs/dcache.c 文件中定义。
static struct hlist_head *dentry_hashtable __read_mostly;
---------------------------------------
static __initdata unsigned long dhash_entries;
static int __init set_dhash_entries(char *str)
{
if (!str)
return 0;
dhash_entries = simple_strtoul(str, &str, 0);
return 1;
}
__setup("dhash_entries=", set_dhash_entries);
这个 dhash_entries 变量表示目录项哈希表入口数目,默认为 0 。
可以通过这个内核启动参数的配置函数为 set_dhash_entries() 函数进行设置。
--------------------------------------
static void __init dcache_init_early(void)
{
int loop;
如果 hashes 是和 NUMA 节点来配置的,延迟 hash 的分配,
直到 vmalloc 空间有效后,再分配。
if (hashdist)
return;
返回的哈希表头地址为 dentry_hashtable 。
哈希表入口项数目 numentries 值 2 的对数,保存到 d_hash_shift 变量。
这个 d_hash_mask 保存入口项的掩码。
dentry_hashtable =
alloc_large_system_hash("Dentry cache",
sizeof(struct hlist_head),
dhash_entries,
13,
HASH_EARLY,
&d_hash_shift,
&d_hash_mask,
0);
对哈希表的每个链表头进行初始化。
for (loop = 0; loop < (1 << d_hash_shift); loop++)
INIT_HLIST_HEAD(&dentry_hashtable[loop]);
}
********************************************
这个 hashdist 变量在 mm/page_alloc.c 文件中定义。参考下面分析。
这个 dentry_hashtable 变量是一个 hashtable 的头。
在 fs/dcache.c 文件中定义。
static struct hlist_head *dentry_hashtable __read_mostly;
---------------------------------------
static __initdata unsigned long ihash_entries;
static int __init set_ihash_entries(char *str)
{
if (!str)
return 0;
ihash_entries = simple_strtoul(str, &str, 0);
return 1;
}
__setup("ihash_entries=", set_ihash_entries);
这个 ihash_entries 变量表示文件节点哈希表入口数目,默认为 0 。
可以通过这个内核启动参数的配置函数为 set_ihash_entries() 函数进行设置。
---------------------------------------
void __init inode_init_early(void)
{
int loop;
这个 hashdist 变量在 mm/page_alloc.c 文件中定义。
如果 hashes 是和 NUMA 节点来配置的,延迟 hash 的分配,
直到 vmalloc 空间有效后,再分配。
if (hashdist)
return;
返回的哈希表头地址为 inode_hashtable 。
哈希表入口项数目 numentries 值 2 的对数,保存到 i_hash_shift 变量。
这个 i_hash_mask 保存入口项的掩码。
inode_hashtable =
alloc_large_system_hash("Inode-cache",
sizeof(struct hlist_head),
ihash_entries,
14,
HASH_EARLY,
&i_hash_shift,
&i_hash_mask,
0);
对哈希表的每个链表头进行初始化。
for (loop = 0; loop < (1 << i_hash_shift); loop++)
INIT_HLIST_HEAD(&inode_hashtable[loop]);
}
********************************************
这个 hashdist 变量在 mm/page_alloc.c 文件中定义。
默认值为 HASHDIST_DEFAULT,这个 hashdist 变量的值可以通过内核启动参数设置,
这个内核启动参数的配置函数为 set_hashdist() 函数。
---------------------------------------
#if defined(CONFIG_NUMA) && (defined(CONFIG_IA64) || defined(CONFIG_X86_64))
#define HASHDIST_DEFAULT 1
#else
#define HASHDIST_DEFAULT 0
#endif
可以看出 HASHDIST_DEFAULT 的值,在我们的系统中默认为 0 。
---------------------------------------
int hashdist = HASHDIST_DEFAULT;
#ifdef CONFIG_NUMA
static int __init set_hashdist(char *str)
{ static struct hlist_head *dentry_hashtable __read_mostly;
if (!str)
return 0;
hashdist = simple_strtoul(str, &str, 0);
return 1;
}
__setup("hashdist=", set_hashdist);
#endif
********************************************
参数含义:
1) tablename 哈希表名字
2) bucketsize 这个是每个元素的尺寸
3) numentries 元素的个数,可以取 0,由系统来确定,
这时可能即使你给了值,系统也会把它变为最接近的 2 的幂
4) scale 取值有 13、14、15、17 之类的,如果 numentries 不为 0,这个参数没有作用。
5) lags 可取 HASH_EARLY 或0,分配内存的地方根据这个有不同
6) _hash_shift 用于返回元素个数的以 2 为底的对数,也就是表示元素个数这个数值所用的比特数
7) _hash_mask 用于返回 *_hash_shift 个比特所能表示的最大数 -1
8) limit 哈希表表元数上限,不是分配内存的总尺寸,不要弄混了。
如果给个 0 值,那么系统使用 1/16 内存所能容纳的元素数作为哈希表表元数。
参数 numentries 和 limit 有关系的,参数 numentries 的值不能超出 limit 的值
(如果不为0,为 0 则不能超出系统计算的哈希表元数目),所以不给 numentries 也行。
---------------------------------------
void *__init alloc_large_system_hash(const char *tablename,
unsigned long bucketsize,
unsigned long numentries,
int scale,
int flags,
unsigned int *_hash_shift,
unsigned int *_hash_mask,
unsigned long limit)
{
unsigned long long max = limit;
unsigned long log2qty, size;
void *table = NULL;
如果传进来的参数 numentries 为 0,则
if (!numentries) {
这个 nr_kernel_pages 变量在 free_area_init_core() 函数中赋值,
参考《linux-mips启动分析(4-3)》,表示不包含高端内存的系统内存共有的内存页面数。
计算不包含高端内存的系统内存共有 多少 MB(不足 1MB,以 1MB 计算)。
numentries = nr_kernel_pages;
numentries += (1UL << (20 - PAGE_SHIFT)) - 1;
numentries >>= 20 - PAGE_SHIFT;
取得 不足 1MB,以 1MB 计算后的页面数目。
numentries <<= 20 - PAGE_SHIFT;
对 numentries 进行缩放。
if (scale > PAGE_SHIFT)
numentries >>= (scale - PAGE_SHIFT);
else
numentries <<= (PAGE_SHIFT - scale);
不足一个页面的空间,以一个页面的空间为最小。
if (unlikely((numentries * bucketsize) < PAGE_SIZE))
numentries = PAGE_SIZE / bucketsize;
}
向上取得 numentries 为 2 的幂次方数值。
numentries = roundup_pow_of_two(numentries);
这个 max 等于 limit,如果等于 0 ,就计算 numentries 的最大值 max 。
if (max == 0) {
这个 nr_all_pages 变量表示所有内存的页面 nr_all_pages 变量。
即哈希表的内存最大为整个内存的 1/16 的空间。
max = ((unsigned long long)nr_all_pages << PAGE_SHIFT) >> 4;
这个 do_div() 宏,使 max 除以 bucketsize,并把结果赋值给 max 变量。
do_div(max, bucketsize);
}
变量 numentries 不能够大于 max 变量,即哈希表的入口不能够大于 max 变量。
if (numentries > max)
numentries = max;
对 numentries 取 2 的对数。
log2qty = ilog2(numentries);
do {
取得要分配空间的大小。
size = bucketsize << log2qty;
可以看出,可以在三个地方,分配需要的空间。
当 flags 为 HASH_EARLY 标志时,表示内核没有启动完毕,使用启动内存分配器来分配空间。
if (flags & HASH_EARLY)
table = alloc_bootmem(size);
如果 hashdist 不等于 0 ,从 vmalloc 空间分配内存。
else if (hashdist)
table = __vmalloc(size, GFP_ATOMIC, PAGE_KERNEL);
下面的在系统启动完毕后,从伙伴内存管理器上分配页面。
else {
unsigned long order;
for (order = 0; ((1UL << order) << PAGE_SHIFT) < size; order++) ;
table = (void*) __get_free_pages(GFP_ATOMIC, order);
}
} while (!table && size > PAGE_SIZE && --log2qty);
如果 table 等于 NULL ,表示没有足够的空间来进行分配。
if (!table)
panic("Failed to allocate %s hash table\n", tablename);
返回 哈希表入口项数目 numentries 值 2 的对数。
if (_hash_shift)
*_hash_shift = log2qty;
if (_hash_mask)
*_hash_mask = (1 << log2qty) - 1;
return table;
}
linux-mips启动分析(21)
在 start_kernel() 函数中调用 mem_init() 函数初始化内存页描述符。
设置内存上下界和页表项初始值
================================================================
void __init mem_init(void)
{
unsigned long codesize, reservedpages, datasize, initsize;
unsigned long tmp, ram;
这个 max_mapnr 变量表示内存空间的结束地址页面祯号(PFN)。
#ifdef CONFIG_HIGHMEM
max_mapnr = highend_pfn;
#else
max_mapnr = max_low_pfn;
#endif
这个 high_memory 变量是一个全局变量,是一个虚拟地址。
表示大于这个地址的虚拟地址空间,为非逻辑地址空间,没有物理地址和这段空间地址一一对应。
high_memory = (void *) __va(max_low_pfn << PAGE_SHIFT);
这个 totalram_pages 变量为全局变量。
这个 free_all_bootmem() 函数参考《linux-mips启动分析(4-5)》。
这个 free_all_bootmem() 函数返回低端空闲页面的总数。
totalram_pages += free_all_bootmem();
建立几个空白页面。
totalram_pages -= setup_zero_pages();
使用 ram 变量记录可用的物理 RAM 的页面数目。
使用 reservedpages 表示页面留给内核代码或者为内存 hole 的数目。
reservedpages = ram = 0;
变量 max_low_pfn 表示低端内存可用的最大的 PFN。
for (tmp = 0; tmp < max_low_pfn; tmp++)
检测页面是否是可用的 RAM,还是内存空洞。
if (page_is_ram(tmp)) {
ram++;
if (PageReserved(pfn_to_page(tmp)))
reservedpages++;
}
num_physpages = ram;
如果系统中有高端内存,定义了 CONFIG_HIGHMEM 宏。
使用 totalhigh_pages 变量表示物理高端内存的页面数目。
#ifdef CONFIG_HIGHMEM
循环遍历高端内存的物理页面。
for (tmp = highstart_pfn; tmp < highend_pfn; tmp++) {
struct page *page = mem_map + tmp;
如果这个页面不是物理 RAM ,设置这个为 Reserved。
if (!page_is_ram(tmp)) {
SetPageReserved(page);
continue;
}
如果这个页面是物理 RAM 。对这个页面进行设置。
ClearPageReserved(page);
设置引用计数为 1。
init_page_count(page);
释放页面,把高端内存的页面加入伙伴内存系统的空闲链表。
在 free_all_bootmem() 函数中只是对低端内存进行了处理。
__free_page(page);
totalhigh_pages++;
}
totalram_pages += totalhigh_pages;
num_physpages += totalhigh_pages;
#endif
这三个变量分别表示内核代码段,数据段, init 段的大小。
codesize = (unsigned long) &_etext - (unsigned long) &_text;
datasize = (unsigned long) &_edata - (unsigned long) &_etext;
initsize = (unsigned long) &__init_end - (unsigned long) &__init_begin;
#ifdef CONFIG_64BIT
if ((unsigned long) &_text > (unsigned long) CKSEG0)
kclist_add(&kcore_kseg0, (void *) CKSEG0, 0x80000000 - 4);
#endif
这个 kcore_mem 和 kcore_vmalloc 变量定义在 arch/mips/mm/init.c 文件,
是一个 kcore_list 链表的节点。
下面的两个函数把 kcore_mem 和 kcore_vmalloc 节点,添加到 kcore_list 链表。
把新的节点添加到链表的最前面。
kclist_add(&kcore_mem, __va(0), max_low_pfn << PAGE_SHIFT);
kclist_add(&kcore_vmalloc, (void *)VMALLOC_START, VMALLOC_END-VMALLOC_START);
}
******************************************
unsigned long setup_zero_pages(void)
{
unsigned int order;
unsigned long size;
struct page *page;
如果 CPU 支持 cpu_has_vce 的,则分配内存块 order 为 3 ,即 8 个内存页面。
if (cpu_has_vce)
order = 3;
else
order = 0;
从伙伴内存系统分配内存页面,返回所分配内存的空间的第一个页的线性地址。
这个 empty_zero_page 变量是一个全局变量。
empty_zero_page = __get_free_pages(GFP_KERNEL | __GFP_ZERO, order);
if (!empty_zero_page)
panic("Oh boy, that early out of memory?");
把线性地址转换成 struct page 指针。
page = virt_to_page((void *)empty_zero_page);
把所分配内存的页面 struct page 的引用计数都置 1 。
split_page(page, order);
把所分配内存的页面 struct page 的 flags 设置 PG_reserved 标志。
while (page < virt_to_page((void *)(empty_zero_page + (PAGE_SIZE << order)))) {
SetPageReserved(page);
page++;
}
size = PAGE_SIZE << order;
这个 zero_page_mask 变量是一个全局变量。
zero_page_mask = (size - 1) & PAGE_MASK;
返回分配的空白页面的数目。
return 1UL << order;
}
********************************************
struct kcore_list {
struct kcore_list *next;
unsigned long addr;
size_t size;
};
---------------------------------------
这个 kcore_mem 和 kcore_vmalloc 变量定义在 arch/mips/mm/init.c 文件,
是一个 kcore_list 链表的节点。
static struct kcore_list kcore_mem, kcore_vmalloc;
---------------------------------------
static struct kcore_list *kclist;
这个 kclist 变量表示 kcore_list 链表的头节点指针。
---------------------------------------
如果没有定义 CONFIG_PROC_KCORE 这个宏,则这个 kclist_add() 函数为空函数。
void kclist_add(struct kcore_list *new, void *addr, size_t size)
{
new->addr = (unsigned long)addr;
new->size = size;
把新的节点添加到链表的最前面。
write_lock(&kclist_lock);
new->next = kclist;
kclist = new;
write_unlock(&kclist_lock);
}
********************************************
问题:
1)这个 total_pages 变量在什么地方定义?
2) 这个宏 cpu_has_vce 的意义?
3)这个 kcore_list 链表的意义?
linux-mips启动分析(21-2)
在 start_kernel() 函数中调用 mem_init() 函数,
而 mem_init() 函数调用 setup_zero_pages() 函数,
而 setup_zero_pages() 函数调用 __get_free_pages() 函数来分配页面。
本章讲述从伙伴系统分配页面的具体过程。
=============================================================
fastcall unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order)
{
struct page * page;
用这个函数分配 2^order 个连续的页面。返回分配空间的第一个页面的描述符地址。
page = alloc_pages(gfp_mask, order);
if (!page)
return 0;
返回 page 所对应页的线性地址。
return (unsigned long) page_address(page);
}
********************************************
如果定义了 WANT_PAGE_VIRTUAL 这个宏,
这个宏决定是否在 struct page 中定义 virtual 成员,这个成员等于这个页面的线性地址。
如果是高端内存,在分配内存后,把 virtual 成员赋值为页面的虚拟地址。
所以可以直接返回页面的虚拟地址。
#define page_address(page) ((page)->virtual)
---------------------------------------
如果定义了 HASHED_PAGE_VIRTUAL 这个宏,这个宏依赖于 CONFIG_HIGHMEM 宏,
表明系统有高端内存,并且系统没有定义 WANT_PAGE_VIRTUAL 这个宏。
void *page_address(struct page *page)
{
unsigned long flags;
void *ret;
struct page_address_slot *pas;
如果这个页面不是高端内存,可以直接通过物理地址直接转换成虚拟地址。
if (!PageHighMem(page))
return lowmem_page_address(page);
如果这个页面是高端内存,则需要
返回的是 page_address_htable[] 数组的一个元素指针。
pas = page_slot(page);
ret = NULL;
锁定返回的page_address_htable[] 数组的元素。
spin_lock_irqsave(&pas->lock, flags);
if (!list_empty(&pas->lh)) {
struct page_address_map *pam;
遍历这个链表查找 page 的虚拟地址。
list_for_each_entry(pam, &pas->lh, list) {
如果这个链表阶段的 page 地址等于我们要查找的 page 的地址。
对 ret 赋值为这个 page 的虚拟地址。
if (pam->page == page) {
ret = pam->virtual;
goto done;
}
}
}
如果这个 page 没有在链表上,则返回 NULL。
done:
释放返回的page_address_htable[] 数组的元素。
spin_unlock_irqrestore(&pas->lock, flags);
return ret;
}
---------------------------------------
表明系统中没有高端内存,并且系统没有定义 WANT_PAGE_VIRTUAL 这个宏。
#define page_address(page) lowmem_page_address(page)
static __always_inline void *lowmem_page_address(struct page *page)
{
return __va(page_to_pfn(page) << PAGE_SHIFT);
}
由于没有高端内存,可以直接通过物理地址直接转换成虚拟地址。
********************************************
定义了一个哈希表,大小为 (1< 这个哈希表的散列函数为 hash_ptr() 函数。
这个散列函数是 page 地址乘以 0x9E370001,这个值是接近 2^32 的黄金比例的一个素数。
在使它小于 哈希表的大小。
如果发生冲突,就在这个链表中添加节点。或在链表中查找节点。
所以 page_slot() 函数返回的是 page_address_htable[] 数组的一个元素指针。
---------------------------------------
static struct page_address_slot {
struct list_head lh;
spinlock_t lock;
} ____cacheline_aligned_in_smp page_address_htable[1<
static struct page_address_slot *page_slot(struct page *page)
{
return &page_address_htable[hash_ptr(page, PA_HASH_ORDER)];
}
******************************************
如果 gfp_mask 没有设置 GFP__WAIT 标志,分配器将不会阻塞,而是在内存紧张时,返回 NULL。
#define alloc_pages(gfp_mask, order) \
alloc_pages_node(numa_node_id(), gfp_mask, order)
---------------------------------------
static inline struct page *alloc_pages_node(int nid, gfp_t gfp_mask,
unsigned int order)
{
if (unlikely(order >= MAX_ORDER))
return NULL;
使用 numa_node_id() 宏函数,取得要分配内存的内存的节点号。
if (nid < 0)
nid = numa_node_id();
这个 node_zonelists 成员是一个 zone_t 的链表,每个管理区分配对应了一个 zonelist 。
参考《linux-mips启动分析(7)》。
return __alloc_pages(gfp_mask, order,
NODE_DATA(nid)->node_zonelists + gfp_zone(gfp_mask));
}
---------------------------------------
#define numa_node_id() (cpu_to_node(raw_smp_processor_id()))
---------------------------------------
#define raw_smp_processor_id() (current_thread_info()->cpu)
---------------------------------------
#define cpu_to_node(cpu) (0)
********************************************
这个 __alloc_pages() 函数负责遍历 zonelist (回退管理区链表),如果选择一个合适
的管理区进行内存分配。
如果内存紧张,就唤醒 kswapd 内核线程,释放内存页面。
---------------------------------------
struct page * fastcall
__alloc_pages(gfp_t gfp_mask, unsigned int order, struct zonelist *zonelist)
{
这个 __GFP_WAIT 表明请求页面分配的进程是否可以睡眠等待。
const gfp_t wait = gfp_mask & __GFP_WAIT;
struct zone **z;
struct page *page;
struct reclaim_state reclaim_state;
请求页面分配进程的进程描述符指针。
struct task_struct *p = current;
int do_retry;
int alloc_flags;
int did_some_progress;
如果设置了 __GFP_WAIT 标志,就执行内核抢占。
might_sleep_if(wait);
如果没有定义 CONFIG_FAIL_PAGE_ALLOC 这个宏,这个 should_fail_alloc_page() 函数就直接返回 0。
先跳过这里。
if (should_fail_alloc_page(gfp_mask, order))
return NULL;
如果在当前内存管理区,没有成功分配内存,就从 zonelist (回退管理区链表)取下一个内存管理区,进行分配。
restart:
z = zonelist->zones; /* the list of zones suitable for gfp_mask */
当 z 为 NULL 时,表示 zonelist (回退管理区链表)已经结束。
if (unlikely(*z == NULL)) {
return NULL;
}
page = get_page_from_freelist(gfp_mask|__GFP_HARDWALL, order,
zonelist, ALLOC_WMARK_LOW|ALLOC_CPUSET);
if (page)
goto got_pg;
if (NUMA_BUILD && (gfp_mask & GFP_THISNODE) == GFP_THISNODE)
goto nopage;
for (z = zonelist->zones; *z; z++)
wakeup_kswapd(*z, order);
alloc_flags = ALLOC_WMARK_MIN;
if ((unlikely(rt_task(p)) && !in_interrupt()) || !wait)
alloc_flags |= ALLOC_HARDER;
if (gfp_mask & __GFP_HIGH)
alloc_flags |= ALLOC_HIGH;
if (wait)
alloc_flags |= ALLOC_CPUSET;
page = get_page_from_freelist(gfp_mask, order, zonelist, alloc_flags);
if (page)
goto got_pg;
rebalance:
if (((p->flags & PF_MEMALLOC) || unlikely(test_thread_flag(TIF_MEMDIE)))
&& !in_interrupt()) {
if (!(gfp_mask & __GFP_NOMEMALLOC)) {
nofail_alloc:
page = get_page_from_freelist(gfp_mask, order,
zonelist, ALLOC_NO_WATERMARKS);
if (page)
goto got_pg;
if (gfp_mask & __GFP_NOFAIL) {
congestion_wait(WRITE, HZ/50);
goto nofail_alloc;
}
}
goto nopage;
}
if (!wait)
goto nopage;
cond_resched();
cpuset_memory_pressure_bump();
p->flags |= PF_MEMALLOC;
reclaim_state.reclaimed_slab = 0;
p->reclaim_state = &reclaim_state;
did_some_progress = try_to_free_pages(zonelist->zones, gfp_mask);
p->reclaim_state = NULL;
p->flags &= ~PF_MEMALLOC;
cond_resched();
if (likely(did_some_progress)) {
page = get_page_from_freelist(gfp_mask, order,
zonelist, alloc_flags);
if (page)
goto got_pg;
} else if ((gfp_mask & __GFP_FS) && !(gfp_mask & __GFP_NORETRY)) {
page = get_page_from_freelist(gfp_mask|__GFP_HARDWALL, order,
zonelist, ALLOC_WMARK_HIGH|ALLOC_CPUSET);
if (page)
goto got_pg;
out_of_memory(zonelist, gfp_mask, order);
goto restart;
}
do_retry = 0;
if (!(gfp_mask & __GFP_NORETRY)) {
if ((order <= 3) || (gfp_mask & __GFP_REPEAT))
do_retry = 1;
if (gfp_mask & __GFP_NOFAIL)
do_retry = 1;
}
if (do_retry) {
congestion_wait(WRITE, HZ/50);
goto rebalance;
}
nopage:
if (!(gfp_mask & __GFP_NOWARN) && printk_ratelimit()) {
printk(KERN_WARNING "%s: page allocation failure."
" order:%d, mode:0x%x\n",
p->comm, order, gfp_mask);
dump_stack();
show_mem();
}
got_pg:
return page;
}
******************************************
static struct page *
get_page_from_freelist(gfp_t gfp_mask, unsigned int order,
struct zonelist *zonelist, int alloc_flags)
{
struct zone **z;
struct page *page = NULL;
这个 zone_idex() 对于 ZONE_DMA 返回 0, 对于 ZONE_NORMAL 返回 1。
int classzone_idx = zone_idx(zonelist->zones[0]);
struct zone *zone;
nodemask_t *allowednodes = NULL;
int zlc_active = 0;
int did_zlc_setup = 0;
遍历 zonelist,查找有足够可分配空间的 zone 。
zonelist_scan:
z = zonelist->zones;
do {
在我们的系统中没有配置 CONFIG_NUMA,为一致内存管理模型(UMA),所以我们不对 NUMA 系统进行分析。
下面的条件永远返回 FALSE。
if (NUMA_BUILD && zlc_active && !zlc_zone_worth_trying(zonelist, z, allowednodes))
continue;
zone = *z;
下面的条件永远返回 FALSE。
if (unlikely(NUMA_BUILD && (gfp_mask & __GFP_THISNODE) &&
zone->zone_pgdat != zonelist->zones[0]->zone_pgdat))
break;
如果 alloc_flags 设置了 ALLOC_CPUSET 标志。
这个 cpuset_zone_allowed_softwall() 函数根据系统是否定义 CONFIG_CPUSETS 这个宏有关系。
if ((alloc_flags & ALLOC_CPUSET) &&
!cpuset_zone_allowed_softwall(zone, gfp_mask))
goto try_next_zone;
如果 alloc_flags 没有设置 ALLOC_NO_WATERMARKS 标志。
if (!(alloc_flags & ALLOC_NO_WATERMARKS)) {
unsigned long mark;
根据 flags 来设置 mark 的值。
if (alloc_flags & ALLOC_WMARK_MIN)
mark = zone->pages_min;
else if (alloc_flags & ALLOC_WMARK_LOW)
mark = zone->pages_low;
else
mark = zone->pages_high;
如果这个内存管理区的 free page 的数目,大于 mark,这个函数返回 1。
if (!zone_watermark_ok(zone, order, mark,
classzone_idx, alloc_flags)) {
if (!zone_reclaim_mode ||
!zone_reclaim(zone, gfp_mask, order))
goto this_zone_full;
}
}
page = buffered_rmqueue(zonelist, zone, order, gfp_mask);
if (page)
break;
this_zone_full:
if (NUMA_BUILD)
zlc_mark_zone_full(zonelist, z);
try_next_zone:
if (NUMA_BUILD && !did_zlc_setup) {
/* we do zlc_setup after the first zone is tried */
allowednodes = zlc_setup(zonelist, alloc_flags);
zlc_active = 1;
did_zlc_setup = 1;
}
} while (*(++z) != NULL);
if (unlikely(NUMA_BUILD && page == NULL && zlc_active)) {
zlc_active = 0;
goto zonelist_scan;
}
return page;
}
********************************************
蛐蛐
http://qgjie456.blog.163.com/
MSN:[email protected]
本文适用于
linux-2.6.22.8
V 0.1
欢迎转载,但请保留作者信息
********************************************