Bootstrap

gdb 调试多进程中多线程的方法

  1. 示例代码
    首先,给出一个简单的示例程序,演示如何使用 fork 创建多个子进程并且每个进程内部创建多个线程。

示例代码 (main.cpp)

#include <iostream>
#include <thread>
#include <vector>
#include <unistd.h>
#include <sys/wait.h>
#include <cstdio>

using namespace std;

// 线程函数
void thread_function(int thread_id) {
    printf("Thread %d in process %d is running\n", thread_id, getpid());
    sleep(2);
}

// 创建多个线程
void create_threads(const int threads_num) {
    vector<thread> threads;
    for (int i = 0; i < threads_num; i++) {
        threads.emplace_back(thread_function, i);
    }
    for (auto& th : threads) {
        if (th.joinable()) {
            th.join();
        }
    }
}

int main() {
    const int num_processes = 8;
    const int num_threads = 8;
    vector<pid_t> child_pids;

    // 创建子进程
    printf("Started to create subprocesses\n");
    for (int i = 0; i < num_processes; i++) {
        pid_t pid = fork();
        if (pid == 0) { // 子进程
            create_threads(num_threads);
            exit(EXIT_SUCCESS);
        } else if (pid == -1) {
            perror("fork");
            exit(EXIT_FAILURE);
        } else { // 父进程记录 PID
            child_pids.push_back(pid);
        }
    }
    printf("End of subprocess creation\n");

    // 等待子进程结束
    for (pid_t pid : child_pids) {
        waitpid(pid, nullptr, 0);
    }

    cout << "All processes and threads completed." << endl;
    return 0;
}

代码说明:
主进程创建多个子进程:通过 fork() 创建 8 个子进程。 每个子进程创建多个线程:每个子进程中创建 8 个线程,通过 std::thread 启动线程并调用 thread_function。 进程和线程的输出:每个线程打印自己的 ID 和所属进程的 PID。
2. 编译程序
使用 g++ 编译程序时,确保添加 -g 选项以生成调试信息,并且链接线程库 -pthread。

Makefile

# 设置编译器
CXX = g++
# 设置编译选项
CXXFLAGS = -Wall
# 设置目标文件和输出可执行文件
TARGET = main
SRC = main.cpp

# Debug 和 Release 的编译选项
DEBUG_FLAGS = -g -O0
RELEASE_FLAGS = -O3

# 默认目标
all: debug release

# 编译 Debug 版本
debug: CXXFLAGS += $(DEBUG_FLAGS)
debug: $(TARGET)_debug

$(TARGET)_debug: $(SRC)
	$(CXX) $(CXXFLAGS) -o $@ $(SRC)

# 编译 Release 版本
release: CXXFLAGS += $(RELEASE_FLAGS)
release: $(TARGET)_release

$(TARGET)_release: $(SRC)
	$(CXX) $(CXXFLAGS) -o $@ $(SRC)

# 清理目标文件
clean:
	rm -f $(TARGET)_debug $(TARGET)_release *.o

gdb ./multi_process_threads
3.1. 调试时如何处理 fork 和多进程
当程序执行到 fork() 时,GDB 默认只跟踪父进程。为了在 fork() 时调试子进程,你需要告诉 GDB 在 fork() 后应该跟踪父进程还是子进程。

跟踪子进程:

在 GDB 中输入以下命令来让调试器跟踪子进程:

(gdb) set follow-fork-mode child

跟踪父进程:

如果你想调试父进程而忽略子进程,可以设置为跟踪父进程:

(gdb) set follow-fork-mode parent

3.2. 设置断点
你可以在程序的关键部分设置断点,例如:

在 fork() 处设置断点:

(gdb) break fork

在 thread_function() 函数内部设置断点:

(gdb) break thread_function

在 main() 函数的开头设置断点:

(gdb) break main

3.3. 查看和切换进程
在调试多进程程序时,GDB 允许你查看当前的所有进程以及切换到不同的进程进行调试。

查看所有进程:

使用 info inferiors 命令查看当前调试的所有进程:

(gdb) info inferiors

GDB 会显示当前的进程和子进程信息,包括进程的 ID。

切换到特定进程:

通过 inferior 命令切换到一个特定的进程。例如,切换到进程 2:

(gdb) inferior 2

你可以在切换到进程后使用 continue 来继续调试当前进程。

3.4. 查看和切换线程
在调试多线程程序时,GDB 允许你查看当前线程列表并切换到特定线程。

查看线程列表:

使用 info threads 查看当前进程中的所有线程:

(gdb) info threads

GDB 会列出所有线程的 ID 和当前执行的位置。

切换到特定线程:

通过 thread <thread_id> 命令切换到指定的线程。例如,切换到线程 2:

(gdb) thread 2

切换到目标线程后,你可以使用 next (n) 或 step (s) 命令逐步调试该线程。

3.5. 锁定线程调度
为了避免 GDB 在多线程调试时自动切换线程,你可以使用 set scheduler-locking 命令来控制线程调度。

锁定当前线程:设置 scheduler-locking on 后,GDB 只会调度当前选中的线程,其他线程会被暂停。

(gdb) set scheduler-locking on

恢复调度所有线程:

(gdb) set scheduler-locking off

3.6. 单步调试
使用 next (n) 或 step (s) 命令单步调试程序时,默认情况下 GDB 会调度其他线程。这时,可以使用 set scheduler-locking 锁定线程,确保只步进当前线程。

(gdb) set scheduler-locking on
(gdb) thread 2  # 切换到线程 2
(gdb) next      # 只步进当前线程
  1. 总结
    在调试 fork 创建的多进程和多线程程序时,GDB 提供了多种工具和命令,帮助我们有效地查看和切换进程与线程。通过使用以下技巧,可以帮助你在复杂的多进程和多线程调试环境中更加高效地工作:
  • 使用 set follow-fork-mode 来选择调试父进程或子进程。
  • 使用 info inferiors 来查看并切换进程。
  • 使用 info threads 和 thread 命令来查看并切换线程。
  • 使用 set scheduler-locking 来锁定当前线程的调度,避免其他线程的干扰。
    这些方法将帮助你更好地调试涉及多个进程和线程的程序,并能够精确控制调试过程。希望这个博客的框架和内容可以帮助你顺利写出详细的文章!如果你需要进一步的帮助或调整内容,请随时告诉我。
;