簡體   English   中英

了解如何連接管道並在自定義C shell中等待它們

[英]Understanding how to connect pipes and wait for them in a custom C shell

編輯更改標題,因為問題不再只是如何連接它們,還包括如何等待它們。 更新我解決了該問題,並在下面更新了我的等待處理代碼以反映現在正在運行的代碼。 等待最后一個子命令之前,我需要關閉所有管道。 以前我是在那之后做的。

我正在將CLI編寫為Linux GNU99 C中的任務,並且目前正在實現管道。 最初,我認為我的問題與管道的連接方式有關,因為我沒有得到理想的結果。 現在我已經意識到,這也與我等待被鏈接的子命令的方式有關。

作為模板,我使用以下命令: ls|grep "hello"|sort -r LS輸出到GREP,GREP輸出到SORT,SORT輸出到stdout。 (常見命令序列)。

參考下圖:

在各自的子進程中,
對於LS ,不使用文件描述符(FD)3、5、6
對於GREP ,不使用文件描述符(FD)4和5
對於SORT ,不使用文件描述符(FD)3,4和6

對於LS dup2(4,STDOUT_FILENO) (將其stdout綁定到fd 4)
對於GREP dup2(3,STDIN_FILENO)和dup2(6,STDOUT_FILENO) (將stdin / stdout綁定到各自的fds)
對於SORT dup2(5,STDIN_FILENO) (將stdin綁定到fd 5)

每個孩子中,完成DUP2()之后,我將關閉所有文件描述符(3-6),然后再通過execvp()將控制權傳遞給實際命令。
進程中,啟動所有 進程后,我 關閉所有文件描述符 (3-6)。 (將其移至啟動器中,請參見下面的代碼。)

//                                              
//  fd#           ls---\
//                     |
//  3        /-----R   |
//           |     |   |
//  4        |     W --/
//           |
//           |
//           \----grep--\
//                      |  
//  5        /-----R    |
//           |     |    |
//  6        |     W ---/
//           |
//           \----sort
//

編輯
感謝'mah'對早期的信心增強,以及'Jon'對稍后的詳細解釋。

我實際上以為我可以一次完成所有工作。 但是,事實證明,僅當所有子命令都作為后台進程執行時。 很好,但不是我想要的,因為后台進程在命令行末尾需要& ,並且最終輸出未與提示同步。

從目前的情況來看,我似乎在一個管道中使用了命令,例如:ls | sort,始終在前台工作,但是當我引入第二個管道(例如:ls | grep | sort)時,有時在復合詞時會打印我的提示命令仍在輸出,這意味着它在后台運行,而不是在前台運行。

這是我的代碼的解釋:

該外殼程序允許用戶鍵入多個命令,這些命令由;分隔; 不使用管道的單個和多個命令都可以很好地用作前台進程和后台。 我還實現了一個“源”命令,該命令能夠在腳本調用另一個腳本時重復出現。

因此,我唯一剩下的問題是使用管道的復合命令。

按照標准解析,我將用戶的輸入分解為以NULL字符分隔的令牌。 我保留了一個指向每個令牌的指針數組(代表命令和參數),並保留了一個並行數組來跟蹤命令。 我認為是相當標准的方法。

我使用管道處理復合命令的策略是盡可能長地將它們視為單個命令。 當需要時,這使子命令與管道的連接更加容易(因為我不必通過程序傳遞額外的信息)。 因此,我設計了解析器,為管道字符分配了一個單獨的令牌。 因此,在我的launchControl功能,這就要求我發射功能(如fork()和execvp()是),我做的子命令的最后准備。

最終的准備工作涉及幾個步驟:
(1)用NULL令牌替換管道令牌(因此將序列拆分為與execvp()兼容的子命令,
(2)確定哪些標記是子命令(與子命令的參數相反),
(3)確定哪個子命令讀取(寫入)哪個管道。

完成這些步驟后,我進入一個循環,該循環將每個子命令的必要信息傳遞給啟動功能本身。 完成此循環后,我關閉所有創建的管道。 這是我的啟動功能的簽名:
int launch (char **tokenList, enum ioTypes procType, int pipeIn, int pipeOut, int *allPipes, enum processType pType)
tokenList是子命令令牌(后跟其參數),
procType (為none,out,in或兩者都不是),並描述其與管道使用的關系,
pipeIn是子命令的輸入文件描述符(如果未使用,則為0),
pipeOut是子命令的輸出文件描述符(如果未使用,則為0),
allPipes是復合命令中使用的所有管道的列表,
pType指示命令是否要運行前台/后台。

(我正在使用Signal處理程序來允許后台任務在完成時報告,與在bash中一樣。)

啟動功能(用於涉及管道的命令)執行以下操作:

阻止SIGCHLD延遲SIGCHLD直到我進入最后一個子命令。
創建fork(),然后使用switch語句:
在孩子中:(案例處理== 0)
根據procType ,將調用dup2將子命令stdin / stdout連接到適當的文件描述符(請參見上圖)。
根據allPipes關閉所有管道(包括dup2函數中使用的管道)
如有必要,請執行重定向。
使用tokenList的子命令/參數調用execvp()
在父母中:(默認情況下)
如果當前子命令是序列中的最后一個命令,則我取消阻止SIGCHLD,
這就是我的問題所在。
下面的代碼是WIP,在某種程度上可以正常工作,但是不太正確。 這是我目前的嘗試。

    //allPipes = NULL for a command that doesn't use pipes.
    // procType == in, only occurs for the last sub-command in the sequence.
    if ( (allPipes == NULL) || ( (allPipes != NULL) && (procType == in) )) {
        if (allPipes != NULL) {
            for (int i=2; i<allPipes[0]; i++) { // Parent closes all pipes.
                close(allPipes[i]);
            }
        }
        int status; // int where child status will be recorded
        pid_t pid;
        do {
            pid = waitpid(WAIT_ANY, &status,0);
//                      fprintf(stderr,"Got a PID = %d\n",pid);
        } while (pid >0);
        if (pid == -1 && !(errno == ECHILD)) {
            perror(NULL);
            exit(errno);
        }
    }

這個版本對於ls | sort來說似乎可以正常工作,因為我有耐心進行測試,所以它可以重復執行多個命令。
但是,當我執行命令ls | sort | grep時,它變得不可靠。 通常,它在前兩次運行良好,但是之后,我的提示開始出現在我的輸出中間,這意味着它在后台運行。

@mah:
這是我的用於跟蹤命令和管道以及如何連接它們的代碼:

struct pipefdRecord {
    int pos;        //  Position of the pipe in the token list
    int aPipe[2];   //  pipe file descriptor [0] read / [1] write
}   pipefdRecord;

struct cmdRecord {
    char **command;     // Pointer to the sub-command token
    int ndxCommand;     // Position of command token in the token list
    enum ioTypes mode;  // none (0), output(1), input(2), or both(3)
    int pipeIn;         // pipe fd assigned to this process' input
    int pipeOut;        // pipe fd assigned to this process' output
}   cmdRecord;


struct pipefdRecord *pipesAt = malloc(sizeof(struct pipefdRecord));
struct cmdRecord * cmdList = (struct cmdRecord *)malloc(sizeof(struct cmdRecord));

for (int i=0; i<noCommands; i++) { // writing side of pipes
    for (int j=0; j<noPipes; j++) {
        if ((cmdList[i].ndxCommand < pipesAt[j].pos) && (pipesAt[j].aPipe[1] !=0)) {
            cmdList[i].pipeOut = pipesAt[j].aPipe[1];  // assign writing
            pipesAt[j].aPipe[1]=0;
            cmdList[i].mode = out;
            break;
        }
    }
}
for (int i=noCommands-1; i>=0; i--) { // reading side of pipes
    for (int j=noPipes-1; j>=0; j--) {
        if (cmdList[i].ndxCommand > pipesAt[j].pos && (pipesAt[j].aPipe[0] !=0)) {
            cmdList[i].pipeIn = pipesAt[j].aPipe[0];  // assign reading
            pipesAt[j].aPipe[0]=0;
            cmdList[i].mode = cmdList[i].mode | in;
            break;
        }
    }
}

使用上面的代碼,我的管道分配對於任意數量的管道總是正確的。

午睡

我認為您缺少一些關閉,但是您很幸運,缺少關閉不會阻止您的代碼正常工作。

從您的描述中看來,您創建了兩個管道,並且返回的描述符為3、4、5、6。

您應該執行的操作是這樣(我從文件描述符名稱中刪除_FILENO ):

  • lsdup2(4, STDOUT) 關閉每個文件描述符3、4、5、6。
  • sortdup2(3, STDIN) dup2(6, STDOUT) ; 關閉每個文件描述符3、4、5、6。
  • grepdup2(5, STDIN) 關閉每個文件描述符3、4、5、6。
  • 在父級中,應該關閉所有文件描述符,然后執行。 好!

注意共同的主題:關閉所有管道文件描述符!

如果不這樣做會怎樣?

  • ls :打開文件描述符4(將管道的末尾從ls寫入sort )打開; 這與ls所做的事情無關緊要,並且事不宜遲地退出,關閉了管道的這一端。
  • sort :打開了文件描述符3(從ls讀取管道的末尾到sort )和6(從sort寫入管道的末尾到grep )。 ls退出時,文件描述符3將報告EOF。 sort完成后,它將關閉6。由於管道的寫端( ls文件描述符1和4, sort 4)都已關閉,所以在讀取ls的最后一個輸出后, sort應該得到干凈的EOF。 請注意, sort在生成任何輸出之前會先讀取其所有輸入。
  • grep :您留下了文件描述符5(從sortgrep讀取管道的結尾)。 在適當的時候,當sort寫入其數據時, grep將能夠讀取它。 sort完成后, grep將在其標准輸入上獲得EOF。

因此,在此示例中,您設法制作了可以正常工作的管道。 但是,總的來說,您仍然應該關閉更多的文件描述符,因為否則很容易以管道的開放寫入端結束,從而阻止程序完成。 例如,如果grep尚未關閉4,則sort將等待來自grep輸入,而grep將等待來自sort輸入,並且直到另一個完成之前,兩個都不會讓步-死鎖。

問題的更正

在您的描述中,您說:

  • 對於LS dup2(4,STDOUT_FILENO)(而不是向STDIO進行操作,它轉到fd 4)
  • 對於SORT dup2(3,STDIN_FILENO)和dup2(6,STDOUT_FILENO)(不使用std(in / out))
  • 對於GREP dup2(5,STDIN_FILENO)(不是讀取STDIO,而是從fd 5讀取)

您尚未描述正確的處理方法。 dup2(4, STDOUT)函數確保標准輸出(文件描述符1)指向與文件描述符4相同的打開文件描述。 dup2()仔細閱讀open()dup2()以區分打開文件描述符和打開文件描述。 !)這意味着當成為ls的孩子寫到標准輸出時,它正在寫到第一個管道的寫端,這意味着它將轉到grep ls程序將一如既往地繼續進行,並寫入標准輸出。 只是標准輸出與文件描述符4相同。

類似的評論適用於其他每個陳述。 sort從標准輸入讀取並寫入標准輸出; grep從標准輸入讀取並寫入標准輸出。 dup2()調用確保這些都是對相關管道的引用,僅此dup2()

注意,重復的描述符可以獨立關閉而不影響其他描述符。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM