Windows 进程在指定CPU上运行

12 篇文章 0 订阅

NUMA, CPU分组简介

传统SMP(Symmetric Multiprocessor)-对称多处理器架构, 多个CPU都是通过总线访问内存、I/0等资源。CPU多了以后,总线成为瓶颈。 于是出现NUMA架构(Non-Uniform Memory Access), 每个CPU节点配套高速访问的本地内存,I/O槽口等,亦可访问其他节点的内存,但要经过互联模块。 特点是本地内存的速度远高于远地内存。操作系统提供NUMA相关的API,让开发者更好地提升软件性能。参考资料 SMP、NUMA、MPP体系结构介绍 - victoryubo - 博客园

没有CPU分组之前的affinity API

// 设置进程相关性
BOOL SetProcessAffinityMask(
  [in] HANDLE    hProcess,
  [in] DWORD_PTR dwProcessAffinityMask
);

// 获取进程相关性
BOOL GetProcessAffinityMask(
  [in]  HANDLE     hProcess,
  [out] PDWORD_PTR lpProcessAffinityMask,
  [out] PDWORD_PTR lpSystemAffinityMask
);

// 设置线程相关性,成功时返回原来的mask,失败返回0, mask不可能为0
DWORD_PTR SetThreadAffinityMask(
  [in] HANDLE    hThread,
  [in] DWORD_PTR dwThreadAffinityMask
);


/*
   获取系统信息, 知道系统有哪些可用的逻辑处理器。 
   这是win32时代的API了,64位系统更准确的信息通过GetNativeSystemInfo去获取
   To retrieve accurate information for an application running on WOW64, call the GetNativeSystemInfo function.
*/
void GetSystemInfo(
  [out] LPSYSTEM_INFO lpSystemInfo
);

typedef struct _SYSTEM_INFO {
  union {
    DWORD dwOemId;
    struct {
      WORD wProcessorArchitecture;
      WORD wReserved;
    } DUMMYSTRUCTNAME;
  } DUMMYUNIONNAME;
  DWORD     dwPageSize;
  LPVOID    lpMinimumApplicationAddress;
  LPVOID    lpMaximumApplicationAddress;
  // 系统当前可用的CPU, 因为系统设置里可以关闭某些CPU, 所以会出现与dwNumberOfProcessors个数不匹配的情况。
  DWORD_PTR dwActiveProcessorMask; 
  DWORD     dwNumberOfProcessors; // 多少个逻辑核
  DWORD     dwProcessorType;
  DWORD     dwAllocationGranularity;
  WORD      wProcessorLevel;
  WORD      wProcessorRevision;
} SYSTEM_INFO, *LPSYSTEM_INFO;

/*
如果需要识别dwNumberOfProcessors中的是物理核还是超线程的逻辑核, 通过GetLogicalProcessorInformationEx去获取。
Note  For information about the physical processors shared by logical processors, call GetLogicalProcessorInformationEx with the RelationshipType parameter set to RelationProcessorPackage (3).
*/

基本用法

SYSTEM_INFO sys_info;
memset(&sys_info, 0x0, sizeof(sys_info));
GetSystemInfo(&sys_info);
DWORD cpu_core_num = sys_info.dwNumberOfProcessors;
DWORD_PTR proc_mask = 0x60;
BOOL success = SetProcessAffinityMask(GetCurrentProcess(), proc_mask);
DWORD_PTR proc_mask;
DWORD_PTR system_mask;
BOOL success = GetProcessAffinityMask(GetCurrentProcess(), &proc_mask, &system_mask);
DWORD_PTR thread_mask = 0x1;
DWORD_PTR last_mask = SetThreadAffinityMask(GetCurrentThread(), thread_mask);

注意事项

  • 如果使用c++11的std::thread线程,参数中线程句柄可以通过std::thread的native_handle()函数来获取。
  • 没有找到直接获取当前线程的affinity的API。 可以通过SetThreadAffinityMask返回上一次的affinity, 再调用SetThreadAffinityMask还原。
  • 设置失败,维持原来的affinity设置。
  • affinity超出系统可用CPU范围时, 调用失败。
  • affinity为0时, 调用失败。
  • 当设置进程affinity后, 再设置超出已设进程affinity范围的线程的affinity时,SetThreadAffinityMask失败返回0。
  • 当成功设置线程affinity后,再设置合法的进程affinity时,成功后会覆盖所有线程原有的affinity, 线程affinity变成与当前进程affinity一致。

支持CPU分组的affinity相关API

// 获取当前系统的最高NUMA节点索引, NUMA节点数就是返回的HighestNodeNumber + 1
// 只有一个CPU分组的系统也能调用, 返回TRUE, HighestNodeNumber被设置为0。
BOOL GetNumaHighestNodeNumber(
  [out] PULONG HighestNodeNumber
);

// 获取NUMA节点的分组以及有效的mask范围。 但是这个API在64位系统,只能获取到调用线程所在的分组的mask,
// GetNumaNodeProcessorMaskEx没有这个限制, 建议用GetNumaNodeProcessorMaskEx代替。
BOOL GetNumaNodeProcessorMask(
  [in]  UCHAR      Node,
  [out] PULONGLONG ProcessorMask
);

// 获取任意合法NUMA节点的分组以及有效的mask范围。
BOOL GetNumaNodeProcessorMaskEx(
  [in]  USHORT          Node,
  [out] PGROUP_AFFINITY ProcessorMask
); 

typedef struct _GROUP_AFFINITY {
    KAFFINITY Mask;
    WORD   Group;
    WORD   Reserved[3];
} GROUP_AFFINITY, *PGROUP_AFFINITY;

// 获取当前进程, 一共在多少个分组里运行。 不同的线程,可以指定在不同的CPU分组上运行。
BOOL GetProcessGroupAffinity(
  [in]      HANDLE  hProcess,
  [in, out] PUSHORT GroupCount,
  [out]     PUSHORT GroupArray
);

// 设置线程的CPU亲缘性, 绑定在哪个CPU分组, 以及哪些CPU上运行。 可返回上一次的CPU分组和相关性信息。
BOOL SetThreadGroupAffinity(
  [in]            HANDLE               hThread,
  [in]            const GROUP_AFFINITY *GroupAffinity,
  [out, optional] PGROUP_AFFINITY      PreviousGroupAffinity
);

基本用法

ULONG highest_node_num = 0;
BOOL num_success = GetNumaHighestNodeNumber(&highest_node_num);
if (num_success) {
	ULONG numa_node_count = highest_node_num + 1;
	printf("NUMA node count = %u\n", numa_node_count);

	for (int node_index = 0; node_index < numa_node_count; node_index++) {
		GROUP_AFFINITY group_affinity;
		BOOL mask_success = GetNumaNodeProcessorMaskEx(node_index, &group_affinity);
		if (mask_success) {
			printf("The %d NUMA node group=%d mask=0x%llx\n", node_index + 1, (int)group_affinity.Group, group_affinity.Mask);
		}
	}
}
USHORT group_ary[4];
USHORT group_ary_size = 4;
BOOL success = GetProcessGroupAffinity(GetCurrentProcess(), &group_ary_size, group_ary);
GROUP_AFFINITY group_affinity;
memset(&group_affinity, 0x0, sizeof(group_affinity));
group_affinity.Group = 1;
group_affinity.Mask = 0x4;

GROUP_AFFINITY last_group_affinity;
BOOL success1 = SetThreadGroupAffinity(GetCurrentThread(), &group_affinity, &last_group_affinity);

BOOL success2 = SetThreadGroupAffinity(GetCurrentThread(), &group_affinity, NULL);

注意事项

  • 如果使用c++11的std::thread线程,参数中线程句柄可以通过std::thread的native_handle()函数来获取。
  • 每新起一个进程时,操作系统以轮循(round-robin)方式把进程分配到一个分组上运行。假如一共有2个分组,进程1分配到组0, 进程2分配到组1,进程3又分配到组0,..., 进程1,2,3可以是相同,也可以是不同的应用程序。
  • 没有设置进程分组的API,只有设置线程分组的API。
  • 新创建的线程都分配到与创建它的线程相同的组。假如一个线程的CPU分组、affinity修改后, 那么由这个线程创建出来的子线程分组与父线程一样。 但affinity为全集不继承父线程的affinity设置。
  • 新创建的进程, 与创建它的线程的CPU分组和affinity都没有关系,仍是由系统来分配CPU分组。
  • SetThreadAffinityMask(不带group)跟SetThreadGroupAffinity(带group)的作用一样,唯一区别就是不能指定CPU分组,只在当前分组上操作。
  • 当修改线程affinity并且未修改过CPU分组时,再设置进程affinity后会覆盖所有线程原有的affinity, 线程affinity变成与当前进程affinity一致。
  • 当修改线程group affinity改变过CPU分组后,再也无法再设置进程的affinity, SetProcessAffinityMask会失败,错误码ERROR_INVALID_PARAMETER, 即使后面把线程的CPU分组改回来,也无法回到能够正常设置进程affinity的状态, 官方说明如下:
On a system with more than 64 processors, 
if the calling process contains threads in more than one processor group, 
the last error code is ERROR_INVALID_PARAMETER.

并且任务管理器也无法查看该进程的相关性信息,提示拒绝访问。 

image.png

  • 不同的线程,可以设置到不同的CPU分组上运行。并且可以配合NUMA相关的内存分配函数( VirtualAllocExNuma etc..),充分发挥多核以及快速访问本地内存的优势。
  • 管理线程的组和affinity信息,方法一:经过一个统一接口去设置,设置成功后,记录下当时的分组和affinity信息;方法二:set来返回上一次的设置, 然后再还原。 从优雅的角度建议使用方法一。除非使用了别人的库,无法保证库的内部对线程的亲缘性做操作。

扩展功能--在多分组的Windows系统,如何修改进程的分组。

1. 所有模块都能修改。

所有线程通过SetThreadAffinity来控制亲缘性。在main函数入口处, 修改main函数主线程的CPU分组, 由于后面所有线程都是从main函数开始创建的,根据子线程与父线程CPU分组必定一致这个特性, 可以确保所有线程CPU分组就与main函数线程一致。有两个缺点,

  1. 设置的分组与进程开始分配的不一样,任务管理器将无法查看分组信息。
  2. 需要找到所有线程创建的地方,在创建时设置指定的affinity, 并且修改affinity时,还要找到所有线程,设置和修改。实现起来比较麻烦。

2. 存在一些模块,内部线程无法控制和设置亲缘性。

把模块包装成一个进程, 通过进程间通信来交互。然后通过修改进程的CPU分组和处理器亲缘来实现。无需关注模块内部有多少线程,确保整个子进程在指定的分组和和核心上调度。

  1. SetProcessAffinityMask, 没有CPU分组的系统,可以这么做。有CPU分组的不行, 因为没有设置进程CPU分组的API, 只有线程才有。
  2. 通过作业对象(Job Object来实现), 作业对象是Windows系统提供的对加入作业的进程做特定限制的内核对象,可以对作业中的进程组统一做时间片分配,网络,CPU亲缘性设置等,作业相关的API支持设置分组以及核心的亲缘性。并且可以设置JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE标志,当作业对象释放,回收时(包括主进程崩溃),系统强制关闭所有相关进程。参考 作业对象 - Win32 apps | Microsoft Docs

实现步骤:

  1. 创建作业对象
  2. 通过传递进程句柄,把进程加入作业对象。
  3. 通过设置JobObjectGroupInformationEx信息类的配置,实现CPU亲缘性的限制。

Job Object相关API

// 创建作业对象
HANDLE CreateJobObjectA(
  [in, optional] LPSECURITY_ATTRIBUTES lpJobAttributes,
  [in, optional] LPCSTR                lpName
);

// 把进程加入作业。
BOOL AssignProcessToJobObject(
  [in] HANDLE hJob,
  [in] HANDLE hProcess
);

// 设置作业的限制内容
BOOL SetInformationJobObject(
  [in] HANDLE             hJob,
  [in] JOBOBJECTINFOCLASS JobObjectInformationClass,
  [in] LPVOID             lpJobObjectInformation,
  [in] DWORD              cbJobObjectInformationLength
);

// 查询作业的限制内容。
BOOL QueryInformationJobObject(
  [in, optional]  HANDLE             hJob,
  [in]            JOBOBJECTINFOCLASS JobObjectInformationClass,
  [out]           LPVOID             lpJobObjectInformation,
  [in]            DWORD              cbJobObjectInformationLength,
  [out, optional] LPDWORD            lpReturnLength
);

实现方法

//=========================     创建进程     ============================//
char Proc_Path[] = "notepad.exe"; // 进程路径

STARTUPINFOA startup_info;
memset(&startup_info, 0x0, sizeof(startup_info));
startup_info.cb = sizeof(startup_info);
startup_info.wShowWindow = SW_SHOW;

PROCESS_INFORMATION proc_info;
memset(&proc_info, 0x0, sizeof(proc_info));
BOOL create_ok = CreateProcessA(
	NULL, 
	Proc_Path, 
	NULL, 
	NULL, 
	TRUE, 
	CREATE_NEW_CONSOLE, 
	NULL, 
	NULL, 
	&startup_info, 
	&proc_info
);
//------------------------------------------------------------------------//


HANDLE job_handle = CreateJobObject(NULL, NULL); // 创建Job对象
BOOL add_job_ok = AssignProcessToJobObject(job_handle, proc_info.hProcess); // 把进程加入Job

//===========    设置Job对象回收, 所有相关进程自动退出   ============//
JOBOBJECT_EXTENDED_LIMIT_INFORMATION limit_info;
memset(&limit_info, 0x0, sizeof(limit_info));
limit_info.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE; // job句柄回收时,所有加入job的进程都会强制结束。
BOOL set_auto_kill_ok = SetInformationJobObject(
	job_handle, 
	JobObjectExtendedLimitInformation, 
	&limit_info, 
	sizeof(limit_info)
);
//-----------------------------------------------------------------//


//=================  限制进程在指定的CPU分组中运行  =================//
{
	USHORT cpu_group = 0;
	//USHORT cpu_group = 1;
	BOOL success = SetInformationJobObject(job_handle, JobObjectGroupInformation, &cpu_group, sizeof(cpu_group));
}
//-------------------------------------------------------------------//


//=================  限制进程在指定的CPU分组以及指定的核心中运行  =================//
{
	GROUP_AFFINITY group_affinity;
	memset(&group_affinity, 0x0, sizeof(group_affinity));
	group_affinity.Group = 1;
	group_affinity.Mask = 0xff;
	BOOL success = SetInformationJobObject(job_handle, JobObjectGroupInformationEx, &group_affinity, sizeof(group_affinity));
}
//---------------------------------------------------------------------------------//


/*
	释放job对象, 调用后,所有相关进程都会退出。
	假如程序崩溃没调用释放, 由系统回收,也能达到结束所有相关进程的效果。 
	在多进程系统中,这个特性必用,应对出现僵尸子进程的现象。
*/
CloseHandle(job_handle); 

注意事项

  • 进程加入新创建的JobObject, 并且JobObject并未设置过group或者affinity限制, 会保留原来进程的group和affinity设置。
  • JobObject调用SetInformationJobObject设置进程的group或affinity后,会覆盖原来的SetProcessAffinityMask设置,并且后面再调用SetProcessAffinityMask会成功,但是实际上不会生效,还是以JobObject的限制为准。
  • JobObject调用SetInformationJobObject设置进程的group或affinity后,会覆盖原来线程的SetThreadGroupAffinity设置,所有线程的group和affinity与JobObject的设置一致。再次调用SetThreadGroupAffinity, 如果group和affinity范围在JobObject限制内,会成功并生效;如果group和affinity超出JobObject范围,设置会失败,Error Code为ERROR_GEN_FAILURE

建议

  1. 设置group和affinity的方法只选择其中一种,避免混用导致迷惑。
  2. 如果系统只需对部分线程做CPU亲缘设置,使用SetThreadGroupAffinity即可。
  3. 如果系统本来就是多进程架构,或者引用了无法设置线程属性的第三方库,又有设置分组的需求,统一使用JobObject来限定CPU分组以及处理器亲缘属性。

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值