内存管理
一、错误处理
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 < 0)
{
printf ("获取文件大小失败!\n");
return -1;
}
printf ("文件大小:%d字节\n", size);
return 0;
}
- 返回有效指针表示成功, 返回空指针(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 ()
{
const char* max = strmax ("hello", "world");
const char* max = strmax ("hello", NULL);
if (!max)
{
printf ("求字符串最大值失败!\n");
return -1;
}
printf ("字符串最大值:%s\n", max);
return 0;
}
- 返回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 ()
{
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;
}
- 永远成功,如:printf
2. 通过errno表示错误
#include <errno.h>
- 根据errno得到错误编号。
- 将errno转换为有意义的字符串
范例:errno.c
#include <stdio.h>
#include <errno.h>
int main ()
{
FILE* fp = fopen ("none", "r");
if (! fp)
{
printf ("fopen: %d\n", errno);
printf ("fopen: %s\n", strerror (errno));
printf ("fopen: %m\n");
perror ("fopen");
return -1;
}
fclose (fp);
return 0;
}
- errno在函数执行成功的情况下不会被修改,因此不能以errno非零,作为发生错误判断依据。
范例:iferr.c
#include <stdio.h>
#include <errno.h>
int main ()
{
FILE* fp = fopen ("none", "r");
fp = fopen ("/etc/passwd", "r");
if (errno)
{
perror ("fopen");
printf ("fp = %p\n", fp);
return -1;
}
fclose (fp);
return 0;
}
- errno是一个全局变量,其值随时可能发生变化。
二、环境变量
1. 环境表
•1) 每个程序都会接收到一张环境表,是一个以NULL指针结尾的字符指针数组。
•2) 全局变量environ保存环境表的起始地址。
.
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>
void printenv ()
{
printf ("---- 环境变量 ----\n");
extern char** environ;
char** env;
for (env = environ; env && *env; ++env)
printf ("%s\n", *env);
printf ("------------------\n");
}
int main ()
{
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;
}
三、内存管理
四、进程映像
-
程序是保存在磁盘上的可执行文件。
-
运行程序时,需要将可执行文件加载到内存,形成进程。
-
一个程序(文件)可以同时存在多个进程(内存)。
-
进程在内存空间中的布局就是进程映像,从低地址到高地址依次为:
•代码区(text): 可执行指令、字面值常量、具有常属性的全局和静态局部变量。只读。 •数据区(data): 初始化的全局和静态局部变量。 •BSS区: 未初始化的全局和静态局部变量。 进程一经加载此区即被清0。 数据区和BSS区有时被合称为全局区或静态区。 •堆区(heap): 动态内存分配。从低地址向高地址扩展。 •栈区(stack): 非静态局部变量, 包括函数的参数和返回值。 从高地址向低地址扩展。 堆区和栈区之间存在一块间隙,一方面为堆和栈的增长预留空间, 同时共享库、共享内存等亦位于此。 •命令行参数与环境区: 命令行参数和环境变量。
范例:maps.c
#include <stdio.h>
#include <stdlib.h>
const int const_global = 0; // 常全局变量
int init_global = 0; // 初始化全局变量
int uninit_global; // 未初始化全局变量
int main (int argc, char* argv[])
{
const static int const_static = 0; // 常静态变量
static int init_static = 0; // 初始化静态变量
static int uninit_static; // 未初始化静态变量
const int const_local = 0; // 常局部变量
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;
}
五、虚拟内存
-
每个进程都有各自互独立的4G字节虚拟地址空间。
-
用户程序中使用的都是虚拟地址空间中的地址,永远无法直接访问实际物理内存地址。
-
虚拟内存到物理内存的映射由操作系统动态维护。
-
虚拟内存一方面保护了操作系统的安全,另一方面允许应用程序,使用比实际物理内存更大的地址空间。
5. 4G进程地址空间分成两部分:
[0, 3G)为用户空间,
如某栈变量的地址0xbfc7fba0=3,217,554,336,约3G;
[3G, 4G)为内核空间。
-
用户空间中的代码
不能直接访问内核空间中的代码和数据, 但可以通过系统调用进入内核态, 间接地与系统内核交互。
-
对内存的越权访问,或试图访问没有映射到物理内存的虚拟内存,将导致段错误。
-
用户空间对应进程,进程一切换,用户空间即随之变化。
内核空间由操作系统内核管理,不会随进程切换而改变。 内核空间由内核根据独立且唯一的页表init_mm.pgd 进行内存映射,而用户空间的页表则每个进程一份。
-
每个进程的内存空间完全独立。不同进程之间交换虚拟内存地址是毫无意义的。
-
标准库内部通过一个双向链表,管理在堆中动态分配的内存
malloc函数分配内存时会附加若干(通常是12个)字节,存放控制信息。该信息一旦被意外损坏,可能在后续操作中引发异常。
范例:crash.c
#include <stdio.h>
#include <stdlib.h>
int main ()
{
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页仍然保留。
#include <unistd.h>
int getpagesize (void);
//返回内存页的字节数。
范例:page.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void presskey ()
{
printf ("查看/proc/%u/maps,按<回车>继续...", getpid ());
getchar ();
}
int main ()
{
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函数根据增量参数调整该指针的位置,同时返回该指针原来的位置。
若发现页耗尽或空闲,则自动追加或取消页映射。
2. 修改虚拟内存块末尾地址
#include <unistd.h>
int brk ( void* end_data_segment //内存块末尾地址);
//成功返回0,失败返回-1。
内部维护一个指针,指向当前堆内存最后一个字节的下一个位置。
brk函数根据指针参数设置该指针的位置。
若发现页耗尽或空闲,则自动追加或取消页映射。
sbrk/brk底层维护一个指针位置,以页(4K)为单位分配和释放虚拟内存。
简便起见,可用sbrk分配内存,用brk释放内存。
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 - 锁定映射区域,保证其不被置换。
4. 销毁虚拟内存到物理内存或文件的映射
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 (/*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的虚拟内存空间,虚拟内存地址只是一个数字,并没有和实际的物理内存将关系。
所谓内存分配与释放,其本质就是建立或取消虚拟内存和物理内存间的映射关系。