【进程和线程】学习笔记(一)----进程和线程初识以及一些API整理

进程和线程

线程

创建线程

头文件#include <Windows.h>

CreateThread() 是 Windows 系统 API 中用于创建线程的函数,它的定义如下:

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

下面是每个参数的详细说明:

  1. lpThreadAttributes:线程对象的安全描述符,用于指定线程对象的安全属性,一般情况下设为 NULL 即可。

  2. dwStackSize:线程的堆栈大小,以字节为单位。如果为 0,则使用默认堆栈大小。

  3. lpStartAddress:线程的入口函数,即线程启动后要执行的函数指针。该函数必须是一个无返回值、无参数的函数,或者是一个返回值为 DWORD、带一个 LPVOID 类型参数的函数。

    DWORD就是typedef unsigned long

    PVOID就是typedef void*

    LPVOID就是typedef void far

    扫描二维码关注公众号,回复: 14730025 查看本文章
  4. lpParameter:指向要传递给线程函数的参数的指针。如果不需要传递参数,则设为 NULL。

  5. dwCreationFlags:指定线程的创建标志,如是否创建暂停的线程、是否继承父进程的优先级等。

    当简历的线程不马上执行时标为CREATE_SUSPENDED,线程将暂停直到呼叫

  6. lpThreadId:指向一个变量,用于接收新线程的标识符,如果不需要获取线程标识符,则设为 NULL。

需要注意的是,CreateThread() 函数创建的线程是在同一个进程中运行的,这些线程共享该进程的资源,如内存空间、全局变量等。如果需要在不同进程之间创建线程,则需要使用其他的技术,如进程间通信技术。

终止线程

1.线程函数返回:

​ 这是确保线程的所有资源被正确地清除的唯一办法。当线程函数返回是,如下情况将会发生:

​ 在线程函数中创建的所有C++对象将通过它们的析构函数正确的撤销

​ 操作系统将正确的释放线程的堆栈使用的内存

​ 系统将线程的退出代码设置为线程函数的返回值

​ 系统递减线程内核对象的引用计数

2.ExitTread函数:

​ 可以通过在线程中调用ExitThread函数,来强制终止自身线程的运行。原型为:

VOID ExitThread(DWORD dwExitCode);

​ 该函数将终止自身线程的运行,并导致操作系统清除该线程使用的所有操作系统资源。但是,C++资源(如C++对象)讲不被正确地撤销。

线程的挂起与恢复运行

任何线程都可以调用SuspendTread()来暂停另一个线程的运行,只要拥有线程的句柄。

原型为:

DWORD SuspendThread(HANDLE hThread);

返回值是前一次暂停计数,一个线程能够被暂停的最多次数是MAXIMUM SUSPEND COUNT

参数HANDLE hThread表示将要被挂起的线程

调用ResumeThread 可以让挂起的线程恢复运行。原型为:

DWORD ResumeThread(HANDLE hThread);

返回值是前一次暂停计数,参数HANDLE hThread表示将要恢复的线程

##线程的优先级

BOOL SetThreadPrioroty //设置线程优先级
    (
    	HANDLE hThread,  //线程的句柄
    	int nPriority    //线程优先级级别
	);
线程优先级等级 标志 优先级值
1 Idle(最低) THREAD_PRIORITY_IDLE 如果进程优先级为realtime则调整16,
其它情况为1
2 LOWEST 低 THREAD_PRIORITY_LOWEST -2(在原有基础上-2)
3 BELOW 低于标准 THREAD_PRIORITY_BELOW -1(在原有基础上-1)
4 NORMAL 标准 THREAD_PRIORITY_NORMAL 不变(取进程优先级值)
5 ABOVE 高于标准 THREAD_PRIORITY_ABOVE +1(在原有基础上+1)
6 HIGHEST 高 THREAD_PRIORITY_HIGHEST +2(在原有基础上+2)
7 CRITICAL 最高 THREAD_PRIORITY_CRITICAL 如果进程优先级为realtime则调整31,
其它情况为15

线程间同步

WaitForSingleObject(h, INFINITE); 等待线程h INFINITE是无限等待的意思

在 Windows 平台下,C++ 中线程间同步的办法有以下几种:

  1. 互斥锁(Mutex):互斥锁是一种常用的线程同步机制,它可以保证在同一时刻只有一个线程访问共享资源。在 Windows 平台下,可以使用 CreateMutex() 函数创建互斥锁,使用 WaitForSingleObject() 函数和 ReleaseMutex() 函数来加锁和解锁互斥锁。
  2. 信号量(Semaphore):信号量是一种用于控制多线程互斥和同步的机制,它可以控制同时访问某个共享资源的线程数量。在 Windows 平台下,可以使用 CreateSemaphore() 函数创建信号量,使用 WaitForSingleObject() 函数和 ReleaseSemaphore() 函数来加锁和解锁信号量。
  3. 事件(Event):事件是一种用于线程间通信和同步的机制,它可以允许一个线程等待另一个线程的操作完成。在 Windows 平台下,可以使用 CreateEvent() 函数创建事件,使用 WaitForSingleObject() 函数和 SetEvent() 函数来等待和设置事件。
  4. 临界区(Critical Section):临界区是一种轻量级的同步机制,它可以确保在同一时刻只有一个线程访问共享资源。在 Windows 平台下,可以使用 InitializeCriticalSection() 函数和 EnterCriticalSection() 函数和 LeaveCriticalSection() 函数来加锁和解锁临界区。
  5. 条件变量(Condition Variable):条件变量是一种用于线程间同步的机制,它允许一个或多个线程等待某个条件成立。在 Windows 平台下,可以使用 CONDITION_VARIABLE 类型和相关函数来实现条件变量。

原子锁

对变量的加减

LONG InterlockedIncrement(LONG volatile* IpAddend);//该函数提供多线程情况下,对一个变量以原子操作方式增加1
LONG InterlockedDecrement(LONG volatile* IpAddend);//该函数提供多线程情况下,对一个变量以原子操作方式减少1
LONG InterlockedExchange(LONG volatile* ipTarget,LONG IValue);//该函数提供多线程情况下,以原子操作方式用IValue给IpTarget指向的目标变量赋值,并返回赋值以前的IpTarget指向的值。
LONG InterlockedEXchangeAdd(LONG volatile* IpAddend,LONG IValue);//该函数提供多线程情况下,以原子操作方式将IpAddend指向的变量增加Value。并返回调用前的IpAddend指向的目标变量的值

临界区

临界区是一段连续的代码区域,它要求在指向前获得对某些共享数据的独占的访问权。如果一个进程中的所有线程中访问这些共享数据的代码都放在临界区中,就能够实现对该共享数据的同步访问。临界区只能用于同步单个进程中的线程。

CRITICAL_SECTION g_sec;			  //实例化临界区对象
initializeCriticalSection(&g_sec);//初始化临界区对象

EnterCriticalSection(&g_sec); //进入临界区
//临界区操作
LeaveCriticalSection(&g_sec); //离开临界区

DeleteCriticalSection(&g_sec); //释放临界区对象

进入临界区,临界区对象的引用计数加1,同一个线程可以多次调用EnterCriticalSection,但是如果调用n次

EnterCriticalSection以后,必须再调用n次的LeaveCriticalSection,使临界区对象的引用计数变为0

其它的线程才能进入临界区EnterCriticalSection(&g_sec);

离开临界区LeaveCriticalSection(&g_sec);

释放临界区对象DeleteCriticalSection(&g_sec);

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

using namespace std;
unsigned int g = 0;
int n = 1000000;

//创建临界区对象
CRITICAL_SECTION g_sec;

DWORD WINAPI ThreadProc(LPVOID lp)
{
	for (int i = 1; i <= n; i++)
	{
		EnterCriticalSection(&g_sec); //进入临界区
		g++;
		LeaveCriticalSection(&g_sec); //离开临界区
	}

	return 0;
}


int main() 
{
	//初始化临界区
	InitializeCriticalSection(&g_sec);

	HANDLE h = CreateThread(NULL,0,ThreadProc,0, 0,0);

	for (int i = 1; i <= n; i++)
	{
		EnterCriticalSection(&g_sec);
		g++;
		LeaveCriticalSection(&g_sec);
	}

	WaitForSingleObject(h, INFINITE);
	cout << "g=" << g << endl;

	CloseHandle(h);  //关闭句柄
    DeleteCriticalSection(&g_sec); //释放临界区对象

	return 0;
}

等待线程函数:

​ 在多线程下面,有时候我们会希望等待某一线程完成了再继续做其他事情,要实现这个目的,可以使用Windows API函数WaitForSingleObject,或者WaitForMultipleObjects。这两个函数都会等待Object被标为有信号(signaled)时才返回的。这两个函数的优点是它们在等待的过程中会进入一个非常高效沉睡状态,只占用极少的CPU时间片。

WaitForSingleObject()

WaitForSingleObject()

1、格式

DWORD WaitForSingleObject(HANDLE hHandle,DWORD dwMilliseconds);有两个参数,分别是THandle和Timeout(毫秒单位)

如果想要等待一条线程,那么需要指定线程的Handle,以及相应的Timeout时间。当然,如果你想无限等待下去,Timeout参数可以指定系统常量INFINITE。

2、使用对象

Event,Mutex,Semaphore,Process,Thread

3、返回类型

WAIT OBJECT 0,表示等待的对象有信号(对线程来说,表示执行结束);WAIT TIMEOUT,表示等待指定时间内,对象一直没有信号(线程没有执行完);

WAIT ABANDONED 表示对象有信号,但还是不能执行 一般是因为未获取到锁或其它原因。

WaitForMultipleObject()

DWORD WaitForMultipleObjects(DWORD nCount,CONST HANDLE *IpHandles,

BOOLfWaitAll,DWORDdwMilliseconds);

四个参数分别是:

1、nCount,DWORD类型,用于指定句柄数组的数量

2、IphObjects,Pointer类型;用于指定句柄数组的内存地址

3、fWaitAll,Boolean类型,True表示函数等待所有指定句柄的Object有信号为止

4、dwTimeout,DWORD类型,用于指定等待的Timeout时间,单位毫秒,可以是INFINITE

当WaitForMultipleObjects等待多个内核对象的时候,如果它的bWaitAll参数设置为false。其返回值减去WAIT OBJECT 0就是参数IpHandles数组的序号。如果同时又多个内核对象被触发,这个函数返回的知识其中序号最小的那个。

如果为TRUE 则等待所有信号量有效在往下执行。(FALSE 当有其中一个信号量有效时就向下执行)

内核对象

临界区非常适合于在同一个进程内部以序列化的方式访问共享的数据。然而,有时用户希望一个线程与其它线程执行的某些操作取得同步,这就需要使用内核对象来同步线程。

常用的内核对象有:互斥变量、信号量和事件、其它的还包括文件、控制台输入、文件变化通知、可等待的计时器。

每一个内核独享在任何时候都处于两种状态之一:信号态(signaled) 和无信号态(nonsignaled)

线程在等待其中的一个或多个内核对象时,如果在等待的一个或多个内核对象处于无信号态,线程自身将被系统挂起,直到等待的内核对象变为信号态是,线程才恢复运行。常用的等待函数有2个;

互斥变量

互斥变量类似于临界区,但它能够同步多个进程间的数据访问。

CreateMutex (NULL,FALSE,"'MutexForSubThread');  //创建互斥变量

WaitForSingleObject (g_hMutex,INFINITE); //等待互斥量,如果互斥量处于信号态,该函数返回,同步互斥量自动变为无信号态

访问共享数据区域

ReleaseMutex(g hMutex); //使互斥量重新处于信号态

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

using namespace std;
unsigned int g = 0;
int n = 100000;

//互斥量
HANDLE hmutex;


DWORD WINAPI ThreadProc(LPVOID lp)
{
	for (int i = 1; i <= n; i++)
	{
		WaitForSingleObject(hmutex, INFINITE); //等待互斥量变为有信号
		g++;
		ReleaseMutex(hmutex); //释放互斥量
	}

	return 0;
}


int main() 
{
	//创建互斥变量
	hmutex = CreateMutex(NULL, FALSE, NULL); //创建互斥变量

	HANDLE h = CreateThread(NULL,0,ThreadProc,0, 0,0);

	for (int i = 1; i <= n; i++)
	{
		WaitForSingleObject(hmutex, INFINITE);
		g++;
		ReleaseMutex(hmutex);
	}

	WaitForSingleObject(h, INFINITE);
	cout << "g=" << g << endl;

	CloseHandle(hmutex); //释放句柄

	return 0;
}

注意:WaitForSingleObject();ReleaseMutex();花的时间比较多如果把g提到1000000级别会导致一直等

信号量

信号量内核对象用于资源计数,每当线程调用WaitForSingleObject() 函数并传入一个信号量对象的句柄,系统将检查该信号量的资源计数是否大于0;

如果大于0,表示有资源可用,此时系统就将资源计数减去1,并唤醒线程;

如果等于0,表示无资源可用,系统就将线程挂起,直到另外一个线程释放该对象,释放信号量意味着增加它的资源计数。

信号量与临界区和互斥量不同,信号量不属于任何线程。因此可以在一个线程中增加信号量的计数,而在另一个线程中减少信号量的计数。

但是在使用过程中,信号量的使用与互斥量非常相似,互斥量可以看作是信号量的一个特殊版本,即可以将互斥量看作最大资源计数为1的信号量。

信号量常用在如下情况下,M个线程对N个共享资源的访问,其中M > N。

CreateSemaphore(NULL,iCount iCount, _T("Semaphore")); //创建信号量
WaitForSingleObject(m hSemaphore,INFINITE); //等待信号量
ReleaseSemaphore(m hSemaphore, 1, NULL); //释放信号量
事件对象

与互斥量和信号量不同,互斥变量和信号量用于控制对共享数据的访问,而事件发送信号表示某一操作已经完成。

有两种事件对象:手动重置事件和自动重置事件。

手动重置事件用于同时向多个线程发送信号;自动重置事件用于向一个线程发送信号。

如果有多个线程调用WaitForSingleObjects() 或者WaitForMultipleObjects()等待一个自动重置事件,那么当该自动重置事件变为信号态时,其中的一个线程会被唤醒,被唤醒的线程开始继续运行,同时自动重置事件又被置为无信号态,其它线程依旧处于挂起状态。从这一点看,自动重置事件有点类似于互斥量。

手动重置事件不会被WaitForSingleObjects() 或者WaitForMultipleObjects()自动重置为无信号态,需要调用相应的函数才能将手动重置事件重置为无信号态。因此,当手工重置事件有信号时,所有等待该事件的线程都将被激活。

例如对于进程或者线程对象,等待成功就表示进程或线程执行结束了;对于互斥量对象,则表示此对象现在不被任何其它线程拥有,并且一旦等待成功,当前线程即拥有了此互斥量,其它线程则不能同时拥有,直接调用ReleaseMutex函数主动释放互斥量。

使用CreateEvent()函数创建事件对象:

HANDLE CreateEvent(
  LPSECURITY_ATTRIBUTES lpEventAttributes,  // 事件对象的安全描述符
  BOOL                  bManualReset,       // 是否为手动重置事件
  BOOL                  bInitialState,      // 初始状态(有信号或无信号)
  LPCTSTR               lpName              // 事件对象的名称
);

下面是每个参数的详细说明:

  1. lpEventAttributes:指向 SECURITY_ATTRIBUTES 结构体的指针,用于设置事件对象的安全描述符。如果为 NULL,则使用默认安全描述符。
  2. bManualReset:指定事件对象的重置方式。如果为 TRUE,则表示事件对象为手动重置,需要调用 ResetEvent() 函数来重置事件;如果为 FALSE,则表示事件对象为自动重置,当一个线程等待事件后,事件会自动重置为无信号状态。
  3. bInitialState:指定事件对象的初始状态。如果为 TRUE,则表示事件对象初始为有信号状态,即调用 SetEvent() 函数后,事件对象会变成有信号状态;如果为 FALSE,则表示事件对象初始为无信号状态。
  4. lpName:指定事件对象的名称,可以用于在进程或系统范围内唯一标识事件对象。如果为 NULL,则表示事件对象没有名称,只能在进程内部使用。参数IpName指定事件对象的名称,其它进程中的线程可以通过该名称调用CreateEvent()或者OpenEvent()函数得到该事件对象的句柄。

通过SetEvent()函数设置为信号态:

注意:无论自动重置事件对象还是手动重置事件对象,都可以设置为有信号状态BOOL SetEvent(HANDLE hEvent);

通过ResetEvent函数设置为无信号态:

注意:无论自动重置事件对象还是手工重置事件对象,都可以BOOL ResetEvent(HANDLE hEvent);

不过对于自动重置事件不必执行ResetEvent,因此系统会在WaitForSingleObject()或者WaitForMultipleObjects()返回前,自动将事件对象置为无信号态。

用CloseHandle() 函数关闭事件

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

using namespace std;
unsigned int g = 0;
int n = 100000;

//事件
HANDLE hEvent;

DWORD WINAPI ThreadProc(LPVOID lp)
{
	for (int i = 1; i <= n; i++)
	{
		WaitForSingleObject(hEvent, INFINITE);//判断事件是否已完成,如果已完成设置为未完成
		g++;
		SetEvent(hEvent);//重新设置为已完成
	}

	return 0;
}

int main() 
{
	//创建事件对象
	hEvent = CreateEvent(NULL, FALSE, TRUE, NULL);//第三为TRUE表示初始化为已完成

	HANDLE h = CreateThread(NULL,0,ThreadProc,0, 0,0);

	for (int i = 1; i <= n; i++)
	{
		WaitForSingleObject(hEvent, INFINITE);
		g++;
		SetEvent(hEvent);
	}

	WaitForSingleObject(h, INFINITE);
	cout << "g=" << g << endl;

	CloseHandle(hEvent); //关闭事件

	return 0;
}

线程死锁

​ 假如有2个线程,一个线程想先锁对象1,再锁对象2,恰好另外有一个线程先锁对象2再锁对象1。再这个过程中,当线程1把对象1锁好以后,就想去锁对象2,但是不巧,线程2已经把对象2锁上了,也正在尝试去锁对象1.什么时候结束呢,只有线程1吧2个对象都锁上并把方法执行完,并且线程2把2个对象也都锁上并且把方法执行完毕,那么就解说了,但是,谁都不肯放掉已经锁上的对象,所以就没有结果,这种情况叫做线程死锁。多个线程间如果相互等待对方拥有的资源,将可能发生死锁。

线程间通信

1、使用全局变量进行通信

2、参数传递方式

3、消息传递方式

参数传递:

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

using namespace std;
unsigned int g = 0;
int n = 100000;

struct node
{
	int age;
	int id;
};

//事件
HANDLE hEvent;

DWORD WINAPI ThreadProc(LPVOID lp)
{
	node n = *(node*)lp;
	for (int i = 1; i <= n; i++)
	{
		WaitForSingleObject(hEvent, INFINITE);
		g++;
		SetEvent(hEvent);
	}

	return 0;
}

int main() 
{
	node n;
	n.id = 1, n.age = 10;
	//创建事件对象
	hEvent = CreateEvent(NULL, FALSE, TRUE, NULL);

	HANDLE h = CreateThread(NULL, 0, ThreadProc, &n, 0, 0);

	for (int i = 1; i <= n; i++)
	{
		WaitForSingleObject(hEvent, INFINITE);
		g++;
		SetEvent(hEvent);
	}

	WaitForSingleObject(h, INFINITE);
	cout << "g=" << g << endl;

	CloseHandle(hEvent); //关闭事件

	return 0;
}

进程

进程的创建

bool CreateProcess()返回bool表示创建成功还是失败

bool CreateProcess(
LPCWSTR               lpApplicationName,     // 可执行文件的文件名或路径
LPWSTR                lpCommandLine,         // 命令行参数
LPSECURITY_ATTRIBUTES lpProcessAttributes,   // 进程的安全属性
LPSECURITY_ATTRIBUTES lpThreadAttributes,    // 线程的安全属性
BOOL                  bInheritHandles,       // 是否继承父进程的句柄
DWORD                 dwCreationFlags,       // 指定如何创建进程
LPVOID                lpEnvironment,         // 指定进程的环境变量
LPCWSTR               lpCurrentDirectory,    // 指定进程的当前工作目录
LPSTARTUPINFOW        lpStartupInfo,         // 指向STARTUPINFO结构体的指针
LPPROCESS_INFORMATION lpProcessInformation   // 指向PROCESS_INFORMATION结构体的指针
);
  1. lpApplicationName:要执行的可执行文件的文件名或路径。如果为NULL,则使用lpCommandLine中的第一个参数作为可执行文件名。
  2. lpCommandLine:命令行参数。如果lpApplicationName为NULL,则此参数必须包含可执行文件名和任何参数。如果lpApplicationName不为NULL,则此参数可以包含可执行文件名、命令行参数和其他参数。
  3. lpProcessAttributes:进程的安全属性。如果为NULL,则进程不会继承安全属性。
  4. lpThreadAttributes:线程的安全属性。如果为NULL,则线程不会继承安全属性。
  5. bInheritHandles:指定是否继承父进程的句柄。如果为TRUE,则子进程继承父进程的句柄。如果为FALSE,则子进程不继承父进程的句柄。
  6. dwCreationFlags:指定如何创建进程。例如,可以指定CREATE_NEW_CONSOLE以创建一个新控制台窗口。
  7. lpEnvironment:指定进程的环境变量。如果为NULL,则使用父进程的环境变量。
  8. lpCurrentDirectory:指定进程的当前工作目录。如果为NULL,则使用父进程的当前工作目录。
  9. lpStartupInfo:指向STARTUPINFO结构体的指针,其中包含有关要创建的进程的信息,例如控制台窗口大小和位置。
  10. lpProcessInformation:指向PROCESS_INFORMATION结构体的指针,其中包含有关新进程的信息,例如进程句柄和进程ID。

控制台窗口STARTUPINFO:

typedef struct _STARTUPINFOW { 
    DWORD   cb;                 // 结构体的大小,以字节为单位 
    LPWSTR  lpReserved;         // 保留字段,必须设置为NULL 
    LPWSTR  lpDesktop;          // 指定要在其中启动新进程的窗口站(Window Station)和桌面(Desktop) 
    LPWSTR  lpTitle;            // 指定新进程的标题栏文本 
    DWORD   dwX;                // 指定新进程的初始x坐标 
    DWORD   dwY;                // 指定新进程的初始y坐标 
    DWORD   dwXSize;            // 指定新进程的初始宽度(以像素为单位) 
    DWORD   dwYSize;            // 指定新进程的初始高度(以像素为单位) 
    DWORD   dwXCountChars;      // 指定新进程的初始控制台屏幕缓冲区的宽度(以字符为单位) 
    DWORD   dwYCountChars;      // 指定新进程的初始控制台屏幕缓冲区的高度(以字符为单位) 
    DWORD   dwFillAttribute;    // 指定新进程的初始控制台屏幕缓冲区的填充字符和颜色 
    DWORD   dwFlags;            // 指定STARTUPINFO结构体的标志 
    WORD    wShowWindow;        // 指定新进程的初始显示状态 
    WORD    cbReserved2;        // 保留字段,必须设置为0 
    LPBYTE  lpReserved2;        // 保留字段,必须设置为NULL 
    HANDLE  hStdInput;          // 指定新进程的标准输入句柄 
    HANDLE  hStdOutput;         // 指定新进程的标准输出句柄 
    HANDLE  hStdError;          // 指定新进程的标准错误句柄 
} STARTUPINFOW, *LPSTARTUPINFOW;

注释:

  1. 结构体的大小,以字节为单位,必须设置为sizeof(STARTUPINFOW)。
  2. lpReserved和lpReserved2成员必须设置为NULL。
  3. lpDesktop成员指定要在其中启动新进程的窗口站(Window Station)和桌面(Desktop)。如果此成员为NULL,则新进程将在与当前进程相同的窗口站和桌面上运行。
  4. dwX、dwY、dwXSize和dwYSize成员用于指定新进程的窗口位置和大小。
  5. dwXCountChars和dwYCountChars成员用于指定新进程的控制台屏幕缓冲区的宽度和高度。
  6. dwFillAttribute成员用于指定新进程的控制台屏幕缓冲区的填充字符和颜色。
  7. dwFlags成员用于指定STARTUPINFO结构体的标志,例如STARTF_USEPOSITION、STARTF_USESIZE等。
  8. wShowWindow成员用于指定新进程的初始显示状态,例如SW_HIDE、SW_SHOWNORMAL等。
  9. hStdInput、hStdOutput和hStdError成员用于指定新进程的标准输入、输出和错误句柄。如果这些成员为NULL,则新进程将继承父进程的句柄。

进程信息结构体:PROCESS_INFORMATION:

typedef struct _PROCESS_INFORMATION {
    HANDLE hProcess;	//表示进程句柄,用于标识一个进程对象。
    HANDLE hThread;		//表示线程句柄,用于标识一个线程对象。
    DWORD dwProcessId;	//表示进程ID,是一个唯一标识一个进程的数字。
    DWORD dwThreadId;	//表示线程ID,是一个唯一标识一个线程的数字。
} PROCESS_INFORMATION, *PPROCESS_INFORMATION, *LPPROCESS_INFORMATION;

创建进程代码示例:打开火狐浏览器然后打开哔哩哔哩

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

using namespace std;

int main() 
{
	TCHAR commandLine[] = TEXT("E:\\Mozilla Firefox\\firefox.exe https://www.bilibili.com/");
	_STARTUPINFOW startinfo = { sizeof(_STARTUPINFOW) };
	_PROCESS_INFORMATION processInfo;

	//创建进程
	bool ret = CreateProcess(
		NULL,				//不指定可执行文件的文件名
		commandLine,		//命令行参数
		NULL,				//默认进程安全性
		NULL,				//默认线程安全性
		FALSE,				//指定当前进程的句柄是否被子进程继承
		0,					//指定附加的、用来控制优先类和进程的创建的标志
		NULL,				//使用当前进程的环境变量
		NULL,				//使用当前进程的驱动和目录
		&startinfo,			//指向一个用于决定新进程的主窗体如何显示的STARTUPINFO结构体的指针
		&processInfo		//进程信息结构体
	);
	if (!ret)
	{
		cout << "创建失败" << endl;
		return 0;
	}
	else
	{
		WaitForSingleObject(processInfo.hProcess, INFINITE);
		cout << "新创建的进程ID:" << processInfo.dwProcessId << endl;
		cout << "新创建的线程ID:" << processInfo.dwThreadId << endl;
		CloseHandle(processInfo.hProcess);
		CloseHandle(processInfo.hThread);
	}

	return 0;
}

进程间通信

  • 无名管道(pipe):管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲戚关系的进程间使用。进程的亲戚关系通常是值父子进程关系。

  • 命名管道(named pipe):有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。

  • 消息队列(message queue):消息队列是由消息的链表,存放在内核中并由消息队列标识符。消息队列客服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。

  • 互斥量

  • 共享内存(shared memory):共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的IPC方式,它是针对其它进程间通信方式运行效率而专门设计的。它往往与其它通信机制,如信号量,配合使用,来实现进程间的同步和通信。

方法一:使用 WM_COPYDATA 消息在不同进程的窗口之间传递数据

// 声明一个接收数据的窗口句柄变量
HWND hReceiveDataWindow = FindWindow(NULL, ...);
// 声明COPYDATASTRUCT结构体变量
COPYDATASTRUCT data;
// 设置传输数据的长度为字符串pStr的长度
data.cbData = strlen(pStr);
// 向指定窗口发送WM_COPYDATA消息,其中WPARAM参数为当前拥有焦点的窗口的句柄,
// LPARAM参数为指向COPYDATASTRUCT结构体变量的指针
SendMessage(hReceiveDataWindow, WM_COPYDATA, (WPARAM)GetFocus(), (LPARAM)&data);

该段代码的工作流程如下:

  1. 通过调用 FindWindow 函数查找具有指定类名或窗口名称的顶级窗口。
  2. 初始化 COPYDATASTRUCT 结构体变量 data,其中包括要发送的数据和数据大小等信息。
  3. 使用 SendMessage 函数向接收数据窗口发送 WM_COPYDATA 消息,并将数据结构体的指针作为消息参数传递。

以下是每个函数的作用和参数说明:

  1. FindWindow 函数:查找具有指定类名或窗口名称的顶级窗口。该函数的参数说明如下:

    HWND FindWindow(
      LPCWSTR lpClassName, // 窗口类名,如果为 NULL,则表示匹配所有窗口类
      LPCWSTR lpWindowName // 窗口名称,如果为 NULL,则表示匹配所有窗口名
    );
    
  2. SendMessage 函数:将指定的消息发送到一个或多个窗口,并等待响应。该函数的参数说明如下:

    LRESULT SendMessage(
      HWND   hWnd,       // 接收消息的窗口句柄
      UINT   Msg,        // 消息类型,这里为 WM_COPYDATA
      WPARAM wParam,     // 消息附加参数,这里为 GetFocus() 的返回值
      LPARAM lParam      // 消息附加参数,这里为 COPYDATASTRUCT 结构体的地址
    );
    
  3. GetFocus 函数:检索当前具有键盘焦点的窗口句柄。该函数没有参数,直接返回当前具有焦点的窗口句柄。

  4. COPYDATASTRUCT 结构体:表示要发送的数据和数据大小等信息。该结构体的成员变量说明如下:

    typedef struct tagCOPYDATASTRUCT {
      ULONG_PTR dwData;  // 指定一些与数据相关的内容,可以为任何值
      DWORD cbData;      // 数据大小,即 pStr 的长度
      PVOID lpData;      // 指向实际数据的指针,即 pStr 的地址
    } COPYDATASTRUCT, *PCOPYDATASTRUCT;
    

方法二:剪切板

OpenClipboard(NULL)  //将OpenClipboard 函数的参数指定为NULL,表明为当前进程打开剪切板

HGLOBAL hGlobalClip;

hGlobalClip = GlobalAlloc (GHND, nCount); //给全局内存对象分配全局内存

pDataBuf = (char *GlobalLock (hGlobalClip));  //通过给全局内存对象加锁获得对全局内存块的引用

GlobalUnlock (hGlobalClip);   //使用全局内存块后需要对全局内存卡解锁

EmptyClipboard(); //清空剪贴板

SetClipboardData (CF TEXT, hGlobalClip);  //设置剪贴板数据,这里直接将数据放到了剪贴板中,而没有使用延迟提交技术

CloseClipboard();  //关闭剪贴板

IsClipboardFormatAvilable (CF_TEXT)  //判断剪贴板中的数据格式是否为CF_TEXT

hGlobalClip = GetClipboardData(CF_TEXT);  //从剪贴板中获取格式为CF_TEXT的数据

pDataBuf = (char *) GlobalLock(hGlobalClip);

该段代码的工作流程如下:

  1. 通过调用 OpenClipboard 函数打开剪贴板,该函数的参数指定为 NULL 表示为当前进程打开剪贴板。
  2. 使用 GlobalAlloc 函数给全局内存对象分配全局内存块,其中 GHND 参数表示分配的内存块内容初始化为零。
  3. 通过使用 GlobalLock 函数给全局内存对象加锁,获取对全局内存块的引用,并将其保存在 pDataBuf 变量中。
  4. 在使用完全局内存块后,使用 GlobalUnlock 函数解锁全局内存块。
  5. 使用 EmptyClipboard 函数清空剪贴板中的内容。
  6. 使用 SetClipboardData 函数向剪贴板设置数据,CF_TEXT 参数指定了数据的格式类型,hGlobalClip 参数指定了数据所在的内存块句柄。
  7. 关闭剪贴板,调用 CloseClipboard 函数。
  8. 使用 IsClipboardFormatAvailable 函数判断剪贴板中是否有指定的数据格式 CF_TEXT。
  9. 通过 GetClipboardData 函数从剪贴板中获取指定格式的数据,CF_TEXT 参数表示获取的数据格式类型,返回值是一个全局内存块句柄。
  10. 再次使用 GlobalLock 函数给获取的全局内存对象加锁,获取对全局内存块的引用,并将其保存在 pDataBuf 变量中。

以下是每个函数的作用和参数说明:

  1. OpenClipboard 函数:打开剪贴板。该函数的参数说明如下:

    BOOL OpenClipboard(
      HWND hWndNewOwner // 新的所有者窗口句柄,这里为 NULL 表示为当前进程打开剪贴板
    );
    
  2. GlobalAlloc 函数:分配全局内存块。该函数的参数说明如下:

    HGLOBAL GlobalAlloc(
      UINT uFlags,   // 分配内存的方式,这里使用 GHND,表示分配后的内存内容初始化为零
      SIZE_T dwBytes // 分配内存的大小,这里为 nCount
    );
    
  3. GlobalLock 函数:给全局内存对象加锁,获取对全局内存块的引用。该函数的参数说明如下:

    LPVOID GlobalLock(
      HGLOBAL hMem // 全局内存块句柄
    );
    
  4. GlobalUnlock 函数:解锁已经加锁的全局内存块。该函数的参数说明如下:

    BOOL GlobalUnlock(
      HGLOBAL hMem // 全局内存块句柄
    );
    
  5. EmptyClipboard 函数:清空剪贴板中的所有数据。该函数没有参数。

  6. SetClipboardData 函数:向剪贴板设置数据。该函数的参数说明如下:

    HANDLE SetClipboardData(
      UINT   uFormat,   // 设置的数据格式类型,这里使用 CF_TEXT
      HANDLE hMem       // 数据所在的内存块句柄
    );
    
  7. CloseClipboard 函数:关闭剪贴板。该函数没有参数。

  8. IsClipboardFormatAvailable 函数:判断剪贴板中是否有指定格式的数据。该函数的参数说明如下:

    BOOL IsClipboardFormatAvailable(
      UINT format // 要检查的数据格式类型,这里使用 CF_TEXT
    );
    
  9. GetClipboardData 函数:从剪贴板获取指定格式的数据。该函数的参数说明如下:

    HANDLE GetClipboardData(
      UINT uFormat // 获取的数据格式类型,这里使用 CF_TEXT
    );
    

方法三:共享文件映射

创建缓冲区

HANDLE CreateFileMapping(
  HANDLE hFile,                    		// 文件句柄
  LPSECURITY_ATTRIBUTES lpAttributes, 	// 安全属性
  DWORD flProtect,                 		// 内存访问权限保护标志位
  DWORD dwMaximumSizeHigh,         		// 映射文件的最大尺寸(高位DWORD)
  DWORD dwMaximumSizeLow,          		// 映射文件的最大尺寸(低位DWORD)
  LPCTSTR lpName                   		// 共享内存对象名
);
  • hFile: 打开的文件句柄,可以是磁盘上的文件、虚拟磁盘设备、物理内存等。如果为INVALID_HANDLE_VALUE则创建一个没有关联文件的映射对象。
  • lpAttributes: 指向安全描述符的指针,控制该内存区域的访问权限和继承性等。如果为NULL,则表示默认安全描述符。
  • flProtect: 内存访问权限保护标志位,包括以下取值之一或多个:
    • PAGE_READONLY:只读方式打开映射文件;
    • PAGE_READWRITE:以可读写方式打开映射文件;
    • PAGE_WRITECOPY:以写时复制方式打开映射文件;
    • PAGE_EXECUTE:执行权限;
    • PAGE_EXECUTE_READ:可读和执行权限;
    • PAGE_EXECUTE_READWRITE:可读写和执行权限;
    • PAGE_EXECUTE_WRITECOPY:写时复制方式、可读和执行权限。
  • dwMaximumSizeHighdwMaximumSizeLow: 创建映射文件的最大大小,以字节为单位。最大值为0,表示创建一个文件的映射对象,映射对象大小与文件大小相同。
  • lpName: 共享内存对象名,与其它进程共享内存需要指定,如果只在当前进程使用则设为NULL。

注释:

CreateFileMapping()函数可以用于将文件或其他对象映射到调用进程的地址空间中,也可以用于创建一个共享内存区域,供多个进程共享访问。

映射文件对象被创建时,该文件必须已经存在,否则会失败。映射文件的大小由dwMaximumSizeHighdwMaximumSizeLow参数指定。如果这两个参数都是0,则会创建一个映射文件对象,该对象的大小与文件大小相同。

在创建映射文件时,flProtect参数指定了保护标志位,用于描述该内存区域的访问权限。

当多个进程打开同一个lpName名称的映射文件时,它们可以通过共享内存来实现相互通信,从而达到数据共享、同步等目的。

创建映射

MapViewOfFile函数可以将一个已经创建的文件映射或共享内存对象映射到调用进程的地址空间中,从而允许进程直接读写这个映射区域。

具体函数原型如下:

LPVOID MapViewOfFile(
  HANDLE hFileMappingObject, // 映射对象句柄
  DWORD dwDesiredAccess,     // 访问权限
  DWORD dwFileOffsetHigh,    // 映射起始偏移(高位)
  DWORD dwFileOffsetLow,     // 映射起始偏移(低位)
  SIZE_T dwNumberOfBytesToMap // 映射大小
);

参数说明:

  • hFileMappingObject: 已经创建的映射对象的句柄。

  • dwDesiredAccess: 访问权限。包括以下取值之一或多个:

    • FILE_MAP_READ:读取映射区域;
    • FILE_MAP_WRITE:写入映射区域;
    • FILE_MAP_ALL_ACCESS:读、写、复制映射区域。
  • dwFileOffsetHighdwFileOffsetLow: 映射起始偏移量,指定了映射区域在文件中的位置。如果需要从文件的开头处开始映射,则设置为0。

  • dwNumberOfBytesToMap: 映射区域的大小,以字节为单位。如果设置为0,则表示映射整个文件或共享内存对象

关闭缓冲区

UnmapViewOfFile函数用于取消先前使用MapViewOfFile函数进行的映射操作,从而释放所映射区域占用的资源。

BOOL UnmapViewOfFile(LPCVOID lpBaseAddress); // 映射区域起始地址
//关闭内核对象的句柄
CloseHandle(HANDLE hObject); //内核对象句柄

代码示例:

发送方:

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

using namespace std;

int main()
{
	HANDLE hMapFile = CreateFileMapping(
		NULL,   //物理
		NULL,	//默认映射文件的安全级别
		PAGE_READWRITE,  //可读可写
		0,		//高位
		128,    //低位
		TEXT("fileMap")   //共享内存名
	);

	char* buf = (char*)MapViewOfFile(hMapFile, FILE_MAP_ALL_ACCESS, 0, 0,128);

	cin.getline(buf,16);

	while (1);

	UnmapViewOfFile(buf);
	CloseHandle(hMapFile);

	return 0;
}

接收方:

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

using namespace std;

int main()
{
	HANDLE hMapFile = CreateFileMapping(
		NULL,   //物理
		NULL,	//默认映射文件的安全级别
		PAGE_READWRITE,  //可读可写
		0,		//高位
		128,    //低位
		TEXT("fileMap")   //共享内存名
	);

	char* buf = (char*)MapViewOfFile(hMapFile, FILE_MAP_ALL_ACCESS, 0, 0,128);

	for(int i = 0;i < 16;i++)
		cout<< *(buf + i); 

	while (1);

	UnmapViewOfFile(buf);
	CloseHandle(hMapFile);

	return 0;
}

内存管理

  • 进程的虚拟地址空间

    ​ 对于32位进程来说,这个地址空间时4GB,因为32位指针可以拥有从0x 00000 00 0至0xFFFFFFFF之间的任何一个值。这使得一个指针能够拥有4 294 967 296个值中的一个值,它覆盖了一个进程的4GB虚拟空间的范围。对于64位进程来说,这个地址空间时16EB(1TB = 1024GB,1 PB = 1024TB,1 EB = 1024PB),因为64位指针可以拥有从0x0 000 00 000 00 0 00 00 至OxFFFFFFFFFFFFFFFF之间的任何值。这使得一个指针可以拥有18446744073709 551616个值中的一个值,它覆盖了一个进程的16 EB虚拟空间的范围。这是相当大的一个范围。

    ​ 32位的CPU的寻址空间时4G,所以虚拟内存的最大值为4G,而windows操作系统把这4G分成2部分,即3G 的用户空间和1G的系统空间,系统空间时各个进程所共享的,它存放的是操作系统及一些内核对象等,而用户空间时分配给各个进程使用的,用户空间包括有:程序代码和数据,堆,共享库,栈。

  • 分页

    ​ 分页的基本方法是,将地址空间分为许多的页。每页的大小由CPU决定,然后操作系统选择页的大小。目前Inter系列的CPU支持4KB或者4MB的页大小,而PC上目前都选择使用4KB。

  • 内核空间:

    ​ 内核空间表示运行在处理器最高级别的超级用户模(supervisor mode)下的代码或数据,内核空间占用从0xC0000000到0xFFFFFFFF的1GB线性地址空间,内核线性地址空间由所有进程共享,但只有运行在内核态的进程才能访问,用户进程可以通过系统调用切换到内核态访问内核空间,进程运行在内核态时产生的地址都属于内核空间

  • 用户空间

    ​ 用户空间占用从0xBFFFFFFF共3GB的线性地址空间,每个进程都有一个独立的3GB用户空间,所以用户空间由每个进程独有,但是内核线程没有用户空间,因为它不产生用户空间地址。另外子进程共享(继承)父进程的用户空间只是使用与父进程相同的用户线性地址到物理内存地址的映射关系,而不是共享父进程用户空间。运行在用户态和内核态的进程都可以访问用户空间。

    在用户空间内内存被分为:0x08048000开始

用户空间分段:

  • text段-代码段

​ text段存放程序代码,运行前就已经确定(编译时确定),通常为只读

  • .rodata-只读数据段

​ 存放一些只可以读的常量数据,比如:被const修饰的全局变量,被define宏定义的常量,和只可读的字符串常量。

  • .data

​ 存放在编译阶段(而非运行时)就能确定的数据,可读可写。也就是通常所说的静态存储区,赋了初值的全局变量和赋初值的静态变量存放在这个区域,通常也存放在这个区域,static声明的变量,不管它是全局变量也好,还是在函数之中的也好,只要是没有赋初值都存放在.bass段,如果赋了初值,则把它放在.data段

  • .bss

​ 定义而没有赋初值的全局变量和静态变量,放在这个区域

  • heap

​ 堆是用于存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩张或缩减。当进程调用malloc等函数分配内存时,新分配的内存就动态添加到堆上(堆被扩张);当利用free等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减)。由低向高。

  • 共享库区域

​ 这里被内核用来把文件内容直接映射到内存。所有的应用程序都可以使用linux提供的mmap()系统调用或者在windows中使用CreateFileMapping()/MapViewOfFile来进行这样的映射。memory mapping是进行文件I/O的高效方法,所以动态库的加载使用这个方式来实现。当然,也可以进行一些不关联到文件的程序数据的匿名memory mapping。在linux中,如果你通过malloc()来申请一块大的内存,C库就会在memory mapping segment中创建一个匿名memory maping而不是使用堆空间。这里的"大"意味着大于MMAP_THRESHOLD字节,默认是128kb,可以通过mallopt()来进行调整。

  • stack

​ stack段存储参数变量和局部变量,由系统进行申请和释放,属于静态内存分配。由高向低

猜你喜欢

转载自blog.csdn.net/weixin_50202509/article/details/129967533