windows核心编程

windows核心编程


前言

主要用于记录windows核心编程这本书籍的学习

第四章 进程

 windows进程实际上可以分为操作GUI的界面进程,与为之服务的后台进程 windows进程实际上可以分为操作GUI的界面进程,与为之服务的后台进程。
   操作系统并不直接显式的调用main函数的接口,并不调用你编写的进入点函数。它调用的是 C / C + +运行期启动函数。该函数负责对C/C++运行期库进行初始化,这样,就可以调用malloc和free之类的函数。它还能够确保已经声明的任何全局对象和静态 C + +对象能够在代码执行以前正确地创建。

4.1进程句柄

  加载到进程地址空间的每个可执行文件或 D L L文件均被赋予一个独一无二的实例句柄。可执行文件的实例作  (w)WinMain的第一个参数hinstExe来传递。对于加载资源的函数调用来说,通常都需要该句柄的值。

4.1进程环境变量

  每个进程都有一个与它相关的环境块。环境块是进程的地址空间中分配的一个内存块。每个环境块都包含一组字符串。常,子进程可以继承一组与父进程相同的环境变量。但是,父进程能够控制子进程继承什么环境变量,后面介绍C r e a t e P r o c e s s函数时就会看到这个情况。所谓继承,指的是子进程获得它自己的父进程的环境块拷贝,子进程与父进程并不共享相同的环境块。这意味着子进程能够添加、删除或修改它的环境块中的变量,而这个变化在父进程的环境块中却得不到反映。进程之间的内存隔离。
   如果父进程创建了一个它想传递给子进程的环境块,子进程的环境块不会自动继承父进程的当前目录。相反,子进程的当前目录将默认为每个驱动器的根目录。如果想要让子进程继承父进程的当前目录,该父进程必须创建这些驱动器名的环境变量。

4.2进程创建

    当一个线程调用CreateProcess时,系统就会创建一个进程内核对象,其初始使用计数是 1。该进程内核对象不是进程本身,而是操作系统管理进程时使用的一个较小的数据结构。可以将进程内核对象视为由进程的统计信息组成的一个较小的数据结构。然后,系统为新进程创建一个虚拟地址空间,并将可执行文件或任何必要的 D L L文件的代码和数据加载到该进程的地址空间中。

在这里插入图片描述
然后,系统为新进程的主线程创建一个线程内核对象(其使用计数为 1)。与进程内核对象一样,线程内核对象也是操作系统用来管理线程的小型数据结构。通过执行 C / C + +运行期启动代码,该主线程便开始运行,它最终调用 WinMain、wWin Main、main或wmain函数。

4.3线程的终止

ExitProcess函数和TerminateProcess函数是两个用于终止进程的函数,它们之间有以下区别:

  1. 结束方式:ExitProcess函数是一个正常的进程终止函数,它会执行正常的清理工作,包括调用各种析构函数和释放进程占用的资源。它会发送一个WM_QUIT消息给与进程相关联的消息队列,并等待所有消息处理完毕后再终止进程。相比之下,TerminateProcess函数是一个强制终止进程的函数,它会直接终止进程的执行,不会执行任何清理工作,包括不会调用全局或局部对象的析构函数和释放进程所占用的资源。

  2. 影响范围:ExitProcess只能终止当前进程。而TerminateProcess函数可以终止任意一个进程,即使不是当前进程,只要有足够的权限。这使得TerminateProcess函数具有潜在的危险性,因为它可以直接中止其他进程的执行,而不给其他进程机会进行善后处理。

  3. 退出码:ExitProcess函数允许在进程退出时指定一个退出码,该退出码可以被父进程或系统获取,用于了解进程的退出状态。而TerminateProcess函数并不提供退出码的机制,因为进程的终止是强制的,并无法给出合理的终止状态。

总的来说,ExitProcess函数是一种正常终止进程的方法,它按照标准的清理流程终止进程。而TerminateProcess函数是一种强制性的终止进程的方法,它可以直接终止进程的执行,但可能导致一些未完成的清理工作和资源泄露。在一般情况下,应优先选择使用ExitProcess函数来终止进程,除非有特殊需要才考虑使用TerminateProcess函数。注意,调用ExitProcess或ExitThread可使进程或线程在函数中就终止运行。就操作系统而言,这很好,进程或线程的所有操作系统资源都将被全部清除。但是, C/C++应用程序应该避免调用这些函数,因为C/C++运行期也许无法正确地清除。

始终都应该这样来设计应用程序,即只有当主线程的进入点函数返回时,它的进程才终止运行。这是保证所有线程资源能够得到正确清除的唯一办法。让主线程的进入点函数返回,可以确保下列操作的实现:
• 该线程创建的任何C + +对象将能使用它们的析构函数正确地撤消。
• 操作系统将能正确地释放该线程的堆栈使用的内存。
• 系统将进程的退出代码(在进程的内核对象中维护)设置为进入点函数的返回值。
• 系统将进程内核对象的返回值递减1。

当进程终止运行时,下列操作将启动运行:

  1. 进程中剩余的所有线程全部终止运行。
  2. 进程指定的所有用户对象和GDI对象均被释放,所有内核对象均被关闭(如果没有其他进程打开它们的句柄,那么这些内核对象将被撤消。但是,如果其他进程打开了它们的句柄,内核对象将不会撤消)。
  3. 进程的退出代码将从STILL_ ACTIVE改为传递给ExitProcess函数和TerminateProcess函数
  4. 进程内核对象的状态变成收到通知的状态(关于传送通知的详细说明,参见第 9章)。系统中的其他线程可以挂起,直到进程终止运行。
  5. 进程内核对象的使用计数递减1

4.4子进程

 父进程创建了子进程,父进程拥有子进程的进程对象句柄,父进程和子进程之间还是有联系的,子进程终止后,由于引用计数不为0,进程句柄对象并没有释放若要放弃与子进程的所有联系, 必须通过调CloseHandle来关闭它与新进程及它的主线程之间的句柄。

CreateProcess函数是Windows操作系统中用于创建新进程的函数,它的参数列表比较长,需要注意每个参数的含义。以下是CreateProcess函数参数的介绍:

lpApplicationName(可选):指向一个以null结尾的字符串,用于指定可执行文件的路径,同时可以在后面添加命令行参数。如果该参数为NULL,则表示路径信息包含在lpCommandLine中。

lpCommandLine(必需):指向一个以null结尾的字符串,用于指定命令行参数。如果lpApplicationName参数为NULL,则该字符串必须以可执行文件的完整路径名开始。

lpProcessAttributes(可选):指向SECURITY_ATTRIBUTES结构的指针,用于指定新进程的安全性属性。

lpThreadAttributes(可选):指向SECURITY_ATTRIBUTES结构的指针,用于指定新进程的主线程的安全性属性。

bInheritHandles(必需):一个标志,指示由该进程创建的其他进程是否继承当前进程的句柄。

dwCreationFlags(必需):控制进程创建标志的标志位。可包含CREATE_NEW_CONSOLE(创建新的控制台)、CREATE_SUSPENDED(创建挂起的进程)等值。

lpEnvironment(可选):指向用于新进程的环境块的指针。如果该参数为NULL,则使用调用进程的环境。

lpCurrentDirectory(可选):指向以null结尾的字符串,用于指定新进程的当前目录。如果该参数为NULL,则使用调用进程的当前目录。

lpStartupInfo(必需):指向一个STARTUPINFO结构的指针,包含了与新进程的窗口外观和标准输入输出流相关的信息。

lpProcessInformation(必需):指向一个PROCESS_INFORMATION结构的指针,用于接收新进程和主线程的标识信息。

下面是使用CreateProcess函数在C++中创建新进程的示例代码:

#include <windows.h>
#include <iostream>

int main() {
    STARTUPINFO si;
    PROCESS_INFORMATION pi;
    ZeroMemory( &si, sizeof(si) );
    si.cb = sizeof(si);
    ZeroMemory( &pi, sizeof(pi) );
    std::wstring cmd = L"cmd /c dir"; // cmd.exe命令行,查看本目录文件
    if (!CreateProcess(NULL, const_cast<LPTSTR>(cmd.c_str()), NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi)) {
        std::cerr << "CreateProcess failed: " << GetLastError() << std::endl;
        return 1;
    }
    WaitForSingleObject(pi.hProcess, INFINITE);
    CloseHandle(pi.hProcess);
    CloseHandle(pi.hThread);
    return 0;
}

第五章 作业

Microsoft Windoss 2000提供了一个新的作业内核对象,使你能够将进程组合在一起,并且创建一个“沙框”,以便限制进程能够进行的操作,可以把它理解为进程容器,由它统一调配内核资源,管理多个进程。
HANDLE hJob = CreateJobObject(NULL, “MyJob”);最后一个参数用于给作业对象命名,使它可以供另一个进程通过下面所示的 OpenJobObject 函数进行访问。如果知道你将不再需要访问代码中的作业对象,那么就必须通过调用 CloseHandle 来关闭它的句柄。关闭作业对象并不会迫使作业中的所有进程终止运行,进程引用计数请0时才会被系统回收。关闭作业的句柄后,尽管该作业仍然存在,但是该作业将无法被所有进程访问,访问权限被关闭。

5.1作业进程的限制

进程创建后,通常需要设置一个沙框(设置一些限制),以便限制作业中的进程能够进行的操作。可以给一个作业加上若干不同类型的限制:
• 基本限制和扩展基本限制,用于防止作业中的进程垄断系统的资源。
• 基本的U I限制,用于防止作业中的进程改变用户界面。
• 安全性限制,用于防止作业中的进程访问保密资源(文件、注册表子关键字等)。

SetInformationJobObject函数用于设置作业对象的属性。它有两个参数:作业对象的句柄以及要设置的信息的类别和数据。
参数介绍:

  1. hJob: 指向作业对象的句柄。
  2. JobObjectInformationClass: 指定要设置的信息的类别。可以是以下常量之一:
    • JobObjectAssociateCompletionPortInformation: 关联完成端口。
    • JobObjectBasicLimitInformation: 基本的作业限制信息。
    • JobObjectBasicUIRestrictions: 基本的UI限制信息。
    • JobObjectEndOfJobTimeInformation: 作业结束的时间信息。
    • JobObjectExtendedLimitInformation: 扩展的作业限制信息。
    • JobObjectGroupInformation: 作业对象的组信息。
    • 其他类别,请参阅 Microsoft 文档获取更多信息。
  3. lpJobObjectInformation: 指向要设置的信息的数据。
  4. cbJobObjectInformationLength: lpJobObjectInformation 指向的缓冲区的大小。
  5. 返回值: 如果函数调用成功,返回非零值;如果函数调用失败,返回零。可以通过调用 GetLastError 函数获取错误代码。

示例:
以下示例演示了如何使用 SetInformationJobObject 函数将 CPU 利用率限制为 50%:

#include <Windows.h>
#include <iostream>

int main() {
    HANDLE hJob = CreateJobObject(NULL, NULL);
    if (hJob == NULL) {
        std::cout << "CreateJobObject failed with error: " << GetLastError() << std::endl;
        return 1;
    }

    JOBOBJECT_BASIC_LIMIT_INFORMATION limitInfo;
    memset(&limitInfo, 0, sizeof(limitInfo));
    limitInfo.LimitFlags = JOB_OBJECT_LIMIT_CPU_RATE;
    limitInfo.CpuRate = 50;

    if (!SetInformationJobObject(hJob, JobObjectBasicLimitInformation, &limitInfo, sizeof(limitInfo))) {
        std::cout << "SetInformationJobObject failed with error: " << GetLastError() << std::endl;
        CloseHandle(hJob);
        return 1;
    }

    std::cout << "CPU utilization limit set to 50% for the job object." << std::endl;

    CloseHandle(hJob);
    return 0;
}

在上面的示例中,我们首先创建了一个作业对象 hJob,然后初始化了 JOBOBJECT_BASIC_LIMIT_INFORMATION 结构体 limitInfo。我们将 LimitFlags 设置为 JOB_OBJECT_LIMIT_CPU_RATE,这表示我们要限制 CPU 利用率。然后,我们将 CpuRate 设置为 50,这表示我们将 CPU 利用率限制为 50%。最后,我们调用 SetInformationJobObject 函数来设置作业对象的属性。如果函数调用成功,就会输出相应的信息。然后,我们关闭作业对象的句柄并返回。

5.2 将进程放入作业

通过 AssignProcessToJobObject 函数,您可以将一个已经存在的进程(如通过 CreateProcess 函数创建的进程)分配给一个作业对象。一旦进程被分配到作业对象中,作业对象可以对其施加诸如资源限制、优先级控制、进程间通信和安全性方面的限制等。

BOOL AssignProcessToJobObject(
  HANDLE hJob,
  HANDLE hProcess
);

StartRestrictedProcess执行之前必须先把进程放入作业中,否则进程将不受限制的运行

5.3 终止作业中所有进程的运行

可以使用 TerminateProcess 函数来终止一个进程。该函数需要传递进程句柄和退出代码作为参数。可以遍历作业中的每个进程,并为每个进程调用 TerminateProcess 函数来终止它们。

BOOL TerminateJobProcesses(HANDLE hJob, DWORD uExitCode)
{
    JOBOBJECT_BASIC_PROCESS_ID_LIST processList;
    BOOL result;

    // 获取作业中所有进程的ID列表
    result = QueryInformationJobObject(hJob, JobObjectBasicProcessIdList,
                                       &processList, sizeof(processList), NULL);
    if (!result) {
        return FALSE;
    }

    // 遍历进程列表并终止每个进程
    for (DWORD i = 0; i < processList.NumberOfProcessIdsInList; i++) {
        HANDLE hProcess = OpenProcess(PROCESS_TERMINATE, FALSE, processList.ProcessIdList[i]);
        if (hProcess) {
            // 终止进程并设置退出代码
            result = TerminateProcess(hProcess, uExitCode);
            CloseHandle(hProcess);
            if (!result) {
                // 处理错误
                return FALSE;
            }
        }
    }

    return TRUE;
}

5.4 查询作业统计信息

是的,您描述的是如何使用 QueryInformationJobObject 函数来获取作业对象的当前限制信息和统计信息。

QueryInformationJobObject 是一个 Windows API 函数,用于查询作业对象的信息。通过该函数,您可以获取有关作业对象的不同方面的信息,例如当前限制、统计信息等。

若要获取关于作业的基本统计信息,可以调用 QueryInformationJobObject,并将第二个参数设置为 JobObjectBasicAccountingInformation,然后传递一个 JOBOBJECT_BASIC_ACCOUNTING_INFORMATION 结构的地址作为输出参数。这将允许您检索作业对象的基本统计信息,例如 CPU 时间、页面错误等。

下面是函数原型:

BOOL QueryInformationJobObject(
  HANDLE       hJob,
  JOBOBJECTINFOCLASS JobObjectInformationClass,
  LPVOID       lpJobObjectInformation,
  DWORD        cbJobObjectInformationLength,
  LPDWORD      lpReturnLength
);

其中参数说明如下:

  • hJob:作业对象的句柄。
  • JobObjectInformationClass:一个枚举值,指定要查询的信息类型。在此情况下,应为 JobObjectBasicAccountingInformation
  • lpJobObjectInformation:指向存储查询结果的缓冲区的指针。
  • cbJobObjectInformationLength:存储查询结果的缓冲区的大小。
  • lpReturnLength:如果提供了缓冲区,将返回实际填充的缓冲区大小。

需要特别注意参数的正确设置和错误处理,以确保调用正常完成并且返回的信息是准确的。同时,建议查阅官方文档和示例代码来详细了解如何使用 QueryInformationJobObject 函数来获取作业对象的信息。

5.5 作业通知信息

使用 SetInformationJobObject 函数将作业与 I/O 完成端口进行关联,并希望线程可以通过 GetQueuedCompletionStatus 函数来监控 I/O 完成端口。这通常用于异步 I/O 操作的处理。
通过调用 SetInformationJobObject 函数,可以将作业对象与 I/O 完成端口相关联,以便在作业中的相关事件发生时向完成端口发送事件。可以编写线程来使用 GetQueuedCompletionStatus 函数来监控这个完成端口,以响应事件并执行相应的处理操作。

BOOL AssociateCompletionPortWithJob(HANDLE hJob, HANDLE hCompletionPort)
{
    JOBOBJECT_ASSOCIATE_COMPLETION_PORT completionPortInfo;
    completionPortInfo.CompletionKey = NULL;
    completionPortInfo.CompletionPort = hCompletionPort;

    BOOL result = SetInformationJobObject(hJob, JobObjectAssociateCompletionPortInformation,
                                          &completionPortInfo, sizeof(completionPortInfo));

    return result;
}

void MonitorCompletionPort(HANDLE hCompletionPort)
{
    // 线程监控完成端口
    while (true) {
        DWORD completionKey;
        ULONG_PTR completionValue;
        LPOVERLAPPED lpOverlapped;
        
        // 监控完成端口
        BOOL result = GetQueuedCompletionStatus(hCompletionPort,
                                                &completionKey,
                                                &completionValue,
                                                &lpOverlapped,
                                                INFINITE);
        
        if (!result) {
            // 处理错误
        } else {
            // 处理完成的事件
        }
    }
}

第6章 线程

本章将核心编程6-12查线程的基础知识和为一体进行学习

6.1线程的基础知识

进程是不活泼的。进程从来不执行任何东西,它只是线程的容器。线程总是在某个进程环境中创建的,而且它的整个寿命期都在该进程中。这意味着线程在它的进程地址空间中执行代码,并且在进程的地址空间中对数据进行操作。因此,如果在单进程环境中,
你有两个或多个线程正在运行,那么这两个线程将共享单个地址空间。这些线程能够执行相同的代码,对相同的数据进行操作。这些线程还能共享内核对象句柄,因为句柄表依赖于每个进程而不是每个线程存在。如你所见,进程使用的系统资源比线程多得多,原因是它需要更多的地址空间。为进程创建一个虚拟地址空间需要许多系统资源。系统中要保留大量的记录,这要占用大量的内存。另外,由于.exe和.dll文件要加载到一个地址空间,因此也需要文件资源。而线程使用的系统资源要少得多。实际上,线程只有一个内核对象和一个堆栈,保留的记录很少,因此需要很少的内存。由于线程需要的开销比进程少,因此始终都应该设法用增加线程来解决编程问题,而要避免创建新的进程。但是,这个建议并不是一成不变的。许多程序设计用多个进程来实现会更好些。应该懂得权衡利弊,经验会指导你的编程实践。

6.2何时不能创建线程

如你所见,线程能够解决某些问题,但是却又会产生新的问题。在开发应用程序的用户界面时,很可能出现对线程的另一种误用。几乎在所有的应用程序中,所有用户界面的组件(窗口)应该共享同一个线程。单个线程应该创建窗口的所有子窗口。有时在不同的线程上创建不同的窗口是有用的,不过这种情况确实非常少见。通常情况下,一个应用程序拥有一个用户界面线程,用于创建所有窗口,并且有一个GetMessage循环。进程中的所有其他线程都是工作线程,它们与计算机或 I/O相关联,但是这些线程从不创建窗口。另外,一个用户界面线程通常拥有比工作线程更高的优先级,因此用户界面负责向用户作出响应。
注:像Qt界面就是典型的单线程加加事件循环来处理界面的操作响应

虽然单个进程拥有多个用户界面线程的情况并不多见,但是这种情况有着某种有效的用途。Windows Explorer为每个文件夹窗口创建了一个独立的线程。它使你能够将文件从一个文件夹拷贝到另一个文件夹,并且仍然可以查看你的系统上的其他文件夹。

6.3线程的创建和终止

6.31.线程的创建

在 Windows 系统中,C 语言提供了 CreateThread 函数来创建线程。这个函数的原型为:

HANDLE CreateThread(
  LPSECURITY_ATTRIBUTES   lpThreadAttributes,
  SIZE_T                  dwStackSize,
  LPTHREAD_START_ROUTINE  lpStartAddress,
  LPVOID                  lpParameter,
  DWORD                   dwCreationFlags,
  LPDWORD                 lpThreadId
);

下面是各个参数的含义:

  • lpThreadAttributes:指向 SECURITY_ATTRIBUTES 结构的指针,用于指定新线程的安全属性,如果不需要,可以设置为 NULL。
  • dwStackSize:指定新线程的堆栈大小,如果设置为 0,则使用默认的堆栈大小。
  • lpStartAddress:线程函数的地址,也就是新线程所执行的函数的入口点,可以是一个函数指针。
  • lpParameter:传递给线程函数的参数,可以是任意类型的指针或者结构体。
  • dwCreationFlags:用于设置线程的创建标志,例如设置为 0 表示线程立即开始执行。
  • lpThreadId:一个指向 DWORD 变量的指针,用于接收新线程的标识符。

接下来是一个简单的使用示例:

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

DWORD WINAPI ThreadFunc(LPVOID lpParam) {
    printf("This is a new thread.\n");
    return 0;
}

int main() {
    HANDLE hThread;
    DWORD dwThreadId;

    hThread = CreateThread(NULL, 0, ThreadFunc, NULL, 0, &dwThreadId);
    if (hThread == NULL) {
        printf("CreateThread failed, error %d\n", GetLastError());
        return 1;
    }

    // 等待线程结束
    WaitForSingleObject(hThread, INFINITE);

    // 关闭句柄
    CloseHandle(hThread);
    return 0;
}
6.3.2.终止线程的运行

若要终止线程的运行,可以使用下面的方法:
• 线程函数返回(最好使用这种方法,正常我们程序退出时都要通过线程函数退出的方式返回,否则可能会有意想不到的错误产生)。
• 通过调用E x i t T h r e a d函数,线程将自行撤消(最好不要使用这种方法)。
• 同一个进程或另一个进程中的线程调用Te r m i n a t e T h r e a d函数(应该避免使用这种方法)。
• 包含线程的进程终止运行(应该避免使用这种方法):
ExitProcess和TerminateExitProcess函数也可以用来终止线程的运行。差别在于这些线程将会使终止运行的进程中的所有线程全部终止运行。另外,由于整个进程已经被关闭,进程使用的所有资源肯定已被清除。这当然包括所有线程的堆栈。这两个函数会导致进程中的剩余线程被强制杀掉

6.3.3 当线程终止运行时,会发生下列操作:

• 线程拥有的所有用户对象均被释放。在 Windows中,大多数对象是由包含创建这些对象的线程的进程拥有的。但是一个线程拥有两个用户对象,即窗口和挂钩。当线程终止运行时,系统会自动撤消任何窗口,并且卸载线程创建的或安装的任何挂钩。其他对象只有在拥有线程的进程终止运行时才被撤消。
• 线程的退出代码从S T I L L _ A C T I V E改为传递给E x i t T h r e a d或Te r m i n a t e T h r e a d的代码。
• 线程内核对象的状态变为已通知。
• 如果线程是进程中最后一个活动线程,系统也将进程视为已经终止运行。
• 线程内核对象的使用计数递减1。
当一个线程终止运行时,在与它相关联的线程内核对象的所有未结束的引用关闭之前,该内核对象不会自动被释放。
一旦线程不再运行,系统中就没有别的线程能够处理该线程的句柄。然而别的线程可以调用G e t E x i t c o d e T h r e a d来检查由h T h r e a d标识的线程是否已经终止运行。

6.4线程的运行和调度

6.4.1.线程的调度以及亲缘性:

Windows不是实时操作系统,被称为抢占式多 线程操作系统,因为一个线程可以随时停止运行,随后另一个线程可进行调度,线程在运行过程中会不断的被cpu切换上下文(context)到cpu中运行,以及从cpu中移出。

  1. 在线程内核对象的内部有一个值,用于指明线程的暂停计数,计数为1表示线程不可以调度,计数为0表示线程可以被调度,暂停线程时需要小心避免在暂停线程时,程序正在堆栈中分配内存。
  2. Sleep SwitchToThread
    Sleep(睡眠): sleep函数用于暂停程序或线程的执行一段指定的时间。它通常用于需要在一定时间后再继续执行的情况,比如在进行定时操作时,或者在需要等待某些资源就绪时暂停执行。
    SwitchToThread(切换至线程): SwitchToThread函数则是用于多线程编程环境中,它允许当前线程主动放弃CPU时间片,让其他线程有机会执行。这对于改善程序的并发性和响应性很有帮助,可以避免某个线程长时间独占CPU而导致其他线程无法执行的情况。
    总的来说,Sleep函数用于暂停执行一段时间,而SwitchToThread函数则用于在多线程环境中主动让出CPU时间片,促进线程之间的切换和平衡。
  3. 线程优先级
    每个线程都会被赋予一个从 0(最低)到31(最高)的优先级号码。当系统确定将哪个线 程分配给CPU时,它首先观察优先级为3 1的线程,并以循环方式对它们进行调度。只要优先级为31的线程是可调度的,系统就绝对不会将优先级为 0到30的线程分配给CPU。 这种情况称为渴求调度(starvation)。人们可能认为,在这样的系统中,低优先级线程永远得不到机会运行。不过正像前面指出 的那样,在任何一个时段内,系统中的大多数线程是不能调度的。
  4. 进程优先级
    在Windows操作系统中,确实可以为进程分配优先级,但是无法直接为线程分配优先级。这是因为在Windows中,线程的优先级是继承自其所属的进程的优先级。当创建一个线程时,它会继承创建它的进程的优先级。这意味着,如果你想要控制线程的优先级,你需要在创建线程之前设置好创建线程的进程的优先级,从而间接地影响新线程的优先级。
  5. 动态提高线程的优先等级:
    通过将线程的相对优先级与线程的进程优先级类综合起来考虑,系统就可以确定线程的优 先级等级。有时这称为线程的基本优先级等级。系统常常要提高线程的优先级等级,以便对窗口消息或读取磁盘等I/O事件作出响应。windows同时也提供方式让系统的动态优先级功能不起作用。
  6. 线程亲缘新
    线程的亲缘性指的是线程与处理器核心之间的关系,它可以影响线程在处理器上的调度和执行效率。在多核处理器系统中,线程的亲缘性决定了线程是否会在特定的核心上执行,以及它在多个核心之间如何迁移。
    有两种主要的线程亲缘性:
  7. 强亲缘性(Strong Affinity):线程被固定在一个特定的处理器核心上执行。这样做可以避免在不同核心之间频繁切换导致的性能损失,并且可以利用处理器缓存的局部性。让线程留在单个处理器上,有助于重复使用仍然在处理器的内存高速缓存中的数据。
  8. 弱亲缘性(Weak Affinity):线程可以在多个处理器核心之间自由迁移。这种情况下,操作系统负责动态地将线程调度到可用的核心上执行,以实现负载均衡和最大化处理器利用率。
6.4.2线程同步:
     在 Windows 环境下,循环锁通常指的是一种同步机制,用于保护共享资源免受并发访问的影响。Windows 提供了多种机制来实现循环锁,其中最常见的是使用临界区(Critical Section)和互斥体(Mutex)。
1. 用户态同步操作
a. 临界区(Critical Section)

临界区是一种轻量级的同步对象,用于保护临界区代码块,使得在同一时间只有一个线程可以进入临界区执行代码,从而保证共享资源的安全访问。在 Windows 中,可以使用 InitializeCriticalSection、EnterCriticalSection 和 LeaveCriticalSection 等函数来创建和管理临界区。

b. 高速缓存行

当一个CPU从内存读取一个字节时,它不只是取出一个字节,它要取出足够的字节来填 入高速缓存行。高速缓存行由 32或64个字节组成(视CPU而定),并且始终在第3 2个字节或第 64个字节的边界上对齐。高速缓存行的作用是为了提高CPU运行的性能。故在程序编写,数据模型定义时要考虑内存对齐。

c. 循环锁和mutex的关系和区别

循环锁(Spinlock)和互斥锁(Mutex)都是用来实现线程或进程间的同步,以保护共享资源不被并发访问导致数据冲突的机制。它们之间有着明显的区别和各自适用的场景:
关系
目的相同:都是为了实现线程安全的访问共享资源或代码段。
同步机制:都可以在多线程编程中用来防止数据竞争和确保线程同步。
区别:
实现方式:
循环锁:通过让线程在尝试获取锁时进行忙等待(循环检查锁是否可用),不立即将线程挂起。这种方式适用于锁被占用时间极短的场景。
互斥锁:当线程尝试获取一个已被占用的锁时,线程会被挂起(进入休眠状态),直到锁被释放,这样可以减少CPU的忙等待时间,适用于锁被占用时间较长的场景。
性能开销:
循环锁:由于忙等待,它在锁被快速释放的情况下非常高效,因为避免了线程的上下文切换。但如果锁被长时间持有,它会浪费CPU资源,因为占用CPU进行无效的检查。
互斥锁:当线程等待锁时,会发生上下文切换,将CPU让给其他线程使用,这在锁被长时间持有时更有效率,但上下文切换的开销在锁被频繁获取和释放时会影响性能。
适用场景:
循环锁:适用于锁持有时间极短且对响应时间要求极高的场景,因为它可以避免线程上下文切换的开销。
互斥锁:适用于锁持有时间较长或者对CPU资源使用较为敏感的场景,因为它通过线程挂起来避免CPU的无效使用。
使用场景示例:
循环锁:适合用于实现低延迟或实时系统中的短时间锁定,如内核驱动或高性能计算任务。
互斥锁:适合用于需要长时间访问共享资源的场景,如文件访问、数据库操作等。
总结,循环锁和互斥锁虽然都是同步机制的一部分,但它们适用的场景和性能开销有显著差异。在选择使用它们时,应该根据具体的需求和场景来做决策。

#include <iostream>
#include <thread>
#include <atomic>

// 循环锁实现
class Spinlock {
private:
    std::atomic_flag flag = ATOMIC_FLAG_INIT;

public:
    void lock() {
        while (flag.test_and_set(std::memory_order_acquire)) {
            // 忙等待直到锁可用
        }
    }

    void unlock() {
        flag.clear(std::memory_order_release);
    }
};

// 共享资源
int shared_resource = 0;

// 使用循环锁保护的临界区
void critical_section_with_spinlock(int id, Spinlock& spinlock) {
    spinlock.lock();
    shared_resource++;
    std::cout << "Thread " << id << " incremented shared_resource to " << shared_resource << std::endl;
    spinlock.unlock();
}

// 互斥锁实现
std::mutex mtx;

// 使用互斥锁保护的临界区
void critical_section_with_mutex(int id) {
    std::lock_guard<std::mutex> lock(mtx);
    shared_resource++;
    std::cout << "Thread " << id << " incremented shared_resource to " << shared_resource << std::endl;
}

int main() {
    const int num_threads = 5;
    std::thread threads[num_threads];

    Spinlock spinlock;

    // 使用循环锁保护的临界区
    std::cout << "Using Spinlock:" << std::endl;
    for (int i = 0; i < num_threads; ++i) {
        threads[i] = std::thread(critical_section_with_spinlock, i, std::ref(spinlock));
    }
    for (int i = 0; i < num_threads; ++i) {
        threads[i].join();
    }

    // 使用互斥锁保护的临界区
    std::cout << "\nUsing Mutex:" << std::endl;
    for (int i = 0; i < num_threads; ++i) {
        threads[i] = std::thread(critical_section_with_mutex, i);
    }
    for (int i = 0; i < num_threads; ++i) {
        threads[i].join();
    }

    return 0;
}
2. 线程与内核对象的同步
  内核对象机制的适应性 远远优于用户方式机制。实际上,内核对象机制的唯一不足之处是它的速度比较慢。当调用本 章中提到的任何新函数时,调用线程必须从用户方式转为内核方式。这个转换需要很大的代价: 往返一次需要占用 x86平台上的大约1000个CPU周期,当然,这还不包括执行内核方式代码, 即实现线程调用的函数的代码所需的时间。

a.等待函数

WaitForSingleObject 是 Windows 平台下用于等待单个内核对象的函数之一,通常用于线程同步。它的原型定义如下:
DWORD WaitForSingleObject(
  HANDLE hHandle,
  DWORD  dwMilliseconds
);
- hHandle:要等待的内核对象的句柄,可以是事件、互斥体、信号量、进程、线程等。
- dwMilliseconds:等待的时间,以毫秒为单位。如果设置为 INFINITE,则函数将无限期等待,直到对象被信号触发或者发生错误。
WaitForSingleObject 函数会阻塞当前线程,直到指定的内核对象变为有信号状态或者等待超时。返回值指示了等待的结果,可能的值包括:
- WAIT_OBJECT_0:对象变为有信号状态,等待成功。
- WAIT_TIMEOUT:等待超时。
- WAIT_FAILED:等待失败,可以通过调用 GetLastError 获取具体的错误代码。
这个函数常用于等待线程的退出、事件的触发、互斥体的可用等场景。
#include <iostream>
#include <Windows.h>

int main() {
    // 创建一个信号量并初始化为 0
    HANDLE hSemaphore = CreateSemaphore(NULL, 0, 1, NULL);
    if (hSemaphore == NULL) {
        std::cerr << "Failed to create semaphore!" << std::endl;
        return 1;
    }

    // 模拟另一个线程增加信号量
    std::thread([&]() {
        Sleep(2000); // 模拟一些任务执行时间
        ReleaseSemaphore(hSemaphore, 1, NULL); // 增加信号量
    }).detach();

    // 等待信号量变为有信号状态
    DWORD dwWaitResult = WaitForSingleObject(hSemaphore, INFINITE);
    if (dwWaitResult == WAIT_OBJECT_0) {
        std::cout << "Semaphore signaled!" << std::endl;
    } else if (dwWaitResult == WAIT_TIMEOUT) {
        std::cerr << "Wait timed out!" << std::endl;
    } else {
        std::cerr << "Wait failed with error code: " << GetLastError() << std::endl;
    }

    // 关闭信号量句柄
    CloseHandle(hSemaphore);

    return 0;
}

b.事件内核对象

Windows 事件内核对象是 Windows 操作系统中用于线程同步和通信的一种基本内核对象。它允许线程在等待某些事件发生时挂起自己,直到事件发生并通知该线程,然后线程被唤醒并可以继续执行。
常见用途:

  1. 多线程同步:在多线程编程中,您可以使用事件对象来同步线程的执行。例如,一个线程在完成某项任务后,可以设置一个事件对象为已通知状态,而其他线程则可以等待这个事件的发生以执行相应的操作。
  2. 线程通信:事件对象也可以用于线程之间的通信。例如,一个线程等待另一个线程完成某项任务并通知它,以便执行接下来的操作。
  3. 信号量实现:在一些情况下,事件对象可以用于实现信号量的功能。通过设置事件对象的初始状态和在事件通知时将其重置,可以实现简单的信号量机制。
  4. 等待超时:线程可以等待事件对象的发生,但也可以指定一个超时时间,在超时时间到达后即使事件未发生也会继续执行。
  5. 异步 I/O:在异步 I/O 操作中,事件对象通常用于通知 I/O 操作的完成状态。当 I/O 操作完成时,相应的事件对象会被设置为已通知状态,通知等待的线程继续执行后续操作。
    如果仅仅是用于线程同步和通信,一般来说,事件内核对象的性能会略好于信号量。因为事件对象只有两种状态(有信号和无信号),而信号量需要维护一个计数器来记录可用资源的数量,这可能会引入额外的开销。因此,在简单的线程同步场景下,使用事件对象可能更为高效。

c.等待定时器内核对象

#include <Windows.h>
#include <iostream>
int main() {// 创建等待定时器
    HANDLE hTimer = CreateWaitableTimer(NULL, TRUE, NULL);
    if (hTimer == NULL)
    {
        std::cerr << "Failed to create waitable timer\n"; return 1;
    }// 设置定时器触发时间为 5 秒后
    LARGE_INTEGER liDueTime;
    liDueTime.QuadPart = -50000000LL; // 5 秒后的 100-nanosecond intervals
    if(!SetWaitableTimer(hTimer, &liDueTime, 0, NULL, NULL, FALSE)) 
    {
        std::cerr << "Failed to set waitable timer\n";
        CloseHandle(hTimer); return 1;
    }


    std::cout << "Wait for 5 seconds...\n";// 等待定时器触发
    DWORD dwWaitResult = WaitForSingleObject(hTimer, INFINITE);
    if (dwWaitResult == WAIT_OBJECT_0) {
        std::cout << "Timer triggered!\n";
    }
    else {
        std::cerr << "Wait for timer failed\n";
    }// 关
}

TimerAPCProc 是一个异步调用函数。在 Windows 操作系统中,APC(Asynchronous Procedure Call)是一种机制,用于在指定线程的上下文中异步执行一段代码。当等待定时器触发时,系统会将指定的 APC 回调函数插入到线程的 APC 队列中,并在合适的时机执行该回调函数,从而实现异步调用的效果。
在下面的示例中,当等待定时器触发时,系统会将 TimerAPCProc 函数插入到当前线程的 APC 队列中,然后在适当的时候执行该函数。因此,TimerAPCProc 函数会以异步的方式在当前线程上执行,而不会阻塞当前线程的执行。

#include <Windows.h>
#include <iostream>

// APC回调函数
VOID CALLBACK TimerAPCProc(LPVOID lpArgToCompletionRoutine, DWORD dwTimerLowValue, DWORD dwTimerHighValue)
{
    std::cout << "APC queued by timer!\n";
}

int main() {
    // 创建等待定时器
    HANDLE hTimer = CreateWaitableTimer(NULL, TRUE, NULL);
    if (hTimer == NULL) {
        std::cerr << "Failed to create waitable timer\n";
        return 1;
    }

    // 设置定时器触发时间为 5 秒后
    LARGE_INTEGER liDueTime;
    liDueTime.QuadPart = -50000000LL; // 5 秒后的 100-nanosecond intervals
    if (!SetWaitableTimerEx(hTimer, &liDueTime, 0, TimerAPCProc, NULL, NULL, WT_EXECUTEONLYONCE)) {
        std::cerr << "Failed to set waitable timer\n";
        CloseHandle(hTimer);
        return 1;
    }

    std::cout << "Wait for 5 seconds...\n";

    // 等待定时器触发
    DWORD dwWaitResult = WaitForSingleObject(hTimer, INFINITE);
    if (dwWaitResult == WAIT_OBJECT_0) {
        std::cout << "Timer triggered!\n";
    } else {
        std::cerr << "Wait for timer failed\n";
    }

    // 关闭定时器句柄
    CloseHandle(hTimer);

    return 0;
}

d.定时器松散特性

  1. 定时器的应用:定时器通常用于通信协议中,例如在客户端向服务器发送请求后,在规定时间内等待服务器响应。如果服务器未能及时响应,客户端可能会认为服务器无法使用。定时器在这种情况下可以帮助管理超时和等待响应的时间。
  2. 定时器的管理:文中提到,对于大多数应用程序来说,创建和管理多个定时器对象会导致系统运行性能受到影响。相比之下,可以创建一个单独的定时器对象,并根据需要修改其触发时间,以减少系统开销。
  3. 定时器与APC:虽然定时器可以用于排队APC项,但是大多数应用程序更倾向于使用I/O完成端口机制。过去,在线程池中可能会使用定时器来唤醒线程,但等待定时器并不直接提供这样的功能。因此,可能需要创建一个专门的线程来等待定时器,然后在定时器触发时通知线程池中的线程。
  4. 用户定时器与等待定时器的比较:用户定时器(通过SetTimer函数设置)与等待定时器有一些差别。用户定时器需要在应用程序中设置许多附加的用户界面结构,而等待定时器属于内核对象,可以供多个线程共享,并且更为高效。另外,用户定时器产生的WM_TIMER消息只会通知调用SetTimer的线程,而等待定时器可以唤醒多个线程。
    用户定时器是由应用程序控制的,用于应用程序中的定时任务处理;而等待定时器是由 操作系统提供的,用于操作系统中任务调度和管理的一种机制。
  5. 结构组织的便利性:如果要执行与用户界面相关的事件响应,使用用户定时器可能更容易组织代码结构,因为等待定时器时线程必须等待各种消息和内核对象,而用户定时器则更容易与消息队列相结合。
    e.信标内核对象
    信号量,它们与所有内核对象一样,包含一个使用数量,但是它们也包含另外两个带符号的32位值,一个是最大资源数量,一个是当前资源数量。最大资源 数量用于标识信标能够控制的资源的最大数量,而当前资源数量则用于标识当前可以使用的资源的数量。
    信标的使用规则如下:
  6. 如果当前资源的数量大于0,则发出信标信号:这意味着当有可用资源时,信标会发出信号,允许其他线程或进程访问这些资源。这个信号可以触发等待在信标上的其他线程或进程继续执行。
  7. 如果当前资源数量是0,则不发出信标信号:当资源已经被占用,没有可用资源时,信标不会发出信号,而是让其他线程或进程等待,直到有资源可用。
  8. 系统决不允许当前资源的数量为负值:这是为了确保资源的数量不会出现负值,即使在高并发情况下,也不会导致资源数量的错误计算。
  9. 当前资源数量决不能大于最大资源数量:这保证了资源数量不会超过预设的最大值,避免了资源溢出或其他异常情况的发生。
    HANDLE CreateSemaphore(
    LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,
    LONG lInitialCount,
    LONG lMaximumCount,
    LPCTSTR lpName
    );

f.互斥对象内核对象 mutex

互斥对象(mutex)内核对象能够确保线程拥有对单个资源的互斥访问权。实际上互斥对 象是因此而得名的。互斥对象包含一个使用数量,一个线程 ID和一个递归计数器。互斥对象的行为特性与关键代码段相同,但是互斥对象属于内核对象,而关键代码段则属于用户方式对象。 这意味着互斥对象的运行速度比关键代码段要慢。

  1. 线程所有权的概念:互斥对象是唯一一种具有线程所有权概念的内核对象。这意味着互斥对象不仅要追踪哪个线程成功地等待到了它,还要追踪哪个线程拥有它。
  2. 释放互斥对象的线程:当一个线程调用 ReleaseMutex 函数来释放互斥对象时,系统会检查调用线程的ID是否与互斥对象中记录的线程ID匹配。如果匹配,则递减递归计数器;如果不匹配,则返回失败,并提示调用者该线程不拥有该互斥对象。
  3. 线程终止时的处理:如果拥有互斥对象的线程在释放互斥对象之前终止了运行(通过 ExitThread、TerminateThread、ExitProcess 或 TerminateProcess 函数),系统会将该互斥对象视为被放弃。系统会将互斥对象的ID复位为0,并将递归计数器复位为0。
  4. 等待线程的处理:当系统检测到互斥对象被放弃后,会选择一个等待该互斥对象的线程,并将其设为可调度状态。不同于通常的情况下等待函数返回 WAIT_OBJECT_0 值,这种情况下等待函数返回 WAIT_ABANDONED 值。这个特殊的返回值指明了该互斥对象是被另一个线程拥有的,并且该线程已经终止运行。
  5. 处理 WAIT_ABANDONED 返回值:在实际应用中,大多数程序不会显式地检查 WAIT_ABANDONED 返回值,因为线程很少会刚刚终止运行。然而,如果出现了这种情况,应用程序需要自行决定如何处理被放弃的互斥对象,因为此时无法确定共享资源的状态。
    总的来说,互斥对象的线程所有权概念以及处理被放弃的互斥对象的机制确保了在多线程环境中对共享资源的正确访问和同步。

互斥对象与关键代码段区别
在等待线程的调度方面,互斥对象与关键代码段确实有一些相似之处,但也存在一些不同之处。
相似之处:

  1. 阻塞特性:在等待线程的调度过程中,互斥对象和关键代码段都具有阻塞特性。当一个线程试图访问被互斥对象保护的资源或者执行被互斥锁保护的代码段时,如果资源已被其他线程占用,则线程会被阻塞,直到资源可用或代码段解锁。
  2. 同步特性:互斥对象和关键代码段都用于实现线程同步,确保对共享资源的安全访问。它们都能够防止多个线程同时访问共享资源,从而避免数据竞争和不一致性。
    不同之处:
  3. 范围:互斥对象的作用范围比关键代码段更广泛。互斥对象可以用于保护任意大小的代码块或资源,而关键代码段通常只能用于保护特定的一小部分代码或资源。
  4. 可重入性:互斥对象通常是可重入的,这意味着同一个线程可以多次获得同一个互斥对象的锁。但是关键代码段可能不具备可重入性,尤其是在一些特定的编程环境中,需要谨慎处理递归调用或者嵌套调用的情况。
  5. 所有权:互斥对象具有所有权的概念,即某个线程拥有该互斥对象的所有权。而关键代码段通常没有所有权的概念,任何线程都可以执行关键代码段,只要它没有被其他线程占用。
  6. 等待机制:在等待线程的调度过程中,互斥对象通常使用等待队列来管理等待线程,而关键代码段可能使用操作系统提供的原子操作或者其他机制来管理等待线程。
    综上所述,尽管互斥对象与关键代码段在等待线程调度方面有一些相似之处,但它们在作用范围、可重入性、所有权和等待机制等方面存在明显的不同。在实际应用中,程序员需要根据具体的需求和情况选择合适的同步机制来确保程序的正确性和性能。

g.其他线程同步函数

  1. 异步设备 I/O:异步设备 I/O 允许程序执行异步输入/输出操作,而不会阻塞当前线程。通过异步 I/O,可以在等待设备响应的同时执行其他任务。
  2. WaitForInputIdle:该函数等待指定的进程在其主线程上进入空闲状态。通常用于等待新启动的进程初始化完成。
  3. MsgWaitForMultipleObjects (Ex):这是一个高级的等待函数,允许线程等待多个内核对象的状态变化。它可以等待多个事件、信号量、互斥体等,并且可以设置超时时间。
  4. WaitForDebugEvent:该函数用于等待一个调试事件的发生,通常在调试器程序中使用。它允许程序在调试时暂停并等待调试事件的发生。
  5. SingleObjectAndWait:该函数等待一个内核对象的状态变化,并同时等待另一个线程的终止。当指定的内核对象进入 signaled 状态或者指定的线程终止时,该函数返回。

第7章 内存管理

第8章 动态链接库

第9章 异常

第10章 窗口

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值