这节课是承接上面的一节课,系统的讲了Virtual Memory。。自己听课没有人讨论没有老师解答表示已经晕掉了。。

Review Of Symbols

Transaction Lookaside Buffer (TLB)

Simple Memory System Example

Simple Memory System TLB

假设CPU执行了一条指令,它产生了一个有效地址,这个地址的虚拟地址是0x3d4,它把这个地址传递给了MMU,我们要找出对应的物理地址,然后从缓存或内存中取出数据。第一件我们做的事,是使用二进制表示这个虚拟地址,然后我们找出它的虚拟页号和虚拟页面偏移,在这个例子中,虚拟页面偏移是0x24,虚拟页号是0xf,TLB索引是虚拟页号的低2位,也就是0b11,也就是0x3,这是我们的虚拟页面标记位,也是3。MMU做的第一件事是查询TLB(Transcation Lookaside Buffer),看虚拟地址对应的页表条目是否在TLB中。所以先取出索引位,值为3,3表明如果页表条目在TLB中,它应该在第3组。我们在第3组中找标记位为3的表项,我们遍历4个条目,这里有一个标记位为7的项,但是它不是我们想要的,并且它的有效位为0,找到了,这里有一个项,标记位为3并且有效位为1,所以我们在TLB中找到了页表条目,所以TLB返回这个值,MMU返回的物理地址是0x0D,现在我们可以构造物理地址,仅仅需要直接拷贝VPO的值到PPO。上节课说到过VPO的值永远等于PPO的值,因为虚拟页和物理页有相同的块大小。所以我们得到了PPO的值和从TLB缓存的PTE中得到了物理页号,合起来一起得到了物理地址。

下一步是使用这个物理地址去看高速缓存中有没有这个物理地址的缓存,把这个值送入高速缓存,请求高速缓存返回对应物理地址上的值,在这个例子中,我们只需要返回一个字节,我们请求高速缓存返回这个物理地址的第一个字节,高速缓存首先去检查高速缓存中是否有块缓存了该字节,高速缓存首先取出物理地址的索引位,是0b00101,所以,如果所请求的字节在高速缓存中的话,它应该在第5组,所以我们去第5组寻找,找标记位为0xd的项,注意到这里有一个匹配的标记并且有效位为1,这就是我们在高速缓存中要找的项,物理页面的偏移量为0。所以我们去请求第五组偏移量为0的字节,值为0x36,因此我们有一个缓存命中,高速缓存把这个字节返回给MMU,然后MMU把它传递给处理器,然后处理器可能把这个字节存储再一个寄存器里面。

这次CPU发送给MMU的虚拟地址是0x0020,我们使用二进制写下虚拟地址。和前面一样,第一步是检查TLB看是否有页表条目的缓存,在这个例子中,如果缓存存在,应该在第0组,并且tag标志位为0,这次是一个TLB miss,查找缓存失败了,我们不得不放弃TLB。通过开销较大的内存访问,去读取页表中对应的页表条目, 所以我们要去查看页表,寻找虚拟页号为0的项,我们检查对应的页表条目看虚拟页是否在内存中,在的话有效位为1,它在内存中,因为它的有效位为1,所以我们有了一个有效的物理地址,所以内存返回物理条目或者PPN给MMU,它是0x28,现在MMU可以使用物理页号去构造物理地址,和前面类似,构造物理地址只需要拷贝虚拟偏移到物理页偏移的位置,PPN是0x28,写成二进制就是0x011000,然后物理页号和虚拟页偏移连起来就是我们的物理地址。现在MMU拥有了物理地址,所以它可以将其发送到高速缓存,并请求高速缓存返回对应的物理地址上的一个字节。当高速缓存得到了这个物理地址后,它取出对应的索引位,在这个例子中是0x8,如果缓存中缓存有我们我们要找的字节在第八组的话,寻找对应的标记为,在这个例子是0x28,恰好和PPN一样(这只是巧合),上图中,第八组有一个条目,它的标记位是24,所以这里是一次缓存不命中,缓存不命中,所以缓存不得不向内存传递那个物理地址去得到所需要的字节。

Intel Core i7 Memory System

数据缓存只缓存数据,指令缓存只缓存指令。下一层是unified cache统一缓存,因为它可以缓存指令和数据。

L1和L2缓存都是位于芯片内部的,在芯片的外面有L3缓存,它是被所有核共享的。

由于L1缓存是最靠近处理器的,它是最快的,访问时间大概是4个时钟周期,L2的大小要大一点,并且离处理器也要远一点,所以访问L2大概需要10个时钟周期,L3是在处理器外边的,需要建立芯片外部到芯片内部的连接,所以L3的访问时间是30-50个时钟周期。现在的MMU也有TLB层次结构。

为什么要有L2cache呢,为什么不把L1的缓存设计得大一点呢?以数据缓存和指令缓存为例,认真去寻找,会发现它们的大小并非是不受限制的,因为它们有一个性质,在缓存中的索引位和偏移位的大小是受限制的。

为什么需要第二级缓存,第一个原因是如果增加L1 TLB的大小,包括DTLB和ITLB。通过把第二级的TLB的资源分割,然后用于第一级TLB,这样是有问题,如果没有L2,L1的数据TLB和指令TLB大小增加了一倍,如果系统运行,仍然可能出现指令工作集大于缓存的情况,就会出现容量不命中,而且,你并不能对情况作出假设,你有可能运行一个程序,它的数据页表条目大于指令页表条目,或者刚好相反,所以通过增加第二即缓存,虽然大部分情况下指令缓存大于数据缓存,但是仍然可能出现缓存不命中的情况,通过使用二级缓存可以减少不命中惩罚。

End-to-end Core i7 Address Translation

物理地址的索引位和偏移位和VPO的偏移是一样的,这不是一个巧合,而是缓存设计刻意的

这是Intel实现缓存查找表的方式,缓存的索引位和偏移位合起来和虚拟地址的VPO一样,然后我们进行缓存查找,告诉缓存使用物理地址进行查找,使用索引位确定是哪一组,使用标记位看是否匹配,如果缓存命中,就返回对应的字给CPU,如果缓存不命中,我们只能去L2,L3去取数据,最后可能去内存中取数据,最坏的情况是从主存中取数据,最坏的情况还有可能是从磁盘中取数据,如果出现了缺页的情况,但是最后都能取得数据,然后返回给CPU

Core i7 Level 1-3 Page Table Entries

要记住页表条目指向的是下一级页表的地址,所以第一级的PTE含有第二级的页表物理基地址,后面的类似,这里有一个有效位,叫做p,p是present的首字母,它标识子页表是否在物理内存中,如果在,值为1,不在,值为0。

有一个标志位XD,这就是现代栈系统降低受到缓存区溢出攻击风险的一个方式。Core i7 Level 4 Page Table Entries

最后一级的页表条目的页表物理基地址不再指向另一个页表,而是指令物理内存中的某一页的基地址,

Core i7 Page Table Translation

地址翻译的图表如下,这是进程PT的物理地址,内核为每个进程都维护了一个PT的物理地址,内核记录了L1的物理页表的首地址,VPN的36个比特划分为4组,每组9比特,表示在页表条目中的绝对偏移,所以VPN1用来计算在L1 PT页中的偏移,在上图中可以看到,每个L1的PTE条目包含了512G的内存区域,所以对今天的大部分程序员,只需要L1的页表,绝大部分程序都只需要一个L1的页表,L1的页表条目包含有L2页表的物理地址,VPN2用于计算在L2中的偏移,后面的L3,L4也是类似的,它们只是级联在了一起,VPN的最后9比特(VPN4)用于表示在VPN4中的偏移,L4中的每一个PTE都表示一个真实页,里面含有物理页号,然后MMU取出物理页号,连上VPO,构成物理地址。

Cute Trick for Speeding Up L1 Access

MMU的地址翻译分为两个步骤,将虚拟地址翻译成物理地址,然后把物理地址送到缓存中,事实上Intel使用了一些技巧加速了L1缓存的范访问,事实上,我们这里的虚拟地址的索引位,emm,物理地址的索引位和偏移位和虚拟地址的PPO是一样的,和虚拟地址的VPO是完全一样的。那意味着MMU得到了虚拟地址后,它可以把VPO送到L1缓存中,尽管L1是通过物理地址来寻址的,但是我们可以送入虚拟地址的VPO,是因为PPO和VPO相等。

Virtual Address Space of a Linux Process

Linux的虚拟内存系统如上图,要了解清楚,我们必须知道fork和exec和加载器是如何工作的,因为虚拟内存系统的工作方式,每个进程的虚拟地址空间看上去都差不多,程序的代码,即.text段,总是位于0x4000000这个虚拟地址上,接下来是来自可执行二进制文件的初始化数据,也就是.data段,紧接着是未初始化数据,即可执行二进制文件中所定义的.bss段,紧接着是heap,它从未初始化数据段开始向上生长,有一个进程的全局变量brk指向堆顶,所以内核能够知道进程的堆顶,这里是共享库的内存映射区域,在用户可以访问内存的顶部,有一个向下生长的用户栈,寄存器%rsp指向它的栈顶,内核代码和数据在地址空间的上部,上面的图其实不完全正确,实际上在用户栈底和内核代码开始直接,有空白的地址空间,原因就是在intel的体系结构中,虚拟地址是48位的,所以如果48位的地址的最高位是0的话,那么剩下的比特也应该是0,也就是未使用的16比特需要设置为0,这有点像sign extension符号扩展,如果48位虚拟地址的最高位为1,那么扩展出来的高位也是1,所以最高位是和48位地址保持一致的,所以它造成内核所在的虚拟地址空间的最高16位全部为1。

下面有一个重点,这是进程的虚拟地址空间,内核也在虚拟地址空间中,这是内核的数据和代码,内核也将一组连续的虚拟页面映射到一组相应的连续物理页面,并且页面的大小等同于系统中DRAM的总量,这为内核提供了一种便利的方法来访问物理内存,这是很重要的,因为你不能关闭地址翻译,内核代码运行的时候,地址翻译也在进行,内核产生的是虚拟地址,这个虚拟地址空间就映射到物理内存,如何内核访问这个区域的第0个字节,实际上访问的是物理地址的第0个字节,如果访问的是这个区域的第1个字节,实际上访问的是物理地址的第一个字节,所以内核在这个虚拟内存上的读写,实际上是对物理内存的读写,所以内核的这一部分空间对所有进程来说是一样的。但是这里还有内核为每个进程保存的与进程相关的数据结构,作为进程的上下文。当然对每个进程来说,进程上下文是不一样的

Linux Organizes VM as Collection of “Areas”

现在,Linux将内存组织成一些区域的集合,一个区域就是已经存在着的虚拟内存的连续片,这些页以某种方式相关联,如代码段,数据段,共享库段,栈,内核为系统中的每个进程维护一个单独的任务结构,它包含一个指针,指向mm_struct,mm_struct有很多字段,mm_struct有一个指向第一级页表的指针,第一级页表指针是进程上下文的一部分,当进程被调度的时候,内核会把pgd条目拷贝到CR3中,这就是切换地址空间的方法,仅仅需要改变寄存器CR3的值,就改变了地址空间,好,一旦CR3的值改变了,当前进程就不能访问上一个进程的页表,mm_struct中海油一个指向一个名为area_struct的结构体,area_struct定义了这个区域的起始处和结束处,还有许可权限,像是不是只读的,例如代码段就被设置为只读的,area_strcut还定义了一些其他的标志位,当我们需要内存共享和映射的时候,这个标志位会告诉我们,这个页面是和其他进程共享的,还是这个进程私有的。所以默认情况下,页面是进程私有的,但是如果你的程序需要共享内存,你可以设置这个标志。

Linux Page Fault Handling

缺页异常是这样一冲情况,在MMU翻译某个地址的时候,发现地址对应的页表不在内存中,这就出发了一个缺页异常。默认的错误处理需要处理可能发生的多种情况,一种情况是,当我们读内存的时候,发现地址是不合法的,因为访问了一个不存在的页面,我们还没有创建该页,内核没有在虚拟内存中创建分配改页,这一个个错误,访问了一个不存在的页面,就会触发一个段错误(segment fault),内核能够识别出来是因为内核可以遍历area_struct这个链表,然后内核发现它并不能找到地址在某个area_struct定义的区域之内,所以这是一个因为访问不存在的页面触发的段错误,另一个可能的情况,就是指令尝试对一个虚拟地址空间的只读页面进行写操作。地址是合法的,MMU通常会检查页表的权限位,发现对一个只读页进行了写操作,然后抛出一个异常,但是这里有一个问题,像现在我们还没有页表条目,因为我们触发了一个缺页异常,所以MMU并不知道这个写操作是异常的(并不知道那个页表是只读的),但是内核可以检查这个区域的保护位,在这个例子中,是代码段,所以是只读的,所以触发一个保护错误,在Linux的报告中,是一个段错误。还有一种情况是我们打算从一个有效区域读取数据,然后内核陷入异常,换入需求的页表,然后取出对应的数据返回给CPU,

Memory mapping

虚拟内存区域初始化的时候和磁盘上的对象关联起来,这个过程叫做内存映射,每一个区域,即每个区域包含的页,都和文件中的某些部分关联起来,并且初始的内容来自那个文件。也就是说,当我们第一次引用到某个区域的时候,它的初始值来自己与磁盘上的普通文件,所以例如该区域是代码区,那么这个区域会被映射到一个可执行文件的某个部分,也就是,这个区域的初始值就来自己哪个可执行文件,这就是我们复合来回的从磁盘拷贝数据到内存,文件也可以是一个匿名文件,匿名文件是有内核创建,包含的全是二进制0,匿名文件的大小是任意的,但只含有0,当然这个文件并不真实存在,它是一个技巧,允许我们创建一个全为0的页,如果一个页面和匿名文件关联在了一起,当第一次引用页面的时候,会分配一个物理页,并初始化为全0,

Sharing Revisited: Shared Objects

前面说过,正常的进程不会和其他进程有任何共享的东西,但是现在利用内存映射可以实现进程间共享对象,因为进程可以映射虚拟内存的区域到同一个对象,假设我们有两个进程,它们都有各自不同的虚拟地址空间,它们的虚拟页都被映射到物理内存的某个地方,我们假设有一个区域是进程1的某个段,被映射到这个对象,也就是一个文件的某个部分,现在进程2也把相同的对象映射到自己的虚拟内存空间,注意他们是不同的虚拟空间,所以进程1映射到这个共享对象的区域,和进程2映射到这个共享对象的区域是没有关系的,因为这个共享对象有一个唯一的名字,所以内核可以检查有没有其他进程映射到那个对象,如果有没那么内核就把虚拟地址对应的区域映射到相同的物理地址。所以,我们现在就有一个办法,是的每个独立虚拟地址空间的进程可以访问一些连续片(chunk),达到对同一物理地址区域访问的目的。

这个方法是很有用的,想象一下,这些进程都是服务器,是多个Apache服务器的进程,你可能需要共享一些缓存,在这些Apache服务器之间共享内存缓存,在这个例子中,你的缓存是文件,一个在磁盘上的文件,然后,当你访问文件的页表区域的时候,这些页表会拷贝到内存中,所有的进程都可以共享这个缓存。有时候,我们也许要是私有对象,意味着他们不能再进程间共享。

Sharing Revisited:

Private Copy-on-write (COW) Objects

还有一种有趣的私有对象,叫做私有写时复制对象,它的思想是,两个进程都将这个对象映射到他们的虚拟地址空间,但是这个对象不是共享对象,而是一个私有写时复制对象,和上面讲的一样,两个虚拟地址空间的对应区域,映射到了相同的物理区域,他们这个区域的页表标为私有写时复制,这意味着,在正常的时候,它是一个共享对象,共享对象的含义是,如果一个进程对共享对象对应的虚拟地址空间进行了写操作,那么这个写操作也会同步到磁盘上的文件,但是如果这个对象被标记位私有写时复制而不是共享。那么,如果进程对这个区域进行了写操作,它并不会同步写操作到共享对象的物理内存,而是将页面拷贝一份,一个独立的拷贝页,并且把它映射到一个没有使用的物理地址,所以这叫写时复制的原因。

实际上,写时复制是一个基本的系统概念,它在高效共享方面使用非常广泛。

好的为什么我们要有这个有用而有趣的写时复制技术呢。想想fork技术,如果想fork一个进城,最简单的方法就是拷贝,你需要拷贝完整的地址空间,一个独立但是一模一样的地址空间,如果你使用这个简单的方式实现,你需要拷贝所有的页表,所有的用户数据结构和内存,所以如果你要fork的进程在它的地址空间内,创建了很多虚拟页,它们全部都需要背拷贝,并且映射到不同的物理地址,开销很大。但是幸运的是,写时复制技术提供了一个高效的解决方案,当我们使用fork,内核只拷贝所有的内核数据结构,mm_struct和area_struct以及页表,这个操作是没有办法避免的,但是它们的大小并不大,它们没有程序正在访问的实际数据那么大,然后内核把两个进程中的每个页面都标记位只读,然后把每一个area_struct都标记位私有时复制,然后fork返回,在返回时,每个进程都有一样的地址空间,因为它们拥有相同的页表,因为我们拷贝了这些页表。

所以有趣的是,对虚拟地址空间只进行读的区域永远不会被复制,这是完美的,因为两个进程分享,没有被修改的物理内存,这是为什么fork的效率并不低的原因。

The execve Function Revisited

execve函数在当前进程中加载并执行一个新函数,它删除当前进程的所有的area_struct结构,所以execve并不创建进程,它只是在当前进程中,在一个新的虚拟地址空间上运行一个程序,所以它删除了当前进程的所有的area_struct和页表,然后为新区域创建了新的 area_struct和页表,然后程序初始化数据,这些区域由文件支持,在这种情况下是可执行二进制文件,并且他们的程序由包含代码数据段的可执行文件部分支,由包含初始化数据的可执行文件部分支持,所以这两者是私密的,我们称之为私人private。好的,这个对象不应该与任何其他东西共享,它是文件支持的,因为这些区域由文件的某些部分支持,未初始化的数据,在二进制文件的.bss段指定,这被定义为私人需求0区域,好的,所以这些页面都将被记住,.bss是未初始化的,好的,所以系统将这些初始化为零,所以任何页面然后堆中的任何页面也是私有要求为零,现在是共享库的内存映射区域,请记住,每个进程共享都是libc和内存中的相同副本,所以这是共享的虚拟地址空间的一个区域,好的,因为它由多个进程共享,它有文件支持,在这种情况下,de文件是.so文件的共享对象文件,此区域的代码由文本支持,包含代码和的此对象文件的一部分,数据初始化数据由包含数据的此文件的一部分支持,现在有......我没有在这里展示它,但还有其他部分,这个区域需要是一个私有的写时复制,因为不同的libc功能,如果libc函数具有静态变量或者可以包含状态,像随机数发生器典型的随机数发生器,在每次调用时保留状态,好的,所以每个流程的状态都不同,,因此,对于要写入的任何数据,需要有一种私有的写时复制区域,而且我想系统就是这样,只要你做了整个地区,现在你必须拥有它,你必须有第二部分,这将是私人拷贝写入,然后堆栈是私人需求零,那么它究竟是什么呢?它只是设置,它创建了由你要执行的目标文件以不同方式支持的新区域,它为匿名文件支持的BSS和堆栈创建区域,它创建了一个这个内存映射区域,它是一个与libc相对应的共享对象,然后它将程序计数器%rip设置为文本中的入口点,然后,一旦这个程序运行,现在注意我们没有加载任何东西,我们所做的就是设置映射,我们刚刚在内核中创建了数据结构,我们已经在地址空间的部分和这些对象之间创建了映射,但实际上没有任何内容被复制到内存中,好的,这就是我们刚创建的所有内容,我们仅仅做了映射,我们修改了数据结构,现在但是一旦加载器将%rip设置为入口点,此文本段中的第一条指令,Linux遇到的代码和数据没有时,会陷入异常,加载要被推迟,直到延迟加载一页代码或数据,直到实际引用和访问该代码或数据页,因此,我认为非常聪明且非常有趣,它是如何又一个有用的例子,虚拟内存与系统操作的紧密集成程度。

后面肝不动了 凌晨3点了 之后想写一个程序 输入文件里面英文数字中文(带格式) 输出文件只有中文。

CMU 15-213 Introduction to Computer Systems学习笔记(17) Virtual Memory:System相关推荐

  1. Introduction to Computer Networking学习笔记(十五):Queue Model 包交换中的缓冲模型

    本章知识点比较零散,因此一篇文章进行总结,并且不具有连贯性,仅记录自己认为有价值的内容. 将较大的包拆分为小包进行传输,可以减小端对端延迟,原因如下图: 数据传输时,突发大量的数据包会增加延迟,简单周 ...

  2. springmvc学习笔记(17)-上传图片

    2019独角兽企业重金招聘Python工程师标准>>> springmvc学习笔记(17)-上传图片 标签: springmvc [TOC] 本文展示如何在springmvc中上传图 ...

  3. iTron3学习笔记(一) System Calls of Memory Pool Management Functions

    iTron3学习笔记(一)  System Calls of Memory Pool Management Functions 1.创建固定内存池(Create Fixed Memory Pool) ...

  4. 2020-4-5 深度学习笔记17 - 蒙特卡罗方法 3 ( 马尔可夫链蒙特卡罗方法MCMC-先验分布/后验分布/似然估计,马尔可夫性质)

    第十七章 蒙特卡罗方法 中文 英文 2020-4-4 深度学习笔记17 - 蒙特卡罗方法 1 (采样和蒙特卡罗方法-必要性和合理性) 2020-4-4 深度学习笔记17 - 蒙特卡罗方法 2 ( 重要 ...

  5. 华为HCIA-datacom 学习笔记17——IPv6基础

    华为HCIA-datacom 学习笔记17--IPv6基础 IPv6基础 1.ipv4与ipv6 地址长度32bit IPv6:IP地址长度128bit IPv4包头(20byte~60byte) I ...

  6. 【计算机网络学习笔记17】网络安全、加密技术、“Virtual Private Network”技术

    [计算机网络学习笔记17]网络安全.加密技术."Virtual Private Network"技术 一.网络安全概述 1.1 网络系统的安全目标: 1.可用性(Availabil ...

  7. Python学习笔记17:实操案例十四(模拟高铁售票系统,推算几天后的日期)

    Python学习笔记17:实操案例十四(模拟高铁售票系统,推算几天后的日期) 1.模拟高铁售票系统 使用漂亮的表格模块PrettyTable 这个模块需要预先安装,不然直接导入会报错: 安装办法: h ...

  8. opencv学习笔记17:梯度运算之laplacian算子及其应用

    laplacian算子理论 前文介绍了sobel算子及其函数使用 和scharr算子及其函数使用 使用方法 不同算子比较 sobel,和scharr算子:右边121列-左边121列.右边-3,10,3 ...

  9. TensorFlow2.0 Guide官方教程 学习笔记17 -‘Using the SavedModel format‘

    本笔记参照TensorFlow官方教程,主要是对'Save a model-Training checkpoints'教程内容翻译和内容结构编排,原文链接:Using the SavedModel f ...

  10. CS269I:Incentives in Computer Science 学习笔记 Lecture 13:Introduction to Auctions(拍卖简介)

    Lecture 13 Introduction to Auctions(拍卖简介) 1 Preamble(前言) 为什么计算机科学家要关心拍卖?直到最近(甚至可能仍然),提到拍卖,我们脑海中联想到的图 ...

最新文章

  1. R语言笔记4:向量、矩阵的数学运算
  2. 自动化控制之线程池的使用
  3. OpenLayers加载搜狗地图
  4. 【Redis6快速深入学习01】NoSQL数据库简介
  5. PWN-PRACTICE-BUUCTF-1
  6. php对象好用吗,在数据库中使用对象的好处_php
  7. Spring : Spring @Transactional 事物管理入口
  8. C++之继承探究(五):子类对象作父类对象使用
  9. 深圳的小伙伴有福利了!
  10. c语言家谱管理系统不是二叉树,二叉树实现的简单家谱管理系统
  11. html粒子特效图片切换,javascript转换静态图片,增加粒子动画效果
  12. tinymce 实现 粘贴图片自动上传
  13. 1. ARMv9-A Overview
  14. 选择与循环:剪刀石头布_剪刀石头布十大奢侈家具,创造高端精致生活就是这么简单!...
  15. 荒木毬菜 小情歌日文版 - 独身OL之歌
  16. 解决 QGC地面站 ( QGroundControl )停止工作-由于win7 ghost精简缺少语音包
  17. PowerManagerService分析(二)之updatePowerStateLocked()核心
  18. C语言中利用Swap函数交换变量a,b
  19. MAC重置NVRAM
  20. unity3d环境搭建

热门文章

  1. iOS之摇一摇功能实现
  2. ubuntu 下 4412烧写SuperBoot
  3. Eclipse @override报错解决
  4. java图片像素90翻转_java后台解决上传图片翻转90的问题,有demo,经过测试可用...
  5. 关于RocketMQ消费者消费队列的消费起始位置源码分析
  6. Java创建型设计模式之简单工厂,工厂方法,抽象工厂
  7. Java货币金额转换为大写形式
  8. 达观数据郭权:用好ngResource和postman,提高你的开发调试效率
  9. 购物商城Web开发第二十二天
  10. [算法题] 安排会议室——贪心算法的应用