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

配合视频学习体验更佳!
a小节:https://www.bilibili.com/video/BV1gk4y1w7u1/?vd_source=701807c4f8684b13e922d0a8b116af31
b小节:https://www.bilibili.com/video/BV1Mm4y1N7fS/?vd_source=701807c4f8684b13e922d0a8b116af31
c小节:https://www.bilibili.com/video/BV1Th4y1A7xw/?vd_source=701807c4f8684b13e922d0a8b116af31
d小节:https://www.bilibili.com/video/BV1cm4y1N7XG/?vd_source=701807c4f8684b13e922d0a8b116af31
e小节:https://www.bilibili.com/video/BV1qh4y1Y7gE/?vd_source=701807c4f8684b13e922d0a8b116af31
f小节:https://www.bilibili.com/video/BV1Su4y1675s/?vd_source=701807c4f8684b13e922d0a8b116af31
g小节:https://www.bilibili.com/video/BV1RP41187cD/?vd_source=701807c4f8684b13e922d0a8b116af31
h小节:https://www.bilibili.com/video/BV1mH4y1D7Ze/?vd_source=701807c4f8684b13e922d0a8b116af31
i小节:https://www.bilibili.com/video/BV1rP411t7bd/?vd_source=701807c4f8684b13e922d0a8b116af31
j小节:https://www.bilibili.com/video/BV1Jk4y1F7ob/?vd_source=701807c4f8684b13e922d0a8b116af31
k小节:https://www.bilibili.com/video/BV1pm4y1N7CH/?vd_source=701807c4f8684b13e922d0a8b116af31

代码仓库:https://github.com/xukanshan/the_truth_of_operationg_system

小节a:
fork

这一小节,我们要实现fork

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

有这样一段代码

#include <unistd.h>
#include <stdio.h>
int main() {
	int pid = fork();
	if (pid == -1)
		return 1;
	printf("who am I ? my pid is %d\n", getpid());
	sleep (5) ;
	return 0;
}

当执行完fork()后,fork之后的代码会由于属于两个进程(调用fork的主进程与被复制出来的子进程)而被执行两次(自然是主进程与子进程各执行一次)。

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

if (pid) {
	printf("I am father, my pid is d\n",getpid());
	sleep(5);
	return 0;
} 
else {
	printf("I am child, my pid is d\n",getpid());
	sleep(5);
	return 0;
}

现在开始实现fork,先实现一些基础设施函数

fork_pid就是封装了allocate_pid,因为allocate_pid之前实现的时候有关键字static,所以作者为了不去修改这个,就采取了进一步封装

修改(myos/thread/thread.c

/* fork进程时为其分配pid,因为allocate_pid已经是静态的,别的文件无法调用.
不想改变函数定义了,故定义fork_pid函数来封装一下。*/
pid_t fork_pid(void)
{
    return allocate_pid();
}

函数声明,修改(myos/thread/thread.h

pid_t fork_pid(void);

get_a_page_without_opvaddrbitmap用于为指定的虚拟地址创建物理页映射,与get_a_page相比,少了操作进程pcb中的虚拟内存池位图

修改(myos/kernel/memory.c

/* 安装1页大小的vaddr,专门针对fork时虚拟地址位图无须操作的情况 */
void *get_a_page_without_opvaddrbitmap(enum pool_flags pf, uint32_t vaddr)
{
    struct pool *mem_pool = pf & PF_KERNEL ? &kernel_pool : &user_pool;
    lock_acquire(&mem_pool->lock);
    void *page_phyaddr = palloc(mem_pool);
    if (page_phyaddr == NULL)
    {
        lock_release(&mem_pool->lock);
        return NULL;
    }
    page_table_add((void *)vaddr, page_phyaddr);
    lock_release(&mem_pool->lock);
    return (void *)vaddr;
}

函数声明,修改(myos/kernel/memory.h

void *get_a_page_without_opvaddrbitmap(enum pool_flags pf, uint32_t vaddr);

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(让子进程拥有自己的用户虚拟地址空间内存池,但是其位图是拷贝父进程的)。这个过程中,内核栈中的内容被完全拷贝了。

myos/userprog/fork.c

#include "fork.h"
#include "stdint.h"
#include "global.h"
#include "thread.h"
#include "string.h"
#include "debug.h"
#include "process.h"

/* 将父进程的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;
}

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

修改(myos/userprog/fork.c

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++;
    }
}

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指令,我们现在经过拷贝后的子进程内核栈布局如图:

在这里插入图片描述
所以,我们直接去用子进程这样的内核栈布局肯定不行,要人为去修改成
在这里插入图片描述
也就是在intr_stack前面增加switch_to栈(也就是书p694提到的thread_stack),让pcb最顶端的esp指向switch_to栈栈顶,并且switch_to栈中返回地址要填上intr_exit函数地址。这样执行ret之后,就能去执行intr_exit,并利用intr_stack执行中断返回,由于intr_stack中拷贝了父进程进入中断时的用户栈信息,cs: ip 信息,所以中断退出后,子进程将会继续执行父进程之后的代码。

修改(myos/userprog/fork.c

/* 为子进程构建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;
}

update_inode_open_cnts由于fork出来的子进程几乎和父进程一样,所以父进程打开的文件,子进程也要打开。所以,父进程的全局打开文件结构中记录文件打开的次数都需要 + 1。原理:遍历进程pcb(父,子均可)中的文件描述符,找到对应的全局打开文件结构索引就行了

修改(myos/user/fork.c

#include <file.h>

/* 更新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++;
    }
}

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

修改(myos/user/fork.c

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

sys_fork用于复制出一个进程,并将其加入就绪队列

myos/userprog/fork.c

#include "interrupt.h"

/* fork子进程,内核线程不可直接调用 */
pid_t sys_fork(void)
{
    struct task_struct *parent_thread = running_thread();
    struct task_struct *child_thread = get_kernel_pages(1); // 为子进程创建pcb(task_struct结构)
    if (child_thread == NULL)
    {
        return -1;
    }
    ASSERT(INTR_OFF == intr_get_status() && parent_thread->pgdir != NULL);

    if (copy_process(child_thread, parent_thread) == -1)
    {
        return -1;
    }

    /* 添加到就绪线程队列和所有线程队列,子进程由调试器安排运行 */
    ASSERT(!elem_find(&thread_ready_list, &child_thread->general_tag));
    list_append(&thread_ready_list, &child_thread->general_tag);
    ASSERT(!elem_find(&thread_all_list, &child_thread->all_list_tag));
    list_append(&thread_all_list, &child_thread->all_list_tag);

    return child_thread->pid; // 父进程返回子进程的pid
}

函数声明(myos/userprog/fork.h

#ifndef __USERPROG_FORK_H
#define __USERPROG_FORK_H

#include "stdint.h"

pid_t sys_fork(void);

#endif

然后我们添加fork系统调用

添加系统调用号,修改(myos/lib/user/syscall.h

#include "thread.h"

enum SYSCALL_NR {
   SYS_GETPID,
   SYS_WRITE,
   SYS_MALLOC,
   SYS_FREE,
   SYS_FORK
};

用户态系统调用入口,修改(myos/lib/user/syscall.c

#include "thread.h"

/* 派生子进程,返回子进程pid */
pid_t fork(void)
{
    return _syscall0(SYS_FORK);
}

函数声明,修改(myos/lib/user/syscall.h

pid_t fork(void);

系统调用表中添加实际系统调用函数,修改(myos/userprog/syscall-init.c

#include "fork.h"

/* 初始化系统调用 */
void syscall_init(void) {
	put_str("syscall_init start\n");
	syscall_table[SYS_GETPID] = sys_getpid;
	syscall_table[SYS_WRITE] = sys_write;
	syscall_table[SYS_MALLOC] = sys_malloc;
   	syscall_table[SYS_FREE] = sys_free;
    syscall_table[SYS_FORK] = sys_fork;
	put_str("syscall_init done\n");
}

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

修改(myos/thread/thread.c/thread_init),让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");
}

测试代码与init进程实现

myos/kernel/main.c

#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)
        ;
}

编译运行会报页错误,经过排查,修改(myos/thread/thread.c/thread_create

    /* 先预留中断使用栈的空间,可见thread.h中定义的结构 */
    // pthread->self_kstack -= sizeof(struct intr_stack);  //-=结果是sizeof(struct intr_stack)的4倍
    // self_kstack类型为uint32_t*,也就是一个明确指向uint32_t类型值的地址,那么加减操作,都是会是sizeof(uint32_t) = 4 的倍数
    pthread->self_kstack = (uint32_t *)((int)(pthread->self_kstack) - sizeof(struct intr_stack));

    /* 再留出线程栈空间,可见thread.h中定义 */
    // pthread->self_kstack -= sizeof(struct thread_stack);
    pthread->self_kstack = (uint32_t *)((int)(pthread->self_kstack) - sizeof(struct thread_stack));

    /* 先预留中断使用栈的空间,可见thread.h中定义的结构 */
    pthread->self_kstack -= sizeof(struct intr_stack);  //-=结果是sizeof(struct intr_stack)的4倍
    // self_kstack类型为uint32_t*,也就是一个明确指向uint32_t类型值的地址,那么加减操作,都是会是sizeof(uint32_t) = 4 的倍数
    // pthread->self_kstack = (uint32_t *)((int)(pthread->self_kstack) - sizeof(struct intr_stack));

    /* 再留出线程栈空间,可见thread.h中定义 */
    pthread->self_kstack -= sizeof(struct thread_stack);
    // pthread->self_kstack = (uint32_t *)((int)(pthread->self_kstack) - sizeof(struct thread_stack));

典型的程序依靠错误运行,暂不知道为何错误

小节b:

获取键盘输入

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

sys_put_char用于向屏幕输出一个字符

修改(myos/fs/fs.c/sys_read

#include "keyboard.h"
#include "ioqueue.h"

/* 从文件描述符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;
}

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

函数声明,修改(myos/fs/fs.h

void sys_putchar(char char_asci);

cls_screen用于清空屏幕,核心原理:向代表80列×25行,共2000个字符位置的内存写入空格符,然后设定光标位置为左上角(即位置0)

修改(myos/lib/kernel/print.S

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

函数声明,修改(myos/lib/kernel/print.h

void cls_screen(void);

sys_readsys_putcharcls_screen做成系统调用

添加系统调用号,修改(myos/lib/user/syscall.h

enum SYSCALL_NR
{
    SYS_GETPID,
    SYS_WRITE,
    SYS_MALLOC,
    SYS_FREE,
    SYS_FORK,
    SYS_READ,
    SYS_PUTCHAR,
    SYS_CLEAR
};

准备好readput_charclear的用户态入口,修改(myos/lib/user/syscall.c

/* 从文件描述符fd中读取count个字节到buf */
int32_t read(int32_t fd, void *buf, uint32_t count)
{
    return _syscall3(SYS_READ, fd, buf, count);
}

/* 输出一个字符 */
void putchar(char char_asci)
{
    _syscall1(SYS_PUTCHAR, char_asci);
}

/* 清空屏幕 */
void clear(void)
{
    _syscall0(SYS_CLEAR);
}

然后声明函数,修改(myos/lib/user/syscall.h

int32_t read(int32_t fd, void* buf, uint32_t count);
void putchar(char char_asci);
void clear(void);

将系统调用实际执行程序,添加至系统调用表中,修改(myos/lib/user/syscall.c

/* 初始化系统调用 */
void syscall_init(void)
{
    put_str("syscall_init start\n");
    syscall_table[SYS_GETPID] = sys_getpid;
    syscall_table[SYS_WRITE] = sys_write;
    syscall_table[SYS_MALLOC] = sys_malloc;
    syscall_table[SYS_FREE] = sys_free;
    syscall_table[SYS_FORK] = sys_fork;
    syscall_table[SYS_READ] = sys_read;
    syscall_table[SYS_PUTCHAR] = sys_putchar;
    syscall_table[SYS_CLEAR] = cls_screen;
    put_str("syscall_init done\n");
}

shell是用户与操作系统之间交互的接口,我们天天使用的Linux终端就是一个shell。它的功能就是获取用户的键盘输入,然后从中解析命令,然后根据命令去执行对应的动作。

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

myos/shell/shell.c

#include "shell.h"
#include "stdio.h"


char cwd_cache[64] = {0};

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


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

修改(myos/shell/shell.c

#include "file.h"
#include "debug.h"
#include "syscall.h"

/* 从键盘缓冲区中最多读入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");
}

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

修改(myos/shell/shell.c

#include "string.h"

#define cmd_len 128 // 最大支持键入128个字符的命令行输入
static char cmd_line[cmd_len] = {0};

/* 简单的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");
}

函数声明,(myos/shell/shell.h

#ifndef __KERNEL_SHELL_H
#define __KERNEL_SHELL_H

void print_prompt(void);
void my_shell(void);

#endif

我们让init来开启shell,修改(myos/kernel/main.c

#include "print.h"
#include "init.h"
#include "fork.h"
#include "stdio.h"
#include "syscall.h"
#include "debug.h"
#include "shell.h"
#include "console.h"

void init(void);

int main(void) {
   put_str("I am kernel\n");
   init_all();
   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");
}

删除(myos/device/keyboard.c/intr_keyboard_handler)中的打印语句

put_char(cur_char);	    // 临时的

makefile记得新增包含静态库

LIB= -I lib/ -I lib/kernel/ -I lib/user/ -I kernel/ -I device/ -I thread/ -I userprog/	-I fs/ -I shell/

这里的问题:

1、main函数中有打印命令提示符的语句,而init_all中调用thread_init调用process_execute创建了init进程,init运行时会fork出只调用shell的进程,这个进程会调用print_prompt打印命令提示符,这就和main当中打印是冲突了的。要想实现书上的效果,那么fork出运行shell的进程调用print_prompt必须在main调用cls_screen之前。这依赖于特定的任务执行顺序,不过一般不会出错。

小节c:

添加快捷键

readline中新增加对于组合键的处理,ctrl + l 清除除了当前行外的其他行。ctrl + u清除本行的输入,效果类似于连续按下多个退格。我们在键盘中断处理程序中已经预先写好了按下ctrl + l 与 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);
            }

也就是说,我们按下ctrl + l 与 ctrl + u时,放入键盘输入缓冲区的字符是ascii 码为 ‘l’ - ‘a’ 与 ‘u’ - ‘a’,这两个ascii码都属于不可见的控制字符。所以我们只需要增加readline读出这两种情况的处理逻辑即可

修改(myos/shell/shell.c

/* 从键盘缓冲区中最多读入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");
}

小节d:

解析键入的字符

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

修改(myos/shell/shell.c

#define MAX_ARG_NR 16	   // 加上命令名外,最多支持15个参数

/* 分析字符串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;
}

my_shell增加测试代码, 输出每个分离出来的单词

修改(myos/shell/shell.c

char *argv[MAX_ARG_NR];              // argv必须为全局变量,为了以后exec的程序可访问参数
char final_path[MAX_PATH_LEN] = {0}; // 用于洗路径时的缓冲
int32_t argc = -1;

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");
}

小节e:

实现输入命令,然后调用对应的函数

先实现一个ps系统调用

pad_print用于对齐输出,也就是有一个buf区长度10字节,然后我们无论要输出什么,都向这个buf中写入,然后空余部分全部填充空格,最后将整个buf输出。比如输出“hello”,经过处理就变成了"hello "

elem2thread_info调用pad_print来对齐输出每个pcb的pid, ppid, status, elapsed_ticks, name

sys_ps调用list_traversal遍历所有任务队列,在其中回调elem2thread_info来输出进程或线程pcb中的信息

修改(myos/thread/thread.c

#include "stdio.h"
#include "fs.h"
#include "file.h"

/* 以填充空格的方式输出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);
}

添加函数声明,修改(myos/thread/thread.h

void sys_ps(void);

然后将上一章和本章实现的sys开头的函数,全部封装成系统调用

首先添加系统调用号,修改(myos/lib/user/syscall.h

#include "fs.h"

enum SYSCALL_NR
{
    SYS_GETPID,
    SYS_WRITE,
    SYS_MALLOC,
    SYS_FREE,
    SYS_FORK,
    SYS_READ,
    SYS_PUTCHAR,
    SYS_CLEAR,
    SYS_GETCWD,
    SYS_OPEN,
    SYS_CLOSE,
    SYS_LSEEK,
    SYS_UNLINK,
    SYS_MKDIR,
    SYS_OPENDIR,
    SYS_CLOSEDIR,
    SYS_CHDIR,
    SYS_RMDIR,
    SYS_READDIR,
    SYS_REWINDDIR,
    SYS_STAT,
    SYS_PS
};

然后实现它们的用户态入口,修改(myos/lib/user/syscall.c


/* 获取当前工作目录 */
char *getcwd(char *buf, uint32_t size)
{
    return (char *)_syscall2(SYS_GETCWD, buf, size);
}

/* 以flag方式打开文件pathname */
int32_t open(char *pathname, uint8_t flag)
{
    return _syscall2(SYS_OPEN, pathname, flag);
}

/* 关闭文件fd */
int32_t close(int32_t fd)
{
    return _syscall1(SYS_CLOSE, fd);
}

/* 设置文件偏移量 */
int32_t lseek(int32_t fd, int32_t offset, uint8_t whence)
{
    return _syscall3(SYS_LSEEK, fd, offset, whence);
}

/* 删除文件pathname */
int32_t unlink(const char *pathname)
{
    return _syscall1(SYS_UNLINK, pathname);
}

/* 创建目录pathname */
int32_t mkdir(const char *pathname)
{
    return _syscall1(SYS_MKDIR, pathname);
}

/* 打开目录name */
struct dir *opendir(const char *name)
{
    return (struct dir *)_syscall1(SYS_OPENDIR, name);
}

/* 关闭目录dir */
int32_t closedir(struct dir *dir)
{
    return _syscall1(SYS_CLOSEDIR, dir);
}

/* 删除目录pathname */
int32_t rmdir(const char *pathname)
{
    return _syscall1(SYS_RMDIR, pathname);
}

/* 读取目录dir */
struct dir_entry *readdir(struct dir *dir)
{
    return (struct dir_entry *)_syscall1(SYS_READDIR, dir);
}

/* 回归目录指针 */
void rewinddir(struct dir *dir)
{
    _syscall1(SYS_REWINDDIR, dir);
}

/* 获取path属性到buf中 */
int32_t stat(const char *path, struct stat *buf)
{
    return _syscall2(SYS_STAT, path, buf);
}

/* 改变工作目录为path */
int32_t chdir(const char *path)
{
    return _syscall1(SYS_CHDIR, path);
}

/* 显示任务列表 */
void ps(void)
{
    _syscall0(SYS_PS);
}

添加系统调用用户态入口函数声明,修改修改(myos/lib/user/syscall.h

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);

最后在系统调用表中添加真正的系统调用执行函数,修改(myos/userprog/syscall-init.c

/* 初始化系统调用 */
void syscall_init(void)
{
    put_str("syscall_init start\n");
    syscall_table[SYS_GETPID] = sys_getpid;
    syscall_table[SYS_WRITE] = sys_write;
    syscall_table[SYS_MALLOC] = sys_malloc;
    syscall_table[SYS_FREE] = sys_free;
    syscall_table[SYS_FORK] = sys_fork;
    syscall_table[SYS_READ] = sys_read;
    syscall_table[SYS_PUTCHAR] = sys_putchar;
    syscall_table[SYS_CLEAR] = cls_screen;
    syscall_table[SYS_GETCWD] = sys_getcwd;
    syscall_table[SYS_OPEN] = sys_open;
    syscall_table[SYS_CLOSE] = sys_close;
    syscall_table[SYS_LSEEK] = sys_lseek;
    syscall_table[SYS_UNLINK] = sys_unlink;
    syscall_table[SYS_MKDIR] = sys_mkdir;
    syscall_table[SYS_OPENDIR] = sys_opendir;
    syscall_table[SYS_CLOSEDIR] = sys_closedir;
    syscall_table[SYS_CHDIR] = sys_chdir;
    syscall_table[SYS_RMDIR] = sys_rmdir;
    syscall_table[SYS_READDIR] = sys_readdir;
    syscall_table[SYS_REWINDDIR] = sys_rewinddir;
    syscall_table[SYS_STAT] = sys_stat;
    syscall_table[SYS_PS] = sys_ps;
    put_str("syscall_init done\n");
}

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

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进行路径转换。

#include "buildin_cmd.h"
#include "debug.h"
#include "dir.h"
#include "string.h"
#include "fs.h"
#include "syscall.h"

/* 将路径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);
}

问题1:代码中:new_abs_path[0] = 0;意义何在?

确保了后续在new_abs_path上的任何字符串连接(例如通过strcat函数)都会从头开始。

函数声明,(myos/shell/buildin_cmd.h

#ifndef __SHELL_BUILDIN_CMD_H
#define __SHELL_BUILDIN_CMD_H

void make_clear_abs_path(char *path, char *final_path);

#endif

支持代码,修改(myos/fs/fs.c

static char *path_parse(char *pathname, char *name_store)

char *path_parse(char *pathname, char *name_store)

my_shell增加测试代码,修改(myos/shell/shell.c

#include "buildin_cmd.h"

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

        char buf[MAX_PATH_LEN] = {0};
        int32_t arg_idx = 0;
        while (arg_idx < argc)
        {
            make_clear_abs_path(argv[arg_idx], buf);
            printf("%s -> %s\n", argv[arg_idx], buf);
            arg_idx++;
        }
    }
    PANIC("my_shell: should not be here");
}

小节f:

实现一系列内建命令

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

每个内建函数都会传入两个参数:

  1. uint32_t argc: 这个参数表示传入到该函数的参数个数。在命令 ls -l 中,ls 是命令,而 -lls的参数。在这个例子中,argc 就是2,因为有两个参数:ls-l
  2. char** argv: 这是一个指向字符串数组的指针,代表传入的参数值。argv 的每一个元素都是一个字符串,表示命令行上的一个参数。

buildin_pwd就是调用了getcwd

修改(myos/shell/buildin_cmd.c

#include "shell.h"
#include "stdio.h"


/* 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");
        }
    }
}

支持代码,修改(myos/shell/shell.h

#include "fs.h"

extern char final_path[MAX_PATH_LEN];

buildin_cd就是调用了make_clear_abs_path解析argv[1]成绝对路径,然后调用chdir来切换目录

修改(myos/shell/buildin_cmd.c

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

buildin_ls:用于列出文件或目录

函数核心原理:

  1. 命令行参数解析:
    使用 while 循环遍历所有的命令行参数 argv,并进行以下处理:
    • 如果参数以 - 开头,那么它被视为一个选项。目前支持两个选项:-l-h。其中 -l 选项使信息以长格式输出,而 -h 选项则打印帮助信息
    • 如果参数不是一个选项,则被视为一个路径参数。函数只支持一个路径参数。
  2. 设置默认路径:
    如果用户未提供路径参数,函数将使用当前工作目录作为默认路径。
  3. 获取文件或目录状态:
    使用 stat 函数检查指定路径文件或目录的状态。如果路径不存在,函数将打印错误信息并返回。
  4. 目录处理:
    如果指定的路径是一个目录:
    • 打开这个目录。
    • 如果使用了 -l 选项,则以长格式输出目录中的每个目录项。这包括文件类型(目录或普通文件)、i节点号、文件大小和文件名。
    • 如果没有使用 -l 选项,则只输出文件名。
    • 最后,关闭目录。
  5. 文件处理:
    如果指定的路径是一个文件:
    • 如果使用了 -l 选项,则以长格式输出文件的信息。
    • 如果没有使用 -l 选项,则只输出文件名。

修改(myos/shell/buildin_cmd.c

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

buildin_ps就是调用ps

修改(myos/shell/buildin_cmd.c

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

buildin_clear就是调用clear

修改(myos/shell/buildin_cmd.c

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

buildin_mkdir就是调用make_clear_abs_path解析argv[1]成绝对路径,然后调用mkdir

修改(myos/shell/buildin_cmd.c

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

buildin_rmdir就是调用make_clear_abs_path解析argv[1]成绝对路径,然后调用rmdir

修改(myos/shell/buildin_cmd.c

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

buildin_rm就是调用make_clear_abs_path解析argv[1]成绝对路径,然后调用unlink

修改(myos/shell/buildin_cmd.c

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

函数声明,修改(myos/shell/buildin_cmd.h

#include "global.h"

void buildin_pwd(uint32_t argc, char **argv UNUSED);
char *buildin_cd(uint32_t argc, char **argv);
void buildin_ls(uint32_t argc, char **argv);
void buildin_ps(uint32_t argc, char **argv UNUSED);
void buildin_clear(uint32_t argc, char **argv UNUSED);
int32_t buildin_mkdir(uint32_t argc, char **argv);
int32_t buildin_rmdir(uint32_t argc, char **argv);
int32_t buildin_rm(uint32_t argc, char **argv);

my_shell就是增加了通过判断arg[0](这个是要调用的命令名)是什么,然后对应调用内建函数

修改(myos/shell/shell.c

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");
}

小节g:

加载用户进程

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

myos/userprog/exec.c

#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;
}

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

修改(myos/userprog/exec.c

#include "string.h"

/* 从文件系统上加载用户程序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;
}

在C和C++中,使用\xHH格式的十六进制转义序列时,需要特别小心,因为这个序列会继续解析所有有效的十六进制数字,直到遇到一个非十六进制数字或序列的长度达到其最大值。字符串 "\x7fELF" 会被解析为一个字符 \x7fE,然后是 LF,而不是我们预期的 \x7fELF

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

修改(myos/userprog/exec.c

#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;
}

支持代码,修改(myos/thread/thread.h

#define TASK_NAME_LEN 16

sys_execv做成系统调用

添加系统调用号,修改(myos/lib/user/syscall.h

enum SYSCALL_NR
{
    SYS_GETPID,
    SYS_WRITE,
    SYS_MALLOC,
    SYS_FREE,
    SYS_FORK,
    SYS_READ,
    SYS_PUTCHAR,
    SYS_CLEAR,
    SYS_GETCWD,
    SYS_OPEN,
    SYS_CLOSE,
    SYS_LSEEK,
    SYS_UNLINK,
    SYS_MKDIR,
    SYS_OPENDIR,
    SYS_CLOSEDIR,
    SYS_CHDIR,
    SYS_RMDIR,
    SYS_READDIR,
    SYS_REWINDDIR,
    SYS_STAT,
    SYS_PS,
    SYS_EXECV
};

用户态系统调用入口,修改(myos/lib/user/syscall.c

int execv(const char *pathname, char **argv)
{
    return _syscall2(SYS_EXECV, pathname, argv);
}

声明用户态系统调用入口,修改(myos/lib/user/syscall.h

int execv(const char* pathname, char** argv);

系统调用表修改,修改(myos/userprog/syscall-init.c

#include "exec.h"

/* 初始化系统调用 */
void syscall_init(void)
{
    put_str("syscall_init start\n");
    syscall_table[SYS_GETPID] = sys_getpid;
    syscall_table[SYS_WRITE] = sys_write;
    syscall_table[SYS_MALLOC] = sys_malloc;
    syscall_table[SYS_FREE] = sys_free;
    syscall_table[SYS_FORK] = sys_fork;
    syscall_table[SYS_READ] = sys_read;
    syscall_table[SYS_PUTCHAR] = sys_putchar;
    syscall_table[SYS_CLEAR] = cls_screen;
    syscall_table[SYS_GETCWD] = sys_getcwd;
    syscall_table[SYS_OPEN] = sys_open;
    syscall_table[SYS_CLOSE] = sys_close;
    syscall_table[SYS_LSEEK] = sys_lseek;
    syscall_table[SYS_UNLINK] = sys_unlink;
    syscall_table[SYS_MKDIR] = sys_mkdir;
    syscall_table[SYS_OPENDIR] = sys_opendir;
    syscall_table[SYS_CLOSEDIR] = sys_closedir;
    syscall_table[SYS_CHDIR] = sys_chdir;
    syscall_table[SYS_RMDIR] = sys_rmdir;
    syscall_table[SYS_READDIR] = sys_readdir;
    syscall_table[SYS_REWINDDIR] = sys_rewinddir;
    syscall_table[SYS_STAT] = sys_stat;
    syscall_table[SYS_PS] = sys_ps;
    syscall_table[SYS_EXECV] = sys_execv;
    put_str("syscall_init done\n");
}

修改my_shell增加对于外部命令去磁盘加载编译好的二进制进程序并执行的代码,核心就是先fork创建子进程,然后子进程调用make_clear_abs_path解析传入的路径,然后调用execv去执行

修改(myos/shell/shell.c/my_shell

#include "syscall.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;
        }

        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)
            { // 父进程
                /* 下面这个while必须要加上,否则父进程一般情况下会比子进程先执行,
                因此会进行下一轮循环将findl_path清空,这样子进程将无法从final_path中获得参数*/
                while (1)
                    ;
            }
            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]);
                }
                else
                {
                    execv(argv[0], argv);
                }
                while (1)
                    ;
            }
        }
        int32_t arg_idx = 0;
        while (arg_idx < MAX_ARG_NR)
        {
            argv[arg_idx] = NULL;
            arg_idx++;
        }
    }
    PANIC("my_shell: should not be here");
}

编译好一个用户程序prog_no_arg,我们需要自行写入hd60M.img,然后用操作系统从hd60M.img中取到编译好的用户程序到内存,再写入有文件系统的hd80M.img,最后再执行prog_no_arg

myos/command/prog_no_arg.c

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

由于这个程序复用了printf,而printf调用了vsprintf,而vsprintf调用了strcpy,而strcpy调用了ASSERT宏,而ASSERT使用了PANIC,而PANIC使用了panic_spin,而panic_spin使用了,intr_disable()。也就是说,我们调用用户态printf的程序,如果中间strcpy的ASSERT出错,将会直接在用户态调用intr_disable,这是绝对不能允许的,运行会报特权级保护错误!正确的做法是,先通过系统调用切换至内核态,然后再调用intr_disable。

所以,我们先实现用户态使用的assert

myos/lib/user/assert.c

#include "assert.h"
#include "stdio.h"
void user_spin(char *filename, int line, const char *func, const char *condition)
{
    printf("\n\n\n\nfilename %s\nline %d\nfunction %s\ncondition %s\n", filename, line, func, condition);
    while (1)
        ;
}

myos/lib/user/assert.h

#ifndef __LIB_USER_ASSERT_H
#define __LIB_USER_ASSERT_H

#include "global.h"

void user_spin(char *filename, int line, const char *func, const char *condition);
#define panic(...) user_spin(__FILE__, __LINE__, __func__, __VA_ARGS__)

#ifdef NDEBUG
#define assert(CONDITION) ((void)0)
#else
#define assert(CONDITION)  \
    if (!(CONDITION))      \
    {                      \
        panic(#CONDITION); \
    }

#endif /*NDEBUG*/

#endif /*__LIB_USER_ASSERT_H*/

如此一来,我们的assert判断出错,将会通过printf内的write系统调用正常进入内核态

我们去把内核中用户态程序用到的ASSERT与PANIC都改掉

修改(myos/lib/string.c)中所有的ASSERTassert,然后将头文件#include "debug.h"修改为#incldue "assert.h"

修改(myos/shell/buildin_cmd.c)中所有的ASSERTassert,然后将头文件#include "debug.h"修改为#incldue "assert.h"

修改(myos/shell/shell.c)中所有的ASSERTassert,修改所有的PANICpanic,然后将头文件#include "debug.h"修改为#incldue "assert.h"

修改(myos/kernel/main.c)中所有的PANICpanic,然后将头文件#include "debug.h"修改为#incldue "assert.h"

给出操作prog_no_arg.c的脚本,该脚本主要功能:编译prog_no_arg.c,然后将其与使用到的.o文件进行链接(这里我们复用了给操作系统用的.o文件,按道理来说,我们需要单独实现用户程序的.o文件,但是我们偷个懒吧),最后写入磁盘hd60M.img偏移300扇区的位置

注意:相较于作者脚本,已经修改好了用到的编译器为gcc-4.4,CFLAGS,ld后面的参数,**一定要自行修改DD_OUT为自己环境中的hd60M.img路径!!!**运行脚本需要在操作系统make all之后(这样用到的.o文件才会出现在build目录下),运行脚本需要在command目录下,脚本运行前需要添加可执行权限,命令:chmod +x compile.sh,执行脚本命令:./compile.sh

myos/command/compile.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
CC="gcc-4.4"
BIN="prog_no_arg"
CFLAGS="-Wall -c -fno-builtin -W -Wstrict-prototypes \
      -Wmissing-prototypes -Wsystem-headers -m32 -fno-stack-protector"
LIB="../lib/"
OBJS="../build/string.o ../build/syscall.o \
      ../build/stdio.o ../build/assert.o"
DD_IN=$BIN
DD_OUT="/home/rlk/Desktop/bochs/hd60M.img" 

$CC $CFLAGS -I $LIB -o $BIN".o" $BIN".c"
ld -e main $BIN".o" $OBJS -o $BIN -m elf_i386
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

##########   以上核心就是下面这三条命令   ##########
#gcc -Wall -c -fno-builtin -W -Wstrict-prototypes -Wmissing-prototypes \
#   -Wsystem-headers -I ../lib -o prog_no_arg.o prog_no_arg.c
#ld -e main prog_no_arg.o ../build/string.o ../build/syscall.o\
#   ../build/stdio.o ../build/assert.o -o prog_no_arg
#dd if=prog_no_arg of=/home/work/my_workspace/bochs/hd60M.img \
#   bs=512 count=10 seek=300 conv=notrunc

测试代码,(myos/kernel/main.c),注意:file_size这个变量请自行修改成自己的prog_no_arg大小,在command目录下ls -l即可查看prog_no_arg大小。我们前后会make all两次,第一次是为了让prog_no_arg有.o文件可以用,第二次是修改main.c以从hd60M.img中加载prog_no_arg到hd80M.img中

#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");
}

运行出错,经过排查,修改(myos/fs/fs.c/sys_getcwd)为

    if (child_inode_nr == 0)
    {
        buf[0] = '/';
        buf[1] = 0;
        return buf;
    }

    if (child_inode_nr == 0)
    {
        buf[0] = '/';
        buf[1] = 0;
        sys_free(io_buf);
        return buf;
    }

修改(myos/kernel/memory.c/get_a_page

... 
	if (cur->pgdir != NULL && pf == PF_USER)
    {
        bit_idx = (vaddr - cur->userprog_vaddr.vaddr_start) / PG_SIZE;
        ASSERT(bit_idx > 0);
        bitmap_set(&cur->userprog_vaddr.vaddr_bitmap, bit_idx, 1);
    }

...
    
    if (page_phyaddr == NULL)
        return NULL;
...

... 
	if (cur->pgdir != NULL && pf == PF_USER)
    {
        bit_idx = (vaddr - cur->userprog_vaddr.vaddr_start) / PG_SIZE;
        ASSERT(bit_idx >= 0);
        bitmap_set(&cur->userprog_vaddr.vaddr_bitmap, bit_idx, 1);
    }

...
    
    if (page_phyaddr == NULL)
    {
        lock_release(&mem_pool->lock);
        return NULL;
    }
...

小节h:

使用户进程支持传参

_start这个函数将会被execv执行,由它去代为执行真正的用户程序,因为这个函数会在链接用户程序前,成为真正的程序入口,这样可以做到给用户程序传参的目的。因为我们在sys_exec中有设定要开启的程序内核栈中的中断栈ebx,ecx的代码,intr_exit会将ebx中放入参数字符串指针数组的地址,ecx中放入参数个数,然后_start又将这两个寄存器入栈,而标准main函数的声明都是int mian(int argc, char** argv),所以main就可以自然在栈中找到自己要的参数。而且由于_start入栈传参符合main的函数声明,也就是从左到右依次入栈,所以这个main的运行无需而外设定,就可以正常运行,就像任何普通有参数的c语言函数一样。

myos/command/start.S

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

prog_arg.c中的main开启了个子进程,去执行argv[1]中指明的程序。

myos/commadn/prog_arg.c

#include "stdio.h"
#include "syscall.h"
#include "string.h"
int main(int argc, char **argv)
{
    int arg_idx = 0;
    while (arg_idx < argc)
    {
        printf("argv[%d] is %s\n", arg_idx, argv[arg_idx]);
        arg_idx++;
    }
    int pid = fork();
    if (pid)
    {
        int delay = 900000;
        while (delay--)
            ;
        printf("\n      I`m father prog, my pid:%d, I will show process list\n", getpid());
        ps();
    }
    else
    {
        char abs_path[512] = {0};
        printf("\n      I`m child prog, my pid:%d, I will exec %s right now\n", getpid(), argv[1]);
        if (argv[1][0] != '/')
        {
            getcwd(abs_path, 512);
            strcat(abs_path, "/");
            strcat(abs_path, argv[1]);
            execv(abs_path, argv);
        }
        else
        {
            execv(argv[1], argv);
        }
    }
    while (1)
        ;
    return 0;
}

处理prog_arg.c脚本(myos/command/compile.sh

相比于上一小节脚本,修改了要编译的程序BIN,包含头文件LIB,链接.o文件OBJS,编译start.S的命令,创建静态库命令,链接命令。

ar rcs simple_crt.a $OBJS start.o的解释:

  1. ar: 这是一个用来创建、修改和提取静态库的程序。静态库通常用于将多个目标文件(object files)打包成一个文件,这样在链接时就可以一次性链接多个目标文件。

  2. rcs:

    • r: 替换或添加指定的目标文件到库中。如果库中已经有了同名的目标文件,那么这个文件会被新文件替换。
    • c: 如果库文件不存在,那么创建一个新的库文件。
    • s: 创建目标文件的索引。这可以加速链接时的速度。
  3. simple_crt.a: 这是你想要创建或修改的静态库的名字。

  4. $OBJS start.o: 这是一个目标文件列表,将被添加或替换到静态库中。

所以,整个命令的意思是:将 $OBJSstart.o 中列出的所有目标文件添加或替换到 simple_crt.a 静态库中,并为这些目标文件创建一个索引。如果 simple_crt.a 还不存在,那么会创建一个新的静态库文件。

####  此脚本应该在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
CC="gcc-4.4"
BIN="prog_arg"
CFLAGS="-Wall -c -fno-builtin -W -Wstrict-prototypes \
    -Wmissing-prototypes -Wsystem-headers -m32 -fno-stack-protector"
LIBS="-I ../lib -I ../lib/user -I ../fs -I ../thread -I ../lib/kernel -I ../kernel"
OBJS="../build/string.o ../build/syscall.o \
      ../build/stdio.o ../build/assert.o start.o"
DD_IN=$BIN
DD_OUT="/home/rlk/Desktop/bochs/hd60M.img"

nasm -f elf ./start.S -o ./start.o
ar rcs simple_crt.a $OBJS start.o
$CC $CFLAGS $LIBS -o $BIN".o" $BIN".c"
ld $BIN".o" simple_crt.a -o $BIN -m elf_i386
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

##########   以上核心就是下面这三条命令   ##########
#gcc -Wall -c -fno-builtin -W -Wstrict-prototypes -Wmissing-prototypes \
#   -Wsystem-headers -I ../lib -o prog_no_arg.o prog_no_arg.c
#ld -e main prog_no_arg.o ../build/string.o ../build/syscall.o\
#   ../build/stdio.o ../build/assert.o -o prog_no_arg
#dd if=prog_no_arg of=/home/work/my_workspace/bochs/hd60M.img \
#   bs=512 count=10 seek=300 conv=notrunc

测试代码(myos/kernel/main.c

#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 = 20840;
    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_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");
}

prog_arg.c会与start.S中的代码编译后共同链接成一个新的程序,然后如同上一个小节一样,先写入裸盘hd60M.img,然后main.c中从裸盘加载新的应用程序prog_arg进入内存,然后写入有文件系统的hd80M.img。当我们启动操作系统,在shell中输入./prog_arg /prog_no_arg后,argv[0] = prog_arg, argv[1] = prog_no_arg。首先exec启动prog_arg(因为argv[0]才是要运行的程序,而后续的argv[1+n]是我们传递给这个程序的参数)。我们在sys_exec(exec的真正实现)中已经将程序要用的到参数字符串指针数组地址传递给了ebx,且_start才是prog_arg的真正入口,而_start中有一句push_ebx的代码,也就是说参数字符串指针数组地址已经传递给了prog_arg程序,自然prog_arg程序能够通过argv[1]去启动prog_no_arg

小节i:

进程终止与资源回收

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

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

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

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

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

孤儿进程:如果一个父进程在其子进程结束之前退出,那么这些子进程将被称为孤儿进程,也就是说没有父进程来回收他们的pcb资源。为了防止资源浪费,这些孤儿进程会被init进程“领养”,即成为init进程的子进程,由init来回收他们的pcb。

僵尸进程:当一个子进程终止,但其父进程没有调用wait来回收其资源时,此时这个子进程也无法过继给init,于是这个子进程就变成了僵尸进程。它们仍然占用pcb,但不执行任何操作。僵尸进程的存在可能会导致资源浪费。

pcb增加表示退出状态的成员,比如正常退出还是其他啥的。修改(myos/thread/thread.h

/* 进程或线程的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的边界
};

free_a_phy_page用于回收物理地址,实质就是回收了物理地址池位图对应的位。如此,这个物理地址下次就会被再次分配。

修改(myos/kernel/memory.c

/* 根据物理页框地址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);
}

添加函数声明,修改(myos/kernel/memory.h

void free_a_phy_page(uint32_t pg_phy_addr);

由于我们的进程在退出后要释放自己的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/thread.c

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

函数声明,修改(myos/thread/thread.h

void thread_exit(struct task_struct* thread_over, bool need_schedule);
struct task_struct* pid2thread(int32_t pid);
void release_pid(pid_t pid);

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

myos/userprog/wait_exit.c

#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++;
    }
}

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

修改(myos/userprog/wait_exit.c

/* 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继续传递下一个元素
}

find_hanging_child会被list_traversal调用,用于对比传入的all_list_tag指针对应任务的ppid是不是传入的ppid,且状态要是不是TASK_HANGING(进程没有完全退出就是这个状态)。此函数用于父进程来找到自己退出的子进程以回收它的剩余资源

修改(myos/userprog/wait_exit.c

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

init_adopt_a_child将传入的all_list_tag指针对应任务parent_pid改为1,也就是将一个子进程过继给init

修改(myos/userprog/wait_exit.c

/* 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继续传递下一个元素
}

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

修改(myos/userprog/wait_exit.c

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

#include "debug.h"

/* 子进程用来结束自己时调用 */
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_waitsys_exit封装成系统调用

添加系统调用号,修改(myos/lib/user/syscall.h

enum SYSCALL_NR {
   SYS_GETPID,
   SYS_WRITE,
   SYS_MALLOC,
   SYS_FREE,
   SYS_FORK,
   SYS_READ,
   SYS_PUTCHAR,
   SYS_CLEAR,
   SYS_GETCWD,
   SYS_OPEN,
   SYS_CLOSE,
   SYS_LSEEK,
   SYS_UNLINK,
   SYS_MKDIR,
   SYS_OPENDIR,
   SYS_CLOSEDIR,
   SYS_CHDIR,
   SYS_RMDIR,
   SYS_READDIR,
   SYS_REWINDDIR,
   SYS_STAT,
   SYS_PS,
   SYS_EXECV,
   SYS_EXIT,
   SYS_WAIT
};

创建用户态系统调用入口,修改(myos/lib/user/syscall.c

/* 以状态status退出 */
void exit(int32_t status)
{
    _syscall1(SYS_EXIT, status);
}

/* 等待子进程,子进程状态存储到status */
pid_t wait(int32_t *status)
{
    return _syscall1(SYS_WAIT, status);
}

声明,用户态系统调用函数入口,修改(myos/lib/user/syscall.h

void exit(int32_t status);
pid_t wait(int32_t* status);

系统调用表中,添加实际系统调用处理函数,修改(myos/userprog/syscall-init.c

#include "wait_exit.h"

/* 初始化系统调用 */
void syscall_init(void)
{
    put_str("syscall_init start\n");
    syscall_table[SYS_GETPID] = sys_getpid;
    syscall_table[SYS_WRITE] = sys_write;
    syscall_table[SYS_MALLOC] = sys_malloc;
    syscall_table[SYS_FREE] = sys_free;
    syscall_table[SYS_FORK] = sys_fork;
    syscall_table[SYS_READ] = sys_read;
    syscall_table[SYS_PUTCHAR] = sys_putchar;
    syscall_table[SYS_CLEAR] = cls_screen;
    syscall_table[SYS_GETCWD] = sys_getcwd;
    syscall_table[SYS_OPEN] = sys_open;
    syscall_table[SYS_CLOSE] = sys_close;
    syscall_table[SYS_LSEEK] = sys_lseek;
    syscall_table[SYS_UNLINK] = sys_unlink;
    syscall_table[SYS_MKDIR] = sys_mkdir;
    syscall_table[SYS_OPENDIR] = sys_opendir;
    syscall_table[SYS_CLOSEDIR] = sys_closedir;
    syscall_table[SYS_CHDIR] = sys_chdir;
    syscall_table[SYS_RMDIR] = sys_rmdir;
    syscall_table[SYS_READDIR] = sys_readdir;
    syscall_table[SYS_REWINDDIR] = sys_rewinddir;
    syscall_table[SYS_STAT] = sys_stat;
    syscall_table[SYS_PS] = sys_ps;
    syscall_table[SYS_EXECV] = sys_execv;
    syscall_table[SYS_EXIT] = sys_exit;
    syscall_table[SYS_WAIT] = sys_wait;
    put_str("syscall_init done\n");
}

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

修改(myos/command/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不会返回


cat用于读取文件内容,不是以系统调用的方式存在,而是以用户进程的方式存在。核心原理就是调用read系统调用,然后调用write系统调用来打印

myos/command/cat.c

#include "syscall.h"
#include "stdio.h"
#include "string.h"
int main(int argc, char **argv)
{
    if (argc > 2 || argc == 1)
    {
        printf("cat: only support 1 argument.\neg: cat filename\n");
        exit(-2);
    }
    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.c的脚本

myos/command/compile.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
CC="gcc-4.4"
BIN="cat"
CFLAGS="-Wall -c -fno-builtin -W -Wstrict-prototypes \
    -Wmissing-prototypes -Wsystem-headers -m32 -fno-stack-protector"
LIBS="-I ../lib/ -I ../lib/kernel/ -I ../lib/user/ -I \
      ../kernel/ -I ../device/ -I ../thread/ -I \
      ../userprog/ -I ../fs/ -I ../shell/"
OBJS="../build/string.o ../build/syscall.o \
      ../build/stdio.o ../build/assert.o start.o"
DD_IN=$BIN
DD_OUT="/home/rlk/Desktop/bochs/hd60M.img"

nasm -f elf ./start.S -o ./start.o
ar rcs simple_crt.a $OBJS start.o
$CC $CFLAGS $LIBS -o $BIN".o" $BIN".c"
ld $BIN".o" simple_crt.a -o $BIN -m elf_i386
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

现在我们有了能够退出程序的机制,终于不用再来用while(1)让程序卡住而不乱跳啦!

修改(myos/shell/shell.c/my_shell

        else
        { // 如果是外部命令,需要从磁盘上加载
            int32_t pid = fork();
            if (pid)
            { // 父进程
                /* 下面这个while必须要加上,否则父进程一般情况下会比子进程先执行,
                因此会进行下一轮循环将findl_path清空,这样子进程将无法从final_path中获得参数*/
                while (1)
                    ;
            }
            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]);
                }
                else
                {
                    execv(argv[0], argv);
                }
                while (1)
                    ;
            }
        }

        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]);
                }
                else
                {
                    execv(argv[0], argv);
                }
            }
        }

修改(myos/kernel/main.c),main函数要完成cat程序从hd60M.img到hd80M.img的加载,并且要退出;init进程不断调用wait来回收过继的僵尸进程资源

#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 = 21196;
    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.c", 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");
}

支持代码,删除(myos/userprog/fork.c/copy_pcb_vaddrbitmap_stack0

    ASSERT(strlen(child_thread->name) < 11); // pcb.name的长度是16,为避免下面strcat越界
    strcat(child_thread->name, "_fork");

小节j:

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

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

  1. 匿名管道:仅对创建它的进程及其子进程可见,其他进程无法访问。
  2. 有名管道:可以被系统中的所有进程访问。
    在这里插入图片描述
    我们的管道机制将会复用现有文件系统

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

修改(myos/device/ioqueue.c

/* 返回环形缓冲区中的数据长度 */
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;
}

函数声明,修改(myos/device/ioqueue.h

uint32_t ioq_length(struct ioqueue *ioq);

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

myos/shell/pipe.c

#include "pipe.h"
#include "stdint.h"
#include "global.h"
#include "file.h"
#include "fs.h"

/* 判断文件描述符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;
}

函数声明,(myos/shell/pipe.h

#ifndef __SHELL_PIPE_H
#define __SHELL_PIPE_H
#include "global.h"


#define PIPE_FLAG 0xFFFF



bool is_pipe(uint32_t local_fd);
#endif

支持代码,修改(myos/fs/fs.c

static uint32_t fd_local2global(uint32_t local_fd)

uint32_t fd_local2global(uint32_t local_fd)

并添加函数声明,修改(myos/fs/fs.h

uint32_t fd_local2global(uint32_t local_fd);

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

修改(myos/shell/pipe.c

#include "ioqueue.h"

/* 创建管道,成功返回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从中读取数据即可。

修改(myos/shell/pipe.c

/* 从管道中读数据 */
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;
}

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

修改(myos/shell/pipe.c

/* 往管道中写数据 */
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;
}

函数声明,修改(myos/shell/pipe.h

int32_t sys_pipe(int32_t pipefd[2]);
uint32_t pipe_read(int32_t fd, void *buf, uint32_t count);
uint32_t pipe_write(int32_t fd, const void *buf, uint32_t count);

sys_pipe做成系统调用

增加系统调用号,修改(myos/lib/user/syscall.h

enum SYSCALL_NR
{
    SYS_GETPID,
    SYS_WRITE,
    SYS_MALLOC,
    SYS_FREE,
    SYS_FORK,
    SYS_READ,
    SYS_PUTCHAR,
    SYS_CLEAR,
    SYS_GETCWD,
    SYS_OPEN,
    SYS_CLOSE,
    SYS_LSEEK,
    SYS_UNLINK,
    SYS_MKDIR,
    SYS_OPENDIR,
    SYS_CLOSEDIR,
    SYS_CHDIR,
    SYS_RMDIR,
    SYS_READDIR,
    SYS_REWINDDIR,
    SYS_STAT,
    SYS_PS,
    SYS_EXECV,
    SYS_EXIT,
    SYS_WAIT,
    SYS_PIPE
};

增加用户态系统调用入口,修改(myos/lib/user/syscall.c

/* 生成管道,pipefd[0]负责读入管道,pipefd[1]负责写入管道 */
int32_t pipe(int32_t pipefd[2])
{
    return _syscall1(SYS_PIPE, pipefd);
}

声明用户态系统调用函数,修改(myos/lib/user/syscall.h

int32_t pipe(int32_t pipefd[2]);

系统调用表中,增加实际系统调用处理函数,修改(myos/userprog/syscall-init.c

#include "pipe.h"

/* 初始化系统调用 */
void syscall_init(void)
{
    put_str("syscall_init start\n");
    syscall_table[SYS_GETPID] = sys_getpid;
    syscall_table[SYS_WRITE] = sys_write;
    syscall_table[SYS_MALLOC] = sys_malloc;
    syscall_table[SYS_FREE] = sys_free;
    syscall_table[SYS_FORK] = sys_fork;
    syscall_table[SYS_READ] = sys_read;
    syscall_table[SYS_PUTCHAR] = sys_putchar;
    syscall_table[SYS_CLEAR] = cls_screen;
    syscall_table[SYS_GETCWD] = sys_getcwd;
    syscall_table[SYS_OPEN] = sys_open;
    syscall_table[SYS_CLOSE] = sys_close;
    syscall_table[SYS_LSEEK] = sys_lseek;
    syscall_table[SYS_UNLINK] = sys_unlink;
    syscall_table[SYS_MKDIR] = sys_mkdir;
    syscall_table[SYS_OPENDIR] = sys_opendir;
    syscall_table[SYS_CLOSEDIR] = sys_closedir;
    syscall_table[SYS_CHDIR] = sys_chdir;
    syscall_table[SYS_RMDIR] = sys_rmdir;
    syscall_table[SYS_READDIR] = sys_readdir;
    syscall_table[SYS_REWINDDIR] = sys_rewinddir;
    syscall_table[SYS_STAT] = sys_stat;
    syscall_table[SYS_PS] = sys_ps;
    syscall_table[SYS_EXECV] = sys_execv;
    syscall_table[SYS_EXIT] = sys_exit;
    syscall_table[SYS_WAIT] = sys_wait;
    syscall_table[SYS_PIPE] = sys_pipe;
    put_str("syscall_init done\n");
}

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

修改(myos/fs/fs.c/sys_close

#include "pipe.h"

/* 关闭文件描述符fd指向的文件,成功返回0,否则返回-1 */
int32_t sys_close(int32_t fd)
{
    int32_t ret = -1; // 返回值默认为-1,即失败
    if (fd > 2)
    {
        uint32_t global_fd = fd_local2global(fd);
        if (is_pipe(fd))
        {
            /* 如果此管道上的描述符都被关闭,释放管道的环形缓冲区 */
            if (--file_table[global_fd].fd_pos == 0)
            {
                mfree_page(PF_KERNEL, file_table[global_fd].fd_inode, 1);
                file_table[global_fd].fd_inode = NULL;
            }
            ret = 0;
        }
        else
        {
            ret = file_close(&file_table[global_fd]);
        }
        running_thread()->fd_table[fd] = -1; // 使该文件描述符位可用
    }
    return ret;
}

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

修改(myos/fs/fs.c/sys_write

/* 将buf中连续count个字节写入文件描述符fd,成功则返回写入的字节数,失败返回-1 */
int32_t sys_write(int32_t fd, const void *buf, uint32_t count)
{
    if (fd < 0)
    {
        printk("sys_write: fd error\n");
        return -1;
    }
    if (fd == stdout_no)
    {
        /* 标准输出有可能被重定向为管道缓冲区, 因此要判断 */
        if (is_pipe(fd))
        {
            return pipe_write(fd, buf, count);
        }
        else
        {
            char tmp_buf[1024] = {0};
            memcpy(tmp_buf, buf, count);
            console_put_str(tmp_buf);
            return count;
        }
    }
    else if (is_pipe(fd))
    { /* 若是管道就调用管道的方法 */
        return pipe_write(fd, buf, count);
    }
    else
    {
        uint32_t _fd = fd_local2global(fd);
        struct file *wr_file = &file_table[_fd];
        if (wr_file->fd_flag & O_WRONLY || wr_file->fd_flag & O_RDWR)
        {
            uint32_t bytes_written = file_write(wr_file, buf, count);
            return bytes_written;
        }
        else
        {
            console_put_str("sys_write: not allowed to write file without flag O_RDWR or O_WRONLY\n");
            return -1;
        }
    }
}

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

修改(myos/fs/fs.c/sys_read

/* 从文件描述符fd指向的文件中读取count个字节到buf,若成功则返回读出的字节数,到文件尾则返回-1 */
int32_t sys_read(int32_t fd, void *buf, uint32_t count)
{
    ASSERT(buf != NULL);
    int32_t ret = -1;
    uint32_t global_fd = 0;
    if (fd < 0 || fd == stdout_no || fd == stderr_no)
    {
        printk("sys_read: fd error\n");
    }
    else if (fd == stdin_no)
    {
        /* 标准输入有可能被重定向为管道缓冲区, 因此要判断 */
        if (is_pipe(fd))
        {
            ret = pipe_read(fd, buf, count);
        }
        else
        {
            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 if (is_pipe(fd))
    { /* 若是管道就调用管道的方法 */
        ret = pipe_read(fd, buf, count);
    }
    else
    {
        global_fd = fd_local2global(fd);
        ret = file_read(&file_table[global_fd], buf, count);
    }
    return ret;
}

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

修改(myos/userprog/fork.c/update_inode_open

#include "pipe.h"

/* 更新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)
        {
            if (is_pipe(local_fd))
            {
                file_table[global_fd].fd_pos++;
            }
            else
            {
                file_table[global_fd].fd_inode->i_open_cnts++;
            }
        }
        local_fd++;
    }
}

release_prog_resource增加程序退出时对于打开的管道文件资源的处理代码,原理与sys_close增加的代码一样

修改(myos/userprog/wait_exit.c/release_prog_resource

#include "pipe.h"
#include "file.h"

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)
        {
            if (is_pipe(local_fd))
            {
                uint32_t global_fd = fd_local2global(local_fd);
                if (--file_table[global_fd].fd_pos == 0)
                {
                    mfree_page(PF_KERNEL, file_table[global_fd].fd_inode, 1);
                    file_table[global_fd].fd_inode = NULL;
                }
            }
            else
            {
                sys_close(local_fd);
            }
        }
        local_fd++;
    }
}

测试管道的用户进程(myos/command/prog_pipe.c

#include "stdio.h"
#include "syscall.h"
#include "string.h"
#include "stdint.h"
int main(int argc, char **argv)
{
    int32_t fd[2] = {-1};
    pipe(fd);
    int32_t pid = fork();
    if (pid)
    {                 // 父进程
        close(fd[0]); // 关闭输入
        write(fd[1], "Hi, my son, I love you!", 24);
        printf("\nI`m father, my pid is %d\n", getpid());
        return 8;
    }
    else
    {
        close(fd[1]); // 关闭输出
        char buf[32] = {0};
        read(fd[0], buf, 24);
        printf("\nI`m child, my pid is %d\n", getpid());
        printf("I`m child, my father said to me: \"%s\"\n", buf);
        return 9;
    }
}

处理prog_pipe的脚本

####  此脚本应该在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
CC="gcc-4.4"
BIN="prog_pipe"
CFLAGS="-Wall -c -fno-builtin -W -Wstrict-prototypes \
    -Wmissing-prototypes -Wsystem-headers -m32 -fno-stack-protector"
LIBS="-I ../lib/ -I ../lib/kernel/ -I ../lib/user/ -I \
      ../kernel/ -I ../device/ -I ../thread/ -I \
      ../userprog/ -I ../fs/ -I ../shell/"
OBJS="../build/string.o ../build/syscall.o \
      ../build/stdio.o ../build/assert.o start.o"
DD_IN=$BIN
DD_OUT="/home/rlk/Desktop/bochs/hd60M.img"

nasm -f elf ./start.S -o ./start.o
ar rcs simple_crt.a $OBJS start.o
$CC $CFLAGS $LIBS -o $BIN".o" $BIN".c"
ld $BIN".o" simple_crt.a -o $BIN -m elf_i386
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

测试代码,用于将prog_pipe从hd60M.img加载到hd80M.img中

#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 = 21432;
    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_pipe", 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");
}

小节k:

在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理解)

修改(myos/shell/pipe.c

/* 将文件描述符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;
    }
}

函数声明,修改(myos/shell/pipe.h

void sys_fd_redirect(uint32_t old_local_fd, uint32_t new_local_fd);

sys_help打印系统支持的内部命令

修改(myos/fs/fs.c

/* 显示系统支持的内部命令 */
void sys_help(void)
{
    printk("\
 buildin commands:\n\
       ls: show directory or file information\n\
       cd: change current work directory\n\
       mkdir: create a directory\n\
       rmdir: remove a empty directory\n\
       rm: remove a regular file\n\
       pwd: show current work directory\n\
       ps: show process information\n\
       clear: clear screen\n\
 shortcut key:\n\
       ctrl+l: clear screen\n\
       ctrl+u: clear input\n\n");
}

添加声明,修改(myos/fs/fs.h

void sys_help(void);

sys_fd_redirectsys_help做成系统调用

添加系统调用号,修改(myos/lib/user/syscall.h

enum SYSCALL_NR
{
    SYS_GETPID,
    SYS_WRITE,
    SYS_MALLOC,
    SYS_FREE,
    SYS_FORK,
    SYS_READ,
    SYS_PUTCHAR,
    SYS_CLEAR,
    SYS_GETCWD,
    SYS_OPEN,
    SYS_CLOSE,
    SYS_LSEEK,
    SYS_UNLINK,
    SYS_MKDIR,
    SYS_OPENDIR,
    SYS_CLOSEDIR,
    SYS_CHDIR,
    SYS_RMDIR,
    SYS_READDIR,
    SYS_REWINDDIR,
    SYS_STAT,
    SYS_PS,
    SYS_EXECV,
    SYS_EXIT,
    SYS_WAIT,
    SYS_PIPE,
    SYS_FD_REDIRECT,
    SYS_HELP
};

实现用户态系统调用入口,修改(myos/lib/user/syscall.c

/* 将文件描述符old_local_fd重定向到new_local_fd */
void fd_redirect(uint32_t old_local_fd, uint32_t new_local_fd)
{
    _syscall2(SYS_FD_REDIRECT, old_local_fd, new_local_fd);
}

/* 显示系统支持的命令 */
void help(void)
{
    _syscall0(SYS_HELP);
}

声明用户态系统调用函数,修改(myos/lib/user/syscall.h

void fd_redirect(uint32_t old_local_fd, uint32_t new_local_fd);
void help(void);

系统调用表添加实际系统调用处理函数,修改(myos/userprog/syscall-init.c

/* 初始化系统调用 */
void syscall_init(void)
{
    put_str("syscall_init start\n");
    syscall_table[SYS_GETPID] = sys_getpid;
    syscall_table[SYS_WRITE] = sys_write;
    syscall_table[SYS_MALLOC] = sys_malloc;
    syscall_table[SYS_FREE] = sys_free;
    syscall_table[SYS_FORK] = sys_fork;
    syscall_table[SYS_READ] = sys_read;
    syscall_table[SYS_PUTCHAR] = sys_putchar;
    syscall_table[SYS_CLEAR] = cls_screen;
    syscall_table[SYS_GETCWD] = sys_getcwd;
    syscall_table[SYS_OPEN] = sys_open;
    syscall_table[SYS_CLOSE] = sys_close;
    syscall_table[SYS_LSEEK] = sys_lseek;
    syscall_table[SYS_UNLINK] = sys_unlink;
    syscall_table[SYS_MKDIR] = sys_mkdir;
    syscall_table[SYS_OPENDIR] = sys_opendir;
    syscall_table[SYS_CLOSEDIR] = sys_closedir;
    syscall_table[SYS_CHDIR] = sys_chdir;
    syscall_table[SYS_RMDIR] = sys_rmdir;
    syscall_table[SYS_READDIR] = sys_readdir;
    syscall_table[SYS_REWINDDIR] = sys_rewinddir;
    syscall_table[SYS_STAT] = sys_stat;
    syscall_table[SYS_PS] = sys_ps;
    syscall_table[SYS_EXECV] = sys_execv;
    syscall_table[SYS_EXIT] = sys_exit;
    syscall_table[SYS_WAIT] = sys_wait;
    syscall_table[SYS_PIPE] = sys_pipe;
    syscall_table[SYS_FD_REDIRECT] = sys_fd_redirect;
    syscall_table[SYS_HELP] = sys_help;
    put_str("syscall_init done\n");
}

help封装成内建命令,修改(myos/shell/buildin_cmd.c

/* 显示内建命令列表 */
void buildin_help(uint32_t argc UNUSED, char **argv UNUSED)
{
    help();
}

声明,修改(myos/shell/buildin_cmd.h

void buildin_help(uint32_t argc UNUSED, char **argv UNUSED);

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

修改(myos/shell/shell.c

/* 执行命令 */
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. 创建一个管道。
    2. 重定向标准输出到管道的写端。
    3. 解析并执行第一个命令。
    4. 重定向标准输入到管道的读端。
    5. 对于每一个中间的命令(除了最后一个):
      • 解析并执行命令。
    6. 恢复标准输出到屏幕。
    7. 执行管道中的最后一个命令。
    8. 恢复标准输入为键盘。
    9. 关闭管道。
  • 如果没有管道命令:
    1. 解析用户输入的命令。
    2. 如果参数数量超过了设定的最大值,则提示错误。
    3. 否则执行命令。

修改(myos/shell/shell.c

#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");
}

cat新增无参数时从键盘获得输入(记得删除原有hd80M.img中的cat)

myos/command/cat.c

#include "syscall.h"
#include "stdio.h"
#include "string.h"
#include "fs.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的脚本

####  此脚本应该在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
CC="gcc-4.4"
BIN="cat"
CFLAGS="-Wall -c -fno-builtin -W -Wstrict-prototypes \
    -Wmissing-prototypes -Wsystem-headers -m32 -fno-stack-protector"
LIBS="-I ../lib/ -I ../lib/kernel/ -I ../lib/user/ -I \
      ../kernel/ -I ../device/ -I ../thread/ -I \
      ../userprog/ -I ../fs/ -I ../shell/"
OBJS="../build/string.o ../build/syscall.o \
      ../build/stdio.o ../build/assert.o start.o"
DD_IN=$BIN
DD_OUT="/home/rlk/Desktop/bochs/hd60M.img"

nasm -f elf ./start.S -o ./start.o
ar rcs simple_crt.a $OBJS start.o
$CC $CFLAGS $LIBS -o $BIN".o" $BIN".c"
ld $BIN".o" simple_crt.a -o $BIN -m elf_i386
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

测试代码,(myos/kernel/main.c

#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 = 21816;
    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");
}

运行出错,排查后需要修改(myos/device/ioqueue.h

#define bufsize 64 //定义缓冲区大小.

#define bufsize 2048 //定义缓冲区大小.
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
WPF(Windows Presentation Foundation)是微软推出的一种用于创建富客户端应用程序的技术框架。而CAD(计算机辅助设计)交互系统是一种用于创建、编辑和管理CAD模型的软件。 WPF的CAD交互系统是指基于WPF技术开发的用于实现CAD设计和编辑功能的软件系统。通过使用WPF的强大图形渲染功能和丰富的用户界面控件,CAD交互系统能够提供更直观、灵活和易用的CAD设计体验。 首先,WPF的CAD交互系统可以利用WPF的矢量图形渲染引擎,在界面上快速准确地显示复杂的CAD模型。用户可以自由旋转、缩放和平移模型,同时还可以应用灯光、纹理等效果,使得设计效果更加真实。 其次,WPF的CAD交互系统提供了丰富的用户界面控件,如按钮、工具栏、下拉菜单等,使得用户可以方便地选择各种设计和编辑操作。例如,用户可以通过点击按钮选择绘图工具,通过拖动鼠标绘制图形,通过调整参数进行编辑等等。 此外,WPF的CAD交互系统还支持用户输入的实时响应和交互反馈。用户可以自由绘制线条、多边形、曲线等等,并通过动态捕捉、智能对象和自动修复等功能,完成精确的CAD设计任务。 综上所述,WPF的CAD交互系统利用WPF技术的优势,提供了强大的图形渲染功能和丰富的用户界面控件,实现了更加直观、灵活和易用的CAD设计和编辑体验。无论是专业的工程师还是普通用户,都能够快速上手并完成高质量的CAD设计工作。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值