向已关闭读端的Pipe写入为何有时返回成功?
问题:向已关闭读端的管道写入为何有时成功有时触发SIGPIPE?
我最近读了一篇Stack Overflow帖子,用户Soner关闭了父子进程的管道读端,但向管道写入时并未触发SIGPIPE信号。我修改代码确认管道读端已完全关闭,却发现write调用有时无错误返回,多次运行程序结果不一致。
第一次测试代码
#include <stdio.h> #include <string.h> #include <unistd.h> #include <sys/types.h> #include <sys/wait.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) { int ret = write(fd[1], bufout, strlen(bufout)+1); if (ret < 0) { perror("write"); } else { printf("write success, ret: %d\n", ret ); } wait(NULL); } else{ bytesin = read(fd[0], bufin, BUFSIZE); if(bytesin == -1) { perror("child: read"); } } fprintf(stderr, "[%ld]:my bufin is {%.*s}, my bufout is {%s}\n", (long)getpid(), bytesin, bufin, bufout); return 0; }
第一次运行输出
write success, ret: 12 child: read: Bad file descriptor [9168]:my bufin is {empty}, my bufout is {hello soner} [9167]:my bufin is {empty}, my bufout is {hello soner}
修改后的测试代码
#include <stdio.h> #include <string.h> #include <unistd.h> #include <sys/types.h> #include <sys/param.h> #include <signal.h> #include <fcntl.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]); bytesin = strlen(bufin); childpid = fork(); if (childpid == -1) { perror("Failed to fork"); return 1; } if (childpid) { int flag = fcntl(fd[0], F_GETFD); if(flag = -1) { printf("pid:[%d], fcntl: %s\n", getpid(), strerror(errno)); } else { printf("pip: [%d], fd[0] is not closed\n"); } if (write(fd[1], bufout, strlen(bufout)+1) < 0) { perror("write"); } // } else{ int flag = fcntl(fd[0], F_GETFD); if(flag = -1) { printf("pid:[%d], fcntl: %s\n", getpid(), strerror(errno)); } else { printf("pip: [%d], fd[0] is not closed\n"); } bytesin = read(fd[0], bufin, BUFSIZE); if(bytesin == -1) { perror("read"); } } fprintf(stderr, "[%ld]:my bufin is {%.*s}, my bufout is {%s}\n", (long)getpid(), bytesin, bufin, bufout); return 0; }
多次运行的不同结果
结果一
pid:[9200], fcntl: Bad file descriptor signal handled SIGPIPE write: Broken pipe pid:[9201], fcntl: Bad file descriptor [9200]:my bufin is {empty}, my bufout is {hello soner} read: Bad file descriptor [9201]:my bufin is {empty}, my bufout is {hello soner}
结果二
pid:[9189], fcntl: Bad file descriptor [9189]:my bufin is {empty}, my bufout is {hello soner} pid:[9190], fcntl: Bad file descriptor read: Bad file descriptor [9190]:my bufin is {empty}, my bufout is {hello soner}
请问为何向已关闭读端的Pipe写入会出现有时成功、有时触发SIGPIPE信号的不一致现象?
解答
这是因为管道的写端写入数据时,内核会先尝试将数据放入管道缓冲区,而SIGPIPE/EPIPE错误只会在缓冲区已满且无读端的情况下触发,或者当数据写入后发现没有读端时的后续操作才会触发。结合代码和进程调度的不确定性,具体原因如下:
管道缓冲区的存在
管道默认带有内核缓冲区(通常几KB),当写入的数据小于缓冲区剩余空间时,write会直接把数据拷贝到缓冲区,此时内核可能还没检测到所有读端都已关闭——因为父子进程的调度顺序是不确定的。进程调度的不确定性
fork之后,父子进程的执行顺序由内核调度器决定:- 情况一:父进程先执行
write,此时内核还没完全清理子进程的文件描述符(子进程还没完成close(fd[0])的后续清理),管道缓冲区能放下数据,write直接返回成功,之后内核才发现所有读端关闭,但此时write已经完成。 - 情况二:父进程执行
write时,子进程已经完成close(fd[0])且内核已标记管道读端全部关闭,此时写入操作会触发SIGPIPE,write返回EPIPE错误。
- 情况一:父进程先执行
代码细节问题
修改后的代码里存在bug:if(flag = -1)是赋值操作而非比较,应该改为if(flag == -1),不过这不会影响核心问题,但会导致错误的输出判断。
总结:管道写入时是否触发SIGPIPE,取决于写入操作发生时内核是否已检测到所有读端关闭,而内核的检测时机和进程调度的不确定性,导致了结果的不一致。当数据能被写入缓冲区且内核还没检测到读端全关时,write成功;当内核已经知道读端全关时,写入就会触发SIGPIPE并返回错误。
内容的提问来源于stack exchange,提问作者Gilbert Tian
相关产品推荐
相关产品推荐

