学习《操作系统导论》05

发布时间 2023-05-21 17:11:57作者: StillLoving

内存操作API

内存类型

  • 堆内存
  • 栈内存

栈内存不需要程序员自己管理,一般都是编译器隐式管理,所以栈内存一般也被成为“自动”内存。

而程序员通过诸如malloc这样的函数申请的内存属于堆内存,这块区域需要程序员自己进行管控。

比如下面这段:

void func() {
    int *x = (int *) malloc(sizeof(int));
    ......
}

上面函数内的这行代码中,实际上既有栈内存的创建,也有堆内存的创建。

malloc调用

malloc函数传入的参数类型为size_t,也就是对应需要申请内存区域的大小。

如果申请成功,返回的是一个指向该区域指针,反之如果申请失败,就会返回NULL。

另外在正常使用中,malloc的参数一般都不会直接填入数字,而是通过sizeof()函数来进行计算的,就像前面看到的那样sizeof(int)

sizeof()内部也可以传入具体的变量:

int x;
int *s = sizeof(x);

这里还有一种情况需要格外注意:

int *x = malloc(10 * sizeof(int));
printf("%d\n", sizeof(x));

这段代码中,第一句申请了一块10个整型的数组空间,但是后面一句的printf()中,此时使用sizeof打印出的结果可能是一个非常小的值,这是因为此时sizeof认为需要计算的只是单个int类型的大小。

如果需要得到确切的大小,可以考虑这样:

int x[10];
printf("%d\n", sizeof(x));

free调用

free()函数本身接收一个指针类型的参数,这个指针就是前面对应malloc()返回的指针,无需再传入具体需要释放的内存大小。

由此可见,free()的释放实际上依赖于内存分配库本身对内存的标记。

常见的使用错误

忘记分配内存

对于用惯了Java这类编程语言的人来说,这可能初期是一个经常会犯的错误,一定要切记,使用之前一定要申请内存区域,否则会报错

没有分配足够的内存

在程序中,一定要仔细计算所需内存空间的大小,一旦内存不够,就及时申请更大的内存区域,否则程序就会出现一些错误。

忘记初始化

内存分配成功后,一定要记得对内存区域进行初始化,否则可能读到一些稀奇古怪的内容, 导致程序运行异常。

忘记释放内存

内存使用完之后,一定记得及时释放,最好的方式就是在malloc的时候,对应及时写一个free,然后再在两个调用之间插入具体的业务逻辑,确保不会出现内存泄漏的问题。

反复释放内存

对于已经释放的内存区域,不要反复进行释放操作,这样做会导致程序崩溃。

用完之前释放内存

这种被称为“悬挂指针(dangling pointer)”,如果在free之后又进行了malloc,就会导致程序数据出现问题,需要格外注意。

错误地调用free

因为free接收的是一个指针类型的参数,所以如果传入的是一些其他指针数据,而非malloc返回的那种指针,可能就会出现各种异常情况。

补充

为什么进程退出的时候没有内存泄漏?

实际上,系统中的内存管理存在两级:

第一级是操作系统进行的内存管理,操作系统会在进行运行时,将一定的内存区域交给进程进行控制,等进程退出后,操作系统会重新收回之前分配的这部分内存空间。

第二级是进程内部进行的内存管理,如前面提到的mallocfree这类操作。

对于那些短期运行的程序,即使有出现没能及时释放的内存,也不用担心,因为进程退出后,操作系统不管内部有没有释放,都会统一全部回收掉,所以也就没有所谓的内存泄漏的问题了。

但是对于像web服务这种,它需要长期驻留内存中的,如果出现了这种没有及时释放的内存空间,长此以往,内存泄漏的就会越来越多,最终就是大量内存被浪费掉了,可用内存越来越少,从而导致程序崩溃。

底层操作系统支持

前面提到的mallocfree这类的函数,实际上它们并非是系统调用,而只是一个库调用,但是这个库调用本身也是建立在一些系统调用之上的,这里给出了两个系统调用介绍:brk、sbrk。

brk调用的作用就是改变程序分段的位置:堆结束的位置。这个调用需要一个新分段地址作为参数,然后将它和现有分段地址进行比较,如果比现有分段地址大,那就是扩充内存,如果比现有的小,那就是释放一定的内存。

sbrk的效果和brk实际上是类似的,只是它操作的是栈。

程序员本身是不建议直接使用brk、sbrk的,很容易出现各种问题,建议坚持一直使用mallocfree

还有一个方法:mmap():它也是申请一块内存区域,但是不同的是,它申请的这块内存区域不和任何具体的文件进行关联,它是一块匿名空间,仅仅只是存在于交换空间(swap space)中,申请后,在使用上和malloc申请的内存区域一模一样。关于交换空间,后续章节有详细介绍。

其他调用

calloc: 分配内存,并在返回之前将其置零,如果认为内存已经归零,并且忘记初始化它,使用calloc可以防止出现一些错误。
realloc: 当需要为某些东西(比如一个数组)分配空间,然后需要添加一些东西时,可以考虑使用它,该方法会申请一块更大的内存区域,同时将老的内存区域的内容复制一份到新的内存空间中,然后返回新内存空间指针。