Gray 大佬已排查到关键之处:

As I mentioned, kernel forbids background processes to call tcsetattr(TCSADRAIN), so the following syscall for sure failed:

1
20:18:26 ioctl(3, TCGETS, ...}) = -1 EIO (Input/output error) <0.000007>

但但但是,我确实不知道内核会有如此行为,也不知道内核为什么要这样做。

那么,问题就变成:ioctl 系统调用为什么返回了 -EIO 呢?

将问题泛化一下,就是 Gray 大佬困惑多年的难题:如何排查系统调用返回错误的原因?

以这次 ioctl 系统调用返回 -EIO 为例,使用 bpfsnoop 排查其原因。

English version: python/cpython#ssues/135329#issuecomment-3235826338.

0. 实验环境信息

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
$ lsb_release -a
Distributor ID: Ubuntu
Description:    Ubuntu 24.04.1 LTS
Release:    24.04
Codename:   noble

$ uname -a
Linux HOSTNAME 6.6.0-47.XXX #47~24.04 SMP Fri Dec 20 16:05:43 +08 2024 x86_64 x86_64 x86_64 GNU/Linux

$ dmesg | grep -i lbr
[   13.116840] Performance Events: PEBS fmt3+, Skylake events, 32-deep LBR, full-width counters, Intel PMU driver.

$ apt info linux-image-$(uname -r)-dbgsym
Package: linux-image-6.6.0-47.XXX-dbgsym
Version: 6.6.0-47.XXX~24.04

其中:

  1. LBR: Intel CPU 提供的 Last Branch Record 功能,用于记录最近的分支信息。
  2. dbgsym: 内核调试符号包,用于 bpfsnoop 获取内核函数的源码行号信息。
  3. 内核版本: v5.17+ 才能支持 bpf_get_branch_snapshot() helper。

1. 确认系统调用结果

已知有问题的 Python 进程 PID 为 289473。

确认内核态 ioctl 系统调用的返回值:

ioctl return -5

-5 就是 -EIO:

1
2
$ errno -l
EIO 5 Input/output error

2. 获取 __x64_sys_ioctl 的 funcgraph

并不清楚 __x64_sys_ioctl 会调用哪些函数。

而 ftrace 的 function_graph tracer 可以帮助我们获取函数调用的图谱信息。

然而,更偏爱 bpfsnoop 的 funcgraph 功能:

__x64_sys_ioctl funcgraph

非常不幸,得到的 funcgraph 信息并没什么用处。这是因为 bpfsnoop funcgraph 功能无法跟踪间接调用的函数。

该如何突破这个限制呢?

3. 获取 __x64_sys_ioctl 的 LBR

Intel CPU 的 LBR 功能正是为此而存在:

__x64_sys_ioctl LBR

完美!

从中可以看出:tty_ioctl 正是藏在 __x64_sys_ioctl 背后的函数。

4. 获取 tty_ioctl 的 funcgraph

那么,具体是哪个函数返回了 -5 呢?

tty_ioctl funcgraph

关注每个函数的返回值,找到返回 -5 的那个:正是 tty_check_change。

5. 获取 tty_check_change 的 funcgraph 和 LBR

1
2
3
$ sudo ./bpfsnoop -k 'tty_check_change' --output-fgraph --filter-pid 289473 --limit-events 10
→ tty_check_change args=((struct tty_struct *)tty=0xffff888a9ea77000) cpu=20 process=(289473:python) timestamp=12:10:00.98960753
← tty_check_change args=((struct tty_struct *)tty=0xffff888a9ea77000) retval=(int)-5 cpu=20 process=(289473:python) duration=3.294µs timestamp=12:10:00.989620588

tty_check_change funcgraph&LBR

关注圈出来的那几条记录。

先看下 tty_check_change 的源代码 tty_jobctrl.c:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
  33   │ int __tty_check_change(struct tty_struct *tty, int sig)
  34   │ {
  35   │         unsigned long flags;
  36   │         struct pid *pgrp, *tty_pgrp;
  37   │         int ret = 0;
  38   │
  39   │         if (current->signal->tty != tty)
  40   │                 return 0;
  41   │
  42   │         rcu_read_lock();
  43   │         pgrp = task_pgrp(current);
  44   │
  45   │         spin_lock_irqsave(&tty->ctrl.lock, flags);
  46   │         tty_pgrp = tty->ctrl.pgrp;
  47   │         spin_unlock_irqrestore(&tty->ctrl.lock, flags);
  48   │
  49   │         if (tty_pgrp && pgrp != tty_pgrp) {
  50   │                 if (is_ignored(sig)) {
  51   │                         if (sig == SIGTTIN)
  52   │                                 ret = -EIO;
  53   │                 } else if (is_current_pgrp_orphaned())
  54   │                         ret = -EIO;
  55   │                 else {
  56   │                         kill_pgrp(pgrp, sig, 1);
  57   │                         set_thread_flag(TIF_SIGPENDING);
  58   │                         ret = -ERESTARTSYS;
  59   │                 }
  60   │         }
  61   │         rcu_read_unlock();
  62   │
  63   │         if (!tty_pgrp)
  64   │                 tty_warn(tty, "sig=%d, tty->pgrp == NULL!\n", sig);
  65   │
  66   │         return ret;
  67   │ }
  68   │
  69   │ int tty_check_change(struct tty_struct *tty)
  70   │ {
  71   │         return __tty_check_change(tty, SIGTTOU);
  72   │ }

LBR 记录直接明了地告知了 -EIO 的出处。

6. is_current_pgrp_orphaned

看下 is_current_pgrp_orphaned 的源代码 exit.c:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
 318   │ /*
 319   │  * Determine if a process group is "orphaned", according to the POSIX
 320   │  * definition in 2.2.2.52.  Orphaned process groups are not to be affected
 321   │  * by terminal-generated stop signals.  Newly orphaned process groups are
 322   │  * to receive a SIGHUP and a SIGCONT.
 323   │  *
 324   │  * "I ask you, have you ever known what it is to be an orphan?"
 325   │  */
 326   │ static int will_become_orphaned_pgrp(struct pid *pgrp,
 327   │                                      struct task_struct *ignored_task)
 328   │ {
 329   │         struct task_struct *p;
 330   │
 331   │         do_each_pid_task(pgrp, PIDTYPE_PGID, p) {
 332   │                 if ((p == ignored_task) ||
 333   │                     (p->exit_state && thread_group_empty(p)) ||
 334   │                     is_global_init(p->real_parent))
 335   │                         continue;
 336   │
 337   │                 if (task_pgrp(p->real_parent) != pgrp &&
 338   │                     task_session(p->real_parent) == task_session(p))
 339   │                         return 0;
 340   │         } while_each_pid_task(pgrp, PIDTYPE_PGID, p);
 341   │
 342   │         return 1;
 343   │ }
 344   │
 345   │ int is_current_pgrp_orphaned(void)
 346   │ {
 347   │         int retval;
 348   │
 349   │         read_lock(&tasklist_lock);
 350   │         retval = will_become_orphaned_pgrp(task_pgrp(current), NULL);
 351   │         read_unlock(&tasklist_lock);
 352   │
 353   │         return retval;
 354   │ }

惊喜发现:will_become_orphaned_pgrp 的注释解释了内核对待孤儿进程组的策略。

7. 结论

ioctl 系统调用返回 -EIO 的原因是:当前进程的父进程组是孤儿进程组。

小结

对系统调用返回错误的情况,可以使用 bpfsnoop 进行排查:

  1. funcgraph 功能 (--output-fgraph): 可以便捷地了解到目标函数会调用哪些函数。
  2. LBR 功能 (--output-lbr): 借助于 CPU 提供的 LBR 能力,突破 funcgraph 功能缺陷,能够追踪到代码逻辑分支记录。
  3. funcstack 功能 (--output-stack): 可以获取到目标函数的函数调用栈记录,帮助分析函数调用关系。
  4. funcinsn 功能 (--output-insns): 可以获取到目标函数的指令级别的执行记录,帮助分析目标函数具体的执行过程。