我正在处理可处理多个管道的自定义外壳。但是每次执行新的管道并使用ls -l /proc/pid/fd
检查过程时,都会得到下图所示的内容,并且列表随着执行的每个新管道而不断扩展:
问题:这是否被视为FD泄漏?以及我该如何解决?
以下是我执行管道的代码段:
enum PIPES {READ, WRITE};
void execute_pipeline(char*** pipeline)
{
int fd[2];
int fd_backup = 0;
pid_t child_pid;
while (*pipeline != '\0')
{
pipe(fd);
child_pid = fork();
if(child_pid == -1)
{
perror("fork");
exit(1);
}
else if(child_pid == 0)
{
dup2(fd_backup, 0);// (old, new)
close(fd[READ]);
if(*(pipeline + 1) != '\0')
{
dup2(fd[WRITE], 1);
}
execvp((*pipeline)[0], *pipeline);
exit(1);
}
else// Parent process
{
wait(NULL);
close(fd[WRITE]);
fd_backup = fd[READ];
pipeline++;
}
}
}
编辑
如何调用execute_pipeline的示例:
char *ls[] = {"ls", "-l", NULL};
char *sort[] = {"sort", "-r", NULL};
char *head[] = {"head", "-n", "3", NULL};
char **pipeline[] = {ls, sort, head, NULL};
execute_pipeline(pipeline);
答案 0 :(得分:1)
正如塔德曼(Tadman)所指出的那样,使用命令结构来传递信息更容易。
我们不能[嗯,我们可以,但是不应该]在管道构造期间做wait
[在父级中] / em>。以后必须是一个单独的循环。创建第一个孩子后,我们将吊起父项。
如果第一个孩子有大量输出,则 kernel 管道缓冲区可能已满,并且第一个孩子会阻塞。但是,由于没有创建第二个孩子,所以没有什么东西可以读取/清空第一个孩子的输出并取消阻止。
此外,在执行dup2
之后关闭管道单元并确保在父级中关闭先前的管道级单元也很重要。
这是所有功能的重构版本。
关于文件描述符泄漏的原始问题,我认为我通过添加更多的close
调用来解决了这个问题。该程序对此具有一些自我验证代码:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <dirent.h>
#include <sys/wait.h>
#define FREEME(ptr_) \
do { \
if (ptr_ == NULL) \
break; \
free(ptr_); \
ptr_ = NULL; \
} while (0)
#define CLOSEME(fd_) \
do { \
if (fd_ < 0) \
break; \
close(fd_); \
fd_ = -1; \
} while (0)
// command control
typedef struct {
unsigned int cmd_opt; // options
int cmd_cldno; // child number
char *cmd_buf; // command buffer
int cmd_argc; // argument count
char **cmd_argv; // arguments
int cmd_pipe[2]; // pipe units
pid_t cmd_pid; // child pid number
int cmd_status; // child status
} cmd_t;
#define CMD_FIRST (1u << 0)
#define CMD_LAST (1u << 1)
char linebuf[1000];
int cmdcount;
cmd_t *cmdlist;
int opt_d;
int opt_l;
#define dbg(fmt_...) \
do { \
if (opt_d) \
printf(fmt_); \
} while (0)
// show open fd's
void
fdshow1(int cldid)
{
char buf[100];
fprintf(stderr,"CLD: %d\n",cldid);
sprintf(buf,"ls -l /proc/%d/fd 1>&2",getpid());
system(buf);
}
// show open fd's
void
fdshow2(int cldid)
{
char dir[100];
char lnkfm[1000];
char lnkto[1000];
int len;
DIR *xf;
struct dirent *ent;
char *bp;
char obuf[1000];
sprintf(dir,"/proc/%d/fd",getpid());
xf = opendir(dir);
bp = obuf;
bp += sprintf(bp,"%d:",cldid);
while (1) {
ent = readdir(xf);
if (ent == NULL)
break;
if (strcmp(ent->d_name,".") == 0)
continue;
if (strcmp(ent->d_name,"..") == 0)
continue;
sprintf(lnkfm,"%s/%s",dir,ent->d_name);
len = readlink(lnkfm,lnkto,sizeof(lnkto));
lnkto[len] = 0;
if (strstr(lnkto,"pipe") != 0)
bp += sprintf(bp," %s-->%s",ent->d_name,lnkto);
switch (ent->d_type) {
case DT_FIFO:
break;
}
}
bp += sprintf(bp,"\n");
fputs(obuf,stderr);
fflush(stderr);
closedir(xf);
}
// show open fd's
void
fdshow(int cldid)
{
fdshow2(cldid);
}
// pipeadd -- add single command to pipe
void
pipeadd(char *buf)
{
char *cp;
char *bp;
char *sv;
cmd_t *cmd;
dbg("pipeadd: buf='%s'\n",buf);
cmdlist = realloc(cmdlist,(cmdcount + 1) * sizeof(cmd_t));
cmd = &cmdlist[cmdcount];
memset(cmd,0,sizeof(cmd_t));
cmd->cmd_pipe[0] = -1;
cmd->cmd_pipe[1] = -1;
cmd->cmd_cldno = cmdcount;
++cmdcount;
bp = buf;
while (1) {
cp = strtok_r(bp," \t",&sv);
bp = NULL;
if (cp == NULL)
break;
cmd->cmd_argv = realloc(cmd->cmd_argv,
(cmd->cmd_argc + 2) * sizeof(char **));
cmd->cmd_argv[cmd->cmd_argc + 0] = cp;
cmd->cmd_argv[cmd->cmd_argc + 1] = NULL;
cmd->cmd_argc += 1;
}
}
// pipesplit -- read in and split up command
void
pipesplit(void)
{
char *cp;
char *bp;
char *sv;
cmd_t *cmd;
printf("> ");
fflush(stdout);
fgets(linebuf,sizeof(linebuf),stdin);
cp = strchr(linebuf,'\n');
if (cp != NULL)
*cp = 0;
bp = linebuf;
while (1) {
cp = strtok_r(bp,"|",&sv);
bp = NULL;
if (cp == NULL)
break;
pipeadd(cp);
}
cmd = &cmdlist[0];
cmd->cmd_opt |= CMD_FIRST;
cmd = &cmdlist[cmdcount - 1];
cmd->cmd_opt |= CMD_LAST;
if (opt_d) {
for (cmd_t *cmd = cmdlist; cmd < &cmdlist[cmdcount]; ++cmd) {
dbg("%d:",cmd->cmd_cldno);
for (int argc = 0; argc < cmd->cmd_argc; ++argc)
dbg(" '%s'",cmd->cmd_argv[argc]);
dbg("\n");
}
}
}
// pipefork -- fork elements of pipe
void
pipefork(void)
{
cmd_t *cmd;
int fdprev = -1;
int fdpipe[2] = { -1, -1 };
for (cmd = cmdlist; cmd < &cmdlist[cmdcount]; ++cmd) {
// both parent and child should close output side of previous pipe
CLOSEME(fdpipe[1]);
// create a new pipe for the output of the current child
if (cmd->cmd_opt & CMD_LAST) {
fdpipe[0] = -1;
fdpipe[1] = -1;
}
else
pipe(fdpipe);
cmd->cmd_pid = fork();
if (cmd->cmd_pid < 0) {
printf("pipefork: fork fail -- %s\n",strerror(errno));
exit(1);
}
// parent the input side for the next pipe stage
if (cmd->cmd_pid != 0) {
CLOSEME(fdprev);
fdprev = fdpipe[0];
continue;
}
// connect up our input to previous pipe stage's output
if (fdprev >= 0) {
dup2(fdprev,0);
CLOSEME(fdprev);
}
// connect output side of our pipe to stdout
if (fdpipe[1] >= 0) {
dup2(fdpipe[1],1);
CLOSEME(fdpipe[1]);
}
// child doesn't care about reading its own output
CLOSEME(fdpipe[0]);
if (opt_l)
fdshow(cmd->cmd_cldno);
// off we go ...
execvp(cmd->cmd_argv[0],cmd->cmd_argv);
}
CLOSEME(fdpipe[0]);
CLOSEME(fdpipe[1]);
if (opt_l)
fdshow(-1);
}
// pipewait -- wait for pipe stages to complete
void
pipewait(void)
{
pid_t pid;
int status;
int donecnt = 0;
while (donecnt < cmdcount) {
pid = waitpid(0,&status,0);
if (pid < 0)
break;
for (cmd_t *cmd = cmdlist; cmd < &cmdlist[cmdcount]; ++cmd) {
if (pid == cmd->cmd_pid) {
cmd->cmd_status = status;
++donecnt;
break;
}
}
}
}
// pipeclean -- free all storage
void
pipeclean(void)
{
for (cmd_t *cmd = cmdlist; cmd < &cmdlist[cmdcount]; ++cmd)
FREEME(cmd->cmd_argv);
FREEME(cmdlist);
cmdcount = 0;
}
// main -- main program
int
main(int argc,char **argv)
{
char *cp;
--argc;
++argv;
for (; argc > 0; --argc, ++argv) {
cp = *argv;
if (*cp != '-')
break;
switch (cp[1]) {
case 'd':
opt_d = ! opt_d;
break;
case 'l':
opt_l = ! opt_l;
break;
default:
break;
}
}
while (1) {
pipesplit();
pipefork();
pipewait();
pipeclean();
}
return 0;
}
答案 1 :(得分:1)
关于文件描述符,我们要准确一些,请记住,在fork和execvp期间,文件描述符是由子进程继承的,除非标记为带有CLOEXEC标志。检查手册页:
叉子(2): 子级继承父级打开文件描述符集的副本。子级文件中的每个文件描述符都与父级文件中的相应文件描述符引用相同的打开文件描述(请参阅open(2))。
打开(2): 默认情况下,新文件描述符设置为在execve(2)上保持打开状态(即,最初禁用了fcntl(2)中描述的FD_CLOEXEC文件描述符标志);如下所述,O_CLOEXEC标志可用于更改此默认值。文件偏移量设置为文件的开头(请参阅lseek(2))。
但是,我想,这种行为正是您在管道后调用fork时所依赖的...
没有别的话,让我们来画一下:
stdin, stdout
/\
/ \
/ \
/ \
/ R; W; \
/ \
Child - - Parent
stdin/out, R(del),W stdin/out, R(fd_backup), W(del)
/\
/ \
/ \
/ \
/ R1; W1;\
/ \
Child - - Parent
stdin/out, R(fd_backup), stdin, stdout
R1(del), W1 R(fd_backup - old);
R1(fd_backup - new); W1(del)
/ \
/ \
/ \
/R2; W2;\
/ \
/ \
Child - - Parent
stdin, stdout, stdin, stdout
R(fd_backup - old), R (fd_backup - old),
R1(fd_backup - new), R1 (fd_backup - new),
R2(del),W2 R2 (fd_backup - newest!),
我希望图片能自我解释。
无论如何子进程都会死亡,并且所有fds都将关闭(因此它们没有问题)。但是父进程剩下3个打开的fds,并且它们随着执行的每个管道而不断增长。