进程
进程是由内核定义的抽象实体,该实体分配用以执行程序的各项系统资源,是拥有资源的基本单位。从内核的角度来看,进程由用户内存空间和一系列内核数据结构组成,其中用户内存空间包含程序代码及代码所使用的变量(程序段和数据段),而内核数据结构则用于维护进程状态信息。
每个进程都有一个进程号,用以标识系统中的某个进程。Linux内核限制进程号小于等于32767,一旦进程号达到32767,会将进程号计数器重置为300,因为低数值的进程为系统进程和守护进程长期占用。1号进程init进程为所有进程的始祖进程。
进程内存布局
每个进程所分配的内存由很多“段”组成。
- 文本段包含了进程运行的程序机器语言指令。
- 初始化数据段包含显式初始化的全局变量和静态变量。
- 未初始化数据段包含了未进行显式初始化的全局变量和静态变量。
- 栈是一个动态增长和收缩的段,由栈帧组成。系统为每个当前调用的函数分配一个栈帧,栈帧中存储函数的实参、局部变量,还有函数调用的链接信息。
- 堆是在运行时动态进行内存分配的一块区域。
虚拟内存管理
虚拟内存管理技术是基于大多数程序的一个典型特性:访问局部性,以求高效使用CPU和RAM资源。正是由于访问局部性,使得程序即使有部分地址空间存在于RAM,依旧可以执行。
- 空间局部性:是指程序倾向于访问在最近访问过的内存地址附近的内存
- 时间局部性:是指程序倾向于在不久的将来再次访问最近刚刚访问过的内存地址
虚拟内存规划之一就是将每个程序使用的内存切割成小型的、固定大小的“页”单元。任何一个时刻只有部分页驻留在物理内存(RAM)中,程序未使用的页拷贝保存在交换区。若进程欲访问的页面目前未驻留在物理内存中,将会发生页面错误,内核即可挂起进程的执行,同时从磁盘中将该页面载入内存。
为支持这一组织方式,内核需要为每个进程维护一张页表,该页表描述了每页在进程虚拟地址空间中的位置。页表中的每个条目要么指出一个虚拟页面在RAM中的位置,要么表明其当前驻留在磁盘上。
虚拟内存管理的优点:
- 进程与进程、进程与内核相互隔离。
- 适当情况下,两个或者更多进程可以共享内存。两个场景:执行同一个程序的多个进程可共享一份程序代码副本,进程可以使用shmget()和mmap()系统调用显示请求与其他进程共享内存区(共享内存,用于进程通信)
- 便于实现内存保护机制。
- 使得每个进程使用的RAM减少了,RAM中可以同时容纳的进程数量增多了,提高CPU利用率。
在堆上分配内存
进程可以通过增加堆的大小来分配内存,所谓堆是一段长度可变的连续虚拟内存,始于进程的未初始化数据段末尾,随着内存的分配和释放而增减,通常将堆的当前内存边界称为“program break”。
UNIX系统提供两个操纵program break的系统调用:brk()和sbrk()
#include <unistd.h>
int brk(void *end_data_segment);
void *sbrk(intptr_t increment);
系统调用brk()会将program break设置为参数所指定的位置。调用sbrk()将program break在原有地址上增加从参数increment传入的大小。
除此之外,C语言一般使用malloc和free在堆上分配和释放内存。主要优点:更易于在多线程程序中使用,接口简单,允许分配小块内存,允许随意释放内存块,维护一张空闲内存列表,在后续内存分配调用时使用。
#include <stdlib.h>
void *malloc(size_t size);
coid free(void *ptr);
malloc函数在堆上分配参数size字节大小的内存,并返回指向新分配内存起始位置的指针,所分配的内存未经初始化。若无法分配内存(program break已经达到地址上限),则malloc返回null。
一般情况下,free并不降低program break位置,而是将这块内存添加到空闲内存列表中,供后续malloc函数循环使用主要原因:
- 被释放的内存块通常会位于堆中间,所以不能降低program break
- 这样会减少程序必须执行sbrk()调用次数
- 在大多数情况下,程序通常倾向于持有已分配内存或是反复释放和重新分配内存
malloc和free的实现
malloc的实现过程:首先他会扫描之前由free释放的空闲内存块列表,以求找到尺寸大于或者等于要求的一块空闲内存,如果在空闲内存列表中根本找不到足够大的空闲内存块,那么malloc会调用sbrk()以分配更多的内存,为了减少sbrk()的调用次数, malloc并未按照严格的字节要求来分配内存,而是更大幅度的增加program break,将超出部分闲置于空闲内存列表(vector原理类似)
由于malloc在分配内存块时,会额外分配几个字节来存放记录这块内存大小的整数值,该整数值就位于内存的起始处。当free()使用内存块本身的空间来存放链表指针。(有点像双向链表)
当内存不断释放、分配,空闲列表中的空闲内存和已分配的内存混杂在一起,就如下图所示。
堆栈上分配内存:alloc()
alloc()也可以动态分配内存,不过不是从堆上分配内存,而是通过增加栈帧的大小,从堆栈上分配。当前调用函数的栈帧位于堆栈顶部,故这种方法是可行的(相当于栈帧从上往下扩展)
alloc()分配内存优点:
- 速度较快,因为编译器将alloc作为内联代码,并通过直接调用堆栈指针来实现。
- 由alloc分配的内存随栈帧的移除而自动释放,不需要free释放内存,即调用alloc的函数返回时。
参考 《TLPI》、《APUE》