linux kernel
在纯技术层面上,内核是硬件与软件之间的一个中间层。其作用是将应用程序的请求传递给硬件,并充当底层驱动程序,对系统中的各种设备和组件进行寻址。
- 从应用程序的视角来看,内核可以被认为是一台增强的计算机,将计算机抽象到一个高层次上。
- 当若干程序在同一系统中并发运行时,也可以将内核视为资源管理程序。
- 另一种研究内核的视角是将内核视为库,其提供了一组面向系统的命令。
进程
每个进程都在CPU的虚拟内存中分配地址空间。各个进程的地址空间是完全独立的,因此进程并不会意识到彼此的存在。从进程的角度来看,它会认为自己是系统中唯一的进程。如果进程想要彼此通信(例如交换数据),那么必须使用特定的内核机制。
由于Linux是多任务系统,它支持(看上去)并发执行的若干进程。系统中同时真正在运行的进程数目最多不超过CPU数目,因此内核会按照短的时间间隔在不同的进程之间切换.
- 内核借助于CPU的帮助,负责进程切换的技术细节。
-
内核还必须确定如何在现存进程之间共享CPU时间。确定哪个进程运行多长时间的过程称为调度。
- init进程
Linux对进程采用了一种层次系统,每个进程都依赖于一个父进程。内核启动init程序作为第一个进程,该进程负责进一步的系统初始化操作,并显示登录提示符或图形登录界面。因此init是进程树的根,所有进程都直接或间接起源自该进程,可以用pstree
命令输出进程树。其中init是一个树型结构的顶端,而树的分支不断向下扩展。
操作系统中有两种创建新进程的机制,分别是fork和exec。
- fork
fork可以创建当前进程的一个副本,父进程和子进程只有PID(进程ID)不同。在该系统调 用执行之后,系统中有两个进程,都执行同样的操作。父进程内存的内容将被复制,至少从程序的角 度来看是这样。Linux使用了一种众所周知的技术来使fork操作更高效,该技术称为写时复制(copy on write),主要的原理是将内存复制操作延迟到父进程或子进程向某内存页面写入数据之前,在只读访 问的情况下父进程和子进程可以共用同一内存页。
- exec
exec将一个新程序加载到当前进程的内存中并执行。旧程序的内存页将刷出,其内容将替换 为新的数据。然后开始执行新程序。
进程间通信(Inter-Process Communication, IPC)
消息队列
产生消息并将其写到队列的进程通常称之为发送者,而一个或多个其他进程(逻辑上称之为接收者)则从队列获取信息。各个消息包含消息正文和一个(正)数,以便在消息队列内实现几种类型的消息。接收者可以根据该数字检索消息,例如,可以指定只接受编号1的消息,或接受编号不大于5的消息。在消息已经读取后,内核将其从队列删除。即使几个进程在同一信道上监听,每个消息仍然只能由一个进程读取。
同一编号的消息按先进先出次序处理。放置在队列开始的消息将首先读取。但如果有选择地读取 消息,则先进先出次序就不再适用。
相对于管道而言,更加适合进程之间较为频繁的交换数据。
但是消息队列也有缺点,消息队列不适合比较大的数据的传输,因为在内核中每个消息体都有一个最大长度的限制,同时所有队列所包含的全部消息体的总长度也是有上限。在 Linux 内核中,会有两个宏定义 MSGMAX 和 MSGMNB,它们以字节为单位,分别定义了一条消息的最大长度和一个队列的最大长度。
消息队列通信过程中,存在用户态与内核态之间的数据拷贝开销,因为进程写入数据到内核中的消息队列时,会发生从用户态拷贝数据到内核态的过程,同理另一进程读取内核中的消息数据时,会发生从内核态拷贝数据到用户态的过程。
共享内存
消息队列的读取和写入的过程当中,都会有发生用户态与内核态之间的消息拷贝过程。那共享内存的方式,就很好的解决了这一问题。
现代操作系统,对于内存管理,采用的是虚拟内存技术,也就是每个进程都有自己独立的虚拟内存空间,不同进程的虚拟内存映射到不同的物理内存中。所以,即使进程 A 和 进程 B 的虚拟地址是一样的,其实访问的是不同的物理内存地址,对于数据的增删查改互不影响。
共享内存的机制,就是拿出一块虚拟地址空间来,映射到相同的物理内存中。这样这个进程写入的东西,另外一个进程马上就能看到了,都不需要拷贝来拷贝去,传来传去,大大提高了进程间通信的速度。
共享内存更强大,但是也更复杂!
信号量
为了防止多进程竞争共享资源,而造成的数据错乱,所以需要保护机制,使得共享的资源,在任意时刻只能被一个进程访问。正好,信号量就实现了这一保护机制。
信号量其实是一个整型的计数器,主要用于实现进程间的互斥与同步,而不是用于缓存进程间通信的数据。
信号
信号是进程间通信唯一的异步通信机制,一旦有信号产生,我们就必须执行下列操作之一:
- 默认操作
- 捕捉
- 忽略
kill命令根据PID向进程发送信号。信号通过-s sig指定,是一个正整数,最大长度取决于处理器类型。该命令有两种最常用的变体:一种是kill不指定信号,实际上是要求进程结束(进程可以忽略该信号);另一种是kill -9,等价于在死刑批准上签字(导致某些进程死亡)。
init进程属于特例。内核会忽略发送给该进程的SIGKILL信号。因为该进程对整个系统尤其重要,不能强制结束该进程,即使无意结束也不行。
管道
shell用户可能比较熟悉管道,在命令行上可以如下使用:
$ prog | ghostscript | lpr-
这里将一个进程的输出用作另一个进程的输入,管道负责数据的传输。顾名思义,管道是用于交换数据的连接。一个进程向管道的一端供给数据,另一个在管道另一端取出数据,供进一步处理。几个进程可以通过一系列管道连接起来。 在通过shell产生管道时,总有一个读进程和一个写进程。应用程序必须调用pipe系统调用产生管道。该调用返回两个文件描述符,分别用于管道的两端,即分别用于管道的读和写。
Unix域协议
Unix域协议是进程间通信 (IPC)的一种形式,可以通过与网络通信中使用的相同Socket API来访问它们。下图的左边表示使用socket写成的客户程序和服务器程序,它们在同一台主机上利用TCP/IP协议进行通信。下图的右边表示用socket写的利用Unix域协议进行通信的客户程序和服务器程序。
线程
进程并不是内核支持的唯一一种程序执行形式。除了重量级进程(有时也称为UNIX进程)之外,还有一种形式是线程(有时也称为轻量级进程)。本质上一个进程可能由若干线程组成,这些线程共享同样的数据和资源,但可能执行程序中不同的代码路径。简而言之,进程可以看作一个正在执行的程序,而线程则是与主程序并行运行的程序函数或例程。
Linux用clone方法创建线程。其工作方式类似于fork,但启用了精确的检查,以确认哪些资源与父进程共享、哪些资源为线程独立创建。
命名空间
对命名空间的支持被集成到了许多子系统中。这使得不同的进程可以看到不同的系统视图。启用命名空间之后,以前的全局资源现在具有不同分组。每个命名空间可以包含一个特定的PID集合,或可以提供文件系统的不同视图,在某个命名空间中挂载的卷不会传播到其他命名空间中。
命名空间很有用处。举例来说,该特性对虚拟主机供应商是有益的。他们不必再为每个用户准备一台物理计算机,而是通过称为容器的命名空间来建立系统的多个视图。从容器内部看来这是一个完整的Linux系统,而且与其他容器没有交互。容器是彼此分离的。每个容器实例看起来就像是运行Linux的一台计算机,但事实上一台物理机器可以同时运转许多这样的容器实例。这有助于更有效地使用资源。与完全的虚拟化解决方案(如KVM)相比,计算机上只需要运行一个内核来管理所有的容器。 并非内核的所有部分都完全支持命名空间.
地址空间与特权级别
由于内存区域是通过指针寻址,因此CPU的字长决定了所能管理的地址空间的最大长度。对32位系统,是2的32次方字节=4GiB,对更现代的64位处理器,可以管理2的64次方个字节。
地址空间的最大长度与实际可用的物理内存数量无关,因此被称为虚拟地址空间。使用该术语的另一个理由是,从系统中每个进程的角度来看,地址空间中只有自身一个进程,而无法感知到其他进程的存在。
Linux将虚拟地址空间划分为两个部分,分别称为内核空间和用户空间,如下图所示。
系统中每个用户进程都有自身的虚拟地址范围,从0到TASK_SIZE。用户空间之上的区域保留给内核专用,用户进程不能访问。TASK_SIZE是一个特定于计算机体系结构的常数,把地址空间按给定比例划分为两部分。例如在IA-32系统中,地址空间在3 GiB处划分,因此每个进程的虚拟地址空间是3 GiB。由于虚拟地址空间的总长度是4 GiB,所以内核空间有1 GiB可用。尽管实际的数字依不同的计算机体系结构而不同,但一般概念都是相同的。
这种划分与可用的内存数量无关。由于地址空间虚拟化的结果,每个用户进程都认为自身有3 GiB内存。各个系统进程的用户空间是完全彼此分离的。而虚拟地址空间顶部的内核空间总是同样的,无论当前执行的是哪个进程。
注意,64位计算机的情况可能更复杂,因为它们在实际管理自身巨大的理论虚拟地址空间时,倾向于使用小于64的位数。实际使用的位数一般小于64位,如42位或47位。因此,地址空间中实际可寻址的部分小于理论长度。但无论如何,该值仍然大于计算机上实际可能的内存数量,因此是完全够用的。
特权级别
内核把虚拟地址空间划分为两个部分,因此能够保护各个系统进程,使之彼此隔离。所有的现代CPU都提供了几种特权级别,进程可以驻留在某一特权级别。每个特权级别都有各种限制,例如对执行某些汇编语言指令或访问虚拟地址空间某一特定部分的限制。IA-32体系结构使用4种特权级别构成的系统,各级别可以看作是环。内环能够访问更多的功能,外环则较少,如图所示。
尽管英特尔处理器区分4种特权级别,但Linux只使用两种不同的状态:核心态和用户状态。两种状态的关键差别在于对高于TASK_SIZE的内存区域的访问。简而言之,在用户状态禁止访问内核空间。用户进程不能操作或读取内核空间中的数据,也无法执行内核空间中的代码。这是内核的专用领域。这种机制可防止进程无意间修改彼此的数据而造成相互干扰。
从用户状态到核心态的切换通过系统调用的特定转换手段完成,且系统调用的执行因具体系统而不同。如果普通进程想要执行任何影响整个系统的操作(例如操作输入/输出装置),则只能借助于系统调用向内核发出请求。内核首先检查进程是否允许执行想要的操作,然后代表进程执行所需的操作,接下来返回到用户状态。
除了代表用户程序执行代码之外,内核还可以由异步硬件中断激活,然后在中断上下文中运行。与在进程上下文中运行的主要区别是,在中断上下文中运行不能访问虚拟地址空间中的用户空间部分。因为中断可能随机发生,中断发生时可能是任一用户进程处于活动状态,由于该进程基本上与中断的原因无关,因此内核无权访问当前用户空间的内容。在中断上下文中运行时,内核必须比正常情况更加谨慎,例如,不能进入睡眠状态。在编写中断处理程序时需要特别注意这些,
除了普通进程,系统中还有内核线程在运行。内核线程也不与任何特定的用户空间进程相关联, 因此也无权处理用户空间。不过在其他许多方面,内核线程更像是普通的用户层应用程序。与在中断上下文运转的内核相比,内核线程可以进入睡眠状态,也可以像系统中的普通进程一样被调度器跟踪。内核线程可用于各种用途:从内存和块设备之间的数据同步,到帮助调度器在CPU上分配进程。
在ps命令的输出中很容易识别内核线程,其名称都置于方括号内。在多处理器系统上,许多线程启动时指定了CPU,并限制只能在某个特定的CPU上运行。从内核线程名称之后的斜线和CPU编号可以看到这一点。
虚拟和物理地址空间
大多数情况下,单个虚拟地址空间就比系统中可用的物理内存要大。在每个进程都有自身的虚拟地址空间时,情况也不会有什么改善。因此内核和CPU必须考虑如何将实际可用的物理内存映射到虚拟地址空间的区域。可取的方法是用页表来为物理地址分配虚拟地址。虚拟地址关系到进程的用户空间和内核空间,而物理地址则用来寻址实际可用的内存。原理如图所示。
图中的箭头标明了虚拟地址空间中的页如何分配到物理内存页。例如,进程A的虚拟内存页1映射到物理内存页4,而进程B的虚拟内存页1映射到物理内存页5。由此可见,不同进程的同一虚拟地址实际上具有不同的含义。
物理内存页经常称作页帧。相比之下,页则专指虚拟地址空间中的页。
虚拟地址空间和物理内存之间的映射也使得进程之间的隔离有一点点松动。我们的例子即包含了一个由两个进程显式共享的页帧。进程A的页5和进程B的页1都指向物理页帧5。这种情况是可能的,因为两个虚拟地址空间中的页(虽然在不同的位置)可以映射到同一物理内存页。由于内核负责将虚拟地址空间映射到物理地址空间,因此可以决定哪些内存区域在进程之间共享,哪些不共享。
上图表明并非虚拟地址空间的所有页都映射到某个页帧。这可能是因为页没有使用,或者是数据尚不需要使用而没有载入内存中。还可能是页已经换出到硬盘,将在需要时再换回内存。
页表
用来将虚拟地址空间映射到物理地址空间的数据结构称为页表。实现两个地址空间的关联最容易的方法是使用数组,对虚拟地址空间中的每一页,都分配一个数组项。该数组项指向与之关联的页帧,但这个方法是不切实际的。
因为虚拟地址空间的大部分区域都没有使用,因而也没有关联到页帧,那么就可以使用功能相同但内存用量少得多的模型:多级分页。
Linux也采用了四级页表。
页表的一个特色在于,对虚拟地址空间中不需要的区域,不必创建中间页目录或页表。与前述使用单个数组的方法相比,多级页表节省了大量内存。
当然,该方法也有一个缺点。每次访问内存时,必须逐级访问多个数组才能将虚拟地址转换为物理地址。CPU试图用下面两种方法加速该过程。
(1) CPU中有一个专门的部分称为MMU(Memory Management Unit,内存管理单元),该单元优化了内存访问操作。
(2) 地址转换中出现最频繁的那些地址,保存到称为地址转换后备缓冲器(Translation Lookaside Buffer,TLB)的CPU高速缓存中。无需访问内存中的页表即可从高速缓存直接获得地址数据,因而大大加速了地址转换。
与CPU的交互
内核与体系结构无关的部分总是假定使用四级页表。对于只支持二级或三级页表的CPU来说,内核中体系结构相关的代码必须通过空页表对缺少的页表进行仿真。因此,内存管理代码剩余部分的实现是与CPU无关的。
内存映射
内存映射是一种重要的抽象手段。在内核中大量使用,也可以用于用户应用程序。映射方法可以将任意来源的数据传输到进程的虚拟地址空间中。作为映射目标的地址空间区域,可以像普通内存那样用通常的方法访问。但任何修改都会自动传输到原数据源。这样就可以使用相同的函数来处理完全不同的目标对象。例如,文件的内容可以映射到内存中。处理只需读取相应的内存即可访问文件内容,或向内存写入数据来修改文件的内容。内核将保证任何修改都会自动同步到文件中。
内核在实现设备驱动程序时直接使用了内存映射。外设的输入/输出可以映射到虚拟地址空间的区域中。对相关内存区域的读写会由系统重定向到设备,因而大大简化了驱动程序的实现。
物理内存的分配
在内核分配内存时,必须记录页帧的已分配或空闲状态,以免两个进程使用同样的内存区域。由于内存分配和释放非常频繁,内核还必须保证相关操作尽快完成。内核可以只分配完整的页帧。将内存划分为更小的部分的工作,则委托给用户空间中的标准库。标准库将来源于内核的页帧拆分为小的区域,并为进程分配内存。
伙伴系统
内核中很多时候要求分配连续页。为快速检测内存中的连续区域,内核采用了一种古老而历经检验的技术:伙伴系统。
系统中的空闲内存块总是两两分组,每组中的两个内存块称作伙伴。伙伴的分配可以是彼此独立的。但如果两个伙伴都是空闲的,内核会将其合并为一个更大的内存块,作为下一层次上某个内存块的伙伴。
在应用程序释放内存时,内核可以直接检查地址,来判断是否能够创建一组伙伴,并合并为一个更大的内存块放回到伙伴列表中,这刚好是内存块分裂的逆过程。这提高了较大内存块可用的可能性。
在系统长期运行时,服务器运行几个星期乃至几个月是很正常的,许多桌面系统也趋向于长期开机运行,那么会发生称为碎片的内存管理问题。频繁的分配和释放页帧可能导致一种情况:系统中有若干页帧是空闲的,但却散布在物理地址空间的各处。换句话说,系统中缺乏连续页帧组成的较大的内存块,而从性能上考虑,却又很需要使用较大的连续内存块。通过伙伴系统可以在某种程度上减少这种效应,但无法完全消除。如果在大块的连续内存中间刚好有一个页帧分配出去,很显然这两块空闲的内存是无法合并的。
slab缓存
内核本身经常需要比完整页帧小得多的内存块。由于内核无法使用标准库的函数,因而必须在伙伴系统基础上自行定义额外的内存管理层,将伙伴系统提供的页划分为更小的部分。该方法不仅可以分配内存,还为频繁使用的小对象实现了一个一般性的缓存——slab缓存。它可以用两种方法分配内存。
(1) 对频繁使用的对象,内核定义了只包含了所需类型对象实例的缓存。每次需要某种对象时,可以从对应的缓存快速分配(使用后释放到缓存)。slab缓存自动维护与伙伴系统的交互,在缓存用尽时会请求新的页帧。
(2) 对通常情况下小内存块的分配,内核针对不同大小的对象定义了一组slab缓存,可以像用户空间编程一样,用相同的函数访问这些缓存。不同之处是这些函数都增加了前缀k,表明是与内核相关联的:kmalloc和kfree。
页面交换和页面回收
页面交换通过利用磁盘空间作为扩展内存,从而增大了可用的内存。在内核需要更多内存时,不经常使用的页可以写入硬盘。如果再需要访问相关数据,内核会将相应的页切换回内存。通过缺页异常机制,这种切换操作对应用程序是透明的。换出的页可以通过特别的页表项标识。在进程试图访问此类页帧时,CPU则启动一个可以被内核截取的缺页异常。此时内核可以将硬盘上的数据切换到内存中。接下来用户进程可以恢复运行。由于进程无法感知到缺页异常,所以页的换入和换出对进程是完全不可见的。
页面回收用于将内存映射被修改的内容与底层的块设备同步,为此有时也简称为数据回写。数据刷出后,内核即可将页帧用于其他用途(类似于页面交换)。内核的数据结构包含了与此相关的所有信息,当再次需要该数据时,可根据相关信息从硬盘找到相应的数据并加载。
系统调用
系统调用是用户进程与内核交互的经典方法。POSIX标准定义了许多系统调用,以及这些系统调用在所有遵从POSIX的系统包括Linux上的语义。传统的系统调用按不同类别分组,如下所示。
可移植操作系统接口(英语:Portable Operating System Interface,缩写为POSIX)是IEEE为要在各种UNIX操作系统上运行软件,而定义API的一系列互相关联的标准的总称。此标准源于一个大约开始于1985年的项目。POSIX这个名称是由理查德·斯托曼(RMS)应IEEE的要求而提议的一个易于记忆的名称。它基本上是Portable Operating System Interface(可移植操作系统接口)的缩写,而X则表明其对Unix API的传承。
- 进程管理:创建新进程,查询信息,调试。
- 信号:发送信号,定时器以及相关处理机制。
- 文件:创建、打开和关闭文件,从文件读取和向文件写入,查询信息和状态。
- 目录和文件系统:创建、删除和重命名目录,查询信息,链接,变更目录。
- 保护机制:读取和变更UID/GID,命名空间的处理。
- 定时器函数:定时器函数和统计信息。
所有这些函数都对内核提出了要求。这些函数不能以普通的用户库形式实现,因为需要特别的保护机制来保证系统稳定性或安全不受危及。此外许多调用依赖内核内部的结构或函数来得到所需的数据或结果,这也导致了无法在用户空间实现。在发出系统调用时,处理器必须改变特权级别,从用户状态切换到核心态。
设备驱动程序、块设备和字符设备
设备驱动程序用于与系统连接的输入/输出装置通信,如硬盘、软驱、各种接口、声卡等。按照经典的UNIX箴言“万物皆文件”(everything is a file),对外设的访问可利用/dev
目录下的设备文件来完成,程序对设备的处理完全类似于常规的文件。设备驱动程序的任务在于支持应用程序经由设备文件与设备通信。换言之,使得能够按适当的方式在设备上读取/写入数据。
外设可分为以下两类。
(1) 字符设备:提供连续的数据流,应用程序可以顺序读取,通常不支持随机存取。相反,此类设备支持按字节/字符来读写数据。举例来说,调制解调器是典型的字符设备。
(2) 块设备:应用程序可以随机访问设备数据,程序可自行确定读取数据的位置。硬盘是典型的块设备,应用程序可以寻址磁盘上的任何位置,并由此读取数据。此外,数据的读写只能以块(通常是512B)的倍数进行。与字符设备不同,块设备并不支持基于字符的寻址。
编写块设备的驱动程序比字符设备要复杂得多,因为内核为提高系统性能广泛地使用了缓存机制。
网络
网卡也可以通过设备驱动程序控制,但在内核中属于特殊状况,因为网卡不能利用设备文件访问。原因在于在网络通信期间,数据打包到了各种协议层中。在接收到数据时,内核必须针对各协议层的处理,对数据进行拆包与分析,然后才能将有效数据传递给应用程序。在发送数据时,内核必须首先根据各个协议层的要求打包数据,然后才能发送。
为支持通过文件接口处理网络连接(按照应用程序的观点),Linux使用了源于BSD的套接字抽象。套接字可以看作应用程序、文件接口、内核的网络实现之间的代理。
文件系统
Linux系统由数以千计乃至百万计的文件组成,其数据存储在硬盘或其他块设备(例如ZIP驱动、软驱、光盘等)。存储使用了层次式文件系统。文件系统使用目录结构组织存储的数据,并将其他元信息(例如所有者、访问权限等)与实际数据关联起来。Linux支持许多不同的文件系统:标准的Ext2和Ext3文件系统、ReiserFS、XFS、VFAT(为兼容DOS),还有很多其他文件系统。不同文件系统所基于的概念抽象,在某种程度上可以说是南辕北辙。Ext2基于inode,即它对每个文件都构造了一个单独的管理结构,称为inode,并存储到磁盘上。inode包含了文件所有的元信息,以及指向相关数据块的指针。目录可以表示为普通文件,其数据包括了指向目录下所有文件的inode的指针,因而层次结构得以建立。相比之下,ReiserFS广泛应用了树形结构来提供同样的功能。
内核必须提供一个额外的软件层,将各种底层文件系统的具体特性与应用层(和内核自身)隔离开来。该软件层称为VFS(Virtual Filesystem或Virtual Filesystem Switch,虚拟文件系统或虚拟文件系统交换器)。VFS既是向下的接口(所有文件系统都必须实现该接口),同时也是向上的接口(用户进程通过系统调用最终能够访问文件系统功能)
虚拟文件系统
为支持各种本机文件系统,且在同时允许访问其他操作系统的文件,Linux内核在用户进程(或C标准库)和文件系统实现之间引入了一个抽象层。该抽象层称之为虚拟文件系统(Virtual File System),简称VFS。 内核支持40多种文件系统,其来源各种各样:来自MS-DOS的FAT文件系统、UFS(Berkeley UNIX)、用于CD-ROM的iso9660、网络文件系统(如coda和NFS)和虚拟的文件系统(如proc)。
文件系统类型
- 基于磁盘的文件系统(Disk-based Filesystem)是在非易失介质上存储文件的经典方法,用以 在多次会话之间保持文件的内容。
- 虚拟文件系统(Virtual Filesystem)在内核中生成,是一种使用户应用程序与用户通信的方法。 proc文件系统是这一类的最佳示例。它不需要在任何种类的硬件设备上分配存储空间。相反,内核建 立了一个层次化的文件结构,其中的项包含了与系统特定部分相关的信息。
- 网络文件系统(Network Filesystem)这种文件系统允许访问另一台计算机上的数据,该计算机通过网络连接到本地计算机.这意味着内核无需关注文件存取、数据组织和硬件通信的细节,这些由远程计算机的内核处理。对此类文件系统中文件的操作都通过网络连接进行。在进程向文件写数据时,数据使用特定的协议(由具体的网络文件系统决定)发送到远程计算机。接下来远程计算机负责存储传输的数据并通知发送者数据已经到达。 尽管如此,即使在内核处理网络文件系统时,仍然需要文件长度、文件在目录层次中的位置以及文件的其他重要信息。它必须也提供函数,使得用户进程能够执行通常的文件相关操作,如打开、读、删除等。由于VFS抽象层的存在,用户空间进程不会看到本地文件系统与网络文件系统之间的区别。
无持久存储的文件系统
统上,文件系统用于在块设备上持久存储数据。但也可以使用文件系统来组织、提供或交换并不存储在块设备上的信息,这些信息可以由内核动态生成。
proc文件系统(proc filesystem)
它使得内核可以生成与系统的状态和配置有关的信息。该信息可以由用户和系统程序从普通文件读取,而无需专门的工具与内核通信。在某些情况下,一个简单的cat命令就足够了。数据不仅可以从内核读取,还可以通过向proc文件系统的文件写入字符串,来向内核发送数据。 该方法利用了一个虚拟文件系统“即时”产生文件信息。换句话说,只有发出读操作请求时,才会生成信息。对于此类文件系统,不需要专用的硬盘分区或其他块存储设备。
- 特定于进程的数据
每个系统进程,无论当前状态如何,都有一个对应的子目录(与其PID同名),包含了该进程的有关信息。顾名思义,进程数据系统(process data system,简称proc)的初衷就是传递进程数据。特定于进程的目录保存了哪些信息?简单的一个ls-l命令,就能看到一些信息:
$ cd /proc/7748
$ ls -l
sysfs文件系统
sysfs是一个向用户空间导出内核对象的文件系统,它不仅提供了察看内核内部数据结构的能力,还可以修改这些数据结构。特别重要的是,该文件系统高度层次化的组织:sysfs的数据项来源于内核对象(kobject),而内核对象的层次化组织直接反映到了sysfs的目录布局中,由于系统的所有设备和总线都是通过kobject组织的,所以sysfs提供了系统的硬件拓扑的一种表示。 sysfs的标准装载点是/sys。
模块和热插拔
模块用于在运行时动态地向内核添加功能,如设备驱动程序、文件系统、网络协议等,实际上内核的任何子系统①几乎都可以模块化。这消除了宏内核与微内核相比一个重要的不利之处。
模块还可以在运行时从内核卸载,这在开发新的内核组件时很有用。
模块在本质上不过是普通的程序,只是在内核空间而不是用户空间执行而已。模块必须提供某些代码段②在模块初始化(和终止)时执行,以便向内核注册和注销模块。另外,模块代码与普通内核代码的权利(和义务)都是相同的,可以像编译到内核中的代码一样,访问内核中所有的函数和数据。
对支持热插拔而言,模块在本质上是必需的。某些总线(例如,USB和FireWire)允许在系统运行时连接设备,而无需系统重启。在系统检测到新设备时,通过加载对应的模块,可以将必要的驱动程序自动添加到内核中。
模块特性使得内核可以支持种类繁多的设备,而内核自身的大小却不会发生膨胀。在检测到连接的硬件后,只需要加载必要的模块,多余的驱动程序无需加入到内核。
缓存
内核使用缓存来改进系统性能。从低速的块设备读取的数据会暂时保持在内存中,即使数据在当时已经不再需要了。在应用程序下一次访问该数据时,它可以从访问速度较快的内存中读取,因而绕过了低速的块设备。由于内核是通过基于页的内存映射来实现访问块设备的,因此缓存也按页组织,也就是说整页都缓存起来,故称为页缓存(page cache)。
为什么内核是特别的
内核很神奇,但归根结底它只是一个大的C程序,带有一些汇编代码。是什么使得内核如此吸引人?原因有几个。首要一点在于,内核是由世界上最好的程序员编写的,源代码可以证实这一点。其结构良好,细节一丝不苟,巧妙的解决方案在代码中处处可见。一言以蔽之:内核应该是什么样子,它现在就是什么样子。但这并不意味着内核是应用教科书风格的程序设计方法学得出的产品。尽管内核采用了设计得非常干净的抽象,以保持代码的模块化和易管理性,但这一点与内核的其他方面混合起来,使得代码非常有趣和独特。在必要的情况下,内核会以上下文相关的方式重用比特位置,多次重载结构成员,从指针已经对齐的部分压榨出又一个存储位,自由地使用goto语句,还有很多其他东西,这些都会使任何强调结构的程序因痛苦而尖叫。