创建一个 fork 导致 fgets 无限地重新读取文件

问题描述 投票:0回答:2

我正在尝试编写一个程序来读取一堆 Unix 命令,并创建子进程来执行它们。它有一个参数决定我想要同时运行的子进程的最大数量。我将

stdin
重定向到一个包含如下命令的单独文件:
./a.out 3 < batchcmds
。当我运行代码时,它最终会一遍又一遍地重新读取所有命令,但是当我注释掉 switch 语句时,它会按预期运行(当然,创建进程和运行命令除外)。我通过测试确认所有子进程在到达 while 循环结束之前都会终止,因此主进程一定以某种方式被搞乱了,导致它重复读取文件。这是我的代码:

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

int makearg(char*, char***);

#define BUFFERSIZE 1024

int main(int argc, char** argv) {
        // reads argument
        int maxpc = atoi(argv[1]);
        int pc = 0;

        // read from the file and assign it to a child
        char command[BUFFERSIZE];
        while(fgets(command, 128, stdin) > 0) {
                char** args;
                makearg(command, &args);

                if(pc >= maxpc) {
                        wait(NULL);
                        pc--;
                }

                switch(fork()) {   // When I comment out from here to the comment below it works as expected
                        case 0:
                                // is child
                                execvp(args[0], args);
                                printf("execv failed to run %s", command);
                                exit(0);
                                printf("child is immortal\n");
                                return -1;
                        case -1:
                                printf("failed to create child\n");
                                break;
                        default:
                                // is parent
                                pc++;
                }  // The comment below
                printf("end while loop: %x:%s", getpid(), command);
        }

        // wait for children to finish
        for(; pc > 0; pc--) wait(NULL);

        return 0;
}

int makearg(char* s, char*** args) {
        // count the tokens in the string
        int tokencount = 1;
        int slength = 1;
        for(char* c = s; *c != '\0'; c++) {
                if(*c == ' ')
                        tokencount++;
                slength++;
        }

        // set up the array of tokens
        char** tokens = (char**)malloc(tokencount*sizeof(char*));
        if(tokens == NULL) {
                return -1;
        }
        for(int i = 0; i < tokencount; i++)
                tokens[i] = (char*)malloc(slength*sizeof(char));
        if(tokens == NULL) {
                for(int i = 0; i < tokencount; i++)
                        free(tokens[i]);
                free(tokens);
                return -1;
        }

        // copy data over to the token array
        int itoken = 0;
        int ichar = 0;
        for(char* c = s; *c != '\0'; c++) {
                if(*c == ' ') {
                        tokens[itoken][ichar] = '\0';
                        itoken++;
                        ichar = 0;
                }
                else if(*c != '\n') {
                        tokens[itoken][ichar] = *c;
                        ichar++;
                }
        }
        tokens[itoken][ichar] = '\0';

        *args = tokens;
        return tokencount;
}

这真的把我难住了,我找不到有类似问题的人。任何帮助将不胜感激!

c process io fork
2个回答
2
投票

你的程序中至少存在三个问题。

One 是

execvp
的第二个参数应该是由空指针终止的指针列表,但是您的
makearg
例程不会构造这样的列表。需要执行两个步骤来解决此问题。首先,
malloc(tokencount*sizeof(char*));
应该是
malloc((tokencount+1)*sizeof(char*));
。其次,在例程末尾附近,用空指针标记列表的末尾:
tokens[tokencount] = 0;

还有一个就是分叉会导致重复输出。这是因为 C 流通常是缓冲的,这意味着

printf
等函数不会立即将输出发送到操作系统以写入设备。相反,数据被写入由 C 流接口管理的缓冲区中。当缓冲区已满或写入换行符时(如果流是行缓冲),它会被发送到操作系统。连接到终端的流通常是行缓冲的。连接到管道的流通常是完全缓冲的。

当您的程序分叉时,任何由

printf
(或其他例程)放入缓冲区但尚未发送到操作系统的数据都将在子程序中复制。最终,父级和子级都可以将其数据副本发送到操作系统,因此您将看到一条消息两次。这些重复的消息可能会让您相信程序多次执行操作,例如重新处理相同的命令。事实并非如此。只有消息被重复,而不是程序之前的工作。

解决此问题的一种方法是设置标准输出流不被缓冲,您可以通过在程序开头执行

setvbuf(stdout, NULL, _IONBF, 0);
来实现。然而,这失去了缓冲的所有好处(特别是效率)。另一种解决方案是在
fflush(stdout);
调用之前立即执行
fork
。这会将所有待处理的输出发送到系统,因此缓冲区中不会有数据被复制。

第三个问题在

fgets(command, 128, stdin) > 0
。 C 标准没有定义将指针(由
fgets
返回)与空指针常量 (
0
) 与
>
进行比较。您可以将其与
!=
进行比较。另外,最好使用
NULL
来明确您正在与空指针进行比较:
fgets(command, 128, stdin) != NULL


2
投票

问题出在

makearg
。您的 arg 列表必须以
NULL
指针终止。 来自手册:

execv()
execvp()
execvP()
函数提供了一系列 指向表示参数列表的以 null 结尾的字符串的指针 可用于新程序。按照惯例,第一个参数应该指向与正在执行的文件关联的文件名。 指针数组必须以 NULL 指针终止。

你可以这样修改:

int makearg(char* s, char*** args) {
        // count the tokens in the string
        int tokencount = 1;
        int slength = 1;
        for(char* c = s; *c != '\0'; c++) {
                if(*c == ' ')
                        tokencount++;
                slength++;
        }

        // set up the array of tokens
        char** tokens = (char**)malloc((tokencount+1)*sizeof(char*)); // One more to store NULL pointer
        if(tokens == NULL) {
                return -1;
        }
        for(int i = 0; i < tokencount; i++) {
                tokens[i] = (char*)malloc(slength*sizeof(char));
                if(tokens[i] == NULL) { // correctly catch malloc problems, here
                    for(int i = 0; i < tokencount; i++)
                        free(tokens[i]);
                    free(tokens);
                    return -1;
                }
        }

        // copy data over to the token array
        int itoken = 0;
        int ichar = 0;
        for(char* c = s; *c != '\0'; c++) {
                if(*c == ' ') {
                        tokens[itoken][ichar] = '\0';
                        itoken++;
                        ichar = 0;
                }
                else if(*c != '\n') {
                        tokens[itoken][ichar] = *c;
                        ichar++;
                }
        }
        tokens[itoken][ichar] = '\0';
        tokens[itoken+1] = NULL; // last is NULL

        *args = tokens;
        return tokencount+1; // returns the right count of args including the NULL
}
© www.soinside.com 2019 - 2024. All rights reserved.