非一致内存访问(NUMA)模型、节点(node)、内存管理区(Zone)、一致内存访问(UMA)模型、内核页表、内存管理区分配器(伙伴系统Buddy System)、slab系统、活动页链表、非活动页链表、保留物理页框、TLB抖动、内存管理的初始化…
直接映射:是指将某一块连续地虚拟内存区间固定地映射到一块物理内存区间,生成相应的映射页表表项,这些表项生成之后页表表项不会改变,映射关系保持不变【固定】,直到操作系统shutdown。
动态映射: 直接映射是将一块虚拟内存区间固定地映射到一块物理内存区间,映射关系一直保持。而动态映射正好相反,它可以先将一块虚拟内存区间映射到某块物理内存区间上,之后还可以改变这种不固定的映射,将这块虚拟内存区间映射到另外的物理内存区间上。为了描述方便,我假定物理内存区间在物理内存地址空间是连续的,但实际上并不需要保证连续性【可以是离散的页组成的物理内存区间】。
永久映射: 将物理高端内存页帧映射到内核线性地址空间的持久映射区[PKMap区],一旦映射建立之后,在解除映射关系之前,对应的页表表项都不能更改为其他映射关系。
虚拟内存区间连续:是指一块虚拟内存区间在虚拟地址空间中是连续的,但不要求与之对应的物理内存区间在物理内存地址空间中连续。
物理内存区间连续:是指一块物理内存区间在物理内存地址空间中连续。
虚拟地址:代码中对地址的引用。
线性地址:虚拟地址经过段机制映射之后的地址[在没有段机制时,虚拟地址就是线性地址]。
物理地址:线性地址经过分页机制映射之后的地址[在没有分页机制时,线性地址就是物理地址]。
内核页目录:内核页目录一般是指内核全局页目录,ilde和init在初始化系统时就使用的这个页目录。这个页目录只在系统启动早期被这几个内核初始化线程使用,后面就不会被任何进程用做页目录。因为调度器选择一个内核线程执行的时候,内核线程会
挪用
被调度出去的进程的mm
[地址空间],所以说内核线程是没有自己的地址空间的【当然,除了系统启动时的idle和init线程,不过,后面idle和init以及其他所有内核线程一样,在进程切换的时候使用被调度走的那个进程的页目录】。进程页目录:进程拥有自己的地址空间
mm
,所以有自己的页目录mm->pgd
,在切换到他时,内核会更新cr3
寄存器,使用他的页目录。
参考《linux内核完全注释 》《Linux Kernel Analysis》,下面的关注点在内存管理。
注:文中代码来自Linux内核版本2.6.11
代码最初运行在实模式下,BIOS完成开机自检等过程,初始化IVT【中断向量表】之后,会读取【用户设定或选择的】启动存储设备的第一个扇区。这个扇区的512B包含系统引导代码[bootsect.s]以及磁盘分区表,其中的启动代码[boot/bootsect.s]属于bootloader[boot/bootsect.s+boot/setup.s] 的一部分。
注:现在的bootloader已经不再是原来的[boot/bootsect.s+boot/setup.s]的形式,而是:
第三方的bootloader[GRUB…] 【完成bootsect.s的功能以及一些其他功能】+ boot/setup.s
加载操作系统内核到内存中,使能段机制,完成实模式到保护模式的转换。
bootloader[boot/bootsect.s]首先将[setup.s]和[内核(包含boot/compressed/head.s)]加载到内存中,然后跳转到setup.s。
接下来bootloader[boot/setup.s] 建立 system’s physical memory map、将内核转移到物理地址0x1000[4k]【小内核】或者0x100000[1M]【大内核】。
boot/compressed/head.s位于这个物理地址处。
建立临时的IDT,然后建立好临时的GDT【全局描述符表/段描述符表】,使得:
virt addr=linear addr = phy addr。【虚拟地址与物理地址的对等映射】
在建立好GDT后使能段机制,进入保护模式。
这里,段机制实际上就是实现了一个对等映射,为什么要这么做呢?必然是为了进入保护模式,扩展内存寻址空间啊【20 -> 32】!
注意,这时候还没有建立页表,也没有使能页机制。这里的代码是基于物理内存地址进行编写和链接的【编写bootloader时就知道它会初始化GDT,实现一个虚拟地址与物理地址的对等映射,然后开启段机制进入保护模式,扩展寻址空间】。
进入保护模式之后,bootloader[boot/setup.s]使pc跳转到0x1000【小内核】或者0x100000 【大内核】[boot/compressed/head.s]去执行指令。
//boot/setup.s ... // jump to startup_32 in arch/i386/boot/compressed/head. // NOTE: For high loaded big kernels we need a // jmpi 0x100000,__BOOT_CS ... /* default: jump to 0x10:0x1000 For high loaded big kernels: jump to 0x10:0x100000 (segment number: 0x10; offset: 0x100000.) */ ...
head.s主要解压缩内核映像(decompress_kernel),解压后的结果最终都将放在0x100000[1M]处(不管大内核、小内核)。然后转入内核[kernel/head.s]:
//boot/compressed/head.s startup_32: ... ljmp $(__KERNEL_CS), $0x100000 //kernel/head.s中的startup_32缺省地址:0x100000,所以 //这行代码执行之后,pc跳到kernel/head.s中的startup_32处 /* 在解压缩的过程中,kernel/head.s会覆盖原来的boot/head.s */ ...
通过这些步骤之后,真正开始了内核初始化过程,包括:
- 启动分页机制;
- 让操作系统各组成部分(内存管理、进程管理等)分别完成自己的初始化,如建立各种管理用的数据结构等;
- 完成外部设备的初始化;
- 创建并启动用户进程;
- 启动Shell或GUI,开始与用户交互
内核最初的初始化任务由[kernel/head.s]来完成。
线性地址空间大小
- 一个页目录大小为一页,4KB,每个页目录项为4字节,因此,一个页目录包含1024个页目录项,即能够描述1024个页表。
- 一个页表大小为一页,4KB,每个页表项为4字节,因此,一个页表包含1024个页表项,即能够描述1024个页。
线性地址由页目录+页表+偏移量组成,而系统中只有一个页目录,那么线性地址空间能表示的最大范围为1024*1024个4KB页=4GB。
kernel/head.s开启页机制
注意,内核【包括kernel/head.s】的代码被链接到了__PAGE_OFFSET之上的线性空间中,实际却被装载在物理地址0x100000[1M]的位置,所以要注意代码中符号的引用[线性地址]对应的物理地址是否正确。
/* * linux/arch/i386/kernel/head.S -- the 32-bit startup code. */ ... ENTRY(startup_32) ... //sets up the final GDT lgdt boot_gdt_descr - __PAGE_OFFSET //__PAGE_OFFSET = 0xC0000000[for 32bit os] /* boot_gdt_descr的线性地址位于__PAGE_OFFSET之上,但实际位于物理地址: boot_gdt_descr - __PAGE_OFFSET处。 */ /* * builds provisional kernel page tables so that paging can be turned on * 建立临时页表 ,代码在后文给出,带有注释 */ ... /* * Enable paging * 使能页机制 */ movl $swapper_pg_dir-__PAGE_OFFSET,%eax movl %eax,%cr...3 /* set the page table pointer.. */ movl %cr0,%eax orl $0x80000000,%eax movl %eax,%cr0 /* ..and set paging (PG) bit */ ljmp $__BOOT_CS,$1f /* Clear prefetch and normalize %eip */ //使能页机制之后,因为建立了临时的映射关系,所以往后对符号的引用不再用手动的计算其 //实际所在的物理地址,MMU会帮你安装页表给出的映射关系自动完成计算。 ... lgdt cpu_gdt_descr ... call setup_idt ... /* * setup_idt * * creates the final interrupt descriptor table */ setup_idt: ... /* This is the default interrupt "handler" :-) */ ALIGN ignore_int: ... iret ... //下面的数据段随内核镜像被加载到了物理内存中,但其符号的线性地址却在链接脚本中被设置为 //相对物理地址偏移__PAGE_OFFSET,所以在建立页表映射并使能页机制之前,对这些符号的使用 //要格外小心,需要手动地在代码中计算出其实际物理地址。所以,head.s需尽快地建立临时页 //表,使能页机制。 /* * BSS section */ .section ".bss.page_aligned","w" ENTRY(swapper_pg_dir) .fill 1024,4,0 ENTRY(empty_zero_page) .fill 4096,1,0 /* * This starts the data section. */ .data ENTRY(stack_start)//kernel stack .long init_thread_union+THREAD_SIZE .long __BOOT_DS ... .globl boot_gdt_descr .globl idt_descr .globl cpu_gdt_descr ALIGN # early boot GDT descriptor (must use 1:1 address mapping) .word 0 # 32 bit align gdt_desc.address boot_gdt_descr: .word __BOOT_DS+7 .long boot_gdt_table - __PAGE_OFFSET .word 0 # 32-bit align idt_desc.address idt_descr: .word IDT_ENTRIES*8-1 # idt contains 256 entries .long idt_table # boot GDT descriptor (later on used by CPU#0): .word 0 # 32 bit align gdt_desc.address cpu_gdt_descr: .word GDT_ENTRIES*8-1 .long cpu_gdt_table .fill NR_CPUS-1,8,0 # space for the other GDT descriptors /* * The boot_gdt_table must mirror the equivalent in setup.S and is * used only for booting. */ .align L1_CACHE_BYTES ENTRY(boot_gdt_table) .fill GDT_ENTRY_BOOT_CS,8,0 .quad 0x00cf9a000000ffff /* kernel 4GB code at 0x00000000 */ .quad 0x00cf92000000ffff /* kernel 4GB data at 0x00000000 */ /* * The Global Descriptor Table contains 28 quadwords, per-CPU. */ .align PAGE_SIZE_asm ENTRY(cpu_gdt_table) .quad 0x0000000000000000 /* NULL descriptor */ .quad 0x0000000000000000 /* 0x0b reserved */ ... .quad 0x0000000000000000 /* 0x33 TLS entry 1 */ .quad 0x0000000000000000 /* 0x3b TLS entry 2 */ .quad 0x0000000000000000 /* 0x43 TLS entry 3 */ ... .quad 0x00cf9a000000ffff /* 0x60 kernel 4GB code at 0x00000000 */ .quad 0x00cf92000000ffff /* 0x68 kernel 4GB data at 0x00000000 */ .quad 0x00cffa000000ffff /* 0x73 user 4GB code at 0x00000000 */ .quad 0x00cff2000000ffff /* 0x7b user 4GB data at 0x00000000 */ ...
Mappings are created both at
virtual address 0
(identity mapping) andPAGE_OFFSET
for up to_end+sizeof(page tables)+INIT_MAP_BEYOND_END
.
/* * This is how much memory *in addition to the memory covered up to * and including _end* we need mapped initially. We need one bit for * each possible page, but only in low memory, which means * 2^32/4096/8 = 128K worst case (4G/4G split.) * This should be a multiple of a page. */ #define INIT_MAP_BEYOND_END (128*1024) //128KB, used as a bitmap covering all pages. //128k的内存,其中1bit代表一页物理页帧,128k能表示2^32/4096个物理页帧,即4G的物理空间 cld //EFLAGS中的方向位置0 /* * builds provisional kernel page tables so that paging can be turned on * 建立临时页表 */ page_pde_offset = (__PAGE_OFFSET >> 20);//__PAGE_OFFSET在PED的偏移 /* __PAGE_OFFSET是0xc0000000,page_pde_offset = 3072 = 0xc00,是页目录中的第 3072/4 = 768个表项:PDE[768] */ //pg0 starts at _end //swapper_pg_dir starts at the beginning of BSS movl $(pg0 - __PAGE_OFFSET), %edi //第0个页表所在的物理地址 movl $(swapper_pg_dir - __PAGE_OFFSET), %edx //页目录所在的物理地址 movl $0x007, %eax /* 0x007 = PRESENT+RW+USER 用来设置表项的标记位*/ 10://外循环:填充PDE //将edi寄存器中的值+0x007然后赋给寄存器ecx,从后面得知:下一次执行到这, //edi+=page_size。 leal 0x007(%edi),%ecx /* Create PDE entry */ //对等映射空间: 将第i个页表的物理地址写入到PDE的第i项中-->PDE[i] movl %ecx,(%edx) /* Store identity PDE entry */ //内核线性空间[__PAGE_OFFSET之上]:将第i个页表的物理地址写入到PDE的第 //page_pde_offset+i项中-->PDE[page_pde_offset+i] movl %ecx,page_pde_offset(%edx) /* Store kernel PDE entry */ addl $4,%edx //每次外循环,i++ movl $1024, %ecx //每次内循环1024次 11://内循环:填充PTE //stols指令将eax中的值保存到es:edi指向的地址中,若设置了EFLAGS中的方向位置位(即在STOSL //指令前使用STD指令)则edi自减4,否则(使用CLD指令)edi自增4 stosl//%eax初始值为$0 + $0x007,其中的$0表示第0个物理页框的起始物理地址 addl $0x1000,%eax //0x1000=4k,所以每次循环都将填充下一个物理页的物理地址到当前页 //的下一个页表项中去 //执行LOOP指令时,CPU自动将CX的值减1,直到CX为0 ,循环结束 loop 11b //因为%ecx=1024,所以循环1024次,一次循环结束,一张页表完成映射 /* End condition: we must map up to and including INIT_MAP_BEYOND_END */ /* bytes beyond the end of our own page tables; the +0x007 is the attribute bits */ //ebp = edi寄存器的数值 + INIT_MAP_BEYOND_END(128K)+ 0x007 leal (INIT_MAP_BEYOND_END+0x007)(%edi),%ebp cmpl %ebp,%eax //%eax-0x007 = 下一个待映射的物理页的物理地址 //%ebp-0x007 = 当前被映射的页表的最后一项的物理地址 + INIT_MAP_BEYOND_END /* 上面的比较要表达的意思是: +----------+ |----------| --if--+----------+ 8M <———如果当前被映射了的物理空间到了这儿,则可以跳出循环 ... +----------+ | | ------+----------+------if condition | 128K | +----------+ |----4b----|<——————最后一个页表项:init_pg_tables_end +----------+ ... |----------| +----------+ _end <————页表起始位置:pg0 ... +----------+ __bss_start <————页目录起始位置:swapper_pg_dir ... |----------| --if--+----------+ 4M <———如果当前被映射了的物理空间到了这儿,则还需映射,继续外循环 |----------| +----------+ ... 1M +----------+ _text <———内核代码被加载到了这里 ... +----------+ physical memory layout 所以就是要保证包括页表所在的物理空间都要映射到,还要保证至少128k的额外空间 [bitmap]被映射到 */ jb 10b //%eax<%ebp,则表示被映射的物理页帧不够,跳回到外循环。 //It’s certain that no bootable kernel will be greater than 8MB in size, //所以只建立了物理内存前8M的映射: //linear address[0,8M]-->physical address[0,8m] //and //linear address[3G,3G+8M]-->physical address[0,8m] //将临时页表结束位置的物理地址赋值给init_pg_tables_end movl %edi,(init_pg_tables_end - __PAGE_OFFSET) /* 页目录占据一页内存[4k],共4k/4=1024项页目录项,每一项目录项对应一张页表[4k,共1024项 页表项],每一项页表项对应一页物理内存[4k],故完整的页目录共映射了: 1024*1024*4k=4G的线性空间 但是,这里只映射了PDE[1~2]和PDE[page_pde_offset+1~page_pde_offset+2]到物理页区 间:[0,4M] */
最后得到的临时页表会是下面的样子:
图示中,swapper_pg_dir的第0项和第1中项中的pg1和pg0指向的页表中的页表项将线性地址映射成对等的物理内存地址【其中0x300[4Gx3/4]和0x301两项后文会做出解释】:
linear addr = phy addr,使得:virt addr = linear addr = phy addr。 【注意: linear addr = phy addr的映射关系对0x300和0x301两项不成立】
例如:访问虚拟内存地址0x00100300,经过GDT段机制映射之后转化为线性地址0x00100300,经过页表页机制映射之后转化为物理地址0x00100300。
还是这个图,不过,现在重点关注0x300和0x301两项。你会看到第0x300项和第0项以及第0x301项和第1项指向了相同的的页表。技巧就在这里:
访问虚拟内存地址0xc0100700,经过GDT段机制映射之后转化为线性地址0xc00100700,经过页表页机制映射之后转化为物理地址0x00100700。
最后的布局就会是这样:
注意:在pg1上面还有一个128k的空间[bitmap]。
开启页机制之后,内核代码就不用顾忌对符号引用的解析了。接下来内核就要让一系列的子系统去完成自己的初始化工作。
kernel/head.s执行完之后跳转到init/main.c
//kernel/head.s call start_kernel //init/main.c::start_kernel
init/main.c::start_kernel:
start_kernel完成了内核所有的初始化工作。
asmlinkage void __init start_kernel(void)
{
char * command_line;
...
page_address_init();
//for i386: arch\i386\kernel\setup.c::setup_arch
//建立前896M的映射页表,初始化node、zone、memmap、buddy system、kmap区等描述物理内存
//的结构体
setup_arch(&command_line);
/*
进程环境的初始化,创建系统第一个进程:idle
*/
sched_init();
page_alloc_init();
/* 异常处理调用函数表排序 */
sort_main_extable();
/* 重新设置中断向量表 */
trap_init();
...
/* 虚拟文件系统的初始化 */
vfs_caches_init_early();
/*
内存初始化,释放前边标志为保留的所有页面
initializes the kernel's memory management subsystem. It also prints a tabulation
of all available memory and the memory occupied by the kernel.
*/
mem_init();
kmem_cache_init();//初始化slab分配器,建立在buddy system之上
...
anon_vma_init();//匿名虚拟内存域初始化
fork_init(num_physpages); /* 根据物理内存大小计算允许创建进程的数量 */
/*
执行proc_caches_init() , bufer_init(), unnamed_dev_init() ,vfs_caches_init(),
signals_init()等函数对各种管理机制建立起专用的slab缓冲区队列。
*/
proc_caches_init();
buffer_init();
...
signals_init();
/* rootfs populating might need page-writeback */
page_writeback_init();
#ifdef CONFIG_PROC_FS
proc_root_init();//对虚拟文件系统/proc进行初始化
#endif
acpi_early_init();
/* Do the rest non-__init'ed, we're now alive */
rest_init();
}
Linux高端内存
/* 初始化管理高端物理内存页的数据结构,用于kmap【PKMap区】。 */ static struct page_address_map page_address_maps[LAST_PKMAP];//1024页,共4M void __init page_address_init(void) { int i; /* page_address_pool:一个全局的page_address_map链表,初始化为1024项page_address_map。 用作page_address_map的缓存池。在需要page_address_map时,如果page_address_pool不为 空,那就可以跳过创建一个page_address_map的过程,直接从page_address_pool中摘取一项 page_address_pool。【缓存---加快分配速度】 */ 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); /* 初始化page_address_htable数组,一共128项【128个槽】,用来快速查找已经创建了 页表项并建立了映射关系的page对应的page_address_map。一旦在表中获得了一个page 对应的page_address_map,则可以获得该page在页表中被映射到线性地址空间中的 线性地址page_address_map->virtual。 */ /* page_address_htable中挂上的【已被映射到线性地址空间的】物理高端内存页全部被映射到 了内核的PKMap区 */ //page_address_htable中挂着的都是page_address_map链表 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); } //--------------------下面是相关的数据结构------------------------ /* * page_address_map freelist, allocated from page_address_maps. */ static struct list_head page_address_pool; /* freelist */ static spinlock_t pool_lock; /* protects page_address_pool */ /* * Describes one page->virtual association */ struct page_address_map { struct page *page; void *virtual; //通过list字段链接到页表池全局链表page_address_pool中或 //page_address_htable[hash_ptr(page,PA_HASH_ORDER)].lh struct list_head list;//将page_address_map结构体实例链接起来 }; #define PA_HASH_ORDER 7 /* * Hash table bucket */ static struct page_address_slot { struct list_head lh; /* List of page_address_maps */ spinlock_t lock; /* Protect this bucket's list */ } ____cacheline_aligned_in_smp page_address_htable[1<
#ifdef CONFIG_X86_PAE #define LAST_PKMAP 512 #else #define LAST_PKMAP 1024 #endif
这部分初始化的主要工作是:
初始化node、zone、mem_map、建立内核页表、初始化PKMap区、初始化固定映射区。
图示1:内核地址空间布局
/*
* Determine if we were loaded by an EFI loader. If so, then we have also been
* passed the efi memmap, systab, etc., so we should use these data structures
* for initialization. Note, the efi init code path is determined by the
* global efi_enabled. This allows the same kernel image to be used on existing
* systems (with a traditional BIOS) as well as on EFI systems.
*/
void __init setup_arch(char **cmdline_p)
{
unsigned long max_low_pfn;
...
//下面是记录内核代码段的起始,结束虚拟地址
init_mm.start_code = (unsigned long) _text;
init_mm.end_code = (unsigned long) _etext;
init_mm.end_data = (unsigned long) _edata;
init_mm.brk = init_pg_tables_end + PAGE_OFFSET;
//下面是记录内核代码段的起始,结束物理地址
code_resource.start = virt_to_phys(_text);
code_resource.end = virt_to_phys(_etext)-1;
data_resource.start = virt_to_phys(_etext);
data_resource.end = virt_to_phys(_edata)-1;
//描述物理内存:低端内存、高端内存。
max_low_pfn = setup_memory();
/*
* NOTE: before this point _nobody_ is allowed to allocate
* any memory using the bootmem allocator. Although the
* alloctor is now initialised only the first 8Mb of the kernel
* virtual address space has been mapped. All allocations before
* paging_init() has completed must use the alloc_bootmem_low_pages()
* variant (which allocates DMA'able memory) and care must be taken
* not to exceed the 8Mb limit.
*/
...
/*
* paging_init() sets up the page tables - note that the first 8MB are
* already mapped by head.S.
*
* This routines also unmaps the page at virtual kernel address 0, so
* that we can trap those pesky NULL-reference errors in the kernel.
*/
paging_init();
/*
* NOTE: at this point the bootmem allocator is fully available.
*/
...
//Request address space for all standard resources
register_memory();
...
}
Describing Physical Memory
//物理内存描述 /* - Find the start and ending PFN for low memory (min_low_pfn, max_low_pfn), the start and end PFN for high memory (highstart_pfn, highend_pfn) and the PFN for the last page in the system (max_pfn). - Initialise the bootmem_data structure and declare which pages may be used by the boot memory allocator - Mark all pages usable by the system as “free” and then reserve the pages used by the bitmap representing the pages Reserve pages used by the SMP config or the initrd image if one exists */ static unsigned long __init setup_memory(void) { unsigned long bootmap_size, start_pfn, max_low_pfn; /* * partially used pages are not usable - thus * we are rounding upwards: */ //有一部分物理页帧不能使用【被内核image占据什么的...】,所以start_pfn记录 //第一个可用的物理页帧号 start_pfn = PFN_UP(init_pg_tables_end);//向上取整【页】 find_max_pfn();//初始化全局变量max_pfn,得到最大的物理帧号 //finds the highest page frame addressable in ZONE_NORMAL[896M位置的物理帧] max_low_pfn = find_max_low_pfn(); #ifdef CONFIG_HIGHMEM highstart_pfn = highend_pfn = max_pfn; if (max_pfn > max_low_pfn) { highstart_pfn = max_low_pfn; //初始化全局变量highstart_pfn[896M之后的物理起始帧号] } #endif /* * Initialize the boot-time allocator (with low memory only): */ bootmap_size = init_bootmem(start_pfn, max_low_pfn); /* 设置全局变量:max_low_pfn = max_low_pfn; 设置全局变量:min_low_pfn = start_pfn; Initialises the appropriate struct bootmem_data_t and inserts the node into the linked list of nodes pgdat_list. */ /* register_bootmem_low_pages() reads the e820 map and calls free_bootmem() for all usable pages in the running system. This is what marks the pages marked as reserved during initialisation as free 将系统中所有可用的物理内存标记为可用 */ register_bootmem_low_pages(max_low_pfn); /* Reserve the pages that are being used to store the bitmap representing the pages */ reserve_bootmem(HIGH_MEMORY, (PFN_PHYS(start_pfn) + bootmap_size + PAGE_SIZE-1) - (HIGH_MEMORY)); /* Reserve page 0 as it is often a special page used by the bios */ reserve_bootmem(0, PAGE_SIZE); /* reserve EBDA region, it's a 4K region */ reserve_ebda_region(); /* could be an AMD 768MPX chipset. Reserve a page before VGA to prevent PCI prefetch into it (errata #56). Usually the page is reserved anyways, unless you have no PS/2 mouse plugged in. */ if (boot_cpu_data.x86_vendor == X86_VENDOR_AMD && boot_cpu_data.x86 == 6) reserve_bootmem(0xa0000 - 4096, 4096); #ifdef CONFIG_SMP /* * But first pinch a few for the stack/trampoline stuff * FIXME: Don't need the extra page at 4K, but need to fix * trampoline before removing it. (see the GDT stuff) */ reserve_bootmem(PAGE_SIZE, PAGE_SIZE); #endif #ifdef CONFIG_ACPI_SLEEP /* * Reserve low memory region for sleep support. */ acpi_reserve_bootmem(); #endif #ifdef CONFIG_X86_FIND_SMP_CONFIG /* * Find and reserve possible boot-time SMP configuration: */ find_smp_config(); #endif ... return max_low_pfn; }
Linux 内存管理之highmemory简介
Describing Physical Memory
Page Table Management
//内核页表初始化 /* * paging_init() sets up the page tables - note that the first 8MB are * already mapped by head.S. * * This routines also unmaps the page at virtual kernel address 0, so * that we can trap those pesky NULL-reference errors in the kernel. */ void __init paging_init(void) { ... //pagetable_init() is responsible for setting up a static page table using //swapper_pg_dir as the PGD //映射内核空间前896M的页表 pagetable_init();//1 load_cr3(swapper_pg_dir);//将页目录基址载入cr3 ... /* This function only exists if CONFIG_HIGHMEM is set during compile time. It is responsible for caching where the beginning of the kmap region is, the PTE referencing it and the protection for the page tables. This means the PGD will not have to be checked every time kmap() is used. */ //initialises the region of pagetables reserved for use with kmap() //kmap_pte:FIX_KMAP_BEGIN项所对应的页表项,从上往下设置。 kmap_init();//请看下面的注释 //--------------load_cr3 and kmap_init-------------- /* #define load_cr3(pgdir) \ asm volatile("movl %0,%%cr3": :"r" (__pa(pgdir))) #define __pa(x) ((unsigned long)(x)-PAGE_OFFSET) */ /* void __init kmap_init(void) { unsigned long kmap_vstart; //cache the first kmap pte kmap_vstart = __fix_to_virt(FIX_KMAP_BEGIN); kmap_pte = kmap_get_fixmap_pte(kmap_vstart); kmap_prot = PAGE_KERNEL; } #define kmap_get_fixmap_pte(vaddr) \ pte_offset_kernel(pmd_offset(pgd_offset_k(vaddr), (vaddr)), (vaddr)) */ //This is the top-level function which is used to initialise each of the //zones. The size of the zones in PFNs was discovered during setup_memory(). zone_sizes_init();//2 } --------------如果不感兴趣,接下来的代码就不用看了-------------- //-----------------1.pagetable_init----------------- /* This function is responsible for statically inialising a pagetable starting with a statically defined PGD called swapper_pg_dir. At the very least, a PTE will be available that points to every page frame in ZONE_NORMAL. */ static void __init pagetable_init (void) { unsigned long vaddr; pgd_t *pgd_base = swapper_pg_dir; ... //内核线性空间低端内存的内核页表项的初始化,并建立与物理页的映射关系 kernel_physical_mapping_init(pgd_base);//1.1 ... //At this point, page table entries have been setup which reference all parts of //ZONE_NORMAL. The remaining regions needed are those for fixed mappings and //those needed for mapping high memory pages with kmap(). //固定映射区和永久映射区的初始化 /* * Fixed mappings, only the page table structure has to be * created - mappings will be set by set_fixmap(): */ /* 为了避免前期可能对固定映射区已经分配了页表项,基于临时内核映射区间要求页表连续性的保 证,所以在此重新申请连续的页表空间将原页表内容拷贝至此。值得注意的是,与低端内存的页表 初始化不同的是,这里的页表只是被分配,相应的PTE项并未初始化,这个工作将会交由以后各个 固定映射区部分的相关代码调用set_fixmap()来将相关的固定映射区页表与物理内存关联。 参考:http://blog.csdn.net/hanchaoman/article/details/6942140 */ vaddr = __fix_to_virt(__end_of_fixed_addresses - 1) & PMD_MASK; //该函数不会更改pgd_base page_table_range_init(vaddr, 0, pgd_base);//1.2 //内部调用一个page_table_range_init,同上,将原页表拷贝到新的一页之中,新的页表所需 //页帧用alloc_bootmem_low_pages(PAGE_SIZE)分配 permanent_kmaps_init(pgd_base);//1.3 ... } //-----------------1.1.pagetable_init----------------- /* * This maps the physical memory to kernel virtual address space, a total * of max_low_pfn pages, by creating page tables starting from address * PAGE_OFFSET. */ //将前896M的物理内存线性映射到了PAGE_OFFSET之上: //virtual address = physical address + PAGE_OFFSET static void __init kernel_physical_mapping_init(pgd_t *pgd_base) { unsigned long pfn; pgd_t *pgd; pmd_t *pmd; pte_t *pte; int pgd_idx, pmd_idx, pte_ofs; ... } //-----------------1.2.page_table_range_init----------------- /* * This function initializes a certain range of kernel virtual memory * with new bootmem page tables, everywhere page tables are missing in * the given range. */ /* * NOTE: The pagetables are allocated contiguous on the physical space * so we can cache the place of the first one and move around without * checking the pgd every time. */ static void __init page_table_range_init (unsigned long start, unsigned long end, pgd_t *pgd_base) { pgd_t *pgd; pud_t *pud; pmd_t *pmd; int pgd_idx, pmd_idx; unsigned long vaddr; vaddr = start; pgd_idx = pgd_index(vaddr); pmd_idx = pmd_index(vaddr); pgd = pgd_base + pgd_idx; for ( ; (pgd_idx < PTRS_PER_PGD) && (vaddr != end); pgd++, pgd_idx++) { if (pgd_none(*pgd)) one_md_table_init(pgd);//新分配一张页表,并将物理地址映射到pgd中 pud = pud_offset(pgd, vaddr); pmd = pmd_offset(pud, vaddr); for (; (pmd_idx < PTRS_PER_PMD) && (vaddr != end); pmd++, pmd_idx++) { if (pmd_none(*pmd)) one_page_table_init(pmd);//新分配一张页表,并将物理地址映射到pmd中 vaddr += PMD_SIZE; } pmd_idx = 0; } } //-----------------1.3.permanent_kmaps_init----------------- void __init permanent_kmaps_init(pgd_t *pgd_base) { pgd_t *pgd; pud_t *pud; pmd_t *pmd; pte_t *pte; unsigned long vaddr; vaddr = PKMAP_BASE; page_table_range_init(vaddr, vaddr + PAGE_SIZE*LAST_PKMAP, pgd_base); pgd = swapper_pg_dir + pgd_index(vaddr); pud = pud_offset(pgd, vaddr); pmd = pmd_offset(pud, vaddr); pte = pte_offset_kernel(pmd, vaddr); pkmap_page_table = pte; } //-----------------2.zone_sizes_init----------------- //先初始化内核页表,然后才初始化nodes and zones void __init zone_sizes_init(void) { unsigned long zones_size[MAX_NR_ZONES] = {0, 0, 0}; unsigned int max_dma, high, low; //DMA用的最大物理页帧号 max_dma = virt_to_phys((char *)MAX_DMA_ADDRESS) >> PAGE_SHIFT; low = max_low_pfn; high = highend_pfn; if (low < max_dma) zones_size[ZONE_DMA] = low; else { zones_size[ZONE_DMA] = max_dma; zones_size[ZONE_NORMAL] = low - max_dma; #ifdef CONFIG_HIGHMEM zones_size[ZONE_HIGHMEM] = high - low; #endif } //初始化物理页帧信息【node、zone、memmap、buddy system...】 free_area_init(zones_size); //zones_size = 3, 3个zone }
struct bootmem_data;
typedef struct pglist_data {
struct zone node_zones[MAX_NR_ZONES];
struct zonelist node_zonelists[GFP_ZONETYPES];
int nr_zones;
struct page *node_mem_map;
struct bootmem_data *bdata;
unsigned long node_start_pfn;
unsigned long node_present_pages; // total number of physical pages
unsigned long node_spanned_pages; //total size of physical page
//range, including holes
int node_id;
struct pglist_data *pgdat_next;
wait_queue_head_t kswapd_wait;
struct task_struct *kswapd;
int kswapd_max_order;
} pg_data_t;
/*
* On machines where it is needed (eg PCs) we divide physical memory
* into multiple physical zones. On a PC we have 3 zones:
*
* ZONE_DMA < 16 MB ISA DMA capable memory
* ZONE_NORMAL 16-896 MB direct mapped by the kernel
* ZONE_HIGHMEM > 896 MB only page cache and user processes
*/
struct zone {
/* Fields commonly accessed by the page allocator */
unsigned long free_pages;
unsigned long pages_min, pages_low, pages_high;
unsigned long lowmem_reserve[MAX_NR_ZONES];
struct per_cpu_pageset pageset[NR_CPUS];
/*
* free areas of different sizes
struct free_area {
struct list_head free_list;
unsigned long nr_free;
};
*/
spinlock_t lock;
struct free_area free_area[MAX_ORDER];
ZONE_PADDING(_pad1_)
/* Fields commonly accessed by the page reclaim scanner */
spinlock_t lru_lock;
struct list_head active_list;
struct list_head inactive_list;
unsigned long nr_scan_active;
unsigned long nr_scan_inactive;
unsigned long nr_active;
unsigned long nr_inactive;
unsigned long pages_scanned; /* since last reclaim */
int all_unreclaimable; /* All pages pinned */
...
/*
* Discontig memory support fields.
*/
struct pglist_data *zone_pgdat;
struct page *zone_mem_map;
/* zone_start_pfn == zone_start_paddr >> PAGE_SHIFT */
unsigned long zone_start_pfn;
unsigned long spanned_pages; /* total size, including holes */
unsigned long present_pages; /* amount of memory (excluding holes) */
/*
* rarely used fields:
*/
char *name;
} ____cacheline_maxaligned_in_smp;
//-----------------2.1.free_area_init-----------------
/*
This is the architecture independant function for setting up a UMA architecture.
It simply calls the core function passing the static contig_page_data as the
node.
*/
void __init free_area_init(unsigned long *zones_size)
{
free_area_init_core(0,
&contig_page_data,
&mem_map,
zones_size,
0,
0,
0);//2.1.1
}
//-----------------2.1.1.free_area_init_core------------------
/*
This function is responsible for initialising all zones and allocating their
local lmem_map within a node. In UMA architectures, this function is called in a
way that will initialise the global mem_map array.
*/
void __init free_area_init_core
( int nid,
pg_data_t *pgdat,
struct page **gmap,
unsigned long *zones_size,
unsigned long zone_start_paddr,
unsigned long *zholes_size,
struct page *lmem_map)
{
...
//要容纳结点[node]所有的page需要的内存大小
map_size = (totalpages + 1)*sizeof(struct page);
//如果lmem_map没有配备空间,就为它分配map_size的空间
if (lmem_map == (struct page *)0) {
lmem_map = (struct page *) alloc_bootmem_node(pgdat, map_size);
lmem_map = (struct page *)
(PAGE_OFFSET + MAP_ALIGN((unsigned long)lmem_map - PAGE_OFFSET));
}
//初始化pgdat描述的结点[node]
//Set the gmap and pgdat→node_mem_map variables to the allocated lmem_map.
//In UMA architectures, this just set mem_map
*gmap = pgdat->node_mem_map = lmem_map;
pgdat->node_size = totalpages;
pgdat->node_start_paddr = zone_start_paddr;
pgdat->node_start_mapnr = (lmem_map - mem_map);
pgdat->nr_zones = 0;
//mem_map是全局page数组
offset = lmem_map - mem_map;//若内存是UMA模型,则lmem_map = mem_map,offset=0
//接下来初始化node的各个zone
...
//Calculate the real size of the zone based on the full size in
//zones_size minus the size of the holes in zholes_size
realsize = size = zones_size[j];
if (zholes_size)
realsize -= zholes_size[j];
...
zone->size = size;
zone->free_pages = 0;
//初始化per-cpu-pages
/*
* The per-cpu-pages pools are set to around 1000th of the
* size of the zone. But no more than 1/4 of a meg - there's
* no point in going beyond the size of L2 cache.
*
* OK, so we don't know how big the cache is. So guess.
*/
batch = zone->present_pages / 1024;
if (batch * PAGE_SIZE > 256 * 1024)
batch = (256 * 1024) / PAGE_SIZE;
batch /= 4; /* We effectively *= 4 below */
if (batch < 1)
batch = 1;
for (cpu = 0; cpu < NR_CPUS; cpu++) {
struct per_cpu_pages *pcp;
pcp = &zone->pageset[cpu].pcp[0]; /* hot */
pcp->count = 0;
pcp->low = 2 * batch;
pcp->high = 6 * batch;
pcp->batch = 1 * batch;
INIT_LIST_HEAD(&pcp->list);
pcp = &zone->pageset[cpu].pcp[1]; /* cold */
pcp->count = 0;
pcp->low = 0;
pcp->high = 2 * batch;
pcp->batch = 1 * batch;
INIT_LIST_HEAD(&pcp->list);
}
INIT_LIST_HEAD(&zone->active_list);
INIT_LIST_HEAD(&zone->inactive_list);
...
//初始化mem_map中的page
785 for (i = 0; i < size; i++) {
786 struct page *page = mem_map + offset + i;
787 set_page_zone(page, nid * MAX_NR_ZONES + j);
788 set_page_count(page, 0);
789 SetPageReserved(page);
790 INIT_LIST_HEAD(&page->list);
791 if (j != ZONE_HIGHMEM)
792 set_page_address(page, __va(zone_start_paddr));
793 zone_start_paddr += PAGE_SIZE;
794 }
/*
785-794:
Initially, all pages in the zone are marked as reserved as there is no
way to know which ones are in use by the boot memory allocator. When the boot
memory allocator is retiring in free_all_bootmem(), the unused pages will have
their PG_reserved bit cleared
786:
Get the page for this offset
787:
The zone the page belongs to is encoded with the page flags.
788:
Set the count to 0 as no one is using it
789:
Set the reserved flag. Later, the boot memory allocator will clear this bit
if the page is no longer in use
790:
Initialise the list head for the page
791-792:
Set the page→virtual field if it is available and the page is in low
memory
793:
Increment zone_start_paddr by a page size as this variable will be used to
record the beginning of the next zone
*/
for (i = 0; ; i++) {
//空闲链表里什么也没有
INIT_LIST_HEAD(&zone->free_area[i].free_list);
if (i == MAX_ORDER-1) {
/*
空闲页的数目(nr_free) 当前仍然规定为O. 这显然没有反映真实情况。直至停用
bootmem分配器、普通的伙伴分配器生效,才会设置正确的数值。
*/
zone->free_area[i].nr_free = 0;
break;
}
}
}
build_zonelists(pgdat);
}
//参见:
//https://www.kernel.org/doc/gorman/html/understand/understand019.html#toc98
到这里内核就正确的初始化了GDT和对应内核空间前896M的内核页目录+页表。
Page frames in high memory that do not have a linear address cannot be accessed
by the kernel. Therefore, part of the last 128 MB of the kernel linear address
space is dedicated to mapping high-memory page frames. Of course, this kind
of mapping is temporary, otherwise only 128 MB of high memory would be accessible.
Instead, by recycling linear addresses the whole high memory can be
accessed, although at different times.
内核可以继续正确的工作下去,去完成进一步的初始化工作。
GDT和内核页目录+页表所在的物理页都被映射到了内核虚拟空间中,所以,内核可以访问相应的表项,更改或设置其中的映射关系【对内核虚拟空间的动态映射部分进行改动】,这就赋予内核自己分配和映射物理页的能力。也就是说,内核现在有能力去管理自己的地址空间,它可以”自己成长啦“。所以”鸡生蛋的过程到这里基本快完成了“。
仔细观察下面的代码。
/* * This maps the physical memory to kernel virtual address space, a total * of max_low_pfn pages, by creating page tables starting from address * PAGE_OFFSET. */ static void __init kernel_physical_mapping_init(pgd_t *pgd_base) { unsigned long pfn; pgd_t *pgd; pmd_t *pmd; pte_t *pte; int pgd_idx, pmd_idx, pte_ofs; pgd_idx = pgd_index(PAGE_OFFSET); pgd = pgd_base + pgd_idx; pfn = 0; for (; pgd_idx < PTRS_PER_PGD; pgd++, pgd_idx++) { pmd = one_md_table_init(pgd); if (pfn >= max_low_pfn) continue; for (pmd_idx = 0; pmd_idx < PTRS_PER_PMD && pfn < max_low_pfn; pmd++, pmd_idx++) { unsigned int address = pfn * PAGE_SIZE + PAGE_OFFSET; /* Map with big pages if possible, otherwise create normal page tables. */ if (cpu_has_pse) { unsigned int address2 = (pfn + PTRS_PER_PTE - 1) * PAGE_SIZE + PAGE_OFFSET + PAGE_SIZE-1; if (is_kernel_text(address) || is_kernel_text(address2)) set_pmd(pmd, pfn_pmd(pfn, PAGE_KERNEL_LARGE_EXEC)); else set_pmd(pmd, pfn_pmd(pfn, PAGE_KERNEL_LARGE)); pfn += PTRS_PER_PTE; } else { pte = one_page_table_init(pmd); for (pte_ofs = 0; pte_ofs < PTRS_PER_PTE && pfn < max_low_pfn; pte++, pfn++, pte_ofs++) { if (is_kernel_text(address)) set_pte(pte, pfn_pte(pfn, PAGE_KERNEL_EXEC)); else set_pte(pte, pfn_pte(pfn, PAGE_KERNEL)); } } } } }
注意:
kernel_physical_mapping_init
将内核虚拟地址的前896M空间全部线性映射到了物理内存的前896M空间,但是,这个函数只设置了映射关系,并没有执行任何分配页的函数【例如alloc_page()、__get_free_pages()】,也就是说,内核事先给出了映射,但是并没有占用这些被映射过的物理页,这些物理页仍然是空闲页,属于空闲页链表。只有在内核显式地申请某个页的时候,它才会占用这个页,将其从空闲链表中摘除。所以,除了内核保留页【包括物理内存中属于前896M空间的一个8M大小的连续物理内存区域】不参与以后的动态内存分配外,其它的页可都是被标记成了空闲页,将来都会参与到动态内存分配中去。
记住,被内核映射了不等于被内核使用了。关于内核内存管理的一些思考
内核虚拟地址空间布局:
下面的讨论中有涉及到诸如kmalloc、kfree、vmalloc、vfree之类的内存分配与释放函数[API],值得注意的是,他们都是基于buddy system的内存分配函数。其中对实际物理内存页的分配与释放都由buddy system接管,所以必须在内存子系统之buddy system初始化完成之后方可使用这些API。
内核线性地址空间大小为1G,分为低端内存区和高端内存区:
内核线性地址空间前896M[0xC0000000:PAGE-OFFSET~ 0xF8000000:high_memory]属于。地址映射关系满足:
virt addr = linear addr = phy addr + 0xC0000000。
被[直接映射区]的映射的物理页帧可以通过
alloc_pages()
函数获取连续的物理页帧,也可以通过kmalloc以字节为单位获取连续的物理内存空间,获取之后即可使用,不用再去设置映射关系了,因为内核初始化时已经替我们完成了页表项的映射工作,kmalloc的另一端就是kfree,kfree函数释放由kmalloc分配了的内存块。根据定义,在[物理高端内存]中的页不能永久地映射到[内核线性地址空间]上。在X86体系结构上,高于896M的所有物理内存的范围都是物理高端内存,它并不会永久地或自动地映射到内核线性地址空间,一旦这些高端物理页帧被分配,就必须在页表中将其映射到内核的线性地址空间中,由于内核线性地址空间的低端内存区【内核线性内存空间前896M的直接映射区】的页表已经在初始化时建立好了与物理低端内存区【物理内存空间前896M】之间的线性映射关系,不可改变。所以,为了将物理高端内存区【物理内存空间:896M~4G】映射到内存线性空间中,内核线性地址空间保留了一个线性地址区间,大小为128M,名为内核线性地址空间的[高端内存区]。
内核分配不连续页 - vmalloc
非连续物理内存分配的线性地址空间从VMALLOC_START到VMALLOC_END。
当需要分配虚拟地址连续但物理地址不连续的页时【非连续物理页分配】,可以映射到[内核线性空间高端内存.vmalloc区]。vmalloc函数就用于此目的。vmalloc函数通过分配非连续的物理内存块,再“设置”页表映射,把内存映射到线性地址空间的连续区域中。
相比kmalloc函数,vmalloc函数更慢,他为了将物理页帧映射到vmalloc区中,需要亲自去建立并设置一系列的页表表项,而且物理上不连续的页还容易导致TLB抖动。vmalloc区一般用来容纳被动态地装载到内核地址空间中的模块。
vfree用来释放vmalloc分配的内存。
/**
* vmalloc - allocate virtually contiguous memory
*
* @size: allocation size
*
* Allocate enough pages to cover @size from the page level
* allocator and map them into contiguous kernel virtual space.
*
* For tight control over page level allocator and protection flags
* use __vmalloc() instead.
*/
void *vmalloc(unsigned long size)
{
return __vmalloc(size, GFP_KERNEL | __GFP_HIGHMEM, PAGE_KERNEL);
}
/**
* __vmalloc - allocate virtually contiguous memory
*
* @size: allocation size
* @gfp_mask: flags for the page level allocator
* @prot: protection mask for the allocated pages
*
* Allocate enough pages to cover @size from the page level
* allocator with @gfp_mask flags. Map them into contiguous
* kernel virtual space, using a pagetable protection of @prot.
*/
void *__vmalloc(unsigned long size, int gfp_mask, pgprot_t prot)
{
struct vm_struct *area;
/*
struct vm_struct {
void *addr; //指向第一个内存单元(线性地址)
unsigned long size; //该块内存区的大小
unsigned long flags;
struct page **pages; //指向页描述符指针数组
unsigned int nr_pages; //内存区对应的页框数
unsigned long phys_addr; //用来映射硬件设备的IO共享内存,其他情况下为0
struct vm_struct *next;
};
*/
struct page **pages;
unsigned int nr_pages, array_size, i;
size = PAGE_ALIGN(size);
if (!size || (size >> PAGE_SHIFT) > num_physpages)
return NULL;
area = get_vm_area(size, VM_ALLOC);
if (!area)
return NULL;
nr_pages = size >> PAGE_SHIFT;//需呀分配的页数
array_size = (nr_pages * sizeof(struct page *));
area->nr_pages = nr_pages;
//分配一块线性内存区,用来容纳指向分配的page[s]的指针*page[s]
/* Please note that the recursion is strictly bounded. */
if (array_size > PAGE_SIZE)
pages = __vmalloc(array_size, gfp_mask, PAGE_KERNEL);
else
pages = kmalloc(array_size, (gfp_mask & ~__GFP_HIGHMEM));
area->pages = pages;
if (!area->pages) {
remove_vm_area(area->addr);
kfree(area);
return NULL;
}
memset(area->pages, 0, array_size);//将分配的用来容纳*page[s]的内存区域清零
for (i = 0; i < area->nr_pages; i++) {
//分配page,将返回的*page赋值到area->pages数组中
area->pages[i] = alloc_page(gfp_mask);
if (unlikely(!area->pages[i])) {//分配了i页,但分配第i+1页时失败了
/* Successfully allocated i pages, free them in __vunmap() */
area->nr_pages = i;
goto fail;
}
}
/*
执行到这里,用来描述请求连续线性内存域的vm_struct area已完成初始化,接下来,需要将area
传递给函数map_vm_area,将其中的pages映射到area描述的属于内核vmalloc区的一块连续线性
空间中---much work to do!
*/
if (map_vm_area(area, prot, &pages))
goto fail;
//执行到这里表示内存分配与页表映射设置均成功完成,返回这块空间起始页的线性地址
return area->addr;
fail:
vfree(area->addr);
return NULL;
}
/*
设置页表映射关系,将pages映射到area
*/
int map_vm_area(struct vm_struct *area, pgprot_t prot, struct page ***pages)
{
unsigned long address = (unsigned long) area->addr;//起始线性地址
unsigned long end = address + (area->size-PAGE_SIZE);//结束线性地址
unsigned long next;
pgd_t *pgd;
int err = 0;
int i;
//一个目录项一个目录项地去完成映射
pgd = pgd_offset_k(address);
spin_lock(&init_mm.page_table_lock);
for (i = pgd_index(address); i <= pgd_index(end-1); i++) {
pud_t *pud = pud_alloc(&init_mm, pgd, address);
if (!pud) {
err = -ENOMEM;
break;
}
next = (address + PGDIR_SIZE) & PGDIR_MASK;
if (next < address || next > end)
next = end;
//以一个目录的映射量来完成映射
if (map_area_pud(pud, address, next, prot, pages)) {
err = -ENOMEM;
break;
}
address = next;
pgd++;
}
spin_unlock(&init_mm.page_table_lock);
flush_cache_vmap((unsigned long) area->addr, end);
return err;
}
static int map_area_pud(pud_t *pud, unsigned long address,
unsigned long end, pgprot_t prot,
struct page ***pages)
{
do {
//分配一个pmd表,在pud表中设置一项映射到pmd表
pmd_t *pmd = pmd_alloc(&init_mm, pud, address);
if (!pmd)
return -ENOMEM;
//以中间页目录为单位,一个页目录一个页目录的去完成pages到[address, end]的映射
if (map_area_pmd(pmd, address, end - address, prot, pages))
return -ENOMEM;
address = (address + PUD_SIZE) & PUD_MASK;
pud++;
} while (address && address < end);
return 0;
}
static int map_area_pmd(pmd_t *pmd, unsigned long address,
unsigned long size, pgprot_t prot,
struct page ***pages)
{
unsigned long base, end;
base = address & PUD_MASK;
address &= ~PUD_MASK;
end = address + size;
if (end > PUD_SIZE)
end = PUD_SIZE;
do {
//分配页表pte,在pmd表中设置到pte的一项映射
pte_t * pte = pte_alloc_kernel(&init_mm, pmd, base + address);
if (!pte)
return -ENOMEM;
//一张页表一张页表地去建立映射
if (map_area_pte(pte, address, end - address, prot, pages))
return -ENOMEM;
address = (address + PMD_SIZE) & PMD_MASK;
pmd++;
} while (address < end);
return 0;
}
static int map_area_pte(pte_t *pte, unsigned long address,
unsigned long size, pgprot_t prot,
struct page ***pages)
{
unsigned long end;
address &= ~PMD_MASK;
end = address + size;
if (end > PMD_SIZE)
end = PMD_SIZE;
do {
struct page *page = **pages;
WARN_ON(!pte_none(*pte));
if (!page)
return -ENOMEM;
set_pte(pte, mk_pte(page, prot));//设置页表映射
address += PAGE_SIZE;
pte++;
(*pages)++;
} while (address < end);
return 0;
}
内核映射 - 持久内核映射
内核页表的顶部保留了一张页表【pkmap_page_table 】用来映射PKMAP_BASE到FIXADDR_START的PKMap区。这张页表名为
pkmap_page_table
在内核[permanent_kmaps_init]初始化期间完成设置【建立了页表,没有建立映射,用kmap可以分配物理高端内存区的页帧,并在页表中建立永久映射】。The current state of the page table entries is managed by a simple array called
pkmap_count
which hasLAST_PKMAP
entries in it.The
kmap_high()
function begins with checking the page→virtual field which is set if the page is already mapped. If it is NULL,map_new_virtual()
provides a mapping for the page.Creating a new virtual mapping with
map_new_virtual()
is a simple case of linearly scanningpkmap_count
. Once a mapping has been created, the corresponding entry in thepkmap_count
array is incremented and the virtual address returned.持久映射区,又被称作永久映射区,whatever~。在哪里体现永久的呢?在页表项中:
void *kmap(struct *page)
用来从物理内存空间中【高端内存或低端内存都可以】获取一页物理页帧。这个函数可以睡眠,因此kmap只能用在进程上下文中。【明白进程的概念很重要,实质上来说就是用一段内存区[task_struct]实现记录一个程序的现场,使得PC可以在不同的程序上下文之间切换,因为PC在切换之时会将程序执行的现场记录下来,存到代表它的进程中去,这样,PC就可以在下次切换到它时,接着之前的现场去执行。要是一个函数可以睡眠,却没有进程来记录现场,一旦切换到其他地方,那么PC就再也无法切换回来了,那么这个函数就无法接着完成之前未完成的任务】
- 物理空间低端内存区:
如果传递到kmap的page结构体描述的是低端内存中的一页,函数只会单纯地返回该页的虚拟地址【描述物理低端内存的page结构体中有虚拟地址成员virtual,直接返回它就行】,显然这页物理页帧已经被映射到了内核线性空间的低端内存区,而不是PKMap区。
- 物理空间高端内存区:
如果传递到kmap的page结构体描述的是高端内存中的一页,函数会在pkmap_page_table指向的页表中建立[
map_new_virtual
]一个永久映射,再返回位于PKMap区的虚拟地址【当传递这样的page到kmap时,直接返回page->virtual】。内核通过这种方式将物理高端内存页帧映射到PKMap区。
void kunmap(struct *page)
该函数用来解除对给定页的映射。pkmap_page_table页表中的表项都是永久映射,在解除映射之前是不可以更改映射关系的。
void *kmap(struct page *page)
{
might_sleep();
//如果页帧来自物理低端内存区则直接返回page的线性地址,自然,这页page不会被映射到
//PKMap区,因为更本不会在pkmap_page_table指向的页表中设置对应该物理帧的永久映射
if (!PageHighMem(page))
return page_address(page); //Routine 1
return kmap_high(page);//Routine 2
}
void *page_address(struct page *page)
{
unsigned long flags;
void *ret;
struct page_address_slot *pas; //参见:1.3.1. mm/highmem.c::page_address_init
if (!PageHighMem(page))
return lowmem_page_address(page); //低端内存,不用建立映射,返回线性地址,
//直接使用。所以说物理低端内存页帧不会映射到内核线性地址空间的PKMap区
/*
static inline void *lowmem_page_address(struct page *page)
{
return __va(page_to_pfn(page) << PAGE_SHIFT);//得到物理低端内存页帧的内核线性地
//址
}
#define page_to_pfn(page) ((page) - mem_map) //得到页帧号
*/
//返回一个hash槽的地址,page_address_map通过链表开散列的方式链入槽中,不同的散列值
//链入不同的槽,相同的散列值链入同一个槽
pas = page_slot(page);//1
ret = NULL;
spin_lock_irqsave(&pas->lock, flags);
//在槽中查找开散列之后的页,看该页是否已经散列到了其中
if (!list_empty(&pas->lh)) {
struct page_address_map *pam;
list_for_each_entry(pam, &pas->lh, list) {
if (pam->page == page) {
ret = pam->virtual;//之前已经建立好映射的高端内存,直接可以使用
goto done;
}
}
}
//如果既不是低端内存也不是已映射的高端内存,那就返回NULL,需要上级函数自己建立好映
//射,否则不能使用【要想使用一页物理页帧,得先将其映射到线性地址空间中,在页表中建立相
//应的页表项,建立映射关系】。
done:
spin_unlock_irqrestore(&pas->lock, flags);
return ret;
}
-------------------------1.page_slot-------------------------
static struct page_address_slot *page_slot(struct page *page)
{
return &page_address_htable[hash_ptr(page, PA_HASH_ORDER)];
}
kmap在调用kmap_high之前的完成的工作只是判断传递过来的请求映射到PKMap区的页是否属于物理低端内存。如果是,则直接返回其在低端内存中的线性地址,该线性地址在初始化内核页表时就已经映射到了这个物理页帧,而现在要用到这个页帧,那就直接拿来用吧,没问题。可是,如果传递给kmap的page属于高端内存呢?下面就来看看遇到这种情况怎么办:
void fastcall *kmap_high(struct page *page) { unsigned long vaddr; /* * For highmem pages, we can't trust "virtual" until * after we have the lock. * * We cannot call this from interrupts, as it may block */ spin_lock(&kmap_lock);//进入临界区之前,先要获得锁 /* 前面有对page_address函数给出分析。 现在的情况是这样: 传递给page_address的page属于物理高端内存页帧,而不是物理低端内存页帧。 所以,page_address会检查page是否已经被映射到线性地址空间中,如果page是已经被映射到 PKMap区的物理高端页帧,那就用不着麻烦再去建立新的映射了,直接使用它就是了。 如若page还没有被映射到线性地址空间,也就是说在page_address_htable中不存在对应该 page的page_address_map,那么返回值会是NULL,在可以使用它之前需要用map_new_virtual 函数将该页帧映射到内核线性地址空间的PKMap区。 */ vaddr = (unsigned long)page_address(page); if (!vaddr) vaddr = map_new_virtual(page); //Routine 1.1 pkmap_count[PKMAP_NR(vaddr)]++;//增加计数,表示PKMap区的这个线性地址【页基址】已 //被占用。pkmap_count数组用来记录PKMap区的线性地址的分配情况---线性地址在某种意义上也 //是内存资源【虚拟的】,需要进行资源管理。 if (pkmap_count[PKMAP_NR(vaddr)] < 2) BUG(); spin_unlock(&kmap_lock);//退出临界区,释放锁 return (void*) vaddr;//返回线性地址 }
之前分析vmalloc时提到一个函数map_vm_area,这个函数专门用来从vmalloc区中分配连续的线性地址空间,它不要求物理页帧之间连续,也没有对物理页帧的来源【是来低端内存区还是高端内存区】做出任何约束。
现在,我们分析专门将物理高端内存映射到PKMap区的函数:
/* 专门用来将物理高端内存页映射到内核线性地址空间的PKMap区 */ static inline unsigned long map_new_virtual(struct page *page) { unsigned long vaddr; int count; start: count = LAST_PKMAP;//PKMap区共1024页的线性地址空间 /* Find an empty entry */ for (;;) { last_pkmap_nr = (last_pkmap_nr + 1) & LAST_PKMAP_MASK; if (!last_pkmap_nr) { flush_all_zero_pkmaps(); count = LAST_PKMAP; } if (!pkmap_count[last_pkmap_nr]) break; /* Found a usable entry */ if (--count) continue; //找遍了1024项都没找到可用的entry,也就是PKMap区的虚拟页都建立了到物理页之间的映 //射,没有可用的虚拟页资源了,需要等带其他进程释放属于这个区的虚拟页。 /* * Sleep for somebody else to unmap their entries */ { DECLARE_WAITQUEUE(wait, current); __set_current_state(TASK_UNINTERRUPTIBLE); add_wait_queue(&pkmap_map_wait, &wait); spin_unlock(&kmap_lock); schedule();//调度 remove_wait_queue(&pkmap_map_wait, &wait); spin_lock(&kmap_lock); //如果在进程睡眠期间其他进程已经将传递过来的page映射到了线性空间,那就直接返回 //它的线性地址。 /* Somebody else might have mapped it while we slept */ if (page_address(page)) return (unsigned long)page_address(page); //否则回到开始,从新之前的查找过程 /* Re-start */ goto start; } } //能跳出循环并走到这里,表示page还没有被映射到线性地址空间,而且在PKMap区中找到了可以 //进行映射的虚拟页,接下来就是实打实的要将page映射到PKMap区了 vaddr = PKMAP_ADDR(last_pkmap_nr); //在页表pkmap_page_table中建立页表项映射,从这里可以看出,这个函数专门用来为PKMap区设 //置映射 set_pte(&(pkmap_page_table[last_pkmap_nr]), mk_pte(page, kmap_prot)); pkmap_count[last_pkmap_nr] = 1; set_page_address(page, (void *)vaddr);//Routine 1.1.1 return vaddr; }
/*
和page_address_init一样,这个函数也只在高端内存存在时才有意义
*/
void set_page_address(struct page *page, void *virtual)
{
unsigned long flags;
struct page_address_slot *pas;
struct page_address_map *pam;
BUG_ON(!PageHighMem(page));
pas = page_slot(page);//通过散列,找到page被散列到的page_address_htable中的槽
/*
两种情况
1.添加映射关系:从page_address_pool【page_address_map pool, or you can call
it page_address_map buffer. if it is not empty, we don't need bother to
create one】中摘除一个page_address_map,设置映射关系,然后将其挂在
page_address_htable的一个散列槽中。
2.取消映射关系:将page对应的page_address_map从page_address_htable中摘除,然后将
其还辉page_address_pool链表中。
*/
if (virtual) { /* Add */
BUG_ON(list_empty(&page_address_pool));
spin_lock_irqsave(&pool_lock, flags);
pam = list_entry(page_address_pool.next,
struct page_address_map, list);
list_del(&pam->list);
spin_unlock_irqrestore(&pool_lock, flags);
pam->page = page;
pam->virtual = virtual;
spin_lock_irqsave(&pas->lock, flags);
list_add_tail(&pam->list, &pas->lh);
spin_unlock_irqrestore(&pas->lock, flags);
} else { /* Remove */
spin_lock_irqsave(&pas->lock, flags);
list_for_each_entry(pam, &pas->lh, list) {
if (pam->page == page) {
list_del(&pam->list);
spin_unlock_irqrestore(&pas->lock, flags);
spin_lock_irqsave(&pool_lock, flags);
list_add_tail(&pam->list, &page_address_pool);
spin_unlock_irqrestore(&pool_lock, flags);
goto done;
}
}
spin_unlock_irqrestore(&pas->lock, flags);
}
done:
return;
}
临时内核映射
kmap_atomic的细节以及改进固定映射区的特点在于: 原子性、页表项可覆盖性、每CPU在Fixmaps线性空间的不重合性。
kmap_pte
指向固定映射区的页表最后一个表项,与其他页表中表项的增长方向不同,固定映射区的页表的增长方向是从上往下式的增长。There are KM_TYPE_NR entries per processor are reserved at boot time for atomic mapping at the location FIX_KMAP_BEGIN and ending at FIX_KMAP_END. Obviously a user of an atomic kmap may not sleep or exit before calling kunmap_atomic() as the next process on the processor may try to use the same entry and fail.
The advantage of fixmap addresses is that at compilation time, the address acts
like a constant whose physical address is assigned when the kernel is booted. Addresses
of this kind can be de-referenced faster than when normal pointers are used.
The kernel also ensures that the page table entries of fixmaps are not flushed from
the TLB during a context switch so that access is always made via fast cache memory.
[Mauerer, Professional Linux Kernel Architecture, Sec. 3.4.2, Architecture-Specific
Setup, p180]
void kmap_atomic(struct page*page, enum km_type type)
用来建立临时映射。这个函数不会阻塞,因此可以用在中断上下文和其他不能重新调度的地方。它还能禁止内核抢占,每一个CPU在内核线性空间的固定映射区都有一段互不重叠的线性内存窗口用作自己的临时映射区间。该函数与kmap执行的逻辑基本相同,主要不同点在于,当page描述的是高端物理页帧时,他会将属于当前cpu的临时映射区的某个页表项【从km_type type计算得到】的映射设置为映射到这个物理页帧,而不管这个页表项之前是否已有映射。
void kunmap_atomic(struct page*page, enum km_type type)
用来解除相应的映射。这个函数也不会阻塞。事实上,除了激活内核抢占,否则kmap_atomic()
根本无事可做。因为下一次的临时映射到来时,同样通过kmap_atomic的原子映射直接将当前的映射覆盖掉,当前的临时映射的生命周期到此也就终结了,这个新建立的下一个的临时映射的生命随之开始。
/*
* kmap_atomic/kunmap_atomic is significantly faster than kmap/kunmap because
* no global lock is needed and because the kmap code must perform a global TLB
* invalidation when the kmap pool wraps.
*
* However when holding an atomic kmap is is not legal to sleep, so atomic
* kmaps are appropriate for short, tight code paths only.
*/
void *kmap_atomic(struct page *page, enum km_type type)
{
enum fixed_addresses idx;
unsigned long vaddr;
/* even !CONFIG_PREEMPT needs this, for in_atomic in do_page_fault */
inc_preempt_count();
if (!PageHighMem(page))
return page_address(page);//物理低端内存,直接返回其映射的线性地址
/*
Fixmaps区的线性地址空间被各个cpu均分了,而单个cpu内部又将自己所得的这片线性地址空间分为
多个页,每个页对应一种类型的用途,type是一个枚举变量,就是用来表示不同用途的虚拟页帧。
*/
/*
在Fixmaps区中得到属于当前cpu的线性地址空间中类型为type的虚拟页帧,用idx表示相对kmap_pte
的位移量。
*/
idx = type + KM_TYPE_NR*smp_processor_id();
//计算idx表示的虚拟页帧的线性地址
vaddr = __fix_to_virt(FIX_KMAP_BEGIN + idx);
#ifdef CONFIG_DEBUG_HIGHMEM
if (!pte_none(*(kmap_pte-idx)))
BUG();
#endif
/*
这里有一个特点:无论是page代表的物理页帧之前已经建立过到线性地址空间的映射,还是页表
项kmap_pte-idx存在到某个物理页的映射。kmap_atomic统统无视,一意孤行地将一页物理高端内
存页page映射到Fixmaps区中。
所以,在页表的其他某个地方还可能存在映射到page高端物理页帧的页表项。也就是说,一页从空闲页
链表中获得的物理高端内存页可以同时被映射到:vmalloc区、PKMap区和Fixmaps区。
*/
set_pte(kmap_pte-idx, mk_pte(page, kmap_prot));
__flush_tlb_one(vaddr);
return (void*) vaddr;
}
start_kernel之sched_init()
/* 初始化每一个CPU上的调度器: 可运行队列,优先级级队列、活动进程队列、过期进程队列、优先级位图... 初始化内核第一个进程idle */ void __init sched_init(void) { runqueue_t *rq; int i, j, k; for (i = 0; i < NR_CPUS; i++) { prio_array_t *array; rq = cpu_rq(i); spin_lock_init(&rq->lock); rq->active = rq->arrays; rq->expired = rq->arrays + 1; rq->best_expired_prio = MAX_PRIO; #ifdef CONFIG_SMP rq->sd = &sched_domain_dummy; rq->cpu_load = 0; rq->active_balance = 0; rq->push_cpu = 0; rq->migration_thread = NULL; INIT_LIST_HEAD(&rq->migration_queue); #endif atomic_set(&rq->nr_iowait, 0); 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); } // delimiter for bitsearch __set_bit(MAX_PRIO, array->bitmap); } } /* * The boot idle thread does lazy MMU switching as well: */ atomic_inc(&init_mm.mm_count); enter_lazy_tlb(&init_mm, current); init_idle(current, smp_processor_id()); }
在完成一系列初始化之后,内核页表对内核线性空间前896M的映射关系已经建立,调度器已完成初始化。内核开始从无到有的构建第一个进程【pid = 0,名为内核空闲进程(idle或init_task,这个进程十分特殊,在它之前,内核中没有任何进程,甚至没有进程这个概念,一般地,一个进程都有一个父进程,这个父进程就是创建它的那个进程,但是idle没有父进程,idle也是唯一一个没有父进程的进程,即idle是唯一一个不是通过do_fork而是由内核静态创建的进程。Linux进程基础】。这个进程优先级最低,只在当就绪队列中为没有其他进程时,它才能被调度。idle内核进程的工作是不断地查询,看是否存在需要调度的进程,如果有,马上调用调度器。
mm/highmem.c::sched_init的最后一步调用了函数init_idle:
/* * Make us the idle thread. Technically, schedule() should not be * called from this thread, however somewhere below it might be, * but because we are the idle thread, we just pick up running again * when this runqueue becomes "idle". */ init_idle(current, smp_processor_id());
关于current宏可以参考进程—Linux进程描述符初印象
current是一个宏变量,在系统经预编译器处理[预编译]后转变为指向当前进程task_struct的一个指针,从系统初始化一路走来,我们似乎并没有看到内核动态的创建任何的task_struct啊?哪么,这个current宏变量取得的task_struct到底是谁?这就要追溯到系统编译之时啦!
初始化0号进程
... #define INIT_MM(name) \ { \ .mm_rb = RB_ROOT, \ //idle使用内核页表:内核页目录swapper_pg_dir .pgd = swapper_pg_dir, \ .mm_users = ATOMIC_INIT(2), \ .mm_count = ATOMIC_INIT(1), \ .mmap_sem = __RWSEM_INITIALIZER(name.mmap_sem), \ .page_table_lock = SPIN_LOCK_UNLOCKED, \ .mmlist = LIST_HEAD_INIT(name.mmlist), \ .cpu_vm_mask = CPU_MASK_ALL, \ .default_kioctx = INIT_KIOCTX(name.default_kioctx, name), \ } /* * INIT_TASK is used to set up the first task table, touch at * your own risk!. Base=0, limit=0x1fffff (=2MB) */ #define INIT_TASK(tsk) \ { \ .state = 0, \ //编译时初始化 .thread_info = &init_thread_info, \ .usage = ATOMIC_INIT(2), \ .flags = 0, \ .lock_depth = -1, \ .prio = MAX_PRIO-20, \ .static_prio = MAX_PRIO-20, \ .policy = SCHED_NORMAL, \ .cpus_allowed = CPU_MASK_ALL, \ /* 内核进程不需要,mm用来管理用户进程的虚拟内存。 从此可知,idle进程的mm域为NULL。 */ .mm = NULL, \ //内存进程的`mm`,INIT_TASK静态的初始化为init_mm .active_mm = &init_mm, \ .run_list = LIST_HEAD_INIT(tsk.run_list), \ .time_slice = HZ, \ .tasks = LIST_HEAD_INIT(tsk.tasks), \ .ptrace_children= LIST_HEAD_INIT(tsk.ptrace_children), \ .ptrace_list = LIST_HEAD_INIT(tsk.ptrace_list), \ //idle进程的父进程是自己 .real_parent = &tsk, \ .parent = &tsk, \ .children = LIST_HEAD_INIT(tsk.children), \ .sibling = LIST_HEAD_INIT(tsk.sibling), \ .group_leader = &tsk, \ .real_timer = { \ .function = it_real_fn \ }, \ .group_info = &init_groups, \ .cap_effective = CAP_INIT_EFF_SET, \ .cap_inheritable = CAP_INIT_INH_SET, \ .cap_permitted = CAP_FULL_SET, \ .keep_capabilities = 0, \ .user = INIT_USER, \ .comm = "swapper", \ .thread = INIT_THREAD, \ //进程的当前工作目录之类的信息都记录在这儿 .fs = &init_fs, \ //与文件系统的接口 .files = &init_files, \ .signal = &init_signals, \ //信号处理 .sighand = &init_sighand, \ .pending = { \ .list = LIST_HEAD_INIT(tsk.pending.list), \ .signal = {{0}}}, \ .blocked = {{0}}, \ .alloc_lock = SPIN_LOCK_UNLOCKED, \ .proc_lock = SPIN_LOCK_UNLOCKED, \ .switch_lock = SPIN_LOCK_UNLOCKED, \ .journal_info = NULL, \ } /* INIT_THREAD_INFO宏定义在include\asm-i386\thread_info.h中,为了方便查看,把它放到这里 */ #define INIT_THREAD_INFO(tsk) \ { \ .task = &tsk, \ .exec_domain = &default_exec_domain, \ .flags = 0, \ .cpu = 0, \ .preempt_count = 1, \ .addr_limit = KERNEL_DS, \ .restart_block = { \ .fn = do_no_restart_syscall, \ }, \ }
... //创建并初始化idle的进程的内存管理结构init_mm struct mm_struct init_mm = INIT_MM(init_mm); //导出符号 EXPORT_SYMBOL(init_mm); /* * Initial thread structure. * * We need to make sure that this is THREAD_SIZE aligned due to the * way process stacks are handled. This is done by having a special * "init_task" linker map entry.. */ //创建并初始化一个init_thread_union结构体到内核栈中[stack_start在head.s中设立] //参考[__attribute__编译属性---section](http://www.jianshu.com/p/cefcc59e7155) union thread_union init_thread_union __attribute__((__section__(".data.init_task"))) = { INIT_THREAD_INFO(init_task) }; /* * Initial task structure. * * All other task structs will be allocated on slabs in fork.c */ struct task_struct init_task = INIT_TASK(init_task); EXPORT_SYMBOL(init_task);
从上面可以看到,在编译内核时就静态地初始化了idle的task_struct[init_task]实例,并且初始化了一个thread_union的实例init_thread_union,该结构体被加载器加载到了内核栈中。这样,可以说我们的内核从设置好内核栈之后,一直运行在内核进程idle维持的上下文中。在内核页表设置好之后,idle使用的是内核页表,由变量
swapper_pg_dir
指向内核页表的页目录。
void __devinit init_idle(task_t *idle, int cpu) { //取得当前cpu的可运行队列【当然,rq中现在一个进程也没有】 runqueue_t *rq = cpu_rq(cpu); unsigned long flags; //初始化idle进程 idle->sleep_avg = 0; idle->array = NULL; idle->prio = MAX_PRIO; idle->state = TASK_RUNNING; set_task_cpu(idle, cpu);//idle->thread_info->cpu = cpu; spin_lock_irqsave(&rq->lock, flags); /* 可以看到,idle进程并未加入到cpu的进程优先级队列中,而是赋给了一个专门用于idle进程的成 员:rq->idle */ rq->curr = rq->idle = idle; /* 设置idle的thread_info->flags = TIF_NEED_RESCHED,表示idle需要调度【注意不是被调度, 而是调度,因为idle默认已经在执行,在相关完成初始化之后,它要立马将控制权交给内核线程 init,即调度init】 */ set_tsk_need_resched(idle); spin_unlock_irqrestore(&rq->lock, flags); /* Set the preempt count _outside_ the spinlocks! */ #if defined(CONFIG_PREEMPT) && !defined(CONFIG_PREEMPT_BKL) idle->thread_info->preempt_count = (idle->lock_depth >= 0); #else idle->thread_info->preempt_count = 0; #endif }
init_idle执行完成之后,idle进程的雏形已经形成,idle跳到start_kernel函数继续去执行内核后续的初始化任务。
csdn.伙伴系统
buddy system用来管理物理内存的分配与释放,它结合了优秀内存分配器的两个关键特征:速度和效率。管理工作较少,是伙伴系统的一个主要优点。基于伙伴系统的内存管理专注于某个结点的某个内存域,例如,DMA或高端内存域。但所有内存域和结点的伙伴系统都通过备用分配列表连接起来。
以下内容摘自《深入理解Linux内核架构》
伙伴系统将在内存中分配 2oder 页,内核中细粒度的分配只能借助于slab分配器(或者slub 、slob分配器), 后者基于伙伴系统。
free_page (struct page)
和free_pages(struct page * , order)
用于将一个或 2oder 页返回给内存管理子系统。内存区的起始地址由指向该内存区的第一个page实例的指针表示。
__free_page(addr)
和__free_pages(addr,orde)
的语义类似于前两个函数,但在表示要释放的内存区时, 使用了虚拟内存地址而不是page指针。
__free_pages
is the base function used to implement all functions of the kernel API.
__free_pages
首先判断所需释放的内存是单页还是较大的内存块?如果释放单页, 则不还给伙
伴系统,而是置于per-CPU缓存中,对很可能出现在CPU高速缓存的页,则放置到热页的列表中。出于该目的, 内核提供了free_hot_page
辅助函数,该函数只是作一下参数转换,接下来调用free_hot_cold_page
。如果
free_hot_cold_page
判断per-CPU 缓存中页的数目超出了pcp->count
, 则将数量为
pcp- >batch
的一批内存页还给伙伴系统。该策略称之为惰性合并(lazy coalescing) 。如果单页直接返回给伙伴系统,那么会发生合并,而为了满足后来的分配请求又需要进行拆分。因而惰性合并策略阻止了大量可能白费时间的合并操作。free_page_bulk
用于将页还给伙伴系统。
如果不超出惰性合并的限制,则页只是保存在per-CPU缓存中。但重要的是将private成员设置
为页的迁移类型。这使得可以从per-CPU缓存分配单页,并选择正确的迁移类型。
如果释放多个页,那么__free_pages
将工作委托给__free_pages_ok
(经过一点迂回,我们对
此不太感兴趣) ,最后到__free_one_page
。与其名称不同,该函数不仅处理单页的释放,也处理复合页的释放。
static inline void __free_one_page (struct page *page, struct zone *zone, unsigned int order)
该函数是内存释放功能的基础。相关的内存区被添加到伙伴系统中适当的
free_area
列表。在释
放伙伴对时,该函数将其合并为一个连续内存区,放置到高一阶的free_area
列表中。如果还能合并一个进一步的伙伴对,那么也进行合并, 转移到更高阶的列表。该过程会一直重复下去,直至所有可能的伙伴对都已经合并,并将改变尽可能向上传播。
LINUX内存初始化
伙伴系统分配器 - __free_pages
之前在物理内存描述的初始化过程中,内核已经将各个zone进行了初始化,不过,前面有提到,mem_map中的所以页被初始化为了保留页,而且free_area的free_list也只是初始化为了一个空闲链表,根本就没有任何页挂在上面。而现在就要正式对物理内存管理子系统[buddy system]进行初始化了。mem_init函数主要以
__free_pages
的方式将所有可以使用的物理内存页返回到buddy system中。
void __init mem_init(void)
{
extern int ppro_with_ram_bug(void);
int codesize, reservedpages, datasize, initsize;
int tmp;
int bad_ppro;
...
bad_ppro = ppro_with_ram_bug();
#ifdef CONFIG_HIGHMEM
/* check that fixmap and pkmap do not overlap */
if (PKMAP_BASE+LAST_PKMAP*PAGE_SIZE >= FIXADDR_START) {
...
}
#endif
set_max_mapnr_init();
#ifdef CONFIG_HIGHMEM
high_memory = (void *) __va(highstart_pfn * PAGE_SIZE);
#else
high_memory = (void *) __va(max_low_pfn * PAGE_SIZE);
#endif
/* this will put all low memory onto the freelists */
//__free_all_bootmem:
totalram_pages += __free_all_bootmem();//根据页面位图释放内存中所有可供动态分配的页面到
//buddy system中
//#define __free_all_bootmem() free_all_bootmem()
reservedpages = 0;
for (tmp = 0; tmp < max_low_pfn; tmp++)
/*
* Only count reserved RAM pages
*/
//This generic page_is_ram() returns true if specified address is registered
//as"System RAM" in iomem_resource list.
if (page_is_ram(tmp) && PageReserved(pfn_to_page(tmp)))
reservedpages++;
set_highmem_pages_init(bad_ppro);//初始化高端页面
codesize = (unsigned long) &_etext - (unsigned long) &_text;
datasize = (unsigned long) &_edata - (unsigned long) &_etext;
initsize = (unsigned long) &__init_end - (unsigned long) &__init_begin;
kclist_add
(&kcore_mem, __va(0), max_low_pfn << PAGE_SHIFT);
kclist_add
(&kcore_vmalloc, (void *)VMALLOC_START, VMALLOC_END-VMALLOC_START);
...
#ifndef CONFIG_SMP
//由startup_32( )函数创建的物理内存前8MB的恒等映射用来完成内核的初始化阶段.
//当这种映射不再必要时,内核调用这函数清除对应的页表项
zap_low_mappings();
#endif
}
unsigned long __init free_all_bootmem (void)
{
return(free_all_bootmem_core(NODE_DATA(0)));
}
unsigned long __init free_all_bootmem_node (pg_data_t *pgdat)
{
return(free_all_bootmem_core(pgdat));
}
//-------------free_all_bootmem_core-----------------
static unsigned long __init free_all_bootmem_core(pg_data_t *pgdat)
{
struct page *page;
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;
/* first extant page of the node */
page = virt_to_page(phys_to_virt(bdata->node_boot_start));
idx = bdata->node_low_pfn - (bdata->node_boot_start >> PAGE_SHIFT);
map = bdata->node_bootmem_map;
/* Check physaddr is O(LOG2(BITS_PER_LONG)) page aligned */
if (bdata->node_boot_start == 0 ||
ffs(bdata->node_boot_start) - PAGE_SHIFT > ffs(BITS_PER_LONG))
gofast = 1;
for (i = 0; i < idx; ) {
unsigned long v = ~map[i / BITS_PER_LONG];
if (gofast && v == ~0UL) {
int j, order;
count += BITS_PER_LONG;
__ClearPageReserved(page);
order = ffs(BITS_PER_LONG) - 1;
set_page_refs(page, order);
for (j = 1; j < BITS_PER_LONG; j++) {
if (j + 16 < BITS_PER_LONG)
prefetchw(page + j + 16);
__ClearPageReserved(page + j);
}
__free_pages(page, order);//将2^order个page归还到buddy system
i += BITS_PER_LONG;
page += BITS_PER_LONG;
} else if (v) {
unsigned long m;
for (m = 1; m && i < idx; m<<=1, page++, i++) {
if (v & m) {
count++;
__ClearPageReserved(page);
set_page_refs(page, 0);
__free_page(page);
}
}
} else {
i+=BITS_PER_LONG;
page += BITS_PER_LONG;
}
}
total += count;
/*
* Now free the allocator bitmap itself, it's not
* needed anymore:
*/
page = virt_to_page(bdata->node_bootmem_map);
count = 0;
for (i = 0; i < ((bdata->node_low_pfn-(bdata->node_boot_start >> PAGE_SHIFT))/8 + PAGE_SIZE-1)/PAGE_SIZE; i++,page++) {
count++;
__ClearPageReserved(page);
set_page_count(page, 1);
__free_page(page);
}
total += count;
bdata->node_bootmem_map = NULL;
return total;
}
到这里,物理内存管理的初始化工作就算基本完成了。所有可用的page都已就位,可以通过请求内存页的API:
alloc_pages(mask, order) alloc_page(mask) get_zeroed_page(mask) __get_free_pages(mask, order) __get_free_page(mask)
从buddy system中分配物理内存页,也可以通过释放内存页的API:
free_page(struct page*) free_pages(struct page*, order) __free_page(addr) __free_pages(addr, order)
释放物理内存页到buddy system。
Linux slab 分配器剖析
linux内存管理之kmem_cache_init
void __init kmem_cache_init(void)
{
size_t left_over;
struct cache_sizes *sizes;
struct cache_names *names;
/*
* Fragmentation resistance on low memory - only use bigger
* page orders on machines with more than 32MB of memory.
*/
if (num_physpages > (32 << 20) >> PAGE_SHIFT)
slab_break_gfp_order = BREAK_GFP_ORDER_HI;
/* Bootstrap is tricky, because several objects are allocated
* from caches that do not exist yet:
* 1) initialize the cache_cache cache: it contains the kmem_cache_t
* structures of all caches, except cache_cache itself: cache_cache
* is statically allocated.
* Initially an __init data area is used for the head array, it's
* replaced with a kmalloc allocated array at the end of the bootstrap.
* 2) Create the first kmalloc cache.
* The kmem_cache_t for the new cache is allocated normally. An __init
* data area is used for the head array.
* 3) Create the remaining kmalloc caches, with minimally sized head arrays.
* 4) Replace the __init data head arrays for cache_cache and the first
* kmalloc cache with kmalloc allocated arrays.
* 5) Resize the head arrays of the kmalloc caches to their final sizes.
*/
/* 1) create the cache_cache */
init_MUTEX(&cache_chain_sem);
INIT_LIST_HEAD(&cache_chain);
list_add(&cache_cache.next, &cache_chain);
cache_cache.colour_off = cache_line_size();
cache_cache.array[smp_processor_id()] = &initarray_cache.cache;
cache_cache.objsize = ALIGN(cache_cache.objsize, cache_line_size());
cache_estimate(0, cache_cache.objsize, cache_line_size(), 0,
&left_over, &cache_cache.num);
if (!cache_cache.num)
BUG();
cache_cache.colour = left_over/cache_cache.colour_off;
cache_cache.colour_next = 0;
cache_cache.slab_size = ALIGN(cache_cache.num*sizeof(kmem_bufctl_t) +
sizeof(struct slab), cache_line_size());
/* 2+3) create the kmalloc caches */
sizes = malloc_sizes;
names = cache_names;
while (sizes->cs_size) {
/* For performance, all the general caches are L1 aligned.
* This should be particularly beneficial on SMP boxes, as it
* eliminates "false sharing".
* Note for systems short on memory removing the alignment will
* allow tighter packing of the smaller caches. */
sizes->cs_cachep = kmem_cache_create(names->name,
sizes->cs_size, ARCH_KMALLOC_MINALIGN,
(ARCH_KMALLOC_FLAGS | SLAB_PANIC), NULL, NULL);
/* Inc off-slab bufctl limit until the ceiling is hit. */
if (!(OFF_SLAB(sizes->cs_cachep))) {
offslab_limit = sizes->cs_size-sizeof(struct slab);
offslab_limit /= sizeof(kmem_bufctl_t);
}
sizes->cs_dmacachep = kmem_cache_create(names->name_dma,
sizes->cs_size, ARCH_KMALLOC_MINALIGN,
(ARCH_KMALLOC_FLAGS | SLAB_CACHE_DMA | SLAB_PANIC),
NULL, NULL);
sizes++;
names++;
}
/* 4) Replace the bootstrap head arrays */
{
void * ptr;
ptr = kmalloc(sizeof(struct arraycache_init), GFP_KERNEL);
local_irq_disable();
BUG_ON(ac_data(&cache_cache) != &initarray_cache.cache);
memcpy(ptr, ac_data(&cache_cache), sizeof(struct arraycache_init));
cache_cache.array[smp_processor_id()] = ptr;
local_irq_enable();
ptr = kmalloc(sizeof(struct arraycache_init), GFP_KERNEL);
local_irq_disable();
BUG_ON(ac_data(malloc_sizes[0].cs_cachep) != &initarray_generic.cache);
memcpy(ptr, ac_data(malloc_sizes[0].cs_cachep),
sizeof(struct arraycache_init));
malloc_sizes[0].cs_cachep->array[smp_processor_id()] = ptr;
local_irq_enable();
}
/* 5) resize the head arrays to their final sizes */
{
kmem_cache_t *cachep;
down(&cache_chain_sem);
list_for_each_entry(cachep, &cache_chain, next)
enable_cpucache(cachep);
up(&cache_chain_sem);
}
/* Done! */
g_cpucache_up = FULL;
/* Register a cpu startup notifier callback
* that initializes ac_data for all new cpus
*/
register_cpu_notifier(&cpucache_notifier);
/* The reap timers are started later, with a module init call:
* That part of the kernel is not yet operational.
*/
}
rest_init函数
在内核创建内核线程init之前,内核静态地构建了idle进程,idle进程的pid=0,是系统中唯一一个不通过fork创建的进程。内核线程init以idle为蓝本,被idle创建出来,继续处理内核后续的初始化任务。init是以内核线程方式被内核线程idle创建,所以init的
mm_struct mm
和mm_struct active_mm
都设置为NULL
。在从idle切换到它时,它挪用
idle的地址空间。这意味着,在系统初始化阶段,它使用与idle相同的页目录【init_mm.pgd】。
static void noinline rest_init(void)
__releases(kernel_lock)
{
/*
init是传递给被创建的内核线程去执行的内核函数
*/
kernel_thread(init, NULL, CLONE_FS | CLONE_SIGHAND);
numa_default_policy();
unlock_kernel();
preempt_enable_no_resched();
/*
* The idle thread. There's no useful work to be
* done, so just try to conserve power and have a
* low exit latency (ie sit in a loop waiting for
* somebody to say that they'd like to reschedule)
*/
cpu_idle();
}
Linux中pt_regs结构体
this struct defines the way the registers are stored on the stack during a system call.
//主要任务是为do_fork构造一个pt_regs,然后从do_fork创建新的进程。
//注意,新的线程这时候并未被调度,所以没有运行,系统中任然是idle进程在运行着。
/*
* Create a kernel thread
*/
int kernel_thread(int (*fn)(void *), void * arg, unsigned long flags)
{
struct pt_regs regs;
memset(®s, 0, sizeof(regs));
regs.ebx = (unsigned long) fn;//内核线程的执行函数
regs.edx = (unsigned long) arg;//参数
regs.xds = __USER_DS;
regs.xes = __USER_DS;
regs.orig_eax = -1;
//被创建线程执行的入口代码地址,当进程被调度时,接着eip执行
regs.eip = (unsigned long) kernel_thread_helper;// 1
regs.xcs = __KERNEL_CS;//内核代码段
regs.eflags = X86_EFLAGS_IF | X86_EFLAGS_SF | X86_EFLAGS_PF | 0x2;
/*
Ok, create the new process. 使用CLONE_VM标志避免复制调用进程的页表,共享
内存描述符合所有的页表
*/
return do_fork(flags | CLONE_VM | CLONE_UNTRACED,
0,
®s, //内核栈顶地址
0,
NULL,
NULL);
}
//----------------------kernel_thread_helper----------------------
// [/arch/i386/kernel/process.c/kernel_thread_helper]
/*
参考:[x86体系结构下Linux-2.6.26的进程调度和切换]
(http://home.ustc.edu.cn/~hchunhui/linux_sched.html#sec10)
每一个用户进程都有两个堆栈:
正常情况下,在进程创建[fork]一个进程时,传递给[do_fork]的pt_regs类型的变量
regs指向当前进程[被创建的进程的父进程]的内核堆栈的栈顶位置,里面的内容是是INT
0x80和ENTRY[syscall]压入的。所以regs中的内容全是当前进程[父进程]的中断现场
的内容,因而子进程从内核返回到用户态时恢复的是父进程的现场。这就导致fork返回时
子进程和父进程执行的下一条指令相同。
kernel_thread:
这里,新建一个内核线程时没有使用指向父进程内核堆栈栈顶位置的pt_regs类型的指针,
而是新建了一个pt_regs类型的变量,由kernel_thread函数将其作为局部变量创建并初
始化,然后传递给do_fork函数,do_fork函数会用传递过来的指针regs去初始化自己的
内核堆栈。
当发生调度时,内核堆栈指针从之前的进程的内核堆栈切换到下一个即将运行的进程的内核
堆栈,然后,内核会代表下一个即将运行的进程执行restore_all汇编代码。在执行
restore_all汇编代码之后,寄存器中内容都被恢复到进程中断之前的状态。如果是用户
进程,则当前堆栈指针切换到用户堆栈。然后,进程就像没有发生过中断一样,接着之前保
存的现场往后执行。
---------------------
restore_all:
RESTORE_ALL
#define RESTORE_ALL \
popl %ebx; \
popl %ecx; \
popl %edx; \
popl %esi; \
popl %edi; \
popl %ebp; \
popl %eax; \
1: popl %ds; \
2: popl %es; \
addl $4,%esp; \
3: iret; \
---------------------
From Intel:
the IRET instruction pops the return instruction pointer,
return code segment selector, and EFLAGS image from the
stack to the EIP, CS, and EFLAGS registers, respectively,
and then resumes execution of the interrupted program or
procedure. If the return is to another privilege level, the
IRET instruction also pops the stack pointer and SS from
the stack, before resuming program execution.
*/
/*
对于内核线程init,函数init被注册到regs.ebx中,kernel_thread_helper被注册到regs.eip。
在,在创建init时,regs中的内容会按规定的格式压入到init的内核堆栈中。在调度到init
时,先完成一个内核栈之间的切换,然后通过restore_all汇编代码段将内核堆栈中的内容恢复到相
应的寄存器中。这时,寄存器%edx中是init函数的参数,%ebx中是init函数地址,cs:eip指向
kernel_thread_helper,所以,若这时候没有中断产生,下一条指令就是movl %edx,%eax。
从下面的代码中,可以看出kernel_thread_helper为init线程执行init函数构造了进入与退出的
环境。
*/
extern void kernel_thread_helper(void); /* 定义成全局变量 */
__asm__(
".section .text\n"
".align 4\n"
"kernel_thread_helper:\n\t"//标记kernel_thread_helper的地址
"movl %edx,%eax\n\t"//转到新创建的线程时执行的第一条指令
"pushl %edx\n\t" /* edx指向参数,压入堆栈 */
"call *%ebx\n\t" /* ebx指向函数地址,执行函数 */
"pushl %eax\n\t"
"call do_exit\n" /* 结束线程 */
".previous");
进程创建的copy_process和进程销毁
分析Linux内核创建一个新进程的过程
参考《深入理解Linux内核》中文第三版.第三章.P121.do_fork()函数一节
fork创建的进程要么是写时拷贝的进程,要么是共享原进程地址空间的轻量级进程,要么是没有自己地址空间的内核线程。
/*
* Ok, this is the main fork-routine.
*
* It copies the process, and if successful kick-starts
* it and waits for it to finish using the VM if required.
*/
//do_fork()执行完毕后,虽然子进程处于可运行状态,但是它并没有立刻运行。子进程
//何时执行完全取决于调度程序,也就是schedule()。
long do_fork(
unsigned long clone_flags,
unsigned long stack_start,
struct pt_regs *regs,
unsigned long stack_size,
int __user *parent_tidptr,
int __user *child_tidptr)
{
struct task_struct *p;
int trace = 0;
long pid = alloc_pidmap();
...
/*
栈:从高地址沿低地址方向增长
依照current进程创建一个新的进程,为其创建新的内核堆栈,[对于用户进程]栈中的内容拷贝自
指向current进程的内核堆栈顶的regs,[对于内核线程]栈中的内容来自kernel_thread创建并传递
进来的regs、创建新的task_struct并完全拷贝父进程的task_struct、设置current进程为其父进
程、如果clone_flags!=CLONE_VM则还需创建页表,复制父进程的页表项、设置进程号pid...
注意,内核线程不需要mm,在调度器切换到一个内核线程时,直接使用被切换走的进程的active_mm
*/
p = copy_process
(clone_flags, stack_start, regs, stack_size, parent_tidptr, child_tidptr, pid);
...
wake_up_new_task(p, clone_flags);//将新建的进程加入可运行队列runqueue
/*
void fastcall wake_up_new_task(task_t * p, unsigned long clone_flags)
{
...
//设置current_thread_info()->flags=TIF_NEED_RESCHED
set_need_resched();//设置need_resched
...
__activate_task(p, rq);// move a task to the runqueue.
...
}
static inline void __activate_task(task_t *p, runqueue_t *rq)
{
enqueue_task(p, rq->active);//将p按优先级加入到active的优先级队列中
rq->nr_running++;
}
*/
...
return pid;
}
static task_t *copy_process(unsigned long clone_flags,
unsigned long stack_start,
struct pt_regs *regs,
unsigned long stack_size,
int __user *parent_tidptr,
int __user *child_tidptr,
int pid)
{
int retval;
struct task_struct *p = NULL;
...
/*
拷贝父进程的task_struct,只是浅拷贝。
*/
p = dup_task_struct(current);
...
tsk->pid = pid;//存入新进程的pid
put_user(p->pid, parent_tidptr)//返回子进程pid到父进程
/*
按clone_flags情况复制当前进程的mm_struct:
1.如果clone_flags=CLONE_VM,则不创建新的mm_struct,直接使用父进程的mm_struct。
2.否则,以写时拷贝的方式创建自己的页表,复制父进程的页表项到自己的页表中
3.如果是从内核进程创建一个新的进程[cloning a kernel thread],则令tsk->mm = NULL;
tsk->active_mm = NULL; 然后直接返回。
*/
retval = copy_mm(clone_flags, p)
/*
用父进程内核栈来初始化子进程的内核栈,其中,用来返回到用户空间的pid存放在 pt_regs->exa
字段,在父进程中,他对应子进程的pid。但是,在这里,我们要将子进程的内核栈中该字段强行存
入0,这样,在父进程从fork返回时得到的返回值是子进程的pid,而子进程返回时得到的返回值是0。
这样就可以在返回代码中将父子进程区分开来。
*/
retval = copy_thread(0, clone_flags, stack_start, stack_size, p, regs);
...
}
//--------------dup_task_struct----------------
static struct task_struct *dup_task_struct(struct task_struct *orig)
{
struct task_struct *tsk;
struct thread_info *ti;
prepare_to_copy(orig);
tsk = alloc_task_struct();
if (!tsk)
return NULL;
ti = alloc_thread_info(tsk);
if (!ti) {
free_task_struct(tsk);
return NULL;
}
*ti = *orig->thread_info;
*tsk = *orig;
tsk->thread_info = ti;
ti->task = tsk;
/* One for us, one for whoever does the "release_task()" (usually parent) */
atomic_set(&tsk->usage,2);
return tsk;
}
fork,你拿什么证明你的写时拷贝(COW)
static int copy_mm(unsigned long clone_flags, struct task_struct * tsk)
{
struct mm_struct * mm, *oldmm;
int retval;
...
tsk->mm = NULL;
tsk->active_mm = NULL;
/*
* Are we cloning a kernel thread?
*
* We need to steal a active VM for that..
*/
oldmm = current->mm;//注意:idle->mm为NULL [内核进程的mm域为空]
if (!oldmm) //如果是内核进程创建一个进程,那么这个进程只能是内核线程。直接返回。
return 0;
if (clone_flags & CLONE_VM) {//CLONE_VM标志:共享当前进程的mm_struct
atomic_inc(&oldmm->mm_users);
mm = oldmm;
goto good_mm;
}
retval = -ENOMEM;
mm = allocate_mm();//从缓存分配一个mm_struct对象
/* Copy the current MM stuff.. */
memcpy(mm, oldmm, sizeof(*mm));//完全拷贝oldmm到mm
if (!mm_init(mm))//mm_alloc_pgd(mm)...分配一个新的页目录,不映射到任何物理页
goto fail_nomem;
/*实现内存写时复制的关键*/
retval = dup_mmap(mm, oldmm);
...
good_mm:
tsk->mm = mm;
tsk->active_mm = mm;
return 0;
free_pt:
mmput(mm);
fail_nomem:
return retval;
fail_nocontext:
/*
* If init_new_context() failed, we cannot use mmput() to free the mm
* because it calls destroy_context()
*/
mm_free_pgd(mm);
free_mm(mm);
return retval;
}
因为
init
是内核线程idle
创建的内核线程,所以init并没有创建自己的虚拟内存空间管理结构体mm_struct
,而是在调度到它时使用被调度走的进程的active_mm
:
tsk->mm = NULL; tsk->active_mm = NULL; /* * Are we cloning a kernel thread? * * We need to steal a active VM for that.. */ oldmm = current->mm;//注意:idle->mm为NULL [内核进程的mm域为空] if (!oldmm) //如果是内核进程创建一个进程,那么这个进程只能是内核线程。直接返回。 return 0;
注意,对于内核进程而言,
task_struct->mm
指针域为NULL。可以看到这时候idle使用的init_mm
作为自己的active_mm
。但是init
的mm
域以及active_mm
域均为空。不过,我们马上就会看到,idle
第一个调度的进程就是init
线程。而在调度时,init
挪用
了idle
的active_mm
。为了防止不必要的TLB刷新,Linux中每一个线程被调度器选择运行时都会
挪用
即将被换走的进程的active_mm
。
前面我们看到了内核线程的创建。idle创建init,init并没有自己的
mm_struct
, 在从idle调度到他时,他会挪用idle的mm_struct
。既然内核空间都共享了,自然就不用考虑copy-on-write
的问题。同样,对于轻量级的线程,他会共享进程的地址空间,以CLONE_VM
的方式创建,也不用考虑copy-on-write
的问题。但是,对于不需要共享地址空间的进程的创建呢?换句话说,创建一个线程是为了共享内存,协同工作;创建进却是为了隔离地址空间,保证安全。
自然,在创建一个进程时,就不会传递
CLONE_VM
到do_fork
函数了。这时候,copy_mm
就必须为新建的进程分配并初始化mm
和active_mm
。mm_struct
中有一个成员pdg
,用来存该进程的页目录地址,函数mm_alloc_pgd(mm)
为mm
分配一个页目录,但是并未建立到物理内存页的映射关系。因为Linux
认为往往一个新建的进程马上会去加载执行别的程序。如果二话不说就依据父进程页表,将父进程的物理内存中的非共享数据全部拷贝到另外的物理内存之上,再在新进程中建立自己的页表项映射,这降低了fork
的执行效率不说,还可能导致fork
所做的这些工作全部是无用功。所以Linux采用了copy-on-write
技术。下面就来看看这是怎么一回事儿。
static inline int dup_mmap(struct mm_struct * mm, struct mm_struct * oldmm)
{
struct vm_area_struct * mpnt, *tmp, **pprev;
struct rb_node **rb_link, *rb_parent;
int retval;
unsigned long charge;
struct mempolicy *pol;
...
pprev = &mm->mmap;// struct vm_area_struct *mmap; /* list of VMAs */
/*针对当前进程的每一个VMA,为新进程分配一个对应的VMA*/
for (mpnt = current->mm->mmap ; mpnt ; mpnt = mpnt->vm_next) {
...
tmp = kmem_cache_alloc(vm_area_cachep, SLAB_KERNEL);
*tmp = *mpnt;
...
tmp->vm_flags &= ~VM_LOCKED;
tmp->vm_mm = mm;
tmp->vm_next = NULL;
anon_vma_link(tmp);
...
/*
* Link in the new vma and copy the page table entries:
* link in first so that swapoff can see swap entries,
* and try_to_unmap_one's find_vma find the new vma.
*/
//将VMA链入链表
spin_lock(&mm->page_table_lock);
*pprev = tmp;
pprev = &tmp->vm_next;
//同时将VMA链入红黑树
__vma_link_rb(mm, tmp, rb_link, rb_parent);
rb_link = &tmp->vm_rb.rb_right;
rb_parent = &tmp->vm_rb;
mm->map_count++;
/*复制当前进程的页表,并不复制页表映射的物理页帧,地址范围由tmp给出*/
retval = copy_page_range(mm, current->mm, tmp);
spin_unlock(&mm->page_table_lock);
...
}
retval = 0;
...
}
/*为vma对应的地址空间建立页表。复制方向为:src->pgd ==> dst->pgd*/
int copy_page_range(struct mm_struct *dst,
struct mm_struct *src,
struct vm_area_struct *vma)
{
pgd_t *src_pgd, *dst_pgd;
unsigned long addr, start, end, next;
int err = 0;
...
start = vma->vm_start;//VMA起始地址
src_pgd = pgd_offset(src, start);//在src->pgd中的表项
dst_pgd = pgd_offset(dst, start);//在det->pgd中的表项
end = vma->vm_end;//VMA结束地址
/*循环次数 = vma空间的大小对应的pgd表项数*/
addr = start;
while (addr && (addr < end-1)) {
next = (addr + PGDIR_SIZE) & PGDIR_MASK;
...
err = copy_pud_range(dst, src,
dst_pgd, src_pgd,
vma,
addr, next);
next_pgd:
src_pgd++;
dst_pgd++;
addr = next;
}
return err;
}
可想而知,一个页目录表项指向一页
pud
表,而这一页表中又有1k个表项,其中每一个表项又分别指向一页pmd
表,一页pmd
表同样包含1k个pmd
表项,每一个pmd
表项指向一页pte
表。最后,pte
表中的每一个表项都映射了一页物理页帧。
/*为addr~end的地址空间建立页表映射*/
static int copy_pte_range(struct mm_struct *dst_mm,
struct mm_struct *src_mm,
pmd_t *dst_pmd, pmd_t *src_pmd,
struct vm_area_struct *vma,
unsigned long addr, unsigned long end)
{
pte_t *src_pte, *dst_pte;
pte_t *s, *d;
unsigned long vm_flags = vma->vm_flags;
//获得页表项位置
d = dst_pte = pte_alloc_map(dst_mm, dst_pmd, addr);
spin_lock(&src_mm->page_table_lock);
//获得页表项位置
s = src_pte = pte_offset_map_nested(src_pmd, addr);
for (; addr < end; addr += PAGE_SIZE, s++, d++) {
//拷贝页表项s到页表项d
copy_one_pte(dst_mm,
src_mm,
d,
s,
vm_flags,
addr);
}
...
return 0;
}
前面说到了
copy-on-write
技术。现在我们可以看到Linux是怎么实现它的了:
static inline void
copy_one_pte(struct mm_struct *dst_mm,
struct mm_struct *src_mm,
pte_t *dst_pte, pte_t *src_pte,
unsigned long vm_flags,
unsigned long addr)
{
pte_t pte = *src_pte;
struct page *page;
unsigned long pfn;
...
pfn = pte_pfn(pte);//得到该pte映射的物理页帧
page = NULL;
if (pfn_valid(pfn))
page = pfn_to_page(pfn);//找到描述这一夜物理页帧的page结构体
...
/*
* If it's a COW[copy-on-write] mapping, write protect it both
* in the parent and the child
*/
if ((vm_flags & (VM_SHARED | VM_MAYWRITE)) == VM_MAYWRITE) {
ptep_set_wrprotect(src_pte);//在父页表项中写入写包含位
pte = *src_pte;//拷贝到pte中
}
/*
* If it's a shared mapping, mark it clean in
* the child
*/
if (vm_flags & VM_SHARED)
pte = pte_mkclean(pte);//如果是共享页,则将拷贝的目的页表项清除脏标志位。
...
set_pte(dst_pte, pte);
...
}
重点在:
/* * If it's a COW[copy-on-write] mapping, write protect it both * in the parent and the child */ if ((vm_flags & (VM_SHARED | VM_MAYWRITE)) == VM_MAYWRITE) { ptep_set_wrprotect(src_pte);//在父页表项中写入写包含位 pte = *src_pte;//拷贝到pte中 }
可以看到,不带
CLONE_VM
标志的do_fork
创建出来的进程拥有自己的页表,这不像内核线程,内核线程没有自己的页表。这意味着什么呢?这意味着新的进程有了自己的地址空间。还有一点值得注意,他的这个地址空间是写时保护
的,他将自己的虚拟地址空间以等同于父进程的映射关系映射到物理内存上。一旦父子进程中任意一方向非共享内存页写入数据都会引写异常,将数据拷贝到另外的物理内存页上,更改页表项的映射关系。
//idle最终会执行到cpu_idle函数: void cpu_idle(void) { //这里只是一个示例,在这个实例中,idle会进入这段循环代码,查看是否有进程需要调度。 while (1) { if (current->need_resched) { schedule(); } } } //现在的操作系统对cpu_idle函数的实现就真的是名副其实的"cpu idle": void cpu_idle (void) { int cpu = _smp_processor_id(); /* endless idle loop with no priority at all */ while (1) { /* need_resched: 检查current_thread_info()->flags是否等于TIF_NEED_RESCHED */ while (!need_resched()) {//不需要调度,就执行hlt指令,进入硬件停机状态 void (*idle)(void); if (cpu_isset(cpu, cpu_idle_map)) cpu_clear(cpu, cpu_idle_map); rmb(); idle = pm_idle; if (!idle) idle = default_idle; irq_stat[cpu].idle_timestamp = jiffies; idle();//default_idle } //需要调度时,调用调度器 schedule(); } } /* 进入default_idle后,会执行hlt, 也就是硬件停机,处于该状态时cpu不能执行任何指令,只有通过中断请求才能唤醒。 中断函数中设置need_resched就会进入schedule又开始正常进程调度。否则继续idle。 链接:http://www.zhihu.com/question/26756156/answer/33912754 来源:知乎 */
idle进程执行到cpu_idle函数中,然后陷入while循环,直到需要调度。在创建内核线程init的时候,将init加入到了runqueue,设置了
current_thread_info()->flags=TIF_NEED_RESCHED
所以idle马上调用调度器,由于可运行队列中仅有内核线程init,此时调度器会选择内核线程init,将内核堆栈从idle切换到init,并将init内核堆栈中的内容加载到相应的寄存器中,控制流转移到内核init进程。恢复内核堆栈[restore_all]之后,init执行kernel_thread_helper,这段代码会为线程代码的执行构造一个进入和退出的环境。之后,init进程调用[call]init函数,进行进一步的初始化工作。init作为内核进程,并不会切换页表[cr3],所以使用的地址空间与idle进程一模一样[init->active_mm = idle->active_mm]。
内核堆栈切换相关内容参考 进程—进程调度(1)
Linux系统下init进程的前世今生
The init Process
注意title:“内核进程init转变为用户空间进程[init]”。非常有意思,init并没有fork一个用户进程,而是将自己转变成一个一个用户进程。
此时,被编译到内核地址空间的文件系统已被初始化并安装,虚拟内存管理已经建立起来,中断完成了初始化,开启了中断机制,设备以及设备驱动程序准备就绪,daemon内核线程已被创建,init正在运转,接下来,init内核线程将自己切换到用户模式中执行[move_to_user_mode]。
先说明一下Linux的请求调页机制:
在do_fork时创建进程[task_struct],do_execve时创建此进程mm_struct的vma布局,在进程代码执行时,由page fault将code、data换入内存。
用户程序对内存的操作并不会直接影响到页表,更不会直接影响到物理内存的分配。比如malloc成功,仅仅是改变了某个vma,页表不会变,物理内存的分配也不会变。
假设用户分配了内存,然后访问这块内存。由于页表里面并没有记录相关的映射,CPU产生一次缺页异常。内核捕捉异常,检查产生异常的地址是不是存在于一个合法的vma中。如果不是,则给进程一个”段错误”,让其崩溃;如果是,则分配一个物理页,并为之建立映射。
static int init(void * unused)
{
lock_kernel();
...
do_basic_setup();//完成一系列的setup,包括创建第二个内核线程daemon【or kthreadd】
/*
* check if there is an early userspace init. If yes, let it do all
* the work
*/
if (sys_access((const char __user *) "/init", 0) == 0)
execute_command = "/init";
else
prepare_namespace();
/*
* Ok, we have completed the initial bootup, and
* we're essentially up and running. Get rid of the
* initmem segments and start the user-mode stuff..
*/
free_initmem();//回收初始化代码段
unlock_kernel();
...
//下面,内核init线程尝试转换到用户态,并加载文件中的init程序执行。
/*
* We try each of these until one succeeds.
*
* The Bourne shell can be used instead of init if we are
* trying to recover a really broken machine.
*/
if (execute_command)
run_init_process(execute_command);
run_init_process("/sbin/init");
run_init_process("/etc/init");
run_init_process("/bin/init");
run_init_process("/bin/sh");
panic("No init found. Try passing init= option to kernel.");
}
/*
加载用户态代码【程序】
*/
static void run_init_process(char *init_filename)
{
argv_init[0] = init_filename;
/*
从内核给出的execve接口【这个函数不是用户库的execve,而是内核自己实现的函数】进入execve
的系统调用:sys_execve。
内核和用户执行的同名接口execve,他们的实现是不同的,内核的execve来自内核给出的实现;用户
的execve来自用户c/c++库,只是个包装函数。
但是它们最终的目的都是一样的:为系统调用sys_execve的执行构造正确地参数环境pt_regs regs。
asmlinkage int sys_execve(struct pt_regs regs)
*/
//从文件中加载init程序,修改返回时的cs: regs->cs = __USER_CS; 在iret之后切换到用户态
execve(init_filename, argv_init, envp_init);
}
#define _syscall3(type,name,type1,arg1,type2,arg2,type3,arg3) \
type name(type1 arg1,type2 arg2,type3 arg3) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \
: "=a" (__res) \
: "0" (__NR_##name),"b" ((long)(arg1)),"c" ((long)(arg2)), \
"d" ((long)(arg3))); \
__syscall_return(type,__res); \
}
#define __syscall_return(type, res) \
do { \
if ((unsigned long)(res) >= (unsigned long)(-(128 + 1))) { \
errno = -(res); \
res = -1; \
} \
return (type) (res); \
} while (0)
//execve函数
static inline _syscall3(int,execve,const char *,file,char **,argv,char **,envp)
_syscall3是个宏定义,所以在预编译阶段
static inline _syscall3(int,execve,const char *,file,char **,argv,char **,envp)
会被展开成:
int execve(const char * file,char ** argv,char ** envp) { long __res; __asm__ volatile ("int $0x80" : "=a" (__res) : "0" (__NR_execve), "b" ((long)(file)), "c" ((long)(argv)), "d" ((long)(envp))); do { if ((unsigned long)(__res) >= (unsigned long)(-(128 + 1))) { errno = -(__res); __res = -1; } return (int) (__res); } while (0); }
内核线程调用的
execve
的定义就来自这里。和用户态调用的库函数功能一样,这个内核函数在寄存器中存储了要传递给sys_execve
的参数。通过int 0x80
指令将当前pc、eflags等寄存器的内容压入内核栈中,然后根据中断号将IDT
中对应索引中的指令地址放入CS:IP
寄存器中,下一个时钟脉冲到来时,cpu
就执行这条指令。IDT
中的中断服务程序入口地址的进入不是通过call
指令,而是类似于jmp
指令,指示pc
转到入口地址处去执行指令,wherever it is and whatever it is。对应int 0x80
指令的中断服务程序入口是system_call
:
//arch\i386\kernel\entry.s # system call handler stub ENTRY(system_call) //%eax中存放了系统调用号__NR_xxx pushl %eax # save orig_eax SAVE_ALL GET_THREAD_INFO(%ebp) # system call tracing in operation testb $(_TIF_SYSCALL_TRACE|_TIF_SYSCALL_AUDIT),TI_flags(%ebp) jnz syscall_trace_entry cmpl $(nr_syscalls), %eax jae syscall_badsys syscall_call: call *sys_call_table(,%eax,4)//调用系统调用,sys_xxx /* 系统调用完事之后返回到到这里接着执行,内核堆栈恢复到执行 call *sys_call_table(,%eax,4)调用之前的状态【所谓之前 的状态并不是指内核堆栈中的内容不会改变,而是指恢复了栈顶 指针的位置。这样,restore_all才能准确的恢复寄存器】。所 以不用理会系统调用陷入的调用路径有多么深,多么复杂,只要 不蓄意破坏内核堆栈,在恢恢[restore_all]之前,栈顶指针一 定会就位。 */ movl %eax,EAX(%esp) # store the return value syscall_exit: cli # make sure we don't miss an interrupt # setting need_resched or sigpending # between sampling and the iret movl TI_flags(%ebp), %ecx testw $_TIF_ALLWORK_MASK, %cx # current->work jne syscall_exit_work /* 如果不考虑其他事情,内核执行到restore_all,然后恢复内核堆栈中的内容 到相应的寄存器中。 */ restore_all: RESTORE_ALL
再来看看
SAVE_ALL
和struct pt_regs
//arch\i386\kernel\entry.s #define SAVE_ALL \ cld; \ //寄存器内容压入内核堆栈 pushl %es; \ pushl %ds; \ pushl %eax; \ pushl %ebp; \ pushl %edi; \ pushl %esi; \ pushl %edx; \ pushl %ecx; \ pushl %ebx; \ movl $(__USER_DS), %edx; \ movl %edx, %ds; \ movl %edx, %es;
//include\asm-i386\ptrace.h /* this struct defines the way the registers are stored on the stack during a system call. */ struct pt_regs { long ebx; long ecx; long edx; long esi; long edi; long ebp; long eax; int xds; //数据段 int xes; //附加段 long orig_eax;//系统调用号:__NR_execve、__NR_open之类的系统调用号 /* 对于系统调用,上面部分的内容都是ENTRY(system_call)压入内核栈中的,下面部分的内容是 int 0X80指令的微操作压入内核栈中的。 */ long eip; int xcs; //代码段 long eflags; long esp; int xss; //堆栈段 };
进入
ENTRY(system_call)
之后,ENTRY(system_call)
会为sys_xxx
在内核堆栈中构造好参数pt_regs
,然后调用系统调用call *sys_call_table(,%eax,4)
进入系统调用,对于__NR_execve
,内核会调用sys_execve
函数。
asmlinkage int sys_execve(struct pt_regs regs)
{
int error;
char * filename;
filename = getname((char __user *) regs.ebx);
error = PTR_ERR(filename);
if (IS_ERR(filename))
goto out;
//do_xxx,接下来就要“do”系统调用啦
error = do_execve(filename,
(char __user * __user *) regs.ecx,
(char __user * __user *) regs.edx,
®s);
if (error == 0) {
task_lock(current);
current->ptrace &= ~PT_DTRACE;
task_unlock(current);
set_thread_flag(TIF_IRET);
}
putname(filename);
out:
return error;
}
linux上应用程序的执行机制
linux中的可执行文件
/*
在linux中,可执行文件的识别被组织成了一个链表,每一种文件格式被定义为一个结构linux_binprm
*/
/*
* MAX_ARG_PAGES defines the number of pages allocated for arguments
* and envelope for the new program. 32 should suffice, this gives
* a maximum env+arg of 128kB w/4KB pages!
*/
#define MAX_ARG_PAGES 32
/* sizeof(linux_binprm->buf) */
#define BINPRM_BUF_SIZE 128
/*
* This structure is used to hold the arguments that are used when loading binaries.
*/
struct linux_binprm{
char buf[BINPRM_BUF_SIZE];// 保存可执行文件的头128字节
struct page *page[MAX_ARG_PAGES];
struct mm_struct *mm;//进程的内存描述符mm
unsigned long p; /* current top of mem */ // 当前内存页最高地址
int sh_bang;
struct file * file;// 要执行的文件
int e_uid, e_gid;// 要执行的进程的有效用户ID和有效组ID
kernel_cap_t cap_inheritable, cap_permitted, cap_effective;
void *security;
int argc, envc; // 命令行参数和环境变量数目
char * filename;// 要执行的文件的名称
char * interp; // 要执行的文件的真实名称,通常和filename相同
unsigned interp_flags;
unsigned interp_data;
unsigned long loader, exec;
};
/*
* sys_execve() executes a new program.
*/
int do_execve(char * filename,
char __user *__user *argv,
char __user *__user *envp,
struct pt_regs * regs)
{
//使用一个linux_binprm结构体记录信息
struct linux_binprm *bprm;
struct file *file;
int retval;
int i;
retval = -ENOMEM;
bprm = kmalloc(sizeof(*bprm), GFP_KERNEL);
if (!bprm)
goto out_ret;
memset(bprm, 0, sizeof(*bprm));
//打开可执行文件
file = open_exec(filename);
...
bprm->p = PAGE_SIZE*MAX_ARG_PAGES-sizeof(void *);
bprm->file = file;
bprm->filename = filename;
bprm->interp = filename;
/*
分配一个进行kernel\fork.c::mm_init初始化的mm_struct。
也就是用作新进程的内存描述符mm。该mm已经进行了基本的初始化,包括分配一个页目录:
mm->pgd = pgd_alloc(mm);
*/
bprm->mm = mm_alloc();
retval = -ENOMEM;
...
/*
* cycle the list of binary formats handler, until one recognizes the image
*/
/*
找到可以用来加载该类型可执行文件的加载函数
*/
retval = search_binary_handler(bprm,regs);
...
return retval;
}
进程的创建与可执行程序的加载
static struct linux_binfmt elf_format = {
.module = THIS_MODULE,
.load_binary = load_elf_binary,
.load_shlib = load_elf_library,
.core_dump = elf_core_dump,
.min_coredump = ELF_EXEC_PAGESIZE
};
/*
search_binary_handle会通过判断文件头部的魔数确定文件的格式,并且调用相应的装载器。比如
ELF可执行文件的装载处理过程叫做load_elf_binary
*/
/*
* cycle the list of binary formats handler, until one recognizes the image
*/
int search_binary_handler(struct linux_binprm *bprm,struct pt_regs *regs){
int try,retval;
struct linux_binfmt *fmt;
...
read_lock(&binfmt_lock);
for (fmt = formats ; fmt ; fmt = fmt->next) {
int (*fn)(struct linux_binprm *, struct pt_regs *) = fmt->load_binary;
if (!fn)
continue;
if (!try_module_get(fmt->module))
continue;
read_unlock(&binfmt_lock);
/*
尝试去加重可执行文件。如果不成功,就开始下轮循环,选择其他fmt->load_binary来继续
尝试加载,直到找到能够加载该可执行文件的fmt->load_binary函数。例如,加载elf格式
可执行文件的函数为:load_elf_binary
*/
retval = fn(bprm, regs);
...
return retval;
}
ELF在Linux下的加载过程
可执行文件(ELF)格式的理解
elf格式分析
//include\linux\elf.h
/*
elf格式可执行文件的文件头(Elf header):
整个ELF映像就是由文件头、区段头表、程序头表、一定数量的区段、以及一定数量的部构成。
elf header就是用来描述整个ELF映像的一幅地图。
ELF映像的装入/启动过程,就是在各种头部信息的指引下将某些部或区段装入一个进程的用户空间,并为
其运行做好准备(例如装入所需的共享库),最后(在目标进程首次受调度运行时)让CPU进入其程序入口的过
程。
*/
#define elfhdr elf32_hdr
typedef struct elf32_hdr{
unsigned char e_ident[EI_NIDENT];
Elf32_Half e_type;//目标文件类型
Elf32_Half e_machine;//硬件平台
Elf32_Word e_version;//elf头部版本
Elf32_Addr e_entry; //程序进入点
Elf32_Off e_phoff;//程序头表偏移量
Elf32_Off e_shoff;//节头表偏移量
Elf32_Word e_flags;
Elf32_Half e_ehsize;//elf头部长度
Elf32_Half e_phentsize;//程序头表中一个条目的长度
Elf32_Half e_phnum;//程序头表条目数目
Elf32_Half e_shentsize;
Elf32_Half e_shnum;
Elf32_Half e_shstrndx;//节头表字符索引
} Elf32_Ehdr;
//include\asm-i386\a.out.h
//定义执行文件a.out的结构
struct exec
{
unsigned long a_info; /* Use macros N_MAGIC, etc for access */
unsigned a_text; /* length of text, in bytes */
unsigned a_data; /* length of data, in bytes */
unsigned a_bss; /* length of uninitialized data area for file, in bytes */
unsigned a_syms; /* length of symbol table data in file, in bytes */
unsigned a_entry; /* start address */
unsigned a_trsize; /* length of relocation info for text, in bytes */
unsigned a_drsize; /* length of relocation info for data, in bytes */
};
//include\linux\elf.h
/*
程序头表(program header table):
程序头表告诉系统如何建立一个进程映像.它是从加载执行的角度来看待elf文件.从它的角度看elf文
件被分成许多段,elf文件中的代码、链接信息和注释都以段的形式存放。每个段都在程序头表中有一个表
项描述,包含以下属性:段的类型,段的驻留位置相对于文件开始处的偏移,段在内存中的首字节地址,段
的物理地址,段在文件映像中的字节数.段在内存映像中的字节数,段在内存和文件中的对齐标记。
*/
#define elf_phdr elf32_phdr
typedef struct elf32_phdr{
Elf32_Word p_type; //段的类型
Elf32_Off p_offset;//段的位置相对于文件开始处的偏移
Elf32_Addr p_vaddr;//段在内存中的首字节地址
Elf32_Addr p_paddr;//物理内存地址,对应用程序来说,这个字段无用
Elf32_Word p_filesz;//段在文件映像中的字节数
Elf32_Word p_memsz;//段在内存映像中的字节数
Elf32_Word p_flags;//段的标记
Elf32_Word p_align;//段在内存中的对齐标记
} Elf32_Phdr;
static int load_elf_binary(struct linux_binprm * bprm, struct pt_regs * regs){
unsigned long elf_entry, interp_load_addr = 0;
struct elf_phdr * elf_ppnt, *elf_phdata;
unsigned long elf_bss, elf_brk;
unsigned long load_addr = 0, load_bias = 0;
char * elf_interpreter = NULL;
struct file *interpreter = NULL;
struct {
struct elfhdr elf_ex;
struct elfhdr interp_elf_ex;
struct exec interp_ex;
} *loc;
int executable_stack = EXSTACK_DEFAULT;
...
/* Get the exec-header, 获取文件头 */
loc->elf_ex = *((struct elfhdr *) bprm->buf);
/*检查我是否能够解释并加载该文件,如果不能,就退出*/
if (memcmp(loc->elf_ex.e_ident, ELFMAG, SELFMAG) != 0)
goto out;
if (loc->elf_ex.e_type != ET_EXEC && loc->elf_ex.e_type != ET_DYN)
goto out;
if (!elf_check_arch(&loc->elf_ex))
goto out;
if (!bprm->file->f_op||!bprm->file->f_op->mmap)
goto out;
...
/*计算所有程序头表的大小*/
size = loc->elf_ex.e_phnum * sizeof(struct elf_phdr);
elf_phdata = (struct elf_phdr *) kmalloc(size, GFP_KERNEL);
/*将所有的程序头表读入到elf_phdata指向的连续物理空间中*/
/*
bprm->file:打开的可执行文件。kernel_read从这个文件读取内容
loc->elf_ex.e_phoff:程序头表在文件中的偏移。kernel_read从这个位置开始读取
elf_phdata:程序头表起始位置的物理内存指针。kernel_read将读取的内容存放到它指向的内存区域
size: 所有程序头表的大小。kernel_read从文件中读取这么多字节的内容到内存中
*/
retval = kernel_read(bprm->file, loc->elf_ex.e_phoff, (char *) elf_phdata, size);
...
elf_ppnt = elf_phdata;
elf_bss = 0;
elf_brk = 0;
...
/*循环:扫描ELF程序段表,查找该elf文件中是否包含解释器段[.interp]。
并不是每一个elf可执行文件都含有解释器段,如果你的elf使用到了动态链接库,那么elf文件
中就会存在这个段。在进程转到被加载程序的入口之前,他首先执行.interp段指定的动态链接
器,动态连接器接下来负责加载该应用程序所需要使用的各种动态库。加载完毕,动态连接器才
将控制传递给应用程序的main函数。如此,你的应用程序才得以运行。
*/
for (i = 0; i < loc->elf_ex.e_phnum; i++)(
/*处理.interp类型的段*/
if (elf_ppnt->p_type == PT_INTERP) {
/*
在这个section中,包含了动态链接过程中所使用的解释器路径和名称。在Linux里面,这个
解释器实际上就是/lib/ld-linux.so
*/
/*读取该section到elf_interpreter指向的内存域*/
retval = kernel_read(bprm->file,
elf_ppnt->p_offset,
elf_interpreter,
elf_ppnt->p_filesz);
...
/*打开动态链接器文件,返回到struct file *interpreter*/
interpreter = open_exec(elf_interpreter);
/*读取动态链接器文件文件头到bprm->buf中*/
retval = kernel_read(interpreter, 0, bprm->buf, BINPRM_BUF_SIZE);
/* Get the exec headers */
loc->interp_ex = *((struct exec *) bprm->buf);//假定为a.out格式的文件头结构
loc->interp_elf_ex = *((struct elfhdr *) bprm->buf);//假定为ELF文件头结构
break;
}
elf_ppnt++;
)
...
/*
如果当前进程是一个内核进程,那么他没有mm_struct mm,mm=NULL,如果当前进程是一个用
户程序他会有一个mm_struct mm来描述和管理自己的虚拟地址空间,mm!=NULL。
在这里,无论是哪一种情况,进程原来的mm到此就得释放啦!因为进程不在理会原来执行的程序,
他要去加载并执行新的程序,并为新的程序建立新的mm、VMA、新的映射...
*/
/* Flush all traces of the currently running executable */
retval = flush_old_exec(bprm);
/* Discard our unneeded old files struct */
if (files) {
steal_locks(files);
put_files_struct(files);
files = NULL;
}
/* OK, This is the point of no return */
current->mm->start_data = 0;
current->mm->end_data = 0;
current->mm->end_code = 0;
current->mm->mmap = NULL;
current->flags &= ~PF_FORKNOEXEC;
current->mm->def_flags = def_flags;
...
/* Now we do a little grungy work by mmaping the ELF image into
the correct location in memory. At this point, we assume that
the image should be loaded at fixed address, not at a variable
address. */
for(i = 0, elf_ppnt = elf_phdata; i < loc->elf_ex.e_phnum; i++, elf_ppnt++) {
int elf_prot = 0, elf_flags;
unsigned long k, vaddr;
if (elf_ppnt->p_type != PT_LOAD)//是否是可加载段
continue;
...
if (elf_ppnt->p_flags & PF_R) elf_prot |= PROT_READ;
if (elf_ppnt->p_flags & PF_W) elf_prot |= PROT_WRITE;
if (elf_ppnt->p_flags & PF_X) elf_prot |= PROT_EXEC;
elf_flags = MAP_PRIVATE|MAP_DENYWRITE|MAP_EXECUTABLE;
vaddr = elf_ppnt->p_vaddr;//读取该段对应用户空间的虚拟地址
...
/*
建立用户空间虚存区间与目标映像文件中某个连续区间之间的映射。这个函数基本上就是
do_mmap(),其返回值就是实际映射的(起始)地址。对于类型为ET_EXEC的可执行程序映像而
言,代码中的load_bias是0,所以装入的起点就是映像自己提供的地址vaddr。
*/
error = elf_map(bprm->file,
load_bias + vaddr,
elf_ppnt,
elf_prot,
elf_flags);
...
}
loc->elf_ex.e_entry += load_bias;
/*
Linux内核应该既支持静态连接的ELF映像、也支持动态连接的ELF映像。装入/启动ELF映像必需由内
核完成,而动态连接的实现则既可以在内核中完成,也可在用户空间完成。因此,GNU把对于动态连
接ELF映像的支持作了分工:把ELF映像的装入/启动放在Linux内核中完成;而把动态连接的实现放
在用户空间完成,并为此提供一个称为“解释器”的工具软件,而解释器的装入/启动 也由内核负责。
*/
if (elf_interpreter) {//如果有解释器,入口指向解释器
if (interpreter_type == INTERPRETER_AOUT)
elf_entry = load_aout_interp(&loc->interp_ex,
interpreter);
else
//装入解释器程序,并返回解释器程序入口
elf_entry = load_elf_interp(&loc->interp_elf_ex,
interpreter,
&interp_load_addr);
...
}else {//否则,指向应用程序入口
elf_entry = loc->elf_ex.e_entry;
}
...
/*
start_thread,为进程返回用户态准备regs,这就是为什么要将regs参数一层层的传递
过来的原因。start_thread要更改regs中的内容,使得进程在系统调用返回时能从内核
态切换到用户态。
*/
start_thread(regs, elf_entry, bprm->p);
...
}
static unsigned long elf_map(struct file *filep,
unsigned long addr,
struct elf_phdr *eppnt,
int prot,
int type)
{
unsigned long map_addr;
down_write(¤t->mm->mmap_sem);
/*创建新的VMA,这就是关键的mmap部分*/
map_addr = do_mmap(filep,
ELF_PAGESTART(addr),
eppnt->p_filesz + ELF_PAGEOFFSET(eppnt->p_vaddr),
prot,
type,//映射类型
eppnt->p_offset - ELF_PAGEOFFSET(eppnt->p_vaddr));
up_write(¤t->mm->mmap_sem);
return(map_addr);
}
与进程原来执行的程序断开联系:
int flush_old_exec(struct linux_binprm * bprm)
{
int retval;
...
/*
* Release all of the old mmap stuff
*/
retval = exec_mmap(bprm->mm);
bprm->mm = NULL; /* We're using it now */
...
flush_thread();
...
flush_signal_handlers(current, 0);
flush_old_files(current->files);
...
return retval;
}
在建立到可执行文件的映射之前需要解除与之前的程序的映射关系,废掉之前的
mm
,使用新的mm
,新的mm
来自bprm->mm
。
static int exec_mmap(struct mm_struct *mm)
{
struct task_struct *tsk;
struct mm_struct * old_mm, *active_mm;
/* Notify parent that we're no longer interested in the old VM */
//无论进程有没有旧的mm,现在都不需要了。
tsk = current;
old_mm = current->mm;
mm_release(tsk, old_mm);//释放current->mm
...
task_lock(tsk);
active_mm = tsk->active_mm;
/*进程使用新的mm和active_mm*/
tsk->mm = mm;
tsk->active_mm = mm;
activate_mm(active_mm, mm);
task_unlock(tsk);
arch_pick_mmap_layout(mm);
...
mmdrop(active_mm);
return 0;
}
下面的描述都是针对进程的用户空间。对于内核空间,用户进程在陷入内核之前是无权访问的。
每一个进程都有唯一的平坦地址空间,统一由
mm_struct
管理。在地址空间中我们更关心的是进程有权访问的虚拟内存地址区间[VMA],比如08048000-0804c000。这些可被访问的合法地址区间被称为虚拟内存区域(vm_area_struct
),通过内核,进程可以给自己的地址空间动态地添加或减少虚拟内存区域。在前面我们看到,当创建一个新的进程时内核调用copy_mm
函数。这个函数通过建立新进程的所有页表和内存描述符mm
来创建进程的地址空间。
do_mmap
就用来在进程的线性地址空间[user space]中创建一个新的虚拟内存区域(vm_area_struct
)
如果创建的地址区间和一个已经存在的地址区间相邻,并且他们具有相同访问权限,那么两个区间合并为一个。若不能合并,就新建一个
VMA
。
/*
file:被映射的文件
offset:文件映射起始偏移
len:映射长度为len字节
addr:可选参数,指定索搜空闲区域的起始位置
prot:指定内存区域中的页面访问权限
flag:指定了VMA标志
file为NULL,offset为0,则为匿名映射
否则为文件映射
*/
static inline unsigned long do_mmap(struct file *file, unsigned long addr,
unsigned long len, unsigned long prot,
unsigned long flag, unsigned long offset)
{
unsigned long ret = -EINVAL;
if ((offset + PAGE_ALIGN(len)) < offset)
goto out;
if (!(offset & ~PAGE_MASK))
/*将mmap任务转移给do_mmap_pgoff*/
ret = do_mmap_pgoff(file,
addr,
len,
prot,
flag,
offset >> PAGE_SHIFT);
out:
return ret;
}
mmap 内核实现–do_mmap_pgoff
Linux内核源码阅读之内存映射篇
/*
do_mmap_pgoff() begins by performing some basic sanity checks. It first checks the
appropriate filesystem or device functions are available if a file or device is
being mapped. It then ensures the size of the mapping is page aligned and that it
does not attempt to create a mapping in the kernel portion of the address space. It
then makes sure the size of the mapping does not overflow the range of pgoff and
finally that the process does not have too many mapped regions already.
This rest of the function is large but broadly speaking it takes the following
steps:
-Sanity check the parameters;
-Find a free linear address space large enough for the memory mapping. If a
filesystem or device specific get_unmapped_area() function is provided, it will be
used otherwise arch_get_unmapped_area() is called;
-Calculate the VM flags and check them against the file access permissions;
-If an old area exists where the mapping is to take place, fix it up so that it is
suitable for the new mapping;
-Allocate a vm_area_struct from the slab allocator and fill in its entries;
-Link in the new VMA;
-Call the filesystem or device specific mmap function;
-Update statistics and exit.
*/
unsigned long do_mmap_pgoff(struct file * file,
unsigned long addr,
unsigned long len,
unsigned long prot,
unsigned long flags,
unsigned long pgoff)
{
struct mm_struct * mm = current->mm;
struct vm_area_struct * vma, * prev;
struct inode *inode;
unsigned int vm_flags;
...
/* Careful about overflows.. */
len = PAGE_ALIGN(len);
if (!len || len > TASK_SIZE)//TASK_SIZE = 3G,只用于映射用户空间以下的虚拟地址空间
return -EINVAL;
/* offset overflow? */
if ((pgoff + (len >> PAGE_SHIFT)) < pgoff)
return -EINVAL;
/* Too many mappings? */
if (mm->map_count > sysctl_max_map_count)
return -ENOMEM;
/* Obtain the address to map to. we verify (or select) it and ensure
* that it represents a valid section of the address space.
*/
addr = get_unmapped_area(file, addr, len, pgoff, flags);
...
inode = file ? file->f_dentry->d_inode : NULL;
...
/*
分配一个vma,并初始化vma
*/
vma = kmem_cache_alloc(vm_area_cachep, SLAB_KERNEL);
memset(vma, 0, sizeof(*vma));
vma->vm_mm = mm;
vma->vm_start = addr;
vma->vm_end = addr + len;
vma->vm_flags = vm_flags;
vma->vm_page_prot = protection_map[vm_flags & 0x0f];
vma->vm_pgoff = pgoff;
/*如果文件存在,映射文件*/
if (file) {
error = -EINVAL;
if (vm_flags & VM_DENYWRITE) {
error = deny_write_access(file);
correct_wcount = 1;
}
vma->vm_file = file;
get_file(file);
/*
调用与文件系统或设备相关的mmap函数,对于多数文件系统而言,这里将调用
generic_file_mmap()
*/
error = file->f_op->mmap(file, vma);
} else if (vm_flags & VM_SHARED) {
error = shmem_zero_setup(vma);
}
...
addr = vma->vm_start;
pgoff = vma->vm_pgoff;
vm_flags = vma->vm_flags;
...
/*把新建的虚拟区插入到进程的地址空间*/
vma_link(mm, vma, prev, rb_link, rb_parent);
file = vma->vm_file;
...
return addr;
}
将程序文件映射到
vma
中之后,内核并没有急着将文件中的代码段和数据段等内容写到物理内存中去,而是充分利用请求调页
机制,通过缺页异常处理程序将不再内存中的合理访问数据从磁盘中写入内存。既节省了内存,又利用了程序的局部性原理不至于使效率下降。
#define start_thread(regs, new_eip, new_esp) do { \
__asm__("movl %0,%%fs ; movl %0,%%gs": :"r" (0)); \
set_fs(USER_DS); \
//将4个段寄存器都设置到用户态,促使进程从系统调用恢复时切换到用户态
regs->xds = __USER_DS; \
regs->xes = __USER_DS; \
regs->xss = __USER_DS; \
regs->xcs = __USER_CS; \
/*
系统调用返回之后,进程从eip指定的指令地址开始执行程序。
即新的程序的入口地址。【可能是解释器入口,也可能直接是应用程序入口】
*/
regs->eip = new_eip; \
/*指向用户栈的新的栈顶指针*/
regs->esp = new_esp; \
} while (0)
执行完这个宏,系统调用从
load_elf_binary
开始返回,退栈。然后回到ENTRY(system_call)
中系统调用指令的下一条指令接着执行,最后,RESTORE_ALL
,切换到用户态,并开始执行新的被加载的程序。需要注意:
init线程作为一个内核线程是没有自己的地址空间的,即他的
mm
域为NULL
。可是前面我们看到在init从内核execve
接口进入sys_execve
系统调用的时候,在接下来的执行过程中,内核会废掉他之前的mm
然后使用新分配的一个bprm->mm
作为他的mm
。所以从这种意义上来说,从这里开始init
就已不再是一个内核线程,他有了自己的崭新的地址空间。接下来,内核修改init
的特权级,将其彻底变为一个用户态的进程。完成的vma
映射之后,init
返回用户态,使用自己全新的页目录,在执行用户态的第一条指令时产生一个缺页中段【因为execve
只为init
创建了vma
,并没有加载任何数据和代码到物理内存中】,分配一个物理页,调入数据,建立页表映射。
Linux.内存管理.一图总结.BBS.chinaunix
Important parts of the kernel
MEMORY MANAGEMENTWALK-THROUGH[PDF]
IBMdeveloperWorks.内存管理内幕
内核页表和进程页表
cnblogs.Linux内存管理原理
The Memory Subsystem[hardware architecture]
Linux内核分析笔记—-内存管理
理解Linux操作系统—Process和Memory
缺页异常详解
Linux物理内存描述[PDF]
slab分配器
Overview of Linux Memory Management Concepts: Slabs
How the Kernel Manages Your Memory
Page Cache, the Affair Between Memory and Files[1.内存共享:页表->页缓存->文件:文件共享;2.存储器映射:mmap]
Linux内存管理之mmap详解 [建立vm_area_struct结构体]
IBM.User space memory access from the Linux kernel
如何实现一个malloc
malloc函数详解
A malloc Tutorial[PDF]
Linux 内核的文件 Cache 管理机制介绍
Linux内核分析笔记—-页高速缓存和页回写
The buffer cache
Linux Page Cache
linux内核分析笔记—-内存管理
[gorman.High Memory Management](https://www.kernel.org/doc/gorman/html/understand/understand012.html#chap: High Memory Management)
stackoverflow.How does the linux kernel manage less than 1GB physical memory?
Memory management
Linux Device Drivers.3rd Edition.Memory Management in Linux
Linux/include/linux/mm_types.h
free_area
free_list
vm_struct
init_mm
Virtual memory
anon_vma
address_space
address_space_operations
slab
kmem_cache
mem_map
页描述符与物理地址(相关主题帖子总结)
wiki.osdev.Paging
kmalloc
vmalloc
kmap
mmap
handle_mm_fault
handle_pte_fault
Page table in Linux kernel space during boot
深入Linux内核架构.3.内存管理.3.4.初始化内存管理.p139
vmalloc返回的地址与page_address得到的地址?
Linux内核源代码情景分析
深入理解Linux内核
Linux Kernel Development Second Edition
KernelAnalysis-HOWTO
MIT.xv6
深入Linux内核架构
深入Linux内存管理