如何在C中演示fork()的不可预测的顺序?

问题描述 投票:4回答:5

我真的被这个任务困住了。这是一个非常简单的程序,我必须用C编写。这个想法是演示fork()和wait()的使用。 指令说“在终止前完成15次迭代。在每次迭代中,程序将创建10个子进程(p0到p9)。每个进程在退出之前将打印其十进制数字而没有换行。创建10个子进程后,父进程将等待它的10个子进程完成,然后在进入下一次迭代或退出之前打印换行符。没有10次显式调用fork(),使用只有一个fork()调用实例的循环。为了等待所有子进程完成,在循环中使用wait(NULL),直到wait(NULL)的返回值为-1。 这是一个示例输出:

9856724310
8149765320
2789654310
0139874265
8765320149
2145367809
0123456798
0124356789
0123546789
9854320761
0123678594
0142685379
0123456789
6795438210
2394567081

这就是我所拥有的,但似乎每一种方式我尝试这个数字只是按顺序打印,没有任何不可预测性。

#include <cstdlib>
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>

using namespace std;
int main(int argc, char** argv) {
    pid_t pid;
    for(int i=0; i<15; i++){
        //fork();
        for(int j=0; j<10; j++){
            pid = fork();
            printf("%d", j);
        }
        wait(NULL);
        printf("\n");
    }
    return 0;
}

任何帮助是极大的赞赏!谢谢!

c fork wait
5个回答
1
投票

您需要区分要在父进程或子进程中运行的代码(现在您在两个进程上运行相同的代码)。这可以使用fork()的返回值来完成,因为fork将child的pid返回给父级,并将0返回给子级。因此,您可以执行以下检查:

pid = fork();
if (pid == 0) {
    //Code for child process
    exit(0);
}
//Code for parent process

1
投票

你写过一个fork bomb。这是一个进程产生子进程然后产生子进程然后产生子进程的子进程...直到所有内存都被消耗或操作系统杀死它为止。

fork可能很难理解。当它被调用时,父进程和子进程都继续执行代码。因此,分离子代码并确保子代退出很重要。如果父母和孩子正在运行相同的代码,您如何确保孩子停止并且父母继续?

父级中的fork返回子进程的ID。子中的fork返回0.如果出现错误,则返回-1。所以典型的习语看起来像这样:

        pid_t pid = fork();
        if( pid == 0 ) {
            // child
            exit(0);
        }
        else if( pid < 0 ) {
            // error
            perror("fork failed");
        }

        // parent

完成后,您必须等待所有孩子完成。当任何子进程完成时,wait将返回,返回子进程。您需要继续调用wait,直到它返回<0表示没有更多的子进程。

void wait_all() {
    while(wait(NULL) > 0);
}

将它们放在一起,并删除15次的汇编...

    for(int j=0; j<10; j++){
        pid_t pid = fork();
        if( pid == 0 ) {
            // print
            printf("%d", j);
            // job's done
            exit(0);
        }
        else if( pid < 0 ) {
            perror("fork failed");
        }
    }

    wait_all();
    printf("\n");

即便如此,我仍然按顺序完成它们。可能是因为每个孩子都在执行完全相同的非常简单和可预测的事情。它们都需要大约相同的执行时间,所以它们可能会以相同的顺序出现。


旁注:stdout是行缓冲的,这意味着它只会在看到换行符时显示或刷新其内容。 printf("%d", j);将不会显示,直到打印换行符或stdout被刷新。 exit将刷新并关闭所有流,所以没关系。

但是,子进程继承了父进程的缓冲区。如果父级在缓冲区上留下任何内容,这可能会导致一些非常奇怪的行为。例如...

    for(int j=0; j<10; j++){
        pid_t pid = fork();
        if( pid == 0 ) {
            printf("child: %d, ", j);
            exit(0);
        }
        else if( pid < 0 ) {
            perror("fork failed");
        }
        printf("parent: %d, ", j);
    }
    wait_all();
    printf("\n");

我们得到:

child: 0, parent: 0, child: 1, parent: 0, parent: 1, child: 2, parent: 0, parent: 1, parent: 2, child: 3, parent: 0, parent: 1, parent: 2, parent: 3, child: 4, parent: 0, parent: 1, parent: 2, parent: 3, parent: 4, child: 5, parent: 0, parent: 1, parent: 2, parent: 3, parent: 4, parent: 5, child: 6, parent: 0, parent: 1, parent: 2, parent: 3, parent: 4, parent: 5, parent: 6, child: 7, parent: 0, parent: 1, parent: 2, parent: 3, parent: 4, parent: 5, parent: 6, parent: 7, child: 8, parent: 0, parent: 1, parent: 2, parent: 3, parent: 4, parent: 5, parent: 6, parent: 7, parent: 8, child: 9, parent: 0, parent: 1, parent: 2, parent: 3, parent: 4, parent: 5, parent: 6, parent: 7, parent: 8, parent: 9,

这是怎么回事?这让我感到困惑了一段时间。如果我们在孩子的印刷品中添加换行符,那就更清楚了。

    for(int j=0; j<10; j++){
        pid_t pid = fork();
        if( pid == 0 ) {
            printf("child: %d\n", j);
            exit(0);
        }
        else if( pid < 0 ) {
            perror("fork failed");
        }
        printf("parent: %d, ", j);
    }
    wait_all();
    printf("\n");

child: 0
parent: 0, child: 1
parent: 0, parent: 1, child: 2
parent: 0, parent: 1, parent: 2, child: 3
parent: 0, parent: 1, parent: 2, parent: 3, child: 4
parent: 0, parent: 1, parent: 2, parent: 3, parent: 4, child: 5
parent: 0, parent: 1, parent: 2, parent: 3, parent: 4, parent: 5, child: 6
parent: 0, parent: 1, parent: 2, parent: 3, parent: 4, parent: 5, parent: 6, child: 7
parent: 0, parent: 1, parent: 2, parent: 3, parent: 4, parent: 5, parent: 6, parent: 7, child: 8
parent: 0, parent: 1, parent: 2, parent: 3, parent: 4, parent: 5, parent: 6, parent: 7, parent: 8, child: 9
parent: 0, parent: 1, parent: 2, parent: 3, parent: 4, parent: 5, parent: 6, parent: 7, parent: 8, parent: 9, 

每次孩子分叉时,它都会得到父母的stdout缓冲区的副本。每次循环时,父级都会将parent: %d,添加到该缓冲区而不进行刷新。当孩子做printf("child: %d\n", j);时,它会添加到现有缓冲区然后将其刷新。

  • 第一个孩子从父母那里复制"",加上child: 0\n和冲洗。
  • 父母将parent: 0,添加到stdout
  • 第二个孩子复制parent: 0,,添加child: 1\n和冲洗。
  • 父母将parent: 1,添加到stdout,现在是parent: 0, parent: 1,
  • 第三个孩子复制parent: 0, parent: 1,,添加child: 2\n和冲洗。

等等。

在父进行部分打印后刷新stdout可以避免这种情况,并确保在发生时显示所有内容。

    for(int j=0; j<10; j++){
        pid_t pid = fork();
        if( pid == 0 ) {
            printf("child: %d\n", j);
            exit(0);
        }
        else if( pid < 0 ) {
            perror("fork failed");
        }
        printf("parent: %d, ", j);
        fflush(stdout);
    }
    wait_all();
    printf("\n");

parent: 0, child: 0
parent: 1, child: 1
parent: 2, child: 2
parent: 3, child: 3
parent: 4, child: 4
parent: 5, child: 5
parent: 6, child: 6
parent: 7, child: 7
parent: 8, child: 8
parent: 9, child: 9

1
投票

无论何时使用子进程,始终要区分父代码和子代码。通过使用pid=0来识别孩子,而根据pid在执行时具有的内容,父进程将具有pid > 0。此外,如果在分叉子进程时发生错误,fork将返回值<0,因此您应该考虑这种情况。

在你的代码中没有这样的东西。经典的方法是做这样的事情:

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

int main(void) {
    pid_t pid;
    for(int i=0;i<15;i++){
        for(int j=0; j<10; j++){
            if((pid = fork())<0){
               perror("Fork Failed.");
               exit(EXIT_FAILURE);
            }
            if(pid==0){ /*Child Code here*/
               printf("%d", j);
               exit(EXIT_SUCCESS);
            }
        }
        while(wait(NULL)!=-1); //while loop to wait
        printf("\n");
    }

    return 0;
}

1
投票

你明白错了。在fork()上,您可以想象fork()的代码同时执行,两个进程共享相同的代码(因为内存被复制)。区分父级和子级的方法是通过if子句,因为fork()为每个进程返回不同的值。如下所示:

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

int main(void) {
    for (int i=0; i<15; i++){
        for(int j=0; j<10; j++){
            if (fork()==0){
                //child process return 0 from fork()
                printf("%d", j);
                exit(0);
            }
            else{
                //parent process return pid of the forked process from fork()
                continue;
            }
        }
        while (wait(NULL)!=-1);//wait for all child processes. wait() only return -1 when no more children to wait for
        printf("\n");
    }
    return 0;
}

0
投票

我试试这个数字只是打印顺序,没有任何不可预测性。

当一个线程调用fork()时:

  • 也许内核的设计是为了让CPU创建新进程并切换到新进程,并且可能(如果有多个CPU)新进程必须等待不同的CPU唤醒并在新进程之前检查其调度程序队列获得任何CPU时间
  • 也许内核的设计是为了让CPU创建新进程并返回到旧进程,并且可能(如果有多个CPU)旧进程必须等待不同的CPU唤醒并在新进程之前检查其调度程序队列获得任何CPU时间
  • 也许内核的设计是为了让CPU在返回旧进程之前尽可能少地工作,然后(可能更晚)当新进程获得CPU时间时内核完成创建新进程所需的任何工作,然后再返回到新进程

请注意,如果您不知道程序将在哪个内核/操作系统上运行,或者是否存在/不会有多个CPU,则所有这些都是“不可预测的”;但是在同一台计算机上使用“相同的内核/操作系统”,它们都是完全可预测的并且不会改变(例如,内核不会在运行时重新设计自己)。

然而:

  • 也许旧任务在调用fork()之前几乎使用了它的所有时间片,并且内核决定旧任务在fork()完成后立即使用了它的整个时间片;这打乱了时机
  • 也许很多其他进程都在不断阻塞/解锁;这打乱了时机
  • 也许IRQ发生在特定点,这会扰乱时间

请注意,即使在同一台计算机上使用“相同的内核/操作系统”,这些内容也会产生“不可预测性”;但所有这些都是相对罕见的。你可能需要fork()几百万次才能获得幸运,其中一个事情会让时间变得足以改变“谁先到达printf()”。

考虑到这一点;演示“同一台计算机上同一内核/操作系统的罕见不可预测性”;我可能会写一个程序:

  • fork()
  • 得到两个进程打印不同的东西,没有任何换行符(例如旧进程打印“0”,新进程打印“1”)
  • 新进程称为exit()
  • 旧进程等待新进程终止
  • 旧进程打印换行符(例如,打印完整行将为“01 \ n”或“10 \ n”)
  • 旧过程回到开始(例如,执行了一百万次的循环)。

这样一来,通过检查整个100万行的输出,您将有相对较高的机会看到一些“罕见的不可预测性”。

© www.soinside.com 2019 - 2024. All rights reserved.