基础知识
可能最简单的在两个程序之间传递数据的方法就是使用popen和pclose函数了。它们的原型如下所示:
#include <stdio.h>
FILE *popen(const char *command, const char *type);
int pclose(FILE *stream);
1.popen函数
popen函数允许一个程序将另一个程序作为新进程来启动,并可以传递数据给它或者通过它接收数据。command字符串是要运行的程序名和相应的参数。open_mode必须是"r"或者"w"。
如果open_mode是"r",被调用程序的输出就可以被调用程序使用,调用程序利用popen函数返回的FILE*文件流指针,就可以通过常用的stdio库函数(如fread)来读取被调用程序的输出。如果open_mode是"w",调用程序就可以用fwrite调用向被调用程序发送数据,而被调用程序可以在自己的标准输入上读取这些数据。被调用的程序通常不会意识到自己正在从另一个进程读取数据,它只是在标准输入流上读取数据,然后做出相应的操作。
每个popen调用都必须指定"r"或"w",在popen函数的标准实现中不支持任何其他选项。这意味着我们不能调用另一个程序并同时对它进行读写操作。popen函数在失败时返回一个空指针。如果想通过管道实现双向通信,最普通的解决方法是使用两个管道,每个管道负责一个方向的数据流。
2.pclose函数
用popen启动的进程结束时,我们可以用pclose函数关闭与之关联的文件流。pclose调用只在popen启动的进程结束后才返回。如果调用pclose时它仍在运行,pclose调用将等待该进程的结束。
pclose调用的返回值通常是它所关闭的文件流所在进程的退出码。如果调用进程在调用pclose之前执行了一个wait语句,被调用进程的退出状态就会丢失,因为被调用进程已结束。此时,pclose将返回-1并设置errno为ECHILD。
实验 读取外部程序的输出
现在来看一个简单的popen和pclose示例程序popen1.c。我们将在程序中用popen访问uname命令给出的信息。命令uname -a的作用是打印系统信息,包括计算机型号、操作系统名称、版本和发行号,以及计算机的网络名。
完成程序的初始化工作后,打开一个连接到uname命令的管道,把管道设置为可读方式并让read_fp指向该命令的输出。最后,关闭read_fp指向的管道。
测试代码:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#define DEBUG_INFO(format, ...) printf("%s - %d - %s :: "format"\n",__FILE__,__LINE__,__func__ ,##__VA_ARGS__)
void test_01(void){
FILE *read_fp;
char buf[BUFSIZ + 1];
memset(buf, 0, sizeof(buf));
DEBUG_INFO("BUFSIZ = %d",BUFSIZ);
read_fp = popen("uname -a","r");
if(read_fp == NULL){
perror("popen:");
return;
}
int len = fread(buf,1,BUFSIZ,read_fp);
if(len <= 0){
perror("fread:");
pclose(read_fp);
return;
}
DEBUG_INFO("len = %d,buf = %s",len,buf);
pclose(read_fp);
}
int main(int argc, char**argv){
test_01();
DEBUG_INFO("hello world");
return 0;
}
CMakeLists.txt文件
cmake_minimum_required(VERSION 3.8)
project(myapp)
# add_compile_options("-std=c++11")
add_executable(popen popen.c)
编译并执行
mkdir _build_ -p
cmake -S ./ -B _build_
make -C _build_
./_build_/popen
输出结果:
/big/work/ipc/popen.c - 12 - test_01 :: BUFSIZ = 8192
/big/work/ipc/popen.c - 24 - test_01 :: len = 89,buf = Linux ubuntu 4.19.260 #1 SMP Thu Sep 29 14:19:07 CST 2022 x86_64 x86_64 x86_64 GNU/Linux
/big/work/ipc/popen.c - 30 - main :: hello world
实验解析
这个程序用popen调用启动带有-a选项的uname命令。然后用返回的文件流读取最多BUFSIZ个字符(这个常量是在stdio.h中用#define语句定义的)的数据,并将它们打印出来显示在屏幕上。因为我们是在程序内部捕获uname命令的输出,所以可以处理它。
将输出送往popen
看过捕获外部程序输出的例子后,我们再来看一个将输出发送到外部程序的示例程序popen2.c,它将数据通过管道送往另一个程序。我们在这里使用的是od(八进制输出)命令。
实验 将输出送往外部程序
我们可以看到,下面这个程序popen2.c非常类似于前面的示例程序,唯一的不同是这个程序是将数据写入管道,而不是从管道中读取。
测试代码:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#define DEBUG_INFO(format, ...) printf("%s - %d - %s :: "format"\n",__FILE__,__LINE__,__func__ ,##__VA_ARGS__)
void test_01(void){
FILE *write_fp;
char buf[BUFSIZ + 1];
memset(buf, 0, sizeof(buf));
DEBUG_INFO("BUFSIZ = %d",BUFSIZ);
sprintf(buf,"0123456789ABCDEFGHIJKLMN\n");
DEBUG_INFO("len = %d,buf = %s",strlen(buf),buf);
write_fp = popen("od -c","w");
if(write_fp == NULL){
perror("popen:");
return;
}
int len = fwrite(buf,sizeof(char),strlen(buf),write_fp);
if(len <= 0){
perror("fwrite:");
pclose(write_fp);
return;
}
pclose(write_fp);
}
int main(int argc, char**argv){
test_01();
return 0;
}
测试结果:
实验解析
程序使用带有参数"w"的popen启动od -c命令,这样就可以向该命令发送数据了。然后它给od -c命令发送一个字符串,该命令接收并处理它,最后把处理结果打印到自己的标准输出上。在命令行上,我们可以用下面的命令得到同样的输出结果:
echo "0123456789ABCDEFGHIJKLMN" | od -c
传递更多的数据
我们目前所使用的机制都只是将所有数据通过一次fread或fwrite调用来发送或接收。有时,我们可能希望能以块方式发送数据,或者我们根本就不知道输出数据的长度。为了避免定义一个非常大的缓冲区,我们可以用多个fread或fwrite调用来将数据分为几部分处理。下面这个程序popen3.c通过管道读取所有数据。
实验 通过管道读取大量数据
在这个程序中,我们从被调用的进程ps ax中读取数据。该进程输出的数据有多少事先无法知道,所以我们必须对管道进行多次读取。
测试代码:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#define DEBUG_INFO(format, ...) printf("%s - %d - %s :: "format"\n",__FILE__,__LINE__,__func__ ,##__VA_ARGS__)
void test_01(void){
FILE *read_fp;
char buf[BUFSIZ + 1];
memset(buf, 0, sizeof(buf));
DEBUG_INFO("BUFSIZ = %d",BUFSIZ);
read_fp = popen("ps ax","r");
if(read_fp == NULL){
perror("popen:");
return;
}
int len = fread(buf,1,BUFSIZ,read_fp);
while(len > 0){
buf[len - 1] = '\0';
DEBUG_INFO("len = %d,buf = %s",len,buf);
len = fread(buf,1,BUFSIZ,read_fp);
}
pclose(read_fp);
}
int main(int argc, char**argv){
test_01();
DEBUG_INFO("hello world");
return 0;
}
输出结果:
实验解析
这个程序调用popen函数时使用了"r"参数,这与popen1.c程序的做法一样。这次,它连续从文件流中读取数据,直到没有数据可读为止。注意,虽然ps命令的执行要花费一些时间,但Linux会安排好进程间的调度,让两个程序在可以运行时继续运行。如果读进程popen3没有数据可读,它将被挂起直到有数据到达。如果写进程ps产生的输出超过了可用缓冲区的长度,它也会被挂起直到读进程读取了一些数据。
在本例中,你可能不会看到Reading:-信息的第二次出现。如果BUFSIZ的值超过了ps命令输出的长度,这种情况就会发生。一些(最新的)Linux系统将BUFSIZ设置为8192或更大的数字。为了测试程序在读取多个输出数据块时能够正常工作,你可以尝试每次读取少于BUFSIZ个字符(比如BUFSIZE/10个字符)。
如何实现popen
请求popen调用运行一个程序时,它首先启动shell,即系统中的sh命令,然后将command字符串作为一个参数传递给它。这有两个效果,一个好,一个不太好。
在Linux(以及所有的类UNIX系统)中,所有的参数扩展都是由shell来完成的。所以,在启动程序之前先启动shell来分析命令字符串,就可以使各种shell扩展(如*.c所指的是哪些文件)在程序启动之前就全部完成。这个功能非常有用,它允许我们通过popen启动非常复杂的shell命令。而其他一些创建进程的函数(如execl)调用起来就复杂得多,因为调用进程必须自己去完成shell扩展。
使用shell的一个不太好的影响是,针对每个popen调用,不仅要启动一个被请求的程序,还要启动一个shell,即每个popen调用将多启动两个进程。从节省系统资源的角度来看,popen函数的调用成本略高,而且对目标命令的调用比正常方式要慢一些。
我们用程序popen4.c来演示popen函数的行为。这个程序对所有popen示例程序的源文件的总行数进行统计,方法是用cat命令显示文件的内容并将输出通过管道传递给命令wc -l,由后者统计总行数。如果是在命令行上完成这一任务,我们可以使用如下命令:
cat popen*.c | wc -l
事实上,输入命令wc -l popen*.c更简单而且更有效率,但我们是为了通过这个例子来演示popen函数的工作原理。
实验popen启动shell
这个程序使用上面给出的命令,但是通过popen来读取命令输出的结果:
测试代码:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#define DEBUG_INFO(format, ...) printf("%s - %d - %s :: "format"\n",__FILE__,__LINE__,__func__ ,##__VA_ARGS__)
void test_01(void){
FILE *read_fp;
char buf[BUFSIZ + 1];
memset(buf, 0, sizeof(buf));
DEBUG_INFO("BUFSIZ = %d",BUFSIZ);
read_fp = popen("cat popen*.c | wc -l","r");
if(read_fp == NULL){
perror("popen:");
return;
}
int len = fread(buf,1,BUFSIZ,read_fp);
while(len > 0){
buf[len - 1] = '\0';
DEBUG_INFO("len = %d,buf = %s",len,buf);
len = fread(buf,1,BUFSIZ,read_fp);
}
pclose(read_fp);
}
int main(int argc, char**argv){
test_01();
DEBUG_INFO("hello world");
return 0;
}
执行结果:
实验解析
这个程序显示,shell在启动后将popen*.c扩展为一个文件列表,列表中的文件名都以popen开头,以.c结尾,shell还处理了管道符(|)并将cat命令的输出传递给wc命令。我们在一个popen调用中启动了shell、cat程序和wc程序,并进行了一次输出重定向。而调用这些命令的程序只看到最终的输出结果。
CMakeLists.txt、编译脚本和sftp.json
CMakeLists.txt
cmake_minimum_required(VERSION 3.8)
project(myapp)
# add_compile_options("-std=c++11")
add_executable(popen popen.c)
add_executable(popen2 popen2.c)
add_executable(popen3 popen3.c)
add_executable(popen4 popen4.c)
编译脚本
rm -rf _build_
mkdir _build_ -p
cmake -S ./ -B _build_
make -C _build_
# ./_build_/popen
# ./_build_/popen2
# ./_build_/popen3
./_build_/popen4
sftp.json
{
"name": "My Server",
"host": "192.168.31.138",
"protocol": "sftp",
"port": 22,
"username": "lkmao",
"password": "lkmao",
"remotePath": "/big/work/ipc",
"uploadOnSave": true,
"useTempFile": false,
"openSsh": false
}