Hands-On Training: Writing a Small OS on Raspberry Pi (6): Experiment 16-5: Process Creation Experiment

This article is excerpted from the “Experiment Guide Manual” second edition, Chapter 16.7. The Experiment Guide Manual is a companion experimental book for “Run Linux Kernel Introduction” second edition, and the PDF version has been released and can be downloaded and printed freely! Download method: Log in to the “Run Linux Community” WeChat public account, enter “Run Linux 2” to get the download link.

This article is Experiment 16-5: Process Experiment in Chapter 16 of the second edition of “Run Linux Kernel Introduction”. In previous experiments, we have completed the printk printing function and the clock interrupt experiment. Next, we can complete the process creation experiment. In this experiment, we will study how processes are created and understand how the newly created process executes. In this experiment, we will implement the fork() function and see how the legendary fork function is actually implemented.

Experiment Guide Manual Printing Tips:

You can find a cheap printing shop on Taobao, print B5+ black and white! Very cheap, 20~30 yuan, and free shipping!

Hands-On Training: Writing a Small OS on Raspberry Pi (6): Experiment 16-5: Process Creation Experiment

Hands-On Training: Writing a Small OS on Raspberry Pi (6): Experiment 16-5: Process Creation Experiment

1. Experiment Purpose

(1) Understand the design and implementation of the process control block. (2) Understand the process creation/execution process.

2. Experiment Requirements

Implement the fork function to create a process that continuously outputs the number “12345”.

3. Experiment Tips

(1) Design the process control block. (2) Allocate resources for the process control block. (3) Design and implement the fork function. (4) Allocate stack space for the new process. (5) See how the newly created process runs.

4. Experiment Details

4.1 Process Control Block PCB

We use the struct task_struct data structure to describe a process control block.

struct task_struct {
    enum task_state state;
    enum task_flags flags;
    long count;
    int priority;
    int pid;
    struct cpu_context cpu_context;
};

State: Represents the state of the process. The enum task_state enumeration type is used to enumerate the process states, including running state TASK_RUNNING, interruptible sleep state TASK_INTERRUPTIBLE, uninterruptible sleep state TASK_UNINTERRUPTIBLE, zombie state TASK_ZOMBIE, and stopped state TASK_STOPPED.

enum task_state {
    TASK_RUNNING = 0,
    TASK_INTERRUPTIBLE = 1,
    TASK_UNINTERRUPTIBLE = 2,
    TASK_ZOMBIE = 3,
    TASK_STOPPED = 4,
};

Flags are used to represent certain flags of the process. Currently, it is only used to indicate whether the process is a kernel thread.

enum task_flags {
    PF_KTHREAD = 1 << 0,
};
  • Count is used to represent the time slice for process scheduling.

  • Priority is used to represent the priority of the process.

  • Pid is used to represent the PID of the process.

  • cpu_context is used to represent the hardware context during process switching.

4.2 Process 0

The startup process of BenOS is: power-on -> Raspberry Pi firmware -> BenOS assembly entry -> BenOS kernel_main function. From the perspective of processes, this can be seen as the system’s “process 0”. We need to manage this process 0. Process 0 also needs to have a process control block for management convenience. Below we use the INIT_TASK macro to statically initialize the process control block of process 0.

/* Process 0 is the init process */
#define INIT_TASK(task) \
{                      \
    .state = 0,     \
    .priority = 1,   \
    .flags = PF_KTHREAD,   \
    .pid = 0,     \
}

In addition, we also need to allocate stack space for process 0. The usual approach is to link the kernel stack space of process 0 to the data segment. Note that this is only done for process 0; the kernel stacks of other processes are dynamically allocated. We first define a kernel stack using the task_union method.

/*
 * task_struct data structure is stored at the bottom of the kernel stack
 */
union task_union {
    struct task_struct task;
    unsigned long stack[THREAD_SIZE/sizeof(long)];
};

This defines a framework for the kernel stack, storing the process control block at the bottom of the kernel stack.

Hands-On Training: Writing a Small OS on Raspberry Pi (6): Experiment 16-5: Process Creation Experiment

Currently, our BenOS is relatively simple, so the size of the kernel stack is defined as one page size, which is 4KB.

/* Temporarily use 1 4KB page as the kernel stack*/
#define THREAD_SIZE  (1 * PAGE_SIZE)
#define THREAD_START_SP  (THREAD_SIZE - 8)

For process 0, we place the kernel stack in the .data.init_task section. Below, we complete the compilation and linking of the task_union to the .data.init_task section using GCC’s attribute attribute.

/* Compile and link the kernel stack of process 0 to the .data.init_task section */
#define __init_task_data __attribute__((__section__(".data.init_task")))

/* Process 0 is the init process */
union task_union init_task_union __init_task_data = {INIT_TASK(task)};

In addition, we also need to add a section named .data.init_task in BenOS’s linker file benos.lds.S. Modify the arch/arm64/kernel/benos.lds.S file to add the .data.init_task section in the data segment.

Hands-On Training: Writing a Small OS on Raspberry Pi (6): Experiment 16-5: Process Creation Experiment

4.3 Implementation of do_fork Function

This experiment requires the implementation of the do_fork function, which is used to fork a new process.

  • Create a new task_struct, allocating a 4KB page to store the kernel stack, where task_struct is at the bottom of the stack.

  • Allocate a PID for the new process.

  • Set the context of the process.

int do_fork(unsigned long clone_flags, unsigned long fn, unsigned long arg)
{
    struct task_struct *p;
    int pid;

    p = (struct task_struct *)get_free_page();
    if (!p)
        goto error;

    pid = find_empty_task();
    if (pid < 0)
        goto error;

    if (copy_thread(clone_flags, p, fn, arg))
        goto error;

    p->state = TASK_RUNNING;
    p->pid = pid;
    g_task[pid] = p;

    return pid;

error:
    return -1;
}

Among them:

  • get_free_page() allocates a physical page for the process’s kernel stack.

  • find_empty_task() searches for an available PID.

  • copy_thread() is used to set the context of the new process.

copy_thread() function is also implemented in the kernel/fork.c file.

/*
 * Set the context information of the child process
 */
static int copy_thread(unsigned long clone_flags, struct task_struct *p,
        unsigned long fn, unsigned long arg)
{
    struct pt_regs *childregs;

    childregs = task_pt_regs(p);
    memset(childregs, 0, sizeof(struct pt_regs));
    memset(&p->cpu_context, 0, sizeof(struct cpu_context));

    if (clone_flags & PF_KTHREAD) {
        childregs->pstate = PSR_MODE_EL1h;
        p->cpu_context.x19 = fn;
        p->cpu_context.x20 = arg;
    }

    p->cpu_context.pc = (unsigned long)ret_from_fork;
    p->cpu_context.sp = (unsigned long)childregs;

    return 0;
}

PF_KTHREAD flag indicates that the newly created process is a kernel thread. In this case, the pstate saves the running mode as PSR_MODE_EL1h, x19 saves the kernel thread’s callback function, and x20 saves the callback function’s parameters. The pc register is set to ret_from_fork, which points to the ret_from_fork assembly function. The sp register is set to point to the pt_regs stack frame of the stack.

4.4 Process Context Switching

The process context switch function in BenOS is switch_to(), which is used to switch to the next process for execution.

void switch_to(struct task_struct *next)
{
    struct task_struct *prev = current;

    if (current == next)
        return;

    current = next;
    cpu_switch_to(prev, next);
}

Among them, the core function is cpu_switch_to(), which aims to save the context of the prev process and restore the context of the next process. The function prototype is:

  cpu_switch_to(struct task_struct *prev, struct task_struct *next);

cpu_switch_to() function is implemented in the arch/arm64/kernel/entry.S file. The context to be saved includes: registers x19 to x29, the sp register, and the lr register, which are saved to the process’s task_struct->cpu_context.

.align
.global cpu_switch_to
cpu_switch_to:
    add     x8, x0, #THREAD_CPU_CONTEXT
    mov     x9, sp
    stp     x19, x20, [x8], #16
    stp     x21, x22, [x8], #16
    stp     x23, x24, [x8], #16
    stp     x25, x26, [x8], #16
    stp     x27, x28, [x8], #16
    stp     x29, x9, [x8], #16
    str     lr, [x8]

    add     x8, x1, #THREAD_CPU_CONTEXT
    ldp     x19, x20, [x8], #16
    ldp     x21, x22, [x8], #16
    ldp     x23, x24, [x8], #16
    ldp     x25, x26, [x8], #16
    ldp     x27, x28, [x8], #16
    ldp     x29, x9, [x8], #16
    ldr     lr, [x8]
    mov     sp, x9
    ret

Hands-On Training: Writing a Small OS on Raspberry Pi (6): Experiment 16-5: Process Creation Experiment

4.5 The First Execution of the New Process

During the process switch, the switch_to() function completes the switching of the hardware context of the process, restoring the contents saved in the cpu_context data structure of the next process (next process) to the processor’s registers, thus completing the process switch. At this point, the processor starts to run the next process. According to the value of the PC register, the processor will start executing from the ret_from_fork assembly function, and the execution process of the new process is shown in the figure.

Hands-On Training: Writing a Small OS on Raspberry Pi (6): Experiment 16-5: Process Creation Experiment

Figure: The execution process of the new process. The ret_from_fork assembly function is implemented in the arch/arm64/kernel/entry.S file.

1     .align 2
2     .global ret_from_fork
3     ret_from_fork:
4         cbz x19, 1f
5         mov x0, x20
6         blr x19
7     1:
8         b ret_to_user
9         
10    .global ret_to_user
11    ret_to_user:
12         inv_entry 0, BAD_ERROR

In line 4, it checks whether the next thread is a kernel thread. If the next process is a kernel thread, the X19 register will be set to point to stack_start during creation. If the value of the X19 register is 0, it indicates that this next process is a user process, directly jumping to line 6 to call the ret_to_user assembly function to return to user space. However, we have not implemented the ret_to_user function here. In lines 4-6, if the next process is a kernel thread, it directly jumps to the callback function of the kernel thread. In summary, when the processor switches to a kernel thread, it starts executing from the ret_from_fork assembly function.

5. Experiment Steps

We first simulate on QEMU. Since the reference code for this experiment has not yet implemented support for the GIC-400 interrupt controller, we can only simulate Raspberry Pi 3B on QEMU. On the Ubuntu Linux host, enter the directory of the reference experimental code.

rlk@rlk :$ cd /home/rlk/rlk/runninglinuxkernel_5.0/kmodules/rlk_lab/rlk_basic/chapter_16_benos/lab05_add_fork/

Enter the make menuconfig menu.

rlk@master:lab05_add_fork $ make menuconfig

Among them:

Board selection (Raspberry 3B) -> Select Raspberry Pi 3B
Uart for Pi (pl_uart) -> Select PL Serial

Hands-On Training: Writing a Small OS on Raspberry Pi (6): Experiment 16-5: Process Creation Experiment

Compile and run.

rlk@master:lab05_add_fork $ make
rlk@master: lab05_add_fork $ make run

Hands-On Training: Writing a Small OS on Raspberry Pi (6): Experiment 16-5: Process Creation Experiment

6. Reference Code for the Experiment

The reference code for the experiment is in the runninglinuxkernel_5.0 git repo.

Access from China:
https://benshushu.coding.net/public/runninglinuxkernel_5.0/runninglinuxkernel_5.0/git/files

github (access from abroad):
https://github.com/figozhang/runninglinuxkernel_5.0

The reference code corresponding to this article is in the following directory: kmodules/rlk_lab/rlk_basic/chapter_16_benos/lab05_add_fork

We provide a configured experimental environment, based on Ubuntu 20.04 VMware/Vbox image, which can be obtained by logging into the “Run Linux Community” WeChat public account and entering “Run Linux 2” to get the download link.

Hands-On Training: Writing a Small OS on Raspberry Pi (6): Experiment 16-5: Process Creation Experiment

Hands-On Training: Writing a Small OS on Raspberry Pi (6): Experiment 16-5: Process Creation Experiment

Leave a Comment

×