7.2-UC-day01PM-第二课:内存管理
================
第二课 内存管理
================
一、错误处理
------------
1. 通过函数的返回值表示错误
~~~~~~~~~~~~~~~~~~~~~~~~~~~
1) 返回合法值表示成功,返回非法值表示失败。
范例:bad.c
2) 返回有效指针表示成功,
返回空指针(NULL/0xFFFFFFFF)表示失败。
范例:null.c
3) 返回0表示成功,返回-1表示失败,
不输出数据或通过指针/引用型参数输出数据。
范例:fail.c
4) 永远成功,如:printf()。
练习:实现四个函数
slen() - 求字符串的长度,若为空指针,则报错。
scpy() - 字符串拷贝,考虑缓冲区溢出,
成功返回目标缓冲区地址,
目标缓冲区无效时报错。
intmin() - 求两个整数的最小值,若二者相等,则报错。
intave() - 求两个整数的平均值,考虑求和溢出,
该函数不会失败。
代码:error.c
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
3) errno在函数执行成功的情况下不会被修改,
因此不能以errno非零,作为发生错误判断依据。
范例:iferr.c
4) errno是一个全局变量,其值随时可能发生变化。
二、环境变量
------------
1. 环境表
~~~~~~~~~
1) 每个程序都会接收到一张环境表,
是一个以NULL指针结尾的字符指针数组。
2) 全局变量environ保存环境表的起始地址。
+---+
environ -> | * --> HOME=/root
+---+
| * --> SHELL=/bin/bash
+---+
| * --> PATH=/bin:/usr/bin:...:.
+---+
| . |
| . |
| . |
+---+
| 0 |
+---+
图示: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
三、内存管理
------------
+----+--------+----------------------------+----------+
| 用 | 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 | ... |
| 层 | ... | ... | ... |
+----+--------+----------------------------+----------+
四、进程映像
------------
1. 程序是保存在磁盘上的可执行文件。
2. 运行程序时,需要将可执行文件加载到内存,形成进程。
3. 一个程序(文件)可以同时存在多个进程(内存)。
4. 进程在内存空间中的布局就是进程映像。
从低地址到高地址依次为:
代码区(text):可执行指令、字面值常量、
具有常属性的全局和静态局部变量。只读。
数据区(data):初始化的全局和静态局部变量。
BSS区:未初始化的全局和静态局部变量。
进程一经加载此区即被清0。
数据区和BSS区有时被合称为全局区或静态区。
堆区(heap):动态内存分配。从低地址向高地址扩展。
栈区(stack):非静态局部变量,
包括函数的参数和返回值。从高地址向低地址扩展。
堆区和栈区之间存在一块间隙,
一方面为堆和栈的增长预留空间,
同时共享库、共享内存等亦位于此。
命令行参数与环境区:命令行参数和环境变量。
图示:maps.bmp
范例:maps.c
比对/proc/<pid>/maps
# size a.out
text data bss dec hex filename
2628 268 28 2924 b6c a.out
| | | | |
+-------+-------+ (10) +---+---+ (16)
V ^
+-------------------+
(+)
五、虚拟内存
------------
1. 每个进程都有各自互独立的4G字节虚拟地址空间。
2. 用户程序中使用的都是虚拟地址空间中的地址,
永远无法直接访问实际物理内存地址。
3. 虚拟内存到物理内存的映射由操作系统动态维护。
4. 虚拟内存一方面保护了操作系统的安全,
另一方面允许应用程序,
使用比实际物理内存更大的地址空间。
图示:vm.png
5. 4G进程地址空间分成两部分:
[0, 3G)为用户空间,
如某栈变量的地址0xbfc7fba0=3,217,554,336,约3G;
[3G, 4G)为内核空间。
6. 用户空间中的代码,
不能直接访问内核空间中的代码和数据,
但可以通过系统调用进入内核态,
间接地与系统内核交互。
图示:kernel.png
7. 对内存的越权访问,
或试图访问没有映射到物理内存的虚拟内存,
将导致段错误。
8. 用户空间对应进程,进程一切换,用户空间即随之变化。
内核空间由操作系统内核管理,不会随进程切换而改变。
内核空间由内核根据独立且唯一的页表init_mm.pgd
进行内存映射,而用户空间的页表则每个进程一份。
9. 每个进程的内存空间完全独立。
不同进程之间交换虚拟内存地址是毫无意义的。
范例:vm.c
10. 标准库内部通过一个双向链表,
管理在堆中动态分配的内存。
malloc函数分配内存时会附加若干(通常是12个)字节,
存放控制信息。
该信息一旦被意外损坏,可能在后续操作中引发异常。
范例:crash.c
11. 虚拟内存到物理内存的映射以页(4K=4096字节)为单位。
通过malloc函数首次分配内存,至少映射33页。
即使通过free函数释放掉全部内存,
最初的33页仍然保留。
图示:address_space.png
#include <unistd.h>
int getpagesize (void);
返回内存页的字节数。
范例:page.c
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
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
mmap/munmap底层不维护任何东西,只是返回一个首地址,
所分配内存位于堆中。
brk/sbrk底层维护一个指针,记录所分配的内存结尾,
所分配内存位于堆中,底层调用mmap/munmap。
malloc底层维护一个双向链表和必要的控制信息,
不可越界访问,所分配内存位于堆中,底层调用brk/sbrk。
每个进程都有4G的虚拟内存空间,
虚拟内存地址只是一个数字,
并没有和实际的物理内存将关联。
所谓内存分配与释放,
其本质就是建立或取消虚拟内存和物理内存间的映射关系。
作业:实现一个基于顺序表的堆栈类模板,
其数据缓冲区内存可根据数据元素的多少自动增减,
但不得使用标准C的内存分配与释放函数。
代码:stack.cpp
思考:该堆栈模板是否适用于类类型的数据元素
day01PM_函数空间分配_虚拟内存
书籍推荐《深入理解Linux虚拟内存管理》
函数调用栈空间的分配与释放
auto/register/const
register和一般变量有什么区别?
LINUX两个方向:内存管理、进程管理
goto标签加&&
![](https://img2018.cnblogs.com/blog/1167156/201811/1167156-20181129230401679-1327107541.png)
(1)对象的栈空间;
(2)临时栈空间;
![](https://img2018.cnblogs.com/blog/1167156/201811/1167156-20181129230405525-1517626300.png)
实际函数就两个临时变量,三个不用;不会出错;
C/C++注意区别:
模板、命名空间、异常处理;
引用、面向对象;
指针如何传递?
函数的参数怎么传递到临时栈的?
产生指针变量 ,地址拷贝过来。
指针和引用都传的是值;
地址值和数据值;
编译器是没有变量说的,
都会转化为绝对地址和偏移地址;
指针就是把空间地址位置告诉你,
把值放到空间里去就是了。(参数指针)
把地址返回出去,双指针;
![](https://img2018.cnblogs.com/blog/1167156/201811/1167156-20181129230407783-794573383.png)
函数在编译的时候可以指定三种调用方式:
![](https://img2018.cnblogs.com/blog/1167156/201811/1167156-20181129230408412-2047895644.png)
cat xxxx.s 看出汇编语言
![](https://img2018.cnblogs.com/blog/1167156/201811/1167156-20181129230408745-1289502468.png)
函数名的转换:
nm 编译器生成额外的代码;
重载函数名会发生转换;
函数是有栈的;
函数的压栈、清空方式是有_stdcall影响的;
LINUX中不考虑,WINDOWS中才会去考虑
WIN32中统一采用的是far指针;
near 16位访问方式
far 32位越间访问方式
huge 综合访问方式
![](https://img2018.cnblogs.com/blog/1167156/201811/1167156-20181129230409300-1750082183.png)
虚拟内存:
![](https://img2018.cnblogs.com/blog/1167156/201811/1167156-20181129230409716-1221564523.png)
![](https://img2018.cnblogs.com/blog/1167156/201811/1167156-20181129230409927-697387204.png)
为什么不能够访问?
理解:
函数地址没有对应,出现“段错误”;
地址只是一个 编号,实际上谈到的逻辑地址仅仅是个编号,编号使用int 4字节整数表示
strace main
跟踪这个程序对系统函数的调用
要访问物理地址得通过操作系统;
内存的 映射;
通过编号访问,编号都是假的 ,如何没和物理内存关联没有任何意义;
![](https://img2018.cnblogs.com/blog/1167156/201811/1167156-20181129230414449-1226376259.png)
P指向的地址有物理内存映射;
其实虚拟地址与物理地址在映射的时候有一个基本单位:4K
16进制 1000 内存页
段错误:地址没有映射到一个物理空间,
无效访问;非法访问;
哪怕要1个字节,也是映射至少4K空间;
合法访问:比如malloc分配的空间之外的空间可以访问,但访问非法。
有效访问和合法访问的概念;
五.虚拟内存的分配
栈:编译器自动生成代码维护;
堆:地址是否映射,映射的空间是否被管理;
1.brk/sbrk内存映射函数
1: Linux系统(shell) 指令
-ls
2: 系统函数(LINUX编程用到的 节)
![](https://img2018.cnblogs.com/blog/1167156/201811/1167156-20181129230416628-1802554531.png)
![](https://img2018.cnblogs.com/blog/1167156/201811/1167156-20181129230416890-33618396.png)
man 3 fopen
![](https://img2018.cnblogs.com/blog/1167156/201811/1167156-20181129230417154-2132363527.png)
man 7 tcp
什么叫TCP/ICMP/UDP/SOCKET网络编程
大多数文档在2/3里面
int brk (void *end);
void *sbrk(int size);
![](https://img2018.cnblogs.com/blog/1167156/201811/1167156-20181129230417497-358381461.png)
![](https://img2018.cnblogs.com/blog/1167156/201811/1167156-20181129230417930-388096133.png)
sbrk和malloc有什么区别?
会造成严重问题;
![](https://img2018.cnblogs.com/blog/1167156/201811/1167156-20181129230419312-1140607996.png)
![](https://img2018.cnblogs.com/blog/1167156/201811/1167156-20181129230419909-1992508548.png)
sbrk(0)没有映射;
分配空间往后面移动;释放空间往前面移;
单独使用;
sbrk()
0:
同时产生一个数据:指向地址。
如果指定值,做页面映射,至少是4K倍数;
否:返回指针,并且把指针位置+size
![](https://img2018.cnblogs.com/blog/1167156/201811/1167156-20181129230422643-409109573.png)
sbrk参数是0或者是非0;
brk(p+1),移动4个指针;
brk改变绝对位置;
malloc也是移来移去的;用的也是brk或者sbrk函数
写一个程序查找1-10000之间所有的素数。
并且存放到缓冲,然后打印。
![](https://img2018.cnblogs.com/blog/1167156/201811/1167156-20181129230423008-574763379.png)
找到一个往后面移动4个,放进去;
流程:
r是随时移动的,p一直保存在页首;
![](https://img2018.cnblogs.com/blog/1167156/201811/1167156-20181129230424374-1226240437.png)
r会随时随时往后面走;
![](https://img2018.cnblogs.com/blog/1167156/201811/1167156-20181129230424702-910021297.png)
头文件可以这样写;
头文件只是保证语法上正确就可以;
c++必须要声明;C是不需要声明的,弱类型语言
malloc是用brk和sbrk来实现的;
流媒体是这样做的10000,
malloc无法保证内存是连续的;
内存分配的选择:
![](https://img2018.cnblogs.com/blog/1167156/201811/1167156-20181129230424960-333352867.png)
数据比较简单大块的用brk/sbrk;
![](https://img2018.cnblogs.com/blog/1167156/201811/1167156-20181129230425757-2115790590.png)
(2)%m 内存错误的输出
![](https://img2018.cnblogs.com/blog/1167156/201811/1167156-20181129230426027-876868743.png)
(3) ::%s\n 错误的输出
![](https://img2018.cnblogs.com/blog/1167156/201811/1167156-20181129230426275-1896189972.png)
相差为1或者2的叫做孪生素数;
(1)先用缓存把所有素数找出来;
(2)再用循环打印;
用sbrk;
指针就是指针,类型就是一个访问范围而已;
变量只是一个访问的首地址而已;
心中有语法,眼中无语法。
![](https://img2018.cnblogs.com/blog/1167156/201811/1167156-20181129230427661-2122318711.png)
<wiz_tmp_tag id="wiz-table-range-border" contenteditable="false" style="display: none;">