64位内核开发第八讲.多线程开发.同步和互斥简介

多线程编程

一丶多线程安全.

1.什么是多线程

现在的程序基本是很多个线程.不想以前一样.而进程和线程的关系就是
一对多的关系.

进程做外键放到线程中. 数据关系是这样的.

简单理解为 进程就是一个 容体. 里面的线程就是它进存储的任务.

一个线程只能做一个事情. 多个线程可以做多个事情.

2.超线程

超线程是一个硬件的CPU技术.是用来模拟双核的.inter所研发的.
以前是用软件模拟的. 英文是HT技术.全名就是 Hyper-Threading.
超线程的意思就是在同一个时刻.应用层可以使用CPU的不同部分的.

3.多线程引入的问题.

同步问题.

当线程访问全局变量.以及资源的时候就会出现问题.

比如我们要对一个变量++ .正常来说会 1 + 1 = 2就是等于2.
而多线程就会出现数字不一样的情况.

如下: ring3代码演示.

#include <windows.h>
#include <stdlib.h>
#include <stdio.h>

int g_Value = 0;

DWORD MyThread(LPVOID lPparame)
{
    for (DWORD i = 0; i < 10000000; i++)
    {
        g_Value++
    }
    return 0;
}
int main()
{
    HANDLE hThread[2];
     hThread[0] = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)MyThread, NULL, 0, NULL);
     hThread[1]= CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)MyThread, NULL, 0, NULL);

    WaitForMultipleObjects(2, hThread, TRUE, INFINITE);
    printf("%d", g_Value);
    system("pause");
    return 0;
}

结果:

数值每次都是随机的.
所以产生了错误.
解决方法就是使用同步函数.

如下:

#include <windows.h>
#include <stdlib.h>
#include <stdio.h>

__int64 g_Value = 0;
int g_Count = 0;
HANDLE g_hEvent = CreateEvent(NULL, FALSE, TRUE, NULL);
DWORD MyThread(LPVOID lPparame)
{
    WaitForSingleObject(g_hEvent, INFINITE);
    for (DWORD i = 0; i < 10000000; i++)
    {
        g_Value++;
    }
    //

    SetEvent(g_hEvent);
    return 0;
}
int main()
{
    HANDLE hThread[2];
     hThread[0] = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)MyThread, NULL, 0, NULL);
     hThread[1]= CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)MyThread, NULL, 0, NULL);

    WaitForMultipleObjects(2, hThread, TRUE, INFINITE);
    printf("%d", g_Value);
    system("pause");
    return 0;
}

使用同步之后就不会出现这类问题了.

如果你的线程使用的是局部变量. 你的程序是多线程安全的.就是各个线程
都有自己的局部变量. 不会影响.
但是如果你使用全局资源.就是多线程不安全的.必须使用同步函数(加锁)
这样才会保证你的程序是安全的.

4.多线程的同步与互斥

同步的概念:

同步就是两个线程协同做一件事情. 我在A线程执行完毕功能了,此时我要告诉B线程我完成了,B线程收到后就开始执行,B线程执行完毕之后就告诉A线程我也执行完了。此时A线程继续干事情。

为什么要同步: 举个例子 假设A线程 在写文件,文件里面设置一个标记为 1 设置完毕之后就告诉B线程 我设置完了,而B线程收到后就会检查 A设置文件的标记,如果为1 执行xxx功能,而B线程执行完毕xxx功能,要给文件写一个 3标记(3代表设置成功) 继续告诉A我设置完了,当A接收到信息之后就会检查文件,是否有3标记,如果有就设置为2 继续通知B线程,B线程拿到2了继续执行xxx功能。 这就是一个很简单的同步功能,我完成一件事我告诉你你等待我,等待我了之后就开始进行功能执行。

互斥

互斥就是多个线程都要访问一个资源,这个资源可以是一个文件 可以是一个全局变量等等。

多个线程是排他性的.排着队访问一个资源.

同步就是我操作完变量.我发一个信号.告诉另一个线程可以工作了。下面是同步来模拟互斥的场景。
如以下代码:

DWORD MyThread(LPVOID lPparame)
{

    for (DWORD i = 0; i < 200; i++)
    {
        WaitForSingleObject(g_hEvent, INFINITE);
        g_Value++;
        SetEvent(g_hEvent); //告诉另一个线程可以操作了.
    }
    //


    return 0;
}

这样两个线程都可以同时操作这个变量. 你操作完毕我告诉另一个线程你能操作了.

二丶内核线程

内核中创建线程很简单.

PsCreateSystemThread进行创建的.
跟ring3的CreateThread类似.
如下演示:

PsCreateSystemThread(&hThread, 0, NULL, (HANDLE)0, NULL, MyProc, MyProcParam);

具体查询MSDN

我们创建线程要注意IRQL级别.

如果作为一个普通线程.运行在的是 PASSIVE级别.
如果当前线程IRQL抬高.可以降到PASSIVE级别.

如下:

if (KeGetCurrentIrql() != PASSIVE_LEVEL)
{
   KfRaiseIrql(PASSIVE_LEVEL);
}

KfRaiseIrql.代表降低级别.
KeLowerIrql(); 则是恢复IRQL

请注意 微软并不让我们直接恢复IRQL 而是你要先提升IRQL 然后在恢复IRQL 。上面的代码大家请谨慎运行。不建议使用。

主线程等待子线程创建完毕.
在Ring3我们可以通过WaitForsingObject来等待.

在内核中可以使用 KeWaitForSingleObject() 来等待.

但是注意,keWaitForSingleObject只是等待一个Object对象.
而不像跟Ring3一样.直接把线程句柄拿过来等待.

所以我们还需要一组函数.

ObReferenceObjectByHandle(); //根据HANDLE.传出Object对象
ObReferenceObjectByName();// 未公开请查询网络文档使用
ObDereference();//取消对象引用.

完整代码如下:

#include <ntddk.h>
#include <ntstrsafe.h>

DRIVER_UNLOAD DriverUnLoad;
KSTART_ROUTINE MyThredProc;
void DriverUnLoad(PDRIVER_OBJECT pDriverObject)
{
    KdPrint(("驱动卸载"));
}



void MyThredProc(
    PVOID StartContext
)
{
    DWORD dwCount = 0;
    while ((dwCount++) <= 10)
    {
        KdPrint(("内核线程输出中.第%d次", dwCount));
    }
}

NTSTATUS InitRun()
{
    //创建线程
    HANDLE hThread;
    PVOID ppObject = NULL;
    NTSTATUS ntSttus;
    ntSttus = PsCreateSystemThread(&hThread,
        0,
        NULL,
        (HANDLE)0,
        NULL,
        MyThredProc,    //你创建函数的回调地址
        NULL);          //给你创建函数传递的参数值
    //等待线程创建完毕.
    if (!NT_SUCCESS(ntSttus))
        return ntSttus;
    //判断IRQL 降低权限 请不要直接使用 测试程序使用
    if (KeGetCurrentIrql() != PASSIVE_LEVEL)
        ntSttus = KfRaiseIrql(PASSIVE_LEVEL);

    ntSttus = ObReferenceObjectByHandle(&hThread,
        THREAD_ALL_ACCESS,
        NULL,
        KernelMode,
        &ppObject,
        NULL);
    if (!NT_SUCCESS(ntSttus))
        return ntSttus;
    //等待对象
    KeWaitForSingleObject(ppObject,
        Executive,
        KernelMode,
        FALSE,
        NULL);
    //因为使用了 ObRefrenceObjceByHandle.所以引用对象+1了.现在需要引用对象-1
    ObDereferenceObject(ppObject);
    //关闭线程句柄
    ZwClose(hThread);
    return ntSttus;

}
NTSTATUS DriverEntry(PDRIVER_OBJECT pDriverObject,PUNICODE_STRING pRegPath)
{
    NTSTATUS ntStatus = 0;
    pDriverObject->DriverUnload = DriverUnLoad;
    ntStatus = InitRun();
    return ntStatus;
}

降低IRQL 还有文章 请查询MSDN

C28111 警告 - |驱动程序微软文档 (microsoft.com)

void driver_utility()
{
    // running at APC level
    KFLOATING_SAVE FloatBuf;
    if (KeSaveFloatingPointState(&FloatBuf))
    {
        KeLowerIrql(PASSIVE_LEVEL);
        ...
        KeRaiseIrql(APC_LEVEL, &old);
        KeRestoreFloatingPointState(&FloatBuf);
    }
}

2.同步与互斥,以及等待函数.

互斥
在内核中有以下几种种互斥锁. 互斥就是AB只能有一个人访问相同的资源.

  1. 自旋锁 KSPIN_LOCK

  2. 资源执行体锁 ERESOURCE

  3. 快速互斥 FAST_MUTEX 效率太低微软放弃

  4. KGUARDED_MUTEX 受保护的互斥锁 一般使用这个

同步:
A 跟 B 协作执行. A做一件事告诉B. B去做另一个.

  1. KEVENT 事件

  2. KSEMAPHORE 信号量

    1. KMUTEX

上面是可等待对象.都可以使用函数来等待.
KeWaitForSingleObject

等待的对象还包括

KPROCESS KQUEUE KMUTANT KSEMAPHORE KTHREAD KTIMER ...
FileObject DriverObject 是不可以转换的. 凡是能等待的内核对象.内核头部都会带有 Dispatcher Header结构的
如下:

typedef struct _KEVENT {
    DISPATCHER_HEADER Header;
} KEVENT, *PKEVENT, *PRKEVENT;

KeWaitForSingleObject 最后一个参数是等待时间. 但是注意一点.他是 负数. 而不是跟ring3一样给个秒数就行. 0不等待. NULL 无线等待.

等待多个对象则使用
KeWaitForMutiipleObject

2.1.1 使用 KSPIN_LOCK 例子

void KstartRoutine_spinlock1(PVOID StartContext)
{
    KIRQL oldIrql;
    KeAcquireSpinLock(&g_spinlock, &oldIrql);
    ULONG uindex = 0;
    for (uindex = 0; uindex < 100000000; uindex++)
    {
        g_textNumber++;
    }
    KeReleaseSpinLock(&g_spinlock, oldIrql);
    PsTerminateSystemThread(STATUS_SUCCESS);
}

void KstartRoutine_spinlock2(PVOID StartContext)
{
    KIRQL oldIrql;
    KeAcquireSpinLock(&g_spinlock, &oldIrql);
    ULONG uindex = 0;
    for (uindex = 0; uindex < 100000000; uindex++)
    {
        g_textNumber++;
    }
    KeReleaseSpinLock(&g_spinlock, oldIrql);
    PsTerminateSystemThread(STATUS_SUCCESS);
}

// Thread and Event syn
VOID ThreadManger_spinlock()
{
    NTSTATUS ntStatus = STATUS_UNSUCCESSFUL;
    HANDLE hThreadHandle1 = NULL;
    HANDLE hThreadHandle2 = NULL;

    UNICODE_STRING uStrTest = {0};
    RtlInitUnicodeString(&uStrTest, L"Hello World");
    PKTHREAD pThread1 = NULL;
    PKTHREAD pThread2 = NULL;
    ntStatus = PsCreateSystemThread(
        &hThreadHandle1,
        0,
        NULL,
        NULL,
        NULL,
        KstartRoutine_spinlock1,
        NULL);

    ntStatus = PsCreateSystemThread(
        &hThreadHandle2,
        0,
        NULL,
        NULL,
        NULL,
        KstartRoutine_spinlock2,
        NULL);

    if (!NT_SUCCESS(ntStatus))
    {
    }
    ObReferenceObjectByHandle(hThreadHandle1, GENERIC_ALL, *PsThreadType, KernelMode, (PVOID *)&pThread1, NULL);
    ObReferenceObjectByHandle(hThreadHandle2, GENERIC_ALL, *PsThreadType, KernelMode, (PVOID *)&pThread2, NULL);
    ObDereferenceObject(pThread1);
    ObDereferenceObject(pThread2);
    KeWaitForSingleObject(pThread1, Executive, KernelMode, FALSE, NULL);
    KeWaitForSingleObject(pThread2, Executive, KernelMode, FALSE, NULL);

    ZwClose(hThreadHandle1);
    ZwClose(hThreadHandle2);
}

直接在DriverEntry中调用 ThreadManger_spinlock 即可。DbgPrint 输出变量即可看到两个线程操作同一个变量是同步执行的。

2.2 内核中使用Event示例

void KstartRoutine_event1(PVOID StartContext)
{

    KeWaitForSingleObject(&g_MyEvent, Executive, KernelMode, FALSE, NULL);
    ULONG uindex = 0;
    for (uindex = 0; uindex < 100000000; uindex++)
    {
        g_textNumber++;
    }

    KeSetEvent(&g_MyEvent, IO_NO_INCREMENT, FALSE);
    PsTerminateSystemThread(STATUS_SUCCESS);
}

void KstartRoutine_event2(PVOID StartContext)
{
    KeWaitForSingleObject(&g_MyEvent, Executive, KernelMode, FALSE, NULL);
    ULONG uindex = 0;
    for (uindex = 0; uindex < 100000000; uindex++)
    {
        g_textNumber++;
    }
    KeSetEvent(&g_MyEvent, IO_NO_INCREMENT, FALSE);
    PsTerminateSystemThread(STATUS_SUCCESS);
}

// Thread and Event syn
VOID ThreadManger_Event()
{
    NTSTATUS ntStatus = STATUS_UNSUCCESSFUL;
    HANDLE hThreadHandle1 = NULL;
    HANDLE hThreadHandle2 = NULL;

    UNICODE_STRING uStrTest = {0};
    RtlInitUnicodeString(&uStrTest, L"Hello World");
    PKTHREAD pThread1 = NULL;
    PKTHREAD pThread2 = NULL;
    ntStatus = PsCreateSystemThread(
        &hThreadHandle1,
        0,
        NULL,
        NULL,
        NULL,
        KstartRoutine_event1,
        NULL);

    ntStatus = PsCreateSystemThread(
        &hThreadHandle2,
        0,
        NULL,
        NULL,
        NULL,
        KstartRoutine_event2,
        NULL);

    if (!NT_SUCCESS(ntStatus))
    {
    }
    // wait thread
    ObReferenceObjectByHandle(hThreadHandle1, GENERIC_ALL, *PsThreadType, KernelMode, (PVOID *)&pThread1, NULL);
    ObReferenceObjectByHandle(hThreadHandle2, GENERIC_ALL, *PsThreadType, KernelMode, (PVOID *)&pThread2, NULL);
    ObDereferenceObject(pThread1);
    ObDereferenceObject(pThread2);
    KeWaitForSingleObject(pThread1, Executive, KernelMode, FALSE, NULL);
    KeWaitForSingleObject(pThread2, Executive, KernelMode, FALSE, NULL);

    KeWaitForSingleObject(&g_MyEvent, Executive, KernelMode, FALSE, NULL);
    // wait event
    ZwClose(hThreadHandle1);
    ZwClose(hThreadHandle2);
}

3.内核中使用等待函数

在Ring3的时候.我们可以直接使用Sleep等函数等待. 现在内核中也有提供
函数原型

KeDelayExecutionThread(KPROCESSOR_MOE waitModel,BOOLEAN Alertable, PLARGE_INTEGER interval)

三个参数的意思分别是 等待的模式 是否允许线程报警,用于重新唤起线程. 以及等待时间
关于前两个.如果内核模式下. 分别填写为 KernelModel FALSE即可.
最后一个是等待时间. 内部会自动进行转化.

所以等待时间我们 要给负数 负数 * 1000就是你要等待的时间

如下:

LARGE_INTEGER sec;
sec.Quadpart = -10 * 1000;
KeDelayExecutionThread(KernelModel,FALSE,&sec); //等待10秒.

4. 线程的结束

内核中使用的线程并不会自己结束. 必须在线程内部自己调用 PsTerminateSystemThread来结束. 句柄也必须由 ZwClose函数来关闭.

三丶 互斥 使用 (简单说明)

3.1 互斥的使用

  1. 快速互斥体

    ExInitializeFastMutex()    //初始化快速互斥体
    ExAcquireFastMutex()       //获取锁
    ExTryToAcquireFastMutex()  //尝试获取锁而不暂停此线程
    ExReleaseFastMutex()       //释放锁
    

例子:

void KstartRoutine_FastMutex(PVOID StartContext)
{

    ULONG uindex = 0;
    for (uindex = 0; uindex < 100000000; uindex++)
    {
        ExAcquireFastMutex(&g_fastMutex);
        g_textNumber++;
        ExReleaseFastMutex(&g_fastMutex);
    }

    PsTerminateSystemThread(STATUS_SUCCESS);
}

void Thread_FastMutex()
{
    NTSTATUS ntStatus = STATUS_UNSUCCESSFUL;
    HANDLE hThreadHandle1 = NULL;
    HANDLE hThreadHandle2 = NULL;

    UNICODE_STRING uStrTest = {0};
    RtlInitUnicodeString(&uStrTest, L"Hello World");
    PKTHREAD pThread1 = NULL;
    PKTHREAD pThread2 = NULL;
    ntStatus = PsCreateSystemThread(
        &hThreadHandle1,
        0,
        NULL,
        NULL,
        NULL,
        KstartRoutine_FastMutex,
        NULL);

    ntStatus = PsCreateSystemThread(
        &hThreadHandle2,
        0,
        NULL,
        NULL,
        NULL,
        KstartRoutine_FastMutex,
        NULL);

    if (!NT_SUCCESS(ntStatus))
    {
    }
    // wait thread
    ObReferenceObjectByHandle(hThreadHandle1, GENERIC_ALL, *PsThreadType, KernelMode, (PVOID *)&pThread1, NULL);
    ObReferenceObjectByHandle(hThreadHandle2, GENERIC_ALL, *PsThreadType, KernelMode, (PVOID *)&pThread2, NULL);

    KeWaitForSingleObject(pThread1, Executive, KernelMode, FALSE, NULL);
    KeWaitForSingleObject(pThread2, Executive, KernelMode, FALSE, NULL);

    ObDereferenceObject(pThread1);
    ObDereferenceObject(pThread2);

    // wait event
    ZwClose(hThreadHandle1);
    ZwClose(hThreadHandle2);
}
  1. 受保护的互斥体

    KeInitializeGuardedMutex()  //同快速互斥注释一样
    KeAcquireGuardedMutex()
    KeTryToAcquireGuardedMutex()
    KeReleaseGuardedMutex()
    

如果代码在APC_LEVEL 级别运行 还可以使用如下方法

 KeAcquireGuardedMutexUnsafe()
 KeReleaseGuardedMutexUnsafe()
posted @ 2019-06-15 10:15  iBinary  阅读(840)  评论(0编辑  收藏  举报