【问题标题】:Issues with shell program in CC中的shell程序问题
【发布时间】:2021-04-14 00:56:47
【问题描述】:

我正在为一个学校项目创建一个 Linux 类型的 shell 程序。到目前为止,我已经使用execvp 和基本管道实现了基本的Linux 外部命令,如“ls”、“ps”等。作为项目的一部分,用户可以在交互模式或批处理模式下运行程序。在交互模式下,用户只需在提示时输入命令。对于批处理模式,用户在命令行中指定一个文件,其中包含要执行的命令列表。

我遇到的问题是批处理模式。在批处理模式下,如果列出了无效命令(例如“kdfg”,输出为“kdfg: command not found”),之后的所有内容都会继续,但之后的所有内容都会执行两次。所以如果我在一行上有一个“kghd”,而下一行是“ls”,那么“ls”命令将被执行两次。我一直在查看我的代码几个小时并尝试了很多东西,但都无济于事。

我的代码如下所示:

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

char* InputString();
char** GetArgs(char* com);
void Free(char** args);
char** GetCommands(char** line);
void PipedCommands(char* line);
int batch = 0; //Acts as bool for if there is a batch file given at command line
FILE* bFile; //This is just to make a quick tweek to the file if necssary to prevent any undefined behavior and keep track of where we are in the fil.
int b_fd;
int stdin_cpy;

int main(int argc, char** argv)
{
    pid_t pid;
    int status;
    int fd;
    int exitCom = 0; //acts as bool to check if an exit command was given.
    char* line;
    
    if(argc > 1) //check if batch file was given.
    {
        /*
        if(freopen(argv[1], "r", stdin) == NULL) //I added this in case the file isn't found
        {
            printf("\nCould not open \"%s\". Terminated\n\n", argv[1]);
            exit(1);
        }
        */
        //The following is to append a newline at the end of the file (if there isn't one).
        //For some reaosn, there seems to be some undefined behavior if the input file isn't
        //stricitly ended with a newline.
        bFile = fopen(argv[1], "r+"); //open for reading and updating
        if(bFile == NULL)
        {
            printf("\nCould not open \"%s\". Terminated\n\n", argv[1]);
            exit(1);
        }
        fseek(bFile, -1, SEEK_END); //go to last character of file
        if(fgetc(bFile) != '\n') //If last character is not a newline, append a newline to the file.
        {
            fprintf(bFile, "\n");
        }
        fclose(bFile); //close the file.
        
        bFile = fopen(argv[1], "r"); //open file to keep track of when it ends
        b_fd = open(argv[1], O_RDONLY); //open file again (with file descriptor this time) to duplicate it to stdin
        stdin_cpy = dup(fileno(stdin)); //keep track of stdin file.
        dup2(b_fd, 0); //duplicate to stdin so program takes input from bFile
        close(b_fd);
        batch = 1;
    }
    
    
    //int i=0; //this was used for debugging purposes
    while(1)
    {
        printf("\n");
        char** coms = GetCommands(&line);
        for(int i=0; coms[i] != NULL; ++i) //loop goes through each command returned from GetCommands(...)
        {
            //fork and wait.
            pid = fork();
            wait(&status);
            
            if(pid == 0)
            {
                int pipedCommand = 0;
                //printf("\ncoms[%d]: %s\n", i, coms[i]);
                for(int j=0; j<strlen(coms[i]); ++j)
                {
                    if(coms[i][j] == '|')
                    {
                        pipedCommand = 1;
                        break;
                    }
                }
                if(pipedCommand == 1)
                {
                    PipedCommands(coms[i]);
                    exit(1);
                }
                char** args = GetArgs(coms[i]);
                //printf("\nargs[0]: %s\n", args[0]);
                if(strcmp(args[0],"exit") == 0)
                {
                    exit(5); //if exit command was given, exit status will be 5 (I just used 5 becuse I felt like it).
                }
                //printf("\nNo exit\n");
                printf("\n");
                execvp(args[0],args);
                printf("%s: command not found\n", args[0]);
                exit(1); //Normal exit exits with 1 or 0.
            }
            //Parent continues after child exits
            else if(pid > 0)
            {
                //check exit status of child
                if(WEXITSTATUS(status) == 5)
                    exitCom = 1; //set bool exitCom to 1 (true), indicating that the exit command was given
            }
        }
        if(pid > 0)
        {
            free(line);
            free(coms);
            //Now that all commands in the line were executed, check exitCom and if it is 1 (exit command was given), the shell can now exit.
            if(exitCom == 1)
            {
                printf("\n");
                exit(0);
            }
        }
/*
        if(i >= 5)
        {
            printf("\nFORCED EXIT\n");  //this was used for debugging purposes
            exit(1);
        }
        ++i;
*/
    }

    return 0;
}

char* InputString()
{
    int len = 20;
    char* str = (char*)malloc(sizeof(char)*len);
    char* buff;
    unsigned int i=0;

    if(str != NULL)
    {
        int c = EOF;
        //printf("%c", fgetc(bFile));
        while( ((c = getchar()) != '\n') && (c != EOF) )
        {   
            /*
            //printf("%c", fgetc(bFile));
            //fgetc(bFile);
            if(feof(bFile))
            {
                printf("\n\nEnd of the line\n\n");
            }
            */
            str[i++] = (char)c;
            if(i == len)
            {
                len = len*2;
                str = (char*)realloc(str,sizeof(char)*len);
            }
        }
        str[i] = '\0';
        buff = (char*)malloc(i);
    }
    if(batch == 1)
    {
        if(fgets(buff, i, bFile) == NULL) //Once the end of file has been reached
        {
            dup2(stdin_cpy, 0); //revert input back to original stdin file so user can now enter commands interactively (this happens if exit command was not given)
            close(stdin_cpy); //close stdin_copy
            fclose(bFile); //close bFile as we have reached the end of it
            batch = 0;
        }
    }
    printf("\n");
    return str;
}

//User enters a line of commands (1 or more). Commands are separated with a ';' being the delimeter.
char** GetCommands(char** line)
{
    char** coms = (char**)malloc(sizeof(char*)); 
    char delim[] = ";";
    
    if(batch == 0)
        printf("prompt> ");
        fflush(stdout);
    
    *line = InputString();
    if(batch == 1)
        printf("%s\n", *line);
    
    strcat(*line, ";");
    
    int i=0;
   coms[i] = strtok(*line, delim);
   while(coms[i] != NULL)
   {
       ++i;
       coms = (char**)realloc(coms, sizeof(char*) * (i+1));
        coms[i] = strtok(NULL, delim);
        //printf("\ni: %d\n", i); 
   }
   
   return coms;
}
    

//A command obtained from GetCommands(...) is separated into various arguments with a space, ' ', being the delimiter.
char** GetArgs(char* com)
{
    
    char** args = (char**)malloc(sizeof(char*));
    char delim[] = " ";

    //printf("\nline: %s\n", line);
   int i=0;
   args[i] = strtok(com, delim);
   while(args[i] != NULL)
   {
       ++i;
       args = (char**)realloc(args, sizeof(char*) * (i+1));
        args[i] = strtok(NULL, delim);
   }
   
   return args;
}


void PipedCommands(char* line)
{
    char** coms = (char**)malloc(sizeof(char*));
    int numComs;
    char delim[] = "|";
    
    int i=0;
    coms[i] = strtok(line, delim);
   while(coms[i] != NULL)
   {
        ++i;
        coms = (char**)realloc(coms, sizeof(char*) * (i+1));
        coms[i] = strtok(NULL, delim);
   }
   numComs = i;
   
   int fd[2];
   pid_t pid;
   int status;
   int prev_p = 0;
    
  // printf("\nnumComs: %d\n", numComs);
    for(int i=0; i<numComs; ++i)
    {
        //printf("\ni: %d\n", i);
        pipe(fd);
        pid = fork();
        wait(&status);
        if(pid == 0)
        {
            //printf("\nChild\n");
            if(i < numComs-1)
            {
                //printf("\ni < numComs-1\n");
                //printf("%s", coms[i]);
                //printf("coms[%d]: %s", i, coms[i]);
                //printf("\nBefore dup2\n");
                char** args = GetArgs(coms[i]);
                //printf("\nexecvp in if\n");
                if(prev_p != 0)
                {
                    dup2(prev_p, 0);
                    close(prev_p);
                }
                dup2(fd[1], 1);
                close(fd[1]);
                execvp(args[0],args);
                printf("%s: command not found\n", args[0]);
                exit(3);
            }
            else
            {
                //printf("\nelse\n");
                //printf("coms[%d]: %s", i, coms[i]);
                //printf("\nBefore dup2 in else\n");
                if(prev_p != 0)
                {
                    dup2(prev_p, 0);
                    close(prev_p);
                }
                //close(fd[0]);
                close(fd[1]);
                char** args = GetArgs(coms[i]);
                printf("\n");
                execvp(args[0],args);
                printf("%s: command not found\n", args[0]);
                exit(3);
            }
        }
        close(prev_p);
        close(fd[1]);
        prev_p = fd[0];
        if(WEXITSTATUS(status) == 3)
        {
            close(fd[0]);
            close(prev_p);
            close(fd[1]);
            return;
        }
    }
    close(fd[0]);
    close(prev_p);
    close(fd[1]);
        
}


您可能可以忽略PipedCommands(...) 函数,因为我认为问题不在于那里。

下面是一个简单的批处理文件:

kldfg
whoami

以下是使用上述批处理文件的输出

kldfg
kldfg: command not found

whoami
jco0100

whoami
jco0100

whoami 命令应该只执行一次,但它似乎执行了两次。之后,程序将恢复到应有的交互模式,并且从那里一切运行良好。有谁知道为什么会这样。这仅在输入未知命令时发生。如果批处理文件中的所有命令都有效,则不会输出两次。仅对于有未知命令的批处理文件,未知命令之后的所有命令都会输出两次。

这是另一个例子:

批处理文件:

date
kldfg
whoami; ls | wc -l
date | wc -c

输出:

date
Tue Apr 13 19:43:19 CDT 2021

kldfg
kldfg: command not found

whoami; ls | wc -l
jco0100
34

date | wc -c
29

whoami; ls | wc -l
jco0100
34

date | wc -c
29

【问题讨论】:

  • 是时候学习调试器了。如果您使用的是 linux,则可以在命令行上使用 gnu 调试器 gdb 开始。这将让您单步执行代码以查看发生了什么。
  • 请将此设为minimal reproducible example,强调最小化。
  • “查看我的代码几个小时并尝试了一堆东西” 并不是对您的调试过程非常有用的描述。在那个时候,您应该将问题隔离到您可以修复的程序的特定部分,或者您需要帮助理解。简单地阅读代码并进行猜测通常不是一种实用的方法,尽管在某些高级案例中这是唯一的选择。
  • 子进程执行kdfg失败后,子进程必须以非零状态退出。否则,父 shell 和子 shell 都会继续解释输入。从表面上看,你似乎是这样做的。错误消息应报告给stderr,而不是stdout
  • 你有pid = fork(); wait(&amp;status); if (pid == 0)wait() 是可疑的。它之所以有效,是因为孩子的呼叫将失败,而父母的呼叫将挂起,直到孩子退出。但是在父代码中只测试pid 的值并捕获退出进程的PID 以及调用wait() 时的状态更为明智。时机成熟时,您还可以更好地处理后台命令。

标签: c linux shell


【解决方案1】:

我通过在运行命令之前断开子进程上的标准输入来使其工作:

...
freopen("/dev/null", "r", stdin); // disconnect
execcvp(args[0], args);
...

来自这个链接:If I fork() and then do an execv(), who owns the console?

【讨论】:

  • 我试图理解为什么断开孩子的标准输入可以解决这个问题。我想我对它的理解很松散,但我不太确定。你能帮我理解这是如何工作的吗?
  • 我也不知道。我们需要一位 linux 系统大师。
  • 我会改变你的程序。在批处理模式下,只需从文件中读取命令,然后退出。无需使用标准输入。
【解决方案2】:

我在调试它时很擅长。为了让您开始这条道路:

用调试符号编译你的 C 程序:

$ gcc --debug your-program.c

现在调试你的 C 程序:

$ gdb a.out

这会启动一个 gdb 交互式 shell。

在 gdb 本身中:

(gdb) list
(gdb) set follow-fork-mode parent
(gdb) breakpoint 69
  1. 列出您的代码
  2. 在 fork() 时告诉调试器跟随父进程
  3. 在第 69 行设置断点

运行程序:

(gdb) run batch.txt

它将在第 69 行暂停。执行下一行:

(gdb) next

打印一个变量:

(gdb) print *coms

继续运行:

(gdb) continue

剩下的留给你去探索。

我仍然不确定它有什么问题。在您的 fork 因未知命令而失败后,InputString() 中发生了一些奇怪的事情。 InputString() 开始返回来自 getchar() 的重复项。

我什至不知道您可以使用标准输入来做到这一点。也许只是以正常方式从文件中读取,而不是破坏标准输入,然后看看问题是否消失。

【讨论】:

  • 我的眼睛在流泪。不过,我们都是从这里开始的,所以当我说找到一本关于如何构建程序的书或网站时,请不要气馁或冒犯。那是一种技能。这需要时间。
【解决方案3】:

不要写太多linux代码,我正在尝试做更多。 我取出了 fork 和 wait 命令,以便可以在 mingw64 中构建它(因为它们在 windows 构建中不受支持)并且似乎无法重现该问题。

所以我认为问题出在你的多线程设置中。

“pid”变量在每个分支之间共享。这意味着当调用 fork 命令时,“pid”被设置为循环中返回的最后一个 fork。

看起来您正在使用 if 语句检查“pid”变量以查看该线程是否可以执行该命令。但是主线程不会一直运行吗?

我不知道 fork() 返回什么,但“pid”未初始化,不知道这是否重要。

也许这有帮助?

【讨论】:

  • The "pid" variable is shared between every fork. 不正确。父母和孩子都有自己的pid 副本。 pid 值是进程如何“知道”它是父进程还是子进程。
  • 你可以通过 docker 在 linux 上运行它。从窗口:docker run -v c:\program-dir:c:/app -it gcc bash
  • 这会打破我对这一切如何运作的理解...... pid 在 main() 范围内定义。这意味着它不是动态分配内存地址,而是在编译时分配该地址。在此示例中,我没有看到任何为孩子创建新副本的代码。除非 fork() 函数有奇怪的堆栈恶作剧。编辑:嗯,那没关系。显然 fork() 创建了进程新堆栈的完整副本。太奇怪了。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-10-12
  • 2011-08-31
  • 2012-05-09
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多