引言:上篇博文中,我们简单的介绍了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内核--进程地址空间(三)的更多相关文章

  1. linux内核--进程地址空间(一)

    引言:现代操作系统提供了一种对内存的抽象概念,叫做虚拟存储器,它为每个进程提供了一个大的,一致的,和私有的地址空间.通过一个很清晰的机制,虚拟存储器提供了3个重要的能力: 1)它将主存看成是一个存储在 ...

  2. Linux内核分析(三)----初识linux内存管理子系统

    原文:Linux内核分析(三)----初识linux内存管理子系统 Linux内核分析(三) 昨天我们对内核模块进行了简单的分析,今天为了让我们今后的分析没有太多障碍,我们今天先简单的分析一下linu ...

  3. 十天学Linux内核之第三天---内存管理方式

    原文:十天学Linux内核之第三天---内存管理方式 昨天分析的进程的代码让自己还在头昏目眩,脑子中这几天都是关于Linux内核的,对于自己出现的一些问题我会继续改正,希望和大家好好分享,共同进步.今 ...

  4. Linux内核设计第三周——构造一个简单的Linux系统

    Linux内核设计第三周 ——构造一个简单的Linux系统 一.知识点总结 计算机三个法宝: 存储程序计算机 函数调用堆栈 中断 操作系统两把宝剑: 中断上下文的切换 进程上下文的切换 linux内核 ...

  5. linux内核分析第三周

    20135103王海宁 linux内核分析第三周 http://mooc.study.163.com/course/USTC-1000029000  按照课堂提供的方法,命令行一行行敲上去,我是手机缓 ...

  6. LINUX内核分析第三周学习总结——构造一个简单的Linux系统MenuOS

    LINUX内核分析第三周学习总结——构造一个简单的Linux系统MenuOS 张忻(原创作品转载请注明出处) <Linux内核分析>MOOC课程http://mooc.study.163. ...

  7. 20135327郭皓--Linux内核分析第三周 构造一个简单的Linux系统MenuOS

    Linux内核分析第三周  构造一个简单的Linux系统MenuOS 前提回顾 1.计算机是如何工作的三个法宝 1.存储程序计算机 2.函数调用堆栈 3.中断 2.操作系统的两把宝剑 中断上下文的切换 ...

  8. Linux内核分析第三周学习笔记

    linux内核分析第三周学习笔记 标签(空格分隔): 20135328陈都 陈都 原创作品转载请注明出处 <Linux内核分析>MOOC课程http://mooc.study.163.co ...

  9. Linux内核分析第三周学习博客——跟踪分析Linux内核的启动过程

    Linux内核分析第三周学习博客--跟踪分析Linux内核的启动过程 实验过程截图: 过程分析: 在Linux内核的启动过程中,一共经历了start_kernel,rest_init,kernel_t ...

随机推荐

  1. Algorithm

    经过慎重考虑,也经过反复思考.查阅网上相关资料 一位高手对我的建议: 一般要做到50行以内的程序不用调试.100行以内的二分钟内调试成功.acm主要是考算法的 ,主要时间是花在思考算法上,不是花在写程 ...

  2. 安卓百度地图开发so文件引用失败问题研究

    博客: 安卓之家 微博: 追风917 CSDN: 蒋朋的家 简书: 追风917 博客园: 追风917 # 问题 首先,下面的问题基本都是在Android Studio下使用不当导致,eclipse是百 ...

  3. java中collection、map、set、list简介 (转)

    Collection接口  Collection是最基本的集合接口,一个Collection代表一组Object,即Collection的元素(Elements).一些Collection允许相同的元 ...

  4. C语言链表全操作(增,删,改,查,逆序,递增排序,递减排序,链式队列,链式栈)

    一,数据结构——链表全操作: 链表形式: 其中,每个节点(Node)是一个结构体,这个结构体包含数据域,指针域,数据域用来存放数据,指针域则用来指向下一个节点: 特别说明:对于单链表,每个节点(Nod ...

  5. HDU 4628 Pieces(DP + 状态压缩)

    Pieces 题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=4628 题目大意:给定一个字符串s,如果子序列中有回文,可以一步删除掉它,求把整个序列删除 ...

  6. C++动态分配内存

    动态分配(Dynamic Memory)内存是指在程序运行时(runtime)根据用户输入的需要来分配相应的内存空间. 1.内存分配操作符new 和 new[] Example: (1)给单个元素动态 ...

  7. 【POJ2094】【差分序列】Angry Teacher

    Description Mr. O'Cruel is teaching Math to ninth grade students. Students of course are very lazy, ...

  8. docker 中搭建tomcat

    关于docker中tomcat的搭建,没有什么多说的,直接下载官方镜像 docker pull tomcat  (可能会下载到好几个镜像,就是tag不同,有知道为什么的可以告诉我) 然后我看的  ht ...

  9. js 作为属性的变量

    当声明一个javascript全局变量时,实际上是定义了全局对象的一个属性. 当使用var声明一个变量时,创建的这个属性是不可配置的,也就是说这个变量无法通过delete运算符来删除.可能你已经注意到 ...

  10. wdcp日志

    apache或nginx都有开关默认日志,一个是正常访问日志,一个是错误的日志,目录在 /www/wdlinux/nginx-1.0.15/logs /www/wdlinux/httpd-2.2.22 ...