fork原理
fork的作用主要是利用老进程克隆出来一个新进程并使新进程执行。所以fork可以简单的分为两步:
- 复制进程资源
- 跳过去执行
/**
* @brief fork子进程
*
* @return pid_t 成功,父进程返回子进程的pid;失败返回-1
*/
pid_t sys_fork(void);
当然,作为新进程其首先肯定是要在内存中有自己的空间的,所以我么首先要做的就是在内存中给他分配内存空间。
pid_t sys_fork(void)
{
struct task_struct *parent_thread = running_thread();
//为进程创建pcb
struct task_struct *child_thread = get_kernel_pages(1);
...
//复制进程体
copy_process(child_thread, parent_thread);
...
}
既然要复制资源,就必须明确进程有哪些资源:
- 进程的PCB
- 程序体,也就是代码段、数据段等等
- 用户栈
- 内核栈,在进入内核态时用其来保存上下文环境
- 虚拟地址池,用于管理虚拟地址
- 页表
上面这些过程就在函数copy_process中调用,其函数原型如下:
/**
* @brief 给子进程拷贝父进程的资源
*
* @param child_thread 子进程指针
* @param parent_thread 父进程指针
* @return uint32_t 成功返回0,失败返回-1
*/
static uint32_t copy_process(struct task_struct *child_thread, struct task_struct *parent_thread);
其主要会执行以下的步骤:
- 复制父进程的pcb、虚拟地址位图、内核栈。这个函数会先复制父进程的PCB到子进程自己的内核空间,之后把pid、priority以及ticks等信息修改为自己的。之后,复制父进程虚拟地址池的位图
//# 1.复制父进程的pcb、虚拟地址位图、内核栈
if (copy_pcb_vaddrbitmap_stack0(child_thread, parent_thread) == -1)
{
//这种情况基本没有
return -1;
}
- 为子进程创建页表,此页表仅包括内核空间
child_thread->pgdir = create_page_dir();
if (child_thread->pgdir == NULL)
{
return -1;
}
- 复制父进程的进程体给子进程。这一步是在父进程的空间中查找所有有数据的页,之后要做的就是把父进程的页拷贝到内核,再激活子进程的页表,再把这个页拷贝给子进程,之后再转回父进程,循环这个过程。
copy_body_stack3(child_thread, parent_thread, buf_page);
- 构建子进程thread_stack和修改返回值。这里会通过子进程中断栈的eax寄存器先修改进程的返回值pid为0,再为switch_to函数构建线程栈,最后把构建thread_stack的栈顶作为switch_to恢复数据时的栈顶
build_child_stack(child_thread);
- 更新文件的inode打开数。当子进程fork父进程的时候,它也会打开这些文件,所以这里主要是更新内核文件表的打开进程数加一。
update_inode_open_cnts(child_thread);
实现Init进程
在操作系统中,所有的其他进程都是init进程的子进程。其实现比较简单:
- 调用fork派生子进程
- 父进程中打印自己的pid以及fork的返回值
/* init进程 */
void init(void)
{
uint32_t ret_pid = fork();
if (ret_pid)
{ // 父进程
while (1)
;
}
else
{ // 子进程
my_shell();
}
panic("init: should not be here");
}
为了保证init进程的pid为1,需要在main_thread之前建立,所以在初始线程的时候我们创建init线程。
/*
* @brief 线程模块初始化
* @note 主要是初始化线程队列和线程就绪队列
* @note 为main函数创建线程
*/
void thread_init(void)
{
put_str("thread init start...\n");
list_init(&thread_ready_list);
list_init(&thread_all_list);
lock_init(&pid_lock);
//创建第一个用户进程
create_process(init, "init");
//为main函数创建线程
make_main_thread();
idle_thread = thread_start("idle", 10, idle, NULL);
put_str("thread init done!\n");
}
shell进程
我们可以看到在init进程中我们的子进程创建了shell程序。shell程序主要是获取用户输入,之后根据用户的输入执行相应的逻辑代码,这里篇幅有限只列举几个命令的实现原理:
/**
* @brief shell程序,从init进程fork出来
*
*/
void my_shell(void)
{
cwd_cache[0] = '/';
while (1)
{
print_tips();
memset(final_path, 0, MAX_PATH_LEN);
memset(cmd_line, 0, MAX_PATH_LEN);
readline(cmd_line, MAX_PATH_LEN);
if (cmd_line[0] == 0)
{
//只键入回车
continue;
}
//解析输入的参数
argc = -1;
argc = cmd_parse(cmd_line, argv, ' ');
if (argc == -1)
{
printf("shell: no parma input!\n");
continue;
}
if (!strcmp("pwd", argv[0]))
{
in_pwd(argc, argv);
}
else if(!strcmp("cd",argv[0]))
{
in_cd(argc, argv);
}
else if (!strcmp("ls", argv[0]))
{
in_ls(argc, argv);
}
else if(!strcmp("ps",argv[0]))
{
in_ps(argc, argv);
}
else if(!strcmp("clear",argv[0]))
{
in_clear(argc, argv);
}
else if (!strcmp("mkdir", argv[0]))
{
in_mkdir(argc, argv);
}
else if (!strcmp("rmdir", argv[0]))
{
in_rmdir(argc, argv);
}
else if (!strcmp("mkfile", argv[0]))
{
in_mkfile(argc, argv);
}
else if (!strcmp("rm", argv[0]))
{
in_rm(argc, argv);
}
}
panic("my_shell: should not be here");
}
pwd命令实现原理
pwd命令的作用是获取当前的路径,这个函数的主体实现在sys_getcwd函数中:
/**
* @brief 得到当前工作路径并放到buf中
*
* @param buf 存放路径的缓冲区,如果其为null,则内核自己分配一个空间
* @param size buf的大小
* @return char* 当buf为空的时候,内核分配的空间的地址。成功返回地址,失败返回null
*/
char *sys_getcwd(char *buf, uint32_t size);
这个函数的原理就是首先获取当前线程的工作路径,这是由一个变量cwd_inode_no保存的:
int32_t child_inode_no = current_thread->cwd_inode_no; //得到当前默认工作路径
这样我们就能得到当前这个目录的文件目录表,文件目录表中保存了父目录的inode节点信息,得到inode节点号之后查表就可以得到父目录的文件名,这样不断的向上回溯,拼接字符串就可以得到。
//从子目录向上遍历,直到找到根目录
while ((child_inode_no))
{
parent_inode_no = get_parent_dir_inode_no(child_inode_no, io_buf);
if (get_child_dir_name(parent_inode_no, child_inode_no, full_path_reverse, io_buf) == -1)
{
//未找到子目录名字,失败退出
sys_free(io_buf);
return NULL;
}
child_inode_no = parent_inode_no;
}
cd命令实现原理
cd命令的表现是更改当前进程的工作路径,实际上就是更改上面的变量cwd_inode_no。其主要实现函数原型如下:
/**
* @brief cd命令,转换当前目录
*
* @param argc 命令参数个数
* @param argv 路径参数
* @return char* 成功返回转到的路径名称,失败返回null
*/
char *in_cd(uint32_t argc, char **argv);
cd函数主要做两件事情:
- 根据传递进来的相对路径转换为绝对路径,这个过程主要就是查文件目录表。
//解析当前路径
make_clear_abs_path(argv[1], final_path);
- 更改当前进程的工作路径,这一步是调用函数chdir,其原理就是根据绝对路径找到这个目录的inode号,之后把这个inode号赋值给cwd_inode_no。
int inode_no = search_file(path, &searched_record);
if (inode_no != -1)
{
if (searched_record.file_type == FT_DIRECTORY)
{
running_thread()->cwd_inode_no = inode_no;
ret = 0;
}
else
{
printk("sys_chdir: %s is not a directory!\n");
}
}
dir_close(searched_record.parent_dir);
rm命令实现原理
rm命令主要是删除一个文件。其主要实现函数函数原型如下:
/**
* @brief rm命令 删除文件
*
* @param argc
* @param argv
* @return int32_t
*/
int32_t in_rm(uint32_t argc, char **argv);
其要做的事情也是两件:
- 相对路径转换成绝对路径
make_clear_abs_path(argv[1], final_path);
- 删除文件,这个会调用unlink函数。这个函数会先保证文件存在且未被打开,之后根据文件的绝对路径,定位到这个文件的inode信息。之后调用delete_dir_entry函数在其父目录的目录文件表删除这个目录项信息,最后把这个文件的磁盘空间释放掉。
//在父目录下删除此文件
struct dir *parent_dir = searched_record.parent_dir;
delete_dir_entry(current_partition, parent_dir, inode_no, io_buf);
参考文献
[1] 操作系统真相还原