内存管理一错误处理通过函数的返回值表示错误范例badc范例nullc范例failc代码errorc通过errno表示错误范例errnoc范例iferrc二环境变量环境表环境变量函数范例envc三内存管理四进程映像范例mapsc五虚拟内存范例vmc范例crashc范例pagec六内存管理APIs增量方式分配虚拟内存修改虚拟内存块末尾地址范例brkcmallocc创建虚拟内存到物理内存或文件的映射范例mmapc
内存管理
一、错误处理
1. 通过函数的返回值表示错误
~~~~~~~~~~~~~~~~~~~~~~~
1) 返回合法值表示成功,返回非法值表示失败。
范例:bad.c
#include <stdio.h>#include <limits.h>// 获取文件大小// 成功返回文件的字节数,失败返回-1long fsize (const char* path) { FILE* fp = fopen (path, "r"); if (! fp) return -1; fseek (fp, 0, SEEK_END); long size = ftell (fp); fclose (fp); return size;}int main (void) { PRintf ("文件路径:"); char path[PATH_MAX+1]; scanf ("%s", path); long size = fsize (path); if (size == -1) { printf ("文件大小获取失败!/n"); return -1; } printf ("文件大小:%ld字节/n", size); return 0;}2) 返回有效指针表示成功, 返回空指针(NULL/0xFFFFFFFF)表示失败。
范例:null.c
#include <stdio.h>#include <string.h>// 求字符串的最大值// 成功返回参数字符串中的最大值,失败返回NULLconst char* strmax (const char* a, const char* b) { return a && b ? (strcmp (a, b) > 0 ? a : b) : NULL;}int main (void) {// const char* max = strmax ("hello", "world"); const char* max = strmax ("hello", NULL); if (! max) { printf ("求字符串最大值失败!/n"); return -1; } printf ("字符串最大值:%s/n", max); return 0;}3) 返回0表示成功,返回-1表示失败, 不输出数据或通过指针/引用型参数输出数据。
范例:fail.c
#include <stdio.h>// 整数取模// 成功返回0,失败返回-1int intmod (int a, int b, int* mod) { if (b == 0) return -1; *mod = a % b; return 0;}int main (void) { printf ("两个整数:"); int a, b; scanf ("%d%d", &a, &b); int mod; if (intmod (a, b, &mod) == -1) { printf ("整数取模失败!/n"); return -1; } printf ("整数取模:%d/n", mod); return 0;}4) 永远成功,如:printf()。
练习:实现四个函数 slen() - 求字符串的长度,若为空指针,则报错。 scpy() - 字符串拷贝,考虑缓冲区溢出, 成功返回目标缓冲区地址, 目标缓冲区无效时报错。 intmin() - 求两个整数的最小值,若二者相等,则报错。 intave() - 求两个整数的平均值,考虑求和溢出, 该函数不会失败。
代码:error.c
#include <stdio.h>#define min(a,b) ((a)<(b)?(a):(b))// 求字符串长度// 成功返回字符串长度,失败返回(size_t)-1size_t slen (const char* s) { if (! s) return -1; size_t len; for (len = 0; s[len]; ++len); return len;}// 字符串拷贝// 成功返回目标字符串,失败返回NULLchar* scpy (char* dst, size_t size, const char* src) { if (! dst || ! size || ! src) return NULL; if (dst != src) { size_t i, count = min (size - 1, slen (src)); if (dst > src) for (i = 0; i < count; ++i) dst[count-1-i] = src[count-1-i]; else for (i = 0; i < count; ++i) dst[i] = src[i]; dst[count] = '/0'; } return dst;}// 求整数最小值,两个数相等视作失败// 成功返回0,失败返回-1int intmin (int a, int b, int* min) { if (a == b) return -1; *min = a < b ? a : b; return 0;}// 求整数平均值// 永远成功,不会失败int intave (int a, int b) { return (a & b) + ((a ^ b) >> 1);}int main (void) {#ifndef ERROR size_t len = slen ("Hello, World !");#else size_t len = slen (NULL);#endif if (len == -1) printf ("求字符串长度失败!/n"); else printf ("字符串长度:%u/n", len); char dst[5];#ifndef ERROR if (! scpy (dst, sizeof (dst) / sizeof (dst[0]), "0123456789"))#else if (! scpy (NULL, 0, "0123456789"))#endif printf ("字符串拷贝失败!/n"); else printf ("字符串副本:%s/n", dst); int min;#ifndef ERROR if (intmin (-1, 0, &min) == -1)#else if (intmin (-1, -1, &min) == -1)#endif printf ("求整数最小值失败!/n"); else printf ("整数最小值:%d/n", min); printf ("整数平均值:%d/n", intave (1234, 5678)); return 0;}2. 通过errno表示错误
~~~~~~~~~~~~~~~~
#include <errno.h>1) 根据errno得到错误编号。
2) 将errno转换为有意义的字符串:
#include <string.h>char* strerror (int errnum);#include <stdio.h>void perror (const char* s);printf ("%m");范例:errno.c
#include <stdio.h>#include <string.h>#include <errno.h>int main (void) { FILE* fp = fopen ("none", "r"); if (! fp) { printf ("fopen出错了:%d/n", errno); printf ("fopen出错了:%s/n", strerror (errno)); perror ("fopen出错了"); printf ("fopen出错了:%m/n"); return -1; } // ... fclose (fp); return 0;}3) errno在函数执行成功的情况下不会被修改, 因此不能以errno非零,作为发生错误判断依据。
范例:iferr.c
#include <stdio.h>#include <errno.h>void foo (void) { fopen ("none", "r");}int main (void) { foo (); FILE* fp = fopen ("/etc/passwd", "r"); if (errno) { perror ("fopen"); printf ("%p/n", fp); return -1; } // ... fclose (fp); return 0;}4) errno是一个全局变量,其值随时可能发生变化。
二、环境变量
1. 环境表
~~~~~
1) 每个程序都会接收到一张环境表, 是一个以NULL指针结尾的字符指针数组。
2) 全局变量environ保存环境表的起始地址。
environ-->---: HOME=/rootenviron-->---: SHELL=/bin/bashenviron-->---: PATH=/bin:/usr/bin:...:.environ-->---: |----|-----|图示:env_list.bmp
2. 环境变量函数
~~~~~~~~~~~
#include <stdlib.h>环境变量:name=value
getenv - 根据name获得value。
putenv - 以name=value的形式设置环境变量, name不存在就添加,存在就覆盖其value。
setenv - 根据name设置value,注意最后一个参数表示, 若name已存在是否覆盖其value。
unsetenv - 删除环境变量。
clearenv - 清空环境变量,environ==NULL。
范例:env.c
#include <stdio.h>#include <stdlib.h>void printenv (void) { printf ("---- 环境变量 ----/n"); extern char** environ; char** env; for (env = environ; env && *env; ++env) printf ("%s/n", *env); printf ("------------------/n");}int main (void) { char env[256]; const char* name="MYNAME"; // 添加环境变量 sprintf (env, "%s=minwei", name); putenv (env); printf ("%s=%s/n", name, getenv (name)); // 修改环境变量 sprintf (env, "%s=bjarne", name); putenv (env); printf ("%s=%s/n", name, getenv (name)); // 不存在就添加,存在不覆盖 setenv (name, "minwei", 0); printf ("%s=%s/n", name, getenv (name)); // 不存在就添加,存在就覆盖 setenv (name, "minwei", 1); printf ("%s=%s/n", name, getenv (name)); printenv (); // 删除环境变量 unsetenv (name); printenv (); // 清空环境变量 clearenv (); printenv (); return 0;}三、内存管理
+—-+——–+—————————-+———-+ | 用 | STL | 自动分配/释放内存资源 | 调C++ | | | C++ | new/delete,构造/析构 | 调标C | | 户 | 标C | malloc/calloc/realloc/free | 调POSIX | | | POSIX | brk/sbrk | 调linux | | 层 | Linux | mmap/munmap | 调Kernel | +—-+——–+—————————-+———-+ | 系 | Kernel | kmalloc/vmalloc | 调Driver | | 统 | Driver | get_free_page | … | | 层 | … | … | … | +—-+——–+—————————-+———-+
四、进程映像
程序是保存在磁盘上的可执行文件。
运行程序时,需要将可执行文件加载到内存,形成进程。
一个程序(文件)可以同时存在多个进程(内存)。
进程在内存空间中的布局就是进程映像。 从低地址到高地址依次为:
代码区(text):可执行指令、字面值常量、 具有常属性的全局和静态局部变量。只读。
数据区(data):初始化的全局和静态局部变量。
BSS区:未初始化的全局和静态局部变量。 进程一经加载此区即被清0。
数据区和BSS区有时被合称为全局区或静态区。
堆区(heap):动态内存分配。从低地址向高地址扩展。
栈区(stack):非静态局部变量, 包括函数的参数和返回值。从高地址向低地址扩展。
堆区和栈区之间存在一块间隙, 一方面为堆和栈的增长预留空间, 同时共享库、共享内存等亦位于此。
命令行参数与环境区:命令行参数和环境变量。
图示:maps.bmp
范例:maps.c
#include <stdio.h>#include <stdlib.h>#include <unistd.h>const int const_global = 10; // 常全局变量int init_global = 10; // 初始化全局变量int uninit_global; // 未初始化全局变量int main (int argc, char* argv[]) { const static int const_static = 10; // 常静态变量 static int init_static = 10; // 初始化静态变量 static int uninit_static; // 未初始化静态变量 const int const_local = 10; // 常局部变量 int prev_local; // 前局部变量 int next_local; // 后局部变量 int* prev_heap = malloc (sizeof (int)); // 前堆变量 int* next_heap = malloc (sizeof (int)); // 后堆变量 const char* literal = "literal"; // 字符串字面值 extern char** environ; // 环境变量 printf ("---- 命令行参数与环境变量 ---- <高>/n"); printf (" 环境变量:%p/n", environ); printf (" 命令行参数:%p/n", argv); printf ("-------------- 栈 ------------/n"); printf (" 常局部变量:%p/n", &const_local); printf (" 前局部变量:%p/n", &prev_local); printf (" 后局部变量:%p/n", &next_local); printf ("-------------- 堆 ------------/n"); printf (" 后堆变量:%p/n", next_heap); printf (" 前堆变量:%p/n", prev_heap); printf ("------------- BSS ------------/n"); printf (" 未初始化全局变量:%p/n", &uninit_global); printf (" 未初始化静态变量:%p/n", &uninit_static); printf ("------------ 数据 ------------/n"); printf (" 初始化静态变量:%p/n", &init_static); printf (" 初始化全局变量:%p/n", &init_global); printf ("------------ 代码 ------------/n"); printf (" 常静态变量:%p/n", &const_static); printf (" 字面值常量:%p/n", literal); printf (" 常全局变量:%p/n", &const_global); printf (" 函数:%p/n", main); printf ("------------------------------ <低>/n"); printf ("查看/proc/%u/maps,按<回车>退出...", getpid ()); getchar (); return 0;}比对/proc//maps
》#size a.out
text data bss dec hex filename 2628 268 28 2924 b6c a.out | | | | | | +———+———+ (10) +———+———+ (16) V ^ +————————————+ (+)
五、虚拟内存
每个进程都有各自互独立的4G字节虚拟地址空间。
用户程序中使用的都是虚拟地址空间中的地址, 永远无法直接访问实际物理内存地址。
虚拟内存到物理内存的映射由操作系统动态维护。
虚拟内存一方面保护了操作系统的安全, 另一方面允许应用程序, 使用比实际物理内存更大的地址空间。
图示:vm.png
4G进程地址空间分成两部分: [0, 3G)为用户空间, 如某栈变量的地址0xbfc7fba0=3,217,554,336,约3G; [3G, 4G)为内核空间。
用户空间中的代码, 不能直接访问内核空间中的代码和数据, 但可以通过系统调用进入内核态, 间接地与系统内核交互。
图示:kernel.png
%207.%20对内存的越权访问,%20%20%20%20或试图访问没有映射到物理内存的虚拟内存,%20%20%20%20将导致段错误。
用户空间对应进程,进程一切换,用户空间即随之变化。%20内核空间由操作系统内核管理,不会随进程切换而改变。%20内核空间由内核根据独立且唯一的页表init_mm.pgd%20进行内存映射,而用户空间的页表则每个进程一份。
每个进程的内存空间完全独立。%20不同进程之间交换虚拟内存地址是毫无意义的。
范例:vm.c
#include%20<stdio.h>int%20g_vm%20=%200;int%20main%20(void)%20{%20%20%20%20printf%20("&g_vm%20=%20%p/n",%20&g_vm);%20%20%20%20printf%20("整数:");%20%20%20%20scanf%20("%d%*c",%20&g_vm);%20%20%20%20printf%20("启动另一个进程,输入不同的数据,"%20%20%20%20%20%20%20%20"按<回车>继续...");%20%20%20%20getchar%20();%20%20%20%20printf%20("g_vm%20=%20%d/n",%20g_vm);%20%20%20%20return%200;}标准库内部通过一个双向链表,%20管理在堆中动态分配的内存。%20malloc函数分配内存时会附加若干(通常是12个)字节,%20存放控制信息。%20该信息一旦被意外损坏,可能在后续操作中引发异常。范例:crash.c
#include%20<stdio.h>#include%20<stdlib.h>int%20main%20(void)%20{%20%20%20%20int*%20p1%20=%20malloc%20(sizeof%20(int));%20%20%20%20int*%20p2%20=%20malloc%20(sizeof%20(int));%20%20%20%20printf%20("%p,%20%p/n",%20p1,%20p2);%20%20%20%20free%20(p2);%20%20%20%20p1[3]%20=%200;%20%20%20%20free%20(p1);%20%20%20%20return%200;}虚拟内存到物理内存的映射以页(4K=4096字节)为单位。%20通过malloc函数首次分配内存,至少映射33页。%20即使通过free函数释放掉全部内存,%20最初的33页仍然保留。图示:address_space.png
#include <unistd.h>int getpagesize (void);返回内存页的字节数。
范例:page.c
#include <stdio.h>#include <stdlib.h>#include <unistd.h>void presskey (void) { printf ("查看/proc/%u/maps,按<回车>继续...", getpid ()); getchar ();}int main (void) { printf ("1页 = %d字节/n", getpagesize ()); char* pc = malloc (sizeof (char)); printf ("pc = %p/n", pc); presskey (); free (pc); printf ("free(%p)/n", pc); presskey (); pc = malloc (sizeof (char) * 100); printf ("pc = %p/n", pc); presskey (); setbuf (stdout, NULL); size_t i = 0; for (;;) { printf ("向堆内存%p写...", &pc[i]); printf ("%c/n", pc[i++] = (i % 26) + 'A'); } return 0;}分析: char* pc = malloc (sizeof (char)); | v<————— 33页 —————>| ——+——-+———-+——————-+—— | 1字节 | 控制信息 | | ——+——-+———-+——————-+—— ^ ^ ^ ^ ^ 段错误 OK 后续错误 不稳定 段错误
六、内存管理APIs
1. 增量方式分配虚拟内存
~~~~~~~~~~~~~~~~~~~
#include <unistd.h>void* sbrk ( intptr_t increment // 内存增量(以字节为单位));返回上次调用brk/sbrk后的末尾地址,失败返回-1。
increment取值:
0 - 获取末尾地址。
0 - 增加内存空间。
<0 - 释放内存空间。
内部维护一个指针, 指向当前堆内存最后一个字节的下一个位置。 sbrk函数根据增量参数调整该指针的位置, 同时返回该指针原来的位置。 若发现页耗尽或空闲,则自动追加或取消页映射。
void* p=sbrk(4); p=sbrk(0); ^ ^ | | 返回 – increment -> 返回 | | v v –+—+—+—+—+—+—+– | B | B | B | B | B | B | –+—+—+—+—+—+—+– |<——— 页 ——–
2. 修改虚拟内存块末尾地址
~~~~~~~~~~~~~~~~~~~~~
#include <unistd.h>int brk ( void* end_data_segment // 内存块末尾地址);成功返回0,失败返回-1。
内部维护一个指针, 指向当前堆内存最后一个字节的下一个位置。 brk函数根据指针参数设置该指针的位置。 若发现页耗尽或空闲,则自动追加或取消页映射。
void* p=sbrk(0); brk(p+4); ^ | | v 返回 * * 设置 | | v v –+—+—+—+—+—+—+– | B | B | B | B | B | B | –+—+—+—+—+—+—+– |<——— 页 ——–
sbrk/brk底层维护一个指针位置, 以页(4K)为单位分配和释放虚拟内存。 简便起见,可用sbrk分配内存,用brk释放内存。
范例:brk.c、malloc.c
brk.c#include <stdio.h>#include <unistd.h>void presskey (void) { printf ("查看/proc/%u/maps,按<回车>继续...", getpid ()); getchar ();}int main (void) { void* p1 = sbrk (4); // RXXX ---- ---- ---- - printf ("p1 = %p/n", p1); void* p2 = sbrk (4); // XXXX RXXX ---- ---- - printf ("p2 = %p/n", p2); void* p3 = sbrk (4); // XXXX XXXX RXXX ---- - printf ("p3 = %p/n", p3); void* p4 = sbrk (4); // XXXX XXXX XXXX RXXX - printf ("p4 = %p/n", p4); void* p5 = sbrk (0); // XXXX XXXX XXXX XXXX R printf ("p5 = %p/n", p5); int* pn = (int*)p1; pn[0] = 0; pn[1] = 1; pn[2] = 2; pn[3] = 3; pn[1023] = 1023; printf ("%d, %d, %d, %d, %d/n", pn[0], pn[1], pn[2], pn[3], pn[1023]);// pn[1024] = 1024; void* p6 = sbrk (-8); // XXXX XXXX ---- ---- R printf ("p6 = %p/n", p6); void* p7 = sbrk (-8); // ---- ---- R--- ---- - printf ("p7 = %p/n", p7); printf ("--------/n"); int page = getpagesize (); printf ("%p/n", sbrk (page)); presskey (); printf ("%p/n", sbrk (1)); presskey (); printf ("%p/n", sbrk (-1)); presskey (); printf ("%p/n", sbrk (-page)); presskey (); printf ("--------/n"); p1 = sbrk (0); // R--- ---- ---- ---- - printf ("p1 = %p/n", p1); brk (p2 = p1 + 4); // XXXX S--- ---- ---- - printf ("p2 = %p/n", p2); brk (p3 = p2 + 4); // XXXX XXXX S--- ---- - printf ("p3 = %p/n", p3); brk (p4 = p3 + 4); // XXXX XXXX XXXX S--- - printf ("p4 = %p/n", p4); brk (p5 = p4 + 4); // XXXX XXXX XXXX XXXX S printf ("p5 = %p/n", p5); pn = (int*)p1; pn[0] = 0; pn[1] = 1; pn[2] = 2; pn[3] = 3; pn[1023] = 1023; printf ("%d, %d, %d, %d, %d/n", pn[0], pn[1], pn[2], pn[3], pn[1023]);// pn[1024] = 1024; brk (p3); // XXXX XXXX S--- ---- - brk (p1); // S--- ---- ---- ---- -// pn[0] = 0; printf ("--------/n"); void* begin = sbrk (sizeof (int)); if ((int)begin == -1) { perror ("sbrk"); return -1; } pn = (int*)begin; *pn = 1234; double* pd = (double*)sbrk (sizeof (double)); if ((int)pd == -1) { perror ("sbrk"); return -1; } *pd = 3.14; char* psz = (char*)sbrk (256 * sizeof (char)); if ((int)psz == -1) { perror ("sbrk"); return -1; } sprintf (psz, "Hello, World !"); printf ("%d, %g, %s/n", *pn, *pd, psz); if (brk (begin) == -1) { perror ("brk"); return -1; } return 0;}malloc.c#include <stdio.h>#include <stdbool.h>#include <unistd.h>// 内存控制块typedef struct mem_control_block { bool free; // 自由标志 struct mem_control_block* prev; // 前块指针 size_t size; // 本块大小} MCB;MCB* g_top = NULL; // 栈顶指针// 分配内存void* my_malloc (size_t size) { MCB* mcb; for (mcb = g_top; mcb; mcb = mcb->prev) if (mcb->free && mcb->size >= size) break; if (! mcb) { mcb = sbrk (sizeof (MCB) + size); if (mcb == (void*)-1) return NULL; mcb->prev = g_top; mcb->size = size; g_top = mcb; } mcb->free = false; return mcb + 1;}// 释放内存void my_free (void* ptr) { if (! ptr) return; MCB* mcb = (MCB*)ptr - 1; mcb->free = true; for (mcb = g_top; mcb->prev; mcb = mcb->prev) if (! mcb->free) break; if (mcb->free) { g_top = mcb->prev; brk (mcb); } else if (mcb != g_top) { g_top = mcb; brk ((void*)mcb + sizeof (mcb) + mcb->size); }}int main (void) { int* pa[10]; size_t size = sizeof (pa) / sizeof (pa[0]), i, j; for (i = 0; i < size; ++i) { if (! (pa[i] = my_malloc((i+1)*sizeof(int)))) { perror ("my_malloc"); return -1; } for (j = 0; j <= i; ++j) pa[i][j] = j; } for (i = 0; i < size; ++i) { for (j = 0; j <= i; ++j) printf ("%d ", pa[i][j]); printf ("/n"); } /* for (i = 0; i < size; ++i) my_free (pa[i]); */ for (;;) { my_free (pa[--i]); if (! i) break; } return 0;}3. 创建虚拟内存到物理内存或文件的映射
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#include <sys/mman.h>void* mmap ( void* start, // 映射区内存起始地址, // NULL系统自动选定,成功返回之 size_t length, // 字节长度,自动按页(4K)对齐 int prot, // 映射权限 int flags, // 映射标志 int fd, // 文件描述符 off_t offset // 文件偏移量,自动按页(4K)对齐);成功返回映射区内存起始地址,失败返回MAP_FAILED(-1)。
prot取值:
PROT_EXEC - 映射区域可执行。
PROT_READ - 映射区域可读取。
PROT_WRITE - 映射区域可写入。
PROT_NONE - 映射区域不可访问。
flags取值:
MAP_FIXED - 若在start上无法创建映射, 则失败(无此标志系统会自动调整)。
MAP_SHARED - 对映射区域的写入操作直接反映到文件中。
MAP_PRIVATE - 对映射区域的写入操作只反映到缓冲区中, 不会真正写入文件。
MAP_ANONYMOUS - 匿名映射, 将虚拟地址映射到物理内存而非文件, 忽略fd。
MAP_DENYWRITE - 拒绝其它对文件的写入操作。
MAP_LOCKED - 锁定映射区域,保证其不被置换。
销毁虚拟内存到物理内存或文件的映射 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~int munmap ( void* start, // 映射区内存起始地址 size_t length, // 字节长度,自动按页(4K)对齐 );
成功返回0,失败返回-1。
范例:mmap.c
#include <stdio.h>#include <unistd.h>#include <sys/mman.h>#define MAX_TEXT 256int main (void) { char* psz = (char*)mmap ( NULL, MAX_TEXT * sizeof (char), PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, 0, 0); if (psz == MAP_FAILED) { perror ("mmap"); return -1; } sprintf (psz, "Hello, Memory !"); printf ("%s/n", psz); printf ("psz = %p/n", psz); printf ("查看/proc/%u/maps,按<回车>退出...", getpid ()); getchar (); if (munmap (psz, MAX_TEXT * sizeof (char)) == -1) { perror ("munmap"); return -1; } return 0;}mmap/munmap底层不维护任何东西,只是返回一个首地址, 所分配内存位于堆中。
brk/sbrk底层维护一个指针,记录所分配的内存结尾, 所分配内存位于堆中,底层调用mmap/munmap。
malloc底层维护一个双向链表和必要的控制信息, 不可越界访问,所分配内存位于堆中,底层调用brk/sbrk。
每个进程都有4G的虚拟内存空间, 虚拟内存地址只是一个数字, 并没有和实际的物理内存将关联。 所谓内存分配与释放, 其本质就是建立或取消虚拟内存和物理内存间的映射关系。