DDR3中bank, 16bit和32bit等概念最近在看内存相关的东东。以前认为内存就是块资源,需要的时候,malloc出来一部分使用即可。对内部的东东没有深入了解过。刚开始看起来,感觉有点丈二和尚。通过各种查询,并请教牛人,对基本概念有了个初步了解,先总结一把。先说说bank。
看到bank首先想到了银行,然后是利率,然后是房贷...扯远了,这儿的bank是存储库的意思。也就是说,一块内存内部划分出了多个存储库,访问的时候指定存储库编号,就可以访问指定的存储库。具体内存中划分了多少个bank,要看地址线中有几位BA地址,如果有两位,说明有4个bank,如果有3位,说明有8个bank.DDR3的地址中有三个BA,即三个Bank Address,BA0, BA1, BA2。所以DDR3单块内存中都是8个bank.存储库里面的内存是怎么组织的呢?
存储库里面类似于一个矩阵。有很多点,没一个点就是一个cell,也就是一个存储点,有一个电容和一个晶体管组成,通过上电与否,来表示1或0.
每一个cell由一个行号和一个列号来唯一标识。也可以这么理解,bank中分成了很多行(row),每一行又有很多列。这样的话,给定bank编号,可以找到bank,给定row编号,可以找到所在行,给定 column 编号,可以找到所在列,也就找到了要访问的cell。一个bank中有多少行,多少列呢?
这要看行和列分别有多少位来表示。假如表示行的有A0~A14,那么单个bank中行的总量为2^15。列类似,如果表示列的有A0~A9,单个bank中列总量为2^10.
下面来看看16bit/32bit内存的概念。这儿所说的16bit/32bit,指的是内存中以多长为单位进行存储。16bit,即是说内存中是以16bit为单位访问内存的,也就是说,你给内存一个地址,内存会给你一个16bit的数据到数据线。32bit的与此类似。
下面来看一个具体例子。
该例子中,用两个16bit的DDR3内存拼成了一个32bit的DDR3.每块16bit DDR3的大小为512M Bytes.看看硬件上连接,我们这儿只关心地址线和数据线,CS, CK, CKE, CAS, RAS等暂时不考虑。
第一片16bit DDR3的BA0, BA1, BA2连接到了CPU的BA0, BA1, BA2。
第二片16bit DDR3的BA0, BA1, BA2也连接到了CPU的BA0, BA1, BA2。
第一片16bit DDR3的A0~A14连接到了CPU的A0~A14。
第二片16bit DDR3的A0~A14连接到了CPU的A0~A14。
第一片16bit DDR3的D0~D15连接到了CPU的D0~D15。
第二片16bit DDR3的D0~D15连接到了CPU的D16~D31。
分析下该实例。bank address有三个bit,所以单个16bit DDR3内部有8个bank.表示行的有A0~A14,共15个bit,说明一个bank中有2^15个行。
表示列的有A0~A9,共10个bit,说明一个bank中有2^10个行。来看看单块16bit DDR3容量:2^3*2^15*2^10=2^28=256M我们的内存是512M,到这儿怎么变成256M了?被骗了?呵呵,当然没有。忘了我们前面一直提到的16bit。16bit是2个byte对吧。访问一个地址,内存认为是访问16bit的数据,也就是两个字节的数据。256M个地址,也就是对应512M的数据了。真相大白。卖内存的没骗俺。
再来看看两个16bit是如何组成一个32bit的。有一个概念一定要清楚,这儿所说的两个16bit组成一个32bit,指的是数据,与地址没有关系。
我开始这一块没搞清楚,一直认为是两个16bit的地址组成了一个32bit的地址。然后高位地址,地位地址,七七八八。。。之后没一点头绪。
将16bit/32bit指的是数据宽度之后,就非常明了了。每一块16bit DDR3中有8个bank,2^15个row,2^10个column。也就是有256M个地址。
看前面的连线可知,两块16bit DDR3的BA0~BA2和D0~D14其实是并行连接到CPU。也就是说,CPU其实认为只有一块内存,访问的时候按照BA0~BA2和D0~D14给出地址。两块16bit DDR3都收到了该地址。它们是怎么响应的呢?两块内存都是16bit,它们收到地址之后,给出的反应是要么将给定地址上2个字节送到数据线上,要么是将数据线上的两个字节写入到指定的地址。再看数据线的连接,第一片的D0~D15连接到了CPU的D0~D15,第二片的D0~D15连接到了CPU的D16~D31。CPU认为自己访问的是一块32bit的内存,所以CPU每给出一个地址,将访问4个字节的数据,读取/写入。这4字节数据对应到CPU的D0~D31,又分别被连接到两片内存的D0~D15,这样一个32bit就被拆成了两个16bit.反过来,也就是两个16bit组成了一个32bit.CPU访问的内存地址有256M个,每访问一个地址,将访问4个字节,这样CPU能访问的内存即为1G.
简单的说:
kmalloc()是内核中最常见的内存分配方式,它最终调用伙伴系统的__get_free_pages()函数分配,根据传递给这个函数的flags参数,决定这个函数的分配适合什么场合,如果标志是GFP_KERNEL则仅仅可以用于进程上下文中,如果标志GFP_ATOMIC则可以用于中断上下文或者持有锁的代码段中。
kmalloc返回的线形地址是直接映射的,而且用连续物理页满足分配请求,且内置了最大请求数(2**5=32页)。
vmalloc优先使用高端物理内存,但性能上会打些折扣。
vmalloc分配的物理页不会被交换出去;
vmalloc返回的虚地址大于(PAGE_OFFSET + SIZEOF(phys memory) + GAP),为VMALLOC_START----VMALLOC_END之间的线形地址;
vmalloc使用的是vmlist链表,与管理用户进程的vm_area_struct要区别,而后者会swapped;
kmap()是主要用在高端存储器页框的内核映射中,一般是这么使用的:
使用alloc_pages()在高端存储器区得到struct page结构,然后调用kmap(struct *page)在内核地址空间PAGE_OFFSET+896M之后的地址空间中(PKMAP_BASE到FIXADDR_STAR)建立永久映射(如果page结构对应的是低端物理内存的页,该函数仅仅返回该页对应的虚拟地址)
kmap()也可能引起睡眠,所以不能用在中断和持有锁的代码中
不过kmap 只能对一个物理页进行分配,所以尽量少用。
对于高端物理内存(896M之后),并没有和内核地址空间建立一一对应的关系(即虚拟地址=物理地址+PAGE_OFFSET这样的关系),所以不能使用get_free_pages()这样的页分配器进行内存的分配,而必须使用alloc_pages()这样的伙伴系统算法的接口得到struct *page结构,然后将其映射到内核地址空间,注意这个时候映射后的地址并非和物理地址相差PAGE_OFFSET.
简单的说:
页大小
内核把物理页作为内存管理的基本单位;内存管理单元(MMU)把虚拟地址转换为物理地址,通常以页为单位进行处理。MMU以页大小为单位来管理系统中的也表。32位系统:页大小4KB。64位系统:页大小8KB。
kzalloc实现了kmalloc以及memset的功能,一个函数起到了两个函数的作用。即原来我们每次申请内存的时候都会这么做 , 先是用 kmalloc() 申请空间 , 然后用 memset() 来初始化 。而现在省事了 , 一步到位 , 直接调用 kzalloc(), 效果等同于原来那两个函数 , 所有申请的元素都被初始化为 0。
kmalloc和get_free_page申请的内存位于物理内存映射区域,而且在物理上也是连续的,它们与真实的物理地址只有一个固定的偏移,因此存在较简单的转换关系,virt_to_phys()可以实现内核虚拟地址转化为物理地址。
kmalloc() 分配连续的物理地址,用于小内存分配。kmalloc() 函数本身是基于 slab 实现的。slab 是为分配小内存提供的一种高效机制。但 slab 这种分配机制又不是独立的,它本身也是在页分配器的基础上来划分更细粒度的内存供调用者使用。也就是说系统先用页分配器分配以页为最小单位的连续物理地址,然后 kmalloc() 再在这上面根据调用者的需要进行切分。
kmalloc的第一个参数是要分配的块的大小,第二个参数是分配标志(flags)。
以下分配内存的方法参见:<linux/gfp.h>
方法 |
描述 |
alloc_page(gfp_mask) | 只分配一页,返回指向页结构的指针 |
alloc_pages(gfp_mask, order) | 分配 2^order 个页,返回指向第一页页结构的指针 |
__get_free_page(gfp_mask) | 只分配一页,返回指向其逻辑地址的指针 |
__get_free_pages(gfp_mask, order) | 分配 2^order 个页,返回指向第一页逻辑地址的指针 |
get_zeroed_page(gfp_mask) | 只分配一页,让其内容填充为0,返回指向其逻辑地址的指针 |
alloc** 方法和 get** 方法的区别在于,一个返回的是内存的物理地址,一个返回内存物理地址映射后的逻辑地址。
如果无须直接操作物理页结构体的话,一般使用 get** 方法。
相应的释放内存的函数如下:也是在 <linux/gfp.h> 中定义的
extern void __free_pages(struct page *page, unsigned int order); extern void free_pages(unsigned long addr, unsigned int order); extern void free_hot_page(struct page *page);
在请求内存时,参数中有个 gfp_mask 标志,这个标志是控制分配内存时必须遵守的一些规则。
gfp_mask 标志有3类:(所有的 GFP 标志都在 <linux/gfp.h> 中定义)
区标志主要以下3种:
区标志 |
描述 |
__GFP_DMA | 从 ZONE_DMA 分配 |
__GFP_DMA32 | 只在 ZONE_DMA32 分配 (注1) |
__GFP_HIGHMEM | 从 ZONE_HIGHMEM 或者 ZONE_NORMAL 分配 (注2) |
注1:ZONE_DMA32 和 ZONE_DMA 类似,该区包含的页也可以进行DMA操作。
唯一不同的地方在于,ZONE_DMA32 区的页只能被32位设备访问。
注2:优先从 ZONE_HIGHMEM 分配,如果 ZONE_HIGHMEM 没有多余的页则从 ZONE_NORMAL 分配。
行为标志主要有以下几种:
#define __GFP_WAIT ((__force gfp_t)0x10u) //表示分配内存的请求可以中断。也就是说,调度器在该请求期间可随意选择另一个过程执行,或者该请求可以被另一个更重要的事件中断。
#define __GFP_HIGH ((__force gfp_t)0x20u) //如果请求非常重要,则设置__GFP_HIGH,即内核急切的需要内存时。在分配内存失败可能给内核带来严重得后果时,一般会设置该标志 can access emergency pool
#define __GFP_IO ((__force gfp_t)0x40u) //在查找空闲内存期间内核可以进行I/O操作。这意味着如果内核在内存分配期间换出页,那么仅当设置该标志时,才能将选择的页写入磁盘。
#define __GFP_FS ((__force gfp_t)0x80u) //允许内核执行VFS操作
#define __GFP_COLD ((__force gfp_t)0x100u) //如果需要分配不在CPU高速缓存中的“冷”页时,则设置__GFP_COLD。
#define __GFP_NOWARN ((__force gfp_t)0x200u) //在分配失败时禁止内核故障警告。
#define __GFP_REPEAT ((__force gfp_t)0x400u) //在分配失败后自动重试,但在尝试若干次之后会停止。
#define __GFP_NOFAIL ((__force gfp_t)0x800u) //在分配失败后一直重试,直至成功。
#define __GFP_NORETRY ((__force gfp_t)0x1000u)//不重试,可能失败
#define __GFP_COMP ((__force gfp_t)0x4000u)//增加复合页元数据
#define __GFP_ZERO ((__force gfp_t)0x8000u)//在分配成功时,将返回填充字节0的页。
#define __GFP_NOMEMALLOC ((__force gfp_t)0x10000u) //不适用紧急分配链表
#define __GFP_HARDWALL ((__force gfp_t)0x20000u) //只在NUMA系统上有意义。它限制只在分配到当前进程的各个CPU所关联的结点分配内存。如果进程允许在所有的CPU上运行(默认情况下),该标志是没有意义的。只有进程可以运行的CPU受限时,该标志才有意义。
#define __GFP_THISNODE ((__force gfp_t)0x40000u)//页只在NUMA系统上有意义,如果设置该比特位,则内存分配失败的情况下不允许使用其他结点作为备用,需要保证在当前结点或者明确指定的结点上成功分配内存。
#define __GFP_RECLAIMABLE ((__force gfp_t)0x80000u) //将分配的内存标记为可回收
#define __GFP_MOVABLE ((__force gfp_t)0x100000u) //将分配的内存标记为可移动
#define __GFP_BITS_SHIFT 21 /* Room for 21 __GFP_FOO bits */
#define __GFP_BITS_MASK ((__force gfp_t)((1 << __GFP_BITS_SHIFT) - 1))
类型标志是编程中最常用的,在使用标志时,应首先看看类型标志中是否有合适的,如果没有,再去自己组合 行为标志和区标志。
由于这些标志总是组合使用,内核做了一些分组,包含了用于各种标准情形的适当地标志。
/* This equals 0, but use constants in case they ever change */
#define GFP_NOWAIT (GFP_ATOMIC & ~__GFP_HIGH)
#define GFP_ATOMIC (__GFP_HIGH)//用于原子分配,在任何情况下都不能中断,可能使用紧急分配链表中的内存
#define GFP_NOIO (__GFP_WAIT)//明确禁止IO操作,但可以被中断
#define GFP_NOFS (__GFP_WAIT | __GFP_IO)//明确禁止访问VFS层操作,但可以被中断
#define GFP_KERNEL (__GFP_WAIT | __GFP_IO | __GFP_FS)//内核分配的默认配置
#define GFP_TEMPORARY (__GFP_WAIT | __GFP_IO | __GFP_FS | __GFP_RECLAIMABLE)
#define GFP_USER (__GFP_WAIT | __GFP_IO | __GFP_FS | __GFP_HARDWALL) //分配用于用户进程的页面。与GFP_KERNEL相比,增加__GFP_HARDWALL。 __GFP_HARDWALL表示可以在进程能够运行的CPU节点上分配内存。
#define GFP_HIGHUSER (__GFP_WAIT | __GFP_IO | __GFP_FS | __GFP_HARDWALL | \
__GFP_HIGHMEM)//是GFP_USER的一个扩展,页用于用户空间,它允许分配无法直接映射的高端内存,使用高端内存页是没有坏处的,因为用户过程的地址空间总是通过非线性页表组织的
#define GFP_HIGHUSER_MOVABLE (__GFP_WAIT | __GFP_IO | __GFP_FS | \
__GFP_HARDWALL | __GFP_HIGHMEM | \
__GFP_MOVABLE)//类似于GFP_HIGHUSER,但分配是在虚拟内存域ZONE_MOVABLE中进行
#define GFP_NOFS_PAGECACHE (__GFP_WAIT | __GFP_IO | __GFP_MOVABLE)
#define GFP_USER_PAGECACHE (__GFP_WAIT | __GFP_IO | __GFP_FS | \
__GFP_HARDWALL | __GFP_MOVABLE)
#define GFP_HIGHUSER_PAGECACHE (__GFP_WAIT | __GFP_IO | __GFP_FS | \
__GFP_HARDWALL | __GFP_HIGHMEM | \
__GFP_MOVABLE)
#ifdef CONFIG_NUMA
#define GFP_THISNODE (__GFP_THISNODE | __GFP_NOWARN | __GFP_NORETRY)
#else
#define GFP_THISNODE ((__force gfp_t)0)
#endif
#define GFP_IOFS (__GFP_IO | __GFP_FS)
/**
* 分配的页面用于透明巨页。
*/
#define GFP_TRANSHUGE (GFP_HIGHUSER_MOVABLE | __GFP_COMP | \
__GFP_NOMEMALLOC | __GFP_NORETRY | __GFP_NOWARN | \
__GFP_NO_KSWAPD)
/**
* 只允许在当前节点中分配。
*/
#ifdef CONFIG_NUMA
#define GFP_THISNODE (__GFP_THISNODE | __GFP_NOWARN | __GFP_NORETRY)
#else
#define GFP_THISNODE ((__force gfp_t)0)
#endif
以上各种类型标志的使用场景总结:
场景 |
相应标志 |
进程上下文,可以睡眠 | 使用 GFP_KERNEL |
进程上下文,不可以睡眠 | 使用 GFP_ATOMIC,在睡眠之前或之后以 GFP_KERNEL 执行内存分配 |
中断处理程序 | 使用 GFP_ATOMIC |
软中断 | 使用 GFP_ATOMIC |
tasklet | 使用 GFP_ATOMIC |
需要用于DMA的内存,可以睡眠 | 使用 (GFP_DMA|GFP_KERNEL) |
需要用于DMA的内存,不可以睡眠 | 使用 (GFP_DMA|GFP_ATOMIC),或者在睡眠之前执行内存分配 |
get_free_page() 分配连续的物理地址,用于整页分配。 __get_free_page() 是页面分配器提供给调用者的最底层的内存分配函数。它分配连续的物理内存。__get_free_page() 函数本身是基于 buddy 实现的。在使用 buddy 实现的物理内存管理中最小分配粒度是以页为单位的。
kmalloc() / __get_free_page() 分配的是物理地址,而返回的则是虚拟地址(虽然这听上去有些别扭)。正是因为有了这种映射关系,所以需要将它们的返回地址减去 PAGE_OFFSET 才可以得到真正的物理地址。
vmalloc申请的内存则位于vmalloc_start~vmalloc_end之间,与物理地址没有简单的转换关系,虽然在逻辑上它们也是连续的,但是在物理上它们不要求连续。
内存区大小的范围一般包括13个几何分布的内存区。一个叫做malloc_sizes的表(其元素类型为cache_sizes)分别指向26个高速缓存描述符,与其相关的内存区大小为32,64,128,256,512,1024,2048,4096,8192,16384,32768,65536,131072。
但是kmalloc最多只能开辟大小为32XPAGE_SIZE的内存,一般的PAGE_SIZE=4kB,也就是128kB的大小的内存。注意kmalloc最大只能开辟128k-16,16个字节是被页描述符结构占用。
ioremap
一般来说,在系统运行时,外设的I/O内存资源的物理地址是已知的,由硬件的设计决定。但是CPU通常并没有为这些已知的外设I/O内存资源的物理地址预定义虚拟地址范围,驱动程序并不能直接通过物理地址访问I/O内存资源,而必须将它们映射到核心虚地址空间内(通过页表),然后才能根据映射所得到的核心虚地址范围,通过访内指令访问这些I/O内存资源。最后,我们要特别强调驱动程序中mmap函数的实现方法。用mmap映射一个设备,意味着使用户空间的一段地址关联到设备内存上,这使得只要程序在分配的地址范围内进行读取或者写入,实际上就是对设备的访问。笔者在Linux源代码中进行包含"ioremap"文本的搜索,发现真正出现的ioremap的地方相当少。所以笔者追根索源地寻找I/O操作的物理地址转换到虚拟地址的真实所在,发现Linux有替代ioremap的语句,但是这个转换过程却是不可或缺的。下面的程序在启动的时候保留一段内存,然后使用ioremap将它映射到内核虚拟空间,同时又用remap_page_range映射到用户虚拟空间,这样一来,内核和用户都能访问。如果在内核虚拟地址将这段内存初始化串"abcd",那么在用户虚拟地址能够读出来:
vmalloc VS ioremap
vmalloc 是一个基本的 Linux 内存分配机制,它在虚拟内存空间分配一块连续的内存区,尽管这些页在物理内存中不连续 (使用一个单独的 alloc_page 调用来获得每个页),但内核认为它们地址是连续的。 应当注意的是:vmalloc 在大部分情况下不推荐使用。因为在某些体系上留给 vmalloc 的地址空间相对小,且效率不高。kmalloc 和 _get_free_pages 返回的内存地址也是虚拟地址,其实际值仍需 MMU 处理才能转为物理地址。vmalloc和它们在使用硬件上没有不同,不同是在内核如何执行分配任务上:kmalloc 和 __get_free_pages 使用的(虚拟)地址范围和物理内存是一对一映射的, 可能会偏移一个常量 PAGE_OFFSET 值,无需修改页表。而vmalloc 和 ioremap 使用的地址范围完全是虚拟的,且每次分配都要通过适当地设置页表来建立(虚拟)内存区域。 vmalloc 可获得的地址在从 VMALLOC_START 到 VAMLLOC_END 的范围中,定义在 <asm/patable.h> 中。vmalloc 分配的地址只在处理器的 MMU 之上才有意义。当驱动需要真正的物理地址时,就不能使用 vmalloc。 调用 vmalloc 的正确场合是分配一个大的、只存在于软件中的、用于缓存的内存区域时。注意:vamlloc 比 __get_free_pages 要更多开销,因为它必须即获取内存又建立页表。因此, 调用 vmalloc 来分配仅仅一页是不值得的。vmalloc 的一个小的缺点在于它无法在原子上下文中使用。因为它内部使用 kmalloc(GFP_KERNEL) 来获取页表的存储空间,因此可能休眠。ioremap 也要建立新页表,但它实际上不分配任何内存,其返回值是一个特殊的虚拟地址可用来访问特定的物理地址区域。为了保持可移植性,不应当像访问内存指针一样直接访问由 ioremap 返回的地址,而应当始终使用 readb 和 其他 I/O 函数。ioremap 和 vmalloc 是面向页的(它们会修改页表),重定位的或分配的空间都会被上调到最近的页边界。ioremap 通过将重映射的地址下调到页边界,并返回第一个重映射页内的偏移量来模拟一个非对齐的映射。
高速缓存
1.对访问内存的缓冲(CACHE),原本意义上的高速缓存。包含对数据和命令的缓存,硬件上分为L1和L2,对软件上是透明的。2.页表缓存TLB,其实是高速缓存的一部分。3.写入缓存,如果系统总线被锁,其他CPU正在装入或者写回cache line 。将要写入的内容缓存。等待条件允许,缓冲区硬件将缓存写入内存。
内存模式
由CPU相关寄存器控制可以设置为多种模式1.不缓存2.缓存,穿透模式3,缓存,回写模式4.写保护模式,只读。