We gave an overview of processes in the previous section on operating systems, and in this article we summarize the API around processes

Fork system call

The system call fork() is used to create a new process. It creates once and returns twice. In the parent process, returns a number greater than 0 as the PID of the child process, returns a number less than 0 as the creation failed (the number of processes in the system has reached the specified maximum number of processes), and returns 0 as the creation succeeded in the child process. So let’s do an example

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(a) {
    int rc = 0;
    printf("my pid = %d \n", getpid());

    rc = fork();

    if(rc < 0) {
        printf("fork fialed");
        exit(1);
    } else if(rc == 0) {
        printf("I am child process, my pid: %d, my parent id: %d \n", getpid(), getppid());
    } else {
        printf("I am parent process, my pid: %d, my child id: %d \n", getpid(), rc);
    }

    return 0;
}
Copy the code
My pid = 10959 I am parent process, my PID: 10959, my child ID: 10960 I am child process, my pid: 10960, my parent id: 10959Copy the code

When we create a child process using fork(), the child process does not start at main, but at fork. In the parent process, fork() returns only greater than or less than 0, while in the child process, a return of 0 indicates success.

The child does not exactly copy the parent; it has its own address space, registers, program counters, and so on.

In addition, after the process is created, it is uncertain whether the child process or the parent process will be executed first, depending on the scheduling algorithm of the process. The printing order may be different for several iterations

The fork() system call requires the unistd.h header file

Wait system call

We can use the wait() system call to make the parent process wait for the child process to finish executing before executing it

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int main(a) {
    int rc = 0;

    printf("current pid: %d\n", getpid());

    rc = fork();

    if(rc < 0) {
        printf("fork failed! \n");
        exit(1);
    } else if (rc == 0) {
        printf("I am child, pid: %d, ppid: %d\n", getpid(), getppid());
    } else {
        rc = wait(NULL);

        if(rc < 0) {
            printf("wait child failed\n");
        } else {
            printf("I am parent, pid: %d\n", getpid()); }}return 0;
}
Copy the code
Current PID: 5886 I am child, PID: 5887, ppID: 5886 I am parent, PID: 5886Copy the code

As you can see, the child process is printed first before the parent process is printed

The wait system call has the sys/wait.h header file

In addition to the wait option, there is a system call waitpid(), which is also used for the parent process to wait for the child process to complete

Exec system call

This system call can let the child to the parent process and execute different programs, in the example above, is the child and parent process execution of the same program, this is meaningless, for example in the network, is the parent process responsible for listening, when there is a connection event comes, fork out a child process to deal with, the two are parallel.

#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>

int main(a) {

    int rc = fork();

    if(rc < 0) {
        printf("fork failed!");
    } else if (rc == 0) {
        char* params[3];

        params[0] = "wc";
        params[1] = "fork_wait_exec.c";
        params[2] = NULL;

        execvp(params[0],  params);
    } else {
        rc = wait(NULL);
    }

    return 0;
}
Copy the code
25 51 349 fork_WAIT_exec.cCopy the code

The exec system call exists in the header file unistd.h

The above code creates a child process that is replaced by the WC command to count the number of lines in fork_wait_exec.c, etc. The parent waits for the child to complete.

When exec performs other functions in the child process, exec loads code and static data from the executable and overwrites its own code segments, static data, heap, stack, and other memory Spaces are reinitialized. Replace the child’s data without starting another process to execute wc

In fact, Exec is a family, which we can check through the MAN manual

It exists in the header file unistd.h

int execl(const char *path, const char *arg, .../* (char *) NULL */);
int execlp(const char *file, const char *arg, .../* (char *) NULL */);
int execle(const char *path, const char *arg, .../*, (char *) NULL, char * const envp[] */);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[], char *const envp[]);
Copy the code

There are six of them, five of which are library functions and only execve is a system call.

Parameter meanings of execl and execv:

  • The first parameter represents the path to the executable
  • The second parameter is the name of the executable file
  • From the third argument it is the argument of the executable (the last element of the array should be NULL, and the last argument of any other single argument must also be NULL)

Execvp and execlp parameters

  • The first argument is the name of the executable file. No path is required and the file will be searched from the environment variable when executed
  • The second and subsequent arguments to the executable (either array or single, null-terminated)

Execle and execvpe parameters

  • The first argument is the name of the executable file. No path is required and the file will be searched from the environment variable when executed
  • The second and subsequent arguments to the executable (either array or single, null-terminated)
  • The final argument is to add a temporary environment variable to the executable (the environment variable must be null-terminated)

The return value for all six functions is the same. On failure, -1 is returned

// An example of execution failure
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>

int main(a) {
    int execRet = 0;
    int rc = fork();

    if(rc < 0) {
        printf("fork failed!");
    } else if (rc == 0) {
        char* params[4];
		
        // Add an A here, not chmod
        params[0] = "chmoad";
        params[1] = "+x";
        params[2] = "/etc/hosts";
        params[3] = NULL;

        execRet = execvp(params[0],  params);

        if(execRet == - 1) {
            printf("exec failed"); }}else {
        rc = wait(NULL);
    }

    return 0;
}
Copy the code
Exec failedCopy the code

Exec may fail in the following cases

  • The file to be executed does not have permissions
  • Unable to find file or path (as in the above example)
  • Forget null-terminated arguments in exec functions

The benefits of designing an API this way

Fork, wait, and exec may not have much effect individually, but when combined, they can create infinite value. In Unix and Linux, shells that interact with users are created by combining the three, similar to our example above. You can do a lot of things with the time between fork and exec, such as the shell doing something to change the environment during this time.

How does the shell perform? In fact, shell is also a process. When interacting with the user, a child process is fork out to process the user’s input. Finally, the shell process waits for the child process to complete execution

A typical use is redirection in Linux

cat fork_wait_exec.c > tmp.c
Copy the code
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <fcntl.h>

int main(a) {
    int rc = fork();

    if(rc < 0) {
        printf("fork failed");
        exit(1);
    } else if(rc == 0) {
        close(STDOUT_FILENO);
        open("./tmp.c", O_CREAT|O_WRONLY|O_TRUNC, S_IRWXU);

        char* params[3];
        params[0] = "cat";
        params[1] = "redirect.c";
        params[2] = NULL;

        execvp(params[0], params);
    } else {
        rc = wait(NULL);
    }

    return 0;
}
Copy the code

The implementation of the redirection is to close the standard input-output file descriptors between fork and exec, and then assign the descriptor of the newly opened file to STDOUT_FILENO, so that when cat is executed, the output is written to tmp.c

In addition, the pipe implementation mechanism in Linux is similar to redirection, but with the pipe() system call, the output of one process is linked to one end of the pipe, and the input of another process is linked to the other end of the pipe, thus forming the pipe mechanism.

Other process apis, such as kill(), are used to kill processes and put them to sleep.

There are many apis for processes, and the challenge is not to use them, but to understand what they do in a particular scenario.