深入理解Linux内核架构

一、简介和概述
二、进程管理和调试
除了代表用户程序执行代码之外,内核还可以由异步硬件中断激活,然后在中断上下文中运行。
与在进程上下文中运行的主要区别是,在中断上下文中运行不能访问虚拟地址空间中的用户空间部
分。因为中断可能随机发生,中断发生时可能是任一用户进程处于活动状态,由于该进程基本上与中
断的原因无关,因此内核无权访问当前用户空间的内容。在中断上下文中运行时,内核必须比正常情
况更加谨慎,例如,不能进入睡眠状态。在编写中断处理程序时需要特别注意这些。
内核线程可用于各种用途:从内存和块设备之间的数据同步,到帮助调度器在CPU上分配进程。
CPU大多数时间都在执行用户空间中的代码。当应用程序执行系统调用时,则切换到核心态,内核将完成其请求。在此期间,内核可以访问虚拟地址空间的用户部分。在系统调用完成之后,CPU切换回用户状态。硬件中断也会使CPU切换到核心态,这种情况下内核不能访问用户空间。

大多数情况下,单个虚拟地址空间就比系统中可用的物理内存要大。在每个进程都有自身的虚拟
地址空间时,情况也不会有什么改善。因此内核和CPU必须考虑如何将实际可用的物理内存映射到虚拟地址空间的区域。可取的方法是用页表来为物理地址分配虚拟地址。虚拟地址关系到进程的用户空间和内核空间,而物理地址则用来寻址实际可用的内存。虚拟地址空间,都被内核划分为很多等长的部分。这些部分页。物理内存页经常称作页帧。相比之下,页则专指虚拟地址空间中的页。用来将虚拟地址空间映射到物理地址空间的数据结构称为页表。因为虚拟地址空间的大部分区域都没有使用,因而也没有关联到页帧,那么就可以使用功能相同但内存用量少得多的模型:多级分页。虚拟地址的第一部分称为全局页目录(Page Global Directory,PGD)。PGD用于索引进程中的一个数组(每个进程有且仅有一个),该数组是所谓的全局页目录或PGD。PGD的数组项指向另一些数组的起始地址,这些数组称为中间页目录(Page Middle Directory,PMD)。虚拟地址中的第二个部分称为PMD,在通过PGD中的数组项找到对应的PMD之后,则使用PMD来索引PMD。PMD的数组项也是指针,指向下一级数组,称为页表或页目录。虚拟地址的第三个部分称为PTE(Page Table Entry,页表数组),用作页表的索引。虚拟内存页和页帧之间的映射就此完成,因为页表的数组项是指向页帧的。虚拟地址最后的一部分称为偏移量。它指定内部的一个字节位置。归根结底,每个地址都指
向地址空间中唯一定义的某个字节。CPU试图用下面两种方法加速该过程。
(1) CPU中有一个专门的部分称为MMU(Memory Management Unit,内存管理单元),该单元优
化了内存访问操作。
(2) 地址转换中出现最频繁的那些地址,保存到称为地址转换后备缓冲器(Translation Lookaside
Buffer,TLB)的CPU高速缓存中。无需访问内存中的页表即可从高速缓存直接获得地址数据,因而
大大加速了地址转换。

  1. 内存映射
    内存映射是一种重要的抽象手段。在内核中大量使用,也可以用于用户应用程序。映射方法可以将任意来源的数据传输到进程的虚拟地址空间中。作为映射目标的地址空间区域,可以像普通内存那样用通常的方法访问。但任何修改都会自动传输到原数据源。这样就可以使用相同的函数来处理完全不同的目标对象。例如,文件的内容可以映射到内存中。处理只需读取相应的内存即可访问文件内容,或向内存写入数据来修改文件的内容。内核将保证任何修改都会自动同步到文件中。内核在实现设备驱动程序时直接使用了内存映射。外设的输入/输出可以映射到虚拟地址空间的区域中。对相关内存区域的读写会由系统重定向到设备,因而大大简化了驱动程序的实现。
    内存映射,简而言之就是将用户空间的一段内存区域映射到内核空间,映射成功后,用户对这段内存区域的修改可以直接反映到内核空间,同样,内核空间对这段区域的修改也直接反映用户空间。那么对于内核空间<---->用户空间两者之间需要大量数据传输等操作的话效率是非常高的。

在内核分配内存时,必须记录页帧的已分配或空闲状态,以免两个进程使用同样的内存区域。由于内存分配和释放非常频繁,内核还必须保证相关操作尽快完成。内核中很多时候要求分配连续页。为快速检测内存中的连续区域,内核采用了一种古老而历经检验的技术:伙伴系统。系统中的空闲内存块总是两两分组,每组中的两个内存块称作伙伴。伙伴的分配可以是彼此独立的。但如果两个伙伴都是空闲的,内核会将其合并为一个更大的内存块,作为下一层次上某个内存块的伙伴内核对所有大小相同的伙伴(1、2、4、8、16或其他数目的页),都放置到同一个列表中管理。在应用程序释放内存时,内核可以直接检查地址,来判断是否能够创建一组伙伴,并合并为一个更大的内存块放回到伙伴列表中,这刚好是内存块分裂的逆过程。这提高了较大内存块可用的可能性。会发生称为碎片的内存管理问题。
slab缓存:分配内存,还为频繁使用的小对象实现了一个一般性的缓存——slab缓存。(1) 对频繁使用的对象,内核定义了只包含了所需类型对象实例的缓存。每次需要某种对象时,可以从对应的缓存快速分配(使用后释放到缓存)。 slab缓存自动维护与伙伴系统的交互,在缓存用尽时会请求新的页帧。(2) 对通常情况下小内存块的分配,内核针对不同大小的对象定义了一组slab缓存,可以像用户空间编程一样,用相同的函数访问这些缓存。不同之处是这些函数都增加了前缀k,表明是与内核相关
联的: kmalloc和kfree。
缺页异常机制,这种切换操作对应用程序是透明的。换出的页可以通过特别的页表项标识。在进程试图访问此类页帧时, CPU则启动一个可以被内核截取的缺页异常。此时内核可以将硬盘上的数据切换到内存中。接下来用户进程可以恢复运行。由于进程无法感知到缺页异常,所以页的换入和换出对进程是完全不可见的。
页面回收用于将内存映射被修改的内容与底层的块设备同步,为此有时也简称为数据回写。数据刷出后,内核即可将页帧用于其他用途(类似于页面交换)。内核的数据结构包含了与此相关的所有信息,当再次需要该数据时,可根据相关信息从硬盘找到相应的数据并加载。
jiffies( ['dʒɪfi] )记录了系统启动以来,经过了多少tick。一个tick代表多长时间,在内核的CONFIG_HZ中定义。比如CONFIG_HZ=200,则一个jiffies对应5ms时间。所以内核基于jiffies的定时器精度也是5ms。计时的周期是可以动态改变的。在没有或无需频繁的周期性操作的情况下,周期性地产生定时器中断是没有意义的,这会阻止处理器降低耗电进入睡眠状态。动态改变计时周期对于供电受限的系统是很有用的,例如笔记本电脑和嵌入式系统。Linux支持不同的文件系统,VFS是屏蔽各文件系统的差异,在文件系统上的一层。
内核使用typedef来定义各种数据类型,以避免依赖于体系结构相关的特性,比如,各个处理器上标准数据类型的位长可能都不见得相同。定义的类型名称如sector_t(用于指定块设备上的扇区编号)、 pid_t(表示进程ID)等,这些都是由内核在特定于体系结构的代码中定义的,以确保相关类型的值落在适当的范围内。在某些时候内核必须使用精确定义了位数的变量,例如,在需要向硬盘存储数据结构时。为允许数据在各种系统之间交换(例如, USB存储棒),无论数据在计算机内部如何表示,必须总是使用同样的外部格式。为此内核定义了若干整数数据类型,不仅明确标明了是有符号数还是无符号数,而且还指定了相关类型的精确位数。例如, __s8和__u8。
普通的用户空间程序设计不会涉及的一个特殊事项就是所谓的per-cpu变量。它们是通过DEFINE_PER_CPU(name, type)声明,其中name是变量名,而type是其数据类型(例如int[3]、 structhash等)。 在单处理器系统上,这与常规的变量声明没有不同。在有若干CPU的SMP系统上,会为每个CPU分别创建变量的一个实例。用于某个特定CPU的实例可以通过get_cpu(name, cpu)获得,其中smp_processor_id()可以返回当前活动处理器的ID,用作前述的cpu参数。采用per-cpu变量有下列好处:所需数据很可能存在于处理器的缓存中,因此可以更快速地访问。如果在多处理器系统中使用可能被所有CPU同时访问的变量,可能会引发一些通信方面的问题,采用上述概念刚好绕过了这些问题。
源代码中的多处指针都标记为__user,该标识符对用户空间程序设计是未知的。内核使用该记号来标识指向用户地址空间中区域的指针,在没有进一步预防措施的情况下,不能轻易访问这些指针指向的区域。这是因为内存是通过页表映射到虚拟地址空间的用户空间部分的,而不是由物理内存直接映射的。因此内核需要确保指针所指向的页帧确实存在于物理内存中。通过显式标记,可以支持利用自动检查工具(sparse)来确认实际上遵守了必要的条件。

2 进程管理和调试

“僵尸”进程,进程死了,其资源(内存、与外设的连接,等等)已经释放,因此
它们无法也决不会再次运行。说它们仍然活着,是因为进程表中仍然有对应的表项。僵尸是如何产生的?其原因在于UNIX操作系统下进程创建和销毁的方式。在两种事件发生时,程序将终止运行。第一,程序必须由另一个进程或一个用户杀死(通常是通过发送SIGTERM或SIGKILL信号来完成,这等价于正常地终止进程);进程的父进程在子进程终止时必须调用或已经调用wait4(读做wait for)系统调用。 这相当于向内核证实父进程已经确认子进程的终结。该系统调用使得内核可以释放为子进程保留的资源。只有在第一个条件发生(程序终止)而第二个条件不成立的情况下(wait4),才会出现“僵尸”状态。在进程终止之后,其数据尚未从进程表删除之前,进程总是暂时处于“僵尸”状态。有时候(例如,如果父进程编程极其糟糕,没有发出wait调用),僵尸进程可能稳定地寄身于进程表中,直至下一次系统重启。从进程工具(如ps或top)的输出,可以看到僵尸进程。因为残余的数据在内核中占据的空间极少,所以这几乎不是问题。
普通进程总是可能被抢占,甚至是由其他进程抢占。在一个重要进程变为可运行时,例如编辑器接收到了等待已久的键盘输入,调度器可以决定是否立即执行该进程,即使当前进程仍然在正常运行。对于实现良好的交互行为和低系统延迟,这种抢占起到了重要作用。 如果系统处于核心态并正在处理系统调用,那么系统中的其他进程是无法夺取其CPU时间的。调度器必须等到系统调用执行结束,才能选择另一个进程执行,但中断可以中止系统调用。 中断可以暂停处于用户状态和核心态的进程。中断具有最高优先级,因为在中断触发后需要尽快处理。在内核2.5开发期间,一个称之为内核抢占(kernel preemption)的选项添加到内核。 该选项支持在紧急情况下换到另一个进程,甚至当前是处于核心态执行系统调用(中断处理期间是不行的)。
尽管内核会试图尽快执行系统调用,但对于依赖恒定数据流的应用程序来说,系统调用所需的时间仍然太长了。内核抢占可以减少这样的等待时间,因而保证“更平滑的”程序执行。但该特性的代价是增加内核的复杂度,因为接下来有许多数据结构需要针对并发访问进行保护,即使在单处理器系统上也是如此。

链接:https://pan.baidu.com/s/18LMTclEgF9VFRniZEu2eFw
提取码:ouqh

你可能感兴趣的:(深入理解Linux内核架构)