当前位置:Linux教程 - 内核技术 - 内核技术 - Linux核心

内核技术 - Linux核心

Linux核心
2004-04-23 15:18 pm
来自:Linux文档
现载:Www.8s8s.coM
地址:无名

Linux 核心--1.前言 原著: 翻译: Banyan & fifa (2001-04-27 13:52:07)
原著: David A Rusling

翻译: Banyan & fifa
 

--------------------------------------------------------------------------------

 

    本书是为那些想了解Linux内核工作原理的Linux狂热爱好者而写。 它并非一本内部手册。主要描叙了Linux设计的原理与机制;以及Linux内核怎样工作及其原因。 

Linux还在不断改进;本书基于目前比较流行且性能稳定的2.0.33核心。 

 

Version 0.8-3 

David A Rusling 

david.rusling@arm.com 


--------------------------------------------------------------------------------
前言 
Linux是互连网上的独特现象。虽然它是由学生的业余爱好发展而来,但是现在它已经成为最为流行的免费操作系统。对很多人来说,Linux是一个谜。免费的东西怎么会变得如此有价值?在个由少数软件公司统治的世界,由一帮HACKER们编写的东西是怎样与那些公司的产品竞争的? 这些软件是如何分发给分布在世界各个角落,希望得到稳定产品的人们的?事实上Linux的确稳定而富有竞争力。许多大学与研究机构都使用Linux完成他们的日常计算任务。人们在家用PC上使用Linux,许多公司也在使用它--尽管他们并不总是乐意承认这点。Linux主要用来浏览WEB,管理WEB站点,撰写与发送EMAIL,以及玩游戏。Linux绝对不是玩具而是具有专业水平的操作系统,它的爱好者遍及世界。

Linux的源头要追溯到最古老的UNIX。1969年,Bell实验室的Ken Thompson开始利用一台闲置的 PDP-7计算机开发了一种多用户,多任务操作系统。很快,Dennis Richie加入了这个项目,在他们共同努力下诞生了最早的UNIX。Richie受一个更早的项目——MULTICS的启发,将此操作系统命名为Unix。早期UNIX是用汇编语言编写的,但其第三个版本用一种崭新的编程语言C重新设计了。C是Richie设计出来并用于编写操作系统的程序语言。通过这次重新编写,Unix得以移植到更为强大的 DEC PDP-11/45与11/70计算机上运行。后来发生的一切,正如他们所说,已经成为历史。Unix从实验室走出来并成为了操作系统的主流,现在几乎每个主要的计算机厂商都有其自有版本的Unix.

Linux起源于一个学生的简单需求。Linus Torvalds,Linux的作者与主要维护者,在其上大学时所买得起的唯一软件是Minix. Minix是一个类似Unix,被广泛用来辅助教学的简单操作系统。Linus 对Minix不是很满意,于是决定自己编写软件。他以学生时代熟悉的Unix作为原型, 在一台Intel 386 PC上开始了他的工作。他的进展很快,受工作成绩的鼓舞,他将这项成果通过互连网与其他同学共享,主要用于学术领域。有人看到了这个软件并开始分发。每当出现新问题时,有人会立刻找到解决办法并加入其中,很快的, Linux成为了一个操作系统。值得注意的是Linux并没有包括Unix源码。它是按照公开的POSIX标准重新编写的。Linux大量使用了由麻省剑桥免费软件基金的GNU软件,同时Linux自身也是用它们构造而成。

许多人将Linux视作简单工具并将其放入CDROM中来分发。很多Linux使用者使用它来编写应用程序或者运行别人编写的应用程序。这些人热切的阅读HOWTO手册,当系统的一部分被正确的设置时,他们总是激动不已,失败时则沮丧气馁。只有少部分人敢于编写设备驱动程序并将核心的补丁提供给Linus Torvalds,Linus Torvalds从每个志愿者那里接收补充代码与对核心的修改代码。

这种情形听起来象非常混乱,但Linus进行了非常严格的质量控制并由他负责将所有的新代码加入核心。只有少部分人对Linux 核心贡献了源代码。 大多数Linux的使用者并不关心系统是如何工作,或者如何组合在一起的。这种情况令人惋惜,因为阅读Linux源代码提供了一个学习操作系统的绝好机会。这不仅仅因为它写得好,还因为它的源码是可以免费得到的。因为虽然作者们对其软件保留版权,但是在免费软件基金的GNU公开授权下源代码是可以自由分发的。第一眼看去,源码是非常复杂的。但是通过进一步观察你可以发现源码目录中包含有Kernel,mm以及net的目录, 不过要想知道这些目录中包含了那些代码以及代码是如何工作的就需要对Linux的总体结构与目标有较深入的理解。简而言之,这也是本书所希望达到的目标,为读者提供一个Linux如何工作清晰的印象。当你将文件从一个目录拷到另一个目录或者阅读电子邮件时,不妨在脑海中勾勒一下系统中正在发生什么事情,我还清楚的记得当我感到第一次认识到操作系统真的在工作时的兴奋。这种兴奋正是我想将它带给本书的读者的。

我第一次接触Linux在1994年下半年当我拜访Jim Paradis时,当时他正在致力于将Linux移植到Alpha AXP处理器系统上。从1984年开始,我曾经在DEC公司任职,主要工作是网络与通讯。1992年我开始为新成立的Digital Semiconductor分部工作。此分部的任务是全面进入商用芯片市场并销售芯片,特别是Alpha AXP系列处理器以及DEC以外的Alpha AXP系统板。当首次听到Linux时我便立刻意识到了这是一个有趣的机会。Jim的狂热是鼓惑人心的,我也开始帮他一起工作。在工作中,我越来越喜欢这个操作系统及创造它的工程师团体。 

Alpha AXP仅仅是Linux可以运行的多种平台中的一个。大多数Linux核心工作在基于Intel处理器 的系统上,但非Intel系统的Linux用户也越来越多。它们是Alpha AXP, ARM, MIPS, Sparc与Power PC。 虽然我可以根据上叙任何一种平台来编写本书的内容,但是我的技术知识与背景让我主要根据Alpha AXP处理器和ARM处理器来编写。这是本书有时使用非Intel硬件来描叙一些重要观点。值得注意的是,不管运行在哪种平台上,95%的Linux核心代码都是相同的。同样,本书95%的内容是关于Linux 内核的机器无关部分的讨论。

本书对读者的知识与经验没有任何要求。我相信对于某一事物的兴趣是鼓励自学的必要因素。不过对于计算机,或者PC和C程序语言的了解将有助于读者从有关材料中获益。

本书的组织
本书并不是特意一本Linux的内部手册。相反它是对操作系统的介绍,同时以Linux作为示例。书中每一章遵循“从共性到特性”的原则。它们将首先给出核心子系统的概叙,然后进行尽可能的详细描叙。 我不会用routine_X()调用routine_Y()来增加bar数据结构中foo域的值这种方式来描叙核心算法。 你自己可以通过阅读代码发现它。每当需要理解一段代码时,我总是将其数据结构画出来。这样我发现了许多相关的核心数据结构以及它们之间的关系。 每一章都是非常独立的,就象Linux核心子系统一样。当然有时它们还是有联系的,比如说,如果你没有理解虚拟内存工作原理就无法描叙进程。 硬件基本概念一章对现代PC做了简要介绍。操作系统必须与硬件系统紧密结合在一起协同工作。操作系统需要一些只能够由硬件提供的服务。为了全面理解Linux,你必须了解有关硬件的基础知识。 软件基本概念一章介绍了软件基本原理与C程序语言。讨论了建立Linux这样的操作系统的工具并且给出了操作系统的目标与功能的概叙。 内存管理这章描叙了Linux如何处理物理内存以及虚拟存储技术。 进程管理描叙了进程的概念以及Linux核心是如何创建、管理与删除系统中的进程。 进程间及进程与核心间通讯以协调它们的活动。Linux支持大量进程间通讯(IPC)机制。信号与管道是 其中的两种,Linux同时还支持系统V IPC机制。这些进程间通讯机制在IPC一章中描叙。 外部设备互连(PCI)标准已经成为PC上低价位高数传率的总线标准。PCI一章将描叙Linux核心是如何初始化并使用PCI总线及设备的。 中断及中断处理一章将着重于Linux核心对中断的处理。虽然处理中断有通用的机制与接口,但某些细节是与硬件及CPU体系结构相关的。 Linux的一个长处是其对现代PC的硬件设备强有力的支持。设备驱动程序一章将描叙Linux核心是如何控制系统中的物理设备。 文件系统一章描叙了Linux核心是如何维护它所支持的文件系统中的文件。同时还描叙了虚拟文件系统(VFS)及Linux核心的每种文件系统是如何得到支持。 网络与Linux几乎是同义的。在某种意义上Linux是WWW时代互连网的产物。其开发者通过Web来交换信息及代码。网络一章描叙了Linux是如何支持TCP/IP这些网络协议。 核心机制一章主要讨论能使Linux核心其他部分有效工作而由核心所提供的一些通用任务与机制。 动态模块一章描叙Linux核心是如何仅在需要时动态加载某些模块,比如文件系统。 处理器一章给出了目前Linux可以在其上运行的一些处理器的简要介绍。 资源一章则提供了有关Linux核心资源的有用信息。

Linux 核心--2.硬件基础
原著: David A Rusling 翻译: Banyan & fifa (2001-04-27 13:53:43)
第一章 硬件基础


操作系统必须与基本硬件系统密切协作。它需要那些仅仅能够由硬件提供的服务。为了全面理解Linux操作系统,你必须要懂得一些有关硬件的知识。本章将对硬件:现代PC做一个简要的介绍。 当1975年一月的"Popular Electronics"杂志以Altair 8080的图片作为封面时,一场革命开始了。 家用电器爱好者能独立组装出来的Altair 8080,当时价格仅仅为397美圆。这种带有256字节内存的8080处理器还没有显示器与键盘,以今天的标准来看,它是微不足道的。它的创造者, Ed Roberts,发明了"personal computer"来描叙他的新发明。但现在PC这一术语已被用来称呼那些自己就可以携带的计算机。在这个定义上,非常强劲的计算机如Alpha AXP 也可称为PC。 狂热的HACKER们看到了Altair的巨大潜力,于是他们开始为它编写软件和设计硬件。对早期的先驱来说这意味者某种自由;一种从顽固的超级批处理主机中解放出来的自由。滚滚而来的财富让许多着迷于此(一台可以放在厨房餐桌上的计算机)的大学生纷纷退学。许多五花八门的硬件开始出现,软件 HACKER们忙着为这些新机器编写软件。有意思的是IBM首先坚定的进行现代PC的设计和制造并于1982 年推出产品。该产品的构造是:8080 CPU、64K字节主存、两个软盘驱动器以及25行80列的彩色CGA 显示器。虽然以现在观点看那些都不是多么先进的东西但当时销售情况却很好。紧接着,1983年,带有昂贵的10MB硬盘驱动器的IBM PC-XT出现了。在IBM PC体系结构成为事实上的标准不久之后,大量仿制者如COMPAQ公司出现了。由于这种事实标准的存在,多个硬件公司在这一快速增长的市场上进行了激烈竞争。 但用户却从低价中获益。许多早期PC中的结构特征还保留在现代PC系统中。比如Intel公司最先进的Pentium Pro处理器还保留着Intel 8086的寻址模式。当Linus Torvalds开始写Linux时,他选择了当时最广泛使用同时价格合理的Intel 80386 PC。 图1.1 典型的PC主板示意图。 从PC的外部来看,最引人注目的是机箱,键盘,鼠标以及显示器。在机箱前部有一些按钮,一个微型显示器显示着一些数字,此外还有一个软驱。今天的大多数机器还包含一个CD ROM,另外,如果想保护你的数据,还可以添加一个磁带机作为备份用。这些设备统称为外部设备。 尽管CPU是系统的总管,但是它仅仅是一个智能设备。所有的这些外设控制器都具有某种层度的智能,如IDE控制器。在PC内部,你可以看到一个包括CPU或者微处理器,主存和许多ISA或PCI外设控制器插槽的主板(图1.1)。有些控制器,如IDE磁盘控制器必须建立在系统板上。 



 
图 1.1: 典型的PC主板. 
  

1.1 CPU
CPU,或者微处理器,是计算机系统的核心。微处理器进行计算或者逻辑操作并且管理来自主存的指令并执行它。在计算机的早期时代,微处理器的功能部件使用的是分立元件(外型很大)。 这就是中央处理单元这一名词的由来。现代微处理器将部件结合到小型硅片上的集成电路中。在本书中CPU和微处理器及处理器有相同的意义。 微处理器的操作对象是二进制数据;数据由0和1组成。 1和0对应着电子开关的开路与断路状态。正如十进制的42表示有4个10和一个2一样,一个二进制数是一系列表示2的次幂的二进制数字组成。二进制0001对应十进制的1,二进制的0010对应十进制 的2,二进制的0011表示3,而0100对应4。十进制42的二进制表示为101010。但是在计算机程序中, 人们常用十进制来表示数而不是直接使用二进制。 在需要使用二进制数时,人们往往使用16进制数。如十进制数只能从0到9一样,16进制数可以从 0疏导15,其中10到15分别用字母A、B、C、D、E及F来表示。这样16进制的2A的十进制表示为42- 2*16+10=42。在C程序语言中,16进制数的前缀为"0x";16进制的2A写成0x2A。 微处理器可以执行如加、乘和除以及象"X是否比Y大"这种逻辑运算。 处理器的执行由外部时钟来监控。这个时钟称为系统时钟,它每隔相同的时间间隔就向CPU发送一个脉冲。在每个时钟脉冲上,处理器都会做一些工作。比如,处理器每个时钟脉冲上执行一条指令。处理器的速度一般以系统时钟的速率来描叙。一个100MHz的处理器每秒将接收100,000,000 个时钟滴答。但是用CPU的时钟频率来描叙CPU的工作能力是不正确的,因为它们执行的指令不相同。 然而,快速的时钟可以在某种程度上代表高性能的CPU。处理器执行的指令是非常简单的;例如"将内存X处的内容读入寄存器Y"。寄存器是微处理器的内部存储部件,用来存储数据并对数据执行某些指令。有些指令有可能使处理器停止当前的工作而跳转到内存中另外一条指令执行。现代微处理器的紧凑设计使得它有可能每秒执行上百万甚至亿条指令。 指令执行前必须从内存中取出来。指令自身要使用的数据也必须从内存中取出来并放置在适当的地方。 微处理器中寄存器的大小、数量以及类型都取决于微处理器的类型。Intel 80486处理器和Alpha AXP 有迥然不同的寄存器,最明显的区别在于Intel 寄存器为32位而Alpha AXP为64位。一般来说,任何处理器都有许多通用寄存器和少量专用寄存器。许多微处理器有以下几种特定的寄存器。

程序计数器(PC) 
此寄存器包含下条指令执行的地址。每当取回一条指令时,PC的内容将自动增加。 
堆栈指针(SP) 
微处理器经常需要访问存储临时数据的外部RAM。堆栈是一种便捷的存放临时数据的方法,处理器提供了特殊指令来将数值压入堆栈然后将其从堆栈中弹出。 堆栈以后进先出(LIFO)的方式工作。换句话说,如果你压入两个值X和Y,然后执行弹栈操作,你将取到Y的值。 有些处理器的堆栈从内存顶部向下增长而有些相反。但有的处理器同时支持这两种方式,如ARM。 

处理机状态字(PS) 
指令的执行将得到执行结果;比如"寄存器X中的内容要大于寄存器Y中的内容?"将得到正确或错误作为结果。PS寄存器包含着这些信息及有关处理器当前状态的其他信息。例如大多数处理器至少有两种执行方式,核心(或管态)与用户方式。PS寄存器包含表示当前执行方式的信息。 

1.2 内存
所有计算机系统都有一个由不同速度与大小的存储器组成的层次结构。最快的的存储器是高速缓存,它被用来暂存主存中的内容。这种存储器速度非常快但非常昂贵,大多数处理器都有少量的片上高速缓存或者将其放在主板上。有些处理器的高速缓存既包含数据也包含指令,但有些将其分成两部分。 Alpha AXP处理器有两个内部高速缓存,一个用来缓存数据(D-Cache)而另一个用来缓存指令(I- Cache)。而外部高速缓存(B-Cache)将两者混合。这样,相对外部高速缓存存储器,主存的速度非常慢。 高速缓存与主存中的内容必须保持一致。换句话说,对应于地址空间的同一个位置,如果该位置的数据被缓存入高速缓存,则其内容必须和主存中的一致。保证高速缓存一致性的工作由硬件和操作系统共同分担。 这就是在系统中硬件和软件必须紧密协作的原因。 

1.3 总线
主板上分立的部件通过称为总线的线路连接在一起。系统总线的功能在逻辑上被划分为三部分: 地址总线、数据总线和控制总线。地址总线为数据传输指明内存位置(地址)。数据总线包含传输的数据。数据总线是双向的;它允许数据读入CPU也支持从CPU读出来。控制总线则包含几条表示路由分时和系统的控制信号。当然还有其他一些总线存在,例如ISA和PCI总线是将外设连接到系统的常用方式。 

1.4 控制器与外设
外设是一些物理设备,比如说图象卡或者磁盘,它们受控于位于主板或者主板上插槽中的控制芯片。 IDE磁盘被IDE控制器芯片控制而SCSI磁盘由SCSI磁盘控制器芯片控制。这些控制器通过各种总线连接到CPU上或相互间互连。目前制造的大多数系统使用PCI和ISA总线来连接主要系统部件。控制器是一些类似CPU的处理器,它们可以看做CPU的智能帮手。CPU则是系统的总控。 虽然所有这些控制器互不相同,但是它们的寄存器的功能类似。运行在CPU上的软件必须能读出或者写入这些控制寄存器。其中有一个寄存器可能包含指示错误的状态码。另一个则用于控制目的,用来改变控制器的运行模式。在总线上的每个控制器可以被CPU所单独寻址,这是软件设备驱动程序能写入寄存器并能控制这些控制器的原因。 

1.5 地址空间
系统总线将CPU与主存连接在一起并且和连接CPU与系统硬件外设的总线隔离开。一般来说,硬件外设存在的主存空间叫I/O空间。I/O空间还可以进一步细分,但这里我们不再深究。CPU既可以访问系统内存空间又可以访问I/O空间内存,而控制器自身只能在CPU协助下间接的访问系统内存。从设备的角度来看,比如说软盘控制器,它只能看到在ISA总线上的控制寄存器而不是系统内存。典型的CPU使用不同指令来访问内存与I/O空间。例如,可能有一条指令"将I/O地址0x3F0的内容读入到寄存器X"。这正是CPU控制系统硬件设备的方式:通过读写I/O地址空间上的外设寄存器。在I/O空间中通用外设(IDE控制器、串行口、软盘控制器等等)上的寄存器经过多年的PC体系结构发展基本保持不变。I/O地址空间0x3f0是串行口(COM1)的控制寄存器之一。 有时控制器需要直接从系统主存中读写大量数据。例如当用户将数据写入硬盘时。在这种情况 下,直接内存访问(DMA)控制器将用来允许硬件外设直接访问系统主存,不过这将处于CPU的严格监控下。 

1.6 时钟
所有的操作系统都必须准确的得到当前时间,所以现代PC包含一个特殊的外设称为实时时钟(RTC)。它提供了 两种服务:可靠的日期和时间以及精确的时间间隔。RTC有其自身的电池这样即使PC掉电时它照样可以工作,这就是PC总是"知道"正确时间和日期的原因。而时间间隔定时器使得操作系统能进行准确的调度工作。 



Linux 核心--3.软件基础
原著: David A Rusling 翻译: Banyan & fifa (2001-04-27 13:54:15)

第二章 软件基础 

程序是执行某个特定任务的计算机指令集合。程序可以用多种程序语言来编写:从低级计算机语言-汇编语言到高级的、与机器本身无关的语言入C程序语言。操作系统是一个允许用户运行如电子表格或者字处理软件等应用程序的特殊程序。本章将介绍程序设计的基本原则,同时给出操作系统设计目标与功能的概述。 


2.1 计算机编程语言
2.1.1 汇编语言
那些CPU从主存读取出来执行的指令对人类来说是根本不可理解的。它们是告诉计算机如何准确动作的机器代码。在Intel 80486指令中16进制数0x89E5表示将ESP寄存器的内容拷入EBP寄存器。为最早的计算机设计的工具之一就是汇编器,它可以将人们可以理解的源文件汇编成机器代码。汇编语言需要显式的操作寄存器和数据,并且与特定处理器相关。比如说Intel X86微处理器的汇编语言与Alpha AXP微处理器的汇编语言决然不同。以下是一段Alpha AXP汇编指令程序: 

    ldr r16, (r15)    ; Line 1
    ldr r17, 4(r15)   ; Line 2
    beq r16,r17,100   ; Line 3
    str r17, (r15)    ; Line 4
100:                  ; Line 5


第一行语句将寄存器15所指示的地址中的值加载到寄存器16中。接下来将邻接单元内容加载到寄存器17中。 第三行语句比较寄存器16和寄存器17中的值,如果相等则跳转到标号100处,否则继续执行第四行语句:将 寄存器17的内容存入内存中。如果寄存器中值相等则无须保存。汇编级程序一般冗长并且很难编写,同时还容易出错。 Linux核心中只有很少一部分是用汇编语言编写,并且这些都是为了提高效率或者是需要兼容不同的CPU。 


2.1.2 C编程语言和编译器
用汇编语言编写程序是一件困难且耗时的工作。同时还容易出错并且程序不可移植:只能在某一特定处理器 家族上运行。而用C语言这样的与具体机器无关的语言就要好得多。C程序语言允许用它所提供的逻辑算法来 描叙程序同时它提供编译器工具将C程序转换成汇编语言并最终产生机器相关代码。好的编译器能产生和汇编语言程序相接近的效率。Linux内核中大部分用C语言来编写,以下是一段C语言片段: 

        if (x != y)
                x = y ;

它所执行的任务和汇编语言代码示例中相同。如果变量X的值和变量Y的不相同则将Y的内容赋予X。C代码被 组织成子程序,单独执行某一任务。子程序可以返回由C支持的任何数据类型的值。较庞大的程序如Linux 核心由许多单独的C源代码模块组成,每个模块有其自身的子程序与数据结构。这些C源代码模块将相关函数组合起来完成如文件处理等功能。 C支持许多类型的变量,变量是一个通过符号名称引用的内存位置。在以上的例子中,X和Y都是内存中的位置。程序员并不关心变量放在什么地方,这些工作由连接程序来完成。有些变量包含不同类型的数据,整数和浮点数,以及指针。 指针是那些包含其他数据内存位置或者地址的变量。假设有变量X,位于内存地址0x80010000处。你可以使用指针变量px来指向X,则px的值为0x80010000。 C语言允许相关变量组合起来形成数据结构,例如: 

        struct {
                int i ;
                char b ;
        } my_struct ;

这是一个叫做my_struct的结构,它包含两个元素,一个是32位的整数i,另外一个是8位的字符b。 


2.1.3 连接程序
连接程序是一个将几个目标模块和库过程连接起来形成单一程序的应用。目标模块是从汇编器或者编译器中产生的机器代码,它包含可执行代码和数据,模块结合在一起形成程序。例如一个模块可能包含程序中所有的数据库函数而另一个主要处理命令行参数。连接程序修改目标模块之间的引用关系,使得在某一模块中引用的数据或者子程序的确存在于其他模块中。Linux核心是由许多目标模块连接形成的庞大程序。 


2.2 操作系统概念 
如果没有软件,计算机只不过是一堆发热的电子器件。如果将硬件比做计算机的心脏则软件就是它的灵魂。操作系统是一组系统程序的集合,它提供给用户运行应用软件的功能。操作系统对系统硬件进行抽象,它提供给系统用户一台虚拟的机器。大多数PC可以运行一种或者多种操作系统,每个操作系统都有不同的外观。Linux由许多独立的功能段组成。比如Linux内核,如果没有库函数和外壳程序,内核是没有什么用的。 为了理解操作系统到底是什么,思考一下当你敲入一个简单命令时,系统中发生了什么: 


$ ls
Mail            c               images          perl
docs            tcl
$ 


$符号是由用户登录外壳(这里指Bash)提供的提示符。它表示正在等待用户敲入一些命令。敲入ls命令,首先键盘驱动程序识别出敲入的内容。然后键盘驱动将它们传递给外壳程序,由外壳程序来负责查找同名的可执行程序(ls)。 如果在/bin/ls目录中找到了ls,则调用核心服务将ls的可执行映象读入虚拟内存并开始执行。ls调用核心的文件子系统来寻找那些文件是可用的。文件系统使用缓冲过的文件系统信息,或者调用磁盘设备驱动从磁盘上读取信息。当然ls还可能引起网络驱动程序和远程机器来交换信息以找出关于系统要访问的远程文件系统信息(文件系统可以通过网络文件系统或者NFS进行远程安装)。当得到这些信息后,ls将这些信息通过调用视频驱动写到显示器屏幕上。 以上这些听起来十分复杂。这个非常简单命令的处理过程告诉我们操作系统是一组协同工作的函数的集合,它们给所有的用户对系统有一致的印象。 


2.2.1 内存管理
由于资源的有限,比如内存,操作系统处理事务的过程看起来十分冗长。操作系统的一个基本功能就是使一个只有少量物理内存的系统工作起来象有多得多的内存一样。这个大内存叫为虚拟内存。其思想就是欺骗系统中运行的软件,让它们认为有大量内存可用。系统将内存划分成易于处理的页面,在系统运行时将这些页面交换到硬盘上去。 由于有另外一个技巧:多处理的存在,这些软件更加感觉不到系统中真实内存的大小。 


2.2.2 进程
进程可以认为是处于执行状态的程序,每个进程有一个特定的程序实体。观察以下Linux系统中的进程,你会发现有比你想象的要多得多的进程存在。比如,在我的系统中敲入ps命令,将得到以下结果: 

$ ps
  PID TTY STAT  TIME COMMAND
  158 pRe 1     0:00 -bash
  174 pRe 1     0:00 sh /usr/X11R6/bin/startx
  175 pRe 1     0:00 xinit /usr/X11R6/lib/X11/xinit/xinitrc --
  178 pRe 1 N   0:00 bowman
  182 pRe 1 N   0:01 rxvt -geometry 120x35 -fg white -bg black
  184 pRe 1 
  185 pRe 1 
  187 pp6 1     9:26 /bin/bash
  202 pRe 1 N   0:00 rxvt -geometry 120x35 -fg white -bg black
  203 ppc 2     0:00 /bin/bash
 1796 pRe 1 N   0:00 rxvt -geometry 120x35 -fg white -bg black
 1797 v06 1     0:00 /bin/bash
 3056 pp6 3 
 3270 pp6 3     0:00 ps
$     

如果系统有许多个CPU,则每个进程可以运行在不同的CPU上。不幸的是,大多数系统中只有一个CPU。这样 操作系统将轮流运行几个程序以产生它们在同时运行的假象。这种方式叫时间片轮转。同时这种方法还骗过了进程使它们都认为只有自己在运行。进程之间被隔离开,以便某个进程崩溃或者误操作不会影响到别的进程。操作系统通过为每个进程提供分立的地址空间来作到这一点。 


2.2.3 设备驱动 
设备驱动组成了Linux核心的主要部分。象操作系统的其他部分一样,它们运行在高权限环境中且一旦出错 将引起灾难性后果。设备驱动控制操作系统和硬件设备之间的相互操作。例如当文件系统通过使用通用块设备接口来对IDE磁盘写入数据块。设备驱动负责处理所有设备相关细节。设备驱动与特定的控制器芯片有关,如果系统中有一个NCR810 SCSI控制卡则需要有NCR810 SCSI的驱动程序。 


2.2.4 文件系统
Linux和Unix一样,系统中的独立文件系统不是通过设备标志符来访问,而是通过表示文件系统的层次树结构来访问。当Linux添加一个新的文件系统到系统中时,会将它mount到一个目录下,比如说/mnt/cdrom。 Linux的一个重要特征就是支持多种文件系统。这使得它非常灵活并且可与其他操作系统并存。Linux中最常用的文件系统是EXT2文件系统,它在大多数Linux分发版本中都得到了支持。 文件系统提供给用户一个关于系统的硬盘上文件和目录的总体映象,而不管文件的类型和底层物理设备的特性。 Linux透明地支持多种文件系统并将当前安装的所有文件和文件系统集成到虚拟文件系统中去。所以,用户和进程一般都不知道某个文件位于哪种文件系统中,他们只是使用它。 块设备驱动将物理块设备类型(例如IDE和SCSI)和文件系统中的差别隐藏起来,物理设备只是数据块的线性存储集合。设备的不同导致块大小的不同,从软盘设备的512字节到IDE磁盘的1024字节。这些都隐藏了起来,对系统用户来说这都是不可见的。不管设备类型如何,EXT2文件系统看起来总是一样。 


2.3 核心数据结构 
操作系统可能包含许多关于系统当前状态的信息。当系统发生变化时,这些数据结构必须做相应的改变以反映这些情况。例如,当用户登录进系统时将产生一个新的进程。核心必须创建表示新进程的数据结构,同时 将它和系统中其他进程的数据结构连接在一起。 大多数数据结构存在于物理内存中并只能由核心或者其子系统来访问。数据结构包括数据和指针;还有其他数据结构的地址或者子程序的地址。它们混在一起让Linux核心数据结构看上去非常混乱。尽管可能被几个核心子系统同时用到,每个数据结构都有其专门的用途。理解Linux核心的关键是理解它的数据结构以及Linux核心中操纵这些数据结构的各种函数。本书把Linux核心的 描叙重点放在数据结构上,主要讨论每个核心子系统的算法,完成任务的途径以及对核心数据结构的使用。 


2.3.1 连接列表
Linux使用的许多软件工程的技术来连接它的数据结构。在许多场合下,它使用linked或者chained数据结构。 每个数据结构描叙某一事物,比如某个进程或网络设备,核心必须能够访问到所有这些结构。在链表结构中,个根节点指针包含第一个结构的地址,而在每个结构中又包含表中下一个结构的指针。表的最后一项必须是0或者NULL,以表明这是表的尾部。在双向链表中,每个结构包含着指向表中前一结构和后一结构的指针。使用双向链表的好处在于更容易在表的中部添加与删除节点,但需要更多的内存操作。这是一种典型的操作系统开销与CPU循环之间的折中。 

2.3.2 散列表
链表用来连接数据结构比较方便,但链表的操作效率不高。如果要搜寻某个特定内容,我们可能不得不遍历整个链表。Linux使用另外一种技术:散列表来提高效率。散列表是指针的数组或向量,指向内存中连续的相邻数据集合。散列表中每个指针元素指向一个独立链表。如果你使用数据结构来描叙村子里的人,则你可以使用年龄作为索引。为了找到某个人的数据,可以在人口散列表中使用年龄作为索引,找到包含此人特定数据的数据结构。但是在村子里有很多人的年龄相同,这样散列表指针变成了一个指向具有相同年龄的人数据链表的指针。搜索这个小链表的速度显然要比搜索整个数据链表快得多。 由于散列表加快了对数据结构的访问速度,Linux经常使用它来实现Caches。Caches是保存经常访问的信息的子集。经常被核心使用的数据结构将被放入Cache中保存。Caches的缺点是比使用和维护单一链表和散列表更复杂。寻找某个数据结构时,如果在Cache中能够找到(这种情况称为cache 命中),这的确很不错。但是如果没有找到,则必须找出它,并且添加到Cache中去。如果Cache空间已经用完则Linux必须决定哪一个结构将从其中抛弃,但是有可能这个要抛弃的数据就是Linux下次要使用的数据。 


2.3.3 抽象接口
Linux核心常将其接口抽象出来。接口指一组以特定方式执行的子程序和数据结构的集合。例如,所有的网络设备驱动必须提供对某些特定数据结构进行操作的子程序。通用代码可能会使用底层的某些代码。例如网络层代码是通用的,它得到遵循标准接口的特定设备相关代码的支持。 通常在系统启动时,底层接口向更高层接口注册(Register)自身。这些注册操作包括向链表中加入结构节点。例如,构造进核心的每个文件系统在系统启动时将其自身向核心注册。文件/proc/filesysems中可以看到已经向核心注册过的文件系统。注册数据结构通常包括指向函数的指针,以文件系统注册为例,它向Linux核心注册时必须将那些mount文件系统连接时使用的一些相关函数的地址传入。 

Linux 核心--4.内存管理
原著: David A Rusling 翻译: Banyan & fifa (2001-04-27 13:54:58)
第三章 存储管理

存储管理子系统时操作系统中最重要的组成部分之一。在早期计算时代,由于人们所需要的内存数目远远大于物理内存,人们设计出了各种各样的策略来解决此问题,其中最成功的是虚拟内存技术。它使得系统中为有限物理内存竞争的进程所需内存空间得到满足。 

虚拟内存技术不仅仅可让我们可以使用更多的内存,它还提供了以下功能: 

巨大的寻址空间 

操作系统让系统看上去有比实际内存大得多的内存空间。虚拟内存可以是系统中实际物理空间的许多倍。每个进程运行在其独立的虚拟地址空间中。这些虚拟空间相互之间都完全隔离开来,所以进程间不会互相影响。同时,硬件虚拟内存机构可以将内存的某些区域设置成不可写。这样可以保护代码与数据不会受恶意程序的干扰。 

内存映射 

内存映射技术可以将映象文件和数据文件直接映射到进程的地址空间。在内存映射中,文件的内容被直接连接到进程虚拟地址空间上。 

公平的物理内存分配 

内存管理子系统允许系统中每个运行的进程公平地共享系统中的物理内存。 

共享虚拟内存 

尽管虚拟内存允许进程有其独立的虚拟地址空间,但有时也需要在进程之间共享内存。 例如有可能系统中有几个进程同时运行BASH命令外壳程序。为了避免在每个进程的虚拟内存空间内都存在BASH程序的拷贝,较好的解决办法是系统物理内存中只存在一份BASH的拷贝并在多个进程间共享。动态库则是另外一种进程间共享执行代码的方式。共享内存可用来作为进程间通讯(IPC)的手段,多个进程通过共享内存来交换信息。 Linux支持SYSTEM V的共享内存IPC机制。 

3.1 虚拟内存的抽象模型



图3.1 虚拟地址到物理地址映射的抽象模型 


在讨论Linux是如何具体实现对虚拟内存的支持前,有必要看一下更简单的抽象模型。 

在处理器执行程序时需要将其从内存中读出再进行指令解码。在指令解码之前它必须向内存中某个位置取出或者存入某个值。然后执行此指令并指向程序中下一条指令。在此过程中处理器必须频繁访问内存,要么取指取数,要么存储数据。 

虚拟内存系统中的所有地址都是虚拟地址而不是物理地址。通过操作系统所维护的一系列表格由处理器实现由虚拟地址到物理地址的转换。 

为了使转换更加简单,虚拟内存与物理内存都以页面来组织。不同系统中页面的大小可以相同,也可以不同,这样将带来管理的不便。Alpha AXP处理器上运行的Linux页面大小为8KB,而Intel X86系统上使用4KB页面。每个页面通过一个叫页面框号的数字来标示(PFN) 。 

页面模式下的虚拟地址由两部分构成:页面框号和页面内偏移值。如果页面大小为4KB,则虚拟地址的 11:0位表示虚拟地址偏移值,12位以上表示虚拟页面框号。处理器处理虚拟地址时必须完成地址分离工作。在页表的帮助下,它将虚拟页面框号转换成物理页面框号,然后访问物理页面中相应偏移处。 

图3.1给出了两个进程X和Y的虚拟地址空间,它们拥有各自的页表。这些页表将各个进程的虚拟页面映射到内存中的物理页面。在图中,进程X的虚拟页面框号0被映射到了物理页面框号4。理论上每个页表入口应包含以下内容: 


有效标记,表示此页表入口是有效的 
页表入口描叙的物理页面框号 
访问控制信息。用来描叙此页可以进行哪些操作,是否可写?是否包含执行代码? 
虚拟页面框号是为页表中的偏移。虚拟页面框号5对应表中的第6个单元(0是第一个)。 

为了将虚拟地址转换为物理地址,处理器首先必须得到虚拟地址页面框号及页内偏移。一般将页面大小设为2的次幂。将图3.1中的页面大小设为0x2000字节(十进制为8192)并且在进程Y的虚拟地址空间中某个地址为0x2194,则处理器将其转换为虚拟页面框号1及页内偏移0x194。 

处理器使用虚拟页面框号为索引来访问处理器页表,检索页表入口。如果在此位置的页表入口有效,则处理器将从此入口中得到物理页面框号。如果此入口无效,则意味着处理器存取的是虚拟内存中一个不存在的区域。在这种情况下,处理器是不能进行地址转换的,它必须将控制传递给操作系统来完成这个工作。 

某个进程试图访问处理器无法进行有效地址转换的虚拟地址时,处理器如何将控制传递到操作系统依赖于具体的处理器。通常的做法是:处理器引发一个页面失效错而陷入操作系统核心,这样操作系统将得到有关无效虚拟地址的信息以及发生页面错误的原因。 

再以图3.1为例,进程Y的虚拟页面框号1被映射到系统物理页面框号4,则再物理内存中的起始位置为 0x8000(4 * 0x2000)。加上0x194字节偏移则得到最终的物理地址0x8194。 

通过将虚拟地址映射到物理地址,虚拟内存可以以任何顺序映射到系统物理页面。例如,在图3.1中,进程X的虚拟页面框号0被映射到物理页面框号1而虚拟页面框号7被映射到物理页面框号0,虽然后者的虚拟页面框号要高于前者。这样虚拟内存技术带来了有趣的结果:虚拟内存中的页面无须在物理内存保持特定顺序。 

  

3.1.1 请求换页
在物理内存比虚拟内存小得多的系统中,操作系统必须提高物理内存的使用效率。节省物理内存的一种方法是仅加载那些正在被执行程序使用的虚拟页面。比如说,某个数据库程序可能要对某个数据库进行查询操作,此时并不是数据库的所有内容都要加载到内存中去,而只加载那些要用的部分。如果此数据库查询是一个搜索查询而无须对数据库进行添加记录操作,则加载添加记录的代码是毫无意义的。这种仅将要访问的虚拟页面载入的技术叫请求换页。 

当进程试图访问当前不在内存中的虚拟地址时,处理器在页表中无法找到所引用地址的入口。在图3.1中,对于虚拟页面框号2,进程X的页表中没有入口,这样当进程X试图访问虚拟页面框号2内容时,处理器不能将此地址转换成物理地址。这时处理器通知操作系统有页面错误发生。 

如果发生页面错的虚拟地址是无效的,则表明进程在试图访问一个不存在的虚拟地址。这可能是应用程序出错而引起的,例如它试图对内存进行一个随机的写操作。此时操作系统将终止此应用的运行以保护系统中其他进程不受此出错进程的影响。 

如果出错虚拟地址是有效的,但是它指向的页面当前不在内存中,则操作系统必须将此页面从磁盘映象中读入到内存中来。由于访盘时间较长,进程必须等待一段时间直到页面被取出来。如果系统中还存在其他进程,操作系统就会在读取页面过程中的等待过程中选择其中之一来运行。读取回来的页面将被放在一个空闲的物理页面框中,同时此进程的页表中将添加对应此虚拟页面框号的入口。最后进程将从发生页面错误的地方重新开始运行。此时整个虚拟内存访问过程告一段落,处理器又可以继续进行虚拟地址到物理地址转换,而进程也得以继续运行。 

Linux使用请求换页将可执行映象加载到进程的虚拟内存中。当命令执行时,可执行的命令文件被打开,同时其内容被映射到进程的虚拟内存。这些操作是通过修改描叙进程内存映象的数据结构来完成的,此过程称为内存映射。然而只有映象的起始部分被调入物理内存,其余部分仍然留在磁盘上。当映象执行时,它会产生页面错误,这样Linux将决定将磁盘上哪些部分调入内存继续执行。 

3.1.2 交换 
如果进程需要把一个虚拟页面调入物理内存而正好系统中没有空闲的物理页面,操作系统必须丢弃位于物理内存中的某些页面来为之腾出空间。 

如果那些从物理内存中丢弃出来的页面来自于磁盘上的可执行文件或者数据文件,并且没有修改过则不需要保存那些页面。当进程再次需要此页面时,直接从可执行文件或者数据文件中读出。 

但是如果页面被修改过,则操作系统必须保留页面的内容以备再次访问。这种页面被称为dirty页面, 当从内存中移出来时,它们必须保存在叫做交换文件的特殊文件中。相对于处理器和物理内存的速度,访问交换文件的速度是非常缓慢的,操作系统必须在将这些dirty页面写入磁盘和将其继续保留在内存中做出选择。 

选择丢弃页面的算法经常需要判断哪些页面要丢弃或者交换,如果交换算法效率很低,则会发生"颠簸"现象。在这种情况下,页面不断的被写入磁盘又从磁盘中读回来,这样一来操作系统就无法进行其他任何工作。以图3.1为例,如果物理页面框号1被频繁使用,则页面丢弃算法将其作为交换到硬盘的侯选者是不恰当的。一个进程当前经常使用的页面集合叫做工作集。高效的交换策略能够确保所有进程的工作集保存在物理内存中。 

Linux使用最近最少使用(LRU)页面衰老算法来公平地选择将要从系统中抛弃的页面。这种策略为系统中的每个页面设置一个年龄,它随页面访问次数而变化。页面被访问的次数越多则页面年龄越年轻;相反则越衰老。年龄较老的页面是待交换页面的最佳侯选者。 

  

3.1.3  共享虚拟内存
虚拟内存让多个进程之间可以方便地共享内存。所有的内存访问都是通过每个进程自身的页表进行。对于两个共享同一物理页面的进程,在各自的页表中必须包含有指向这一物理页面框号的页表入口。 

图3.1中两个进程共享物理页面框号4。对进程X来说其对应的虚拟页面框号为4而进程Y的为6。这个有趣的现象说明:共享物理页面的进程对应此页面的虚拟内存位置可以不同。 

  

3.1.4  物理与虚拟寻址模式
操作系统自身也运行在虚拟内存中的意义不大。如果操作系统被迫维护自身的页表那将是一个令人恶心的方案。多数通用处理器同时支持物理寻址和虚拟寻址模式。物理寻址模式无需页表的参与且处理器不会进行任何地址转换。Linux核心直接运行在物理地址空间上。 

Alpha AXP处理器没有特殊的物理寻址模式。它将内存空间划分为几个区域并将其中两个指定为物理映射地址。核心地址空间被称为KSEG地址空间,它位于地址0xfffffc0000000000以上区域。为了执行位于KSEG的核心代码或访问那里的数据,代码必须在核心模式下执行。Alpha上的Linux核心从地址0xfffffc0000310000开始执行. 

  

3.1.5  访问控制
页表入口包含了访问控制信息。由于处理器已经将页表入口作为虚拟地址到物理地址的映射,那么可以很方便地使用访问控制信息来判断处理器是否在以其应有的方式来访问内存。 

诸多因素使得有必要严格控制对内存区域的访问。有些内存,如包含执行代码的部分,显然应该是只读的,操作系统决不能允许进程对此区域的写操作。相反包含数据的页面应该是可写的, 但是去执行这段数据肯定将导致错误发生。多数处理器至少有两种执行方式:核心态与用户态。任何人都不会允许在用户态下执行核心代码或者在用户态下修改核心数据结构。 

  



图3.2 Alpha AXP页表入口 


页表入口中的访问控制信息是处理器相关的;图3.2是Alpha AXP处理器的PTE(Page Table Entry)。这些位域的含义如下: 


V 
有效,如果此位置位,表明此PTE有效 
  
FOE 
“执行时失效”,无论合时只要执行包含在此页面中的指令,处理器都将报告页面错误并将控制传递 
FOW 
“写时失效”, 除了页面错误发生在对此页面的写时,其他与上相同。 
FOR 
“读时失效”,除了页面错误发生在对此页面的读时,其他与上相同。 
ASM 
地址空间匹配。被操作系统用于清洗转换缓冲中的某些入口。 
KRE 
运行在核心模式下的代码可以读此页面。 
URE 
运行在用户模式下的代码可以读此页面。 
GH 
将整个块映射到单个而不是多个转换缓冲时的隐含粒度。 
KWE 
运行在核心模式下的代码可以写此页面。 
UWE 
运行在用户模式下的代码可以写此页面。 
page frame number 
对于V位置位的PTE,此域包含了对应此PTE的物理页面框号;对于无效PTE,此域不为0,它包含了页面在交换文件中位置的信息。 
以下两位由Linux定义并使用。 


_PAGE_DIRTY 
如果置位,此页面要被写入交换文件。 
_PAGE_ACCESSED 
Linux用它表示页面已经被访问过。 

3.2  高速缓冲
如果用上述理论模型来实现一个系统,它可能可以工作,但效率不会高。操作系统设计者和处理器设计者都在努力以提高系统的性能。除了制造更快的CPU和内存外,最好的办法是在高速缓冲中维护有用信息和数据以加快某些操作。Linux使用了许多与高速缓冲相关的内存管理策略。 
Buffer Cache 
这个buffer cache中包含了被块设备驱动使用的数据缓冲。 

这些缓冲的单元的大小一般固定(例如说512字节)并且包含从块设备读出或者写入的信息块。块设备是仅能够以固定大小块进行读写操作的设备。所有的硬盘都是块设备。 

  
利用设备标志符和所需块号作索引可以在buffer cache中迅速地找到数据。块设备只能够通过buffer cache来存取。如果数据在buffer cache中可以找到则无需从物理块设备(如硬盘)中读取,这样可以加速访问。 

Page Cache

用来加速硬盘上可执行映象文件与数据文件的存取。 

它每次缓冲一个页面的文件内容。页面从磁盘上读入内存后缓存在page cache中。 

  
  

Swap Cache 
只有修改过的页面存储在交换文件中。 

只要这些页面在写入到交换文件后没有被修改,则下次此页面被交换出内存时,就不必再进行更新写操作,这些页面都可以简单的丢弃。在交换频繁发生的系统中,Swap Cache可以省下很多不必要且耗时的磁盘操作。 

  
  

Hardware Caches 
一个常见的hardware cache是处理器中的页表入口cache。处理器不总是直接读取页表而是在需要时缓存页面的转换。这种cache又叫做转换旁视缓冲(Translation Look-aside Buffers),它包含系统中一个或多个处理器的页表入口的缓冲拷贝。 

  
当发出对虚拟地址的引用时,处理器试图找到相匹配的TLB入口。如果找到则直接将虚拟地址转换成物理地址并对数据进行处理。如果没有找到则向操作系统寻求帮助。处理器将向操作系统发出TLB失配信号,它使用一个特定的系统机制来将此异常通知操作系统。操作系统则为此地址匹配对产生新的TLB入口。当操作系统清除此异常时,处理器将再次进行虚拟地址转换。由于此时在TLB中已经有相应的入口,这次操作将成功。 

  
使用高速缓存的缺点在于Linux必须消耗更多的时间和空间来维护这些缓存,并且当缓存系统崩溃时系统也将崩溃。 


3.3  Linux 页表



图3.3 Linux的三级页表结构 


Linux总是假定处理器有三级页表。每个页表通过所包含的下级页表的页面框号来访问。图3.3给出了虚拟地址是如何分割成多个域的,每个域提供了某个指定页表的偏移。为了将虚拟地址转换成物理地址,处理器必须得到每个域的值。这个过程将持续三次直到对应于虚拟地址的物理页面框号被找到。最后再使用虚拟地址中的最后一个域,得到了页面中数据的地址。 

为了实现跨平台运行,Linux提供了一系列转换宏使得核心可以访问特定进程的页表。这样核心无需知道 页表入口的结构以及它们的排列方式。 

这种策略相当成功,无论在具有三级页表结构的Alpha AXP还是两级页表的Intel X86处理器中,Linux总是使 用相同的页表操纵代码。 

3.4  页面分配与回收
对系统中物理页面的请求十分频繁。例如当一个可执行映象被调入内存时,操作系统必须为其分配页面。当映象执行完毕和卸载时这些页面必须被释放。物理页面的另一个用途是存储页表这些核心数据结构。虚拟内存子系统中负责页面分配与回收的数据结构和机制可能用处最大。 

系统中所有的物理页面用包含mem_map_t结构的链表mem_map来描叙,这些结构在系统启动时初始化。每个 mem_map_t描叙了一个物理页面。其中与内存管理相关的重要域如下: 

count 

记录使用此页面的用户个数。当这个页面在多个进程之间共享时,它的值大于1。 
age 
此域描叙页面的年龄,用于选择将适当的页面抛弃或者置换出内存时。 
map_nr 
记录本mem_map_t描叙的物理页面框号。 
页面分配代码使用free_area数组来寻找和释放页面,此机制负责整个缓冲管理。另外此代码与处理器使用的页面大小和物理分页机制无关。 

free_area中的每个元素都包含页面块的信息。数组中第一个元素描叙1个页面,第二个表示2个页面大小的块而接下来表示4个页面大小的块,总之都是2的次幂倍大小。list域表示一个队列头,它包含指向mem_map数组中page数据结构的指针。所有的空闲页面都在此队列中。map域是指向某个特定页面尺寸的页面组分配情况位图的指针。当页面的第N块空闲时,位图的第N位被置位。 

图free-area-figure画出了free_area结构。第一个元素有个自由页面(页面框号0),第二个元素有4个页面大小的2个自由块,前一个从页面框号4开始而后一个从页面框号56开始。 

  

3.4.1  页面分配
Linux使用Buddy算法来有效的分配与回收页面块。页面分配代码每次分配包含一个或者多个物理页面的内存块。页面以2的次幂的内存块来分配。这意味着它可以分配1个、2个和4个页面的块。只要系统中有足够的空闲页面来满足这个要求(nr_free_pages > min_free_page),内存分配代码将在free_area中寻找一个与请求大小相同的空闲块。free_area中的每个元素保存着一个反映这样大小的已分配与空闲页面 的位图。例如,free_area数组中第二个元素指向一个反映大小为四个页面的内存块分配情况的内存映象。 

分配算法首先搜寻满足请求大小的页面。它从free_area数据结构的list域着手沿链来搜索空闲页面。如果没有这样请求大小的空闲页面,则它搜索两倍于请求大小的内存块。这个过程一直将持续到free_area 被搜索完或找到满足要求的内存块为止。如果找到的页面块大于请求的块则对其进行分割以使其大小与请求块匹配。由于块大小都是2的次幂所以分割过程十分简单。空闲块被连进相应的队列而这个页面块被分配给调用者。 





图3.4 free_area数据结构 

在图3.4中,当系统中有大小为两个页面块的请求发出时,第一个4页面大小的内存块(从页面框号4开始)将分成两个2页面大小的块。前一个,从页面框号4开始的,将分配出去返回给请求者,而后一个,从页面框号6开始,将被添加到free_area数组中表示两个页面大小的空闲块的元素1中。 

3.4.2  页面回收
将大的页面块打碎进行分配将增加系统中零碎空闲页面块的数目。页面回收代码在适当时机下要将这些页面结合起来形成单一大页面块。事实上页面块大小决定了页面重新组合的难易程度。 

当页面块被释放时,代码将检查是否有相同大小的相邻或者buddy内存块存在。如果有,则将它们结合起来形成一个大小为原来两倍的新空闲块。每次结合完之后,代码还要检查是否可以继续合并成更大的页面。最佳情况是系统的空闲页面块将和允许分配的最大内存一样大。 

在图3.4中,如果释放页面框号1,它将和空闲页面框号0结合作为大小为2个页面的空闲块排入free_area的第一个元素中。 

3.5  内存映射
映象执行时,可执行映象的内容将被调入进程虚拟地址空间中。可执行映象使用的共享库同样如此。然而可执行文件实际上并没有调入物理内存,而是仅仅连接到进程的虚拟内存。当程序的其他部分运行时引用到这部分时才把它们从磁盘上调入内存。将映象连接到进程虚拟地址空间的过程称为内存映射。 



图3.5 虚拟内存区域 


每个进程的虚拟内存用一个mm_struct来表示。它包含当前执行的映象(如BASH)以及指向vm_area_struct 的大量指针。每个vm_area_struct数据结构描叙了虚拟内存的起始与结束位置,进程对此内存区域的存取权限以及一组内存操作函数。这些函数都是Linux在操纵虚拟内存区域时必须用到的子程序。其中一个负责处理进程试图访问不在当前物理内存中的虚拟内存(通过页面失效)的情况。此函数叫nopage。它用在Linux试图将可执行映象的页面调入内存时。 

可执行映象映射到进程虚拟地址时将产生一组相应的vm_area_struct数据结构。每个vm_area_struct数据结构表示可执行映象的一部分:可执行代码、初始化数据(变量)、未初始化数据等等。Linux支持许多标准的虚拟内存操作函数,创建vm_area_struct数据结构时有一组相应的虚拟内存操作函数与之对应。 

3.6  请求换页
当可执行映象到进程虚拟地址空间的映射完成后,它就可以开始运行了。由于只有很少部分的映象调入内存,所以很快就会发生对不在物理内存中的虚拟内存区域的访问。当进程访问无有效页表入口的虚拟地址时,处理器将向Linux报告一个页面错误。 

页面错误带有失效发生的虚拟地址及引发失效的访存方式。Linux必须找到表示此区域的vm_area_struct结构。对vm_area_struct数据结构的搜寻速度决定了处理页面错误的效率,而所有vm_area_struct结构是通过一种AVL(Adelson-Velskii and Landis) 树结构连在一起的。如果无法找到vm_area_struct与此失效虚拟地址的对应关系,则系统认为此进程访问了非法虚拟地址。这时Linux将向进程发送SIGSEGV信号,如果进程没有此信号的处理过程则终止运行。 

如果找到此对应关系,Linux接下来检查引起该页面错误的访存类型。如果进程以非法方式访问内存,比如对不可写区域进行写操作,系统将产生内存错误的信号。 

如果Linux认为页面出错是合法的,那么它需要对这种情况进行处理。 

首先Linux必须区分位于交换文件中的页面和那些位于磁盘上的可执行映象。Alpha AXP的页表中有可能存在有效位没有设置但是在PFN域中有非0值的页表入口。在这种情况下,PFN域指示的是此页面在交换文件中的位置。如何处理交换文件中的页面将在下章讨论。 

不是所有的vm_area_struct数据结构都有一组虚拟内存操作函数,它们有的甚至没有nopage函数。这是因为 Linux通过分配新的物理页面并为其创建有效的页表入口来修正这次访问。如果这个内存区域存在nopage操作函数,Linux将调用它。 

一般Linux nopage函数被用来处理内存映射可执行映象,同时它使用页面cache将请求的页面调入物理内存中去。 

当请求的页面调入物理内存时,处理器页表也必须更新。更新这些入口必须进行相关硬件操作,特别是处理器使用TLB时。这样当页面失效被处理完毕后,进程将从发生失效虚拟内存访问的位置重新开始运行。 

3.7  Linux页面cache



图3.6 Linux页面Cache 


Linux使用页面cache的目的是加快对磁盘上文件的访问。内存映射文件以每次一页的方式读出并将这些页面存储在页面cache中。图3.6表明页面cache由page_hash_table,指向mem_map_t数据结构的指针数组组成。 

Linux中的每个文件通过一个VFS inode(在文件系统一章中讲叙)数据结构来标识并且每个VFS inode都是唯一的,它可以并仅可以描叙一个文件。页表的索引从文件的VFS inode和文件的偏移中派生出来。 

从一个内存映射文件中读出页面,例如产生换页请求时要将页面读回内存中,系统尝试从页面cache来读出。如果页面在cache中,则返回页面失效处理过程一个指向mem_map_t数据结构;否则此页面将从包含映象的文件系统中读入内存并为之分配物理页面。 

在映象的读入与执行过程中,页面cache不断增长。当不再需要某个页面时,即不再被任何进程使用时,它将被从页面cache中删除。 

3.8  换出与丢弃页面
当系统中物理内存减少时,Linux内存管理子系统必须释放物理页面。这个任务由核心交换后台进程(kswapd )来完成。 

核心交换后台进程是一种特殊的核心线程。它是没有虚拟内存的进程,在物理地址空间上以核心态运行。核心交换后台进程的名字容易使人误解,其实它完成的工作比仅仅将页面交换到系统的交换文件中要多得多。其目标是保证系统中有足够的空闲页面来维持内存管理系统运行效率。 

此进程由核心的init进程在系统启动时运行,被核心交换定时器周期性的调用。 

当定时器到时后,交换后台进程将检查系统中的空闲页面数是否太少。它使用两个变量:free_pages_high 和free_page_low来判断是否该释放一些页面。只要系统中的空闲页面数大于free_pages_high,核心交换后台进程不做任何工作;它将睡眠到下一次定时器到时。在检查中,核心交换后台进程将当前被写到交换文件中的页面数也计算在内,它使用nr_async_pages来记录这个数值;当有页面被排入准备写到交换文件队列中时,它将递增一次,同时当写入操作完成后递减一次。如果系统中的空闲页面数在free_pages_high甚至 free_pages_low以下时,核心交换后台进程将通过三个途径来减少系统中使用的物理页面的个数: 

减少缓冲与页面cache的大小,
将系统V类型的内存页面交换出去, 
换出或者丢弃页面。 
如果系统中空闲页面数低于free_pages_low,核心交换后台进程将在下次运行之前释放6个页面。否则它只释放3个。以上三种方法将依次使用直到系统释放出足够的空闲页面。当核心交换后台进程试图释放物理页面时它将记录使用的最后一种方法。下一次它会首先运行上次最后成功的算法。 
  
当释放出足够页面后,核心交换后台进程将再次睡眠到下次定时器到时。如果导致核心交换后台进程释放页面的原因是系统中的空闲页面数小于free_pages_low,则它只睡眠平时的一半时间。一旦空闲页面数大于 free_pages_low则核心交换进程的睡眠时间又会延长。 

3.8.1  减少Page Cache和Buffer Cache的大小
Page Cache和Buffer cache中的页面将被优先考虑释放到free_area数组中。Page Cache中包含的是内存映射文件的页面,其中有些可能是不必要的,它们浪费了系统的内存。而Buffer Cache中包含的是从物理设备中读写的缓冲数据,有些可能也是不必要的。当系统中物理页面开始耗尽时,从这些cache中丢弃页面比较简单(它不需要象从内存中交换一样,无须对物理设备进行写操作)。除了会使对物理设备及内存映射文件的访问速度降低外,页面丢弃策略没有太多的副作用。如果策略得当,则所有进程的损失相同。 

每次核心交换后台进程都会尝试去压缩这些cache。 

它首先检查mem_map页面数组中的页面块看是否有可以从物理内存中丢弃出去的。当系统中的空闲页面数降低 到一个危险水平时,核心后台交换进程频繁进行交换,则检查的页面块一般比较大。检查的方式为轮转,每次试图压缩内存映象时,核心后台交换进程总是检查不同的页面块。这是众所周知的clock算法,每次在整个mem_map页面数组中对页面进行检查。 

核心后台交换进程将检查每个页面看是否已经被page cache或者buffer cache缓冲。读者可能已经注意到共享页面不在被考虑丢弃的页面之列,这种页面不会同时出现在这两种cache中。如果页面不在这两者中任何一种之中时,它将检查mem_map页面数组中的下一个页面。 

缓存在buffer cache(或者页面中的缓冲被缓存)中的页面可以使缓冲分配和回收更加有效。内存压缩代码将 力图释放在受检页面中包含的缓冲区。 

如果页面中包含的所有缓冲区都被释放,这个页面也将被释放。如果受检页面在Linux的page cache中,则它会从page cache中删除并释放。 

如果释放出来了足够的页面,核心交换后台进程将等待到下一次被唤醒。这些被释放的页面都不是任何进程虚拟内存的一部分,这样无须更新页表。如果没有足够的缓冲页面丢弃则交换进程将试图将一些共享页面交换出去。 

3.8.2  换出系统V内存页面
系统V共享内存是一种用来在进程之间通过共享虚拟内存来实现进程通讯的机制。进程是如何共享内存将在IPC 一章中详细讨论。现在只需要说明系统V共享内存的任何区域都可以用一个shmid_ds数据结构来表示就足够了。 此结构包含一个指向vm_area的链表指针,vm_area是为每个共享此虚拟内存区域设计的结构。它们之间通过 vm_next_shared和vm_prev_shared指针来连接。每个shmid_ds数据结构包含一个页表入口,每个入口描叙物理页面与共享虚拟页面之间的映射关系。 

核心交换后台进程同样使用clock算法来将系统V共享内存页面交换出去。 

每次运行时,它要记得哪个共享虚拟内存区域的哪个页面是最后一个被交换出去的。两个索引可以协助它完成这项工作,其一是一组shmid_ds数据结构的索引,另一个是系统V共享内存区域的页表入口链表的索引。 这能够保证对系统V共享内存区域作出公平的选择。 

由于对于给定的系统V共享虚拟内存的物理页面框号被保存在所有共享此虚拟内存区域进程的页表中,核心 交换后台进程必须同时修改所有的页表以表示页面不再在内存而在交换文件中。对于每个要交换出去的共享 页面,核心交换后台进程可以在每个共享进程的页表中的页表入口中找到它们(通过vm_area_struct数据结 构)。如果对应此系统V共享内存的页面的进程页表入口是有效的,它可以将其转变成无效,这样换出页表入口和共享页面的用户数将减一。换出系统V共享页表入口的格式中包含一个对应于一组shmid_ds数据结构的索引以及一个对系统V共享内存区域的页表入口索引。 

如果所有共享进程的页表都被修改后此页面的记数为0则共享页面可以被写到交换文件中。同样指向此系统V共享内存区域的shmid_ds数据结构链表中的页表入口也被换出页表入口代替。换出页表入口虽然无效但是它包含一组打开的交换文件的索引,同时还能找到换出页面在文件中的偏移。当页面重新被带入物理内存时,这些信息十分有用。 

3.8.3  换出和丢弃页面
交换后台进程依次检查系统中的每个进程以确认谁最适合交换出去。 

比较好的候选者是那些可以被交换出去(有些是不可被交换出去的)并且只有一个或者几个页面在内存中的进程。只有那些包含的数据无法检索的页面才会从物理内存中交换到系统交换文件中去。 

可执行映象的许多内容都可以从映象文件中读出并且可以很容易重读出来。例如,映象中的可执行指令不能被映象本身修改,所以决不会写到交换文件中去。这些页面直接丢弃就可以。当进程再次引用它们时,只需要从可执行映象文件中读入内存即可。 

一旦确定了将要被交换出去的进程,交换后台进程将搜索其整个虚拟内存区域以找到那些没有共享或者加锁的区域。 

Linux并不会将选中的进程的整个可交换页面都交换出去,它只删除一小部分页面。 

如果内存被加锁则页面不能被交换或者丢弃。 

Linux交换算法使用页面衰老算法。每个页面有一个计数器来告诉核心交换后台进程这个页面是否值得交换出 去(此计数器包含在mem_map_t结构中)。当页面没有使用或者没有找到时将会衰老;交换后台进程仅仅交换 出那些老页面。缺省操作是:当页面被首次分配时,其年龄初始值为3,每次引用其年龄将加3,最大值为20。 每次核心交换后台进程运行它来使页面衰老-将年龄减1。这个缺省操作可以改变并且由于这个原因它们被存储在swap_control数据结构中。 

如果页面变老了(age=0),则交换后台进程将进一步来处理它。dirty页面可以被交换出去。Linux在PTE中使 用一个硬件相关位来描叙页面的这个特性(见图3.2)。然而不是所有的dirty页面都有必要写入到交换文件 中去。进程的每个虚拟内存区域可能有其自身的交换操作(由vm_area_struct结构中的vm_ops指针表示),在 交换时使用的是这些方法。否则,交换后台进程将在交换文件中分配一个页面并将页面写到设备上去。 

页面的页表入口被标志成无效但是它包含了页面在在交换文件中位置的信息,包括一个表示页面在交换文件中位置的偏移值以及使用的是哪个交换文件。但是不管使用的是哪种交换算法,以前那个物理页面将被标志成空闲并放入free_area中。Clean(或者not dirty)的页面可以丢弃同时放入free_area以备重新使用。 

如果有足够的可交换进程页面被交换出去或丢弃,则交换后台进程将再次睡眠。下次它醒来时将考虑系统中 的下一个进程。通过这种方法,交换后台进程一点一点地将每个进程的可交换或可丢弃物理页面收回知道系 统再次处于平衡状态。这比将整个进程交换出去要公平得多。 

3.9  The Swap Cache
当将页面交换到交换文件中时,Linux总是避免页面写,除非必须这样做。当页面已经被交换出内存但是当有进程再次访问时又要将它重新调入内存。只要页面在内存中没有被写过,则交换文件中的拷贝是有效的。 

Linux使用swap cache来跟踪这些页面。这个swap cache是一个页表入口链表,每个对应于系统中的物理页面。这是一个对应于交换出页面的页表入口并且描叙页面放置在哪个交换文件中以及在交换文件中的位置。 如果swap cache入口为非0值,则表示在交换文件中的这一页没有被修改。如果此页被修改(或者写入)。 则其入口从swap cache中删除。 

当Linux需要将一个物理页面交换到交换文件时,它将检查swap cache,如果对应此页面存在有效入口,则 不必将这个页面写到交换文件中。这是因为自从上次从交换文件中将其读出来,内存中的这个页面还没有被修改。 

swap cache中的入口是已换出页面的页表入口。它们虽被标记为无效但是为Linux提供了页面在哪个交换文件中以及文件中的位置等信息。 


3.10  页面的换入
保存在交换文件中的dirty页面可能被再次使用到,例如,当应用程序向包含在已交换出物理页面上的虚拟内存区域写入时。对不在物理内存中的虚拟内存页面的访问将引发页面错误。由于处理器不能将此虚拟地址转换成物理地址,处理器将通知操作系统。由于已被交换出去,此时描叙此页面的页表入口被标记成无效。处理器不能处理这种虚拟地址到物理地址的转换,所以它将控制传递给操作系统,同时通知操作系统页面错误的地址与原因。这些信息的格式以及处理器如何将控制传递给操作系统与具体硬件有关。 
处理器相关页面错误处理代码将定位描叙包含出错虚拟地址对应的虚拟内存区域的vm_area_struct数据结构。 它通过在此进程的vm_area_struct中查找包含出错虚拟地址的位置直到找到为止。这些代码与时间关系重大,进程的vm_area_struct数据结构特意安排成使查找操作时间更少。 

执行完这些处理器相关操作并且找到出错虚拟地址的有效内存区域后,页面错处理过程其余部分和前面类似。 

通用页面错处理代码为出错虚拟地址寻找页表入口。如果找到的页表入口是一个已换出页面,Linux必须将其 交换进入物理内存。已换出页面的页表入口的格式与处理器类型有关,但是所有的处理器将这些页面标记成无效并把定位此页面的必要信息放入页表入口中。Linux利用这些信息以便将页面交换进物理入内存。 

此时Linux知道出错虚拟内存地址并且拥有一个包含页面位置信息的页表入口。vm_area_struct数据结构可能包含将此虚拟内存区域交换到物理内存中的子程序:swapin。如果对此虚拟内存区域存在swapin则Linux会使用它。这是已换出系统V共享内存页面的处理过程-因为已换出系统V共享页面和普通的已换出页面有少许不同。如果没有swapin操作,这可能是Linux假定普通页面无须特殊处理。 

系统将分配物理页面并将已换出页面读入。关于页面在交换文件中位置信息从页表入口中取出。 

如果引起页面错误的访问不是写操作则页面被保留在swap cache中并且它的页表入口不再标记为可写。如果 页面随后被写入,则将产生另一个页面错误,这时页面被标记为dirty,同时其入口从swap cache中删除。 如果页面没有被写并且被要求重新换出,Linux可以免除这次写,因为页面已经存在于交换文件中。 

如果引起页面从交换文件中读出的操作是写操作,这个页面将被从swap cache中删除并且其页表入口被标记 成dirty且可写。 

Linux 核心--5.Linux进程
原著: David A Rusling 翻译: Banyan & fifa (2001-04-27 13:55:46)
第四章 进程管理

本章重点讨论Linux内核如何在系统中创建、管理以及删除进程。 

进程在操作系统中执行特定的任务。而程序是存储在磁盘上包含可执行机器指令和数据的静态实体。进程或者任务是处于活动状态的计算机程序。 

进程是一个随执行过程不断变化的实体。和程序要包含指令和数据一样,进程也包含程序计数器和所有CPU寄存器的值,同时它的堆栈中存储着如子程序参数、返回地址以及变量之类的临时数据。当前的执行程序,或者说进程,包含着当前处理器中的活动状态。Linux是一个多处理操作系统。进程具有独立的权限与职责。如果系统中某个进程崩溃,它不会影响到其余的进程。每个进程运行在其各自的虚拟地址空间中,通过核心控制下可靠的通讯机制,它们之间才能发生联系。 

进程在生命期内将使用系统中的资源。它利用系统中的CPU来执行指令,在物理内存来放置指令和数据。使用文件系统提供的功能打开并使用文件,同时直接或者间接的使用物理设备。Linux必须跟踪系统中每个进程以及资源,以便在进程间实现资源的公平分配。如果系统有一个进程独占了大部分物理内存或者CPU的使用时间,这种情况对系统中的其它进程是不公平的。 

系统中最宝贵的资源是CPU,通常系统中只有一个CPU。Linux是一个多处理操作系统,它最终的目的是:任何时刻系统中的每个CPU上都有任务执行,从而提高CPU的利用率。如果进程个数多于CPU的个数,则有些进程必须等待到CPU空闲时才可以运行。多处理是的思路很简单;当进程需要某个系统资源时它将停止执行并等待到资源可用时才继续运行。单处理系统中,如DOS,此时CPU将处于空等状态,这个时间将被浪费掉。在多处理系统中,因为可以同时存在多个进程,所以当某个进程开始等待时,操作系统将把CPU控制权拿过来并交给其它可以运行的进程。调度器负责选择适当的进程来运行,Linux使用一些调度策略以保证CPU分配的公平性。 

Linux支持多种类型的可执行文件格式,如ELF,JAVA等。由于这些进程必须使用系统共享库,所以对它们的管理要具有透明性。 


4.1  Linux进程
为了让Linux来管理系统中的进程,每个进程用一个task_struct数据结构来表示(任务与进程在Linux中可以混用)。数组task包含指向系统中所有task_struct结构的指针。 

这意味着系统中的最大进程数目受task数组大小的限制,缺省值一般为512。创建新进程时,Linux将从系统内存中分配一个task_struct结构并将其加入task数组。当前运行进程的结构用current指针来指示。 

Linux还支持实时进程。这些进程必须对外部时间作出快速反应(这就是“实时”的意思),系统将区分对待这些进程和其他进程。虽然task_struct数据结构庞大而复杂,但它可以分成一些功能组成部分: 


State 
进程在执行过程中会根据环境来改变state。Linux进程有以下状态: 
Running 
进程处于运行(它是系统的当前进程)或者准备运行状态(它在等待系统将CPU分配给它)。 
Waiting 
进程在等待一个事件或者资源。Linux将等待进程分成两类;可中断与不可中断。可中断等待进程可以被信号中断;不可中断等待进程直接在硬件条件等待,并且任何情况下都不可中断。 
Stopped 
进程被停止,通常是通过接收一个信号。正在被调试的进程可能处于停止状态。 
Zombie 
这是由于某些原因被终止的进程,但是在task数据中仍然保留task_struct结构。 它象一个已经死亡的进程。 

Scheduling Information 
调度器需要这些信息以便判定系统中哪个进程最迫切需要运行。 

Identifiers 
系统中每个进程都有进程标志。进程标志并不是task数组的索引,它仅仅是个数字。每个进程还有一个用户与组标志,它们用来控制进程对系统中文件和设备的存取权限。 

Inter-Process Communication 
Linux支持经典的Unix IPC机制,如信号、管道和信号灯以及系统V中IPC机制,包括共享内存、信号灯和消息队列。我们将在IPC一章中详细讨论Linux中IPC机制。 

Links 
Linux系统中所有进程都是相互联系的。除了初始化进程外,所有进程都有一个父进程。新进程不是被创建,而是被复制,或者从以前的进程克隆而来。每个进程对应的task_struct结构中包含有指向其父进程和兄弟进程(具有相同父进程的进程)以及子进程的指针。我们可以使用pstree 命令来观察Linux系统中运行进程间的关系: 

init(1)-+-crond(98)
        |-emacs(387)
        |-gpm(146)
        |-inetd(110)
        |-kerneld(18)
        |-kflushd(2)
        |-klogd(87)
        |-kswapd(3)
        |-login(160)---bash(192)---emacs(225)
        |-lpd(121)
        |-mingetty(161)
        |-mingetty(162)
        |-mingetty(163)
        |-mingetty(164)
        |-login(403)---bash(404)---pstree(594)
        |-sendmail(134)
        |-syslogd(78)
        `-update(166)


另外,系统中所有进程都用一个双向链表连接起来,而它们的根是init进程的task_struct数据结构。这 个链表被Linux核心用来寻找系统中所有进程,它对ps或者kill命令提供了支持。 

Times and Timers 
核心需要记录进程的创建时间以及在其生命期中消耗的CPU时间。时钟每跳动一次,核心就要更新保存在jiffies变量中,记录进程在系统和用户模式下消耗的时间量。Linux支持与进程相关的interval定时器,进程可以通过系统调用来设定定时器以便在定时器到时后向它发送信号。这些定时器可以是一次性的或者周期性的。 

File system 
进程可以自由地打开或关闭文件,进程的task_struct结构中包含一个指向每个打开文件描叙符的指针以及指向两个VFS inode的指针。每个VFS inode唯一地标记文件中的一个目录或者文件,同时还对底层文件系统提供统一的接口。Linux对文件系统的支持将在filesystem一章中详细描叙。这两个指针,一个指向进程的根目录,另一个指向其当前或者pwd目录。pwd从Unix命令pwd中派生出来, 用来显示当前工作目录。这两个VFS inode包含一个count域,当多个进程引用它们时,它的值将增加。这就是为什么你不能删除进程当前目录,或者其子目录的原因。 

Virtual memory 
多数进程都有一些虚拟内存(核心线程和后台进程没有),Linux核心必须跟踪虚拟内存与系统物理内存的映射关系。 

Processor Specific Context 
进程可以认为是系统当前状态的总和。进程运行时,它将使用处理器的寄存器以及堆栈等等。进程被挂起时,进程的上下文-所有的CPU相关的状态必须保存在它的task_struct结构中。当调度器重新调度该进程时,所有上下文被重新设定。 

4.2  Identifiers
和其他Unix一样,Linux使用用户和组标志符来检查对系统中文件和可执行映象的访问权限。Linux系统中所有的文件都有所有者和允许的权限,这些权限描叙了系统使用者对文件或者目录的使用权。基本的权限是读、写和可执行,这些权限被分配给三类用户:文件的所有者,属于相同组的进程以及系统中所有进程。每类用户具有不同的权限,例如一个文件允许其拥有者读写,但是同组的只能读而其他进程不允许访问。 

Linux使用组将文件和目录的访问特权授予一组用户,而不是单个用户或者系统中所有进程。如可以为某个软件项目中的所有用户创建一个组,并将其权限设置成只有他们才允许读写项目中的源代码。一个进程可以同时属于多个组(最多为32个),这些组都被放在进程的task_struct中的group数组中。只要某组进程可以存取某个文件,则由此组派生出的进程对这个文件有相应的组访问权限。 

task_struct结构中有四对进程和组标志符: 


uid, gid 
表示运行进程的用户标志符和组标志符。 
effective uid and gid 
有些程序可以在执行过程中将执行进程的uid和gid改成其程序自身的uid和gid(保存在描叙可执行映象的VFS inode属性中)。这些程序被称为setuid程序,常在严格控制对某些服务的访问时使用,特别是那些为别的进程而运行的进程,例如网络后台进程。有效uid和gid是那些setuid执行过程在执行时变化出的uid 和gid。当进程试图访问特权数据或代码时,核心将检查进程的有效gid和uid。 
file system uid and gid 
它们和有效uid和gid相似但用来检验进程的文件系统访问权限。如运行在用户模式下的NFS服务器存取文件时,NFS文件系统将使用这些标志符。此例中只有文件系统uid和gid发生了改变(而非有效uid和gid)。这样可以避免恶意用户向NFS服务器发送KILL信号。 
saved uid and gid 
POSIX标准中要求实现这两个标志符,它们被那些通过系统调用改变进程uid和gid的程序使用。当进程的原始uid和gid变化时,它们被用来保存真正的uid和gid。 

4.3  调度
所有进程部分时间运行于用户模式,部分时间运行于系统模式。如何支持这些模式,底层硬件的实现各不相同,但是存在一种安全机制可以使它们在用户模式和系统模式之间来回切换。用户模式的权限比系统模式下的小得多。进程通过系统调用切换到系统模式继续执行。此时核心为进程而执行。在Linux中,进程不能被抢占。只要能够运行它们就不能被停止。当进程必须等待某个系统事件时,它才决定释放出CPU。例如进程可能需要从文件中读出字符。一般等待发生在系统调用过程中,此时进程处于系统模式;处于等待状态的进程将被挂起而其他的进程被调度管理器选出来执行。 

进程常因为执行系统调用而需要等待。由于处于等待状态的进程还可能占用CPU时间,所以Linux采用了预加载调度策略。在此策略中,每个进程只允许运行很短的时间:200毫秒,当这个时间用完之后,系统将选择另一个进程来运行,原来的进程必须等待一段时间以继续运行。这段时间称为时间片。 

调度器必须选择最迫切需要运行而且可以执行的进程来执行。 

可运行进程是一个只等待CPU资源的进程。Linux使用基于优先级的简单调度算法来选择下一个运行进程。当选定新进程后,系统必须将当前进程的状态,处理器中的寄存器以及上下文状态保存到task_struct结构中。同时它将重新设置新进程的状态并将系统控制权交给此进程。为了将CPU时间合理的分配给系统中每个可执行进程,调度管理器必须将这些时间信息也保存在task_struct中。 

policy 
应用到进程上的调度策略。系统中存在两类Linux进程:普通与实时进程。实时进程的优先级要高于其它进程。如果一个实时进程处于可执行状态,它将先得到执行。实时进程又有两种策略:时间片轮转和先进先出。在时间片轮转策略中,每个可执行实时进程轮流执行一个时间片,而先进先出策略每个可执行进程按各自在运行队列中的顺序执行并且顺序不能变化。 
priority 
调度管理器分配给进程的优先级。同时也是进程允许运行的时间(jiffies)。系统调用renice可以改变进程的优先级。 
rt_priority 
Linux支持实时进程,且它们的优先级要高于非实时进程。调度器使用这个域给每个实时进程一个相对优先级。同样可以通过系统调用来改变实时进程的优先级。 
counter 
进程允许运行的时间(保存在jiffies中)。进程首次运行时为进程优先级的数值,它随时间变化递减。 
核心在几个位置调用调度管理器。如当前进程被放入等待队列后运行或者系统调用结束时,以及从系统模式返回用户模式时。此时系统时钟将当前进程的counter值设为0以驱动调度管理器。每次调度管理器运行时将进行下列操作: 


kernel work 
调度管理器运行底层处理程序并处理调度任务队列。kernel一章将详细描叙这个轻量级核心线程。 
Current process 
当选定其他进程运行之前必须对当前进程进行一些处理。 
如果当前进程的调度策略是时间片轮转,则它被放回到运行队列。 

  
如果任务可中断且从上次被调度后接收到了一个信号,则它的状态变为Running。 

  
如果当前进程超时,则它的状态变为Running。 

  
如果当前进程的状态是Running,则状态保持不变。 那些既不处于Running状态又不是可中断的进程将会从运行队列中删除。这意味着调度管理器选择运行进程时不会将这些进程考虑在内。 

 

Process selection 
调度器在运行队列中选择一个最迫切需要运行的进程。如果运行队列中存在实时进程(那些具有实时调度策略的进程),则它们比普通进程更多的优先级权值。普通进程的权值是它的counter值,而实时 进程则是counter加上1000。这表明如果系统中存在可运行的实时进程,它们将总是在任何普通进程之前运行。如果系统中存在和当前进程相同优先级的其它进程,这时当前运行进程已经用掉了一些时间片,所以它将处在不利形势(其counter已经变小);而原来优先级与它相同的进程的counter值显然比它大,这样位于运行队列中最前面的进程将开始执行而当前进程被放回到运行队列中。在存在多个相同优先级进程的平衡系统中,每个进程被依次执行,这就是Round Robin策略。然而由于进程经常需要等待某些资源,所以它们的运行顺序也常发变化。 
Swap processes 
如果系统选择其他进程运行,则必须被挂起当前进程且开始执行新进程。进程执行时将使用寄存器、物理内存以及CPU。每次调用子程序时,它将参数放在寄存器中并把返回地址放置在堆栈中,所以调度管理器总是运行在当前进程的上下文。虽然可能在特权模式或者核心模式中,但是仍然处于当前运行进程中。当挂起进程的执行时,系统的机器状态,包括程序计数器(PC)和全部的处理器寄存器,必须存储在进程的task_struct数据结构中。同时加载新进程的机器状态。这个过程与系统类型相关,不同的CPU使用不同的方法完成这个工作,通常这个操作需要硬件辅助完成。 
 

进程的切换发生在调度管理器运行之后。以前进程保存的上下文与当前进程加载时的上下文相同,包括进程程序计数器和寄存器内容。 
  
如果以前或者当前进程使用了虚拟内存,则系统必须更新其页表入口,这与具体体系结构有关。如果处理器使用了转换旁视缓冲或者缓冲了页表入口(如Alpha AXP),那么必须冲刷以前运行进程的页表入口。 

4.3.1  多处理器系统中的调度
在Linux世界中,多CPU系统非常少见。但是Linux上已经做了很多工作来保证它能运行在SMP(对称多处理)机器上。Linux能够在系统中的CPU间进行合理的负载平衡调度。这里的负载平衡工作比调度管理器所做的更加明显。 

在多处理器系统中,人们希望每个处理器总处与工作状态。当处理器上的当前进程用完它的时间片或者等待系统资源时,各个处理器将独立运行调度管理器。SMP系统中一个值得注意的问题是系统中不止一个idle进程。在单处理器系统中,idle进程是task数组中的第一个任务,在SMP系统中每个CPU有一个idle进程,同时每个CPU都有一个当前进程,SMP系统必须跟踪每个处理器中的idle进程和当前进程。 

在SMP系统中,每个进程的task_struct结构中包含着当前运行它的处理器的编号以及上次运行时处理器的编号。 把进程每次都调度到不同CPU上执行显然毫无意义,Linux可以使用processor_mask来使得某个进程只在一个或者几个处理器上运行:如果N位置位,则进程可在处理器N上运行。当调度管理器选择新进程运行时,它 不会考虑一个在其processor_mask中在当前处理器位没有置位的进程。同时调度管理器将给予上次在此处理器中运行的进程一些优先权,因为将进程迁移到另外处理器上运行将带来性能的损失。 


4.4  文件



图4.1 进程所使用的文件 


图4.1给出了两个描叙系统中每个进程所使用的文件系统相关信息。第一个fs_struct包含了指向进程的VFS inode和其屏蔽码。这个屏蔽码值是创建新文件时所使用的缺省值,可以通过系统调用来改变。 

第二个数据结构files_struct包含了进程当前所使用的所有文件的信息。程序从标准输入中读取并写入到标准输出中去。任何错误信息将输出到标准错误输出。这些文件有些可能是真正的文件,有的则是输出/输入终端或者物理设备,但程序都将它们视为文件。每个文件有一个描叙符,files_struct最多可以包含256个文件数据结构,它们分别描叙一个被当前进程使用的文件。f_mode域表示文件将以何种模式创建:只读 、读写还是只写。f_pos中包含下一次文件读写操作开始位置。f_inode指向描叙此文件的VFS inode, f_ops指向一组可以对此文件进行操作的函数入口地址指针数组。这些抽象接口十分强大,它们使得Linux 能够支持多种文件类型。在Linux中,管道是用我们下面要讨论的机制实现的。 

每当打开一个文件时,位于files_struct中的一个空闲文件指针将被用来指向这个新的文件结构。Linux进 程希望在进程启动时至少有三个文件描叙符被打开,它们是标准输入,标准输出和标准错误输出,一般进程 会从父进程中继承它们。这些描叙符用来索引进程的fd数组,所以标准输入,标准输出和标准错误输出分别 对应文件描叙符0,1和2。每次对文件的存取都要通过文件数据结构中的文件操作子程序和VFS inode一起来完成, 


4.5  虚拟内存
进程的虚拟内存包括可执行代码和多个资源数据。首先加载的是程序映象,例如ls。ls和所有可执行映象一样,是由可执行代码和数据组成的。此映象文件包含所有加载可执行代码所需的信息,同时还将程序数据连接进入进程的虚拟内存空间。然后在执行过程中,进程定位可以使用的虚拟内存,以包含正在读取的文件内容。新分配的虚拟内存必须连接到进程已存在的虚拟内存中才能够使用。 最后Linux进程调用通用库过程,比如文件处理子程序。如果每个进程都有库过程的拷贝,那么共享就变得没有意义。而Linux可以使多个进程同时使用共享库。来自共享库的代码和数据必须连接进入进程的虚拟地址空间以及共享此库的其它进程的虚拟地址空间。 

任何时候进程都不同时使用包含在其虚拟内存中的所有代码和数据。虽然它可以加载在特定情况下使用的那些代码,如初始化或者处理特殊事件时,另外它也使用了共享库的部分子程序。但如果将这些没有或很少使用的代码和数据全部加载到物理内存中引起极大的浪费。如果系统中多个进程都浪费这么多资源,则会大大降低的系统效率。Linux使用请求调页技术来把那些进程需要访问的虚拟内存带入物理内存中。核心将进程页表中这些虚拟地址标记成存在但不在内存中的状态,而无需将所有代码和数据直接调入物理内存。当进程试图访问这些代码和数据时,系统硬件将产生页面错误并将控制转移到Linux核心来处理之。这样对于处理器地址空间中的每个虚拟内存区域,Linux都必须知道这些虚拟内存从何处而来以及如何将其载入内存以处理页面错误。 






图4.2 进程的虚拟内存 

Linux核心需要管理所有的虚拟内存地址,每个进程虚拟内存中的内容在其task_struct结构中指向的 vm_area_struct结构中描叙。进程的mm_struct数据结构也包含了已加载可执行映象的信息和指向进程页表 的指针。它还包含了一个指向vm_area_struct链表的指针,每个指针代表进程内的一个虚拟内存区域。 

此链表按虚拟内存位置来排列,图4.2给出了一个简单进程的虚拟内存以及管理它的核心数据结构分布图。 由于那些虚拟内存区域来源各不相同,Linux使用vm_area_struct中指向一组虚拟内存处理过程的指针来抽 象此接口。通过使用这个策略,所有的进程虚拟地址可以用相同的方式处理而无需了解底层对于内存管理的区别。如当进程试图访问不存在内存区域时,系统只需要调用页面错误处理过程即可。 

为进程创建新虚拟内存区域或处理页面不在物理内存错误时,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 结构来描叙此虚拟内存,此结构被连接到进程的虚拟内存链表中。当进程试图对新分配的虚拟内存进行写操作时,系统将产生页面错。处理器会尝试解析此虚拟地址,但是如果找不到对应此虚拟地址的页表入口时,处理器将放弃解析并产生页面错误异常,由Linux核心来处理。Linux则查看此虚拟地址是否在当前进程的虚拟地址空间中。如果是Linux会创建正确的PTE并为此进程分配物理页面。包含在此页面中的代码或数据可能需要从文件系统或者交换磁盘上读出。然后进程将从页面错误处开始继续执行,由于物理内存已经存在,所以不会再产生页面异常。 


4.6  进程创建
系统启动时总是处于核心模式,此时只有一个进程:初始化进程。象所有进程一样,初始化进程也有一个由堆栈、寄存器等表示的机器状态。当系统中有其它进程被创建并运行时,这些信息将被存储在初始化进程的task_struct结构中。在系统初始化的最后,初始化进程启动一个核心线程(init)然后保留在idle状态。 如果没有任何事要做,调度管理器将运行idle进程。idle进程是唯一不是动态分配task_struct的进程,它的 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中。 

复制完成后,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"。当进程之一试图对虚拟内存进行写操作时将产生页面错误。这时Linux将拷贝这一块内存并修改两个进程的页表以及虚拟内存数据结构。 


4.7  时钟和定时器
核心跟踪着进程的创建时间以及在其生命期中消耗的CPU时间。每个时钟滴答时,核心将更新当前进程在系统 模式与用户模式下所消耗的时间(记录在jiffies中)。 
除了以上记时器外,Linux还支持几种进程相关的时间间隔定时器。 

进程可以使用这些定时器在到时时向它发送各种信号,这些定时器如下: 


Real 
此定时器按照实时时钟记数,当时钟到期时,向进程发送SIGALRM信号。 
Virtual 
此定时器仅在进程运行时记数,时钟到期时将发送SIGVTALRM信号。 
Profile 
此定时器在进程运行和核心为其运行时都记数。当到时时向进程发送SIGPROF信号。 
以上时间间隔定时器可以同时也可以单独运行,Linux将所有这些信息存储在进程的task_struct数据结构中。通过系统调用可以设置这些时间间隔定时器并启动、终止它们或读取它们的当前值。Virtual和Profile定时器以相同方式处理。 

每次时钟滴答后当前进程的时间间隔定时器将递减,当到时之后将发送适当的信号。 

Real时钟间隔定时器的机制有些不同,这些将在kernel一章中详细讨论。每个进程有其自身的timer_list数 据结构,当时间间隔定时器运行时,它们被排入系统的定时器链表中。当定时器到期后,底层处理过程将把它从队列中删除并调用时间间隔处理过程。此过程将向进程发送SIGALRM信号并重新启动定时器,将其重新放入系统时钟队列。 

  

4.8  程序执行
象Unix一样,Linux程序通过命令解释器来执行。命令解释器是一个用户进程,人们将其称为shell程序。 

在Linux中有多个shell程序,最流行的几个是sh、bash和tcsh。除了几个内置命令如cd和pwd外,命令都是 一个可执行二进制文件。当键入一个命令时,Shell程序将搜索包含在进程PATH环境变量中查找路径中的目 录来定位这个可执行映象文件。如果找到,则它被加载且执行。shell使用上面描叙的fork机制来复制自身 然后用找到的二进制可执行映象的内容来代替其子进程。一般情况下,shell将等待此命令的完成或者子进 程的退出。你可以通过按下control-Z键使子进程切换到后台而shell再次开始运行。同时还可以使用shell 命令bg将命令放入后台执行,shell将发送SIGCONT信号以重新启动进程直到进程需要进行终端输出和输入。 

可执行文件可以有许多格式,甚至是一个脚本文件。脚本文件需要恰当的命令解释器来处理它们;例如 /bin/sh解释shell脚本。可执行目标文件包含可执行代码和数据,这样操作系统可以获得足够的信息将其 加载到内存并执行之。Linux最常用的目标文件是ELF,但是理论上Linux可以灵活地处理几乎所有目标文件 格式。 




图4.3 已注册的二进制格式 


通过使用文件系统,Linux所支持的二进制格式既可以构造到核心又可以作为模块加载。核心保存着一个可以支持的二进制格式的链表(见图4.3),同时当执行一个文件时,各种二进制格式被依次尝试。 

Linux上支持最广的格式是a.out和ELF。执行文件并不需要全部读入内存,而使用一种请求加载技术。进程 使用的可执行映象的每一部分被调入内存而没用的部分将从内存中丢弃。 


4.8.1  ELF
ELF(可执行与可连接格式)是Unix系统实验室设计的一种目标文件格式,现在已成为Linux中使用最多的格式。但与其它目标文件格式相比,如ECOFF和a.out,ELF的开销稍大,它的优点是更加灵活。ELF可执行文件 中包含可执行代码,即正文段:text和数据段:data。位于可执行映象中的表描叙了程序应如何放入进程的 虚拟地址空间中。静态连接映象是通过连接器ld得到,在单个映象中包含所有运行此映象所需代码和数据。 此映象同时也定义了映象的内存分布和首先被执行的代码的地址。 






图4.4 ELF可执行文件格式 

图4.4给出了一个静态连接的ELF可执行映象的构成。 

这是一个打印"Hello World"并退出的简单C程序。文件头将其作为一个带两个物理文件头(e_phnum = 2)的ELF映象来描叙,物理文件头位于映象文件起始位置52字节处。第一个物理文件头描叙的是映象中的可执行代码。它从虚拟地址0x8048000开始,长度为65532字节。这是因为它包含了printf()函数库代码以输出"Hello World"的静态连接映象。映象的入口点,即程序的第一条指令,不是位于映象的起始位置 而在虚拟地址0x8048090(e_entry)处。代码正好接着第二个物理文件头。这个物理文件头描叙了此程序使用的数据,? 患釉氐叫槟饽诖嬷?x8 的大小在文件中是2200字节(p_filesz)但是在内存中的大小为4248字节。这是因为开始的2200字节包含的是预先初始化数据而接下来的2048字节包含的是被执行代码初始化的数据。 

当Linux将一个ELF可执行映象加载到进程的虚拟地址空间时,它并不真的加载映象。首先它建立其虚拟内存数据结构:进程的vm_area_struct树和页表。当程序执行时将产生页? bf9 娲恚鸪绦虼牒褪荽游锢砟诖嬷腥〕觥3绦蛑忻挥惺褂玫降牟糠执永炊疾换峒釉氐侥诖嬷腥ァR坏〦LF二进制格式加载器发现这个映象是有效的ELF可执行映象,它将把进程的当前可执行映象从虚拟内存冲刷出去。当进程是一个复制映象时(所有的进程都是),父进程执行的是老的映象程序,例如象bash这样的命令解释器。同时还将清除任何信号处理过程并且关闭打开的文件,在冲刷的最后,进程已经为新的可执行映象作好了准备。不管可执行映象是哪种格式,进程的mm_struct结构中将存入相同信息,它们是指向映象代码和数据的指针。当ELF可执行映象从文件中读出且相关程序代码被映射到进程虚拟地址空间后,这些指针的值都被确定下来。同时vm_area_struct也被建立起来,进程的页表也被修改。mm_struct结构中还包含传递给程序和进程环境变量的参数的指针。 


ELF 共享库
另一方面,动态连接映象并不包含全部运行所需要的代码和数据。其中的一部分仅在运行时才连接到共享库中。ELF共享库列表还在运行时连接到共享库时被动态连接器使用。Linux使用几个动态连接器,如ld.so.1,libc.so.1和ld-linux.so.1,这些都放置在/lib目录中。这些库中包含常用代码,如C语言子程序等。如果没有动态连接,所有程序将不得不将所有库过程拷贝一份并连接进来,这样将需要更多的磁盘与虚拟内存空间。通过动态连接,每个被引用库过程的信息都可以包含在ELF映象列表中。这些信息用来帮助动态连接器定位库过程并将它连入程序的地址空间。 

  


4.8.2  脚本文件
脚本文件的运行需要解释器的帮助。Linux中有许许多多的解释器;例如wish、perl以及命令外壳程序tcsh。 Linux使用标准的Unix规则,在脚本文件的第一行包含了脚本解释器的名字。典型的脚本文件的开头如下: 

#!/usr/bin/wish 

此时命令解释器会试着寻找脚本解释器。 然后它打开此脚本解释器的执行文件得到一个指向此文件的VFS inode并使此脚本解释器开始解释此脚本。这时脚本文件名变成了脚本解释器的0号参数(第一个参数)并且其余参数向上挪一个位置(原来的第一个参数变成第二个)。脚本解释器的加载过程与其他可执行文件相同。Linux会逐个尝试各种二进制可执行格式直到它可以执行。 

Linux 核心--6.进程间通讯机制
原著: David A Rusling 翻译: Banyan & fifa (2001-04-27 13:56:30)
第五章 进程间通讯机制

进程在核心的协调下进行相互间的通讯。Linux支持大量进程间通讯(IPC)机制。除了信号和管道外,Linux 还支持Unix系统V中的IPC机制。 


5.1  信号
信号是Unix系统中的最古老的进程间通讯方式。它们用来向一个或多个进程发送异步事件信号。信号可以从键盘中断中产生,另外进程对虚拟内存的非法存取等系统错误环境下也会有信号产生。信号还被shell程序用来向其子进程发送任务控制命令。 
系统中有一组被详细定义的信号类型,这些信号可以由核心或者系统中其它具有适当权限的进程产生。使用kill命令(kill -l)可以列出系统中所有已经定义的信号。在我的系统(Intel系统)上运行结果如下: 

 1) SIGHUP  2) SIGINT  3) SIGQUIT  4) SIGILL
 5) SIGTRAP  6) SIGIOT  7) SIGBUS  8) SIGFPE
 9) SIGKILL 10) SIGUSR1 11) SIGSEGV 12) SIGUSR2
13) SIGPIPE 14) SIGALRM 15) SIGTERM 17) SIGCHLD
18) SIGCONT 19) SIGSTOP 20) SIGTSTP 21) SIGTTIN
22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO
30) SIGPWR

当我在Alpha AXP中运行此命令时,得到了不同的信号个数。除了两个信号外,进程可以忽略这些信号中的绝大部分。其一是引起进程终止执行的SIGSTOP信号,另一个是引起进程退出的SIGKILL信号。 至于其它信号,进程可以选择处理它们的具体方式。进程可以阻塞信号,如若不阻塞,则可以在自行处理此信号和将其转交核心处理之间作出选择。如果由核心来处理此信号,它将使用对应此信号的缺省处理方法。 比如当进程接收到SIGFPE(浮点数异常)时,核心的缺省操作是引起core dump和进程的退出。信号没有固有的相对优先级。如果在同一时刻对于一个进程产生了两个信号,则它们将可能以任意顺序到达进程并进行处理。同时Linux并不提供处理多个相同类型信号的方式。即进程无法区分它是收到了1个还是42个SIGCONT信号。 

Linux通过存储在进程task_struct中的信息来实现信号。信号个数受到处理器字长的限制。32位字长的处理器最多可以有32个信号而64位处理器如Alpha AXP可以有最多64个信号。当前未处理的信号保存在signal域中,并带有保存在blocked中的被阻塞信号的屏蔽码。除了SIGSTOP和SIGKILL外,所有的信号都能被阻塞。当产生可阻塞信号时,此信号可以保持一直处于待处理状态直到阻塞释放。Linux保存着每个进程处理每个可能信号的信息,它们保存在每个进程task_struct中的sigaction数组中。这些信息包括进程希望处理的信号所对应的过程地址,或者指示是忽略信号还是由核心来处理它的标记。通过系统调用,进程可以修改缺省的信号处理过程,这将改变某个信号的sigaction以及阻塞屏蔽码。 

并不是系统中每个进程都可以向所有其它进程发送信号:只有核心和超级用户具有此权限。普通进程只能向具有相同uid和gid的进程或者在同一进程组中的进程发送信号。信号是通过设置task_struct结构中signal域里的某一位来产生的。如果进程没有阻塞信号并且处于可中断的等待状态,则可以将其状态改成Running,同时如确认进程还处在运行队列中,就可以通过信号唤醒它。这样系统下次发生调度时,调度管理器将选择它运行。如果进程需要缺省的信号处理过程,则Linux可以优化对此信号的处理。例如SIGWINCH(X窗口的焦点改变)信号,其缺省处理过程是什么也不做。 

信号并非一产生就立刻交给进程,而是必须等待到进程再次运行时才交给进程。每次进程从系统调用中退出前,它都会检查signal和blocked域,看是否有可以立刻发送的非阻塞信号。这看起来非常不可靠,但是系统中每个进程都在不停地进行系统调用,如向终端输出字符。当然进程可以选择去等待信号,此时进程将一直处于可中断状态直到信号出现。对当前不可阻塞信号的处理代码放置在sigaction结构中。 

如果信号的处理过程被设置成缺省则由核心来应付它。SIGSTOP信号的缺省处理过程是将当前进程的状态改变成为Stopped并运行调度管理器以选择一个新进程继续运行。SIGFPE的缺省处理过程则是引起core dump并使进程退出。当然,进程可以定义其自身的信号处理过程。一旦信号产生,这个过程就将被调用。它的地址存储在sigaction结构中。核心必须调用进程的信号处理例程,具体如何去做依赖于处理器类型,但是所有的CPU 必须处理这个问题:如果信号产生时,当前进程正在核心模式下运行并且马上要返回调用核心或者系统例程的进程,而该进程处在用户模式下。解决这个问题需要操纵进程的堆栈及寄存器。进程的程序计数器被设置成其信号处理过程的地址,而参数通过调用框架或者寄存器传递到处理例程中。当进程继续执行时,信号处理例程好象普通的函数调用一样。 

Linux是POSIX兼容的,所以当某个特定信号处理例程被调用时,进程可以设定哪个信号可以阻塞。这意味着可以在进程信号处理过程中改变blocked屏蔽码。当信号处理例程结束时,此blocked屏蔽码必须设置成原有值。 因此,Linux添加了一个过程调用来进行整理工作,通过它来重新设置被发送信号进程调用栈中的原有blocked屏蔽码。 对于同一时刻几个信号处理过程,Linux通过堆栈方式来优化其使用,每当一个处理过程退出时,下一个处理过程必须等到整理例程结束后才执行。 


5.2  管道
一般的Linux shell程序都允许重定向。如 

$ ls | pr | lpr


在这个管道应用中,ls列当前目录的输出被作为标准输入送到pr程序中,而pr的输出又被作为标准输入送到lpr程序中。管道是单向的字节流,它将某个进程的标准输出连接到另外进程的标准输入。但是使用管道的进程都不会意识到重定向的存在,并且其执行结果也不会有什么不同。shell程序负责在进程间建立临时的管道。 






图5.1 管道 

在Linux中,管道是通过指向同一个临时VFS inode的两个file数据结构来实现的,此VFS inode指向内存中的一个物理页面。图5.1中每个file数据结构指向不同的文件操作例程向量,一个是实现对管道的写,另一个从管道中读。 

这样就隐藏了读写管道和读写普通的文件时系统调用的差别。当写入进程对管道写时,字节被拷贝到共享数据页面中,当读取进程从管道中读时,字节从共享数据页面中拷贝出来。Linux必须同步对管道的访问。它必须保证读者和写者以确定的步骤执行,为此需要使用锁、等待队列和信号等同步机制。 

当写者想对管道写入时,它使用标准的写库函数。表示打开文件和打开管道的描叙符用来对进程的file数据 结构集合进行索引。Linux系统调用使用由管道file数据结构指向的write过程。这个write过程用保存在表示管道的VFS inode中的信息来管理写请求。 

如果没有足够的空间容纳对所有写入管道的数据,只要管道没有被读者加锁。则Linux为写者加锁,并把从写入进程地址空间中写入的字节拷贝到共享数据页面中去。如果管道被读者加锁或者没有足够空间存储数据,当前进程将在管道inode的等待队列中睡眠,同时调度管理器开始执行以选择其它进程来执行。如果写入进程是可中断的,则当有足够的空间或者管道被解锁时,它将被读者唤醒。当数据被写入时,管道的VFS inode被解锁,同时任何在此inode的等待队列上睡眠的读者进程都将被唤醒。 

从管道中读出数据的过程和写入类似。 

进程允许进行非阻塞读(这依赖于它们打开文件或者管道的方式),此时如果没有数据可读或者管道被加锁, 则返回错误信息表明进程可以继续执行。阻塞方式则使读者进程在管道inode的等待队列上睡眠直到写者 进程结束。当两个进程对管道的使用结束时,管道inode和共享数据页面将同时被遗弃。 

Linux还支持命名管道(named pipe),也就是FIFO管道,因为它总是按照先进先出的原则工作。第一个被写入 的数据将首先从管道中读出来。和其它管道不一样,FIFO管道不是临时对象,它们是文件系统中的实体并且 可以通过mkfifo命令来创建。进程只要拥有适当的权限就可以自由使用FIFO管道。打开FIFO管道的方式稍有不同。其它管道需要先创建(它的两个file数据结构,VFS inode和共享数据页面)而FIFO管道已经存在,只需要由使用者打开与关闭。在写者进程打开它之前,Linux必须让读者进程先打开此FIFO管道;任何读者进程从中读取之前必须有写者进程向其写入数据。FIFO管道的使用方法与普通管道基本相同,同时它们使用相同数据结构和操作。 


5.3  套接口

5.3.1  系统V IPC机制
Linux支持Unix系统V(1983)版本中的三种进程间通讯机制。它们是消息队列、信号灯以及共享内存。这些系统V IPC机制使用共同的授权方法。只有通过系统调用将标志符传递给核心之后,进程才能存取这些资源。这些系统V IPC对象使用与文件系统非常类似的访问控制方式。对象的引用标志符被用来作为资源表中的索引。这个索引值需要一些处理后才能得到。 
系统中所有系统V IPC对象的Linux数据结构包含一个ipc_perm结构,它含有进程拥有者和创建者及组标志符。另外还有对此对象(拥有者,组及其它)的存取模式以及IPC对象键。此键值被用来定位系统V IPC对象的引用标志符。这样的键值一共有两组:公有与私有。如果此键为公有,则系统中任何接受权限检查的进程都可以找到系统V IPC对象的引用标志符。系统V IPC对象绝不能用一个键值来引用,而只能使用引用标志符。 


5.3.2  消息队列
消息队列允许一个或者多个进程向它写入与读取消息。Linux维护着一个msgque消息队列链表,其中每个元素 指向一个描叙消息队列的msqid_ds结构。当创建新的消息队列时,系统将从系统内存中分配一个msqid_ds结构,同时将其插入到数组中。 





图5.2 系统V IPC消息队列 

每个msqid_ds结构包含一个ipc_perm结构和指向已经进入此队列消息的指针。另外,Linux保留有关队列修改时间信息,如上次系统向队列中写入的时间等。msqid_ds包含两个等待队列:一个为队列写入进程使用而另一个由队列读取进程使用。 

每次进程试图向写入队列写入消息时,系统将把其