菜鸟笔记
提升您的技术认知

操作系统笔记

进程,线程,协程与并行,并发进程线程协程的区别死锁进程,线程,多线程i++的线程安全性同步和异步孤儿进程和僵尸进程/proc进程信息linux中的分段和分页互斥量 mutex线程进程间通信进程创建进程优先级进程的基础知识进程与线程的区别(面试题)线程的控制(创建,终止,等待,分离)可重入 VS 线程安全死锁的概念一级缓存和二级缓存的理解一句话解说内存屏障 Memory barrierbrk(), sbrk() 用法详解malloc/free函数的简单实现一文讲透 “进程、线程、协程”Linux进程状态线程池的陷阱linux内核学习之进程和线程进程与线程的区别和联系内存寻址linux IO子系统和文件系统读写流程Page cache和buffer cache的区别与联系漫谈linux文件IO多线程和多进程的区别内存泄漏字节、字、位、比特的概念和关系如何避免死锁ANSI是什么编码?CPU寻址范围(寻址空间)CPU 使用率低高负载的原因创建多少个线程合适操作系统下spinlock锁解析、模拟及损耗分析线程堆栈堆和栈的内存分配堆和栈的概念和区别堆和栈的区别,申请方式,程序的内存分配什么是 POD 数据类型Linux内存分配小结--malloc、brk、mmap系统调用与内存管理(sbrk、brk、mmap、munmap)进程描述和控制CPU执行程序的原理编译的基本概念Linux虚拟地址空间布局一个程序从源代码到可执行程序的过程程序的运行机制——CPU、内存、指令的那些事分页内存管理——虚拟地址到物理地址的转换深刻理解Linux进程间通信fork之后父子进程的内存关系fork之后,子进程继承了父进程哪些内容关于协程及其锁的一些认识对协程的一点理解std::thread join和detach区别CAS和ABA问题CAS算法锁和无锁无锁队列的实现Lock-Free 编程锁开销优化以及CAS

系统调用与内存管理(sbrk、brk、mmap、munmap)

阅读 : 214

一、系统调用(System Call):

在Linux中,4G内存可分为两部分——内核空间1G(3~4G)与用户空间3G(0~3G),我们通常写的C代码都是在对用户空间即0~3G的内存进行操作。而且,用户空间的代码不能直接访问内核空间,因此内核空间提供了一系列的函数,实现用户空间进入内核空间的接口,这一系列的函数称为系统调用(System Call)。比如我们经常使用的open、close、read、write等函数都是系统级别的函数(man 2 function_name),而像fopen、fclose、fread、fwrite等都是用户级别的函数(man 3 function_name)。不同级别的函数能够操作的内存区域自然也就不同。

我们用一幅图来描述函数的调用过程:

对于C++中new与delete的底层则是用malloc和free实现。而我们所用的malloc()、free()与内核之间的接口(桥梁)就是sbrk()等系统函数;当然我们也可以直接调用系统调用(系统函数),达到同样的作用。我们可以用下面这幅图来描述基本内存相关操作之间的关系:

虽然使用系统调用会带来一定的好处,但是物极必反,系统调用并非能频繁使用。由于程序由用户进入内核层时,会将用户层的状态先封存起来,然后到内核层运行代码,运行结束以后,从内核层出来到用户层时,再把数据加载回来。因此,频繁的系统调用效率很低。今天我们就系统调用层面来对内存操作做进一步的了解。

二、内存管理(Memory Management)系统调用:

1、brk()与sbrk():

(1)、函数原型与实现:

//函数原型:
#include<unistd.h>
int brk(void * addr); 
void * sbrk(intptr_t increment);

由于sbrk()与brk()这两个系统函数有点所谓怪异,我们先来看看man手册对于sbrk()与brk()的描述:

DESCRIPTION

   brk() and sbrk() change the location of the program break, which
   defines the end of the process's data segment (i.e., the program
   break is the first location after the end of the uninitialized data
   segment).  Increasing the program break has the effect of allocating
   memory to the process; decreasing the break deallocates memory.

   brk() sets the end of the data segment to the value specified by
   addr, when that value is reasonable, the system has enough memory,
   and the process does not exceed its maximum data size (see
   setrlimit(2)).

   sbrk() increments the program's data space by increment bytes.
   Calling sbrk() with an increment of 0 can be used to find the current
   location of the program break.

RETURN VALUE

   On success, brk() returns zero.  On error, -1 is returned, and errno
   is set to ENOMEM.

   On success, sbrk() returns the previous program break.  (If the break
   was increased, then this value is a pointer to the start of the newly
   allocated memory).  On error, (void *) -1 is returned, and errno is
   set to ENOMEM.

描述:
brk()和sbrk()改变程序间断点的位置。程序间断点就是程序数据段的结尾。(程序间断点是为初始化数据段的起始位置).通过增加程序间断点进程可以更有效的申请内存 。当addr参数合理、系统有足够的内存并且不超过最大值时brk()函数将数据段结尾设置为addr,即间断点设置为addr。sbrk()将程序数据空间增加increment字节。当increment为0时则返回程序间断点的当前位置。

返回值:
brk()成功返回0,失败返回-1并且设置errno值为ENOMEM(注:在mmap中会提到)。
sbrk()成功返回之前的程序间断点地址。如果间断点值增加,那么这个指针(指的是返回的之前的间断点地址)是指向分配的新的内存的首地址。如果出错失败,就返回一个指针并设置errno全局变量的值为ENOMEM。

总结:
这两个函数都用来改变 “program break” (程序间断点)的位置,改变数据段长度(Change data segment size),实现虚拟内存到物理内存的映射。
brk()函数直接修改有效访问范围的末尾地址实现分配与回收。sbrk()参数函数中:当increment为正值时,间断点位置向后移动increment字节。同时返回移动之前的位置,相当于分配内存。当increment为负值时,位置向前移动increment字节,相当与于释放内存,其返回值没有实际意义。当increment为0时,不移动位置只返回当前位置。参数increment的符号决定了是分配还是回收内存。而关于program break的位置如图所示:

(2)、简单测试:

对于分配好的内存,我们只要有其首地址old与长度MAX*MAX即可不越界的准确使用(如下图所示),其效果与malloc相同,只不过sbrk()与brk()是C标准函数的底层实现而已,其机制较为复杂(测试中,死循环是为了查看maps文件,不至于进程消亡文件随之消失)。

虽然,sbrk()与brk()均可分配回收兼职,但是我们一般用sbrk()分配内存,而用brk()回收内存,上例中回收内存可以这样写:

int err = brk(old);
/**或者brk(p);效果与sbrk(-MAX*MAX);是一样的,但brk()更方便与清晰明了。**/
if(-1 == err){
    perror("brk");
    exit(EXIT_FAILURE);
}

2、mmap()与munmap():

mmap函数(地址映射):mmap将一个文件或者其它对象映射进内存。文件被映射到多个页上,如果文件的大小不是所有页的大小之和,最后一个页不被使用的空间将会清零(Linux堆空间未使用内存均清零)。这里我们只研究mmap的内存映射,而暂时不讨论文件方面的问题。关于mmap的文件映射的更详细的内容可参考认真分析mmap:是什么 为什么 怎么用

//函数原型:
#incldue<sys/mman.h>
void * mmap(void * addr, size_t length,int prot,int flags,int fd,off_t offset);

参数:

(1)、addr:
起始地址,置零让系统自行选择并返回即可.
(2)、length:
长度,不够一页会自动凑够一页的整数倍,我们可以宏定义#define MIN_LENGTH_MMAP 4096为一页大小
(3)、prot:
读写操作权限,PROT_READ可读、PROT_WRITE可写、PROT_EXEC可执行、PROT_NONE映射区域不能读取。(注意PROT_XXXXX与文件本身的权限不冲突,如果在程序中不设定任何权限,即使本身存在读写权限,该进程也不能对其操作)
(4)、flags常用标志:
MAP_SHARED【share this mapping】、MAP_PRIVATE【Create a private copy-on-write mapping】
MAP_SHARED只能设置文件共享,不能地址共享,即使设置了共享,对于两个进程来说,也不会生效。而MAP_PRIVATE则对于文件与内存都可以设置为私有。
MAP_ANON【Deprecated】、MAP_ANONYMOUS:匿名映射,如果映射地址需要加该参数,如果不加默认映射文件。MAP_ANON已经过时,只需使用MAP_ANONYMOUS即可
(5)、文件描述符:fd
(6)、文件描述符偏移量:offset
(fd和offset对于一般性内存分配来说设置为0即可)

返回值:

失败返回MAP_FAILED,即(void * (-1))并设置errno全局变量。
成功返回指向mmap area的指针pointer。

常见errno错误:

①ENOMEM:内存不足;
②EAGAIN:文件被锁住或有太多内存被锁住;
③EBADF:参数fd不是有效的文件描述符;
④EACCES:存在权限错误,。如果是MAP_PRIVATE情况下文件必须可读;使用MAP_SHARED则文件必须能写入,且设置prot权限必须为PROT_WRITE。
⑤EINVAL:参数addr、length或者offset中有不合法参数存在。

munmap函数:解除映射关系
int munmap(void * addr, size_t length);//addr为mmap函数返回接收的地址,length为请求分配的长度。

这张图描述了mmap内存地址映射的位置关系(栈区以上为内核空间)。关于这一点我们可以作以简单的测试(我采用MIN_LENGTH_MMAP宏,当然你也可以用多少申请多少,系统总是以最小1页来映射的,关于内存分页与虚拟地址映射可参考Linux系统内存管理与内存分页机制 ):

mmap映射的地址处于堆区与栈区中间,malloc映射的堆区内存为33页(最小映射大小),而mmap映射的内存为3页,也是4096B的整数倍。