临江网

 找回密码
 点这里注册

QQ登录

只需一步,快速开始

搜索
热搜: 临江老照片

编辑推荐

查看: 2782|回复: 0
打印 上一主题 下一主题

Linux Kernel核心中文手册 4-1

[复制链接]

1450

主题

3

好友

3808

积分

版主

Rank: 7Rank: 7Rank: 7

帖子
1491
积分
3808
注册时间
2007-4-28
跳转到指定楼层
楼主
发表于 2007-7-18 23:02:57 |只看该作者 |倒序浏览
每一次打开一个文件,就使用 files_struct 中的一个空闲的 file 指针指向这个新的 file 结构。 Linux 进程启动时有 3 个文件描述符已经打开。这就是标准输入、标准输出和标准错误,这都是从创建它们的父进程中继承过来的。对于文件的访问都是通过标准的系统调用,需要传递或返回文件描述符。这些描述符是进程的 fd 向量表中的索引,所以标准输入、标准输出和标准错误的文件描述符分别是 0 , 1 和 2 。对于文件的所有访问都是利用 file 数据结构中的文件操作例程和它的 VFS I 节点一起来实现的。

4.5 Virtual Memory (虚拟内存)

    进程的虚拟内存包括多种来源的执行代码和数据。第一种是加载的程序映像,例如 ls 命令。这个命令,象所有的执行映像一样,由执行代码和数据组成。映像文件中包括将执行代码和相关的程序数据加载到进程地虚拟内存中所需要的所有信息。第二种,进程可以在处理过程中分配(虚拟)内存,比如用于存放它读入的文件的内容。新分配的虚拟内存需要连接到进程现存的虚拟内存中才能使用。第三中, Linux 进程使用通用代码组成的库,例如文件处理。每一个进程都包括库的一份拷贝没有意义, Linux 使用共享库,几个同时运行的进程可以共用。这些共享库里边的代码和数据必须连接到该进程的虚拟地址空间和其他共享该库的进程的虚拟地址空间。

    在一个特定的时间,进程不会使用它的虚拟内存中包括的所有代码和数据。它可能包括旨在特定情况下使用的代码,比如初始化或者处理特定的事件。它可能只是用了它的共享库中一部分例程。如果把所有这些代码都加载到物理内存中而不使用只会是浪费。把这种浪费和系统中的进程数目相乘,系统的运行效率会很低。 Linux 改为使用 demand paging 技术,进程的虚拟内存只在进程试图使用的时候才调入物理内存中。所以, Linux 不把代码和数据直接加载到内存中,而修改进程的页表,把这些虚拟区域标志为存在但是不在内存中。当进程试图访问这些代码或者数据,系统硬件会产生一个 page fault ,把控制传递给 Linux 核心处理。因此,对于进程地址空间的每一个虚拟内存区域, Linux 需要直到它从哪里来和如何把它放到内存中,这样才可以处理这些 page fault 。

    Linux 核心需要管理所有的这些虚拟内存区域,每一个进程的虚拟内存的内容通过一个它的 task_struct 指向的一个 mm_struct mm_struc 数据结构描述。该进程的 mm_struct 数据结构也包括加载的执行映像的信息和进程页表的指针。它包括了指向一组 vm_area_struct 数据结构的指针,每一个都表示该进程中的一个虚拟内存区域。

    这个链接表按照虚拟内存顺序排序。图 4.2 显示了一个简单进程的虚拟内存分布和管理它的核心数据结构。因为这些虚拟内存区域来源不同, Linux 通过 vm_area_struct 指向一组虚拟内存处理例程(通过 vm_ops )的方式抽象了接口。这样进程的所有虚拟内存都可以用一种一致的方式处理,不管底层管理这块内存的服务如何不同。例如,会有一个通用的例程,在进程试图访问不存在的内存时调用,这就是 page fault 的处理。

    当 Linux 为一个进程创建新的虚拟内存区域和处理对于不在系统物理内存中的虚拟内存的引用时,反复引用进程的 vm_area_struct 数据结构列表。这意味着它查找正确的 vm_area_struct 数据结构所花的事件对于系统的性能十分重要。为了加速访问, Linux 也把 vm_area_struct 数据结构放到一个 AVL ( Adelson-Velskii and Landis )树。对这个树进行安排使得每一个 vm_area_struct (或节点)都有对相邻的 vm_area_struct 结构的一个左和一个右指针。左指针指向拥有较低起始虚拟地址的节点,右指针指向一个拥有较高起始虚拟地址的节点。为了找到正确的节点, Linux 从树的根开始,跟从每一个节点的左和右指针,直到找到正确的 vm_area_struct 。当然,在这个树中间释放不需要时间,而插入新的 vm_area_struct 需要额外的处理时间。



    当一个进程分配虚拟内存的时候, Linux 并不为该进程保留物理内存。它通过一个新的 vm_area_struct 数据结构来描述这块虚拟内存,连接到进程的虚拟内存列表中。当进程试图写这个新的虚拟内存区域的时候,系统会发生 page fault 。处理器试图解码这个虚拟地址,但是没有对应该内存的页表条目,它会放弃并产生一个 page fault 异常,让 Linux 核心处理。 Linux 检查这个引用的虚拟地址是不是在进程的虚拟地址空间, 如果是, Linux 创建适当的 PTE 并为该进程分配物理内存页。也许需要从文件系统或者交换磁盘中加载相应的代码或者数据,然后进程从引起 page fault 的指令重新运行,因为这次该内存实际存在,可以继续。

4.6 Creating a Process (创建一个进程)

    当系统启动的时候它运行在核心态,这时,只有一个进程:初始化进程。象所有其他进程一样,初始进程有一组用堆栈、寄存器等等表示的机器状态。当系统中的其他进程创建和运行的时候这些信息存在初始进程的 task_struct 数据结构中。在系统初始化结束的时候,初始进程启动一个核心线程(叫做 init )然后执行空闲循环,什么也不做。当没有什么可以做的时候,调度程序会运行这个空闲的进程。这个空闲进程的 task_struct 是唯一一个不是动态分配而是在核心连接的时候静态定义的,为了不至于混淆,叫做 init_task 。

    Init 核心线程或进程拥有进程标识符 1 ,是系统的第一个真正的进程。它执行系统的一些初始化的设置(比如打开系统控制它,安装根文件系统),然后执行系统初始化程序。依赖于你的系统,可能是 /etc/init , /bin/init 或 /sbin/init 其中之一。 Init 程序使用 /etc/inittab 作为脚本文件创建系统中的新进程。这些新进程自身可能创建新的进程。例如: getty 进程可能会在用户试图登录的时候创建一个 login 的进程。系统中的所有进程都是 init 核心线程的后代。

    新的进程的创建是通过克隆旧的进程,或者说克隆当前的进程来实现的。一个新的任务是通过系统调用创建的( fork 或 clone ),克隆发生在核心的核心态。在系统调用的最后,产生一个新的进程,等待调度程序选择它运行。从系统的物理内存中为这个克隆进程的堆栈(用户和核心)分配一个或多个物理的页用于新的 task_struct 数据结构。一个进程标识符将会创建,在系统的进程标识符组中是唯一的。但是,也可能克隆的进程保留它的父进程的进程标识符。新的 task_struct 进入了 task 向量表中,旧的(当前的)进程的 task_struct 的内容拷贝到了克隆的 task_struct 。

参见 kernel/fork.c do_fork()

    克隆进程的时候, Linux 允许两个进程共享资源而不是拥有不同的拷贝。包括进程的文件,信号处理和虚拟内存。共享这些资源的时候,它们相应的 count 字段相应增减,这样 Linux 不会释放这些资源直到两个进程都停止使用。例如,如果克隆的进程要共享虚拟内存,它的 task_struct 会包括一个指向原来进程的 mm_struct 的指针, mm_struct 的 count 域增加,表示当前共享它的进程数目。

    克隆一个进程的虚拟内存要求相当的技术。必须产生一组 vm_area_struct 数据结构、相应的 mm_struct 数据结构和克隆进程的页表,这时没有拷贝进程的虚拟内存。这会是困难和耗时的任务,因为一部分虚拟内存可能在物理内存中而另一部分可能在交换文件中。替代底, Linux 使用了叫做“ copy on write ”的技术,即只有两个进程中的一个试图写的时候才拷贝虚拟内存。任何不写入的虚拟内存,甚至可能写的,都可以在两个进程之间共享二部会有什么害处。只读的内存,例如执行代码,可以共享。为了实现“ copy on write ”,可写的区域的页表条目标记为只读,而描述它的 vm_area_struct 数据结构标记为“ copy on write ”。当一个进程试图写向着这个虚拟内存的时候会产生 page fault 。这时 Linux 将会制作这块内存的一份拷贝并处理两个进程的页表和虚拟内存的数据结构。


Times and Timer (时间和计时器)

    核心跟踪进程的 CPU 时间和其他一些时间。每一个时钟周期,核心更新当前进程的 jiffies 来表示在系统和用户态下花费的时间总和。

    除了这些记账的计时器, Linux 还支持进程指定的间隔计时器( interval timer )。进程可以使用这些计时器在这些计时器到期的时候发送给自身信号。支持三种间隔计时器:

参见 kernel/itimer.c

Real 这个计时器使用实时计时,当计时器到期,发送给进程一个 SIGALRM 信号。

Virtual 这个计时器只在进程运行的时候计时,到期的时候,发送给进程一个 SIGVTALARM 信号。

Profile 在进程运行的时候和系统代表进程执行的时候都及时。到期的时候,会发送 SIGPROF 信号。

    可以运行一个或者所有的间隔计时器, Linux 在进程的 task_struct 数据结构中记录所有的必要信息。可以使用系统调用建立这些间隔计时器,启动、停止它们,读取当前的数值。虚拟和 profile 计时器的处理方式相同:每一次时钟周期,当前进程的计时器递减,如果到期,就发出适当的信号

参见 kernel/sched.c do_it_virtual() , do_it_prof()

    实时间隔计时器稍微不同。 Linux 使用计时器的机制在第 11 章描述。每一个进程都有自己的 timer_list 数据结构,当时使用实时计时器的时候,使用系统的 timer 表。当它到期的时候,计时器后半部分处理把它从队列中删除并调用间隔计时器处理程序。它产生 SIGALRM 信号并重启动间隔计时器,把它加回到系统计时器队列。

参见: kernel/iterm.c it_real_fn()


Executing Programs (执行程序)
     在 Linux 中,象 Unix 一样,程序和命令通常通过命令解释器执行。命令解释程序是和其他进程一样的用户进程,叫做 shell (想象一个坚果,把核心作为中间可食的部分,而 shell 包围着它,提供一个接口)。 Linux 中有许多 shell ,最常用的是 sh 、 bash 和 tcsh 。除了一些内部命令之外,比如 cd 和 pwd ,命令是可执行的二进制文件。对于输入的每一个命令, shell 在当前进程的搜索路径指定的目录中(放在 PATH 环境变量)查找匹配的名字。如果找到了文件,就加载并运行。 Shell 用上述的 fork 机制克隆自身,并在子进程中用找到的执行映像文件的内容替换它正在执行的二进制映像( shell )。通常 shell 等待命令结束,或者说子进程退出。你可以通过输入 control-Z 发送一个 SIGSTOP 信号给子进程,把子进程停止并放到后台,让 shell 重新运行。你可以使用 shell 命令 bg 让 shell 向子进程发送 SIGCONT 信号,把子进程放到后台并重新运行,它会持续运行直到它结束或者需要从终端输入或输出。



    执行文件可以由许多格式甚至可以是一个脚本文件( script file )。脚本文件必须用合适的解释程序识别并运行。例如 /bin/sh 解释 shell script 。可执行的目标文件包括了执行代码和数据以及足够的其他信息,时的操作系统可以把它们加载到内存中并执行。 Linux 中最常用的目标文件类型是 ELF ,而理论上, Linux 灵活到足以处理几乎所有的目标文件格式。

    好像文件系统一样, Linux 可以支持的二进制格式也是在核心连接的时候直接建立在核心的或者是可以作为模块加载的。核心保存了支持的二进制格式(见图 4.3 )的列表,当试图执行一个文件的时候,每一个二进制格式都被尝试,直到可以工作。通常, Linux 支持的二进制文件是 a.out 和 ELF 。可执行文件不需要完全读入内存,而使用叫做 demand loading 的技术。当进程使用执行映像的一部分的时候它才被调入内存,未被使用的映像可以从内存中废弃。

参见 fs/exec.c do_execve()




ELF
   

ELF ( Executable and Linkable Format 可执行可连接格式)目标文件,由 Unix 系统实验室设计,现在成为 Linux 最常用的格式。虽然和其他目标文件格式比如 ECOFF 和 a.out 相比,有性能上的轻微开支, ELF 感觉更灵活。 ELF 可执行文件包括可执行代码(有时叫做 text )和数据( data )。执行映像中的表描述了程序应该如何放到进程的虚拟内存中。静态连接的映像是用连接程序( ld )或者连接编辑器创建的,单一的映像中包括了运行该映像所需要的所有的代码和数据。这个映像也描述了该映像在内存中的布局和要执行的第一部分代码在映像中的地址。

    图 4.4 象是了静态连接的 ELF 可执行映像的布局。这是个简单的 C 程序,打印“ hello world ”然后退出。头文件描述了它是一个 ELF 映像,有两个物理头( e_phnum 是 2 ),从映像文件的开头第 52 字节开始( e_phoff )。第一个物理头描述映像中的执行代码,在虚拟地址 0x8048000 ,有 65532 字节。因为它是静态连接的,所以包括输出“ hello world ”的调用 printf ()的所有的库代码。映像的入口,即程序的第一条指令,不是位于映像的起始位置,而在虚拟地址 0x8048090 ( e_entry )。代码紧接着在第二物理头后面开始。这个物理头描述了程序的数据,将会加载到虚拟内存地址 0x8059BB8 。这块数据可以读写。你会注意到文件中数据的大小是 2200 字节( p_filesz )而在内存中的大小是 4248 字节。因为前 2200 字节包括预先初始化的数据,而接着的 2048 字节包括会被执行代码初始化的数据。

参见 include/linux/elf.h

    当 Linux 把 ELF 可执行映像加载到进程的虚拟地址空间的时候,它不是实际的加载映像。它设置虚拟内存数据结构,即进程的 vm_area_struct 和它的页表。当程序执行了 page fault 的时候,程序的代码和数据会被放到物理内存中。没有用到的程序部分将不会被放到内存中。一旦 ELF 二进制格式加载程序满足条件,映像是一个有效的 ELF 可执行映像,它把进程的当前可执行映像从它的虚拟内存中清除。因为这个进程是个克隆的映像(所有的进程都是),旧的映像是父进程执行的程序的映像(例如命令解释程序 shell bash )。清除旧的可执行映像会废弃旧的虚拟内存的数据结构,重置进程的页表。它也会清除设置的其他信号处理程序,关闭打开的文件。在清除过程的最后,进程准备运行新的可执行映像。不管可执行映像的格式如何,进程的 mm_struct 中都要设置相同的信息。包括指向映像中代码和数据起始的指针。这些数值从 ELF 可执行映像的物理头中读入,它们描述的部分也被映射到了进程的虚拟地址空间。这也发生在进程的 vm_area_struct 数据结构建立和页表修改的时候。 mm_struct 数据结构中也包括指针,指向传递给程序的参数和进程的环境变量。

ELF Shared Libraries ( ELF 共享库)

    动态连接的映像,反过来,不包含运行所需的所有的代码和数据。其中一些放在共享库并在运行的时候连接到映像中。当运行时动态库连接到映像中的时候,动态连接程序( dynamic linker )也要使用 ELF 共享库的表。 Linux 使用几个动态连接程序, ld.so.1 , libc.so.1 和 ld-linux.so.1 ,都在 /lib 目录下。这些库包括通用的代码,比如语言子例程。如果没有动态连接,所有的程序都必须有这些库的独立拷贝,需要更多的磁盘空间和虚拟内存。在动态连接的情况下, ELF 映像的表中包括引用的所有库例程的信息。这些信息指示动态连接程序如何定位库例程以及如何连接到程序的地址空间。

Scripts Files

    脚本文件是需要解释器才能运行的可执行文件。 Linux 下有大量的解释器,例如 wish 、 perl 和命令解释程序比如 tcsh 。 Linux 使用标准的 Unix 约定,在脚本文件的第一行包括解释程序的名字。所以一个典型的脚本文件可能开头是:

#!/usr/bin/wish

    脚本文件加载器试图找出文件所用的解释程序。它试图打开脚本文件第一行指定的可执行文件。如果可以打开,就得到一个指向该文件的 VFS I 节点的指针,然后执行它去解释脚本文件。脚本文件的名字成为了参数 0 (第一个参数),所有的其他参数都向上移动一位(原来的第一个参数成为了第二个参数等等)。加载解释程序和 Linux 加载其他可执行程序一样。 Linux 依次尝试各种二进制格式,直到可以工作。这意味着理论上你可以把几种解释程序和二进制格式堆积起来,让 Linux 的二进制格式处理程序更加灵活。

参见 fs/binfmt_script.c do_load_script()
分享到: QQ空间QQ空间 腾讯微博腾讯微博 腾讯朋友腾讯朋友
收藏收藏0 顶0 踩0

发表回复

高级模式
B Color Image Link Quote Code Smilies
验证码 换一个

回顶部