【book】linux内核设计与实现

目录

1.进程

2.系统调用

3.中断处理

4.描述一下缺页中断

5.signal:

6.内核态与用户态的切换

7.定时器和时间管理

8.文件系统


《linux内核设计与实现》学习笔记

1.进程

    内核把进程的列表存放在叫做任务队列的双向循环链表中,链表的每一个节点都是类型为task_struct(称为进程描述符)的结构。

在32位机器上,一个task_struct约为1.7KB,其中包含的数据能完整地描述一个正在执行的程序:它打开的文件、进程的地址空间、挂起的信号、进程的状态等

                                

进程的常见状态之间的转化:

                                     

以下文字来自进程的D和S状态

Linux进程状态:S (TASK_INTERRUPTIBLE),可中断的睡眠状态。

处于这个状态的进程因为等待某某事件的发生(比如等待socket连接、等待信号量),而被挂起。这些进程的task_struct结构被放入对应事件的等待队列中。当这些事件发生时(由外部中断触发、或由其他进程触发),对应的等待队列中的一个或多个进程将被唤醒。

通过ps命令我们会看到,一般情况下,进程列表中的绝大多数进程都处于TASK_INTERRUPTIBLE状态(除非机器的负载很高)。毕竟CPU就这么一两个,进程动辄几十上百个,如果不是绝大多数进程都在睡眠,CPU又怎么响应得过来。

Linux进程状态:D (TASK_UNINTERRUPTIBLE),不可中断的睡眠状态。

与TASK_INTERRUPTIBLE状态类似,进程处于睡眠状态,但是此刻进程是不可中断的。不可中断,指的并不是CPU不响应外部硬件的中断,而是指进程不响应异步信号。
绝大多数情况下,进程处在睡眠状态时,总是应该能够响应异步信号的。否则你将惊奇的发现,kill -9竟然杀不死一个正在睡眠的进程了!于是我们也很好理解,为什么ps命令看到的进程几乎不会出现TASK_UNINTERRUPTIBLE状态,而总是TASK_INTERRUPTIBLE状态。

而TASK_UNINTERRUPTIBLE状态存在的意义就在于,内核的某些处理流程是不能被打断的。如果响应异步信号,程序的执行流程中就会被插入一段用于处理异步信号的流程(这个插入的流程可能只存在于内核态,也可能延伸到用户态),于是原有的流程就被中断了。(参见《linux内核异步中断浅析》)
在进程对某些硬件进行操作时(比如进程调用read系统调用对某个设备文件进行读操作,而read系统调用最终执行到对应设备驱动的代码,并与对应的物理设备进行交互),可能需要使用TASK_UNINTERRUPTIBLE状态对进程进行保护,以避免进程与设备交互的过程被打断,造成设备陷入不可控的状态。这种情况下的TASK_UNINTERRUPTIBLE状态总是非常短暂的,通过ps命令基本上不可能捕捉到。

linux系统中也存在容易捕捉的TASK_UNINTERRUPTIBLE状态。执行vfork系统调用后,父进程将进入TASK_UNINTERRUPTIBLE状态,直到子进程调用exit或exec(参见《神奇的vfork》)。

进程为什么会被置于uninterruptible sleep状态呢?处于uninterruptiblesleep状态的进程通常是在等待IO,比如磁盘IO,网络IO,其他外设IO,如果进程正在等待的IO在较长的时间内都没有响应,那么就很会不幸地被ps看到了,同时也就意味着很有可能有IO出了问题,可能是外设本身出了故障,也可能是比如挂载的远程文件系统已经不可访问了(由down掉的NFS服务器引起的D状态)。

正是因为得不到IO的相应,进程才进入了uninterruptible sleep状态,所以要想使进程从uninterruptiblesleep状态恢复,就得使进程等待的IO恢复,比如如果是因为从远程挂载的NFS卷不可访问导致进程进入uninterruptiblesleep状态的,那么可以通过恢复该NFS卷的连接来使进程的IO请求得到满足。

D状态,往往是由于 I/O 资源得不到满足,而引发等待,在内核源码 fs/proc/array.c 里,其文字定义为“ "D (disk sleep)", /* 2 */ ”(由此可知 D 原是Disk的打头字母),对应着 include/linux/sched.h 里的“ #define TASK_UNINTERRUPTIBLE 2 ”。举个例子,当 NFS 服务端关闭之时,若未事先 umount 相关目录,在 NFS 客户端执行 df 就会挂住整个登录会话,按 Ctrl+C 、Ctrl+Z 都无济于事。断开连接再登录,执行 ps axf 则看到刚才的 df 进程状态位已变成了 D ,kill -9 无法杀灭。正确的处理方式,是马上恢复 NFS 服务端,再度提供服务,刚才挂起的 df 进程发现了其苦苦等待的资源,便完成任务,自动消亡。若 NFS 服务端无法恢复服务,在 reboot 之前也应将 /etc/mtab 里的相关 NFS mount 项删除,以免 reboot 过程例行调用 netfs stop 时再次发生等待资源,导致系统重启过程挂起。

 

抢占和上下文切换:

上下文切换,就是从一个可执行进程切换到另一个可执行进程,由kernel/sched.c中的context_switch()函数负责处理,每当一个新的进程被选出来准备投入运行的时候,schedule()就会调用该函数。它完成了两项基本的工作:

    1.调用声明在<asm/mmu_context.h>中的switch_mm(),该函数负责把虚拟内存从上一个进程映射切换到新进程中。

    2.调用声明在<asm/system.h>中的switch_to(),该函数负责从上一个进程的处理器状态切换到新进程的处理器状态,这包括保存、恢复栈信息和寄存器信息,还有其他任何与体系结构相关的状态信息,都必须以每个进程为对象进行管理和保存。

                                    

进程的地址空间:

    linux是一个基于虚拟内存的操作系统,内核表示进程空间用(mm_struct),内核表示该空间中的内存区域用(结构体vm_area_struct),内核创建和撤销这些内存区域分别用mmap()和munmap()。

    地址空间中内一个vm_area_struct[每次mmap()创建地址空间都会新建一个,或是与现有的一个合并起来公用一个vm_area_struct]都被内存描述符mm_struct中的mmap[单链表串起来]和mm_rb[红黑树管理]来管理。

 

一个二级页表的例子:

假设采用X86机器下的2级分页表的形式管理地址转换,

MMU 将虚拟地址转换成物理地址的方式是,32位机器上的虚拟地址中,取虚拟地址的 22~31bits 表示页目录的下标,获得页目录项定位到页表,再取 12~21bits 表示页表的下标,获得页表项定位到页,最后取 0~11bits 表示页内偏移。页目录项和页表项的下标分别用 10bits 表示,刚好最大 1024 项,页内偏移用 12bits 表示,刚好 4KB

                             

 

2.系统调用

    现代操作系统中,内核提供了用户进程与内核进行交互的一组接口,这些接口让应用程序受限地访问硬件设备,提供了创建新进程并与已有进程进行通信的机制,也提供了申请操作系统其他资源的能力。这样做的目的是为了保证系统稳定可靠,避免应用程序恣意忘形。

与内核通信

    系统调用在用户空间进程与硬件设备之间添加了一个中间层,该层主要作用有:

1. 他为用户空间提供了一种硬件的抽象接口。如要读写文件的时候,应用程序可以不管磁盘类型和介质,甚至不用管文件所在的文件系统到底是哪种类型。

2.系统调用保护了系统的稳定和安全。作为硬件设备和应用程序之间的中间人,内核可以基于权限、用户类型和其他一些规则对需要进行的访问进行裁决。这样可以避免应用程序不正确地使用硬件设备、窃取其他进程的资源,或做出其他危害系统的事情。

系统调用和API、c库函数

                                

     一个API定义了一组应用程序使用的编程接口。他们可以实现成一个系统调用,也可以通过调用多个系统调用来实现,而完全不使用任何系统调用也不存在问题。实际上,API可以在不同的操作系统上实现,给应用程序提供完全相同的接口,而他们本身在这些系统上的实现却可能不同。

     从程序员的角度看,系统调用无关紧要,他们只需要跟API打交道就可以了。相反,内核只跟系统调用打交道;库函数及应用程序是怎么使用系统调用,不是内核所关心的。

 

系统调用过程:

    先介绍一个概念:系统调用号:在linux中,每个系统调用被赋予了一个系统调用号。这样,通过这个独一无二的号就可以关联系统调用。当用户空间的进程执行一个系统调用的时候,这个系统调用号就用来指明到底是要执行哪个系统调用;进程不会提及系统调用的名称。内核记录了系统调用表中所有已注册过的系统调用列表,存储在sys_call_table中。

    用户程序无法直接执行内核代码。他们不能直接调用内核空间中的函数,因为内核主流在受保护的地址空间上。

    所以,应用程序应该以某种方式通知系统,告诉内核自己家需要执行一个系统调用,希望系统切换到内核态,这样内核就能代表应用程序在内核空间中执行系统调用。

      通知内核的机制是靠软中断来实现的:通过引发一个异常来促使系统切换到内核态去执行异常处理程序。此时的异常处理程序实际上就是系统调用处理程序。在x86系统上预定义的软中断是中断号128,通过int $0x80指令触发该中断(推测:cpu执行这条指令后跳转到异常向量表)。这条指令会触发一个异常导致系统切换到内核态并执行第128号异常处理程序,而该程序正是系统调用处理程序:名为system_call()。    最近x86处理器增加了一条叫做sysenter的指令,与int中断指令相比,这条指令提供更快、更专业的陷入内核执行系统调用的方式。

指定恰当的系统调用:    

    因为所有的系统调用陷入内核的方式都一样,所以仅仅是陷入内核空间是不够的。因此必须把系统调用号一并传给内核。在x86上,系统调用号是通过eax寄存器传递给内核的。在陷入内核之前,用户空间就把相应的系统调用号放入eax中,这样系统调用处理程序一旦运行,就可以从eax中得到数据。其他体系结构上的实现也类似。

    系统调用的参数传递:在x86-32系统上,ebx、ecx、edx、esi和edi按照顺序存放前5个参数。需要六个或以上参数的情况不多见,若有,则用一个单独的寄存器存放指向所有参数在用户空间地址的指针。

     给用户空间的返回值也通过寄存器传递,在x86上,他存放在eax寄存器中。

 

在用户空间中直接访问系统调用:

    linux本身提供了一组宏,用于直接对系统调用进行访问,他会设置好寄存器并调用陷入指令。这些宏是_syscalln(),其中n的范围从0到6,代表需要传递给系统调用的参数个数。

              

 

3.中断处理

详见:https://www.cnblogs.com/edver/p/7260696.html

中断概念

    中断是指在CPU正常运行期间,由于内外部事件或由程序预先安排的事件引起的CPU暂时停止正在运行的程序,转而为该内部或外部事件或预先安排的事件服务的程序中去,服务完毕后再返回去继续运行被暂时中断的程序。Linux中通常分为外部中断(又叫硬件中断)和内部中断(又叫异常)。

    在实地址模式中,CPU把内存中从0开始的1KB空间作为一个中断向量表。表中的每一项占4个字节。但是在保护模式中,有这4个字节的表项构成的中断向量表不满足实际需求,于是根据反映模式切换的信息和偏移量的足够使得中断向量表的表项由8个字节组成,而中断向量表也叫做了中断描述符表(IDT)。在CPU中增加了一个用来描述中断描述符表寄存器(IDTR),用来保存中断描述符表的起始地址。

中断号与中断向量

       I/O设备把中断信号发送给中断控制器(8259A)时与之相关联的是一个中断号,当中断控制器把中断信号发送给CPU时与之关联的是一个中断向量。换个角度分析就是中断号是从中断控制器层面划分,中断向量是从CPU层面划分,所以中断号与中断向量之间存在一对一映射关系。在Intel X86中最大支持256种中断,从0到255开始编号,这个8位的编号就是中断向量。其中将0到31保留用于异常处理和不可屏蔽中断。

中断描述符表(IDT)初始化

中断描述符表初始化需要经过两个过程:

     

  • 0
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值