REN

Ph.D. in Computer Science at Rutgers University

Unix/Linux creating process trick - fork(), exec() families

In the prev->prev post in Unix/Linux category, we've discussed and analysed the implementation of process and thread in Linux. As we know, fork() is the well-known system call to create a process in Unix family operating system. This post will mainly dig on fork() system call and it related ones like exec() family, wait().


fork() - Create a Process

"Turn left at the fork", you must be very familiar with this if you often use google map! The fork() system call, creating a process, runs exactly the same as its name - fork. The fork() function, duplicates everything of father process including code, data, stack to a new born child process. The child process is an exact duplicate of its parent process except for some points like PID.

The new process (child) gets a different process ID (PID) and has the the PID of the old process (parent) as its parent PID (PPID). Because the two processes are now running exactly the same code, they can tell which is which by the return code of fork - the child gets 0, the parent gets the PID of the child. Now let's see an example:

/* test.c */
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int main(void) {
	pid_t pid = fork();
	if (pid < 0) {
		perror("fork");
		exit(0);
	} else {
		if (pid == 0) {
			/* return in Child Process */
			printf("Hello From The Child Process: pid = %d\n", getpid());
		} else {
			/* return in Parent Process*/
			printf("Hello From The Parent Process: pid = %d\n", getpid());
			printf("The Child Process I created is: pid = %d\n", pid);
		}
	}

	wait(NULL);

	return 0;
}

Let's compile this program in GCC:

gcc -o test test.c

The result of those codes are: (on x86_64 Linux)

Hello From The Parent Process: pid = 27756
The Child Process I created is: pid = 27757
Hello From The Child Process: pid = 27757

The code is very self-explanatory. After the fork() is invoked, two copies of the code, data etc. exists. The parent process get pid equals to the process id of its child process while the child process get pid equals to 0. If fork() fails, it will return a negative number. After the fork() being invoked, parent process and child process will execute exactly the same code.

Copy-on-write Implementation

The fork() system call duplicates almost everything of parent process so the overhead is qutie considerable. Typically, the process does not modify any memory and immediately executes a new process, replacing the address space entirely. In Linux implementation, a delayed write mechanism call copy-on-write is used. Copy-On-Write avoids this expense by being lazy. Rather than copy all the memory at once it pretends it was copied and only actually copies when the parent and child need to hold different values at the same physical address. The child is presented with a view of memory by having the virtual memory segments backed by the same physical memory as the parent. If either process writes to memory the parent retains the physical page and the child is given a new one with the correct value stored.


Wait(), Waitpid() - Wait on Child Process

When a process dies, it doesn't really go away completely. It's dead, so it's no longer running, but a small remnant is waiting around for the parent process to pick up. This remnant contains the return value from the child process and some other goop. So after a parent process fork()s a child process, it must wait() (or waitpid()) for that child process to exit. If a parent process invokes fork() without wait(), then the child process still has an entry in the process table where the entry is still needed to allow the parent process to read its child's exit status: once the exit status is read via the wait system call, the zombie's entry is removed from the process table and it is said to be "reaped". It is this act of wait() system call that allows all remnants of the child to vanish.

The use of wait is very simple, as it shows in the code above. When the parent process has more than one child, waitpid() could be used in some particular scenario.


exec() family - Child Process "Grows up"

After fork() is invoked, the parent process and child proces will have the same process image. To replace the child process with a new image and run a seperate task, exec() family coudl be used. Now here is an example of exec() family.

/* test.c */
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int main(void) {
	char *argv[3] = {"Command-line", ".", NULL};

	pid_t pid = fork();

	if (pid < 0) {
		perror("fork");
		exit(0);
	} else {
		if (pid == 0) {
			/* return in Child Process */
			printf("Hello From The Child Process: pid = %d\n", getpid());
			execvp("find", argv);
		} else {
			printf("Hello From The Parent Process: pid = %d\n", getpid());
		}
	}
	wait(NULL);

	printf("Parent Process pid = %d finished, this message only appears once!\n", getpid());

	return 0;
}

Let's compile this program in GCC:

gcc -o test test.c

The result of those codes are: (on x86_64 Linux)

Hello From The Parent Process: pid = 5457
Hello From The Child Process: pid = 5458
.
.test.c
.test.o
.test
Parent Process pid = 5457 finished, this message only appears once!

The little program above shows the use of execvp() - a member in exec() family. In the program above, child process will execute a command "find ." recursively in the current directory. By using exec() family functions, child process will totally seperate from parent process and has its own process image.


vfork(), clone() - Alternatives

fork(), as we know, duplicate almost everything of parent process to child process; Even in a copy-on-write implementation, the kernel has to create a new virtual address space for the child process. In contrast to the system call fork(), child and parent processes share the same virtual address space. NOTE! Using the same virtual address space, both the parent and child use the same stack, the stack pointer and the instruction pointer, as in the case of the classic fork()! To prevent unwanted interference between parent and child, which use the same stack, execution of the parent process is frozen until the child will call either exec() (create a new virtual address space and a transition to a different stack) or _exit() (termination of the process execution).


More examples on fork()

After equipped with basic knowledges on fork(), let's look at an example:

/* test.c */
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int main(void) {
	int i;
	for (i = 0; i < 2; ++i) {
		fork();
		printf("-");
	}

	wait(NULL);
	wait(NULL);

	return 0;
}

Now, the question comes: how many "-" will be show on the command line? First of all, let's analyze it first.

   

This piece of code will totally have 4 processes with original process and three new process as it shown in the picture above. Process 4310 will print two "-", so will process 4311. And process 4312 and 4313 will print one "-" respectively. So theoretially totally 6 "-" will appear on the screen.

Let's compile this program in GCC:

gcc -o test test.c

The result of those codes are: (on x86_64 Linux)

--------

The result turns out to have eight "-". It seems to be weird, but if you look into how printf() will work, you'll finally figure out the reason. As we know, printf() print the content to the stdout. In Unix family, stdout is a block device. Thus, the content in stdout are buffered, and they're outputed until the following circumstances: 1) an Enter charactor. 2) buffer is full. 3) flush() is invoked. And now we go back to see fork(), child process is totally a duplication of parent process including the standard output buffer. So, in this case, for process 4312 and 4313, even they will only invoke printf() once, the buffer still have another "-" duplicated by fork().

To make it correctly output six "-", we could either use "\n" at end of printf() or just invoke flush() immediately after printf().

/* test.c */
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int main(void) {
	int i;
	for (i = 0; i < 2; ++i) {
		fork();
		printf("-\n");
	}

	wait(NULL);
	wait(NULL);

	return 0;
}

Let's compile this program in GCC:

gcc -o test test.c

The result of those codes are: (on x86_64 Linux)

-
-
-
-
-
-
/* test.c */
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int main(void) {
	int i;
	for (i = 0; i < 2; ++i) {
		fork();
		printf("-");
		fflush(stdout);
	}

	wait(NULL);
	wait(NULL);

	return 0;
}

Let's compile this program in GCC:

gcc -o test test.c

The result of those codes are: (on x86_64 Linux)

------