传输 16K 字节后 Ksh 丢失数据

Pet*_*los 7 solaris ksh pipe buffer stdout

我最近发现,如果 ksh 被阻塞几秒钟,则在向 stdout 打印超过 16K 字节后可能会丢失一些数据。

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
Run Code Online (Sandbox Code Playgroud)

我进行了以下测试:

0 $ ./test.sh | wc -c
   16448
0 $ ./test.sh | (sleep 3; wc -c)
   16384
Run Code Online (Sandbox Code Playgroud)

该行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
Run Code Online (Sandbox Code Playgroud)

使用 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
Run Code Online (Sandbox Code Playgroud)

Sté*_*las 7

这确实看起来像是ksh.

我怀疑的是在

x=$(file /tmp)
Run Code Online (Sandbox Code Playgroud)

ksh产生一个新进程来运行file命令并通过管道读取其输出,并且不等待其终止(就像所有现代 shell 一样,包括现代版本的 ksh),但是该命令在读取时一到达 EOF 就返回从那个管道。

该行为可以通过运行来确认:

ksh -c 'x=$(exec sh -c "echo foo;exec >&-; sleep 10"); echo "$x"'
Run Code Online (Sandbox Code Playgroud)

并检查是否ksh在输出foo后立即返回或在 10 秒后返回。

如果是这样的话,那么这意味着该file命令将终止并导致SIGCLD被发送到它的父(壳),之后x=...返回命令。

shell 旨在处理那些 SIGCLD 以查询其孩子的死亡。如果 shell 有一个孩子在后台运行,它应该准备好随时发生死亡。该 SIGCLD 信号,就像任何未被忽略的信号会导致阻塞系统调用被中断一样。shell 应该为这种情况的发生做好准备,要么在执行可能被中断的系统调用时阻塞信号,要么在处理信号后重新尝试被中断的系统调用。

在这种情况下,看起来这些都没有发生。大多数情况下,writeksh 执行的关于运行内置echo函数的write系统调用会立即返回,因此它不会被中断,但是在 stdout 指向的管道已满后,系统调用最终会阻塞,这就是它获取的时间被 SIGCLD 中断。并且 ksh 不会重新尝试它,这是错误。

如果我们运行,我们甚至可以在 Linux 上看到相同的行为

strace -e write ksh -c 'i=0; while [ "$i" -lt 2000 ]; do : &
  echo xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
  i=$(($i+1)); done' | (sleep 3; wc)
Run Code Online (Sandbox Code Playgroud)

然后我们看到:

write(1, "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"..., 61) = ? ERESTARTSYS (To be restarted)
--- SIGCHLD (Child exited) @ 0 (0) ---
write(1, "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"..., 61...
Run Code Online (Sandbox Code Playgroud)

一样,终止:命令会导致阻塞write系统调用被中断,但是这次,write重新尝试。

解决方法可能包括在调用 builtin 之前避免命令替换echo,或者确保write由与获取 SIGCLD 的进程不同的进程完成,例如通过echo在子 shell 中运行命令:

(echo "012...")
Run Code Online (Sandbox Code Playgroud)

编辑:仔细查看truss输出显示它是来自第二个循环的跟踪,该跟踪旨在与运行另一个循环的进程在单独的进程中运行,因此不应该从file命令的死亡中获取 SIGCLD 。但是,它可以从运行第一个循环的子外壳的终止中获得一个 SIGCLD。

此外,如果正如您的测试结果所表明的那样,ksh 确实等待为命令替换而产生的进程,则无法通过file命令的异步终止来解释收到的 SIGCLD 信号。

看起来更有可能的是外部管道已满,但不是两个 while 循环之间的管道,SIGCLDecho在第二个循环中的阻塞期间收到,并且来自第一个循环的终止。因此,更有效的解决方法是在子 shell 中运行第二个循环而不是其中的每个echo命令。

while ...; done | (while ...;done)
Run Code Online (Sandbox Code Playgroud)