1. 程式人生 > >Linux函式popen/pclose學習

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命令進行通訊。