Everything you need to know to start coding your own shell

Process

A process is an executing program with a unique process ID (PID). The PID is an integer value used by the operating system for process management, resource allocation, and inter-process communication.

PPID, or Parent Process IDentifier, represents the PID of the process that created another process, establishing a parent-child relationship. Understanding PID and PPID is crucial for managing processes, determining hierarchies, and enabling communication and synchronization within an operating system.

You can use the system call getpid (man 2 getpid)

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

/**
 * main - PID
 *
 * Return: Always 0.
 */
int main(void)
{
    pid_t my_pid;

    my_pid = getpid();
    printf("%u\n", my_pid);
    return (0);
}

Note in the example above, that every time you run the program, a new process is created, and its ID is different.

Each process has a parent: the process that created it. It is possible to get the PID of a parent process by using the getppid system call (man 2 getppid), from within the child process.

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

/**
 * main - PID
 *
 * Return: Always 0.
 */
int main(void)
{
    pid_t my_ppid;

    my_ppid = getppid();
    printf("%u\n", my_ppid);
    return (0);
}

In other words, the PPID represents the PID of the process that initiated the execution of another process. It establishes a hierarchical relationship between processes, forming a parent-child relationship. When a process creates a child process, the child process inherits the PPID of its parent.

Arguements

Command parsing and execution: Learn how to parse user input to identify commands, arguments, and options. Implement the logic to execute commands by creating child processes, handling input/output redirection, and managing process execution.

The command line arguments are passed through the main function: int main(int ac, char **av);

  • av is a NULL terminated array of strings

  • ac is the number of items in av

Write a program that prints all the arguments, without using ac.

#include <stdio.h>

// The main function that serves as the entry point of the program
// It takes in two arguments:
// - ac: the argument count, representing the number of command line arguments
// - av: the argument vector, a NULL-terminated array of strings containing the command line arguments
int main(int ac, char **av) {
    int i = 1;  // Start from av[1] to skip av[0] (program name)

    // Loop through the arguments until encountering NULL
    while (av[i] != NULL) {
        printf("%s\n", av[i]);  // Print the current argument
        i++;
    }

    return 0;
}

Write a program that prints "$ ", wait for the user to enter a command, and prints it on the next line.

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

int main(int ac, char **av) {
    printf("$ ");  // Print the prompt symbol

    char command[100];  // Buffer to store the entered command

    // Read the command until encountering end-of-file (EOF)
    if (fgets(command, sizeof(command), stdin) != NULL) {
        printf("%s", command);  // Print the entered command
    }

    return 0;
}

Executing a program

The system call execve allows a process to execute another program. Note that this system call does load the new program into the current process’ memory in place of the “previous” program: on success execve does not return to continue the rest of the “previous” program.

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

/**
 * main - execve example
 *
 * Return: Always 0.
 */
int main(void)
{
    char *argv[] = {"/bin/ls", "-l", "/usr/", NULL};

    printf("Before execve\n");
    if (execve(argv[0], argv, NULL) == -1)
    {
        perror("Error:");
    }
    printf("After execve\n");
    return (0);
}

Creating processes

The system call fork creates a new child process, almost identical to the parent (the process that calls fork). Once fork successfully returns, two processes continue to run the same program, but with different stacks, data and heaps.

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

/**
 * main - fork example
 *
 * Return: Always 0.
 */
int main(void)
{
    pid_t my_pid;
    pid_t pid;

    printf("Before fork\n");
    pid = fork();
    if (pid == -1)
    {
        perror("Error:");
        return (1);
    }
    printf("After fork\n");
    my_pid = getpid();
    printf("My pid is %u\n", my_pid);
    return (0);
}

Wait

The wait system call (man 2 wait) suspends execution of the calling process until one of its children terminates.

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

/**
 * main - fork & wait example
 *
 * Return: Always 0.
 */
int main(void)
{
    pid_t child_pid;
    int status;

    child_pid = fork();
    if (child_pid == -1)
    {
        perror("Error:");
        return (1);
    }
    if (child_pid == 0)
    {
        printf("Wait for me, wait for me\n");
        sleep(3);
    }
    else
    {
        wait(&status);
        printf("Oh, it's all better now\n");
    }
    return (0);
}

fork + wait + execve

Write a program that executes the command ls -l /tmp in 5 different child processes. Each child should be created by the same process (the father). Wait for a child to exit before creating a new child.

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

int main() {
    int num_children = 5;
    int i;

    for (i = 0; i < num_children; i++) {
        pid_t pid = fork();

        if (pid == -1) {
            perror("fork error");
            exit(1);
        } else if (pid == 0) {
            // Child process
            printf("Child %d: PID %d - Start\n", i + 1, getpid());
            execl("/bin/ls", "ls", "-l", "/tmp", NULL);
            perror("execl error");
            exit(1);
        } else {
            // Parent process
            int status;
            wait(&status);  // Wait for the child to exit
            printf("Child %d: PID %d - End\n", i + 1, pid);
        }
    }

    return 0;
}

Next

We will look at File system operations