41、多处理器性能与Linux调试技巧

多处理器性能与Linux调试技巧

1. 多处理器相关知识

在如今的计算环境中,多核CPU几乎适用于各种架构。为了简化讨论,这里主要以AMD和英特尔基于英特尔架构的实现为例,不过相关问题适用于所有架构。

1.1 多核CPU概述

英特尔和AMD的第一代多核CPU是双核的。从功能上看,双核CPU相当于两个单核CPU(例如在多处理器主板上)。每个核心都有自己的寄存器、缓存、指令流水线、执行单元、MMU等。原则上,双核处理器的性能与具有两个相同时钟频率单核处理器的SMP系统相当。

未来的双核和四核CPU将在一定程度上共享片上缓存,这既有缺点也有优点。一方面,它限制了单个CPU在不与其他CPU竞争的情况下可访问的缓存量;另一方面,共享缓存减少了同步单独缓存所需的周期数。因此,一些应用程序会从多处理器共享缓存中受益,而另一些则会受到影响,很难简单地判断哪种方法更好。

1.2 SMP机器编程

大多数应用程序无需知道它们运行在多CPU机器上,硬件和操作系统会处理大部分细节。操作系统负责在CPU之间分配任务和平衡负载,但有些应用程序需要了解CPU的数量和类型,以充分利用硬件资源。

  • Linux调度器与SMP :Linux内核2.0版本引入了SMP。SMP调度器试图在CPU之间高效地分配任务和线程,充分利用硬件资源。其启发式算法基于所有CPU平等的假设,这也是对称多处理(SMP)中“对称”的含义。然而,SMT和多核处理器等创新正在挑战这一假设。在高级多处理器架构中,通常需要应用程序了解硬件特性并为调度器提供线索。
    SMP调度器倾向于将进程保持在同一个CPU上,因为(由于延迟TLB刷新)进程很可能能够重用TLB。但对于SMT CPU来说,这是一种浪费,因为两个逻辑CPU使用相同的MMU和缓存,这可能导致进程因调度器认为将其排队到另一个CPU成本更高而被迫等待一个CPU。
    用户应用程序可以通过亲和掩码为调度器提供线索。调度器为系统中的每个进程(和线程)维护一个亲和掩码,它是一个位图,系统中的每个CPU对应一位。默认的亲和掩码全为1,表示任何处理器都可以执行该进程。当调度器找到一个可运行状态的任务时,它会检查哪些CPU可用于执行代码,然后将其与进程的亲和掩码进行比较,以确定哪个CPU将执行该进程。可以通过适当设置亲和掩码来限制进程在一个或多个处理器上执行。随着内核的成熟,调度器也在不断跟上技术发展。在Linux 2.6.7中,调度器除了支持SMP外,还增加了对SMT的支持,因此调度器在选择CPU执行时可能会做出更明智的选择。

  • 使用亲和性强制进程使用特定CPU :schedutils包包含taskset命令,可用于为特定进程设置亲和掩码。它可以应用于正在运行的进程或单个命令。例如,可以使用taskset命令通过设置亲和掩码的第0位(其他位为0)来强制进程仅在SMP系统的第一个处理器上运行:

$ taskset 1 ./myprogram

也可以使用 -p 选项来设置或检查正在运行进程的亲和掩码:

$ taskset -p 1234

Linux允许任何用户检查任何进程的亲和掩码,但只有root用户可以更改进程的亲和掩码,无论进程的所有者是谁。

  • 何时以及为何修改进程亲和性 :尽可能让Linux调度器负责调度,避免硬编码进程亲和性,因为这可能会在设计中嵌入许多假设,代码在测试目标上可能运行良好,但在新的或不同的架构上可能不是最优的。只有在少数情况下才需要更改亲和掩码,例如有一个内存密集型应用程序,希望将其保持在单个处理器上,尽管Linux调度器会尝试这样做,但不能保证它会一直留在一个CPU上。如果在双CPU系统上运行两个这样的进程,将它们锁定在单独的处理器上可能是有意义的。另一个适合使用亲和性的情况是有专用硬件,如嵌入式计算机,在这种情况下,系统设计师可以充分了解底层硬件,使用亲和性可以确保最有效地利用硬件。

  • 进程亲和性API :进程和线程可以通过为此目的定义的系统调用来检查和修改其亲和掩码,但更改亲和掩码需要root权限。可以使用以下GLIBC扩展来检查自身或其他进程的亲和掩码:

int sched_setaffinity(pid_t  pid,  size_t setsize,  cpu_set_t *cpuset);
int sched_getaffinity(pid_t  pid,  size_t setsize,  cpu_set_t *cpuset);

这些函数成功时返回0,出错时返回 -1。 cpu_set_t 是前面讨论的位掩码, setsize 参数是掩码的大小。 cpu_set_t 定义为提供一个位掩码,允许比无符号长整型更多的CPU。因此,需要特殊的宏来设置和清除此掩码中的位,定义如下:

CPU_ZERO(p)    - Clears the mask pointed to by p.
CPU_SET(n,p)   – Sets the bit for CPU n in mask pointed to by p.
CPU_CLR(n,p)   – Clears the bit for CPU n in the mask pointed to by p.
CPU_ISSET(n,p) – Returns nonzero when bit n of the mask pointed to by p is set.

要调用其中一个 setaffinity 函数,必须使用这些宏来初始化 cpu_set_t 。进程必须具有root权限,否则函数将返回错误。这些函数不适用于线程,GLIBC为此提供了对POSIX pthreads API的扩展。

  • 线程亲和性API :GNU Native POSIX Threads Library (NPTL) 包含支持线程亲和性的函数。使用这些函数,可以将正在运行的线程限制在系统中的一个或多个CPU上,这有助于通过将使用公共内存的线程保持在一个CPU上来提高性能,从而减少缓存未命中。POSIX pthreads标准目前不支持亲和性,因此NPTL中的函数是扩展,通过 _np(非POSIX)后缀表示。这些函数定义如下:
int pthread_setaffinity_np(pthread_t tid, size_t setsize, cpu_set_t *cpuset);
int pthread_setaffinity_np(pthread_t tid, size_t setsize, cpu_set_t *cpuset);

这些函数需要一个正在运行的线程(由 tid 给出)才能正确操作。要影响当前正在运行的线程,调用者可以传递 pthread_self 的返回值作为 tid 的值。如果想在线程启动前初始化亲和性,可以通过线程属性来实现。给定一个正确初始化的 pthread_attr_t 对象,可以使用以下函数设置亲和性:

int pthread_attr_setaffinity_np(pthread_attr_t *attr, size_t setsize, cpu_set_t *cpuset);
int pthread_attr_getaffinity_np(pthread_attr_t *attr, size_t setsize, cpu_set_t *cpuset);

这些函数不接受线程ID作为参数,调用者提供 attr 的存储空间,并将其作为 pthread_create 的参数,创建的线程将具有在属性中设置的亲和掩码。

2. 基本调试工具 - printf

在过去,几乎没有调试器,程序员几乎完全依赖向终端打印消息进行调试。即使在今天的一些嵌入式环境中,由于内存或CPU资源有限,打印消息仍然是唯一可行的调试方法。然而,使用printf进行调试也存在一些问题。

2.1 使用printf的问题
  • 性能影响 :根据使用的输出设备,printf语句会影响代码性能。例如,将代码输出到X终端时,由于伪终端的缓冲,实际数据滚动显示的时间可能比程序运行时间长。将相同的命令重定向到 /dev/null 会运行得快得多,这表明任何形式的文本输出都会影响性能。
$ time od -v /dev/zero -N200000
real    0m1.257s
user    0m0.052s
sys     0m0.092s

$ time od -v /dev/zero -N200000 > /dev/null
real    0m0.059s
user    0m0.048s
sys     0m0.012s
  • 同步问题 :当程序输出定向到屏幕时,通常认为printf语句是同步的,但当输出重定向到其他设备时,情况会发生变化。例如,以下代码:
for (i = 0; i < 3; i++) {
    printf("Hello World\n");
    sleep(1);
}

直接运行时,每秒会打印一次 Hello World ,但使用 tee 命令将输出保存到文件时,会在3秒后一次性输出三行 Hello World 。这是因为C标准库使用基于普通文件描述符的流,根据输出是否为终端使用不同的缓冲策略。当输出是终端时,使用行缓冲,遇到换行符时才将字符写入设备;当输出不是终端时,字符直到缓冲区满或程序显式调用 fflush 才会发送到设备。

  • 缓冲与C文件流 :为了提高效率,通常以字符块的形式将字符发送到设备,用户空间缓冲区允许驱动程序以块的形式将字符发送到输出设备。C文件流允许通过 setvbuf 函数选择三种基本的缓冲策略:
int setvbuf(FILE *stream, char *buf, int mode, size_t size);

mode 参数可以取以下值:
| 模式 | 说明 |
| ---- | ---- |
| _IONBF | 无缓冲,字符一次写入一个 |
| _IOLBF | 行缓冲,字符缓冲到第一个换行符 |
| _IOFBF | 全缓冲,字符缓冲到缓冲区满 |

标准输出 stdout 在连接到终端时默认使用行缓冲,重定向到文件或管道时变为全缓冲。如果想强制输出同步,可以使用 setvbuf 将缓冲模式改回行缓冲或使用 fflush 手动刷新缓冲区,但这都会使代码运行变慢,因为会增加系统调用次数。

  • 缓冲与文件系统 :除了C库提供的用户空间缓冲区,文件系统在内核中也维护缓冲区。刷新用户空间缓冲区的 fflush 不会刷新文件系统缓冲区,数据直到系统需要写入或应用程序调用 fsync 才会写入磁盘。当从不同计算机查看文件时,文件系统缓冲可能会成为问题,例如媒体位于远程NFS服务器上时,看到的文件内容可能不是最新的。可以使用 fsync fdatasync 函数强制更新文件系统:
int fsync(int fd);
int fdatasync(int fd);

这两个函数会将用户数据从文件系统缓存写入设备,并阻塞调用者直到设备驱动程序表明数据已写入。

下面是I/O缓冲的流程图:

graph LR
    classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px
    classDef buffer fill:#FFF6CC,stroke:#FFBC52,stroke-width:2px
    classDef disk fill:#FFEBEB,stroke:#E68994,stroke-width:2px

    A(Process A):::process -->|fwrite| B(User Space Buffer):::buffer
    A -->|fflush| B
    B -->|write| C(Kernel Space):::buffer
    C -->|fsync| D(Disk):::disk

    E(Process B):::process -->|read| C
    E -->|fread| B

以上介绍了多处理器性能相关的知识以及使用printf进行调试时可能遇到的问题和解决方法。在后续部分,将继续探讨其他调试工具和技巧。

多处理器性能与Linux调试技巧

3. 调试工具的综合运用与其他调试技巧

在了解了多处理器性能以及 printf 调试的相关问题后,下面将进一步介绍一些常见调试工具的特点和使用方法,以及一些在其他调试技术失效时可用的非常规技巧。

3.1 常见调试工具
工具名称 用途
gprof gcov 用于在源代码级别帮助优化代码
OProfile 强大的系统工具,可用于帮助优化应用程序
strace ltrace 以最小的侵入性监控代码行为
time top vmstat iostat mpstat 用于识别内存问题和系统吞吐量问题

下面详细介绍其中部分工具的应用场景:
- gprof gcov :这两个工具主要针对代码优化。 gprof 可以分析程序运行时各个函数的调用频率和执行时间,帮助找出性能瓶颈函数。使用时,需要在编译程序时加上 -pg 选项,程序运行结束后会生成一个 gmon.out 文件,然后使用 gprof 命令分析该文件即可。 gcov 则侧重于代码覆盖率分析,通过在编译时加上 -fprofile-arcs -ftest-coverage 选项,程序运行后会生成 .gcda .gcno 文件,使用 gcov 命令分析这些文件就能得到代码的覆盖情况。
- OProfile :这是一个功能强大的系统级性能分析工具。它可以收集整个系统的性能数据,包括CPU使用率、函数调用关系等。使用时,首先需要启动 opcontrol 守护进程,然后使用 opcontrol --start 开始收集数据,程序运行结束后使用 opcontrol --stop 停止收集,最后使用 opreport 命令查看分析结果。

3.2 gdb 调试器深入解析

gdb 是一款功能强大的调试器,虽然有一些优秀的GUI界面增强了其功能,但文本界面仍然非常强大且功能丰富。以下通过示例详细介绍 gdb 的一些常用功能:

// 示例代码
#include <stdio.h>

int add(int a, int b) {
    return a + b;
}

int main() {
    int x = 5;
    int y = 3;
    int result = add(x, y);
    printf("The result is: %d\n", result);
    return 0;
}
  • 启动 gdb :首先使用 -g 选项编译代码,然后使用 gdb 命令启动调试器:
$ gcc -g -o test test.c
$ gdb test
  • 设置断点 :可以在函数名或行号处设置断点,例如在 add 函数处设置断点:
(gdb) break add
  • 运行程序 :使用 run 命令开始运行程序,程序会在断点处停止:
(gdb) run
  • 查看变量值 :使用 print 命令查看变量的值,例如查看 x 的值:
(gdb) print x
  • 单步执行 :使用 next 命令单步执行代码,不进入函数内部;使用 step 命令单步执行代码,会进入函数内部:
(gdb) next
(gdb) step
3.3 内存检查工具比较

在调试过程中,内存问题是常见且难以排查的问题之一。以下是几种常见的内存检查工具的比较:
| 工具名称 | 特点 | 局限性 |
| ---- | ---- | ---- |
| Valgrind | 功能强大,可检测内存泄漏、越界访问等多种内存问题 | 运行速度慢,对系统资源要求较高 |
| AddressSanitizer | 速度快,能快速定位内存错误 | 可能会产生一些误报 |
| Electric Fence | 简单易用,能检测堆内存越界访问 | 只能检测堆内存问题,功能相对单一 |

使用 Valgrind 检测内存泄漏的示例:

$ valgrind --leak-check=full ./test
3.4 非常规调试技巧

当其他调试技术都失效时,可以尝试以下非常规技巧:
- 日志记录 :在代码中添加详细的日志记录,记录关键变量的值和程序执行的流程。可以使用不同的日志级别,如 DEBUG INFO WARN ERROR 等,方便在不同情况下查看日志。
- 二分查找法 :对于复杂的程序,可以采用二分查找法逐步缩小问题范围。例如,将程序的一部分代码注释掉,观察问题是否仍然存在,逐步定位问题所在。
- 模拟环境调试 :如果问题只在特定环境下出现,可以尝试在本地模拟该环境进行调试。例如,模拟网络延迟、高负载等情况,观察程序的行为。

4. 总结

本文围绕多处理器性能和Linux调试技巧展开了全面的介绍。在多处理器方面:
- 多核CPU的发展使得计算性能得到了显著提升,但共享缓存等特性也带来了新的问题,需要根据应用场景合理利用。
- 通过进程和线程的亲和性设置,可以优化多CPU系统上的程序性能,但要谨慎使用,避免硬编码带来的兼容性问题。

在调试技巧方面:
- printf 调试虽然简单直接,但存在性能影响和同步问题,需要合理使用缓冲策略来解决。
- 多种调试工具如 gprof OProfile gdb 等各有其特点和适用场景,应根据具体问题选择合适的工具。
- 内存检查工具可以帮助检测内存问题,而非常规调试技巧则在其他方法失效时提供了新的思路。

通过合理运用这些知识和工具,能够提高程序的性能和稳定性,更高效地解决开发过程中遇到的问题。

下面是一个总结多处理器和调试相关内容的流程图:

graph LR
    classDef topic fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px
    classDef subtopic fill:#FFF6CC,stroke:#FFBC52,stroke-width:2px

    A(多处理器与调试):::topic --> B(多处理器性能):::subtopic
    A --> C(调试技巧):::subtopic
    B --> B1(多核CPU特性):::subtopic
    B --> B2(进程与线程亲和性):::subtopic
    C --> C1(基本调试工具 - printf):::subtopic
    C --> C2(常见调试工具):::subtopic
    C --> C3(内存检查工具):::subtopic
    C --> C4(非常规调试技巧):::subtopic
(Kriging_NSGA2)克里金模型结合多目标遗传算法求最优因变量及对应的最佳自变量组合研究(Matlab代码实现)内容概要:本文介绍了克里金模型(Kriging)多目标遗传算法NSGA-II相结合的方法,用于求解最优因变量及其对应的最佳自变量组合,并提供了完整的Matlab代码实现。该方法首先利用克里金模型构建高精度的代理模型,逼近复杂的非线性系统响应,减少计算成本;随后结合NSGA-II算法进行多目标优化,搜索帕累托前沿解集,从而获得多个最优折衷方案。文中详细阐述了代理模型构建、算法集成流程及参数设置,适用于工程设计、参数反演等复杂优化问题。此外,文档还展示了该方法在SCI一区论文中的复现应用,体现了其科学性实用性。; 适合人群:具备一定Matlab编程基础,熟悉优化算法和数值建模的研究生、科研人员及工程技术人员,尤其适合从事仿真优化、实验设计、代理模型研究的相关领域工作者。; 使用场景及目标:①解决高计算成本的多目标优化问题,通过代理模型降低仿真次数;②在无法解析求导或函数高度非线性的情况下寻找最优变量组合;③复现SCI高水平论文中的优化方法,提升科研可信度效率;④应用于工程设计、能源系统调度、智能制造等需参数优化的实际场景。; 阅读建议:建议读者结合提供的Matlab代码逐段理解算法实现过程,重点关注克里金模型的构建步骤NSGA-II的集成方式,建议自行调整测试函数或实际案例验证算法性能,并配合YALMIP等工具包扩展优化求解能力。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值