28、调试多任务与Linux移植指南

调试多任务与Linux移植指南

1. 调试多任务

在处理多线程执行时,开发者通常会遇到两种不同的调试场景。进程可以拥有自己独立的地址空间,也可以与其他执行线程共享地址空间(以及其他系统资源)。对于不共享公共地址空间的独立进程,必须使用单独的独立调试会话进行调试。你可以在目标系统的多个进程上使用gdbserver,并在开发主机上分别调用GDB,以协调多个协作但独立的进程的调试会话。

1.1 调试多个进程

当在GDB下调试的进程使用 fork() 系统调用创建新进程时,GDB有两种操作方式。它可以继续控制和调试父进程,也可以停止调试父进程并附加到新创建的子进程。你可以使用 set follow-fork-mode 命令来控制这种行为,该命令有两种模式: follow parent follow child ,默认情况下GDB会跟随父进程。在这种情况下,子进程在 fork() 成功后立即执行。

以下是一个简单的程序片段,它从 main() 例程中创建多个进程:

for( i=0; i<MAX_PROCESSES; i++ ) {
    /* Creating child process */
    pid[i] = fork();                /* Parent gets non-zero PID */
    if ( pid[i] == -1 ) {
        perror("fork failed");
        exit(1);
    }
    if ( pid[i] == 0 ) {      /* Indicates child's code path */
       worker_process();      /* The forked process calls this */
    }
}
/* Parent's main control loop */
while ( 1 ) {
    ...
}

这个简单的循环使用 fork() 系统调用创建 MAX_THREADS 个新进程。每个新创建的进程都会执行 worker_process() 函数定义的代码体。在默认模式下在GDB中运行时,GDB会检测到新执行线程(进程)的创建,但仍会附加到父进程的执行线程上。

以下是GDB在 follow-fork-mode = parent 模式下的会话示例:

(gdb) target remote 192.168.1.141:2001
0x40000790 in ?? ()
(gdb)  b main
Breakpoint 1 at 0x8888: file forker.c, line 104.
(gdb)  c
Continuing.
[New Thread 356]
[Switching to Thread 356]
Breakpoint 1, main (argc=0x1, argv=0xbe807dd4) at forker.c:104
104       time(&start_time);
(gdb)  b worker_process
Breakpoint 2 at 0x8784: file forker.c, line 45.
(gdb)  c
Continuing.
Detaching after fork from child process 357.
Detaching after fork from child process 358.
Detaching after fork from child process 359.
Detaching after fork from child process 360.
Detaching after fork from child process 361.
Detaching after fork from child process 362.
Detaching after fork from child process 363.
Detaching after fork from child process 364.

这里创建了8个子进程,PID值从357到364,父进程的PID为356。当在 main() 中遇到断点时,我们在 worker_process() 例程中设置了断点,但由于GDB附加到主进程,而主进程从不执行 worker_process() 例程,所以子进程不会命中该断点。

如果你需要调试每个进程,必须执行单独的独立GDB会话,并在子进程 fork() 后附加到该子进程。如果你只需要跟随子进程,在父进程执行 fork() 系统调用之前,将 follow-fork-mode 设置为 follow child 。以下是GDB在 follow-fork-mode = child 模式下的会话示例:

(gdb) target remote 192.168.1.141:2001
0x40000790 in ?? ()
(gdb) set follow-fork-mode child
(gdb)  b worker_process
Breakpoint 1 at 0x8784: file forker.c, line 45.
(gdb)  c
Continuing.
[New Thread 401]
Attaching after fork to child process 402.
[New Thread 402]
[Switching to Thread 402]
Breakpoint 1, worker_process () at forker.c:45
45         int my_pid = getpid();
(gdb)  c
Continuing.

这里父进程的PID为401,当第一个子进程被创建时,GDB会从父进程的执行线程中静默分离,并附加到PID为402的新子进程上。GDB现在控制第一个子进程,并会命中 worker_process() 处设置的断点。但需要注意的是,其他子进程不会被调试,它们会继续运行直到完成。

综上所述,以这种方式使用GDB时,你一次只能调试一个进程。你可以通过 fork() 系统调用进行调试,但必须决定在 fork() 调用时跟随哪个执行线程,即父进程或子进程。如果你需要同时调试多个协作进程,可以使用多个独立的GDB会话。

1.2 调试多线程应用程序

如果你的应用程序使用POSIX线程库进行线程操作,GDB具有额外的功能来处理多线程应用程序的并发调试。Native Posix Thread Library (NPTL)已成为Linux系统(包括嵌入式Linux系统)中事实上的标准线程库。以下讨论假设你使用的是这个线程库。

我们使用一个演示程序,它在一个简单的循环中使用 pthread_create() 库函数创建多个线程。线程创建后, main() 例程只需等待键盘输入以终止应用程序。每个线程在屏幕上显示一条短消息,并休眠一段预定的时间。

以下是目标板上的启动序列:

root@coyote:/workspace # gdbserver localhost:2001 ./tdemo
Process ./tdemo created; pid = 671
Listening on port 2001
Remote debugging from host 192.168.1.10
tdemo main() entered: My pid is 671
Starting worker thread 0
Starting worker thread 1
Starting worker thread 2
Starting worker thread 3

和之前的例子一样,gdbserver为应用程序的运行做好准备,并等待来自主机的cross-gdb的连接。当GDB连接时,gdbserver会报告连接信息。

以下是主机GDB连接到目标线程演示的会话:

$ xscale_be-gdb -q tdemo
(gdb) target remote 192.168.1.141:2001
0x40000790 in ?? ()
(gdb)   b tdemo.c:97
Breakpoint 1 at 0x88ec: file tdemo.c, line 97.
(gdb)   c
Continuing.
[New Thread 1059]
[New Thread 1060]
[New Thread 1061]
[New Thread 1062]
[New Thread 1063]
[Switching to Thread 1059]
Breakpoint 1, main (argc=0x1, argv=0xbefffdd4) at tdemo.c:98
98               int c = getchar();

这里我们连接到目标,在创建新线程的循环之后设置一个断点并继续执行。当新线程创建时,GDB会显示通知和线程ID。线程1059是TDemo应用程序,直接从 main() 函数执行工作。线程1060到1063是通过 pthread_create() 调用创建的新线程。

当GDB命中断点时,它会显示 [Switching to Thread 1059] 消息,表明这是遇到断点的执行线程,它是调试会话的活动线程,在GDB文档中称为当前线程。

GDB允许我们在不同线程之间切换,并执行常见的调试操作,如设置额外的断点、检查数据、显示回溯信息以及处理当前线程内的各个栈帧。以下是这些操作的示例:

(gdb) c
Continuing.
                  <<< Ctl-C to interrupt program execution
Program received signal SIGINT, Interrupt.
0x400db9c0 in read () from /opt/mvl/.../lib/tls/libc.so.6
(gdb)  i threads
  5 Thread 1063  0x400bc714 in nanosleep ()
   from /opt/mvl/.../lib/tls/libc.so.6
  4 Thread 1062  0x400bc714 in nanosleep ()
   from /opt/mvl/.../lib/tls/libc.so.6
  3 Thread 1061  0x400bc714 in nanosleep ()
   from /opt/mvl/.../lib/tls/libc.so.6
  2 Thread 1060  0x400bc714 in nanosleep ()
   from /opt/mvl/.../lib/tls/libc.so.6
* 1 Thread 1059  0x400db9c0 in read ()
   from /opt/mvl/.../lib/tls/libc.so.6
(gdb) thread 4               <<< Make Thread 4 the current thread
[Switching to thread 4 (Thread 1062)]
#0  0x400bc714 in nanosleep ()
   from /opt/mvl/.../lib/tls/libc.so.6
(gdb)  bt
#0  0x400bc714 in nanosleep ()
   from /opt/mvl/.../lib/tls/libc.so.6
#1  0x400bc4a4 in __sleep (seconds=0x0) at sleep.c:137
#2  0x00008678 in go_to_sleep (duration=0x5) at tdemo.c:18
#3  0x00008710 in worker_2_job (random=0x5) at tdemo.c:36
#4  0x00008814 in worker_thread (threadargs=0x2) at tdemo.c:67
#5  0x40025244 in start_thread (arg=0xfffffdfc) at pthread_create.c:261
#6  0x400e8fa0 in clone () at../sysdeps/unix/sysv/linux/arm/clone.S:82
#7  0x400e8fa0 in clone () at../sysdeps/unix/sysv/linux/arm/clone.S:82
(gdb)   frame 3
#3  0x00008710 in worker_2_job (random=0x5) at tdemo.c:36
36          go_to_sleep(random);
(gdb)  l                    <<< Generate listing of where we are
31      }
32
33      static void worker_2_job(int random)
34      {
35          printf("t2 sleeping for %d\n", random);
36          go_to_sleep(random);
37      }
38
39      static void worker_3_job(int random)
40      {

需要注意的是,GDB会为每个线程分配一个整数值,并使用这些值来引用各个线程。当一个线程命中断点时,进程内的所有线程都会暂停以便检查。GDB会用星号 (*) 标记当前线程。你可以在每个线程中设置唯一的断点,但前提是它们存在于唯一的上下文中。如果你在所有线程都会执行的公共代码部分设置断点,那么首先命中该断点的线程是不确定的。

1.3 调试引导加载程序/闪存代码

调试闪存驻留代码有其独特的挑战。最明显的限制是GDB和gdbserver在设置目标断点时的协作方式。GDB通常会用特定于架构的操作码替换断点位置的操作码,以将控制权传递给调试器。但在ROM或闪存中,GDB无法覆盖操作码,因此这种设置断点的方法无效。

大多数现代处理器包含一些调试寄存器,可以用来克服这个限制。这些功能必须由特定于架构和处理器的硬件探针或桩程序支持。调试闪存和ROM驻留代码最常用的技术是使用JTAG硬件探针,这些探针支持设置特定于处理器的硬件断点。

1.4 额外的远程调试选项
1.4.1 通过串口调试

通过串口调试非常简单。当然,你需要在目标系统上有一个未被其他进程使用的串口,如串口控制台。主机也需要有可用的串口。如果满足这两个条件,只需将传递给gdbserver的IP:端口规范替换为串口规范。在从主机的GDB连接到目标时,使用相同的技术。

在目标系统上:

root@coyote:/workspace # gdbserver /dev/ttyS0 ./tdemo
Process ./tdemo created; pid = 698
Remote debugging using /dev/ttyS0

在主机上:

$ xscale_be-gdb -q tdemo
(gdb) target remote /dev/ttyS1
Remote debugging using /dev/ttyS1
0x40000790 in ?? ()
1.4.2 附加到正在运行的进程

连接到正在运行的进程以检查其状态通常比杀死进程并重新启动更有利。使用gdbserver,这很容易实现:

root@coyote:/workspace # ps ax | grep tdemo
 1030 pts/0    Sl+    0:00 ./tdemo
root@coyote:/workspace # gdbserver localhost:2001 --attach 1030
Attached; pid = 1030
Listening on port 2001

当你完成对被调试进程的检查后,可以发出 gdb detach 命令。这会将gdbserver从目标应用程序中分离,并终止调试会话。应用程序会从暂停的地方继续运行。需要注意的是,当你附加到进程时,它会暂停,等待你的指令。直到你使用 continue 命令或 detach 命令,它才会恢复执行。此外,你几乎可以在任何时候使用 detach 命令结束调试会话,并让应用程序在目标系统上继续运行。

2. Linux源组织

曾经,不同版本的Linux有多个存放位置,例如PowerPC版本的Linux有专门的存放处,ARM版本也是如此。这并非设计使然,而是必要之举。将各种架构的基础设施和功能合并到主线内核需要时间,拥有单独的源代码树意味着可以更快地访问给定架构的最新功能。

如今,内核开发者已尽力统一Linux内核源代码,将不同的架构整合到一个公共的源代码树中。对于Linux 2.6源代码,除了少数例外情况,现在可以直接从www.kernel.org下载并编译适用于各种处理器和行业标准参考板的可用内核。

2.1 架构分支

Linux内核源代码树的 .../arch 子目录在大小上是第二大的,在最近的Linux版本中,除了 .../include 目录外,它的文件数量最多。只有 .../drivers 子目录的大小更大。

以下是 .../arch 目录的内容列表:

[chris@pluto linux]$ ls ./arch
alpha  cris   i386  m68k       parisc  s390  sparc    v850
arm    frv    ia64  m68knommu  ppc     sh    sparc64  x86_64
arm26  h8300  m32r  mips       ppc64   sh64  um       xtensa

从这个列表中可以看出,Linux内核支持24种不同的架构,我们将每个架构称为一个架构分支。

每个架构分支都有一些共同的组件。例如,每个顶级架构分支都包含一个 Kconfig 文件,它驱动内核配置工具。当然,每个顶级架构分支也都有相应的 makefile 。所有顶级架构都包含一个 kernel 子目录,因为许多内核功能依赖于架构。除了两个架构外,其他架构都包含一个 mm 子目录,这里存放着依赖于架构的内存管理代码。

许多顶级架构分支包含一个 boot 子目录,用于通过其自己的 makefile 构建特定于该架构的可引导目标。许多分支还包含 mach-* 子目录,用于存放特定机器或硬件平台的代码。另一个经常出现在架构分支中的子目录是 configs ,它存在于许多较流行的架构中,包含每个受支持硬件平台的默认配置。

接下来我们以PowerPC架构为例,它是最流行的架构之一,支持许多处理器和开发板。以下是 .../arch/ppc PowerPC分支的 configs 目录的内容列表:

[chris@pluto linux]$ ls ./arch/ppc/configs/
ads8272_defconfig   IVMS8_defconfig        prpmc750_defconfig
apus_defconfig      katana_defconfig       prpmc800_defconfig
bamboo_defconfig    lite5200_defconfig     radstone_defconfig
bseip_defconfig     lopec_defconfig        redwood5_defconfig
bubinga_defconfig   luan_defconfig         redwood6_defconfig
chestnut_defconfig  mbx_defconfig          rpx8260_defconfig
common_defconfig    mpc834x_sys_defconfig  rpxcllf_defconfig
cpci405_defconfig   mpc8540_ads_defconfig  rpxlite_defconfig
cpci690_defconfig   mpc8548_cds_defconfig  sandpoint_defconfig
ebony_defconfig     mpc8555_cds_defconfig  spruce_defconfig
ep405_defconfig     mpc8560_ads_defconfig  stx_gp3_defconfig
est8260_defconfig   mpc86x_ads_defconfig   sycamore_defconfig
ev64260_defconfig   mpc885ads_defconfig    TQM823L_defconfig
ev64360_defconfig   mvme5100_defconfig     TQM8260_defconfig
FADS_defconfig      ocotea_defconfig       TQM850L_defconfig
gemini_defconfig    pmac_defconfig         TQM860L_defconfig
hdpu_defconfig      power3_defconfig       walnut_defconfig
ibmchrp_defconfig   pplus_defconfig

PowerPC架构分支的 configs 目录中的每个条目都代表一个特定的硬件平台移植。例如, walnut_defconfig 定义了AMCC Walnut PPC405评估平台的默认配置, mpc8540_ads_defconfig 文件代表了飞思卡尔MPC8540 ADS评估板的默认配置。要为这些参考平台构建内核,首先需要使用这些配置默认值配置内核源代码树,如下所示:

$ make ARCH=ppc CROSS_COMPILE=ppc_85xx- mpc8540_ads_defconfig

这个 make 命令(从Linux顶级目录执行)会使用飞思卡尔MPC8540 ADS评估板的默认配置来配置内核源代码树。

Linux内核源代码树中尚未完全统一的一个方面是每个架构处理特定于平台的文件的方式。在PowerPC分支中,有一个 platforms 目录,包含特定于平台的代码。而ARM分支包含一系列 mach-* 目录,每个目录代表一个特定的硬件平台,MIPS分支则有一组以特定平台命名的子目录。

调试多任务与Linux移植指南

3. 移植Linux到新硬件平台

将Linux移植到新的硬件平台并非难事,Linux源代码树中包含了跨越20多种架构和更多处理器的众多开发板的移植代码。然而,知道从哪里开始往往是最困难的部分。下面将介绍将Linux移植到自定义开发板以支持基本以太网和串口控制台操作的基础知识。

3.1 了解早期内核初始化代码

要成功移植Linux,需要深入了解早期内核初始化代码,以掌握平台初始化的机制。虽然不同架构的具体实现有所差异,但总体上早期内核初始化会完成以下几个关键步骤:
1. 硬件初始化 :对处理器、内存、外设等硬件进行初始化设置。
2. 内存管理初始化 :建立内存映射,初始化页表等。
3. 中断系统初始化 :配置中断控制器,设置中断处理程序。
4. 设备驱动初始化 :加载并初始化各种设备驱动。

下面是一个简化的早期内核初始化流程的mermaid流程图:

graph TD;
    A[硬件初始化] --> B[内存管理初始化];
    B --> C[中断系统初始化];
    C --> D[设备驱动初始化];
3.2 以PowerPC平台为例的移植过程

接下来以PowerPC架构为例,介绍一个典型的移植到自定义PowerPC硬件平台的过程。

  1. 选择合适的默认配置
    从前面介绍的PowerPC架构分支的 configs 目录中选择与目标硬件最接近的默认配置文件。例如,如果目标硬件与Freescale MPC8540 ADS评估板类似,可以选择 mpc8540_ads_defconfig
    plaintext $ make ARCH=ppc CROSS_COMPILE=ppc_85xx- mpc8540_ads_defconfig

  2. 修改平台特定代码
    在PowerPC分支的 platforms 目录中,找到与目标硬件相关的源文件,如果没有则创建新的文件。根据目标硬件的具体情况,修改或添加以下内容:

    • 硬件初始化代码 :如设置处理器的时钟频率、初始化内存控制器等。
    • 设备驱动代码 :如果目标硬件有特殊的外设,需要编写或修改相应的设备驱动。
  3. 调试与测试
    编译修改后的内核代码,并将其烧录到目标硬件上进行测试。在调试过程中,可以使用前面介绍的调试方法,如通过GDB进行调试。

4. 总结

通过以上内容,我们了解了调试多任务的不同场景和方法,以及Linux源代码的组织方式和将Linux移植到新硬件平台的基本步骤。

4.1 调试多任务总结
  • 调试多个进程 :使用GDB调试多个进程时,可通过 set follow-fork-mode 命令选择跟随父进程或子进程,一次只能调试一个进程,若需同时调试多个协作进程,可使用多个独立的GDB会话。
  • 调试多线程应用程序 :对于使用POSIX线程库的多线程应用程序,GDB可进行并发调试,能在不同线程之间切换并执行常见的调试操作。
  • 调试引导加载程序/闪存代码 :调试闪存驻留代码可使用JTAG硬件探针设置硬件断点。
  • 额外的远程调试选项 :可通过串口进行调试,也可将GDB附加到正在运行的进程进行状态检查。
4.2 Linux移植总结
  • Linux源组织 :Linux内核源代码树通过 .../arch 目录支持多种架构,每个架构分支有共同组件和特定的子目录,不同架构处理平台特定文件的方式有所不同。
  • 移植过程 :移植Linux到新硬件平台需了解早期内核初始化代码,选择合适的默认配置,修改平台特定代码,并进行调试和测试。

以下是一个总结表格:
| 调试场景 | 方法 |
| ---- | ---- |
| 调试多个进程 | 用 set follow-fork-mode 选择跟随父/子进程,多进程调试用多GDB会话 |
| 调试多线程应用程序 | GDB支持并发调试,可切换线程执行调试操作 |
| 调试引导加载程序/闪存代码 | 使用JTAG硬件探针设置硬件断点 |
| 额外远程调试选项 | 串口调试,附加到正在运行的进程 |

移植步骤 操作
了解早期内核初始化代码 掌握硬件、内存管理、中断系统和设备驱动初始化步骤
选择默认配置 configs 目录选合适的默认配置文件
修改平台特定代码 platforms 目录修改或添加硬件初始化和设备驱动代码
调试与测试 编译内核,烧录到目标硬件进行测试

通过掌握这些知识和技能,开发者可以更高效地进行多任务调试和Linux移植工作。

【电能质量扰动】基于ML和DWT的电能质量扰动分类方法研究(Matlab实现)内容概要:本文研究了一种基于机器学习(ML)和离散小波变换(DWT)的电能质量扰动分类方法,并提供了Matlab实现方案。首先利用DWT对电能质量信号进行多尺度分解,提取信号的时频域特征,有效捕捉电压暂降、暂升、中断、谐波、闪变等常见扰动的关键信息;随后结合机器学习分类器(如SVM、BP神经网络等)对提取的特征进行训练分类,实现对不同类型扰动的自动识别准确区分。该方法充分发挥DWT在信号去噪特征提取方面的优势,结合ML强大的模式识别能力,提升了分类精度鲁棒性,具有较强的实用价值。; 适合人群:电气工程、自动化、电力系统及其自动化等相关专业的研究生、科研人员及从事电能质量监测分析的工程技术人员;具备一定的信号处理基础和Matlab编程能力者更佳。; 使用场景及目标:①应用于智能电网中的电能质量在线监测系统,实现扰动类型的自动识别;②作为高校或科研机构在信号处理、模式识别、电力系统分析等课程的教学案例或科研实验平台;③目标是提高电能质量扰动分类的准确性效率,为后续的电能治理设备保护提供决策依据。; 阅读建议:建议读者结合Matlab代码深入理解DWT的实现过程特征提取步骤,重点关注小波基选择、分解层数设定及特征向量构造对分类性能的影响,并尝试对比不同机器学习模型的分类效果,以全面掌握该方法的核心技术要点。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值