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

2024-09-28 22:45:04 浏览数 (3)

引言

CPU亲和性是指将特定的计算资源(如处理器核心)与特定的任务或线程相关联的能力。它允许系统管理员或开发人员指定将特定任务或线程绑定到特定的处理器核心上运行,以提高系统性能和效率。通过控制核心分配和利用CPU亲和性,可以确保任务或线程在运行过程中始终与特定的处理器核心保持关联,避免频繁的核心切换和资源争用,从而提高处理速度和效率。

CPU亲和性的定义可以根据具体的操作系统和硬件平台而有所不同。在多核处理器系统中,通常可以使用操作系统提供的API或工具来设置和管理CPU亲和性。通过合理配置CPU亲和性,可以优化多线程应用程序的性能、提高并行计算效果,并最大化系统的整体运行效率。

控制核心分配对于提高速度和效率具有重要性。通过合理地分配处理器核心和利用CPU亲和性,可以实现以下几个方面的优化:

1.当任务或线程频繁在不同的核心之间切换时,会引入一定的切换开销,包括上下文切换和缓存切换。通过控制核心分配,将特定的任务或线程绑定到特定的核心上,可以减少切换开销,提高处理速度和响应性能。

2. 处理器核心通常会有多级缓存,而不同的核心之间的缓存是独立的。当任务或线程在不同的核心上运行时,可能会导致缓存失效,从而降低缓存的效率。通过控制核心分配和利用CPU亲和性,可以使任务或线程一直在与之关联的核心上运行,从而充分利用核心的缓存,提高缓存命中率和访问效率。

3. 多个任务或线程在竞争相同的资源时,可能会引发资源争用问题,导致性能下降。通过控制核心分配,可以将具有相互竞争关系的任务或线程分配到不同的核心上运行,避免资源争用,提高效率。

4. 对于多线程应用程序,合理地设置CPU亲和性可以将不同的线程分配到不同的核心上运行,充分利用多核处理器的并行计算能力,提高多线程应用程序的性能和吞吐量。

一、相关函数

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

函数原型:

代码语言:javascript复制
#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()

函数原型:

代码语言:javascript复制
#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():获取线程标识。

函数原型:

代码语言:javascript复制
#include <sys/types.h>

pid_t gettid(void);

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

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

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

函数原型:

代码语言:javascript复制
#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中。

使用示例:

代码语言:javascript复制
#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集的宏。

函数原型:

代码语言:javascript复制
#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关联掩码 函数原型:

代码语言:javascript复制
#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()

三、示例代码

代码语言:javascript复制
#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: %dn", 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数据结构,然后等待内核的任务调度器调度执行。

0 人点赞