前情提要
前面我们已经实现了文件系统,后面我们将实现linux中非常重要的一个调用,fork
fork是什么?叉子?对,fork就是叉子,没有比这个更完美的答案了。
一、什么是fork
fork
函数原型是pid_t fork(void)
,返回值是数字,该数字有可能是子进程的pid,有可能是0,也有可能是−1。为了让父进程获知自己的孩子是谁,fork会给父进程返回子进程的pid。子进程可以通过系统调用getppid
获知自己的父亲是谁,并且没有pid为0的进程,因此fork给子进程返回0,以从返回值上和父进程区分开来。如果fork失败了,返回的数字便是−1,自然也没有子进程产生。
让我们看一下下面的例子
#include "unistd.h"
#include "stdio.h"
int main() {
int pid = fork();
if (pid == -1) {
return -1;
}
if (pid) {
printf("I am father, my pid is %d\n",getpid());
return 0;
} else {
printf("I am child, my pid is %d\n",getpid());
return 0;
}
}
在fork函数调用之后,后面会根据pid的结果跳转到不同的分支,注意,此时已经有两个进程在执行相同的代码了,只是两个进程中fork的返回值不同。
如果进程的pid值非0,那么说明此时执行的是父进程。
如果进程的pid值为0,那么说明此时执行的是子进程。
fork之后,由之前的一个进程变成了两个进程,fork的作用就是克隆进程。进程拥有独立的地址空间,因此两个进程执行的是独立且相同的代码,也就是两套代码,而且它们各自的指令中都包括fork调用,只是子进程是在fork函数返回之后才开始执行的,因此执行的是fork之后的代码(其实可以让子进程的执行流回到fork之前重新调用fork,但意义不大且非常麻烦),所以在fork之后,父子进程像是“分道扬镳”了。
程序是指在磁盘上存储的文件,是静态的,进程是指程序被加载到内存后,在内存中运行中的程序映像,简而言之,进程就是运行的程序。父子进程的进程体(代码段数据段等)是一模一样的,相当于执行了同一程序的两个实例进程。
二、fork的实现
fork 的实现分为两步,先复制资源,再跳过去执行
fork 需要复制的资源有
(1)进程的pcb,即task_struct,这是让任务有“存在感”的身份证。
(2)程序体,即代码段数据段等,这是进程的实体。
(3)用户栈,不用说了,编译器会把局部变量在栈中创建,并且函数调用也离不了栈。
(4)内核栈,进入内核态时,一方面要用它来保存上下文环境,另一方面的作用同用户栈一样。
(5)虚拟地址池,每个进程拥有独立的内存空间,其虚拟地址是用虚拟地址池来管理的。
(6)页表,让进程拥有独立的内存空间。
克隆出来的进程该如何执行呢?这个简单,只要将新进程加入到就绪队列中就可以啦,当然提前要把相关的栈准备好才行。
#include "fork.h"
#include "process.h"
#include "memory.h"
#include "interrupt.h"
#include "assert.h"
#include "thread.h"
#include "string.h"
#include "file.h"
#include "mlfq.h"
extern void intr_exit(void);
/**
* @description: 将父进程的pcb、虚拟地址位图拷贝给子进程
* @param {task_struct*} child_thread 子进程
* @param {task_struct*} parent_thread 父进程
* @return {*}
*/
static int32_t copy_pcb_vaddrbitmap_stack0(struct task_struct* child_thread, struct task_struct* parent_thread) {
// a 复制pcb所在的整个页,里面包含进程pcb信息及特级0极的栈,里面包含了返回地址, 然后再单独修改个别部分
memcpy(child_thread, parent_thread, PG_SIZE);
child_thread->pid = fork_pid();
child_thread->elapsed_ticks = 0;
child_thread->status = TASK_READY;
child_thread->parent_pid = parent_thread->pid;
child_thread->general_tag.prev = child_thread->general_tag.next = NULL;
child_thread->all_tag.prev = child_thread->all_tag.next = NULL;
block_desc_init(child_thread->u_block_desc);
// b 复制父进程的虚拟地址池的位图
uint32_t bitmap_pg_cnt = DIV_ROUND_UP((0xc0000000 - USER_VADDR_START) / PG_SIZE / 8, PG_SIZE);
// c 获得一个内核地址页面
void* vaddr_btmp = get_kernel_pages(bitmap_pg_cnt);
if (vaddr_btmp == NULL) return -1;
/* 此时child_thread->userprog_vaddr.vaddr_bitmap.bits还是指向父进程虚拟地址的位图地址
* 下面将child_thread->userprog_vaddr.vaddr_bitmap.bits指向自己的位图vaddr_btmp */
memcpy(vaddr_btmp, child_thread->userprog_vaddr.vaddr_bitmap.bits, bitmap_pg_cnt * PG_SIZE);
child_thread->userprog_vaddr.vaddr_bitmap.bits = vaddr_btmp;
// pcb.name的长度是16,为避免下面strcat越界
ASSERT(strlen(child_thread->name) < 11);
strcat(child_thread->name, "_fork");
return 0;
}
/**
* @description: 复制子进程的进程体(代码和数据)及用户栈
* @param {task_struct*} child_thread 子进程
* @param {task_struct*} parent_thread 父进程
* @param {void*} buf_page
* @return {*}
*/
static void copy_body_stack3(struct task_struct* child_thread, struct task_struct* parent_thread, void* buf_page) {
uint8_t* vaddr_btmp = parent_thread->userprog_vaddr.vaddr_bitmap.bits;
uint32_t btmp_bytes_len = parent_thread->userprog_vaddr.vaddr_bitmap.btmp_bytes_len;
uint32_t vaddr_start = parent_thread->userprog_vaddr.vaddr_start;
/* 在父进程的用户空间中查找已有数据的页 */
for (uint32_t idx_byte = 0; idx_byte < btmp_bytes_len; idx_byte++) {
if (vaddr_btmp[idx_byte]) {
for (uint32_t idx_bit = 0; idx_bit < 8; idx_bit++) {
if ((BITMAP_MASK << idx_bit) & vaddr_btmp[idx_byte]) {
uint32_t prog_vaddr = (idx_byte * 8 + idx_bit) * PG_SIZE + vaddr_start;
// 下面的操作是将父进程用户空间中的数据通过内核空间做中转,最终复制到子进程的用户空间
// a 将父进程在用户空间中的数据复制到内核缓冲区buf_page,目的是下面切换到子进程的页表后,还能访问到父进程的数据
memcpy(buf_page, (void*)prog_vaddr, PG_SIZE);
// b 将页表切换到子进程,目的是避免下面申请内存的函数将pte及pde安装在父进程的页表中
page_dir_activate(child_thread);
// c 申请虚拟地址prog_vaddr
get_a_page_without_opvaddrbitmap(PF_USER, prog_vaddr);
// d 从内核缓冲区中将父进程数据复制到子进程的用户空间
memcpy((void*)prog_vaddr, buf_page, PG_SIZE);
// e 恢复父进程页表
page_dir_activate(parent_thread);
}
}
}
}
}
/**
* @description: 为子进程构建thread_stack和修改返回值
* @param {task_struct*} child_thread
* @return {*}
*/
static int32_t build_child_stack(struct task_struct* child_thread) {
// a 使子进程pid返回值为0
struct intr_stack* intr_0_stack = (struct intr_stack*)((uint32_t)child_thread + PG_SIZE - sizeof(struct intr_stack));
intr_0_stack->eax = 0;
// b 为switch_to 构建 struct thread_stack,将其构建在紧临intr_stack之下的空间
uint32_t* ret_addr_in_thread_stack = (uint32_t*)intr_0_stack - 1;
uint32_t* esi_ptr_in_thread_stack = (uint32_t*)intr_0_stack - 2;
uint32_t* edi_ptr_in_thread_stack = (uint32_t*)intr_0_stack - 3;
uint32_t* ebx_ptr_in_thread_stack = (uint32_t*)intr_0_stack - 4;
uint32_t* ebp_ptr_in_thread_stack = (uint32_t*)intr_0_stack - 5;
// c switch_to的返回地址更新为intr_exit,直接从中断返回
*ret_addr_in_thread_stack = (uint32_t)intr_exit;
// d 这几个值会在出栈时被覆盖
*ebp_ptr_in_thread_stack = 0;
*ebx_ptr_in_thread_stack = 0;
*edi_ptr_in_thread_stack = 0;
*esi_ptr_in_thread_stack = 0;
// e 把构建的thread_stack的栈顶做为switch_to恢复数据时的栈顶
child_thread->self_kstack = ebp_ptr_in_thread_stack;
return 0;
}
/**
* @description: 更新inode打开数
* @param {task_struct*} thread
* @return {*}
*/
static void update_inode_open_cnts(struct task_struct* thread) {
int32_t local_fd = 3, global_fd = 0;
while (local_fd < MAX_FILES_OPEN_PER_PROC) {
global_fd = thread->fd_table[local_fd];
ASSERT(global_fd < MAX_FILE_OPEN);
if (global_fd != -1) {
file_table[global_fd].fd_inode->i_open_cnts++;
}
local_fd++;
}
}
/**
* @description: 拷贝父进程本身所占资源给子进程
* @param {task_struct*} child_thread
* @param {task_struct*} parent_thread
* @return {*}
*/
static int32_t copy_process(struct task_struct* child_thread, struct task_struct* parent_thread) {
// 内核缓冲区,作为父进程用户空间的数据复制到子进程用户空间的中转
void* buf_page = get_kernel_pages(1);
if (buf_page == NULL) return -1;
// a 复制父进程的pcb、虚拟地址位图、内核栈到子进程
if (copy_pcb_vaddrbitmap_stack0(child_thread, parent_thread) == -1) return -1;
// b 为子进程创建页表,此页表仅包括内核空间
child_thread->pgdir = create_page_dir();
if (child_thread->pgdir == NULL) return -1;
// c 复制父进程进程体及用户栈给子进程
copy_body_stack3(child_thread, parent_thread, buf_page);
// d 构建子进程thread_stack和修改返回值pid
build_child_stack(child_thread);
// e 更新文件inode的打开数
update_inode_open_cnts(child_thread);
// 释放内核缓冲区
mfree_page(PF_KERNEL, buf_page, 1);
return 0;
}
/**
* @description: fork子进程,内核线程不可直接调用
* @return {*}
*/
uint32_t sys_fork(void) {
// 获取父进程pcb
struct task_struct* parent_thread = running_thread();
// 为子进程创建pcb(task_struct结构)
struct task_struct* child_thread = get_kernel_pages(1);
// 确保关中断以及当前调用的线程不是内核线程
ASSERT(INTR_OFF == intr_get_status() && parent_thread->pgdir != NULL);
// 拷贝父进程本身所占资源给子进程
if (copy_process(child_thread, parent_thread) == -1) return -1;
// 添加到就绪线程队列和所有线程队列,子进程由调试器安排运行
mlfq_push_wspt(child_thread);
// 父进程返回子进程的pid
return child_thread->pid;
}
实现了sys_fork,只需要把这个加到中断调用中就好。这里不在赘述
三、实现init进程
在UNIX系统中,init进程是系统的第一个用户级进程,其作用是初始化操作系统并管理系统的运行。具体来说,init进程主要负责以下几个方面的工作:
- 系统初始化: 当计算机启动时,init进程是第一个被内核启动的用户级进程。它负责初始化系统的各个方面,包括启动必要的系统服务、加载必要的驱动程序等。
- 进程管理: init进程负责创建和管理系统中的其他进程。它根据系统配置文件(如
/etc/inittab
或/etc/init.d
)启动和终止系统服务和守护进程,确保系统中的进程按照预期运行。 - 系统运行级别管理: 在UNIX系统中,运行级别(runlevel)是指系统的工作状态,例如单用户模式、多用户模式等。init进程负责根据系统配置文件切换不同的运行级别,以满足用户或系统管理员的需求。
- 处理系统关机和重启: 当用户执行关机或重启命令时,init进程负责协调系统的关机和重启过程。它会发送信号给系统中的其他进程,通知它们进行清理和关闭工作,然后执行关机或重启操作。
/**
* @description: init进程
* @return {*}
*/
static void init_th(void) {
uint32_t ret_pid = fork();
if (ret_pid) {
printf("i am father, my pid is %d, child pid is %d\n", getpid(), ret_pid);
}
else {
printf("i am child, my pid is %d, ret pid is %d\n", getpid(), ret_pid);
}
while (1);
}
需要在线程初始化代码中创建init进程
/**
* @description: 初始化线程环境
* @return {*}
*/
void thread_init(void) {
put_str("thread_init start\n");
// pid锁初始化
lock_init(&pid_lock);
// 多级队列初始化
mlfq_init();
// 创建第一个用户进程init
process_execute(init_th, "init");
// 创建主线程
make_main_thread();
// 创建idle线程
idle_thread = thread_start("idle", idle, NULL);
put_str("thread_init done\n");
}
四、仿真
结束语
本节我们实现了fork调用,以及init进程,下节我们将简单的实现一个shell
老规矩,本节的代码地址:https://github.com/lyajpunov/os