4

I'm really stuck on this assignment. It is a pretty simple program I have to write in C. The idea is to demonstrate the use of fork() and wait().
The instructions say to "Perform exactly 15 iterations before terminating. In each iteration, the program will create 10 child processes (p0 to p9). Each process will print its decimal digit once without a newline before exiting. After creating the 10 children, the parent process will wait until it's 10 children have completed, then print a newline before going to the next iteration or exiting. Do not have 10 explicit calls to fork(), use a loop with only one instance of the fork() call. In order to wait for all the children to complete, use wait(NULL) in a loop until the return value of wait(NULL) is -1."
Here is a sample output:

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



Here is what I have but it seems that every way I try this the numbers just print in order with no unpredictability at all.

#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;
}

Any help is greatly appreciated! Thank you!

John Smith
  • 63
  • 3

5 Answers5

1

You understood it wrong. On fork(), you can imagine the code from fork() onward is executed simultaneously, with both processes share the same code ( because memory is copied). The way to distinguish between parent and child is via an if-clause, because fork() returns different values for each process. As demonstrated below:

#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;
}
Jonathan Leffler
  • 730,956
  • 141
  • 904
  • 1,278
Gnut
  • 541
  • 1
  • 5
  • 19
  • I'd probably be a little gentler than "You understood wrong". The key point is that if `fork()` does not fail, it returns twice — once in the parent process, once in the child process. There aren't many differences between them (see [`fork()`](http://pubs.opengroup.org/onlinepubs/9699919799/functions/fork.html) for the details), but the key one is the return value from `fork()`. It is crucial, as you say, to handle the two cases (parent, child) differently and appropriately. And making sure the child exits when done is a key part of 'appropriately'. – Jonathan Leffler Feb 20 '19 at 22:44
  • Oh, I didn't know that phrase was harsh. English is not my first language. I wrote that because I learned about this stuff not too long ago and the fact that fork() returns different values for each process was one of the "gotcha" moments for me. Based on what the OP wrote I think OP probably misunderstood it like me. So no hard feeling here ! – Gnut Feb 21 '19 at 00:41
  • I am not surprised to learn English is not your first language, based primarily on your user name. Your command of English is good. I think it would be sufficient to use "I think you misunderstood how `fork()` behaves" (or 'misunderstand' — either is OK, though there are subtle differences in meaning). Only nit-pickers like me would be likely to pick up on the original; I've not yet inured myself to all the modern techniques of communication and it likely would pass unremarked by most people. – Jonathan Leffler Feb 21 '19 at 00:45
1

You need to differentiate between the code to be run in the parent or child process (now you are running the same code on both processes). This can be done with the return value of fork(), as fork returns the child's pid to the parent and returns 0 to the child. Therefore, you can implement a check like:

pid = fork();
if (pid == 0) {
    //Code for child process
    exit(0);
}
//Code for parent process
Tucker
  • 36
  • 4
1

Whenever you use child processes, always differentiate between parents code and child code. A child is identified by having pid=0, while the parent process will have a pid > 0 according to what pid has at execution time. Also, if an error occur while forking a child process, fork will return a value <0 so you should consider this case.

In your code there is no such thing. The classical way to go about this is to do something like this:

#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;
}
Jonathan Leffler
  • 730,956
  • 141
  • 904
  • 1,278
Vrantamar
  • 83
  • 1
  • 7
1

You've written a fork bomb. This is when a process spawns children which then spawn children which then spawn children... until all memory is consumed or the operation system kills it.

fork can be difficult to get your head around. When its called, both the parent and child processes continue on executing the code. Because of this it's important to separate the child code and make sure the child exits. If the parent and child are running the same code, how do you ensure the child stops and the parent continues?

fork in the parent returns the process ID of the child. fork in the child returns 0. And if there's an error it will return -1. So the typical idiom looks like this:

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

        // parent

Once done you have to wait for all children to complete. wait will return when any child process completes, returning the child pid. You need to keep calling wait until it returns with < 0 indicating there were no more child processes.

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

Putting them all together, and removing the compilation of doing it 15 times...

    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");

Even so I still get them in order. Probably because each child is executing the exact same very simple and predictable thing. They all take roughly the same amount of time to execute, so they're probably going to come out in the same order.


Side Note: stdout is line buffered, meaning it will only display, or flush, its contents when it sees a newline. printf("%d", j); won't display until a newline is printed or stdout gets flushed. exit will flush and close all streams, so that's ok.

However, the child inherits the parent's buffer. This can lead to some pretty weird behavior if the parent leaves anything on the buffer. For example...

    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");

We get:

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,

What's going on? This boggled me for a while. It's a little clearer if we put a newline in the child's print.

    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, 

Each time the child is forked it gets a copy of the parent's stdout buffer. Each time through the loop the parent adds parent: %d, to that buffer without flushing. When the child does printf("child: %d\n", j); it adds to the existing buffer and then flushes it.

  • The first child copies "" from the parent, adds child: 0\n, and flushes.
  • The parent adds parent: 0, to stdout.
  • The second child copies parent: 0,, adds child: 1\n, and flushes.
  • The parent adds parent: 1, to stdout, its now parent: 0, parent: 1,
  • The third child copies parent: 0, parent: 1,, adds child: 2\n, and flushes.

And so on.

Flushing stdout after the parent does a partial print avoids this, and ensures everything is displayed as it happens.

    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
Schwern
  • 153,029
  • 25
  • 195
  • 336
  • `exit(0)` forces the flush of standard output. – Jonathan Leffler Feb 20 '19 at 20:43
  • @JonathanLeffler Ah ha, it's the *parent* that needs to flush stdout if it prints without a newline. – Schwern Feb 20 '19 at 21:22
  • Yes — though if the output is line buffered (not being piped to `less`, for example), then simply using `printf("\n");` in the parent should force the output. – Jonathan Leffler Feb 20 '19 at 21:26
  • @JonathanLeffler See my edits just now. This really bedeviled me. I spent a long time wondering how I'd messed up the forking. – Schwern Feb 20 '19 at 21:35
  • I wrote a variant of the code in which (a) each child exits with status 20 + the child number based on the launch loop counter and (b) the parent collects the PID and status of each child into arrays in the `while (wait)` loop, then prints the newline and flushes standard output, then prints the information from the arrays. (It also took care to avoid the [`printf()` anomaly after `fork()`](https://stackoverflow.com/questions/2530663/).) I consistently got the output `0123456789`, but the sequence in which the corpses were returned was very variable. The consistency of output surprised me. – Jonathan Leffler Feb 20 '19 at 21:58
0

I try this the numbers just print in order with no unpredictability at all.

When a thread calls fork():

  • maybe the kernel is designed so that the CPU creates the new process and switches to the new process, and maybe (if there's multiple CPUs) the new process has to wait for a different CPU to wake up and check its scheduler queue before the new process gets any CPU time
  • maybe the kernel is designed so that the CPU creates the new process and returns to the old process, and maybe (if there's multiple CPUs) the old process has to wait for a different CPU to wake up and check its scheduler queue before the new process gets any CPU time
  • maybe the kernel is designed so that the CPU does the least work possible before returning to the old process, then (possibly much later) when the new process gets CPU time the kernel finished doing any work needed to create the new process before returning to the new process

Notice that all of these things are "unpredictable" if you don't know which kernel/OS your program will run on or if there will/won't be multiple CPUs; but with "same kernel/OS on same computer" they're all completely predictable and won't change (e.g. a kernel doesn't just redesign itself while running).

However:

  • maybe the old task has used almost all of its time slice before calling fork() and the kernel decides that the old task has used its entire time slice immediately after fork() finishes; and this upsets the timing
  • maybe lots of other processes are constantly blocking/unblocking; and this upsets the timing
  • maybe an IRQ occurs at a specific point and that upsets the timing

Notice that these things create "unpredictability" even with "same kernel/OS on same computer"; but all of them are relatively infrequent. You might need to fork() a few million times before you get lucky and one of these things upsets the timing enough to change "who reaches printf() first".

With this in mind; to demonstrate "rare unpredictability on same kernel/OS on same computer"; I'd probably write a program that:

  • calls fork()
  • gets both processes to print something different without any newline characters (e.g. old process prints "0" and new process prints "1")
  • the new process calls exit()
  • the old process waits for the new process to terminate
  • the old process prints a newline character (e.g. so that the complete line printed will be "01\n" or "10\n")
  • the old process goes back to the start (e.g. a loop that's executed a million times).

That way, by checking the whole 1 million lines of output you'd have a relatively high chance of seeing some "rare unpredictability".

Brendan
  • 35,656
  • 2
  • 39
  • 66