c - 后台execvp:如何正确执行?
问题描述
像许多其他人一样,我正在尝试模拟外壳。我已经execvp
在来自用户的字符串上正确使用了。解析字符串并生成字符串数组(每个单词都有其数组,在space
字符上拆分),包括NULL
最后的 a 。
当我发现用户输入的最后一个单词是&
时,我设置了一个标志来通知我的 shell 该命令将在后台执行,同时让用户立即输入另一个命令。“后台执行”命令将其&
替换为NULL
传递给的字符串数组中的一个字符execvp
。
事实上,我一直在尝试使用 apthread
在后台运行该进程,但它的行为有些奇怪:通过线程函数传递的命令需要我在发送命令后execvp
按两次。ENTER
这是我模拟外壳的简化main
功能:
int main (void) {
fprintf (stdout, "%% ");
bool running = true;
while(running) {
/* Ask for an instruction and parses it. */
char** args = query_and_split_input();
/* Executing the commands. */
if (args == NULL) { // error while reading input
running = false;
} else {
printf("shell processing new command\n");
int count = count_words(args);
split_line* line = form_split_line(args, count);
Expression* ast = parse_line(line, 0, line->size - 1);
if(line->thread_flag) {
pthread_t cmd_thr;
/* Setting up the content of the thread. */
thread_data_t thr_data;
thr_data.ast = *ast;
thr_data.line = *line;
/* Executing the thread. */
int thr_err;
if ((thr_err = pthread_create(&cmd_thr, NULL, thr_func, &thr_data))) {
fprintf(stderr, "error: pthread_create, rc: %d\n", thr_err);
return EXIT_FAILURE;
}
printf("thread has been created.\n");
} else {
run_shell(args);
}
free(line);
printf("done running shell on one command\n");
}
}
/* We're all done here. See you! */
printf("Bye!\n");
exit (0);
}
这是我的线程的功能:
void *thr_func(void *arg) {
thread_data_t *data = (thread_data_t *)arg;
data->line.content[data->line.size-1] = NULL; // to replace the trailing '&' from the command
run_shell(data->line.content);
printf("thread should have ran the command\n");
pthread_exit(NULL);
}
以及运行命令的实际行:
void run_shell(char** args) {
/* Forking. */
int status;
pid_t pid; /* Right here, the created THREAD somehow awaits a second 'ENTER' before going on and executing the next instruction that forks the process. This is the subject of my first question. */
pid = fork();
if (pid < 0) {
fprintf(stderr, "fork failed");
} else if (pid == 0) { // child
printf("Child executing the command.\n");
/* Executing the commands. */
execvp(args[0], args);
/* Child process failed. */
printf("execvp didn't finish properly: running exit on child process\n");
exit(-1);
} else { // back in parent
waitpid(-1, &status, 0); // wait for child to finish
if (WIFEXITED(status)) { printf("OK: Child exited with exit status %d.\n", WEXITSTATUS(status)); }
else { printf("ERROR: Child has not terminated correctly. Status is: %d\n", status); }
free(args);
printf("Terminating parent of the child.\n");
}
}
所以基本上,作为一个例子,run_shell(args)
接收的是["echo","bob","is","great",NULL]
(在顺序执行的情况下)或["echo","bob","is","great",NULL,NULL]
(在后台执行的命令的情况下)。
我留下了printf
痕迹,因为它可以帮助您理解执行流程。
如果我输入echo bob is great
,输出(printf 跟踪)是:
shell processing new command
Child executing the command.
bob is great
OK: Child exited with exit status 0.
Terminating parent of the child.
done running shell on one command
但是,如果我输入echo bob is great &
,则输出为:
shell processing new command
thread has been created.
done running shell on one command
然后我实际上需要ENTER
再次按下以获得以下输出:
Child executing the command.
bob is great
OK: Child exited with exit status 0.
Terminating parent of the child.
thread should have ran the command
(在最后一次执行中,我还获得了查询和解析用户输入的函数的痕迹,但这似乎无关紧要,所以我抽象了整个部分。)
所以我的问题是:
ENTER
创建的线程如何在运行之前等待一秒钟execvp
?(thr_func
停止执行run_shell
并等待指令ENTER
前的第二个)pid = fork();
- 我有解决手头问题的正确方法吗?(尝试在后台执行 shell 命令。)
解决方案
您不能使用线程来模拟进程。好吧,严格来说你可以,但这样做没有用。问题是属于一个进程的所有线程共享相同的虚拟地址空间。没有理由创建线程,因为您最终需要fork()
创建一个新进程(您将需要它,原因如下所述),那么如果其中一个将一直停止等待,为什么要创建两个执行线程子进程完成。这个模式没有用。
历史上需要fork()
系统调用来进行简单调用以创建新进程(具有不同的虚拟内存映射)以允许执行新程序。你需要在调用系统调用之前创建一个新的、完整exec(2)
的进程,因为进程地址空间将被新程序的文本和数据段覆盖。如果您在线程中执行此操作,您将覆盖整个进程地址空间(这是外壳)并杀死您可以代表该进程运行的所有线程。要遵循的架构是(伪代码):
/* create pipes for redirection here, before fork()ing, so they are available
* in the parent process and the child process */
int fds[2];
if (pipe(fds) < 0) { /* error */
... /* do error treatment */
}
pid_t child_pid = fork();
switch(child_pid) {
case -1: /* fork failed for some reason, no subprocess created */
...
break;
case 0: /* this code is executed in the childd process, do redirections
* here on pipes acquired ***before*** the fork() call */
if (dup2(0 /* or 1, or 2... */, fds[0 /* or 1, or 2... */]) < 0) { /* error */
... /* do error management, considering you are in a different process now */
}
execvpe(argc, argv, envp);
... /* do error management, as execvpe failed (exec* is non-returning if ok) */
break; /* or exit(2) or whatever */
default: /* we are the parent, use the return value to track the child */
save_child_pid(child_pid);
... /* close the unused file descriptors */
close(fds[1 /* or 0, or 2, ... */]);
... /* more bookkeeping */
/* next depends on if you have to wait for the child or not */
wait*(...); /* wait has several flavours */
} /* switch */
Exec 和 fork 系统调用有两个不同的原因:
- 您需要能够在两个调用之间进行内务处理,才能在之前的子进程中执行实际重定向
exec()
。 - 曾经有一段时间,unix 没有多任务处理或受保护,而 exec 调用只是将系统中的所有内存替换为要执行的新程序(包括内核代码,以应对未受保护的系统可能被执行破坏的事实程序)这在旧操作系统中很常见,我在 CP/M 或 TRS-DOS 等系统上看到过。unix 中的实现几乎保留了 exec() 调用的所有语义,并且只添加了 fork() 不可用的功能。这很好,因为它允许父进程和子进程在管道时间到来时进行必要的簿记。
只有当您需要不同的线程与每个孩子进行通信时,您才可能使用不同的线程来完成任务。但是认为一个线程与父级共享所有虚拟空间(我们可以讨论线程之间的父/子关系),如果您执行 exec 调用,您将覆盖整个进程的虚拟空间(那里的所有线程)
推荐阅读
- json - JsonConvert.DeserializeObject 知道类名(字符串)
- amazon-s3 - Ansible aws_s3 模块获取文件位置
- python - Python中矢量化器的网格搜索
- docker - 是否可以在同一个 Cloud Run 服务中部署两个不同的 docker 镜像
- c# - 正则表达式 - 单引号之间的匹配空间
- javascript - React - 深拷贝数组和对象问题
- python - Python:从现有创建新列并添加生成的数字
- azure - 如果 Azure Function App 在消费计划中缩减到 0,它如何侦听来自事件中心的传入事件?
- c++ - 如何提示程序要求用户多次给出命令行参数?
- flutter - 如何从 Map 访问特定值