第 8 章 存储管理

Post on 24-Jan-2016

120 Views

Category:

Documents

3 Downloads

Preview:

Click to see full reader

DESCRIPTION

第 8 章 存储管理. 如何进行存储管理,使系统在有限的硬件环境发挥出尽可能高的性能是存储管理的核心内容。 一般 Linux 中的内存管理以及 uClinux 内存管理有什么特点。. 本章主要介绍了: ◆ 缺少 MMU 支持的内存管理。 ◆ 内存管理 3 种模型。 ◆ 标准 Linux 的内存管理、 uClinux 内存管理及其的局限性。 ◆ 内存管理模块的启动过程。 ◆ 用户程序的内存分布、 reloc 段机制、可执行文件格式和执行文件加载流程。. 8.1 缺少 MMU 支持的内存管理. 内存管理是操作系统中非常重要的子模块。 - PowerPoint PPT Presentation

TRANSCRIPT

第第 88 章 存储管理章 存储管理

如何进行存储管理,使系统在有限的如何进行存储管理,使系统在有限的硬件环境发挥出尽可能高的性能是存储管硬件环境发挥出尽可能高的性能是存储管理的核心内容。理的核心内容。

一般一般 LinuxLinux 中的内存管理以及中的内存管理以及 uClinuClinuxux 内存管理有什么特点。内存管理有什么特点。

本章主要介绍了:本章主要介绍了:◆ ◆ 缺少缺少 MMUMMU 支持的内存管理。支持的内存管理。◆ ◆ 内存管理内存管理 33 种模型。种模型。◆ ◆ 标准标准 LinuxLinux 的内存管理、的内存管理、 uClinuxuClinux 内存内存管理及其的局限性。管理及其的局限性。◆ ◆ 内存管理模块的启动过程。内存管理模块的启动过程。◆ ◆ 用户程序的内存分布、用户程序的内存分布、 relocreloc 段机制、段机制、可执行文件格式和执行文件加载流程。可执行文件格式和执行文件加载流程。

8.1 8.1 缺少缺少 MMUMMU 支持的内存管理支持的内存管理

内存管理是操作系统中非常重要的子内存管理是操作系统中非常重要的子模块。模块。

如同普通操作系统一样,在嵌入式操如同普通操作系统一样,在嵌入式操作系统中,内存管理实现的好坏对系统性作系统中,内存管理实现的好坏对系统性能有决定性的作用。 能有决定性的作用。

LinuxLinux 内核的内存管理子模块性能优内核的内存管理子模块性能优越,目前大多数使用的嵌入式越,目前大多数使用的嵌入式 LinuxLinux 解决解决方案中都直接使用了标准方案中都直接使用了标准 LinuxLinux 内存管理内存管理机制,或者对机制,或者对 LinuxLinux 内核的内存管理子模内核的内存管理子模块做了很小的改动,但嵌入式块做了很小的改动,但嵌入式 LinuxLinux 的内的内存管理仍然是值得关心的问题。 存管理仍然是值得关心的问题。

目前的目前的 LinuxLinux 内核已经被移植到大内核已经被移植到大量的非量的非 x86x86 平台上,包括平台上,包括 ARMARM ,, M68KM68K ,,PPCPPC ,, AlphaAlpha ,, SparcSparc 等。等。

其中其中 uClinuxuClinux 主要针对缺少主要针对缺少 MMUMMU 内内存管理的优秀嵌入式存管理的优秀嵌入式 LinuxLinux 操作系统。操作系统。

目前,由于体积限制或者出于降低成目前,由于体积限制或者出于降低成本的考虑,嵌入式系统中所使用的微处理本的考虑,嵌入式系统中所使用的微处理器大多缺少器大多缺少 MMUMMU 。。

这些硬件平台包括数字信号处理器这些硬件平台包括数字信号处理器(DSP)(DSP) ,, SoC(System on Chip)SoC(System on Chip) ,以及那,以及那些不具有些不具有 MMUMMU 的微处理器,的微处理器,

例如例如 ARM7TDMIARM7TDMI ,, MotorolaMotorola 的的 M6M68K8K 龙珠系列等。 龙珠系列等。

这些没有这些没有 MMUMMU 的微处理器为传统的微处理器为传统 LiLinuxnux 的应用造成了一个障碍,它们要求使的应用造成了一个障碍,它们要求使用用 flatflat 内存模型——即没有虚拟内存内存模型——即没有虚拟内存 (( 分页分页/页面交换/页面交换 )) 、内存地址转换、内存地址转换 (( 分段分段 )) 和内和内存保护的内存管理机制。存保护的内存管理机制。

在基于在基于 LinuxLinux 的嵌入式操作系统中,的嵌入式操作系统中,uClinuxuClinux 因为具有在不带内存管理单元因为具有在不带内存管理单元 (M(MMU)MU) 的硬件平台上运行的能力而独领风骚。的硬件平台上运行的能力而独领风骚。

它通过提供修改过的它通过提供修改过的 LinuxLinux 内核、内核、 CC库和编译器为嵌入式系统开发者提供了与库和编译器为嵌入式系统开发者提供了与标准标准 LinuxLinux 相同的相同的 APlAPl ,实现了从,实现了从 LinuxLinux到无到无 MMUMMU 的微处理器上的无缝转化。的微处理器上的无缝转化。

目前,在无目前,在无 MMUMMU 平台上能够运行的平台上能够运行的LinuxLinux 仅有仅有 uClinuxuClinux 一家。一家。

8.1.1 38.1.1 3 种内存管理模型种内存管理模型

操作系统的内存管理模型可以分成如操作系统的内存管理模型可以分成如下下 33 种:种:

1. 1. 单一程序模型单一程序模型

这是没有硬件地址转换的内存管理模型。这是没有硬件地址转换的内存管理模型。 应用程序始终在物理内存中的同一地址 应用程序始终在物理内存中的同一地址

空间运行,一个时刻只有一个应用程序被加载空间运行,一个时刻只有一个应用程序被加载运行,程序加载器把应用程序加载到内存低端,运行,程序加载器把应用程序加载到内存低端,将操作系统加载到高端。 将操作系统加载到高端。

一个应用程序可以对所有的物理内存一个应用程序可以对所有的物理内存地址进行寻址。地址进行寻址。

早期的操作系统受限于硬件能力多采早期的操作系统受限于硬件能力多采用这种模型,现代操作系统中这种内存管用这种模型,现代操作系统中这种内存管理方式已经不见了。理方式已经不见了。

2. 2. 多程序模型多程序模型

这也是没有硬件地址转换的内存管理这也是没有硬件地址转换的内存管理模型。模型。

即使没有硬件地址转换功能,多个程即使没有硬件地址转换功能,多个程序也可以共享相同的物理地址。序也可以共享相同的物理地址。

在程序被加载到内存的时候,改变程在程序被加载到内存的时候,改变程序中寻址指令序中寻址指令 (load(load ,, storestore ,, jump)jump) 所所使用的地址值为当前被加载到的位置。使用的地址值为当前被加载到的位置。

uClinuxuClinux 正是使用了这种模型。正是使用了这种模型。

3. 3. 具有地址转换硬件的内存管理模型具有地址转换硬件的内存管理模型

应用程序使用的是虚拟地址,应用程序使用的是虚拟地址, CPUCPU实际执行程序所使用的是物理地址,从虚实际执行程序所使用的是物理地址,从虚拟地址到物理地址的转换需要操作系统和拟地址到物理地址的转换需要操作系统和MMUMMU(Memory Management Unit)(Memory Management Unit) 硬件的硬件的参与。参与。

标准标准 LinuxLinux 以及大多数现代操作系以及大多数现代操作系统都使用这种内存管理模型。统都使用这种内存管理模型。

8.1.2 8.1.2 标准标准 LinuxLinux 的内存管理的内存管理

为了理解为了理解 uClinuxuClinux 对标准对标准 LinuxLinux 的的裁减,首先必须清楚标准裁减,首先必须清楚标准 LinuxLinux 中内存管中内存管理的各种基本概念。理的各种基本概念。

这里对相关于虚拟内存的各个概念作这里对相关于虚拟内存的各个概念作一个总结。一个总结。

LinuxLinux 使用了上述的第三个模型,即使用了上述的第三个模型,即使用了地址转换硬件的内存管理模型。使用了地址转换硬件的内存管理模型。

虚拟内存是一种对虚拟内存是一种对 RAMRAM 和磁盘和磁盘 (( 或或称做主存和辅存称做主存和辅存 )) 进行无缝混合访问的技术。进行无缝混合访问的技术。

所有的虚拟内存对于应用程序来说好所有的虚拟内存对于应用程序来说好像是真的存在一样,应用程序在加载的时像是真的存在一样,应用程序在加载的时候并不需要关心是否会超过系统中实际的候并不需要关心是否会超过系统中实际的物理物理 RAMRAM 的大小的大小

内核主要通过页目录和页表的地址转内核主要通过页目录和页表的地址转换功能将应用程序的虚拟地址转换成物理换功能将应用程序的虚拟地址转换成物理地址 地址

这个过程中可能将应用程序中使用的这个过程中可能将应用程序中使用的超过了实际物理内存大小的虚拟地址映射超过了实际物理内存大小的虚拟地址映射到适当的真实物理地址,从而使应用程序到适当的真实物理地址,从而使应用程序可以随心所欲地使用巨大的虚拟存储空间可以随心所欲地使用巨大的虚拟存储空间(( 对对 Linux 2.4Linux 2.4内核来说为内核来说为 4GB)4GB) 。。

但是只通过地址映射还不能解决有限但是只通过地址映射还不能解决有限的物理内存被虚拟地址空间所使用的问题,的物理内存被虚拟地址空间所使用的问题,操作系统还必须使用页面交换机制将那些操作系统还必须使用页面交换机制将那些暂时不再使用的内存空间交换到外存中以暂时不再使用的内存空间交换到外存中以使其他程序,能够使用物理内存。使其他程序,能够使用物理内存。

LinuxLinux 没有将整个进程所使用的空间没有将整个进程所使用的空间都交换到外存中,而是对部分不再使用的都交换到外存中,而是对部分不再使用的大小为大小为 4KB 4KB 的页面进行交换,这样就获得的页面进行交换,这样就获得了更多的灵活性。了更多的灵活性。

应用程序在标准应用程序在标准 LinuxLinux 中的加载使中的加载使用了“按需调页”的策略,也就是说,应用了“按需调页”的策略,也就是说,应用程序在开始被加载的时候并没有一次被用程序在开始被加载的时候并没有一次被全部装载到内存中,只有那些现在必需的全部装载到内存中,只有那些现在必需的代码和数据在开始进行了加载。 代码和数据在开始进行了加载。

在程序执行的过程中,如果遇到了不在程序执行的过程中,如果遇到了不在内存中的程序部分将产生页面错误,操在内存中的程序部分将产生页面错误,操作系统在处理这个错误中断的时候会到外作系统在处理这个错误中断的时候会到外存中找到相应的应用程序部分进行加载。 存中找到相应的应用程序部分进行加载。

这种设计是基于计算机科学中著名的这种设计是基于计算机科学中著名的90-1090-10 规则的:规则的: 9090 %的程序执行时间花费%的程序执行时间花费在整个程序在整个程序 1010 %的代码上。%的代码上。

所以只要保持我们用到的程序在内存所以只要保持我们用到的程序在内存中,就可以既满足程序的执行速度又节约中,就可以既满足程序的执行速度又节约物理内存空间。 物理内存空间。

标准标准 LinuxLinux 中的内存管理模型中的内存管理模型如图如图8-18-1所示所示。。

8.1.3 uClinux8.1.3 uClinux 的内存管理的内存管理 由于由于 M68KM68K 系列微处理器中没有分系列微处理器中没有分

段的概念,所以标准段的概念,所以标准 LinuxLinux 内核中从虚拟内核中从虚拟地址到线性地址的映射已经不必存在了。地址到线性地址的映射已经不必存在了。

而且由于缺少了而且由于缺少了 MMUMMU 硬件的支持,硬件的支持,uClinuxuClinux 不能支持虚拟内存管理和内存保护。不能支持虚拟内存管理和内存保护。

这就意味着它完全不使用标准这就意味着它完全不使用标准 LinuxLinux内核中的分页管理机制,也就没有了页表内核中的分页管理机制,也就没有了页表和页目录对线性地址的映射,从而线性地和页目录对线性地址的映射,从而线性地址到物理地址的转换也是不需要进行任何址到物理地址的转换也是不需要进行任何工作的。工作的。

换句话说,换句话说, uClinuxuClinux 中所使用的都是中所使用的都是直接物理地址。 直接物理地址。

而且,由于没有了虚拟内存管理功能,而且,由于没有了虚拟内存管理功能,uClinuxuClinux 不再使用“按需调页”算法。不再使用“按需调页”算法。

这样在程序载入内存执行的时候需要这样在程序载入内存执行的时候需要将程序的全部映像都一次装入。将程序的全部映像都一次装入。

那些比物理内存还大的程序将无法执那些比物理内存还大的程序将无法执行。行。

尽管如此,尽管如此, uClinuxuClinux 还是将整个物理还是将整个物理内存划分成大小为内存划分成大小为 4KB4KB的页面。的页面。

由数据结构由数据结构 pagepage 管理,有多少页面管理,有多少页面就有多少就有多少 pagepage 结构,它们又作为元素组成结构,它们又作为元素组成一个数组一个数组 mem_map[]mem_map[]。。

物理页面可以作为进程的代码、数据物理页面可以作为进程的代码、数据和堆栈的一部分,还可以存储装入的文件,和堆栈的一部分,还可以存储装入的文件,也可以当作缓冲区。也可以当作缓冲区。

uClinuxuClinux 仍然使用标准仍然使用标准 LinuxLinux 内核中内核中的变型的变型 Buddy SystemBuddy System 机制来管理空闲的机制来管理空闲的物理页面,物理页面, bitmapbitmap 表和表和 free_areafree_area 数组,数组,以及相关的函数或宏以及相关的函数或宏 __get_free_pages()__get_free_pages() 、、free_pages()free_pages() 、、 __get_free_page()__get_free_page() 现在现在仍然在被使用。仍然在被使用。

进程可以向核心申请使用物理内存。进程可以向核心申请使用物理内存。这仍然通过使用传统的这仍然通过使用传统的 kmalloc()kmalloc() 和和 kfreekfree()() 实现。实现。

这些内存块来自于这些内存块来自于 free_areafree_area 数组,数组,由由 blocksizeblocksize表、表、 sizesize表、表、 page_descrippage_descriptortor 结构和结构和 block_headerblock_header 结构共同管理。 结构共同管理。

而过去的而过去的 vmalloc()vmalloc() 和和 vfree()vfree() 由于由于是从虚拟空间是从虚拟空间 3GB3GB以上的虚拟空间的最高以上的虚拟空间的最高端分配内存,所以现在对它们的实现只是端分配内存,所以现在对它们的实现只是简单地调用简单地调用 kmalloc()kmalloc() 和和 kfree()kfree() ,实际上,实际上分配的也是从空闲物理页面链表中获得的分配的也是从空闲物理页面链表中获得的页面。页面。

不使用虚拟空间的概念,虚存段结构不使用虚拟空间的概念,虚存段结构vm_area_structvm_area_struct 以及由它构成的线性链表以及由它构成的线性链表和和 AVLAVL 树都不再使用。树都不再使用。

没有了虚拟内存的应用,也就不再有没有了虚拟内存的应用,也就不再有将页面换出到外存中的机制。将页面换出到外存中的机制。

所以过去的所以过去的 kswapdkswapd 页面换出守护页面换出守护进程和交换空间的页面管理数据结构在进程和交换空间的页面管理数据结构在 uCluClinuxinux 中都被删除。中都被删除。

8.1.4 uClinux8.1.4 uClinux 内存管理的局限内存管理的局限性性

由于缺少了由于缺少了 MMUMMU 硬件的支持,硬件的支持, uCliuClinuxnux 的多任务管理功能受到一定限制:的多任务管理功能受到一定限制:

1. uClinux1. uClinux 中无法实现中无法实现 fork()fork() 而只能使用而只能使用 vforkvfork()() 。。

这并不意味着这并不意味着 uClinuxuClinux 不具有多任务功不具有多任务功能,而是父进程在调用能,而是父进程在调用 vfork()vfork()之后必须在子之后必须在子进程调用进程调用 exec()exec() 或者或者 exit()exit() 之前阻塞。之前阻塞。

2. 2. 标准标准 LinuxLinux 中的内存分段为应用程序提供中的内存分段为应用程序提供了接近无限的堆空间和栈空间,而了接近无限的堆空间和栈空间,而 uClinuxuClinux为可执行程序在紧随它的数据段结束处分为可执行程序在紧随它的数据段结束处分配堆栈空间。配堆栈空间。

这样如果堆增长的太大,它将可能覆这样如果堆增长的太大,它将可能覆盖程序的静态数据段和代码段。盖程序的静态数据段和代码段。

3. 3. uClinuxuClinux 中没有自动扩展的栈,也没有中没有自动扩展的栈,也没有 brbrk()k()调用。调用。

用户必须通过使用用户必须通过使用 mmap()mmap() 来分配来分配内存空间。内存空间。

可以在程序的编译过程中指定它所使可以在程序的编译过程中指定它所使用的栈大小。用的栈大小。

4. 4. 不具有内存保护。任何程序都有可能导致不具有内存保护。任何程序都有可能导致内核崩溃。 内核崩溃。

uClinuxuClinux 与标准与标准 LinuxLinux 的主要区别在的主要区别在于它针对没有于它针对没有 MMUMMU 的处理器进行改造。的处理器进行改造。

uClinuxuClinux 所做的修改最大的部分理所所做的修改最大的部分理所当然位于内存管理部分,而内存管理上最当然位于内存管理部分,而内存管理上最大变化就是大变化就是 uClinuxuClinux 没有使用虚拟内存机没有使用虚拟内存机制。 制。

这样,标准这样,标准 LinuxLinux 的内存管理模块的内存管理模块中的许多功能实际上都被抛弃了,诸如对中的许多功能实际上都被抛弃了,诸如对页目录和页表的管理,对于交换空间的维页目录和页表的管理,对于交换空间的维护,页交换内核守护进程和页面换出功能,护,页交换内核守护进程和页面换出功能,缺页中断和页面保护机制等。 缺页中断和页面保护机制等。

而为了解决由此产生的新问题,而为了解决由此产生的新问题, uClinuClinuxux 同时设计了一种新的可执行文件格式:同时设计了一种新的可执行文件格式: flaflatt ,以及修改了部分进程管理的代码:如用,以及修改了部分进程管理的代码:如用 vfvfork()ork()来代替了来代替了 fork()fork()调用,实现了无法共享调用,实现了无法共享页面的页面的 do_mmap()do_mmap() 等。等。

uClinuxuClinux 下的多任务管理远比下的多任务管理远比 LinuxLinux 简简单,因为没有进程的页表项和保护机制要处单,因为没有进程的页表项和保护机制要处理。 理。

内核的调度器不需要进行修改,唯一内核的调度器不需要进行修改,唯一需要完成的工作就是进行程序上下文的正需要完成的工作就是进行程序上下文的正确保存和恢复。确保存和恢复。

这些上下文包括所有的在进程被中断这些上下文包括所有的在进程被中断的时候必须保存的寄存器的值。的时候必须保存的寄存器的值。

这些机制的缺少归根结底都在于微处这些机制的缺少归根结底都在于微处理芯片没有理芯片没有 MMUMMU 的支持。的支持。

例如,在没有例如,在没有 MMUMMU 的处理器上不可的处理器上不可能实现内存共享和保护,这是由于各种能实现内存共享和保护,这是由于各种 UNUNIXIX 的内存共享都是需要的内存共享都是需要 MMUMMU 中的页表和页中的页表和页目录管理功能支持的。 目录管理功能支持的。

标准标准 LinuxLinux 中的内存共享以页面共中的内存共享以页面共享的形式实现,共享该页的各个进程的页享的形式实现,共享该页的各个进程的页表项直接指向共享页,当共享状态发生变表项直接指向共享页,当共享状态发生变化的时候共享该页的各进程的页表都进行化的时候共享该页的各进程的页表都进行修改。修改。

另一个例子是内存保护机制的缺少。 另一个例子是内存保护机制的缺少。

标准标准 LinuxLinux 中对内存的加锁保护是中对内存的加锁保护是在虚拟内存段在虚拟内存段 VMAVMA 的基础上进行的,的基础上进行的,

也就是每一个也就是每一个 VMAVMA 段由一个自己的段由一个自己的保护权限标志保护权限标志 vm_flagsvm_flags ,将这个标志设置,将这个标志设置成成 PROT_READPROT_READ ,, PROT_ WRITEPROT_ WRITE 和和 PROTPROT_EXEC_EXEC就可以实现保护机制,就可以实现保护机制,

但实际上在但实际上在 VMAVMA 操作的最底层,保操作的最底层,保护的实现还是要依靠各个页面本身的页表护的实现还是要依靠各个页面本身的页表项标志。项标志。

8.2 8.2 内存管理模块的启动初始化内存管理模块的启动初始化

在标准在标准 LinuxLinux 的启动过程中要进行的启动过程中要进行许多与内存管理相关的功能模块的初始化许多与内存管理相关的功能模块的初始化工作,诸如页目录和页表映射的建立等。工作,诸如页目录和页表映射的建立等。

在把它改造成不使用在把它改造成不使用 MMUMMU 的系统的的系统的过程中必然要对这些功能进行修改。过程中必然要对这些功能进行修改。

所以在这一部分中将展开对所以在这一部分中将展开对 uClinuxuClinux中内存模块的启动初始化分析。中内存模块的启动初始化分析。

arch/armnommu/kernel/entry_ararch/armnommu/kernel/entry_armv.Smv.S 是一个汇编文件,它包含了一个是一个汇编文件,它包含了一个 kernkernel_entryel_entry 的定义,这是整个内核的进入点。的定义,这是整个内核的进入点。

在完成某些平台相关的初始化工作之在完成某些平台相关的初始化工作之后,执行流程跳转到后,执行流程跳转到 start_kernel()start_kernel() 处。处。

从这里开始考察从这里开始考察 uClinuxuClinux 的内存模块的内存模块启动初始化是如何实现的。启动初始化是如何实现的。

start_kernel()start_kernel() 中与内存模块相关的函数调中与内存模块相关的函数调用流程如下:用流程如下:setup_arch() paging_init() free_areasetup_arch() paging_init() free_area_init() mem_init()_init() mem_init()下面分别分析这些函数各自的功能以及下面分别分析这些函数各自的功能以及 uCluClinuxinux 对它们的改造。对它们的改造。

8.2.1 setup_arch()8.2.1 setup_arch()

setup_arch()setup_arch() 首先根据目前内核所配首先根据目前内核所配置的平台向某些特定地址写入特殊字符序置的平台向某些特定地址写入特殊字符序列,以完成对特定硬件的初始化,列,以完成对特定硬件的初始化,

比如工作状态发光二极管、错误和报比如工作状态发光二极管、错误和报警发光二极管等。警发光二极管等。

存储了可用物理内存起始地址的变量存储了可用物理内存起始地址的变量memory_startmemory_start 的初始化是通过一个的初始化是通过一个 ldld 脚脚本中定义的变量本中定义的变量 _end_end 进行的。进行的。

ldld 脚本是用来控制脚本是用来控制 GNU ldGNU ld 连接器在连接器在连接内核各个目标文件部分的时候的配置连接内核各个目标文件部分的时候的配置动作,比如这样一个脚本:动作,比如这样一个脚本:

SECTIONSSECTIONS{{ .=0x10000.=0x10000;; .text .text :: { *{ * (( .text.text )) }} .=0x8000000.=0x8000000;; .data .data :: { *{ * (( .data.data )) }} .bss : { * (.bss) }.bss : { * (.bss) }}}

用来配置用来配置 ldld 连接目标文件的时候将连接目标文件的时候将所有目标文件中的存储程序正文的所有目标文件中的存储程序正文的 ..

texttext 段段 (section)(section) 连接到一起,并且连接到一起,并且映射到输出文件的地址映射到输出文件的地址 0x100000x10000处,将所处,将所有目标文件中已初始化的数据 有目标文件中已初始化的数据

datadata 段连接到一起并放置到输出可段连接到一起并放置到输出可执行文件的执行文件的 0x80000000x8000000地址处,而所有目地址处,而所有目标文件中还未初始化的数据段标文件中还未初始化的数据段 ..

bssbss 连接起来后映射到输出文件中紧连接起来后映射到输出文件中紧跟在跟在 .data.data 段之后的位置。段之后的位置。

这个这个 ldld 配置脚本文件对每个平台都配置脚本文件对每个平台都是不同的。是不同的。

如为如为 MICETEKMICETEK 上所使用的上所使用的 uClinuxuClinux版本使用的版本使用的 ldld 配置文件为配置文件为 arch/armnomarch/armnommu/vmLinux.ldsmu/vmLinux.lds 。。

可以通过修改某个平台上的可以通过修改某个平台上的 ldld 脚本脚本配置文件中的配置文件中的 _end_end 变量来达到配置其可用变量来达到配置其可用物理内存起始地址的目的。物理内存起始地址的目的。

setup_arch()setup_arch() 在完成对在完成对 memory_stmemory_startart 变量的初始化之后,通过某些特定手段变量的初始化之后,通过某些特定手段检测不同类型的内存分布情况。 检测不同类型的内存分布情况。

比如为检测某段地址范围是否为比如为检测某段地址范围是否为 RARAMM 的方法是通过将某个地址的数据读出来,的方法是通过将某个地址的数据读出来,将它加将它加 11 后写回内存地址中,然后再读出后写回内存地址中,然后再读出来和原始数据比较看看其值是否成功增加来和原始数据比较看看其值是否成功增加了了 11,这样反复操作两次,最后将原数据,这样反复操作两次,最后将原数据恢复。 恢复。

如果是可读可写的如果是可读可写的 RAMRAM ,那么这个,那么这个测试的结果就是每次比较都是成功的,否测试的结果就是每次比较都是成功的,否则就不能将这个地址当作则就不能将这个地址当作 RAMRAM 。。

在在 setup_arch()setup_arch() 中还可能根据所用中还可能根据所用平台进行对平台进行对 flashmemoryflashmemory 和和 ROMROM 的测试。的测试。

在这些平台相关的工作完成以后,在这些平台相关的工作完成以后, ssetup_arch()etup_arch() 将对系统运行的第一个进程将对系统运行的第一个进程 ininit_taskit_task的的 mm_structmm_struct 结构中描述地址空结构中描述地址空间分布的变量间分布的变量 start_codestart_code ,, end_codeend_code ,,end_dataend_data 和和 brkbrk进行初始化,进行初始化, start_codstart_codee 为为 00,其他三个数值分别为来自于,其他三个数值分别为来自于 ldld 脚脚本配置文件中定义的相关变量本配置文件中定义的相关变量 _etext_etext 、、 _e_edatadata 和和 _end_end 。。

此后此后 setup_arch()setup_arch() 将根据将根据 LinuxLinux 中中为系统中的第—块为系统中的第—块 rom/flash memory carrom/flash memory cardd 所分配的固定的主/从设备号所分配的固定的主/从设备号 (( 可以从可以从 DDocumentocument // devicesdevices .. txttxt 中得到中得到 ))来创来创建根文件系统的设备号,并存储在后来将建根文件系统的设备号,并存储在后来将要用到的全局变量要用到的全局变量 ROOT_DEVROOT_DEV 中。中。

Setup_arch()Setup_arch() 最后完成对系统启动最后完成对系统启动参数的保存。参数的保存。

在调用在调用 setup_arch()setup_arch() 返回之后,返回之后, ststart_kernel()art_kernel() 中得到了系统可用物理内存的中得到了系统可用物理内存的起始和结束地址,以及系统启动时的命令起始和结束地址,以及系统启动时的命令行参数。行参数。

8.2.2 paging_init()8.2.2 paging_init()

在在 LinuxLinux 中,中, paging_init()paging_init() 的一项的一项主要功能是建立页目录和页表,而且将主要功能是建立页目录和页表,而且将 LinLinuxux 移植到不同平台的过程中非常重要的一移植到不同平台的过程中非常重要的一个步骤就是修改这个函数来适应新的硬件个步骤就是修改这个函数来适应新的硬件平台的虚拟内存体系。 平台的虚拟内存体系。

但是由于在但是由于在 uClinuxuClinux 中不再使用虚拟中不再使用虚拟内存机制,也就不再需要维护页目录和页内存机制,也就不再需要维护页目录和页表数据结构了,所以表数据结构了,所以 paging_init()paging_init() 在这里在这里只是为系统启动的时候保留一部分特殊用只是为系统启动的时候保留一部分特殊用途的内存区间。途的内存区间。

它返回后,从可以使用的内存区间开它返回后,从可以使用的内存区间开始,依次是如下的数据结构:始,依次是如下的数据结构:

empty_bad_page_table empty_bad_page_table 占用占用 11页页 empty_bad_page empty_bad_page 占用占用 11页页 empty_zero_page empty_zero_page 占用占用 11页,页,

并且被初始化为全并且被初始化为全 00 mem_mapmem_map bitmapbitmap

paging_init()paging_init() 函数在返回前通过调用函数在返回前通过调用free_area_init(start_memfree_area_init(start_mem ,, end_mem)end_mem)进行建立进行建立 buddy systembuddy system 的映射位图关系,的映射位图关系,以及建立空闲物理页面链表的操作。以及建立空闲物理页面链表的操作。

8.2.3 free_area_init()8.2.3 free_area_init()

这个函数用于建立管理物理页帧的数这个函数用于建立管理物理页帧的数据结构据结构 mem_mapmem_map ,有多少物理页帧就有,有多少物理页帧就有多少多少 mem_map_tmem_map_t 类型的结构体与之相对类型的结构体与之相对应。 应。

每个页面的每个页面的 mem_map_tmem_map_t 结构中的结构中的flagsflags 被标明为被标明为 PG_DMAPG_DMA 和和 PG_reservedPG_reserved ,,并且页帧号被赋给相应的数值。并且页帧号被赋给相应的数值。

同时建立了管理空闲页面的同时建立了管理空闲页面的 bitmapbitmap映射表,并且所有的位都被清映射表,并且所有的位都被清 00。。

8.2.4 mem_init()8.2.4 mem_init()

mem_init()mem_init() 函数遍历整个可用物理函数遍历整个可用物理内存地址空间,将每个页面相对应的内存地址空间,将每个页面相对应的 strucstruct paget page结构中结构中 flagsflags 的的 PG_reservedPG_reserved 标志标志位清除,标志用户个数的位清除,标志用户个数的 countcount 计数器置计数器置11,并同时统计可用物理页面数量,然后打,并同时统计可用物理页面数量,然后打印系统的各个内存参数,如可用印系统的各个内存参数,如可用 RAMRAM 和和 RROMOM 的大小、内核代码段和数据段大小等。的大小、内核代码段和数据段大小等。

8.3 8.3 可执行程序的加载可执行程序的加载

在普通的在普通的 LinuxLinux 中,虚拟内存技术中,虚拟内存技术的使用使我们不必关心一个应用程序是从的使用使我们不必关心一个应用程序是从什么地址开始的。什么地址开始的。

即使所有的应用程序都使用同一个连即使所有的应用程序都使用同一个连接脚本配置。 接脚本配置。

也就是说,即使它们使用的虚拟地址也就是说,即使它们使用的虚拟地址是重叠的,经过页表和页目录的转换之后是重叠的,经过页表和页目录的转换之后它们也可以被映射到不同的物理地址。它们也可以被映射到不同的物理地址。

但是在但是在 uClinuxuClinux 中,由于缺少了中,由于缺少了 MMMMUU 的硬件支持,在内核中不会发生地址的的硬件支持,在内核中不会发生地址的映射转换,这样就必须解决应用程序的加映射转换,这样就必须解决应用程序的加载问题。载问题。

uClinuxuClinux 系统使用系统使用 flatflat 可执行文件格可执行文件格式,式, gccgcc 的编译器不能直接形成这种文件格的编译器不能直接形成这种文件格式,式,

但是可以首先编译生成但是可以首先编译生成 coffcoff 可执行格可执行格式或者式或者 elfelf 可执行格式的文件,可执行格式的文件,

然后使用格式转化工具然后使用格式转化工具 (coff2flt(coff2flt 或者或者 eelf2flt)lf2flt) 将这些中间代码转换成将这些中间代码转换成 flatflat 文件格式。文件格式。

当用户执行一个当用户执行一个 flatflat 格式的可执行格式的可执行程序时,内核的执行文件加载器将对程序时,内核的执行文件加载器将对 flatflat文件进行进一步处理,主要是对文件进行进一步处理,主要是对 relocreloc 段进段进行修正。行修正。

下面对下面对 do_load_flat_binary()do_load_flat_binary() 函数函数的分析将详细描述其实现过程。 的分析将详细描述其实现过程。

8.3.1 8.3.1 用户程序的内存分布用户程序的内存分布

1.1. 堆堆 标准标准 LinuxLinux 上用户程序的动态内存上用户程序的动态内存

分配是通过调用分配是通过调用 glibcglibc 库的库的 mallocmalloc 函数函数从程序的堆空间中获得内存页面。从程序的堆空间中获得内存页面。

在虚拟内存系统中在虚拟内存系统中 mallocmalloc 是使用是使用sbrksbrk 调用将程序的数据段向后扩展得到。调用将程序的数据段向后扩展得到。

应用程序的内存使用分布图应用程序的内存使用分布图如图如图8-28-2所示所示。。

而在而在 uClinuxuClinux 的平地址模式中,堆空的平地址模式中,堆空间是通过间是通过 mmapmmap 调用获得的。调用获得的。

uClibcuClibc 中的中的 mallocmalloc 函数是一个非常函数是一个非常简单的实现,它将所有分配内存的细节都简单的实现,它将所有分配内存的细节都交给了内核。交给了内核。

2. 2. 栈栈

uClinuxuClinux 中的栈紧随着用户程序的数中的栈紧随着用户程序的数据段,而堆是从栈底向下扩张的,据段,而堆是从栈底向下扩张的,如图如图8-28-2所示所示。。

由于由于 uClinuxuClinux 中没有内存保护机制,中没有内存保护机制,这样必须在程序编译连接的时候就确保为这样必须在程序编译连接的时候就确保为栈保留了足够的空间使它不会覆盖程序的栈保留了足够的空间使它不会覆盖程序的数据段和代码段。数据段和代码段。

8.3.2 reloc8.3.2 reloc 段机制段机制

一个可执行程序通常包含代码段、数一个可执行程序通常包含代码段、数据段、堆栈段、未初始化数据段。据段、堆栈段、未初始化数据段。

在普通的在普通的 LinuxLinux 系统上,程序连接系统上,程序连接的时候,连接器的时候,连接器 ldld 都要受一个叫做连接器都要受一个叫做连接器脚本的配置文件所控制,这个脚本用连接脚本的配置文件所控制,这个脚本用连接器命令语言写成,连接器会根据它来决定器命令语言写成,连接器会根据它来决定将程序的各个段加载到内存中的什么位置。将程序的各个段加载到内存中的什么位置。

但是由于但是由于 uClinuxuClinux 中没有使用虚拟中没有使用虚拟内存机制,也就没有了从虚拟地址到物理内存机制,也就没有了从虚拟地址到物理地址之间的映射,地址之间的映射,

所以程序连接时所指定的程序运行空所以程序连接时所指定的程序运行空间在间在 uClinuxuClinux 中成为实际的物理地址而不中成为实际的物理地址而不是像是像 LinuxLinux 那样可以通过内核维护的页目那样可以通过内核维护的页目录和页表来映射到任意物理地址空间。 录和页表来映射到任意物理地址空间。

在后面的分析中可以看到,程序加载在后面的分析中可以看到,程序加载的时候它所加载的地址是由内核的页面分的时候它所加载的地址是由内核的页面分配机制所决定的,在程序加载之前不可能配机制所决定的,在程序加载之前不可能被预知。被预知。

这样就形成了一个矛盾:程序连接时这样就形成了一个矛盾:程序连接时连接器所假定的程序运行空间与实际程序连接器所假定的程序运行空间与实际程序加载到的内存空间可能不同。 加载到的内存空间可能不同。

比如下面的一条指令:比如下面的一条指令: bl app_startbl app_start ;;

这一条指令采用直接寻址,跳转到这一条指令采用直接寻址,跳转到 appapp_start_start 地址处执行,连接程序将在编译完成时地址处执行,连接程序将在编译完成时按照连接器按照连接器 1d1d的配置文件计算出的配置文件计算出 app_startapp_start的实际地址的实际地址 ((设若实际地址为设若实际地址为 Oxl0000)Oxl0000) ,,

这个实际地址是根据这个实际地址是根据 ldld的配置文件做的配置文件做出的假设出的假设 (( 因为连接器总是假定该程序将被加因为连接器总是假定该程序将被加载到由载到由 ldld 配置文件指明的内存空间配置文件指明的内存空间 )) 。 。

但实际上操作系统在加载程序时根本但实际上操作系统在加载程序时根本没有考虑到要按照没有考虑到要按照 ldld 配置文件规定的地址配置文件规定的地址进行加载。进行加载。

这时如果程序仍然跳转到绝对地址这时如果程序仍然跳转到绝对地址 OOxl0000xl0000处执行,通常情况这是不正确的。 处执行,通常情况这是不正确的。

uClinuxuClinux 采用的解决办法是在连接生采用的解决办法是在连接生成的可执行文件中增加一个变量用于在程成的可执行文件中增加一个变量用于在程序加载的时候动态修正序加载的时候动态修正 app_startapp_start 的实际的实际地址地址

这种变量的类型如下:这种变量的类型如下:include/asm-armnommu/flat.h:include/asm-armnommu/flat.h:struct flat_reloc{struct flat_reloc{ signed long offset:30signed long offset:30;; unsigned long type:2unsigned long type:2;;}} ;;

这实际上是一块这实际上是一块 3232 位的空间,前位的空间,前 3030位用来纪录程序中的那些标明绝对地址的变量位用来纪录程序中的那些标明绝对地址的变量在数据段中离数据段的开始位置的偏移量,在数据段中离数据段的开始位置的偏移量,

后后 22 位用来标明这个变量究竟是指示一位用来标明这个变量究竟是指示一个什么地址:分为代码地址,数据地址和未初个什么地址:分为代码地址,数据地址和未初始化数据地址三种,始化数据地址三种,

以便于在程序加载的时候,相应程序的以便于在程序加载的时候,相应程序的代码段,数据段和代码段,数据段和 BSSBSS 段被加载到内存中的段被加载到内存中的实际位置对这个地址变量做出相应调整。实际位置对这个地址变量做出相应调整。

在上面的例子中,对于程序中的标明地址在上面的例子中,对于程序中的标明地址值的变量值的变量 app_startapp_start就需要这样一个就需要这样一个 flat_relocflat_reloc结构的变量。结构的变量。

设若使用变量设若使用变量 addraddr表示这个变量的存储表示这个变量的存储空间,则空间,则 addraddr 的前的前 3030 位记录了位记录了 app_startapp_start 离数离数据段开始位置的偏移量。据段开始位置的偏移量。

因为这个地址因为这个地址 app_startapp_start 是跳转代码执行是跳转代码执行地址,所以地址,所以 addraddr 的后的后 22 位被赋值为位被赋值为 FLAT_RELOFLAT_RELOC_TYPE_TEXT(C_TYPE_TEXT( 数值为数值为 0)0) 。 。

增加的增加的 44字节变量字节变量 addraddr 被存放在称被存放在称为为 relocreloc 的段内。的段内。

程序中的所有的这样的变量被连续存程序中的所有的这样的变量被连续存放在可执行文件的头部放在可执行文件的头部 relocreloc 段中。段中。

连接器连接程序的时候,变量连接器连接程序的时候,变量 app_app_startstart 存储的是根据存储的是根据 ldld 配置脚本计算出的地配置脚本计算出的地址,通常只是距离代码段、数据段或者址,通常只是距离代码段、数据段或者 BSBSSS 段头的相对偏移量。 段头的相对偏移量。

在可执行文件加载时,可执行文件加在可执行文件加载时,可执行文件加载器遍历可执行文件头部的整个载器遍历可执行文件头部的整个 relocreloc 段,段,对每一个其中的对每一个其中的 flat_relocflat_reloc 类型的变量首先类型的变量首先根据根据 offsetoffset 的值得到它所对应的地址变量的值得到它所对应的地址变量在数据段中的位置在数据段中的位置

然后根据然后根据 typetype 的值将这个数据段中的值将这个数据段中的地址变量加上程序在物理内存中的实际的的地址变量加上程序在物理内存中的实际的代码段,数据段和代码段,数据段和 BSSBSS 段的起始地址进行段的起始地址进行修正。修正。

这样程序中的地址变量中保存的就是这样程序中的地址变量中保存的就是正确的物理地址。正确的物理地址。

8.3.3 flat8.3.3 flat 可执行文件格式可执行文件格式

为了适应为了适应 uClinuxuClinux 中新的内存管理模中新的内存管理模式而引入了一种专为它所使用的式而引入了一种专为它所使用的 flatflat 可执可执行文件格式。行文件格式。

可执行文件头是前面描述的可执行文件头是前面描述的 relocreloc 段,段,紧接着是程序的文本段、数据段、未初始紧接着是程序的文本段、数据段、未初始化数据段。化数据段。

可执行文件加载到内存之后程序的堆可执行文件加载到内存之后程序的堆栈段紧随在栈段紧随在 BSSBSS 段的后面。段的后面。

flatflat 可执行文件格式可执行文件格式如图如图8-28-2所示所示。。 但是注意在可执行文件被加载到内存但是注意在可执行文件被加载到内存

的时候的时候 relocreloc 段仍然在外存中。段仍然在外存中。

每实现一个可执行二进制文件格式,每实现一个可执行二进制文件格式,只要通过调用内核文件系统中的只要通过调用内核文件系统中的 register_ register_ binfmt( )binfmt( ) 函数将一个函数将一个 struct Linux_binfmtstruct Linux_binfmt类型的结构注册到内核中。类型的结构注册到内核中。

这个结构中包含了这种文件格式的执这个结构中包含了这种文件格式的执行文件加载器,共享库加载器和行文件加载器,共享库加载器和 core dumcore dumpp 内存镜像文件生成器:内存镜像文件生成器:

include/linux/binfmts.hinclude/linux/binfmts.h ::struct linux_binfmt{struct linux_binfmt{ struct linux_binfmt *nextstruct linux_binfmt *next ;; long *use_countlong *use_count ;; int (*load_binary)(struct linux_binprm int (*load_binary)(struct linux_binprm ** , , struct pt_regs * regs)struct pt_regs * regs) ;; int (*load_shlib)(int fd)int (*load_shlib)(int fd) ;; int (*core_dump)(long signr,struct pt_rint (*core_dump)(long signr,struct pt_regs * egs * regs)regs) ;;}} ;;

其中其中 load_binaryload_binary 函数是每一个二函数是每一个二进制文件格式的实现都必须提供的,而其进制文件格式的实现都必须提供的,而其他的两个函数指针可以为空,他的两个函数指针可以为空,

比如在比如在 uClinuxuClinux 中,由于内存管理的中,由于内存管理的特定方式决定了不能使用代码的共享,所特定方式决定了不能使用代码的共享,所有的可执行程序都在编译时进行了静态库有的可执行程序都在编译时进行了静态库的连接 的连接

所以没有必要实现运行时动态加载连所以没有必要实现运行时动态加载连接库的功能,而且接库的功能,而且 uClinuxuClinux 也没有实现也没有实现 corcore dumpe dump 机制的支持,机制的支持,

所以它的所以它的 flatflat 可执行文件格式注册可执行文件格式注册过程是这样的:过程是这样的:

static struct linux_binfmt flat_format = {static struct linux_binfmt flat_format = {#ifndef MODULE#ifndef MODULE NULL,NULL,load_flat_binary,NULL,NULLNULL,NULL,load_flat_binary,NULL,NULL#else#else NULL,&mode_use_count_,load_flat_binNULL,&mode_use_count_,load_flat_binary,ary,

NULL,NULLNULL,NULL#endif#endif}} ;;register_binfmt(&flat_format)register_binfmt(&flat_format) ;;

注册完成后,内核文件系统在加载注册完成后,内核文件系统在加载 flflat binaryat binary 格式的可执行程序的时候自动调格式的可执行程序的时候自动调用用 load_flat_binary( )load_flat_binary( ) 函数完成加载。函数完成加载。

8.3.4 8.3.4 执行文件加载流程执行文件加载流程

下面考察一个磁盘上的可执行文件是下面考察一个磁盘上的可执行文件是如何被加载到内存中并执行的。如何被加载到内存中并执行的。

在已注册的在已注册的 flatflat 可执行文件加载器可执行文件加载器被调用的时候,被调用的时候, load_flat_binary()load_flat_binary() 通过调通过调用用 do_load_flat_binary()do_load_flat_binary() 函数完成加载功函数完成加载功能。能。

每一个应用程序在内核中对应一个进每一个应用程序在内核中对应一个进程,由一个进程控制块描述,也就是程,由一个进程控制块描述,也就是 task_task_structstruct 结构。结构。

在调用在调用 do_load_flat_binary()do_load_flat_binary() 的时的时候,已经为这个任务的运行创建了进程上候,已经为这个任务的运行创建了进程上下文环境,包括下文环境,包括 task_structtask_struct 结构空间的分结构空间的分配和初始化。 配和初始化。

Do_load_flat_binary()Do_load_flat_binary() 所要完成的所要完成的就是从磁盘文件中按照可执行文件的格式就是从磁盘文件中按照可执行文件的格式读取可执行文件的各个段到读取可执行文件的各个段到 RAMRAM 中中 ((正文正文段可以在段可以在 ROMROM 中执行,中执行,

所以可能并不加载到所以可能并不加载到 RAMRAM 内存中,内存中,而数据段、未初始化数据段而数据段、未初始化数据段 BSSBSS必须被加必须被加载到载到 RAMRAM 中,程序的堆栈空间也必须在中,程序的堆栈空间也必须在 RRAMAM 中被分配中被分配 )) 。 。

并且由于并且由于 flat binaryflat binary 的特性,还要的特性,还要在必要的时候进行在必要的时候进行 relocreloc 段的内存偏移量修段的内存偏移量修正。正。

函数函数 do_load_flat_binary()do_load_flat_binary() 负责实负责实际加载二进制文件的工作,其定义如下:际加载二进制文件的工作,其定义如下:

inline int do_load_flat_binary(struct linuinline int do_load_flat_binary(struct linux_binprm * prm,struct pt_regs * regs)x_binprm * prm,struct pt_regs * regs) ;;

其中函数其中函数 do_load_flat_binary( )do_load_flat_binary( ) 的的参数参数 bprmbprm 是一个如下类型的结构变量:是一个如下类型的结构变量:

include/linux/binfmts.h:include/linux/binfmts.h:struct linux_binprm{struct linux_binprm{ char buf[128]char buf[128];;#ifndef NO_MM#ifndef NO_MM unsigned long page[MAX_ARG_PAGEunsigned long page[MAX_ARG_PAGES]S];;#else #else /*!NO_MM *//*!NO_MM */ char ** envp,**argvchar ** envp,**argv ;;

#endif #endif /* /* !! NO_MM */NO_MM */ unsigned long punsigned long p ;; int sh_bangint sh_bang;; struct inode * inodestruct inode * inode ;; int e_uid,e_gidint e_uid,e_gid;; int argc,envcint argc,envc ;; char * filenamechar * filename ; ; /*Name of binary *//*Name of binary */ unsigned long loader,execunsigned long loader,exec ;; int dont_iputint dont_iput ; ; /*binfmt handler has put inode *//*binfmt handler has put inode */}} ;;

它用来在加载各种可执行文件的时候它用来在加载各种可执行文件的时候存储参数信息。存储参数信息。

在加载在加载 flat_binflat_bin 格式的可执行文件格式的可执行文件的情况下,开始的的情况下,开始的 bufbuf 域实际上存储了一域实际上存储了一个个 struct flat_hdrstruct flat_hdr 类型的结构变量(只使类型的结构变量(只使用了用了 6464个字节)。个字节)。

envpenvp 和和 argvargv是执行程序时的环境变是执行程序时的环境变量指针列表和参数指针列表。 量指针列表和参数指针列表。

inodeinode 是可执行文件在文件系统中的是可执行文件在文件系统中的inodeinode指针,通过它可以得到这个文件的所指针,通过它可以得到这个文件的所有相关信息。有相关信息。

FilenameFilename 是这个可执行文件在磁盘是这个可执行文件在磁盘上的文件名的字符串指针等。上的文件名的字符串指针等。

其中其中 bufbuf 域中保持的域中保持的 flat_hdrflat_hdr 结构结构用于每个用于每个 flat binaryflat binary 格式的磁盘文件的格格式的磁盘文件的格式描述,定义如下:式描述,定义如下:

include/asm-armnommu/flat.hinclude/asm-armnommu/flat.hstruct flat_hdr{struct flat_hdr{ char magic[4]char magic[4];; unsigned long revunsigned long rev;; unsigned long entryunsigned long entry ;; /* offset of first executable instr/* offset of first executable instruction with text segment from beginninuction with text segment from beginning of file */g of file */

unsigned long data_startunsigned long data_start ;; /*offset of data segment from beginning of fil/*offset of data segment from beginning of file*/e*/unsigned long bss_endunsigned long bss_end ;; /*offset of end of bss segment from beginnin/*offset of end of bss segment from beginning of file */g of file */ /*(It is assumed that data_end through bss_/*(It is assumed that data_end through bss_end forms the bss segment.)*/end forms the bss segment.)*/ unsigned long stack_sizeunsigned long stack_size ;; /*Size of stack,in bytes *//*Size of stack,in bytes */

unsigned long reloc_startunsigned long reloc_start ;; /* offset of relocation records from beginnin/* offset of relocation records from beginning of file */g of file */ unsigned long reloc_countunsigned long reloc_count ;; /* Number of relocation records *//* Number of relocation records */ unsigned long flagsunsigned long flags ;; unsigned long filler[6]unsigned long filler[6];; /*Reserbered,set to zero*//*Reserbered,set to zero*/};};

magicmagic 字符数组中必须包含字符串字符数组中必须包含字符串““ bFLT”bFLT” ,其他的各个变量描述了在这个,其他的各个变量描述了在这个flatbinaryflatbinary 可执行文件中各个段的起始位置可执行文件中各个段的起始位置离文件头的偏移量和长度,并且说明了这离文件头的偏移量和长度,并且说明了这个文件中的个文件中的 relocreloc 结构数组离文件头的偏移结构数组离文件头的偏移量和数组项个数。量和数组项个数。

do_load_flat_binary()do_load_flat_binary() 的流程如下:的流程如下: 1.1. 首先调用首先调用 flush_o1d_exec()flush_o1d_exec() 根据当前进根据当前进

程程 (current)(current) 的的 mm_structmm_struct 结构创建一个结构创建一个新的新的 mm_structmm_struct 结构,结构, 它的引用计数器它的引用计数器 countcount 设为设为 11,,占用内存页面数占用内存页面数 rssrss 为为 00,, tblock.rblocktblock.rblock和和 tblock.nexttblock.next 置为置为 00, ,

然后将然后将 task_structtask_struct 中的中的 mmmm指针指针指向这个指向这个 mm_structmm_struct 结构,将原来的结构,将原来的 mmmm_struct_struct 以及它所包含的内存空间使用以及它所包含的内存空间使用 kfrekfree()e() 释放。释放。 这个过程结束后,当前进程只 这个过程结束后,当前进程只有—个新分配的有—个新分配的 mm_structmm_struct 结构,,它里结构,,它里面不包含任何内存空间。面不包含任何内存空间。

2.2. 调用调用 do_mmap()do_mmap() 尝试将根据调用参数尝试将根据调用参数 bpbprmrm 中的中的 inodeinode 所代表的文件映射到当前进所代表的文件映射到当前进程管理的内存空间中:程管理的内存空间中:

error = do_mmap(file,error = do_mmap(file, 0,0, code_len + data_len + bss_len + stack_lencode_len + data_len + bss_len + stack_len ,, PROT_READ|PROT_EXEC|((hdr ->flags & FLAPROT_READ|PROT_EXEC|((hdr ->flags & FLAT_FLAG_RAM)T_FLAG_RAM) PROT_WRITE : 0),PROT_WRITE : 0), 0 /*MAP_ * */,0 /*MAP_ * */, 0)0) ;;

其中进行映射的文件内容包括正文段、其中进行映射的文件内容包括正文段、数据段、未初始化数据段,以及预先计算数据段、未初始化数据段,以及预先计算出来的需要保留大小的堆栈段。出来的需要保留大小的堆栈段。

映射的保护权限设置为可读、可执行,映射的保护权限设置为可读、可执行,并且根据并且根据 hdrhdr 中的调用参数中的调用参数 flagflag中是否有中是否有FLAT_FLAG_RAMFLAT_FLAG_RAM 被设置被设置 ((表明是否希望在表明是否希望在ROMROM 中直接执行还是装载到中直接执行还是装载到 RAMRAM 中再执中再执行行 )) 而决定是否设置可写位。 而决定是否设置可写位。

do_mmap()do_mmap() 根据根据 flagsflags参数决定是参数决定是否真正进行否真正进行 RAMRAM 映射,如果映射,如果 flagsflags 中的中的 FLFLAT_FLAG_RAMAT_FLAG_RAM 位被设置,它只简单的返位被设置,它只简单的返回可执行程序在回可执行程序在 ROMROM 中的地址而没有从中的地址而没有从 RRAMAM 中申请空间,这种情况下需要为程序的中申请空间,这种情况下需要为程序的数据段、数据段、 BSSBSS 段和堆栈段再分配空间。 段和堆栈段再分配空间。

如果成功地将这些内容装入物理内存如果成功地将这些内容装入物理内存并且建立了用户对他们的管理映射之后,返并且建立了用户对他们的管理映射之后,返回的是它们在物理内存中的地址。回的是它们在物理内存中的地址。

所以所以 do_mmap()do_mmap() 返回之后需要通过返回之后需要通过检查它的返回值来确定是否需要再为数据段、检查它的返回值来确定是否需要再为数据段、BSSBSS 段和堆栈段在段和堆栈段在 RAMRAM 中分配空间。中分配空间。

因为因为 uClinuxuClinux 没有使用虚拟内存技术,没有使用虚拟内存技术,所以这也是文件被装载到的实际物理地址。 所以这也是文件被装载到的实际物理地址。

通过通过 is_in_rom()is_in_rom() 调用检查所返回的调用检查所返回的内存地址是否是内存地址是否是 romrom 空间的地址,如果属空间的地址,如果属实,进行如下操作,否则将变量实,进行如下操作,否则将变量 pospos 赋值赋值为内存中数据段的起始地址,跳转步骤为内存中数据段的起始地址,跳转步骤 66 。。

3.3. 上次上次 do_mmap()do_mmap() 调用返回的是代码段调用返回的是代码段 roromm映射的地址,所以有必要为数据段、未映射的地址,所以有必要为数据段、未初始化数据段初始化数据段 BSSBSS ,以及堆栈段分配,以及堆栈段分配 RAMRAM空间。继续使用读、写和执行权限调用空间。继续使用读、写和执行权限调用 dodo_mmap()_mmap() 。 。

但是注意这次调用但是注意这次调用 do_mmap()do_mmap() 使用使用的参数的参数 filefile 为空,为空,

在这种情况下在这种情况下 do_mmap()do_mmap() 将只分配将只分配一块为此进程所管理的大小为“数据段大一块为此进程所管理的大小为“数据段大小小 ++ 未初始化数据段未初始化数据段 BSSBSS 大小大小 ++堆栈段大堆栈段大小”的空间,以及与此相应的一个小”的空间,以及与此相应的一个 struct struct mm_tblock_structmm_tblock_struct 结构和一个结构和一个 struct mmstruct mm_rblock_struct_rblock_struct 结构, 结构,

但是没有任何通过但是没有任何通过 filefile 指针进行的操指针进行的操作作 (( 如如 file->f_op->mmapfile->f_op->mmap 和和 file->f_op->rfile->f_op->read)ead) 。。

这次调用的返回值这次调用的返回值 pospos 是一块全部是一块全部被初始化为“\被初始化为“\ 0”0” 的内存空间的起始地的内存空间的起始地址。址。

44..调用调用 read_exec()read_exec() 函数将文件中的数据段函数将文件中的数据段读入读入 pospos 所指的内存空间中。所指的内存空间中。

55 ..将所映射的未初始化数据段将所映射的未初始化数据段 BSSBSS 和堆栈和堆栈段全部初始化为“段全部初始化为“ \0”\0”。。

66 ..将当前进程的将当前进程的 mm_structmm_struct 结构中的结构中的 start_start_codecode ,, end_codeend_code 分别赋值为二进制文件加分别赋值为二进制文件加载之后代码段在物理内存中的实际起始地址载之后代码段在物理内存中的实际起始地址和结束地址,和结束地址,

start_datastart_data 和和 end_dataend_data 分别赋值为分别赋值为数据段在物理内存中的实际起始地址和结束数据段在物理内存中的实际起始地址和结束地址,地址,

brkbrk赋值为堆栈段在物理内存中的实际赋值为堆栈段在物理内存中的实际起始地址。起始地址。

77 ..修正程序中使用的地址变量的值。修正程序中使用的地址变量的值。 注意注意 flatflat 可执行程序的可执行程序的 relocreloc 段在程序段在程序

加载的时候并没有像其他段那样被加载,也加载的时候并没有像其他段那样被加载,也就是说,它总是存在于外存的文件内部的。 就是说,它总是存在于外存的文件内部的。

这样,如果代码段是放在这样,如果代码段是放在 ROMROM 中的,中的,那么只要直接从中读取放在离文件头偏移那么只要直接从中读取放在离文件头偏移 hdhdr->reloc_startr->reloc_start 的的 struct flat_relocstruct flat_reloc 类型变量类型变量就可以得到就可以得到 relocreloc 段中第一个变量的地址,段中第一个变量的地址,

否则要调用否则要调用 read_ exec()read_ exec() 函数从磁盘函数从磁盘文件中读取这些信息。对于文件中读取这些信息。对于 relocreloc 段中的每一段中的每一个变量,调用个变量,调用 do_reloc()do_reloc() 函数修正它所对应函数修正它所对应的程序中的地址变量的值。 的程序中的地址变量的值。

do_reloc()do_reloc() 函数很简单,它首先根函数很简单,它首先根据据 struct flat_relocstruct flat_reloc 指针类型的参数指针类型的参数 rr 得到得到这个这个 flat_relocflat_reloc 类型的变量所对应的程序中类型的变量所对应的程序中的地址变量的地址,的地址变量的地址,

然后对这个地址变量进行修正:如果然后对这个地址变量进行修正:如果它指示的是程序地址,那么将它加上程序它指示的是程序地址,那么将它加上程序的正文段被实际加载到的内存地址; 的正文段被实际加载到的内存地址;

如果是数据地址,那么将它加上程序如果是数据地址,那么将它加上程序的数据段加载的实际地址;的数据段加载的实际地址;

如果是如果是 BSSBSS 地址,那么将它加上程地址,那么将它加上程序的序的 BSSBSS 段被加载到的实际物理地址。段被加载到的实际物理地址。

88 ..将可执行文件名,环境变量列表和参数列将可执行文件名,环境变量列表和参数列表依次压入程序堆栈。表依次压入程序堆栈。

注意这个栈的栈底在离注意这个栈的栈底在离 BSSBSS 段结束处段结束处最远的高端地址处,栈顶上限即最远的高端地址处,栈顶上限即 BSSBSS 的结束的结束地址,栈是由高地址向低地址的方向增长的。地址,栈是由高地址向低地址的方向增长的。

然后调用然后调用 create_flat_tables()create_flat_tables() 将环将环境变量列表和参数列表解析出来通过境变量列表和参数列表解析出来通过 put_put_user()user() 调用使用户能够访问它们,调用使用户能够访问它们,

并将此进程的并将此进程的 mm_structmm_struct 结构中的结构中的记录栈顶位置的变量记录栈顶位置的变量 start_stackstart_stack赋值为此赋值为此时的栈顶指针。时的栈顶指针。

99..此时所有的程序运行环境都已经创建,此时所有的程序运行环境都已经创建,调用调用 start_thread()start_thread() 开始此程序的执行。开始此程序的执行。

8.4 8.4 本章小结本章小结 本章主要介绍了缺少本章主要介绍了缺少 MMUMMU 支持的内支持的内

存管理,给出了内存管理存管理,给出了内存管理 33 种模型、标准种模型、标准 LiLinuxnux 的内存管理、的内存管理、 uClinuxuClinux 内存管理及其的内存管理及其的局限性;局限性;

介绍了内存管理模块的启动过程;介绍了内存管理模块的启动过程; 给出了用户程序的内存分布、给出了用户程序的内存分布、 relocreloc

段机制、可执行文件格式和执行文件加载流段机制、可执行文件格式和执行文件加载流程。程。

练 习 题练 习 题1. 1. 简述内存管理简述内存管理 33 种模型。种模型。22.简述.简述 uClinuxuClinux 内存管理机制。内存管理机制。33 .简述.简述 relocreloc 段机制。段机制。44.简述.简述 flatflat 可执行文件格式及加载过程。可执行文件格式及加载过程。

top related