为什么在使用 boost::asio for STDIN/STDOUT 管道传输到程序时,read() 会因 EAGAIN 失败?

Why does read() fail with EAGAIN when piping to a program using boost::asio for STDIN/STDOUT?

我有一个小程序,它与服务器建立 SSL 连接,然后将数据从 STDIN 复制到服务器,并将数据从服务器复制到 STDOUT(很像 openssl s_client)。我正在使用 boost::asio 读取和写入 STDIN、STDOUT 和 SSL 套接字。问题是我无法从我的程序的另一个程序中传输数据,例如

cat | myprog

我输入一行并按回车键,一切正常:文本行通过我的程序找到它的路径,到达响应的服务器,并将响应打印到我的控制台。下次我发送命令时,cat 发送它但在下一次 read() 调用时失败(我键入以 "echo" 开头的行):

echo foo
foo
echo bar
cat: -bar
: Resource temporarily unavailable

为什么会这样?

strace 证实了这一点,来自 cat:

read(0, "echo foo\n", 32768)            = 9
write(1, "echo foo\n", 9)               = 9
read(0, "echo bar\n", 32768)            = 9
write(1, "echo bar\n", 9)               = 9
read(0, 0xa02c000, 32768)               = -1 EAGAIN (Resource temporarily unavailable)

理论 #1:boost::asio 为此目的将 STDIN 设置为非阻塞,但它也影响了 cat 的 STDIN。如果我将代码更改为 fork() 关闭预处理器,这应该不是问题,允许它继承 STDIN 和 STDERR,并捕获 asio 可以直接读取的 STDOUT。这样 asio 就不必接触 STDIN。这已经完成并且 strace 确认文件描述符 0 已被保留。

理论 #2:当我的程序写入 STDOUT 时,它会做一些改变 cats STDIN 从阻塞到非阻塞的事情。我不认为是这样:

14211 read(0,  <unfinished ...>
//myprog (pid 14209) does epoll stuff here
//cat (pid 14211) receives my command
14211 <... read resumed> "echo foo\n", 32768) = 9
//more epoll
//cat writes
14211 write(1, "echo foo\n", 9)         = 9
14209 <... epoll_wait resumed> {{EPOLLIN, {u32=136519504, u64=136519504}}}, 128, -1) = 1
//cat starts reading again
14211 read(0,  <unfinished ...>
//my prog receives command from cat
14209 readv(3, [{"echo foo\n[=15=][=15=][=15=][=15=][=15=][=15=][=15=][=15=][=15=][=15=][=15=][=15=][=15=][=15=][=15=][=15=][=15=][=15=][=15=][=15=][=15=][=15=][=15=]"..., 512}], 1) = 9
//sends it to the server (encrypted)
14209 sendmsg(7, {msg_name(0)=NULL, msg_iov(1)=[{"[=15=]'T217w50}h22%46FA12167750o2"..., 44}], msg_controllen=0, msg_flags=0}, MSG_NOSIGNAL) = 44
14209 epoll_wait(5, {{EPOLLIN|EPOLLOUT, {u32=136514856, u64=136514856}}}, 128, 0) = 1
14209 readv(3, 0xbfd8be04, 1)           = -1 EAGAIN (Resource temporarily unavailable)
//receives response
14209 recvmsg(7, {msg_name(0)=NULL, msg_iov(1)=[{"[=15=]\"7176a36C6z724A\f\t|\f75u4\1 0"..., 17408}], msg_controllen=0, msg_flags=0}, 0) = 39
//myprog sets non-blocking IO on STDOUT
14209 ioctl(1, FIONBIO, [1])            = 0
//writes out response
14209 writev(1, [{"foo\n", 4}], 1)      = 4
//myprog does more epoll stuff again
//cat receives seccond command, not that that this call started before myprog wrote anything or called ioctl()
14211 <... read resumed> "echo bar\n", 32768) = 9
14209 <... epoll_wait resumed> {{EPOLLOUT, {u32=136514720, u64=136514720}}}, 128, -1) = 1
14211 write(1, "echo bar\n", 9 <unfinished ...>
14209 epoll_wait(5,  <unfinished ...>
14211 <... write resumed> )             = 9
14209 <... epoll_wait resumed> {{EPOLLIN, {u32=136519504, u64=136519504}}}, 128, -1) = 1
14211 read(0,  <unfinished ...>
14209 readv(3,  <unfinished ...>
//cat's next read fails
14211 <... read resumed> 0x8d6f000, 32768) = -1 EAGAIN (Resource temporarily unavailable)

我的程序确实将其自身的 STDOUT 更改为非阻塞,但我可以看到它单独保留了 fd 0。 full trace可用。

大多数 shell 和终端的工作方式是 stdin 和 stdout 是同一文件的 read/write 文件描述符。考虑以下因素:

$ echo FOO >&0
FOO

恭喜,您刚刚向 stdin 的文件描述符写入了一些内容。

文件的另一半可以在 fcntl(2) 手册页中找到,在设置各种文件状态标志的 F_SETFL fcntl() 调用的描述旁边:

File status flags

Each open file description has certain associated status flags, ini‐ tialized by open(2) and possibly modified by fcntl(). Duplicated file descriptors (made with dup(2), fcntl(F_DUPFD), fork(2), etc.) refer to the same open file description, and thus share the same file status flags.

所以,您的第 1 条理论大部分是正确的。如果您在脑海中计算出所有文件描述符是如何创建的,那么由于 shell 设置的方式,您程序的标准输出和 cat 的标准输入最终会成为同一文件的不同描述符设置每个启动程序的标准输入、输出和错误;并使用文件描述符设置非阻塞模式会影响同一底层文件句柄的所有描述符。

请注意,上面引用的文档明确引用了 fork(),因此您将无法通过分叉解决此问题,分叉只会复制相同的文件描述符。

我有两个建议。

  1. 显式open("/dev/tty")得到一个完全独立的文件句柄

  2. 为什么您甚至需要将程序的标准输出设置为非阻塞模式?由于它进入了终端,因此非阻塞模式并没有真正完成任何事情。