控制核心分配:利用CPU亲和性最大化速度和效率

举报
Lion Long 发表于 2023/09/29 12:12:01 2023/09/29
【摘要】 本文探讨了如何通过控制核心分配来利用CPU亲和性,从而最大化处理速度和效率。我们详细介绍了CPU亲和性的概念和原理,并提供了实际的设置方法和优化技巧。通过正确配置CPU亲和性,可以实现更好的多线程应用程序性能和并行计算效果。我们还讨论了如何解锁处理器核心的潜力,以及如何通过优化核心分配来加速应用程序的执行。本文旨在帮助读者深入理解CPU亲和性的重要性,以及如何通过控制核心分配来实现最佳性能和效率。

一、相关函数

1.1、sysconf():读取系统配置文件。

函数原型:

#include <unistd.h>

long sysconf(int name);

描述:
POSIX允许应用程序在编译或运行时测试是否支持某些选项,或者测试某些可配置常量或限制的值。

在编译时,这是通过包含<unistd.h>和、或<limits.h>并测试某些宏的值来完成的。

在运行时,可以使用当前函数sysconf()请求数值。可以通过调用fpathconf和pathconf来请求可能取决于文件所在的文件系统的数值。可以使用confstr请求字符串值。

从这些函数获得的值是系统配置常数。它们在过程的生命周期内不会改变。

对于选项,通常有一个常量_POSIX_FOO,可以在<unistd.h>中定义。

  • 如果未定义,则应在运行时询问。
  • 如果定义为-1,则不支持该选项。
  • 如果将其定义为0,则存在相关函数和标头,但必须在运行时询问可用的支持程度。
  • 如果将其定义为-1或0以外的值,则支持该选项。
  • 通常,该值(如200112L)表示描述该选项的POSIX修订的年份和月份。只要POSIX版本尚未发布,Glibc就使用值1表示支持。sysconf()参数将是_SC_FOO。

对于变量或限制,通常有一个常量_FOO,可能在<limit.h>中定义 或者_POSIX_FOO,可以在<unistd.h>中定义。如果未指定限制,则不会定义常数。如果定义了常量,它会给出一个保证值,并且实际上可能支持更大的值。如果应用程序希望利用可能在

在系统中,可以调用sysconf()。sysconf()参数将是_SC_FOO。

参数:name可以参看系统调用的宏定义,比如查阅CPU数量的宏为_SC_NPROCESSORS_CONF

返回值:

  • 如果名称无效,则返回-1,并将errno设置为EINVAL。
  • 否则,返回的值是系统资源的值,errno不会更改。
  • 对于选项,如果查询的选项可用,则返回正值,如果不可用,则为-1。在限制的情况下,-1表示没有明确的限制。

1.2、fork()

函数原型:

#include <unistd.h>

pid_t fork(void);

描述:
fork()通过复制调用进程创建一个新进程。新进程称为子进程。调用进程称为父进程。

子进程和父进程在单独的内存空间中运行。在fork()时,两个内存空间具有相同的内容。其中一个进程执行的内存写入、文件映射【mmap()】和取消映射【munmap()】,不会影响另一个进程。

返回值:
成功时,子进程的PID在父进程中返回,0在子进程中返回。失败时,在父进程中返回-1,不创建子进程,并适当设置errno。

错误:

错误代码 含义
EAGAIN 遇到系统对线程数量施加的限制。可能触发此错误的限制有很多:已达到RLIMIT_NPROC软资源限制(通过setrlimit()设置),该限制限制了真实用户ID的进程和线程数;已达到内核对进程和线程数的系统范围限制,即/proc/sys/kernel/threads max(请参阅proc());或者达到最大pid数/proc/sys/kernel/pid_max(见proc())。
EAGAIN 调用方在SCHED_DELADATE调度策略下运行,并且未设置reset on fork标志。
ENOMEM fork()无法分配必要的内核结构,因为内存紧张。
ENOSYS 此平台不支持fork()(例如,没有内存管理单元的硬件)。

1.3、gettid():获取线程标识。

函数原型:

#include <sys/types.h>

pid_t gettid(void);

注意:此系统调用没有glibc包装器。
描述:
返回调用方的线程ID(TID)。在单线程进程中,线程ID等于进程ID(PID,由getpid()返回)。在多线程进程中,所有线程都具有相同的PID,但每个线程都具有唯一的TID。

返回值:
总是成功的,返回调用进程的线程ID。

1.4、syscall():间接系统调用。

函数原型:

#define _GNU_SOURCE         /* See feature_test_macros(7) */
#include <unistd.h>
#include <sys/syscall.h>   /* For SYS_xxx definitions */

long syscall(long number, ...);

描述:
syscall()是一个小型库函数,用于调用系统调用,该系统调用的汇编语言接口具有指定数量和指定参数。例如,当调用C库中没有包装函数的系统调用时,使用syscall()非常有用。

syscall()在进行系统调用之前保存CPU寄存器,在系统调用返回时恢复寄存器,如果发生错误,则将系统调用返回的任何错误代码存储在errno中。

系统调用号的符号常量可以在头文件<sys/syscall.h>中找到。

返回值:
返回值由调用的系统调用定义。通常,0返回值表示成功。返回值-1表示错误,错误代码存储在errno中。

使用示例:

#define _GNU_SOURCE
#include <unistd.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <signal.h>

int
main(int argc, char *argv[])
{
    pid_t tid;

    tid = syscall(SYS_gettid);
    tid = syscall(SYS_tgkill, getpid(), tid, SIGHUP);
}

1.5、CPU_*:用于操作CPU集的宏。

函数原型:

#define _GNU_SOURCE             /* See feature_test_macros(7) */
#include <sched.h>

void CPU_ZERO(cpu_set_t *set);

void CPU_SET(int cpu, cpu_set_t *set);
void CPU_CLR(int cpu, cpu_set_t *set);
int  CPU_ISSET(int cpu, cpu_set_t *set);

int  CPU_COUNT(cpu_set_t *set);

void CPU_AND(cpu_set_t *destset,
             cpu_set_t *srcset1, cpu_set_t *srcset2);
void CPU_OR(cpu_set_t *destset,
             cpu_set_t *srcset1, cpu_set_t *srcset2);
void CPU_XOR(cpu_set_t *destset,
             cpu_set_t *srcset1, cpu_set_t *srcset2);

int  CPU_EQUAL(cpu_set_t *set1, cpu_set_t *set2);

cpu_set_t *CPU_ALLOC(int num_cpus);
void CPU_FREE(cpu_set_t *set);
size_t CPU_ALLOC_SIZE(int num_cpus);

void CPU_ZERO_S(size_t setsize, cpu_set_t *set);

void CPU_SET_S(int cpu, size_t setsize, cpu_set_t *set);
void CPU_CLR_S(int cpu, size_t setsize, cpu_set_t *set);
int  CPU_ISSET_S(int cpu, size_t setsize, cpu_set_t *set);

int  CPU_COUNT_S(size_t setsize, cpu_set_t *set);

void CPU_AND_S(size_t setsize, cpu_set_t *destset,
             cpu_set_t *srcset1, cpu_set_t *srcset2);
void CPU_OR_S(size_t setsize, cpu_set_t *destset,
             cpu_set_t *srcset1, cpu_set_t *srcset2);
void CPU_XOR_S(size_t setsize, cpu_set_t *destset,
             cpu_set_t *srcset1, cpu_set_t *srcset2);
             
int  CPU_EQUAL_S(size_t setsize, cpu_set_t *set1, cpu_set_t *set2);

描述:
cpu_set_t数据结构表示一组CPU。sched_setaffinity()和类似接口使用CPU集。
cpu_set_t数据类型实现为位掩码。然而,被视为不透明的数据结构:所有CPU集的操作都应通过以下中描述的宏完成。

提供以下宏用于在CPU集上操作:

含义
CPU_ZERO 清除集合,使其不包含CPU。
CPU_SET 将CPU添加到设置。
CPU_CLR 从集合中删除CPU和CPU。
CPU_ISSET 测试CPU CPU是否是集合的成员。
CPU_COUNT 返回集合中的CPU数量。
CPU_AND 将集合srcset1和srcset2的交集存储在destset中(可能是源集合之一)。
CPU_OR 将集合srcset1和srcset2的并集存储在destset中(可能是源集合之一)。
CPU_XOR 将集合srcset1和srcset2的XOR存储在destset(可能是源集合之一)中。XOR表示srcset1或srcset2中的一组CPU,但不是两个CPU。
CPU_EQUAL 测试两个CPU集是否包含完全相同的CPU。

以下宏用于分配和解除分配CPU集:

含义
CPU_ALLOC 分配一个足够大的CPU集,以容纳范围为0到num_CPUs-1的CPU。
CPU_ALLOC_SIZE 返回CPU集的大小(以字节为单位),该大小将用于保存范围为0到num_CPUs-1的CPU。该宏提供了可用于下面描述的CPU_*_S()宏中的setsize参数的值。
CPU_FREE 释放先前由CPU_ALLOC()分配的CPU集。

sched_setaffinity()和sched_getaffinity

ched_setaffinity():设置线程的CPU关联掩码
sched_ getaffinity():获取线程的CPU关联掩码
函数原型:

#define _GNU_SOURCE             /* See feature_test_macros(7) */
#include <sched.h>

int sched_setaffinity(pid_t pid, size_t cpusetsize, const cpu_set_t *mask);

int sched_getaffinity(pid_t pid, size_t cpusetsize,cpu_set_t *mask);

描述:
线程的CPU关联掩码决定了它有资格运行的CPU集。在多处理器系统上,设置CPU关联掩码可用于获得性能优势。
例如,通过将一个CPU专用于特定线程(即,将该线程的关联掩码设置为指定单个CPU,并将所有其他线程的关联屏蔽设置为排除该CPU),可以确保该线程的最大执行速度。将线程限制为在单个CPU上运行还可以避免因缓存失效而导致的性能成本,当线程停止在一个CPU上执行,然后在另一个CPU重新开始执行时,会发生缓存失效。

CPU亲和性掩码由cpu_set_t结构表示,即“CPU集”,由掩码指向。CPU_SET()中描述了一组用于操作CPU集的宏。

sched_setaffinity()将ID为pid的线程的CPU关联掩码设置为掩码指定的值。如果pid为零,则使用调用线程。参数cpusetsize是掩码指向的数据的长度(以字节为单位)。通常,此参数将指定为sizeof(cpu_set_t)。

如果pid指定的线程当前未在掩码中指定的一个CPU上运行,则该线程将迁移到掩码中指定一个CPU。

sched_getaffinity()将ID为pid的线程的关联掩码写入掩码指向的cpu_set_t结构。cpusetsize参数指定掩码的大小(以字节为单位)。如果pid为零,则返回调用线程的掩码。

返回值:

  • 成功时返回0。
  • 失败时返回-1,并适当设置errno。

二、设置流程



实现CPU亲缘:
1、设置 cpt_set_t
2、绑定CPU,CPU_SET()
3、设置亲缘性,sched_setaffinity()

三、示例代码

#define _GNU_SOURCE             /* See feature_test_macros(7) */
#include <sched.h>

#include <unistd.h>
#include <stdio.h>
#include <sys/syscall.h>

void process_affinity(int num)
{
	// pid_t self_id = gettid();
	pid_t self_id = syscall(__NR_gettid);

	cpu_set_t mask;
	CPU_ZERO(&mask);// 初始化为0

	// 绑定哪个CPU就将那个bit置 1
	CPU_SET(self_id%num, &mask);

	// 黏合
	sched_setaffinity(self_id,sizeof(mask),&mask);
	
	/*以下为业务逻辑*/

	while (1) ;//让CPU爆满,方便看效果
}

int main(int argc, char **argv)
{
	int num = sysconf(_SC_NPROCESSORS_CONF);
	printf("CPU number: %d\n", num);

	int i = 0;
	pid_t pid = 0;
	for (i = 0; i < num / 2; i++)
	{
		pid = fork();
		if (pid == 0)
			break;
	}

	if (pid == 0) {
		process_affinity(num);
	}
	else{
		while (1) usleep(1);// 让主线程做切换,不然执行主线程的CPU也会爆满
	}
	return 0;
}

总结

CPU亲缘性/粘合,是进程或线程只运行在所设置的CPU上,而不是CPU只运行设置的线程或进程。
进程或线程创建的时候,其实是在内核中创建了一个task_struct数据结构,然后等待内核的任务调度器调度执行。

欢迎关注公众号《Lion 莱恩呀》学习技术,每日推送文章。

image.png

【版权声明】本文为华为云社区用户原创内容,未经允许不得转载,如需转载请自行联系原作者进行授权。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。