尝试使用两个或更多管道实现一个shell,但程序挂起 - C

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了尝试使用两个或更多管道实现一个shell,但程序挂起 - C相关的知识,希望对你有一定的参考价值。

我已经成功实现了一个小shell程序,能够在两个命令之间实现管道

ls -l | wc -l

但是,当我尝试实现一个或多个shell时,我可以这样做

ls -l | wc -l | wc -l

我的程序一直持续到I ^ C.

我已经把我的头围绕着这几个小时了,我似乎无法弄清楚我做错了什么。我的方法是创建与我拥有的命令一样多的子进程,所有这些都使用相同的父亲。这是我的完整代码:

#define ARGVMAX 100
#define LINESIZE 1024
#define PIPESYMB "|"
#define EXITSYMB "exit"


int makeargv(char *s, char *argv[]) {
    if ( s==NULL || argv==NULL || ARGVMAX==0)
        return -1;

    int ntokens = 0;
    argv[ntokens]=strtok(s, " \t\n");
    while ( (argv[ntokens]!=NULL) && (ntokens<ARGVMAX) ) {
        ntokens++;
        argv[ntokens]=strtok(NULL, " \t\n");
    }

    argv[ntokens] = NULL;
    return ntokens;
}

void changeOutput(int mypipe[]) {
    dup2(mypipe[1], 1);
    close(mypipe[0]);
    close(mypipe[1]);
}

void changeInput(int mypipe[]) {
    dup2(mypipe[0], 0);
    close(mypipe[1]);
    close(mypipe[0]);
}

void pipeFork(char *argv[], int i, int mypipe[]) {
    int h = i;
    int mypipe1[2];
    int found = 0;
    while((argv[h] != NULL) && !found) {
        if(!(strcmp(argv[h], PIPESYMB))) {
            argv[h] = NULL;
            found = 1;
        }
        h++;
    }
    if (pipe(mypipe1)==-1) 
                abort();
    switch ( fork() ) {
                    case -1: 
                        perror("fork error"); 
                        exit(1);
                    case 0:
                        changeInput(mypipe);
                        if(found)
                            changeOutput(mypipe1);
                        execvp( argv[i], &argv[i] );
                        perror("exec");
                        exit(1);
                    default:
                        if(found)
                            pipeFork(argv, h, mypipe1);
    }
    close(mypipe1[0]);
    close(mypipe1[1]); 
    wait(NULL);
}
void runcommand(char *argv[]) {
    int i = 0;
    int mypipe[2];
    int found = 0;
    if(!(strcmp(argv[0], EXITSYMB))) 
        exit(0);
    if (pipe(mypipe)==-1) 
                abort();
    while((argv[i] != NULL) && !found) {
        if(!(strcmp(argv[i], PIPESYMB))) {
            argv[i] = NULL;
            found = 1;
        }
        i++;
    }
    switch ( fork() ) {
        case -1: 
            perror("fork error"); 
            exit(1);
        case 0:
            if(found)
                changeOutput(mypipe);
            execvp( argv[0], argv );
            perror("exec");
            exit(1);
        default: 
             if(found)
               pipeFork(argv, i, mypipe);

    }
    close(mypipe[0]);
    close(mypipe[1]); 
    wait(NULL);
} 

int main(int argc, char *argv[]) {
    char line[LINESIZE];
    char* av[ARGVMAX];

    printf("> "); fflush(stdout);
    while ( fgets ( line, LINESIZE, stdin) != NULL ) {
        if ( makeargv( line, av) > 0 ) runcommand( av );
        printf("> "); fflush(stdout);
    }

    return 0;
}

这是我第一次使用多个流程,虽然这可能不是最好的方法,但我现在只是对于错误所在的超级好奇。

非常感谢你!

答案

我希望你早就解决了这个问题,但......

Diagnosis

与以往一样,主要问题是没有关闭足够的文件描述符。第二个问题是证明这一点。我拿了原始代码并添加了大量的仪器 - 打印(标准错误)。消息的前缀是执行打印的进程的PID,这在有多个进程时非常重要。

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <unistd.h>

#define ARGVMAX 100
#define LINESIZE 1024
#define PIPESYMB "|"
#define EXITSYMB "exit"

static void dump_fds(int max_fd)
{
    char buffer[64];
    char *base = buffer + snprintf(buffer, sizeof(buffer), "%d: fds ", (int)getpid());
    for (int i = 0; i < max_fd; i++)
    {
        struct stat sb;
        if (fstat(i, &sb) == 0)
            *base++ = 'o';
        else
            *base++ = '-';
    }
    *base = '\0';
    fprintf(stderr, "%s\n", buffer);
}

static void dump_argv(const char *tag, char **argv)
{
    fprintf(stderr, "%d: %s:\n", (int)getpid(), tag);
    int i = 0;
    while (*argv)
        fprintf(stderr, "%d: argv[%d] = \"%s\"\n", (int)getpid(), i++, *argv++);
    dump_fds(20);
}

static int makeargv(char *s, char *argv[])
{
    if (s == NULL || argv == NULL || ARGVMAX == 0)
        return -1;

    int ntokens = 0;
    argv[ntokens] = strtok(s, " \t\n");
    while ((argv[ntokens] != NULL) && (ntokens < ARGVMAX))
    {
        ntokens++;
        argv[ntokens] = strtok(NULL, " \t\n");
    }

    argv[ntokens] = NULL;
    return ntokens;
}

static void changeOutput(int mypipe[])
{
    fprintf(stderr, "%d: (%d closed) (%d to 1)\n", (int)getpid(), mypipe[0], mypipe[1]);
    dup2(mypipe[1], 1);
    close(mypipe[0]);
    close(mypipe[1]);
}

static void changeInput(int mypipe[])
{
    fprintf(stderr, "%d: (%d to 0) (%d closed)\n", (int)getpid(), mypipe[0], mypipe[1]);
    dup2(mypipe[0], 0);
    close(mypipe[1]);
    close(mypipe[0]);
}

static void wait_for(int pid)
{
    int corpse;
    int status;
    while ((corpse = wait(&status)) > 0)
    {
        fprintf(stderr, "%d: child %d exit status 0x%.4X\n", (int)getpid(), corpse, status);
        if (pid == 0 || corpse == pid)
            break;
    }
}

static void pipeFork(char *argv[], int i, int mypipe[])
{
    int h = i;
    int mypipe1[2];
    int found = 0;
    dump_argv("pipeFork", &argv[h]);
    while ((argv[h] != NULL) && !found)
    {
        if (!(strcmp(argv[h], PIPESYMB)))
        {
            argv[h] = NULL;
            found = 1;
        }
        h++;
    }
    if (pipe(mypipe1) == -1)
        abort();
    fprintf(stderr, "%d: %s - pipe (%d,%d)\n", (int)getpid(), __func__, mypipe1[0], mypipe1[1]);
    int pid = fork();
    switch (pid)
    {
    case -1:
        perror("fork error");
        exit(1);
    case 0:
        fprintf(stderr, "%d: pipeFork - child\n", (int)getpid());
        changeInput(mypipe);
        if (found)
            changeOutput(mypipe1);
        dump_argv("pipefork:execvp", &argv[i]);
        execvp(argv[i], &argv[i]);
        perror("exec");
        exit(1);
    default:
        fprintf(stderr, "%d: forked child %d\n", (int)getpid(), pid);
        if (found)
        {
            dump_argv("recurse-pipeFork", &argv[h]);
            pipeFork(argv, h, mypipe1);
        }
        break;
    }
    fprintf(stderr, "%d: pipeFork: close %d %d\n", (int)getpid(), mypipe1[0], mypipe1[1]);
    close(mypipe1[0]);
    close(mypipe1[1]);
    fprintf(stderr, "%d: waiting in pipeFork for %d\n", (int)getpid(), pid);
    wait_for(0);
}

static void runcommand(char *argv[])
{
    int i = 0;
    int mypipe[2];
    int found = 0;
    if (!(strcmp(argv[0], EXITSYMB)))
        exit(0);
    if (pipe(mypipe) == -1)
        abort();
    fprintf(stderr, "%d: %s - pipe (%d,%d)\n", (int)getpid(), __func__, mypipe[0], mypipe[1]);
    while ((argv[i] != NULL) && !found)
    {
        if (!(strcmp(argv[i], PIPESYMB)))
        {
            argv[i] = NULL;
            found = 1;
        }
        i++;
    }
    int pid = fork();
    switch (pid)
    {
    case -1:
        perror("fork error");
        exit(1);
    case 0:
        fprintf(stderr, "%d: runcommand - child\n", (int)getpid());
        if (found)
            changeOutput(mypipe);
        dump_argv("about to exec", argv);
        execvp(argv[0], argv);
        perror("exec");
        exit(1);
    default:
        fprintf(stderr, "%d: forked child %d\n", (int)getpid(), pid);
        if (found)
        {
            dump_argv("call-pipeFork", &argv[i]);
            pipeFork(argv, i, mypipe);
        }
        break;
    }
    fprintf(stderr, "%d: runcommand: close %d %d\n", (int)getpid(), mypipe[0], mypipe[1]);
    close(mypipe[0]);
    close(mypipe[1]);
    fprintf(stderr, "%d: waiting in runcommand for %d\n", (int)getpid(), pid);
    wait_for(0);
}

int main(void)
{
    char line[LINESIZE];
    char *av[ARGVMAX];

    printf("> ");
    fflush(stdout);
    while (fgets(line, LINESIZE, stdin) != NULL)
    {
        if (makeargv(line, av) > 0)
        {
            dump_argv("after reading", av);
            runcommand(av);
        }
        printf("> ");
        fflush(stdout);
    }

    return 0;
}

dump_fds()函数构建缓冲区并打印单行,因为当它逐个输出字符时,我在同时运行循环的进程之间受到干扰。

有了仪器,我们可以看到ls | wc会发生什么:

$ ./shell23
> ls | wc
6418: after reading:
6418: argv[0] = "ls"
6418: argv[1] = "|"
6418: argv[2] = "wc"
6418: fds ooo-----------------
6418: runcommand - pipe (3,4)
6418: forked child 6419
6418: call-pipeFork:
6418: argv[0] = "wc"
6418: fds ooooo---------------
6418: pipeFork:
6418: argv[0] = "wc"
6418: fds ooooo---------------
6418: pipeFork - pipe (5,6)
6418: forked child 6420
6418: pipeFork: close 5 6
6419: runcommand - child
6419: (3 closed) (4 to 1)
6418: waiting in pipeFork for 6420
6419: about to exec:
6419: argv[0] = "ls"
6419: fds ooo-----------------
6420: pipeFork - child
6420: (3 to 0) (4 closed)
6420: pipefork:execvp:
6420: argv[0] = "wc"
6420: fds ooo--oo-------------
6418: child 6419 exit status 0x0000
6418: runcommand: close 3 4
6418: waiting in runcommand for 6419
       8       8      81
6418: child 6420 exit status 0x0000
> 
> …

如果你在execvp()之前查看进程6420的fds输出,你会发现文件描述符5和6仍然是打开的。幸运的是,在这种情况下,它不会造成任何损害; ls(6419)只有三个desriptors打开(应该),当它退出时,wc(6420)的输入被关闭。

ls | wc | wc的痕迹更成问题:

> …
>
> ls | wc | wc
6418: after reading:
6418: argv[0] = "ls"
6418: argv[1] = "|"
6418: argv[2] = "wc"
6418: argv[3] = "|"
6418: argv[4] = "wc"
6418: fds ooo-----------------
6418: runcommand - pipe (3,4)
6418: forked child 6421
6418: call-pipeFork:
6418: argv[0] = "wc"
6418: argv[1] = "|"
6418: argv[2] = "wc"
6418: fds ooooo---------------
6418: pipeFork:
6418: argv[0] = "wc"
6418: argv[1] = "|"
6418: argv[2] = "wc"
6418: fds ooooo---------------
6418: pipeFork - pipe (5,6)
6418: forked child 6422
6421: runcommand - child
6421: (3 closed) (4 to 1)
6418: recurse-pipeFork:
6418: argv[0] = "wc"
6418: fds ooooooo-------------
6418: pipeFork:
6418: argv[0] = "wc"
6418: fds ooooooo-------------
6421: about to exec:
6418: pipeFork - pipe (7,8)
6421: argv[0] = "ls"
6421: fds ooo-----------------
6422: pipeFork - child
6422: (3 to 0) (4 closed)
6418: forked child 6423
6418: pipeFork: close 7 8
6418: waiting in pipeFork for 6423
6422: (5 closed) (6 to 1)
6422: pipefork:execvp:
6422: argv[0] = "wc"
6422: fds ooo-----------------
6423: pipeFork - child
6423: (5 to 0) (6 closed)
6423: pipefork:execvp:
6423: argv[0] = "wc"
6423: fds ooooo--oo-----------
6418: child 6421 exit status 0x0000
6418: pipeFork: close 5 6
6418: waiting in pipeFork for 6422
^C
$

进程6421(ls)和6422(wc第一个)都可以,但是6423有文件描述符3,4,7,8打开(以及0,1,2),这次,这些会导致麻烦;其中一个是标准输入管道的写入端,因此无法报告EOF,因为有一个进程(6423)可以写入管道,如果它没有挂断读取它。

父进程(本例中为6418)也有可能不正确地打开管道文件描述符 - 在处方(下面)上使用的代码的修改版本显示父进程在等待时打开描述符0-4第二次wc完成。

Prescription

简单的处方是“足够接近文件描述符”。

实际上关闭正确的文件描述符是一个更大的问题。在pipeFork()以上是关于尝试使用两个或更多管道实现一个shell,但程序挂起 - C的主要内容,如果未能解决你的问题,请参考以下文章

实现一个shell程序

Linux进程间通信

Linux下基于管道的程序

shell 管道命令与过滤器

24 shell 管道命令与过滤器

c - 生成了一个 bash shell。壳死了但管子没坏?