![74ab1f94-a312-eb11-8da9-e4434bdf6706.png](http://p01.5ceimg.com/content/74ab1f94-a312-eb11-8da9-e4434bdf6706.png)
X86进程布局
![76ab1f94-a312-eb11-8da9-e4434bdf6706.png](http://p04.5ceimg.com/content/76ab1f94-a312-eb11-8da9-e4434bdf6706.png)
- 1.代码段(text)
代码段由各个函数产生,函数的每一个语句将最终经过编绎和汇编生成二进制机器代码(具体生生哪种体系结构的机器代码由编译器决定)。 - 2.只读数据段(rodata)
只读数据段由程序中所使用的数据产生,该部分数据的特点是在运行中不需要改变,因此编译器会将该数据段放入只读的部分中。C语言中的只读全局变量,程序中使用的字符串常量等会在编译时被放入到只读数据区。const修饰的全局变量在常量区;const修饰的局部变量只是为了防止修改,没有放入常量区。 - 3.读写数据段(data)
读写数据段表示了在目标文件中一部分可以读也可以写的数据区,在某些场合它们又被称为已初始化数据段,这部分数据段和代码段,与只读数据段一样都属于程序中的静态区域,但具有可写性的特点。通常已初始化的全局变量和局部静态变量被放在了读写数据段。 - 4.未初始化数据段(bss)
与读写数据段类似,它也属于静态数据区,但是该段中的数据没有经过初始化。被初始化为0的全局变量也会被放入.bss段。如 全局变量 int* p_pro = NULL; - 5.堆(heap)
堆是用于存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩张或缩减。当进程调用malloc等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张);当利用free等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减) - 6.栈
栈是用户存放程序临时创建的局部变量,也就是说我们函数括弧“{}”中定义的变量(但不包括static声明的变量,static意味着在数据段中存放变量)。除此以外,在函数被调用时,其参数也会被压入发起调用的进程栈中,并且待到调用结束后,函数的返回值也会被存放回栈中。由于栈的先进后出特点,所以栈特别方便用来保存/恢复调用现场。从这个意义上讲,我们可以把堆栈看成一个寄存、交换临时数据的内存区。
堆(heap)
为什么需要虚拟内存?
核心思想就是用有限内存承载尽量多的进程。
程序是一系列代码段,数据段的集合,而程序要运行必须是加载到内存里的,但是物理内存就那么大,如何能保证很多个程序都装载进去呢?这里就引进了虚拟内存的概念,虚拟内存基本思想就是,给每个程序都分配一个4G的虚拟的内存,但这部分内存占用的不是物理内存,而是磁盘空间,这部分叫做虚拟存储器,就是安装Linux系统时候的SWAP空间。而对应的物理内存就是物理存储器。
有没有觉得整个过程像开了“空头支票”一样?程序要跑起来,操作系统许诺它给你4G的空间,但却是不能用的,那真要执行的时候怎么办呢?操作系统会把磁盘上的程序代码数据“移”到内存里,把不需要的还会“移”出去到磁盘上,这样看上去就好像可以跑很多进程了。
虚拟内存和物理内存映射实现(MMU)
CPU中含有一个被称为内存管理单元(Memory Management Unit, MMU)的硬件,它的功能是将虚拟地址转换为物理地址。MMU需要借助存放在内存中的页表来动态翻译虚拟地址,该页表由操作系统管理。
页表
操作系统通过将虚拟内存分割为大小固定的块来作为硬盘和内存之间的传输单位,这个块被称为虚拟页(Virtual Page, VP),每个虚拟页的大小为P=2^p字节。物理内存也会按照这种方法分割为物理页(Physical Page, PP),大小也为P字节。
CPU在获得虚拟地址之后,需要通过MMU将虚拟地址翻译为物理地址。而在翻译的过程中还需要借助页表,所谓页表就是一个存放在物理内存中的数据结构,它记录了虚拟页与物理页的映射关系。
页表是一个元素为页表条目(Page Table Entry, PTE)的集合,每个虚拟页在页表中一个固定偏移量的位置上都有一个PTE。下面是PTE仅含有一个有效位标记的页表结构,该有效位代表这个虚拟页是否被缓存在物理内存中。
![79ab1f94-a312-eb11-8da9-e4434bdf6706.png](http://p03.5ceimg.com/content/79ab1f94-a312-eb11-8da9-e4434bdf6706.png)
虚拟页VP 0、VP 4、VP 6、VP 7被缓存在物理内存中,虚拟页VP 2和VP 5被分配在页表中,但并没有缓存在物理内存,虚拟页VP 1和VP 3还没有被分配。
在进行动态内存分配时,例如malloc()函数或者其他高级语言中的new关键字,操作系统会在硬盘中创建或申请一段虚拟内存空间,并更新到页表(分配一个PTE,使该PTE指向硬盘上这个新创建的虚拟页)。
由于CPU每次进行地址翻译的时候都需要经过PTE,所以如果想控制内存系统的访问,可以在PTE上添加一些额外的许可位(例如读写权限、内核权限等),这样只要有指令违反了这些许可条件,CPU就会触发一个一般保护故障,将控制权传递给内核中的异常处理程序。一般这种异常被称为“段错误(Segmentation Fault)”。
内存分配
从操作系统角度来看,进程分配内存有两种方式,分别由两个系统调用完成:brk和mmap(不考虑共享内存)。
- 1、brk是将数据段(.data)的最高地址指针_edata往高地址推;
- 2、mmap是在进程的虚拟地址空间中(堆和栈中间,称为文件映射区域的地方)找一块空闲的虚拟内存。
情况一、malloc小于128k的内存,使用brk分配内存
![7bab1f94-a312-eb11-8da9-e4434bdf6706.png](http://p04.5ceimg.com/content/7bab1f94-a312-eb11-8da9-e4434bdf6706.png)
这里写图片描述
brk分配的内存需要等到高地址内存释放以后才能释放(例如,在B释放之前,A是不可能释放的,这就是内存碎片产生的原因,什么时候紧缩看下面),而mmap分配的内存可以单独释放。
情况二、malloc大于128k的内存,使用mmap分配内存,在堆和栈之间找一块空闲内存分配
![80ab1f94-a312-eb11-8da9-e4434bdf6706.png](http://p02.5ceimg.com/content/80ab1f94-a312-eb11-8da9-e4434bdf6706.png)
这里写图片描述
mmap是用来建立从虚拟空间到磁盘空间的映射的,可以将一个虚拟空间地址映射到一个磁盘文件上,当不设置这个地址时,则由系统自动设置,函数返回对应的内存地址(虚拟地址),当访问这个地址的时候,就需要把磁盘上的内容拷贝到内存了,然后就可以读或者写,最后通过manmap可以将内存上的数据换回到磁盘,也就是解除虚拟空间和内存空间的映射,这也是一种读写磁盘文件的方法,也是一种进程共享数据的方法 共享内存。
代码段/数据段/BSS段/栈
#include
![82ab1f94-a312-eb11-8da9-e4434bdf6706.png](http://p04.5ceimg.com/content/82ab1f94-a312-eb11-8da9-e4434bdf6706.png)
保存于.data的变量有data_var0、data_var1 占用8字节
保存于.bss的变量有p_pro 占用8字节(64bit机器)
保存于.rodata的变量有global_var1、"123456" 共占用11字节
内核栈
内核栈存在于内核地址空间,用于实现系统调用。
系统调用
比如查看文件时,需要执行多次系统调用:open、read、write、close等。
过程如下
首先,把 CPU 寄存器里原来用户态的指令位置保存起来
为了执行内核代码,CPU 寄存器需要更新为内核态指令的新位置,最后跳转到内核态运行内核任务。
系统调用结束后,CPU 寄存器需要恢复原来保存的用户态,然后再切换到用户空间,继续运行进程
所以,一次系统调用的过程,其实是发生了两次 CPU 上下文切换。
线程栈
从 Linux 内核的角度来说,其实它并没有线程的概念,Linux 把所有线程都当做进程来实现,
线程仅仅被视为一个与其他进程共享某些资源的进程,
而是否共享地址空间几乎是进程和 Linux 中所谓线程的唯一区别。
pthread库是通过调用mmap()来为新的线程创建栈空间。
线程共享的资源
地址空间
全局变量
文件描述符
子进程
信号及信号处理函数
独享的资源
程序计数器
寄存器
线程栈
举一反三之Segmentation Fault错误原因总结
当程序试图访问不被允许访问的内存区域(比如,尝试写一块属于操作系统的内存),或以错误的类型访问内存区域(比如,尝试写一块只读内存),即段错误应该就是访问了不可访问的内存,这个内存要么是不存在的,要么是受系统保护的
产生SIGSEGV的可能情况
我们把指针运算(加减)引起的越界、野指针、空指针都归为指针越界。SIGSEGV在很多时候是由于指针越界引起的,但并不是所有的指针越界都会引发SIGSEGV。一个越界的指针,如果不引用它,是不会引起SIGSEGV的。而即使引用了一个越界的指针,也不一定引起SIGSEGV。
- 1、试图写只读数据
char
由上 str执行的内容是放在.rodata中的是只读的。
- 2、访问不存在的内存地址
int i=0;
scanf ("%d", i); /* should have used &i */
printf ("%dn", i);
return 0;
- 3、访问空地址
int *p = null;
*p = 1;
- 4、数组访问越界
#include
- 5、 试图把一个整数按照字符串的方式输出
int main() {
int b = 10;
printf("%sn", b);
return 0;
}
- 6、 堆栈溢出
1、就是不顾堆栈中分配的局部数据块大小,向该数据块写入了过多的数据,导致数据越界,结果覆盖了别的数据
此种情况不一定会引发段错误。
2、超过系统设置的栈大小上限。
举一反三之使用mmap实现共享内存
map_normalfile2试图打开命令行参数指定的一个普通文件,把该文件映射到进程的地址空间,并对映射后的地址空间进行写操作。map_normalfile1把命令行参数指定的文件映射到进程地址空间,然后对映射后的地址空间执行读操作。这样,两个进程通过命令行参数指定同一个文件来实现共享内存方式的进程间通信。
注:实现共享内存也可以基于shm(Linux环境下的低延迟通信 https://github.com/MengRao/tcpshm)
举一反三之多个进程调用同一个动态库
多个进程为什么可以同时调用同一个动态库而没有任何问题?
原因是多个进程只共享so的代码段,不共享数据段。
详见:https://blog.csdn.net/u010312436/article/details/81263980
参考
http://blog.51cto.com/wulingdong/2047496 邬领东-漫游计算机系统之虚拟存储器http://www.kerneltravel.net/journal/v/mem.htm Linux内存管理https://juejin.im/post/59f8691b51882534af254317 虚拟内存那点事儿http://codefine.site/1191.html 从一个非典型的内存越界访问问题看Linux的进程内存布局http://blog.jobbole.com/110272/ 一个由进程内存布局异常引起的问题https://stackoverflow.com/questions/2346806/what-is-a-segmentation-fault https://blog.csdn.net/u010150046/article/details/77775114 Segmentation Fault错误原因总结