Java程序员自我修养——Linux中断,内核同步与系统调用

目录

一、中断与异常处理     

二、内核同步

三、系统调用


一、中断与异常处理     

    中断可以分为可屏蔽中断和非屏蔽中断,IO设备发出的所有中断请求都是可屏蔽中断,只有硬件故障等特殊情形才会产生非屏蔽中断,由CPU识别;可屏蔽中断有两种状态,屏蔽的和非屏蔽的,控制单元会忽略一个处理屏蔽状态的可屏蔽中断。

    异常由CPU触发的中断,异常分为处理器探测异常和编程异常,内核必须为每种异常提供一个专门的异常处理程序。由int指令触发的用于执行系统调用的异常或者由int3指令触发的用于调试程序的异常称为编程异常,又称为软中断,由CPU执行指令时探测到反常条件而引起的异常称为处理器探测异常,可以进一步分成三种:故障,陷阱和异常中止。故障是可以纠正的异常,纠正完成CPU会重新执行保存在eip寄存中引起故障的指令;CPU执行陷阱指令触发陷阱异常,通知调试程序被调试程序的某个断点已执行,并将控制权交给调试程序;异常中止是发生一个严重错误后由CPU控制单元产生的,对应的中断处理程序会让对应的进程强制终止。

    每个中断和异常是由0-255之间的一个整数标识的,称之为向量,非屏蔽中断和异常的向量是固定的,可屏蔽中断的向量可以通过中断控制器来改变。

    中断描述符表(Interrupt Descriptor Table,IDT)是一个系统表,每个中断或者异常通过向量在表中查找对应的处理程序入口地址,IDT的内存地址由idtr寄存器指定。IDT包含三种类型的描述符,任务门,中断门和陷阱门,任务门用于保存待切换进程的TSS选择符,中断门用于处理中断,陷阱门用于处理异常,这三种只能在内核态下访问;Linux在此基础上增加了两种用户态可以访问的描述符,系统门和系统中断门,系统门包含向量4,5,128三个的异常处理程序,属于陷阱门;系统中断门包含向量3对应异常处理程序,属于中断门。CPU接受到中断或者异常信号时会将其转化成对应的向量,然后在IDT中查找对应的处理程序的入口地址并执行对应的处理程序。

    中断(异常)处理程序是一个内核控制路径,比进程要“轻”,即中断的上下文很少,建立和终止中断处理的所需的CPU时间很短。中断处理必须满足以下约束:

1、内核将中断响应后需要进行的操作分为两部分,关键而紧急的部分,内核立即执行,其余推迟的部分延后执行

2、内核应该尽可能多的允许一个新的中断事件中断前一个不同类型的中断的处理程序,即中断处理程序必须能以嵌套的方式运行,当一个中断处理程序终止时内核能够恢复被其中断的中断处理程序。

3、内核应该尽可能限制禁止中断的内核操作,即减少禁止中断的内核操作

异常的处理程序大多是由内核给对应进程发送一个Unix信号,由进程的信号处理程序决定如何处理;中断的处理程序比较复杂,不同的中断类型有不同的处理机制。

    上面的中断都是硬件产生的,为了把中断处理过程中可延迟的操作抽离出来从而保证内核能够很快的处理完中断,Linux引入了软中断机制,处理中断时紧急的部分立即执行,将可延迟的中断处理操作(又称可延迟函数)保存起来然后结束中断处理,由内核线程ksoftirqd周期性的检查是否存在未执行的可延迟函数,如果存在则自动执行。可延迟函数要求是可重入的,对函数编写要求较高,为了简化使用Linux提供了tasklet,tasklet是在两种软中断类型的基础上实现的,它具有以下特性: 
a)一种特定类型的tasklet只能运行在一个CPU上,不能并行,只能串行执行
b)多个不同类型的tasklet可以并行在多个CPU上
c)软中断是静态分配的,在内核编译好之后,就不能改变。但tasklet就灵活许多,可以在运行时改变(比如添加模块时)

     软中断的执行由内核线程决定,无法指定延后执行的时间,如果需要延迟特定时间来执行,可以使用工作队列,即将可延迟的中断处理函数放入工作队列中,由叫做工作者线程的内核线程负责在指定时间后执行函数。

    参考: Linux内核中的软中断、tasklet和工作队列详解

二、内核同步

    主要介绍内核同步使用的几种技术,其中RCU无锁同步技术值得深入学习。

     1、每CPU变量是一个数组形式的变量,每个CPU只能访问与之对应的数组元素,因此每CPU变量只能在多个CPU的同一变量在逻辑上是独立的特殊情况下才能使用。内核保证不同CPU对应的数组元素存放在硬件高速缓存的不同的行,避免伪共享问题。每CPU变量只能为来自不同CPU的并发访问提供保护,但对来自中断处理程序和可延迟函数的并发访问不提供保护,需要借助其他同步技术。

    2、原子操作是指利用CPU汇编指令将读取数据-修改数据-写入内存这三步作为一个原子操作来执行,内核提供了atomic_t类型和专门的函数和宏来方便编程过程中实现原子操作。

    3、内存屏障确保编译器不会对内存屏障之前和之后的汇编指令做重排序,同时内存屏障之后的指令开始执行前,内存屏障之前的指令已经执行完成,内核提供了6个宏来实现内存屏障。

    4、自旋锁是一种特殊的锁,当请求锁的进程发现锁被占用时会不断的循环判断锁是否被释放,不断循环期间进程依然在CPU上保持运行,不会因为等待锁而发生进程切换,注意不断循环期间允许内核抢占的。因为大部分内核资源被锁定时间在1ms左右,使用自旋锁可以避免进程切换的巨大开销。读写自旋锁是对自旋锁的扩展,允许多个进程对同一个资源执行读操作,但是只允许一个进程执行写操作,从而提高系统性能。

     5、顺序锁同读写自旋锁类似,区别是使用顺序锁时写操作不用等到读操作执行完成,可以在读操作执行过程中执行写操作,而读写自旋锁读操作和写操作具有相同优先级,任何一个操作执行前必须等待上一个操作执行完成。使用顺序锁写数据是需要自增顺序计数器,表明数据已修改,读取数据时需要前后两次读取顺序计数器,如果两次读取结果不一致说明有写操作修改了数据,需要重新读取数据。顺序锁的好处时写操作永远不会被读操作阻塞,坏处是读操作可能需要执行多次。

    6、RCU(Read-Copy Update)是一种不需要共享数据来实现同步的技术,性能比顺序锁,读写自旋锁等依赖共享数据结构的同步技术更好。被RCU保护的共享数据结构,读者不需要获得任何锁就可以访问它,但写者在访问它时首先拷贝一个副本,然后对副本进行修改,最后使用一个回调(callback)机制在适当的时机把指向原来数据的指针重新指向新的被修改的数据。这个时机就是所有引用该数据的CPU都退出对共享数据的操作。

   7、信号量同步技术是指进程请求信号量时如果信号量等于0则挂起休眠,直到信号量被其他占用进程释放而大于0时恢复执行。读写信号量时对信号量的一个扩展,当信号量被释放时,读写信号量会检查等待进程队列上第一个进程是否是读进程,如果是则唤醒该进程及该进程后面第一个写进程之前的所有读进程,从而提高读操作的并发度。

   8、补充同步技术类似于信号量,就是为了解决不同进程对同一信号量的并发操作问题而引入的,底层实现最大的差别在于如何使用等待队列中包含的自旋锁。

   9、禁止本地中断和可延迟函数可以确保内核代码执行过程中不会被中断或者可延迟函数的执行打断,与之发生竞争条件,从而实现一定程度的同步。

   参考:Linux 2.6内核中新的锁机制--RCU

             linux内核 RCU机制详解

  三、系统调用

       操作系统为用户态进程与硬件设备(如CPU,内存,磁盘等)进行交互提供了一组接口,这组接口就是系统调用,系统调用封装了不同型号的硬件驱动的差异,提高上层应用程序的可移植性,内核可利用系统调用对用户进程操作硬件设备的请求做安全检查,提高系统安全性。系统调用通过汇编语言实现,以系统调用号的形式调用,在内核以服务例程的形式保存在系统调用分派表中,第n个表项表示系统调用号为n的服务例程的入口地址,该分派表由内核初始化。系统调用为xy,则对应的服务例程通常命名为sys_xy。

      POSIX API定义了一组标准API,Linux对应的实现就是libc库,一个API接口可能组合多个系统调用完成特定功能。因为系统调用的调用方式特殊,libc库中对每个系统调用的调用都做了一层封装,从而屏蔽与内核交互的细节,这层封装称为封装例程,API接口通过普通函数调用的方式调用封装例程完成系统调用。封装例程通常返回一个整数,正数或者0通常标识调用成功,负数标识调用失败的原因,具体失败错误码由系统调用定义。

    因为系统调用属于内核代码,调用前需要通过软中断将进程由用户态切换到内核态,进入内核态后内核会根据进程描述符中的thread_info字段找到该进程的内核栈地址,然后执行如下操作:

1、在内核栈保存大多数寄存器的内容,避免系统调用执行过程中覆盖寄存器中的值。

2、将系统调用号写入eax寄存器中,将调用参数写入对应寄存器中,然后从寄存器保存到内核栈中,要求每个参数的长度不能超过寄存器的长度,个数不能超过6个,超过寄存器长度或者6个通过指针的方式传递。

3、调用系统调用号对应的服务例程,并将调用结果写入内核栈保存eax寄存器值的单元中,用户态进程从eax寄存器中获取调用结果,执行过程中会验证请求参数,访问进程地址空间,内核提供了多个从用户空间读取写入数据的函数。

4、将内核栈中的寄存器值恢复到寄存器中,并将进程从内核态切换到用户态,恢复用户态进程代码执行

    整体过程如下图:

     内核代码也可调用系统调用,通过_syscall0到_syscall6的一组宏调用,数字0-6表示系统调用需要的参数个数,如执行fork调用通过_syscall0(int,fork), 其中int表示系统调用号,fork是系统调用的名字。这组宏称为内核封装例程。

    参考:《深入理解Linux内核》

                 Linux系统调用详解(实现机制分析)--linux内核剖析(六)

猜你喜欢

转载自blog.csdn.net/qq_31865983/article/details/89789099
今日推荐