Unix C, Day02

================
第二课  内存管理
================

一、错误处理
------------

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;
}


思考:该堆栈模板是否适用于类类型的数据元素。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值