Home

cuipf blog

10 Nov 2018

基础总结-linux相关

内存管理

1. 应用程序内存结构

结构图如下:

程序内存结构图

用户区内存结构

名称 内容
栈(stack) 局部变量,函数形参,返回地址等
堆(heap) 动态分配的内存new/malloc申请的
BSS段 未初始化或者初始值为0的全局变量和静态变量
数据段 已初始化的全局变量和静态局部变量
代码段 可执行代码,字符串面值,只读变量等

详细说明:

  1. 内核区

内核总是驻留在内存中,是操作系统的一部分。内核空间为内核保留,不允许应用程序读写该区域的内容或直接调用内核代码定义的函数。

  1. 栈stack

    1. 栈又称堆栈,由编译器自动分配释放,行为类似数据结构中的栈(先进后出)。

    2. 主要有三个用途:
      • 为函数内部声明的非静态局部变量(C语言中称“自动变量”)提供存储空间。
      • 记录函数调用过程相关的维护性信息,称为栈帧或过程活动记录;它包括函数返回地址,不适合装入寄存器的函数参数及一些寄存器值的保存;
      • 临时存储区,用于暂存长算术表达式部分计算结果或allocate()函数分配的栈内内存。
    3. 进程中的每个线程都有属于自己的栈。向栈中不断压入数据时,若超出其容量会触发一个页错误。此时若栈的大小低于堆栈最大值RLIMIT_STACK(通常是8M),则栈会动态增长,程序继续运行。映射的栈区扩展到所需大小后,不再收缩。

    4. Linux中ulimit -s命令可查看和设置堆栈最大值,当程序使用的堆栈超过该值时, 发生栈溢出(Stack Overflow),程序收到一个段错误(Segmentation Fault)。注意,调高堆栈容量可能会增加内存开销和启动时间。

    5. 堆栈既可向下增长(向内存低地址)也可向上增长, 依赖于系统。

    6. 堆栈的大小在运行时由内核动态调整。
  2. 内存映射区
    • 此处,内核将硬盘文件的内容直接映射到内存, 任何应用程序都可通过Linux的mmap()系统调用或Windows的CreateFileMapping()/MapViewOfFile()请求这种映射。
    • 内存映射是一种方便高效的文件I/O方式,因而被用于装载动态共享库。
    • 用户也可创建匿名内存映射,该映射没有对应的文件, 可用于存放程序数据。
    • 该区域用于映射可执行文件用到的动态链接库。
  3. 堆heap
    • 堆用于存放进程运行时动态分配的内存段,可动态扩张或缩减。
    • 堆中内容是匿名的,不能按名字直接访问,只能通过指针间接访问。
    • 当进程调用malloc(C)/new(C++)等函数分配内存时,新分配的内存动态添加到堆上(扩张);当调用free(C)/delete(C++)等函数释放内存时,被释放的内存从堆中剔除(缩减) 。
    • 分配的堆内存是经过字节对齐的空间,以适合原子操作。
    • 堆管理器通过链表管理每个申请的内存,由于堆申请和释放是无序的,最终会产生内存碎片。
    • 堆内存一般由应用程序分配释放,回收的内存可供重新使用。若程序员不释放,程序结束时操作系统可能会自动回收。
    • 注意,堆不同于数据结构中的”堆”,其行为类似链表。
  4. BSS段
  5. 数据段
    • 数据段通常用于存放程序中已初始化且初值不为0的全局变量和静态局部变量;
    • 数据段属于静态内存分配(静态存储区),可读可写;
  6. 代码段
    • 通常用于存放程序执行代码(即CPU执行的机器指令)
    • 一般C语言执行语句都编译成机器代码保存在代码段;
    • 通常代码段是可共享的,因此频繁执行的程序只需要在内存中拥有一份拷贝即可;
    • 代码段通常属于只读,以防止其他程序意外地修改其指令(有些架构是可写);
    • 代码段最容易受优化措施影响。
  7. 保留段
    • 位于虚拟地址空间的最低部分,未赋予物理地址。任何对它的引用都是非法的,用于捕捉使用空指针和小整型值指针引用内存的异常情况。
  8. 每个地址上保存一个字节;0x08048000保存一个字节;关于字节对齐:32位系统默认是4字节对齐;64位系统默认是8字节对齐;

堆和栈的区别

老生常谈的问题:

  1. 管理方式

栈由编译器自动管理;堆由程序员控制,使用方便,但易产生内存泄露。

  1. 生长方向

栈向低地址扩展(即”向下生长”),是连续的内存区域;堆向高地址扩展(即”向上生长”),是不连续的内存区域。这是由于系统用链表来存储空闲内存地址,自然不连续,而链表从低地址向高地址遍历。

  1. 空间大小

栈顶地址和栈的最大容量由系统预先规定(通常默认2M或10M);堆的大小则受限于计算机系统中有效的虚拟内存;

  1. 存储内容

栈在函数调用时,首先压入主调函数中下条指令(函数调用语句的下条可执行语句)的地址,然后是函数实参,然后是被调函数的局部变量。本次调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的指令地址,程序由该点继续运行下条可执行语句。堆通常在头部用一个字节存放其大小,堆用于存储生存期与函数调用无关的数据,具体内容由程序员安排。

  1. 分配方式:

栈可静态分配或动态分配。静态分配由编译器完成,如局部变量的分配。动态分配由alloca函数在栈上申请空间,用完后自动释放。堆只能动态分配且手工释放。

  1. 分配效率:

栈由计算机底层提供支持:分配专门的寄存器存放栈地址,压栈出栈由专门的指令执行,因此效率较高。堆由函数库提供,机制复杂,效率比栈低得多。

  1. 分配后系统响应:

只要栈剩余空间大于所申请空间,系统将为程序提供内存,否则报告异常提示栈溢出。

OS为堆维护一个记录空闲内存地址的链表;当系统收到程序的内存分配申请时,会遍历该链表寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点空间分配给程序。 若无足够大小的空间(可能由于内存碎片太多),有可能调用系统功能去增加程序数据段的内存空间,以便有机会分到足够大小的内存,然后进行返回。 大多数系统会在该内存空间首地址处记录本次分配的内存大小,供后续的释放函数(如free/delete)正确释放本内存空间。 此外,由于找到的堆结点大小不一定正好等于申请的大小,系统会自动将多余的部分重新放入空闲链表中。

  1. 碎片问题:

栈: 不会存在碎片问题,因为栈是先进后出的队列,内存块弹出栈之前,在其上面的后进的栈内容已弹出。

堆:而频繁申请释放操作会造成堆内存空间的不连续,从而造成大量碎片,使程序效率降低。

动态库和静态库的区别

静态库:

静态库编译成的文件大,因为整个函数库的所有数据都被整合到了目标代码中,优点显而易见:编译后的执行程序不需要外部函数库的支持,所有的函数都已经被编译进去了,当然这也是他的缺点:因为静态库变了,你的程序必须重新编译。

优点:

  • 代码装载速度快,执行速度略比动态库快;

缺点:

  • 使用静态连接生成的可执行文件体积较大,包含相同的公共代码,造成浪费;

动态库

动态库在编译的时候,并没有被编译进目标代码中,你的程序执行到相关函数时才调用该函数库里相应函数,使用动态库所产生可执行文件较小,动态库并没有整合到你的程序中,所以程序的运行环境必须提供相应的库。

优点:

  • 更加节省内存并减少页面交换;
  • 动态库和执行文件相互独立,更换动态库对应用程序不造成任何影响。极大的提高了可维护性和扩展性。
  • 不同的编程语言编写的程序只要按照函数调用约定就可以调用同一个动态库;
  • 适用于大规模的软件开发,开发过程独立,耦合度小,便于不同的开发者和开发组织之间进行开发测试;

缺点:

  • 使用动态链接库的应用程序不是自完备的,它依赖的so模块也要存在,如果使用载入时动态链接,程序启动时发现so不存在,系统将终止程序并给出错误信息。而使用运行时动态链接,系统不会终止,但由于so中的导出函数不可用,程序会加载失败;速度比静态链接慢。

网络序和主机序

不同的CPU有不同的字节序类型,这些字节序是指整数在内存中保存的顺序; 1. Little endian:将低序字节存储在起始地址 2. Big endian:将高序字节存储在起始地址

例子:在内存中双字0x01020304(DWORD)的存储方式 

内存地址 
4000 4001 4002 4003 
LE 04 03 02 01 
BE 01 02 03 04 

网络字节顺序是TCP/IP中规定好的一种数据表示格式,它与具体的CPU类型、操作系统等无关,从而可以保证数据在不同主机之间传输时能够被正确解释。网络字节顺序采用big endian排序方式。

cuipf

scribble