Basic Process Creation#

Process States#

  • Many of the states reflect the process’s runtime behavior: does it wait for something, or has it expired its timeslice, or is it offended (or the offender itself) in a realtime scenario. A process is always under the control of the scheduler.

  • Much more complex than can be shown on a sketch

  • ⟶ Uninterruptible vs. interruptible sleep

  • ⟶ Various ways to terminate a process

../../../../../../../_images/process-states.svg

Creative Weirdness: fork()#

  • Creates a child process

  • Exact copy of the calling process

  • On return - returns twice - both parent and child continue where they left off, independently

    • Parent’s return value: PID of the newly created child process

    • Child’s return value: 0 - can use getpid() if needed (here)

../../../../../../../_images/fork-basic.svg

(Live demo start …)

#include <unistd.h>
#include <print>

int main()
{
    pid_t pid = fork();                                // <-- returns TWICE!!
    if (pid == -1) {
        perror("fork");
        return 1;
    }

    if (pid == 0) {                                    // <-- child
        std::println("child: pid = {}, ppid = {}", 
                     getpid(), getppid());
        return 0;                                      // <-- careful!!
    }
    else {                                             // <-- parent
        std::println("parent: pid = {}, child pid = {}", 
                     getpid(), pid);
        return 0;                                      // <-- careful!!
    }
}

return From main(), And exit()#

  • main() is special (entry point from C startup/runtime)

  • Return statement in main() is special

    • Terminates process

    • Return value is exit status of the process

    • Only one byte: 0-255

  • Return from main() has the same effect as calling exit() instead

#include <unistd.h>
#include <print>

int main()
{
    pid_t pid = fork();
    if (pid == -1) {
        perror("fork");
        exit(1);                                       // <-- same as "return 1;"
    }

    if (pid == 0) {
        std::println("child: pid = {}, ppid = {}", 
                     getpid(), getppid());
        exit(0);                                       // <-- same as "return 0;"
    }
    else {
        std::println("parent: pid = {}, child pid = {}", 
                     getpid(), pid);
        exit(0);                                       // <-- same as "return 0;"
    }
}

Bugs Ahead: Code Flow Leakage#

  • Attention fork() returns twice

  • ⟶ two code flow paths

  • Usually it is a good idea to keep them separate!

#include <unistd.h>
#include <print>

int main()
{
    pid_t pid = fork();
    if (pid == -1) {
        perror("fork");
        return 1;
    }

    if (pid == 0) {
        std::println("child: pid = {}, ppid = {}", 
                     getpid(), getppid());
        /* <-- falling through into unconditional code*/;
    }
    else {
        std::println("parent: pid = {}, child pid = {}", 
                     getpid(), pid);
        /* <-- falling through into unconditional code*/;
    }

    std::println("goodbye from {}", getpid());         // <-- executed by both
    return 0;
}

File Descriptors Are Inherited#

  • Parent opens a file

  • Creates child process

  • ⟶ file descriptor is inherited

  • Semantics just like dup() (see here

../../../../../../../_images/fd-inher.svg
#include <fcntl.h>
#include <unistd.h>
#include <assert.h>
#include <print>

int main()
{
    int fd = open("/tmp/somefile", O_RDONLY);
    if (fd == -1) {
        perror("open");
        return 1;
    }

    pid_t pid = fork();
    if (pid == -1) {
        perror("fork");
        return 1;
    }

    if (pid == 0) {                                    // <-- child
        char one_byte;
        ssize_t nread = read(fd, &one_byte, 1);
        assert(nread == 1);
        std::println("child has read one byte: {0}/{0:#x}", one_byte);
        return 0;
    }
    else {                                             // <-- parent
        std::println("parent pid = {}, child pid = {}", getpid(), pid);

        char one_byte;
        ssize_t nread = read(fd, &one_byte, 1);
        assert(nread == 1);
        std::println("parent has read one byte: {0}/{0:#x}", one_byte);
        return 0;
    }
}
$ echo abc > /tmp/somefile
$ ./sysprog-fork-fd-inher
parent pid = 267811, child pid = 267812
parent has read one byte: a/0x61
child has read one byte: b/0x62

Care For Your Children - Waiting#

  • Parents must care - wait - for their children

  • If they don’t, kids become zombies (see below)

  • ⟶ retrieve the child’s termination status (see below)

  • If child has not yet terminated, parent blocks in wait()

#include <unistd.h>
#include <sys/wait.h>
#include <assert.h>
#include <print>

int main()
{
    pid_t pid = fork();
    if (pid == -1) {
        perror("fork");
        return 1;
    }
    
    if (pid == 0) {
        sleep(3);
        std::println("child (PID={}): exiting", getpid());
        return 7;
    }
    else {
        std::println("parent (PID={}): waiting for child (PID={})", getpid(), pid);

        pid_t waited_for = wait(NULL);                 // <-- wait, no termination status needed
        if (waited_for == -1) {
            perror("wait");
            return 1;
        }
        std::println("parent (PID={}): child {} terminated", getpid(), waited_for);
        return 0;
    }
}

Copy-On-Write Memory (COW)#

  • Parent and child run the same code ⟶ share code memory pages

  • They do so read-only (code cannot be written to)

  • Question: how about data? Do they share data?

  • Answer: No!

  • Share data pages initially; copy is made at first write

  • ⟶ Copy On Write (COW)

#include <unistd.h>
#include <sys/wait.h>
#include <assert.h>
#include <print>

int main()
{
    int variable = 42;

    pid_t pid = fork();
    if (pid == -1) {
        perror("fork");
        return 1;
    }
    
    if (pid == 0) {
        variable = 666;                                // <-- copy-on-write
        return 7;
    }
    else {
        wait(NULL);                                    // <-- be sure child has touched variable
        std::println("parent: variable=={}", variable);
        return 0;
    }
}

Waiting, And Exit Status#

  • Now what about status?

  • ⟶ More information than just the exit value (7 in this case)

  • Exit status: WIFEXITED(), WEXITSTATUS()

#include <unistd.h>
#include <sys/wait.h>
#include <assert.h>
#include <print>

int main()
{
    pid_t pid = fork();
    if (pid == -1) {
        perror("fork");
        return 1;
    }

    if (pid == 0) {
        sleep(3);
        std::println("child (PID={}): exiting", getpid());
        return 7;
    }
    else {
        std::println("parent (PID={}): waiting for child (PID={})", getpid(), pid);

        int status;
        pid_t waited_for = wait(&status);
        if (waited_for == -1) {
            perror("wait");
            return 1;
        }
        std::println("child {0} has changed state, status {1:#x}, {1:#b}", waited_for, status);

        if (WIFEXITED(status))                         // <-- what if not? alternatives?
            std::println("child has exited with exit status {}", WEXITSTATUS(status));
        return 0;
    }
}

More Exit Information#

More state changes than simple exit:

  • Signaled, e.g.

    • SIGINT: Ctrl-C from terminal

    • SIGTERM: similar, but explicitly sent by another process

    • SIGSEGV, SIGBUS: software memory error (likely a pointer bug)

    • More, see man -s 7 signal

  • Stopped and continued

    • Have to use waitpid() (wait() does not give that information)

    • SIGTSTP: Ctrl-Z from terminal

    • SIGSTOP: explicitly sent

    • SIGCONT: request to continue

#include <unistd.h>
#include <sys/wait.h>
#include <assert.h>
#include <print>

int main()
{
    pid_t pid = fork();
    if (pid == -1) {
        perror("fork");
        return 1;
    }

    if (pid == 0) {
        char one_byte;
        ssize_t nread = read(STDIN_FILENO, &one_byte, 1);  // <-- continue when RETURN key hit
        assert(nread == 1);
        std::println("child (PID={}): exiting", getpid());
        return 7;
    }
    else {
        std::println("parent pid = {}, child pid = {}", getpid(), pid);

        while (true) {                                 // <-- loop: child could be
                                                       // <-- stopped and continued 
                                                       // <-- multiple times in a row
            int status;
            pid_t waited_for = waitpid(pid, &status, WUNTRACED|WCONTINUED);
            if (waited_for == -1) {
                perror("wait");
                return 1;
            }
            std::println("child {0} has changed state, status {1:#x}, {1:#b}", waited_for, status);

            if (WIFEXITED(status)) {
                std::println("child has regularly exited with exit status {}", WEXITSTATUS(status));
                break;
            }
            else if (WIFSIGNALED(status)) {
                std::println("child has been blown out of the water by signal {} (core dumped: {})", WTERMSIG(status), WCOREDUMP(status));
                break;
            }
            else if (WIFSTOPPED(status))
                std::println("child has been stopped by signal {}", WSTOPSIG(status));
            else if (WIFCONTINUED(status))
                std::println("child has been continued");
        }
            
        return 0;
    }
}

Zombies: Consequences Of Not Caring For Children#

  • Terminated process are still there - shown in ps as <defunct>

  • ⟶ Carry information for parents

  • “Zombie”

  • “Reaped” by their parents when they call wait()

#include <unistd.h>
#include <sys/wait.h>
#include <assert.h>
#include <print>

int main()
{
    pid_t pid = fork();
    if (pid == -1) {
        perror("fork");
        return 1;
    }

    if (pid == 0) {
        sleep(3);
        std::println("child exiting");
        return 7;
    }
    else {
        std::println("parent pid = {}, child pid = {}", getpid(), pid);
        pause();                                       // <-- don't care for kids
        return 0;
    }
}

Orphanage (Parent’s Death)#

  • Final question: what if a process’s parent terminates?

  • Nobody can then reap the zombie!

  • ⟶ Parentless child is reparented to PID 1 (according to good ol’ UNIX)

  • On modern Linux, one can set a dedicated reaper process (man -s 2 PR_SET_CHILD_SUBREAPER)

  • Normally the login session leader (systemd --user)

#include <unistd.h>
#include <sys/wait.h>
#include <assert.h>
#include <print>

int main()
{
    pid_t pid = fork();
    if (pid == -1) {
        perror("fork");
        return 1;
    }

    if (pid == 0)
        pause();
    else {
        std::println("parent pid = {}, child pid = {}", getpid(), pid);
        std::println("parent exits");
        return 0;
    }
}