本文节选自《实验指导手册》第二版第16.7章
实验指导手册是奔跑吧Linux内核入门篇第二版配套实验书,pdf版本已经release,可以免费下载和自由打印!
下载方法:
登陆“奔跑吧linux社区”微信公众号,输入“奔跑吧2”获取下载地址。

本文是《奔跑吧Linux内核 入门篇》第二版中第16章的实验16-5:进程实验。我们在前面的实验中已经完成了printk打印函数以及时钟中断的实验了,接下来我们就可以来完成进程创建的实验了。在本实验里,我们要研究进程是如何创建的,了解新创建的进程是如何执行的。在这个实验里,我们要完成fork()函数,看看传说中的fork函数究竟是如何实现的。

实验指导手册 打印小Tips:

大家可以在某宝上随便找一个便宜的打印店,打印B5+黑白即可!很便宜,20~30元,还包邮!

20220318_155337_033.jpg
20220318_155337_034.jpg

1.实验目的

(1)了解进程控制块的设计与实现。
(2)了解进程的创建/执行过程。

2.实验要求

实现fork函数以创建一个进程,该进程一直输出数字“12345”。

3.实验提示

(1)设计进程控制块。
(2)为进程控制块分配资源。
(3)设计和实现fork函数。
(4)为新进程分配栈空间。
(5)看看新创建的进程是如何运行的。

4.实验详解

4.1 进程控制块PCB
我们使用struct task_struct数据结构来描述一个进程控制块。

struct task_struct {

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

};
State:表示进程的状态。使用enum task_state枚举类型来列举出进程的状态,有运行状态TASK_RUNNING、可中断睡眠状态TASK_INTERRUPTIBLE、不可中断的睡眠状态TASK_UNINTERRUPTIBLE、僵尸态TASK_ZOMBIE以及终止态TASK_STOPPED。

enum task_state {

TASK_RUNNING = 0,
TASK_INTERRUPTIBLE = 1,
TASK_UNINTERRUPTIBLE = 2,
TASK_ZOMBIE = 3,
TASK_STOPPED = 4,

};
Flags用来表示进程的某些标志位。目前只用来表示进程是否为内核线程。

enum task_flags {

PF_KTHREAD = 1 << 0,

};
Count用来表示进程调度用的时间片。

Priority用来表示进程的优先级。

Pid用来表示进程的PID。

cpu_context用来表示进程切换时候的硬件上下文。

4.2 0号进程
BenOS的启动流程是:上电->树莓派固件->BenOS汇编入口->BenOS kernel_main函数。这个过程,从进程的角度来看,可以看出是系统的“0号进程”。
我们需要对这个0号进程进行管理。0号进程也是需要有一个进程控制块,以方便管理。下面使用INIT_TASK宏来静态初始化0号进程的进程控制块。

/ 0号进程即init进程 /

define INIT_TASK(task) \

{ \

.state = 0,     \
.priority = 1,   \
.flags = PF_KTHREAD,   \
.pid = 0,     \

}
另外,我们还需要为0号进程分配栈空间。通常的做法是把0号进程的内核栈空间链接到数据段里。注意,这里仅仅是0号进程是这么做的,其他进程的内核栈是动态分配的。
我们首先使用task_union的方式来定义一个内核栈。

/*

  • task_struct数据结构存储在内核栈的底部
    */

union task_union {

struct task_struct task;
unsigned long stack[THREAD_SIZE/sizeof(long)];

};
这样,定义了一个内核栈的框架,在内核栈的底部,用来存储进程控制块。

640 (1).png

目前我们的BenOS还比较简单,所以内核栈的大小定义为一个页面大小,即4KB。

/ 暂时使用1个4KB页面来当作内核栈/

define THREAD_SIZE (1 * PAGE_SIZE)

define THREAD_START_SP (THREAD_SIZE - 8)

对于0号进程,我们把内核栈放到.data.init_task段里。下面通过GCC的attribute属性来完成编链接,把task_union编译链接到.data.init_task段中。

/ 把0号进程的内核栈 编译链接到.data.init_task段中 /

define __init_task_data __attribute__((__section__(".data.init_task")))

/ 0号进程为init进程 /
union task_union init_task_union __init_task_data = {INIT_TASK(task)};
另外,我们还需要在BenOS的链接文件benos.lds.S中新增一个名为.data.init_task段。修改arch/arm64/kernel/benos.lds.S文件,在数据段中新增.data.init_task段。

640 (2).png

4.3 do_fork函数的实现
本实验需要实现do_fork函数,该函数的功能是为了fork一个新进程。

新建一个task_strut,为期分配4KB页面用来存储内核栈, task_struct存在栈底。

为新进程分配PID。

设置进程的上下文。

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;

}
其中:

get_free_page()分配一个物理页面,用于进程的内核栈。

find_empty_task()查找一个空闲的PID。

copy_thread()用于设置新进程的上下文。

copy_thread()函数也是实现在kernel/fork.c文件里。

/*

  • 设置子进程的上下文信息
    */

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标志位表示新创建的进程为内核线程,这时候pstate保存了将要运行的模式为PSR_MODE_EL1h,x19保存了内核线程的回调函数,x20保存了回调函数的参数。
设置pc寄存器为ret_from_fork,即指向ret_from_fork汇编函数。设置sp寄存器指向栈的pt_regs栈框。

4.4 进程上下文切换
BenOS里的进程上下文切换函数为switch_to(),用来切换到next进程来运行。

void switch_to(struct task_struct *next)
{

struct task_struct *prev = current;

if (current == next)
    return;

current = next;
cpu_switch_to(prev, next);

}
其中核心的函数为cpu_switch_to()函数,其目的为保存prev进程的上下文,并且恢复next进程的上下文,函数原型为:

cpu_switch_to(struct task_struct prev, struct task_struct next);
cpu_switch_to()函数实现是在arch/arm64/kernel/entry.S文件里。需要保存的上下文,包括:x19 ~ x29寄存器,sp寄存器以及 lr寄存器,并且保存到进程的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

20220318_155337_035.jpg

4.5 新进程的第一次执行
在进程切换时,switch_to()函数会完成进程硬件上下文的切换,即把下一个进程(next进程)的cpu_context数据结构保存的内容恢复到处理器的寄存器中,从而完成进程的切换。此时,处理器开始运行next进程了。根据PC寄存器的值,处理器会从ret_from_fork汇编函数里开始执行,新进程的执行过程如图所示。

20220318_155337_036.jpg

图 新进程的执行过程
ret_from_fork汇编函数实现在arch/arm64/kernel/entry.S文件中。

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
在第4行中,判断next线程是否为内核线程。如果next进程是内核线程,在创建时会设置X19寄存器指向stack_start。如果X19的值寄存器为0,说明这个next进程是用户进程,直接跳转到第6行,调用ret_to_user汇编函数,返回用户空间。不过我们这里ret_to_user函数并没有实现。
在第4~6行中,如果next进程是内核线程,那么直接跳转到内核线程的回调函数里。
综上所述,当处理器切换到内核线程时,它从ret_from_fork汇编函数开始执行。

5.实验步骤

我们先在QEMU上模拟。由于本实验的参考代码还没有实现对GIC-400中断控制器的支持,因此,在QEMU里我们只能模拟树莓派3b了。
在Ubuntu Linux主机中,进入参考实验代码目录。

rlk@rlk :$ cd /home/rlk/rlk/runninglinuxkernel_5.0/kmodules/rlk_lab/rlk_basic/chapter_16_benos/lab05_add_fork/
进入make menuconfig菜单。

rlk@master:lab05_add_fork $ make menuconfig
其中:

Board selection (Raspberry 3B) -> 选树莓派3b
Uart for Pi (pl_uart) -> 选PL串口
图片

编译并运行。

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

6. 实验参考代码

实验参考代码runninglinuxkenrel_5.0的git repo。

国内访问:
https://benshushu.coding.net/...

github(国外访问):
https://github.com/figozhang/...
本文对应的参考代码在如下目录:kmodules/rlk_lab/rlk_basic/chapter_16_benos/lab05_add_fork

我们提供配置好的实验环境,基于ubuntu 20.04的VMware/Vbox镜像,可以通过如下方式获取下载地址:
登陆“奔跑吧linux社区”微信公众号,输入“奔跑吧2”获取下载地址。

20220318_155337_037.jpg
20220318_155337_038.jpg


奔跑吧Linux社区
4 声望4 粉丝

奔跑吧Linux社区,为广大小伙伴布道Linux开源!