操作系统真象还原实验记录之实验三十三:实现系统调用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++;
}
}
回收页表中的物理页,虚拟内存池占用的物理页,关闭打开的文件。
回收内存页,一定遵循三步、物理池位图置0,页表有效位置0,虚拟池位图置0.
首先关于内存页管理的代码均在memory.c中,分别在十三、十九(增加用户内存管理)、二十二(增加内存释放和mem_block、arena管理模式)依次补全。
mfree_page可以根据指定的pf、虚拟地址、页数释放指定页数,包括清除物理池位图、虚拟池位图、清楚页表。但是想要一次性清楚页表所有页,仍然需要release_prog_resource来遍历页目录表、页表。
所以release_prog_resource的逻辑是,遍历该用户进程所有页目录页表,将每一页物理池位图置0,
然后mfree_page(PF_KERNEL, user_vaddr_pool_bitmap, bitmap_pg_cnt)
将用户进程虚拟位图释放掉(占用的是内核池)
至于页表为什么不修改,因为thread_exit会利用mfree_page将该进程页表、PCB释放掉。
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给eax。
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;
/*此处init不停回收孤儿进程*/
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。
shell.c
/* 简单的shell */
void my_shell(void) {
cwd_cache[0] = '/';
while (1) {
print_prompt();
printf("what the fuck?");
memset(final_path, 0, MAX_PATH_LEN);
memset(cmd_line, 0, cmd_len);
readline(cmd_line, cmd_len);
if (cmd_line[0] == 0) { // 若只键入了一个回车
continue;
}
argc = -1;
argc = cmd_parse(cmd_line, argv, ' ');
if (argc == -1) {
printf("num of arguments exceed %d\n", MAX_ARG_NR);
continue;
}
if (!strcmp("ls", argv[0])) {
buildin_ls(argc, argv);
} else if (!strcmp("cd", argv[0])) {
if (buildin_cd(argc, argv) != NULL) {
memset(cwd_cache, 0, MAX_PATH_LEN);
strcpy(cwd_cache, final_path);
}
} else if (!strcmp("pwd", argv[0])) {
buildin_pwd(argc, argv);
} else if (!strcmp("ps", argv[0])) {
buildin_ps(argc, argv);
} else if (!strcmp("clear", argv[0])) {
buildin_clear(argc, argv);
} else if (!strcmp("mkdir", argv[0])){
buildin_mkdir(argc, argv);
} else if (!strcmp("rmdir", argv[0])){
buildin_rmdir(argc, argv);
} else if (!strcmp("rm", argv[0])) {
buildin_rm(argc, argv);
}else { // 如果是外部命令,需要从磁盘上加载
int32_t pid = fork();
if (pid) { // 父进程
int32_t status;
int32_t child_pid = wait(&status);
//此时子进程若没有执行exit,my_shell会被阻塞,不再响应键入的命令
if (child_pid == -1){
panic("my_shell: no child");
}
printf("child_pid %d, it is status: %d\n", child_pid, status);
}else { // 子进程
make_clear_abs_path(argv[0], final_path);
argv[0] = final_path;
/* 先判断下文件是否存在 */
struct stat file_stat;
memset(&file_stat, 0, sizeof(struct stat));
if (stat(argv[0], &file_stat) == -1) {
printf("my_shell: cannot access %s: No such file or directory\n", argv[0]);
exit(-1);
} else {
execv(argv[0], argv);
}
}
}
}
panic("my_shell: should not be here");
}
start.s
[bits 32]
extern main
extern exit
section .text
global _start
_start:
;下面这两个要和execv中load之后指定的寄存器一致
push ebx ;压入argv
push ecx ;压入argc
call main
;将main的返回值通过栈传给exit,gcc用eax存储返回值,这是ABI规定的
push eax;
call exit
;exit 不会返回
若程序未中途执行exit(0 -1 -2),那么会执行call exit,向status传递正确运行的值。
5. 实验结果及流程梳理
核心总结是:init进程fork出了一个子进程my_shell,init由于自己没有孤儿进程所以永远不会被唤醒
(
在这里我们的代码的父进程只有一个子进程,由于my_shell不会exit,
只会打印子进程任务情况即使外部命令执行失败,
所以不会有孤儿进程,
不会出现init收集到孤儿进程情况,所以永远不会被唤醒)
)
然后my_shell若遇到外部命令会又fork出一个子进程exec来专门执行exec来执行外部命令。
exec会中途自己exit(-1或0或-2视执行情况)或正常return 66再call exit,然后被my_shell给wait,同时打印自己的执行情况status