Fork rule #2 – the return

Let's take a look at the code we've used so far:

    if (fork() == -1)
FATAL("fork failed! ");
printf("PID %d: Hello, fork. ", getpid());
exit(EXIT_SUCCESS);

OK, we now understand from the first rule that the printf will be run twice and in parallel—once by the parent, and once by the child process.

But, think about it: is this really useful? Can a real-world application benefit from this? No. What we are really after, what would be useful, is a division of labor, that is to say, have the child perform some task or tasks, and the parent perform some other task(s), in parallel. That makes the fork attractive and useful.

For example, after the fork, have the child run the code of some function foo and the parent run the code of some other function bar (of course, these functions can internally invoke any number of other functions as well). Now that would be interesting and useful.

To arrange for this, we would require some means of distinguishing between the parent and child after the fork. Again, at first glance, it might appear that querying their PIDs (via the getpid(2)) would be the way to do this. Well, you could, but that's a crude way to do so. The proper way to distinguish between the processes is built into the framework itself: It's—guess what—based on the value returned by the fork.

In general, you might quite correctly state that if a function is called once, it returns once. Well, fork is special—when you call a fork(3), it returns twice. How? Think about it, the job of the fork is to create a copy of the parent, the child; once done, both processes must now return to user space from kernel mode; thus fork is called once but returns twice; once in the parent and once in the child process context.

The key though, is that the kernel guarantees that the return values in parent and child differ; here are the rules regarding the return value of fork:

  • On success:
    • The return value in the child process is zero (0)
    • The return value in the parent process is a positive integer, the PID of the new child
  • On failure, -1 is returned and errno is set accordingly (do check!)

So, here we go:

Fork rule #2: To determine whether you are running in the parent or child process, use the fork return value: it's always 0 in the child, and the PID of the child in the parent.

Here's another detail: look for a moment at the fork's signature:

pid_t fork(void);

The return value's data type is a pid_t, certainly a typedef. What is it? Lets find out:

$ echo | gcc -E -xc -include 'unistd.h' - | grep "typedef.*pid_t"
typedef int __pid_t;
typedef __pid_t pid_t;
$

There we are: it's just an integer, after all. But that's not the point. The point here is that when writing code, do not assume it's integer; just declare the data type as per what the man page specifies; in the case of fork, as pid_t. This way, even if in future the library developers change pid_t to, say, long, our code will just require a re-compile. We future-proof our code, keeping it portable.

Now that we understand three fork rules, let's write a small, but better, fork-based application to demonstrate the same. In our demo program, we will write two simple functions foo and bar; their code is identical, they will emit a print and have the process sleep for the number of seconds passed to them as a parameter. The sleep is to mimic the working of a real program (of course, we can do better, but for now we'll just keep it simple).

The main function is as follows (as usual, find the full source code on the GitHub repository, ch10/fork4.c):

int main(int argc, char **argv)
{
pid_t ret;

if (argc != 3) {
fprintf(stderr,
"Usage: %s {child-alive-sec} {parent-alive-sec} ",
argv[0]);
exit(EXIT_FAILURE);
}
/* We leave the validation of the two parameters as a small
* exercise to the reader :-)
*/

switch((ret = fork())) {
case -1 : FATAL("fork failed, aborting! ");
case 0 : /* Child */
printf("Child process, PID %d: "
" return %d from fork() "
, getpid(), ret);
foo(atoi(argv[1]));
printf("Child process (%d) done, exiting ... ",
getpid());
exit(EXIT_SUCCESS);
default : /* Parent */
printf("Parent process, PID %d: "
" return %d from fork() "
, getpid(), ret);
bar(atoi(argv[2]));
}
printf("Parent (%d) will exit now... ", getpid());
exit(EXIT_SUCCESS);
}

First, here is a number of points to note:

  • The return variable has been declared as pid_t.
  • Rule #1—execution in both the parent and child process continues at the instruction following the fork. Here, the instruction following the fork is not the switch (as is commonly mistaken), but rather the initialization of the variable ret! Think about it: it will guarantee that ret is initialized twice: once in the parent and once in the child, but to different values.
  • Rule #2—to determine whether you are running in the parent or child process, use the fork return value: it's always 0 in the child, and the PID of the child in the parent. Ah, thus we see that the effect of both rules is to make sure that ret gets correctly initialized and, therefore, we can switch correctly
  • A bit of an aside—the need for input validation. Have a look at the parameters we pass to the fork4 program as follows:
$ ./fork4 -1 -2
Parent process, PID 6797 :: calling bar()...
fork4.c:bar :: will take a nap for 4294967294s ...
Child process, PID 6798 :: calling foo()...
fork4.c:foo :: will take a nap for 4294967295s ...
[...]

Need we say more (see the output)? This is a defect (a bug). As mentioned in the source code comment, we leave the validation of the two parameters as a small exercise to the reader.

  • Instead of an if condition, we would prefer to use the switch-case syntax; in your author's opinion, it makes the code more readable and thus better maintainable.
  • As we learned in rule 2, fork returns 0 in the child and the PID of the child in the parent; we use this knowledge in the switch-case and we thus effectively, and very readably, distinguish between the child and parent in the code.
  • When the child process ID is done, we do not have it call break; instead, we have it exit. The reason should be obvious: clarity. Have the child do whatever it requires within its business logic (foo()), and then simply have it go away. No fuss; clean code. (If we did use a break, we would require another if condition after the switch statement; this would be ugly and harder to understand.)
  • The parent process falls though the switch-case, it just emits a print, and exits.

Because the functions foo and bar are identical, we show the code for foo only here:

static void foo(unsigned int nsec)
{
printf(" %s:%s :: will take a nap for %us ... ",
__FILE__, __FUNCTION__, nsec);
sleep(nsec);
}

OK, let's run it:

$ ./fork4
Usage: ./fork4 {child-alive-sec} {parent-alive-sec}
$ ./fork4 3 7
Parent process, PID 8228:
return 8229 from fork()
fork4.c:bar :: will take a nap for 7s ...
Child process, PID 8229:
return 0 from fork()
fork4.c:foo :: will take a nap for 3s ...
Child process (8229) done, exiting ...
Parent (8228) will exit now...
$

As you can see, we chose to keep the child alive for three seconds and the parent alive for seven seconds respectively. Study the output: the return values from fork are as expected.

Now let's run it again but in the background (Also, we give more sleep time, 10 seconds and 20 seconds to the child and parent respectively.) Back on the shell, we shall use ps(1) to see the parent and child processes:

$ ./fork4 10 20 &
[1] 308
Parent process, PID 308:
return 312 from fork()
fork4.c:bar :: will take a nap for 20s ...
Child process, PID 312:
return 0 from fork()
fork4.c:foo :: will take a nap for 10s ...
$ ps
PID TTY TIME CMD
308 pts/0 00:00:00 fork4
312 pts/0 00:00:00 fork4
314 pts/0 00:00:00 ps
32106 pts/0 00:00:00 bash
$ ps -l
F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD
0 S 1000 308 32106 0 80 0 - 1111 hrtime pts/0 00:00:00 fork4
1 S 1000 312 308 0 80 0 - 1111 hrtime pts/0 00:00:00 fork4
0 R 1000 319 32106 0 80 0 - 8370 - pts/0 00:00:00 ps
0 S 1000 32106 32104 0 80 0 - 6003 wait pts/0 00:00:00 bash
$
$ Child process (312) done, exiting ... << after 10s >>
Parent (308) will exit now... << after 20s >>
<Enter>
[1]+ Done ./fork4 10 20
$

The ps -l (l: long listing) reveals more details about each process. (For example, we can see both the PID as well as the PPID.)

In the preceding output, did you notice how the PPID (parent process ID) of the fork4 parent happens to be the value 32106 and the PID is 308 . Isn't this odd? You usually expect the PPID to be a smaller number than the PID. This is often true, but not always! The reality is that the kernel recycles PIDs from the earliest available value.

An experiment to simulate work in the child and parent processes.

Let's do this: We create a copy of the fork4.c program, calling it ch10/fork4_prnum.c. Then, we modify the code slightly: We eliminate the functions foo and bar, and, instead of just sleeping, we have the processes simulate some real work by invoking a simple macro DELAY_LOOP. (The code is in the header file common.h .) The macro prints a given character a given number of times, which we pass as input parameters to fork4_prnum. Here is a sample run:

$ ./fork4_prnum 
Usage: ./fork4_prnum {child-numbytes-to-write} {parent-numbytes-to-write}
$ ./fork4_prnum 20 100
Parent process, PID 24243:
return 24244 from fork()
pChild process, PID 24244:
return 0 from fork()
ccpcpcpcpcpcpcpcpcpcpcpcpcpcpcpcpcpcpcpChild process (24244) done, exiting ...
ppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppParent (24243) will exit now...
$

The DELAY_LOOP macro is coded to print the character p (for parent) and c (for child); the number of times it's printed is passed along as parameters. You can quite literally see the scheduler context switching between the parent and child process! (the interleaved p's and c's demonstrate when each of them has the CPU).

To be pedantic, we should ensure both processes run on exactly one CPU; this can be easily achieved with the taskset(1) utility on Linux. We run taskset specifying a CPU mask of 0 implying that the job(s) should run only on the CPU 0 . (Again, we leave it as a simple look-up exercise for the reader: check out the man page on taskset(1), and learn how to use it:

$ taskset -c 0 ./fork4_prnum 20 100
Parent process, PID 24555:
return 24556 from fork()
pChild process, PID 24556:
return 0 from fork()
ccppccpcppcpcpccpcpcppcpccpcppcpccppccppChild process (24556) done, exiting ...
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppParent (24555) will exit now...
$

We recommend that you actually try out these programs on their system to get a feel for how they work.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset