我最近发现,如果 ksh 被阻塞几秒钟,则在将超过 16K 字节打印到标准输出后,ksh 可能会丢失一些数据。
此test.sh
脚本打印出 257*64 (16448) 字节:
#!/usr/bin/ksh
i=0
while [[ i -lt 257 ]]
do
x=$(file /tmp)
echo "0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE"
i=$((i+1))
done |
while read datafile
do
echo $datafile
done
我进行了以下测试:
0 $ ./test.sh | wc -c
16448
0 $ ./test.sh | (sleep 3; wc -c)
16384
该行x=$(file /tmp)
似乎会影响此行为,尽管它不会将任何内容传送到第二个循环。
如果我使用 bash,它会按预期工作。
对我来说,这看起来像是 ksh 中的一个错误。我使用的是 Solaris 5.10。有解决方案或解决方法吗?这个问题的根本原因是什么?我猜这可能与管道缓冲区大小有关。
谢谢,彼得
编辑:
因此,使用 运行测试truss
,我可以看到写入最后 64 个字节时出现错误:
ioctl(0, I_PEEK, 0x08046B40) = 0
Received signal #18, SIGCLD, in write() [caught]
siginfo: SIGCLD CLD_EXITED pid=6561 status=0x0000
write(1, " 0 1 2 3 4 5 6 7 8 9 A B".., 64) Err#4 EINTR
lwp_sigmask(SIG_SETMASK, 0x00020000, 0x00000000) = 0xFFBFFEFF [0x0000FFFF]
setcontext(0x08046670)
read(0, 0x0809064C, 1) = 0
ioctl(0, TCGETA, 0x08046B18) Err#22 EINVAL
使用 dtksh 运行相同的脚本如下所示。正如 Stephane 所指出的,失败的写入将被重新尝试。
ioctl(0, I_PEEK, 0x08046694) = 1
read(0, " 0 1 2 3 4 5 6 7 8 9 A B".., 64) = 64
Received signal #18, SIGCLD, in write() [caught]
siginfo: SIGCLD CLD_EXITED pid=28276 status=0x0000
write(1, " 0 1 2 3 4 5 6 7 8 9 A B".., 64) Err#4 EINTR
lwp_sigmask(SIG_SETMASK, 0x00020000, 0x00000000) = 0xFFBFFEFF [0x0000FFFF]
waitid(P_ALL, 0, 0x08046500, WEXITED|WTRAPPED|WSTOPPED|WNOHANG) = 0
waitid(P_ALL, 0, 0x08046500, WEXITED|WTRAPPED|WSTOPPED|WNOHANG) Err#10 ECHILD
sigaction(SIGCLD, 0x08046510, 0x08046580) = 0
setcontext(0x08046430)
write(1, 0x080F0FD8, 64) (sleeping...)
write(1, " 0 1 2 3 4 5 6 7 8 9 A B".., 64) = 64
ioctl(0, I_PEEK, 0x08046694) = 0
答案1
这确实看起来像 中的一个错误ksh
。
我怀疑的是
x=$(file /tmp)
ksh
生成一个新进程来运行该file
命令并通过管道读取其输出,并且不等待其终止(所有现代 shell 都会如此,包括现代版本的 ksh),但在读取时一旦到达 EOF 该命令就会返回从那根管子。
该行为可以通过运行来确认:
ksh -c 'x=$(exec sh -c "echo foo;exec >&-; sleep 10"); echo "$x"'
并检查是否ksh
有输出后立即返回foo
或10秒后返回。
如果是这种情况,则意味着该file
命令将终止并导致 SIGCLD 被发送到其父级(shell),后命令x=...
已返回。
shell 的目的是处理那些 SIGCLD 来询问其子进程的死亡。如果 shell 有一个子进程在后台运行,那么它应该做好随时死亡的准备。 SIGCLD 信号,就像任何不可忽略的信号一样,会导致阻塞系统调用被打断。 shell 应该为发生这种情况做好准备,或者通过阻塞在执行可能被中断的系统调用时发出信号,或者在处理信号后重新尝试中断的系统调用。
在这种情况下,看起来这一切都没有发生。大多数情况下,write
ksh 执行的有关运行内置函数的系统调用echo
会立即返回,因此它没有机会被中断,但是在 stdout 指向的管道已满后,write
系统调用最终会阻塞,此时它就会被阻塞。被 SIGCLD 中断。 ksh 不会重新尝试它,这就是错误。
如果我们运行,即使在 Linux 上我们也可以看到相同的行为
strace -e write ksh -c 'i=0; while [ "$i" -lt 2000 ]; do : &
echo xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
i=$(($i+1)); done' | (sleep 3; wc)
然后我们看到:
write(1, "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"..., 61) = ? ERESTARTSYS (To be restarted)
--- SIGCHLD (Child exited) @ 0 (0) ---
write(1, "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"..., 61...
同样,终止:
命令会导致阻塞write
系统调用被中断,但这次会write
重新尝试。
解决方法可能包括避免在调用内置命令之前进行命令替换echo
,或者确保替换write
是由与获取 SIGCLD 的进程不同的进程完成的,例如通过echo
在子 shell 中运行命令:
(echo "012...")
编辑:仔细查看truss
输出会发现它是来自第二个循环的跟踪,该跟踪旨在与运行另一个循环的进程在单独的进程中运行,因此不应从命令的终止中获取 SIGCLD file
。不过,它可以从运行第一个循环的子 shell 终止中获得一个 SIGCLD。
此外,如果正如您的测试结果所示,ksh 确实等待为命令替换而生成的进程,则接收到的 SIGCLD 信号不能用命令的异步终止来解释file
。
看起来更有可能的是外部管道已满,但两个 while 循环之间的管道未满,SIGCLDecho
在第二个循环的阻塞期间收到,并且来自第一个循环的终止。因此,更有效的解决方法是在子 shell 中运行第二个循环,而不是echo
在其中运行每个命令。
while ...; done | (while ...;done)