1.B-tree和B+树
B树特征:
- 根节点至少有两个孩子
- 所有的叶子结点位于同一层
- 每个节点中的元素都是从小到大排列,节点中k-1个元素正好是k个孩子的值域划分。
- 每个中间节点包含k-1个元素和k个孩子
- 每个叶子节点都包含k-1个元素(m/2 <= k <= m)
- 无论中间节点还是叶子节点都带有卫星数据
B+树的特征:
- 每个中间节点包含k个子树和k个元素,元素不保存数据,所有的数据都保存在叶子结点
- 叶子节点本身按照关键字大小顺序连接。
- 父节点的元素都出现在子节点,在叶子结点中是最大或最小元素。
- 根节点的最大元素,等同于B+树的最大元素。
- 只有叶子结点带有卫星数据,中间节点仅仅是索引,没有数据关联。
查询性能上,B+树相比B树的性能提升 :
- B+树相比B树的中间节点不包含卫星数据,所以可以存储更多的节点元素。因此在数据量相同的情况下,B+树更加矮胖,查询IO次数也会更少
- B+树每次查询都要最终查询到叶子结点,B树则不一定。因此B+树查询性能更稳定一些。B树区分最好和最坏的情况。
- B树的范围查询需要用中序遍历,B+树只需要在链表上做遍历(范围查询更方便)
应用
B+树是为了磁盘或者其他直接存取辅助设备而设计的一种平衡查找树,主要用于mysql数据库
B树主要用在文件系统,和部分数据库索引,如文档型数据库mongodb。
2.数据库索引相关
- 数据库索引有两类,hash索引和B+树索引
hash索引的值查询效率较高,但是不能排序,也不能进行范围查询。
b+树索引数据有序,可以进行范围查询。 - 为什么不用二叉树作为数据库索引?
- 二叉树查询效率很高logn,但是索引文件可能会很大,不可能一次性全部读入内存
- 用索引+磁盘页的方式,从磁盘读入数据,会有很高的IO次数,和树的高度有关。
- B+树相比B树的优点
- B+树中间节点不包含卫星数据,空间利用率高,树高度更低,磁盘IO次数更少,性能更好。
- 查询性能更稳定。
- 范围查询更方便。
- 为什么数据库索引不用红黑树而用B+树
红黑树插入删除元素的时候会进行频繁的变色和旋转来保证红黑树的性质,浪费时间。
当数据量比较小的时候,数据完全可以放入内存中不用使用磁盘IO时,红黑树的时间复杂度比B+树低。
3.红黑树
-
红黑树的五条性质
- 每个节点是黑色或者红色
- 根节点是黑色的
- 叶子结点是黑色的
- 如果一个节点是红色,那么子节点一定是黑色
- 从一个节点到根节点的所有路径,包含数目相同的黑节点
- (其他)最长路径长度不超过最短路径长度的2倍
-
红黑树是一种自平衡二叉查找树,通过牺牲插入和删除节点数据的,从而获得较高的查找性能(logn)。
-
代价分析
- 查找代价:红黑树虽然不想AVL一样严格平衡,但平衡性能比二叉查找树摇号,查找代价基本维持在logn。只有在最差的情况下,比二叉查找树逊色。
- 插入代价:插入需要旋转和变色。插入节点最多需要两次旋转,和AVL一样。变色的时间复杂度为logn,但是十分简单,代价小。
- 删除代价:删除一个节点最多只需要3次旋转,比AVL好得多。
- 其他情况:插入和删除改变树的平衡性的概率要远远小于AVL,因此需要旋转操作的可能性要小。
4.类型转换
- static_cast:可以显示的执行类型转换,对于大数转小数这种精度损失的转换,编译器不会给出警告信息。另外可以用来找回void*类型原有的指针所指类型,但是需要由程序员保证类型没问题,否则出现未定义后果。
- const_cast:只有const_cast能改变表达式的常量属性,其他形式的类型转换都会引发编译器错误。
- reinterpret_cast:为运算对象的位模式提供较低层次上的重新解释。和旧式类型转换相同。
- dynamic_cast:有三种使用方式,指针、左值引用、右值。运算符本身在转换的过程中提供动态类型检查。指针返回0,引用抛出bad_alloc异常。
dynamic_cast<type*>(e);
dynamic_cast<type&>(e);
dynamic_cast<type&&>(e);
5.智能指针
智能指针包括auto_ptr ,unique_pyr,shared_ptr和weak_ptr 其中后三个是由c++11定义的
- unique_ptr是一个独享所有权的智能指针,不能进行复制赋值,不能使用两个unique_ptr指向同一个对象。当指针本身释放的时候,同时释放他指向的对象.
- shared_ptr指向的资源可以被多个shared_ptr引用,它在堆空间中维护一个叫引用计数的数据,每增加一个指向该对象的share_ptr,引用计数都会增加1,当引用计数减少到0的时候,shared_ptr指向的对象才会被释放。由此衍生出循环引用的问题。
- weak_ptr是对对象的一种弱引用,不会增加对象的引用计数,和shared_ptr之间可以相互转化。
循环引用:
当一个shared_ptr指向的对象a中包含有另一个shared_ptr对象b的时候,如果b也持有a的shared_ptr指针,那么两个对象在出作用域等待释放的时候,由于引用计数不能正常减到1而造成内存泄漏问题。解决方法就是,shared_ptr对象成员中使用weak_ptr指针。
引用计数增加的情况:
- 使用一个shared_ptr初始化另一个shared_ptr时,引用计数+1;
- 将他作为参数传递给一个函数,中间会有使用实参初始化形参的过程,引用计数+1;
- 作为函数的返回值,计数器+1;
- 给shared_ptr赋予一个新值,会递减等号左边指向对象的引用计数,递增等号右边的引用计数;
- 对象出作用域被销毁的时候,引用计数递减;
其他:
- 使用new动态分配const对象是合法的
- new申请不到内存的时候会抛bad_alloc异常,可以使用定位new的形式来调用没有异常的版本,申请失败返回nullptr;
- 不能用内置隐式指针转换为一个只能指针,必须使用直接初始化形式。
- 不能混用普通指针和智能指针,不能为智能指针赋值或get初始化另一个智能指针
- 不是所有的类都良好定义了析构函数,那些为兼容C和C++设计的类就是这样,它们被称作 哑类,用户必须显式的释放所有资源,否则同样会发生内存泄漏。shared_ptr在管理这种对象的时候,需要提前定义一个删除器传入。
- weak_ptr需要使用shared_ptr来初始化。访问的时候需要调用 lock() 方法返回一个shared_ptr对象来判断weak_ptr指向的对象是否存在。
6.malloc的实现,缺页中断,linux内存管理
malloc函数被调用之后,内核的工作流程
- malloc()是一个API,这个函数在库中封装了brk调用。因此调用malloc的话首先会引起brk系统调用的执行过程。
- brk系统调用服务例程会首先确定heap段的起始地址,然后检查资源限制的问题。然后将新老heap地址分别按照页大小对齐,保存在newbrk和oldbrk中。
- brk系统调用本身通过do_munmap()既可以扩大堆也可以缩小堆。如果要扩大堆的大小,首先要通过find_vma_intersection()检查扩大后的堆是否与已经存在的某个虚拟内存重合,重合退出,不重合继续调用do_brk()来完成接下来的工作。
- brk主要是为当前进程分配一个新的线性区,在没有设置VM_LOCKED标志情况下,它不会立刻为该线性区分配物理页框,而是通过vma一直将分配物理内存的工作进行延迟,直到发生缺页异常。
- 如果用户进程访问了这个线性地址,就会触发缺页中断,CPU会跳转到page_fault()异常处理程序中,异常处理程序调用do_page_fault()来选择合适的方案处理这个异常
缺页中断
要访问的页不在主存,需要操作系统将其调入主存后在进行访问
缺页中断的算法FIFO,LRU,(LFU)
参考:http://blog.chinaunix.net/uid-13246637-id-5185352.html
内核空间动态内存申请
- kmalloc()
底层实现依靠__get_free_pages(),第二个参数是分配标志,用于控制kmalloc的行为,一般为GFP_KERNEL,申请不到空间的时候会睡眠引起进程阻塞,因此不能在中断上线文或者持有自旋锁的时候使用GFP_KERNEL申请内存。一般用于分配少量内存。 - vmalloc()
用于申请较大的顺序缓冲区,申请开销远大于__get_free_pages(),过程中需要建立新的页表。 - __get_free_pages()
有一系列。释放时用free_page
区别: kmalloc()和__get_free_pages()申请的内存位于物理内存映射区域,在物理上连续;vmalloc()实在虚拟内存空间给出一块连续的内存区,物理内存不一定连续。
linux虚拟地址空间布局
一个32位系统所拥有的的虚拟地址空间是个4GB大小的内存块。其中内核进程和用户进程所占虚拟内存比例为1:3,windows为2:2(可以修改设置)。参考:https://blog.csdn.net/FreeeLinux/article/details/53782986
用户地址空间中的蓝色条带怼英语映射到物理内存的不同内存段,灰白区域表示未映射的部分。random_stack_offset和random_mmap_offset等随机值是为了防止恶意程序。
- 内核空间(kernel space)
- 栈(stack)
- 为函数内部声明的非静态局部变量提供存储空间。
- 记录函数调用过程相关的维护性信息
- 临时存储区,用于暂存长算术表达式部分计算结果或alloca()函数分配的栈内内存
另1:持续地重用栈空间有助于使活跃的栈内存保持在CPU缓存中,从而加速访问。进程中的每个线程都有属于自己的栈。向栈中不断压入数据时,若超出其容量就会耗尽栈对应的内存区域,从而触发一个页错误。此时若栈的大小低于堆栈最大值RLIMIT_STACK(通常是8M),则栈会动态增长,程序继续运行。映射的栈区扩展到所需大小后,不再收缩。
另2:alloca()函数可以在当前栈帧分配空间。优点是当函数返回的时候,自动释放它所使用的栈帧。缺点是增加了栈帧长度,并且某些系统在函数已被调用后不能增加栈帧的长度,所以不支持alloca。
- 内存映射段(memory mapping segment)
- 内核把硬盘文件的内容直接映射到内存,是一种高效的文件I/O方式,常用于装载动态共享库。用户的匿名映射也分配在这里,对于通过malloc申请的大于MMAP_THRESHOLD大小的内存,也是通过创建匿名映射分配到这里,而不是在堆中申请。
- 在linux内核2.67版本之前,内存映射段在0x4000 0000位置处向上增长,和栈相对,在2.67之后内存映射段贴近改为贴近栈顶向下增长,malloc理论申请内存值在2.9GB左右。
- 堆(heap)
- 当释放次数少于申请次数时,可能已造成内存泄漏。泄漏的内存往往比忘记释放的数据结构更大,因为所分配的内存通常会圆整为下个大于申请数量的2的幂次
- BSS段(BSS segment)
- 数据段(data segment)
用于存放程序中已初始化且初值不为0的全局变量和静态局部变量。数据段属于静态内存分配(静态存储区),可读可写。 - 正文段(text segment)
代码段通常属于只读,以防止其他程序意外地修改其指令。若进行递归,则需要借助栈来实现。 代码段最容易受优化措施影响。 - 保留区(reserved)
位于虚拟地址空间的最低部分,未赋予物理地址。任何对它的引用都是非法的,但通过使用mmap系统调用,可访问0x08048000以下的地址空间。
堆和栈的区别
- 管理方式:
- 生长方向:
- 存储内容:
- 分配方式:
- 分配效率:
- 分配后系统响应:操作系统为堆维护一个记录空闲内存地址的链表。当系统收到程序的内存分配申请时,会遍历该链表寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点空间分配给程序。若无足够大小的空间(可能由于内存碎片太多),有可能调用系统功能去增加程序数据段的内存空间,以便有机会分到足够大小的内存,然后进行返回。,大多数系统会在该内存空间首地址处记录本次分配的内存大小,供后续的释放函数(如free/delete)正确释放本内存空间。此外,由于找到的堆结点大小不一定正好等于申请的大小,系统会自动将多余的部分重新放入空闲链表中。
- 碎片问题:
数据段与BSS段的区别:
- BSS段不占用物理文件尺寸,但占用内存空间;数据段占用物理文件,也占用内存空间。
- 当程序读取数据段的数据时,系统会出发缺页故障,从而分配相应的物理内存;当程序读取BSS段的数据时,内核会将其转到一个全零页面,不会发生缺页故障,也不会为其分配相应的物理内存。
分段的好处
- 代码重用性,节约指令空间。
- 数据区对于进程而言可读写,而指令区对于进程只读,可以防止程序指令被有意或无意地改写。
- 指令区和数据区的分离有利于提高程序的局部性,有利于提高CPU缓存命中率。
- 当运行多个相同的进程,内存中只须保存一份该程序的指令部分,通过共享指令将节省大量空间。
- 分类存储:临时数据及需要再次使用的代码在运行时放入栈区中,生命周期短。全局数据和静态数据可能在整个程序执行过程中都需要访问,因此单独存储管理。堆区由用户自由分配,以便管理。
栈的增长过程
进程在运行的过程中,通过不断向栈区压入数据,当超出栈区容量时,就会耗尽栈所对应的内存区域,这将触发一个 缺页异常 (page fault)。通过异常陷入内核态后,异常会被内核的 expand_stack() 函数处理,进而调用 acct_stack_growth() 来检查是否还有合适的地方用于栈的增长。
如果栈的大小低于 RLIMIT_STACK(通常为8MB),那么一般情况下栈会被加长,程序继续执行,感觉不到发生了什么事情,这是一种将栈扩展到所需大小的常规机制。然而,如果达到了最大栈空间的大小,就会发生 栈溢出(stack
overflow),进程将会收到内核发出的 段错误(segmentation fault) 信号。
栈的分类
- 进程栈
- 线程栈
- 进程的栈大小是随机确定的至少比线程的栈要大,但是不到线程栈大小的2倍 ;
- 线程栈固定大小,不增长;
- 内核栈
在每一个进程的生命周期中,必然会通过到系统调用陷入内核。在执行系统调用陷入内核之后,这些内核代码所使用的栈并不是原先进程用户空间中的栈,而是一个单独内核空间的栈,这个称作进程内核栈。一般来说是一个页大小4K; - 中断栈
进程陷入内核态的时候,需要内核栈来支持内核函数调用。中断也是如此,当系统收到中断事件后,进行中断处理的时候,也需要中断栈来支持函数调用。
c程序的存储空间布局
用户进程分段存储内容
名称 | 存储内容 |
---|---|
栈 | 局部变量、函数参数、返回地址等 |
堆 | 动态分配的内存 |
BSS段 | 未初始化或初值为0的全局变量和静态局部变量 |
数据段 | 已初始化且初值非0的全局变量和静态局部变量 |
代码段 | 可执行代码、字符串字面值、只读变量 |
内存的延迟分配
只有在真正访问一个地址的时候才建立这个地址的物理映射,这是Linux内存管理的基本思想。Linux内核在用户申请内存的时候,只是给它分配了一个线性区(也就是虚拟内存),并没有分配实际物理内存;只有当用户使用这块内存的时候,内核才会分配具体的物理页面给用户,这时候才占用宝贵的物理内存。内核释放物理页面是通过释放先行区,找到其对应的物理页面,将其全部释放的过程。