剖析虚幻渲染体系(18)- 操作系统(内核)

18.3 内核

下面两图显示了主流的内核总体架构。

内核架构1。

内核架构2。

图的底部显示了内核的一部分,该部分(以及从那里调用的函数)在监督者模式下执行。在监督器模式下执行的所有代码都是用汇编程序编写的,并包含在文件crt0.S中。crt0.S中的代码分为启动代码、访问硬件的函数、中断服务例程、任务开关(调度程序)和出于性能原因而用汇编程序写的信号量函数。

图的中间部分显示了在用户模式下执行的内核的其余部分。对crt0.S中代码的任何调用都需要更改为监控模式,即从中间到下部的每个箭头都与一个或多个TRAP指令相关,这些指令会导致监控模式的更改。类os包含一组带有TRAP指令的包装函数,使应用程序能够访问某些硬件部件。SerialIn和SerialOut类称为串行I/O,需要硬件访问,也可以从中断服务例程访问。Class Task包含与任务管理相关的任何内容,并使用内核的supervisor部分进行(显式)任务切换。任务切换也由中断服务例程引起。Semaphore类提供包装函数,使其成员函数的实现在用户模式下可用。内核内部使用了几个Queue类,并且应用程序也可以使用这些类;他们中的大多数使用Semaphore类。

通常,应用程序与内部内核接口无关,与内核相关的接口是在类os、SerialIn、SerialOut、Task、Queue和Semaphore中定义的接口。

18.3.1 内核概述

下图给出了内核的主要组件的简单概述。可以看到底部的硬件,硬件由芯片、电路板、磁盘、键盘、显示器和类似的物理对象组成。硬件之上是软件,大多数计算机有两种操作模式:内核模式和用户模式。操作系统是软件中最基本的部分,它以内核模式(也称为管理器模式)运行。在这种模式下,它可以完全访问所有硬件,并可以执行机器能够执行的任何指令。软件的其余部分以用户模式运行,在该模式下,只有机器指令的一个子集可用。特别是,那些影响机器控制或进行I/O输入/输出的指令“被禁止用于用户模式程序。本文反复讨论内核模式和用户模式之间的区别,它在操作系统的工作方式中起着至关重要的作用。

更详细的结构图如下:

传统的Unix内核架构图如下:

现代Unix内核已经进化成如下架构:

Linux内核模块列表样例如下:

Linux内核组件如下所示:

18.3.2 内核对象

Windows内核公开各种类型的对象,供用户模式进程、内核本身和内核模式驱动程序使用。这些类型的实例是系统(内核)空间中的数据结构,当用户或内核模式代码请求时,由对象管理器(执行程序的一部分)创建和管理。内核对象使用了引用计数,因此只有当对象的最后一个引用被释放时,对象才会被销毁并从内存中释放。

Windows内核支持很多对象类型,可从Sysinternals运行WinObj工具,并找到ObjectTypes目录(下图)。可以根据其可见性和用途进行分类:

  • 通过Windows API导出到用户模式的类型。例如:互斥、信号量、文件、进程、线程和计时器。
  • 未导出到用户模式,但记录在Windows驱动程序工具包(WDK)中供设备驱动程序编写者使用的类型。例如设备、驱动程序和回调。
  • 即使在WDK中也没有记录的类型(至少在编写时),仅由内核本身使用。例如分区、键控事件和核心消息传递。

内核对象的主要属性如下图所示:

某些类型的对象可以具有基于字符串的名称,这些名称可用于使用适当的打开函数按名称打开对象。注意,并非所有对象都有名称,例如进程和线程没有名称——它们有ID。这就是为什么OpenProcess和OpenThread函数需要进程/线程标识符(数字)而不是基于string的名称。

在用户模式代码中,如果不存在具有名称的对象,则调用具有名称的创建函数将创建具有该名称的对象;如果存在,则只打开现有对象。

提供给创建函数的名称不是对象的最终名称。在经典(桌面)进程中,它前面有\Sessions\x\BaseNamedObjects\其中x是调用方的会话ID。如果会话为零,则名称前面只加上\BaseNamedObjects\。如果调用方碰巧在AppContainer(通常是通用Windows平台进程)中运行,则前缀字符串更复杂,由唯一的AppContainerSID: \Sessions\x\AppContaineerNameObjects\组成。

18.3.3 内核对象共享

内核对象的句柄是进程私有的,但在某些情况下,进程可能希望与另一个进程共享内核对象。这样的进程不能简单地将句柄的值传递给其他进程,因为在其他进程的句柄表中,句柄值可能指向其他对象或为空。显然,必须有某种机制来允许这种分享。事实上,有三种分享机制:

  • 按名称共享。如果可用,是最简单的方法。“可用”表示所讨论的对象可以有名称,并且确实有名称。典型的场景是,协作进程(2个或更多)将使用相同的对象名调用相应的Create函数。进行调用的第一个进程将创建对象,其他进程的后续调用将为同一对象打开其他句柄。

  • 通过句柄继承共享。通常是父进程创建子进程时,传入继承属性和数据而达成。

  • 通过复制句柄共享。句柄复制没有固有的限制(除了安全性),几乎可以在任何内核对象上工作,无论是有命字的还是没有名字的,并且可以在任何时间点工作。然而,有一个缺陷,是实践中最困难的分享方式(后面会提及)。Windows通过调用DuplicateHandle复制句柄。

    Windows复制句柄应用案例图示。

18.3.4 句柄

由于内核对象驻留在系统空间中,因此无法直接从用户模式访问它们。应用程序必须使用间接机制来访问内核对象,称为句柄(Handle)。句柄至少具有以下优点:

  • 在未来的Windows版本中,对象类型数据结构的任何更改都不会影响任何客户端。
  • 可通过安全访问检查控制对对象的访问。
  • 句柄是进程私有的,因此在一个进程中拥有特定对象的句柄在另一个进程上下文中没有意义。

内核对象是引用计数的。对象管理器维护句柄计数和指针计数,其和是对象的总引用计数(直接指针可以从内核模式获得)。一旦不再需要用户模式客户端使用的对象,客户端代码应通过调用CloseHandle关闭用于访问该对象的句柄。之后句柄将无效,尝试通过关闭句柄访问对象将失败。在一般情况下,客户端不知道对象是否已被销毁。如果对象的引用降至零,则对象管理器将删除该对象。

句柄值是4的倍数,其中第一个有效句柄是4;零永远不是有效的句柄值,在64位系统上亦是如此。句柄间接指向内核空间中的一个小数据结构,该结构包含句柄的一些信息。下图描述了32位和64位系统的数据结构。

在32位系统上,该句柄条目的大小为8字节,在64位系统上为16字节(从技术上而言,12字节已足够,但为了对齐目的,会扩展为16字节)。每个条目包含以下成分:

  • 指向实际对象的指针。由于低位用于标记,并通过地址对齐提高CPU访问时间,因此在32位系统上,对象的地址是8的倍数,在64位系统上是16的倍数。
  • 访问掩码,指示可使用此手柄执行的操作。换句话说,访问掩码是句柄的力量。
  • 三个标志:继承、关闭时保护和关闭时核验。

访问掩码是位掩码,其中每个“1”位表示可以使用该句柄执行的特定操作。当通过创建对象或打开现有对象创建句柄时,将设置访问掩码。如果创建了对象,则调用者通常具有对该对象的完全访问权。但是,如果对象被打开,调用方需要指定所需的访问掩码,它可能会得到,也可能不会得到。

某些句柄具有特殊值,不可关闭,被称为伪句柄(Pseudo Handles),尽管它们在需要时与任何其他句柄一样使用。在伪句柄上调用CloseHandle总是失败。

当不再需要句柄时,关闭句柄非常重要。如果应用程序未能正确执行此操作,则可能会出现“句柄泄漏”,即如果应用程序打开句柄但“忘记”关闭它们,则句柄的数量将无法控制地增长。帮助代码管理句柄而不忘记关闭它们的一种方法是使用C++实现一个众所周知的习惯用法,称为资源获取即初始化(Resource Acquisition is Initialization,RAII)。其思想是对包装在类型中的句柄使用析构函数,以确保在包装对象被销毁时关闭句柄。下面是一个简单的句柄RAII封装器:

struct Handle
{
    explicit Handle(HANDLE h = nullptr) 
        :_h(h) // 初始化
    {}
    // 析构函数关闭句柄。
    ~Handle() { Close(); }
    // 删除拷贝构造和拷贝赋值
    Handle(const Handle&) = delete;
    Handle& operator=(const Handle&) = delete;
    
    // 允许移动(所有权转移)
    Handle(Handle&& other) : _h(other._h) 
    {
        other._h = nullptr;
    }
    Handle& operator=(Handle&& other) 
    {
        if (this != &other) 
        {
            Close();
            _h = other._h;
            other._h = nullptr;
        }
        return *this;
    }
    
    operator bool() const 
    {
        return _h != nullptr && _h != INVALID_HANDLE_VALUE;
    }
    HANDLE Get() const 
    {
        return _h;
    }
    void Close() 
    {
        if (_h) 
        {
            ::CloseHandle(_h);
            _h = nullptr;
        }
    }

private:
    HANDLE _h;
};

18.3.5 其它内核对象

Windows中还有其他常用内核对象,即用户对象和GDI对象。以下是这些对象的简要描述以及这些对象的句柄。

  • 任务管理器。可以通过添加“用户对象”和“GDI对象”列来显示每个进程的此类对象的数量。
  • 用户对象。用户对象是窗口(HWND)、菜单(HNU)和挂钩(HHOOK)。这些对象的句柄具有以下属性:
    • 无引用计数。第一个销毁用户对象的调用方——它已经消失了。
    • 句柄值的范围在窗口工作站(Window Station)下。窗口工作站包含剪贴板、桌面和原子表。例如,意味着这些对象的句柄可以在共享桌面的所有应用程序之间自由传递。
  • GDI对象。图形设备接口(GDI)是Windows中的原始图形API,即使有更丰富和更好的API(例如Direct2D)。例如设备上下文(HDC)、笔(HPEN)、画笔(HBRUSH)、位图(HBITMAP)等。以下是它们的属性:
    • 无引用计数。
    • 句柄仅在创建过程中有效。
    • 不能在进程之间共享。

18.3.6 中断

任何操作系统内核的核心职责是管理连接到机器硬盘驱动器和蓝光光盘、键盘和鼠标、3D处理器和无线收音机的硬件。为了履行这一职责,内核需要与机器的各个设备进行通信。考虑到处理器的速度可能比它们与之对话的硬件快几个数量级,内核发出请求并等待明显较慢的硬件的响应是不理想的。相反,由于硬件的响应速度相对较慢,内核必须能够自由地去处理其他工作,只有在硬件实际完成工作之后才处理硬件。

处理器如何在不影响机器整体性能的情况下与硬件一起工作?这个问题的一个答案是轮询(polling),内核可以定期检查系统中硬件的状态并做出相应的响应。然而,轮询会产生开销,因为无论硬件是活动的还是就绪的,轮询都必须重复进行。更好的解决方案是提供一种机制,让硬件在需要注意时向内核发出信号,这种机制称为中断(interrupt)。在本节中,我们将讨论中断以及内核如何响应它们,并使用称为中断处理程序(interrupt handler)的特殊函数。

无中断和有中断的程序控制流程。

18.3.6.1 中断概述

中断使硬件能够向处理器发送信号,例如,当用键盘键入时,键盘控制器(管理键盘的硬件设备)向处理器发出电信号,以提醒操作系统新可用的按键,这些电信号就是中断。处理器接收中断并向操作系统发送信号以使操作系统能够响应新数据,硬件设备相对于处理器时钟异步地生成中断,它们可以在任何时间发生。因此,内核可以随时中断以处理中断。

中断是由来自硬件设备的电子信号物理产生的,并被引导到中断控制器的输入引脚,中断控制器是一个简单的多线程芯片,将多条中断线组合成一条到处理器的单线。在接收到中断时,中断控制器向处理器发送信号,处理器检测到该信号并中断其当前执行以处理该中断。然后,处理器可以通知操作系统发生了中断,操作系统可以适当地处理中断。

不同的设备可以通过与每个中断相关联的唯一值与不同的中断相关联,来自键盘的中断与来自硬盘的中断是不同的,使得操作系统能够区分中断并知道哪个硬件设备导致了哪个中断。反过来,操作系统可以使用相应的处理程序为每个中断提供服务。

这些中断值通常称为中断请求(interrupt request,IRQ)线(line)。每个IRQ行都分配了一个数值,例如,在经典PC上,IRQ 0是计时器中断,IRQ 1是键盘中断。然而,并非所有的中断号都是如此严格地定义的,例如,与PCI总线上的设备相关的中断通常是动态分配的。其他非PC架构对中断值具有类似的动态分配,重要的是,一个特定的中断与一个特定设备相关联,内核知道这一点。然后硬件发出中断以引起内核的注意:“嘿,我有新的按键在等待!读取并处理这些坏孩子!”

带中断的指令周期。

18.3.6.2 中断请求级别

每个硬件中断都与一个优先级相关联,称为中断请求级别(Interrupt Request Level,IRQL)(注意,不要与称为IRQ的中断物理线混淆),由HAL确定。每个处理器的上下文都有自己的IRQL,就像任何寄存器一样。IRQL可以由CPU硬件实现,也可以不由CPU硬件来实现,但本质上并不重要,IRQL应该像其他CPU寄存器一样对待。

基本规则是处理器执行具有最高IRQL的代码。例如,如果某个CPU的IRQL在某个时刻为零,并且出现了一个IRQL为5的中断,它将在当前线程的内核堆栈中保存其状态(上下文),将其IRQL提升到5,然后执行与该中断相关联的ISR。一旦ISR完成,IRQL将下降到其先前的级别,继续执行先前执行的代码,就像中断不存在一样。当ISR执行时,IRQL为5或更低的其他中断无法中断此处理器。另一方面,如果新中断的IRQL高于5,CPU将再次保存其状态,将IRQL提升到新级别,执行与第二个中断相关联的第二个ISR,完成后,将返回到IRQL 5,恢复其状态并继续执行原始ISR。本质上,提升IRQL会暂时阻止IRQL等于或低于IRQL的代码。中断发生时的基本事件序列如下图所示,下下图显示了中断嵌套的样子。

基本中断调度流程。

嵌套中断流程。

上两图所示场景的一个重要事实是,所有ISR的执行都是由最初被中断的同一线程完成的。Windows没有处理中断的特殊线程,它们由当时在中断的处理器上运行的任何线程处理。我们很快就会发现,当处理器的IRQL为2或更高时,上下文切换是不可能的,因此在这些ISR执行时,不可能有其他线程潜入。

由于这些“中断”,被中断的线程不会减少其数量。可以说,这不是它的错。

当执行用户模式代码时,IRQL始终为0,这就是为什么在任何用户模式文档中都没有提到IRQL这个术语的原因之一——它总是为0,不能更改。大多数内核模式代码也使用IRQL 0运行,在内核模式下,可以在当前处理器上提升IRQL。

Windows使用API提升或降低IRQL的示例:

// assuming current IRQL <= DISPATCH_LEVEL
KIRQL oldIrql; // typedefed as UCHAR

// 提升IRQL
KeRaiseIrql(DISPATCH_LEVEL, &oldIrql);

NT_ASSERT(KeGetCurrentIrql() == DISPATCH_LEVEL);

// 在IRQL的DISPATCH_LEVEL执行工作。

// 降低IRQ
KeLowerIrql(oldIrql);

18.3.6.3 线程优先级和IRQL

IRQL是处理器的一个属性,优先级是线程的属性,线程优先级仅在IRQL<2时才有意义。一旦执行线程将IRQL提升到2或更高,它的优先级就不再有任何意义了,理论上它拥有无限量程——会一直执行,直到IRQL降低到2以下。

当然,在IRQL>=2上花费大量时间不是一件好事,用户模式代码肯定没有运行,这只是在这些级别上执行代码的能力受到严格限制的原因之一。

Windows任务管理器显示使用称为系统中断的伪进程在IRQL2或更高版本中花费的CPU时间,Process Explorer将其称为“中断”。下图上半部分显示了Task Manager的屏幕截图,下半部分显示了Process Explorer中的相同信息。

18.3.6.4 中断处理器

内核响应特定中断而运行的函数称为中断处理程序或中断服务例程(interrupt service routine,ISR),每个产生中断的设备都有一个相关的中断处理程序。例如,一个函数处理来自系统计时器的中断,而另一个函数则处理键盘产生的中断。设备的中断处理程序是设备驱动程序(管理设备的内核代码)的一部分。

在Linux中,中断处理程序是正常的C函数。它们匹配一个特定的原型,这使得内核能够以标准方式传递处理程序信息,但除此之外,它们都是普通函数。中断处理程序与其他内核函数的区别在于,内核在响应中断时调用它们,并且它们在一个称为中断上下文(interrupt context)的特殊上下文中运行,这个特殊的上下文有时被称为原子上下文(atomic context),在此上下文中执行的代码不能被阻塞。

因为中断可以在任何时候发生,所以中断处理程序可以在任何时间执行,处理程序必须快速运行,以便尽快恢复被中断代码的执行。因此,虽然操作系统无延迟地为中断提供服务对硬件很重要,但对系统的其余部分来说,中断处理程序在尽可能短的时间内执行也是很重要的。

至少,中断处理程序的工作是向硬件确认中断的接收:“嘿,硬件,我听到了;现在回去工作吧!”然而,中断处理程序通常要执行大量的工作,例如,考虑网络设备的中断处理程序。除了响应硬件之外,中断处理器还需要将网络数据包从硬件复制到内存中,对其进行处理,并将数据包向下推送到适当的协议栈或应用程序。显然,可能需要大量工作,尤其是今天的千兆和10千兆以太网卡。

通过中断传输控制权。

在Linux的驱动程序中,请求中断行并安装处理程序是通过request_irq()完成的:

if(request_irq(irqn, my_interrupt, IRQF_SHARED, "my_device", my_dev)) 
{
    printk(KERN_ERR "my_device: cannot register IRQ %d\n", irqn);
    return -EIO;
}

18.3.6.5 中断上下文

执行中断处理程序时,内核处于中断上下文中,进程上下文是内核代表进程执行时的操作模式,例如,执行系统调用或运行内核线程。在进程上下文中,当前宏指向关联的任务。此外,由于进程在进程上下文中耦合到内核,进程上下文可以休眠或以其他方式调用调度器。

另一方面,中断上下文与进程无关,当前宏不相关(尽管它指向被中断的进程)。如果没有后备进程,中断上下文将无法休眠,它将如何重新调度?因此,不能从中断上下文中调用某些函数,如果函数处于休眠状态,则不能从中断处理程序中使用它,这限制了从中断处理函数中调用的函数。

中断上下文是时间关键的,因为中断处理程序会中断其他代码。代码应该快速简单。繁忙的循环是可能的,但不鼓励。请记住,中断处理程序中断了其他代码(甚至可能是另一行上的另一个中断处理程序!)。由于这种异步特性,所有中断处理程序都必须尽可能快和简单。尽可能地,工作应该从中断处理程序中推出,并在下半部分中执行,在更方便的时间运行。

中断处理程序堆栈的设置是一个配置选项,从历史上看,中断处理程序没有收到自己的堆栈,相反,他们将共享中断的进程堆栈。内核堆栈大小为两页,通常在32位体系结构上为8KB,在64位体系结构中为16KB。因为在这种设置中,中断处理程序共享堆栈,所以它们在分配数据时必须格外节约。当然,内核堆栈一开始是有限的,因此所有内核代码都应该谨慎。

在内核进程的早期,可以将堆栈大小从两页减少到一页,在32位系统上只提供4KB的堆栈,此举减少了内存压力,因为系统上的每个进程以前都需要两页连续的、不可扩展的内核内存。为了处理减少的堆栈大小,中断处理程序被赋予了自己的堆栈,每个处理器一个堆栈,一个页面大小,此堆栈称为中断堆栈(interrupt stack)。尽管中断堆栈的总大小是原始共享堆栈的一半,但可用的平均堆栈空间更大,因为中断处理程序可以自己获取整个内存页。

中断处理程序不应该关心正在使用的堆栈设置或内核堆栈的大小,应该始终使用绝对最小的堆栈空间。

18.3.6.6 实现中断处理程序

Linux中中断处理系统的实现依赖于体系结构,实现取决于处理器、使用的中断控制器类型以及体系结构和机器的设计。下图是中断通过硬件和内核的路径图。

设备通过其总线向中断控制器发送电信号来发出中断,如果中断线被启用,中断控制器将中断发送到处理器。在大多数架构中,通过特殊引脚发送到处理器的电信号来实现。除非中断在处理器中被禁用,否则处理器会立即停止它正在做的事情,禁用中断系统,并跳转到内存中的一个预定义位置并执行位于该位置的代码。这个预定义点由内核设置,是中断处理程序的入口点。

中断在内核中的过程从这个预定义的入口点开始,就像系统调用通过预定义的异常处理程序进入内核一样。对于每个中断行,处理器跳转到内存中的一个唯一位置并执行位于该位置的代码。通过这种方式,内核知道传入中断的IRQ号,初始入口点简单地保存该值并将当前寄存器值(属于中断的任务)存储在堆栈上,则内核调用do_IRQ()。从这里开始,大多数中断处理代码都是用C编写的,然而,它仍然依赖于体系结构。下面是处理IRQ的代码:

/**
* handle_IRQ_event - irq action chain handler
* @irq: the interrupt number
* @action: the interrupt action chain for this irq
*
* Handles the action chain of an irq event
*/
irqreturn_t handle_IRQ_event(unsigned int irq, struct irqaction* action)
{
    irqreturn_t ret, retval = IRQ_NONE;
    unsigned int status = 0;
    if (!(action->flags & IRQF_DISABLED))
        local_irq_enable_in_hardirq();
    do {
        trace_irq_handler_entry(irq, action);
        ret = action->handler(irq, action->dev_id);
        trace_irq_handler_exit(irq, action, ret);
        switch (ret) {
        case IRQ_WAKE_THREAD:
            /*
            * Set result to handled so the spurious check
            * does not trigger.
            */
            ret = IRQ_HANDLED;
            /*
            * Catch drivers which return WAKE_THREAD but
            * did not set up a thread function
            */
            if (unlikely(!action->thread_fn)) {
                www.it - ebooks.info
                warn_no_thread(irq, action);
                break;
            }
            /*
            * Wake up the handler thread for this
            * action. In case the thread crashed and was
            * killed we just pretend that we handled the
            * interrupt. The hardirq handler above has
            * disabled the device interrupt, so no irq
            * storm is lurking.
            */
            if (likely(!test_bit(IRQTF_DIED,
                &action->thread_flags))) {
                set_bit(IRQTF_RUNTHREAD, &action->thread_flags);
                wake_up_process(action->thread);
            }
            /* Fall through to add to randomness */
        case IRQ_HANDLED:
            status |= action->flags;
            break;
        default:
            break;
        }
        retval |= ret;
        action = action->next;
    } while (action);
    if (status & IRQF_SAMPLE_RANDOM)
        add_interrupt_randomness(irq);
    local_irq_disable();
    return retval;
}

18.3.6.7 延迟过程调用

下图显示了客户端调用某些I/O操作时的典型事件序列,在图中,用户模式线程打开文件的句柄,并使用ReadFile函数发出读取操作。由于线程可以进行异步调用,因此它几乎立即重新获得控制权,并可以执行其他工作。接收到此请求的驱动程序将调用文件系统驱动程序(例如NTFS),该驱动程序可能会调用其下面的其他驱动程序,直到请求到达磁盘驱动程序,该磁盘驱动程序将在实际磁盘硬件上启动操作。在这一点上,没有代码需要执行,因为硬件“做它的事情”。

当硬件完成读取操作时,它发出一个中断,导致与中断相关联的中断服务例程在设备IRQL上执行(请注意,处理请求的线程是任意的,因为中断是异步到达的)。典型的ISR访问设备的硬件以获得操作结果,它的最终行动应该是完成最初的请求。

简单的中断处理过程。

18.3.6.8 异步过程调用

在Windows中,延迟过程调用(DPC)是封装在IRQLDISPATCH_LEVEL调用的函数的对象。就DPC而言,调用线程并不重要。异步过程调用(APC)也是封装要调用的函数的数据结构。但与DPC相反,APC的目标是特定线程,因此只有该线程才能执行该函数,意味着每个线程都有一个与其关联的APC队列。APC有三种类型:

  • 用户模式APC。仅当线程进入可报警状态时,这些APC在IRQL PASSIVE_LEVEL的用户模式下执行,通常通过调用API(如SleepEx、WaitForSingleObjectEx、WaitForMultipleObjectsEx和类似API)来完成。这些函数的最后一个参数可以设置为TRUE,以使线程处于可报警状态。在此状态下,它查看其APC队列,如果不是空的,则APC现在执行,直到队列为空。
  • 正常内核模式APC。这些APC在IRQL PASSIVE_LEVEL的内核模式下执行,并抢占用户模式代码和用户模式APC。
  • 特殊内核APC。这些APC在IRQL APC_LEVEL(1)的内核模式下执行,并抢占用户模式代码、正常内核APC和用户模式APC。I/O系统使用这些APC来完成I/O操作。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值