操作系统真象还原实验记录之实验三十三:实现系统调用wait和exit
1.wait、exit、孤儿进程、僵尸进程
exit由子进程调用,表面上功能是使子进程结束运行并传递返回值给内核,本质上是内核在幕后回收该子进程除了pcb一页外的所有资源。
wait由父进程调用,表面上功能是使父进程阻塞自己,直到子进程调用exit结束自己时,获取子进程的返回值,本质上是内核在幕后将子进程的返回值传递给父进程后会唤醒父进程,并且将子进程pcb回收。
孤儿进程就是子进程生命周期尚未结束,尚未调用exit,但是父进程提前结束,这样子进程所有资源无法回收,所以叫孤儿进程。Linux中孤儿进程会被init进程收养,init是所有孤儿进程的父进程,管理所有孤儿进程的资源回收。
僵尸进程就是子进程调用了exit,除了pcb其他资源全部回收,同时返回值也传递给了内核,但是父进程没有调用wait。这样僵尸进程的pcb会一直留在内核,占用pid,当僵尸进程数量非常庞大,操作系统将无pid可分配,从而导致加载进程失败。
解决方法是ps -ef 找到所有状态为Z的进程的ppid,kill -9父进程。
2.基础代码
thread.h补充进程退出状态值成员变量
struct task_struct{
int8_t exit_status;
uint32_t stack_magic;
}
memory.c之free_a_phy_page
/* 根据物理页框地址pg_phy_addr在相应的内存池的位图清0,不改动页表*/
void free_a_phy_page(uint32_t pg_phy_addr) {
struct pool* mem_pool;
uint32_t bit_idx = 0;
if (pg_phy_addr >= user_pool.phy_addr_start) {
mem_pool = &user_pool;
bit_idx = (pg_phy_addr - user_pool.phy_addr_start) / PG_SIZE;
} else {
mem_pool = &kernel_pool;
bit_idx = (pg_phy_addr - kernel_pool.phy_addr_start) / PG_SIZE;
}
bitmap_set(&mem_pool->pool_bitmap, bit_idx, 0);
}
接受物理页框地址,在相应内存池的位图清0,不改变页表。
thread.c增加
之前只有分配pid的函数,没有释放pid函数
进程被create在初始化时可以分配pid
或者进程被fork出来也分配pid
但是没有释放pid函数。
下面是pid池、pid位图来管理pid
/* pid的位图,最大支持1024个pid */
uint8_t pid_bitmap_bits[128] = {0};
/* pid池 */
struct pid_pool {
struct bitmap pid_bitmap; // pid位图
uint32_t pid_start; // 起始pid
struct lock pid_lock; // 分配pid锁
}pid_pool;
/* 初始化pid池 */
static void pid_pool_init(void) {
pid_pool.pid_start = 1;
pid_pool.pid_bitmap.bits = pid_bitmap_bits;
pid_pool.pid_bitmap.btmp_bytes_len = 128;
bitmap_init(&pid_pool.pid_bitmap);
lock_init(&pid_pool.pid_lock);
}
/* 分配pid */
static pid_t allocate_pid(void) {
lock_acquire(&pid_pool.pid_lock);
int32_t bit_idx = bitmap_scan(&pid_pool.pid_bitmap, 1);
bitmap_set(&pid_pool.pid_bitmap, bit_idx, 1);
lock_release(&pid_pool.pid_lock);
return (bit_idx + pid_pool.pid_start);
}
/* 释放pid */
void release_pid(pid_t pid) {
lock_acquire(&pid_pool.pid_lock);
int32_t bit_idx = pid - pid_pool.pid_start;
bitmap_set(&pid_pool.pid_bitmap, bit_idx, 0);
lock_release(&pid_pool.pid_lock);
}
thread_init里调用了pid_pool_init();
下面是为了释放进程的pcb以及页表
thread.c之thread_exit、pid_check、pid2thread
/* 回收thread_over的pcb和页表,并将其从调度队列中去除 */
void thread_exit(struct task_struct* thread_over, bool need_schedule) {
/* 要保证schedule在关中断情况下调用 */
intr_disable();
thread_over->status = TASK_DIED;
/* 如果thread_over不是当前线程,就有可能还在就绪队列中,将其从中删除 */
if (elem_find(&thread_ready_list, &thread_over->general_tag)) {
list_remove(&thread_over->general_tag);
}
if (thread_over->pgdir) { // 如是进程,回收进程的页表
mfree_page(PF_KERNEL, thread_over->pgdir, 1);
}
/* 从all_thread_list中去掉此任务 */
list_remove(&thread_over->all_list_tag);
/* 回收pcb所在的页,主线程的pcb不在堆中,跨过 */
if (thread_over != main_thread) {
mfree_page(PF_KERNEL, thread_over, 1);
}
/* 归还pid */
release_pid(thread_over->pid);
/* 如果需要下一轮调度则主动调用schedule */
if (need_schedule) {
schedule();
PANIC("thread_exit: should not be here\n");
}
}
/* 比对任务的pid */
static bool pid_check(struct list_elem* pelem, int32_t pid) {
struct task_struct* pthread = elem2entry(struct task_struct, all_list_tag, pelem);
if (pthread->pid == pid) {
return true;
}
return false;
}
/* 根据pid找pcb,若找到则返回该pcb,否则返回NULL */
struct task_struct* pid2thread(int32_t pid) {
struct list_elem* pelem = list_traversal(&thread_all_list, pid_check, pid);
if (pelem == NULL) {
return NULL;
}
struct task_struct* thread = elem2entry(struct task_struct, all_list_tag, pelem);
return thread;
}
3.实现wait和exit系统调用
wait_exit.c之release_prog_resource
/* 释放用户进程资源:
* 1 页表中对应的物理页
* 2 虚拟内存池占物理页框
* 3 关闭打开的文件 */
static void release_prog_resource(struct task_struct* release_thread) {
uint32_t* pgdir_vaddr = release_thread->pgdir;
uint16_t user_pde_nr = 768, pde_idx = 0;
uint32_t pde = 0;
uint32_t* v_pde_ptr = NULL; // v表示var,和函数pde_ptr区分
uint16_t user_pte_nr = 1024, pte_idx = 0;
uint32_t pte = 0;
uint32_t* v_pte_ptr = NULL; // 加个v表示var,和函数pte_ptr区分
uint32_t* first_pte_vaddr_in_pde = NULL; // 用来记录pde中第0个pte的地址
uint32_t pg_phy_addr = 0;
/* 回收页表中用户空间的页框 */
while (pde_idx < user_pde_nr) {
v_pde_ptr = pgdir_vaddr + pde_idx;
pde = *v_pde_ptr;
if (pde & 0x00000001) { // 如果页目录项p位为1,表示该页目录项下可能有页表项
first_pte_vaddr_in_pde = pte_ptr(pde_idx * 0x400000); // 一个页表表示的内存容量是4M,即0x400000
pte_idx = 0;
while (pte_idx < user_pte_nr) {
v_pte_ptr = first_pte_vaddr_in_pde + pte_idx;
pte = *v_pte_ptr;
if (pte & 0x00000001) {
/* 将pte中记录的物理页框直接在相应内存池的位图中清0 */
pg_phy_addr = pte & 0xfffff000;
free_a_phy_page(pg_phy_addr);
}
pte_idx++;
}
/* 将pde中记录的物理页框直接在相应内存池的位图中清0 */
pg_phy_addr = pde & 0xfffff000;
free_a_phy_page(pg_phy_addr);
}
pde_idx++;
}
/* 回收用户虚拟地址池所占的物理内存*/
uint32_t bitmap_pg_cnt = (release_thread->userprog_vaddr.vaddr_bitmap.btmp_bytes_len) / PG_SIZE;
uint8_t* user_vaddr_pool_bitmap = release_thread->userprog_vaddr.vaddr_bitmap.bits;
mfree_page(PF_KERNEL, user_vaddr_pool_bitmap, bitmap_pg_cnt);
/* 关闭进程打开的文件 */
uint8_t local_fd = 3;
while(local_fd < MAX_FILES_OPEN_PER_PROC) {
if (release_thread->fd_table[local_fd] != -1) {
sys_close(local_fd);
}
local_fd++;
}
}
回收页表中的物理页,虚拟内存池占用的物理页,关闭打开的文件。
使用的是free_a_phy_page来释放,不修改页表即页表有效位P仍是1。
其中页表中的物理页也可以遍历虚拟内存池来释放。
find_child、find_hanging_child、init_adopt_a_child
/* list_traversal的回调函数,
* 查找pelem的parent_pid是否是ppid,成功返回true,失败则返回false */
static bool find_child(struct list_elem* pelem, int32_t ppid) {
struct task_struct* pthread = elem2entry(struct task_struct, all_list_tag, pelem);
if (pthread->parent_pid == ppid) { // 若该任务的parent_pid为ppid,返回
return true; // list_traversal只有在回调函数返回true时才会停止继续遍历,所以在此返回true
}
return false; // 让list_traversal继续传递下一个元素
}
/* list_traversal的回调函数,
* 查找状态为TASK_HANGING的任务 */
static bool find_hanging_child(struct list_elem* pelem, int32_t ppid) {
struct task_struct* pthread = elem2entry(struct task_struct, all_list_tag, pelem);
if (pthread->parent_pid == ppid && pthread->status == TASK_HANGING) {
return true;
}
return false;
}
/* list_traversal的回调函数,
* 将一个子进程过继给init */
static bool init_adopt_a_child(struct list_elem* pelem, int32_t pid) {
struct task_struct* pthread = elem2entry(struct task_struct, all_list_tag, pelem);
if (pthread->parent_pid == pid) { // 若该进程的parent_pid为pid,返回
pthread->parent_pid = 1;
}
return false; // 让list_traversal继续传递下一个元素
}
都是list_traversal的回调函数
find_child:查找父进程pid为ppid的子进程
find_hanging_child:专门找状态为TASK_HANGING且父进程为ppid的子进程
init_adopt_a_child:找所有进程的parent_pid等于pid的进程交给Init进程收养。
sys_wait
/* 等待子进程调用exit,将子进程的退出状态保存到status指向的变量.
* 成功则返回子进程的pid,失败则返回-1 */
pid_t sys_wait(int32_t* status) {
struct task_struct* parent_thread = running_thread();
while(1) {
/* 优先处理已经是挂起状态的任务 */
struct list_elem* child_elem = list_traversal(&thread_all_list, find_hanging_child, parent_thread->pid);
/* 若有挂起的子进程 */
if (child_elem != NULL) {
struct task_struct* child_thread = elem2entry(struct task_struct, all_list_tag, child_elem);
*status = child_thread->exit_status;
/* thread_exit之后,pcb会被回收,因此提前获取pid */
uint16_t child_pid = child_thread->pid;
/* 2 从就绪队列和全部队列中删除进程表项*/
thread_exit(child_thread, false); // 传入false,使thread_exit调用后回到此处
/* 进程表项是进程或线程的最后保留的资源, 至此该进程彻底消失了 */
return child_pid;
}
/* 判断是否有子进程 */
child_elem = list_traversal(&thread_all_list, find_child, parent_thread->pid);
if (child_elem == NULL) { // 若没有子进程则出错返回
return -1;
} else {
/* 若子进程还未运行完,即还未调用exit,则将自己挂起,直到子进程在执行exit时将自己唤醒 */
thread_block(TASK_WAITING);
}
}
}
sys_wait:接受一个存储子进程返回值的地址status,功能是等待子进程调用sys_exit。将子进程的退出状态保存到status指向的变量。
流程如下:
利用find_hanging_child在所有进程队列中过滤出父进程为当前进程且状态为TASK_HANGING的进程,
将子进程exit_stauts和child_pid保存,用于返回,然后调用thread_exit把子进程从队列中删除,回收子进程页表、pcb,释放pid,这里把false传入thread_exit,意味着thread_exit里不会调度新进程,而是会返回到sys_wait,然后sys_wait返回刚刚结束掉的子进程的返回值,
调用sys_wait的父进程会根据这个值来判断子进程的任务有没有完成。
如果没有,就调用find_child再遍历一遍所有进程,找出还没有运行sys_exit的子进程,如果有,将父进程阻塞。当它的子进程调用sys_exit后,就会唤醒父进程。
sys_exit
/* 子进程用来结束自己时调用 */
void sys_exit(int32_t status) {
struct task_struct* child_thread = running_thread();
child_thread->exit_status = status;
if (child_thread->parent_pid == -1) {
PANIC("sys_exit: child_thread->parent_pid is -1\n");
}
/* 将进程child_thread的所有子进程都过继给init */
list_traversal(&thread_all_list, init_adopt_a_child, child_thread->pid);
/* 回收进程child_thread的资源 */
release_prog_resource(child_thread);
/* 如果父进程正在等待子进程退出,将父进程唤醒 */
struct task_struct* parent_thread = pid2thread(child_thread->parent_pid);
if (parent_thread->status == TASK_WAITING) {
thread_unblock(parent_thread);
}
/* 将自己挂起,等待父进程获取其status,并回收其pcb */
thread_block(TASK_HANGING);
}
sys_exit:接受一个参数status,子进程用来结束自己时调用。
流程如下:
先获取自己的pcb,将status存入自己的pcb的exit_status中,
然后调用init_adopt_a_child在自己结束前,将自己的子进程全部托付给init进程,init进程会死循环调用sys_wait来结束这些孤儿进程。
再调用release_prog_resource释放自己除了pcb以外的资源、
再用pid2thread获得自己的父进程pcb,如果父进程已经调用了sys_wait在等自己,就将它唤醒。
最后将自己阻塞,状态为TASK_HANGING,当父进程调用sys_wait看见TASK_HANGING就会回收此子进程的pcb和pid。
最后添加wait、exit系统调用
syscall.c
syscall.h
syacall_init.c
4.实现cat命令
用于查看文件内容
command/cat.c
#include "syscall.h"
#include "stdio.h"
#include "string.h"
int main(int argc, char** argv) {
if (argc > 2) {
printf("cat: argument error\n");
exit(-2);
}
if (argc == 1) {
char buf[512] = {0};
read(0, buf, 512);
printf("%s",buf);
exit(0);
}
int buf_size = 1024;
char abs_path[512] = {0};
void* buf = malloc(buf_size);
if (buf == NULL) {
printf("cat: malloc memory failed\n");
return -1;
}
if (argv[1][0] != '/') {
getcwd(abs_path, 512);
strcat(abs_path, "/");
strcat(abs_path, argv[1]);
} else {
strcpy(abs_path, argv[1]);
}
int fd = open(abs_path, O_RDONLY);
if (fd == -1) {
printf("cat: open: open %s failed\n", argv[1]);
return -1;
}
int read_bytes= 0;
while (1) {
read_bytes = read(fd, buf, buf_size);
if (read_bytes == -1) {
break;
}
write(1, buf, read_bytes);
}
free(buf);
close(fd);
return 66;
}
cat只接受一个参数即待查看的文件名或者绝对路径。
处理成绝对路径并open系统调用打开文件
read、write系统调用将文件一个扇区一个扇区的读到内存并输出到显示屏。一直读到文件尾结束,关闭文件,返回66。
compile3.sh
#### 此脚本应该在command目录下执行
if [[ ! -d "../lib" || ! -d "../build" ]];then
echo "dependent dir don\`t exist!"
cwd=$(pwd)
cwd=${cwd##*/}
cwd=${cwd%/}
if [[ $cwd != "command" ]];then
echo -e "you\`d better in command dir\n"
fi
exit
fi
BIN="cat"
CFLAGS="-Wall -c -fno-builtin -W -Wstrict-prototypes \
-Wmissing-prototypes -Wsystem-headers"
#LIBS= "-I ../lib -I ../lib/user -I ../fs"
OBJS="../build/string.o ../build/syscall.o \
../build/stdio.o ../build/assert.o start.o"
DD_IN=$BIN
DD_OUT="/home/Seven/bochs2.68/bin/Seven.img"
nasm -f elf ./start.S -o ./start.o
ar rcs simple_crt.a $OBJS start.o
gcc -m32 $CFLAGS -I "../lib/" -I "../lib/kernel/" -I "../lib/user/" -I "../kernel/" -I "../device/" -I "../thread/" -I "../userprog/" -I "../fs/" -I "../shell/" -o $BIN".o" $BIN".c"
ld -m elf_i386 $BIN".o" simple_crt.a -o $BIN
SEC_CNT=$(ls -l $BIN|awk '{printf("%d", ($5+511)/512)}')
if [[ -f $BIN ]];then
dd if=./$DD_IN of=$DD_OUT bs=512 \
count=$SEC_CNT seek=300 conv=notrunc
fi
cat执行流程应该是这样的,cat属于外部命令,cat.o要提前写入文件系统,通过shell调用。
当shell里面输入cat 文件名后,shell.c检测到外部命令,所以执行execv系统调用执行cat进程,配合start.s让文件名参数正确传递给cat.c的main函数,start.s搞定了参数传递后call main,从而打印此文件于显示屏。
compile3.sh用于将cat.o写入文件系统根目录下。
和compile2.sh改个进程名cat就够了。
写进入是5553字节
main.c
int main(void) {
put_str("I am kernel\n");
init_all();
/************* 写入应用程序 *************/
uint32_t file_size = 5553;
uint32_t sec_cnt = DIV_ROUND_UP(file_size, 512);
struct disk* sda = &channels[0].devices[0];
void* prog_buf = sys_malloc(file_size);
ide_read(sda, 300, prog_buf, sec_cnt);
int32_t fd = sys_open("/cat", O_CREAT|O_RDWR);
if (fd != -1) {
if(sys_write(fd, prog_buf, file_size) == -1) {
printk("file write error!\n");
while(1);
}
}
/************* 写入应用程序结束 *************/
cls_screen();
console_put_str("[rabbit@localhost /]$ ");
thread_exit(running_thread(), true);
return 0;
}
/* init进程 */
void init(void) {
uint32_t ret_pid = fork();
if(ret_pid) { // 父进程
int status;
int child_pid;
while(1){
child_pid = wait(&status);
printf("I'm init, My pid is 1, I recieve a child, It's pid is %d, status is %d\n", child_pid, status);
}
} else { // 子进程
my_shell();
}
panic("init: should not be here");
}
将cat用户程序写入Seven80.img文件系统,main结束前使用thread_exit将主线程pcb的pid回收,注意主线程页表不能回收。因为传的是true,所以调度新进程。
注意:exit回收的是内存,wait回收的是pid和页表,wait调用的是thread_exit,只回收了主线程pid。所以主线程的内核内存并不能回收。
子进程调用exit,父进程调用wait。
5. 实验结果及流程梳理
本次实验核心增加了exit、wait、cat。
exit和wait是为了父子进程执行顺序,也就是fork。
当父进程需要实现某功能时,他会调用fork来开一个子进程,
根据fork返回值pid,父进程走if,子进程走else。
然后子进程实现功能后,一直使用的是while(1)死循环,资源得不到释放,同时父进程的代码逻辑在if,父进程需要子进程告知功能是否完成,没完成需要等子进程,因此增加了exit与wait。
调用wait会返回两个值,一个是已结束子进程的child_pid,一个是该子进程的status。status由子进程调用exit提供,一般来说功能执行失败那么子进程将调用exit(-1)。依靠这两个值可以知道哪个子进程执行的功能是否成功,从而编写父进程代码。
现在来模拟一下所有涉及fork的代码执行顺序:
首先main函数的init_all,创造了init线程、主线程、idle线程。
主线程执行main函数,init线程执行init函数。
init函数中调用了fork后,父进程死循环调用wait,目的是为了回收所有孤儿进程的pid以及页表。
当孤儿进程执行exit后,返回它的status与pid,我们的操作系统编写的init进程只会打印他们。
再来看init进程fork出来的子进程,调用了my_shell(),my_shell是个死循环,不断接受并解析用户输入的命令,永远不会结束,故没有调用exit。
my_shell()不断打印命令行并readline接受用户输入命令,然后cmd_parse配合if-else逻辑解析此命令,决定执行哪一个系统调用。
我们以这次试验./cat file1举例分析:
因为./cat是外部命令,cat是一个提前存储在文件系统的用户程序,所以,my_shell()调用fork开了一个子进程专门来执行这个外部命令,而与之对应的父进程调用wait用来接收子进程返回的child_pid与status,即使子进程功能失败,依然只是打印他们,但是如果child_pid为-1表示没有子进程,就要panic报错了。
接下来看专门用于执行外部命令的子进程,
make_clear_abs_path里面的wash_path可以把./cat这样带点的绝对路径洗成不带点的绝对路径,argv[0]=/cat,argv[1]=file1。检查了一下cat在不在文件系统,然后调用execv(argv[0], argv),
execv调用load(argv[0]),
load解析cat的elf头,elf头里面是有每个可加载段应该被加载的内存地址,利用此信息,把cat每一个可加载段加载到相应内存地址中,返回
cat的入口地址即_start。
随后execv强制把当前进程修改成cat,使cpu执行cat,从start.s里的_start处执行,这样可以将argv传给cat的main,然后call main执行cat.c的功能。
cat.c的main函数自然知道argv[1]就是file1,这是个相对路径,故利用getcwd获取当前工作目录,拼成绝对路径,最后依靠open打开file1返回fd,read将file1以1024字节单位全部多次读入内存;write打印在显存。从而完成查看file1。