看起來你正在試圖編寫一個shell來運行從輸入讀取的命令(如果不是這種情況,請編輯你的問題,因爲它不清楚)。
我不知道你爲什麼認爲管道被用在像cat file.txt > file2.txt
這樣的命令中,但是無論如何它們都不是。讓我們來看看引擎蓋下會發生什麼,當你在一個殼狀的bash鍵入cat file.txt > file2.txt
:創建
- 孩子的過程,其中
cat(1)
運行。
- 孩子的過程打開
file2.txt
寫作(稍後更多)。
- 如果
open(2)
成功,則子進程將新打開的文件描述符複製到stdout
(因此stdout
將實際指向與file2.txt
相同的文件表項)。
cat(1)
通過調用七個exec()
函數之一來執行。參數file.txt
傳遞給cat(1)
,因此cat(1)
將打開file.txt
並讀取所有內容,將其內容複製到stdout
(重定向到file2.txt
)。
cat(1)
完成執行並終止,這會導致任何打開的文件描述符被關閉和刷新。當cat(1)
終止時,file2.txt
是file.txt
的副本。
- 同時,在打印下一個提示符並等待更多命令之前,父shell進程會等待子進程終止。
如您所見,管道不用於I/O重定向。管道是一種進程間通信機制,用於將進程的輸出提供給另一進程的輸入。你只有一個進程在這裏運行(cat
),那麼爲什麼你甚至需要管道?
這意味着你應該調用redirect()
STDOUT_FILENO
作爲destinfd
(而不是管道通道)輸出重定向。同樣,輸入重定向應該調用redirect()
和STDIN_FILENO
。這些常量在unistd.h
中定義,因此請確保包含該標題。
你也可能想要退出的孩子,如果exec()
失敗,否則你將運行2個shell進程的副本。
最後但並非最不重要的是,您不應該使輸入或輸出重定向排他。可能是用戶想要輸入和輸出重定向的情況。所以,而不是else if
做I/O重定向時,我只會使用2個獨立的ifs。
考慮到這一點,主要的代碼你貼應該是這個樣子:
if((pid = fork()) < 0)
{
perror("fork error");
}
else if(pid > 0) // Parent
{
if(waitpid(pid,NULL,0) < 0)
{
perror("waitpid error");
}
}
else // Child
{
int flags = 0;
if(structVariables->outfile != NULL)
{
flags = 1; // Write
// We need STDOUT_FILENO here
redirect(structVariables->outfile, flags, STDOUT_FILENO);
}
if(structVariables->infile != NULL)
{
flags = 2; // Read
// Similarly, we need STDIN_FILENO here
redirect(structVariables->infile, flags, STDIN_FILENO);
}
// This line changed; see updated answer below
if(execvp(structVariables->argv[0], structVariables->argv) < 0)
{
perror("execvp error");
// Terminate
exit(EXIT_FAILURE);
}
}
作爲另一個答覆中提到,您redirect()
功能是容易出現競爭情況,因爲有時間的文件之間的窗口存在檢查和另一個進程可以創建文件的實際文件創建(這稱爲TOCTTOU錯誤:檢查到使用時間的時間)。您應該使用O_CREAT | O_EXCL
自動測試存在並創建文件。
另一個問題是,你總是關閉newfd
。出於某種原因,如果newfd
和destinfd
碰巧是相同的呢?那麼你會錯誤地關閉文件,因爲如果你傳入兩個相同的文件描述符,dup2(2)
本質上是一個無操作。即使您認爲這種情況永遠不會發生,在關閉原件之前先檢查重複的fd是否與原始fd不同,總是一個好習慣。
下面是這些問題的解決代碼:
int redirect(char *filename, int flags, int destinfd)
{
int newfd;
if(flags == 1)
{
newfd = open(filename, O_WRONLY | O_CREAT | O_EXCL, 0666);
if(newfd == -1)
{
perror("Open for write failed");
return -1;
}
}
else if(flags == 2)
{
newfd = open(filename, O_RDONLY);
if(newfd == -1)
{
perror("Open for read failed");
return -1;
}
}
else
return -1;
if(dup2(newfd, destinfd) == -1)
{
perror("dup2 failed");
close(newfd);
return -1;
}
if (newfd != destinfd)
close(newfd);
return destinfd;
}
考慮open(2)
以上S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH
更換0666
(確保包括sys/stat.h
和fcntl.h
)。您可能需要使用#define
以使其更清潔,但如果您這樣做,則仍然認爲它更好,更具描述性,而不是硬編碼一些幻數(但這是主觀的)。
我不會對dupPipe()
發表評論,因爲在這個問題中不需要/使用它。 I/O重定向是您所需要的。如果您想將討論擴展到管道,請隨時編輯問題或創建另一個問題。
UPDATE
好了,現在我有一個看看完整的源代碼,我有一對夫婦更多的言論。
原因cat(1)
懸掛是因爲這樣:
if (execvp(structVariables->argv[0], argv) < 0)
到execvp(2)
的第二個參數應該是structVariables->argv
,不argv
,因爲argv
是殼程序的參數數組,這是(通常是)空着。將一個空的參數列表傳遞給cat(1)
使其從stdin
而不是從文件中讀取,這就是它爲什麼會掛起的原因 - 它正在等待您提供輸入。因此,繼續與替換該行:
if (execvp(structVariables->argv[0], structVariables->argv) < 0)
這解決您的問題之一:之類的東西cat <file.txt> file2.txt
將現在的工作(我測試)。
關於管道重定向
所以現在我們需要在管道重定向工作。每當我們在命令行上看到|
時,都會發生管道重定向。讓我們通過一個示例來了解當我們輸入ls | grep "file.txt" | sort
時發生的情況。瞭解這些步驟非常重要,以便您可以建立系統工作原理的精確模型;如果沒有這樣的設想,你就不會真正理解實現:
- shell(通常)首先通過管道符號分割命令。這也是你的代碼所做的。這意味着在解析之後,shell已經收集到足夠的信息,並且命令行被分成3個實體(
ls
命令,grep
命令和sort
命令)。
殼牌分叉並呼叫兒童上的七個exec()
功能之一運行ls
。現在請記住,管道意味着程序的輸出是下一個的輸入,因此在exec()
之前,外殼必須創建管道。將要運行ls(1)
的子進程在exec()
之前調用dup2(2)
將管道的寫入通道複製到stdout
。同樣,父進程調用dup2(2)
將管道的讀取通道複製到stdin
。理解這一步非常重要:因爲父母將管道的讀取端複製到stdin
,那麼我們接下來做的任何事情(例如再次分叉以執行更多命令)將始終從管道讀取輸入。所以,在這一點上,我們有ls(1)
寫入stdout
,它被重定向到由shell的父進程讀取的管道。
該shell現在將執行grep(1)
。再次,它推出了一個新的流程來執行grep(1)
。請記住,文件描述符是通過fork繼承的,並且父shell的進程將stdin
綁定到連接到ls(1)
的管道的讀端,因此將執行grep(1)
的新子進程將從該管道「自動」讀取!但是,等等,還有更多!shell知道管道中還有另一個進程(sort
命令),因此在執行grep之前(以及之前分叉),shell創建另一個管道,以將grep(1)
的輸出連接到sort(1)
的輸入。然後,它重複相同的步驟:在子進程中,管道的寫入通道被複制到stdout
上。在父項中,管道的讀取通道複製到stdin
上。再一次,真正理解這裏發生的事情非常重要:即將執行的進程grep(1)
已經從連接到ls(1)
的管道讀取其輸入,現在它的輸出已連接到將輸入sort(1)
的管道。所以grep(1)
基本上是從管道讀取並寫入管道。 OTOH,母殼程序將最後一個管道的讀取通道複製到stdin
,從讀取ls(1)
的輸出(因爲grep(1)
將處理它)有效地「放棄」,而是更新輸入流以從grep(1)
讀取結果。
最後,shell看到sort(1)
是最後一個命令,所以它只是分叉+執行sort(1)
。結果被寫入stdout
,因爲我們從來沒有在外殼工藝改變stdout
,但輸入是從那麼這是怎麼實現的,因爲我們在第3步
動作連接grep(1)
到sort(1)
管道讀?
簡單:只要有多個命令需要處理,我們就創建一個管道和叉子。在孩子身上,我們關閉了管道的讀取通道,將管道的寫入通道複製到stdout
上,並呼叫七個exec()
函數中的一個。在父級,我們關閉管道的寫通道,並將管道的讀通道複製到stdin
。
當只有一個命令要處理時,我們只需fork + exec,而不創建管道。
只有最後一個細節需要澄清:在開始pipe(2)
重定向派對之前,我們需要存儲對原始shell標準輸入的引用,因爲我們將(可能)在整個過程中多次更改它。如果我們沒有保存它,我們可能會丟失對原始文件stdin
的引用,然後我們將無法再讀取用戶輸入!在代碼中,我通常使用fcntl(2)
和F_DUPFD_CLOEXEC
(請參閱man 2 fcntl
)執行此操作,以確保在子進程中執行命令時描述符是關閉的(在使用子進程時通常保留打開的文件描述符)。
此外,shell進程需要wait(2)
上的上一個進程正在流水線中。如果你仔細想想,這是有道理的:管道固有地同步管道中的每個命令;只有當最後一條命令從管道讀取EOF
(即我們知道我們只在所有數據在整個管道中流動時才完成)時,該命令集纔會被覆蓋。如果shell沒有等待最後一個進程,而是等待管道中間(或開始處)的其他進程,它會很快返回到命令提示符,並讓其他命令仍然運行在後臺 - 不是一個明智的舉動,因爲用戶希望shell在等待更多內容之前完成當前作業的執行。
所以...這是很多的信息,但它是非常重要的,你理解它。因此,修改後的主要代碼是在這裏:
int saved_stdin = fcntl(STDIN_FILENO, F_DUPFD_CLOEXEC, 0);
if (saved_stdin < 0) {
perror("Couldn't store stdin reference");
break;
}
pid_t pid;
int i;
/* As long as there are at least two commands to process... */
for (i = 0; i < n-1; i++) {
/* We create a pipe to connect this command to the next command */
int pipefds[2];
if (pipe(pipefds) < 0) {
perror("pipe(2) error");
break;
}
/* Prepare execution on child process and make the parent read the
* results from the pipe
*/
if ((pid = fork()) < 0) {
perror("fork(2) error");
break;
}
if (pid > 0) {
/* Parent needs to close the pipe's write channel to make sure
* we don't hang. Parent reads from the pipe's read channel.
*/
if (close(pipefds[1]) < 0) {
perror("close(2) error");
break;
}
if (dupPipe(pipefds, READ_END, STDIN_FILENO) < 0) {
perror("dupPipe() error");
break;
}
} else {
int flags = 0;
if (structVariables[i].outfile != NULL)
{
flags = 1; // Write
if (redirect(structVariables[i].outfile, flags, STDOUT_FILENO) < 0) {
perror("redirect() error");
exit(EXIT_FAILURE);
}
}
if (structVariables[i].infile != NULL)
{
flags = 2; // Read
if (redirect(structVariables[i].infile, flags, STDIN_FILENO) < 0) {
perror("redirect() error");
exit(EXIT_FAILURE);
}
}
/* Child writes to the pipe (that is read by the parent); the read
* channel doesn't have to be closed, but we close it for good practice
*/
if (close(pipefds[0]) < 0) {
perror("close(2) error");
break;
}
if (dupPipe(pipefds, WRITE_END, STDOUT_FILENO) < 0) {
perror("dupPipe() error");
break;
}
if (execvp(structVariables[i].argv[0], structVariables[i].argv) < 0) {
perror("execvp(3) error");
exit(EXIT_FAILURE);
}
}
}
if (i != n-1) {
/* Some error caused an early loop exit */
break;
}
/* We don't need a pipe for the last command */
if ((pid = fork()) < 0) {
perror("fork(2) error on last command");
}
if (pid > 0) {
/* Parent waits for the last command to execute */
if (waitpid(pid, NULL, 0) < 0) {
perror("waitpid(2) error");
}
} else {
int flags = 0;
/* Execute last command. This will read from the last pipe we set up */
if (structVariables[i].outfile != NULL)
{
flags = 1; // Write
if (redirect(structVariables[i].outfile, flags, STDOUT_FILENO) < 0) {
perror("redirect() error");
exit(EXIT_FAILURE);
}
}
if (structVariables[i].infile != NULL)
{
flags = 2; // Read
if (redirect(structVariables[i].infile, flags, STDIN_FILENO) < 0) {
perror("redirect() error");
exit(EXIT_FAILURE);
}
}
if (execvp(structVariables[i].argv[0], structVariables[i].argv) < 0) {
perror("execvp(3) error on last command");
exit(EXIT_FAILURE);
}
}
/* Finally, we need to restore the original stdin descriptor */
if (dup2(saved_stdin, STDIN_FILENO) < 0) {
perror("dup2(2) error when attempting to restore stdin");
exit(EXIT_FAILURE);
}
if (close(saved_stdin) < 0) {
perror("close(2) failed on saved_stdin");
}
約dupPipe()
最後的一些言論:
- 兩個
dup2(2)
和close(2)
可能會返回一個錯誤;你應該檢查這一點,並採取相應的行動(即通過返回-1將錯誤傳遞給調用堆棧)。
- 同樣,在複製它之後,不應該盲目地關閉描述符,因爲它可能是源和目標描述符相同的情況。
- 您應該驗證
end
要麼READ_END
或WRITE_END
,如果這是不正確的返回一個錯誤(而不是返回destinfd
不管是什麼,這可能會給成功的錯覺給調用者代碼)
以下是我將如何改進它:
int dupPipe(int pip[2], int end, int destinfd)
{
if (end != READ_END && end != WRITE_END)
return -1;
if(end == READ_END)
{
if (dup2(pip[0], destinfd) < 0)
return -1;
if (pip[0] != destinfd && close(pip[0]) < 0)
return -1;
}
else if(end == WRITE_END)
{
if (dup2(pip[1], destinfd) < 0)
return -1;
if (pip[1] != destinfd && close(pip[1]) < 0)
return -1;
}
return destinfd;
}
玩你的殼!
目前尚不清楚你想要做什麼。請發佈完整的代碼,它是如何調用的樣本,以及您期望發生的事情。 – dbush