GDB 源码分析系列文章四:gdb 事件处理异步模式分析 ---- 以 ctrl-c 信号为例

系列文章:

GDB 源码分析系列文章一:ptrace 系统调用和事件循环(Event Loop)
GDB 源码分析系列文章二:gdb 主流程 Event Loop 事件处理逻辑详解
GDB 源码分析系列文章三:调试信息的处理、符号表的创建和使用
GDB 源码分析系列文章四:gdb 事件处理异步模式分析 ---- 以 ctrl-c 信号为例
GDB 源码分析系列文章五:动态库延迟断点实现机制

GDB 源码分析系列文章四:gdb 事件处理异步模式分析 ---- 以 ctrl-c 信号为例

gdb 处理的事件主要包括用户事件和目标程序事件。事件的处理可以分为同步模式和异步模式。本文介绍 gdb 事件处理的异步模式,并以 gdb 调试过程中 ctrl-c 信号为例展开介绍,其他信号事件的处理也是类似的。

gdb 调试目标程序时,如果目标程序正在运行,此时你输入 ctrl-c 信号,gdb 将暂停目标程序。本文将结合 gdb 源码分析一下 gdb 如何处理 ctrl-c 信号。

事件循环机制回顾

在前面的文章中,我们比较详细地介绍了 gdb 的事件循环机制,这里我们做下简单的前情回顾。

gdb 在完成初始化后,即进入事件循环(Event Loop)中。对于 gdb 而言,事件主要包括用户输入事件和目标程序事件。目标程序状态发生变化时(例如击中断点),目标程序会将自己暂停,并向 gdb 发送 SIGCHLD 信号,gdb 再收到 SIGCHLD 信号后,向 Event Loop 事件源 pipe 写入目标程序事件,Event Loop 通过 poll / select 读取该事件,即进入相应的事件处理。比如如果当前断点是条件断点,则判断条件是否满足,不满足则让目标程序继续执行,如果满足则进行相关处理,最后写入 stdin(用户输入) 事件。Event Loop 的 poll /select 检查到 stdin 事件后则进入等待用户命令状态,接收到用户命令则继续相应的处理。

异步处理模式分析

我们以 gdb 调试多线程程序为例,分析 gdb 如何处理 ctrl-c 信号。

多线程程序

下面是一个简单的多线程程序。

#include <stdio.h>
#include <unistd.h> //sleep
#include <pthread.h>

void* func_1() {
  while(1) {
      sleep(1);
      printf("========== thread 1 \n");
  }
  return NULL;
}

void* func_2() {
  while(1) {
      sleep(1);
      printf("========== thread 2 \n");
  }
  return NULL;
}

int main() {
    pthread_t tid_1;
    pthread_t tid_2;

    pthread_create(&tid_1, NULL, func_1, NULL);
    pthread_create(&tid_2, NULL, func_2, NULL);
    
    while(1) {
      sleep(1);
      printf("========== thread 0 \n");
    }

    pthread_join(tid_1, NULL);
    pthread_join(tid_2, NULL);

    return 0;
}

程序中创建了两个线程,加上进程的主线程,该进程共有三个线程。linux 没有真正的线程,线程是由进程模拟,称作为轻量级进程(lwp)。通过 ps 命令可以查看,本程序的 pid 和 lwpid:

UID        PID  PPID   LWP  C NLWP STIME TTY      STAT   TIME CMD
loongkn+    44    11    44  0    3 18:19 tty1     Sl     0:00 ./a.out
loongkn+    44    11    45  0    3 18:19 tty1     Sl     0:00 ./a.out
loongkn+    44    11    46  0    3 18:19 tty1     Sl     0:00 ./a.out

其中,主线程的 lwpid 和 pid 的编号一样。

基本流程简介

对于异步模式,gdb 始终准备好处理用户输入事件和目标程序事件。因而不会使用阻塞式的 waitpid 等待目标子进程状态变化。只要发生目标程序事件就异步地通知 gdb 的 Event Loop,并且通过对SIGCHLD 信号处理来监控目标程序事件。

对于上面程序,gdb 调试过程中,收到 ctrl-c 信号后主要流程如下:

  • 主线程收到 ctrl-c 信号后将自己停止住,并向 gdb 发送 SIGCHLD 信号。
  • gdb 进程接收到 SIGCHLD 信号后,sigchld_handler 信号处理函数向 pipe 源写入 target event。
  • gdb 的 Event Loop 通过 poll/select 接收到 target event,进入目标程序事件处理。
  • gdb 首先去查询子进程的状态变化,可以得到子进程(也即inferior)中主线程状态处于 stopped ,并且信号为 ctrl-c 信号(也即SIGINT),接着 gdb 将主动去将 inferior 的其他线程停止,并进入用户输入状态等待用户命令。

源码结构和分析

SIGCHLD 信号处理

gdb 通过信号接收机制,分别注册相应的处理函数,对于子进程的 SIGCHLD 信号,注册 sigchld_handler 函数。

sigchld_handler
async_file_mark

async_file_mark 函数中,向 linux_nat_event_pipe 写入数据。

Event Loop 处理

gdb Event Loop 接收 SIGCHLD 信号处理如下:

gdb_do_one_event
gdb_wait_for_event
    fetch_inferior_event
      do_target_wait
        linux_nat_wait
          linux_nat_wait_1
            waitpid
            linux_nat_filter_event
      handle_inferior_event
       handle_signal_stop
          stop_waiting
           stop_all_threads
             target_stop
      normal_stop

gdb_do_one_event 采用 round-robin 的方式公平处理各种事件源。

gdb_wait_for_event 使用 poll 或 select(取决于操作系统的支持)接口监控事件源,poll 或 select 可以是阻塞式的也可以是非阻塞式的。poll 或 select 捕获到事件源,即进入相应的事件处理。这里,gdb 监控到 pipe 事件源变化,表明发生目标程序事件,接着进入到 fetch_inferior_event 的处理。

fetch_inferior_event 中首先通过 do_target_wait —> linux_nat_wait —> linux_nat_wait_1 —> waitpid 和 linux_nat_filter_event 非阻塞式地等待 inferior 子进程的状态发生变化。在我们的这个例子中 waitpid 将等到 inferior 的主线程状态变成 stopped,并且导致主线程 stopped 的信号是 ctrl-c 信号(也即 SIGINT)。一般 ctrl-c 信号会发送给整个进程(即 inferior),也即该进程的所有线程(all lwps)都会接收到 ctrl-c 信号。除非这些线程采用 CLONE_THREAD 方式共享信号,这个时候只有一个线程会接收到 ctrl-c 信号(一般是主线程)。不管是哪种情况,gdb 只会上报一个线程的 ctrl-c 信号。我们的这个例子中,只有主线程会接收到 ctrl-c 信号,此时主线程处于 stopped 模式。

这里介绍一下 gdb 多线程调试的两个概念:all-stop 和 non-stop 模式。所谓 all-stop 模式,也即当发生一个线程 stopped时,gdb 会控制其他线程也 stopped。反之,non-stop 模式即一个线程 stopped,其他线程继续原来状态。gdb 默认是 all-stop 模式。另外还有一个概念 target_is_non_stop,举个例子,比如我们在一个线程的函数中打断点,我们的硬件是只将该线程 stopped 还是将所有线程都 stopped,如果只是将该线程 stopped,则 target_is_non_stop 为 true,否则为 false。因而 target_is_non_stop 是硬件的行为,而 all-stop 和 non-stop 是用户期望的调试模式。

fetch_inferior_event 在接收到主线程的 ctrl-c 信号后,进入到 ctrl-c 信号的事件处理,即 handle_inferior_event,handle_inferior_event 判断此信号是导致线程 stopped 的事件,随即进入 handle_signal_stop 和 stop_waiting 的处理。stop_waiting 判断是调试模式为 non-stop 模式,并且 target_is_non_stop 为 true,则会进入 stop_all_threads 流程。

stop_all_threads 会死循环处理所有线程,直到所有线程都 stopped。stop_all_threads 遍历所有线程,若当前线程不处于 stopped 状态,将通过 target_stop 接口去强行 stop 线程,target_stop 的具体实现因硬件不同而异。

fetch_inferior_event 接着进入 normal_stop 流程,对于 ctrl-c 信号的 stopped 事件,gdb 会输出Program received signal SIGINT, Interrupt. 的信息,并且做一些收尾工作,最后会插入用户输入事件(stdin),随机 Event Loop 进入到等待用户输入状态,用户可以选择 continue 或 quit。

至此,gdb 事件处理的异步模式就介绍到这里,更多 gdb 的源码分析尽情期待。

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值