Linux内存管理部分系统调用与示例分析

 
Category: Linux-Shell

写在前面

总结Linux系统调用的内存分配与虚拟内存管理部分, 包括堆内存和栈内存的操作, 分配, 释放等.

参考: Linux/Unix系统编程手册第六/七章.

  • 基本系统调用: brk/sbrk
  • C库函数: malloc/realloc/calloc/alloca/free

虚拟内存管理

简介

首先来看进程的内存布局, 如下图:

截屏2023-02-04 00.37.08.jpg

但是实际上, 这个布局并不是真实存在物理内存中的, 而是位于虚拟内存中.

利用访问局部性以追求高效地使用CPU和RAM(物理内存, 随机访问存储器)资源.

所谓访问局部性, 可以表现为以下两种情况:

  • 时间局部性: 程序倾向于在不久的将来再次访问最近刚访问过的内存地址(例如循环)
  • 空间局部性: 程序倾向于访问最近访问过的内存地址附近的内存(由于指令的顺序执行)

基本规划

  1. 将每个程序使用的内存切割成小型的/固定大小的页(page, 内存页)单元, 相应地, 将RAM划分成一系列与虚拟内存页尺寸相同的页帧.

虚拟内存的优点

  1. 进程与进程之间/进程与内核之间相互隔离, 所以一个进程不能读取或修改另一进程或内核的内存(通过页表建立纽带, 物理内存间隔离);
  2. 适当情况下, 两个或者更多的进程能够共享内存(不同进程的页表可以指向同一RAM物理内存分页)
  3. 便于实现内存保护机制, 通过对页表条目进行标记来实现.
  4. 程序员/编译器/链接器等无需在意程序在物理内存(RAM)中的布局
  5. 程序加载运行速度提高.
  6. 物理内存中可同时容纳的进程数量增多了, CPU利用率也相应提高.

brk与sbrk

简介

brk一词来源于program break, 指的是堆的当前内存边界. 来看下图:

截屏2023-02-04 00.37.08.jpg

图中程序中断箭头所指位置, 就是堆内存的边界, 最初, 程序中断位置位于未初始化数据段的末尾之后, 与&end位置相同(图中bss段)

在程序中断位置抬升之后, 程序就可以方位新分配区域内的任何内存地址, 此时物理内存页尚未分配.

内核会在进程首次试图访问这些虚拟内存地址时自动分配新的物理内存页.

基本调用格式

#include <unistd.h>

int brk(void *end_data_segment); // return 0 on success, or -1 on error
void *sbrk(intptr_t increment); // return previous program break on success, or (void *) -1 on error

对于brk系统调用, 虽然传入的是指定的数据段结束位置, 但是实际上是四舍五入之后到下一个内存页边界处的结果, 因为内存分配是按照内存页为单位分配的.

brk可能会引起段错误, 因为当访问的位置低于其初始值(图中&end位置), 会产生未定义行为

sbrk是由brk封装得到的系统调用(在Linux下), 通过增量形式进行内存分配, 当参数increment为0时, 返回当前program break(程序中断)的地址, 不做任何改变. 用于 :

  • 跟踪堆的大小
  • 监视内存分配函数包的行为

malloc与free

简介

使用C标准库函数malloc/free来分配内存, 具有以下几种优点:

  1. 属于C标准库的一部分
  2. 多线程程序常用
  3. 接口简单, 允许分配小块内存
  4. 允许随意释放内存块, 维护于一张空闲内存列表中, 在后续的内存分配调用时循环使用

函数声明

#include <stdlib.h>

void *malloc(size_t size);
void free(void *ptr);
  • malloc(0): 返回NULL或者一块小的内存(可以由free释放).
  • 若无法分配内存, 则malloc返回NULL, 并设置errno.
  • free释放ptr指向的内存块.
  • 为free传入空指针相当于空语句.
  • 不能对同一块内存free两次.
  • 在使用malloc分配了堆内存之后, 虽然程序结束之后会由系统回收对应的内存, 但是还是最好调用free释放对应的内存.

一般情况下, free不会降低program break的位置, 而是将这块内存添加到空闲内存列表中, 供后续的malloc函数循环使用. 其原因有:

  1. 被释放的内存块通常位于堆的中间, 非堆顶, 所以降低program break是不可能的
  2. 最大限度减少了程序必须执行的sbrk的调用次数(即使是系统调用也会有开销)
  3. 大多数情况下降低program break的位置不会对那些分配大量内存的程序有多少帮助, 因为它们通常倾向于持有已分配内存或是反复释放和重新分配内存, 而非释放所有内存后再持续运行一段时间.

内存分配与释放示例

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <limits.h>
#include <unistd.h>

const int MAX_ALLOCS = 1000000;

const int NUM = 1000;
const int BLOCK_SIZE = 10240;
const int FREE_STEP = 1;

const int FREE_MIN = 500;
const int FREE_MAX = NUM;

int main(int argc, char *argv[]) {
    char *ptr[MAX_ALLOCS];
    int j;

    if (NUM > MAX_ALLOCS) fprintf(stderr, "num-allocs > %d\n", MAX_ALLOCS);
    if (FREE_MAX > NUM) fprintf(stderr, "free-max > num-allocs\n"), exit(1);

    printf("Initial program break:          %10p\n", sbrk(0));

    printf("Allocating %d*%d bytes\n", NUM, BLOCK_SIZE);
    for (j = 0; j < NUM; j++) {
        ptr[j] = malloc(BLOCK_SIZE);
        if (ptr[j] == NULL) fprintf(stderr, "malloc");
    }

    printf("Program break is now:           %10p\n", sbrk(0));

    printf("Freeing blocks from %d to %d in steps of %d\n", FREE_MIN, FREE_MAX,
           FREE_STEP);
    for (j = FREE_MIN - 1; j < FREE_MAX; j += FREE_STEP) free(ptr[j]);

    printf("After free(), program break is: %10p\n", sbrk(0));

    exit(EXIT_SUCCESS);
}

下面是几个例子:

/* Initial program break:            0x645000 */
/* Allocating 1000*10240 bytes */
/* Program break is now:            0x102e000 */
/* Freeing blocks from 1 to 1000 in steps of 1 */
/* After free(), program break is:   0x666000 */

/* Initial program break:           0x10b0000 */
/* Allocating 1000*10240 bytes */
/* Program break is now:            0x1a99000 */
/* Freeing blocks from 1 to 1000 in steps of 2 */
/* After free(), program break is:  0x1a99000 */

/* Initial program break:            0xf52000 */
/* Allocating 1000*10240 bytes */
/* Program break is now:            0x193b000 */
/* Freeing blocks from 1 to 999 in steps of 1 */
/* After free(), program break is:  0x193b000 */

/* Initial program break:           0x222f000 */
/* Allocating 1000*10240 bytes */
/* Program break is now:            0x2c18000 */
/* Freeing blocks from 500 to 1000 in steps of 1 */
/* After free(), program break is:  0x2731000 */

从这些例子中可以看出, free(glibc版)会在释放内存时将相邻的空闲内存块合并为一整块更大的内存, 这样做是为了避免在空闲内存列表中包含大量较小的内存碎片, 这些碎片可能因为过小而无法满足后续的malloc请求, 因此free也有能力识别出堆顶的整个内存空间.

实现与原理

malloc

扫描之前由free释放的空闲内存块列表, 以求找到尺寸大于或等于要求的一块空闲内存

  • 尺寸相当: 返回地址(指针)给调用者
  • 较大内存: 进行分割, 返回大小相当的内存的同时, 将切下来的另一块内存保留在空闲列表中.
  • 找不到足够大的空闲内存块, 那么malloc会调用sbrk以分配更多的内存.

实际上, malloc分配内存块的时候, 实际上会额外分配几个字节来存放这块内存大小的整数值, 该整数位域内存块的起始位置, 实际返回给调用者的内存地址恰好位于这一长度记录字节之后.

空闲内存列表实现采用的是双向链表结构, 前后指针分别指向前一块和后一块空闲内存块.

随着对内存不断地释放和重新分配, 空闲列表中的空闲内存会和已分配的在用内存混杂在一起.

free

使用内存块本身的空间来存放链表指针, 并将自身添加到列表中.

避免错误的方法

  • 分配一块内存之后, 应小心谨慎, 不要改变这块内存范围之外的任何内容
  • 释放一块已分配内存有且只能有一次, 多于一次会发生段错误
  • 若非经由malloc函数返回的指针, 不能作为free的参数
  • 编写daemon程序或长时间运行的程序时一定要注意free的使用, 否则会造成内存泄漏

其他内存分配技术

动态内存分配: calloc

用于给一组相同对象分配内存.

#include <stdlib.h>
void *calloc(size_t numitems, size_t size);

第一参数指定分配对象的数量, 第二参数指定每一个待分配对象的大小.

例子:

#include <stdio.h>
#include <stdlib.h>

struct {
    int age;
    char *name;
} P;
int main(int argc, char *argv[]) {
    struct P *p;
    p = calloc(1000, sizeof(P));
    if (p == NULL) fprintf(stderr, "calloc error\n");
    return 0;
}

动态内存分配: realloc

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <malloc.h>

const int NUM = 10;
int main(int argc, char *argv[]) {
    int *p = (int *)malloc(NUM * sizeof(int));
    memset(p, 1, sizeof(int) * NUM);
    printf("p=%p\n", p);
    printf("sizeof(p)=%lu\n", malloc_usable_size(p));

    int *np = realloc(p, 3);
    if (np == NULL) {
        fprintf(stderr, "realloc error\n");
        exit(1);
    } else
        p = np;
    printf("sizeof(np)=%lu\n", malloc_usable_size(np));
    printf("np=%p\n", np);

/* p=0x16a52a0 */
/* sizeof(p)=40 */
/* sizeof(np)=40 */
/* np=0x16a52a0 */

    return 0;
}

分配对齐的内存: memalign, posix_memalign

起始地址要与2的整数次幂边界对齐, 提高寻址效率.

#include <malloc.h>

void *memalign(size_t boundary, size_t size); // 起始地址是boundary的整数倍, boundary必须是2的整数次幂

POSIX版本:

#include <stdlib.h> 

int posix_memalign(void **memptr, size_t alignment, size_t size);

栈内存分配: alloca

#include <alloca.h> 

void *alloca(size_t size);
#include <stdio.h>
#include <stdlib.h>
#include <alloca.h>

void func(void* x) { printf("func\n"); }

int main(int argc, char* argv[]) {
    void* y;
    y = alloca(10);
    func(y);
    /* func(alloca(10)); */
    return 0;
}

直接在函数参数中传入alloca(size), 会使得alloca分配的栈内存出现在当前函数参数的空间内, 而函数参数都位于栈帧内部的固定位置, 所以要先声明指针变量再传参.