全方位剖析 Linux 操作系统,太全了(12)


和 代码段(Text segment) 不一样,data segment 数据段可以改变 。程序总是修改它的变量 。而且,许多程序需要在执行时动态分配空间 。Linux 允许数据段随着内存的分配和回收从而增大或者减小 。为了分配内存,程序可以增加数据段的大小 。在 C 语言中有一套标准库 malloc 经常用于分配内存 。进程地址空间描述符包含动态分配的内存区域称为 堆(heap) 。
第三部分段是 栈段(stack segment) 。在大部分机器上,栈段会在虚拟内存地址顶部地址位置处,并向低位置处(向地址空间为 0 处)拓展 。举个例子来说,在 32 位 x86 架构的机器上,栈开始于 0xC0000000,这是用户模式下进程允许可见的 3GB 虚拟地址限制 。如果栈一直增大到超过栈段后,就会发生硬件故障并把页面下降一个页面 。
当程序启动时,栈区域并不是空的,相反,它会包含所有的 shell 环境变量以及为了调用它而向 shell 输入的命令行 。举个例子,当你输入
cp cxuan lx时,cp 程序会运行并在栈中带着字符串 cp cxuan lx ,这样就能够找出源文件和目标文件的名称 。
当两个用户运行在相同程序中,例如编辑器(editor),那么就会在内存中保持编辑器程序代码的两个副本,但是这种方式并不高效 。Linux 系统支持共享文本段作为替代 。下面图中我们会看到 A 和 B 两个进程,它们有着相同的文本区域 。

全方位剖析 Linux 操作系统,太全了

文章插图
 
数据段和栈段只有在 fork 之后才会共享,共享也是共享未修改过的页面 。如果任何一个都需要变大但是没有相邻空间容纳的话,也不会有问题,因为相邻的虚拟页面不必映射到相邻的物理页面上 。
除了动态分配更多的内存,Linux 中的进程可以通过内存映射文件来访问文件数据 。这个特性可以使我们把一个文件映射到进程空间的一部分而该文件就可以像位于内存中的字节数组一样被读写 。把一个文件映射进来使得随机读写比使用 read 和 write 之类的 I/O 系统调用要容易得多 。共享库的访问就是使用了这种机制 。如下所示
全方位剖析 Linux 操作系统,太全了

文章插图
 
我们可以看到两个相同文件会被映射到相同的物理地址上,但是它们属于不同的地址空间 。
映射文件的优点是,两个或多个进程可以同时映射到同一文件中,任意一个进程对文件的写操作对其他文件可见 。通过使用映射临时文件的方式,可以为多线程共享内存提供高带宽,临时文件在进程退出后消失 。但是实际上,并没有两个相同的地址空间,因为每个进程维护的打开文件和信号不同 。
Linux 内存管理系统调用下面我们探讨一下关于内存管理的系统调用方式 。事实上,POSIX 并没有给内存管理指定任何的系统调用 。然而,Linux 却有自己的内存系统调用,主要系统调用如下
系统调用描述s = brk(addr)改变数据段大小a = mmap(addr,len,prot,flags,fd,offset)进行映射s = unmap(addr,len)取消映射
如果遇到错误,那么 s 的返回值是 -1,a 和 addr 是内存地址,len 表示的是长度,prot 表示的是控制保护位,flags 是其他标志位,fd 是文件描述符,offset 是文件偏移量 。
brk 通过给出超过数据段之外的第一个字节地址来指定数据段的大小 。如果新的值要比原来的大,那么数据区会变得越来越大,反之会越来越小 。
mmap 和 unmap 系统调用会控制映射文件 。mmp 的第一个参数 addr 决定了文件映射的地址 。它必须是页面大小的倍数 。如果参数是 0,系统会分配地址并返回 a 。第二个参数是长度,它告诉了需要映射多少字节 。它也是页面大小的倍数 。prot 决定了映射文件的保护位,保护位可以标记为 可读、可写、可执行或者这些的结合 。第四个参数 flags 能够控制文件是私有的还是可读的以及 addr 是必须的还是只是进行提示 。第五个参数 fd 是要映射的文件描述符 。只有打开的文件是可以被映射的,因此如果想要进行文件映射,必须打开文件;最后一个参数 offset 会指示文件从什么时候开始,并不一定每次都要从零开始 。
Linux 内存管理实现内存管理系统是操作系统最重要的部分之一 。从计算机早期开始,我们实际使用的内存都要比系统中实际存在的内存多 。内存分配策略克服了这一限制,并且其中最有名的就是 虚拟内存(virtual memory) 。通过在多个竞争的进程之间共享虚拟内存,虚拟内存得以让系统有更多的内存 。虚拟内存子系统主要包括下面这些概念 。


推荐阅读