九维我操你爹
上面的结构完美符合我们最初的场景: p1 是操作系统的 1 号进程 init,p2 是 bash shell,p3 是 strace(ctrl-c 之后进程死掉),p4 是 python repl。这个结构恐怕很难简化,如果有哪位老师发现 3 进程可以复现请教教我
运行 docker run --name repl -td -v $(pwd):/src python:3.14.0rc2 python /src/main.py,观察到下面的进程树。
strace -p 最新的 python 进程,确认无限 EIO,QED

#!/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 \_ pythonstrace -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:
再 ctrl-c,居然也可以复现!
那就好办了,strace -fTttp $(pidof python) &>crash.strace.log 启动!
ioctl 返回 EIO 循环了。
继续剥离,用 strace 注入 syscall error:
此时的 python 进程不需要套娃,shell 直起也能 ctrl-c 报错。
所以可以确定 cpython bug 了:cpython 无法正确 EIO 报错,一旦 EIO 就触发 cpython 做栈回溯 + ioctl,进而触发新一轮的 EIO,无限循环。注意这是 python 3.13 new repl 的功能,所以也解释了为什么旧版 cpython repl 不会陷入此报错循环。
以上你并不需要知道 OS 的细节就可以追查到,希望大家都可以反复练习熟练掌握。
下面进入内核,为什么内核返回 EIO?因为伊洪要写测试,所以在容器内正确地创造出 EIO 还是很重要的。
retsnoop 抓一波
去看内核 __tty_check_change 的源码,里面只有两处 return -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,满足了孤儿进程组。
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
绝对没有看不起这位网友说的中文的意思,我觉得我能把日语学到他的一半好,睡觉都能乐醒
只是我个人而言,我说我自己啊,看到这段话里这么多「儿」字就觉得浑身发痒
我讲了二十多年的中文可能都没有这一段话里用到的「儿」字多
我为什么总是对这个现象耿耿于怀呢?我觉得是因为作为南方人,我自己确实没有这样频繁使用儿化音的习惯。当我看到外国人学到的标准中文和我习惯的大不一样的时候,我就会觉得很焦虑
谁不希望在「标准」里照见自己的影子呢?至少我是这么希望的
绝对没有看不起这位网友说的中文的意思,我觉得我能把日语学到他的一半好,睡觉都能乐醒
只是我个人而言,我说我自己啊,看到这段话里这么多「儿」字就觉得浑身发痒
我讲了二十多年的中文可能都没有这一段话里用到的「儿」字多
我为什么总是对这个现象耿耿于怀呢?我觉得是因为作为南方人,我自己确实没有这样频繁使用儿化音的习惯。当我看到外国人学到的标准中文和我习惯的大不一样的时候,我就会觉得很焦虑
谁不希望在「标准」里照见自己的影子呢?至少我是这么希望的
又是被日式英语乱缩写折磨的一天
看到用 cropped 来指代齐腰短夹克或者七分裤的那一瞬间我感觉整个大脑皮层都展开了……
难怪日本能诞生出《柯南》这样的作品呢,有片假名这个宝库当然会迷上猜谜啦~片假名的谜题这辈子都猜不完🫡
https://www.felissimo.co.jp/niau/words/16792/
https://github.com/GoogleContainerTools/kaniko
一段时间不见,kaniko 项目原来停止开发了啊……
edit:
https://github.com/chainguard-dev/kaniko chainguard fork 了这个项目并且在继续开发
一段时间不见,kaniko 项目原来停止开发了啊……
edit:
https://github.com/chainguard-dev/kaniko chainguard fork 了这个项目并且在继续开发
https://www.tokyo-np.co.jp/article/430900
調査は、直轄している国道のうち橋やトンネル区間を除く2万810キロを対象に実施。24年度は3079キロの状況を調べた。
国交省は28年度までに残りの対象区間も調べる。