我正在尝试编写一个程序来读取一堆 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;
}
这真的把我难住了,我找不到有类似问题的人。任何帮助将不胜感激!
你的程序中至少存在三个问题。
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
。
问题出在
makearg
。您的 arg 列表必须以 NULL
指针终止。
来自手册:
、execv()
和execvp()
函数提供了一系列 指向表示参数列表的以 null 结尾的字符串的指针 可用于新程序。按照惯例,第一个参数应该指向与正在执行的文件关联的文件名。 指针数组必须以 NULL 指针终止。execvP()
你可以这样修改:
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
}