# Process Management

This blog will discuss process creation and termination from the programmer’s point of view.

## Process creation

Init is the first process that gets spawned during the boot process and this process gets process Id 1. All the other processes are created using a system call called, fork().

The process which calls the fork is called the parent process and the process which gets created as a result of the fork call is called a child process. A child process will get the parent process Id (PPID) that of the parent process which created it.

A new *task\_struct* data structure gets created, every time a new process gets created. The memory requirements are handled by the SLAB cache.

<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">task_struct has been discussed in another blog <a target="_self" rel="noopener noreferrer nofollow" href="https://hashnode.com/post/cmh9g1f8g000002l50rzi3wqv" style="pointer-events: none">here</a>.</div>
</div>

Let’s see how a process is created.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1763241892194/c8d77534-3f01-47a2-9ea9-aae5fd9e1a19.png align="left")

* A parent process calls fork() system call.
    
* The fork() call is intercepted in ‘./kernel/fork.c’ and calls kernel\_clone(). The later takes a parameter called *flags. Flags* help decide which resources can be shared between the child and the parent process.
    

> flags have been explained later in this blog.

* In kernel\_clone(), a call to copy\_process() creates a new process( say p), but not activated or scheduled yet. This step involves copying the relevant resources as per the *flags defined.*
    
* Process ‘p’ is scheduled and it starts running.
    

Different flags that decide how the child process will be created as below:

| CLONE\_VM | Parent and Child share address space |
| --- | --- |
| CLONE\_FILES | Parent and Child share open files |
| CLONE\_FS | Parent and Child share file system information |
| CLONE\_SIGHAND | Parent and Child share the signal handlers and blocked signals |
| CLONE\_PIDFD | If pidfd of the child should be placed in the parent process |
| CLONE\_PTRACE | Continue tracing the child too |
| CLONE\_VFORK | Used when vfork() is used instead of the fork(). Parent process will be forced to sleep until the child process returns |
| CLONE\_PARENT | Child enforces to have the same parent which created it |
| CLONE\_THREAD | Parent and child are in the same thread group |
| CLONE\_NEWNS | Create a new namespace for the child process |
| CLONE\_SYSVSEM | Parent and child share the system-V semantics |
| CLONE\_SETTLS | Create a new TLS for the child process |
| CLONE\_PARENT\_SETTID | Set the TID in the parent |
| CLONE\_CHILD\_CLEARTID | Clear the TID in the child |
| CLONE\_CHILD\_SETTID | Set the TID of the child |
| CLONE\_DETACHED | Detach and ignore the child process |
| CLONE\_UNTRACED | Ignore the CLONE\_PTRACE on the child |
| CLONE\_NEWCGROUP | Create a new cgroup namespace |
| CLONE\_NEWUTS | Create a new utsname namespace |
| CLONE\_NEWIPC | Create a new ipc namespace |
| CLONE\_NEWUSER | Create a new user namespace |
| CLONE\_NEWPID | Create a new pid namespace |
| CLONE\_NEWNET | Create a new network namespace |
| CLONE\_IO | Clone the IO context |

**copy\_process()**

* Make a call to dup\_task\_struct(), which creates a new kernel stack, thread\_info and task\_struct structure for the new process. The new values are identical to those of the current task. At this point, the child and parent process descriptors are identical.
    
* It then checks that the new child will not exceed the resource limits on the number of processes for the current user.
    
* The child needs to differentiate itself from the parent. Various members of the process descriptor are cleared or set to initial values. Members of the process descriptor not inherited are primarily statistically information. The bulk of the values in task\_struct remain unchanged.
    
* The child’s state is set to TASK\_UNINTERRUPTIBLE to ensure that it does not yet run.
    
* copy\_process() calls copy\_flags() to update the flags member of he task\_struct.
    
* It calls alloc\_pid() to assign an available PID to the new task.
    
* Depending on the flags passed to kernel\_clone(), copy\_process() either duplicates or shares open files, filesystem information, signal handlers, process address space and namespace. These resources are typically shared between threads in the same process, otherwise they are unique and thus copied here.
    
* Finally, copy\_process() cleans up and returns to the caller a pointer to the new child.
    

**Process states**

Each process goes through the below states, through its life time.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1764350143856/5fad2a20-2436-49fb-89de-1addc830f07d.png align="center")

* When the process gets created, it gets the TASK\_RUNNING(ready) status. In this state, the process is ready to be run by the scheduler.
    
* Once the scheduler puts the process to run, its state is moved to TASK\_RUNNING(running) status.
    
* When the process is pre-emptied by the scheduler for another higher priority one, it moves back from TASK\_RUNNING(running) to TASK\_RUNNING(ready) state.
    
* When a process waits for any event(like IO), its moved to TASK\_INTERRUPTABLE(waiting) state.
    
* When the event is full-filled, the process is moved back to TASK\_RUNNING(ready) state.
    
* When a process calls exit() or receives any signal or exception, it is moved to TASK\_STOPPPED state and is terminated.
    

---

## Process address space

> Linux is a virtual memory operating system, meaning the physical memory is virtualized for each process. Meaning, each process’s view of memory is as if it has the entire physical memory to access.

The process address space consists of the virtual memory addressable by a process and the address within the virtual memory that the process is allowed to use. Each process is given a flat 32-bit or 64-bit address space, with the size dependent on the architecture.

A memory address is a given value in the given address space, **0X5012E300**. This value identifies a byte in a process’s 32-bit address space. A process address applicable for a particular process can be represented as a range, for ex, **0X0400A000 - 0X0400AFFF**. These intervals are usually called memory areas. The process, through the kernel, can add or remove memory areas to its address space.

> Each memory area is associated with permissions like readable, writable and executable.If a process accesses any address which it doesn’t have permission to, it results in segmentation fault.

Each process is associated with the below memory areas:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1764286582632/718a3652-2676-456d-9d7e-b3db211ce3eb.png align="center")

* A memory map of the executable file’s code, called a ***text*** section.
    
* A memory map of the executable file’s initialized global variables, called the ***data*** section.
    
* A memory map of the executable file’s uninitialized global variables, called the **bss** section.
    
* A memory map for the process’s user space stack.
    
* An additional text, data and bss section for each of the shared library( such as glibc, dynamic linker etc) loaded into the process’s address space.
    
* Any memory mapped files.
    
* Any shared memory segments(ipc related).
    
* Any anonymous memory mappings, such as those associated with malloc().
    

---

## Virtual memory areas

Each of the color coded segment in the above process address space is called a virtual memory area(VMA). This is represented by a data structure called **vm\_area\_struct,** which is defined in the file ./linux/mm\_types.h.

The above process address space may look like below with VMAs

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1764288773551/89c55694-6836-4a65-a9c5-8c793600a0e0.png align="center")

Programmatically, each process is represented by a data structure of type ‘struct task\_struct’. This data structure has a field called ‘struct mm\_struct’ which maintains a linked list of type ‘struct vm\_area\_struct’.

Each of the VMA has a start address (vm\_start) and ending address(vm\_end). All the address ranges being held by the process are virtual in nature. We will learn later that each of the VMA points to the physical memory through **pages**.

System calls like mmap(), mmap2(), do\_mmap() can be used to add VMAs. System calls like munmap() and do\_munmap() can be used to remove them.

> More about memory management will be covered in another blog

---

**Process address space in real life…**

Let’s create some simple and sample programs and check practically how the process address space looks like by running some commands.

<details data-node-type="hn-details-summary"><summary>Commands</summary><div data-type="detailsContent">We can use commands like ‘pmap &lt;pid&gt;’ or ‘cat /proc/&lt;pid&gt;/maps’ to read the process address map of a process. The first command gives a more user friendly output. We can use these commands only when the process is running.</div></details>

1. A program with no global, static or local variables. I am using C but the program language shouldn’t matter.
    
    ```c
    #include<unistd.h>
    
    int main(int argc, char *argv[]) { 
      # without this sleep() command, the process doesnt persists for 
      # enough time to get the details
      sleep(100);
      return 0;
    }
    ```
    

The process address space is as below. The memory range may be different each time. Also, the memory ranges may not be contiguous, as the kernel assigns the memory as per the availability.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1764447784300/b200874b-517a-499d-8bcf-93499a7bd46d.png align="center")

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1764447806856/4542c0f3-9fd2-4b25-bd7a-a5651f333ef3.png align="center")

Let’s try to understand what each of the above attributes mean:

* \[vdso\]: **V**irtual **D**ynamically Linked **S**hared **O**bjects. This is a mechanism through which the interaction between the user space and kernel space is optimized for better performance.
    
* \[vsyscall\]: This is a static allocation (each process uses the same vma), used to optimize the system call performance. The output of the system call is stored here for future reference. Very similar to cache used in other places.
    
* \[vvar\]: Not much information is available about this vma.
    
* \[stack\]: This vma is the user space stack. We have read and write permissions on this vma.
    
* \[ anon \]: vma for anonymous memory. This memory is usually assigned to the process by the kernel which is not backed by a file on the file system. Mostly for internal purposes.
    
* \[libc.so.6\]: This is the C library(glibc), which is required to interpret the code.There are 5 entries, as below:
    
    * r——: This is a vma of type ‘rodata’(read only) for the C library(libc.so.6)
        
    * r-x—: This is a vma with read and execute permissions, meaning this hold the actual code of text of libc.so.6
        
    * r——: Another ‘rodata’ vma
        
    * r——: Another ‘rodata’ vma
        
    * rw—-: A read and write vma(rwdata) for the variables which allows reads and writes.
        
* \[ld-linux-x86-64.so.2\]: This is a dynamic linker which is required to link the code with other shared libraries. We have similar ‘rodata ‘and ‘rwdata’ vmas for this library, similar to glibc.
    
* \[p1\]: This is my actual C code file name. Kernel has assigned 5 vma’s for this code too, similar to above entries.
    

As you might have noted, a minimum of 4K has been assigned for each vma. This is the page size on my machine. All the memory assignments are in multiples of this pagesize.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1764449199544/bdb53473-ab2b-43a7-b07b-4a878265d4d2.png align="center")

Also, a total of 2616K (2.6 MB) has been assigned even to a simple program like this.

2. Process address map for all the below scenarios were same as above.
    
    * A program with an uninitialized global variable.
        
    * A program with initialized or uninitialized local variables.
        
    * A program with static variables.
        
    
    This means that the kernel assigns a minimum of one page size memory to each vma irrespective of whether the program needs it or not.
    
3. A program makes a call to malloc() for dynamic memory allocation.
    
    ```c
    #include<unistd.h>
    #include<cstdlib>
    #include<cstring>
    
    struct emp_det {
            long emp_id;
            char name[100];
    };
    
    int main(int argc, char *argv[]) {
      struct emp_det* ep = (struct emp_det *)malloc(sizeof(struct emp_det));
      ep->emp_id = 1234;
      strcpy(ep->name, "citika");
    
      sleep(100);
      return 0;
    }
    ```
    

The process address space in this case as shown below:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1764455619727/005ba035-bcbd-4ede-9da7-ac0145ac6fa4.png align="center")

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1764455645685/509088b6-52d5-4d94-ae66-db0457cc54c4.png align="center")

As you can see, there is an extra 132K of memory assignment to a \[heap\] region. This is for the dynamic allotments for the call to malloc().

4. Two processes which are running the same code.
    
    ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1764456256545/4b745828-db5c-4869-bc0b-5345fc489085.png align="center")
    
    ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1764456271404/ac4d032f-846f-46fe-a23d-7947cb3943ed.png align="center")
    
    Both the process address maps look similar. Also, note that though they use the same glibc and linker’s, they are mapped to different vmas. This is an interesting finding, as the expectation was all the processes which use the same shared libraries point to the same memory areas.
    
5. A child process created using the fork() system call. The goal is to check the process memory space for both the child and parent.
    
    ```c
    #include <stdio.h>
    #include <unistd.h>
    #include <sys/types.h>
    #include <sys/wait.h>
    
    int main() {
        pid_t pid = fork();
    
        if (pid < 0) {
            // fork() failed
            perror("fork failed");
            return 1;
        }
        else if (pid == 0) {
            // Child process
            printf("Child process: PID = %d, Parent PID = %d\n",
                   getpid(), getppid());
    
            // Simulate some work
            sleep(100);
    
            printf("Child exiting...\n");
        }
        else {
            // Parent process
            printf("Parent process: PID = %d, waiting for child PID = %d\n",
                   getpid(), pid);
    
            int status;
            wait(&status);   // Wait for child to finish
    
            printf("Parent: child exited with status %d\n", WEXITSTATUS(status));
        }
    
        return 0;
    }
    ```
    
    Process address map of the parent is:
    
    ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1764458043288/57bc61db-57e0-46a3-99ca-1e8b43aa263b.png align="center")
    
    Process address map of the child is:
    
    ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1764458060874/385cf0b5-b272-4465-ab2b-2293c658acf8.png align="center")
    
    As you see, the parent and child are sharing the same process address space.
    
    ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1764458400164/da558475-aa6e-4fa5-8c4d-20d3455b4701.png align="center")
    
    This is as per the design technique called Copy-On-Write(CoW) in which the kernel delays assigning the child to new address space until it is required. In this case, child process was not doing anything and hence was sharing the parent address space. This technique allows faster process execution.
    
6. Let’s see a case where a child process is created and executes a new program. Here we are making the child process run ‘ls’ program.
    

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

int main() {
    pid_t pid = fork();

    if (pid < 0) {
        perror("fork failed");
        return 1;
    }
    else if (pid == 0) {
        // Child process
        printf("Child: PID = %d, Parent PID = %d\n", getpid(), getppid());
        printf("Child: sleeping for 100 seconds inside exec...\n");

        // exec: run "sleep 100; ls -l"
        execl("/bin/sh", "sh", "-c", "sleep 100; ls -l", NULL);

        // Only reached if exec fails
        perror("exec failed");
        return 1;
    }
    else {
        // Parent process
        printf("Parent: PID = %d, Child PID = %d\n", getpid(), pid);
        printf("Parent: waiting for child to finish...\n");

        int status;
        wait(&status);

        printf("Parent: child exited with status %d\n", WEXITSTATUS(status));
    }

    return 0;
}
```

The parent process address map is as shown below:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1764459949091/ceefe4a8-5d46-4719-9728-3d2f8efa41ba.png align="center")

The child process address map is as shown below:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1764459970204/500b6211-12c4-4968-a809-7f4576b1b9b3.png align="center")

As you can see, both parent and child are using different process address spaces.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1764460445054/086e30f7-2c58-4e2e-ac46-b40e252b847c.png align="center")

Again, its CoW feature working behind the scenes.

---

## Process Termination

A Process is terminated in the below ways:

* A voluntary termination when the process calls exit() call or just a ‘return’ statement.
    
* An involuntary termination happens when the process receives a signal or exception which it cannot handle or ignore.
    

When a process exits, a series of actions happen, which might be the reverse of the actions during the process creation. Process runs do\_exit() call which takes care of the process termination.

* Set the PF\_EXITING flag in the flags member of task\_struct.
    
* Call del\_timer\_sync() to remove any kernel timers, which might have been activated for this process.
    
* Flush out the accounting information, when it is set.
    
* Calls the exit\_mm() to release the mm\_struct held by the process. If no other process is using this address space, then release the memory from the slab cache.
    
* Calls exit\_sem(), to release any IPC related semaphore’s from the waiting queues.
    
* Calls exit\_files() and exit\_fs() to decrement the usage count of the objects related to file descriptors and file system data. If this count is zero, then remove the entries from the slab cache too.
    
* Set the exit\_code in the task\_struct construct as volunteered by the exit process.
    
* Calls exit\_notify() to send signals to the parent process. This process children are de-parented to any other process in the same thread group or to the init process. The exit\_state in the task\_struct is set to EXIT\_ZOMBIE.
    
* Send communication to the scheduler to schedule another process. This happens as the final step in the do\_exit() and this call never returns.
    

At this stage, the process is not in runnable state and doesn’t have any process address space of its own and is in EXIT\_ZOMBIE state. The only memory associated is with the task\_struct, thread\_info in the slab cache. This memory exists so that the parent process can claim some information out of it. Once this is completed, the slab cache is freed too.

> A process becomes zombie, when its parent tries to clear the process table of the dead child(alive parent and dead child).
> 
> A process becomes orphan when its parent is dead and is being re-parented(dead parent and alive child).

---
