fork exec

operating systems unix

fork

This is a system call that creates a clone of the calling process and marks the new process as a child of the calling process.

The kernel does the following when a fork syscall is made

  • Creates a new process
  • Copies the content in address space of the calling process into the new process.
    • These includes the code, static data, stack, heap etc
  • Copies values of all registers of the calling process including the stack pointer, frame pointer and instruction pointer/program counter (PC)
  • Copies the open file descriptors
  • Makes the new process a child of the calling process

All these mean the new process is exactly like the calling process with the same code and instruction pointer. They will both resume at the same instruction (which is the return of the fork function call). The kernel sets different values as the return of the fork call

  • In the parent/calling process: ret = <PID of the child process>
  • In the child/new process: ret = 0

Example

#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/time.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdarg.h>

int wait_or_die() {
    int rc = wait(NULL);
    assert(rc > 0);
    return rc;
}

int fork_or_die() {
    int rc = fork();
    assert(rc >= 0);
    return rc;
}

int pid_printf(const char *fmt, ...) {
    // Prepends the current process pid to every message
    pid_t pid = getpid();
    int written = 0;
    va_list ap;
    written += fprintf(stdout, "%d: ", (int)pid);
    va_start(ap, fmt);
    written += vfprintf(stdout, fmt, ap);
    va_end(ap);
    return written;
}

int main(int argc, char *argv[]) {
    char* message = "Hello, World!\n";
    int rc = fork_or_die();
    if (pid == 0) {
        pid_printf("I am the child\n");
        pid_printf("%s", message);
        message = "Hello, Universe!\n"; // only modifies it's copy
        pid_printf("Child changed message to: %s", message);
        exit(0);
    } else {
        int rc = wait_or_die(); // wait for child to finish
        pid_printf("wait returned: %d\n", rc);
        pid_printf("I am the parent. I created %d\n", pid);
        pid_printf("%s", message); // should still be "Hello, World!\n"
    }
    return 0;
}
  cpu-api git:(master)  gcc ./fork_pg.c -o fork_pg && ./fork_pg
54875: I am the child
54875: Hello, World!
54875: Child changed message to: Hello, Universe!
54874: wait returned: 54875
54874: I am the parent. I created 54875
54874: Hello, World!

exec

This syscall replaces the process image of the calling process with that of the executable passed to the exec syscall. A process image consist of code and static data of the executable. The registers, heap and stack are cleared as well. The PC register is set to point to the entrypoint of the executable. The exec call does not return to the calling process due to this.

The PID, PPID, PGID and file descriptors of the calling process are not modified.

Example

#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/time.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdarg.h>

int pid_printf(const char *fmt, ...) {
    pid_t pid = getpid();
    int written = 0;
    va_list ap;
    written += fprintf(stdout, "%d: ", (int)pid);
    va_start(ap, fmt);
    written += vfprintf(stdout, fmt, ap);
    va_end(ap);
    return written;
}

int wait_or_die() {
    int rc = wait(NULL);
    assert(rc > 0);
    return rc;
}

int fork_or_die() {
    int rc = fork();
    assert(rc >= 0);
    return rc;
}

int main(int argc, char *argv[]) {
    int rc = fork();
    if (rc < 0) {
        pid_printf("fork failed\n");
        perror("fork failed");
        exit(1);
    }
    if (rc == 0) {
        pid_printf("Child 1: about to exec\n");

        // replace the code of this child process with that of hello.py. 
        execl("./hello.py", "hello.py", NULL);

        // The value of the program counter will be set to hello.py's entrypoint.
        // It means the code below will not be executed
        perror("execl failed");
        printf("This code should not be reached if exec is successful\n");

        exit(0);
    } 

    rc = fork();
    if (rc == 0) {
        pid_printf("Child 2: about to exec\n");

        // any exec variant with p searches for the program in PATH environment variable.
        execlp("python3", "python3", "-c", "print('Hello from Python')", NULL);
        perror("execl failed");
        printf("This code should not be reached if exec is successful\n");

        exit(0);
    } 

    wait_or_die(); 
    wait_or_die(); 
    pid_printf("Children finished\n");

    return 0;
}
# Output
  cpu-api git:(master)  gcc exec_pg.c -o exec_pg && ./exec_pg
36986: Child 1: about to exec
36987: Child 2: about to exec
Hello, CPU API! # output from hello.py
Hello from Python # output from python command
36985: Children finished
← Back to all posts