九维我操你爹
上面的结构完美符合我们最初的场景: p1 是操作系统的 1 号进程 init,p2 是 bash shell,p3 是 strace(ctrl-c 之后进程死掉),p4 是 python repl。这个结构恐怕很难简化,如果有哪位老师发现 3 进程可以复现请教教我
#!/usr/bin/env python3
import os, sys, pty, fcntl, termios, signal, time

def handler(sig, f):
    pass

def spawn_p2():
    master_fd, slave_fd = pty.openpty()
    os.setsid()
    fcntl.ioctl(slave_fd, termios.TIOCSCTTY, 0)

    p2_pgid = os.getpgrp()

    pid3 = os.fork()
    if pid3 == 0:
        os.setpgid(0, 0)
        os.dup2(slave_fd, 0)
        os.dup2(slave_fd, 1)
        os.dup2(slave_fd, 2)
        if slave_fd > 2:
            os.close(slave_fd)
        pid4 = os.fork()
        if pid4 == 0:
            signal.signal(signal.SIGTTOU, handler)
            exe = 'python'
            os.execlp(exe, os.path.basename(exe))
        else:
            os.tcsetpgrp(0, p2_pgid)
            os._exit(0)

    try:
        os.setpgid(pid3, pid3)
    except ProcessLookupError:
        pass

    os.tcsetpgrp(slave_fd, pid3)

    if slave_fd > 2:
        os.close(slave_fd)

    os.waitpid(pid3, 0)
    time.sleep(1)

    os.tcsetpgrp(master_fd, p2_pgid)

    while 1:
        os.read(master_fd, 1024)

def main():
    pid2 = os.fork()
    if pid2 == 0:
        spawn_p2()
    else:
        time.sleep(1 << 30)

if __name__ == "__main__":
    main()

运行 docker run --name repl -td -v $(pwd):/src python:3.14.0rc2 python /src/main.py,观察到下面的进程树。
root       67924   67901  1 13:49 pts/0    00:00:00  \_ python /src/orp_repl2.py
root       67977   67924  1 13:49 pts/1    00:00:00      \_ python /src/orp_repl2.py
root       67979   67924 79 13:49 pts/1    00:00:02      \_ python


strace -p 最新的 python 进程,确认无限 EIO,QED
我们来讨论一下伊洪大哥最近在查的 cpython bug https://github.com/python/cpython/issues/135329

TLDR 不要看里面 j 某人的回复,是错的

我们从头开始。

strace ./python 然后 ctrl-c 发现 python 进程无限报错循环。 strace 很复杂,先想办法剥离关键因素,试试单独跑 ./python 然后 strace -fTp $(pidof python) 再 ctrl-c,嘿不报错!太好了说明不是 ptrace 引发的错误,而是进程管理、终端、信号相关的问题。太好了,不然我又要去看一遍 signal-delivery-stop...

那就手动来一层套娃。已知 shell -> strace -> python repl 的进程树有问题,但是 strace ptrace 又并非关键,那就把 strace 换掉试试看: shell -> python -> python repl:
pid = os.fork()
if pid == 0:
    exe = sys.executable
    os.execlp(exe, os.path.basename(exe))
    os._exit(127)

os.waitpid(pid, 0)

再 ctrl-c,居然也可以复现!

那就好办了,strace -fTttp $(pidof python) &>crash.strace.log 启动!
20:18:26 --- SIGINT {si_signo=SIGINT, si_code=SI_USER, si_pid=4678, si_uid=1000} ---
20:18:26 rt_sigreturn({mask=[]})        = -1 EINTR (Interrupted system call) <0.000007>
20:18:26 write(4, "\33[?2004l", 8)      = 8 <0.000013>
20:18:26 write(4, "\33[?1l\33>", 7)     = 7 <0.000012>
20:18:26 ioctl(3, TCGETS, {c_iflag=IXON|IUTF8, c_oflag=NL0|CR0|TAB0|BS0|VT0|FF0|OPOST|ONLCR, c_cflag=B38400|CS8|CREAD, c_lflag=ISIG|ECHOE|ECHOK|IEXTEN|ECHOCTL|ECHOKE, ...}) = 0 <0.000008>
20:18:26 ioctl(3, TCGETS, {c_iflag=IXON|IUTF8, c_oflag=NL0|CR0|TAB0|BS0|VT0|FF0|OPOST|ONLCR, c_cflag=B38400|CS8|CREAD, c_lflag=ISIG|ECHOE|ECHOK|IEXTEN|ECHOCTL|ECHOKE, ...}) = 0 <0.000006>
20:18:26 ioctl(3, TCSETSW, {c_iflag=ICRNL|IXON|IUTF8, c_oflag=NL0|CR0|TAB0|BS0|VT0|FF0|OPOST|ONLCR, c_cflag=B38400|CS8|CREAD, c_lflag=ISIG|ICANON|ECHO|ECHOE|ECHOK|IEXTEN|ECHOCTL|ECHOKE, ...}) = -1 EIO (Input/output error) <0.000007>
...
20:18:26 ioctl(3, TCSETSW, {c_iflag=BRKINT|IUTF8, c_oflag=NL0|CR0|TAB0|BS0|VT0|FF0|ONLCR, c_cflag=B38400|CS8|CREAD, c_lflag=ISIG|ECHOE|ECHOK|ECHOCTL|ECHOKE, ...}) = -1 EIO (Input/output error) <0.000008>

...
20:18:26 ioctl(3, TCSETSW, {c_iflag=BRKINT|IUTF8, c_oflag=NL0|CR0|TAB0|BS0|VT0|FF0|ONLCR, c_cflag=B38400|CS8|CREAD, c_lflag=ISIG|ECHOE|ECHOK|ECHOCTL|ECHOKE, ...}) = -1 EIO (Input/output error) <0.000008>


ioctl 返回 EIO 循环了。

继续剥离,用 strace 注入 syscall error:
sudo strace -e inject=ioctl:error=5 -p $(pidof python)

此时的 python 进程不需要套娃,shell 直起也能 ctrl-c 报错。

所以可以确定 cpython bug 了:cpython 无法正确 EIO 报错,一旦 EIO 就触发 cpython 做栈回溯 + ioctl,进而触发新一轮的 EIO,无限循环。注意这是 python 3.13 new repl 的功能,所以也解释了为什么旧版 cpython repl 不会陷入此报错循环。

以上你并不需要知道 OS 的细节就可以追查到,希望大家都可以反复练习熟练掌握。

下面进入内核,为什么内核返回 EIO?因为伊洪要写测试,所以在容器内正确地创造出 EIO 还是很重要的。

retsnoop 抓一波
# sudo ./retsnoop -x EIO -a '*tty*' -e '*tty*'

10:10:44.178847 -> 10:10:44.178850 TID/PID 30657/30657 (python/python):

                 entry_SYSCALL_64_after_hwframe+0x76  
                 do_syscall_64+0x7e                   
                 x64_sys_call+0x131e                  
                 __x64_sys_ioctl+0xa4                 
     2us [-EIO]  tty_ioctl+0x515                      
 ⌇   1us [-EIO]  n_tty_ioctl+0x85                     
 ⌇   1us [-EIO]  n_tty_ioctl_helper+0x2d              
 ⌇   0us [-EIO]  tty_mode_ioctl+0x1c1                 
                 set_termios+0x5d                     
 ⌇   0us [-EIO]  tty_check_change+0x13                
!⌇   0us [-EIO]  __tty_check_change 


去看内核 __tty_check_change 的源码,里面只有两处 return -EIO
int tty_check_change(struct tty_struct *tty)
{
  return __tty_check_change(tty, SIGTTOU);
}
int __tty_check_change(struct tty_struct *tty, int sig)
{
  if (tty_pgrp && pgrp != tty_pgrp) {
    if (is_ignored(sig)) {
      if (sig == SIGTTIN)
        ret = -EIO;
    } else if (is_current_pgrp_orphaned())
      ret = -EIO;

所以是进程收到 SIGTTOU 时候,如果 TTOU 没有被 ignore,而进程所在的进程组是孤儿(注意:孤儿进程组 != 孤儿进程),则返回 EIO。

SIGTTOU 的触发条件是后台进程写控制终端,所以这里要求的进程结构至少应该是 p1 (1号进程,创建 session 和 pty) -> p2 (repl), p1 设置自己为前台进程组,设置 p2 repl 为后台进程组,此时 repl IO 触发 TTOU。

孤儿进程组要麻烦一点,我们先考虑最简单的三层进程树: p1 (1号进程,创建 session pty) -> p2 (独立进程组,exit immediately) -> p3 (repl),由于 p2 退出,p3 孤儿被 re-parent 到 pid 1 即 p1,此时由于 p1, p3 依然在一个 session,p3 并非处在孤儿进程组。

因此要考虑一个复杂的四层进程树: p1 (1号,不做事) -> p2 (创建 session pty) -> p3 (独立进程组,exit) -> p4 (repl)。此时 p4 会被 reparent 到 p1 ,所以 p4 所在进程组的全部进程(只有 p3 p4 但 p3 无了)的父进程们都和进程组的父进程们不在同一个 session,满足了孤儿进程组。
https://mp.weixin.qq.com/s/5bAUfJz_DG6dJcMsHQUeIw

特作如下调休:9月3日至5日调休放假,共3天。其中9月3日(星期四)放假,9月4日(星期五)调休,9月6日(星期日)上班。


苹果测试工程师的日常
我也会对日本人讲类似的话,但是我自己其实是没有这种言外之意的…… 单纯就是自己在学习外语的时候也特别希望能先学好要紧的东西,不要紧的东西先放一放。所以对学习中文的外国人,就觉得儿化音不是那么重要这个信息值得讲一讲 因为我不知道为什么外国人的中文教材里放那么多儿化音,有些说法我在北京生活这么久也没听到有人讲过(不敢断言北方人都不这么讲,只是说明在我的生活经验里这种东西确实不用先学…… 把这种时间投入到音调、抑扬顿挫的训练里,可能会收获成倍的效果 想要自己说的中文听起来更自然,不是掌握单个字的音调就可以了,…
https://fixupx.com/steveninbj/status/1960356585043009772

绝对没有看不起这位网友说的中文的意思,我觉得我能把日语学到他的一半好,睡觉都能乐醒
只是我个人而言,我说我自己啊,看到这段话里这么多「儿」字就觉得浑身发痒
我讲了二十多年的中文可能都没有这一段话里用到的「儿」字多

我为什么总是对这个现象耿耿于怀呢?我觉得是因为作为南方人,我自己确实没有这样频繁使用儿化音的习惯。当我看到外国人学到的标准中文和我习惯的大不一样的时候,我就会觉得很焦虑
谁不希望在「标准」里照见自己的影子呢?至少我是这么希望的 steven in BJ (@steveninbj)
北京电信政企 55 档套餐,下月起不再免费送宽带,需要的抓紧去办(咨询你家附近营业厅,需有社保)
#北京 #电信
#片假不留
又是被日式英语乱缩写折磨的一天
看到用 cropped 来指代齐腰短夹克或者七分裤的那一瞬间我感觉整个大脑皮层都展开了……
难怪日本能诞生出《柯南》这样的作品呢,有片假名这个宝库当然会迷上猜谜啦~片假名的谜题这辈子都猜不完🫡

https://www.felissimo.co.jp/niau/words/16792/
Back to Top