当前进程stdin为何影响popen()调用的命令?如何解决?
根据popen的手册说明[重点标注]:
popen()函数通过创建管道、fork子进程、调用shell的方式启动进程。由于管道本质是单向的,type参数仅可指定读或写模式,不可同时指定两种模式,返回的文件流对应为只读或只写。
基于上述说明,popen返回的文件流无法同时进行读写操作,很容易理所当然认为当前进程的stdin不会影响传入popen()的shell命令的运行行为,但实际运行测试程序时会出现完全超出预期的现象。
//filename pipe_cmd.c #include <stdio.h> #include <string.h> #include <stdlib.h> #include <unistd.h> int main(void) { int ret = 0; FILE* file = popen("ssh -t jhone@192.168.1.51 bash -c 'top' ", "r"); int fd = fileno(file); char buffer[1024*100]; if(NULL != file) { while (true) { memset(buffer, '\0', sizeof(buffer)); int ret_read = read(fd, buffer, sizeof(buffer)); if (ret_read > 0) { printf("%s", buffer); } else if (0 == ret_read) { ret = 0; break; } else { ret = -1; break; } } } return ret; }
使用如下命令编译代码:gcc pipe_cmd.c -o pipe_cmd
运行生成的pipe_cmd程序时,可正常看到远程主机的进程信息,符合预期。但按下h键时,本地终端会显示top命令的帮助信息,该现象完全超出预期,显示内容如下:
Help for Interactive Commands - procps-ng version 3.3.9 Window 1:Def: Cumulative mode Off. System: Delay 0.1 secs; Secure mode Off. Z,B,E,e Global: 'Z' colors; 'B' bold; 'E'/'e' summary/task memory scale l,t,m Toggle Summary: 'l' load avg; 't' task/cpu stats; 'm' memory info 0,1,2,3,I Toggle: '0' zeros; '1/2/3' cpus or numa node views; 'I' Irix mode f,F,X Fields: 'f'/'F' add/remove/order/sort; 'X' increase fixed-width L,&,<,> . Locate: 'L'/'&' find/again; Move sort column: '<'/'>' left/right R,H,V,J . Toggle: 'R' Sort; 'H' Threads; 'V' Forest view; 'J' Num justify c,i,S,j . Toggle: 'c' Cmd name/line; 'i' Idle; 'S' Time; 'j' Str justify x,y . Toggle highlights: 'x' sort field; 'y' running tasks z,b . Toggle: 'z' color/mono; 'b' bold/reverse (only if 'x' or 'y') u,U,o,O . Filter by: 'u'/'U' effective/any user; 'o'/'O' other criteria n,#,^O . Set: 'n'/'#' max tasks displayed; Show: Ctrl+'O' other filter(s) C,... . Toggle scroll coordinates msg for: up,down,left,right,home,end k,r Manipulate tasks: 'k' kill; 'r' renice d or s Set update interval W,Y Write configuration file 'W'; Inspect other output 'Y' q Quit ( commands shown with '.' require a visible task display window ) Press 'h' or '?' for help with Windows,
该表现如同top命令是直接在本地bash中调用的,但实际上它是通过popen启动的,和手册说明的预期行为不符。
备注:
- 代码中的IP地址
192.168.1.51是本地计算机的IP,经测试不是导致该现象的原因。
更新:
- 不对代码做任何修改,将IP更换为其他不同IP的远程主机运行时,仍存在相同现象。
出现该现象的核心原因是popen只会重定向子进程的单一标准IO流:当type为r时仅将子进程stdout接入管道,当type为w时仅将子进程stdin接入管道,其余文件描述符默认直接继承父进程状态。
- 使用
popen(..., "r")启动命令时,子进程的stdin依然绑定在父进程(也就是你的测试程序)的终端输入上,没有被断开或重定向。 - 调用ssh时加了
-t参数,会强制在远程分配伪终端,远程的top作为交互式程序会主动读取终端输入;本地ssh进程的stdin没有被重定向,会直接把本地终端输入的按键内容透传到远程伪终端,最终被top进程接收。因此按h键会触发top的帮助输出,输出内容再通过popen创建的管道传回测试程序、打印到本地终端,就出现了top仿佛在本地运行的错觉。
不需要自行实现pipe+fork的全套逻辑,只需要在popen调用的命令行中提前通过shell重定向把stdin绑定到空设备,切断子进程和本地终端输入的关联即可,修改后的popen调用写法如下:
FILE* file = popen("ssh -t jhone@192.168.1.51 bash -c 'top' </dev/null", "r");
该方案改动量最小,原理是让popen启动的shell子进程先把stdin重定向到/dev/null,后续fork出的ssh进程会继承这个重定向后的stdin,不再读取本地终端的输入,从根源上避免本地按键影响远程命令运行。
如果需要更灵活的控制逻辑,也可以在popen调用前临时将当前进程的stdin重定向到/dev/null,popen执行完成后再恢复原stdin,但直接在命令行追加</dev/null是最简洁、兼容性最好的实现方式。
内容的提问来源于stack exchange,提问作者John

