1. 环境变量
#env // 查看环境变量
环境表
1) 每个程序都会接收到一张环境表,
是一个以 NULL 指针结尾的字符指针数组。
2) 全局变量 environ 保存环境表的起始地址。
+---+
environ -> | * --> HOME=/root
+---+
| * --> SHELL=/bin/bash
+---+
| * --> PATH=/bin:/usr/bin:...:.
+---+
| . |
| . |
| . |
+---+
| 0 |
图示:env_list.bmp
范例:env.c
#include <stdio.h>
void printenv () {
extern char** environ; // 不是定义,只是声明,名字只能写 environ;
char** env;
for (env = environ; env && *env; ++env)
printf ("%s\n", *env);
}
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=beijing", name);
putenv (env);
printf("%s=%s\n", name, getenv (name));
setenv (name, "minwei", 0); // 如果存在,不覆盖;
printf("%s=%s\n", name, getenv (name));
printenv ();
return 0;
}
2. 环境变量函数
#include <stdlib.h>
环境变量:name=value
getenv - 根据 name 获得 value。
putenv - 以 name=value 的形式设置环境变量,
name 不存在就添加,存在就覆盖其 value。
setenv - 根据 name 设置 value,不存在就添加;注意最后一个参数表示,
最后一个参数为 0:若 name 已存,不覆盖其 value;
最后一个参数为 1:若 name 已存,覆盖其 value;
unsetenv - 删除一个环境变量。
clearenv - 清空环境变量,environ==NULL。
上面这些环境变量函数,都是改变本进程的环境变量,不会改变系统 shell 的环境变量;
当程序结束时,设置的所有环境变量也都将失效。
范例:env.c
#include <stdio.h>
void printenv () {
extern char** environ; // 不是定义,只是声明,名字只能写 environ;
char** env;
for (env = environ; env && *env; ++env)
printf ("%s\n", *env);
}
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=beijing", 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));
unsetenv (name);
return 0;
}
3. 内存管理
+----+--------+----------------------------+----------+
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 ...
. . . . . . . . .
+----+--------+----------------------------+----------+
4. 进程映像
1)程序是保存在磁盘上的可执行文件。
2)运行程序时,需要将可执行文件加载到内存,形成进程。
3)一个程序(文件)可以同时存在多个进程(内存)。
4)进程在内存空间中的布局就是进程映像。
从高地址到低地址依次为:
===============================================================
命令行参数与环境区:
命令行参数和环境变量;
---------------------------------------------------------
栈区(stack):0xbf
非静态局部变量,包括函数的参数和返回值;从高地址向低地址扩展; /* 所有局部(非静态)变量都位于这个区域 */
---------------------------------------------------------
堆区和栈区之间存在一块间隙(shared):0x40
一方面为堆和栈的增长预留空间,
同时共享库、共享内存等亦位于此;
(后面进程间通信会用到共享内存,参见 DAY09 “共享内存”)
---------------------------------------------------------
堆区(heap):0x08
动态内存分配;从低地址向高地址扩展;
---------------------------------------------------------
BSS区:
未初始化的全局和静态局部变量;进程一经加载此区即被清 0;
(数据区和BSS区有时被合称为全局区或静态区)
--------------------------------------------------------- /* 静态位于这个区域 */
数据区(data):
初始化的全局和静态局部变量;
---------------------------------------------------------
代码区(text):
可执行指令、字面值常量、具有常属性的全局和静态局部变量;只读;
===============================================================
范例: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 ("环境变量 %p\n", environ);
printf ("命令行参数 %p\n", argv);
// 栈
printf ("常局部变量 %p\n", &const_local);
printf ("前局部变量 %p\n", &prev_local);
printf ("后局部变量 %p\n", &next_local);
// 堆
printf ("后堆变量 %p\n", next_heap);
printf ("前堆变量 %p\n", prev_heap);
// BSS
printf ("未初始化全局变量 %p\n", &unint_global);
printf ("未初始化静态变量 %p\n", &unint_static);
// 数据
printf ("初始化静态变量 %p\n", &init_static);
printf ("初始化全局变量 %p\n", &init_global);
// 代码区
printf ("常静态变量 %p\n", &const_static);
printf ("字面值常量 %p\n", literal);
printf ("常全局变量 %p\n", &const_global);
printf ("主函数 %p\n", main);
// 查看 pid
printf ("查看/proc/%u/maps");
getpid();
// 用户不按回车,系统就不会结束,用于有时间查看 pid
getchar ();
return 0;
}
查看进程映像 pid
#vi /proc/<pid>/maps
#size a.out
text data bss dec hex filename
2628 268 28 2924 b6c a.out
| | | | |
+-------+-------+ (10) +---+---+ (16)
V ^
+-------------------+
(+)
堆和栈的区别:
1)申请方式
栈: 由系统自动分配。
例如,声明在函数中一个局部变量 int b; 系统自动在栈中为 b 开辟空间;
堆: 需要程序员自己申请,并指明大小,在 c 中 malloc 函数
如 p1 = (char*)malloc(10);
在 C++ 中用 new 运算符
如 p2 = (char*)malloc(10);
但是注意 p1、p2 本身是在栈中的。
2)申请后系统的响应
栈:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。
堆:首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,
寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序,
另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,
这样,代码中的 delete 语句才能正确的释放本内存空间。
另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。
3)申请大小的限制
栈:在 Windows 下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。
这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,
在 WINDOWS 下,栈的大小是 2M(也有的说是 1M,总之是一个编译时就确定的常数),
如果申请的空间超过栈的剩余空间时,将提示 overflow。因此,能从栈获得的空间较小。
堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,
自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。
由此可见,堆获得的空间比较灵活,也比较大。
4)申请效率的比较:
栈: 由系统自动分配,速度较快。但程序员是无法控制的。
堆: 是由 new 分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便.
另外,在 WINDOWS 下,最好的方式是用 Virtual Alloc 分配内存,他不是在堆,也不是在栈,
而是直接在进程的地址空间中保留一块内存,虽然用起来最不方便。但是速度快,也最灵活。
5)堆和栈中的存储内容
栈:在函数调用时,第一个进栈的是主函数中后的下一条指令(函数调用语句的下一条可执行语句)的地址,
然后是函数的各个参数,在大多数的 C 编译器中,参数是由右往左入栈的,然后是函数中的局部变量。
注意:静态变量是不入栈的。
当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,
也就是主函数中的下一条指令,程序由该点继续运行。
堆:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容由程序员安排。
6)存取效率的比较
char s1[]="aaaaaaaaaaaaaaa";
char *s2="bbbbbbbbbbbbbbbbb";
aaaaaaaaaaa 是在运行时刻赋值的;
而 bbbbbbbbbbb 是在编译时就确定的;
但是,在以后的存取中,在栈上的数组比指针所指向的字符串(例如堆)快。
比如:
#include
void main() {
char a=1;
char c[]="1234567890";
char *p="1234567890";
a = c[1];
a = p[1];
return;
}
对应的汇编代码
10:a=c[1];
004010678A4DF1movcl,byteptr[ebp-0Fh]
0040106A884DFCmovbyteptr[ebp-4],cl
11:a=p[1];
0040106D8B55ECmovedx,dwordptr[ebp-14h]
004010708A4201moval,byteptr[edx+1]
004010738845FCmovbyteptr[ebp-4],al
第一种在读取时直接就把字符串中的元素读到寄存器 cl 中,
而第二种则要先把指针值读到 edx 中,在根据 edx 读取字符,显然慢了。
5. 虚拟内存
1)每个进程都有各自互独立的 4G 字节虚拟地址空间:
4G 进程地址空间分成两部分:
[0, 3G) 为用户空间,
如某栈变量的地址 0xbfc7fba0 = 3,217,554,336,约 3G;
[3G, 4G) 为内核空间。
2)用户程序中使用的都是虚拟地址空间中的地址,
永远无法直接访问实际物理内存地址;
用户空间中的代码,不能直接访问内核空间中的代码和数据,
但可以通过系统调用进入内核态,间接地与系统内核交互。
用户空间对应进程,进程一切换,用户空间即随之变化;
内核空间由操作系统内核管理,不会随进程切换而改变;
内核空间由内核根据独立且唯一的页表 init_mm.pgd 进行内存映射,而用户空间的页表则每个进程一份;
所以,两个同时运行的进程,即便拿到的地址一样,那也是虚拟地址,他们映射的物理地址一定是不一样的;
每个进程的内存空间完全独立。
不同进程之间交换虚拟内存地址是毫无意义的。
对内存的越权访问,或试图访问没有映射到物理内存的虚拟内存,将导致段错误。
3)虚拟内存到物理内存的映射由操作系统动态维护。
4)虚拟内存一方面保护了操作系统的安全,
另一方面允许应用程序,使用比实际物理内存更大的地址空间。
图示:vm.png
图示:kernel.png
范例:vm.c
#include <stdio.h>
int g_vm = 0;
int mian () {
printf ("%p\n", &g_vm);
scanf ("%d%*c", &g_vm); // %*c 忽略回车
// 启动另一个进程,按回车继续
getchar ();
printf ("%d\n", g_vm);
}
6. 标准库内部通过一个双向链表,管理在堆中动态分配的内存。
malloc 函数分配内存时会附加若干(通常是 12 个)字节,存放控制信息;
该信息一旦被意外损坏,可能在后续操作中引发异常。
范例:crash.c
#include <stdio.h>
#include <stdlib.h> // malloc
int main () {
int* p1 = malloc (sizeof (int));
int* p2 = malloc (sizeof (int));
printf ("%p\n", p1);
printf ("%p\n", p2);
free (p2);
free (p1);
return 0;
}
输出结果会发现,p1 和 p2 之间相差 16 字节,而非 4 字节;
所以 p1 和 p2 之间有 12 字节的空间,用于存放控制信息;
如果在 free (p2); 和 free (p1); 之间加入 p1[3] = 0;
导致段错误;
7. 页
虚拟内存到物理内存的映射以页 (4K = 4096 字节) 为单位。
通过 malloc 函数首次分配内存,至少映射 33 页。
即使通过 free 函数释放掉全部内存,
最初的 33 页仍然保留。
(下面的 sbrk/mmap 一次分配一页)
图示:address_space.png
#include <unistd.h>
int getpagesize (void);
返回内存 1 页的字节数。
范例:page.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void presskey () {
printf ("查看/proc/%u/maps,按<enter>继续 . . .", getpid ());
getchar ();
}
int main () {
printf ("%d\n", getpagesize ());
char* pc = malloc (sizeof (char));
printf ("%p\n", pc);
presskey ();
free (pc);
presskey ();
setbuf (stdout, NULL); // 关闭输出缓冲区,保证下面 printf 可以正常输出;
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 后续错误 不稳定 段错误
8. 内存管理 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
#include <stdio.h>
#include <unistd.h>
void presskey () {
printf ("查看/proc/%u/maps,按<enter>继续 . . .", getpid ());
getchar ();
}
int main () {
// sbrk 举例
void* p1 = sbrk (4); // RXXX(R 为 sbrk 返回的地址)
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 (0); // XXXX XXXX XXXX R
printf ("p4 = %p\n", p4);
int* pn = (int*)p1;
pn[0] = 0; // OK
pn[1] = 1; // OK
pn[2] = 2; // OK
pn[1023] = 1023; // OK
pn[1024] = 1024; // 段错误
void* p5 = sbrk (-8); // XXXX ---- ---- R
void* p6 = sbrk (-4); // ---R(返回上一次 sbrk 调用后的地址)
pn[0] = 10; // 段错误;和 malloc 不一样,映射结束;
int page = getpagesize ();
printf ("%p\n", sbrk (page));
printf ("%p\n", sbrk (1));
printf ("%p\n", sbrk (-page));
printf ("%p\n", sbrk (-1));
// brk 举例
p1 = sbrk (0); // 用 brk 需要先计算初始地址
printf ("%p\n", p1);
brk (p2 = p1 + 4); // XXXX S
brk (p3 = p2 + 4); // XXXX XXXX S
brk (p4 = p3 + 4); // XXXX XXXX XXXX S
pn = (int*)p1;
pn[0] = 0; // OK
pn[1] = 1; // OK
pn[2] = 2; // OK
pn[1023] = 1023; // OK
pn[1024] = 1024; // 段错误
brk (p2); // XXXX S
brk (p1); // S
pn[0] = 10; // 段错误;
// 用 sbrk 分配内存:
void* begin = sbrk (sizeof (int));
pn = (int*)begin;
*pn = 1234;
double* pd = (double*)sbrk (sizeof (double));
*pd = 3014;
char* psz = (char*)sbrk (sizeof (char) * 256);
sprintf (psz, "hello word");
brk (begin); // 一下子全部释放;
return 0;
}
分析:p1,p2,p3 之间相差 4 字节,没用 malloc,所以没有多加 12 字节;
9. 练习:自己实现 malloc
思路:定义一个结构体,表示 12 字节的内存控制块;
申请内存的时候,从后往前遍历,遇到空闲内存块大于等于需要内存的时候,就直接返回该内存块地址;
否则,就需要自己新定义一个内存(页);
#include <stdio.h>
#inlcude <unistd.h>
#include <stdbool.h>
// 内存控制块 12 字节
typedef struct men_control_block { // 重命名
bool free; // 自由标志,用于判断本块内存是否闲置
struct men_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->prev) // 在现有页中,从后往前遍历
if (mcb-prev && mcb->size >= size) // 找到合适大小的内存块
break;
if (! mcb) { // 现有页中,没找到合适大小的内存块
mcb = sbrk (sizeof (MCB) + size); // 重新分配一页内存
if (mcb == (void*)-1) // 分配失败
// sbrk 分配失败返回 -1,但是 mcb 是指针类型,所以对 -1 进行强制类型转换;
return NULL; // malloc 分配失败也是返回 NULL,同步;
mcb->prev = g_top;
mcb->size = size;
g_top = mcb;
}
// 无论是在现有里面找到还是新分配的,都需要把标志置为零;
mcb->free = false; // 0 说明这块内存闲置;
return mcb + 1;
// 此时 mcb 指向控制块首部,+1 相当于加 12 字节,指向控制块尾部
}
// 释放内存
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 (g_top != mcb) {
g_top = mcb;
brk ((void*)mcb + sizeof (mcb) +mcb->size);
}
}
int main () {
int* p1 = my_malloc (10 * sizeof (int));
int* p1 = my_malloc (5 * sizeof (int));
size_t i;
for (i = 0; i < 10; ++i)
p1[i] = i + 1;
for (i = 0; i < 10; ++i)
p2[i] = 10 + i + 1;
my_free (p1);
my_free (p2);
}
10. 创建虚拟内存到物理内存或文件的映射 mmap
#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 - 锁定映射区域,保证其不被置换。
11. 销毁虚拟内存到物理内存或文件的映射 munmap
int munmap (
void* start, // 映射区内存起始地址
size_t length, // 字节长度,自动按页(4K)对齐
);
成功返回 0,失败返回-1。
mmap/munmap 底层不维护任何东西,只是返回一个首地址,所分配内存位于堆中。
brk/sbrk 底层维护一个指针,记录所分配的内存结尾,
所分配内存位于堆中,底层调用 mmap/munmap。
malloc 底层维护一个双向链表和必要的控制信息,
不可越界访问,所分配内存位于堆中,底层调用 brk/sbrk。
切记:malloc 和 brk/sbrk 只能用其一,不要混合使用,会出错;
每个进程都有 4G 的虚拟内存空间,虚拟内存地址只是一个数字,并没有和实际的物理内存相关联。
所谓内存分配与释放,其本质就是建立或取消虚拟内存和物理内存间的映射关系。
范例:mmap.c
#include <stdio.h>
#include <unistd.h>
#include <sys/mman.h>
#define MAX_TEXT 256
int mian () {
char* psz = (char*)mmap (NULL, // 返回为地址,需要强制类型转换
MAX_TEXT * sizeof (char), // 分配大小
PROT_READ | PROT_WRITE, // 可读可写
MAP_PRIVATE | MAP_ANONYMOUS, // 不用文件
0, 0);
if (psz == MAP_FAILED) { // if (psz == -1)
perror ("mmap");
return -1;
}
sprintf (psz, "hello wrod");
printf ("%s\n", psz);
printf ("%p\n", psz);
if (munmap (psz, MAX_TEXT * sizeof (char)) == -1) {
perror ("munmap");
return -1;
}
}
作业:实现一个基于顺序表的堆栈类模板,
其数据缓冲区内存可根据数据元素的多少自动增减,
但不得使用标准C的内存分配与释放函数。
代码:stack.cpp
思考:该堆栈模板是否适用于类类型的数据元素。
#env // 查看环境变量
环境表
1) 每个程序都会接收到一张环境表,
是一个以 NULL 指针结尾的字符指针数组。
2) 全局变量 environ 保存环境表的起始地址。
+---+
environ -> | * --> HOME=/root
+---+
| * --> SHELL=/bin/bash
+---+
| * --> PATH=/bin:/usr/bin:...:.
+---+
| . |
| . |
| . |
+---+
| 0 |
图示:env_list.bmp
范例:env.c
#include <stdio.h>
void printenv () {
extern char** environ; // 不是定义,只是声明,名字只能写 environ;
char** env;
for (env = environ; env && *env; ++env)
printf ("%s\n", *env);
}
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=beijing", name);
putenv (env);
printf("%s=%s\n", name, getenv (name));
setenv (name, "minwei", 0); // 如果存在,不覆盖;
printf("%s=%s\n", name, getenv (name));
printenv ();
return 0;
}
2. 环境变量函数
#include <stdlib.h>
环境变量:name=value
getenv - 根据 name 获得 value。
putenv - 以 name=value 的形式设置环境变量,
name 不存在就添加,存在就覆盖其 value。
setenv - 根据 name 设置 value,不存在就添加;注意最后一个参数表示,
最后一个参数为 0:若 name 已存,不覆盖其 value;
最后一个参数为 1:若 name 已存,覆盖其 value;
unsetenv - 删除一个环境变量。
clearenv - 清空环境变量,environ==NULL。
上面这些环境变量函数,都是改变本进程的环境变量,不会改变系统 shell 的环境变量;
当程序结束时,设置的所有环境变量也都将失效。
范例:env.c
#include <stdio.h>
void printenv () {
extern char** environ; // 不是定义,只是声明,名字只能写 environ;
char** env;
for (env = environ; env && *env; ++env)
printf ("%s\n", *env);
}
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=beijing", 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));
unsetenv (name);
return 0;
}
3. 内存管理
+----+--------+----------------------------+----------+
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 ...
. . . . . . . . .
+----+--------+----------------------------+----------+
4. 进程映像
1)程序是保存在磁盘上的可执行文件。
2)运行程序时,需要将可执行文件加载到内存,形成进程。
3)一个程序(文件)可以同时存在多个进程(内存)。
4)进程在内存空间中的布局就是进程映像。
从高地址到低地址依次为:
===============================================================
命令行参数与环境区:
命令行参数和环境变量;
---------------------------------------------------------
栈区(stack):0xbf
非静态局部变量,包括函数的参数和返回值;从高地址向低地址扩展; /* 所有局部(非静态)变量都位于这个区域 */
---------------------------------------------------------
堆区和栈区之间存在一块间隙(shared):0x40
一方面为堆和栈的增长预留空间,
同时共享库、共享内存等亦位于此;
(后面进程间通信会用到共享内存,参见 DAY09 “共享内存”)
---------------------------------------------------------
堆区(heap):0x08
动态内存分配;从低地址向高地址扩展;
---------------------------------------------------------
BSS区:
未初始化的全局和静态局部变量;进程一经加载此区即被清 0;
(数据区和BSS区有时被合称为全局区或静态区)
--------------------------------------------------------- /* 静态位于这个区域 */
数据区(data):
初始化的全局和静态局部变量;
---------------------------------------------------------
代码区(text):
可执行指令、字面值常量、具有常属性的全局和静态局部变量;只读;
===============================================================
范例: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 ("环境变量 %p\n", environ);
printf ("命令行参数 %p\n", argv);
// 栈
printf ("常局部变量 %p\n", &const_local);
printf ("前局部变量 %p\n", &prev_local);
printf ("后局部变量 %p\n", &next_local);
// 堆
printf ("后堆变量 %p\n", next_heap);
printf ("前堆变量 %p\n", prev_heap);
// BSS
printf ("未初始化全局变量 %p\n", &unint_global);
printf ("未初始化静态变量 %p\n", &unint_static);
// 数据
printf ("初始化静态变量 %p\n", &init_static);
printf ("初始化全局变量 %p\n", &init_global);
// 代码区
printf ("常静态变量 %p\n", &const_static);
printf ("字面值常量 %p\n", literal);
printf ("常全局变量 %p\n", &const_global);
printf ("主函数 %p\n", main);
// 查看 pid
printf ("查看/proc/%u/maps");
getpid();
// 用户不按回车,系统就不会结束,用于有时间查看 pid
getchar ();
return 0;
}
查看进程映像 pid
#vi /proc/<pid>/maps
#size a.out
text data bss dec hex filename
2628 268 28 2924 b6c a.out
| | | | |
+-------+-------+ (10) +---+---+ (16)
V ^
+-------------------+
(+)
堆和栈的区别:
1)申请方式
栈: 由系统自动分配。
例如,声明在函数中一个局部变量 int b; 系统自动在栈中为 b 开辟空间;
堆: 需要程序员自己申请,并指明大小,在 c 中 malloc 函数
如 p1 = (char*)malloc(10);
在 C++ 中用 new 运算符
如 p2 = (char*)malloc(10);
但是注意 p1、p2 本身是在栈中的。
2)申请后系统的响应
栈:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。
堆:首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,
寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序,
另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,
这样,代码中的 delete 语句才能正确的释放本内存空间。
另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。
3)申请大小的限制
栈:在 Windows 下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。
这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,
在 WINDOWS 下,栈的大小是 2M(也有的说是 1M,总之是一个编译时就确定的常数),
如果申请的空间超过栈的剩余空间时,将提示 overflow。因此,能从栈获得的空间较小。
堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,
自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。
由此可见,堆获得的空间比较灵活,也比较大。
4)申请效率的比较:
栈: 由系统自动分配,速度较快。但程序员是无法控制的。
堆: 是由 new 分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便.
另外,在 WINDOWS 下,最好的方式是用 Virtual Alloc 分配内存,他不是在堆,也不是在栈,
而是直接在进程的地址空间中保留一块内存,虽然用起来最不方便。但是速度快,也最灵活。
5)堆和栈中的存储内容
栈:在函数调用时,第一个进栈的是主函数中后的下一条指令(函数调用语句的下一条可执行语句)的地址,
然后是函数的各个参数,在大多数的 C 编译器中,参数是由右往左入栈的,然后是函数中的局部变量。
注意:静态变量是不入栈的。
当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,
也就是主函数中的下一条指令,程序由该点继续运行。
堆:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容由程序员安排。
6)存取效率的比较
char s1[]="aaaaaaaaaaaaaaa";
char *s2="bbbbbbbbbbbbbbbbb";
aaaaaaaaaaa 是在运行时刻赋值的;
而 bbbbbbbbbbb 是在编译时就确定的;
但是,在以后的存取中,在栈上的数组比指针所指向的字符串(例如堆)快。
比如:
#include
void main() {
char a=1;
char c[]="1234567890";
char *p="1234567890";
a = c[1];
a = p[1];
return;
}
对应的汇编代码
10:a=c[1];
004010678A4DF1movcl,byteptr[ebp-0Fh]
0040106A884DFCmovbyteptr[ebp-4],cl
11:a=p[1];
0040106D8B55ECmovedx,dwordptr[ebp-14h]
004010708A4201moval,byteptr[edx+1]
004010738845FCmovbyteptr[ebp-4],al
第一种在读取时直接就把字符串中的元素读到寄存器 cl 中,
而第二种则要先把指针值读到 edx 中,在根据 edx 读取字符,显然慢了。
5. 虚拟内存
1)每个进程都有各自互独立的 4G 字节虚拟地址空间:
4G 进程地址空间分成两部分:
[0, 3G) 为用户空间,
如某栈变量的地址 0xbfc7fba0 = 3,217,554,336,约 3G;
[3G, 4G) 为内核空间。
2)用户程序中使用的都是虚拟地址空间中的地址,
永远无法直接访问实际物理内存地址;
用户空间中的代码,不能直接访问内核空间中的代码和数据,
但可以通过系统调用进入内核态,间接地与系统内核交互。
用户空间对应进程,进程一切换,用户空间即随之变化;
内核空间由操作系统内核管理,不会随进程切换而改变;
内核空间由内核根据独立且唯一的页表 init_mm.pgd 进行内存映射,而用户空间的页表则每个进程一份;
所以,两个同时运行的进程,即便拿到的地址一样,那也是虚拟地址,他们映射的物理地址一定是不一样的;
每个进程的内存空间完全独立。
不同进程之间交换虚拟内存地址是毫无意义的。
对内存的越权访问,或试图访问没有映射到物理内存的虚拟内存,将导致段错误。
3)虚拟内存到物理内存的映射由操作系统动态维护。
4)虚拟内存一方面保护了操作系统的安全,
另一方面允许应用程序,使用比实际物理内存更大的地址空间。
图示:vm.png
图示:kernel.png
范例:vm.c
#include <stdio.h>
int g_vm = 0;
int mian () {
printf ("%p\n", &g_vm);
scanf ("%d%*c", &g_vm); // %*c 忽略回车
// 启动另一个进程,按回车继续
getchar ();
printf ("%d\n", g_vm);
}
6. 标准库内部通过一个双向链表,管理在堆中动态分配的内存。
malloc 函数分配内存时会附加若干(通常是 12 个)字节,存放控制信息;
该信息一旦被意外损坏,可能在后续操作中引发异常。
范例:crash.c
#include <stdio.h>
#include <stdlib.h> // malloc
int main () {
int* p1 = malloc (sizeof (int));
int* p2 = malloc (sizeof (int));
printf ("%p\n", p1);
printf ("%p\n", p2);
free (p2);
free (p1);
return 0;
}
输出结果会发现,p1 和 p2 之间相差 16 字节,而非 4 字节;
所以 p1 和 p2 之间有 12 字节的空间,用于存放控制信息;
如果在 free (p2); 和 free (p1); 之间加入 p1[3] = 0;
导致段错误;
7. 页
虚拟内存到物理内存的映射以页 (4K = 4096 字节) 为单位。
通过 malloc 函数首次分配内存,至少映射 33 页。
即使通过 free 函数释放掉全部内存,
最初的 33 页仍然保留。
(下面的 sbrk/mmap 一次分配一页)
图示:address_space.png
#include <unistd.h>
int getpagesize (void);
返回内存 1 页的字节数。
范例:page.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void presskey () {
printf ("查看/proc/%u/maps,按<enter>继续 . . .", getpid ());
getchar ();
}
int main () {
printf ("%d\n", getpagesize ());
char* pc = malloc (sizeof (char));
printf ("%p\n", pc);
presskey ();
free (pc);
presskey ();
setbuf (stdout, NULL); // 关闭输出缓冲区,保证下面 printf 可以正常输出;
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 后续错误 不稳定 段错误
8. 内存管理 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
#include <stdio.h>
#include <unistd.h>
void presskey () {
printf ("查看/proc/%u/maps,按<enter>继续 . . .", getpid ());
getchar ();
}
int main () {
// sbrk 举例
void* p1 = sbrk (4); // RXXX(R 为 sbrk 返回的地址)
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 (0); // XXXX XXXX XXXX R
printf ("p4 = %p\n", p4);
int* pn = (int*)p1;
pn[0] = 0; // OK
pn[1] = 1; // OK
pn[2] = 2; // OK
pn[1023] = 1023; // OK
pn[1024] = 1024; // 段错误
void* p5 = sbrk (-8); // XXXX ---- ---- R
void* p6 = sbrk (-4); // ---R(返回上一次 sbrk 调用后的地址)
pn[0] = 10; // 段错误;和 malloc 不一样,映射结束;
int page = getpagesize ();
printf ("%p\n", sbrk (page));
printf ("%p\n", sbrk (1));
printf ("%p\n", sbrk (-page));
printf ("%p\n", sbrk (-1));
// brk 举例
p1 = sbrk (0); // 用 brk 需要先计算初始地址
printf ("%p\n", p1);
brk (p2 = p1 + 4); // XXXX S
brk (p3 = p2 + 4); // XXXX XXXX S
brk (p4 = p3 + 4); // XXXX XXXX XXXX S
pn = (int*)p1;
pn[0] = 0; // OK
pn[1] = 1; // OK
pn[2] = 2; // OK
pn[1023] = 1023; // OK
pn[1024] = 1024; // 段错误
brk (p2); // XXXX S
brk (p1); // S
pn[0] = 10; // 段错误;
// 用 sbrk 分配内存:
void* begin = sbrk (sizeof (int));
pn = (int*)begin;
*pn = 1234;
double* pd = (double*)sbrk (sizeof (double));
*pd = 3014;
char* psz = (char*)sbrk (sizeof (char) * 256);
sprintf (psz, "hello word");
brk (begin); // 一下子全部释放;
return 0;
}
分析:p1,p2,p3 之间相差 4 字节,没用 malloc,所以没有多加 12 字节;
9. 练习:自己实现 malloc
思路:定义一个结构体,表示 12 字节的内存控制块;
申请内存的时候,从后往前遍历,遇到空闲内存块大于等于需要内存的时候,就直接返回该内存块地址;
否则,就需要自己新定义一个内存(页);
#include <stdio.h>
#inlcude <unistd.h>
#include <stdbool.h>
// 内存控制块 12 字节
typedef struct men_control_block { // 重命名
bool free; // 自由标志,用于判断本块内存是否闲置
struct men_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->prev) // 在现有页中,从后往前遍历
if (mcb-prev && mcb->size >= size) // 找到合适大小的内存块
break;
if (! mcb) { // 现有页中,没找到合适大小的内存块
mcb = sbrk (sizeof (MCB) + size); // 重新分配一页内存
if (mcb == (void*)-1) // 分配失败
// sbrk 分配失败返回 -1,但是 mcb 是指针类型,所以对 -1 进行强制类型转换;
return NULL; // malloc 分配失败也是返回 NULL,同步;
mcb->prev = g_top;
mcb->size = size;
g_top = mcb;
}
// 无论是在现有里面找到还是新分配的,都需要把标志置为零;
mcb->free = false; // 0 说明这块内存闲置;
return mcb + 1;
// 此时 mcb 指向控制块首部,+1 相当于加 12 字节,指向控制块尾部
}
// 释放内存
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 (g_top != mcb) {
g_top = mcb;
brk ((void*)mcb + sizeof (mcb) +mcb->size);
}
}
int main () {
int* p1 = my_malloc (10 * sizeof (int));
int* p1 = my_malloc (5 * sizeof (int));
size_t i;
for (i = 0; i < 10; ++i)
p1[i] = i + 1;
for (i = 0; i < 10; ++i)
p2[i] = 10 + i + 1;
my_free (p1);
my_free (p2);
}
10. 创建虚拟内存到物理内存或文件的映射 mmap
#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 - 锁定映射区域,保证其不被置换。
11. 销毁虚拟内存到物理内存或文件的映射 munmap
int munmap (
void* start, // 映射区内存起始地址
size_t length, // 字节长度,自动按页(4K)对齐
);
成功返回 0,失败返回-1。
mmap/munmap 底层不维护任何东西,只是返回一个首地址,所分配内存位于堆中。
brk/sbrk 底层维护一个指针,记录所分配的内存结尾,
所分配内存位于堆中,底层调用 mmap/munmap。
malloc 底层维护一个双向链表和必要的控制信息,
不可越界访问,所分配内存位于堆中,底层调用 brk/sbrk。
切记:malloc 和 brk/sbrk 只能用其一,不要混合使用,会出错;
每个进程都有 4G 的虚拟内存空间,虚拟内存地址只是一个数字,并没有和实际的物理内存相关联。
所谓内存分配与释放,其本质就是建立或取消虚拟内存和物理内存间的映射关系。
范例:mmap.c
#include <stdio.h>
#include <unistd.h>
#include <sys/mman.h>
#define MAX_TEXT 256
int mian () {
char* psz = (char*)mmap (NULL, // 返回为地址,需要强制类型转换
MAX_TEXT * sizeof (char), // 分配大小
PROT_READ | PROT_WRITE, // 可读可写
MAP_PRIVATE | MAP_ANONYMOUS, // 不用文件
0, 0);
if (psz == MAP_FAILED) { // if (psz == -1)
perror ("mmap");
return -1;
}
sprintf (psz, "hello wrod");
printf ("%s\n", psz);
printf ("%p\n", psz);
if (munmap (psz, MAX_TEXT * sizeof (char)) == -1) {
perror ("munmap");
return -1;
}
}
作业:实现一个基于顺序表的堆栈类模板,
其数据缓冲区内存可根据数据元素的多少自动增减,
但不得使用标准C的内存分配与释放函数。
代码:stack.cpp
思考:该堆栈模板是否适用于类类型的数据元素。