深入理解LINUX内核 第三版2.6 笔记

第二章 内存寻址

  • 逻辑地址:每一个逻辑地址都由一个段segment和偏移量offset组成,偏移量指明了从段开始的地方到实际地址之间的距离。
  • 线性地址:也叫虚拟地址
  • 物理地址:内存芯片及内存单元寻址。
分段 P41
  • 内存控制单元MMU通过一种称为分段单元(segmentation unit)的硬件电路把一个逻辑地址转换成线性地址;接着,第二个称为分页单元(paging unit)的硬件电路把线性地址转换成物理地址。
  • 在多处理器系统中,所有CPU共享同一内存;这意味着RAM芯片可以由独立的CPU并发访问。因为RAM芯片上的读和写必须串行执行,一种所谓内存仲裁器的硬件电路插在总线和每个芯片之间。其作用是RAM芯片空闲,就准许一个CPU访问,如果芯片忙于处理另一个处理器请求,就延迟CPU的访问。
  • 段选择符:16位长字段。13位索引,1位表指示器TI【标明在GDT(0)还是LDT(1)】,2位请求特权级RPL
  • 段寄存器:
    • cs 代码段寄存器,指向包含程序指令的段
    • ss 栈寄存器,指向当前程序栈的段
    • ds 数据段寄存器,指向包含静态数据或者全局数据段。
    • 其他三个段es、fs、gs做一般用途
    • cs寄存器还有一个功能。表明当前CPU的特权级。linux只用0级和3级,分别称为内核态和用户态
  • 段描述符:每个段由8字节的段描述符表示,它描述了段的特征,段描述符防止全局描述表GDT或局部描述表里LDT。
    • 代码段描述符:GDT和LDT均可
    • 数据段描述符:GDT和LDT均可
    • 任务状态段描述符:保存处理器寄存器内容,支出在GDT。
    • 局部描述符表描述符:包含LDT的段,只出现在GDT
  • 通常只定义一个GDT,而每个进程除了存放GDT中的段之外如果还需要创建附加的段,就可以有自己的LDT。GDT在主存中的地址和大小存放在gdtr控制寄存器,当前正在被使用的LDT地址和大小放在ldtr控制寄存器中。
  • 分段单元
    • 先检查段选择符的TI字段,0指明描述符在GDT中,分段单元从gdtr寄存器中得到GDT线性基地址;1指明在LDT中,从ldtr寄存器中获取
    • 从段选择符的index字段计算段选择符的地址,index字段乘以8(一个段描述符大小),这个结果和gdtr或ldtr寄存器内容相加
    • 把逻辑地址额偏移量与段描述符的BASE字段相加得到线性地址。
    • 逻辑地址转换
  • 快速访问段描述符:每当一个段选择符装入段寄存器时,相应的段描述符就由内存装入到对应的非编程寄存器。从那时起,针对哪个段的逻辑转换就不访问主存中的GDT或LDT,处理器直接引用存放段描述符的非编程CPU寄存器即可。仅当段寄存器的内容改变时,才有必要访问GDT或LDT
Linux分段 P46
  • 分段可以给一个进程分配不同的线性地址空间,而分页可以把同一线性地址空间映射到不同的物理空间。
  • 运行在用户态的所有Linux进程都使用一对相同的段来对指令和数据寻址。这两个段就是用户代码段和用户数据段,内核亦如此,内核代码段和内核数据段。所有段都从0x00000000开始,Linux下逻辑地址与线性地址是一致的,即逻辑地址的偏移字段值与相应的线性地址值总是一致的。
  • Linux GDT,单处理器只有一个,多处理器每个CPU对应一个。所有的GDT都存放在cpu_gdt_table数组里,而所有的GDT的地址和它们的大小(初始化gdtr寄存器使用)被存放在cpu_gdt_descr数组中。除了少数情况下,所有的GDT的副本都存放相同的项,首先,每个处理器都有它自己的TSS段,因此对应的GDT表项不同;其次,GDT只有少数项可能依赖于正在执行的进程(LDT和TLS段描述符)。
  • Linux LDT,大多数用户态下的Linux不使用局部描述符表,这样内核就定义了一个缺省的LDT供大多数进程共享。modify_ldt()系统调用允许进程创建自己的LDT,当处理器开始执行这个进程时,该CPU的LDT副本中的LDT表项相应的就被更改了。
硬件中的分页 P50
  • 分页单元把线性地址转换为物理地址。把所有请求的访问类型和线性地址的访问权限相比较,如果内存访问是无效的,就产生一个缺页异常。
  • 线性地址被分成以固定长度为单位的组,称为页(page)。页内部连续的线性地址被映射到连续的物理地址中。这样,内核可以指定一个页的物理地址和其存取权限,而不用指定页所包含的全部线性地址的权限。
  • 分页单元把所有的RAM分成固定长度的页框(page frame)(有时也叫物理页)。页框和页的长度一致。页框是RAM的一部分,而页只是一个数据块,可以存放在任何页框或磁盘中
  • 页表:线性地址映射到物理地址的数据结构表。页表放在主存里,并在启动分页单元之前必须由内核进行适当初始化。
  • 分页
    • 使用二级模式的目的在于减少每个进程页表所需RAM数量。如果简单使用以及页表,将需要2^20个表项(4MB)。
    • 每个活动进程必须有一个分配给它的页目录。不过,不必马上为进程的所有页表都分配RAM,在实际需要时再分配更有效率。
    • 页目录和页表项具有同样的结构
  • 扩展分页,把页框大小该为4M,省去中间页表的转换,从而节省内存并保留TLB。
  • 硬件保护方案:页由User/Supervisor标志所控制。若这个标志为0,只有当CPL小于3(对于Linux而言,这意味着处理器内核态)时才能对页寻址,若标志为1,则总能寻址。页的存取权限只有两种(读、写)。段由3种存取权限(读、写、执行)如果页目录项或页表项的Read/Write标志为0,说明相关的页表或页是只读的,否则为可读写的。
  • 64位系统的分页,采用多级分页,寻址位数没有用到全部64位,只会使用部分位。
  • 硬件高速缓存:基于局部性原理。
    • 当访问一个RAM存储单元时,CPU从物理地址中提取出子集的索引号并把子集中所有行的标签与物理地址的高几位相比较。如果发现某一个行的标签与这个物理地址的高位相同,则CPU命中一个高速缓存,否则,高速缓存没有命中。
    • 当命中一个高速缓存时,高速缓存器进行不同的操作。
      • 对于读操作,控制器从高速缓存中选择数据并送到CPU寄存器;不需要访问RAM而节约和CPU时间。
      • 对于写操作,控制器可能采用以下的两个基本策略之一
        • 通写:既写RAM也写高速缓存行,
        • 回写:只更新高速缓存行。只有当CPU执行一条要求刷新缓存表项的指令时,或者当一个FLUSH硬件信号产生时,高速缓存控制器才把高速缓存行写回RAM中。
    • 当高速缓存没有命中时,高速缓存行被写回到内存中,如果有必要的话,把正确的行从RAM中取出放到高速缓存的表项中。
    • 多处理器每一个处理器都有一个单独的硬件高速缓存,数据同步由硬件保证。
  • TLB 快表:加速线性地址转换。第一次通过页目录找到页表,通过页表找到页框,计算出物理地址【3次内存访问】,同时存储为一个TLB表项,下一次再访问直接获取即可。

第三章 进程

  • 进程是程序执行时的一个实例。你可以把它看做充分描述程序已经执行到何种程度的数据结构的汇集。从内核观点看,进程的目的就是担当分配系统资源(CPU时间、内存等)的实体。

  • Linux使用轻量级进程对多线程应用程序提供更好的支持。两个轻量级进程基本上可以共享一些资源,诸如地址空间、打开的文件等等。

  • 进程描述符:task_struct,它的字段包含了与一个进程相关的所有信息。

  • 进程状态:

    • 可运行状态TASK_RUNNING:进程要么在CPU上执行,要么准备执行
    • 可中断的等待状态TASK_INTERRUPTIBLE:进程被挂起(睡眠),直到某个条件为真。产生一个硬件中断,释放进程正在等待的系统资源,或传递一个信号都是可以唤醒进程的条件
    • 不可中断的等待状态TASK_UNINTERRUPTIBLE:与可中断状态类似,但是有一个例外,把信号传递到睡眠进程不能改变它的状态。例如,当进程打开一个设备文件,其相应的驱动程序开始探测相应的硬件设备时会用到这种状态。
    • 暂停状态TASK_STOPPED:进程的执行被暂停。当进程收到SIGSTOP、SIGTSTP、SIGTTIN或SIGTTOU信号后,进入暂停状态
    • 跟踪状态TASK_TRACED:进程执行已由debugger程序暂停。当一个进程被另一个进程监控时,任何信号都可以把这个进程置于TASK_TRACED状态。
    • 僵死状态TASK_ZOMBIE:进程的执行被终止,但是,父进程还没有发布wait4或waitid系统调用来返回有关死亡进程的信息。发布wait类系统调用前,内核不能丢弃包含在死亡进程描述符中的数据,因为父进程可能还会用到它。
    • 僵死撤销状态EXIT_DEAD
  • 进程标识PID:Linux引入线程组的表示。一个线程组中的所有线程使用和该线程组的领头线程相同的PID,也就是该组中的第一个轻量级进程的PID,它被存入进程描述符的tgid字段中。getpid()系统调用返回当前进程的tgid值而不是pid的值,因此,一个多线程应用的所有线程共享相同的PID。

  • 进程链表:把所有的进程描述符链接起来。进程链表的头是init_task描述符,它是所谓的0号进程或swapper进程的进程描述符。

  • pidhash:存放pid到进程描述符的映射。

  • 进程组织:

    • 等待队列:中断处理、进程同步和定时。等待队列链表中的每个元素代表一个睡眠进程。
  • 进程切换:

    • 硬件上下文:进程恢复执行前必须装入寄存器的一组数据。在Linux中,进程硬件上下文的一部分存放在TSS段,而剩余部分存放在内核态堆栈中。进程切换只发生在内核态,在执行进程切换之前,。用户态进程使用的所有寄存器内容都已保存在内核态堆栈上。
    • 任务状态段:TSS
    • 执行进程切换:schedule()
      • 切换页目录以安装一个新的地址空间
      • 切换内核态堆栈和硬件上下文,因为硬件上下文提供了内核执行新进程所需要的所有信息,包含CPU寄存器。
  • 创建进程:

    • 写时复制技术允许父子进程读相同的物理页。只要其中有一个试图写一个物理页,内核就把这个页的内容拷贝到一个新的物理页,并把这个新的物理页分配给正在写的进程。
    • 轻量级进程允许父子进程共享每进程在内核的很多数据结构,如页表(也就是整个用户态地址空间)、打开文件表及信号处理。
    • vfork()系统调用创建的进程能共享其父进程的内存地址空间。为了防止父进程重写子进程需要的数据,阻塞父进程的执行,一直到子进程退出或执行一个新的程序为止。clone()的flags指定为SIGCHLD信号与CLONE_VM及CLONE_VFORK标志,clone()的参数child_stack等于父进程当前的栈指针。
    • do_fork()函数负责处理clone()、fork()和vfork()系统调用。do_work()利用辅助函数copy_process()来创建进程描述符以及子进程执行所需要的所有其他内核结构
  • 内核线程:

    • 内核线程只运行在内核态,而普通进程既可以运行在内核态,也可以运行在用户态
    • 因为内核线程只运行在内核态,它们只使用大于PAGE_OFFSET的线性地址空间。另一方面,不管在用户态还是在内核态,普通进程都可以使用4GB的线性地址空间。
    • kernel_thread()函数创建一个内核线程。该函数本质上也是调用do_fork()函数。
  • 进程0:所有进程的祖先叫做进程0,idle进程或因为历史的原因叫做swapper进程,它是在Linux的初始化阶段从无到有创建的一个内核线程。

    • 创建init进程,PID为1,与进程0共享每进程所有的内核数据结构。之后,进程0执行cpu_idle()函数,该函数本质上是在开中断的情况下重复执行hlt汇编语言指令。只有当没有其他进程处于TASK_RUNNING状态时,调度程序才会选择进程0.
    • 在多处理器系统中,每个CPU都有一个进程0。运行在CPU 0上的swapper进程初始化内核数据结构,然后激活其他的CPU,并通过copy_process()函数创建另外的swapper进程, 把0传递给新创建的swapper进程作为它们的新PID。
  • 进程 1:init内核线程,拥有自己的每进程(per-process)内核数据结构。在系统关闭之前,init进程一直存活,因为它创建和监控在操作系统外层执行的所有进程活动。

  • 其他内核线程:

    • keventd(也称为事件):执行keventd_wq工作队列中的函数
    • kapmd:处理与高级电源管理APM相关的事件
    • kswapd:执行内存回收
    • pdflush:刷新"脏"缓冲区中的内容到磁盘以回收内存
    • kblock:执行kblockd_workqueue工作队列中的函数。实质上,它周期性的激活块设备驱动程序。
    • ksoftirqd:运行tasklet;系统中每个CPU都有这样一个内核线程
  • 撤销进程:

    • 终止用户态应用的系统调用
      • exit_group()系统调用,它终止整个线程组,即整个基于多线程的应用。do_group_exit()是实现这个系统调用的主要内核函数。
      • exit()系统调用,它终止某一线程,而不管该线程所属线程组中的所有其他进程。do_exit()是实现这个系统调用的主要内核函数。
  • 进程删除:

    • Unix允许进程查询内核以获得父进程的PID,或者任何子进程的执行状态。为了遵循着些设计选择,不允许Unix内核在进程一终止后就丢弃包含在进程描述符字段中的数据。只有父进程发出了与被终止的进程相关的wait()类系统调用之后,才允许这样做。这就是引入僵死状态的原因:尽管从技术上来说进程已死,但必须保存它的描述符,直到父进程得到通知。
    • 父进程先于子进程退出。此时子进程称为init进程的子进程来解决。

第四章 中断和异常

  • 中断类型
    • 同步中断是当指令执行时由CPU控制单元产生的,之所以称为同步,是因为只有在一条指令终止执行后CPU才会发出中断。
    • 异步中断是由其他硬件设备依照CPU时钟信号随机产生。
    • 在Intel微处理器手册中,把同步和异步中断分别称为异常exception和中断interrupt。
  • 中断是由间隔定时器和I/O设备产生的,例如,用户一次按键会引起一个中断。
  • 异常是由程序的错误产生的,或者是由内核必须处理的异常条件产生的。
    • 第一种情况内核通过发送一个每个Unix程序员都熟悉的信号来处理异常。
    • 第二种情况下,内核执行恢复异常需要的所有步骤,例如缺页,或对内核服务的一个请求(通过一条int或sysenter指令)
  • 中断信号提供了一种特殊的方式,使处理器转而去执行正常控制流之外的代码。当一个中断信号到达时,CPU必须停止它正在做的的事情,并且切换到一个新的活动。为了做到这一点,就要在内核态堆栈保存程序计数器的当前值(即eip或cs寄存器内容),并把与中断相关的一个地址放进程序计数器。
  • 中断处理约束:
    • 当内核打算去完成一些别的事情时,中断随时会到来。因此,内核的目标就是让中断尽可能块地处理完,尽其所能把更多的处理向后推迟。内核响应中断需要将操作分为两部分:关键而紧急的部分,内核立即执行;其余推迟的部分,内核随后执行。
    • 因为中断随时会到来,所以内核可能正在处理其中一个中断时,另一个中断(不同类型)又发生了。应该尽可能多地允许这种情况发生,因为这能维持更多的I/O设备处于忙状态。因此,中断处理程序必须编写成使相应的内核控制路径能以嵌套的方式运行。当最后一个内核控制路径终止时,内核必须能恢复被中断进程的执行,或者,如果中断信号以导致了重新调度,内核能切换到另外的进程。
    • 尽管中断在处理前一个中断时可以接受一个新的中断,但是内核代码中还是存在一些临界区,在临界区,中断必须被禁止。必须尽可能地限制这样的临界区,因为根据以前的要求,内核,尤其是中断处理程序,应该在大部分时间内以开中断的方式运行。
  • 中断:可屏蔽中断和非屏蔽中断
  • 异常:
    • 处理器探测异常:当CPU执行指令时探测到一个反常条件所产生的异常。
      • 故障fault:通常可以纠正;一旦纠正,程序就可以在不失连贯性的情况下重新开始。保存在eip中的值是引发故障的指令地址。
      • 陷阱trap:在陷阱指令执行后立即报告;内核把控制权返回给程序后就继续它的执行而不失连贯性。保存在eip中的值是一个随后要执行的指令地址。陷阱的主要用途是为了调试程序
      • 异常终止abort:发生了一个严重的错误;控制单元除了问题,不能在eip寄存器中保存引起异常的指令所在的确切位置。异常中止用于报告严重的错误,如硬件故障或系统表中无效的值或不一致的值。强制受影响的进程终止。
      • 编程异常:在编程者发出请求时发生。是由int或int3指令触发的;当into(检查溢出)和bound(检查地址出界)指令检查的条件不为真时,也引起编程异常。
  • IRQ:每个能发出中断请求的硬件控制器都有一条名为IRQ的输出线
  • PIC可编程中断控制器:所有现有的IRQ线都与一个名为PIC的硬件电路的输入引脚相连。
    • 监视IRQ线,检查产生的信号
    • 如果一个引发信号出现在IRQ线上:
      • 把接收到的引发信号转换成对应的向量
      • 把这个向量存放在中断控制器的一个I/O端口,从而允许CPU通过数据总线读此向量
      • 把引发信号发送到处理器的INTR引脚,即产生一个中断
      • 等待,直到CPU通过把这个中断信号写进PIC的一个端口来确认它;当这种情况出现时,清INTR线。
  • I/O高级可编程中断控制器(I/O APIC):
    • 多处理器每一个CPU都有一个本地APIC,都连接到一个外部I/O APIC,连接到APIC总线,I/O APIC也连在APIC总线上。所有的外部中断通过I/O APIC接收。
    • 当一个CPU希望把一个中断发送给另一个CPU时,它就在自己本地APIC的中断指令寄存器ICR中存放这个中断向量和目标本地APIC的标识符。然后通过APIC总线向目标本地APIC发送一条消息,从而向自己的CPU发出一个相应中断。处理器间中断(IPI)是SMP体系结构至关重要的组成部分,并由Linux有效的用来在CPU间交换信息
  • 中断描述符表IDT:它与每一个中断异常向量相联系,每一个向量在表中有相应的中断或异常处理程序的入口地址。分类如下:
    • 任务门:当中断信号发生时,必须取代当前进程的那个进程的TSS选择符存放在任务门中
    • 中断门:包含段选择符和中断或异常处理程序的段内偏移量。当控制权转移到一个适当的段时,处理器清IF标志,从而关闭将来会发生的可屏蔽中断。
    • 陷阱门:与中断门相似,只是控制权传递到一个适当的段时处理器不修改IF标志。
    • Linux利用中断门处理中断,利用陷阱门处理异常
  • 中断和异常的硬件处理:CPU控制单元如何处理中断和异常
    • 当执行一条指令后,cs和eip这对寄存器包含下一条将要执行的指令的逻辑地址。在处理这条指令之前,控制单元会检查在运行前一条指令时是否已经发生了一个中断或异常。
    • 如果发生了,那么控制单元将会执行一些列操作。校验异常的权限等…。
    • 控制单元执行的最后一步就是跳转到中断或者异常处理程序。换句话说,处理完中断信号后,控制单元所执行的指令就是被选中处理程序的第一条指令。
    • 中断或异常被处理完后,响应的处理程序必须产生一条iret指令,把控制权转交给被中断的进程。
  • 中断和处理程序的嵌套运行:
    • 允许嵌套运行的代价就是中断处理程序必须永不阻塞,换句话说,中断处理程序运行期间不能发生进程切换。事实上,嵌套的内核控制路径恢复执行时需要的所有数据结构都存放在内核态堆栈中,这个栈毫无疑义的属于当前进程。
    • 假定内核没有bug,那么大多数异常就只在CPU处于用户态时发生。事实上,异常要么是由编程错误引起,要么是由调试程序触发。然而,"Page Fault(缺页)"异常发生在内核态。当处理缺页异常时,内核可以挂起当前进程,并用另一个进程代替它,直到请求的页可以使用为止。只要被挂起的进程又获得处理器,处理缺页异常的内核控制路径就恢复执行。
    • 一个中断处理程序可以抢占其他的中断处理程序,也可以抢占异常处理程序。相反,异常处理程序从不强占中断处理程序。
  • 异常处理:CPU产生的大部分异常都由Linux解释为出错条件。当其中一个异常发生时,内核就向引起异常的进程发送一个信号向它通知一个反常条件
    • 例如,如果进程执行一个被0除的操作,CPU就会产生一个"Divide error"异常,并由响应的异常处理程序向当前进程发送一个SIGFPE信号,这个进程将采取若干必要的步骤来(从出错中)恢复或者中止运行(如果没有为这个信号设置处理程序的话)。
    • 缺页异常:推迟给进程分配新的页框,直到不能推迟为止。
    • 为异常处理程序保存寄存器的值
    • 进入和离开异常处理程序:执行异常处理程序的C函数名总是由do_前缀和处理器程序名组成。
      • 向当前进程发送一个适当的信号
      • 当异常处理程序终止,当前进程就关注这个信号。该信号要么在用户态由进程自己的信号处理程序(如果存在)来处理,要么由内核来处理(内核一般会杀死这个进程)。
  • 中断处理:由于会出现一个进程(请求数据传输进程)被挂起好久后中断才到达,这时候运行的进程很可能是一个无关的进程,因此直接给进程发送信号显然不行了。
    • I/O处理依赖中断类型:
      • I/O中断:某些I/O设备需要关注;相应的中断处理程序必须查询设备以确定适当的操作过程。
      • 时钟中断:某种时钟(本地APIC时钟,或者外部时钟)产生一个中断;这种中断告诉内核一个固定的时间间隔已经过去。
      • 处理器间中断:多处理器系统中一个CPU对另一个CPU发出一个中断。
    • I/O中断处理
      • I/O中断处理程序必须足够灵活以给多个设备同时提供服务。它的灵活性有如下两种方式实现:
        • IRQ共享:中断处理程序执行多个中断服务例程ISR。每个ISR是一个与单独设备(共享IRQ线)相关的函数。每个ISR都执行,已验证它的设备是否需要关注
        • IRQ动态分配:一条IRQ线在可能的最后时刻才与一个设备驱动程序相关联。这样,即使几个硬件设备不共享IRQ线,同一个IRQ向量也可以由这几个设备在不同时刻使用。
      • 当一个中断处理程序正在运行时,相应的IRQ线上发出的信号就被暂时忽略。更重要的是,中断处理程序是代表进程执行的,它所代表的的进程必须总处于TASK_RUNNING状态,否则,就可能出现系统僵死状态。因此,中断处理程序不能执行任何阻塞过程,如磁盘I/O操作。
      • Linux把紧随中断要执行的操作分为三类:
        • 紧急的:例如对PIC应答中断,对PIC或设备控制器重编程,或者修改由设备和处理器同时访问的数据结构。紧急操作要在一个中断处理程序内立即执行,而且是在禁止可屏蔽中断的情况下。
        • 非紧急的:如修改哪些只有处理器才会访问的数据结构。中断处理程序立即执行,但是必须在开中断的情况下。
        • 非紧急的可延迟的:如把缓冲区的内容拷贝到某个进程的地址空间。这些操作可能被延迟较长的时间间隔而不影响内核操作,有兴趣的进程将会等待数据。非紧急可延迟操作由独立的函数来执行。
      • I/O中断处理程序基本操作:
        • 在内核态堆栈保存IRQ的值和寄存器的内容
        • 为正在给IRQ线服务的PIC发送一个应答,这将允许PIC进一步发出中断
        • 执行共享这个IRQ的所有设备的中断服务例程ISR
        • 调到ret_from_intr()的地址后终止。
      • 内核栈类型
        • 异常栈:用于处理异常(包括系统调用)。这个栈包含在每个进程的thread_union数据结构中,因此对系统中的每个进程,内核使用不同的异常栈。
        • 硬中断请求栈:用于处理中断。系统的每个CPU都有一个硬中断请求栈,而且每个栈占用一个单独的页框。
        • 软中断请求栈:用于处理可延迟的函数(软中断或tasklet)。
    • 软中断及tasklet
      • 把可延迟中断从中断处理程序中抽出来有助于使内核保持较短的响应时间。
      • 可延迟函数(软中断与tasklets)和工作队列。
      • tasklet是在软中断之上实现的。
      • 软中断的分配是静态的(编译时定义),而tasklet的分配和初始化可以在运行时进行(例如:安装一个模块时)。软中断(即使是同一类型的软中断)可以并发地运行在多个CPU上。因此,软中断是可重入函数而且必须明确地使用自旋锁保护其数据结构。tasklet不必担心这些问题,因为内核对tasklet的执行进行了更加严格的控制。相同类型的tasklet总是被串行的运行。也就是说不能再两个CPU上运行相同类型的tasklet。但是,不同类型的tasklet可以在几个CPU上并发的运行。
      • __do_softirq()函数读取本地CPU的软中断掩码并执行与每个设置位相关的可延迟函数。
      • 每个CPU都有自己的ksoftirqd/n内核线程。每个内核线程都运行ksoftirqd()函数。do_softirq()函数确定哪些软中断是挂起的,并执行它们的函数。如果已经执行的软中断又被激活,do_softirq()则唤醒内核线程并终止。内核线程有较低的优先级,因此用户程序就有机会运行;但是如果机器空闲,挂起的软中断就很快被执行。
      • tasklet是I/O驱动程序中实现可延迟函数的首选。
      • 工作队列:由worker thread的特殊内核线程执行。可延迟函数运行在中断上下文中,而工作队列中的函数运行在进程上下文中。执行可阻塞函数(例如需要访问磁盘数据块的函数)的唯一方式是在进程上下文中运行。因为在中断上下文中不可能发生进程切换。可延迟函数和工作队列中的函数都不能访问进程的用户态地址空间。事实上,可延迟函数执行时不能确定那个进程在运行。另一方面,工作队列的函数是由内核线程来执行的。因此,根本不存在它要访问的用户态地址空间。

第五章 内核同步

  • 把内核看做是不断对请求进行响应的服务器,这些请求可能来自在CPU上执行的进程,也可能来自发出中断请求的外部设备。
  • 如果进程正执行内核函数时,即它在内核态运行时,允许发生内核切换(被替换的进程是正在执行内核函数的进程),这个内核就是抢占的。
    • 无论是抢占内核还是非抢占内核,运行在内核态的进程都可以自动放弃CPU,如等待资源
  • 一个内核态运行的进程,可能在执行内核函数期间被另一个进程取代。
  • 使内核可抢占的目的是减少用户态进程的分派延迟,即从进程变为可执行状态到它实际开始运行之间的时间间隔。
  • 禁止内核抢占的情形
    • 内核正在执行中断服务例程
    • 可延迟函数被禁止(当内核正在执行软中断或tasklet时经常如此)
    • 通过把抢占计数器设置为正数而显示地禁用内核抢占。
  • 只有当内核正在执行异常处理程序(尤其是系统调用),而且内核抢占没有被显示的禁用时,才可能抢占内核。
  • 如果是单CPU的系统,可以采用访问共享数据结构时关闭中断的方式来实现临界区,因为只有在开中断的情况下,才可能发生内核控制路径的嵌套
  • 同步原语
    • 每CPU变量(per-cpu variable):主要的数据结构的数组,系统的每个CPU对应数组的一个元素。一个CPU不应该访问其他CPU对应的数组元素,它可以随意读取和修改它自己的元素而不用担心出现竞争条件。
      • 虽然每CPU变量可以保护从不同CPU的并发访问,但是它并不能保护异步访问,比如中断和可延迟函数。另外,如果支持内核抢占,则每CPU变量可能会存在竞态。因而内核在访问每CPU变量时应该禁止内核抢占。
    • 原子操作:操作码前缀是lock字节的"读-修改-写"汇编语言指令即使在多处理器系统中也是原子的。当控制单元检测到这个前缀时,就"锁定"内存总线,直到这条指令执行完成为止。
      • Linux内核提供一个专门的atomic_t类型(一个原子访问计数器)和一些专门的函数和宏,这些函数和宏作用于atomic_t类型的变量,并当做单独的、原子的汇编语言指令来使用。
    • 优化和内存屏障:当处理同步时,必须避免指令重新排序。如果放在同步原语之后的一条指令在同步原语之前执行,事情很快就会失控。事实上,所有的同步原语起优化和内存屏障的作用。
      • 优化屏障原语保证编译程序不会混淆放在原语操作之前的汇编语言指令和放在原语操作之后的汇编指令,这些汇编语言指令在C中都有对应语句。优化屏障并不保证不使当前CPU把汇编语言指令混在一起执行–这是内存屏障的工作。
      • 内存屏障原语保证,在原语之后的操作开始执行之前,原语之前的操作已经完成。
    • 自旋锁(spin lock):用来在多处理器环境中工作的一种特殊的锁
      • 自旋锁循环指令表示"忙等"。
      • 一般来说,由自旋锁保护的每个临界区都是禁止内核抢占的。在单处理器系统上,这种锁本身并不起锁的作用,自旋锁原语仅仅是禁止或启用内核抢占。注意,在自旋锁忙等待期间,内核抢占还是有效的,因此,等待自旋锁释放的进程有可能被更高优先级的进程替代。
    • 读/写自旋锁:内核控制路径发出执行read_lock或write_lock操作的请求具有相同的优先权:读者必须等待,直到写操作完成。同样地,写着也必须等待,直到读操作完成。
    • 顺序锁seqlock:与读写自旋锁类似,只是它为写着赋予了较高的优先级。即使读者正在读的时候也允许写着执行。
    • 读-拷贝-更新(RCU):允许多个读者和写着并发执行。不使用锁。读原数据,写副本,写完指针指向新的副本,旧的读继续读取旧的数据,新的读读取新的副本。采用内存屏障保证在数据结构被修改之后,已更新的指针对其他CPU才是可见的。
      • RCU值保护被动态分配并通过指针引用的数据结构
      • 在被RCU保护的临界区中,任何内核控制路径都不能休眠。
    • 信号量
      • 内核信号量,由内核控制路径使用
      • System V IPC,由用户态进程使用
      • 内核信号量类似于自旋锁,因为当锁关闭着时,它不允许内核控制路径继续执行。然而,当内核控制路径视图获取内核信号量所保护的忙资源时,相应的进程挂起。只有在资源被释放时,进程才再次变为可运行的。因此,只有可以睡眠的函数才能获取内核信号量;中断处理程序和可延迟函数都不能使用内核信号量
    • 读/写信号量:在信号量再次被打开之前,等待进程挂起。
    • 禁止本地中断
    • 禁止和激活可延迟函数
  • 对内核数据结构的同步访问
    • 为了使I/O吞吐量最大化,应该使中断禁止保持在很短的时间
    • 为了有效地利用CPU,应该尽可能避免使用基于自旋锁的同步原语。

第六章 定时测量

  • Linux定时测量
    • 保存当前的时间和日期
    • 维护定时器,告诉内核或用户程序某一时间间隔已经过去了。
  • 内核与时间相关的主要任务:
    • CPU分时
    • 更新系统时间
    • 资源使用统计
      • 检查运行进程的CPU资源限制
      • 更新与本地CPU工作负载有关的统计数
      • 计算平均系统负载
      • 监管内核代码
    • 维护软定时器和延迟函数
      • 定时器:允许在将来的某个时刻,函数在给定的时间间隔用完时被调用
      • 延迟函数:执行一个紧凑的指令循环直到指定的时间间隔用完。当内核需要等待一个较短的时间间隔–比方说,不超过几毫秒时,就无须使用软定时器。内核使用udelay()和ndelay函数:前者接收一个微秒级的时间间隔作为它的参数,并在指定的延迟结束后返回
  • 时钟和定时器电路
    • 时钟电路同时用于跟踪当前时间和产生精确的时间度量
    • 定时器电路由内核编程,所以它们以固定的、预先定义的频率发出中断。
    • 实时时钟RTC:在IRQ8上发出周期性中断
    • 时间戳计数器TSC:接收外部振荡器的时钟信号,每个时钟信号到来加1.可获得比PIT更精准的时间测量
    • 可编程间隔定时器PIT:以内核确定的固定频率不停地发出中断。

第七章 进程调度

  • Linux的调度基于分时(time sharing)技术。分时依赖于定时中断,因此对进程是透明的。不需要在程序中插入额外的代码来保证CPU分时。
  • 调度策略是根据进程优先级来进行分类的。在Linux中,进程的优先级是动态的。调度程序跟踪进程正在做什么,并周期性地调整它们的优先级。在这种方式下,较长时间间隔内没有使用CPU的进程,通过动态地增加它们的优先级来提升它们。运行较长时间的进程,减少它们的优先级。
  • 进程分类:I/O受限和CPU受限
  • 另一种分类:交互式进程、批处理进程和实时进程。
    • 实时进程:不会被低优先级进程阻塞,它们应该有一个短的响应时间。
  • 进程抢占
    • 如果进程进入TASK_RUNNING状态,内核检查它的动态优先级是否大于当前正在运行进程的优先级。如果是,current的执行被中断,并调用调度程序选择另一个进程运行(通常是刚刚变为可运行的进程)。
    • 进程在它的时间片到期也会被抢占。
    • 当前进程thread_info结构的TIF_NEED_RESCHED[强迫进行重新调度]标志被设置,以便时钟中断处理程序终止时调度程序被调用。
    • 被抢占的进程并没有被挂起,因为它还在TASK_RUNNING状态,只不过不再使用CPU
  • 调度程序总能成功地找到要执行的进程。事实上,总是至少有一个可运行进程,即swapper进程,它的PID为0,而且它只有在CPU不能执行其他进程时才执行。
  • 每个普通进程都有它自己的静态优先级,调度程序使用静态优先级来估价系统中这个进程与其他普通进程之间的调度程度。
  • 调度类型
    • SCHED_FIFO:先进先出的实时进程。当调度程序把CPU分配给进程的时候,它把该进程描述符保留在运行队列链表的当前位置。如果没有其他可运行的更高优先级实时进程,进程就继续使用CPU,想用多久用多久,即使还有其他相同优先级的实时进程处于可运行状态
    • SCHED_RR:时间片轮转的实时进程。当调度程序把CPU分配给进程的时候,它把该进程的描述符放在运行队列链表的尾部。这种策略保证对所有具有相同优先级的SCHED_RR实时进程公平地分配CPU时间。
    • SCHED_NORMAL:普通的分时进程
  • 普通进程的调度
    • 静态优先级[100,139]本质上决定了进程的基本事件片。静态优先级越高(其值越小),基本事件片越长
    • 动态优先级[100,139]是调度程序在选择新进程来运行的时候使用的数。
    • 为了避免进程饥饿,当一个进程用完它的时间片时,他应该被还没有用完时间片的低优先级进程取代,为了实现这个机制,调度程序维持两个不想交的可运行进程的集合。
      • 活动进程:这些进程还没有用完它们的时间片,因此允许它们允许
      • 过期进程:这些可运行进程已经用完了它们的时间片,并因此被禁止运行,直到所有活动进程都过期。
  • 实时进程的调度
    • 每个实时进程都与一个实时优先级有关,实时优先级是一个范围从1(最高优先级)-99的值。调度程序总是让最高优先级的进程运行,换句话说,实时进程运行的过程中,禁止低优先级进程的执行。与普通进程相反,实时进程总是被当成活动进程。
    • 实时进程被取代的情况:
      • 进程被另一个更高实时优先级的实时进程抢占
      • 进程执行了阻塞操作并进入睡眠
      • 进程停止或被杀死
      • 进程通过系统调用sched_yield()自愿放弃CPU
      • 进程是基于时间片轮转的实时进程,并且用完了它的时间片。
  • 调度程序使用函数:
    • scheduler_tick():维持当前最新的time_slice计数器
    • try_to_wake_up():通过把进程状态设置为TASK_RUNNING,并把该进程插入本地CPU的运行队列来唤醒睡眠或停止的进程。
    • recalc_task_prio():更新进程的平均睡眠时间和动态优先级。
    • schedule():实现调度程序。它的任务是从可运行队列的链表中找到一个进程,并随后将CPU分配给这个进程。可以由几个内个控制路径调用,可以采取直接调用和延迟调用。
      • 直接调用:如果current进程因不能获取必须的资源而立刻被阻塞,就直接调用调度程序。
      • 延迟调用:也可以吧current进程的TIF_NEED_RESCHED标志设置为1,而以延迟方式调度程序。由于总是在恢复用户态进程的执行之前检查这个标志的值,所有schedule()将在不久之后的某个时间被明确调用
    • load_balance():它检查是否可以通过把最繁忙的组中的一些进程迁移到本地CPU的运行队列来减轻不平衡的状况。

第八章 内存管理

  • RAM的某些部分永久地分配给内核,并用来存放内核代码以及静态内核数据结构。RAM的其余部分称为动态内存。
页框管理
  • 内核必须记录每个页框当前的状态。例如,内核必须能区分哪些页框属于进程的页,而哪些页框包含的是内核代码或内核数据。类似的,内核还必须能够确定动态内存中的页框是否空闲。
  • 页框的状态信息保存在一个类型为page的页描述符中。所有的页描述符存放在mem_map数组中。
    • virt_to_page(addr)宏产生线性地址addr对应的页描述符地址
    • pfn_to_page(pfn)宏产生以页框号pfn对应的页描述符地址。
  • 系统的物理内存被划分为几个节点(node)。每个节点的物理内存又可以划分为几个管理区(zone)。
内存管理区 内核
  • Linux2.6把每个内存节点的物理内存划分为3个管理区(zone).
    • ZONE_DMA:包含低于16MB的内存页框。
    • ZONE_NORMAL:包含高于16MB且低于896MB的内存页框。
    • ZONE_HIGHMEM:包含从896MB开始高于896MB的内存页框。
  • ZONE_DMA和ZONE_NORMAL区包含内存的“常规”页框,通过把它们线性地映射到线性地址空间的第4个GB,内核就可以直接访问。相反,ZONE_HIGHMEM区包含的内存页不能由内核直接访问。
  • 每个内存管理区都有自己的内存描述符。
  • 每个页描述符都有到内存节点和到节点内管理区(包含相应页框)的链接。为节省空间,这些链接的存放方式与典型的指针不同,而且被编码成索引存放在flags字段的高位。page_zone()函数接收一个页描述符的地址作为它的参数,它读取页描述符中flags字段的最高位,然后通过查看zone_table数组来确定相应管理区描述符的地址。
  • 保留的页框池
    • 可以有两种方式来满足内存分配请求。如果有足够的空闲内存可用,请求就会被立即满足。否则,必须回收一些内存,并且将发出请求的内核控制路径阻塞,直到有内存被释放。
    • 当请求内存时,一些内核控制路径不能被阻塞–例如,处理中断或在执行临界区内的代码时。在这种情况下,一条内核控制路径应当产生原子内存分配请求。原子请求从不阻塞:如果没有足够的空闲页,则仅仅是分配失败而已。
    • 内核为原子分配内存请求保留了一个页框池,只在内核不足时才使用。管理区描述符的page_min字段存储了管理区内保存页框的数目。
  • 分区页框分配器:处理对连续页框组的内存分配请求。
  • 高端内存页框的内核映射:与直接映射的物理内存末端、高端内存的始端所对应的线性地址存放在high_memory变量中,它被设置为896MB。896MB边界以上的页框并不映射在内核线性地址空间的第4个GB,因此,内核不能直接访问它们。这就意味着,返回所分配页框线性地址的页分配函数不适用于高端内存,即不适用于ZONE_HIGHMEM内存管理区的页框。
    • 永久内核映射
    • 临时内核映射
  • 伙伴系统算法
    • 内核应该为分配一组连续的页框而建立一种健壮、高效的分配策略。外碎片问题。
    • 把所有的空闲页框分组为11个块链表,每个块链表分别包含大小为1, … ,1024个连续的页框。
  • 每CPU页框高速缓存:内核经常请求和释放单个页框。为了提升系统性能,每个内存管理区定义了一个“每CPU”页框高速缓存。所有“每CPU”高速缓存包含一些预先分配的页框,它们被用于满足本地CPU发出的单一内存请求。
  • 管理区分配器:内核页框分配器的前端。该构件必须分配一个包含足够多空闲页框的内存区,使它能满足内存请求。
  • 内存区管理
  • 内碎片问题:slab分配器解决。最多50%的内存碎片
  • slab分配器,内核建立13个按几何分布的空闲内存区链表,它们的大小从32到131072字节。p324
    • 所存放数据类型可以影响内存区的分配方式。把内存区看做对象,有一组构造和析构函数分别初始化和回收内存区。为了避免重复初始化对象,不丢弃已分配的对象,而是释放但把他们保存在内存中。
    • 内存倾向于反复请求同一类型的内存区。
    • 对内存区可以根据它们发生的频率来分类
    • slab分配器把对象分组放进高速缓存。每个高速缓存都是同种类型对象的一种“储备”。例如,当一个文件被打开时,存放相应“打开文件”对象所需的内存区是从一个叫做flib(“文件指针”)的分配器的高速缓存中得到的。
    • 内核周期性地扫描高速缓存并释放空slab对应的页框。
  • 内存池:一个内存池允许一个内核成分,如块设备子系统,仅在内存不足的紧急情况下分配一些动态内存来使用。

第九章 进程地址空间

  • 内核分配内存简单
    • 内核是操作系统中优先级最高的成分。如果内核函数请求某个内存,那么就没有理由推迟分配
    • 内核信任自己。假定所有内核函数是无错误的,因此内核不必插入错误错误的任何保护措施。
  • 用户态分配内存时:
    • 进程对动态内存的请求被认为是不紧迫的。内核总是尽量推迟给用户态进程分配内存。
    • 由于用户进程是不可信任的,因此,内核必须能随时准备捕获用户态进程引起的任何寻址错误。
  • 当用户态进程请求动态内存时,并没有获得请求的页框,而仅仅获得对一个新的线性地址的使用权,而这一线性地址区间就成为进程地址空间的一部分。
进程地址空间
  • 进程地址空间(address space)是允许进程使用的全部线性地址组成。
  • 内核通过所谓的线性区的资源来表示线性地址空间,线性区是由起始线性地址、长度和一些访问权限来描述的。为了效率,起始地址和线性区的长度都必须是4096的倍数,以便每个线性区所识别的数据完全填满分配给它的页框。
内存描述符
  • 与进程相关的全部信息都包含在一个叫做内存描述符的数据结构中。结构类型为mm_struct,进程描述符中的mm字段就指向这个结构。
  • 所有内存描述符存在一个双向链表里。每个描述符在mmlist字段存放链表相邻元素的地址。
  • mm_users字段存放共享mm_struct数据结构的轻量级进程的个数。mm_count字段是内存描述符的主使用计数器,每次递减,内核都要检查它是否变为0,如果是,就解除这个内存描述符。
  • mm_alloc()函数用来获取一个新的内存描述符。由于这些描述符被保存在slab分配器高速缓存中,因此,mm_alloc()调用kmem_cache_alloc()来初始化新的内存描述符,并把mm_count和mm_users字段都置为1.
  • 内核线程仅运行在内核态,因此,它们永远不会访问低于TASK_SIZE(等于PAGE_OFFSET,通常为0xc0000000)的地址。大于TASK_SIZE线性地址的相应页表项都应该总是相同的。为了避免无用的TLB和高速缓存刷新,内核线程使用一组最近运行的普通进程的页表。结果,在每个进程描述符中包含了两种内存描述符指针:mm和active_mm。
  • 进程描述符中的mm字段指向进程所拥有的内存描述符,而active_mm字段指向进程运行时所使用的的内存描述符。对于普通进程,这两个字段存放相同的指针。当内核线程得以运行时,它的active_mm字段被初始化为前一个运行进程的active_mm值。
线性区
  • 进程所拥有的线性区从来不重叠,并且内核尽力把新分配的线性区与紧邻的现有线性区进行合并。如果两个相邻区的访问权限相匹配,就能把它们合在一起。

  • 进程所拥有的所有线性区是通过一个简单的链表链接在一起的。出现在链表中的线性区是按内存地址升序排列的;内核通过进程的内存描述符的mmap字段来查找线性区,其中mmap字段指向链表中的第一个线性区描述符。内存描述符的mao_count字段存放进程拥有的线性区数目。

  • 进程地址空间

  • 分配线性地址区间:do_mao()函数为当前进程创建被初始化一个新的线性区。不过,分配成功之后,可以把这个新的线性区与进程已有的其他线性区进行合并。

  • 释放线性地址区间:内核使用do_munmap()函数从当前进程的地址空间中删除一个线性地址区间。函数分两阶段,第一阶段,扫描进程所拥有的线性区链表,并把包含在进程地址空间的线性地址区间中的所有线性区从链表中删除;第二阶段,更新进程的页表,并把第一阶段找到并标识出的线性区删除。

  • 请求调页:如果访问的页不存在,也就是说,这个页还没有存放在任何一个页框中,那么,内核分配一个新的页框并适当地初始化。请求调页把页框的分配推迟到不能推迟为止,也就是说,一直推迟到进程要访问的页不在RAM中时为止,由此引起一个缺页异常。

    • 由于请求调页所引发的每个”缺页“异常必须由内核处理,这将浪费CPU的时钟周期
    • 被访问的页不在主存中,或者是内核已经回收了相应的页框,这两种情况缺页程序必须为进城分配新的页框。
  • 写时复制COW:如果被访问的内存存在但是标记为只读,也就是说,它已经存放在一个页框中,那么,内核分配一个新的页框,并把旧页框的数据拷贝到新页框来初始化它的内容。当进程试图写入时,内核检查写进程是否为页框的唯一属主,如果是,就把页框标记为对这个进程是可写的。

  • 创建和删除进程地址空间

  • 堆的管理:堆用于满足进程的动态内存请求。内存描述符的start_brk与brk字段分别限定了这个区的开始地址和结束地址。brk(addr)直接修改堆的大小,它是以系统调用实现的函数。

第十章 系统调用

  • Unix系统通过向内核发出系统调用(system call)实现了用户态进程和硬件设备之间的大部分接口。
  • 应用程序和硬件之间加一个额外层的优点:
    • 编程容易
    • 系统安全:调用时就可校验
    • 可移植
  • 系统调用是通过一个软中断向内核态发送一个明确请求。
  • 系统调用处理程序:
    • 在内核态栈保存大多数寄存器的内容
    • 调用名为系统调用服务例程的相应的C函数来处理系统调用
    • 退出系统调用处理程序:用保存在内核态中的值加载寄存器,CPU从内核态切换回搭配用户态
  • 通过int $0x80指令发出系统调用。【Linux内核老版本唯一的用户态切换到内核态的方式】
  • 通过sysenter指令发出系统调用。sysexit指令退出系统调用,从内核态切换到用户态
  • 参数传递:在发出系统调用前,系统调用的参数被写入CPU寄存器,然后在调用系统调用服务例程之前,内核再把存放在CPU中的参数拷贝到内核态堆栈中。

第十一章 信号

  • 信号用于用户态进程间通信。内核也用信号通知进程系统所发生的事件。

  • 使用信号的两个主要目的:

    • 让进程知道已经发生了一个特定的事件。
    • 强迫进程执行它自己代码中的信号处理程序。
  • kill -l

  • 常规信号(regular signal):1-31,同种类型的常规信号不排队:如果一个常规信号被连续发送多次,那么,只有其中的一个发送到接收进程。

  • 实时信号(real-time signal):32-64,它们必须被排队以便发送的多个信号能被接收到。

  • 尽管Linux内核并不使用实时信号,它还是通过几个特定的系统调用完全实现了POSIX标准。

  • 信号的一个重要特点是它们可以随时被发送给状态经常不可预知的进程。发送给非运行进程的信号必须由内核保存,直至进程恢复执行阻塞一个信号要求信号的传递拖延,直到随后解除阻塞,这使得信号产生一段时间之后才能对其传递这一问题更加严重。

  • 内核区分信号传递的两个阶段:

    • 信号产生:内核更新目标进程的数据结构以表示一个新信号已被发送。
    • 信号传递:内核强迫目标进程通过以下方式对信号做出反应:或改变目标进程的执行状态,或开始执行一个特定的信号处理程序,或两者都是。
  • 每个所产生的信号至多被传递一次。信号是可消费资源:一旦它们被传递出去,进程描述符中有关这个信号的所有信息都被取消。

  • 已经产生但还没有传递的信号称为挂起信号(pending singal)。【从信号产生到信号被解除 阻塞这一段时间称之为挂起】任何时候,一个进程仅存在给定类型的一个挂起信号,同一进程同种类型的其他信号不被排队,只是简单地丢弃。但是,实时信号是不同的:同种类型的挂起信号可以由好几个。

  • 一般来说,信号可以保留不可预知的挂起时间。必须考虑如下因素:

    • 信号通常只被当前正运行的进程传递(即由current进程传递);
    • 给定类型的信号可以由进程选择性地阻塞blocked,在这种情况下,在取消阻塞前进程不接收这个信号。
    • 当进程执行一个信号处理程序的函数时,通常”屏蔽“相应的信号,即自动阻塞这个信号直到处理程序结束。因此,所处理的信号的另一次出现不能中断处理程序,所以,信号处理函数不必是不可重入的。
  • 进程以三种方式之一对一个信号做出应答:

    • 显示的忽略信号
    • 执行相关的缺省操作。由内核预定义的缺省操作取决于信号的类型:
      • Terminate:进程被终止(杀死)。
      • Dump:进程被终止(杀死),并且,如果可能,创建包含进程执行上下文的核心转储文件;这个文件可用于调试。
      • Ignore:信号被忽略
      • Stop:进程被停止,即把进程置为TASK_STOPPED状态
      • Continue:如果进程被停止TASK_STOPPED,就把它置为TASK_RUNNING状态。
    • 通过调用相应的信号处理程序捕获信号。
  • 对一个信号的阻塞和忽略是不同的:只要信号被阻塞,它就不被传递;只有在信号解除阻塞后才传递它。而一个被忽略的信号总是被传递,只是没有进一步的操作。

  • 不可能给进程0(swapper)发送信号,而发送给进程1(init)的信号在捕获到它们之前也总是被丢弃。因此进程0永不死亡,而进程1只有当init程序终止时才死亡。

  • 如果一个挂起信号被发送给了某个特定进程,那么这个信号是私有的,如果被发送给了整个线程组,它就是共享的。

  • 相关数据结构

    • 信号描述符:进程描述符的signal字段指向信号描述符(signal descriptor)–一个 signal_struct类型的数据结构,用来跟踪共享挂起信号
    • 信号处理程序描述符(signad handler deseriplor),它是一个sighand_struct类型的数据结构,用来描述每个信号必须怎样被线程组处理。
    • sigaction:信号的特性存放在k_sigsction结构中,k_sigaction结构既包含对用户态进程所隐藏的特性,也包含sigaction,该结构保存了对用户态进程能看见的所有特性。
    • 挂起信号队列:
      • 共享挂起队列:存放整个线程组的挂起信号。信号描述符shared_pending
      • 私有挂起队列:存放特定进程(轻量级进程)的挂起信号。进程描述符的pending
  • 内核在允许进程恢复用户态下的执行之前,检查进程TIF_SIGPENDING标志的值。每当内核处理完一个中断或异常时,就检查是否存在挂起信号。

    • 为了处理非阻塞的挂起信号,内核调用do_signal函数。
    • do_signal()函数的核心由重复调用dequeue_signal()函数的循环组成,直到在私有挂起信号队列和共享挂起信号队列中没有非阻塞的挂起信号时,循环才结束。
    • 如果信号没有设置专门的处理程序,则执行缺省操作,否则do_signal()函数必须强迫执行该处理程序。
  • 系统调用的重新执行:内核并不总是立即能满足系统调用发出的请求,在这种情况下,把发出系统调用的进程置为TASK_INTERRUPRIBLE或TASK_UNINTERRUPRIBLE状态。

    • 如果进程处于TASK_INTERRUPRIBLE状态,某个进程向它发送一个信号,那么。内核不完成系统调用就把进程置为TASK_RUNNING状态。当切换回用户态时信号被传递给进程。当这种情况发生时,系统调用服务例程没有完成它的工作,但返回EINTR、ERESTARTNOHAND、ERESTART_RESTARTBLOCK、ERESTRTSYS、或ERESTARTNOINTR错误码。这种情况下用户态进程获得的唯一错误码是EINTR,这个错误码表示系统调用还没有执行完(应用程序的编写者可以测试这个错误码并决定是否重新发出系统调用)。内核内部使用剩余的错误码来指定信号处理程序结束后是否自动重启系统调用。
  • 相关系统调用

    • kill()系统调用向普通进程或多线程应用发送信号,其相应的服务例程是sys_kill()函数。
    • tkill()和tgkill()向线程组中指定进程发送信号。
    • sigaction(aig,act,oact)系统调用允许用户为信号指定一个操作。如果没有,内核执行与传递信号相关的缺省操作。
    • sigpending()允许进程检查挂起的阻塞信号的集合。
    • sigprocmask()允许进程修改阻塞信号的集合。
    • sigsuspend()把进城置为TASK_INTERRUPRIBLE状态,挂起进程,这是把mask参数指向的为掩码数组所指定的标准信号阻塞以后设置的。只有当一个非忽略、非阻塞的信号发送到进程以后,进程才被唤醒。
    • 实时信号的系统调用:rt_sigaction()、rt_sigpending()、rt_sigprocmask()、rt_sigsuspend()

第十二章 虚拟文件系统

  • 虚拟文件系统VFS是一个内核软件层,用来处理与Unix标准文件系统相关的所有系统调用。
  • VFS支持的文件系统类型:
    • 磁盘文件系统:管理在本地磁盘分区中可用的存储空间或其他起到磁盘作用的设备(如USB闪存)
    • 网络文件系统:其他网络计算机的文件系统所包含的文件,
    • 特殊文件系统:不管理本地或者远程文件系统,如/proc
  • 通用文件模型由下列对象类型组成:
    • 超级块对象(superblock object):存放已安装文件系统的有关信息。对于基于磁盘的文件系统,这类对象通常对应于存放在磁盘上的文件系统控制块。
    • 索引节点对象(inode object):存放关于具体文件的一般信息。对于磁盘文件系统,这类对象通常对应于存放在磁盘的文件控制块。每个索引节点对象都有一个索引节点号,这个节点号唯一地标识文件系统的文件。
    • 文件对象(file object):存放打开文件与进程之间交互的有关信息。这类信息仅当进程访问文件期间存在于内核内存中。
    • 目录项对象(dentry object):存放目录项(也就是文件的特地名称)与对应文件进行连接的有关信息。每个磁盘文件系统都以自己特有的方式将该类信息存在磁盘上。状态:
      • 空闲状态free:处于该状态的目录项对象不包含有效的信息,且还没有被VFS使用。对应的内存区由slab分配器处理
      • 未使用状态unused:处于该状态的目录项对象当前还没有被内核使用。该对象的d_count的值为0,但其d_inode字段仍然指向关联的索引节点。该目录项包含有效的信息,但是为了必要时回收内存,内容可能被丢弃。
      • 正在使用状态in use:目录项对象正在被内核使用。d_count值为正数,d_inode指向关联的索引节点对象
      • 负状态negative:与目录项关联的索引节点不复存在,那是因为相应的磁盘索引节点已被删除,或者因为目录项对象是通过解析一个不存在文件的路径名创建的。
  • 每个VFS对象都存放在一个适当的数据结构中,其中包括对象的属性和指向对象方法表的指针。
  • 文件对象描述进程怎样与一个打开的文件进行交互。文件对象是在文件被打开时创建的,由一个file结构组成。注意,文件对象在磁盘上没有对应的映像,因此,file结构中没有“脏”字段来表示文件对象是否已被修改。
  • 目录项高速缓存:
    • 一个处于正在使用、未使用或负状态的目录项对象集合
    • 一个散列表,从中能够快速获取与给定文件名和目录名对应的目录项对象。同样,如果访问的对象不在目录项高速缓存中,则散列函数返回一个空值。
  • 每个进程都有它自己当前的工作目录和它自己的根目录。进程当前打开的文件。
  • 特殊文件系统不限于物理块设备。内核给每个安装的特殊文件系统分配一个虚拟的块设备,让其主设备号为0而次设备号具有任意值。
  • 作为一个目录树,每个文件系统都拥有自己的根目录。安装文件系统的这个目录称为安装点(mount point)。已安装文件系统属于安装点目录的一个子文件系统。已安装文件系统的根目录隐藏了父文件系统的安装点目录原来的内容,而且父文件系统的整个子树位于安装点之下。
  • 每个进程拥有自己的已安装文件系统树----叫做进程的命名空间namespace。通常大多数进程共享一个命名空间,即位于系统根文件系统且被init进程使用已安装文件系统树。不过,如果clone()系统调用以CLONE_NEWNS标志创建一个新进程,那么进程将获得一个新的命名空间。这个新的命名空间随后由子进程继承(如果父进程没有以CLONE_NEWNS标志创建这些子进程)。
  • 安装普通文件系统:
    • mount()
    • do_kern_mount():检查文件系统标志以决定安装操作是如何完成的
    • 分配超级块对象
  • 安装根文件系统:
    • 内核安装特殊rootfs文件系统,该文件系统仅提供一个作为初始化安装点的空目录。在系统初始化过程中执行。
    • 内核在空目录上安装实际根文件系统。内核在系统初始化即将结束时进行的。
  • umount()系统调用卸载一个文件系统。
  • 文件加锁:劝告锁和强制锁

第十三章 I/O体系模型和设备驱动程序

  • 两条高速总线用于在内存上来回传输数据:前端总线将CPU连接到RAM控制器上,而后端总线将CPU直接连接到外部硬件的高速缓存上。
  • 任何I/O设备有且仅能连接一条总线。
  • CPU和I/O设备之间的数据通路通常称为I/O总线。每个I/O设备依次连接到I/O总线上,这种连接使用了包含3个元素的硬件组织层次:I/O端口、接口和设备控制器。
    • 地址总线负责寻址
    • 数据总线传输数据
  • I/O端口:每个连接到I/O总线上的设备都有自己的I/O地址集,通常称为I/O端口。
    • 有四条专用的汇编语言指令可以允许CPU和I/O端口进行读写,它们是in、ins、out和outs。在执行其中一条指令时,CPU使用地址总线选择所请求的I/O端口,使用数据总线在CPU寄存器和端口之间传输数据。
    • I/O端口还可以被映射到物理地址空间。因此,处理器和I/O设备之间的通信就可以使用对内存直接进行操作的汇编语言指令。现代的硬件设备更倾向于映射I/O,因为这样处理的速度较快,并可以和DMA结合使用。
    • 系统设计者主要目的是对I/O编程提供统一的方法,而又不牺牲性能。为了此目的,每个设备的I/O端口都被组织成一组专用寄存器:CPU把要发送给设备的命令写入设备控制寄存器,并从设备状态寄存器中读取表示设备内部状态的值。CPU还可以通过读取设备输入寄存器的内容从设备取得数据,也可以通过向设备输出寄存器中写入字节而把数据输出到设备。
    • 内核通过”资源“来记录分配给每个硬件设备的I/O端口。资源表示某个实体的一部分,这部分互斥地分配给设备驱动程序。
  • I/O接口:处于一组I/O端口和对应的设备控制器之间的一种硬件电路。它起翻译器的作用,即把I/O端口中的值转换成设备所需要的命令和数据。在相反的方向上,它检测设备状态的变化,并对起状态寄存器作用的I/O端口进行相应的更新。还可以通过一条IRQ线把这种电路连接到可编程中断寄存器上,以使它代表相应的设备发出中断请求。
    • 专用I/O接口:专门用于一个特定的设备硬件。
      • 键盘接口:连接到一个键盘控制器上,这个控制器包含一个专用的微处理器。这个微处理器对按下的组合键进行译码,产生一个中断并把相应的键盘扫描码写入输入寄存器。
      • 图形接口、磁盘接口、网络接口
    • 通用I/O接口:用来连接多个不同的硬件设备。
      • 并口:数据以每次一字节(8位)为单位进行。打印机、可移动磁盘、扫描仪等
      • 串口:数据传输逐位进行。通常用于不需要高速操作的外部设备
      • USB
  • 设备控制器:复杂的设备可能需要一个设备控制器来驱动。作用如下
    • 对I/O接口接收到的高级命令进行解释,并通过向设备发送适当的电信号序列强制设备执行特定的操作。
    • 对从设备接收到的电信号进行转换和适当地解释,并修改(通过I/O接口)状态寄存器的值。
  • 系统设备驱动模型:为系统所有的总线、设备以及设备驱动程序提供了一个统一的视图。
  • sysfs文件系统的目标是展现设备驱动程序模型组件间的层次关系。
  • 设备文件:把I/O设备当做设备文件来处理。与普通文件读写的系统调用一致。
    • 块设备:随机访问,传输任何数据块所需的时间都是较少且大致相同。
    • 字符设备:不可随机访问,或者可随机访问,但是访问随机数据所需时间很大程度依赖于数据在设备内的位置(如磁带驱动器)
  • 设备标识符由设备文件类型(字符或块)和一对参数组成。第一个参数为主设备号,标识了设备的类型,通常,具有同主设备号和类型的所有设备文件共享相同的文件集合,因为它们是由同一设备驱动程序处理的。第二个参数为次设备号,标识了和主设备号相同设备组中的一个特定设备。mknod()系统调用创建设备文件
  • 设备驱动程序是内核例程的集合,它使得硬件设备响应控制设备的编程接口,而该接口是一组规范的VFS函数集。这些函数的实际实现由设备驱动程序全权负责。
  • 设备驱动程序应当尽快被注册,以便用户态应用程序能通过相应的设备文件使用它。相反,设备驱动程序在最后可能的时刻才被初始化。事实上,初始化驱动程序意味着分配宝贵的系统资源,这些资源因此就对其他驱动程序不可用了。
    • 引用计数器记录当前访问设备文件的进程数。在设备文件的open方法中计数器被增加,在release方法中被减少。
    • open方法在增加引用计数器的值之前先检查它。如果计数器为0,则设备驱动程序必须分配资源并激活硬件设备上的中断和DMA
    • release方法在减少计数器的值之后检查它,如果计数器为0,说明没有进程使用这个硬件设备。如果是这样,该方法禁用I/O控制器上的中断和DMA,然后释放所分配的资源。
  • 监控I/O操作
    • 轮询模式:CPU重复检查(轮询)设备的状态寄存器,直到寄存器的值表明I/O操作已经完成为止
    • 中断模式:如果I/O控制器能够通过IRQ总线发出I/O操作结束的信号,那么中断模式才能使用。中断处理程序从设备输入寄存器中读字符,并把它存放在foo全局变量指向的驱动程序描述符的数据字段里。然后设置intr标志。并调用唤醒函数唤醒在foo->wait等待队列上阻塞的进程。
  • DMA一旦被激活,就可以自行传输数据,当数据传输完成之后,DMA发出一个中断请求。当CPU和DMA同时访问同一内存单元时,所产生的冲突由一个名为内存仲裁器的硬件电路来解决。
    • 同步DMA:数据的传送由进程触发。如声卡
    • 异步DMA:数据传送由硬件设备触发:如网卡
    • 总线地址:它是除CPU之外的硬件设备驱动数据总线时所用的存储器地址。在DMA操作中,数据传输不需要CPU的参与:I/O设备和DMA电路直接驱动数据总线。因此,当内核开始DMA操作时,必须把所涉及的内存缓冲区总线地址或写入DMA适当的I/O端口,或写入I/O设备适当的I/O端口。

第十四章 块设备驱动程序

  • 块设备的特点:CPU和总线读写数据所花时间与磁盘硬件的速度不匹配。
  • 块设备操作
  • 块设备的处理:以read()系统调用为例
    • read()系统调用的服务例程调用一个适当的VFS函数,将文件描述符和文件内的偏移量传递给它。虚拟文件系统位于块设备处理体系结构的上层,它提供一个通用的文件模型,Linux支持所有的文件系统均采用该模型。
    • VFS函数确定所请求的数据是否已经存在,如果有必要的话,它决定如何执行read操作。有时候没有必要访问磁盘上的数据,因为内核将大多数最近从块设备读出或写入其中的数据保存在RAM中。
    • 我们假设内核从块设备读数据,那么它就必须确定数据的物理位置。为了做到这一点,内核必须依赖于映射层(mapping layer),主要执行下面两步:
      • 内核确定该文件所在文件系统的块大小,并根据文件块的大小计算所请求数据的长度。本质上,文件被看做拆分成许多块,因此内核确定请求数据所在的块号(文件开始位置的相对索引)。
      • 接下来,映射层调用一个具体文件系统的函数,它访问文件的磁盘节点,然后根据逻辑块号确定所请求数据在磁盘上的位置。事实上,磁盘也被看做拆分成许多块,因此内核必须确定存放所请求数据的块对应的号(磁盘或分区开始位置的相对索引)。由于一个文件可能存储在磁盘上的不连续块中。因此存放在磁盘索引节点中的数据结构将每个文件块号映射为一个逻辑块号。
    • 现在内核可以对块设备发出读请求。内核利用通用块层启动I/O操作来传送所请求的数据。一般而言,每个I/O操作只针对磁盘上一组连续的块。由于请求的数据不必位于相邻的块中,所以通用块层可以启动几次I/O操作。每次I/O操作是由一个"块I/O"(简称"bio")结构描述,它收集底层组件需要的所有信息以满足所发出的请求。
    • 通用块层为所有的块设备提供了一个抽象视图,因而隐藏了硬件块设备间的差异性。几乎所有的块设备都是磁盘,所以通用块层也提供了一些通用数据结构来描述"磁盘"或"磁盘分区"。
    • 通用块层下面的"I/O调度程序"根据预先定义的内核策略将待处理的I/O数据传送请求进行分类。调度程序的作用是把物理介质上相邻的数据请求聚集在一起。
    • 最后,块设备驱动程序向磁盘控制器的硬件接口发送适当的命令,从而进行实际的数据传送。
  • 硬件块设备控制器采用称为“扇区”的固定长度的块来传送数据。因此。I/O调度程序和块设备驱动程序必须管理数据扇区。
  • 虚拟文件系统、映射层和文件系统将磁盘数据存放在"块"的逻辑单元里。一个块对应文件系统单元中一个最小的磁盘存储单元。
  • 块设备驱动程序应该能够处理"段":一个段就是一个内存页或内存页的一部分,它们包含磁盘上物理相邻的数据块。
  • 磁盘高速缓存作用于磁盘数据的"页"上,每页正好装在一个页框里。
  • 通用块层将所有上层和下层的组件组合在一起,因此,它了解数据额扇区、块、段以及页。
  • 扇区是硬件设备传送数据的基本单位,而块是VFS和文件系统传送数据的基本单位。
  • 分散-聚集DMA传送方式:磁盘可以与一些非连续内存区相互传送数据。
    • 启动一次这样的传送,块设备驱动程序需要向磁盘控制器发送:
      • 要传送的起始磁盘扇区号和总的扇区数
      • 内存区的描述符链表,其中链表的每项包含一个地址和一个长度
    • 磁盘控制器负责整个数据传送;例如,在读操作中控制器从相邻磁盘扇区中获取数据,然后将它们存放到不同的内存区中。
    • 为了使用分散-聚集DMA的传送方式,块设备驱动程序必须能够处理称为段的数据存储单元。一次DMA操作可能同时传送几个段。
  • 通用块层:处理来自系统中的所有块设备发出的请求。
    • 可以做到如下功能:
      • 将数据缓冲区放在高端内存–仅当CPU访问其数据时,才将页框映射为内核中的线性地址空间,并在数据访问完后取消映射。
      • 通过一些附加手段,实现一个所谓的"零-复制"模式,将磁盘数据直接存放在用户态地址空间而不是首先 复制到内核内存区;事实上,内核为I/O数据传送所使用的的缓冲区所在的页框就映射在进程的用户态线性地址空间中。
      • 管理逻辑卷,例如由LVM(逻辑卷管理器)和RAID使用的逻辑卷:几个磁盘分区,即使位于不同的块设备中,也可以被看做是一个单一的分区。
      • 发挥大部分新磁盘控制器的高级特性,例如大主板磁盘高速缓存、增强的DMA性能、I/O传送请求的相关调度等。
    • 核心数据结构:bio的描述符,描述了块设备的I/O操作。
  • 磁盘是一个由通用块层处理的逻辑块设备。
  • I/O调度程序:
    • 内核总是试图把几个扇区合并在一起,并作为一个整体来处理,这样就减少了磁头的平均移动时间。
    • 内核读写磁盘数据时,创建一个块设备请求。但并不是请求一发出,内核就满足它,I/O操作仅仅被调度,执行会向后推迟。这种人为的延迟是提高块设备性能的关键机制。当请求传送一个新的数据块时,内核检查能否通过稍微扩展前一个一直处于等待状态的请求而满足新请求(也就是说,能否不用进一步的寻道操作就能满足需求)。
    • 假设某个进程打开一个普通文件,然后,文件系统的驱动程序就要从磁盘读取相应的索引节点。块设备驱动程序把这个请求加入一个队列,并把这个进程挂起,直到存放索引节点的块被传送为止。然而,块设备驱动程序本身不会被阻塞,因为试图访问同一磁盘的任何其他进程也可能被阻塞。
    • 为了防止块设备驱动程序被挂起,每个I/O操作都是异步处理的。特别是块设备驱动程序是中断驱动的:通用块层调用I/O调度程序产生一个新的块设备请求或扩展一个已有的块设备请求,然后终止。随后激活的块设备驱动程序会调用一个所谓的策略例程选择一个待处理的请求,并向磁盘控制器发出一条适当的命令来满足这个请求。当I/O操作终止时,磁盘控制器就产生一个中断,如果需要,相应的中断处理程序就又调用策略例程去处理队列中的另一个请求。
    • 每个块设备驱动程序都维持着自己的请求队列,它包含设备待处理的请求链表。如果磁盘控制器正在处理几个磁盘,那么通常每个物理块设备都有一个请求队列。在每个请求队列上单独执行I/O调度,这样可以提高磁盘性能。
    • 每个请求队列都有一个允许处理的最大请求数。缺省情况下,一个队列待读和待写请求分别为128个。一个填满的请求队列对系统性能有负面影响,因为它会强制许多进程去睡眠以等待I/O数据传输的完成。

第十五章 页高速缓存

  • 磁盘高速缓存是一种软件机制,它允许系统把通常存放在磁盘上的一些数据保留在RAM中,以便对那些数据的进一步访问不用再访问磁盘而能尽快得到满足。
  • 读磁盘时,如果页不在高速缓存中,新页就被加到高速缓存中,然后用从磁盘读出的数据填充它,如果内存有足够的空闲空间,就让该页在高速缓存中长期保留
  • 写磁盘时,内核首先检查对应的页是否已经在高速缓存中;如果不在,就要先在其中增加一个新项,并用要写到磁盘中的数据填充该项。I/O数据传输并不是马上开始,而是要延迟几秒之后才对磁盘进行更新,从而使进程有机会对要写入磁盘的数据做进一步的修改
  • 页高速缓存中的每个页所包含的数据肯定属于某个文件。
  • 页高速缓存操作:基树的操作
    • 查找页:find_get_page()
    • 增加页:add_to_page_cache()
    • 删除页:remove_from_page_cache()
    • 更新页:read_cache_page()确保高速缓存中包括最新版本的指定页。
  • 页高速缓存不经允许内核快速获取含有块设备中指定数据的页,还允许内核从高速设备中快速获取给定状态的页(PG_dirty)。
  • 缓冲区页:保存在页高速缓存中。
    • 每个块缓冲区都有buffer_head类型的缓冲区首部描述符。该描述符包含内核必须了解的、有关如何处理块的所有信息。因此,在对所有块操作之前,内核检查缓冲区首部。
    • 缓存区首部的b_count字段是相应的块缓冲区的引用计数器。在每次对块缓冲区进行操作之前递增计数器并在操作之后递减它。
    • 只要内核必须单独地访问一个块,就要涉及存放缓冲区的缓冲区页,并检查相应的缓冲区首部。
  • pdflush内核线程:刷新脏页到磁盘
    • /proc/sys/vm/dirty_background_ratio 当脏页数超过这个比值,触发pdflush内核线程刷新
    • /proc/sys/vm/dirty_writeback_centisecs: 这个参数的值是个时间值,以百分之一秒为单位,缺省值是500,也就是5秒钟。它表示每5秒钟会唤醒内核的flush线程来处理dirty pages。
  • sync()允许进程把所有的脏缓冲区刷新到磁盘
  • fsync()允许进程把属于特定打开文件的所有块刷新到磁盘
  • fdatasync()与fsync类型,但不刷新文件的索引节点块。

第十六章 访问文件

  • 读文件是基于页的,内核一次传送几个完整的数据页。如果进程发出read()系统调用来读取一些字节,而这些数据还不在RAM中,那么,内核就要分配一个新页框,并调用来读取一些字节,而这些数据还不在RAM中,那么,内核就要分配一个新页框,并使用文件的适当部分来填充这个页,把该页加入页高速缓存,最后把所请求的字节拷贝到进程地址空间中
    • do_generic_file_read()从磁盘读入所请求的页,并把它们拷贝到用户态缓冲区
    • readpage把一个个页从磁盘读到内存
  • 文件的预读:在实际请求前读普通文件或块文件的几个相邻的数据页。预读算法使用下面两个页面集:
    • 当前窗内的页是进程请求的页和内核预读的页,且位于高速缓存内(当前窗内的页不必是最新的,因为I/O数据传输仍可能在运行中)。当前重包含进程顺序访问的最后一页,且可能有内核预读但进程未请求的页
    • 预读窗内的页紧接着当前窗内的页,它们是内核正在预读的页。预读窗内的页都不是进程请求的,但内核假定进程会迟早请求。
  • 写入文件:把数据从调用进程的用户态地址空间中移动到内核数据结构中,然后再移动到磁盘上。一般写文件都是一个过程,该过程主要标识写操作所涉及的磁盘块,把数据从用户态地址空间拷贝到页高速缓存的某些页中,然后把这些页中的缓冲区标记为脏。
  • 把脏页刷到磁盘:在基树中寻找脏页,并把它们刷新到磁盘。
  • 内存映射:内核把对区线性中页内某个字节的访问转换成对文件中相应字节的操作。分为共享型和私有型
    • 内存映射创建之后并没有立即把页框分配给它,而是尽可能向后推迟到不能再推迟–也就是说,当进程试图对其中一页进行寻址时,就产生一个"缺页"异常。
  • 直接I/O传送:内核对磁盘控制器进行编程,以便在自缓存的应用程序的用户态地址空间中的页与磁盘之间直接传送数据。
  • 异步I/O:当用户态进程调用库函数读写文件时,一旦读写操作进入队列函数就结束,甚至有可能真正的I/O数据传输还没有开始。应用程序稍后调用系统提供的I/O状态函数来检查完成状况。

第十七章 回收页框

  • 页框回收算法的目的之一就是保存最小的空闲页框池以便内核可以安全地从"内存紧缺"的情形中恢复过来。
  • PFRA设计的几个原则
    • 首先释放无害页:在进程用户态地址空间的页回收之前,必须先回收没有被任何进程使用的磁盘与内存高速缓存中的页。实际上,回收磁盘与内存高速缓存的页框并不需要修改任何页表项。
    • 将用户态进程的所有页定为可回收页:除了锁定页,PFRA必须能窃得任何用户态进程页,包括匿名页。这样,睡眠较长时间的进程将逐渐失去所有页框。
    • 同时取消引用一个共享页框的所有页表项的映射,就可以回收该共享页框。为达这一目的,Linux 2.6内核能够快速定位指向同一页看的所有页表项。这一过程叫反向映射(reverse mapping)。
    • 只回收"未用"页:使用简化的最近最少使用LRU置换算法,PFRA将页分为"在用in_use"与"未用unused"
  • 页面回收算法(PFRA)执行的三种基本情形
    • 内存紧缺回收:内核发现内存紧缺
      • grow_buffers()无法获得新的缓冲区页
      • alloc_page_buffers()无法获得页临时缓冲区首部
      • __alloc_pages()函数无法在给定的内存管理区中分配一组连续页框
    • 睡眠回收:在进入suspend-to-disk状态时,内核必须释放内存
    • 周期回收:必要时,周期性激活内核线程执行内存回收算法
      • kswapd内核线程,它检查某个内存管理区中空闲页框数是否低于pages_high值的标高
      • events内核线程,它是预定义工作队列的工作者线程;PFRA周期性地调度预定义工作队列中的一个任务执行,从而回收slab分配器处理位于内存高速缓存中的所有空闲slab
  • 内存不足删除程序OOM
  • 交换标记(swap token):把它赋给系统中的单个进程,该标记可以使该进程免子页框回收,所以进程可以实质性运行,而且即使内存十分稀少,也有希望运行至结束。
  • 交换(swapping)用来为非映射页在磁盘上提供备份。
    • 交换子系统的主要功能:
      • 在磁盘上建立交换区swap area,用于存放没有磁盘映像的页。
      • 管理交换区间。当需求发生时,分配和释放页槽(page slot)
      • 提供函数用于从RAM中把页换出(swap out)到交换区或从交换区换入(swap in)到RAM中。
      • 利用页表项(现已被换出的换出页页表项)中的换出页标识符跟踪数据在交换区中的位置。
    • 交换式页回收的一个最高级特性。如果我们要确保进程所有的页框都能被PFRA随意回收,而不仅仅是回收有磁盘映像的页,那么就必须使用交换。
    • 交换可以用来扩展内存地址空间,使之被用户态进程有效地利用。

第十八章 Ext2和Ext3文件系统

  • 文件系统把磁盘块分为组。每组包含存放在相邻磁道上的数据块和索引节点。正是这种结构,使得可以使用较少的磁盘平均寻道时间对存放在一个单独块组中的文件进行访问。
  • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jabE148f-1625307427523)(http://jm.gongzicp.com/images/linux/ext4.webp)] 转自https://www.jianshu.com/p/27416740e781
  • dumpe2fs /dev/vda1|more
Group 0: (Blocks 0-32767) csum 0x5748 [ITABLE_ZEROED]
  Primary superblock at 0, Group descriptors at 1-13
  Reserved GDT blocks at 14-1024
  Block bitmap at 1025 (+1025), csum 0xeb47946c
  Inode bitmap at 1041 (+1041), csum 0xea3aa665
  Inode table at 1057-1568 (+1057)
  20447 free blocks, 81 free inodes, 2224 directories
  Free blocks: 10509, 10732, 10960-11035, 11080, 11165, 12068-12287, 12362, 12364-12512, 12514-15103, 15360-26622, 26624-32767
  Free inodes: 679, 681, 723, 743, 831, 852, 854, 856, 996, 1038, 1040, 1351, 1367, 1459, 1586, 1618, 1681, 1698, 1986, 2601, 2634, 2657, 2663, 2689-2692, 2
709, 2757, 2770-2773, 3301-3302, 3813, 3825, 3831, 3833-3838, 3856, 4024, 4046, 4049, 4051-4052, 4081, 4112, 4114, 4128, 4432, 4435, 4449-4450, 4452-4453, 4
455-4456, 4673, 5664, 6931-6936, 7247, 7357, 7580, 7584, 7586-7587, 7609, 7991, 8030, 8059, 8083
Group 1: (Blocks 32768-65535) csum 0xf907 [ITABLE_ZEROED]
  Backup superblock at 32768, Group descriptors at 32769-32781
  Reserved GDT blocks at 32782-33792
  Block bitmap at 1026 (bg #0 + 1026), csum 0x904728a0
  Inode bitmap at 1042 (bg #0 + 1042), csum 0x733e7aa6
  Inode table at 1569-2080 (bg #0 + 1569)
  22 free blocks, 3903 free inodes, 814 directories
  Free blocks: 56805, 56821-56823, 60068-60069, 60072-60082, 60462-60463, 63492, 64022-64023
  Free inodes: 8263-8301, 8310, 8319, 8332, 8341-8358, 8360, 12471-13481, 13520-13523, 13525, 13527-13531, 13533, 13535-13557, 13559-13560, 13566-13610, 136
15, 13617-13618, 13620-13704, 13707-13712, 13714-13728, 13730-13741, 13743-13747, 13749-13751, 13754-13908, 13911-14889, 14891-14902, 14907, 14909-14919, 14
922-16384
Group 8: (Blocks 262144-294911) csum 0x2eac [INODE_UNINIT, ITABLE_ZEROED]
  Block bitmap at 1033 (bg #0 + 1033), csum 0x84929ba8
  Inode bitmap at 1049 (bg #0 + 1049), csum 0x00000000
  Inode table at 5153-5664 (bg #0 + 5153)
  9837 free blocks, 8192 free inodes, 0 directories, 8192 unused inodes
  Free blocks: 262145-262149, 262151, 262154-262156, 262160-262242, 262244-262245, 262256-262634, 262638-262993, 263030, 263068-264191, 266240-266895, 26706
7-267068, 267071, 267076-268031, 268048, 268053-269247, 269250-269534, 269646-270335, 285755, 290816-294911
  Free inodes: 65537-73728
  • 超级块:磁盘信息
  • inode位图和数据块位图: 位图是位的索引,值为0 表示对应的索引节点块或数据块空闲,1表示占用。因为每个位图必须存放在一个单独的块中,又因为块的大小可以是1024、2048或4096字节,因此,一个单独的位图描述8192、16384或32768个块的状态。
  • inode列表: 索引节点表由一连串连续的块组成,其中每一块包含索引节点的一个预定义号和其他信息。
  • 数据块: 上面元数据之外的存储区域都成为数据块区域,这些区域作为文件扩展属性和文件内数据的存放容器。
  • 文件的洞file hole:是普通文件的一部分,它是一些空字符但没有存放在磁盘的任何数据块中。

第十九章 进程通信

  • 管道pipe是所有Unix都愿意提供的一种进程间通信机制。管道是进程之间的一个单向数据流:一个进程写入管道的所有数据都由内核定向到另一个进程,另一个进程由此就可以从管道中读取数据了。
    • 管道是作为一组VFS对象来实现的,因此没有对应的磁盘映像。在Linux2.6 中,把这些VFS对象组织为pipefs特殊文件系统以加速它们的处理。因为这种文件系统在系统目录树中没有安装点,因此用户根本看不到它。但是,有了pipefs,管道完全可以整合到VFS层,内核就可以以命名管道或FIFO的方式处理它们,FIFO是以终端用户认可的文件而存在的。
    • 无法打开已经存在的管道,这就使得任意两个进程不可能共享同一管道,除非管道由一个共同祖先进程创建。
  • 命名管道(FIFO或named pipe):最先写入文件的字节总是被最先读出的特殊文件类型。在文件系统中不拥有磁盘块,打开的FIFO总是与一个内核缓冲区相关联,这一缓冲区中存放两个或多个进程之间交换数据。
    • 因为FIFO文件名包含在系统的目录树中,有了磁盘索引节点,使得任何进程都可以访问FIFO。
  • IPC是进程间通信的缩写,通常用户态进程执行下列操作的一组机制:
    • 通过信号量与其他进程进行同步
    • 向其他进程发送消息或者从其他进程接收消息
    • 和其他进程共享一段内存区
  • IPC数据结构是在进程请求IPC资源(信号量、消息队列或者共享内存区)时创建的。每个IPC资源都是持久的:除非被进程显示地释放,否则永远驻留在内存中(直到系统关闭)。IPC资源可以由任一进程使用,包括那些不共享祖先进程所创建的进程。
  • IPC信号量:计数器,用来为多个进程共享的数据结构提供受控访问。
    • 可取消的信号量操作:如果一个进程突然放弃执行,那么它就不能取消已经开始执行的操作(例如,释放自己保留的信号量);因此通过把这些操作定义成可取消的,进程就可以让内核信号量返回到一致状态并允许其他进程继续执行。
    • 挂起的请求的队列:内核为每个IPC信号量都分配一个挂起请求队列,用来标识正在等待数组中的一个(或多个)信号量的进程。
  • IPC消息:进程彼此之间可以通过IPC消息进行通信。进程产生的每条消息都被发送到一个IPC消息队列中,这个消息一直存放在队列中直到另一个进程将其读走为止。消息是由固定长度你的首部和可变长度的正文组成的,可以使用一个整数值标识消息。这就允许进程有选择地从消息队列中获取消息。只要进程从IPC消息队列中读出一条消息,内核就把这个消息删除;因此,只有一个进程接收一条给定的消息。
  • IPC共享内存:这种机制允许两个或多个进程通过把公共数据结构放入一个共享内存区(IPC shared memory region)来访问它们。如果进程要访问这种存放在共享内存区的数据结构,就必须在自己的地址空间中增加一个新内存区,它将映射与这个共享内存区相关的页框。这样的页框可以很容易地由内核通过请求调页进行处理。
    • IPC共享内存与VFS紧密结合。具体来说,每个IPC共享内存区与属于shm特殊文件系统的一个普通文件相关联
    • 因为shm文件系统在系统目录树中没有安装点,因此,用户不能通过普通的VFS系统调用打开并访问它的文件。但是这要进程"附加"一个内存段,内核就调用do_mmap(),并在进程的地址空间创建文件的一个新的共享内存映射。因此,shm特殊文件系统的文件只有一个文件对象mmap,该方法是由shm_mmap()函数实现的。
  • POSIX消息队列:相比于"IPC消息",具有如下优点:
    • 更简单的基于文件的应用接口
    • 完全支持消息优先级(优先级最终决定队列中消息的位置)
    • 完全支持消息到达的异步通知,这通过信号或是线程创建实现
    • 用于阻塞发送和接收操作的超时机制

第二十章 程序的执行

  • 程序是以可执行文件的形式存放在磁盘上的,可执行文件既包括被执行函数的目标代码,也包括这些函数所使用的的数据。也可把进程定义为"执行上下文"。
  • 进程的信任状:
    • uid,gid 用户和组的实际标识符
    • euid,egid 用户和组的有效标识符
    • fsuid,fsgid 文件访问的用户和组的有效标识符
    • suid,sgid 用户和组保存的标识符
    • 当一个进程被创建时,总是继承父进程的信任状。通常情况下,进程的uid,euid,fsuid及suid字段具有相同的值。然而,当进程执行setuid程序时,及可执行文件的setuid标志被设置时,euid和fsuid字段被设置为这个文件拥有者的标识符。几乎所有的检查都涉及这两个字段中的一个:fsuid用于与文件相关的操作,而euid用于其他所有的操作
  • 进程权能capablity:一种权能仅仅是一个标志,它表明是否允许进程执行一个特定的操作或一组特定的操作。这个模型不同于传统的"超级用户VS普通用户"模型,在后一种模型中,一个进程要么能做任何事情,要么什么都不能做,这取决于它的有效UID。
    • 权能的主要优点是,任何时候每个进程只需要有限种权能。因此,即使有恶意的用户发现一种利用有潜在错误的程序的方法,他也只能非法地执行有限个操作类型。
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
当着手翻译第三时,我不由得回想起开始接触Linux 的那投日子。 几年前,当我们拿到Linux 内核代码开始研究时,可以说茫然无措。其规模之大,叫“覆 压三百余里,隔离天日”似乎不为过;其关系错综复杂,叫"廊腰线回,檐牙高啄,各 抱地势,勾心斗角”也非言过其实。阿房宫在规模和结构上给人的震撼可能与Linux 有 异曲同工之妙。“楚人一炬,可怜焦土”,可能正是因为它的结构和规模,阿房宫在中国 两十多年矗极的计建历史中终于没有再现,只能叫后人扼腕叹息;但是, Linux 却实实 在在地矗立在我们面前,当我们徘徊在这宏伟宫殿之前时,攻许,我们也需要火炬 不是用来效灭,而是为了照亮勇者脚下的征途。 Linus Torvalds 在我们面前展现的Linux 魔法卷轴,让我们的视野进入一个自由而开放 的新世界。自由意味着自我价值的实现,开放代表着团结协作的理想,这对于从没把握 过核心操作系统的中国人来说,无疑燃起了心中的梦想。于是,许多人毫不犹豫地走进 来了,希望深入到那散发自由光彩、由众人团结协力搭造起的殿堂。但是很快,不少人 迻缩了。面对这样一个汪洋大诲,有的人迷惑了,出诲的航道在哪里?有的人倒下了. 漫漫征途何时是尽头?我常常想,如果那时他们手中就有这本书的话…… Daniel P.Bovet 和Marco Cesati 携手为我们打造了这本浅无巨著,自此我们有了火把, 有了航诲图,于是我们就有了彼岸,有了航道,也有了补给码头。不是吗?中断虽繁, 但笫四、六两章切中肯紧的剖析,肯定能让你神清气爽;内存管双虽淮, 但多达三章细 致入微的说理一定会让你茅塞顿开。内容的组织更是别具匠心,每章开始部分一般性原 理的描述打破了知识的局限,将每个部分的全景展现在你面前。而针对每个知识点芯到 实处的独到分析,又会使你沉迷于知识的社会贯通之中。第三Linux 2.6 的全面描 述会使你为2.4 与2.6 之间的沟壑而感叹`但请放心,你曾从Linux本中荻取的点滴 依然是你前进的基石。总之,你面对的不再是赤裸裸的代码,而是真正能雅俗共赏的艺 术。 对整

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值