当前位置: 首页 > article >正文

【第14节】windows sdk编程:进程与线程介绍

目录

一、进程与线程概述

1.1 进程查看

1.2 何为进程

1.3 进程的创建

1.4 进程创建实例

1.5 线程查看

1.6 何为线程

1.7 线程的创建

1.8 线程函数

1.9 线程实例

二、内核对象

2.1 何为内核对象

2.2 内核对象的公共特点

2.3 内核对象句柄

2.4 内核对象的跨进程访问


一、进程与线程概述

1.1 进程查看

方法1
        在屏幕底部的任务栏上右击鼠标,从弹出的快捷菜单中选择“任务管理器”,随后任务管理器窗口将会打开,您便可以在其中浏览当前运行的进程。

方法2
        点击开始菜单,在搜索栏中输入“cmd”并打开命令提示符,然后在命令提示符中输入“tasklist”命令来查看当前运行的进程。

方法3
        在命令提示符中,首先键入“wmic”并按下回车,接着输入“process”来查看进程信息。

1.2 何为进程

        进程是指一个正在运行的程序实例。它最早在20世纪60年代由麻省理工学院和IBM公司的CTSS/360系统引入,并成为操作系统结构的基础。进程是操作系统进行资源分配和调度的基本单位,代表程序在数据集上的执行过程。一个程序可能同时属于多个进程。

        进程本身并不执行代码,而是作为线程的容器。每个进程在创建时都会生成一个主线程来执行代码。如果主线程结束,系统会销毁该进程的内核对象。

        进程可以分为系统进程和用户进程。系统进程负责完成操作系统的各种功能,相当于运行状态下的操作系统本身;用户进程则是由用户启动的程序实例。

        在Windows系统中,进程进一步细分为线程,一个进程可以包含多个独立运行的线程。与静止的程序不同,进程是动态的。为了描述多任务操作系统中多道程序并发执行时的资源分配、程序执行的间断性、通信可能性以及同步互斥等动态特性,引入了进程的概念来参与系统的并发执行。

1.3 进程的创建

CreateProcess()函数可以用来创建一个进程,原型如下所示:

BOOL WINAPI CreateProcess(
    _In_opt_ LPCTSTR lpApplicationName, //可执行文件名
    _Inout_opt  LPTSTR lpCommandLine,  //命令行
    _In_opt_  LPSECURITY_ATTRIBUTES  lpProcessAttributes,
    _In_opt_ LPSECURITY_ATTRIBUTES lpThreadAttributes,
    _In       BOOL  bInheritHandles,      //句柄继承是否
    _In       DWORD  dwCreationFlags,     //创建方式标志
    _In_opt   LPVOID   lpEnvironment,     //环境字符串块
    _In_opt   LPCTSTR lpCurrentDirectory, //新进程的当前目录
    _In       LPSTARTUPINFO lpStartupInfo, //进程配置结构体
    _Out_     LPPROCESS_INFORMATION lpProcessInformation
);


        此函数执行成功后将创建一个进程内核对象,但需要注意的是,“进程内核对象”与“进程”所指的并非是同一个东西。因此,CreateProcess()函数可以创建成功并不代表进程本身就能正常加载并运行。

更详细的参数解释:
- `lpApplicationName`:要执行的可执行文件名称。
- `lpCommandLine`:传递给新进程的命令行字符串,需要注意的是,此参数类型为PTSTR,这意味着CreateProces()在执行过程中可能会修改我们传入的值。
- `lpProcessAttributes`:进程安全属性,这些都属于内核对象。
- `lpThreadAttributes`:线程安全属性。
- `bInheritHandles`:表示新创建的子进程是否继承父进程中的所有句柄,是的话子进程就可以访问父进程中创建的所有句柄。
- `dwCreationFlags`:子进程的创建方式。
- `lpEnvironment`:指向保存有进程环境字符串的内存块。
- `lpCurrentDirectory`:新创建子进程的当前目录。
- `lpStartupInfo`:指向子进程创建配置结构体,此结构可以详细控制子进程的各种创建状态。
- `lpProcessInformation`:返回进程创建的详细信息。

1.4 进程创建实例

bool CreateChildProcess(LPWSTR lpPath, BOOL bWait) {
    // 初始化 STARTUPINFO 结构体,设置其大小为结构体本身的大小
    STARTUPINFO si = {0};
    si.cb = sizeof(si);

    // 初始化 PROCESS_INFORMATION 结构体,用于接收新进程的信息
    PROCESS_INFORMATION pi = {0};

    // 尝试创建子进程
    if (!CreateProcess(
            lpPath,   // 可执行文件的路径
            NULL,     // 命令行参数(未使用)
            NULL,     // 进程句柄不可继承
            NULL,     // 线程句柄不可继承
            FALSE,    // 不继承句柄
            0,        // 无特殊标志
            NULL,     // 使用父进程的环境变量
            NULL,     // 使用父进程的当前目录
            &si,      // 指向 STARTUPINFO 结构体
            &pi)) {   // 指向 PROCESS_INFORMATION 结构体
        // 如果创建进程失败,返回 false
        return false;
    }

    // 如果需要等待子进程执行结束
    if (bWait) {
        // 无限等待子进程结束
        WaitForSingleObject(pi.hProcess, INFINITE);
    }

    // 关闭进程句柄
    CloseHandle(pi.hProcess);
    // 关闭线程句柄
    CloseHandle(pi.hThread);

    // 返回 true 表示成功创建子进程
    return true;
}

1.5 线程查看

        使用PCHunter或者火绒剑工具可以查看指定进程的线程及线程的信息。

1.6 何为线程

        线程是操作系统中的一个内核对象。在Windows操作系统的内核中,并没有进程的概念,只有线程的概念。进程实际上是从逻辑上对一组线程及其相关资源进行的整合。

        线程是“进程”中的一个单一顺序的控制流,也被称为轻量级进程(lightweight processes),是程序运行时的调度单位。一个线程可以创建多个子线程,而这些子线程又可以继续创建更多的线程。

        从资源占用的角度来看,线程所需的系统资源远远少于进程,但它在功能实现上并不逊色。因此,在开发项目时,应优先考虑创建新的线程,而不是启动一个新的进程。

1.7 线程的创建

创建线程的函数原型如下:

HANDLE WINAPI CreateThread(
    _In_opt_  LPSECURITY_ATTRIBUTES lpThreadAttributes,
    _In_      SIZE_T                dwStackSize,  //栈大小,默认1MB
    _In_       LPTHREAD_START_ROUTINE lpStartAddress, //不能为0
    _In_opt_  LPVOID                lpParameter,
    _In_      DWORD                 dwCreationFlags,
    _Out_opt_ LPDWORD               lpThreadId
);


参数解释:
- `dwStackSize`:指定线程可以拥有多少栈,属于线程。
- `lpStartAddress`:线程函数起始地址。
- `lpParameter`:线程函数参数。
- `dwCreationFlags`:线程创建标记。
- `lpThreadId`:新创建线程的ID。

1.8 线程函数

        像主线程的初始函数为mainCRTStartup/wmainCRTStartup一样,我们创建线程时也需要指定一个线程函数。为了取得一致性,Windows对线程函数做了限定,其原型如下所示:

DWORD WINAPI ThreadProc(LPVOID lpParam) {
    return 0;
}

        就像线程可指定线程函数一样,主线程同样可以指定自己的线程函数,在VisualStudio下,可以通过以下编译选项指定主线程函数:

//指定程序入口函数为MyFun()
#pragma comment(linker,"/entry:\"MyFun\"")

1.9 线程实例

// 线程函数,显示一个消息框
DWORD WINAPI ThreadProc(LPVOID lpParam) {
    // 显示消息框
    MessageBox(NULL, (LPCWSTR)lpParam, L"CreateThread", MB_OK);
    return 0; // 线程正常退出
}

// 创建子线程的函数
bool CreateChildThread() {
    DWORD dwThreadId = 0; // 用于保存线程ID
    HANDLE hThread = CreateThread(
        NULL,          // 默认安全属性
        0,             // 默认堆栈大小
        ThreadProc,    // 线程函数
        (LPVOID)L"Hello from the new thread!", // 传递给线程的参数
        0,             // 默认创建标志
        &dwThreadId);  // 返回线程ID

    // 检查线程是否创建成功
    if (hThread == NULL) {
        std::cerr << "CreateThread failed: " << GetLastError() << std::endl;
        return false; // 创建线程失败,返回false
    }

    // 关闭线程句柄,避免资源泄漏
    CloseHandle(hThread);

    std::cout << "子线程创建成功,线程ID: " << dwThreadId << std::endl;
    return true; // 创建线程成功,返回true
}

二、内核对象

2.1 何为内核对象

        Windows操作系统虽然是用C语言和汇编语言编写的,但它是一个面向对象的操作系统,系统中广泛使用了对象的概念。例如,窗口、设备环境、代表DLL的模块、程序实例、文件、进程、画笔、画刷等都是对象。从本质上说,这些对象就是一个个结构体变量。然而,Windows系统不希望程序员直接定义、访问或修改这些结构体变量,因此它们被完全保护了起来。在使用这些对象时,都有一个共同点:必须先获取它们的句柄,然后通过调用相应的API来操作这些对象。

        简单来说,句柄就像是访问对象的“钥匙”,或者用程序员的话来说,句柄是对象的索引。

        根据对象的不同用途,Windows将对象分为多个类别,主要包括用户对象(user对象)、图形设备接口对象(GDI对象)和内核对象。其中,内核对象通常与系统的全局功能相关。

        常见的Windows内核对象包括:进程、线程、访问令牌、文件、文件映射、I/O完成端口、邮槽、管道、互斥体、信号量、事件、计时器和线程池等。

2.2 内核对象的公共特点

所有内核对象都遵循统一的使用模式:
(1)第一步:创建对象:创建一个内核对象,一般都是使用CreateXXX的方式,比如:
    - `CreateProcess`:创建进程。
    - `CreateThread`:创建线程。
    - `CreateFile`:创建文件。
    - `CreateFileMapping`:创建文件映射。
    - `CreateEvent`:创建事件对象。
    - `CreateSemaphore`:创建信号量。
(2)第二步:打开对象,得到句柄 (可与第一步合并在一起,表示创建的时后就打开)。
(3)第三步:通过API访问对象。
(4)第四步:关闭句柄。
(5)第五步:句柄全部关完,对象自动销毁。

        所有的内核对象都由操作系统内核管理,并且可以在不同进程之间访问,这就是所谓的“内核对象是跨进程的”。在许多情况下,我们需要在不同的进程中访问同一个内核对象,比如实现进程间的同步或共享数据。

        每个内核对象都有一个引用计数。可以理解为每个内核对象的结构体中都有一个字段用来记录引用计数。当一个进程创建或打开这个内核对象时,引用计数会增加1;当进程终止或关闭句柄时,引用计数会减少1。当引用计数减到0时,内核对象就会被销毁。

        举个例子,如果内核对象M由进程A创建,之后进程B也使用了这个对象,那么即使进程A退出了,M也不会被销毁,因为它仍然被进程B使用。只有当没有任何进程使用这个内核对象时,它才会被自动销毁。

        在创建内核对象时,每个对象都会有一个安全属性。这个安全属性定义了对象的使用方式,比如是否可读可写,以及对象具有的权限等。一旦内核对象以某种安全属性创建,它就只能在规定的权限范围内工作。通常,在创建内核对象时,我们需要指定一个`SECURITY_ATTRIBUTES`结构体来设置安全属性。如果传入`NULL`,系统会使用默认的安全属性。

2.3 内核对象句柄

        在Windows操作系统中,操作对象需要通过句柄来实现,内核对象也不例外。不过,内核对象的句柄是与进程相关的,这意味着同一个内核对象在不同进程中的句柄值是不同的。这一点与GDI对象不同,GDI对象的句柄值是全局有效的,不同进程可以使用相同的句柄值访问同一个GDI对象。由此可见,不同类型的对象在管理方式上存在差异。

        每个进程都有一个句柄表,用于记录该进程打开的所有内核对象。可以简单地把句柄表理解为一个一维的结构体数组,而句柄值就是这个数组中的索引。因此,内核对象的句柄值仅对当前进程有效。

        句柄表中的每一项不仅记录了通过该句柄访问内核对象的权限,还指明了该句柄是否可以被其子进程继承。

2.4 内核对象的跨进程访问

        要访问内核对象,进程的句柄表中必须有一个句柄项指向该内核对象。对于创建该内核对象的进程,句柄会直接插入到其句柄表中。而对于其他进程,通常有三种方式可以跨进程访问内核对象:

(1)父进程继承给子进程:当父进程创建子进程时,如果指定了句柄可继承的属性,子进程会继承父进程中所有可继承的句柄。需要注意的是,即使子进程继承了句柄,它也不知道具体继承了哪些句柄以及它们的值,这些信息只能由父进程通过进程间通信的方式传递给子进程。

(2)通过命名内核对象:在进程A中创建内核对象时,可以为该对象命名。进程B可以通过名称打开该内核对象。如果内核对象无法命名或没有唯一标识,则无法使用这种方式。

(3)使用`DuplicateHandle`函数:通过这个函数,可以将一个句柄从一个进程传递给另一个进程,从而在目标进程中获得源进程中内核对象的句柄。

        通常,获取内核对象句柄的方式有以下四种:
(1)创建对象:例如,使用`CreateFile`创建新文件时,会同时打开该文件对象并获得句柄。
(2)继承句柄:子进程继承父进程句柄表中的句柄。
(3)显式打开:例如,使用`OpenFile`、`OpenMutex`、`OpenProcess`等函数显式打开某个对象。
(4)`DuplicateHandle`:通过该函数间接打开对象并获得句柄。

        需要注意的是,句柄的概念可以理解为“具有指定权限和属性的访问句柄”。句柄代表对对象的一次打开操作,其权限决定了可以通过该句柄执行的操作。例如,如果打开文件时申请的权限是只读,那么即使当前用户拥有写权限,使用该句柄调用`WriteFile`也会被拒绝访问。

        句柄的属性中,一个重要特性是它是否可以被继承给子进程。


http://www.kler.cn/a/592752.html

相关文章:

  • Java-泛型总结
  • 如何使用PHP爬虫根据关键词获取Shopee商品列表?
  • 两个docker app调用
  • 2025年2月AGI技术月评|重构创作边界:从视频生成革命到多模态生态的全面爆发
  • 【华为OD-E卷 - 求符合条件元组个数 100分(python、java、c++、js、c)】
  • Django初窥门径-Django REST Framework 基础使用
  • 单片机—中断系统
  • L2TP实验 作业
  • 数据通信与计算机网络——网络模型
  • 10、基于osg引擎生成热力图高度图实现3D热力图可视化、3D热力图实时更新(带过渡效果)
  • skywalking微服务链路追踪
  • LLVM学习--外部项目
  • Mistral AI发布开源多模态模型Mistral Small 3.1:240亿参数实现超越GPT-4o Mini的性能
  • NVIDIA Isaac GR00T N1:世界首个开源通用人形机器人基础模型
  • 3D点云数据处理中的聚类算法总结
  • 15 数据结构及算法应用
  • 蓝桥杯真题——洛谷Day13 找规律(修建灌木)、字符串(乘法表)、队列(球票)
  • SqlServer Sql学习随笔
  • Apache Tomcat漏洞公开发布仅30小时后即遭利用
  • SQL Server数据库简介及应用