shell中的输出重定向是如何为Linux中C中的fork()生成的子进程工作的



我目前正在研究操作系统和并发性,我关于进程调度程序的一个实践是使用C语言来计算多个进程如何在Linux中以毫秒为粒度"并行"工作。这是我的代码:

/* This file's name is Task05_3.c */
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/time.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <errno.h>
#include <string.h>
int kill(pid_t pid, int sig);
unsigned usleep(unsigned seconds);
#define NUMBER_OF_PROCESSES 7
#define MAX_EXPERIMENT_DURATION 4
long int getDifferenceInMilliSeconds(struct timeval start, struct timeval end)
{
int seconds = end.tv_sec - start.tv_sec;
int useconds = end.tv_usec - start.tv_usec;
int mtime = (seconds * 1000 + useconds / 1000);
return mtime;
}
int main(int argc, char const *argv[])
{
struct timeval startTime, currentTime;
int diff;
int log[MAX_EXPERIMENT_DURATION + 2] = {-1};
/* initialization */
for (int k = 0; k < MAX_EXPERIMENT_DURATION + 2; ++k)
log[k] = -1;
gettimeofday(&startTime, NULL);
pid_t pid_for_diss = 0;
for (int i = 0; i < NUMBER_OF_PROCESSES; ++i)
{
pid_for_diss = fork();
if (pid_for_diss < 0) {
printf("fork error, errno(%d): %sn", errno, strerror(errno));
} else if (pid_for_diss == 0) {
/* This loop is for logging when the child process is running */
while (1) {
gettimeofday(&currentTime, NULL);
diff = getDifferenceInMilliSeconds(startTime, currentTime);
if (diff > MAX_EXPERIMENT_DURATION)
{
break;
}
log[diff] = i;
}
// for (int k = 0; k < MAX_EXPERIMENT_DURATION + 2; ++k)
// {
//     if (log[k] != -1)
//     {
//         printf("%d, %dn", log[k], k);
//     }
// }
// exit(0);
break;
}
}
/* This loop is for print the logged results out */
if (pid_for_diss == 0)
{
for (int k = 0; k < MAX_EXPERIMENT_DURATION + 2; ++k)
{
if (log[k] != -1)
{
printf("%d, %dn", log[k], k);
}
}
kill(getpid(), SIGKILL);
}
int status;
while (wait(&status) != -1);// -1 means wait() failed
printf("Bye from the parent!n");
}

基本上,我在这里的想法是,我为父进程设置一个for循环,用fork()生成7个子进程,并将它们设置为while循环,迫使它们在一段时间内竞争CPU的使用。每次计划运行子进程时,我都会将父进程的当前时间和开始时间之间的差异大致记录到属于正在运行的子进程的数组中。然后,在所有7个进程都打破while循环后,我为每个子进程设置了另一个for循环,以打印出它们记录的结果。

然而,当我试图将输出重定向到Linux机器中的.csv文件时,发生了一些奇怪的事情:首先,我将循环设置为在main for循环之外打印(如您在我的代码中所见),然后直接在bash中运行./Task05_3,结果如下:

psyhq@bann:osc$ gcc -std=c99 Task05_3.c -o Task05_3
psyhq@bann:osc$ ./Task05_3
5, 0
4, 0
6, 0
4, 1
1, 0
4, 2
4, 3
4, 4
0, 0
1, 1
6, 1
1, 2
1, 3
1, 4
5, 1
5, 2
5, 3
5, 4
6, 2
6, 3
2, 0
6, 4
2, 1
2, 2
2, 3
2, 4
0, 1
3, 0
0, 2
0, 3
0, 4
3, 1
3, 2
3, 3
3, 4
Bye from the parent!
psyhq@bann:osc$

您可以在这里看到,所有结果(来自父进程和子进程)都已在终端中打印出来,子进程的结果是随机顺序的(我认为这可能是由于多个进程同时写入标准输出)。然而,如果我尝试通过./Task05_3 > 5output_c.csv运行它,我会发现我的目标.csv文件只包含来自父进程的结果,它看起来像:result_in_csv01

所以我的第一个问题是.csv文件怎么可能只包含父进程的提示?是因为我在bash中键入的指令只重定向父进程的输出,与子进程的输出流无关吗?

更重要的是,当我试图将for循环(用于打印)放入main for循环中(请参阅上面代码中注释的for循环)并通过./Task05_3 > 5output_c.csv运行代码时,发生了更令人困惑的事情,.csv文件现在看起来像:Result_in_csv02

它现在包含所有结果!并且子进程结果的顺序不再是随机的!!(很明显,其他子进程一直在等待,直到正在运行的子进程打印出所有结果)。所以我的第二个问题是,在我简单地改变了for循环的位置之后,这怎么会发生?

PS。我运行代码的Linux机器位于:

psyhq@bann:osc$ cat /proc/version
Linux version 3.10.0-693.2.2.el7.x86_64 (builder@kbuilder.dev.centos.org) (gcc version 4.8.5 20150623 (Red Hat 4.8.5-16) (GCC) ) #1 SMP Tue Sep 12 22:26:13 UTC 2017

GCC的版本是:

psyhq@bann:osc$ gcc --version
gcc (GCC) 4.8.5 20150623 (Red Hat 4.8.5-16)
Copyright (C) 2015 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
默认情况下,通过stdio函数的输出是缓冲的。这意味着它不会立即写入,而是在某种内部结构(FILE内部)中积累,直到。。。事情发生了。有三种可能性:
  • FILE是无缓冲的。然后立即写入输出
  • 行缓冲。当缓冲区已满或看到'n'(换行符)时写入输出
  • 块缓冲。缓冲区已满时写入输出

您始终可以使用fflush手动强制写入。

默认情况下,您打开的文件(使用fopen)是块缓冲的。stderr开始时没有缓冲。如果stdout是指一个终端,则它是行缓冲的,否则它是块缓冲的。

您的子进程打印整行(printf("%d, %dn", log[k], k);)。这意味着只要stdout到达一个终端,所有内容都会立即出现(因为它是行缓冲的)。

但是,当您将输出重定向到文件时,stdout将变为块缓冲。缓冲区可能很大,所以所有的输出都会累积在缓冲区中(它永远不会满)。通常,当FILE句柄关闭(使用fclose)时,缓冲区也会被刷新(即写入和清空),并且通常当程序结束时(通过从main调用return或通过调用exit),所有打开的文件都会自动关闭。

但是,在这种情况下,您通过向进程发送一个(致命的、不可捕获的)信号来终止进程。这意味着你的文件永远不会被关闭,你的缓冲区永远不会被写入,它们的内容也会丢失。这就是为什么你看不到任何输出。


在第二个版本中,您调用exit,而不是向自己发送信号。这将执行调用atexit处理程序、关闭所有打开的文件并刷新其缓冲区的正常清理。


顺便说一句,你可以写raise(X)而不是kill(getpid(), X)。它更短、更便携(raise是标准C)。

最新更新