c /c++复习笔记 第二天
2017-02-16 16:32
351 查看
内存管理
一错误处理
通过函数的返回值表示错误
范例badc
范例nullc
范例failc
代码errorc
通过errno表示错误
范例errnoc
范例iferrc
二环境变量
环境表
环境变量函数
范例envc
三内存管理
四进程映像
范例mapsc
五虚拟内存
范例vmc
范例crashc
范例pagec
六内存管理APIs
增量方式分配虚拟内存
修改虚拟内存块末尾地址
范例brkcmallocc
创建虚拟内存到物理内存或文件的映射
范例mmapc
1) 返回合法值表示成功,返回非法值表示失败。
2) 返回有效指针表示成功,
返回空指针(NULL/0xFFFFFFFF)表示失败。
3) 返回0表示成功,返回-1表示失败,
不输出数据或通过指针/引用型参数输出数据。
4) 永远成功,如:printf()。
练习:实现四个函数
slen() - 求字符串的长度,若为空指针,则报错。
scpy() - 字符串拷贝,考虑缓冲区溢出,
成功返回目标缓冲区地址,
目标缓冲区无效时报错。
intmin() - 求两个整数的最小值,若二者相等,则报错。
intave() - 求两个整数的平均值,考虑求和溢出,
该函数不会失败。
1) 根据errno得到错误编号。
2) 将errno转换为有意义的字符串:
3) errno在函数执行成功的情况下不会被修改,
因此不能以errno非零,作为发生错误判断依据。
4) errno是一个全局变量,其值随时可能发生变化。
1) 每个程序都会接收到一张环境表,
是一个以NULL指针结尾的字符指针数组。
2) 全局变量environ保存环境表的起始地址。
图示:env_list.bmp
环境变量:name=value
getenv - 根据name获得value。
putenv - 以name=value的形式设置环境变量,
name不存在就添加,存在就覆盖其value。
setenv - 根据name设置value,注意最后一个参数表示,
若name已存在是否覆盖其value。
unsetenv - 删除环境变量。
clearenv - 清空环境变量,environ==NULL。
| 用 | 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
比对/proc//maps
》#size a.out
text data bss dec hex filename
2628 268 28 2924 b6c a.out
| | | | | |
+———+———+ (10) +———+———+ (16)
V ^
+————————————+
(+)
用户程序中使用的都是虚拟地址空间中的地址,
永远无法直接访问实际物理内存地址。
虚拟内存到物理内存的映射由操作系统动态维护。
虚拟内存一方面保护了操作系统的安全,
另一方面允许应用程序,
使用比实际物理内存更大的地址空间。
图示:vm.png
4G进程地址空间分成两部分:
[0, 3G)为用户空间,
如某栈变量的地址0xbfc7fba0=3,217,554,336,约3G;
[3G, 4G)为内核空间。
用户空间中的代码,
不能直接访问内核空间中的代码和数据,
但可以通过系统调用进入内核态,
间接地与系统内核交互。
图示:kernel.png
7. 对内存的越权访问,
或试图访问没有映射到物理内存的虚拟内存,
将导致段错误。
用户空间对应进程,进程一切换,用户空间即随之变化。
内核空间由操作系统内核管理,不会随进程切换而改变。
内核空间由内核根据独立且唯一的页表init_mm.pgd
进行内存映射,而用户空间的页表则每个进程一份。
每个进程的内存空间完全独立。
不同进程之间交换虚拟内存地址是毫无意义的。
标准库内部通过一个双向链表,
管理在堆中动态分配的内存。
malloc函数分配内存时会附加若干(通常是12个)字节,
存放控制信息。
该信息一旦被意外损坏,可能在后续操作中引发异常。
虚拟内存到物理内存的映射以页(4K=4096字节)为单位。
通过malloc函数首次分配内存,至少映射33页。
即使通过free函数释放掉全部内存,
最初的33页仍然保留。
图示:address_space.png
返回内存页的字节数。
分析:
char* pc = malloc (sizeof (char));
|
v<————— 33页 —————>|
——+——-+———-+——————-+——
| 1字节 | 控制信息 | |
——+——-+———-+——————-+——
^ ^ ^ ^ ^
段错误 OK 后续错误 不稳定 段错误
返回上次调用brk/sbrk后的末尾地址,失败返回-1。
increment取值:
0 - 获取末尾地址。
0 - 增加内存空间。
<0 - 释放内存空间。
内部维护一个指针,
指向当前堆内存最后一个字节的下一个位置。
sbrk函数根据增量参数调整该指针的位置,
同时返回该指针原来的位置。
若发现页耗尽或空闲,则自动追加或取消页映射。
void* p=sbrk(4); p=sbrk(0);
^ ^
| |
返回 – increment -> 返回
| |
v v
–+—+—+—+—+—+—+–
| B | B | B | B | B | B |
–+—+—+—+—+—+—+–
|<——— 页 ——–
成功返回0,失败返回-1。
内部维护一个指针,
指向当前堆内存最后一个字节的下一个位置。
brk函数根据指针参数设置该指针的位置。
若发现页耗尽或空闲,则自动追加或取消页映射。
void* p=sbrk(0); brk(p+4);
^ |
| v
返回 * * 设置
| |
v v
–+—+—+—+—+—+—+–
| B | B | B | B | B | B |
–+—+—+—+—+—+—+–
|<——— 页 ——–
sbrk/brk底层维护一个指针位置,
以页(4K)为单位分配和释放虚拟内存。
简便起见,可用sbrk分配内存,用brk释放内存。
malloc.c
成功返回映射区内存起始地址,失败返回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/munmap底层不维护任何东西,只是返回一个首地址,
所分配内存位于堆中。
brk/sbrk底层维护一个指针,记录所分配的内存结尾,
所分配内存位于堆中,底层调用mmap/munmap。
malloc底层维护一个双向链表和必要的控制信息,
不可越界访问,所分配内存位于堆中,底层调用brk/sbrk。
每个进程都有4G的虚拟内存空间,
虚拟内存地址只是一个数字,
并没有和实际的物理内存将关联。
所谓内存分配与释放,
其本质就是建立或取消虚拟内存和物理内存间的映射关系。
一错误处理
通过函数的返回值表示错误
范例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> // 获取文件大小 // 成功返回文件的字节数,失败返回-1 long 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> // 求字符串的最大值 // 成功返回参数字符串中的最大值,失败返回NULL const 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,失败返回-1 int 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)-1 size_t slen (const char* s) { if (! s) return -1; size_t len; for (len = 0; s[len]; ++len); return len; } // 字符串拷贝 // 成功返回目标字符串,失败返回NULL char* 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,失败返回-1 int 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=/root environ-->---: SHELL=/bin/bash environ-->---: 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=roselind", 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, "roselind", 0); printf ("%s=%s\n", name, getenv (name)); // 不存在就添加,存在就覆盖 setenv (name, "roselind", 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
7. 对内存的越权访问,
或试图访问没有映射到物理内存的虚拟内存,
将导致段错误。
用户空间对应进程,进程一切换,用户空间即随之变化。
内核空间由操作系统内核管理,不会随进程切换而改变。
内核空间由内核根据独立且唯一的页表init_mm.pgd
进行内存映射,而用户空间的页表则每个进程一份。
每个进程的内存空间完全独立。
不同进程之间交换虚拟内存地址是毫无意义的。
范例:vm.c
#include <stdio.h> int g_vm = 0; int main (void) { printf ("&g_vm = %p\n", &g_vm); printf ("整数:"); scanf ("%d%*c", &g_vm); printf ("启动另一个进程,输入不同的数据," "按<回车>继续..."); getchar (); printf ("g_vm = %d\n", g_vm); return 0; }
标准库内部通过一个双向链表,
管理在堆中动态分配的内存。
malloc函数分配内存时会附加若干(通常是12个)字节,
存放控制信息。
该信息一旦被意外损坏,可能在后续操作中引发异常。
范例:crash.c
#include <stdio.h> #include <stdlib.h> int main (void) { int* p1 = malloc (sizeof (int)); int* p2 = malloc (sizeof (int)); printf ("%p, %p\n", p1, p2); free (p2); p1[3] = 0; free (p1); return 0; }
虚拟内存到物理内存的映射以页(4K=4096字节)为单位。
通过malloc函数首次分配内存,至少映射33页。
即使通过free函数释放掉全部内存,
最初的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 256 int 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的虚拟内存空间,
虚拟内存地址只是一个数字,
并没有和实际的物理内存将关联。
所谓内存分配与释放,
其本质就是建立或取消虚拟内存和物理内存间的映射关系。
相关文章推荐
- 复习笔记二 二叉排序树(C++ C#)
- C++复习笔记
- effective C++第六章 复习笔记
- 《C++ Primer Plus》12.6 复习各种(类和动态内存分配的)技术 笔记
- C++复习笔记
- c++复习笔记--c++语言程序设计题典
- c /c++复习笔记 第一天
- C++复习笔记(一)文件结构
- C++复习笔记01
- C++复习笔记(3)类的多态性
- 复习C++ 走一遍基础 留下笔记----{一}
- 复习C++ 走一遍基础 留下笔记----{六}
- 复习C++ 走一遍基础 留下笔记----{五}
- effective C++ 第五章 复习笔记
- 【C/C++】C语言复习笔记-17种小算法-解决实际问题
- 【复习笔记】C++复习笔记一
- 复习C++ 走一遍基础 留下笔记----{四}
- 复习笔记 --C++之重载
- 第二天复习多线程的笔记整理
- C++静态成员复习笔记