據說有統計數據表明,代碼的缺陷率是一定的,與所使用的語言無關。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);
函數說明
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 popen()
函數原型
FILE *popen(const char *command, const char *type);
int pclose(FILE *stream);
函數說明
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.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[];
示例
使用 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;
}
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);
}
這種使用使用了臨時文件作爲應用程序和外部命令之間的聯繫橋樑,在應用程序中需要讀取文件,然後再刪除該臨時文件,比較繁瑣,優點是實現簡單,容易理解。
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;
}
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;
}