一、内存管理的调用关系
用户层
STL 自动申请/释放内存 调用C++
C++ new/delete 调用C
C malloc/free 调用POSIX或者 Linux
POSIX brk/sbrk 调用内核
Linux mmap/munmap 调用内核
系统层
kernal kmalloc/vmalloc 调用驱动
driver get_free_page
二、进程映像
程序是存储在磁盘上的可执行文件(二进制文件、脚本文件)
当执行程序时,系统会自动将该文件加载到内存中,在内存中的分布情况称为进程映像
从低地址到高地址的分区:
text 代码段
data 数据段
bss 静态数据段
heap 堆
stack 栈
environ 环境变量表
argv 命令行参数
命令:ps -aux 查看当前所有进程信息
可以查看进程号
/proc/进程号/maps
总结:
1、栈内存的增长方向受操作系统影响,大部分是从高地址向低地址增长,但有些系统例如Ubuntu就是从低地址向高地址增长
2、如果是栈内存存储数组数据,数组中元素的增长方向一定是从低地址向高地址增长
虚拟内存:
1、操作系统会为每个进程分配4G的虚拟内存
2、用户只能使用虚拟内存,不能直接使用物理内存
3、虚拟内存要与物理内存进行映射后才能被用户使用,如果使用了没有映射的虚拟内存就会产生段错误
4、虚拟内存地址与物理内存的映射由操作系统(MMU)动态维护
5、虚拟内存能让系统使用更安全,不会暴露真实的物理内存地址,另一方面操作系统可以让进程使用比实际物理内存更大的地址空间
6、4G的虚拟内存地址分为两个部分
[0G,3G) 用户空间
[3G,4G) 内核空间
7、当进程\线程运行在用户空间时称为进程处于用户态,当进程\线程运行在内核空间时称为进程处于内核态
8、当进程处于内核态时,进程运行存储使用在内核空间,此时cpu可以发出并执行任何指令,并且运行的代码不受任何限制,可以自由地访问任意有效的地址,也可以直接访问接口
9、当进程处于用户态时,进程运行存储使用在用户空间,此时被执行的代码要受到CPU很多的检查,例如:用户进程只能访问自己映射过的内存
10、所有进程的内核空间的代码、数据都是映射在同一块物理内存中,有内核来负责维护
11、用户空间的代码不能直接访问内核空间的代码和数据,可以通过系统调用(API 调用系统接口函数)切换到内核态,间接的访问内核、与内核交换数据
映射虚拟内存与物理内存的函数:
sbrk\brk\mmap\munmap
关于malloc获取虚拟内存空间的底层实现,跟libc.so版本有关,大概的逻辑:
1、如果分配的内存小于128k时,调用sbrk、brk
2、如果大于128时,调用mmap、munmap
注意:系统映射内存是以页(1页=4096字节)为最小单位的
注意:sbrk、brk底层共同维护一个映射位置指针,该指针指向映射过的内存的下一个位置或者说是未映射的内存的第一个地址
void* sbrk(intptr_t increment);
功能:通过increment参数调整映射位置指针的位置,既可以映射内存也可以取消映射
increment: 字节为单位
0 获取当前指针的位置
>0 映射内存
<0 取消映射
返回值:该指针映射前原来的位置
int brk(void* addr);
功能:直接使用addr地址修改映射指针位置指针的位置
addr:
>原来位置指针的位置 映射内存
<原来位置指针的位置 取消映射
返回值:成功0,失败-1
注意:sbrk、brk都可以单独进行映射、取消映射的操作,但是一般习惯配合使用:sbrk辅助记录映射前的位置+映射操作,brk负责取消整个映射
#include <sys/mman.h>
注意:mmap、munmap底层不维护任何东西
void *mmap(void* addr,size_t length,int prot,int flags,int fd,off_t offset);
功能:映射虚拟内存与物理内存
addr:指定要映射的虚拟内存首地址,可以手动指定,如果是NULL,则是系统自动指定
length:映射的长度,字节为单位
prot:映射后的权限
PROT_EXEC 执行权限
PROT_READ 读权限
PROT_WRITE 写权限
PROT_NONE 没有权限
PROT_READ | PROT_WRITE
flags:映射方式标志
MAP_SHARED 映射内存与文件内容后,如果修改内存中的数据,文件内容会随之改变
MAP_PRIVATE 映射内存与文件内容后,如果修改内存中的数据,文件内容不会随之改变 MAP_SHARE互斥,必须选一个
MAP_ANONYMOUS 映射到虚拟内存,而不是映射文件,会忽略fd、offset参数
MAP_FIXED 如果手动提供的addr无法进行映射,默认情况下会映射一个正确的地址,但加了该标志,则不会调整,直接执行失败
fd:文件描述符,如果不映射文件,则给0即可
offset:文件的偏移位置,不用给0即可
返回值:成功返回映射后的内存首地址,失败返回(void*) -1\0xFFFFFFFF
int munmap(void* addr,size_t length);
功能:取消映射
addr:要取消映射的首地址
length:取消的字节数
返回值:成功0,失败-1
注意:可以使用strace ./a.out(可执行文件名)可以大概跟踪代码底层的执行情况
sbrk底层调用了brk
总结:
1、重点是理解Linux内存管理机制(虚拟内存),而不是brk\sbrk\mmap\munmap系统函数的使用
2、brk\sbrk底层维护一个虚拟内存位置指针,通过移动该指针来建立映射关系和取消映射关系
3、mmap\munmap底层不维护任何东西,只返回一个映射后的虚拟内存地址
4、malloc\free底层调用的是sbrk\brk 或者 mmap\munmap
系统调用(系统API)
系统调用就是操作系统提供的一些功能以函数的行驶个程序员使用,注意虽然格式很像标准C的函数,但是它们不是标准C的内容,也不是真正的函数
一般程序大部分时间都工作在用户态(0~3G),当偶尔发生系统调用时就会工作在内核态(3~4G)
系统调用的代码使内核的一部分,其外部接口以函数形式定义在共享库中(linux-gate.so.1 /lib/ld-linux.so.2)
系统调用的执行是借助软中断的方式从用户态进入到内核态后执行真正的系统调用
time ./a.out 测试进程的运行时间分布
real 0m0.001s 进程总执行时间
user 0m0.001s 用户态执行时间
sys 0m0.000s 内核态执行时间
ldd\time\strace\size ./a.out
Linux文件IO:
一切皆文件
NUIX/Linux系统把所有的服务、设备都抽象成文件看待,并提供了一套简单而同一的系统接口,这部分系统接口就是所谓的系统文件读写,简称系统IO
如果使用的是标准C库中的文件读写函数,简称为标准IO
文件的分类:
普通文件 - 包括纯文本文件、二进制文件、压缩文件等
目录文件 d 必须有x权限才能进入目录
块设备文件 b 用于存储大块数据的设备 例如硬盘
字符设备文件 c 例如键盘、鼠标
管道文件 p 有名管道文件
链接文件 l 类似于Windows的快捷方式
Sockst文件 s 通常用于网络设备之间数据的链接交互
文件操作相关的系统调用:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char* pathname,int flags);
功能:打开文件
pathname:文件的路径
flags:文件的打开方式
O_RDONLY 只读
O_WRONLY 只写
O_RDWR 读写
O_APPEND 追加
O_CREAT 文件不存在则创建
O_EXCL 文件存在,如果配合O_CREAT则失败
O_TRUNC 文件存在则清空打开
返回值:文件描述符,成功返回一个非负整数,类似FILE*,代表了打开后的文件
int open(const char* pathname,int flags,mode_t mode);
功能:创建文件
flags:必须为O_CREAT mode参数就需要给
mode:
S_IRWXU 00700 用户 读写执行权限
S_IRUSR 00400 读权限
S_IWUSR 00200 写权限
S_IXUSR 00100 执行权限
S_IRWXG 00070 组 读写执行权限
S_IRGRP 00040 读权限
S_IWGRP 00020 写权限
S_IXGRP 00010 执行
S_IRWXO 00007 其他用户
S_IROTH 00004 读权限
S_IWOTH 00002 写
S_IXOTH 00001 执行
注意:可以直接使用八进制数表示三组权限,例如
0644 0664 0775
返回值:文件描述符,成功返回一个非负整数,类似FILE*,代表了打开后的文件
int creat(const char* pathname,mode_t mode);
功能:创建文件
mode:同上
试验:fopen各种打开方式的open底层调用的的参数分别是什么
strace ./a.out
ssize_t write(int fd,const void* buf,size_t count);
功能:把内存中的数据写入文件中
fd:文件描述符,open的返回值
buf:待写入的内存首地址
count:要写入的字节数
返回值:成功写入的字节数
ssize_t read(int fd,const void* buf,size_t count);
功能:从文件中读取数据到内存中
fd:文件描述符,open的返回值
buf:存储数据的内存首地址
count:想要读取的字节数
返回值:实际读取到的字节数
int close(int fd);
功能:关闭文件
注意:write、read是fwrite和fread的底层调用
结论:正常情况下,标准IO比系统IO的速度更快
原因:
1、标准IO中缓冲区机制,在写数据时不时每次都调用系统IO,而是把写入的数据存储在临时的缓冲区中,当缓冲区满时,才会调用一次系统IO写入数据到文件中
2、直接使用系统IO时会频繁地切换内核态和用户态,非常耗时
如果给系统IO增加缓冲区,它的速度一定会比标准IO快
随机读写:
系统IO下,每个打开的文件都有一个写的位置指针,记录了从哪个字节开始进行读写文件,并且对文件进行读写操作时,它会自动往后移动
当想要在任意位置进行读写文件时,可以通过改变位置指针的位置进行随机读写
//标准IO
fseek
返回值:成功0,失败-1 借助ftell
//系统IO
off_t lseek(int fd, off_t offset,int whence);
offset:偏移值 字节为单位
whence:基础位置
SEEK_SET 文件开头
SEEK_CUR 当前位置
SEEK_END 文件末尾
返回值:调整后位置指针所在文件的第几个字节
注意:当文件位置指针越过末尾后在写入数据时,越过的地方会形成"黑洞",该段"黑洞"会计算入文件大小中,但是不占用磁盘空间