我试图理解为什么需要关闭文件描述符的基本原因。我知道读者端关闭写描述符的原因。但是,相反地,我无法看到(模拟)编写侧关闭读取描述符的原因。我试着申请一个,
当某个进程尝试写入没有进程有权限的管道时 打开读取描述符,内核将
SIGPIPE
信号发送到 写作过程。默认情况下,此信号会终止进程。来源,Linux编程接口Michael Kerrisk
write()
,错误时返回-1
,并正确设置errno。EPIPE
fd连接到读取端为 关闭。发生这种情况时,写作过程还将收到SIGPIPE
信号。 (因此,只有在 程序会捕获,阻止或忽略此信号。)来源,手册页。
为此,我在fork()
之前关闭已经读取的描述符。但是,我既无法抓住SIGPIPE
,也无法write()
打印perror()
的错误。
#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
答案 0 :(得分:1)
在以下情况下,关闭管道的读取端很重要:
seq 65536 | sed 10q
如果启动seq
的进程没有关闭管道的读取端,那么seq
将填充管道缓冲区(它想写入382,110字节,但管道缓冲区不是这么大),但是因为有一个进程的管道的读取端处于打开状态(seq
),所以它将不会得到SIGPIPE或写入错误,因此它将永远不会完成。
考虑此代码。该程序运行seq 65536 | sed 10q
,但是根据是否使用任何参数调用该程序,它是否关闭对seq
程序的管道读取端。当不带参数运行该程序时,seq
程序将永远不会在其标准输出上收到SIGPIPE或写入错误,因为存在一个进程的管道读取端处于打开状态–该进程本身就是seq
。 / p>
#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.c
和stderr.h
提供。
这是一对示例运行(该程序称为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)我们如何确定这里
seq
在sed
之前执行?怎么没有种族?
两个程序(seq
和sed
)同时执行。 sed
在产生seq
之前无法读取任何内容。 seq
可能在sed
读取任何内容之前就填充了管道,或者可能仅在sed
退出之后才填充。
(2)为什么我们同时关闭
fd[0]
中的fd[1]
和sed
?为什么不仅fd[1]
?与seq
类似。
经验法则:如果您
dup2()
管道的一端到标准输入或标准输出,同时关闭两个
由返回的原始文件描述符
pipe()
尽快地。
特别是,在使用任何
exec*()
系列功能。
如果您将描述符与任何一个重复,则该规则也适用
dup()
要么
fcntl()
与F_DUPFD
sed
的代码遵循经验法则。 seq
的代码仅在有条件的情况下执行,因此您可以查看不遵循经验法则的情况。
在以下情况下,关闭管道的写端很重要:
ls -l | sort
如果启动sort
的进程没有关闭管道的写端,那么sort
可能会写入管道,因此它将永远不会在管道上看到EOF,因此它将永远不会完成
考虑此代码。该程序运行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]
?
父进程未积极使用其创建的管道。它必须完全关闭它,否则其他程序将不会结束。尝试一下-我(无意间)做了,并且程序没有达到我的预期(预期)。我花了几秒钟才意识到我还没做完!
snr发布了'answer',试图演示信号处理以及关闭(或不关闭)管道文件描述符的读取端时发生的情况。这是将该代码改编为可以通过命令行选项控制的程序,其中选项的排列可以产生不同且有用的结果。使用-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
的{{1}})使孩子小睡一秒钟。
上面显示的程序代码(child_nap
,fork29.c
和fork13.c
在GitHub上的SOQ(堆栈溢出问题)存储库中以文件{{ 1}},pipe71.c
,fork13.c
在src/so-5100-4470子目录中。
答案 1 :(得分: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
的说法得到证实。