父子程序的雙向通訊簡明解讀(c程式)。
兩個程序之間通訊的方式
你可以用system, popen,pipe 實現兩個程序的通訊!
1.如何利用一個現有的程式? 系統呼叫system()是個不錯的選擇,
1.system 呼叫的實現方式
//下面是uClibc-0.9.33 的實現,為方便閱讀,程式碼有刪減。
//由以下程式碼可知, system 就是呼叫 fork() 函式,
//子程序呼叫execl 執行command命令;
//主程序等待子程序完成。
int __libc_system(const char *command)
{
//此簡化程式碼忽略了對訊號的處理程式碼,那部分功能主要實現主程序忽略SIGQUIT, SIGINT ...
int wait_val, pid;
if (command == 0)
return 1;
if ((pid = vfork()) < 0) return -1;
if (pid == 0) { // 子程序執行程式 execl,
execl("/bin/sh", "sh", "-c", command, (char *) 0);
_exit(127);
}
//主程序等待子程序結束
if (wait4(pid, &wait_val, 0, 0) == -1) return -1
return wait_val;
}
優點: system() 呼叫使用簡單,
缺點:當我們需要與程式互動時,system 就不能勝任了。
system() 函式呼叫相當於批命令呼叫或者執行程式命令
2. 用pipe實現父子程序的通訊popen()
popen 函式可以按讀的方式或者寫的方式開啟管道,通過管道與command 程式進行通訊。
2.1 popen() 函式的實現方式
程式碼有刪減, 英文是原註釋/**/,含中文行為本人註釋//。
由以下程式碼可知,popen 就是建立一個pipe, 父程序開啟pipe的一端,子程序開啟另一端。
子程序把pipe 端與stdin 或 stdout 關聯,執行 execl(command) 命令, 這樣通過pipe完成
了父子程序的通訊。
它就等價於linux-shell 中的重定向> 或者<
FILE *popen(const char *command, const char *modes)
{
FILE *fp;
int pipe_fd[2]; //pipe_fd[0]為讀端,pipe_fd[1]為寫端
int parent_fd;
int child_fd;
int zeroOrOne;
pid_t pid;
zeroOrOne = 0; // Assume write mode.
if (modes[0] != 'w') { // if not write mode, it must be read mode
++zeroOrOne;
if (modes[0] != 'r') { /* Oops! Parent not reading either! */
__set_errno(EINVAL); // 開啟模式只能是讀或者寫, 否則出錯。
goto RET_NULL; // 寫模式,zeroOrOne = 0; 讀模式 zeroOrOne = 1;
}
}
if (pipe(pipe_fd)) {// 建立pipe,得到兩個描述符 總是fd[0]為輸入端, fd[1]為輸出端
goto FREE_PI;
}
child_fd = pipe_fd[zeroOrOne]; //使得child_fd, parent_fd 為管道的兩端
parent_fd = pipe_fd[1-zeroOrOne]; //寫模式(父寫子讀),pipe_fd[0]->child_fd, pipe_fd[1]->parent_fd
//讀模式(父讀子寫),pipe_fd[0]->parent_fd,pipe_fd[1]->child_fd
if (!(fp = fdopen(parent_fd, modes))) { // 父程序讀或寫管道。返回檔案流指標。
close(parent_fd);
close(child_fd);
goto FREE_PI;
}
if ((pid = vfork()) == 0) { /* Child of vfork... */
close(parent_fd);
if (child_fd != zeroOrOne) {
dup2(child_fd, zeroOrOne);// 子程序會複製描述符到zeroOrOne,
close(child_fd);// 實際上是將管道端關聯重定向到stdin(主程序寫模式) 或關聯重定向到stdout(主程序讀模式)
}
execl("/bin/sh", "sh", "-c", command, (char *)0);
_exit(127);
}
if (pid > 0) { /* Parent of vfork... */
return fp;
}
/* If we get here, vfork failed. */
fclose(fp); /* Will close parent_fd. */
FREE_PI:
free(pi);
RET_NULL:
return NULL;
}
補充: dup2(child_fd, 0); 就是把child_fd 複製到stdin上,這樣子程序從stdin讀取,實際上是讀取的child_fd, 就是所謂的輸入重定向.
同理:dup2(child_fd, 1); 就是把child_fd 複製到stdout上,這樣子程序向stdout輸出資訊,實際上是向child_fd輸出資訊.
哈哈哈!!! 使用了偷樑換柱之法. 由此騙過了子程序. 子程序以為向stdout列印,實際上打到了管道的一端, 另一端連線的是父程序的讀端,被父程序讀走了. 同理,父程序打管道的一端搭在了子程序的stdin端, 父程序向管道寫東西,子程序以為是從stdin讀進來的呢! 很有趣! 是嗎? 這是欺騙的手段,或者溝通的橋樑!
2.2 popen應用1: 將ls命令的輸出逐行讀出到記憶體,再顯示到螢幕上,
//本例演示了popen,pclose的使用
#include <stdio.h>
int main()
{
FILE * fp;
char buf[20] = {0};
fp = popen("ls","r");
if(NULL == fp)
{
perror("popen error!\n");
return -1;
}
while(fgets(buf, 20, fp) != NULL)
{
printf("%s", buf);
}
pclose(fp);
return 0;
}
popen按讀方式開啟”ls”程式,已經將ls 輸出重定向到管道輸入端,我們的fp 是管道輸出端, 所以程式執行達到了目的.
2.3 popen應用2: 將應用程式的輸出儲存到一個變數中。
//執行一個shell命令,輸出結果逐行儲存在vecStr中,並返回行數
int readPipe(const char *cmd, vector<string> &vecStr) {
vecStr.clear();
FILE *pp = popen(cmd, "r"); //建立管道
if (!pp) {
return -1;
}
char buf[1024]; //設定一個合適的長度,以儲存每一行輸出
while (fgets(buf, sizeof(buf), pp) != NULL) {
if (buf[strlen(buf) - 1] == '\n') {
buf[strlen(buf) - 1] = '\0'; //去除換行符
}
vecStr.push_back(buf);
}
pclose(pp); //關閉管道
return vecStr.size();
}
//但是,當我們即想向pipe 寫, 又想從pipe 讀, 現成的popen 就不能勝任了,
//popen 只能建立一條管道,或者是讀管道,或者是寫管道。
//要想同時與程式實現讀,寫操作(雙向互動), 需要我們自己書寫程序程式碼.
//是的,通過兩個管道,一個讀管道,一個寫管道。下面給一個範例.
3.靈活使用pipe()函式
3.1 範例1,重定向子程序的stdin,stdout
這個範例演示了我們的父程序與子程序通訊, 並列印了與子程序的通訊內容.
子程序並不知道與它通訊的到底是人通過鍵盤跟它下命令,還是程式在跟它下命令,它的描述符已經被接到管道上了.
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#define READ 0
#define WRITE 1
// 注,回車有非常重要的作用,否則對端會阻塞在讀上而不能繼續
// printf 沒有"\n"也不會顯示列印,除非用fflush或程式退出。不信可以試試。
// 這些都是緩衝若得禍!
int doubleInteract(const char* cmdstring)
{
const char *sendStr = "hello,child\n";
int pipeR[2]; // 父讀子寫
int pipeW[2]; // 父寫子讀
pid_t pid;
char buf[1024];
int len=sizeof(buf);
memset(buf, 0, len);
/*初始化管道*/
pipe(pipeR);
pipe(pipeW);
if ((pid = fork()) < 0) return -1;
if (pid > 0) /* parent process */
{
close(pipeW[READ]);
close(pipeR[WRITE]);
read(pipeR[READ], buf, len); //由於這個len足夠大,讀不到回車又不夠len長度不返回。
// 讀到了東西,需要分析內容,做出正確迴應... , 更好的做法是啟動一個執行緒,專門接受管道輸入
// 這裡只簡單迴應"hello..."
printf("child:%s", buf); // 這裡會寫到螢幕上
write(pipeW[WRITE], sendStr, strlen(sendStr)+1); // 這裡把"..." 發到管道上
printf("parent:%s", sendStr);
#if 1
memset(buf,0,len);
read(pipeR[READ], buf, len); //再讀child 響應
printf("child:%s", buf); // 這裡會寫到螢幕上
#endif
close(pipeW[WRITE]);
close(pipeR[READ]);
waitpid(pid, NULL, 0);
}
else /* child process, 關閉寫管道的寫端,讀管道的讀端,與父程序正好相對 */
{
close(pipeW[WRITE]);
close(pipeR[READ]);
//重定向子程序的標準輸入,標準輸出到管道端
if (pipeW[READ] != STDOUT_FILENO)
{
if (dup2(pipeW[READ], STDIN_FILENO) != STDIN_FILENO)
{
return -1;
}
close(pipeW[READ]);
}
if (pipeR[WRITE] != STDOUT_FILENO)
{
if (dup2(pipeR[WRITE], STDOUT_FILENO) != STDOUT_FILENO)
{
return -1;
}
close(pipeR[WRITE]);
}
execl("/bin/sh", "sh", "-c", cmdstring, (char*)0);
exit(127);
}
return 0;
}
int main()
{
doubleInteract("./myShell");
return 0;
}
//這裡給一個myShell指令碼範例,從鍵盤輸入,向螢幕輸出.如下:
#!/bin/bash
echo "hello! say Something"
read
echo "you say:$REPLY,bye bye!"
3.2另一個簡單的父子程序雙向互動的例子,
該例沒有采用把管道端向輸入或輸出重定向的技術,
而是直接用pipe通訊, 父子程序協作,將數值從0加到10;
規則是從管道中拿到數,加1,再把數推出去。
$ cat main.cpp
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#define READ 0
#define WRITE 1
int main(void)
{
int x;
pid_t pid;
int pipe1[2],pipe2[2];
/*初始化管道*/
pipe(pipe1);
pipe(pipe2);
pid = fork();
if(pid < 0)
{
printf("create process error!/n");
exit(1);
}
if(pid == 0) //子程序
{
close(pipe1[WRITE]);
close(pipe2[READ]);
do
{
read(pipe1[READ],&x,sizeof(int));
printf("child %d read: %d\n",getpid(),x++);
write(pipe2[WRITE],&x,sizeof(int));
}while(x<=9);
//讀寫完成後,關閉管道
close(pipe1[READ]);
close(pipe2[WRITE]);
exit(0);
}
else if(pid > 0) //父程序
{
close(pipe2[WRITE]);
close(pipe1[READ]);
x = 1;
//每次迴圈向管道1 的1 端寫入變數X 的值,並從
//管道2 的0 端讀一整數寫入X 再對X 加1,直到X 大於10
do{
write(pipe1[WRITE],&x,sizeof(int));
read(pipe2[READ],&x,sizeof(int));
printf("parent %d read: %d\n",getpid(),x++);
}while(x<=9);
//讀寫完成後,關閉管道
close(pipe1[WRITE]);
close(pipe2[READ]);
waitpid(pid,NULL,0);
exit(0);
}
}
範例均經過測試,可直接使用。