簡體   English   中英

關閉未使用的管道文件描述符

[英]Closing unused pipe file descriptors

我試圖理解為什么需要關閉文件描述符的基本原因。 我知道讀者端關閉寫描述符的原因。 但是,相反地,我無法看到(模擬)編寫側關閉讀取描述符的原因。 我嘗試以下一項,

當某個進程嘗試寫入沒有進程具有打開的讀取描述符的管道時,內核會將SIGPIPE信號發送到寫入進程。 默認情況下,此信號會終止進程。

來源,Linux編程接口,Michael Kerrisk

錯誤時返回write() ,返回-1 ,並正確設置errno。 EPIPE fd連接到讀數端關閉的管道或插座。 發生這種情況時,寫入過程還將收到SIGPIPE信號。 (因此,僅當程序捕獲,阻止或忽略此信號時,才能看到寫返回值。)

來源,手冊頁。

為此,我在fork()之前關閉已讀取的描述符。 但是,我既無法捕獲SIGPIPE ,也無法通過perror()打印write()的錯誤。

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/param.h>
#include <signal.h>
#define BUFSIZE 100

char const * errMsgPipe = "signal handled SIGPIPE\n";
int errMsgPipeLen;

void handler(int x) {
    write(2, errMsgPipe, errMsgPipeLen);
}

int main(void) {
    errMsgPipeLen = strlen(errMsgPipe);
    char bufin[BUFSIZE] = "empty";
    char bufout[] = "hello soner";
    int bytesin;
    pid_t childpid;
    int fd[2];

    struct sigaction sa;
    memset(&sa, 0, sizeof(sa));
    sa.sa_flags = 0;
    sigfillset(&sa.sa_mask);
    sa.sa_handler = handler;
    sigaction(SIGPIPE, &sa, 0);

    if (pipe(fd) == -1) {
        perror("Failed to create the pipe");
        return 1;
    }
    bytesin = strlen(bufin);
    childpid = fork();
    if (childpid == -1) {
        perror("Failed to fork");
        return 1;
    }

    close(fd[0]);

    if (childpid) {
        if (write(fd[1], bufout, strlen(bufout)+1) < 0) {
            perror("write");
        }
    }
    else
        bytesin = read(fd[0], bufin, BUFSIZE);
    fprintf(stderr, "[%ld]:my bufin is {%.*s}, my bufout is {%s}\n",
            (long)getpid(), bytesin, bufin, bufout);
    return 0;
}

輸出:

[22686]:my bufin is {empty}, my bufout is {hello soner}
[22687]:my bufin is {empty}, my bufout is {hello soner}

預期產量:

[22686]:my bufin is {empty}, my bufout is {hello soner}
signal handled SIGPIPE or similar stuff

獨立演示為何關閉管道的讀取端很重要

在以下情況下,關閉管道的讀取端很重要:

seq 65536 | sed 10q

如果啟動seq的進程沒有關閉管道的讀取端,則seq將填充管道緩沖區(它想寫入382,110字節,但管道緩沖區不是那么大),但是因為存在一個帶有讀取管道打開端( seq )的末尾,它將不會獲得SIGPIPE或寫入錯誤,因此它將永遠不會完成。

考慮下面的代碼。 該程序運行seq 65536 | sed 10q seq 65536 | sed 10q ,但是根據是否使用任何參數調用它,它是否關閉對seq程序的管道的讀取端。 如果在不帶參數的情況下運行seq程序,則它的標准輸出永遠不會獲得SIGPIPE或寫入錯誤,因為存在一個進程,該進程的管道的讀取端處於打開狀態–該進程本身就是seq

#include "stderr.h"
#include <stdio.h>
#include <sys/wait.h>
#include <unistd.h>

int main(int argc, char **argv)
{
    err_setarg0(argv[0]);
    int fd[2];
    int pid1;
    int pid2;

    if (pipe(fd) != 0)
        err_syserr("failed to pipe: ");
    if ((pid1 = fork()) < 0)
        err_syserr("failed to fork 1: ");
    else if (pid1 == 0)
    {
        char *sed[] = { "sed", "10q", 0 };
        if (dup2(fd[0], STDIN_FILENO) < 0)
            err_syserr("failed to dup2 read end of pipe to standard input: ");
        close(fd[0]);
        close(fd[1]);
        execvp(sed[0], sed);
        err_syserr("failed to exec %s: ", sed[0]);
    }
    else if ((pid2 = fork()) < 0)
        err_syserr("failed to fork 2: ");
    else if (pid2 == 0)
    {
        char *seq[] = { "seq", "65536", 0 };
        if (dup2(fd[1], STDOUT_FILENO) < 0)
            err_syserr("failed to dup2 write end of pipe to standard output: ");
        close(fd[1]);
        if (argc > 1)
            close(fd[0]);
        execvp(seq[0], seq);
        err_syserr("failed to exec %s: ", seq[0]);
    }
    else
    {
        int corpse;
        int status;
        close(fd[0]);
        close(fd[1]);
        printf("read end of pipe is%s closed for seq\n", (argc > 1) ? "" : " not");
        printf("shell process is PID %d\n", (int)getpid());
        printf("sed launched as PID %d\n", pid1);
        printf("seq launched as PID %d\n", pid2);
        while ((corpse = wait(&status)) > 0)
            printf("%d exited with status 0x%.4X\n", corpse, status);
        printf("shell process is exiting\n");
    }
}

庫代碼在GitHub上的我的SOQ (堆棧溢出問題)存儲庫中以src / libsoq子目錄中的stderr.cstderr.h文件的stderr.c

這是一對示例運行(該程序稱為fork29 ):

$ fork29
read end of pipe is not closed for seq
shell process is PID 90937
sed launched as PID 90938
seq launched as PID 90939
1
2
3
4
5
6
7
8
9
10
90938 exited with status 0x0000
^C
$ fork29 close
read end of pipe is closed for seq
shell process is PID 90940
sed launched as PID 90941
seq launched as PID 90942
1
2
3
4
5
6
7
8
9
10
90941 exited with status 0x0000
90942 exited with status 0x000D
shell process is exiting
$

請注意,第二個示例中seq的退出狀態表明它已死於信號13 SIGPIPE。

有關上述解決方案的問題

(1)我們如何確定seqsed之前執行? 怎么沒有種族?

這兩個程序( seqsed )同時執行。 seq生成之前, sed無法讀取任何內容。 seq可能在sed讀取任何內容之前就填充了管道,或者可能僅在sed退出之后才填充了管道。

(2)為什么在sed同時關閉fd[0]fd[1] 為什么不僅是fd[1] seq類似。

經驗法則 :如果將管道的一端dup2()為標准輸入或標准輸出,請盡快關閉pipe()返回的兩個原始文件描述符。 特別是,在使用任何exec*()系列函數之前,應關閉它們。

如果您使用帶有F_DUPFD dup()fcntl()復制描述符,則該規則也適用

sed的代碼遵循經驗法則。 seq的代碼僅在有條件的情況下執行,因此您可以看到不遵循經驗法則時會發生什么。

獨立演示為何關閉管道的寫入結束很重要

在以下情況下,關閉管道的寫端很重要:

ls -l | sort

如果啟動sort的進程沒有關閉管道的寫入端,則sort可能會寫入管道,因此它將永遠不會在管道上看到EOF,因此它將永遠不會完成。

考慮下面的代碼。 該程序運行ls -l | sort ls -l | sort ,但是取決於是否使用任何參數調用它,它是否關閉對sort程序的管道的寫端。 當它不帶參數運行時, sort程序就永遠不會在其標准輸入上看到EOF,因為有一個進程的管道的寫端處於打開狀態–該進程本身就是sort

#include "stderr.h"
#include <stdio.h>
#include <sys/wait.h>
#include <unistd.h>

int main(int argc, char **argv)
{
    err_setarg0(argv[0]);
    int fd[2];
    int pid1;
    int pid2;

    if (pipe(fd) != 0)
        err_syserr("failed to pipe: ");
    if ((pid1 = fork()) < 0)
        err_syserr("failed to fork 1: ");
    else if (pid1 == 0)
    {
        char *sort[] = { "sort", 0 };
        if (dup2(fd[0], STDIN_FILENO) < 0)
            err_syserr("failed to dup2 read end of pipe to standard input: ");
        close(fd[0]);
        if (argc > 1)
            close(fd[1]);
        execvp(sort[0], sort);
        err_syserr("failed to exec %s: ", sort[0]);
    }
    else if ((pid2 = fork()) < 0)
        err_syserr("failed to fork 2: ");
    else if (pid2 == 0)
    {
        char *ls[] = { "ls", "-l", 0 };
        if (dup2(fd[1], STDOUT_FILENO) < 0)
            err_syserr("failed to dup2 write end of pipe to standard output: ");
        close(fd[1]);
        close(fd[0]);
        execvp(ls[0], ls);
        err_syserr("failed to exec %s: ", ls[0]);
    }
    else
    {
        int corpse;
        int status;
        close(fd[0]);
        close(fd[1]);
        printf("write end of pipe is%s closed for sort\n", (argc > 1) ? "" : " not");
        printf("shell process is PID %d\n", (int)getpid());
        printf("sort launched as PID %d\n", pid1);
        printf("ls   launched as PID %d\n", pid2);
        while ((corpse = wait(&status)) > 0)
            printf("%d exited with status 0x%.4X\n", corpse, status);
        printf("shell process is exiting\n");
    }
}

這是一對示例運行(該程序稱為fork13 ):

$ fork13
write end of pipe is not closed for sort
shell process is PID 90737
sort launched as PID 90738
ls   launched as PID 90739
90739 exited with status 0x0000
^C
$ fork13 close
write end of pipe is closed for sort
shell process is PID 90741
sort launched as PID 90742
ls   launched as PID 90743
90743 exited with status 0x0000
-rw-r--r--  1 jleffler  staff   1583 Jun 23 14:20 fork13.c
-rwxr-xr-x  1 jleffler  staff  22216 Jun 23 14:20 fork13
drwxr-xr-x  3 jleffler  staff     96 Jun 23 14:06 fork13.dSYM
total 56
90742 exited with status 0x0000
shell process is exiting
$

(3)為什么我們需要同時關閉它們的父級中的fd[0]fd[1]

父進程未積極使用其創建的管道。 它必須完全關閉它,否則其他程序將不會結束。 嘗試一下-我(無意間)做了,並且程序沒有達到我的預期(預期)。 我花了幾秒鍾才意識到我還沒做完!

通過OP改編代碼

snr發布了一個“答案”,試圖演示信號處理以及關閉(或不關閉)管道文件描述符的讀取端時發生的情況。 這是將該代碼改編為可以通過命令行選項控制的程序,其中選項的排列可以產生不同且有用的結果。 -b-a選項允許您在派生叉之前或之后關閉管道的讀取端(或完全不關閉)。 -h-i允許您使用信號處理程序來處理SIGPIPE或將其忽略(或使用默認處理-終止)。 -d選項使您可以在父級嘗試寫入之前將其延遲1秒。

#include <errno.h>
#include <signal.h>
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include "stderr.h"

#define BUFSIZE 100

static char const *errMsgPipe = "signal handled SIGPIPE\n";
static int errMsgPipeLen;

static void handler(int x)
{
    if (x == SIGPIPE)
        write(2, errMsgPipe, errMsgPipeLen);
}

static inline void print_bool(const char *tag, bool value)
{
    printf("  %5s: %s\n", (value) ? "true" : "false", tag);
}

int main(int argc, char **argv)
{
    err_setarg0(argv[0]);

    bool sig_ignore = false;
    bool sig_handle = false;
    bool after_fork = false;
    bool before_fork = false;
    bool parent_doze = false;
    static const char usestr[] = "[-abdhi]";

    int opt;
    while ((opt = getopt(argc, argv, "abdhi")) != -1)
    {
        switch (opt)
        {
        case 'a':
            after_fork = true;
            break;
        case 'b':
            before_fork = true;
            break;
        case 'd':
            parent_doze = true;
            break;
        case 'h':
            sig_handle = true;
            break;
        case 'i':
            sig_ignore = true;
            break;
        default:
            err_usage(usestr);
        }
    }

    if (optind != argc)
        err_usage(usestr);

    /* Both these happen naturally - but should be explicit when printing configuration */
    if (sig_handle && sig_ignore)
        sig_ignore = false;
    if (before_fork && after_fork)
        after_fork = false;

    printf("Configuration:\n");
    print_bool("Close read fd before fork", before_fork);
    print_bool("Close read fd after  fork", after_fork);
    print_bool("SIGPIPE handled", sig_handle);
    print_bool("SIGPIPE ignored", sig_ignore); 
    print_bool("Parent doze", parent_doze);

    err_setlogopts(ERR_PID);

    errMsgPipeLen = strlen(errMsgPipe);
    char bufin[BUFSIZE] = "empty";
    char bufout[] = "hello soner";
    int bytesin;
    pid_t childpid;
    int fd[2];

    struct sigaction sa;
    memset(&sa, 0, sizeof(sa));
    sa.sa_flags = 0;
    sigfillset(&sa.sa_mask);
    sa.sa_handler = SIG_DFL;
    if (sig_ignore)
        sa.sa_handler = SIG_IGN;
    if (sig_handle)
        sa.sa_handler = handler;
    if (sigaction(SIGPIPE, &sa, 0) != 0)
        err_syserr("sigaction(SIGPIPE) failed: ");

    printf("Parent: %d\n", (int)getpid());

    if (pipe(fd) == -1)
        err_syserr("pipe failed: ");

    if (before_fork)
        close(fd[0]);

    int val = -999;
    bytesin = strlen(bufin);
    childpid = fork();
    if (childpid == -1)
        err_syserr("fork failed: ");

    if (after_fork)
        close(fd[0]);

    if (childpid)
    {
        if (parent_doze)
            sleep(1);
        val = write(fd[1], bufout, strlen(bufout) + 1);
        if (val < 0)
            err_syserr("write to pipe failed: ");
        err_remark("Parent wrote %d bytes to pipe\n", val);
    }
    else
    {
        bytesin = read(fd[0], bufin, BUFSIZE);
        if (bytesin < 0)
            err_syserr("read from pipe failed: ");
        err_remark("Child read %d bytes from pipe\n", bytesin);
    }

    fprintf(stderr, "[%ld]:my bufin is {%.*s}, my bufout is {%s}\n",
            (long)getpid(), bytesin, bufin, bufout);

    return 0;
}

跟蹤父進程發生的事情可能很困難(很明顯)。 當孩子從信號中死亡時,Bash會生成退出狀態128 +信號編號。 在這台機器上,SIGPIPE為13,因此退出狀態為141表示已從SIGPIPE中退出。

示例運行:

$ pipe71; echo $?
Configuration:
  false: Close read fd before fork
  false: Close read fd after  fork
  false: SIGPIPE handled
  false: SIGPIPE ignored
  false: Parent doze
Parent: 97984
pipe71: pid=97984: Parent wrote 12 bytes to pipe
[97984]:my bufin is {empty}, my bufout is {hello soner}
pipe71: pid=97985: Child read 12 bytes from pipe
[97985]:my bufin is {hello soner}, my bufout is {hello soner}
0
$ pipe71 -b; echo $?
Configuration:
   true: Close read fd before fork
  false: Close read fd after  fork
  false: SIGPIPE handled
  false: SIGPIPE ignored
  false: Parent doze
Parent: 97987
pipe71: pid=97988: read from pipe failed: error (9) Bad file descriptor
141
$ pipe71 -a; echo $?
Configuration:
  false: Close read fd before fork
   true: Close read fd after  fork
  false: SIGPIPE handled
  false: SIGPIPE ignored
  false: Parent doze
Parent: 98000
pipe71: pid=98000: Parent wrote 12 bytes to pipe
[98000]:my bufin is {empty}, my bufout is {hello soner}
0
pipe71: pid=98001: read from pipe failed: error (9) Bad file descriptor
$ pipe71 -a -d; echo $?
Configuration:
  false: Close read fd before fork
   true: Close read fd after  fork
  false: SIGPIPE handled
  false: SIGPIPE ignored
   true: Parent doze
Parent: 98004
pipe71: pid=98005: read from pipe failed: error (9) Bad file descriptor
141
$ pipe71 -h -a -d; echo $?
Configuration:
  false: Close read fd before fork
   true: Close read fd after  fork
   true: SIGPIPE handled
  false: SIGPIPE ignored
   true: Parent doze
Parent: 98007
pipe71: pid=98008: read from pipe failed: error (9) Bad file descriptor
signal handled SIGPIPE
pipe71: pid=98007: write to pipe failed: error (32) Broken pipe
1
$ pipe71 -h -a; echo $?
Configuration:
  false: Close read fd before fork
   true: Close read fd after  fork
   true: SIGPIPE handled
  false: SIGPIPE ignored
  false: Parent doze
Parent: 98009
pipe71: pid=98009: Parent wrote 12 bytes to pipe
[98009]:my bufin is {empty}, my bufout is {hello soner}
pipe71: pid=98010: read from pipe failed: error (9) Bad file descriptor
0
$ pipe71 -i -a; echo $?
Configuration:
  false: Close read fd before fork
   true: Close read fd after  fork
  false: SIGPIPE handled
   true: SIGPIPE ignored
  false: Parent doze
Parent: 98013
pipe71: pid=98013: Parent wrote 12 bytes to pipe
[98013]:my bufin is {empty}, my bufout is {hello soner}
0
pipe71: pid=98014: read from pipe failed: error (9) Bad file descriptor
$ pipe71 -d -i -a; echo $?
Configuration:
  false: Close read fd before fork
   true: Close read fd after  fork
  false: SIGPIPE handled
   true: SIGPIPE ignored
   true: Parent doze
Parent: 98015
pipe71: pid=98016: read from pipe failed: error (9) Bad file descriptor
pipe71: pid=98015: write to pipe failed: error (32) Broken pipe
1
$ pipe71 -i -a; echo $?
Configuration:
  false: Close read fd before fork
   true: Close read fd after  fork
  false: SIGPIPE handled
   true: SIGPIPE ignored
  false: Parent doze
Parent: 98020
pipe71: pid=98020: Parent wrote 12 bytes to pipe
[98020]:my bufin is {empty}, my bufout is {hello soner}
0
pipe71: pid=98021: read from pipe failed: error (9) Bad file descriptor
$

在我的機器上(運行macOS High Sierra 10.13.5且運行GCC 8.1.0的MacBook Pro),如果我不延遲父級,則父級會在子級關閉文件描述符之前始終寫入管道。 但是,這不能保證行為。 可以添加另一個選項(例如, -n表示child_nap )以使子級小睡一秒鍾。

代碼在GitHub上可用

上面顯示的程序代碼( fork29.cfork13.cpipe71.c )在GitHub上的我的SOQ (堆棧溢出問題)存儲庫中以src /中的fork13.cfork29.cpipe71.c文件fork13.cso-5100-4470子目錄。

我的問題與close(fd[0]);位置有關close(fd[0]); 我在代碼中注釋掉了它的原因。 現在,我得到了預期的錯誤。

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/param.h>
#include <signal.h>
#include <errno.h>
#define BUFSIZE 100

char const * errMsgPipe = "signal handled SIGPIPE\n";
int errMsgPipeLen;

void handler(int x) {
    write(2, errMsgPipe, errMsgPipeLen);
}

int main(void) {
    errMsgPipeLen = strlen(errMsgPipe);
    char bufin[BUFSIZE] = "empty";
    char bufout[] = "hello soner";
    int bytesin;
    pid_t childpid;
    int fd[2];

    struct sigaction sa;
    memset(&sa, 0, sizeof(sa));
    sa.sa_flags = 0;
    sigfillset(&sa.sa_mask);
    sa.sa_handler = handler;
    sigaction(SIGPIPE, &sa, 0);

    if (pipe(fd) == -1) {
        perror("Failed to create the pipe");
        return 1;
    }

    close(fd[0]); // <-- it's in order for no process has an open read descriptor


    int val = -999;
    bytesin = strlen(bufin);
    childpid = fork();
    if (childpid == -1) {
        perror("Failed to fork");
        return 1;
    }


/*
 * close(fd[0]); <---- if it were here, we wouldn't get expected error and signal
 *                      since, parent can be reached to write(fd[1], .... ) call
 *                      before the child close(fd[0]); call defined here it. 
 *          It means there is by child open read descriptor seen by parent.
 */

// sleep(1);     <---- we can prove my saying by calling sleep() here



    if (childpid) {

       val = write(fd[1], bufout, strlen(bufout)+1);
       if (val < 0) {
           perror("writing process error");
       }

    }
    else {
        bytesin = read(fd[0], bufin, BUFSIZE);
    }
    fprintf(stderr, "[%ld]:my bufin is {%.*s}, my bufout is {%s}\n",
            (long)getpid(), bytesin, bufin, bufout);
    return 0;
}

輸出:

signal handled SIGPIPE
writing process error: Broken pipe
[27289]:my bufin is {empty}, my bufout is {hello soner}
[27290]:my bufin is {empty}, my bufout is {hello soner}

因此,可以說, 如果父母的寫操作失敗,則孩子的bufin包含empty

暫無
暫無

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

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