搜档网
当前位置:搜档网 › 深入理解Linux内存映射机制

深入理解Linux内存映射机制

深入理解Linux内存映射机制
深入理解Linux内存映射机制

深入理解Linux内存映射机制

创建时间:2008-08-31

文章属性:原创

文章提交:wzt (wzt_at_https://www.sodocs.net/doc/a56787579.html,)

深入理解Linux内存映射机制

Author: wzt

EMail: wzt@https://www.sodocs.net/doc/a56787579.html,

Site: https://www.sodocs.net/doc/a56787579.html, & https://www.sodocs.net/doc/a56787579.html,/wzt85

Date: 2008-6-13

一. 绪论

二. X86的硬件寻址方法

三. 内核对页表的设置

四. 实例分析映射机制

一. 绪论

我们经常在程序的反汇编代码中看到一些类似0x32118965这样的地址,操作系统中称为线性地址,或虚拟地址。虚拟地址有什么用?虚拟地址

又是如何转换为物理内存地址的呢?本章将对此作一个简要阐述。

1.1 Linux内存寻址概述

现代意义上的操作系统都处于32位保护模式下。每个进程一般都能寻址4G的物理空间。但是我们的物理内存一般都是几百M,进程怎么能获得4G

的物理空间呢?这就是使用了虚拟地址的好处,通常我们使用一种叫做虚拟内存的技术来实现,因为可以使用硬盘中的一部分来当作内存使用

。例外一点现在操作系统都划分为系统空间和用户空间,使用虚拟地址可以很好的保护内核空间被用户空间破坏。

对于虚拟地址如何转为物理地址,这个转换过程有操作系统和CPU共同完成. 操作系统为CPU设置好页表。CPU通过MMU单元进行地址转换。

1.2 浏览内核代码的工具

现在的内核都很大,因此我们需要某种工具来阅读庞大的源代码体系,现在的内核开发工具都选用vim+ctag+cscope浏览内核代码,网上已有

现成的makefile文件用来生成ctags/cscope/etags。

一、用法:

找一个空目录,把附件Makefile拷贝进去。然后在该目录中选择性地运行如下make命令:$ make

将处理/usr/src/linux下的源文件,在当前目录生成ctags, cscope

注:SRCDIR用来指定内核源代码目录,如果没有指定,则缺省为/usr/src/linux/

1) 只创建ctags

$ make SRCDIR=/usr/src/linux-2.6.12/ tags

2) 只创建cscope

$ make SRCDIR=/usr/src/linux-2.6.12/ cscope

3) 创建ctags和cscope

$ make SRCDIR=/usr/src/linux-2.6.12/

4) 只创建etags

$ make SRCDIR=/usr/src/linux-2.6.12/ TAGS

二、处理时包括的内核源文件:

1) 不包括drivers,sound目录

2) 不包括无关的体系结构目录

3) fs目录只包括顶层目录和ext2,proc目录

三、最简单的ctags命令

1) 进入

进入vim后,用

:tag func_name

跳到函数func_name

2) 看函数(identifier)

想进入光标所在的函数,用

CTRL + ]

3) 回退

回退用 CTRL + T

1.3 内核版本的选取

本次论文分析,我选取的是linux-2.6.10版本的内核。最新的内核代码为2.6.25。但是现在主流的服务器都使用的是RedHat AS4的机器,它使

用2.6.9的内核。我选取2.6.10是因为它很接近2.6.9,现在红帽企业Linux 4以Linux2.6.9内核为基础,是最稳定、最强大的商业产品。在2004

年期间,Fedora等开源项目为Linux 2.6内核技术的更加成熟提供了一个环境,这使得红帽企业Linux v.4内核可以提供比以前版本更多更好的

功能和算法,具体包括:

? 通用的逻辑CPU调度程序:处理多内核和超线程CPU。

? 基于对象的逆向映射虚拟内存:提高了内存受限系统的性能。

? 读复制更新:针对操作系统数据结构的SMP算法优化。

? 多I/O调度程序:可根据应用环境进行选择。

? 增强的SMP和NUMA支持:提高了大型服务器的性能和可扩展性。

? 网络中断缓和(NAPI):提高了大流量网络的性能。

Linux 2.6 内核使用了许多技术来改进对大量内存的使用,使得Linux 比以往任何时候都更适用于企业。包括反向映射(reverse mapping)

、使用更大的内存页、页表条目存储在高端内存中,以及更稳定的管理器。因此,我选取linux-2.6.10内核版本作为分析对象。

二. X86的硬件寻址方法

请参考Intel x86手册^_^

三. 内核对页表的设置

CPU做出映射的前提是操作系统要为其准备好内核页表,而对于页表的设置,内核在系统启动的初期和系统初始化完成后都分别进行了设置。

3.1 与内存映射相关的几个宏

这几个宏把无符号整数转换成对应的类型

#define __pte(x) ((pte_t) { (x) } )

#define __pmd(x) ((pmd_t) { (x) } )

#define __pgd(x) ((pgd_t) { (x) } )

#define __pgprot(x) ((pgprot_t) { (x) } )

根据x把它转换成对应的无符号整数

#define pte_val(x) ((x).pte_low)

#define pmd_val(x) ((x).pmd)

#define pgd_val(x) ((x).pgd)

#define pgprot_val(x) ((x).pgprot)

把内核空间的线性地址转换为物理地址

#define __pa(x) ((unsigned long)(x)-PAGE_OFFSET)

把物理地址转化为线性地址

#define __va(x) ((void *)((unsigned long)(x)+PAGE_OFFSET))

x是页表项值,通过pte_pfn得到其对应的物理页框号,最后通过pfn_to_page得到对应的物理页描述符

#define pte_page(x) pfn_to_page(pte_pfn(x))

如果对应的表项值为0,返回1

#define pte_none(x) (!(x).pte_low)

x是页表项值,右移12位后得到其对应的物理页框号

#define pte_pfn(x) ((unsigned long)(((x).pte_low >> PAGE_SHIFT)))

根据页框号和页表项的属性值合并成一个页表项值

#define pfn_pte(pfn, prot) __pte(((pfn) << PAGE_SHIFT) | pgprot_val(prot))

根据页框号和页表项的属性值合并成一个中间表项值

#define pfn_pmd(pfn, prot) __pmd(((pfn) << PAGE_SHIFT) | pgprot_val(prot))

向一个表项中写入指定的值

#define set_pte(pteptr, pteval) (*(pteptr) = pteval)

#define set_pte_atomic(pteptr, pteval) set_pte(pteptr,pteval)

#define set_pmd(pmdptr, pmdval) (*(pmdptr) = pmdval)

#define set_pgd(pgdptr, pgdval) (*(pgdptr) = pgdval)

根据线性地址得到高10位值,也就是在目录表中的索引

#define pgd_index(address) (((address)>>PGDIR_SHIFT) & (PTRS_PER_PGD-1))

根据页描述符和属性得到一个页表项值

#define mk_pte(page, pgprot) pfn_pte(page_to_pfn(page), (pgprot))

3.2内核页表的初始化

内核在进入保护模式前,还没有启用分页功能,在这之前内核要先建立一个临时内核页表,因为在进入保护模式后,内核继续初始化直到建

立完整的内存映射机制之前,仍然需要用到页表来映射相应的内存地址。临时页表的初始化是在arch/i386/kernel/head.S中进行的:

swapper_pg_dir是临时页全局目录表,它是在内核编译过程中静态初始化的.

pg0是第一个页表开始的地方,它也是内核编译过程中静态初始化的.

内核通过以下代码建立临时页表:

ENTRY(startup_32)

…………

/* 得到开始目录项的索引,从这可以看出内核是在swapper_pg_dir的768个表项开始进行建立的,其对应的线性地址就是0xc0000000以上的地

址,也就是内核在初始化它自己的页表*/

page_pde_offset = (__PAGE_OFFSET >> 20);

/* pg0地址在内核编译的时候,已经是加上0xc0000000了,减去0xc00000000得到对应的物理地址*/

movl $(pg0 - __PAGE_OFFSET), %edi

/* 将目录表的地址传给edx,表明内核也要从0x00000000开始建立页表,这样可以保证从以物理地址取指令到以线性地址在系统空间取指令

的平稳过渡,下面会详细解释*/

movl $(swapper_pg_dir - __PAGE_OFFSET), %edx

movl $0x007, %eax

leal 0x007(%edi),%ecx

Movl %ecx,(%edx)

movl %ecx,page_pde_offset(%edx)

addl $4,%edx

movl $1024, %ecx

11:

stosl addl $0x1000,%eax

loop 11b

/* 内核到底要建立多少页表,也就是要映射多少内存空间,取决于这个判断条件。在内核初始化程中内核只要保证能映射到包括内

核的代码段,数据段,初始页表和用于存放动态数据结构的128k大小的空间就行*/

leal (INIT_MAP_BEYOND_END+0x007)(%edi),%ebp

cmpl %ebp,%eax

jb 10b

movl %edi,(init_pg_tables_end - __PAGE_OFFSET)

在上述代码中,内核为什么要把用户空间和内核空间的前几个目录项映射到相同的页表中去呢,虽然在head.S中内核已经进入保护模式,但是

内核现在是处于保护模式的段式寻址方式下,因为内核还没有启用分页映射机制,现在都是以物理地址来取指令,如果代码中遇到了符号地址

,只能减去0xc0000000才行,当开启了映射机制后就不用了现在cpu中的取指令指针eip 仍指向低区,如果只建立内核空间中的映射,那么当

内核开启映射机制后,低区中的地址就没办法寻址了,应为没有对应的页表,除非遇到某个符号地址作为绝对转移或调用子程序为止。因此

要尽快开启CPU的页式映射机制.

movl $swapper_pg_dir-__PAGE_OFFSET,%eax

movl %eax,%cr3 /* cr3控制寄存器保存的是目录表地址*/

movl %cr0,%eax /* 向cr0的最高位置1来开启映射机制*/

orl $0x80000000,%eax

movl %eax,%cr0

ljmp $__BOOT_CS,$1f /* Clear prefetch and normalize %eip */

1:

lss stack_start,%esp

通过ljmp $__BOOT_CS,$1f这条指令使CPU进入了系统空间继续执行因为__BOOT_CS是个符号地址,地址在0xc0000000以上。

在head.S完成了内核临时页表的建立后,它继续进行初始化,包括初始化INIT_TASK,也就是系统开启后的第一个进程;建立完整的中断处理程

序,然后重新加载GDT描述符,最后跳转到init/main.c中的start_kernel函数继续初始化.

3.3内核页表的完整建立

内核在start_kernel()中继续做第二阶段的初始化,因为在这个阶段中,内核已经处于保护模式下,前面只是简单的设置了内核页表,内核

必须首先要建立一个完整的页表才能继续运行,因为内存寻址是内核继续运行的前提。pagetable_init()的代码在mm/init.c中:

[start_kernel()>setup_arch()>paging_init()>pagetable_init()]

为了简单起见,我忽略了对PAE选项的支持。

static void __init pagetable_init (void)

{

……

pgd_t *pgd_base = swapper_pg_dir;

……

kernel_physical_mapping_init(pgd_base);

……

}

在这个函数中pgd_base变量指向了swapper_pg_dir,这正是内核目录表的开始地址,pagetable_init()函数在通过

kernel_physical_mapping_init()函数完成内核页表的完整建立。

kernel_physical_mapping_init函数同样在mm/init.c中,我略去了与PAE模式相关的代码: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;

……

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));

……

}

}

通过作者的注释,可以了解到这个函数的作用是把整个物理内存地址都映射到从内核空间的开始地址,即从0xc0000000的整个内核空间中,

直到物理内存映射完毕为止。这个函数比较长,而且用到很多关于内存管理方面的宏定义,理解了这个函数,就能大概理解内核是如何建立

页表的,将这个抽象的模型完全的理解。下面将详细分析这个函数:

函数开始定义了4个变量pgd_t *pgd,pmd_t *pmd,pte_t *pte,pfn;

pgd指向一个目录项开始的地址,pmd指向一个中间目录开始的地址,pte指向一个页表开始的地址pfn是页框号被初始为0. pgd_idx根据

pgd_index宏计算结果为768,也是内核要从目录表中第768个表项开始进行设置。从768到1024这个256个表项被linux内核设置成内核目录项,

低768个目录项被用户空间使用. pgd = pgd_base + pgd_idx; pgd便指向了第768个表项。

然后函数开始一个循环即开始填充从768到1024这256个目录项的内容。

one_md_table_init()函数根据pgd找到指向的pmd表。

它同样在mm/init.c中定义:

static pmd_t * __init one_md_table_init(pgd_t *pgd)

{

pmd_t *pmd_table;

#ifdef CONFIG_X86_PAE

pmd_table = (pmd_t *) alloc_bootmem_low_pages(PAGE_SIZE);

set_pgd(pgd, __pgd(__pa(pmd_table) | _PAGE_PRESENT));

if (pmd_table != pmd_offset(pgd, 0))

BUG();

#else

pmd_table = pmd_offset(pgd, 0);

#endif

return pmd_table;

}

可以看出,如果内核不启用PAE选项,函数将通过pmd_offset返回pgd的地址。因为linux 的二级映射模型,本来就是忽略pmd中间目录表的。

接着又个判断语句:

>> if (pfn >= max_low_pfn)

>> continue;

这个很关键,max_low_pfn代表着整个物理内存一共有多少页框。当pfn大于max_low_pfn 的时候,表明内核已经把整个物理内存都映射到了系

统空间中,所以剩下有没被填充的表项就直接忽略了。因为内核已经可以映射整个物理空

间了,没必要继续填充剩下的表项。

紧接着的第2个for循环,在linux的3级映射模型中,是要设置pmd表的,但在2级映射中忽略,只循环一次,直接进行页表pte的设置。

>> address = pfn * PAGE_SIZE + PAGE_OFFSET;

address是个线性地址,根据上面的语句可以看出address是从0xc000000开始的,也就是从内核空间开始,后面在设置页表项属性的时候会用

到它.

>> pte = one_page_table_init(pmd);

根据pmd分配一个页表, 代码同样在mm/init.c中:

static pte_t * __init one_page_table_init(pmd_t *pmd)

{

if (pmd_none(*pmd)) {

pte_t *page_table = (pte_t *) alloc_bootmem_low_pages(PAGE_SIZE);

set_pmd(pmd, __pmd(__pa(page_table) | _PAGE_TABLE));

if (page_table != pte_offset_kernel(pmd, 0))

BUG();

return page_table;

}

return pte_offset_kernel(pmd, 0);

}

pmd_none宏判断pmd表是否为空, 如果为空则要利用alloc_bootmem_low_pages分配一个4k大小的物理页面。然后通过set_pmd(pmd, __pmd

(__pa(page_table) | _PAGE_TABLE));来设置pmd表项。page_table显然属于线性地址,先通过__pa宏转化为物理地址,在与上_PAGE_TABLE宏,

此时它们还是无符号整数,在通过__pmd把无符号整数转化为pmd类型,经过这些转换,就得到了一个具有属性的表项,然后通过set_pmd宏设

置pmd表项.

接着又是一个循环,设置1024个页表项。

is_kernel_text函数根据前面提到的address来判断address线性地址是否属于内核代码段,它同样在mm/init.c中定义:

static inline int is_kernel_text(unsigned long addr)

{

if (addr >= (unsigned long)_stext && addr <= (unsigned long)__init_end)

return 1;

return 0;

}

_stext, __init_end是个内核符号,在内核链接的时候生成的,分别表示内核代码段的开始和终止地址.

如果address属于内核代码段,那么在设置页表项的时候就要加个PAGE_KERNEL_EXEC 属性,如果不是,则加个PAGE_KERNEL属性.

#define _PAGE_KERNEL_EXEC \

(_PAGE_PRESENT | _PAGE_RW | _PAGE_DIRTY | _PAGE_ACCESSED)

#define _PAGE_KERNEL \

(_PAGE_PRESENT | _PAGE_RW | _PAGE_DIRTY | _PAGE_ACCESSED | _PAGE_NX)

最后通过set_pte(pte, pfn_pte(pfn, PAGE_KERNEL));来设置页表项,先通过pfn_pte宏根据页框号和页表项的属性值合并成一个页表项值,

然户在用set_pte宏把页表项值写到页表项里。

当pagetable_init()函数返回后,内核已经设置好了内核页表,紧着调用load_cr3(swapper_pg_dir);

#define load_cr3(pgdir) \

asm volatile("movl %0,%%cr3": :"r" (__pa(pgdir)))

将控制swapper_pg_dir送入控制寄存器cr3. 每当重新设置cr3时,CPU就会将页面映射目录所在的页面装入CPU内部高速缓存中的TLB部分. 现

在内存中(实际上是高速缓存中)的映射目录变了,就要再让CPU装入一次。由于页面映射机制本来就是开启着的,所以从这条指令以后就扩大

了系统空间中有映射区域的大小, 使整个映射覆盖到整个物理内存(高端内存)除外. 实际上此时swapper_pg_dir中已经改变的目录项很可能还

在高速缓存中,所以还要通过__flush_tlb_all()将高速缓存中的内容冲刷到内存中,这样才能保证内存中映射目录内容的一致性。

3.4 对如何构建页表的总结

通过上述对pagetable_init()的剖析,我们可以清晰的看到,构建内核页表,无非就是向相应的表项写入下一级地址和属性。在内核空间

保留着一部分内存专门用来存放内核页表.当cpu要进行寻址的时候,无论在内核空间,还是在用户空间,都会通过这个页表来进行映射。对于

这个函数,内核把整个物理内存空间都映射完了,当用户空间的进程要使用物理内存时,岂不是不能做相应的映射了?其实不会的,内核

只是做了映射,映射不代表使用,这样做是内核为了方便管理内存而已。

四. 实例分析映射机制

4.1示例代码

通过前面的理论分析,我们通过编写一个简单的程序,来分析内核是如何把线性地址映射到物理地址的。

[root@localhost temp]# cat test.c

#include

void test(void)

{

printf("hello, world.\n");

}

int main(void)

{

test();

}

这段代码很简单,我们故意要main调用test函数,就是想看下test函数的虚拟地址是如何映射成物理地址的。

4.2 段式映射分析

我们先编译,在反汇编下test文件

[root@localhost temp]# gcc -o test test.c

[root@localhost temp]# objdump -d test

08048368 :

8048368: 55 push %ebp

8048369: 89 e5 mov %esp,%ebp

804836b: 83 ec 08 sub $0x8,%esp

804836e: 83 ec 0c sub $0xc,%esp

8048371: 68 84 84 04 08 push $0x8048484

8048376: e8 35 ff ff ff call 80482b0

804837b: 83 c4 10 add $0x10,%esp

804837e: c9 leave

804837f: c3 ret

08048380

:

8048380: 55 push %ebp

8048381: 89 e5 mov %esp,%ebp

8048383: 83 ec 08 sub $0x8,%esp

8048386: 83 e4 f0 and $0xfffffff0,%esp

8048389: b8 00 00 00 00 mov $0x0,%eax

804838e: 83 c0 0f add $0xf,%eax

8048391: 83 c0 0f add $0xf,%eax

8048394: c1 e8 04 shr $0x4,%eax

8048397: c1 e0 04 shl $0x4,%eax

804839a: 29 c4 sub %eax,%esp

804839c: e8 c7 ff ff ff call 8048368

80483a1: c9 leave

80483a2: c3 ret

80483a3: 90 nop

从上述结果可以看到, ld给test()函数分配的地址为0x08048368.在elf格式的可执行文件代码中,ld的实际位置总是从0x8000000开始安排程序

的代码段,对每个程序都是这样。至于程序在执行时在物理内存中的实际位置就要由内核在为其建立内存映射时临时做出安排,具体地址则

取决于当时所分配到的物理内存页面。假设该程序已经运行,整个映射机制都已经建立好,并且CPU正在执行main()中的call 8048368这条指

令,要转移到虚拟地址0x08048368去运行. 下面将详细介绍这个虚拟地址转换为物理地址的映射过程.

首先是段式映射阶段。由于0x08048368是一个程序的入口,更重要的是在执行的过程中是由CPU中的指令计数器EIP所指向的,所以在代码段中

。因此,i386CPU使用代码段寄存器CS的当前值作为段式映射的选择子,也就是用它作为在段描述表的下标.那么CS的值是多少呢?

用GDB调试下test:

(gdb) info reg

eax 0x10 16

ecx 0x1 1

edx 0x9d915c 10326364

ebx 0x9d6ff4 10317812

esp 0xbfedb480 0xbfedb480

ebp 0xbfedb488 0xbfedb488

esi 0xbfedb534 -1074940620

edi 0xbfedb4c0 -1074940736

eip 0x804836e 0x804836e

eflags 0x282 642

cs 0x73 115

ss 0x7b 123

ds 0x7b 123

es 0x7b 123

fs 0x0 0

gs 0x33 51

可以看到CS的值为0x73, 我们把它分解成二进制:

0000 0000 0111 0011

最低2位为3,说明RPL的值为3,应为我们这个程序本省就是在用户空间,RPL的值自然为3.

第3位为0表示这个下标在GDT中。

高13位为14,所以段描述符在GDT表的第14个表项中,我们可以到内核代码中去验证下:

在i386/asm/segment.h中:

#define GDT_ENTRY_DEFAULT_USER_CS 14

#define __USER_CS (GDT_ENTRY_DEFAULT_USER_CS * 8 + 3)

可以看到段描述符的确就是GDT表的第14个表项中。

我们去GDT表看看具体的表项值是什么,GDT的内容在arch/i386/kernel/head.S中定义: ENTRY(cpu_gdt_table)

.quad 0x0000000000000000 /* NULL descriptor */

.quad 0x0000000000000000 /* 0x0b reserved */

.quad 0x0000000000000000 /* 0x13 reserved */

.quad 0x0000000000000000 /* 0x1b reserved */

.quad 0x0000000000000000 /* 0x20 unused */

.quad 0x0000000000000000 /* 0x28 unused */

.quad 0x0000000000000000 /* 0x33 TLS entry 1 */

.quad 0x0000000000000000 /* 0x3b TLS entry 2 */

.quad 0x0000000000000000 /* 0x43 TLS entry 3 */

.quad 0x0000000000000000 /* 0x4b reserved */

.quad 0x0000000000000000 /* 0x53 reserved */

.quad 0x0000000000000000 /* 0x5b reserved */

.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 */

.quad 0x0000000000000000 /* 0x80 TSS descriptor */

.quad 0x0000000000000000 /* 0x88 LDT descriptor */

/* Segments used for calling PnP BIOS */

.quad 0x00c09a0000000000 /* 0x90 32-bit code */

.quad 0x00809a0000000000 /* 0x98 16-bit code */

.quad 0x0080920000000000 /* 0xa0 16-bit data */

.quad 0x0080920000000000 /* 0xa8 16-bit data */

.quad 0x0080920000000000 /* 0xb0 16-bit data */

/*

* The APM segments have byte granularity and their bases

* and limits are set at run time.

*/

.quad 0x00409a0000000000 /* 0xb8 APM CS code */

.quad 0x00009a0000000000 /* 0xc0 APM CS 16 code (16 bit) */

.quad 0x0040920000000000 /* 0xc8 APM DS data */

.quad 0x0000000000000000 /* 0xd0 - unused */

.quad 0x0000000000000000 /* 0xd8 - unused */

.quad 0x0000000000000000 /* 0xe0 - unused */

.quad 0x0000000000000000 /* 0xe8 - unused */

.quad 0x0000000000000000 /* 0xf0 - unused */

.quad 0x0000000000000000 /* 0xf8 - GDT entry 31: double-fault TSS */

.quad 0x00cffa000000ffff /* 0x73 user 4GB code at 0x00000000 */

我们把这个值展开成二进制:

0000 0000 1100 1111 1111 1010 0000 0000 0000 0000 0000 0000 1111 1111 1111 1111

根据上述对段描述符表项值的描述,可以得出如下结论:

B0-B15, B16-B31是0,表示基地址全为0.

L0-L15, L16-L19是1,表示段的上限全是0xffff.

G位是1 表示段长度单位均为4KB。

D位是1 表示对段的访问都是32位指令

P位是1 表示段在内存中。

DPL是3 表示特权级是3级

S位是1 表示为代码段或数据段

type为1010 表示代码段,可读,可执行,尚未收到访问

这个描述符指示了段从0地址开始的整个4G虚存空间,逻辑地址直接转换为线性地址。

所以在经过段式映射后就把逻辑地址转换成了线性地址,这也是在linux中,为什么逻辑地址等同于线性地址的原因了。

4.3 页式映射分析

现在进入页式映射的过程了, Linux系统中的每个进程都有其自身的页面目录PGD, 指向这个目录的指针保存在每个进程的mm_struct数据结构

中。每当调度一个进程进入运行的时候,内核都要为即将运行的进程设置好控制寄存器cr3,而MMU的硬件则总是从cr3中取得指向当前页面目

录的指针。当我们在程序中要转移到地址0x08048368去的时候,进程正在运行,cr3早以设置好,指向我们这个进程的页面目录了。先将线性

地址0x08048368展开成二进制:

0000 1000 0000 0100 1000 0011 0110 1000

对照线性地址的格式,可见最高10位为二进制的0000 1000 00, 也就是十进制的32,所以

MMU就以32为下标在其页面目录中找到其目录项。这个

目录项的高20位指向一个页面表,CPU在这20位后添上12个0就得到页面表的指针。找到页面表以后,CPU再来看线性地址中的中间10位,

0001001000,即十进制的72.于是CPU就以此为下标在页表中找相应的表项。表项值的高20位指向一个物理内存页面,在后边添上12个0就得到物

理页面的开始地址。假设物理地址在0x620000的,线性地址的最低12位为0x368. 那么test()函数的入口地址就为0x620000+0x368 = 0x620368

6. 4 地址映射机制

顾名思义地址映射就是建立几种存储媒介(内存,辅存,虚存)间的关联,完成地址间的相互转换,它既包括磁盘文件到虚拟内存的映射,也包括虚拟内存到物理内存的映射,如图6.

13 所示。本节主要讨论磁盘文件到虚拟内存的映射,虚拟内存到物理内存的映射实际上是请页机制完成的( 请看下节) 。

图6.13 存储介质间的映射关系

6.4.1 描述虚拟空间的数据结构

前几节介绍的数据结构如存储节点(node )、管理区(zone )、页面(page )及空闲区(free_area )都用于物理空间的管理。这一节主要关注虚拟空间的管理。虚拟空间的管理是以进程为基础的,每个进程都有各自的虚存空间(或叫用户空间,地址空间),除此之外,每个进程的“内核空间”是为所有的进程所共享的。

一个进程的虚拟地址空间主要由两个数据结来描述。一个是最高层次的:mm_struct ,一个是较高层次的:vm_area_structs 。最高层次的mm_struct 结构描述了一个进程的整个虚拟地址空间。较高层次的结构vm_area_truct 描述了虚拟地址空间的一个区间(简称虚拟区)。

1. MM_STRUCT 结构

mm_strcut 用来描述一个进程的虚拟地址空间,在/include/linux/sched.h 中描述如下:struct mm_struct {

struct vm_area_struct * mmap; /* 指向虚拟区间(VMA )链表*/

rb_root_t mm_rb; /* 指向red_black 树*/

struct vm_area_struct * mmap_cache; /* 指向最近找到的虚拟区间*/

pgd_t * pgd; /* 指向进程的页目录*/

atomic_t mm_users; /* 用户空间中的有多少用户*

/

atomic_t mm_count; /* 对"struct mm_struct" 有多少引用*

/

int map_count; /* 虚拟区间的个数*/

struct rw_semaphore mmap_sem;

spinlock_t page_table_lock; /* 保护任务页表和mm->rss * /

struct list_head mmlist; /* 所有活动(active )mm 的链表*/ unsigned long start_code, end_code, start_data, end_data;

unsigned long start_brk, brk, start_stack;

unsigned long arg_start, arg_end, env_start, env_end;

unsigned long rss, total_vm, locked_vm;

unsigned long def_flags;

unsigned long cpu_vm_mask;

unsigned long swap_address;

unsigned dumpable:1;

/* Architecture-specific MM context */

mm_context_t context;

};

对该结构进一步说明如下:

·在内核代码中,指向这个数据结构的变量常常是mm 。

·每个进程只有一个mm_struct 结构,在每个进程的task_struct 结构中,有一个指向该进程的结构。可以说,mm_struct 结构是对整个用户空间的描述。

·一个进程的虚拟空间中可能有多个虚拟区间(参见下面对vm_area_struct 描述),对这些虚拟区间的组织方式有两种,当虚拟区较少时采用单链表,由mmap 指

针指向这个链表,当虚拟区间多时采用“红黑树(red_black tree )”结构,由m

m_rb 指向这颗树。在2.4.10 以前的版本中,采用的是AVL 树,因为与AVL

树相比,对红黑树进行操作的效率更高。

·因为程序中用到的地址常常具有局部性,因此,最近一次用到的虚拟区间很可能下一次还要用到,因此,把最近用到的虚拟区间结构应当放入高速缓存,这个虚

拟区间就由mmap_cache 指向。

·指针pgt 指向该进程的页目录(每个进程都有自己的页目录,注意同内核页目录的区别), 当调度程序调度一个程序运行时,就将这个地址转成物理地址,并写

入控制寄存器(CR3 )。

·由于进程的虚拟空间及其下属的虚拟区间有可能在不同的上下文中受到访问,而这些访问又必须互斥,所以在该结构中设置了用于P 、V 操作的信号量mmap_

sem 。此外,page_table_lock 也是为类似的目的而设置。

·虽然每个进程只有一个虚拟地址空间,但这个地址空间可以被别的进程来共享,如,子进程共享父进程的地址空间(也即共享mm_struct 结构)。所以,用mm

_user 和mm_count 进行计数。类型atomic_t 实际上就是整数,但对这种整数的

操作必须是“原子”的。

·另外,还描述了代码段、数据段、堆栈段、参数段以及环境段的起始地址和结束地址。这里的段是对程序的逻辑划分,与我们前面所描述的段机制是不同的。

·mm_context_t 是与平台相关的一个结构,对i386 几乎用处不大。

在后面对代码的分析中对有些域给予进一步说明。

2. VM_AREA_STRUCT 结构

vm_area_struct 描述进程的一个虚拟地址区间,在/include/linux/mm.h 中描述如下:

struct vm_area_struct

struct mm_struct * vm_mm; /* 虚拟区间所在的地址空间*/

unsigned long vm_start; /* 在vm_mm 中的起始地址*/

unsigned long vm_end; /* 在vm_mm 中的结束地址*/

/* linked list of VM areas per task, sorted by address */

struct vm_area_struct *vm_next;

pgprot_t vm_page_prot; /* 对这个虚拟区间的存取权限*/

unsigned long vm_flags; /* 虚拟区间的标志. */

rb_node_t vm_rb;

/*

* For areas with an address space and backing store,

* one of the address_space->i_mmap{,shared} lists,

* for shm areas, the list of attaches, otherwise unused.

*/

struct vm_area_struct *vm_next_share;

struct vm_area_struct **vm_pprev_share;

/* 对这个区间进行操作的函数*/

struct vm_operations_struct * vm_ops;

/* Information about our backing store: */

unsigned long vm_pgoff; /* Offset (within vm_file) in PAGE_SIZE

units , *not* PAGE_CACHE_SIZE */

struct file * vm_file; /* File we map to (can be NULL). */

unsigned long vm_raend; /* XXX: put full readahead info here. */ void * vm_private_data; /* was vm_pte (shared mem) */

};

vm_flag 是描述对虚拟区间的操作的标志,其定义和描述如下:

表6.1 虚拟区间的标志

标志名描述

VM_DENYWRITE 在这个区间映射一个打开后不能用来写的文件。

VM_EXEC 页可以被执行。

VM_EXECUTABLE 页含有可执行代码。

VM_GROWSDOWN 这个区间可以向低地址扩展。

VM_GROWSUP 这个区间可以向高地址扩展。

VM_IO 这个区间映射一个设备的I/O 地址空间。

VM_LOCKED 页被锁住不能被交换出去。

VM_MAYEXEC VM_EXEC 标志可以被设置。

VM_MAYREAD VM_READ 标志可以被设置。

VM_MAYSHARE VM_SHARE 标志可以被设置。

VM_MAYWRITE VM_WRITE 标志可以被设置。

VM_READ 页是可读的。

VM_SHARED 页可以被多个进程共享。

VM_ SHM 页用于IPC 共享内存。

VM_WRITE 页是可写的。

较高层次的结构vm_area_structs 是由双向链表连接起来的,它们是按虚地址的降顺序来排列的,每个这样的结构都对应描述一个相邻的地址空间范围。之所以这样分割,是因为每个虚拟区间可能来源不同,有的可能来自可执行映象,有的可能来自共享库,而有的则可能是动态分配的内存区,所以对每一个由vm_area_structs 结构所描述的区间的处理操作和它前后范围的处理操作不同。因此Linux 把虚拟内存分割管理,并利用了虚拟内存处理例程(vm_ops )来抽象对不同来源虚拟内存的处理方法。不同的虚拟区间其处理操作可能不同,Linux 在这里利用了面向对象的思想,即把一个虚拟区间看成一个对象,用vm_are a_structs 描述了这个对象的属性,其中的vm_operation 结构描述了在这个对象上的操作,其定义在/include /linux /mm.h 中:

/*

* These are the virtual MM functions - opening of an area, closing and

* unmapping it (needed to keep files on disk up-to-date etc), pointer

* to the functions called when a no-page or a wp-page exception occurs.

*/

struct vm_operations_struct {

void (*open)(struct vm_area_struct * area);

void (*close)(struct vm_area_struct * area);

struct page * (*nopage)(struct vm_area_struct * area, unsigned long addre

ss, int unused);

};

vm_operations 结构中包含的是函数指针;其中,open 、close 分别用于虚拟区间的打开、关闭,而nopage 用于当虚存页面不在物理内存而引起的“缺页异常”时所应该调用的函数。如图6.14 给出虚拟区间的操作集。

linux内存管理子系统 笔记

4-4 linux内存管理子系统 4-4-1 linux内存管理(参考课件) 物理地址:cpu地址总线上寻址物理内存的地址信号,是地址变换的最终结果 逻辑地址:程序代码经过编译后,出现在汇编程序中的地址(程序设计时使用的地址) 线性地址:又名虚拟地址,32位cpu架构下4G地址空间 CPU要将一个逻辑地址转换为物理地址,需要两步: 1、首先CPU利用段式内存管理单元,将逻辑地址转换成线性地址; 2、再利用页式内存管理单元,把线性地址最终转换为物理地址 相关公式: 逻辑地址=段基地址+段内偏移量(段基地址寄存器+段偏移寄存器)(通用的) 16位CPU:逻辑地址=段基地址+段内偏移量(段基地址寄存器+段偏移寄存器) 线性地址=段寄存器的值×16+逻辑地址的偏移部分 物理地址=线性地址(没有页式管理) 32位CPU:逻辑地址=段基地址+段内偏移量(段基地址寄存器+段偏移寄存器) 线性地址=段寄存器的值+逻辑地址的偏移部分 物理地址<——>线性地址(mapping转换) ARM32位:逻辑地址=段基地址+段内偏移量(段基地址寄存器+段偏移寄存器) 逻辑地址=段内偏移量(段基地址为0) 线性地址=逻辑地址=段内偏移量(32位不用乘以32) 物理地址<——>线性地址(mapping转换) ************************!!以下都是x86模式下!!********************************* 一、段式管理 1.1、16位CPU:(没有页式管理) 1.1.1、段式管理的由来: 16位CPU内部有20位地址总线,可寻址2的20次方即1M的内存空间,但16位CPU 只有16位的寄存器,因此只能访问2的16次方即64K。因此就采用了内存分段的管理模式,在CPU内部加入了段寄存器,这样1M被分成若干个逻辑段,每个逻辑段的要求如下: 1、逻辑段的起始地址(段地址)必须是16的整数倍,即最后4个二进制位须全是0 (因此不必保存)。 2、逻辑段的最大容量为64K。 1.1.2、物理地址的形成方式: 段地址:将段寄存器中的数值左移4位补4个0(乘以16),得到实际的段地址。 段偏移:在段偏移寄存器中。 1)逻辑地址=段基地址+段内偏移量(段基地址寄存器+段偏移寄存器) 2)由逻辑地址得到物理地址的公式为:(因为没有页式管理,所以这一步就得到了物理地址)物理地址PA=段寄存器的值×16+逻辑地址的偏移部分(注意!!)(段与段可能会重叠)

操作系统内存管理复习过程

操作系统内存管理

操作系统内存管理 1. 内存管理方法 内存管理主要包括虚地址、地址变换、内存分配和回收、内存扩充、内存共享和保护等功能。 2. 连续分配存储管理方式 连续分配是指为一个用户程序分配连续的内存空间。连续分配有单一连续存储管理和分区式储管理两种方式。 2.1 单一连续存储管理 在这种管理方式中,内存被分为两个区域:系统区和用户区。应用程序装入到用户区,可使用用户区全部空间。其特点是,最简单,适用于单用户、单任务的操作系统。CP/M和 DOS 2.0以下就是采用此种方式。这种方式的最大优点就是易于管理。但也存在着一些问题和不足之处,例如对要求内

存空间少的程序,造成内存浪费;程序全部装入,使得很少使用的程序部分也占用—定数量的内存。 2.2 分区式存储管理 为了支持多道程序系统和分时系统,支持多个程序并发执行,引入了分区式存储管理。分区式存储管理是把内存分为一些大小相等或不等的分区,操作系统占用其中一个分区,其余的分区由应用程序使用,每个应用程序占用一个或几个分区。分区式存储管理虽然可以支持并发,但难以进行内存分区的共享。 分区式存储管理引人了两个新的问题:内碎片和外碎片。 内碎片是占用分区内未被利用的空间,外碎片是占用分区之间难以利用的空闲分区(通常是小空闲分区)。 为实现分区式存储管理,操作系统应维护的数据结构为分区表或分区链表。表中各表项一般包括每个分区的起始地址、大小及状态(是否已分配)。

分区式存储管理常采用的一项技术就是内存紧缩(compaction)。 2.2.1 固定分区(nxedpartitioning)。 固定式分区的特点是把内存划分为若干个固定大小的连续分区。分区大小可以相等:这种作法只适合于多个相同程序的并发执行(处理多个类型相同的对象)。分区大小也可以不等:有多个小分区、适量的中等分区以及少量的大分区。根据程序的大小,分配当前空闲的、适当大小的分区。 优点:易于实现,开销小。 缺点主要有两个:内碎片造成浪费;分区总数固定,限制了并发执行的程序数目。 2.2.2动态分区(dynamic partitioning)。 动态分区的特点是动态创建分区:在装入程序时按其初始要求分配,或在其执行过程中通过系统调用进行分配或改变分区大小。与固定分区相比较其优点是:没有内碎

linux内核之内存管理

Linux内核之内存管理 作者:harvey wang 邮箱:harvey.perfect@https://www.sodocs.net/doc/a56787579.html, 新浪博客地址:https://www.sodocs.net/doc/a56787579.html,/harveyperfect,有关于减肥和学习英语相关的博文,欢迎交流 把linux内存管理分为下面四个层面 (一)硬件辅助的虚实地址转换 (二)内核管理的内存相关 (三)单个进程的内存管理 (四)malloc软件 (一)处理器硬件辅助的虚实地址转换(以x86为例) 在x86中虚实地址转换分为段式转换和页转换。段转换过程是由逻辑地址(或称为虚拟地址)转换为线性地址;页转换过程则是将线性地址转换为物理地址。段转换示意图如下 X86支持两种段,gdt和ldt(全局描述段表和局部描述符段表),在linux中只使用了4个全局描述符表,内核空间和用户空间分别两个gdt,分别对应各自的代码段和数据段。也可以认为在linux中变相地disable了x86的段式转换功能。 页转换示意图如下

在linux中x86 的cr3寄存器(页表基地址寄存器)保存在进程的上下文中,在进程切换时会保存或回复该寄存器的内容,这样每个进程都有自己的转换页表,从而保证了每个进程有自己的虚拟空间。 (二)内核管理的内存相关 从几个概念展开内存管理:node、zone、buddy、slab 1、Node SGI Altix3000系统的两个结点 如上图,NUMA系统的结点通常是由一组CPU(如,SGI Altix 3000是2个Itanium2 CPU)和本地内存组成。由于每个结点都有自己的本地内存,因此全系统的内存在物理上是分布的,每个结点访问本地内存和访问其它结点的远地内存的延迟是不同的,为了优化对NUMA 系统的支持,引进了Node 来将NUMA 物理内存进行划分为不同的Node。而操作系统也必须能感知硬件的拓扑结构,优化系统的访存。

linux复习题

一单选题 1.最初开发了Linux系统的是() A.Andrew S. Tanwnbaum B.Linus Torvalds C.Ken Thompson D.Dennis Ritchie 2.linux操作系统内核创始人是() A.Bill Gates B.Richard Stallman C.Linus Torvalds D.Dennis Ritchie 3.linux操作系统下有很多应用软件,其中大部分软件包括linux本身属于() A.商业软件 B. 共享软件 C.自由软件 D.其他类型软件 4.Linux系统是一个什么样的操作系统() A.单用户、单任务B.单用户、多任务 C.多用户、单任务D.多用户、多任务 5.Linux 核心的许可证是什么() A.NDA B.GDP C.GPL D.GNU 6.若要将鼠标从VM中释放出来,可按什么键来实现() A. Ctrl + Alt B. Ctrl +Alt +Del C. Ctrl +Alt +Enter D Ctrl +Enter 7.用"rm -i",系统会提示什么来让你确认() A. 命令行的每个选项 B. 是否真的删除 C. 是否有写的权限 D. 文件的位置 8.下列提法中,不属于ifconfig命令作用范围的是() A 配置本地回环地址 B 配置网卡的IP地址 C 激活网络适配器 D 加载网卡到内核中 9.下列文件中,包含了主机名到IP地址的映射关系的文件是() A /etc/HOSTNAME B /etc/hosts C /etc/resolv.conf D /etc/networks 10.在shell中变量的赋值有四种方法,其中,采用name=12的方法称() A 直接赋值B使用read命令 C 使用命令行参数D使用命令的输出 11.显示文件的头部的命令是() A.fdisk B.mount C.head D.man 12.删除不需要的文件的命令是() A.mkdir B.rm C.mv D.remove 13.Linux的根分区的文件系统类型是() A.FAT16 B.FAT32 C.ext3 D.NTFS 14.登录后希望重新加载fstab文件中的所有条目,我们可以以root身份执行哪个命令 () A.mount –d B.mount –c C.mount –a D.mount -b 15.下面不具备循环功能的语句是() A.if B.for C.while D.until 16.内核不包括的子系统是() A 进程管理系统 B 内存管理系统 C 文件管理系统D硬件管理系统 17.对名为fido的文件用chmod 551 fido 进行了修改,则它的许可权是() A -rwxr-xr-x B -rwxr--r-- C -r--r--r-- D -r-xr-x--x

Solaris 8内存管理机制研究

Solaris 8内存管理机制研究 吴海燕 戚丽 冯珂 摘 要:寻找性能瓶颈是性能分析中的一项重要任务,内存瓶颈的表现并不像CPU或磁盘那样直接,本文通过对Solaris 8内存管理机制的研究,给出了寻找Solaris 8系统内存瓶颈的方法。 关键词:Solaris 8,内存管理,性能优化 一、问题的提出 清华大学计算机与信息管理中心数据中心现有服务器近百台,其中包括了SUN Fire 15000、SUN Enterprise 5500、SUN Enterprise 5000等大型SUN服务器,Solaris 8是主流操作系统。为了对服务器的资源(如CPU、内存、磁盘、网络)的使用情况进行长期监控,建立性能优化(performance tuning)的基准值,我们开发了一套脚本程序定时采集系统运行参数。在长期的监控中,我们发现Solaris 8系统的空闲内存(freemem)呈现一个有趣的变化规律,如图1所示: 图1 空闲内存(freemem)变化图 图1是某Solaris 8系统(在下文中我们称之为15k-a)自2003年2月份以来的freemem 变化情况,横坐标是时间,纵坐标是freemem的数量,以8K字节为单位。15k-a配置是10路Super SPARCIII CPU,10GB物理内存。从上图可以看到在正常运行时,freemem应该是比较稳定的,15k-a主要是运行数据库,数据库在运行时会占用2G内存作为SGA区使用,因此在通常的负载下,freemem保持在6~7G之间是比较正常的。稳定一段时间后,

15k-a的freemem会持续走低,直到最低值,约为18893×8KMB,然后系统开始回收内存,我们就会看到freemem数量急剧上升。freemem的陡降都发生在凌晨1:00之后,检查系统作业发现每天1:00都会有一个数据库备份脚本开始运行:首先是用“exp”命令给数据库做逻辑备份,然后用“cp”命令把备份出来的文件拷贝到后备存储上。这两个命令都是正常退出,没有任何报错。开始时我们曾怀疑是有内存泄漏,当某一天freemem大幅攀升时,此怀疑被解除了,因为如果有内存泄漏,系统是无法将内存回收回来的。 对于一个物理内存为10GB的系统来说,如果空闲内存(freemem)真的减少到不到二百兆,那将存在着严重的问题。但奇怪的是系统的CPU使用率一直很低,所有进程的反应也很快,系统没有任何资源匮乏的迹象。如何解释这些问题呢,为此我们对Solaris 2.x 的内存管理机制进行了研究。 二、Solaris的内存管理机制 Solaris 8的内存管理为虚拟内存管理。[1]简单地说,虚拟内存就是进程看到比它实际使用的物理内存多得多的内存空间,对于64位的Solaris 8操作系统,进程可以通过8K 大小的段寻址访问2的64次方字节的内存空间,这种8K的段被称为页(page)。传统的UNIX通过进程(pagedaemon)完成虚拟地址和物理地址间的转换,在Solaris中这些是通过一个硬件-MMU(Memory Management Unit)-来实现的。在多处理器系统中,每个CPU 都有自己的MMU。Solaris 8的虚拟存储体系由系统寄存器、CPU CACHE、主存(RAM,物理内存)、外存(磁盘、磁带等)构成。 有两个基本的虚拟内存系统管理模型[2]:交换(swapping)和按需换页(demand paged)模型。交换模型的内存管理粒度是用户进程,当内存不足时,最不活跃的进程被交换出内存(swapping out)。按需换页模型的内存管理粒度是页(page),当内存匮乏时,只有最不经常使用的页被换出。Solaris 8结合使用了这两种内存管理模型,在通常情况下使用按需换页模型,当内存严重不足时,使用交换模型来进行内存释放。 与传统UNIX系统相比,Solaris虚拟内存系统的功能要丰富得多,它负责管理所有与I/O和内存相关的对象,包括内核、用户应用程序、共享库和文件系统。传统的UNIX系统V(System V)使用一个单独的缓冲区来加速文件系统的I/O, Solaris 8则使用虚拟内存系统来管理文件系统的缓存,系统的所有空闲内存都可以被用来做为文件I/O缓存,因为RAM的访问速度比磁盘快得多,所以这样做带来的性能提高是可观的。这也意味着在存在大量文件系统I/O的系统上,空闲内存的数量几乎是0。 了解系统内存被分配到了什么地方,系统在什么情况下进行内存整理是系统管理的重

JVM原理以及JVM内存管理机制

一、 JVM简介 JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。JVM工作原理和特点主要是指操作系统装入JVM是通过jdk中Java.exe来完成, 首先来说一下JVM工作原理中的jdk这个东西, .JVM 在整个jdk中处于最底层,负责于操作系统的交互,用来屏蔽操作系统环境,提供一个完整的Java运行环境,因此也就虚拟计算机. 操作系统装入JVM是通过jdk中Java.exe来完成。 通过下面4步来完成JVM环境. 1.创建JVM装载环境和配置 2.装载JVM.dll 3.初始化JVM.dll并挂界到JNIENV(JNI调用接口)实例 4.调用JNIEnv实例装载并处理class类。 对于JVM自身的物理结构,我们可以从下图了解:

JVM的一个重要的特征就是它的自动内存管理机制,在执行一段Java代码的时候,会把它所管理的内存划分 成几个不同的数据区域,其中包括: 1. 程序计数器,众所周知,JVM的多线程是通过线程轮流切换并 分配CPU执行时间的方式来实现的,那么每一个线程在切换 后都必须记住它所执行的字节码的行号,以便线程在得到CPU 时间时进行恢复,这个计数器用于记录正在执行的字节码指令的地址,这里要强调的是“字节码”,如果执行的是Native方法,那么这个计数器应该为null; 2.

3. Java计算栈,可以说整个Java程序的执行就是一个出栈入栈 的过程,JVM会为每一个线程创建一个计算栈,用于记录线程中方法的调用和变量的创建,由于在计算栈里分配的内存出栈后立即被抛弃,因此在计算栈里不存在垃圾回收,如果线程请求的栈深度大于JVM允许的深度,会抛出StackOverflowError 异常,在内存耗尽时会抛出OutOfMemoryError异常; 4. Native方法栈,JVM在调用操作系统本地方法的时候会使用到 这个栈; 5. Java堆,由于每个线程分配到的计算栈容量有限,对于可能会 占据大量内存的对象,则会被分配到Java堆中,在栈中包含了指向该对象内存的地址;对于一个Java程序来说,只有一个Java堆,也就是说,所有线程共享一个堆中的对象;由于Java堆不受线程的控制,如果在一个方法结束之后立即回收这个方法使用到的对象,并不能保证其他线程是否正在使用该对象;因此堆中对象的回收由JVM的垃圾收集器统一管理,和某一个线程无关;在HotSpot虚拟机中Java堆被划分为三代:o新生代,正常情况下新创建的对象会被分配到新生代,但如果对象占据的内存足够大以致超过了新生代的容量限 制,也可能被分配到老年代;新生代对象的一个特点是最 新、且生命周期不长,被回收的可能性高;

《深入理解LINUX内存管理》学习笔记.

引子 为什么要写这个笔记: 1,这本书的中文版翻译了太垃圾,没法阅读。阅读英文原版,可以很好的理解作者的思路。作此笔记备忘 2,一直以来学习LINUX kernel的知识缺乏系统化,借对这本书的学习,系统化的学习一下LINUX kernel。 3,自己一直在做一个too small,too simple的单进程,特权模式,64bit保护模式的称不上OS的OS,已经做完了bootloader, 构思kernel的实现的时候,困惑在内存管理的实现上,阅读这本书,希望能有利于自己的OS的编写。 4,克服惰性,多读书,希望一天能阅读5页,争取半年内阅读完这本原版700多页的巨著。 不足: 我不可能完全理解LINUX 内存管理的精髓,肯定有很多地方理解错误。希望大家能够指正,以便提高,谢谢。 学习方法: 可能您第一次阅读的时候很多地方都不理解,不用担心。那您可能需要阅读一些文件系统的知识。 或者阅读全部笔记后,再回头阅读,有些地方您就理解了。 言归正传: 一、概要 可用工具 CodeViz: 生成代码调用关系图的工具,这个工具我现在还没有去使用,有兴趣的可以自己试试去建立调用关系图。 http://www.csn.ul.ie/~mel/projects/codeviz/ Linux cross reference (LXR): 以web的方式阅读和查找LINUX内核源代码的工具。这个工具安装相当麻烦,我建议直接到它的官方网站直接读代码。 http://lxr.linux.no/linux+v2.6.24/ 模块 LINUX内存管理代码模块主要分为4个部分: 1.Out of memory 代码在mm/oom_kill.c 貌似用于杀进程的时候对内存的操作 2.虚拟内存的分配代码在mm/vmalloc.c

主板芯片和内存映射

astrotycoon 大道至简,贵在恒久力行

Diagram for modern motherboard. The northbridge and southbridge make up the chipset.

(补充: 北桥芯片用于与CPU、内存和AGP视频接口,这些接口具有很高的传输速率。北桥芯片还起着存储器控制作用,因此Intel把该芯片标号为MCH(Memory Controller Hub)芯片。南桥芯片用来管理低、中速的组件,例如,PCI总线、IDE硬盘接口、USB端口等,因此南桥芯片的名称为ICH(I/O Controller Hub)) As you look at this, the crucial thing to keep in mind is that the CPU doesn’t really know anything about what it’s connected to. It talks to the outside world through its pins bu t it doesn’t care what that outside world is. It might be a motherboard in a computer but it could be a toaster, network router, brain implant, or CPU test bench. There are thre e main ways by which the CPU and the outside communicate: memory address space, I/O address space, and interrupts. We only worry about motherboards and memory for now. 正如你所看到的,其实CPU是完全不知道自己与哪些外部器件相连接的。 CPU仅仅通过自己的引脚与外界沟通,而它并不关心自己是与什么设备在沟通。或许是另一台计算机的主板,或许是烤面包机,网络路由器,脑植入医疗设备,又或许是CPU测试仪。 CPU主要通过三种方式与外界通信:内存地址空间,IO地址空间,和中断。我们目前只关注主板和内存。 In a motherboard the CPU’s gateway to the world is the front-side bus connecting it to the northbridge. Whenever the CPU needs to read or write memory it does so via this b us. It uses some pins to transmit the physical memory address it wants to write or read, while other pins send the value to be written or receive the value being read. An Intel Core 2 QX6600 has 33 pins to transmit the physical memory address (so there are 233 choices of memory locations) and 64 pins to send or receive data (so data is transmitte d in a 64-bit data path, or 8-byte chunks). This allows the CPU to physically address 64 gigabytes of memory (233 locations * 8 bytes) although most chipsets only handle up to 8 gigs of RAM. CPU通过前端总线与北桥芯片连接,作为与外界通信的桥梁。无论何时,CPU都可以通过前端总线来读写内存。 CPU通过一些引脚来传送想要读写物理内存的地址,同时通过另一些引脚来发送将要写入内存的数据或者接收从内存读取到的数据。 Intel Core 2 QX6600 用33个引脚来传送物理内存地址(因此共有233 个内存地址),并且用64个引脚来发送或接收数据(所以数据在64位通道中传输,也就是8字节的数据块)。因此C PU可以访问64G的物理内存(233*8字节),尽管多数芯片组只能处理8G大小的物理内存。 Now comes the rub. We’re used to thinking of memory only in terms of RAM, the stuff programs read from and write to all the time. And indeed most of the memory requests from the processor are routed to RAM modules by the northbridge. But not all of them. Physical memory addresses are also used for communication with assorted devices on t he motherboard (this communication is called memory-mapped I/O). These devices include video cards, most PCI cards (say, a scanner or SCSI card), and also the flash mem ory that stores the BIOS. 那么现在的问题是,通常一提起内存我们仅仅联想到RAM,以为程序一直读写的就只是RAM。的确,绝大多数来自CPU的内存访问请求都被北桥芯片映射到了RAM。但是,注意,不是全部。物理内存同样可以用来与主板上的各种设备通信(这种通信方式被称为I/O内存映射)。这些设备包括显卡,大多数PCI卡(比如,扫描仪,或者是SCSI卡),也包括存储BIOS的flash存储器。 When the northbridge receives a physical memory request it decides where to route it: should it go to RAM? Video card maybe? This routing is decided via the memory addres s map. For each region of physical memory addresses, the memory map knows the device that owns that region. The bulk of the addresses are mapped to RAM, but when the y aren’t the memory map tells the chipset which device should service requests for those addresses. This mapping of memory addresses away from RAM modules causes the c lassic hole in PC memory between 640KB and 1MB. A bigger hole arises when memory addresses are reserved for video cards and PCI devices. This is why 32-bit OSes have pr oblems using 4 gigs of RAM. In Linux the file /proc/iomem neatly lists these address range mappings. The diagram below shows a typical memory map for the first 4 gigs of p hysical memory addresses in an Intel PC:

全面介绍Windows内存管理机制

全面介绍Windows内存管理机制及C++内存分配实例 文章整理: https://www.sodocs.net/doc/a56787579.html, 文章来源: 网络- - 本文背景: 在编程中,很多Windows或C++的内存函数不知道有什么区别,更别谈有效使用;根本的原因是,没有清楚的理解操作系统的内存管理机制,本文企图通过简单的总结描述,结合实例来阐明这个机制。 本文目的: 对Windows内存管理机制了解清楚,有效的利用C++内存函数管理和使用内存。本文内容: 本文一共有六节,由于篇幅较多,故按节发表。 1.进程地址空间 1.1地址空间 ?32|64位的系统|CPU 操作系统运行在硬件CPU上,32位操作系统运行于32位CPU 上,64位操作系统运行于64位CPU上;目前没有真正的64位CPU。 32位CPU一次只能操作32位二进制数;位数多CPU设计越复杂,软件设计越简单。 软件的进程运行于32位系统上,其寻址位也是32位,能表示的空间是232=4G,范围从0x0000 0000~0xFFFF FFFF。 ?NULL指针分区 范围:0x0000 0000~0x0000 FFFF 作用:保护内存非法访问 例子:分配内存时,如果由于某种原因分配不成功,则返回空指针0x0000 0000;当用户继续使用比如改写数据时,系统将因为发生访问违规而退出。 那么,为什么需要那么大的区域呢,一个地址值不就行了吗?我在想,是不是因为不让8或16位的程序运行于32位的系统上呢?!因为NULL分区刚好范围是16的进程空间。 ?独享用户分区 范围:0x0001 0000~0x7FFE FFFF 作用:进程只能读取或访问这个范围的虚拟地址;超越这个范围的行为都 会产生违规退出。 例子: 程序的二进制代码中所用的地址大部分将在这个范围,所有exe 和dll文件都加载到这个。每个进程将近2G的空间是独享的。 注意:如果在boot.ini上设置了/3G,这个区域的范围从2G扩大为3G: 0x0001 0000~0xBFFE FFFF。 ?共享内核分区 范围:0x8000 0000~0xFFFF FFFF 作用:这个空间是供操作系统内核代码、设备驱动程序、设备I/O高速缓存、非页面内存池的分配、进程目表和页表等。 例子: 这段地址各进程是可以共享的。

操作系统内存管理原理

内存分段和请求式分页 在深入i386架构的技术细节之前,让我们先返回1978年,那一年Intel 发布了PC处理器之母:8086。我想将讨论限制到这个有重大意义的里程碑上。如果你打算知道更多,阅读Robert L.的80486程序员参考(Hummel 1992)将是一个很棒的开始。现在看来这有些过时了,因为它没有涵盖Pentium处理器家族的新特性;不过,该参考手册中仍保留了大量i386架构的基本信息。尽管8086能够访问1MB RAM的地址空间,但应用程序还是无法“看到”整个的物理地址空间,这是因为CPU寄存器的地址仅有16位。这就意味着应用程序可访问的连续线性地址空间仅有64KB,但是通过16位段寄存器的帮助,这个64KB大小的内存窗口就可以在整个物理空间中上下移动,64KB逻辑空间中的线性地址作为偏移量和基地址(由16位的段寄存器给处)相加,从而构成有效的20位地址。这种古老的内存模型仍然被最新的Pentium CPU支持,它被称为:实地址模式,通常叫做:实模式。 80286 CPU引入了另一种模式,称为:受保护的虚拟地址模式,或者简单的称之为:保护模式。该模式提供的内存模型中使用的物理地址不再是简单的将线性地址和段基址相加。为了保持与8086和80186的向后兼容,80286仍然使用段寄存器,但是在切换到保护模式后,它们将不再包含物理段的地址。替代的是,它们提供了一个选择器(selector),该选择器由一个描述符表的索引构成。描述符表中的每一项都定义了一个24位的物理基址,允许访问16MB RAM,在当时这是一个很不可思议的数量。不过,80286仍然是16位CPU,因此线性地址空间仍然被限制在64KB。 1985年的80386 CPU突破了这一限制。该芯片最终砍断了16位寻址的锁链,将线性地址空间推到了4GB,并在引入32位线性地址的同时保留了基本的选择器/描述符架构。幸运的是,80286的描述符结构中还有一些剩余的位可以拿来使用。从16位迁移到32位地址后,CPU的数据寄存器的大小也相应的增加了两倍,并同时增加了一个新的强大的寻址模型。真正的32位的数据和地址为程序员带了实际的便利。事实上,在微软的Windows平台真正完全支持32位模型是在好几年之后。Windows NT的第一个版本在1993年7月26日发布,实现了真正意义上的Win32 API。但是Windows 3.x程序员仍然要处理由独立的代码和数据段构成的64KB内存片,Windows NT提供了平坦的4GB地址空间,在那儿可以使用简单的32位指针来寻址所有的代码和数据,而不需要分段。在内部,当然,分段仍然在起作用,就像我在前面提及的那样。不过管理段的所有责任都被移给了操作系统。

Linux内核空间和用户空间

Linux 操作系统和驱动程序运行在内核空间,应用程序运行在用户空间,两者不能简单地使用指针传递数据,因为Linux使用的虚拟内存机制,用户空间的数据可能被换出,当内核空间使用用户空间指针时,对应的数据可能不在内存中。 Linux内核地址映射模型 x86 CPU采用了段页式地址映射模型。进程代码中的地址为逻辑地址,经过段 页式地址映射后,才真正访问物理内存。 段页式机制如下图。 Linux内核地址空间划分

通常32位Linux内核地址空间划分0~3G为用户空间,3~4G为内核空间。注意这里是32位内核地址空间划分,64位内核地址空间划分是不同的。 Linux内核高端内存的由来 当内核模块代码或线程访问内存时,代码中的内存地址都为逻辑地址,而对应到真正的物理内存地址,需要地址一对一的映射,如逻辑地址0xc0000003对应的物理地址为0×3,0xc0000004对应的物理地址为0×4,… …,逻辑地址与物理地址对应的关系为 物理地址= 逻辑地址– 0xC0000000

么物理地址为0×40000001的内存,内核该怎么去访问呢?代码中必须要有内存逻辑地址的,0xc0000000 ~ 0xffffffff的地址空间已经被用完了,所以无法访问物理地址0×40000000以后的内存。 显然不能将内核地址空间0xc0000000 ~ 0xfffffff全部用来简单的地址映射。因此x86架构中将内核地址空间划分三部分:ZONE_DMA、ZONE_NORMAL和ZONE_HIGHMEM。 ZONE_HIGHMEM即为高端内存,这就是内存高端内存概念的由来。 在x86结构中,三种类型的区域如下: ZONE_DMA 内存开始的16MB ZONE_NORMAL 16MB~896MB ZONE_HIGHMEM896MB ~ 结束 Linux内核高端内存的理解 前面我们解释了高端内存的由来。Linux将内核地址空间划分为三部分ZONE_DMA、 ZONE_NORMAL和ZONE_HIGHMEM,高端内存HIGH_MEM地址空间范围为0xF8000000 ~ 0xFFFFFFFF(896MB~1024MB)。那么如内核是如何借助128MB高端内存地址空间是如何实现访问可以所有物理内存?

Windows内存管理机制及C++内存分配实例(三):虚拟内存

本文背景: 在编程中,很多Windows或C++的内存函数不知道有什么区别,更别谈有效使用;根本的原因是,没有清楚的理解操作系统的内存管理机制,本文企图通过简单的总结描述,结合实例来阐明这个机制。 本文目的: 对Windows内存管理机制了解清楚,有效的利用C++内存函数管理和使用内存。 本文内容: 3. 内存管理机制--虚拟内存 (VM) · 虚拟内存使用场合 虚拟内存最适合用来管理大型对象或数据结构。比如说,电子表格程序,有很多单元格,但是也许大多数的单元格是没有数据的,用不着分配空间。也许,你会想到用动态链表,但是访问又没有数组快。定义二维数组,就会浪费很多空间。 它的优点是同时具有数组的快速和链表的小空间的优点。 · 分配虚拟内存 如果你程序需要大块内存,你可以先保留内存,需要的时候再提交物理存储器。在需要的时候再提交才能有效的利用内存。一般来说,如果需要内存大于1M,用虚拟内存比较好。 · 保留 用以下Windows 函数保留内存块

VirtualAlloc (PVOID 开始地址,SIZE_T 大小,DWORD 类型,DWORD 保护 属性) 一般情况下,你不需要指定“开始地址”,因为你不知道进程的那段空间 是不是已经被占用了;所以你可以用NULL。“大小”是你需要的内存字 节;“类型”有MEM_RESERVE(保留)、MEM_RELEASE(释放)和 MEM_COMMIT(提交)。“保护属性”在前面章节有详细介绍,只能用前 六种属性。 如果你要保留的是长久不会释放的内存区,就保留在较高的空间区域, 这样不会产生碎片。用这个类型标志可以达到: MEM_RESERVE|MEM_TOP_DOWN。 C++程序:保留1G的空间 LPVOID pV=VirtualAlloc(NULL,1000*1024*1024,MEM_RESERVE|MEM_TOP_DOWN,PAGE_READW if(pV==NULL) cout<<"没有那么多虚拟空间!"<