linux内核--进程地址空间(三)
引言:上篇博文中,我们简单的介绍了Linux虚拟存储器的概念及组成情况,下面来分析分析进程的创建和终结及跟进程地址空间的联系。
这里首先介绍一个比较重要的概念:存储器映射
在Linux系统中,通过将一个虚拟存储器区域与一个磁盘上的对象关联起来,以初始化这个虚拟存储器区域的内容,这个过程称为存储器映射。存储器映射为共享数据、创建新的进程以及加载程序提供了一种高效的机制。
虚拟存储器区域可以映射到两种类型对象中:
1)普通文件:一个虚拟区域可以映射到普通磁盘文件的连续部分,例如可执行目标文件。虚拟区域分为若干的虚拟页面,这些虚拟页面初始化时并没有实际交换进物理存储器,直到CPU第一次引用页面时才真正的加载进物理内存。如果虚拟区域比映射的文件要大,则剩下的部分用零填充。
2)匿名文件:匿名文件是由内核创建的,包含的全部是二进制零。映射到匿名文件的区域中的页面有时称为 请求二进制零的页。
注意:无论映射到何种文件,一旦一个虚拟页面被初始化了,它就在一个由内核维护的专门的交换文件之间换来换去。这里的交换文件也称为 交换空间。由此可见任何时候,交换空间限制着当前运行着的进程能够分配的虚拟页面的总数。
有了前面的一些概念的基础下面我们开始看进程的创建、执行、退出。
一:进程创建
Unix的进程创建比较特别。许多其他操作系统提供了产生机制。首先在新的地址空间里创建进程,读入可执行文件,最后开始执行。Unix采用了不同方式:它把上述步骤分为两步,分解到两个单独的函数中执行:fork( ) 和 exec( )。
fork( )函数被当前进程调用时,内核为新进程创建各种数据结构(例如内核栈、thread_info结构、task_struct结构)并分配给它一个唯一的PID。为了给新进程创建虚拟存储器,它创建了当前进程所有资源的原样拷贝。它将两个进程的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为 私有的写时拷贝。
Linux的fork( )使用写时拷贝页实现。写时拷贝是一种可以推迟甚至免除拷贝数据的技术。内核此时并不复制整个进程地址空间,而是让父进程和子进程共享一个拷贝。
只有在需要写入的时候,数据才会被复制,从而使各个进程拥有各自的拷贝,从而为父子进程保持了私有地址空间的抽象概念。资源的复制只有在需要写入的时候才进行,在此之前,只是以只读的方式共享。这中技术使得地址空间上的页的拷贝被推迟到实际发生写入的时候才进行。在页根本不会写入的情况下(例如fork()之后立即调用exec() )它们就无须复制了。
下面看个实例:
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
int main( )
{
int pid;
int x = ;
pid = fork();
if(pid == ) /* Child */
{
printf(" child : x = %d\n",++x);
exit();
} /* Parent */
printf("parent : x = %d\n",--x);
exit();
}

输出如下:

由此可以看出:
1)fork()调用一次,返回两次:一次返回到父进程,一次返回到子进程。
2)并发执行:父进程和子进程并发运行,内核能够以任意方式交替运行它们,这里是父进程先运行,然后是子进程。但是在另外一个系统上运行时不一定是这个顺序。
3)父子进程都有自己的私有地址空间,父子进程对x的操作都是独立的,不会反应在另外一个进程的存储器中。
函数 int execl(const char *filename,const char *argv[],const char char *envp[]):
下面我们举例看exec 函数是如何加载和执行程序的:
#include <stdlib.h>
#include<stdio.h>
#include <unistd.h>
int main()
{
int pid = fork();
if(pid<)
{
perror("fork");
} else if(pid == )
{ execl("hello",NULL,NULL);
/* We can only reach this code when there is an error in execl*/
perror("execl");
}
else
{
sleep();
printf("This is parent\n");
} exit(); }

这里的execl调用:execl("hello",NULL,NULL);中的hello是上篇博文中提到的hello.c程序对应的可执行目标文件。

hello.c
#include<stdio.h>
int main()
{ printf("Hello\n");
return ;
}

execl("hello",NULL,NULL)调用后在当前子进程中加载并运行包含在可执行目标文件 hello中的程序,用hello程序有效的替代了当前程序。加载并运行hello的步骤如下:
1)删除已经存在的用户区域,删除当前调用execl的子进程的虚拟地址的用户部分中的已存在的区域结构。
2)将可执行文件hello的连续的片映射到连续的虚拟存储器中。
段头部表 描述了这种映射关系:
下面我们用readelf 查看可执行文件 hello 的段头部:

从图中可以看出:
1:映射私有区域:即为新程序的代码、数据、bss、和栈区域创建和初始化新的区域结构。
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
LOAD 0x000000 0x08048000 0x08048000 0x005bc 0x005bc R E 0x1000
代码段:对齐到一个4KB(0x1000=2^12,为X86平台一个页面的大小)的边界,有可读、可执行权限。从可执行目标文件中偏移量为0开始的0x005bc字节长的代码段(其中包括了ELF头部、段头部表、以及.init、.text、和 .rodata节)被映射到开始于虚拟地址0x08048000,长度为0x005bc字节的虚拟存储区域中。
LOAD 0x000f14 0x08049f14 0x08049f14 0x00100 0x00108 RW 0x1000
数据段:同样对齐到4KB大小的边界,有可读可写权限。可执行文件偏移0x000f14处开始,长度为0x100个字节的数据段被映射到开始于虚拟地址0x08049f14处,长度为0x00108字节的虚拟存储区域中。
bss段、栈段、堆段被初始化为0,即被映射到匿名文件。
2:映射共享区域:如果hello程序与共享对象链接,比如标准C库libc.so,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间的共享区域内。
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align 1
DYNAMIC 0x000f28 0x08049f28 0x08049f28 0x000c8 0x000c8 RW 0x4

动态链接ELF中最重要的结构就是.dynamic段,这个段保存了动态链接器所需要的基本信息,比如依赖于哪些共享对象、动态链接符号表位置、动态链接重定位表的位置、共享对象初始化代码的地址等。
如上图所示:这里列出了hello可执行文件动态链接依赖的对象。
动态链接共享库的步骤:
1:加载和运行动态链接器
动态链接器本身就是一个共享目标,所以要先加载本身,在可执行文件hello中的.interp段包含了动态链接器的路径名:

2:装载所有需要的共享对象
启动完动态链接器之后,动态链接器将可执行文件hello和链接器本身的符号表都合并在一起,组成全局符号表。然后链接器开始寻找可执行文件所依赖的共享对象,在之前提到的.dynamic段中,有一种入口的类型是 DT _NEEDED,它所指出的Shared library: [libc.so.6]就是该可执行文件所依赖的共享对象。由此,链接器可以列出可执行文件hello所需要的所有共享对象,并将这些对象名字放入到一个装载集合中。然后链接器根据名字找到相应的文件,并将它相应的代码段、数据段映射到进程地址空间的共享区域中。
3:重定位和初始化
当上面步骤完成后,根据进程的全局符号表,对GOT/PLT中的每个需要重定位的位置进行修正,这种技术称为延迟绑定。
完成重定位和初始化后,所有准备工作结束,所需要的共享对象也都已经装载并且链接完成。最后将进程的控制权转交给hello程序的入口并开始执行。
可执行elf文件格式,及加载完可执行文件hello后的进程地址空间如下图:


在文章中介绍了几个重要的结构体,关于这几个结构体的含义和之间的关系如下:
主要的数据结构有:
task_struct : 进程描述符结构,定义在<linux/sched.h>文件中,描述了该进程打开的文件、进程的地址空间、进程的状态等信息。
mm_struct :进程虚拟内存描述符,定义在<linux/sched.h>文件中,该结构包含了和进程地址空间相关的全部信息。
vm_area_struct:虚拟内存区域描述符,定义在<linux/mm.h>文件中,该结构体描述了指定地址空间内连续区间上的一个独立的内存范围(比如进程用户空间栈、代码段、数据段等)。
三个结构之间的关系:
task_struct 中的一个 字段 mm 指向当前进程的虚拟地址空间描述符 mm_struct , mm_struct中的字段 mmap 指向一个vm_area_struct组成的链表,其中每个vm_area_struct 都描述了当前虚拟地址空间的一个区域。
除了这三个结构体外,还有几个重要的结构体,在其他临近的博文中有介绍,比如thread_info结构体,这个结构体中包括了task_struct结构体,这几个结构体的内容和含义层次不同。
linux内核--进程地址空间(三)的更多相关文章
- linux内核--进程地址空间(一)
引言:现代操作系统提供了一种对内存的抽象概念,叫做虚拟存储器,它为每个进程提供了一个大的,一致的,和私有的地址空间.通过一个很清晰的机制,虚拟存储器提供了3个重要的能力: 1)它将主存看成是一个存储在 ...
- Linux内核分析(三)----初识linux内存管理子系统
原文:Linux内核分析(三)----初识linux内存管理子系统 Linux内核分析(三) 昨天我们对内核模块进行了简单的分析,今天为了让我们今后的分析没有太多障碍,我们今天先简单的分析一下linu ...
- 十天学Linux内核之第三天---内存管理方式
原文:十天学Linux内核之第三天---内存管理方式 昨天分析的进程的代码让自己还在头昏目眩,脑子中这几天都是关于Linux内核的,对于自己出现的一些问题我会继续改正,希望和大家好好分享,共同进步.今 ...
- Linux内核设计第三周——构造一个简单的Linux系统
Linux内核设计第三周 ——构造一个简单的Linux系统 一.知识点总结 计算机三个法宝: 存储程序计算机 函数调用堆栈 中断 操作系统两把宝剑: 中断上下文的切换 进程上下文的切换 linux内核 ...
- linux内核分析第三周
20135103王海宁 linux内核分析第三周 http://mooc.study.163.com/course/USTC-1000029000 按照课堂提供的方法,命令行一行行敲上去,我是手机缓 ...
- LINUX内核分析第三周学习总结——构造一个简单的Linux系统MenuOS
LINUX内核分析第三周学习总结——构造一个简单的Linux系统MenuOS 张忻(原创作品转载请注明出处) <Linux内核分析>MOOC课程http://mooc.study.163. ...
- 20135327郭皓--Linux内核分析第三周 构造一个简单的Linux系统MenuOS
Linux内核分析第三周 构造一个简单的Linux系统MenuOS 前提回顾 1.计算机是如何工作的三个法宝 1.存储程序计算机 2.函数调用堆栈 3.中断 2.操作系统的两把宝剑 中断上下文的切换 ...
- Linux内核分析第三周学习笔记
linux内核分析第三周学习笔记 标签(空格分隔): 20135328陈都 陈都 原创作品转载请注明出处 <Linux内核分析>MOOC课程http://mooc.study.163.co ...
- Linux内核分析第三周学习博客——跟踪分析Linux内核的启动过程
Linux内核分析第三周学习博客--跟踪分析Linux内核的启动过程 实验过程截图: 过程分析: 在Linux内核的启动过程中,一共经历了start_kernel,rest_init,kernel_t ...
随机推荐
- ElastciSearch常用APi
列出所有的索引: GET /_cat/indices?v 删除索引 DELETE /customer?pretty
- 16Aspx.com源码2014年7月详细
Web电子商务网(三层)V2.0源码 2014-07-31 [VS2010] 源码介绍: Web电子商务网(三层)V2.0源码 源码描述: 一.源码特点 采用三层架构开发, ...
- C#测量程序运行时间及cpu使用时间
转载:http://www.cnblogs.com/yanpeng/archive/2008/10/15/1943369.html 对一个服务器程序想统计每秒可以处理多少数据包,要如何做?答案是用处理 ...
- 使用solr搭建你的全文检索
Solr 是一个可供企业使用的.基于 Lucene 的开箱即用的搜索服务器.对Lucene不熟?那么建议先看看下面两篇文档: 实战Lucene,第 1 部分: 初识 Lucene:http://www ...
- Ext.Net学习笔记13:Ext.Net GridPanel Sorter用法
Ext.Net学习笔记13:Ext.Net GridPanel Sorter用法 这篇笔记将介绍如何使用Ext.Net GridPanel 中使用Sorter. 默认情况下,Ext.Net GridP ...
- 九度OJ 1370 数组中出现次数超过一半的数字
题目地址:http://ac.jobdu.com/problem.php?pid=1370 题目描述: 数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字.例如输入一个长度为9的数组{1,2 ...
- (redis缓存更新策略)postgres 9.4.1 && redis 3.7.0 && redis_fdw_REL9_4_STABLE
首先下载redis_fdw,这里要注意下载的版本.(https://github.com/pg-redis-fdw/redis_fdw) 一开始,我下载了REL9_4_STABLE_pre2.8版本, ...
- python 自动化之路 day 06
ATM作业讲解: 数据访问层 业务逻辑层 time & datetime模块 import time # print(time.clock()) #返回处理器时间,3.3开始已废弃 , 改成了 ...
- SQL Function(方法)
1.为什么有存储过程(procedure)还需要(Function) fun可以再select语句中直接调用,存储过程是不行的. 一般来说,过程显示的业务更为复杂:函数比较有针对性. create f ...
- javascript 对象的创建,引用,释放,删除方法
1.用函数构造 A.声明时同时设置属性和方法 function func(){ this.name = "myname"; this.say = function(){aler ...