================
第二课 内存管理
================
一、错误处理
------------
1. 通过函数的返回值表示错误
~~~~~~~~~~~~~~~~~~~~~~~~~~~
1) 返回合法值表示成功,返回非法值表示失败。
范例:bad.c
2) 返回有效指针表示成功,
返回空指针(NULL/0xFFFFFFFF)表示失败。
3) 返回0表示成功,返回-1表示失败,要传给主调函数的数据同通过输出参数(通过指针/引用型参数)输出数据。
范例:fail.c
4) 永远成功,不必考虑出现错误。如:printf()。再如 int avarage(int a, int b) {return (a & b) + ((a ^ b) >> 1);}
2. 通过定义在errno.h中的全局变量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");//%m可以打印错误信息
范例:errno.c
3) errno在函数执行成功的情况下不会被修改,因此不能以errno非零,作为发生错误判断依据。
4) errno是一个全局变量,其值随时可能发生变化。
二、环境变量
------------
1. 环境表(environment list)
~~~~~~~~~
#include <stdlib.h>
环境变量一般形式都是name=value, 例如HOME=/home/lotus 。程序中可以通过标准C库提供的函数修改环境变量
getenv - 根据name获得value。
putenv - 以name=value的形式设置环境变量, name不存在就添加,存在就覆盖其value。
setenv - 根据name设置value,注意最后一个参数表示, 若name已存在是否覆盖其value。
unsetenv - 删除环境变量。
clearenv - 清空环境变量,environ==NULL。
范例:env.c
三、内存管理
------------
内存管理函数调用层次关系
四、进程映像
------------
1. 程序是保存在磁盘上的可执行文件。 例如可执行文件a.out
#ls -l a.out
-rwxr-xr-x 1 lotus root 11008 6月 12 10:50 code/a.out
使用size命令可以显示二进制文件中节的大小, 例如 size a.out
# size a.out
text data bss dec hex filename
3244 568 40 3852 f0c a.out
2. 运行程序时,加载器需要将程序(可执行文件)加载到内存,形成进程。
3. 一个程序(可执行文件)可以同时存在多个进程。
例如:当同时挂了两个QQ,QQ程序就存在两个进程, 有两个不同的进程映像。所以说可执行文件和其进程、进程映像是一种一对多的对应关系,一个可执行文件可被加载器加载多次,加载一次形成一个进程、进程映像。
4. 进程在内存空间中的布局称为进程映像。进程和进程映像是一一对应的。
进程映像从低地址到高地址依次为:
代码区(text):可执行指令、字面值常量、 具有常属性的全局或静态局部变量。(代码区只读)。
数据区(data):初始化的全局和静态局部变量。 (p.s. .text 和 .data在程序(可执行文件)中也存在,当程序被加载形成进程时,这部分是直接拷贝到内存中的)
BSS区:未初始化的全局和静态局部变量。 当程序(可执行文件)被加载时,会立即将BSS区整块清0, 然后配给未初始化的静态局部变量和未初始化的全局变量。 (p.s.数据区和BSS区有时被合称为全局区或静态区。)
堆区(heap):动态内存分配。从低地址向高地址扩展。
栈区(stack):非静态局部变量, 包括函数的参数和返回值。从高地址向低地址扩展。
堆区和栈区之间存在一块间隙, 一方面为堆和栈的增长预留空间, 同时共享库、共享内存等亦位于此。
命令行参数与环境区:命令行参数和环境变量字符串。
图示:maps.bmp
五、虚拟内存
------------
1. 每个进程都有各自互独立的4G字节虚拟地址空间。
2. 用户程序中使用的都是虚拟地址空间中的地址,永远无法直接访问实际物理内存地址。
3. 虚拟内存到物理内存的映射由操作系统动态维护。
4. 虚拟内存一方面保护了操作系统的安全,另一方面允许应用程序,使用比实际物理内存更大的地址空间。
图示:kernel.png
8. 用户空间对应进程,进程一切换,用户空间即随之变化。内核空间由操作系统内核管理,不会随进程切换而改变。内核空间由内核根据独立且唯一的页表init_mm.pgd进行内存映射,而用户空间的页表则每个进程一份。
9. 每个进程的内存空间完全独立。不同进程之间交换虚拟内存地址是毫无意义的。
范例:vm.c
10. 标准库内部通过一个双向链表,管理在堆中动态分配的内存。malloc函数分配内存时会附加若干(通常是12个)字节,存放控制信息。该信息一旦被意外损坏,可能在后续操作中引发异常。
范例:crash.c
11. 虚拟内存到物理内存的映射以页(4K=4096字节)为单位。通过malloc函数首次分配内存,至少映射33页。即使通过free函数释放掉全部内存,最初的33页仍然保留。
int getpagesize (void); // 返回内存页的字节数。
六、内存管理APIs
----------------
1. 增量方式分配虚拟内存
~~~~~~~~~~~~~~~~~~~~~~~
#include <unistd.h>
void* sbrk (
intptr_t increment // 内存增量(以字节为单位)
);
返回上次调用brk/sbrk后的末尾地址,失败返回-1。
increment取值:
0 - 获取末尾地址。
>0 - 增加内存空间。
<0 - 释放内存空间。
内部维护一个指针,
指向当前堆内存最后一个字节的下一个位置。
sbrk函数根据增量参数调整该指针的位置,
同时返回该指针原来的位置。
通过sbrk函数分配内存按页映射,每次映射1页。 若发现页耗尽或空闲,则自动立即追加或立即取消页映射。
(p.s. malloc为了提高页映射效率free的时候并不一定会取消页映射,但是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释放内存。
3. 创建虚拟内存到物理内存或文件的映射
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#include <sys/mman.h>
void* mmap (
void* start, // 映射区内存起始地址,若给出的实参为 NULL 那么系统自动选定,成功返回之
size_t length, // 字节长度。因为映射的最小单位是页(page),所以映射时候不一定映射了length个字节,映射会自动按页(4Kbyte)对齐,不足一页按照一页映射。
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 - 锁定映射区域,保证其不被置换。
4. 销毁虚拟内存到物理内存或文件的映射
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
int munmap (
void* start, // 映射区内存起始地址
size_t length, // 字节长度,自动按页(4K)对齐
);
成功返回0,失败返回-1。
范例:mmap.c
mmap/munmap底层不维护任何东西,只是返回一个首地址,
所分配内存位于堆中。
brk/sbrk底层维护一个指针,记录所分配的内存结尾,
所分配内存位于堆中,底层调用mmap/munmap。
malloc底层维护一个双向链表和必要的控制信息,
不可越界访问,所分配内存位于堆中,底层调用brk/sbrk。
每个进程都有4G的虚拟内存空间,
虚拟内存地址只是一个数字,
并没有和实际的物理内存将关联。
所谓内存分配与释放,
其本质就是建立或取消虚拟内存和物理内存间的映射关系。
---------------------------------------------------------------------------------------------------
第二课 内存管理
================
一、错误处理
------------
1. 通过函数的返回值表示错误
~~~~~~~~~~~~~~~~~~~~~~~~~~~
1) 返回合法值表示成功,返回非法值表示失败。
范例:bad.c
/*
* 异常控制练习
* 通过返回值控制、管理异常
* 当函数返回值在某值域内,那么成功返回正确的值,失败则返回值域外的某一个值
* */
#include <stdio.h>
#include <limits.h>//定义了宏PATH_MAX
// 获取文件大小
// 成功返回文件大小,失败返回-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];//PATH_MAX:用来标识文件路径最大长度
scanf ("%s", path);
long size = fsize (path);
if (size < 0) {
printf ("获取文件大小失败!\n");
return -1;
}
printf ("文件大小:%d字节\n", size);
return 0;
}
2) 返回有效指针表示成功,
返回空指针(NULL/0xFFFFFFFF)表示失败。
范例:null.c
/*
* 异常管控练习
*
* 通过返回NULL来表示失败
* */
#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
/*
* 异常管控练习
*
*
* 当函数值域外找不着可以用来代表失败的值,那么可以考虑
* 返回0表示成功,返回-1表示失败,要传给主调函数的数据同通过输出参数(通过指针/引用型参数)输出数据。
*/
#include <stdio.h>
// 整数取模
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()。再如 int avarage(int a, int b) {return (a & b) + ((a ^ b) >> 1);}
/*
练习:实现四个函数
slen() - 求字符串的长度,若为空指针,则报错。
scpy() - 字符串拷贝,考虑缓冲区溢出,
成功返回目标缓冲区地址,
目标缓冲区无效时报错。
intmin() - 求两个整数的最小值,若二者相等,则报错。
intave() - 求两个整数的平均值,考虑求和溢出,
该函数不会失败。
*/
#include <stdio.h>
// 求字符串长度
// 成功返回字符串长度,失败返回(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)
return NULL;
size_t len = slen (src);
if (len == -1)
return NULL;
size_t i, chs = size - 1 < len ? size - 1 : len;
for (i = 0; i < chs; ++i)
dst[i] = src[i];
dst[i] = '\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) / 2;
return (a & b) + ((a ^ b) >> 1);
/* 理解平均值计算方法
* a = 55; b = 109;
* 55 = 00110111 = 00100101 + 00010010
* 109 = 01101101 = 00100101 + 01001000
* +--------------------
* 55 + 109 = (55 & 109)*2 + (55 ^ 109)
* (55 + 109) / 2 = (55 & 109) + ((55 ^ 109) >> 1)
*/
}
int main (void) {
size_t len = slen ("Hello World !");
// size_t len = slen (NULL);
if (len == -1)
printf ("求字符串长度失败!\n");
else
printf ("字符串长度:%u\n", len);
char dst[5];
if (! scpy (dst, sizeof (dst) / sizeof (dst[0]), "0123456789"))
// if (! scpy (NULL, 0, "0123456789"))
printf ("字符串拷贝失败!\n");
else
printf ("字符串副本:%s\n", dst);
int min;
if (intmin (-1, 0, &min) == -1)
// if (intmin (-1, -1, &min) == -1)
printf ("求整数最小值失败!\n");
else
printf ("整数最小值:%d\n", min);
printf ("整数平均值:%d\n", intave (1234, 5678));
return 0;
}
2. 通过定义在errno.h中的全局变量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");//%m可以打印错误信息
范例:errno.c
/*
*通过errno.h 中定义的全局变量errno表示错误
使用方法:
1) 根据errno得到错误编号。
2) 通过函数将errno转换为有意义的字符串:
#include <string.h>
char* strerror (int errnum);
另外两个重要的函数:
void perror (const char* s);
printf ("%m");//%m可以打印错误信息
*/
#include <stdio.h>
#include <errno.h>
#include <string.h>
int main (void) {
FILE* fp = fopen ("none", "r");
if (! fp) {
printf ("fopen:错误号: %d\n", errno);//errno是一个全局变量,其值随时可能发生变化。
printf ("fopen:错误字符串: %s\n", strerror (errno));
// errno在函数执行成功的情况下不会被修改, 因此不能以errno非零,作为发生错误判断依据。
perror ("fopen:错误信息前缀");//perror先输出给出的"fopen:错误信息前缀",然后会再调用strerror输出错误信息
printf ("fopen:附加信息 %m\n");
return -1;
}
fclose (fp);
return 0;
}
3) errno在函数执行成功的情况下不会被修改,因此不能以errno非零,作为发生错误判断依据。
范例:iferr.c
/*
* 使用全局变量errno来显示错误信息
*
* p.s. errno是一个全局变量,其值随时可能发生变化。
*/
#include <stdio.h>
#include <errno.h>
int main (void) {
FILE* fp = fopen ("none", "r");//执行失败,全局变量errno的值被赋值为2
fp = fopen ("/etc/passwd", "r");//执行成功,不对全局变量errno的值作任何处理
if (errno) {//errno在函数执行成功的情况下不会被修改, 因此不能以errno非零,作为发生错误判断依据。
perror ("fopen");
printf ("fp = %p\n", fp);
return -1;
}
fclose (fp);
return 0;
}
4) errno是一个全局变量,其值随时可能发生变化。
二、环境变量
------------
1. 环境表(environment list)
~~~~~~~~~
图示: env_list.bmp
2. 环境变量函数
~~~~~~~~~~~~~~~#include <stdlib.h>
环境变量一般形式都是name=value, 例如HOME=/home/lotus 。程序中可以通过标准C库提供的函数修改环境变量
getenv - 根据name获得value。
putenv - 以name=value的形式设置环境变量, name不存在就添加,存在就覆盖其value。
setenv - 根据name设置value,注意最后一个参数表示, 若name已存在是否覆盖其value。
unsetenv - 删除环境变量。
clearenv - 清空环境变量,environ==NULL。
范例:env.c
/*
* 通过预定义的全局变量environ打印进程的环境变量
*
* */
#include <stdio.h>
#include <stdlib.h>
void printenv (void) {
printf ("---- 当前进程的全部环境变量 ----\n");
extern char** environ;//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";
printenv();
// 添加环境变量
sprintf (env, "%s=LIBIN", name);//env: MYNAME=LIBIN
putenv (env);
// 获取名为name的环境变量的值
printf ("%s=%s\n", name, getenv (name));//输出MYNAME=LIBIN
// 修改环境变量
sprintf (env, "%s=CHENXIN", name);
putenv (env);//不存在就添加,存在就覆盖
printf ("%s=%s\n", name, getenv (name));//输出MYNAME=CHENXIN
setenv (name, "LIWEN", 0);//不存在就添加,存在就保持原来不覆盖
printf ("%s=%s\n", name, getenv (name));//输出MYNAME=CHENXIN
setenv (name, "WENWEN", 1);// 不存在就添加,存在就覆盖
printf ("%s=%s\n", name, getenv (name));//MYNAME=输出WENWEN
printenv();
unsetenv (name);//删除名为name的环境变量
printenv ();
// 清空环境变量
clearenv ();//清空当前进程的所有环境变量
printenv ();
return 0;
}
三、内存管理
------------
内存管理函数调用层次关系
四、进程映像
------------
1. 程序是保存在磁盘上的可执行文件。 例如可执行文件a.out
#ls -l a.out
-rwxr-xr-x 1 lotus root 11008 6月 12 10:50 code/a.out
使用size命令可以显示二进制文件中节的大小, 例如 size a.out
# size a.out
text data bss dec hex filename
3244 568 40 3852 f0c a.out
2. 运行程序时,加载器需要将程序(可执行文件)加载到内存,形成进程。
3. 一个程序(可执行文件)可以同时存在多个进程。
例如:当同时挂了两个QQ,QQ程序就存在两个进程, 有两个不同的进程映像。所以说可执行文件和其进程、进程映像是一种一对多的对应关系,一个可执行文件可被加载器加载多次,加载一次形成一个进程、进程映像。
4. 进程在内存空间中的布局称为进程映像。进程和进程映像是一一对应的。
进程映像从低地址到高地址依次为:
代码区(text):可执行指令、字面值常量、 具有常属性的全局或静态局部变量。(代码区只读)。
数据区(data):初始化的全局和静态局部变量。 (p.s. .text 和 .data在程序(可执行文件)中也存在,当程序被加载形成进程时,这部分是直接拷贝到内存中的)
BSS区:未初始化的全局和静态局部变量。 当程序(可执行文件)被加载时,会立即将BSS区整块清0, 然后配给未初始化的静态局部变量和未初始化的全局变量。 (p.s.数据区和BSS区有时被合称为全局区或静态区。)
堆区(heap):动态内存分配。从低地址向高地址扩展。
栈区(stack):非静态局部变量, 包括函数的参数和返回值。从高地址向低地址扩展。
堆区和栈区之间存在一块间隙, 一方面为堆和栈的增长预留空间, 同时共享库、共享内存等亦位于此。
命令行参数与环境区:命令行参数和环境变量字符串。
图示:maps.bmp
范例:maps.c
/*
*编辑程序打印代码段、数据段(.data .bss)、堆栈区中的地址, 从高到底! 包括命令行参数字符串、环境变量字符串
*/
#include <stdio.h>
#include <stdlib.h>
const int const_global = 0; // 常全局变量。存在于.text
int init_global = 0; // 初始化全局变量。存在于.data
int uninit_global; // 未初始化全局变量。存在于.bss
int main (int argc, char* argv[]) {
const static int const_static = 0; // 常静态变量。存在于.text
static int init_static = 0; // 初始化静态变量。存在于.data
static int uninit_static; // 未初始化静态变量。存在于.bss
const int const_local = 0; // 常局部变量。存在于stack
int prev_local; // 前局部变量.存在于stack。
int next_local; // 后局部变量.存在于stack。地址脚低
int* prev_heap = malloc (sizeof (int)); // 前堆变量
int* next_heap = malloc (sizeof (int)); // 后堆变量
const char* literal = "literal"; // 字面值常量.存在于.text
extern char** environ; // 环境变量.存在于conmand-line arguments and environment variables
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;
}
post script: size命令可以显示二进制文件节的大小
用法:size [选项] [文件]
显示二进制文件中节的大小
没有给出输入文件,默认为 a.out
The options are:
-A|-B --format={sysv|berkeley} Select output style (default is berkeley)
-o|-d|-x --radix={8|10|16} Display numbers in octal, decimal or hex
-t --totals Display the total sizes (Berkeley only)
--common Display total size for *COM* syms
--target=<bfdname> Set the binary file format
@<file> Read options from <file>
-h --help Display this information
-v --version Display the program's version
size:支持的目标: elf64-x86-64 elf32-i386 elf32-x86-64 a.out-i386-linux pei-i386 pei-x86-64 elf64-l1om elf64-k1om elf64-little elf64-big elf32-little elf32-big pe-i386 plugin srec symbolsrec verilog tekhex binary ihex
将 bug 报告到 <http://www.sourceware.org/bugzilla/ and mailto:hjl.tools@gmail.com>
五、虚拟内存
------------
1. 每个进程都有各自互独立的4G字节虚拟地址空间。
2. 用户程序中使用的都是虚拟地址空间中的地址,永远无法直接访问实际物理内存地址。
3. 虚拟内存到物理内存的映射由操作系统动态维护。
4. 虚拟内存一方面保护了操作系统的安全,另一方面允许应用程序,使用比实际物理内存更大的地址空间。
图示:vm.png
5. 4G进程地址空间分成两部分:
[3G, 4G)为内核空间。
[0, 3G)为用户空间,如某栈变量的地址0xbfc7fba0=3,217,554,336,约3G, 下图是用户地址空间到物理地址空间的映射示意图;
6. 用户空间中的代码,不能直接访问内核空间中的代码和数据,但可以通过系统调用进入内核态,间接地与系统内核交互。
图示:kernel.png
7. 对内存的越权访问,
或试图访问没有映射到物理内存的虚拟内存,将导致段错误。8. 用户空间对应进程,进程一切换,用户空间即随之变化。内核空间由操作系统内核管理,不会随进程切换而改变。内核空间由内核根据独立且唯一的页表init_mm.pgd进行内存映射,而用户空间的页表则每个进程一份。
9. 每个进程的内存空间完全独立。不同进程之间交换虚拟内存地址是毫无意义的。
范例: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;
}
10. 标准库内部通过一个双向链表,管理在堆中动态分配的内存。malloc函数分配内存时会附加若干(通常是12个)字节,存放控制信息。该信息一旦被意外损坏,可能在后续操作中引发异常。
范例:crash.c
/*
*文件名: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);
// p1
// |
// v
// IIIIPPPPFXXXNNNNIIII
// ^
// |
// p2
free (p2);
p1[3] = 0;//给p1后的第12-15字节(共4byte)放上一个整型的0
free (p1);
return 0;
}
11. 虚拟内存到物理内存的映射以页(4K=4096字节)为单位。通过malloc函数首次分配内存,至少映射33页。即使通过free函数释放掉全部内存,最初的33页仍然保留。
#include <unistd.h>
int getpagesize (void); // 返回内存页的字节数。
范例:page.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>// 函数getpagesize返回内存页大小
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));
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');
}
free (pc);
return 0;
}
六、内存管理APIs
----------------
1. 增量方式分配虚拟内存
~~~~~~~~~~~~~~~~~~~~~~~
#include <unistd.h>
void* sbrk (
intptr_t increment // 内存增量(以字节为单位)
);
返回上次调用brk/sbrk后的末尾地址,失败返回-1。
increment取值:
0 - 获取末尾地址。
>0 - 增加内存空间。
<0 - 释放内存空间。
内部维护一个指针,
指向当前堆内存最后一个字节的下一个位置。
sbrk函数根据增量参数调整该指针的位置,
同时返回该指针原来的位置。
通过sbrk函数分配内存按页映射,每次映射1页。 若发现页耗尽或空闲,则自动立即追加或立即取消页映射。
(p.s. malloc为了提高页映射效率free的时候并不一定会取消页映射,但是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释放内存。
/*
* 内存管理函数sbrk练习
* (POSIX)
* */
#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);
// pn[0] = 0;//ERROR//试图向未映射到物理内存的虚拟内存存放0导致段错误. 决不可以试图访问未映射到物理内存的虚拟内存
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, %lf, %s\n", *pn, *pd, psz);
if (brk (begin) == -1) {
perror ("brk");
return -1;
}
return 0;
}
/*
* 模仿标准C库的malloc/free实现my_malloc/my_free
* */
#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; // 栈顶指针
// +----------------------+ g_top
// v | |
// +------+------------+--|---+------------+------+------------+
// | prev | | prev | | prev | |
// | free | | free | | free | |
// | size | | size | | size | |
// +------+------------+------+------------+------+------------+
// MCB |<-- size -->|
// 分配内存
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 {
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] = (int*)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, // 字节长度。因为映射的最小单位是页(page),所以映射时候不一定映射了length个字节,映射会自动按页(4Kbyte)对齐,不足一页按照一页映射。
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 - 锁定映射区域,保证其不被置换。
4. 销毁虚拟内存到物理内存或文件的映射
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
int munmap (
void* start, // 映射区内存起始地址
size_t length, // 字节长度,自动按页(4K)对齐
);
成功返回0,失败返回-1。
范例:mmap.c
/*
* mmap/munmap练习
*
* mmap创建虚拟内存到物理内存或者文件的映射
* munmap销毁虚拟内存到物理内存或者文件的映射
*
* */
#include <stdio.h>
#include <unistd.h>
#include <sys/mman.h>
#define MAX_TEXT 256
int main (void) {
char* psz = (char*)mmap (/*sbrk (0)*/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, World !");
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的内存分配与释放函数。
*/
#include <iostream>
using namespace std;
template<class T = int> class Stack {
public:
Stack (void) : m_begin (sbrk (0)) {}
~Stack (void) {
brk (m_begin);
}
void push (const T& data) {
*(T*)sbrk (sizeof (T)) = data;
}
void pop (void) {
if (sbrk (0) != m_begin)
sbrk (-sizeof (T));
}
bool top (T& data) {
if (sbrk (0) != m_begin) {
data = *((T*)sbrk (0) - 1);
return true;
}
return false;
}
private:
void* m_begin;
};
int main (void) {
Stack<> sn;
for (int i = 0; i < 5; i++)
sn.push (i + 1);
for (int i; sn.top (i); sn.pop ())
cout << i << ' ';
cout << endl;
Stack<const char*> ss;
ss.push ("heze");
ss.push ("zibo");
ss.push ("jinan");
ss.push ("weifang");
ss.push ("qingdao");
for (const char* s; ss.top (s); ss.pop ())
cout << s << ' ';
cout << endl;
return 0;
}
思考:该堆栈模板是否适用于类类型的数据元素。