一位大师级的人物写的,不看要后悔的哟!!
如果以为到了c代码可以松一口气的话,就大错特措了,linux的c也不比汇编好懂多少,相反到掩盖了汇编的一些和机器相关的部分,有时候更难懂。其实作为编写操作系统的c代码,只不过是汇编的另一种写法,和机器代码的联系是很紧密的。
start_kernel在 /linux/init/main.c中定义:
asmlinkage void __init start_kernel(void)
{
char * command_line;
unsigned long mempages;
extern char saved_command_line[];
lock_kernel();
printk(linux_banner);
setup_arch(&command_line); //arm/kernel/setup.c
printk("Kernel command line: %s\n", saved_command_line);
parse_options(command_line);
trap_init(); // arm/kernle/traps.c install
。。。。。。。。。
start_kernel中的函数个个都是重量级的,首先用printk(linux_banner);打出
系统版本号,这里面就大有文章,系统才刚开张,你让他打印到哪里去呢?
先给大家交个底,以后到console的部分自然清楚,printk和printf不同,他首先输出到系统的一个缓冲区内,大约4k,如果登记了console,则调用console->wirte函数输出,否则就一直在buffer里呆着。所以,用printk输出的信息,如果超出了4k,会冲掉前面的。在系统引导起来后,用dmesg看的也就是这个buffer中的东东。
下面就是一个重量级的函数:
setup_arch(&command_line); //arm/kernel/setup.c
完成内存映像的初始化,其中command_line是从bootloader中传下来的。
void __init setup_arch(char **cmdline_p)
{
struct param_struct *params = NULL;
struct machine_desc *mdesc; //arch structure, for your ads, defined in include/arm-asm/mach/arch.h very long
struct meminfo meminfo;
char *from = default_command_line;
memset(&meminfo, 0, sizeof(meminfo));
首先把meminfo清零,有个背景介绍一下,从linux 2.4的内核开始,支持内存的节点(node),也就是可支持不连续的物理内存区域。这一点在嵌入式系统中很有用,例如对于SDRAM和FALSH,性质不同,可作为不同的内存节点。
meminfo结构定义如下:
/******************************************************/
#define NR_BANKS 4
//define the systen mem region, not consistent
struct meminfo {
int nr_banks;
unsigned long end;
struct {
unsigned long start;
unsigned long size;
int node;
} bank[NR_BANKS];
};
/******************************************************/
下面是:ROOT_DEV = MKDEV(0, 255);
ROOT_DEV是宏,指明启动的设备,嵌入式系统中通常是flash disk.
这里面有一个有趣的悖论:linux的设备都是在/dev/下,访问这些设备文件需要设备驱动程序支持,而访问设备文件才能取得设备号,才能加载驱动程序,那么第一个设备驱动程序是怎么加载呢?就是ROOT_DEV, 不需要访问设备文件,直接指定设备号。
下面我们准备初始化真正的内核页表,而不再是临时的了。
首先还是取得当前系统的内存映像:
mdesc = setup_architecture(machine_arch_type);
//find the machine type in mach-integrator/arch.c
//the ads name, mem map, io map
返回如下结构:
mach-integrator/arch.c
MACHINE_START(INTEGRATOR, "Motorola MX1ADS")
MAINTAINER("ARM Ltd/Deep Blue Solutions Ltd")
BOOT_MEM(0x08000000, 0x00200000, 0xf0200000)
FIXUP(integrator_fixup)
MAPIO(integrator_map_io)
INITIRQ(integrator_init_irq)
MACHINE_END
我们在前面介绍过这个结构,不过这次用它可是玩真的了。
书接上回,
下面是init_mm的初始化,init_mm定义在/arch/arm/kernel/init_task.c:
struct mm_struct init_mm = INIT_MM(init_mm);
从本回开始的相当一部分内容是和内存管理相关的,凭心而论,操作系统的
内存管理是很复杂的,牵扯到处理器的硬件细节和软件算法,
限于篇幅所限制,请大家先仔细读一读arm mmu的部分,
中文参考资料:linux内核源代码情景对话,
linux2.4.18原代码分析。
init_mm.start_code = (unsigned long) &_text;
内核代码段开始
init_mm.end_code = (unsigned long) &_etext;
内核代码段结束
init_mm.end_data = (unsigned long) &_edata;
内核数据段开始
init_mm.brk = (unsigned long) &_end;
内核数据段结束
每一个任务都有一个mm_struct结构管理任务内存空间,init_mm
是内核的mm_struct,其中设置成员变量* mmap指向自己,
意味着内核只有一个内存管理结构,设置* pgd=swapper_pg_dir,
swapper_pg_dir是内核的页目录,在arm体系结构有16k,
所以init_mm定义了整个kernel的内存空间,下面我们会碰到内核
线程,所有的内核线程都使用内核空间,拥有和内核同样的访问
权限。
memcpy(saved_command_line, from, COMMAND_LINE_SIZE);
//clear command array
saved_command_line[COMMAND_LINE_SIZE-1] = '\0';
//set the end flag
parse_cmdline(&meminfo, cmdline_p, from);
//将bootloader的参数拷贝到cmdline_p,
bootmem_init(&meminfo);
定义在arm/mm/init.c
这个函数在内核结尾分一页出来作位图,根据具体系统的内存大小
映射整个ram
下面是一个非常重要的函数
paging_init(&meminfo, mdesc);
定义在arm/mm/init.c
创建内核页表,映射所有物理内存和io空间,
对于不同的处理器,这个函数差别很大,
void __init paging_init(struct meminfo *mi, struct machine_desc *mdesc)
{
void *zero_page, *bad_page, *bad_table;
int node;
//static struct meminfo meminfo __initdata = { 0, };
memcpy(&meminfo, mi, sizeof(meminfo));
/*
* allocate what we need for the bad pages.
* note that we count on this going ok.
*/
zero_page = alloc_bootmem_low_pages(PAGE_SIZE);
bad_page = alloc_bootmem_low_pages(PAGE_SIZE);
bad_table = alloc_bootmem_low_pages(TABLE_SIZE);
分配三个页出来,用于处理异常过程,在armlinux中,得到如下
地址:
zero_page=0xc0000000
bad page=0xc0001000
bad_table=0xc0002000
上回我们说到在paging_init中分配了三个页:
zero_page=0xc0000000
bad page=0xc0001000
bad_table=0xc0002000
但是奇怪的很,在更新的linux代码中只分配了一个
zero_page,而且在源代码中找不到zero_page
用在什么地方了,大家讨论讨论吧。
paging_init的主要工作是在
void __init memtable_init(struct meminfo *mi)
中完成的,为系统内存创建页表:
meminfo结构如下:
struct meminfo {
int nr_banks;
unsigned long end;
struct {
unsigned long start;
unsigned long size;
int node;
} bank[NR_BANKS];
};
是用来纪录系统中的内存区段的,因为在嵌入式
系统中并不是所有的内存都能映射,例如sdram只有
64m,flash 32m,而且不见得是连续的,所以用
meminfo纪录这些区段。
void __init memtable_init(struct meminfo *mi)
{
struct map_desc *init_maps, *p, *q;
unsigned long address = 0;
int i;
init_maps = p = alloc_bootmem_low_pages(PAGE_SIZE);
其中map_desc定义为:
struct map_desc {
unsigned long virtual;
unsigned long physical;
unsigned long length;
int domain:4, //页表的domain
prot_read:1, //保护标志
prot_write:1, //写保护标志
cacheable:1, //是否cache
bufferable:1, //是否用write buffer
last:1; //空
};init_maps
map_desc是区段及其属性的定义,属性位的意义请
参考ARM MMU的介绍。
下面对meminfo的区段进行遍历,同时填写init_maps
中的各项内容:
for (i = 0; i < mi->nr_banks; i++) {
if (mi->bank.size == 0)
continue;
p->physical = mi->bank.start;
p->virtual = __phys_to_virt(p->physical);
p->length = mi->bank.size;
p->domain = DOMAIN_KERNEL;
p->prot_read = 0;
p->prot_write = 1;
p->cacheable = 1; //可以CACHE
p->bufferable = 1; //使用write buffer
p ++; //下一个区段
}
如果系统有flash,
#ifdef FLUSH_BASE
p->physical = FLUSH_BASE_PHYS;
p->virtual = FLUSH_BASE;
p->length = PGDIR_SIZE;
p->domain = DOMAIN_KERNEL;
p->prot_read = 1;
p->prot_write = 0;
p->cacheable = 1;
p->bufferable = 1;
p ++;
#endif
其中的prot_read和prot_write是用来设置页表的domain的,
下面就是逐个区段建立页表:
q = init_maps;
do {
if (address < q->virtual || q == p) {
clear_mapping(address);
address += PGDIR_SIZE;
} else {
create_mapping(q);
address = q->virtual + q->length;
address = (address + PGDIR_SIZE - 1) & PGDIR_MASK;
q ++;
}
} while (address != 0);
上次说到memtable_init中初始化页表的循环,
这个过程比较重要,我们看仔细些:
q = init_maps;
do {
if (address < q->virtual || q == p) {
//由于内核空间是从c000 0000开始,所以c000 0000
//以前的页表项全部清空
clear_mapping(address);
address += PGDIR_SIZE;
//每个表项增加1m,这里感到了section的好处
}
其中clear_mapping()是个宏,根据处理器的
不同,在920下被展开为
cpu_arm920_set_pmd(((pmd_t *)(((&init_mm )->pgd+
(( virt) >> 20 )))),((pmd_t){( 0 )}));
其中init_mm为内核的mm_struct,pgd指向
swapper_pg_dir,在arch/arm/kernel/init_task.c中定义
ENTRY(cpu_arm920_set_pmd)
#ifdef CONFIG_CPU_ARM920_WRITETHROUGH
eor r2, r1, #0x0a
tst r2, #0x0b
biceq r1, r1, #4
#endif
str r1, [r0]
把pmd_t填写到页表项中,由于pmd_t=0,
实际等于清除了这一项,由于d cache打开,
这一条指令实际并没有写回内存,而是写到cache中
mcr p15, 0, r0, c7, c10, 1
把cache中 地址r0对应的内容写回内存中,
这一条语句实际是写到了write buffer中,
还没有真正写回内存。
mcr p15, 0, r0, c7, c10, 4
等待把write buffer中的内容写回内存。在这之前core等待
mov pc, lr
在这里我们看到,由于页表的内容十分关键,为了确保写回内存,
采用了直接操作cache的方法。由于在arm core中,打开了d cache
则必定要用write buffer.所以还有wb的回写问题。
由于考虑到效率,我们使用了cache和buffer,
所以在某些地方要用指令保证数据被及时写回。
下面映射c000 0000后面的页表
else {
create_mapping(q);
address = q->virtual + q->length;
address = (address + PGDIR_SIZE - 1) & PGDIR_MASK;
q ++;
}
} while (address != 0);
create_mapping也在mm-armv.c中定义;
static void __init create_mapping(struct map_desc *md)
{
unsigned long virt, length;
int prot_sect, prot_pte;
long off;
prot_pte = L_PTE_PRESENT | L_PTE_YOUNG | L_PTE_DIRTY |
(md->prot_read ? L_PTE_USER : 0) |
(md->prot_write ? L_PTE_WRITE : 0) |
(md->cacheable ? L_PTE_CACHEABLE : 0) |
(md->bufferable ? L_PTE_BUFFERABLE : 0);
prot_sect = PMD_TYPE_SECT | PMD_DOMAIN(md->domain) |
(md->prot_read ? PMD_SECT_AP_READ : 0) |
(md->prot_write ? PMD_SECT_AP_WRITE : 0) |
(md->cacheable ? PMD_SECT_CACHEABLE : 0) |
(md->bufferable ? PMD_SECT_BUFFERABLE : 0);
由于arm中section表项的权限位和page表项的位置不同,
所以根据struct map_desc 中的保护标志,分别计算页表项
中的AP,domain,CB标志位。
有一段时间没有写了,道歉先,前一段时间在做arm linux的xip,终于找到了
在flash中运行kernel的方法,同时对系统的存储管理
的理解更深了一层,我们继续从上回的create_mapping往下看:
while ((virt & 0xfffff || (virt + off) & 0xfffff) && length >= PAGE_SIZE) {
alloc_init_page(virt, virt + off, md->domain, prot_pte);
virt += PAGE_SIZE;
length -= PAGE_SIZE;
}
while (length >= PGDIR_SIZE) {
alloc_init_section(virt, virt + off, prot_sect);
virt += PGDIR_SIZE;
length -= PGDIR_SIZE;
}
while (length >= PAGE_SIZE) {
alloc_init_page(virt, virt + off, md->domain, prot_pte);
virt += PAGE_SIZE;
length -= PAGE_SIZE;
}
这3个循环的设计还是很巧妙的,create_mapping的作用是设置虚地址virt
到物理地址virt + off的映射页目录和页表。arm提供了4种尺寸的页表:
1M,4K,16K,64K,armlinux只用到了1M和4K两种。
这3个while的作用分别是“掐头“,“去尾“,“砍中间“。
第一个while是判断要映射的地址长度是否大于1m,且是不是1m对齐,
如果不是,则需要创建页表,例如,如果要映射的长度为1m零4k,则先要将“零头“
去掉,4k的一段需要中间页表,通过第一个while创建中间页表,
而剩下的1M则交给第二个while循环。最后剩下的交给第三个while循环。
alloc_init_page分配并填充中间页表项
static inline void
alloc_init_page(unsigned long virt, unsigned long phys, int domain, int prot)
{
pmd_t *pmdp;
pte_t *ptep;
pmdp = pmd_offset(pgd_offset_k(virt), virt);//返回页目录中virt对应的表项
if (pmd_none(*pmdp)) {//如果表项是空的,则分配一个中间页表
pte_t *ptep = alloc_bootmem_low_pages(2 * PTRS_PER_PTE *
sizeof(pte_t));
ptep += PTRS_PER_PTE;
//设置页目录表项
set_pmd(pmdp, __mk_pmd(ptep, PMD_TYPE_TABLE | PMD_DOMAIN(domain)));
}
ptep = pte_offset(pmdp, virt);
//如果表项不是空的,则表项已经存在,只需要设置中间页表表项
set_pte(ptep, mk_pte_phys(phys, __pgprot(prot)));
}
alloc_init_section只需要填充页目录项
alloc_init_section(unsigned long virt, unsigned long phys, int prot)
{
pmd_t pmd;
pmd_val(pmd) = phys | prot;//将物理地址和保护标志合成页目录项
set_pmd(pmd_offset(pgd_offset_k(virt), virt), pmd);
}
通过create_mapping可为内核建立所有的地址映射,最后是映射中断向量表
所在的区域:
init_maps->physical = virt_to_phys(init_maps);
init_maps->virtual = vectors_base();
init_maps->length = PAGE_SIZE;
init_maps->domain = DOMAIN_USER;
init_maps->prot_read = 0;
init_maps->prot_write = 0;
init_maps->cacheable = 1;
init_maps->bufferable = 0;
create_mapping(init_maps);
中断向量表的虚地址init_maps,是用alloc_bootmem_low_pages分配的,
通常是在c000 8000前面的某一页, vectors_base()是个宏,arm规定中断
向量表的地址只能是0或ffff0000,在cp15中设置。所以上述代码映射一页到
0或ffff0000,下面我们还会看到,中断处理程序中的汇编部分也被拷贝到
这一页中。
Kernel硬件中断的初始化流程
Kernel硬件中断的初始化流程
aZULinux联盟aZULinux联盟Porting kernel到一个全新的开发板时,通常hardware irq的初始化函数是要我们自己实现的。
aZULinux联盟那我们实现了自己硬件的中断初始化函数之后,内核是如何调用到它的呢?内核有自己的一套支持多平台的架构。
aZULinux联盟下面我们分析内核中断初始化的过程以及如何调用到一个新平台的irq初始化函数。
aZULinux联盟这里我们以s3c2410平台为例,他的中断初始化函数定义在:
aZULinux联盟/* arch/arm/mach-s3c2410/irq.c */
aZULinux联盟void __init s3c24xx_init_irq(void)
aZULinux联盟{
aZULinux联盟……
aZULinux联盟}
aZULinux联盟aZULinux联盟在arch/arm/mach-s3c2410/mach-smdk2410.c内通过MACHINE_START宏将s3c24xx_init_irq赋值给mach_desc结构体的.init_irq成员。
aZULinux联盟aZULinux联盟MACHINE_START(SMDK2410, "SMDK2410") /* @TODO: request a new identifier and switch
aZULinux联盟 * to SMDK2410 */
aZULinux联盟/* Maintainer: Jonas Dietsche */
aZULinux联盟.phys_io = S3C2410_PA_UART,
aZULinux联盟.io_pg_offst = (((u32)S3C24XX_VA_UART) >> 18) & 0xfffc,
aZULinux联盟.boot_params = S3C2410_SDRAM_PA + 0x100,
aZULinux联盟.map_io = smdk2410_map_io,
aZULinux联盟.init_irq = s3c24xx_init_irq,
aZULinux联盟.init_machine = smdk_machine_init,
aZULinux联盟.timer = &s3c24xx_timer,
aZULinux联盟MACHINE_END
aZULinux联盟注:MACHINE_START宏的作用是对mach_desc结构体进行初始化。mach_desc里定义了一些关键的体系架构相关的函数。Porting kernel到新平台时,这个结构体是非常关键的。
aZULinux联盟aZULinux联盟init_irq这个成员在系统初始化的时候会被赋值给init_arch_irq全局变量,如下:
aZULinux联盟/* arch/arm/kernel/setup.c */
aZULinux联盟void __init setup_arch(char **cmdline_p)
aZULinux联盟{
aZULinux联盟……
aZULinux联盟cpu_init();
aZULinux联盟/*
aZULinux联盟 * Set up various architecture-specific pointers
aZULinux联盟 */
aZULinux联盟init_arch_irq = mdesc->init_irq;
aZULinux联盟system_timer = mdesc->timer;
aZULinux联盟init_machine = mdesc->init_machine;
aZULinux联盟……
aZULinux联盟}
aZULinux联盟注:可以看到这里不仅初始化了init_arch_irq 全局变量,同时初始化了system_timer,init_machine等全局变量。这是kernel支持多平台的一种机制。当然这里system_timer和init_machine我不多描述,有兴趣的可以大家自己去看。机制和init_arch_irq大同小异。
aZULinux联盟aZULinux联盟init_arch_irq函数指针定义在体系架构无关的arch/arm/kernel/irq.c内
aZULinux联盟/* arch/arm/kernel/irq.c */
aZULinux联盟void (*init_arch_irq)(void) __initdata = NULL;
aZULinux联盟aZULinux联盟并且在init_IRQ函数内会去执行它。
aZULinux联盟/* arch/arm/kernel/irq.c */
aZULinux联盟void __init init_IRQ(void)
aZULinux联盟{
aZULinux联盟int irq;
aZULinux联盟for (irq = 0; irq NR_IRQS; irq++)
aZULinux联盟 irq_desc[irq].status |= IRQ_NOREQUEST | IRQ_DELAYED_DISABLE |
aZULinux联盟 IRQ_NOPROBE;
aZULinux联盟#ifdef CONFIG_SMP
aZULinux联盟bad_irq_desc.affinity = CPU_MASK_ALL;
aZULinux联盟bad_irq_desc.cpu = smp_processor_id();
aZULinux联盟#endif
aZULinux联盟init_arch_irq();
aZULinux联盟}
aZULinux联盟aZULinux联盟那init_IRQ在哪里被调用呢? 我们猜想肯定是在系统初始化的时会调用它。
aZULinux联盟实际结果也正是,init_IRQ会在init/main.c里的start_kernel函数内被调用:
aZULinux联盟asmlinkage void __init start_kernel(void)
aZULinux联盟{
aZULinux联盟……
aZULinux联盟trap_init();
aZULinux联盟rcu_init();
aZULinux联盟init_IRQ();
aZULinux联盟pidhash_init();
aZULinux联盟clockevents_init();
aZULinux联盟init_timers();
aZULinux联盟……
aZULinux联盟}
aZULinux联盟这样,我们定义的新平台irq初始化函数就会在系统启动时被调用,对我们的硬件中断进行初始化后再去使用它。这里搞清楚了,再porting其他东西如GP Timer Driver等到新平台就变得清晰多了。
Linux kernel2.6.25 CS8900网卡驱动移植
一般来说,我们在编译kernel时,设备驱动的选择有两种方式:一种是直接编译到kernel里,另一种是以模块方式挂接。CS8900网卡驱动如果以模块方式挂接,函数init_module就是入口;如果是直接编译到kernel里,那么函数cs89x0_probe才是入口。在此入口函数中,将完成网卡驱动的各项初始化。如注册虚拟地址,设备号,中断号,以及各个相关寄存器的初始化。
cs89x0_probe函数里会去调用真正的初始化函数cs89x0_probe1。下面说一下该初始化函数里需要完成的几个重要地方:
1、 注册虚拟地址。
通过request_region函数注册虚拟地址。在kenel里面,我们所操作的寄存器的地址其实都是虚拟地址,但是每一个寄存器的虚拟地址都有唯一和其对应的物理地址,因为在kernel里面任何虚拟地址都会通过MMU转化成物理地址。所以在kernel里,定义完所要用到的寄存器后,都必须使用一个函数ioremap将我们所要用到的寄存器的物理地址转换成为在kernel里可以操作的虚拟地址,然后才能将他们用以具体的操作,否则一切都是徒劳。
ioaddr = (int)ioremap(BASE_ADDR,16);
2、填充net_device结构体。
该结构体的成员都是和网络设备有关的变量。其中比较重要的有两个:dev_addr和open。dev_addr里要存的是主机的MAC地址,一般都是从eeproom中读出来再存放到该变量中,当然也可以根据自己的需要手动赋值。
for (i=0; i < ETH_ALEN/2; i++) {
unsigned int Addr;
Addr = readreg(dev, PP_IA+i*2);
dev->dev_addr[i*2] = Addr & 0xFF;
dev->dev_addr[i*2+1] = Addr >> 8;
}
|
Open是一个函数指针,需要把net_open函数赋值给他。net_open函数是一个专门用来注册网络设备中断号的函数,输入ifconfig命令时,最后就会调用到这个函数。在这个函数中要把中断号设置一下。
writereg(dev, PP_BusCTL, ENABLE_IRQ | MEMORY_ON);
request_irq(dev->irq, &net_interrupt, 0, dev->name, dev);
|
3、 I/O端口的中断请求设置。
网卡不可能也不需要时时刻刻都处于中断状态,合理的中断触发时机是一个必要条件。根据硬件电路图的引脚可知,相对应的中断请求寄存器是GPG1和EINT9。在GPG1寄存器里面要把EINT9寄存器功能激活,而在EINT9寄存器里面则要把中断设置为上跳沿触发。
writel(readl(S 3C2410_GPGCON) | 0x8, S3C2410_GPGCON);
writel(readl(S3C2410_EXTINT1) | 0x40, S3C2410_EXTINT1);
|
还有一点要注意,CS8900网卡的寄存器都是16位的,所以在选择读写函数时也必须选择16位寄存器的读写函数。
static u16 readword(unsigned long base_addr, int portno)
{
return inw(base_addr + portno);
}
static void writeword(unsigned long base_addr, int portno, u16 value)
{
outw(value, base_addr + portno);
}
|
以上便是Linux kernel2.6.25 CS8900网卡驱动移植所需注意的内容。Kernel里面涉及和兼容的东西非常多,去除容易产生冲突的部分,添加自己需要实现的功能,可以使得移植工作能够顺利进行。
struct的初始化,拷贝及指针成员的使用技巧
先列提纲,有时间再写!Wh3Linux联盟
struct TempWh3Linux联盟
{Wh3Linux联盟
int data;Wh3Linux联盟
char name[100];Wh3Linux联盟
int len;Wh3Linux联盟
char *path;Wh3Linux联盟
};Wh3Linux联盟
Wh3Linux联盟
1 初始化Wh3Linux联盟
struct 型的变量有3中初始化方法。Wh3Linux联盟
1)顺序Wh3Linux联盟
2)乱序(C风格)Wh3Linux联盟
3)乱序(C++风格)Wh3Linux联盟
2 拷贝Wh3Linux联盟
struct有两种拷贝方式,一是直接赋值(=),另一种是用memcpy等库函数实行内存拷贝,如:Wh3Linux联盟
strcut TestWh3Linux联盟
{Wh3Linux联盟
TypeA data1;Wh3Linux联盟
.....Wh3Linux联盟
TypeN datan;Wh3Linux联盟
};Wh3Linux联盟
struct Test a, b;Wh3Linux联盟
a = b;Wh3Linux联盟
memcpy(&a, &b, sizeof(a));Wh3Linux联盟
Wh3Linux联盟
但不管是哪种拷贝方式,都Wh3Linux联盟
3 指针成员的两种使用技巧Wh3Linux联盟
1) 为多个成员指针同时分配内存Wh3Linux联盟
2)为最后一个成员预留空间
分页机制启用以后,与内存管理相关的操作就是调用init/main.c中的start_kernel()函数,start_kernel()函数要调用一个叫setup_arch()的函数,setup_arch()位于arch/i386/kernel/setup.c文件中,我们所关注的与物理内存探测相关的内容就在这个函数中。
1.setup_arch()函数
这个函数比较繁琐和冗长,下面我们只对setup_arch()中与内存相关的内容给予描述。
· 首先调用setup_memory_region()函数,这个函数处理内存构成图(map),并把内存的分布信息存放在全局变量e820中,后面会对此函数进行具体描述。
· 调用parse_mem_cmdline(cmdline_p)函数。在特殊的情况下,有的系统可能有特殊的RAM空间结构,此时可以通过引导命令行中的选择项来改变存储空间的逻辑结构,使其正确反映内存的物理结构。此函数的作用就是分析命令行中的选择项,并据此对数据结构e820中的内容作出修正,其代码也在setup.c中。
· 宏定义:
#define PFN_UP(x) (((x) + PAGE_SIZE-1) >> PAGE_SHIFT)
#define PFN_DOWN(x) ((x) >> PAGE_SHIFT)
#define PFN_PHYS(x) ((x) << PAGE_SHIFT)
PFN_UP() 和PFN_DOWN()都是将地址x转换为页面号(PFN即Page Frame Number的缩写),二者之间的区别为:PFN_UP()返回大于x的第一个页面号,而PFN_DOWN()返回小于x的第一个页面号。宏PFN_PHYS()返回页面号x的物理地址。
· 宏定义
/*
* 128MB for vmalloc and initrd
*/
#define VMALLOC_RESERVE (unsigned long)(128 << 20)
#define MAXMEM (unsigned long)(-PAGE_OFFSET-VMALLOC_RESERVE)
#define MAXMEM_PFN PFN_DOWN(MAXMEM)
#define MAX_NONPAE_PFN (1 << 20)
对这几个宏描述如下:
VMALLOC_RESERVE :为vmalloc()函数访问内核空间所保留的内存区,大小为128MB。
MAXMEM :内核能够直接映射的最大RAM容量,为1GB-128MB=896MB(-PAGE_OFFSET就等于1GB)
MAXMEM_PFN :返回由内核能直接映射的最大物理页面数。
MAX_NONPAE_PFN :给出在4GB之上第一个页面的页面号。当页面扩充(PAE)功能启用时,才能访问4GB以上的内存。
· 获得内核映像之后的起始页面号
/*
* partially used pages are not usable - thus
* we are rounding upwards:
*/
start_pfn = PFN_UP(__pa(&_end));
在上一节已说明,宏__pa()返回给定虚拟地址的物理地址。其中标识符_end表示内核映像在内核空间的结束位置。因此,存放在变量start_pfn中的值就是紧接着内核映像之后的页面号。
· 找出可用的最高页面号
/*
* Find the highest page frame number we have available
*/
max_pfn = 0;
for (i = 0; i < e820.nr_map; i++) {
unsigned long start, end;
/* RAM? */
if (e820.map[i].type != E820_RAM)
continue;
start = PFN_UP(e820.map[i].addr);
end = PFN_DOWN(e820.map[i].addr + e820.map[i].size);
if (start >= end)
continue;
if (end > max_pfn)
max_pfn = end;
}
上面这段代码循环查找类型为E820_RAM(可用RAM)的内存区,并把最后一个页面的页面号存放在max_pfn中。
· 确定最高和最低内存范围
/*
* Determine low and high memory ranges:
*/
max_low_pfn = max_pfn;
if (max_low_pfn > MAXMEM_PFN) {
max_low_pfn = MAXMEM_PFN;
#ifndef CONFIG_HIGHMEM
/* Maximum memory usable is what is directly addressable */
printk(KERN_WARNING "Warning only %ldMB will be used.\n",
MAXMEM>>20);
if (max_pfn > MAX_NONPAE_PFN)
printk(KERN_WARNING "Use a PAE enabled kernel.\n");
else
printk(KERN_WARNING "Use a HIGHMEM enabled kernel.\n");
#else /* !CONFIG_HIGHMEM */
#ifndef CONFIG_X86_PAE
if (max_pfn > MAX_NONPAE_PFN) {
max_pfn = MAX_NONPAE_PFN;
printk(KERN_WARNING "Warning only 4GB will be used.\n");
printk(KERN_WARNING "Use a PAE enabled kernel.\n");
}
#endif /* !CONFIG_X86_PAE */
#endif /* !CONFIG_HIGHMEM */
}
有两种情况:
(1) 如果物理内存RAM大于896MB,而小于4GB,则选用CONFIG_HIGHMEM选项来进行访问。
(2) 如果物理内存RAM大于4GB,则选用CONFIG_X86_PAE(启用PAE模式)来进行访问。
上面这段代码检查了这两种情况,并显示适当的警告信息。
#ifdef CONFIG_HIGHMEM
highstart_pfn = highend_pfn = max_pfn;
if (max_pfn > MAXMEM_PFN) {
highstart_pfn = MAXMEM_PFN;
printk(KERN_NOTICE "%ldMB HIGHMEM available.\n",
pages_to_mb(highend_pfn - highstart_pfn));
}
#endif
如果使用了CONFIG_HIGHMEM 选项,上面这段代码仅仅打印出大于896MB的可用物理内存数量。
· 初始化引导时的分配器
* Initialize the boot-time allocator (with low memory only):
*/
bootmap_size = init_bootmem(start_pfn, max_low_pfn);
通过调用init_bootmem()函数,为物理内存页面管理机制的建立做初步准备,为整个物理内存建立起一个页面位图。这个位图建立在从start_pfn开始的地方,也就是说,把内核映像终点_end上方的若干页面用作物理页面位图。在前面的代码中已经搞清楚了物理内存顶点所在的页面号为max_low_pfn,所以物理内存的页面号一定在0~max_low_pfn之间。可是,在这个范围内可能有空洞(hole),另一方面,并不是所有的物理内存页面都可以动态分配。建立这个位图的目的就是要搞清楚哪一些物理内存页面可以动态分配的。后面会具体描述bootmem分配器。
· 用bootmem 分配器,登记全部低区(0~896MB)的可用RAM页面
/*
* Register fully available low RAM pages with the
* bootmem allocator.
*/
for (i = 0; i < e820.nr_map; i++) {
unsigned long curr_pfn, last_pfn, size;
/*
* Reserve usable low memory
*/
if (e820.map[i].type != E820_RAM)
continue;
/*
* We are rounding up the start address of usable memory:
*/
curr_pfn = PFN_UP(e820.map[i].addr);
if (curr_pfn >= max_low_pfn)
continue;
/*
* ... and at the end of the usable range downwards:
*/
last_pfn = PFN_DOWN(e820.map[i].addr + e820.map[i].size);
if (last_pfn > max_low_pfn)
last_pfn = max_low_pfn;
/*
* .. finally, did all the rounding and playing
* around just make the area go away?
*/
if (last_pfn <= curr_pfn)
continue;
size = last_pfn - curr_pfn;
free_bootmem(PFN_PHYS(curr_pfn), PFN_PHYS(size));
}
这个循环仔细检查所有可以使用的RAM,并调用free_bootmem()函数把这些可用RAM标记为可用。这个函数调用以后,只有类型为1(可用RAM)的内存被标记为可用的,参看后面对这个函数的具体描述。
· 保留内存
/*
* Reserve the bootmem bitmap itself as well. We do this in two
* steps (first step was init_bootmem()) because this catches
* the (very unlikely) case of us accidentally initializing the
* bootmem allocator with an invalid RAM area.
*/
reserve_bootmem(HIGH_MEMORY, (PFN_PHYS(start_pfn) +
bootmap_size + PAGE_SIZE-1) - (HIGH_MEMORY));
这个函数把内核和bootmem位图所占的内存标记为“保留”。 HIGH_MEMORY为1MB,即内核开始的地方,后面还要对这个函数进行具体描述
· 分页机制的初始化
paging_init();
这个函数初始化分页内存管理所需要的数据结构,参见后面的详细描述。
2. setup_memory_region() 函数
这个函数用来处理BIOS的内存构成图和并把这个构成图拷贝到全局变量e820中。如果操作失败,就创建一个伪内存构成图。这个函数的主要操作为:
· 调用sanitize_e820_map()函数,以删除内存构成图中任何重叠的部分,因为BIOS所报告的内存构成图可能有重叠。
· 调用copy_e820_map()进行实际的拷贝。
· 如果操作失败,创建一个伪内存构成图,这个伪构成图有两部分:0到640K及1M到最大物理内存。
· 打印最终的内存构成图
3.copy_e820_map() 函数
函数原型为:
static int __init sanitize_e820_map(struct e820entry * biosmap, char * pnr_map)
其主要操作为:
· 如果物理内存区间小于2,那肯定出错。因为BIOS至少和RAM属于不同的物理区间。
if (nr_map < 2)
return -1;
· 从BIOS构成图中读出一项
do {
unsigned long long start = biosmap->addr;
unsigned long long size = biosmap->size;
unsigned long long end = start + size;
unsigned long type = biosmap->type;
· 进行检查
/* Overflow in 64 bits? Ignore the memory map. */
if (start > end)
return -1;
· 一些BIOS把640K~1MB之间的区间作为RAM来用,这是不符合常规的。因为从0xA0000开始的空间用于图形卡,因此,在内存构成图中要进行修正。如果一个区的起点在0xA0000以下,而终点在1MB之上,就要将这个区间拆开成两个区间,中间跳过从0xA0000到1MB边界之间的那一部分。
/*
* Some BIOSes claim RAM in the 640k - 1M region.
* Not right. Fix it up.
*/
if (type == E820_RAM) {
if (start < 0x100000ULL && end > 0xA0000ULL) {
if (start < 0xA0000ULL)
add_memory_region(start, 0xA0000ULL-start, type)
if (end <= 0x100000ULL)
continue;
start = 0x100000ULL;
size = end - start;
}
}
add_memory_region(start, size, type);
} while (biosmap++,--nr_map);
return 0;
4. add_memory_region() 函数
这个函数的功能就是在e820中增加一项,其主要操作为:
· 获得已追加在e820中的内存区数
int x = e820.nr_map;
· 如果数目已达到最大(32),则显示一个警告信息并返回
if (x == E820MAX) {
printk(KERN_ERR "Oops! Too many entries in
the memory map!\n");
return;
}
· 在e820中增加一项,并给nr_map加1
e820.map[x].addr = start;
e820.map[x].size = size;
e820.map[x].type = type;
e820.nr_map++;
5. print_memory_map() 函数
这个函数把内存构成图在控制台上输出,函数本身比较简单,在此给出一个运行实例。例如函数的输出为(BIOS所提供的物理RAM区间):
BIOS-e820: 0000000000000000 - 00000000000a0000 (usable)
BIOS-e820: 00000000000f0000 - 0000000000100000 (reserved)
BIOS-e820: 0000000000100000 - 000000000c000000 (usable)
BIOS-e820: 00000000ffff0000 - 0000000100000000 (reserved)