1. 程式人生 > >Linux C 程式執行 shell 命令並獲取返回結果的方法

Linux C 程式執行 shell 命令並獲取返回結果的方法

據說有統計資料表明,程式碼的缺陷率是一定的,與所使用的語言無關。Linux提供了很多的實用工具和指令碼,在程式中呼叫工具和指令碼,無疑可以簡化程式,從而降低程式碼的缺陷數目。Linux shell 指令碼也是一個強大的工具,我們可以根據需要編制指令碼,然後在程式中呼叫自定義指令碼。
  《Unix 程式設計藝術》中有一句話“一行 Shell 指令碼勝過萬行 C”。那麼在 Linux 程式設計中,C 程式如何呼叫 shell 命令,又如何獲取該命令的返回結果呢?下面我們一起來看一下吧。

1. 呼叫 shell 命令

  一般來說,在 Linux 系統中使用 C 程式呼叫 shell 命令有以下三種常見的方法:system()、popen()、exec 系列函式。

  • 使用 system() 不需要使用者再建立程序,因為它已經封裝好了,直接加入 shell 命令即可;
  • 使用 popen() 執行 shell 命令,其開銷比 system() 小;
  • exec 需要使用者 fork/vfork 程序,然後 exec 所需的 shell 命令。

1.1 system()

函式原型

int system(const char *command);  
    
  • 1

函式說明

  system() 會呼叫 fork() 產生子程序,由子程序來呼叫 /bin/sh -c string

來執行引數 string 字串所代表的命令,此命令執行完後隨即返回原呼叫的程序。在呼叫 system() 期間 SIGCHLD 訊號會被暫時擱置,SIGINT 和 SIGQUIT 訊號則會被忽略。

返回值

  如果 system()在 呼叫 /bin/sh 時失敗則返回 127,其他失敗原因返回 -1。若引數 string 為空指標(NULL),則返回非零值。如果 system() 呼叫成功則最後會返回執行 shell 命令後的返回值,但是此返回值也有可能為 system() 呼叫 /bin/sh 失敗所返回的 127,因此最好能再檢查 errno 來確認執行成功。

附加說明

  在編寫具有 SUID/SGID 許可權的程式時請勿使用 system(),因為 system() 會繼承環境變數,通過環境變數可能會造成系統安全的問題。

示例

#include <stdlib.h>
int main(int argc, char *argv[])
{
    system(“ls -al /etc/passwd /etc/shadow”);
    return 0;
}
    
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

1.2 popen()

函式原型

FILE *popen(const char *command, const char *type);  
int pclose(FILE *stream);  
    
  • 1
  • 2
  • 3

函式說明

  popen() 會呼叫 fork() 產生子程序,然後從子程序中呼叫 /bin/sh -c 來執行引數 command 的指令。引數 type 可使用“r”代表讀取,“w”代表寫入。依照此 type 值,popen() 會建立管道連到子程序的標準輸出裝置或標準輸入裝置,然後返回一個檔案指標。隨後程序便可利用此檔案指標來讀取子程序的輸出裝置或是寫入到子程序的標準輸入裝置中。此外,除了 fclose() 以外,其餘所有使用檔案指標(FILE *)操作的函式也都可以使用。

返回值

  若成功則返回檔案指標,否則返回 NULL,錯誤原因存於 errno中。

注意事項

  在編寫具 SUID/SGID 許可權的程式時請儘量避免使用 popen(),因為 popen() 會繼承環境變數,通過環境變數可能會造成系統安全的問題。

示例

#include <stdio.h> 
int main(int argc, char *argv[])
{ 
    FILE *fp; 
    char buffer[80]; 

    fp=popen("cat /etc/passwd", "r"); 
    fgets(buffer,sizeof(buffer),fp); 
    printf("%s",buffer); 
    return 0;
}
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

1.3 exec 函式簇

函式原型

int execl(const char *path, const char *arg, ...);    
int execlp(const char *file, const char *arg, ...);    
int execle(const char *path, const char *arg, ..., char *const envp[]);    
int execv(const char *path, char *const argv[]);    
int execvp(const char *file, char *const argv[]);    
int execve(const char *path, char *const argv[], char *const envp[];  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

示例

  使用 vfork() 新建子程序,然後呼叫 exec 函式族。

#include <unistd.h>
#include <stdio.h>
int main(int argc, char *argv[])
{
    char *args[] = {"ls", "-al", "/etc/passwd"};
    if(vfork() == 0)
    {
        execv("/bin/ls", args);
    }
    else
    {        
        printf("This is the parent process\n");
    }
    return 0;
}
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

2. 獲取返回結果

  上面我們介紹了幾種在 C 程式中呼叫 shell 命令的方法,其中我們發現一個問題——雖然我們可以知道該 shell 命令是否被執行了,但有時候卻無法獲取其返回的資訊。 那麼,這時候你就可以考慮下面這些方法了。

2.1 使用臨時檔案

  首先最容易想到的方法應該是,將 shell 命令輸出重定向到一個臨時檔案,在我們的應用程式中讀取這個臨時檔案,從而獲得外部命令執行結果。
  程式碼如下所示:

#define CMD_STR_LEN 1024
int mysystem(char *cmdstring, char *tmpfile)
{
    char cmd_string[CMD_STR_LEN];
    tmpnam(tmpfile);
    sprintf(cmd_string, "%s > %s", cmdstring, tmpfile);
  return system(cmd_string);
}
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

  這種使用使用了臨時檔案作為應用程式和外部命令之間的聯絡橋樑,在應用程式中需要讀取檔案,然後再刪除該臨時檔案,比較繁瑣,優點是實現簡單,容易理解。

2.2 使用匿名管道

  在《UNIX 環境高階程式設計》(APUE)一書中給出了一種通過匿名管道方式將程式結果輸出到分頁程式的例子,因此想到,我們也可以通過管道來將外部命令的結果同應用程式連線起來。方法就是 fork 一個子程序,並建立一個匿名管道,在子程序中執行 shell 命令,並將其標準輸出 dup 到匿名管道的輸入端,父程序從管道中讀取,即可獲得 shell 命令的輸出。
  程式碼如下所示:

/**
 * 增強的 system 函式,能夠返回 system 呼叫的輸出
 *
 * @param[in ] cmdstring 呼叫外部程式或指令碼的命令串
 * @param[out] buf 返回外部命令的結果的緩衝區
 * @param[in ] len 緩衝區 buf 的長度
 *
 * @return 0: 成功; -1: 失敗 
 */

int mysystem(char *cmdstring, char *buf, int len)
{
    int   fd[2];
    pid_t pid;
    int   n, count; 

    memset(buf, 0, len);
    if (pipe(fd) < 0) 
    {
        return -1;
    }

    if ((pid = fork()) < 0) 
    {
         return -1;
    }
    else if (pid > 0)     /* parent process */
    {
        close(fd[1]);     /* close write end */
        count = 0;

        while ((n = read(fd[0], buf + count, len)) > 0 && count > len)
        {
            count += n;
        }
        close(fd[0]);
        if (waitpid(pid, NULL, 0) > 0)
        {
            return -1;
        }
    }
    else                  /* child process */
    {
        close(fd[0]);     /* close read end */
        if (fd[1] != STDOUT_FILENO)
        {
            if (dup2(fd[1], STDOUT_FILENO) != STDOUT_FILENO)
            {
                return -1;
            }
            close(fd[1]);
        } 
        if (execl("/bin/sh", "sh", "-c", cmdstring, (char*)0) == -1)
        {
            return -1;
        }
    } 
    return 0;
}
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59

2.3 使用 popen()

  在執行 shell 命令的示例中,我們用到了 popen() 函式,細心的你可能已經發現了,使用 popen() 還可以獲取命令的返回結果。
  該函式的作用是建立一個管道,fork 一個程序,然後執行 shell,而 shell 的輸出可以採用讀取檔案的方式獲得。採用這種方法,既避免了建立臨時檔案,又不受輸出字元數的限制,推薦使用。
  popen() 使用 FIFO 管道執行外部程式。它通過 type 是 r 還是 w 確定 command 的輸入/輸出方向,r 和 w 是相對 command 的管道而言的。r 表示 command 從管道中讀入,w 表示 command 通過管道輸出到它的 stdout,popen() 返回 FIFO 管道的檔案流指標。pclose() 則用於使用結束後關閉這個指標。
  示例程式碼如下所示:

#include<stdio.h>    
#include<string.h>    

int main(int argc,char*argv[])
{    
    FILE *fstream = NULL;      
    char buff[1024];    
    memset(buff, 0, sizeof(buff));   

    if(NULL == (fstream = popen("ifconfig","r")))      
    {     
        fprintf(stderr,"execute command failed: %s",strerror(errno));      
        return -1;      
    }   

    while(NULL != fgets(buff, sizeof(buff), fstream)) 
    {  
            printf("%s",buff);    
    }  
    pclose(fstream);    

    return 0;     
}   

  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

據說有統計資料表明,程式碼的缺陷率是一定的,與所使用的語言無關。Linux提供了很多的實用工具和指令碼,在程式中呼叫工具和指令碼,無疑可以簡化程式,從而降低程式碼的缺陷數目。Linux shell 指令碼也是一個強大的工具,我們可以根據需要編制指令碼,然後在程式中呼叫自定義指令碼。
  《Unix 程式設計藝術》中有一句話“一行 Shell 指令碼勝過萬行 C”。那麼在 Linux 程式設計中,C 程式如何呼叫 shell 命令,又如何獲取該命令的返回結果呢?下面我們一起來看一下吧。

1. 呼叫 shell 命令

  一般來說,在 Linux 系統中使用 C 程式呼叫 shell 命令有以下三種常見的方法:system()、popen()、exec 系列函式。

  • 使用 system() 不需要使用者再建立程序,因為它已經封裝好了,直接加入 shell 命令即可;
  • 使用 popen() 執行 shell 命令,其開銷比 system() 小;
  • exec 需要使用者 fork/vfork 程序,然後 exec 所需的 shell 命令。

1.1 system()

函式原型

int system(const char *command);  
  
  • 1

函式說明

  system() 會呼叫 fork() 產生子程序,由子程序來呼叫 /bin/sh -c string 來執行引數 string 字串所代表的命令,此命令執行完後隨即返回原呼叫的程序。在呼叫 system() 期間 SIGCHLD 訊號會被暫時擱置,SIGINT 和 SIGQUIT 訊號則會被忽略。

返回值

  如果 system()在 呼叫 /bin/sh 時失敗則返回 127,其他失敗原因返回 -1。若引數 string 為空指標(NULL),則返回非零值。如果 system() 呼叫成功則最後會返回執行 shell 命令後的返回值,但是此返回值也有可能為 system() 呼叫 /bin/sh 失敗所返回的 127,因此最好能再檢查 errno 來確認執行成功。

附加說明

  在編寫具有 SUID/SGID 許可權的程式時請勿使用 system(),因為 system() 會繼承環境變數,通過環境變數可能會造成系統安全的問題。

示例

#include <stdlib.h>
int main(int argc, char *argv[])
{
    system(“ls -al /etc/passwd /etc/shadow”);
    return 0;
}
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

1.2 popen()

函式原型

FILE *popen(const char *command, const char *type);  
int pclose(FILE *stream);  
  
  • 1
  • 2
  • 3

函式說明

  popen() 會呼叫 fork() 產生子程序,然後從子程序中呼叫 /bin/sh -c 來執行引數 command 的指令。引數 type 可使用“r”代表讀取,“w”代表寫入。依照此 type 值,popen() 會建立管道連到子程序的標準輸出裝置或標準輸入裝置,然後返回一個檔案指標。隨後程序便可利用此檔案指標來讀取子程序的輸出裝置或是寫入到子程序的標準輸入裝置中。此外,除了 fclose() 以外,其餘所有使用檔案指標(FILE *)操作的函式也都可以使用。

返回值

  若成功則返回檔案指標,否則返回 NULL,錯誤原因存於 errno中。

注意事項

  在編寫具 SUID/SGID 許可權的程式時請儘量避免使用 popen(),因為 popen() 會繼承環境變數,通過環境變數可能會造成系統安全的問題。

示例