Linux函式popen/pclose學習
本文針對專案中用到的幾個函式進行詳細分析,並儘可能的新增示例進行驗證學習。比如fcntl/ioctl函式、system/exec函式、popen/pclose函式、mmap函式等。 重點參考了《UNP》和《Linux程式設計》第四版。
一、概念
#include <stdio.h>
FILE * popen ( const char * command , const char * type );
int pclose ( FILE * stream );
popen() 函式通過建立一個管道,呼叫 fork 產生一個子程序,執行一個 shell 以執行命令來開啟一個程序。
- command 引數是一個指向以 NULL 結束的 shell 命令字串的指標。這行命令將被傳到 bin/sh 並使用-c 標誌,shell 將執行這個命令。
- type 引數只能是讀或者寫中的一種,得到的返回值(標準 I/O 流)也具有和 type 相應的只讀或只寫型別。如果 type 是 “r” 則檔案指標連線到 command 的標準輸出;如果 type 是 “w” 則檔案指標連線到 command 的標準輸入。
- popen 的返回值是個標準 I/O 流,必須由 pclose 來終止,否則會產生殭屍子程序。pclose呼叫只在popen啟動的程序結束後才返回。
- 當使用popen()時,不要遮蔽SIGCHLD訊號,popen()使用fork()建立了子程序來執行所給的命令,需要通過此訊號判斷子程序是否已經退出。
核心:標準I/O函式庫提供的popen函式,本質上是對無名管道的使用,它建立一個管道並啟動另外一個程序,該程序要麼從該管道讀出標準輸入,要麼從該管道寫入標準輸出。popen在呼叫程序和指定命令之間建立一個管道。需要主要的使用該方法的缺點:指定命令將出錯資訊寫到標準錯誤輸出,而popen不對標準錯誤輸出做任何處理,它僅僅重定向標準輸出。
二、讀寫示例
1. 讀取外部程式的輸出
我們在程式中用popen訪問uname命令給出的資訊。命令uname -a的作用是列印系統資訊,包括計算機型號、作業系統名稱、版本和發行號、以及計算機網路名。
完成程式的初始化工作後,開啟一個連線到uname命令的管道,把管道設定為可讀方式並讓read_fp指向該命令的輸出。最後,關閉read_fp指向的管道。
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
int main()
{
FILE *read_fp;
char buf[1024];
int chars_read;
memset(buf, '\0', sizeof(buf) );//初始化buf,以免後面寫如亂碼到檔案中
read_fp = popen( "uname -a", "r" ); //將“uname -a”命令的輸出通過管道讀取(“r”引數)到FILE* stream
if(read_fp != NULL)
{
chars_read = fread( buf, sizeof(char), sizeof(buf), read_fp);//將資料流讀取到buf中
if(chars_read >0)
printf("my output:\n%s\n",buf);
pclose( read_fp );
}
return 0;
}
2. 將輸出送往外部程式
這裡將資料寫入管道,使用的額是od(八進位制輸出)的命令。
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
int main()
{
FILE *write_fp;
char buf[1024];
int chars_write;
memset(buf, '\0', sizeof(buf) );//初始化buf,以免後面寫如亂碼到檔案中
sprintf(buf,"hello world...\n");
write_fp = popen( "od -c", "w" ); //通過管道(“w”引數)寫入到FILE* stream
if(write_fp != NULL)
{
fwrite( buf, sizeof(char), sizeof(buf), write_fp); //將FILE* write_fp的資料流寫入到buf中
pclose( write_fp );
}
return 0;
}
程式使用帶有引數“w”的popen啟動od -c命令,這樣就可以向該命令傳送資料了。然後它給od -c命令傳送一個字串,該命令接收並處理它,最後把處理結果列印到自己的標準輸出上。
在命令列上,我們可以使用下面命令得到同樣的輸出結果:
echo “hello world…\n” | od -c
三、返回值分析
和system呼叫類似,也需要考慮呼叫返回值。popen執行一個 shell 以執行命令來開啟一個程序。pclose() 函式關閉標準 I/O 流,等待命令執行結束,然後返回 shell 的終止狀態。如果 shell 不能被執行,則 pclose() 返回的終止狀態與 shell 已執行 exit 一樣。
這裡重點參考一位博主的文章,首先要明確幾點:
- pclose 失敗返回 -1, 成功則返回 exit status, 同 system 類似,需要用 WIFEXITED, WEXITSTATUS 等獲取命令返回值。
- 和 system 一樣,SIGCHLD 依然會影響 popen,如果設定了SIGCHLD 則獲取不到子程序的狀態。
- 管道只能處理標準輸出,不能處理標準錯誤輸出。
#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
int main(int argc, char* argv[])
{
char cmd[1024];
char line[1024];
FILE* pipe;
int rv;
if (argc != 2)
{
printf("Usage: %s <path>\n", argv[0]);
return -1;
}
// pclose fail: No child processes
//signal(SIGCHLD, SIG_IGN);
snprintf(cmd, sizeof(cmd), "ls -l %s 2>/dev/null", argv[1]);
pipe = popen(cmd, "r");
if(NULL == pipe)
{
printf("popen() failed: %s\n", cmd);
return -1;
}
while(fgets(line, sizeof(line),pipe) != NULL)
{
printf("%s", line);
}
rv = pclose(pipe);
if (-1 == rv)
{
printf("pclose() failed: %s\n", strerror(errno));
return -1;
}
if (WIFEXITED(rv))
{
printf("subprocess exited, exit code: %d\n", WEXITSTATUS(rv));
if (0 == WEXITSTATUS(rv))
{
// if command returning 0 means succeed
printf("command succeed\n");
}
else
{
if(127 == WEXITSTATUS(rv))
{
printf("command not found\n");
return WEXITSTATUS(rv);
}
else
{
printf("command failed: %s\n", strerror(WEXITSTATUS(rv)));
return WEXITSTATUS(rv);
}
}
}
else
{
printf("subprocess exit failed\n");
return -1;
}
return 0;
}
四、總結
總的來說,請求popen呼叫執行一個程式時,它首先啟動shell,即系統的shell命令,然後將command字串作為一個引數傳遞給它。這樣就有了優缺點:
優點是:由於所有類Unix系統中引數擴充套件都是由shell完成的,所有它執行我們通過popen完成非常複雜的shell命令。而其他一些建立程序的函式(如execl)呼叫起來就複雜的多,因為呼叫程序必須自己完成shell擴充套件。
缺點是:針對每個popen呼叫,不僅要啟動一個被請求的程式,還要啟動一個shell,即每個popen呼叫將啟動兩個程序。從節省系統資源的角度來看,popen函式的呼叫成本略高,並且對目標命令的呼叫比正常方式慢一些(通過pipe改進)。
和system相比,system就是執行shell命令最後返回是否執行成功,popen執行命令並且通過管道和shell命令進行通訊。