[英]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
):
ls
: dup2(4, STDOUT)
; 關閉每個文件描述符3、4、5、6。 sort
: dup2(3, STDIN)
; dup2(6, STDOUT)
; 關閉每個文件描述符3、4、5、6。 grep
: dup2(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(從sort
到grep
讀取管道的結尾)。 在適當的時候,當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.