Raoxiaoman

Viva La Viva


  • 首页

  • 归档

  • 搜索

lightnvm_pblk_structure

发表于 2019-03-13 |
字数统计: 1,912 | 阅读时长 ≈ 9

pblk_structure

code list

1
2
3
4
5
6
7
8
9
10
11
12
13
core.c           //lightnvm层。与用户库不一样,主要是使用其bio和request的支持
pblk-cache.c //pblk的写缓存,用户和GC的写入调用不同的函数。基于pblk-rb.c,即写缓存是使用ring buffer来实现。
pblk-core.c //很多核心的内容。pblk_line的元数据管理,l2p map的管理,bb(bad block)的管理等等
pblk-gc.c //gc读线程和gc写线程
pblk-init.c //初始化工作
pblk-map.c //逻辑地址到物理地址的映射,不局限于这个文件,很多地方都会涉及到l2p
pblk-rb.c //ring buffer 缓存
pblk-read.c //pblk读线程
pblk-recovery.c //恢复数据的逻辑,写失败等等情况
pblk-rl.c //rate limtter 速率限制器?应该是一些统计信息用来控制系统的运行?
pblk-sysfs.c //输出信息到/sys/block/pblkdev
pblk-write.c //pblk写线程
pblk.h //所有的定义

块组织

  • lun和line的关系

     lun 0   lun 1   lun 2   lun 3
    [blk 0] [blk 0] [blk 0] [blk 0]   line 0
    [blk 1] [blk 1] [blk 1] [blk 1]   line 1
    [ ... ] [ ... ] [ ... ] [ ... ]    ...
    [blk n] [blk n] [blk n] [blk n]   line n
    

pblk structure

  • pblk.h
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    struct pblk {
    struct pblk_lun *luns; //LUN
    struct pblk_line *lines; //LINE
    struct pblk_line_mgmt l_mg; //用于管理pblk_line
    struct pblk_line_meta lm; //line的元数据,主要是维护各个bitmap结构
    struct pblk_rb rwb; //ring buffer的缓存
    struct task_struct *writer_ts; //写线程
    unsigned char *trans_map; //l2p : logical address to physical address map 逻辑地址到物理地址的映射map
    struct pblk_gc gc; GC
    //.. 其他的就先省略了
    };

pblk-init

  • pblk-init.c

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    pblk_init(...)
    {
    pblk = kzalloc(sizeof(struct pblk), GFP_KERNEL);
    ret = pblk_luns_init(pblk, dev->luns); //初始化pblk->luns的物理地址bppa,并对lun中坏块检查
    ret = pblk_lines_init(pblk); //初始化line管理者pblk->l_mg(主要是初始化坏块表和各个链表,
    //链表包括free_list(读写逻辑的line管理)和gc_list
    //(gc逻辑的line管理 )),
    //初始化lines元数据pblk->lm(主要是初始化各个bimap),
    //同时构建一个个lines中的line结构,并把line添加在free_list中
    ret = pblk_core_init(pblk); //创建几个经常使用的struct的slab内核缓存区,
    //并创建相应的mempool,管理内核缓存,
    //并创建close工作队列和bb工作队列。
    ret = pblk_l2p_init(pblk); //初始化l2p map(逻辑地址到物理地址映射表),使用的是一个trans_map
    //维护两者的关系
    ret = pblk_lines_configure(pblk, flags); //conf of lines
    ret = pblk_writer_init(pblk); //设置写操作内核定时器,创建写操作内核线程
    ret = pblk_gc_init(pblk); //创建GC内核线程,GC写操作内核线程,GC读操作内核线程,设置GC操作
    //内核定时器,创建两个GC操作的工作队列,初始化gc链表
    wake_up_process(pblk->writer_ts); //唤醒写线程
    return pblk;
    }


    struct pblk_line {
    struct pblk *pblk;
    unsigned int id; /* Line number corresponds to the
    * block line
    */
    unsigned int seq_nr; /* Unique line sequence number */

    int state; /* PBLK_LINESTATE_X */
    int type; /* PBLK_LINETYPE_X */
    int gc_group; /* PBLK_LINEGC_X */
    struct list_head list; /* Free, GC lists */

    unsigned long *lun_bitmap; /* Bitmap for LUNs mapped in line */

    struct pblk_smeta *smeta; /* Start metadata */
    struct pblk_emeta *emeta; /* End medatada */

    int meta_line; /* Metadata line id */
    int meta_distance; /* Distance between data and metadata */

    u64 smeta_ssec; /* Sector where smeta starts */
    u64 emeta_ssec; /* Sector where emeta starts */

    unsigned int sec_in_line; /* Number of usable secs in line */

    atomic_t blk_in_line; /* Number of good blocks in line */
    unsigned long *blk_bitmap; /* Bitmap for valid/invalid blocks */
    unsigned long *erase_bitmap; /* Bitmap for erased blocks */

    unsigned long *map_bitmap; /* Bitmap for mapped sectors in line */
    unsigned long *invalid_bitmap; /* Bitmap for invalid sectors in line */

    atomic_t left_eblks; /* Blocks left for erasing */
    atomic_t left_seblks; /* Blocks left for sync erasing */

    int left_msecs; /* Sectors left for mapping */
    unsigned int cur_sec; /* Sector map pointer */
    unsigned int nr_valid_lbas; /* Number of valid lbas in line */

    __le32 *vsc; /* Valid sector count in line */

    struct kref ref; /* Write buffer L2P references */

    spinlock_t lock; /* Necessary for invalid_bitmap only */
    };

    由pblk_init调用的写线程

  • pblk-write.c

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    int pblk_write_ts(void *data)

    static int pblk_submit_write(struct pblk *pblk)

    //从ring buffer缓存中读取数据到bio
    unsigned int pblk_rb_read_to_bio(struct pblk_rb *rb, struct nvm_rq *rqd,struct bio *bio, unsigned int pos,
    unsigned int nr_entries, unsigned int count)

    //提交nvm_rq到底层的lightnvm层
    static int pblk_submit_io_set(struct pblk *pblk, struct nvm_rq *rqd)

    //ring_buffer数据结构
    struct pblk_rb {
    struct pblk_rb_entry *entries; /* Ring buffer entries */
    unsigned int mem; /* Write offset - points to next
    * writable entry in memory
    */
    unsigned int subm; /* Read offset - points to last entry
    * that has been submitted to the media
    * to be persisted
    */
    unsigned int sync; /* Synced - backpointer that signals
    * the last submitted entry that has
    * been successfully persisted to media
    */
    unsigned int sync_point; /* Sync point - last entry that must be
    * flushed to the media. Used with
    * REQ_FLUSH and REQ_FUA
    */
    unsigned int l2p_update; /* l2p update point - next entry for
    * which l2p mapping will be updated to
    * contain a device ppa address (instead
    * of a cacheline
    */
    unsigned int nr_entries; /* Number of entries in write buffer -
    * must be a power of two
    */
    unsigned int seg_size; /* Size of the data segments being
    * stored on each entry. Typically this
    * will be 4KB
    */

    struct list_head pages; /* List of data pages */

    spinlock_t w_lock; /* Write lock */
    spinlock_t s_lock; /* Sync lock */

    };//ring buffer 就是一个双向page的链表
    1. 从ring buffer缓存中读取数据到bio
    2. 将bio组织成nvm_rq
    3. 提交nvm_rq到底层的lightnvm层
      从顶层发下来的写请求应该是直接调用了pblk_write_to_cache(…)
      这里的写线程只负责把cache中的数据下放到设备

GC

pblk-gc.c

  • gc初始化

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    int pblk_gc_init(struct pblk *pblk)
    { struct pblk_gc *gc = &pblk->gc;
    //创建内核gc线程
    gc->gc_ts = kthread_create(pblk_gc_ts, pblk, "pblk-gc-ts");
    //创建内核gc写线程
    gc->gc_writer_ts = kthread_create(pblk_gc_writer_ts, pblk, "pblk-gc-writer-ts");
    //创建内核gc读线程
    gc->gc_reader_ts = kthread_create(pblk_gc_reader_ts, pblk, "pblk-gc-reader-ts");
    //设置内核定时器
    setup_timer(&gc->gc_timer, pblk_gc_timer, (unsigned long)pblk);
    mod_timer(&gc->gc_timer, jiffies + msecs_to_jiffies(GC_TIME_MSECS));
    //内核定时器唤醒gc线程和gc的读线程和写线程

    }
  • gc线程: 检查line的状态(是否写满、是否有脏数据等等),处理全是无效数据的line,把需要搬有效数据的line放到pblk_gc结构体的r_list中

    (pblk_gc_ts -> pblk_gc_run)
    
  • gc读线程: 根据pblk_gc结构体中的r_list对line进行读取,把读到的有效数据存入w_list

    (pblk_gc_reader_ts ->pblk_gc_read ->pblk_gc_line)
    
  • gc写线程: 将w_list的内容写入cache(调用gc用来写cache的函数)也就是写入ring buffer,并数据的逻辑地址指向ring buffer中的某个cacheline中

    (pblk_gc_writer_ts ->pblk_gc_write -> pblk_write_gc_to_cache -> pblk_rb_write_entry_gc->
    pblk_update_map_gc)
    

写流程

  1. pblk-cache.c

    1
    2
    3
    4
    5
    6
    7
    int pblk_write_to_cache(...)
    {
    sector_t lba = pblk_get_lba(bio); //从bio结构体获取逻辑地址
    pblk_ppa_set_empty(&w_ctx.ppa); //置ppa为空,也就是还没有分配物理地址
    pblk_rb_write_entry_user(..) //pblk-rb.c 写入到ring buffer
    pblk_write_should_kick(pblk); //唤醒pblk写线程(读cache,写入设备)
    }
  2. pblk-rb.c:304

    1
    2
    3
    4
    5
    6
    7
    void pblk_rb_write_entry_user(...)
    {
    __pblk_rb_write_entry(rb, data, w_ctx, entry); //memcpy 写入到ring buffer缓存
    pblk_update_map_cache(pblk, w_ctx.lba, entry->cacheline); //更新l2p表
    //此时当前逻辑地址对应的物理地址是一个指向cache的标记,即entry->cacheline
    //对当前逻辑地址的读操作会直接从cache中进行
    }
  3. pblk-write.c

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    static int pblk_submit_write(struct pblk *pblk)
    {
    pblk_rb_read_to_bio(...) //从ring buffer缓存中读取到bio

    pblk_submit_io_set(...) //提交bio
    }
    static int pblk_submit_io_set(struct pblk *pblk, struct nvm_rq *rqd)
    {
    pblk_setup_w_rq(pblk, rqd, c_ctx, &erase_ppa); //创建request
    //调用pblk_map_rq()来管理l2p表
    //pblk_map_rq()调用了pblk_map_page_data()
    //分配物理地址ppa并对其加锁(信号量)
    pblk_submit_io(pblk, rqd); //提交request到nvm层 in pblk-core.c
    }
  4. pblk-map.c

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    pblk_map_page_data(...)
    {
    struct pblk_line *line = pblk_line_get_data(pblk); //获取pblk的data line(当前用来写入数据的line)

    paddr = pblk_alloc_page(pblk, line, nr_secs); //从当前line中分配一个页

    ppa_list[i] = addr_to_gen_ppa(pblk, paddr, line->id); //循环获取ppa地址
    w_ctx->ppa = ppa_list[i]; //将ppa赋值给w_ctx(每个w_ctx是一个bio的写请求的上下文)
    //这一步才真正的给逻辑地址分配了物理地址
    //但是还找不到具体如何update了l2p表。。。

    pblk_down_rq(pblk, ppa_list, nr_secs, lun_bitmap); 加锁 pblk-core.c
    }

论文格式设置方法(奇偶页眉不同,页码连续)

发表于 2019-03-02 |
字数统计: 523 | 阅读时长 ≈ 2

如何设置论文的格式

  1. 光标放在每一章前面,插入分节符,此步骤要将目录、概要和绪论及文章结尾处的参考文献、致谢和成果都作为独立的章节进行分节,具体操作:光标放在该章第一个字前面页面布局分隔符(分节符)连续。
  2. 设连续页码。先在第一节插入页码,接着在下一节插入页码时,首先点击“链接前一条页眉”,以防它和上一节的格式相同,然后按照正常步骤插入页码。目录章节不需要页码时将页码选中敲击空格。论文正文由于分了很多节,因此页码可能不连续,在每一节设置时选择“设置页码格式”,点“续前节”即可。即点击该节起始页的页脚页码设置页码格式勾中续前节。以后每章都可以这样达到连续编页码的目的。
  3. 分奇偶页插入页眉。具体操作:第一章:插入页眉选“奇偶页不同”奇数页上输入该章标题,偶数页上输入整个论文的题目或者学校的名称;然后点击下一节,依次编辑奇偶页,奇数页因为每一节会不同,在编辑之前先点击“链接前一条页眉”,同样是为了防止它和上一节的格式相同,偶数页可以不管,因为内容相同。
  4. 在偶数页页脚处插入页码。由于第二步选中了奇偶页不同,所以偶数页页码不见了,这时要插入偶数页页码。具体操作:鼠标点击偶数页页脚页码页面底端普通数字2(当然页码的位置、形状可以自选)。此时注意一点,第一章之前的页码用罗马数字单独编排的,此时也会受到影响,只要鼠标点击第二页的页脚,将罗马数字“II”粘贴到原来的页码上。

Java面试-Xmind

发表于 2018-08-20 |
字数统计: 78 | 阅读时长 ≈ 1

秋招准备Java开发工程师面试,自己整理的一些Xmind思维导图

Java虚拟机

 

类加载机制

 

Java集合

 

多线程

 

Java IO和协程

 

javaWeb

 

Spring Aop

 

Spring Ioc

 

Spring Bean生命周期

 

Spring MVC运行过程

 

TCP

 

Mysql数据库事务

 

Mysql数据库索引

 

分布式锁

mysql索引

发表于 2018-07-04 |
字数统计: 496 | 阅读时长 ≈ 2

索引概述

  1. 索引被用来快速找出在一个列上用一特定值的行。没有索引,MySQL不得不首先以第一条记录开始并然后读完整个表直到它找出相关的行。
  2. 快速找出匹配一个WHERE子句的行。

Mysql中key和index的区别

key 是数据库的物理结构,它包含两层意义和作用,

  1. 约束(偏重于约束和规范数据库的结构完整性),
  2. 索引(辅助查询用的)。

index是数据库的物理结构,只用来辅助查询,它创建时会在另外的表空间(mysql中的innodb表空间)以一个类似目录的结构存储。
总结:key包含index,当单独使用key时,key和index有着相同作用,但当key前面加有约束时,key就不是简单建立普通索引了。

key的分类

  1. primary key:约束作用(constraint),用来规范一个存储主键和唯一性,但同时也在此key上建立了一个主键索引
  2. unique key:约束作用(constraint),规范数据的唯一性,但同时也在这个key上建立了一个唯一索引;
  3. foregin key:约束作用(constraint),规范数据的引用完整性,但同时也在这个key上建立了一个普通索引;

索引的分类

  1. 主键索引
  2. 唯一索引
  3. 普通索引

B树,B+树为什么更新适合外存索引

简单的解释就是,如果每次IO操作只能读取一个节点的话,作为多叉平衡树的B树,B+树,
读一次节点可以获得的分叉信息比二叉树(红黑树)多得多。
这样每次读取节点可以选择比较的索引信息就多,IO一次可以比较的次数就多,
而红黑树IO一次只能比较两个索引,所以B树,B+树找目标节点需要的IO次数就少了。

Hikv阅读

发表于 2018-04-19 |
字数统计: 146 | 阅读时长 ≈ 1

主要思想

主要是针对内存的,混合的索引用在混合的内存设备上,将Hash-index索引用在NVM上,将B+-Tree索引用在DRAM上,其实也就是存储B+-Tree在DRAM来支持k-v数据库的范围查询,Hash-index存在NVM来支持更好的单点查询,以及k-v的持久化。

比较

  1. NVM和DRAM比较,差不多的读延迟,写延迟DRAM比NVM好得多。
  2. Hash-index,单点的操作都有很大的优势,但对于范围查询不好,而B+-Tree有良好的范围查询性能。

wisckey阅读

发表于 2018-04-08 |
字数统计: 1,168 | 阅读时长 ≈ 4

Log-Structured Merge Tree(LSM-Tree)的特点:

维护顺序模式的写操作(更新操作也是写),而在B-Tree中一个更新操作可能会带来很多随机写。

LSM-Tree存在的问题:

LSM-Tree在HDD上表现的很好,因为HDD的随机I/O比顺序I/O慢得多,使用额外的顺序读写(Merge(compact),Sort)减少随机的读写可以带来很大的性能提高。

  1. 但是SSD的随机I/O和顺序I/O的差异不像HDD那么大。
  2. SSD写放大问题会在LSM-Tree增加的额外的写上更加严重,这样会影响SSD的命。
  3. LSM-Tree没有很好的利用SSD内部的并行。
  4. 写放大:两个level之间的compaction造成,合并一个L(i-1)的文件,可能最差10个L(i)的文件,排序后,再重新写下去。
  5. 读放大:查询一个key,可能要查询多个文件,并且还需要读取元数据(index block,boolom fiter block)等。

WiscKey特点:

  1. WiscKeys 从键值中分离键,只保留 LSM-tree 中的键,并将值保存在单独的日志文件中。
  2. 为了处理未分类值(在范围查询期间需要随机访问),WiscKey 使用 SSD 设备的并行随机读取特性。
  3. WiscKe 利用独特的崩溃一致性和垃圾收集技术来高效地管理值日志。
  4. WiscKey 通过在不牺牲一致性的情况下移除 LSM-tree 日志来优化性能,从而减少小写入造成的系统调用开销。

WiscKey可能的问题:

  1. 如果小的Value按顺序编写,并且按顺序查询一个大数据集,则 WiscKey 执行比 LevelDB 差。
  2. 需要对Vlog(value值的日志文件进行垃圾回收)!
  3. 用于key和value分开,范围查询(range)由原来的顺序读,变成了随机读(读取vlog中的value是随机的)
  4. Wisckey的空间放大比levelDB的严重,(实际存储的数据比逻辑的多)重点!
  5. 在范围查询中,value小的情况,WiscKey不是很好,(value不是排序的。。。)重点!

WiscKey的问题的解决方式

  • 对于问题2,采用一种lightweight的垃圾回收机制,改变了Vlog存储的格式,在head插入新的value,在tail开始选出一定大小的chunk,进行垃圾回收。
     
  • 对于问题3,采用预取key的方式,当出现连续的kv查询时,判断为范围查询,这时候就预取多个key_valueAddress,开多个线程通过预取出来的valueAddress获取value(利用设备的并行性)

WiscKey的添加的新特性

  1. 使用一个用户态的vlog buffer,处理小写(每个value_size都小)多次使用系统调用write所带来的开销。
  2. Vlog既用来存储value,又用来crash恢复,移除了Level_DB的wal。

两者存储结构的对比

 

相关工作

  • 对原始 LSM 树键值存储进的优化。
  1. bLSM 提出了一种新的合并调度器来限制写入延迟,从而保持稳定的写入吞吐量,并且还使用 bloom 过滤器来提高性能。
  2. VT-tree 通过使用间接层来避免在压缩期间排序任何先前排序的键值对。 WiscKey 直接将键值与键分离,显着减少了写入放大,无论工作负载中的密钥分配如何。
  3. LOCS 将内部闪存通道暴露给 LSM 树键值存储区,该存储区可以利用丰富的并行性来实现更高效的压缩。
  4. Atlas是基于ARM处理器和擦除编码的分布式键值存储,并将密钥和值存储在不同的硬盘上。 WiscKey 是独立的键值存储,其中键和值之间的分隔高度优化,以便SSD设备实现明显的性能提升。
  5. LSM-trie 使用一个 trie结构来组织密钥,并提出基于该 trie 的更高效的压缩; 然而,这种设计牺牲了 LSM-tree 的功能,例如对范围查询的有效支持。
  6. RocksDB 由于其设计与 LevelDB 基本相似,仍然表现出很高的写入放大率; RocksDB 的优化与 WiscKey 的设计是正交的。
  7. Walnut 是一个混合对象存储,它将小对象存储在 LSM 树中,并将大对象直接写入文件系统。
  8. IndexFS 将其元数据存储在 LSM 树中,并使用列样式模式来加速插入的吞吐量。
  9. Purity 还通过对索引进行排序并按时间顺序存储元组,将其索引与数据元组分开。
    7,8,9三个系统都使用与 WiscKey 相似的技术。

linux C++ 内存结构梳理

发表于 2018-03-15 |
字数统计: 5,598 | 阅读时长 ≈ 19

前提

最近复习了Linux内存管理以及C++的内存管理,这里做一个详细的梳理。

章节

  • 内存的分页和分段
  • 逻辑地址,线性地址以及虚拟地址之间的关系
  • linux内存分配算法:伙伴系统和slab
  • linux 进程通信中共享内存技术(mmap和shm)
  • C++程序进程地址空间,程序中各个变量与常量分别位于哪一个段
  • C++内存池技术

内存的分页和分段

虚拟内存地址空间

由来

当直接让进程使用直接的物理内存时,会对物理内存操作时会出现混乱。
比如进程 A 装在 0-30 的物理内层,在 29 处是一条 ADD 指令。而进程 B 装在 30-40 处第一条指令为 JMP 29. 没有使用虚拟内存的话,进程 B 将直接跳到进程 A 从而使两者程序都破坏掉。
 
有两种解决这个问题:一种通过基址寄存器和界线寄存器形成地址空间,通过交换技术解决内存超载。另外一种就是基于分页的虚拟地址技术。

  1. 交换技术:把一个进程完整调入内存运行一段时间,然后把他存回磁盘,空闲进程主要存储在磁盘上。缺点:当进程空间大于内存时,不能使用。
  2. 虚拟内存:把一个进程的一部分调入内存中运行,当内存没有空闲空间时,将新的覆盖旧的页,同时将旧 是写入磁盘。虚拟内存主要使用分页存储管理模式。

介绍

  • 在多任务操作系统中,每个进程都运行在属于自己的内存沙盘中。这个沙盘就是虚拟地址空间 (Virtual Address Space),在 32 位模式下它是一个 4GB 的内存地址块。在 Linux 系统中, 内核进程和用户进程所占的虚拟内存比例是 1:3,而 Windows 系统为 2:2(通过设置 Large-Address-Aware Executables 标志也可为 1:3)。这并不意味着内核使用那么多物理内存,仅表示它可支配这部分地址空间,根据需要将其映射到物理内存。
     
  • CPU 通过一个虚拟地址(virtual address,VA)来访问主存,这个虚拟地址在被送到主存之前会先转换成一个物理地址。将虚拟地址转换成物理地址的任务叫做地址翻译(translation)。地址翻译需要 CPU 硬件和操作系统之间的配合。CPU 芯片上叫做内存管理单元(Menory Management Unit, MMU)的专用硬件,利用存放在主存中的(段表)页表来动态翻译虚拟地址,该表的内容由操作系统管理。
  • 虚拟地址通过页表 (Page Table) 映射到物理内存,页表由操作系统维护并被处理器引用。内核空间在页表中拥有较高特权级,因此用户态程序试图访问这些页时会导致一个页错误 (page fault)。在 Linux 中,内核空间是持续存在的,并且在所有进程中都映射到同样的物理内存。内核代码和数据总是可寻址,随时准备处理中断和系统调用。与此相反,用户模式地址空间的映射随进程切换的发生而不断变化。
     

    在C++程序进程地址空间,程序中各个变量与常量分别位于哪一个段会详细的介绍虚拟地址空间(进程地址空间)的各个段信息

分页存储管理

  1. 基本思想
    用户程序的地址空间被划分成若干固定大小的区域,称为 “页”,相应地,内存空间分成若干个物理块(页框),页和块的大小相等。可将用户程序的任一页放在内存的任一块中,实现了离散分配。
    • 等分内存
      页式存储管理将内存空间划分成等长的若干物理块,成为物理页面也成为物理块,每个物理块的大小一般取 2 的整数幂。内存的所有物理块从 0 开始编号,称作物理页号
    • 逻辑地址
      系统将程序的逻辑空间按照同样大小也划分成若干页面,称为逻辑页面也称为页。程序的各个逻辑页面从 0 开始依次编号,称作逻辑页号或相对页号。每个页面内从 0 开始编址,称为页内地址。程序中的逻辑地址由两部分组成:页号P和页内位移量W。
  2. 分页存储管理的地址机构
     

    页号的位数x,表示最多2的x次方个页,页内位移量的位数表示页的大小,若页内位移量的位数为y,即页的大小为2的y次方,页内地址从000000000000开始到2的y次方
  3. 内存分配
    相邻的页面在内存中不一定相邻,即分配给程序的内存块之间不一定连续。对程序地址空间的分页是系统自动进行的,即对用户是透明的。由于页面尺寸为 2 的整数次幂,故相对地址中的高位部分即为页号,低位部分为页内地址。

  4. 页表
    分页系统中,允许将进程的每一页离散地存储在内存的任一物理块中,为了能在内存中找到每个页面对应的物理块,系统为每个进程建立一张页表,用于记录进程逻辑页面与内存物理页面之间的对应关系。页表的作用是实现从页号到物理块号的地址映射,地址空间有多少页,该页表里就登记多少行,且按逻辑页的顺序排列,形如:
     

  5. 地址转换(MMU)
    若给定一个逻辑地址为A,页面大小为 L,则页号 P=INT[A/L],页内地址 W=A MOD L
     

    根据上面的三个步骤可以很容易得到结果:物理地址 = 3 8 1024 + 9612 % 8192 = 25996

  6. 具有快表的地址变换机构
    分页系统中,CPU 每次要存取一个数据,都要两次访问内存(访问页表、访问实际物理地址)。为提高地址变换速度,增设一个具有并行查询能力的特殊高速缓冲存储器,称为 “联想存储器” 或 “快表”,存放当前访问的页表项。

  7. 页面的共享与保护
    当多个不同进程中需要有相同页面信息时,可以在主存中只保留一个副本,只要让这些进程各自的有关项中指向内存同一块号即可。同时在页表中设置相应的 “存取权限”,对不同进程的访问权限进行各种必要的限制。

  8. 页面置换:
    当进程在物理内存中运行时,调用到不在物理内存中的虚拟页面时,MMU 注意到该页面没有被映射到物理内存,于是 cpu 陷入到操作系统,这个陷阱称为缺页中断,操作系统找到一个很少使用的页框且把他的内容写入磁盘备份。随后把需要访问的虚拟页面读到刚才回收的页框中,修改映射关系,然后重新启动引起陷阱的指令。
    主要的页面置换算法有:页面置换算法

分段存储管理

  1. 基本思想
    页面是主存物理空间中划分出来的等长的固定区域。分页方式的优点是页长固定,因而便于构造页表、易于管理,且不存在外碎片。但分页方式的缺点是页长与程序的逻辑大小不相关。例如,某个时刻一个子程序可能有一部分在主存中,另一部分则在辅存中。这不利于编程时的独立性,并给换入换出处理、存储保护和存储共享等操作造成麻烦。
    另一种划分可寻址的存储空间的方法称为分段。段是按照程序的自然分界划分的长度可以动态改变的区域。通常,程序员把子程序、操作数和常数等不同类型的数据划分到不同的段中(写 c 程序时会用到),并且每个程序可以有多个相同类型的段。
    段表本身也是一个段,可以存在辅存中,但一般是驻留在主存中。将用户程序地址空间分成若干个大小不等的段,每段可以定义一组相对完整的逻辑信息。存储分配时,以段为单位,段与段在内存中可以不相邻接,也实现了离散分配。

  2. 分段地址结构
    作业的地址空间被划分为若干个段,每个段定义了一组逻辑信息。例程序段、数据段等。每个段都从 0 开始编址,并采用一段连续的地址空间。段的长度由相应的逻辑信息组的长度决定,因而各段长度不等。整个作业的地址空间是二维的。
    在段式虚拟存储系统中,虚拟地址由段号和段内地址组成,虚拟地址到实存地址的变换通过段表来实现。每个程序设置一个段表,段表的每一个表项对应一个段,每个表项至少包括三个字段:有效位(指明该段是否已经调入主存)、段起址 (该段在实存中的首地址) 和段长(记录该段的实际长度)。

  3. 地址变换
    针对每一个虚拟地址,存储管理部件首先以段号 S 为索引访问段表的第 S 个表项。若该表项的有效位为 1,则将虚拟地址的段内地址 D 与该表项的段长字段比较;若段内地址较大则说明地址越界,将产生地址越界中断;否则,将该表项的段起址与段内地址相加,求得主存实地址并访存。如果该表项的有效位为 0,则产生缺页中断,从辅存中调入该页,并修改段表。段式虚拟存储器虚实地址变换过程如图所示。
     

段页式存储

  1. 段页式存储管理的基本思想
    段页式存储组织是分段式和分页式结合的存储组织方法,这样可充分利用分段管理和分页管理的优点。

    • 用分段方法来分配和管理虚拟存储器。程序的地址空间按逻辑单位分成基本独立的段,而每一段有自己的段名,再把每段分成固定大小的若干页。
    • 用分页方法来分配和管理实存。即把整个主存分成与上述页大小相等的存储块,可装入作业的任何一页。程序对内存的调入或调出是按页进行的。但它又可按段实现共享和保护。
    • 逻辑地址结构。一个逻辑地址用三个参数表示:段号 S;页号 P;页内地址 d。
       
    • 段表、页表、段表地址寄存器。为了进行地址转换,系统为每个作业建立一个段表,并且要为该作业段表中的每一个段建立一个页表。系统中有一个段表地址寄存器来指出作业的段表起始地址和段表长度。
       
  2. 一个逻辑地址为:基地址 x、段号 s、页号 p 和页内地址 d,求物理地址的过程
     

在段页式系统中,为了便于实现地址变换,须配置一个段表寄存器,其中存放段表始址和段表长 TL。

  • 进行地址变换时,首先利用段号 S,将它与段表长 TL 进行比较。若 S小于TL,表示未越界
  • 于是利用段表始址和段号来求出该段所对应的段表项在段表中的位置,从中得到该段的页表始址
  • 利用逻辑地址中的段内页号 P 来获得对应页的页表项位置,从中读出该页所在的物理块号 b
  • 再利用块号 b 和页内地址来构成物理地址。

上图示出了段页式系统中的地址变换机构。在段页式系统中,为了获得一条指令或数据,须三次访问内存。第一次访问是访问内存中的段表,从中取得页表始址;第二次访问是访问内存中的页表,从中取出该页所在的物理块号,并将该块号与页内地址一起形成指令或数据的物理地址;第三次访问才是真正从第二次访问所得的地址中,取出指令或数据。显然,这使访问内存的次数增加了近两倍。为了提高执行速度,在地址变换机构中增设一个高速缓冲寄存器。每次访问它时,都须同时利用段号和页号去检索高速缓存,若找到匹配的表项,便可从中得到相应页的物理块号,用来与页内地址一起形成物理地址;若未找到匹配表项,则仍须再三次访问内存。

逻辑地址,线性地址以及虚拟地址之间的关系

在 Intel 32 位下,并且以代码段为例 (之所以不讲 64 位是因为在 64-bit long mode 下分段直接被禁用了,内存完全平坦,没什么可以讲的…)

  • 在 Intel 平台下,逻辑地址 (logical address) 是 selector:offset 这种形式,selector 是 CS 寄存器的值,offset 是 EIP 寄存器的值。
    如果用 selector 去 GDT(全局描述符表) 里拿到 segment base address(段基址) 然后加上 offset(段内偏移),这就得到了linear address。
    这个过程称作段式内存管理。如果再把 linear address 切成四段,用前三段分别作为索引去 PGD、PMD、Page Table 里查表,最终就会得到一个页表项 (Page Table Entry),那里面的值就是一页物理内存的起始地址,把它加上 linear address 切分之后第四段的内容 (又叫页内偏移) 就得到了最终的 physical address。这个过程称作页式内存管理。
  • virtual address,这是个什么东西?
    其实在 Intel IA-32 手册里并没有提到这个术语,但是在内核的确是用到了这个概念,比如__va 和__pa 这两个宏定义。
    看似神秘的 virtual address 究其本质就是程序里面使用的地址比如一个指针值,指针的本质就是 EIP 寄存器里的值,说直白点,virtual address 就是 EIP 寄存器的值。上面说过,logical address 由 selector 和 offset 两部分组成,offset 也是 EIP 寄存器的值,所以结论为:logical address 的 offset 正是 virtual address,它俩是一个东西。既然搞明白了 logical address 和 virtual address 的关系。
    那么再来看下,linear address 和 virtual address 是什么关系。在上面讲到的段式内存管理中,Linux 内核会将 segment base address(段基址) 设成 0,于是就有 linear address = 0+offset,又因为 virtual address 就是 offset,所以算出的 linear address 在数值上等于 virtual address,注意,是数值上等于,它们之间是差了段基址的,只不过段基址为 0 罢了。网上很多资料认为逻辑地址是虚拟地址的别名,其实它们不是一个东西。还有很多资料把线性地址当作虚拟地址的别名,其实它们也不是一个东西,只是 Linux 在 x86 下将它们搞得数值相等而已,虽然值相等但是本质不同。

linux内存分配算法:伙伴系统和slab

外部内存锁片

外部内存碎片就是内存被分割成很小很小的一些块,这些块虽然是空闲的,但是因为是不连续的,小到无法使用。

伙伴系统

Linux 便是采用这著名的伙伴系统算法来解决外部碎片的问题。把所有的空闲页框分组为 11 块链表,每一块链表分别包含大小为1,2,4,8,16,32,64,128,256,512 和 1024 个连续的页框。对1024 个页框的最大请求对应着 4MB 大小的连续RAM 块。每一块的第一个页框的物理地址是该块大小的整数倍。例如,大小为 16个页框的块,其起始地址是 16 * 2^12 (2^12 = 4096,这是一个常规页的大小)的倍数。

假设要请求一个256(129~256)个页框的块。算法先在256个页框的链表中检查是否有一个空闲块。如果没有这样的块,算法会查找下一个更大的页块,也就是,在512个页框的链表中找一个空闲块。如果存在这样的块,内核就把512的页框分成两等分,一般用作满足需求,另一半则插入到256个页框的链表中。如果在512个页框的块链表中也没找到空闲块,就继续找更大的块——1024个页框的块。如果这样的块存在,内核就把1024个页框块的256个页框用作请求,然后剩余的768个页框中拿512个插入到512个页框的链表中,再把最后的256个插入到256个页框的链表中。如果1024个页框的链表还是空的,算法就放弃并发出错误信号。

简而言之,就是在分配内存时,首先从空闲的内存中搜索比申请的内存大的最小的内存块。如果这样的内存块存在,则将这块内存标记为“已用”,同时将该内存分配给应用程序。如果这样的内存不存在,则操作系统将寻找更大块的空闲内存,然后将这块内存平分成两部分,一部分返回给程序使用,另一部分作为空闲的内存块等待下一次被分配。

以上过程的逆过程就是页框块的释放过程,也是该算法名字的由来。

假设要释放一个256个页框的块,算法就把其插入到256个页框的链表中,然后检查与该内存相邻的内存,如果存在同样大小为256个页框的并且空闲的内存,就将这两块内存合并成512个页框,然后插入到512个页框的链表中,如果不存在,就没有后面的合并操作。然后再进一步检查,如果合并后的512个页框的内存存在大小为512个页框的相邻且空闲的内存,则将两者合并,然后插入到1024个页框的链表中。

简而言之,就是当程序释放内存时,操作系统首先将该内存回收,然后检查与该内存相邻的内存是否是同样大小并且同样处于空闲的状态,如果是,则将这两块内存合并,然后程序递归进行同样的检查。

内部内存碎片

内部碎片是已经被分配出去(能明确指出属于哪个进程)的内存空间大于请求所需的内存空间,不能被利用的内存空间就是内部碎片。
为了有效的利用内存,使内存产生更少的外部碎片,要对内存分页,内存以页为单位来使用,最后一页往往装不满,于是形成了内部碎片

slab高速缓存

通俗的讲,slab 就是专门为某一模块预先一次性申请一定数量的内存备用,当这个模块想要使用内存的时候,就不再需要从系统中分配内存了(因为从系统中申请内存的时间开销相对来说比较大),而是直接从预申请的内存中拿出一部分来使用,这样就提高了这个模块的内存申请速度。
条件:

  1. 当某一子系统需要频繁地申请和释放内存时,使用 slab 才会合理一些。
  2. 利用 slab 申请的内存必须是大小固定的。

Linux 内核提供了 slab 层(slab 分配器),其扮演了通用数据结构缓存层的角色。slab 层把不同的对象划分为所谓高速缓存组,其中每个高速缓存组都存放不同类型的对象,每种对象类型对应一个高速缓存。例如,一个高速缓存用于存放进程描述符,而另一个高速缓存存放索引节点对象,然后这些高速缓存又被划分为 slab。slab 由一个或多个物理上连续的页组成。一般情况下,slab 也就是仅仅由一页组成,每个高速缓存可以由多个 slab 组成。

slab 分配器把每一个请求的内存称之为对象。每个 slab 都包含一些对象成员,这里的对象指的是被缓存的数据结构。每个 slab 处于三种状态之一:满、部分满或空。一个满的 slab 没有空闲的对象(slab 中的对象都已被分配)。一个空的 slab 没有分配出任何对象(slab 中所有对象都是空闲的)。一个部分满的 slab 有一些对象已分配出去,有些对象还空闲着。当内核的某一部分需要一个新的对象时,先从部分满的 slab 中进行分配,如果没有部分满的 slab,就从空的 slab 中进行分配。如果没有空的 slab,就要创建一个 slab 了。
高速缓存、slab、对象之间的关系:
 
20140514202314640.jpg

参考

linux-伙伴算法
linux-slab

Raoxiaoman

Raoxiaoman

linux C++ Python Java

7 日志
5 标签
RSS
GitHub E-Mail
© 2019 Raoxiaoman