快速上手popen()
该函数用于运行指定命令,并且让刚启动的程序看起来像文件一样可以被读写。
2 个 demo
1) 从外部程序中读数据:
int main(int argc, char **argv)
{
FILE *fp;
char buf[100];
int i = 0;
fp = popen("ls -1X", "r");
if (fp != NULL) {
while(fgets(buf, 100, fp) != NULL) {
printf("%d: %s", i++, buf);
}
pclose(fp);
return 0;
}
return 1;
}
运行效果:
$ ./001_popen_r
0: 001_popen_r
1: 002_popen_w
2: 001_popen_r.c
3: 002_popen_w.c
4: 004_popen_intern.c
2) 写数据到外部程序:
int main(int argc, char *argv)
{
FILE *fp = NULL;
char buffer[BUFSIZE];
sprintf(buffer, "hello worldn");
fp = popen("od -tcx1", "w");
if (fp != NULL) {
fwrite(buffer, sizeof(char), strlen(buffer), fp);
pclose(fp);
return 0;
}
return 1;
}
运行效果:
0000000 h e l l o w o r l d n
68 65 6c 6c 6f 20 77 6f 72 6c 64 0a
0000014
相关要点
函数原型
FILE *popen(const char *command, const char *type);
popen() 会先执行 fork,然后调用 exec 执行 command,并且返回一个标准 I/O 文件指针。
type = "r":
- 文件指针连接到 command 的标准输出。
type = "w":
- 文件指针连接到 command 的标准输入。
点击查看大图
优缺点
优点:
- 由于调用了 shell,所以可以支持通配符 (例如*.c) 等各种 shell 扩展特性;减少了代码量;
缺点:
- 要启动 2 个程序:shell 和 目标程序,调用成本略高,比起直接 exec 某个程序来说要慢一些;
内部实现
popen() 的内部实现思路如下:
FILE *_popen(const char *command, const char *type)
{
pipe()
fork();
if (pid > 0)
close() child's fd
return fdopen() parent's fd
else
close(parent's fd)
dup2() child's data fd to stdin or stdout
close() child's fd
exec("/bin/sh -c") command
}
- 创建一个管道,用于父子进程间的通讯;父进程:
- 关闭未使用的管道端;返回父进程数据管道端的 FILE *, 它可能连接父进程的 stdin / stdout;
子进程:
- 关闭未使用的管道端;重定位子进程的数据管道端到 stdin / stdout;执行目标命令;
初步的代码实现:
FILE *_popen(const char *command, const char *type)
{
int pfp[2];
int parent_end, child_end;
int pid;
if (*type == 'r') {
parent_end = READ;
child_end = WRITE;
} else if (*type == 'w') {
parent_end = WRITE;
child_end = READ;
} else {
return NULL;
}
pipe(pfp);
pid = fork();
if (pid > 0 ) {
close(pfp[child_end]);
return fdopen(pfp[parent_end], type);
} else {
close(pfp[parent_end]);
dup2(pfp[child_end], child_end);
close(pfp[child_end]);
execl("/bin/sh", "sh", "-c", command, NULL);
exit(0);
}
return NULL;
}
这里的实现有一些不足的地方,例如:
为了便于阅读,省略了错误检查;
没有保存子进程的 pid,后续无法使用 wait() 进行收尸;
一个进程可能调用 popen() 多次,需要用数组 / 链表来存储所有子进程的 pid;
更完善的实现可以参考:
https://android.googlesource.com/platform/bionic/+/3884bfe9661955543ce203c60f9225bbdf33f6bb/libc/unistd/popen.c
应用案例
MJPG-streamer 是什么?
https://github.com/jacksonliam/mjpg-streamer
通过 mjpg-streamer,你可以通过 PC 浏览器访问到板子上的摄像头图像。
MJPG-streamer 就是通过 popen() 来支持 CGI 功能的:
CGI 是早期出现的一种简单、流行的服务端应用程序执行接口,http server 通过运行 CGI 程序来完成更复杂的处理工作,在 MJPG-streamer . 里的相关代码如下:
plugins/output_http/httpd.c
void execute_cgi(int id, int fd, char *parameter, char *query_string)
{
// prepare
// 执行浏览器指定的 CGI 程序
f = popen(buffer, "r");
// 获得 CGI 程序的输出
while((i = fread(buffer, 1, sizeof(buffer), f)) > 0) {
if (write(fd, buffer, i) < 0) {
fclose(f);
free(buffer);
close(lfd);
return;
}
}
}
这里只是简单地了解一下 MJPG-Streamer,有兴趣的小伙伴们自行阅读更多的代码吧。
相关参考
Unix-Linux 编程实践教程 / 11.4 popen: 让进程看似文件
Linux 程序设计(第 4 版) / 13.3 将输出送往 popen
Unix 环境高级编程第 3 版 / 15.3 函数 popen 和 pclose
HTTP 权威指南
思考技术,也思考人生
要学习技术,更要学习如何生活。
你和我各有一个苹果,如果我们交换苹果的话,我们还是只有一个苹果。但当你和我各有一个想法,我们交换想法的话,我们就都有两个想法了。
对 嵌入式系统 (Linux、RTOS、OpenWrt、Android) 和 开源软件 感兴趣,关注公众号:嵌入式 Hacker。
觉得文章对你有价值,还请多多 转发。