Skip to content

LV050-CPU核

一、基本概念

我们在 windows 下可以看到这样的一些参数:

image-20260404200029122
  • 插槽:指主板上物理安装 CPU 的接口,一个插槽对应一个物理 CPU 芯片。对于大多数个人电脑来说,通常只有 1 个插槽。服务器或高端工作站主板可能有 2 个或更多插槽。任务管理器显示的数量就是主板上实际插着的 CPU 个数。
  • 内核:每个物理 CPU 芯片内部包含的 物理核心 数量。例如“8 核” CPU 表示这个芯片里有 8 个独立工作的物理核心。核心是真正执行计算任务的硬件单元。多核能让电脑同时处理多个任务而不明显卡顿。
  • 逻辑处理器:操作系统能调度的 最小执行单元。它的数量取决于 CPU 是否支持 超线程 技术。如果 不支持 超线程:逻辑处理器数 = 物理内核数。如果 支持 超线程(常见于 Intel 和 AMD):一个物理内核可以模拟出两个逻辑处理器,让操作系统认为有两个 CPU 可用。此时逻辑处理器数 = 物理内核数 × 2。

我们在创建 VMware 的时候需要为虚拟机分配 CPU,例如:

image-20260404200225802

先要知道一个东西,就是 VMware 会通过硬件模拟,让虚拟机操作系统“以为”自己真的安装在了有这么多物理 CPU 插槽的主板上。上面我的 windows 一共是由 1 个物理 CPU,8 个核心,16 个逻辑处理器,那么对于 VMware 来说,处理器内核总数不超过 16 就可以了。若是配置为处理器数量 2,每个处理器内核数量 2,那么若是 VMware 中安装了 Windows 系统,那么资源管理器的 CPU 将会显示 2 个插槽了。

  • CPU 亲和性(CPU Affinity):也称为 CPU 绑定,指定一个进程或线程只能在某些特定的 CPU 核心上运行。默认情况下,Linux 调度器可以在任意可用的 CPU 上调度进程,但通过设置亲和性,可以将进程 "绑定" 到指定的核心上运行。合理配置亲和性可以减少缓存失效、降低跨核心调度的开销、避免资源竞争,从而显著提升系统性能

  • CPU 掩码(CPU Mask / Bitmask):是 Linux 内核中用于表示系统中 CPU 集合的一种位图。每个位代表一个 CPU 序号,通过这种方式,可以方便地管理和操作多个 CPU 的状态。

shell
bit 0 (最低位) → CPU 0
bit 1 CPU 1
...
bit N CPU N

该位 = 1 表示允许在该 CPU 上运行,该位 = 0 表示禁止在该 CPU 上运行。

二、CPU 核心数

1. 虚拟机设置

先来了解一下怎么查看 CPU 和操作 CPU 核的相关内容吧。

text
总核数 = 物理CPU个数 X 每颗物理CPU的核数
总逻辑CPU数 = 物理CPU个数 X 每颗物理CPU的核数 X 超线程数

我的下边这些操作都是在虚拟机中进行,我的虚拟机参数如下,这就相当于有 2 个物理 CPU,每个 CPU 又有 2 个核心,一共就是有 4 个逻辑 CPU:

image-20230305093805949

2. 查看 CPU 的核心数

2.1 查看物理 CPU 个数

shell
cat /proc/cpuinfo| grep "physical id"| sort| uniq| wc -l
cat /proc/cpuinfo | grep "physical id" | uniq

在终端输入以上命令可以看到:

shell
hk@vm:/$ cat /proc/cpuinfo| grep "physical id"| sort| uniq| wc -l
2

hk@vm:/$ cat /proc/cpuinfo | grep "physical id" | uniq
physical id	: 0
physical id	: 1

2.2 查看逻辑 CPU 个数

shell
cat /proc/cpuinfo| grep "processor"| wc -l
cat /proc/cpuinfo | grep "processor" | uniq

processor:指明每个物理 CPU 中逻辑处理器信息(序号:0~5 表示有 6 个逻辑 CPU)。在终端输入以上命令可以看到:

c
hk@vm:/$ cat /proc/cpuinfo| grep "processor"| wc -l
4
    
hk@vm:/$ cat /proc/cpuinfo | grep "processor" | uniq
processor	: 0
processor	: 1
processor	: 2
processor	: 3

2.3 查看单个 CPU 核数

shell
cat /proc/cpuinfo | grep "cpu cores" | uniq

cpu cores:指明每颗物理 CPU 中总核数,在终端输入以上命令可以看到:

shell
hk@vm:/$ cat /proc/cpuinfo | grep "cpu cores" | uniq
cpu cores	: 2

2.4 直接查看 CPU 所有信息

shell
cat /proc/cpuinfo

在终端输入以上命令可以看到:

shell
太多了,这里就不写了

3. C 语言获取 CPU 核心数

我们在程序中使用可以使用系统调用 sysconf 获取 cpu 核心数。

3.1 函数说明

c
/* 需要包含的头文件 */
#include <unistd.h>
// 返回系统可以使用的核数,但是其值会包括系统中禁用的核的数目,因 此该值并不代表当前系统中可用的核数
int sysconf(_SC_NPROCESSORS_CONF);
// 返回值真正的代表了系统当前可用的核数
int sysconf(_SC_NPROCESSORS_ONLN);

/* 以下两个函数与上述类似 */
#include <sys/sysinfo.h>

int get_nprocs_conf (void);/* 可用核数 */
int get_nprocs (void);     /* 真正的反映了当前可用核数 */

3.2 使用实例

c
#include <stdio.h>
#include <unistd.h>
#include <sys/sysinfo.h>

int main(int argc, const char *argv[])
{
    int sc_nprocessor_conf = sysconf(_SC_NPROCESSORS_CONF);
    int sc_nprocessor_onln = sysconf(_SC_NPROCESSORS_ONLN);
    printf("sc_nprocessor_conf:%d\n", sc_nprocessor_conf);
    printf("sc_nprocessor_onln:%d\n", sc_nprocessor_onln);

    int nprocs_conf = get_nprocs_conf();
    int nprocs_onln = get_nprocs();
    printf("nprocs_conf:%d\n", nprocs_conf);
    printf("nprocs_onln:%d\n", nprocs_onln);
    return 0;
}

编译执行后如下所示:

shell
sc_nprocessor_conf:4
sc_nprocessor_onln:4
nprocs_conf:4
nprocs_onln:4

四、多进程和多线程在 CPU 核上的运行

每个 CPU 核运行一个进程的时候,由于每个进程的资源都独立,所以 CPU 核心之间切换的时候无需考虑上下文。每个 CPU 核运行一个线程的时候,有时不同线程之间需要共享资源,所以这些资源必须从 CPU 的一个核心被复制到另外一个核心,这会造成额外的开销。

1. taskset 命令

1.1 测试程序

这里使用进程来进行测试。

c
#include <stdio.h>
#include <time.h>
#include <sys/types.h>
#include <unistd.h>

int main(int argc, const char *argv[]) {
    pid_t ppid = getppid();
    pid_t pid = getpid();
    
    printf("ppid %d, pid %d\n", ppid, pid);
    while (1) {
        sleep(5);
    }
    return 0;
}

使用以下命令编译程序:

shell
gcc main.c -Wall

我们就会得到一个 a.out 的可执行文件,我们需要在后台运行这个进程方便后续测试使用:

shell
./a.out &

1.2 获取进程的 PID

shell
ps -aux | grep "a.out"

我们会得到以下输出信息:

shell
sumu@sumu-vm:~/workspace/c-learning/03-os/05-pthread/03-cpu$ ps -aux | grep a.out
sumu       10522  0.0  0.0   2496   576 pts/0    S    20:19   0:00 ./a.out
sumu       10641  0.0  0.0  12132   712 pts/0    R+   20:19   0:00 grep --color=auto a.out

1.3 查看进程当前运行在哪些 CPU 上

shell
taskset -p 10522

我们会得到以下输出信息:

shell
sumu@sumu-vm:~/workspace/c-learning/03-os/05-pthread/03-cpu$ taskset -p 10522
pid 10522 的当前亲和力掩码:f

显示为 0x0f(十六进制) 转换为二进制为 1111(二进制),最右起第一个 1 表示 CPU 核心 0,第二个 1 表示 CPU 核心 1 依次类推。所以进程运行在 4 个 CPU 上。

1.4 指定进程运行在 CPU1 和 3 上

shell
taskset -pc 1,3 10522

我们可以看到以下输出:

shell
sumu@sumu-vm:~/workspace/c-learning/03-os/05-pthread/03-cpu$ taskset -pc 1,3 10522
pid 10522 的当前亲和力列表:0-3
pid 10522 的新亲和力列表:1,3

sumu@sumu-vm:~/workspace/c-learning/03-os/05-pthread/03-cpu$ taskset -p 10522
pid 10522 的当前亲和力掩码:a

0x0a 转化为二进制就是 0000 1010,就代表了 CPU1 和 3。

1.5 运行程序时绑定 CPU

shell
taskset -c 3 ./a.out &

可以看到以下信息

shell
sumu@sumu-vm:~/workspace/c-learning/03-os/05-pthread/03-cpu$ taskset -c 3 ./a.out &
[4] 10913
ppid 2777, pid 10913

sumu@sumu-vm:~/workspace/c-learning/03-os/05-pthread/03-cpu$ taskset -p 10913
pid 10913 的当前亲和力掩码:8

0x08 转化为二进制就是 0000 1000,可以看到进程就运行在了 CPU3 上了。

2. C 语言中的绑定相关函数

CPU Affinity (CPU 亲合力),CPU 亲合力就是指在 Linux 系统中能够将一个或多个进程绑定到一个或多个处理器上运行。一个进程的 CPU 亲合力掩码决定了该进程将在哪个或哪几个 CPU 上运行。在一个多处理器系统中,设置 CPU 亲合力的掩码可能会获得更好的性能。

一个 CPU 的亲合力掩码用一个 cpu_set_t 结构体来表示一个 CPU 集合, 下面的几个宏分别对这个掩码集进行操作:

c
void CPU_ZERO(cpu_set_t *set);          // 清空一个集合
void CPU_SET(int cpu, cpu_set_t *set);  // 将一个给定的 CPU 号加到一个集合
void CPU_CLR(int cpu, cpu_set_t *set);  // 将一个给定的 CPU 号从一个集合中去掉
int  CPU_ISSET(int cpu, cpu_set_t *set);// 检查一个 CPU 号是否在这个集合中
int  CPU_COUNT(cpu_set_t *set);         // 返回集合中包含的 CPU 数量

cpu_set_t 是一个数据类型,具体是啥样的没在内核源码找到,后边找到了再补充吧,反正我们操作的时候都是定义这样一个变量,然后用宏去进行操作。

2.1 sched_setaffinity()

在 linux 下可以使用 man sched_setaffinity 命令查看该函数的帮助手册。

c
/* 需包含的头文件 */
#define _GNU_SOURCE             /* See feature_test_macros(7) */
// 实际使用的时候加下边的宏
#define __USE_GNU
#include <sched.h>

/* 函数声明 */
int sched_setaffinity(pid_t pid, size_t cpusetsize, const cpu_set_t *mask);

函数说明】该函数用于设置进程号(线程)为 pid 的进程运行在 mask 所设定的 CPU 上。

函数参数

  • pid :pid_t 类型变量,表示进程 pid,如果 pid 的值为 0, 则表示指定的是当前进程
  • cpusetsize :size_t 类型,是 mask 所指定的数的长度,通常设定为 sizeof(cpu_set_t)。
  • mask :cpu_set_t 类型指针变量,CPU 亲和力掩码,就是指定在哪几个 CPU 上运行。

返回值】 int 类型,成功返回 0,失败返回-1,并设置错误号。

使用格式】none

注意事项】按照查阅的资料来看,可以设定进程和线程,后边发现有什么问题再更新。

2.2 sched_getaffinity()

在 linux 下可以使用 man sched_getaffinity 命令查看该函数的帮助手册。

c
/* 需包含的头文件 */
#define _GNU_SOURCE             /* See feature_test_macros(7) */
// 实际使用的时候加下边的宏
#define __USE_GNU
#include <sched.h>

/* 函数声明 */
int sched_getaffinity(pid_t pid, size_t cpusetsize, cpu_set_t *mask);

函数说明】该函数用于获取进程号(线程)为 pid 所指示的进程的 CPU 位掩码,并将该掩码返回到 mask 所指向的结构中。

函数参数

  • pid :pid_t 类型变量,表示进程 pid,如果 pid 的值为 0, 则表示指定的是当前进程
  • cpusetsize :size_t 类型,是 mask 所指定的数的长度,通常设定为 sizeof(cpu_set_t)。
  • mask :cpu_set_t 类型指针变量,CPU 亲和力掩码,就是指在哪几个 CPU 上运行。

返回值】 int 类型,成功返回 0,失败返回-1,并设置错误号。

使用格式】none

注意事项】按照查阅的资料来看,可以设定进程和线程,后边发现有什么问题再更新。

2.3 pthread_setaffinity_np()

在 linux 下可以使用 man pthread_setaffinity_np 命令查看该函数的帮助手册。

c
/* 需包含的头文件 */
#define _GNU_SOURCE             /* See feature_test_macros(7) */
#include <pthread.h>

/* 函数声明 */
int sched_setaffinity(pid_t pid, size_t cpusetsize, const cpu_set_t *mask);

函数说明】该函数用于设置线程为 pid 的进程运行在 mask 所设定的 CPU 上。

函数参数

  • pid :pid_t 类型变量,表示线程 pid,如果 pid 的值为 0, 则表示指定的是当前线程。
  • cpusetsize :size_t 类型,是 mask 所指定的数的长度,通常设定为 sizeof(cpu_set_t)。
  • mask :cpu_set_t 类型指针变量,CPU 亲和力掩码,就是指定在哪几个 CPU 上运行。

返回值】 int 类型,成功返回 0,失败返回-1,并设置错误号。

使用格式】none

注意事项】none

2.4 pthread_getaffinity_np()

在 linux 下可以使用 man pthread_getaffinity_np 命令查看该函数的帮助手册。

c
/* 需包含的头文件 */
#define _GNU_SOURCE             /* See feature_test_macros(7) */
#include <pthread.h>

/* 函数声明 */
int sched_getaffinity(pid_t pid, size_t cpusetsize, cpu_set_t *mask);

函数说明】该函数用于获取线程为 pid 所指示的进程的 CPU 位掩码,并将该掩码返回到 mask 所指向的结构中。

函数参数

  • pid :pid_t 类型变量,表示线程 pid ,如果 pid 的值为 0, 则表示指定的是当前线程
  • cpusetsize :size_t 类型,是 mask 所指定的数的长度,通常设定为 sizeof(cpu_set_t) 。
  • mask :cpu_set_t 类型指针变量,CPU 亲和力掩码,就是指在哪几个 CPU 上运行。

返回值】 int 类型,成功返回 0,失败返回-1,并设置错误号。

使用格式】none

注意事项】none

2.4 绑定实例

2.4.1 使用实例 1
c
#include <stdio.h>
#include <sys/types.h>
#include <sys/sysinfo.h>
#include <unistd.h>

#define __USE_GNU
#include <sched.h>

#include <pthread.h>

#define MAX_THREAD_NUM 200 // 1 个 CPU 内的最多进程数

int   cpu_cores_num = 0; // cpu 中核数
void *thread_test(void *arg) {
    cpu_set_t mask;     // CPU 核的集合
    cpu_set_t get_mask; // 获取在集合中的 CPU
    int      *cpu_index = (int *)arg;

    printf("the thread is:%d\n", *cpu_index);            // 显示是第几个线程
    CPU_ZERO(&mask);                                     // 置空
    CPU_SET(*cpu_index, &mask);                          // 设置亲和力值
    if (sched_setaffinity(0, sizeof(mask), &mask) == -1) // 设置线程 CPU 亲和力
    {
        printf("warning: could not set CPU affinity, continuing...\n");
    }

    CPU_ZERO(&get_mask);
    if (sched_getaffinity(0, sizeof(get_mask), &get_mask) == -1) // 获取线程 CPU 亲和力
    {
        printf("warning: cound not get thread affinity, continuing...\n");
    }
    for (int cpu_core_index = 0; cpu_core_index < cpu_cores_num; cpu_core_index++) {
        if (CPU_ISSET(cpu_core_index, &get_mask)) // 判断线程与哪个 CPU 有亲和力
        {
            printf("this thread %d is running processor : %d\n", cpu_core_index, cpu_core_index);
        }
    }

    return NULL;
}

int main(int argc, char *argv[]) {
    int       tid[MAX_THREAD_NUM] = {0};
    int       cpu_core_index = 0;
    pthread_t thread[MAX_THREAD_NUM];

    cpu_cores_num = sysconf(_SC_NPROCESSORS_CONF); // 获取核数
    if (cpu_cores_num > MAX_THREAD_NUM) {
        printf("cpu_cores_num of cores[%d] is bigger than MAX_THREAD_NUM[%d]!\n", cpu_cores_num, MAX_THREAD_NUM);
        return -1;
    }
    printf("system has %i processor(s). \n", cpu_cores_num);

    for (cpu_core_index = 0; cpu_core_index < cpu_cores_num; cpu_core_index++) {
        tid[cpu_core_index] = cpu_core_index; // 每个线程必须有个 tid [i]
        pthread_create(&thread[cpu_core_index], NULL, thread_test, (void *)&tid[cpu_core_index]);
    }
    for (cpu_core_index = 0; cpu_core_index < cpu_cores_num; cpu_core_index++) {
        pthread_join(thread[cpu_core_index], NULL); // 等待所有的线程结束,线程为死循环所以 CTRL+C 结束
    }
    return 0;
}

通过以下指令编译并运行:

shell
gcc main.c -Wall -lpthread
./a.out

然后可以看到以下输出信息:

shell
sumu@sumu-vm:~/workspace/c-learning/03-os/05-pthread/03-cpu$ gcc 050-c-api-demo1.c -lpthread
sumu@sumu-vm:~/workspace/c-learning/03-os/05-pthread/03-cpu$ ./a.out 
system has 4 processor(s). 
the thread is:0
this thread 0 is running processor : 0
the thread is:1
the thread is:3
this thread 1 is running processor : 1
the thread is:2
this thread 2 is running processor : 2
this thread 3 is running processor : 3
2.4.2 使用实例 2
c
#define _GNU_SOURCE
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>

#define handle_error_en(en, msg) \
    do                           \
    {                            \
        errno = en;              \
        perror(msg);             \
        exit(EXIT_FAILURE);      \
    } while (0)

int main(int argc, char *argv[])
{
    int err, i;
    cpu_set_t cpuset;

    pthread_t thread = pthread_self();
    /* Set affinity mask to include CPUs 0 to 7 */

    CPU_ZERO(&cpuset);
    for (i = 0; i < 8; i++)
    {
        CPU_SET(i, &cpuset);
    }

    err = pthread_setaffinity_np(thread, sizeof(cpu_set_t), &cpuset);
    if (err != 0)
    {
        handle_error_en(err, "pthread_setaffinity_np");
    }

    /* Check the actual affinity mask assigned to the thread */
    err = pthread_getaffinity_np(thread, sizeof(cpu_set_t), &cpuset);
    if (err != 0)
    {
        handle_error_en(err, "pthread_getaffinity_np");
    }
    printf("Set returned by pthread_getaffinity_np() contained:\n");
    for (i = 0; i < CPU_SETSIZE; i++)
    {
        if (CPU_ISSET(i, &cpuset))
            printf("CPU %d\n", i);
    }

    exit(EXIT_SUCCESS);

    return 0;
}

通过以下指令编译并运行:

shell
gcc 02_cpu.c -Wall -lpthread
./a.out

然后可以看到以下输出信息:

shell
Set returned by pthread_getaffinity_np() contained:
CPU 0
CPU 1
CPU 2
CPU 3