《操作系统真象还原》第十五章 系统交互

第十五章 系统交互

本文是对《操作系统真象还原》第十五章学习的笔记,欢迎大家一起交流,目前所有代码已托管至 fdx-xdf/MyTinyOS

fork

fork 实现

fork 是用于复制进程的,也就是根据父进程复制出一个子进程。但是由于他们本质是两个进程,所以还是有很多不相同的地方,比如独立的资源,单独的 pid 之类的。

由于 fork 复制进程,而且复制步骤是在 fork 自己的代码结束前就完成(假设 fork 代码 1000 行,第 800 行就完成了复制),所以 fork 代码最后一行的 return 就会被执行两次。对于父进程来说,fork 会返回子进程 pid。对于子进程来说,fork 会返回 0。我们就可以根据 fork 返回的不同值来区别父子进程,以让父子进程执行不同的代码。

进程有哪些资源呢?确定了之后咱们就知道该复制什么了,梳理一下。
( I )进程的 pcb ,即 task_struct,这是让任务有“存在感”的身份证。
( 2 )程序体, 即代码段数据段等,这是进程的实体。
(3 )用户栈,不用说了,编译器会把局部变量在战中创建,并且函数调用也离不了棋。
( 4 )内核栈,进入内核态时, 一方面要用它来保存上下文环境,另一方面的作用同用户枝一样。
( 5 ) 虚拟地址池,每个进程拥有独立的内存空间,其虚拟地址是用虚拟地址池来管理的。
( 6 )页表 ,让进程拥有独立的内存空间。

/* 将父进程的pcb、虚拟地址位图拷贝给子进程 */
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->ticks = child_thread->priority; // 为新进程把时间片充满
    child_thread->parent_pid = parent_thread->pid;
    child_thread->general_tag.prev = child_thread->general_tag.next = NULL;
    child_thread->all_list_tag.prev = child_thread->all_list_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);
    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;
    /* 调试用 */
    ASSERT(strlen(child_thread->name) < 11); // pcb.name的长度是16,为避免下面strcat越界
    strcat(child_thread->name, "_fork");
    return 0;
}

extern void intr_exit(void);

/* 复制子进程的进程体(代码和数据)及用户栈 */
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;
    uint32_t idx_byte = 0;
    uint32_t idx_bit = 0;
    uint32_t prog_vaddr = 0;

    /* 在父进程的用户空间中查找已有数据的页 */
    while (idx_byte < btmp_bytes_len)
    {
        if (vaddr_btmp[idx_byte])
        {
            idx_bit = 0;
            while (idx_bit < 8)
            {
                if ((BITMAP_MASK << idx_bit) & vaddr_btmp[idx_byte])
                {
                    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);
                }
                idx_bit++;
            }
        }
        idx_byte++;
    }
}

copy_pcb_vaddrbitmap_stack0 用于根据传入的父子进程 pcb 指针,先复制整个父进程 pcb 内容到子进程 pcb 中,然后再针对设置子进程 pcb 内容,包含:pid, elapsed_ticks, status, ticks, parent_pid, general_tag, all_list_tag, u_block_desc, userprog_vaddr(让子进程拥有自己的用户虚拟地址空间内存池,但是其位图是拷贝父进程的)。这个过程中,内核栈中的内容被完全拷贝了。

copy_body_stack3 用于根据传入的父子进程 pcb 指针,复制进程的用户空间堆与栈中的数据。核心原理:遍历父进程的 userprog_vaddr 当中的虚拟地址空间位图,来判断父进程的用户虚拟地址空间中是否有数据。如果有,就拷贝到内核空间的中转区中,然后调用 page_dir_activate,切换到子进程页表,调用 get_a_page_without_opvaddrbitmap 为子进程特定虚拟地址申请一个物理页(其中并不涉及子进程 userprog_vaddr 中的位图修改),然后从内核中转区中把数据拷贝到子进程相同的虚拟地址内。

/* 为子进程构建thread_stack和修改返回值 */
static int32_t build_child_stack(struct task_struct *child_thread)
{
    /* a 使子进程pid返回值为0 */
    /* 获取子进程0级栈栈顶 */
    struct intr_stack *intr_0_stack = (struct intr_stack *)((uint32_t)child_thread + PG_SIZE - sizeof(struct intr_stack));
    /* 修改子进程的返回值为0 */
    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;

    /***   这三行不是必要的,只是为了梳理thread_stack中的关系 ***/
    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;
    /**********************************************************/

    /* ebp在thread_stack中的地址便是当时的esp(0级栈的栈顶),
    即esp为"(uint32_t*)intr_0_stack - 5" */
    uint32_t *ebp_ptr_in_thread_stack = (uint32_t *)intr_0_stack - 5;

    /* switch_to的返回地址更新为intr_exit,直接从中断返回 */
    *ret_addr_in_thread_stack = (uint32_t)intr_exit;

    /* 下面这两行赋值只是为了使构建的thread_stack更加清晰,其实也不需要,
     * 因为在进入intr_exit后一系列的pop会把寄存器中的数据覆盖 */
    *ebp_ptr_in_thread_stack = *ebx_ptr_in_thread_stack =
        *edi_ptr_in_thread_stack = *esi_ptr_in_thread_stack = 0;
    /*********************************************************/

    /* 把构建的thread_stack的栈顶做为switch_to恢复数据时的栈顶 */
    child_thread->self_kstack = ebp_ptr_in_thread_stack;
    return 0;
}

build_child_stack 用于修改子进程的返回值和设定其内核栈。子进程返回 0 原理:我们之前构建系统调用机制时,系统调用的返回值会放入内核栈中的中断栈(intr_stack)eax 的位置,这样中断退出(intr_exit)就会 push eax 时将返回值放入 eax 中。所以我们将子进程的内核栈中断栈 eax 的值改成 0。

我们的子进程上机运行是通过让自己就绪之后,等待某个时钟中断调用 switch_to 函数上机

   mov eax, [esp + 24]		 
   mov esp, [eax]		 
   pop ebp
   pop ebx
   pop edi
   pop esi
   ret

switch_to 会从子进程的 pcb 中找到内核栈的栈顶放入 esp 中,然后执行 switch_to 的那 4 条 pop 和 ret 指令,我们现在经过拷贝后的子进程内核栈布局如图:

image

所以,我们直接去用子进程这样的内核栈布局肯定不行,要人为去修改成

image

也就是在 intr_stack 前面增加 switch_to 栈(也就是书 p694 提到的 thread_stack),让 pcb 最顶端的 esp 指向 switch_to 栈栈顶,并且 switch_to 栈中返回地址要填上 intr_exit 函数地址。这样执行 ret 之后,就能去执行 intr_exit,并利用 intr_stack 执行中断返回,由于 intr_stack 中拷贝了父进程进入中断时的用户栈信息,cs: ip 信息,所以中断退出后,子进程将会继续执行父进程之后的代码。

/* 更新inode打开数 */
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++;
    }
}

/* 拷贝父进程本身所占资源给子进程 */
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;
}

copy_process 就是 fork 时用于复制父进程资源的函数,就是前面函数的封装。原理:调用 copy_pcb_vaddrbitmap_stack0 复制父进程的 pcb、虚拟地址位图、内核栈到子进程;然后调用 create_page_dir 为子进程创建页表,这个页表已经包含了内核地址空间的映射;然后调用 copy_body_stack3 复制进程的用户空间堆与栈中的数据;然后调用 build_child_stack 用于修改子进程的返回值和设定其内核栈;最后调用 update_inode_open_cnts 更新 inode 的打开数。

然后给 fork 添加系统调用(不再赘述)

fork 应用 init 进程

init ​进程:我们学习 Linux 做法,让 init ​作为 pid 为 1 的用户进程,所以必须要放在主线程创建之创建。后续所有的进程都是它的孩子,它还负责所有子进程的资源回收

extern void init(void);
/* 初始化线程环境 */
void thread_init(void)
{
    put_str("thread_init start\n");
    list_init(&thread_ready_list);
    list_init(&thread_all_list);
    lock_init(&pid_lock);
    /* 先创建第一个用户进程:init */
    process_execute(init, "init");         // 放在第一个初始化,这是第一个进程,init进程的pid为1
    /* 将当前main函数创建为线程 */
    make_main_thread();
    /* 创建idle线程 */
    idle_thread = thread_start("idle", 10, idle, NULL);
    put_str("thread_init done\n");
}

#include "print.h"
#include "init.h"
#include "fork.h"
#include "stdio.h"
#include "syscall.h"

void init(void);

int main(void)
{
    put_str("I am kernel\n");
    init_all();
    while (1)
        ;
    return 0;
}

/* init进程 */
void init(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)
        ;
}

获取键盘输入

sys_read ​用于从指定文件描述符中获取 conunt 字节数据,如果文件描述符是 stdin_no,那么直接循环调用 ioq_getchar ​从键盘获取内容,否则调用 file_read ​从文件中读取内容

/* 从文件描述符fd指向的文件中读取count个字节到buf,若成功则返回读出的字节数,到文件尾则返回-1 */
int32_t sys_read(int32_t fd, void *buf, uint32_t count)
{
    ASSERT(buf != NULL);
    int32_t ret = -1;
    if (fd < 0 || fd == stdout_no || fd == stderr_no)
    {
        printk("sys_read: fd error\n");
    }
    else if (fd == stdin_no)
    {
        char *buffer = buf;
        uint32_t bytes_read = 0;
        while (bytes_read < count)
        {
            *buffer = ioq_getchar(&kbd_buf);
            bytes_read++;
            buffer++;
        }
        ret = (bytes_read == 0 ? -1 : (int32_t)bytes_read);
    }
    else
    {
        uint32_t _fd = fd_local2global(fd);
        ret = file_read(&file_table[_fd], buf, count);
    }
    return ret;
}

添加 putchar、clear 系统调用

global cls_screen
cls_screen:
	pushad
															; 由于用户程序的cpl为3,显存段的dpl为0,故用于显存段的选择子gs在低于自己特权的环境中为0,
															; 导致用户程序再次进入中断后,gs为0,故直接在put_str中每次都为gs赋值. 
	mov ax, SELECTOR_VIDEO	       							; 不能直接把立即数送入gs,须由ax中转
	mov gs, ax

	mov ebx, 0
	mov ecx, 80*25
.cls:
	mov word [gs:ebx], 0x0720		  						;0x0720是黑底白字的空格键
	add ebx, 2
	loop .cls 
	mov ebx, 0

.set_cursor:				  								;直接把set_cursor搬过来用,省事
															;;;;;; 1 先设置高8位 ;;;;;;;;
	mov dx, 0x03d4			  								;索引寄存器
	mov al, 0x0e				  							;用于提供光标位置的高8位
	out dx, al
	mov dx, 0x03d5			  								;通过读写数据端口0x3d5来获得或设置光标位置 
	mov al, bh
	out dx, al

															;;;;;;; 2 再设置低8位 ;;;;;;;;;
	mov dx, 0x03d4
	mov al, 0x0f
	out dx, al
	mov dx, 0x03d5 
	mov al, bl
	out dx, al
	popad
	ret

/* 向屏幕输出一个字符 */
void sys_putchar(char char_asci)
{
    console_put_char(char_asci);
}

实现一个简单的 shell

#define cmd_len 128	   // 最大支持键入128个字符的命令行输入
#define MAX_ARG_NR 16	   // 加上命令名外,最多支持15个参数

/* 存储输入的命令 */
static char cmd_line[cmd_len] = {0};

/* 用来记录当前目录,是当前目录的缓存,每次执行cd命令时会更新此内容 */
char cwd_cache[64] = {0};

/* 输出提示符 */
void print_prompt(void) {
   printf("[rabbit@localhost %s]$ ", cwd_cache);
}

/* 从键盘缓冲区中最多读入count个字节到buf。*/
static void readline(char* buf, int32_t count) {
   assert(buf != NULL && count > 0);
   char* pos = buf;
   while (read(stdin_no, pos, 1) != -1 && (pos - buf) < count) { // 在不出错情况下,直到找到回车符才返回
      switch (*pos) {
       /* 找到回车或换行符后认为键入的命令结束,直接返回 */
	 case '\n':
	 case '\r':
	    *pos = 0;	   // 添加cmd_line的终止字符0
	    putchar('\n');
	    return;

	 case '\b':
	    if (buf[0] != '\b') {		// 阻止删除非本次输入的信息
	       --pos;	   // 退回到缓冲区cmd_line中上一个字符
	       putchar('\b');
	    }
	    break;

	 /* 非控制键则输出字符 */
	 default:
	    putchar(*pos);
	    pos++;
      }
   }
   printf("readline: can`t find enter_key in the cmd_line, max num of char is 128\n");
}

/* 简单的shell */
void my_shell(void) {
   cwd_cache[0] = '/';
   while (1) {
      print_prompt(); 
      memset(cmd_line, 0, cmd_len);
      readline(cmd_line, cmd_len);
      if (cmd_line[0] == 0) {	 // 若只键入了一个回车
	 continue;
      }
   }
   panic("my_shell: should not be here");
}


print_prompt ​用于输出命令提示符,也就是我们在终端输入命令时,前面那串字符

readline 循环调用 read 从键盘输入缓冲读取字符,每次读取一个,最多读入 count 个字节到 buf。根据每次读入的值不同,处理方式也不同:/n,/r 表示按下 enter 键,用户输入命令结束,缓冲区输入个 0 表示命令字符串结尾。/b 表示按下退格键,就删除一个字符。普通字符就直接读入 buf。每种字符都调用了 putchar 进行打印,是因为我们的键盘中断处理函数已经删除打印功能。

my_shell ​就是 shell 进程,不断循环:调用 print_prompt ​输出命令提示符,然后调用 readline ​获取用户输入

然后通过 init 开启 myshell

/* init进程 */
void init(void)
{
    uint32_t ret_pid = fork();
    if (ret_pid)
    { // 父进程
        while (1)
            ;
    }
    else
    { // 子进程
        my_shell();
    }
    PANIC("init: should not be here");
}

添加 ctrl+c ctrl+u 快捷键

我们之前在键盘中断处理程序已经留下来了伏笔

	        if ((ctrl_status && cur_char == 'l') || (ctrl_status && cur_char == 'u')) {
	            cur_char -= 'a';
	        }
            if (!ioq_full(&kbd_buf)) {
                ioq_putchar(&kbd_buf, cur_char);
            }


然后再在 readline 中对两种情况进行特殊处理

/* 从键盘缓冲区中最多读入count个字节到buf。*/
static void readline(char *buf, int32_t count)
{
    ASSERT(buf != NULL && count > 0);
    char *pos = buf;

    while (read(stdin_no, pos, 1) != -1 && (pos - buf) < count)
    { // 在不出错情况下,直到找到回车符才返回
        switch (*pos)
        {
            /* 找到回车或换行符后认为键入的命令结束,直接返回 */
        case '\n':
        case '\r':
            *pos = 0; // 添加cmd_line的终止字符0
            putchar('\n');
            return;

        case '\b':
            if (cmd_line[0] != '\b')
            {          // 阻止删除非本次输入的信息
                --pos; // 退回到缓冲区cmd_line中上一个字符
                putchar('\b');
            }
            break;

        /* ctrl+l 清屏 */
        case 'l' - 'a':
            /* 1 先将当前的字符'l'-'a'置为0 */
            *pos = 0;
            /* 2 再将屏幕清空 */
            clear();
            /* 3 打印提示符 */
            print_prompt();
            /* 4 将之前键入的内容再次打印 */
            printf("%s", buf);
            break;

        /* ctrl+u 清掉输入 */
        case 'u' - 'a':
            while (buf != pos)
            {
                putchar('\b');
                *(pos--) = 0;
            }
            break;

        /* 非控制键则输出字符 */
        default:
            putchar(*pos);
            pos++;
        }
    }
    printf("readline: can`t find enter_key in the cmd_line, max num of char is 128\n");
}

解析键入的字符

其实就是解析我们的命令

/* 分析字符串cmd_str中以token为分隔符的单词,将各单词的指针存入argv数组 */
static int32_t cmd_parse(char* cmd_str, char** argv, char token) {
   assert(cmd_str != NULL);
   int32_t arg_idx = 0;
   while(arg_idx < MAX_ARG_NR) {
      argv[arg_idx] = NULL;
      arg_idx++;
   }
   char* next = cmd_str;
   int32_t argc = 0;
   /* 外层循环处理整个命令行 */
   while(*next) {
      /* 去除命令字或参数之间的空格 */
      while(*next == token) {
	 next++;
      }
      /* 处理最后一个参数后接空格的情况,如"ls dir2 " */
      if (*next == 0) {
	 break; 
      }
      argv[argc] = next;

     /* 内层循环处理命令行中的每个命令字及参数 */
      while (*next && *next != token) {	  // 在字符串结束前找单词分隔符
	 next++;
      }

      /* 如果未结束(是token字符),使tocken变成0 */
      if (*next) {
	 *next++ = 0;	// 将token字符替换为字符串结束符0,做为一个单词的结束,并将字符指针next指向下一个字符
      }
   
      /* 避免argv数组访问越界,参数过多则返回0 */
      if (argc > MAX_ARG_NR) {
	 return -1;
      }
      argc++;
   }
   return argc;
}

char* argv[MAX_ARG_NR];    // argv必须为全局变量,为了以后exec的程序可访问参数
int32_t argc = -1;
/* 简单的shell */
void my_shell(void) {
   cwd_cache[0] = '/';
   while (1) {
      print_prompt(); 
      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("num of arguments exceed %d\n", MAX_ARG_NR);
	 continue;
      }
    
      int32_t arg_idx = 0;
      while(arg_idx < argc) {
	 printf("%s ", argv[arg_idx]); 
	 arg_idx++;
      }
      printf("\n");
   }
   panic("my_shell: should not be here");
}

cmd_parse ​分析字符串 cmd_str 中以 token 为分隔符的单词,将各单词的指针存入 argv 数组。这个函数就是个字符串处理函数,从诸如 'ls dir ’ 这样的命令中拆单词,拆成 ‘ls’ 与 ‘dir’

实现一些系统调用

ps 系统调用如下:

/* 以填充空格的方式输出buf */
static void pad_print(char* buf, int32_t buf_len, void* ptr, char format) {
   memset(buf, 0, buf_len);
   uint8_t out_pad_0idx = 0;
   switch(format) {
      case 's':
	 out_pad_0idx = sprintf(buf, "%s", ptr);
	 break;
      case 'd':
	 out_pad_0idx = sprintf(buf, "%d", *((int16_t*)ptr));
      case 'x':
	 out_pad_0idx = sprintf(buf, "%x", *((uint32_t*)ptr));
   }
   while(out_pad_0idx < buf_len) { // 以空格填充
      buf[out_pad_0idx] = ' ';
      out_pad_0idx++;
   }
   sys_write(stdout_no, buf, buf_len - 1);
}

/* 用于在list_traversal函数中的回调函数,用于针对线程队列的处理 */
static bool elem2thread_info(struct list_elem* pelem, int arg UNUSED) {
   struct task_struct* pthread = elem2entry(struct task_struct, all_list_tag, pelem);
   char out_pad[16] = {0};

   pad_print(out_pad, 16, &pthread->pid, 'd');

   if (pthread->parent_pid == -1) {
      pad_print(out_pad, 16, "NULL", 's');
   } else { 
      pad_print(out_pad, 16, &pthread->parent_pid, 'd');
   }

   switch (pthread->status) {
      case 0:
	 pad_print(out_pad, 16, "RUNNING", 's');
	 break;
      case 1:
	 pad_print(out_pad, 16, "READY", 's');
	 break;
      case 2:
	 pad_print(out_pad, 16, "BLOCKED", 's');
	 break;
      case 3:
	 pad_print(out_pad, 16, "WAITING", 's');
	 break;
      case 4:
	 pad_print(out_pad, 16, "HANGING", 's');
	 break;
      case 5:
	 pad_print(out_pad, 16, "DIED", 's');
   }
   pad_print(out_pad, 16, &pthread->elapsed_ticks, 'x');

   memset(out_pad, 0, 16);
   ASSERT(strlen(pthread->name) < 17);
   memcpy(out_pad, pthread->name, strlen(pthread->name));
   strcat(out_pad, "\n");
   sys_write(stdout_no, out_pad, strlen(out_pad));
   return false;	// 此处返回false是为了迎合主调函数list_traversal,只有回调函数返回false时才会继续调用此函数
}

/* 打印任务列表 */
void sys_ps(void) {
   char* ps_title = "PID            PPID           STAT           TICKS          COMMAND\n";
   sys_write(stdout_no, ps_title, strlen(ps_title));
   list_traversal(&thread_all_list, elem2thread_info, 0);
}

其他的如下所示:

char *getcwd(char *buf, uint32_t size);
int32_t open(char *pathname, uint8_t flag);
int32_t close(int32_t fd);
int32_t lseek(int32_t fd, int32_t offset, uint8_t whence);
int32_t unlink(const char *pathname);
int32_t mkdir(const char *pathname);
struct dir *opendir(const char *name);
int32_t closedir(struct dir *dir);
int32_t rmdir(const char *pathname);
struct dir_entry *readdir(struct dir *dir);
void rewinddir(struct dir *dir);
int32_t stat(const char *path, struct stat *buf);
int32_t chdir(const char *path);
void ps(void);

路径解析

操作系统为了方便用户使用,一般都会提供相对路径功能。比如我们当前工作路径是/home/kanshan/Desktop,我们想要运行一个编译好的程序输入./test,实际上是被操作系统解析成了/home/kanshan/Desktop/test,也就是当前工作路径 + 相对路径 = 绝对路径。

/* 将路径old_abs_path中的..和.转换为实际路径后存入new_abs_path */
static void wash_path(char* old_abs_path, char* new_abs_path) {
   assert(old_abs_path[0] == '/');
   char name[MAX_FILE_NAME_LEN] = {0};  
   char* sub_path = old_abs_path;
   sub_path = path_parse(sub_path, name);
   if (name[0] == 0) { // 若只键入了"/",直接将"/"存入new_abs_path后返回 
      new_abs_path[0] = '/';
      new_abs_path[1] = 0;
      return;
   }
   new_abs_path[0] = 0;	   // 避免传给new_abs_path的缓冲区不干净
   strcat(new_abs_path, "/");
   while (name[0]) {
      /* 如果是上一级目录“..” */
      if (!strcmp("..", name)) {
	 char* slash_ptr =  strrchr(new_abs_path, '/');
       /*如果未到new_abs_path中的顶层目录,就将最右边的'/'替换为0,
	 这样便去除了new_abs_path中最后一层路径,相当于到了上一级目录 */
	 if (slash_ptr != new_abs_path) {	// 如new_abs_path为“/a/b”,".."之后则变为“/a”
	    *slash_ptr = 0;
	 } else {	      // 如new_abs_path为"/a",".."之后则变为"/"
      /* 若new_abs_path中只有1个'/',即表示已经到了顶层目录,
	 就将下一个字符置为结束符0. */
	    *(slash_ptr + 1) = 0;
	 }
      } else if (strcmp(".", name)) {	  // 如果路径不是‘.’,就将name拼接到new_abs_path
	 if (strcmp(new_abs_path, "/")) {	  // 如果new_abs_path不是"/",就拼接一个"/",此处的判断是为了避免路径开头变成这样"//"
	    strcat(new_abs_path, "/");
	 }
	 strcat(new_abs_path, name);
      }  // 若name为当前目录".",无须处理new_abs_path

      /* 继续遍历下一层路径 */
      memset(name, 0, MAX_FILE_NAME_LEN);
      if (sub_path) {
	 sub_path = path_parse(sub_path, name);
      }
   }
}

/* 将path处理成不含..和.的绝对路径,存储在final_path */
void make_clear_abs_path(char* path, char* final_path) {
   char abs_path[MAX_PATH_LEN] = {0};
   /* 先判断是否输入的是绝对路径 */
   if (path[0] != '/') {      // 若输入的不是绝对路径,就拼接成绝对路径
      memset(abs_path, 0, MAX_PATH_LEN);
      if (getcwd(abs_path, MAX_PATH_LEN) != NULL) {
	 if (!((abs_path[0] == '/') && (abs_path[1] == 0))) {	     // 若abs_path表示的当前目录不是根目录/
	    strcat(abs_path, "/");
	 }
      }
   }
   strcat(abs_path, path);
   wash_path(abs_path, final_path);
}

wash_path 将路径 old_abs_path(这是调用者提供的绝对路径)中的…和.转换为实际路径后存入 new_abs_path。例如,给定路径/a/b/..应被转换成/a。给定路径/a/b/.应被转换成/a/b。核心原理:调用 path_parse 解析路径,如果是..,则退回上一层路径。如果是.,则什么都不做。带入一个例子,比如/a/../home/.就可以明白次函数如何工作

make_clear_abs_path 将路径(包含相对路径与绝对路径两种)处理成不含…和.的绝对路径,存储在 final_path 中。核心原理:判断输入路径是相对路径还是绝对路径,如果是相对路径,调用 getcwd 获得当前工作目录的绝对路径,将用户输入的路径追加到工作目录路径之后形成绝对目录路径,将其作为参数传给 wash_path 进行路径转换。

实现内建命令

shell 命令分为外部命令与内部命令。执行外部命令,实际上就是执行了一个进程。而内部命令,就是执行操作系统自带的函数。我们现在来实现一系列内部命令所需要的内建函数。

/* pwd命令的内建函数 */
void buildin_pwd(uint32_t argc, char **argv UNUSED)
{
    if (argc != 1)
    {
        printf("pwd: no argument support!\n");
        return;
    }
    else
    {
        if (NULL != getcwd(final_path, MAX_PATH_LEN))
        {
            printf("%s\n", final_path);
        }
        else
        {
            printf("pwd: get current work directory failed.\n");
        }
    }
}

/* cd命令的内建函数 */
char *buildin_cd(uint32_t argc, char **argv)
{
    if (argc > 2)
    {
        printf("cd: only support 1 argument!\n");
        return NULL;
    }

    /* 若是只键入cd而无参数,直接返回到根目录. */
    if (argc == 1)
    {
        final_path[0] = '/';
        final_path[1] = 0;
    }
    else
    {
        make_clear_abs_path(argv[1], final_path);
    }

    if (chdir(final_path) == -1)
    {
        printf("cd: no such directory %s\n", final_path);
        return NULL;
    }
    return final_path;
}

/* ls命令的内建函数 */
void buildin_ls(uint32_t argc, char **argv)
{
    char *pathname = NULL;
    struct stat file_stat;
    memset(&file_stat, 0, sizeof(struct stat));
    bool long_info = false;
    uint32_t arg_path_nr = 0;
    uint32_t arg_idx = 1; // 跨过argv[0],argv[0]是字符串“ls”
    while (arg_idx < argc)
    {
        if (argv[arg_idx][0] == '-')
        { // 如果是选项,单词的首字符是-
            if (!strcmp("-l", argv[arg_idx]))
            { // 如果是参数-l
                long_info = true;
            }
            else if (!strcmp("-h", argv[arg_idx]))
            { // 参数-h
                printf("usage: -l list all infomation about the file.\n-h for help\nlist all files in the current dirctory if no option\n");
                return;
            }
            else
            { // 只支持-h -l两个选项
                printf("ls: invalid option %s\nTry `ls -h' for more information.\n", argv[arg_idx]);
                return;
            }
        }
        else
        { // ls的路径参数
            if (arg_path_nr == 0)
            {
                pathname = argv[arg_idx];
                arg_path_nr = 1;
            }
            else
            {
                printf("ls: only support one path\n");
                return;
            }
        }
        arg_idx++;
    }

    if (pathname == NULL)
    { // 若只输入了ls 或 ls -l,没有输入操作路径,默认以当前路径的绝对路径为参数.
        if (NULL != getcwd(final_path, MAX_PATH_LEN))
        {
            pathname = final_path;
        }
        else
        {
            printf("ls: getcwd for default path failed\n");
            return;
        }
    }
    else
    {
        make_clear_abs_path(pathname, final_path);
        pathname = final_path;
    }

    if (stat(pathname, &file_stat) == -1)
    {
        printf("ls: cannot access %s: No such file or directory\n", pathname);
        return;
    }
    if (file_stat.st_filetype == FT_DIRECTORY)
    {
        struct dir *dir = opendir(pathname);
        struct dir_entry *dir_e = NULL;
        char sub_pathname[MAX_PATH_LEN] = {0};
        uint32_t pathname_len = strlen(pathname);
        uint32_t last_char_idx = pathname_len - 1;
        memcpy(sub_pathname, pathname, pathname_len);
        if (sub_pathname[last_char_idx] != '/')
        {
            sub_pathname[pathname_len] = '/';
            pathname_len++;
        }
        rewinddir(dir);
        if (long_info)
        {
            char ftype;
            printf("total: %d\n", file_stat.st_size);
            while ((dir_e = readdir(dir)))
            {
                ftype = 'd';
                if (dir_e->f_type == FT_REGULAR)
                {
                    ftype = '-';
                }
                sub_pathname[pathname_len] = 0;
                strcat(sub_pathname, dir_e->filename);
                memset(&file_stat, 0, sizeof(struct stat));
                if (stat(sub_pathname, &file_stat) == -1)
                {
                    printf("ls: cannot access %s: No such file or directory\n", dir_e->filename);
                    return;
                }
                printf("%c  %d  %d  %s\n", ftype, dir_e->i_no, file_stat.st_size, dir_e->filename);
            }
        }
        else
        {
            while ((dir_e = readdir(dir)))
            {
                printf("%s ", dir_e->filename);
            }
            printf("\n");
        }
        closedir(dir);
    }
    else
    {
        if (long_info)
        {
            printf("-  %d  %d  %s\n", file_stat.st_ino, file_stat.st_size, pathname);
        }
        else
        {
            printf("%s\n", pathname);
        }
    }
}

/* ps命令内建函数 */
void buildin_ps(uint32_t argc, char **argv UNUSED)
{
    if (argc != 1)
    {
        printf("ps: no argument support!\n");
        return;
    }
    ps();
}

/* clear命令内建函数 */
void buildin_clear(uint32_t argc, char **argv UNUSED)
{
    if (argc != 1)
    {
        printf("clear: no argument support!\n");
        return;
    }
    clear();
}

/* mkdir命令内建函数 */
int32_t buildin_mkdir(uint32_t argc, char **argv)
{
    int32_t ret = -1;
    if (argc != 2)
    {
        printf("mkdir: only support 1 argument!\n");
    }
    else
    {
        make_clear_abs_path(argv[1], final_path);
        /* 若创建的不是根目录 */
        if (strcmp("/", final_path))
        {
            if (mkdir(final_path) == 0)
            {
                ret = 0;
            }
            else
            {
                printf("mkdir: create directory %s failed.\n", argv[1]);
            }
        }
    }
    return ret;
}

/* rmdir命令内建函数 */
int32_t buildin_rmdir(uint32_t argc, char **argv)
{
    int32_t ret = -1;
    if (argc != 2)
    {
        printf("rmdir: only support 1 argument!\n");
    }
    else
    {
        make_clear_abs_path(argv[1], final_path);
        /* 若删除的不是根目录 */
        if (strcmp("/", final_path))
        {
            if (rmdir(final_path) == 0)
            {
                ret = 0;
            }
            else
            {
                printf("rmdir: remove %s failed.\n", argv[1]);
            }
        }
    }
    return ret;
}

/* rm命令内建函数 */
int32_t buildin_rm(uint32_t argc, char **argv)
{
    int32_t ret = -1;
    if (argc != 2)
    {
        printf("rm: only support 1 argument!\n");
    }
    else
    {
        make_clear_abs_path(argv[1], final_path);
        /* 若删除的不是根目录 */
        if (strcmp("/", final_path))
        {
            if (unlink(final_path) == 0)
            {
                ret = 0;
            }
            else
            {
                printf("rm: delete %s failed.\n", argv[1]);
            }
        }
    }
    return ret;
}

然后在 my_shell 中对 argv[0]进行判断即可

void my_shell(void)
{
    cwd_cache[0] = '/';
    while (1)
    {
        print_prompt();
        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("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
        {
            printf("external command\n");
        }
    }
    PANIC("my_shell: should not be here");
}

加载用户进程

加载

之前用汇编实现过加载磁盘上的程序,这里使用 c 加载,核心思想还是差不多的,解析 elf 然后加载到内存空间中即可

#include "exec.h"
#include "stdint.h"
#include "global.h"
#include "memory.h"
#include "fs.h"

typedef uint32_t Elf32_Word, Elf32_Addr, Elf32_Off;
typedef uint16_t Elf32_Half;

/* 32位elf头 */
struct Elf32_Ehdr
{
    unsigned char e_ident[16];
    Elf32_Half e_type;
    Elf32_Half e_machine;
    Elf32_Word e_version;
    Elf32_Addr e_entry;
    Elf32_Off e_phoff;
    Elf32_Off e_shoff;
    Elf32_Word e_flags;
    Elf32_Half e_ehsize;
    Elf32_Half e_phentsize;
    Elf32_Half e_phnum;
    Elf32_Half e_shentsize;
    Elf32_Half e_shnum;
    Elf32_Half e_shstrndx;
};

/* 程序头表Program header.就是段描述头 */
struct Elf32_Phdr
{
    Elf32_Word p_type; // 见下面的enum segment_type
    Elf32_Off p_offset;
    Elf32_Addr p_vaddr;
    Elf32_Addr p_paddr;
    Elf32_Word p_filesz;
    Elf32_Word p_memsz;
    Elf32_Word p_flags;
    Elf32_Word p_align;
};

/* 段类型 */
enum segment_type
{
    PT_NULL,    // 忽略
    PT_LOAD,    // 可加载程序段
    PT_DYNAMIC, // 动态加载信息
    PT_INTERP,  // 动态加载器名称
    PT_NOTE,    // 一些辅助信息
    PT_SHLIB,   // 保留
    PT_PHDR     // 程序头表
};

/* 将文件描述符fd指向的文件中,偏移为offset,大小为filesz的段加载到虚拟地址为vaddr的内存 */
static bool segment_load(int32_t fd, uint32_t offset, uint32_t filesz, uint32_t vaddr)
{
    uint32_t vaddr_first_page = vaddr & 0xfffff000;               // vaddr地址所在的页框
    uint32_t size_in_first_page = PG_SIZE - (vaddr & 0x00000fff); // 加载到内存后,文件在第一个页框中占用的字节大小
    uint32_t occupy_pages = 0;
    /* 若一个页框容不下该段 */
    if (filesz > size_in_first_page)
    {
        uint32_t left_size = filesz - size_in_first_page;
        occupy_pages = DIV_ROUND_UP(left_size, PG_SIZE) + 1; // 1是指vaddr_first_page
    }
    else
    {
        occupy_pages = 1;
    }

    /* 为进程分配内存 */
    uint32_t page_idx = 0;
    uint32_t vaddr_page = vaddr_first_page;
    while (page_idx < occupy_pages)
    {
        uint32_t *pde = pde_ptr(vaddr_page);
        uint32_t *pte = pte_ptr(vaddr_page);

        /* 如果pde不存在,或者pte不存在就分配内存.
         * pde的判断要在pte之前,否则pde若不存在会导致
         * 判断pte时缺页异常 */
        if (!(*pde & 0x00000001) || !(*pte & 0x00000001))
        {
            if (get_a_page(PF_USER, vaddr_page) == NULL)
            {
                return false;
            }
        } // 如果原进程的页表已经分配了,利用现有的物理页,直接覆盖进程体
        vaddr_page += PG_SIZE;
        page_idx++;
    }
    sys_lseek(fd, offset, SEEK_SET);
    sys_read(fd, (void *)vaddr, filesz);
    return true;
}

/* 从文件系统上加载用户程序pathname,成功则返回程序的起始地址,否则返回-1 */
static int32_t load(const char *pathname)
{
    int32_t ret = -1;
    struct Elf32_Ehdr elf_header;
    struct Elf32_Phdr prog_header;
    memset(&elf_header, 0, sizeof(struct Elf32_Ehdr));

    int32_t fd = sys_open(pathname, O_RDONLY);
    if (fd == -1)
    {
        return -1;
    }

    if (sys_read(fd, &elf_header, sizeof(struct Elf32_Ehdr)) != sizeof(struct Elf32_Ehdr))
    {
        ret = -1;
        goto done;
    }

    /* 校验elf头 */
    if (memcmp(elf_header.e_ident, "\177ELF\1\1\1", 7) || elf_header.e_type != 2 || elf_header.e_machine != 3 || elf_header.e_version != 1 || elf_header.e_phnum > 1024 || elf_header.e_phentsize != sizeof(struct Elf32_Phdr))
    {
        ret = -1;
        goto done;
    }

    Elf32_Off prog_header_offset = elf_header.e_phoff;
    Elf32_Half prog_header_size = elf_header.e_phentsize;

    /* 遍历所有程序头 */
    uint32_t prog_idx = 0;
    while (prog_idx < elf_header.e_phnum)
    {
        memset(&prog_header, 0, prog_header_size);

        /* 将文件的指针定位到程序头 */
        sys_lseek(fd, prog_header_offset, SEEK_SET);

        /* 只获取程序头 */
        if (sys_read(fd, &prog_header, prog_header_size) != prog_header_size)
        {
            ret = -1;
            goto done;
        }

        /* 如果是可加载段就调用segment_load加载到内存 */
        if (PT_LOAD == prog_header.p_type)
        {
            if (!segment_load(fd, prog_header.p_offset, prog_header.p_filesz, prog_header.p_vaddr))
            {
                ret = -1;
                goto done;
            }
        }

        /* 更新下一个程序头的偏移 */
        prog_header_offset += elf_header.e_phentsize;
        prog_idx++;
    }
    ret = elf_header.e_entry;
done:
    sys_close(fd);
    return ret;
}

segment_load 将文件描述符 fd 指向的文件中,偏移为 offset,大小为 filesz 的段加载到虚拟地址为 vaddr 的内存。核心原理:我们编译程序后,编译器已经指定好了可加载段的虚拟地址,我们直接按照这个虚拟地址,把段加载到内存中对应的虚拟地址就可以了。由于这个函数是 fork 之后从磁盘编译好的程序加载可加载段时使用,所以我们使用的是调用 fork 的进程的页表,所以我们要判断目的内存虚拟地址是否在页表中有效,如果无效,则为指定虚拟地址申请物理内存。申请内存完毕,我们调用 sys_read 从磁盘中加载可加载段到指定内存虚拟地址中即可。

load 根据传入的路径,加载磁盘中的程序的可加载段,最后返回程序入口地址。原理:编译好的程序在磁盘中,起始就是 ELF header,我们去把这个读出来,从中得到 program header 的偏移、数量、每个大小。然后我们根据这些信息去循环读出 program header,根据每个 program header 信息去调用 segment_load 将可加载段加载到内存中。

#include "thread.h"

/* 用path指向的程序替换当前进程 */
int32_t sys_execv(const char *path, const char *argv[])
{
    uint32_t argc = 0;
    while (argv[argc])
    {
        argc++;
    }
    int32_t entry_point = load(path);
    if (entry_point == -1)
    { // 若加载失败则返回-1
        return -1;
    }

    struct task_struct *cur = running_thread();
    /* 修改进程名 */
    memcpy(cur->name, path, TASK_NAME_LEN);
    cur->name[TASK_NAME_LEN - 1] = 0;

    struct intr_stack *intr_0_stack = (struct intr_stack *)((uint32_t)cur + PG_SIZE - sizeof(struct intr_stack));
    /* 参数传递给用户进程 */
    intr_0_stack->ebx = (int32_t)argv;
    intr_0_stack->ecx = argc;
    intr_0_stack->eip = (void *)entry_point;
    /* 使新用户进程的栈地址为最高用户空间地址 */
    intr_0_stack->esp = (void *)0xc0000000;

    /* exec不同于fork,为使新进程更快被执行,直接从中断返回 */
    asm volatile("movl %0, %%esp; jmp intr_exit" : : "g"(intr_0_stack) : "memory");
    return 0;
}

sys_execv 用 path 指向的程序替换当前进程,注意,这个函数是 fork 之后调用的。原理:先调用 load 加载程序可执行段到内存中,并得到了程序入口地址。然后修改 pcb 中的数据即可,包括:程序名字、内核栈中中断栈中用于传参的寄存器(该函数运行在内核态下,通过 intr_exit 返回到用户态执行新的进程,所以中断栈中的数据会被 intr_exit 的 push 操作送入寄存器,以此达到传参目的)、中断栈中 eip 用于跳转程序入口、中断栈 esp 用于设定新进程的栈顶位置(fork 中拷贝了父进程页表、重新申请了物理地址空间用于拷贝用户栈数据,所以并不用担心新进程用户栈用到的虚拟地址没有映射物理地址)。最后通过内联汇编设定 esp 为中断栈的位置,然后跳转执行 intr_exit,就可以执行新的进程了。

测试程序如下:

#include "stdio.h"
int main(void)
{
    printf("prog_no_arg from disk\n");
    while (1)
        ;
    return 0;
}

编译完之后用 dd 命令写到固定位置去,然后 main 函数使用 ide_read 去读取加到文件系统中

#include "print.h"
#include "init.h"
#include "fork.h"
#include "stdio.h"
#include "syscall.h"
#include "assert.h"
#include "shell.h"
#include "console.h"
#include "ide.h"
#include "stdio-kernel.h"

void init(void);

int main(void)
{
    put_str("I am kernel\n");
    init_all();

    uint32_t file_size = 20684;      //这个变量请自行修改成自己的prog_no_arg大小
    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("/prog_no_arg", 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 /]$ ");
    while (1)
        ;
    return 0;
}

/* init进程 */
void init(void)
{
    uint32_t ret_pid = fork();
    if (ret_pid)
    { // 父进程
        while (1)
            ;
    }
    else
    { // 子进程
        my_shell();
    }
    panic("init: should not be here");
}

使用户进程支持传参

借鉴 crt,我们实现一个简单的 crt

image

[bits 32]
extern	 main
section .text
global _start
_start:
   ;下面这两个要和execv中load之后指定的寄存器一致
   push	 ebx	  ;压入argv
   push  ecx	  ;压入argc
   call  main

我们在 sys_execv 这将 argc 和 argv 放到对应位置,这里就可以拿到了

进程终止与资源回收

首先介绍几个重要的概念:

exit 系统调用:此调用用于终止进程。当一个进程调用 exit 时,它会释放除进程控制块(pcb)以外的所有资源。pcb 需要被特别处理,因为它包含了进程的重要信息,如退出状态。特别注意:exit 系统调用属于程序运行库内容,无论进程是否主动调用,都会执行。就像我们那个_start 函数一样。

wait 系统调用:这是一个与进程同步和资源回收相关的调用。具体来说,它有以下功能:

  • 阻塞父进程,直到一个子进程退出,并接收子进程的返回值。
  • 回收子进程使用过的 pcb 资源,从而确保没有资源浪费。

当一个父进程创建一个子进程来执行某项任务时,父进程可能需要知道子进程的退出状态。子进程完成其任务后,会将其退出状态保存在 pcb 中并调用 exit 退出。此时,子进程的 pcb 不会被立即回收,因为它包含了子进程的退出状态。只有当父进程通过 wait 系统调用来查询子进程的状态时,子进程的 pcb 才会被回收。

  • 孤儿进程:如果一个父进程在其子进程结束之前退出,那么这些子进程将被称为孤儿进程,也就是说没有父进程来回收他们的 pcb 资源。为了防止资源浪费,这些孤儿进程会被 init 进程“领养”,即成为 init 进程的子进程,由 init 来回收他们的 pcb。
  • 僵尸进程:当一个子进程终止,但其父进程没有调用 wait ​来回收其资源时,此时这个子进程也无法过继给 init,于是这个子进程就变成了僵尸进程。它们仍然占用 pcb,但不执行任何操作。僵尸进程的存在可能会导致资源浪费。

总结一下,进程结束时会通过 exit 留下点“遗言",也就是返回值,它代表了子进程这一生工作的结果,父进程为了获知子进程的成果如何,必须要获得子进程的返回值,而获得子进程返回值的方法,就是父进程调用 wait 系统调用。如果把父子进程之间的通信比喻成邮信,子进程通过 exit 来给父进程写信,exit 把信交给了内核,父进程知道子进程一定会写信给它,因此它主动调用 wait 收信,内核在父子进程之间起到了邮递员的作用,把子进程的返回值投递给父进程 。
僵尸进程虽然没有进程体,只在内存中保留一个 pcb,但由于 pcb 不释放,它原本的 pid 也会继续被占用,当僵尸进程数量很大时,系统将无可用 pid 分配给新进程,从而加载进程失败。然后僵尸进程并不是问题所在, 问题的根源在于产生僵尸进程的父进程,因此对于这种情况,就要将僵尸进程的父进程 kill 掉。 在 Linux 中可以利用 ps-ef 查看所有任务的 pid 和 ppid,找到状态为 Z 的进程,查看其 ppid,跟着向 pid 为 ppid 的进程发送 kill -9,手起刀落,系统又和谐了。
总结:
exit 是由子进程调用的,表面上功能是使子进程结束运行并传递返回值给内核,本质上是内核在幕后会将进程除 pcb 以外的所有资源都回收。 wait 是父进程调用的,表面上功能是使父进程阻塞自己,直到子进程调用 exit 结束运行,然后获得子进程的返回值,本质上是内核在幕后将子进程的返回值传递给父进程并会唤醒父进程,然后将子进程的 pcb 回收。

pcb增加表示退出状态的成员

/* 进程或线程的pcb,程序控制块, 此结构体用于存储线程的管理信息*/
struct task_struct
{
    uint32_t *self_kstack; // 用于存储线程的栈顶位置,栈顶放着线程要用到的运行信息
    pid_t pid;
    enum task_status status;
    uint8_t priority; // 线程优先级
    char name[16];    // 用于存储自己的线程的名字

    uint8_t ticks;                                // 线程允许上处理器运行还剩下的滴答值,因为priority不能改变,所以要在其之外另行定义一个值来倒计时
    uint32_t elapsed_ticks;                       // 此任务自上cpu运行后至今占用了多少cpu嘀嗒数, 也就是此任务执行了多久*/
    struct list_elem general_tag;                 // general_tag的作用是用于线程在一般的队列(如就绪队列或者等待队列)中的结点
    struct list_elem all_list_tag;                // all_list_tag的作用是用于线程队列thread_all_list(这个队列用于管理所有线程)中的结点
    uint32_t *pgdir;                              // 进程自己页表的虚拟地址
    struct virtual_addr userprog_vaddr;           // 用户进程的虚拟地址
    int32_t fd_table[MAX_FILES_OPEN_PER_PROC];    // 已打开文件数组
    uint32_t cwd_inode_nr;                        // 进程所在的工作目录的inode编号
    int16_t parent_pid;                           // 父进程pid
    struct mem_block_desc u_block_desc[DESC_CNT]; // 用户进程内存块描述符
    int8_t exit_status;                           // 进程结束时自己调用exit传入的参数
    uint32_t stack_magic;                         // 如果线程的栈无限生长,总会覆盖地pcb的信息,那么需要定义个边界数来检测是否栈已经到了PCB的边界
};

由于我们的进程在退出后要释放自己的pid,然而原有的pid管理只有分配,而无回收。所以我们要实现用pid位图来管理pid的分配与回收,修改(myos/thread/thread.c)(以下新allocate_pid函数需要替代原有的allocate_pid函数)

allocate_pid用于根据pid位图中空余位的偏移 + 起始pid来分配pid

pid_pool_init用于初始化pid位图,并在thread_init内调用

release_pid来释放pid,实质就是将释放pid对应的pid位图中的位置0

thread_init增加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 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池 */
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 */
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);
}

void thread_init(void)
{
    put_str("thread_init start\n");

    list_init(&thread_ready_list);
    list_init(&thread_all_list);
    pid_pool_init();

    /* 先创建第一个用户进程:init */
    process_execute(init, "init"); // 放在第一个初始化,这是第一个进程,init进程的pid为1

    /* 将当前main函数创建为线程 */
    make_main_thread();

    /* 创建idle线程 */
    idle_thread = thread_start("idle", 10, idle, NULL);

    put_str("thread_init done\n");
}

thread_exit用于回收指定任务的pcb和页表,并将其从就绪队列中删除

pid_check会被list_traversal调用,用于对比传入的all_list_tag指针对应任务的pid是不是要找的传入pid

pid2thread根据传入pid找pcb,原理是使用list_traversal调用pid_check,当pid_check找到了会返回true,于是list_traversal会返回pcb指针

/* 回收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;
}

release_prog_resource​用于根据传入的pcb指针,释放任务的资源,包括
1、页表中对应的物理页面(这里用的方法是遍历页表);
2、虚拟内存池占用的物理页框;
3、关闭打开的文件

#include "wait_exit.h"
#include "stdint.h"
#include "global.h"
#include "thread.h"
#include "fs.h"

/* 释放用户进程资源:
 * 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 fd_idx = 3;
    while (fd_idx < MAX_FILES_OPEN_PER_PROC)
    {
        if (release_thread->fd_table[fd_idx] != -1)
        {
            sys_close(fd_idx);
        }
        fd_idx++;
    }
}

然后是几个简单的回调函数:

/* list_traversal的回调函数,
 * 查找pelem的parent_pid是否是ppid,成功返回true,失败则返回false */
static bool find_child(struct list_elem* pelem, int32_t ppid) {
   /* elem2entry中间的参数all_list_tag取决于pelem对应的变量名 */
   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继续传递下一个元素
}

然后实现wait和exit

/* 等待子进程调用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); 
      }
   }
}

/* 子进程用来结束自己时调用 */
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_wait等待子进程调用exit,将子进程的退出状态保存到status指向的变量,并回收子进程的pcb与页表,最后返回子进程pid。如果子进程都在运行,那么就阻塞自己。这个函数有两种用法,一种是initwhile(1)不断调用,来不断回收子进程的资源;一种是父进程fork之后调用,然后等待子进程退出后继续运行,然后回收子进程剩余资源。

sys_exit子进程用来结束自己,退出时的事项:1、在自己的pcb中留下退出状态;2、将自己的子进程全部过继给init;3、回收自己除pcb与页表外的资源;4、可能有父进程在等待自己调用exit,所以还要唤醒等待的父进程;5、阻塞自己,也就是换下cpu。这个函数会被运行库调用,进程即使不主动调用,也会执行

然后我们修改我们的简单crt,将exit函数集成到运行库中。这样,即使程序中没有明确调用exit,它也会在程序结束时自动被调用,与_start相同。需要特别注意的是,子进程的退出机制与普通的函数返回机制不同。当子进程终止时,它并不是“返回”给其父进程;相反,它只是简单地结束了自己的执行。父进程和子进程在内存地址空间和执行上下文中是完全独立的,子进程不可能按照常规的函数调用方式“返回”一个值给父进程(做到这点需要其他的进程间通信机制支持)。取而代之的是,子进程提供一个退出状态,来描述其终止的方式或原因。因此,这里的push eax并不是我们在普通函数调用中看到的那种返回值——比如一个指针或某种计算结果。实际上,它代表了子进程的结束状态,就像我们在每个main函数中写的return 0一样。

[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不会返回

此时我们的shell就有了连续对话的能力

管道

本节要支持管道,管道是用于父子进程通信的机制

管道本质上是位于内核空间的环形缓冲区。遵循Linux的设计哲学——一切皆文件,我们将管道也视为一个文件。这样,我们就可以通过文件描述符进行对管道的读写操作。在进行父子进程之间的通信时,父进程首先创建一个管道,从而得到两个文件描述符,一个用于读,另一个用于写。随后,父进程使用fork创建子进程。子进程继承了父进程打开的文件,因此也可以通过这些文件描述符与管道进行通信,从而实现与父进程的交互。

匿名管道:仅对创建它的进程及其子进程可见,其他进程无法访问。
有名管道:可以被系统中的所有进程访问。

image

管道的实现

这里我们对原有文件结构进行复用,使得inode指向内存区域,fd_flag ​为一宏,用来区分正常文件,fd_pos复用为管道打开数

ioq_length​返回环形缓冲区内的数据长度

/* 返回环形缓冲区中的数据长度 */
uint32_t ioq_length(struct ioqueue *ioq)
{
    uint32_t len = 0;
    if (ioq->head >= ioq->tail)
    {
        len = ioq->head - ioq->tail;
    }
    else
    {
        len = bufsize - (ioq->tail - ioq->head);
    }
    return len;
}


is_pipe​判断文件描述符对应的文件是不是管道

/* 判断文件描述符local_fd是否是管道 */
bool is_pipe(uint32_t local_fd)
{
    uint32_t global_fd = fd_local2global(local_fd);
    return file_table[global_fd].fd_flag == PIPE_FLAG;
}

sys_pipe用于创建管道,核心就是创建了个全局打开文件结构,然后申请一页内核页,并让之前的文件结构内的fd_inode成员指向这个内核页(之前的文件系统中,该成员指向一个struct inode),之后再将这个内核页起始位置创建struct ioqueue并初始化。然后在进程中安装两个文件描述符,指向这个文件结构。最后记录下这两个文件描述符。

/* 创建管道,成功返回0,失败返回-1 */
int32_t sys_pipe(int32_t pipefd[2])
{
    int32_t global_fd = get_free_slot_in_global();

    /* 申请一页内核内存做环形缓冲区 */
    file_table[global_fd].fd_inode = get_kernel_pages(1);

    /* 初始化环形缓冲区 */
    ioqueue_init((struct ioqueue *)file_table[global_fd].fd_inode);
    if (file_table[global_fd].fd_inode == NULL)
    {
        return -1;
    }

    /* 将fd_flag复用为管道标志 */
    file_table[global_fd].fd_flag = PIPE_FLAG;

    /* 将fd_pos复用为管道打开数 */
    file_table[global_fd].fd_pos = 2;
    pipefd[0] = pcb_fd_install(global_fd);
    pipefd[1] = pcb_fd_install(global_fd);
    return 0;
}

pipe_read​传入管道的文件描述符、一个缓冲地址、读取字节数。通过管道的文件描述符找到环形缓冲区struct ioqueue​,然后调用ioq_getchar​从中读取数据即可。
pipe_write​传入管道的文件描述符、一个缓冲地址、写入字节数。通过管道的文件描述符找到环形缓冲区struct ioqueue​,然后调用ioq_putchar​向其写入数据即可。

/* 从管道中读数据 */
uint32_t pipe_read(int32_t fd, void* buf, uint32_t count) {
   char* buffer = buf;
   uint32_t bytes_read = 0;
   uint32_t global_fd = fd_local2global(fd);

   /* 获取管道的环形缓冲区 */
   struct ioqueue* ioq = (struct ioqueue*)file_table[global_fd].fd_inode;

   /* 选择较小的数据读取量,避免阻塞 */
   uint32_t ioq_len = ioq_length(ioq);
   uint32_t size = ioq_len > count ? count : ioq_len;
   while (bytes_read < size) {
      *buffer = ioq_getchar(ioq);
      bytes_read++;
      buffer++;
   }
   return bytes_read;
}

/* 往管道中写数据 */
uint32_t pipe_write(int32_t fd, const void* buf, uint32_t count) {
   uint32_t bytes_write = 0;
   uint32_t global_fd = fd_local2global(fd);
   struct ioqueue* ioq = (struct ioqueue*)file_table[global_fd].fd_inode;

   /* 选择较小的数据写入量,避免阻塞 */
   uint32_t ioq_left = bufsize - ioq_length(ioq);
   uint32_t size = ioq_left > count ? count : ioq_left;

   const char* buffer = buf;
   while (bytes_write < size) {
      ioq_putchar(ioq, *buffer);
      bytes_write++;
      buffer++;
   }
   return bytes_write;
}

然后在sys_close增加对管道文件的关闭代码,先调用is_pipe判断文件描述符对应的文件结构是管道文件,然后文件结构中的fd_pos -1(该成员记录管道文件的打开次数,在之前文件系统中,该成员记录文件当前操作的位置),如果此时fd_pos为0,那么直接释放环形缓冲区对应的那页内存即可。

sys_write增加对管道文件的写入代码,在fd == stdout_no增加调用is_pipe判断文件描述符对应的文件是不是管道文件(标准输出有可能会被重定向为管道文件),如果是,则调用pipe_write。然后增加fd即使不是标准输出判断是不是管道文件,如果是,调用pipe_write写入。

sys_read增加对管道文件的读入代码,在fd == stdoin_no增加调用is_pipe判断文件描述符对应的文件是不是管道文件(标准输入有可能会被重定向为管道文件),如果是,则调用pipe_read。然后增加fd即使不是标准输入判断是不是管道文件,如果是,调用pipe_read读出。

update_inode_open​增加对于管道文件的处理代码,如果是,那么fd_pos​ + 1

很简单,不再贴代码

在shell中支持管道

一般来说,键盘充当程序的输入源,而屏幕则是程序的输出目标,这被称为标准输入和输出。然而,程序也可以从文件接收输入或将其输出发送到文件中,这种方式被称为非标准输入和输出。当我们想从标准输入输出切换到文件输入输出时,我们使用输入输出重定向。通过这种方式,我们可以将一个命令的输出用作另一个命令的输入,这正是管道的功能。在Linux中,这种操作通常是通过命令行的管道符“|”完成的。例如,在命令ls | grep kanshan中,ls命令列出当前目录下的所有文件并原本会将其输出到屏幕,但由于存在管道符|,它的输出会利用管道重定向为grep命令的输入。

sys_fd_redirect其功能是将一个已有的文件描述符old_local_fd重定向为另一个文件描述符new_local_fd。实际用法如:fd_redirect(1,fd[1]);(fd[1]是管道文件对应的文件描述符,其全局文件结构索引一定大于2)用于标准输入重定位到管道文件(结合sys_write理解);fd_redirect(1,1);用于恢复标准输出(结合sys_write理解)

/* 将文件描述符old_local_fd重定向为new_local_fd */
void sys_fd_redirect(uint32_t old_local_fd, uint32_t new_local_fd)
{
    struct task_struct *cur = running_thread();
    /* 针对恢复标准描述符 */
    if (new_local_fd < 3)
    {
        cur->fd_table[old_local_fd] = new_local_fd;
    }
    else
    {
        uint32_t new_global_fd = cur->fd_table[new_local_fd];
        cur->fd_table[old_local_fd] = new_global_fd;
    }
}

cmd_execute​去取代原有shell中执行内部与外部命令功能

/* 执行命令 */
static void cmd_execute(uint32_t argc, char **argv)
{
    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 if (!strcmp("help", argv[0]))
    {
        buildin_help(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)
            { // 按理说程序正确的话不会执行到这句,fork出的进程便是shell子进程
                panic("my_shell: no child\n");
            }
            printf("child_pid %d, it's 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);
            }
        }
    }
}

新的my_shell,主要功能是从用户获取命令行输入,解析并执行命令,尤其支持管道|命令的功能。

主要新增部分:检查用户输入中是否包含管道符号|

  1. 如果有管道命令:

    1. 创建一个管道。

    2. 重定向标准输出到管道的写端。

    3. 解析并执行第一个命令。

    4. 重定向标准输入到管道的读端。

    5. 对于每一个中间的命令(除了最后一个):

      1. 解析并执行命令。
    6. 恢复标准输出到屏幕。

    7. 执行管道中的最后一个命令。

    8. 恢复标准输入为键盘。

    9. 关闭管道。

  2. 如果没有管道命令:

    1. 解析用户输入的命令。
    2. 如果参数数量超过了设定的最大值,则提示错误。
    3. 否则执行命令。
#include "pipe.h"

void my_shell(void)
{
    cwd_cache[0] = '/';
    while (1)
    {
        print_prompt();
        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;
        }

        /* 针对管道的处理 */
        char *pipe_symbol = strchr(cmd_line, '|');
        if (pipe_symbol)
        {
            /* 支持多重管道操作,如cmd1|cmd2|..|cmdn,
             * cmd1的标准输出和cmdn的标准输入需要单独处理 */

            /*1 生成管道*/
            int32_t fd[2] = {-1}; // fd[0]用于输入,fd[1]用于输出
            pipe(fd);
            /* 将标准输出重定向到fd[1],使后面的输出信息重定向到内核环形缓冲区 */
            fd_redirect(1, fd[1]);

            /*2 第一个命令 */
            char *each_cmd = cmd_line;
            pipe_symbol = strchr(each_cmd, '|');
            *pipe_symbol = 0;

            /* 执行第一个命令,命令的输出会写入环形缓冲区 */
            argc = -1;
            argc = cmd_parse(each_cmd, argv, ' ');
            cmd_execute(argc, argv);

            /* 跨过'|',处理下一个命令 */
            each_cmd = pipe_symbol + 1;

            /* 将标准输入重定向到fd[0],使之指向内核环形缓冲区*/
            fd_redirect(0, fd[0]);
            /*3 中间的命令,命令的输入和输出都是指向环形缓冲区 */
            while ((pipe_symbol = strchr(each_cmd, '|')))
            {
                *pipe_symbol = 0;
                argc = -1;
                argc = cmd_parse(each_cmd, argv, ' ');
                cmd_execute(argc, argv);
                each_cmd = pipe_symbol + 1;
            }

            /*4 处理管道中最后一个命令 */
            /* 将标准输出恢复屏幕 */
            fd_redirect(1, 1);

            /* 执行最后一个命令 */
            argc = -1;
            argc = cmd_parse(each_cmd, argv, ' ');
            cmd_execute(argc, argv);

            /*5  将标准输入恢复为键盘 */
            fd_redirect(0, 0);

            /*6 关闭管道 */
            close(fd[0]);
            close(fd[1]);
        }
        else
        { // 一般无管道操作的命令
            argc = -1;
            argc = cmd_parse(cmd_line, argv, ' ');
            if (argc == -1)
            {
                printf("num of arguments exceed %d\n", MAX_ARG_NR);
                continue;
            }
            cmd_execute(argc, argv);
        }
    }
    panic("my_shell: should not be here");
}

posted @   fdx_xdf  阅读(9)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)
历史上的今天:
2024-02-18 PEB及其武器化
点击右上角即可分享
微信分享提示