Windows内核学习笔记(五)

一点定义

作业(job) 是由执行体支持的一种内核对象,它能够将一个或多个进程视为一个整体来进行管理和控制。管理程序借助 Windows API,能够对作业所使用的系统资源(包括 CPU 或内存)的诸多限制进行控制,比如用户模式 CPU 时间的限制、进程的处理器亲和性、工作集的最大值与最小值、虚拟内存的使用限制等。

迁程(fiber) 是一种用户线程,对于内核来说是不可见的,是由 kernel32.dll 来实现的。应用程序可以在一个线程环境中创建多个迁程,并且可以手动控制这些迁程的执行。迁程不会自动执行,应用程序需要明确地选择某个迁程来执行。一旦迁程开始运行,它会一直运行,直到它退出,或者它显式地切换到另一个迁程。

目前没有看到这俩的用处,先记录下来,像这种定义却没有实际接触的东西可不好背,记下来方便以后回顾

同步、自旋锁

Windows根据执行环境中的IRQL大于APC_LEVEL 或者等于PASSIVE_LEVEL,将同步机制分为“不依赖线程调度的同步机制”和“基于线程调度的同步机制”。

不依赖线程调度的同步机制主要是在IRQL的高优先级下执行的,通常用于中断处理程序和内核模式代码中,以确保在处理中断或执行关键内核代码时,不会被其他线程打断。常见的不依赖线程调度的同步机制包括:

自旋锁(Spin Lock):自旋锁用于在多个线程之间互斥的访问共享资源,本质上是一种忙等待(busy-wait),意思是线程会一直自旋(忙等待)直到锁可用为止,而不会被挂起等待。常使用在高IRQL下,因为此时是不允许切换线程的,使用自旋锁可以确保关键代码不会被其他线程打断。一些自旋锁扩展:执行体自旋锁(支持共享和独占的语义)、排队自旋锁(queued spin lock)和栈内排队自旋锁(in-stack queued spin lock)。
中断服务例程(ISR):ISR用于响应硬件中断,当硬件设备触发中断时,操作系统会立即执行ISR来处理中断。ISR运行在IRQL的高优先级下,不允许进行线程调度,以确保快速响应中断。
延迟过程调用(Deferred Procedure Call,DPC):DPC用于延迟执行一些代码块或处理程序,常与中断处理有关。当硬件设备触发中断时,操作系统会将中断服务例程(ISR) 用于快速响应中断,但有时需要执行一些耗时的操作,例如数据传输或资源释放,这时就会使用DPC来延迟执行这些操作。
另一种基于线程调度的同步机制:当一个线程的执行条件不满足时,该线程进入等待状态,系统将控制权交由其他满足执行条件但没有得到处理器资源的线程;以后,当该线程的执行条件满足时,它又有机会继续执行。这里的执行条件正是Windows提供的线程同步机制中的语义。Windows定义了统一的机制来支持各种线程同步原语:分发器对象(dispatcher object),其数据结构头部为DISAPATCH_HEADER。

自旋锁为什么不能挂起等待?这样不是会消耗更少资源吗

自旋锁不能挂起等待的原因主要与它的设计目标和运行环境(高IRQL)有关。以下是详细的解释:

  1. 高IRQL环境的限制
    在高IRQL(中断请求级别)下,线程调度是被禁止的。这是因为高IRQL通常用于处理中断或关键的内核操作,这些操作需要快速完成,不能被其他线程打断。如果在高IRQL下允许线程挂起等待,会导致以下问题:
    中断处理延迟:如果线程在高IRQL下挂起等待锁,其他中断可能会被延迟处理,影响系统的实时性和响应性。
    调度复杂性:高IRQL下不允许线程调度,因此挂起等待机制无法在这种环境下工作。
  2. 自旋锁的设计目标
    自旋锁的设计目标是快速获取和释放锁,适用于保护短时间的临界区。它的特点包括:
    快速获取:自旋锁的获取和释放操作非常简单,通常只需要几条指令。这使得它在高IRQL下能够快速完成锁的获取和释放,不会引入额外的调度开销。
    忙等待:自旋锁通过忙等待(busy-wait)的方式等待锁可用。虽然这种方式会消耗CPU资源,但由于锁的持有时间通常很短,这种开销是可以接受的。
  3. 为什么不能挂起等待
    如果自旋锁采用挂起等待的方式,会有以下问题:
    调度开销:挂起等待需要操作系统进行线程调度,这会引入额外的上下文切换开销。在高IRQL下,这种开销是不可接受的,因为高IRQL的目的是快速处理中断或关键操作。
    实时性问题:挂起等待会导致线程在高IRQL下被挂起,这会延迟其他中断的处理。例如,如果一个线程在高IRQL下挂起等待锁,其他中断可能会被延迟处理,从而影响系统的实时性和响应性。
    锁的持有时间:自旋锁通常用于保护短时间的临界区。如果锁的持有时间很短,忙等待的开销相对较小,而挂起等待的开销则可能更大。例如,如果一个锁只被持有几微秒,忙等待可能只需要几十个CPU周期,而挂起等待可能需要几百个CPU周期。
  4. 资源消耗问题
    虽然忙等待会消耗CPU资源,但在高IRQL下,这种消耗是可以接受的。原因如下:
    锁的持有时间短:自旋锁通常用于保护短时间的临界区,锁的持有时间很短,忙等待的开销相对较小。
    高IRQL的特殊性:高IRQL下不允许线程调度,因此挂起等待机制无法在这种环境下工作。忙等待虽然会消耗CPU资源,但不会引入额外的调度开销。
  5. 其他同步机制的对比
    互斥体(Mutex):互斥体支持挂起等待,但只能在低IRQL下使用。互斥体适用于保护长时间的临界区,其获取和释放操作相对复杂,需要进行线程调度。
    信号量(Semaphore):信号量也支持挂起等待,适用于线程间的同步,但同样只能在低IRQL下使用。
    总结
    自旋锁不能挂起等待的原因是:
    高IRQL环境的限制:高IRQL下不允许线程调度,挂起等待机制无法在这种环境下工作。
    设计目标:自旋锁用于保护短时间的临界区,忙等待的开销相对较小,而挂起等待会引入额外的调度开销,影响系统的实时性和响应性。
    资源消耗:虽然忙等待会消耗CPU资源,但在锁的持有时间很短的情况下,这种开销是可以接受的。
    因此,自旋锁的设计是为了在高IRQL下快速获取和释放锁,确保系统的实时性和响应性。

自旋锁的详细使用例子

自旋锁是一种用于多线程环境下的同步机制,主要用于保护短时间的临界区。它通过忙等待(busy-wait)的方式等待锁可用,适用于高IRQL(中断请求级别)环境。以下是自旋锁的详细使用例子,包括在内核模式下的具体实现和应用场景。

  1. 自旋锁的基本概念
    自旋锁是一种轻量级的同步机制,适用于保护短时间的临界区。它的特点包括:
    忙等待:线程会一直等待锁可用,而不是被挂起。
    快速获取和释放:锁的获取和释放操作非常简单,通常只需要几条指令。
    适用于高IRQL:在高IRQL下,线程调度是被禁止的,自旋锁通过忙等待确保锁的获取和释放不会引入额外的调度开销。
  2. 自旋锁的使用场景
    自旋锁通常用于以下场景:
    内核数据结构的保护:保护内核数据结构,如队列、链表等。
    中断处理程序(ISR)和延迟过程调用(DPC):在ISR和DPC中使用,确保在高IRQL下不会被其他线程打断。
  3. 自旋锁的实现
    在Windows内核模式下,自旋锁通过KSPIN_LOCK数据结构实现。以下是一个简单的例子,展示如何在内核模式下使用自旋锁。
    3.1 定义自旋锁
#include <ntifs.h>

KSPIN_LOCK MySpinLock;

3.2 初始化自旋锁
在驱动程序加载时初始化自旋锁:

NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath) {
    // 初始化自旋锁
    KeInitializeSpinLock(&MySpinLock);

    // 其他初始化代码
    return STATUS_SUCCESS;
}

3.3 获取和释放自旋锁
在需要保护的临界区中,使用KeAcquireSpinLock和KeReleaseSpinLock来获取和释放自旋锁。这两个函数会自动提升IRQL到DISPATCH_LEVEL,确保在高IRQL下不会被其他线程打断。

void CriticalSectionFunction() {
    KIRQL OldIrql;

    // 获取自旋锁,提升IRQL到DISPATCH_LEVEL
    KeAcquireSpinLock(&MySpinLock, &OldIrql);

    // 临界区代码
    // 例如,访问共享资源
    // ...

    // 释放自旋锁,恢复原来的IRQL
    KeReleaseSpinLock(&MySpinLock, OldIrql);
}
  1. 自旋锁的使用例子
    以下是一个完整的例子,展示如何在内核模式下使用自旋锁保护一个简单的链表。
    4.1 定义链表结构
typedef struct _MY_LIST_ENTRY {
    struct _MY_LIST_ENTRY* Next;
    struct _MY_LIST_ENTRY* Prev;
    ULONG Data;
} MY_LIST_ENTRY, *PMY_LIST_ENTRY;

MY_LIST_ENTRY MyListHead;
KSPIN_LOCK MySpinLock;

4.2 初始化链表和自旋锁

NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath) {
    // 初始化链表头
    MyListHead.Next = &MyListHead;
    MyListHead.Prev = &MyListHead;

    // 初始化自旋锁
    KeInitializeSpinLock(&MySpinLock);

    // 其他初始化代码
    return STATUS_SUCCESS;
}

4.3 插入链表节点

void InsertListEntry(PMY_LIST_ENTRY ListHead, PMY_LIST_ENTRY Entry) {
    KIRQL OldIrql;

    // 获取自旋锁,提升IRQL到DISPATCH_LEVEL
    KeAcquireSpinLock(&MySpinLock, &OldIrql);

    // 插入链表节点
    Entry->Next = ListHead->Next;
    Entry->Prev = ListHead;
    ListHead->Next->Prev = Entry;
    ListHead->Next = Entry;

    // 释放自旋锁,恢复原来的IRQL
    KeReleaseSpinLock(&MySpinLock, OldIrql);
}

4.4 删除链表节点

void RemoveListEntry(PMY_LIST_ENTRY Entry) {
    KIRQL OldIrql;

    // 获取自旋锁,提升IRQL到DISPATCH_LEVEL
    KeAcquireSpinLock(&MySpinLock, &OldIrql);

    // 删除链表节点
    Entry->Prev->Next = Entry->Next;
    Entry->Next->Prev = Entry->Prev;

    // 释放自旋锁,恢复原来的IRQL
    KeReleaseSpinLock(&MySpinLock, OldIrql);
}

4.5 遍历链表

void TraverseList(PMY_LIST_ENTRY ListHead) {
    KIRQL OldIrql;
    PMY_LIST_ENTRY Entry;

    // 获取自旋锁,提升IRQL到DISPATCH_LEVEL
    KeAcquireSpinLock(&MySpinLock, &OldIrql);

    // 遍历链表
    Entry = ListHead->Next;
    while (Entry != ListHead) {
        DbgPrint("Data: %d\n", Entry->Data);
        Entry = Entry->Next;
    }

    // 释放自旋锁,恢复原来的IRQL
    KeReleaseSpinLock(&MySpinLock, OldIrql);
}
  1. 注意事项
    锁的持有时间:自旋锁适用于保护短时间的临界区。如果临界区代码执行时间过长,会导致CPU资源浪费,建议使用其他同步机制,如互斥体。
    IRQL级别:在高IRQL下,线程调度是被禁止的,因此自旋锁不会引入额外的调度开销。
    死锁问题:在使用自旋锁时,要避免死锁问题。例如,不要在持有自旋锁的情况下调用可能阻塞的函数。
  2. 总结
    自旋锁是一种轻量级的同步机制,适用于高IRQL环境。通过忙等待的方式,自旋锁能够快速获取和释放锁,确保在高IRQL下不会被其他线程打断。在内核模式下,自旋锁常用于保护内核数据结构和中断处理程序中的临界区

我在你给出的代码中只看到了普通的锁机制,为什么它被成为“自旋”?

你提出了一个非常好的问题!“自旋锁”(Spin Lock)之所以被称为“自旋”,是因为它的核心机制是忙等待(busy-waiting),而不是线程挂起等待。这种忙等待的过程就像线程在“原地打转”,等待锁变为可用状态,因此得名“自旋锁”。
自旋锁的核心机制:忙等待
自旋锁的核心思想是:
尝试获取锁:当一个线程尝试获取自旋锁时,它会检查锁的状态。
忙等待:如果锁已经被其他线程占用,当前线程不会被挂起,而是进入一个循环,不断检查锁的状态,直到锁变为可用。
获取锁:一旦锁变为可用,当前线程立即获取锁,并进入临界区。
释放锁:线程完成临界区操作后,释放锁,其他线程可以继续尝试获取锁。
这种机制的关键在于线程不会被挂起,而是通过忙等待来获取锁。这种设计在高IRQL(中断请求级别)下非常有用,因为高IRQL下不允许线程调度。
为什么叫“自旋”?
“自旋”这个术语形象地描述了线程在等待锁时的行为。线程在等待锁的过程中,会不断检查锁的状态,就像在原地打转一样。这种忙等待的方式虽然会消耗CPU资源,但在锁的持有时间很短的情况下,这种开销是可以接受的。
示例代码中的自旋逻辑
在Windows内核模式下,自旋锁的实现和使用是通过内核提供的函数来完成的。虽然我们没有直接看到“自旋”的代码,但这些函数内部实现了忙等待的逻辑。例如:

void CriticalSectionFunction() {
    KIRQL OldIrql;

    // 获取自旋锁,提升IRQL到DISPATCH_LEVEL
    KeAcquireSpinLock(&MySpinLock, &OldIrql);

    // 临界区代码
    // 例如,访问共享资源
    // ...

    // 释放自旋锁,恢复原来的IRQL
    KeReleaseSpinLock(&MySpinLock, OldIrql);
}

在KeAcquireSpinLock函数内部,如果锁已经被占用,它会进入一个循环,不断检查锁的状态,直到锁变为可用。这个循环就是“自旋”的具体实现。
自旋锁的内部实现
虽然我们通常不会直接看到自旋锁的内部实现,但可以简单了解一下它的原理。以下是一个简化的伪代码,展示自旋锁的忙等待逻辑:

typedef struct _KSPIN_LOCK {
    volatile LONG Lock;
} KSPIN_LOCK;

void KeInitializeSpinLock(KSPIN_LOCK* SpinLock) {
    SpinLock->Lock = 0;
}

void KeAcquireSpinLock(KSPIN_LOCK* SpinLock, KIRQL* OldIrql) {
    // 提升IRQL到DISPATCH_LEVEL
    *OldIrql = KeRaiseIrql(DISPATCH_LEVEL);

    // 忙等待获取锁
    while (InterlockedExchange(&SpinLock->Lock, 1) != 0) {
        // 锁被占用,继续等待
    }
}

void KeReleaseSpinLock(KSPIN_LOCK* SpinLock, KIRQL OldIrql) {
    // 释放锁
    InterlockedExchange(&SpinLock->Lock, 0);

    // 恢复原来的IRQL
    KeLowerIrql(OldIrql);
}

在这个伪代码中:
KeAcquireSpinLock通过InterlockedExchange尝试获取锁。如果锁已经被占用,它会进入一个循环,不断检查锁的状态,直到锁变为可用。
KeReleaseSpinLock通过InterlockedExchange释放锁。
总结
自旋锁被称为“自旋”,是因为它的核心机制是忙等待,线程在等待锁时会不断检查锁的状态,就像在原地打转一样。这种设计虽然会消耗CPU资源,但在锁的持有时间很短的情况下,这种开销是可以接受的,尤其是在高IRQL环境下,忙等待可以避免引入额外的调度开销。

自旋锁的使用基于进程还是线程还是系统?这样的while是否会造成两个进程/线程同时进入互斥区域?

自旋锁的使用基于线程
自旋锁的作用范围是线程级别的,而不是进程级别或系统级别。自旋锁主要用于保护共享资源,确保在同一时刻只有一个线程可以访问该资源。它通过忙等待的方式实现互斥,防止多个线程同时进入临界区。
关于while循环和互斥性
你提到的while循环是自旋锁实现忙等待的核心部分。虽然看起来线程会不断检查锁的状态,但自旋锁的机制确保了同一时刻只有一个线程可以进入临界区。以下是详细的解释:

  1. 自旋锁的互斥机制
    自旋锁通过原子操作(如InterlockedExchange)来确保互斥性。以下是关键点:
    1.1 原子操作
    自旋锁的获取和释放操作是原子的,这意味着这些操作在硬件级别上是不可分割的。例如,InterlockedExchange函数会原子性地交换两个值,并返回旧值。这种原子性确保了多个线程在尝试获取锁时不会发生冲突。
    1.2 锁的状态检查
    当一个线程尝试获取锁时,它会调用InterlockedExchange函数。如果锁已经被占用(即锁的状态为1),InterlockedExchange会返回1,表示锁不可用。线程会进入一个循环,不断检查锁的状态,直到锁变为可用(即锁的状态为0)。
    1.3 锁的释放
    当持有锁的线程完成临界区操作后,它会调用InterlockedExchange将锁的状态设置为0,表示锁已经释放。此时,其他等待的线程可以获取锁。
  2. 防止多个线程同时进入临界区
    自旋锁的机制确保了同一时刻只有一个线程可以进入临界区。以下是具体过程:
    2.1 获取锁
    假设线程A和线程B同时尝试获取锁:
    线程A调用InterlockedExchange,发现锁的状态为0,成功获取锁,将锁的状态设置为1。
    线程B调用InterlockedExchange,发现锁的状态为1,表示锁已被占用,进入忙等待循环。
  3. 示例代码(笔者懒得复制了,反正都是给以后的自己看的)
    以下是一个简化的自旋锁实现,展示如何通过原子操作实现互斥:
typedef struct _KSPIN_LOCK {
    volatile LONG Lock;
} KSPIN_LOCK;

void KeInitializeSpinLock(KSPIN_LOCK* SpinLock) {
    SpinLock->Lock = 0;
}

void KeAcquireSpinLock(KSPIN_LOCK* SpinLock) {
    while (InterlockedExchange(&SpinLock->Lock, 1) != 0) {
        // 锁被占用,继续等待
    }
}

void KeReleaseSpinLock(KSPIN_LOCK* SpinLock) {
    InterlockedExchange(&SpinLock->Lock, 0);
}
  1. 总结
    自旋锁的作用范围是线程级别的,用于保护共享资源,确保同一时刻只有一个线程可以访问该资源。
    自旋锁通过原子操作实现互斥,确保多个线程在尝试获取锁时不会发生冲突。
    忙等待循环不会导致多个线程同时进入临界区,因为锁的获取和释放操作是原子的,确保了互斥性。
    通过这种方式,自旋锁能够有效地保护临界区,防止多个线程同时访问共享资源。

“原子的”这个操作是受到操作系统支持还是受到硬件支持?

“原子的”(Atomic)操作是指在执行过程中不会被中断的操作。这种操作要么完全完成,要么完全不发生,中间状态不会被其他线程或进程观察到。原子操作的实现可以依赖于操作系统,也可以依赖于硬件支持。在自旋锁的实现中,原子操作通常是通过硬件支持的指令来实现的。

  1. 硬件支持的原子操作
    在现代计算机系统中,硬件提供了许多原子操作指令,这些指令可以直接在CPU级别上执行,确保操作的原子性。这些指令通常包括:
    cmpxchg(Compare and Exchange):比较并交换操作。
    xchg(Exchange):交换操作。
    lock前缀:用于确保多核处理器上的操作是原子的。
    这些指令由CPU直接支持,确保在多核处理器环境下,操作不会被中断。例如,InterlockedExchange函数在Windows内核中通常通过硬件指令实现。
    示例:InterlockedExchange的硬件实现
    在x86架构中,InterlockedExchange可以通过xchg指令实现:
xchg eax, [memory_address]

这条指令会将eax寄存器的值与内存地址[memory_address]中的值交换,并且这个操作是原子的。即使在多核处理器环境下,这条指令也能确保操作的原子性。
2. 操作系统支持的原子操作
操作系统通常会提供一些高级的原子操作接口,这些接口底层依赖于硬件支持的原子指令。例如,Windows内核提供了以下原子操作函数:
InterlockedExchange:原子性地交换两个值。
InterlockedCompareExchange:原子性地比较并交换值。
InterlockedIncrement:原子性地增加一个值。
InterlockedDecrement:原子性地减少一个值。
这些函数在内核模式下可以直接使用,它们的实现依赖于硬件支持的原子指令。
3. 为什么原子操作是重要的
原子操作在多线程和多核环境中非常重要,因为它们确保了操作的完整性和一致性。例如:
互斥锁:通过原子操作确保同一时刻只有一个线程可以获取锁。
计数器:通过原子操作确保计数器的增减操作不会被其他线程中断。
4. 自旋锁中的原子操作
在自旋锁的实现中,InterlockedExchange函数用于原子性地检查和设置锁的状态。例如:

void KeAcquireSpinLock(KSPIN_LOCK* SpinLock) {
    while (InterlockedExchange(&SpinLock->Lock, 1) != 0) {
        // 锁被占用,继续等待
    }
}

InterlockedExchange:这个函数通过硬件支持的原子指令(如xchg)实现,确保在多核处理器环境下,锁的状态检查和设置是原子的。
忙等待:线程在忙等待循环中不断调用InterlockedExchange,直到锁的状态变为0,表示锁可用。
5. 总结
原子操作:是指在执行过程中不会被中断的操作,确保操作的完整性和一致性。
硬件支持:原子操作通常通过硬件指令(如xchg、cmpxchg)实现,这些指令在CPU级别上确保操作的原子性。
操作系统支持:操作系统提供了高级的原子操作接口,这些接口底层依赖于硬件支持的原子指令。
自旋锁:通过原子操作(如InterlockedExchange)实现锁的获取和释放,确保同一时刻只有一个线程可以进入临界区。
因此,原子操作的实现依赖于硬件支持,而操作系统提供了方便的接口供开发者使用。

posted @ 2025-03-09 14:15  Timmoc  阅读(92)  评论(0)    收藏  举报