C++中的CPU亲和性(Affinity)与核心隔离:实现硬实时系统(Hard Real-Time)调度

好的,下面是关于C++中CPU亲和性与核心隔离,以及在硬实时系统调度中的应用的讲座内容。

C++中的CPU亲和性与核心隔离:实现硬实时系统调度

大家好,今天我们来探讨一个非常重要的主题,尤其是在构建硬实时系统时:CPU亲和性与核心隔离。我们将深入了解如何在C++中利用这些技术,以实现可预测且可靠的实时调度。

1. 什么是CPU亲和性与核心隔离?

CPU亲和性指的是将一个进程或线程绑定到特定的CPU核心或核心集合上运行。这意味着该进程/线程只能在指定的这些核心上执行,而不会被操作系统调度到其他核心上。

核心隔离更进一步,它不仅绑定进程到特定核心,而且还会阻止其他非关键进程在该核心上运行。这可以有效地消除来自其他进程的干扰,确保实时任务能够获得专用的计算资源。

为什么我们需要CPU亲和性与核心隔离?

在硬实时系统中,任务必须在严格的时间限制内完成。任何延迟都可能导致系统故障。然而,现代操作系统通常是通用的,它们为了公平性和资源利用率,会动态地调度进程到不同的CPU核心上。这种调度方式会引入不可预测的延迟,例如:

  • 缓存污染: 进程频繁切换到不同的核心会导致缓存失效,每次切换都需要重新加载数据,增加了执行时间。
  • 上下文切换开销: 频繁的上下文切换本身就需要时间,而且会导致流水线停顿。
  • 中断干扰: 其他进程触发的中断可能会打断实时任务的执行。

通过使用CPU亲和性和核心隔离,我们可以最大限度地减少这些干扰,确保实时任务能够以可预测的方式运行。

2. 如何在C++中使用CPU亲和性?

C++本身并没有直接提供设置CPU亲和性的功能,我们需要借助操作系统提供的API。在Linux和Windows上,都有相应的系统调用来实现这一目的。

2.1 Linux下的CPU亲和性

Linux提供了sched_setaffinitysched_getaffinity这两个系统调用来设置和获取进程/线程的CPU亲和性。

  • sched_setaffinity: 用于设置进程或线程的CPU亲和性掩码。
  • sched_getaffinity: 用于获取进程或线程当前的CPU亲和性掩码。

以下是一个简单的例子,展示了如何在Linux下将一个线程绑定到CPU核心0:

#define _GNU_SOURCE // Required for CPU_ macros
#include <iostream>
#include <pthread.h>
#include <sched.h>
#include <unistd.h> // For getpid()

void* task(void* arg) {
    cpu_set_t mask;
    CPU_ZERO(&mask);
    CPU_SET(0, &mask); // 将线程绑定到CPU核心0

    if (pthread_setaffinity_np(pthread_self(), sizeof(cpu_set_t), &mask) != 0) {
        perror("pthread_setaffinity_np");
        return nullptr;
    }

    // Verify the affinity
    cpu_set_t get_mask;
    CPU_ZERO(&get_mask);
    if (pthread_getaffinity_np(pthread_self(), sizeof(cpu_set_t), &get_mask) != 0) {
        perror("pthread_getaffinity_np");
        return nullptr;
    }

    if (CPU_ISSET(0, &get_mask)) {
        std::cout << "Thread is running on CPU core 0" << std::endl;
    } else {
        std::cout << "Thread is NOT running on CPU core 0" << std::endl;
    }

    while(true) {
        // Simulate some work
        usleep(100000); // Sleep for 100 milliseconds
    }

    return nullptr;
}

int main() {
    pthread_t thread;
    pthread_create(&thread, nullptr, task, nullptr);

    pthread_join(thread, nullptr);

    return 0;
}

代码解释:

  1. #define _GNU_SOURCE: 启用GNU扩展,因为CPU_相关的宏可能需要它。
  2. cpu_set_t: 一个位集合,其中每一位代表一个CPU核心。
  3. CPU_ZERO(&mask): 将mask中的所有位清零,表示没有任何核心被选中。
  4. CPU_SET(0, &mask): 将mask中代表CPU核心0的位设置为1,表示选择CPU核心0。
  5. pthread_setaffinity_np(pthread_self(), sizeof(cpu_set_t), &mask): 将当前线程(pthread_self())的亲和性设置为masknp后缀表示这是一个非POSIX标准的函数,但它在Linux系统上可用。
  6. pthread_getaffinity_np(...): 用来验证是否成功设置了CPU亲和性。

编译和运行:

使用以下命令编译代码:

g++ -pthread affinity_example.cpp -o affinity_example

然后运行:

./affinity_example

你应该会看到输出 Thread is running on CPU core 0

2.2 Windows下的CPU亲和性

在Windows上,可以使用SetProcessAffinityMaskSetThreadAffinityMask这两个函数来设置进程和线程的CPU亲和性。

#include <iostream>
#include <windows.h>
#include <process.h> // For _beginthreadex

unsigned __stdcall task(void* arg) {
    DWORD_PTR mask = 1; // 二进制 00000001,表示CPU核心0

    if (SetThreadAffinityMask(GetCurrentThread(), mask) == 0) {
        std::cerr << "SetThreadAffinityMask failed: " << GetLastError() << std::endl;
        return 1;
    }

    // Verify the affinity (not directly possible with a standard API)
    // One would need to use performance counters or similar to verify if the thread executes on the core.

    while(true) {
        // Simulate some work
        Sleep(100); // Sleep for 100 milliseconds
    }

    return 0;
}

int main() {
    HANDLE threadHandle = (HANDLE)_beginthreadex(nullptr, 0, task, nullptr, 0, nullptr);

    if (threadHandle == nullptr) {
        std::cerr << "_beginthreadex failed: " << GetLastError() << std::endl;
        return 1;
    }

    WaitForSingleObject(threadHandle, INFINITE);
    CloseHandle(threadHandle);

    return 0;
}

代码解释:

  1. DWORD_PTR mask = 1;: 创建一个DWORD_PTR类型的变量mask,并将其设置为1。在二进制中,1表示只有最低位是1,其余位是0。这对应于CPU核心0。
  2. SetThreadAffinityMask(GetCurrentThread(), mask): 将当前线程(GetCurrentThread())的亲和性设置为mask。如果函数调用失败,返回值为0。
  3. GetLastError(): 如果SetThreadAffinityMask调用失败,GetLastError()函数可以用来获取错误代码,帮助诊断问题。
  4. _beginthreadex: 用于创建线程。
  5. WaitForSingleObject: 等待线程结束。
  6. CloseHandle: 关闭线程句柄。

编译和运行:

使用Visual Studio或其他C++编译器编译代码。 确保链接器配置为使用多线程运行时库(/MT或/MD)。

运行程序后,可以使用任务管理器或其他系统监视工具来观察线程是否在指定的CPU核心上运行。 然而,Windows并没有提供标准的API来直接验证线程的CPU亲和性,因此可能需要使用性能计数器或其他更高级的技术来进行验证。

3. 核心隔离的实现

核心隔离需要在操作系统层面进行配置。 通常,这意味着需要修改操作系统的启动参数,将某些核心预留给特定的应用程序。

3.1 Linux下的核心隔离

在Linux下,可以使用isolcpus启动参数来实现核心隔离。例如,要在启动时隔离CPU核心2和3,可以在/etc/default/grub文件中添加以下内容:

GRUB_CMDLINE_LINUX_DEFAULT="quiet isolcpus=2,3"

然后,更新GRUB配置并重启系统:

sudo update-grub
sudo reboot

重启后,CPU核心2和3将被隔离,不会被通用调度器使用。 之后,你可以使用sched_setaffinity将你的实时任务绑定到这些隔离的核心上。

3.2 Windows下的核心隔离

Windows也支持核心隔离,但配置过程相对复杂,通常涉及到修改注册表和使用Processor Power Management相关的设置。具体步骤可以参考Microsoft的官方文档。

4. 实时调度的C++代码示例

以下是一个更完整的示例,展示了如何在Linux下创建一个实时线程,并将其绑定到隔离的CPU核心上:

#define _GNU_SOURCE
#include <iostream>
#include <pthread.h>
#include <sched.h>
#include <unistd.h>
#include <sys/time.h>
#include <cstring> // For memset

#define CORE_ID 2 // 隔离的CPU核心ID

void* real_time_task(void* arg) {
    // 1. 设置线程优先级为最高
    struct sched_param sp;
    sp.sched_priority = sched_get_priority_max(SCHED_FIFO);
    if (pthread_setschedparam(pthread_self(), SCHED_FIFO, &sp) != 0) {
        perror("pthread_setschedparam");
        return nullptr;
    }

    // 2. 绑定到隔离的CPU核心
    cpu_set_t mask;
    CPU_ZERO(&mask);
    CPU_SET(CORE_ID, &mask);
    if (pthread_setaffinity_np(pthread_self(), sizeof(cpu_set_t), &mask) != 0) {
        perror("pthread_setaffinity_np");
        return nullptr;
    }

    // 3. 验证CPU亲和性
    cpu_set_t get_mask;
    CPU_ZERO(&get_mask);
    if (pthread_getaffinity_np(pthread_self(), sizeof(cpu_set_t), &get_mask) != 0) {
        perror("pthread_getaffinity_np");
        return nullptr;
    }

    if (CPU_ISSET(CORE_ID, &get_mask)) {
        std::cout << "Real-time thread is running on CPU core " << CORE_ID << std::endl;
    } else {
        std::cout << "Real-time thread is NOT running on CPU core " << CORE_ID << std::endl;
    }

    // 4. 实时任务循环
    struct timespec start, end;
    while (true) {
        clock_gettime(CLOCK_MONOTONIC, &start);

        // Simulate some real-time work (e.g., reading sensor data, processing data)
        usleep(1000); // Simulate 1 millisecond of work

        clock_gettime(CLOCK_MONOTONIC, &end);

        long elapsed_ns = (end.tv_sec - start.tv_sec) * 1000000000L + (end.tv_nsec - start.tv_nsec);
        std::cout << "Elapsed time: " << elapsed_ns << " ns" << std::endl;
    }

    return nullptr;
}

int main() {
    // 1. 设置主线程的CPU亲和性(可选,但推荐)
    cpu_set_t main_mask;
    CPU_ZERO(&main_mask);
    CPU_SET(0, &main_mask); // 将主线程绑定到CPU核心0
    if (sched_setaffinity(0, sizeof(cpu_set_t), &main_mask) != 0) {
        perror("sched_setaffinity (main thread)");
    }

    // 2. 创建实时线程
    pthread_t thread;
    pthread_attr_t attr;
    pthread_attr_init(&attr);
    pthread_attr_setinheritsched(&attr, PTHREAD_EXPLICIT_SCHED); // 设置调度策略为显式指定
    pthread_attr_setschedpolicy(&attr, SCHED_FIFO); // 设置调度策略为FIFO
    struct sched_param param;
    param.sched_priority = sched_get_priority_max(SCHED_FIFO) - 1; // 设置优先级(略低于实时线程)
    pthread_attr_setschedparam(&attr, &param);

    if (pthread_create(&thread, &attr, real_time_task, nullptr) != 0) {
        perror("pthread_create");
        return 1;
    }

    pthread_attr_destroy(&attr); // 销毁属性对象

    // 3. 等待实时线程结束(通常不需要,因为实时线程会一直运行)
    pthread_join(thread, nullptr);

    return 0;
}

代码解释:

  1. #define CORE_ID 2: 定义了隔离的CPU核心ID。确保这个ID与你在isolcpus中设置的值一致。
  2. sched_param sp;: 用于设置线程的调度参数,例如优先级。
  3. pthread_setschedparam(pthread_self(), SCHED_FIFO, &sp): 将当前线程的调度策略设置为SCHED_FIFO(先进先出),并将优先级设置为最高。 SCHED_FIFO是一种实时调度策略,它保证优先级最高的线程总是先运行。
  4. clock_gettime(CLOCK_MONOTONIC, &start)clock_gettime(CLOCK_MONOTONIC, &end): 使用CLOCK_MONOTONIC时钟来测量时间。 CLOCK_MONOTONIC提供了一个单调递增的时钟,不受系统时间调整的影响,因此更适合用于测量时间间隔。
  5. usleep(1000): 模拟1毫秒的工作。 在实际的实时系统中,这部分代码会被替换为真正的实时任务,例如读取传感器数据或控制执行器。
  6. 主线程的CPU亲和性: 将主线程绑定到CPU核心0,防止主线程影响实时线程的执行。
  7. pthread_attr_t attr: 用于创建线程属性对象。通过设置线程属性,可以更精细地控制线程的调度策略和优先级。
  8. pthread_attr_setinheritsched(&attr, PTHREAD_EXPLICIT_SCHED): 设置线程的调度继承属性。PTHREAD_EXPLICIT_SCHED表示线程使用显式设置的调度策略和优先级,而不是从父线程继承。
  9. pthread_attr_setschedpolicy(&attr, SCHED_FIFO): 设置线程的调度策略为FIFO。
  10. param.sched_priority = sched_get_priority_max(SCHED_FIFO) – 1: 设置线程的优先级。通常,实时线程的优先级应该设置为尽可能高,但要低于内核线程的优先级。这里将优先级设置为最高优先级减1,以确保实时线程能够优先执行,但不会干扰内核操作。
  11. pthread_attr_destroy(&attr): 销毁线程属性对象,释放资源。

编译和运行:

  1. 确保你的系统已经配置了核心隔离(使用isolcpus)。
  2. 使用以下命令编译代码:

    g++ -pthread real_time_example.cpp -o real_time_example -lrt

    -lrt 链接实时库,因为 clock_gettime 需要它。

  3. 以root权限运行程序,因为设置实时调度策略需要root权限:

    sudo ./real_time_example

    你应该会看到输出,显示实时线程正在隔离的CPU核心上运行,并显示每次循环的执行时间。

5. 额外的考虑因素

  • 中断处理: 确保中断处理程序不会在隔离的核心上运行。可以使用irqbalance或手动配置中断亲和性来将中断分配到非隔离的核心上。
  • 内存锁定: 使用mlockall函数可以防止进程的内存被交换到磁盘,这可以减少延迟。
  • 系统调用: 尽量减少实时任务中的系统调用,因为系统调用可能会阻塞任务的执行。
  • 测试和验证: 使用专门的工具(例如cyclictest)来测试和验证实时系统的性能。

6. CPU亲和性与核心隔离的优缺点

特性 优点 缺点
CPU亲和性 提高缓存命中率,减少上下文切换开销,提高任务执行的可预测性。 仍然可能受到其他进程的影响,不能完全保证实时性。配置不当可能导致资源利用率下降。
核心隔离 彻底消除其他进程的干扰,提供专用的计算资源,最大程度地提高实时性。 牺牲了资源利用率,被隔离的核心可能在某些时候处于空闲状态。配置复杂,需要修改操作系统启动参数。
实时调度策略 确保高优先级任务优先执行,减少任务的延迟。 需要root权限才能设置,可能导致低优先级任务饥饿。
内存锁定 确保关键数据始终驻留在内存中,避免因页面交换导致的延迟。 增加了内存占用,可能导致系统内存不足。

7. 总结

CPU亲和性与核心隔离是构建硬实时系统的关键技术。通过将实时任务绑定到特定的CPU核心,并隔离这些核心免受其他进程的干扰,我们可以最大限度地减少延迟,并确保任务能够以可预测的方式运行。 然而,这些技术也需要仔细的配置和测试,以确保它们能够满足系统的实时性要求,同时不会对资源利用率产生负面影响。

请记住,硬实时系统设计是一个复杂的过程,需要综合考虑硬件、操作系统和应用程序等各个方面。 CPU亲和性与核心隔离只是其中的一部分,但它们对于实现可靠的实时性能至关重要。

希望今天的讲座能够帮助大家更好地理解和应用这些技术。 谢谢!

8. 实现可靠的实时性能需要考虑这些

  • 硬件、操作系统和应用程序都需要综合考虑。
  • 需要仔细的配置和测试。
  • CPU亲和性与核心隔离是可靠的实时性能的组成部分。

更多IT精英技术系列讲座,到智猿学院

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注