2015-07-20 129 views
5

我正在創建一個可以讀取命令的小外殼。當我運行我的程序並鍵入:"cat file.txt > file2.txt"它創建文件,然後它卡在行:if(execvp(structVariables->argv[0], argv) < 0).(等待輸入/輸出??)。如果我用ctrl + d結束程序,我可以在我的文件夾中看到該文件已創建,但沒有寫入任何內容。 (dupPipe是用來處理更多的指令,還沒有使用,因爲上述問題)程序卡住了,管道文件描述符何時打開?

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 
       redirect(structVariables->outfile, flags, STDOUT_FILENO); 
     } 
     if(structVariables->infile != NULL) 
     { 
       flags = 2;  // Read 
       redirect(structVariables->infile, flags, STDIN_FILENO); 
     } 

     if(execvp(structVariables->argv[0], argv) < 0) 
     { 
       perror("execvp error"); 
       exit(EXIT_FAILURE); 
     } 
} 

兩個功能我在程序中使用是這樣的: dupPipe和重定向

int dupPipe(int pip[2], int end, int destinfd) 
{ 
    if(end == READ_END) 
    { 
     dup2(pip[0], destinfd); 
     close(pip[0]); 
    } 
    else if(end == WRITE_END) 
    { 
     dup2(pip[1], destinfd); 
     close(pip[1]); 
    } 

    return destinfd; 
} 

int redirect(char *filename, int flags, int destinfd) 
{ 
     int newfd; 

     if(flags == 1) 
     { 
       if(access(filename, F_OK) != -1)  // If file already exists 
       { 
         errno = EEXIST; 
         printf("Error: %s\n", strerror(errno)); 
         return -1; 
       } 

       newfd = open(filename, O_WRONLY | O_CREAT | O_EXCL, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH); 
       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; 
} 
+0

目前尚不清楚你想要做什麼。請發佈完整的代碼,它是如何調用的樣本,以及您期望發生的事情。 – dbush

回答

7

看起來你正在試圖編寫一個shell來運行從輸入讀取的命令(如果不是這種情況,請編輯你的問題,因爲它不清楚)。

我不知道你爲什麼認爲管道被用在像cat file.txt > file2.txt這樣的命令中,但是無論如何它們都不是。讓我們來看看引擎蓋下會發生什麼,當你在一個殼狀的bash鍵入cat file.txt > file2.txt:創建

  1. 孩子的過程,其中cat(1)運行。
  2. 孩子的過程打開file2.txt寫作(稍後更多)。
  3. 如果open(2)成功,則子進程將新打開的文件描述符複製到stdout(因此stdout將實際指向與file2.txt相同的文件表項)。
  4. cat(1)通過調用七個exec()函數之一來執行。參數file.txt傳遞給cat(1),因此cat(1)將打開file.txt並讀取所有內容,將其內容複製到stdout(重定向到file2.txt)。
  5. cat(1)完成執行並終止,這會導致任何打開的文件描述符被關閉和刷新。當cat(1)終止時,file2.txtfile.txt的副本。
  6. 同時,在打印下一個提示符並等待更多命令之前,父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。出於某種原因,如果newfddestinfd碰巧是相同的呢?那麼你會錯誤地關閉文件,因爲如果你傳入兩個相同的文件描述符,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.hfcntl.h)。您可能需要使用#define以使其更清潔,但如果您這樣做,則仍然認爲它更好,更具描述性,而不是硬編碼一些幻數(但這是主觀的)。

我不會對dupPipe()發表評論,因爲在這個問題中不需要/使用它。 I/O重定向是您所需要的。如果您想將討論擴展到管道,請隨時編輯問題或創建另一個問題。

UPDATE

好了,現在我有一個看看完整的源代碼,我有一對夫婦更多的言論。

原因cat(1)懸掛是因爲這樣:

if (execvp(structVariables->argv[0], argv) < 0) 

execvp(2)的第二個參數應該是structVariables->argvargv,因爲argv是殼程序的參數數組,這是(通常是)空着。將一個空的參數列表傳遞給cat(1)使其從stdin而不是從文件中讀取,這就是它爲什麼會掛起的原因 - 它正在等待您提供輸入。因此,繼續與替換該行:

if (execvp(structVariables->argv[0], structVariables->argv) < 0) 

這解決您的問題之一:之類的東西cat <file.txt> file2.txt將現在的工作(我測試)。

關於管道重定向

所以現在我們需要在管道重定向工作。每當我們在命令行上看到|時,都會發生管道重定向。讓我們通過一個示例來了解當我們輸入ls | grep "file.txt" | sort時發生的情況。瞭解這些步驟非常重要,以便您可以建立系統工作原理的精確模型;如果沒有這樣的設想,你就不會真正理解實現:

  1. shell(通常)首先通過管道符號分割命令。這也是你的代碼所做的。這意味着在解析之後,shell已經收集到足夠的信息,並且命令行被分成3個實體(ls命令,grep命令和sort命令)。
  2. 殼牌分叉並呼叫兒童上的七個exec()功能之一運行ls。現在請記住,管道意味着程序的輸出是下一個的輸入,因此在exec()之前,外殼必須創建管道。將要運行ls(1)的子進程在exec()之前調用dup2(2)將管道的寫入通道複製到stdout。同樣,父進程調用dup2(2)將管道的讀取通道複製到stdin。理解這一步非常重要:因爲父母將管道的讀取端複製到stdin,那麼我們接下來做的任何事情(例如再次分叉以執行更多命令)將始終從管道讀取輸入。所以,在這一點上,我們有ls(1)寫入stdout,它被重定向到由shell的父進程讀取的管道。

  3. 該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)讀取結果。

  4. 最後,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_ENDWRITE_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; 
} 

玩你的殼!

+0

這個想法是,我應該能夠輸入ex:cat file.txt |尾部-5> file2.txt。但是,即使我編寫了cat file.txt> file2.txt,我發現它不起作用,所以這就是爲什麼不需要管道(還)。無論如何感謝回答!我根據你輸入的內容更新了我的代碼,並且發現我做錯了,但仍然無效......程序在執行execvp時仍然卡住。然後我使用ctrl + d並查找該文件,並且它實際上已經創建。沒有什麼。你能找到其他錯誤的東西嗎? – Fjodor

+0

@Fjodor,但現在可以使用'cat file.txt> file2.txt'嗎?在我們討論管道之前,我需要知道這個問題是否已經解決。 –

+0

對不起,也許我不清楚。問題仍然存在,但我明白爲什麼我不應該涉及管道。所以不,cat file.txt> file2.txt只是把程序「擱置」。但正如我所提到的,我可以在我的文件夾中找到該文件,但我必須用ctrl + d退出該程序。 – Fjodor

2

execvp做除非有錯誤才能返回。

因此,原始程序將(通常)不執行超出調用代碼execvp()

代碼的正常順序是:

1) fork() 
2) if child then call execvp(); 
3) if parent .... 
+0

我不明白爲什麼在孩子之後或之前安置父母會幫助我解決問題。 – Fjodor

0

你在redirect()如果使用不當open()flags == 1

if(flags == 1) 
    { 
      if(access(filename, F_OK) != -1)  // If file already exists 
      { 
        errno = EEXIST; 
        printf("Error: %s\n", strerror(errno)); 
        return -1; 
      } 
      newfd = open(filename, O_CREAT, O_WRONLY); 
      if(newfd == -1) 
      { 
        perror("Open for write failed"); 
        return -1; 
      } 
    } 

newfd = open(filename, O_CREAT, O_WRONLY);O_WRONLY是(不正確LY)代替mode說法用來open(),而不是在flags或運算的結果:

if(flags == 1) 
    { 
      if(access(filename, F_OK) != -1)  // If file already exists 
      { 
        errno = EEXIST; 
        printf("Error: %s\n", strerror(errno)); 
        return -1; 
      } 
      newfd = open(filename, O_CREAT | O_WRONLY, mode); //whatever mode you want, but remember umask. 
      if(newfd == -1) 
      { 
        perror("Open for write failed"); 
        return -1; 
      } 
    } 

此外,對於文件的以前存在的檢查是活潑的,另一個程序可以創建後的文件access()之前和open()。使用open(filename, O_CREAT | O_EXCL, mode)自動創建並打開一個文件。

+0

我已經創建了一個模式,但我不確定如何使用umask,之後我是否會調用它? – Fjodor

+0

如果你想改變你的進程的文件創建掩碼,你需要在open()/ creat()之前調用'umask()'*。如果你不知道該怎麼做,你應該閱讀'umask()'的手冊頁,但是如果你很着急,你可以調用'umask(0)',在這種情況下''' open()'將被不加修改地使用。 – EOF

+0

好的,我通過在open(3)中輸入0666來修復它。現在,當我想在文件創建後打開文件時,它就可以工作。然而,文件中沒有任何內容......這表明管道有問題嗎?我找不到解決方案... – Fjodor

相關問題