为什么 read() 在管道连接到使用 boost::asio for STDIN/STDOUT 的程序时,使用 EAG



我有一个小程序,它可以与服务器建立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 foon", 32768)            = 9
write(1, "echo foon", 9)               = 9
read(0, "echo barn", 32768)            = 9
write(1, "echo barn", 9)               = 9
read(0, 0xa02c000, 32768)               = -1 EAGAIN (Resource temporarily unavailable)

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

理论#2:当我的程序写入STDOUT时,它会做一些事情,将cat的STDIN从阻塞更改为非阻塞。我认为情况并非如此:

14211 read(0,  <unfinished ...>
//myprog (pid 14209) does epoll stuff here
//cat (pid 14211) receives my command
14211 <... read resumed> "echo foon", 32768) = 9
//more epoll
//cat writes
14211 write(1, "echo foon", 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 foon"..., 512}], 1) = 9
//sends it to the server (encrypted)
14209 sendmsg(7, {msg_name(0)=NULL, msg_iov(1)=[{"2733'T252251317w255310}h322222%204326FA2713022413762377377275250o262"..., 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)=[{"2733"3573513276a233356C326z31725234427Aft|f307275u344\351 320"..., 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, [{"foon", 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 barn", 32768) = 9
14209 <... epoll_wait resumed> {{EPOLLOUT, {u32=136514720, u64=136514720}}}, 128, -1) = 1
14211 write(1, "echo barn", 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。完整的跟踪可用。

大多数 shell 和终端的工作方式是 stdin 和 stdout 是同一文件的读/写文件描述符。请考虑以下事项:

$ echo FOO >&0
FOO

恭喜,您刚刚在 stdin 的文件描述符中写了一些东西。

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

文件状态标志

每个打开的文件描述都有某些关联的状态标志, ini- 由 open(2) 组织化,可能由 fcntl() 修改。 重复的文件描述符(由 dup(2)、fcntl(F_DUPFD) 制作, 叉(2)等)引用相同的打开文件描述,因此 共享相同的文件状态标志。

所以,你的理论#1基本上是正确的。如果你在脑海中计算出所有文件描述符是如何创建的,你的程序的标准输出和cat的标准输入最终是同一个文件的不同描述符,因为 shell 设置每个启动程序的标准输入、输出和错误的方式;使用文件描述器设置非阻塞模式会影响同一基础文件句柄的所有描述符。

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

我有两个建议。

  1. 显式open("/dev/tty")以获取完全独立的文件句柄。

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

最新更新