《操作系统-真象还原》15. 系统交互

news2024/12/23 6:02:46

文章目录

    • fork 的原理与实现
      • 简介
      • 什么是 fork
      • fork 的实现
        • 思路
        • 代码
          • get_a_page_without_opvaddrbitmap
          • copy_pcb_vaddrbitmap_stack0
          • copy_body_stack3
          • build_child_stack
          • update_inode_open_cnts
          • copy_process
          • sys_fork
      • 添加 fork 系统调用与实现 init 进程
        • 添加 fork 系统调用
        • 实现 init 进程
    • shell 的前置准备工作
    • 实现一个简单的 shell
      • readline / print_prompt 函数
      • 添加 Ctrl+u 和 Ctrl+l 快捷键
      • cmd_str 分割输入的指令,相对于高级语言中字符串的 split 函数。
      • 解析路径
      • 实现 ls、c、mkdir、ps、rm 等指令
      • shell 主程序
    • 现在 Shell 的问题

fork 的原理与实现

简介

pid_t fork( void);

pid_t 是一个宏定义,其实质是int 被定义在 #includesys/types.h> 中。

返回值: 若成功调用一次则返回两个值,其返回值的意义如下

  • 0:子进程

  • 子进程ID,即 >0:父进程

  • -1:错误

fork 的另一个特性是所有由父进程打开的描述符都被复制到子进程中。父、子进程中相同编号的文件描述符在内核中指向同一个file结构体,也就是说,file结构体的引用计数要增加。(相关函数请看 fork 的实现 -> 思路

什么是 fork

#include <unistd.h>
#include <stdio.h>

int main() {
    printf("I will fork in 5 seconds.\n");
    sleep(5);
    int pid = fork();
    if(pid == -1) {
        printf("fork error!\n");
        return 1;
    }
    if(pid) {
        printf("[fork->pid=%d] I am father, my pid is %d\n.", pid, getpid());
        sleep(5);
        return 0;
    } else {
        printf("[fork->pid=%d] I am child, my pid is %d\n.", pid, getpid());
        sleep(5);
        return 0;
    }
}

需要打开两个终端,一个用于运行程序,一个用于查看进程信息。
终端一:运行测试程序

image-20230107152613896

终端二:查看进程信息

image-20230107152648429

一开始并无相关进程信息,接下来执行测试程序,首先执行父进程,得到 pid=8917,休眠 5s 后执行 fork() 函数,之后会发现输出了两条语句,也就是说 fork 返回一次返回了两个不同的返回值,为什么呢?

看进程信息,fork 后多了一个“子进程”,其 pid=8932,也就是说 fork 其实是将父进程克隆了一份,而进程拥有独立的地址空间,因此两个进程执行的是独立且相同的代码,所以它们并不共享同一内存空间,也就是执行的是两套代码(代码是相同的,只是有两套而已)。

看输出的结果,是先执行的父进程,其 fork 返回父进程的 pid,其次执行的才是子进程,其子进程 fork 返回的是 0。

注意,这里并不是父子进程各调用一次,父子进程合计才调用了一次 fork,也就是说执行一次 fork 会返回两个值,只是返回的地方不一样,一次是在父进程,一次是在父进程

fork 的实现

思路

前面说明了 fork 本质是将父进程克隆了一份,称为子进程。首先我们需要明确要复制的资源,复制完成后让处理器的 cs:eip 指向新进程的指令部分。
实现 fork 分两步:

  1. 先复制进程资源。
  2. 让处理器的 cs:eip 指向子进程的指令部分(也就是程序代码部分)。

明确要复制的资源有:

  1. 进程的 PCB,即 task_struct。相关函数:copy_pcb_vaddrbitmap_stack0
  2. 程序体,即代码段、数据段等,这是进程的实体。相关函数:copy_body_stack3
  3. 用户栈,编译器会把局部变量存入栈,而调用函数也需要压栈执行。相关函数:copy_body_stack3
  4. 内核栈,进入内核态时,一方面要用它来保护上下文环境,而另一方面和用户栈一样。相关函数:copy_pcb_vaddrbitmap_stack0
  5. 虚拟地址池,每个进程都拥有独立的内存空间,其虚拟地址是虚拟地址池来管理的。相关函数:copy_pcb_vaddrbitmap_stack0
  6. 页表,让进程拥有独立的内存空间。相关函数:create_page_dir

克隆后的进程要如何执行:将新进程加入到就绪队列就可以了,当然要提前把相关的栈准备好。相关函数:build_child_stack

注意:复制完 PCB 后,要记得文件描述符所对应打开的文件的次数要增加。相关函数:update_inode_open_cnts

代码

get_a_page_without_opvaddrbitmap

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

userprog/fork.c

// 将父进程的 PCB、虚拟地址位图拷贝给子进程
static int32_t copy_pcb_vaddrbitmap_stack0(struct task_struct* child_thread, struct task_struct* parent_thread) {
    // 复制 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);
    // 复制父进程的虚拟内存池位图
    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 其实也可以换成 parent_thread
    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
// 复制子进程的进程体(代码和数据)以及用户栈
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_byte_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_byte_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;

                    // 将父进程所在用户空间中的数据复制到内核缓冲区 buf_page
                    // 目的是下面切换到子进程的页表后,还能访问到父进程的数据
                    memcpy(buf_page, (void*) prog_vaddr, PG_SIZE);

                    // 将页表切换到子进程,目的是避免下面申请内存的函数将 pte 及 pde 安装到父进程的页表中
                    page_dir_activate(child_thread);
                    // 申请虚拟地址 prog_vaddr
                    get_a_page_without_opvaddrbitmap(PF_USER, prog_vaddr);

                    // 从内核缓冲区中将父进程数据复制到子进程的用户空间中
                    memcpy((void*) prog_vaddr, buf_page, PG_SIZE);

                    // 恢复父进程页表
                    page_dir_activate(parent_thread);
                }
                idx_bit++;
            }
        }
        idx_byte++;
    }
}

将父进程用户空间中的数据复制到子进程的用户空间。但各用户进程的低3G空间是独立的,因此用户进程不能互相访问彼此的空间,但高1G是内核空间,内核空间是所有用户共享的,因此要把数据从一个进程拷贝到另一个进程,必须要借助内核空间作为数据中转,即先父进程用户空间的数据先复制到位于内核空间的 buf_page 中,最后再将 buf_page 复制到子进程的用户空间中。

这里采用一页一页对拷的形式,即父进程找到一页,子进程就申请一页空间,然后对拷。但不同进程之所以有单独的虚拟地址空间,是因为它们各自有单独的页目录,我们分配内存时,会在页表中产生新的 PTE,若申请的内存跨越 4MB 的页表大小,则还需要在页目录表中创建 PDE,既然是为子进程分配内存,那么就要保证这些 PTE 和 PDE 是创建在子进程的页目录表中的。所以在将 buf_page 的数据拷贝到子进程之前,一定要将页表替换为子进程的页表。

build_child_stack
// 为子进程构建 thread_stack 和修改返回值
static int32_t build_child_stack(struct task_struct* child_thread) {
    // -----------------------
    // 使子进程 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;

    // 为 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
// 更新 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_cnt++;
        local_fd++;
    }
}
copy_process
// 拷贝父进程本身所占资源给子进程(对前面函数的封装罢了)
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;

    // 复制父进程的 PCB、虚拟地址位图、内核栈到子进程
    if(copy_pcb_vaddrbitmap_stack0(child_thread, parent_thread) == -1) return -1;

    // 为子进程创建页表,此页表仅包含内核空间
    child_thread -> pgdir = create_page_dir();
    if(child_thread -> pgdir == NULL) return -1;

    // 复制父进程的进程体及用户栈给子进程
    copy_body_stack3(child_thread, parent_thread, buf_page);

    // 构建子进程的 thread_stack 和修改返回值 pid
    build_child_stack(child_thread);

    // 更新文件(inode)被打开的次数
    update_inode_open_cnts(child_thread);

    mfree_page(PF_KERNEL, buf_page, 1);

    return 0;
}
sys_fork
// fork 子进程,内核线程不可直接调用
pid_t sys_fork(void) {
    struct task_struct* parent_thread = running_thread();
    struct task_struct* child_thread = get_kernel_pages(1);
    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
}

添加 fork 系统调用与实现 init 进程

添加 fork 系统调用

这个没啥好说,老套路走一遍就行。

实现 init 进程

在 Linux 中,init 是用户级进程,它是第一个启动的程序,因此它的 pid=1,后续的所有进程都是它的孩子,故 init 是所有进程父进程,所以它还负责所有子进程的资源回收。

既然 init 是父进程,也就是说它要主动调用 fork 才能派生出子子孙孙,所以在实现它之前要先实现 fork 系统调用。

kernel/main.c

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

thread/thread.c

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"); // 由于是第一个创建的进程,因此该进程 pid = 1

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

    // 创建 idle 线程
    idle_thread = thread_start("idle", 10, idle, NULL);
    put_str("thead_init done\n");
}

shell 的前置准备工作

三个新的j基础系统调用:

  • read 系统调用,获取键盘输入。
  • putchar 系统调用,输出字符。
  • clear 系统调用,清空屏幕。

其中 clear 的内核实现是 cls_screen,采用纯汇编实现,代码位于:kernel/print.S

这里就贴出 read 的内核实现函数:

fs/fs.c

// 从文件描述符 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(&kdb_buf); // 从键盘缓冲区 kdb_buf 中读取1个字节
            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;
}

若干个系统操作,添加完后总共有:

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 // ps 指令
};

ps 指令的实现:

  • 思路:遍历所有进程队列 thread_all_list,输出 PCB,即 task_struct 中的相关信息。
  • 代码位于:thread/thread.c
  • 相关函数:pad_print 填充函数(控制输出的格式),elem2thread_info 核心函数,sys_ps 对 ele2thread_info 函数的封装。

实现一个简单的 shell

操作系统如果想和用户交互,那么就必须知道用户的输入,知道了输入那么就要做出相应的输出,此乃交互。各种操作系统的交互方式不过只是提供了一个“外壳”供用户操作,Window 下有 GUI 图形化界面和命令行窗口,而 Linux 则是通过命令行的形式和用户交互,只是 Linux 的叫法更加直接,就直接称为“Shell”(外壳的英文)。

readline / print_prompt 函数

shell/shell.c

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

// 从键盘缓冲区中最多读入 count 个字节到 buf
static void readline(char* buf, int32_t count) {
    assert(buf != NULL && count > 0);
    char* pos = buf;
    while(read(stdin_no, pos, 1) != -1 && (pos - buf) < count) {
        switch(*pos) {
            // 清空屏幕,保留当前输入
            case 'l' - 'a':
                *pos = 0; // 重置
                clear(); // 清空屏幕
                print_prompt(); // 重新打印提示符
                printf("%s", buf); // 重新将之前输入的命令再次打印
                break;
            // 清空当前输入
            case 'u' - 'a':
                while(buf != pos) {
                    putchar('\b');
                    *(pos--) = 0;
                }
                break;
            case '\n':
            case '\r':
                *pos = 0;
                putchar('\n');
                return;
            case '\b':
                if(buf[0] != '\b') {
                    --pos;
                    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");
}

添加 Ctrl+u 和 Ctrl+l 快捷键

Ctrl+u:清空屏幕,但保留正在输入的指令。

Ctrl+l:清空当前输入。

简单说一下,两者的实现都是 u-al-a 各自所得差形成的属于 ASC 码表 中的不可见字符,因此不会产生可见字符,利用这个特点作为快捷键。

cmd_str 分割输入的指令,相对于高级语言中字符串的 split 函数。

例如:ls /opt -a 解析完后变成:[ls, /opt, -a]

shell/shell.c

// 将 cmd_str 字符串以 token 字符分割
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 dir  */
        if(*next == 0) break;
        argv[argc] = next;
        // 获取整个有效参数
        while(*next && *next != token) next++;
        // 若未结束
        if(*next) *next++ = 0; // 则以字符串结束符0来表示一个单词的结束
        // 避免越界
        if(argc > MAX_ARG_NR) return -1;
        argc++; // 下一个参数
    }
    return argc;
}

解析路径

我们经常在终端中使用相对路径 ...,我们需要将这些路径转换成绝对路径 /**/**,这样便于我们操作。

shell/buildin_cmd.c

// 将路径 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[0] = '/';
        new_abs_path[1] = 0;
        return;
    }

    new_abs_path[0] = 0;
    strcat(new_abs_path, "/");
    while(name[0]) {
        if(!strcmp("..", name)) { // 若解析出来的目录是 ..
            char* slash_ptr = strrchr(new_abs_path, '/');
            if(slash_ptr != new_abs_path) { // 若 .. 后还未到达顶层,例如 /a/b .. 后为 /a
                *slash_ptr = 0;
            } else { // 若 .. 后到达了顶层,例如 /a .. 后为 /
                *(slash_ptr + 1) = 0;
            }
        } else if(strcmp(".", name)) { // 若解析出来的目录不是 .
            if(strcmp(new_abs_path, "/")) { // 判断顶层是否为 /
                strcat(new_abs_path, "/"); // 不是,则追加,这个判断是为了避免开头变成 // 的情况
            }
            // 追加目录
            strcat(new_abs_path, name);
        } // 若解析出来的目录是 . 则无需任何操作

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

// 将 path 处理成不含 .. 和 . 的绝对路径,保存到 final_path 中,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))) {
                strcat(abs_path, "/");
            }
        }
    }
    // 将键入的路径 path 拼接到当前层的绝对路径后面
    strcat(abs_path, path);
    // 将 abs_path 中的 . or .. 转为不含 . or .. 的绝对路径 final_path
    wash_path(abs_path, final_path);
}

实现 ls、c、mkdir、ps、rm 等指令

命令分为两类:外部命令、内部命令。

外部命令:存储在文件系统上的外部程序,执行该命令实际上是从文件系统上加载该程序到内存中运行,也就是说外部命令会以进程的方式执行。例如:ls,存储路径为 /bin/ls

内部命令(内建命令):是系统本身提供的功能,并不以单独的程序文件存在,只是一些单独功能的函数,执行内部命令实际上就是调用这些函数。例如:cd、fg、jobs 等命令都是由 bash 提供的,因此它们称为 BASH_BUILTINS。

**内部命令的编写规则: **

  1. 命名方式:前缀 buildin_ + 命令名
  2. 形参均是 argc 和 argv,argv 是数组,其 argc 是数组长度,即命令参数的个数。
  3. 函数实现是调用同功能的系统调用实现的,如 buildin_cd 是调用系统调用 chdir 完成的。
  4. 在进行系统调用前,调用函数 make_clear_abs_path 把相对路径转为绝对路径。

这里就只贴出 ls 和 ps 的构建函数 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 { // 得到路径参数值
            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) { // 若没有给明确的路径,则默认当前路径为路径参数
        if(getcwd(final_path, MAX_PATH_LEN) != NULL) {
            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);

        // 保证路径为 /a/b/c/ 而不是 /a/b/c 这是为了便于后面 stat 读取
        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);
    }
}
// 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)) return ret; // 不能创建根目录
        if(mkdir(final_path) == 0) 
            ret = 0;
        else
            printf("mkdir: create directory %s failed.\n", argv[1]);
    }
    return ret;
}
// ps 命令内建函数
void buildin_ps(uint32_t argc, char** argv UNUSED) {
    if(argc != 1) {
        printf("ps: no argument support!\n");
        return;
    }
    ps(); // 系统调用,内核实现是 sys_ps,位于 thread/thread.c
}

shell 主程序

// 存储输入的命令
static char cmd_line[MAX_PATH_LEN] = {0};
char final_path[MAX_PATH_LEN] = {0}; // 用于洗路径时的缓冲

// 记录当前操作的所在目录,每次 cd 都会更新这个路径
char cwd_cache[MAX_PATH_LEN] = {0};

char* argv[MAX_ARG_NR]; // argv 必须为全局变量,为了以后 exec 的程序可以访问到参数
int32_t argc = -1; // 参数个数

...

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

现在 Shell 的问题

看!我们通过 if-else 的形式来判断用户输入命令,若每个命令都要这样判断,工作量大不说,我们的外部命令也无法执行,因为我们的外部命令的命令名是未知的,这是无法预判的。

或许这可以通过 exec 解决?

但…我无法继续走下去了,因为…

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/147238.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

计算机网络复习之DL层(数据链路层)与LAN(Local Area Network局域网)

文章目录封装成帧透明传输字符计数法字符填充法零比特填充法违规编码法差错控制透明网桥网桥自学习转发表网桥的自学习和转发帧的步骤透明网桥工作原理交换机/路由器的广播域、冲突域冲突域广播域交换机/路由器的两域区别CSMA/CD协议PPP协议HDLC协议PPP和HDLC的对比简述IEEE802…

2. 数据类型、向量、向量索引、向量修改、向量运算

课程视频链接&#xff1a;https://www.bilibili.com/video/BV19x411X7C6?p1 本笔记参照该视频&#xff0c;笔记顺序做了些调整【个人感觉逻辑顺畅】&#xff0c;并删掉一些不重要的内容 系列笔记目录【持续更新】&#xff1a;https://blog.csdn.net/weixin_42214698/category_…

【学习】Q learning、Q-learning for continuous actions、关于深度学习的猜想

文章目录一、Q learning评估状态值函数Vπ(s)MCTDdouble DQNdueling DQNprioritized replymulti-stepnoisy netdistributionalrainbow二、Q-learning for continuous actions三、关于深度学习的猜想一、Q learning value-based的方法&#xff0c;评论家不会直接决定行动。给定…

低度酒“百家争鸣”,谁能俘获年轻人的芳心?

【潮汐商业评论/原创】随着消费升级和女性消费力量的崛起&#xff0c;“她经济”逐渐成为新零售消费环境下一道“靓丽风景线”。女性消费者的消费偏好和消费习惯&#xff0c;正在促使低度酒、食品、服装、护肤、美妆、家居、育儿等行业发生新的变革。特别是酒水市场&#xff0c…

Spring Boot学习篇(八)

Spring Boot学习篇(八) 1.thymeleaf模板引擎使用篇(二) 1.1 配置通过地址栏直接访问html页面 1.1.1 在zlz包下创建filter包,其目录结构如下所示 1.1.2 在filter包下创建HTMLFilter类,其内容如下所示 package com.zlz.filter;import javax.servlet.*; import javax.servlet.…

启用分页机制

前言 本博客记录《操作系统真象还原》第五章第2个实验的操作~ 实验环境&#xff1a;ubuntu18.04VMware &#xff0c; Bochs下载安装 实验内容&#xff1a;启动内存分页机制 实验原理&#xff1a;内存分页机制 前置知识 前置知识可食用内存分页机制 代码 include/boot.in…

UML类图入门实战

介绍 UML——Unified modeling language UML (统一建模语言)&#xff0c;是一种用于软件系统分析和设计的语言工具&#xff0c;它用于帮助软件开发人员进行思考和记录思路的结果。 UML 本身是一套符号的规定&#xff0c;就像数学符号和化学符号一样&#xff0c;这些符号用于描述…

设计模式学习(二):Adapter适配器模式

一、什么是Adapter模式我们先举个例子&#xff1a;如果想让额定工作电压是直流12V的笔记本电脑在交流220V的电源下工作&#xff0c;应该怎么做呢?通常&#xff0c;我们会使用适配器&#xff0c;将家庭用的交流220V电压转换成我们所需要的直流12V电压。这就是适配器的工作&…

Hive数据仓库简介与安装

文章目录Hive数据仓库简介及安装配置一、数据仓库简介1. 什么是数据仓库2. 数据仓库的结构1&#xff09;数据源2&#xff09;数据存储与管理3&#xff09;OLAP服务器4&#xff09;前端工具3. 数据仓库的数据模型1&#xff09;星状模型2&#xff09;雪花模型二、Hive简介1. 什么…

【踩坑总结】解决pycharm下载依赖一直失败的问题

目录前言正文问题复现问题本质解决方案补充总结检查是否安装成功下载的依赖存在哪总结前言 pycharm下载依赖失败这个问题对于我来说已经是个老生常谈的问题&#xff0c;与之共交手三次。 首次交锋是在大二利用 树莓派 做图像采集传输时&#xff0c;在树莓派的ubantu上使用pyt…

docker 容器使用 loki 插件收集日志

相关资料: The log-opts in the /etc/docker/daemon.json is not relaoded - General Discussions - Docker Community ForumsRecently I want to use loki-log-driver to ship logs to the loki server. The docker-deamon is controlled by systemd. The /etc/docker/daemon…

Day2 Spring

1 BeanFactory 与ApplicationContext的关系BeanFactory与ApplicationContext的关系BeanFactory是Spring的早期接口&#xff0c;称为Spring的Bean工厂&#xff0c;ApplicationContext是后期更高级接口&#xff0c;称之为Spring 容器;ApplicationContext在BeanFactory基础上对功能…

Pytorch 多层感知机

一、什么是多层感知机 多层感知机由感知机推广而来&#xff0c;最主要的特点是有多个神经元层&#xff0c;因此也叫深度神经网络(DNN: Deep Neural Networks)。 二、如何实现多层感知机 1、导入所需库并加载fashion_mnist数据集 %matplotlib inline import torch import to…

java弹幕视频网站源码

简介 Java基于ssm的弹幕视频系统&#xff0c;用户注册后可以上传视频进行投稿&#xff0c;也可以浏览视频发送弹幕&#xff0c;在个人中心管理视频、管理弹幕、管理评论等。管理员可以管理视频弹幕评论&#xff0c;查看统计图。 演示视频&#xff1a; https://www.bilibili.c…

CVE-2020-0014 Toast组件点击事件截获漏洞

文章目录前言漏洞分析组件源码触摸属性漏洞利用POC分析漏洞复现漏洞修复总结前言 Toast 组件是 Android 系统一个消息提示组件&#xff0c;比如你可以通过以下代码弹出提示用户“该睡觉了…”&#xff1a; Toast.makeText(this, "该睡觉了…", Toast.LENGTH_SHORT)…

C语言文件操作-从知识到实践全程

目录 引入 文件的打开和关闭 文件如何使用程序来打开? 绝对路径需要转义字符 fopen函数 fclose函数 文件的打开方式(fopen第二参数const char* mode): 文件的顺序读写 fgetc和fputc的使用 fputc fgetc fgets和fputs的使用 fputs fgets perror的使用 fprint…

哪些程序员适合自由工作?(附平台推荐)

在早些时候进行远程办公&#xff0c;接私活或者跨国进行编程&#xff0c;赚点外快等也不是什么奇怪的事情。但是那时候没有人想到会把这些工作完全变成自己的主要业务——也就是我们说的自由工作。也不知道是哪一个第1个吃了螃蟹的人发现自由工作还不错&#xff0c;于是经过后面…

【JavaScript】DOM 学习总结-基础知识

获取元素方法&#xff1a; // 获取三个非常规的标签 console.log(document.documentElement) console.log(document.head) console.log(document.body)通过id/class获取&#xff1a;getElementById / getElementsByClassName // 获取常规的用id,class,tag var boxdocument.g…

Android 自定义Activity的主题

一. 前言 当在某个app中做一个新界面时, 我们要考虑一下主题风格相符合一致. 本篇文章讲解的是,如何新创建的Activity 与整个app主题符合, 特别是状态栏的颜色需要和这个app的状态栏颜色保持一致. 在读本篇文章之前, 可以移步一下笔者之前写的文章:Android style&#xf…

代码随想录算法训练营第十一天字符串 java :20. 有效的括号 1047. 删除字符串中的所有相邻重复项 150. 逆波兰表达式求值

文章目录Leetcode 20. 有效的括号题目详解数据结构 双端队列(deque)Deque有三种用途&#xff1a;思路报错Ac代码Leetcode1047. 删除字符串中的所有相邻重复项题目详解数据结构 ArrayDeque类思路AC代码150. 逆波兰表达式求值题目详解报错难点AC代码收获Leetcode 20. 有效的括号 …