操作系统真象还原 学习笔记18--系统交互2

selph
selph
发布于 2021-03-04 / 828 阅读
0
1

操作系统真象还原 学习笔记18--系统交互2

本篇对应书籍第十五章15.5--15.7的内容

本篇实现了用户进程的加载,介绍了程序的执行过程,实现了wait和exit,以及管道

但由于未知的原因,我编译出来的程序似乎没法运行,所以只好用别人编译好的程序来测试了

加载用户进程

实现 exec

exec会把一个可执行文件的绝对路径作为参数, 把当前正在运行的用户进程的进程体(代码段、 数据段、 堆、 栈)用该可执行文件的进程体替换,从而实现了新进程的执行。 注意,exec只是 用新进程的进程体替换老进程进程体, 因此新进程的pid依然是老进程pid。

这里要实现的是execv,失败后返回-1,成功后不返回,因为jmp过去之后一去不复返了

userprog/exec.c

extern void intr_exit(void);
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             // 程序头表
};

这里定义了elf文件相关的结构

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

将文件描述符fd指向的文件中,偏移为offset,大小为filesz的段加载到虚拟地址为vaddr的内存

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

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

   /* 修改栈中参数 */
   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;
}

load 函数,从文件系统上加载用户程序pathname,成功则返回程序的起始地址,否则返回-1

sys_execv 函数,用path指向的程序替换当前进程

这里实际上就是解析ELF文件格式,按照ELF格式将文件加载到内存中,跳转执行

接下来就是添加系统调用

让 shell 接受外部命令

执行外部命令时 bash 会 fork 出子进程并调用 exec 从磁盘上加载外部命令对应的程序,然后执行该程序,从而实现了外部命令的执行

shell/shell.c

         }else{
            int32_t pid = fork();
            if (pid) {	   // 父进程
	            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]);
                  exit(-1);
               } else {
                  execv(argv[0], argv);
               }
            }
   }
   panic("my_shell: should not be here");
}

执行外部命令,是从当前进程中fork一个进程,判断文件是否存在,如果存在就加载执行

这里while(1)是用来让父进程悬停,以免父进程进入下一轮循环将final_path给覆盖掉

这也就是为什么,终端执行一个外部命令的时候,会在前台执行,执行的时候不能进行其他操作(Linux 里有其他办法来执行后台进程)

加载硬盘上的用户进程执行

这里要做三件事:

  1. 编写第一个真正的用户程序。
  2. 将用户程序写入文件系统。
  3. 在shell中执行用户程序, 即外部命令。

先写一个用户程序:

command/prog_no_arg.c

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

command/complie.sh

下面看编译

####  此脚本应该在command目录下执行

if [[ ! -d "../lib" || ! -d "../build" ]];then
   echo "dependent dir don\`t exist!"
   cwd=$(pwd)
   cwd=${cwd##*/}
   cwd=${cwd%/}
   if [[ $cwd != "command" ]];then
      echo -e "you\`d better in command dir\n"
   fi 
   exit
fi

BIN="prog_no_arg"
CFLAGS="-Wall -c -fno-builtin -W -Wstrict-prototypes \
      -Wmissing-prototypes -Wsystem-headers"
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/work/my_workspace/bochs/hd60M.img" 

nasm -f elf ./start.S -o ./start.o
ar rcs simple_crt.a $OBJS start.o
gcc $CFLAGS $LIBS -o $BIN".o" $BIN".c"
ld $BIN".o" simple_crt.a -o $BIN
SEC_CNT=$(ls -l $BIN|awk '{printf("%d", ($5+511)/512)}')

if [[ -f $BIN ]];then
   dd if=./$DD_IN of=$DD_OUT bs=512 \
   count=$SEC_CNT seek=300 conv=notrunc
fi

这里是把用户程序编译、链接、写入hd60M.img硬盘里

我们的目的是把程序写入到文件系统中,也就是hd80M.img,这需要两步:

  1. 先把程序写入hd60M.img里,作为中转,因为hd60M.img不存在文件系统,不存在破坏文件系统的问题
  2. 将hd60M.img的文件读取出来,写入hd80M.img文件系统中,这得在main.c中完成

kernel/main.c

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

/*************    写入应用程序    *************/
   uint32_t file_size = 4488; 
   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("[selph@localhost /]$ ");

   while(1);
   return 0;
}

先运行编译脚本,生成出可执行程序,写入hd60M.img的300扇区的位置,然后在主函数中将该文件写入硬盘2文件系统中

这里我的64位Ubuntu生成出来的elf文件和32位系统生成的elf不一样,会导致程序无法正常执行,故此处用别人现成编译好的可执行文件来代替一下,至于解决方案,本次学习以原理为主,这个细节回头有空再研究吧,有知道的朋友还望留言告知

至于参考的文件,我会放在下面参考资料部分

运行 Bochs

编译,运行:

image-20210303221254916

使用户进程支持参数

这里要介绍函数运行的原理

CRT,C运行库,因操作系统不同而不同,负责初始化运行环境,在main函数之前为用户进程准备条件,传递参数等,等准备好之后再调用main函数,用户进程结束时,CRT还要负责回收资源。

main函数是被call调用的

其实CRT代码才是用户程序的第一部分,我们的main函数实质上是被夹在CRT中执行的,它只是用户程序的中间部分,编译后的二进制可执行程序中还包括了CRT的指令, 其结构如图所示:

image-20210303222121522

这里要实现一个简易版本的CRT

command/start.S

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

这才是程序真正的入口,默认是_start这里也就不改了

程序链接时,把这里链接成入口,然后链接main函数,实现CRT来执行程序,实现参数传递

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

打印接收的参数,fork出子进程,主进程和子进程都输出当前进程信息

command/compile.sh

if [[ ! -d "../lib" || ! -d "../build" ]];then
    echo "dependent dir don't exist!"
    cwd=${pwd}
    cwd=${cwd##*/}
    cwd=${cwd%/}
    if [[ $cwd != "command" ]];then
        echo -e "you'd better in command dir\n"
    fi
    exit
fi

BIN="prog_arg"
CFLAGS="-Wall -m32 -c -fno-builtin -W -Wstrict-prototypes 
    -Wmissing-prototypes -Wsystem-headers"
LIB="-I ../lib -I ../lib/user -I ../fs"
OBJS="../build/string.o ../build/syscall.o \
    ../build/stdio.o ../build/assert.o start.o"
DD_IN=$BIN
DD_OUT="../hd60M.img"

nasm -f elf ./start.S -o ./start.o
ar rcs simple_crt.a $OBJS start.o
gcc $CFLAGS $LIB -o $BIN".o" $BIN".c"
ld -melf_i386 $BIN".o" simple_crt.a -o $BIN
SEC_CNT=$(ls -l $BIN | awk '{printf("%d", ($5+511)/512)}')

if [[ -f $BIN ]];then
    dd if=./$DD_IN of=$DD_OUT bs=512 count=$SEC_CNT seek=300 conv=notrunc
fi

kernel/main.c

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

/*************    写入应用程序    *************/
   uint32_t file_size = 4972; 
   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("[selph@localhost /]$ ");

   while(1);
   return 0;
}

运行 Bochs

编译,运行:

image-20210303223410917

实现系统调用 wait 和 exit

这里主要讲的是父与子进程的关系及其处理。

wait 和 exit 的作用

exit的作用是使进程”主动 ”退出,结束运行。在C运行库中调用main函数执行, main函数执行结束后程序流程会回到C运行库,C运行库的结束代码处会调用exit。

wait的作用是阻塞父进程自己, 直到任意一个子进程结束运行。 wait通常是由父进程调用的;

或者说,尽管某个进程没有子进程,但只要它调用了wait系统调用,该进程就被认为是父进程,内核就要去查找它的子进程,由于它没有子进程,此时wait 会返回 -1 表示其没有子进程。

如果有子进程,这时候该进程就被阻塞,不再运行,内核就要去遍历其所有的子进程,查找哪个子进程退出了,并将子进程退出时的返回值传递给父进程,随后将父进程唤醒。

孤儿进程与僵尸进程

Linux 系统中有孤儿进程和僵尸进程的原因是因为有 wait 和 exit 系统调用。

进程的返回值存放在 pcb 中。这表示 pcb 是进程最后占用的资源,它应该在父进程调用 wait 获取子进程的返回值后,再由内核回收子进程 pcb 所占的 1 页框内存。

在子进程提交给父进程返回值的通信中,有这样一种情况,当父进程提前退出时,它所有的子进程还在运行,这些进程就称为孤儿进程。这时候所有的子进程会被 int 进程收养,init 进程会成为这些子进程的新父亲,当子进程退出时会由 init 负责为其 “收尸”。

如果父进程在派生出子进程后没有调用 wait 等待接收子进程的返回值,这时某个子进程调用 exit 退出了,自然没人来接收返回值了(父进程未退出,子进程不能过继给 init),因此其 pcb 所占的空间不能释放,称为僵尸进程。(只有PCB,没有内容)

Linux 中,用 ps 命令查看的任务列表中,stat 为 “Z” 的进程就是僵尸进程。

exit是由子进程调用的,表面上功能是使子进程结束运行并传递返回值给内核,本质上是内核在幕后会将进程除pcb以外的所有资源都回收。wait是父进程调用的,表面上功能是使父进程阻塞自己,直到子 进程调用exit结束运行,然后获得子进程的返回值,本质上是内核在幕后将子进程的返回值传递给父进程并会唤醒父进程,然后将子进程的pcb回收。

一些基础代码

进程的返回值,也就是退出状态,存在PCB里,要在PCB中新增成员:

thread/thread.h

    uint32_t cwd_inode_nr;          // 进程所在工作目录的inode编号
    int16_t parent_pid;             // 父进程 pid
    int8_t exit_status;             // 进程结束时自己调用 exit 传入的参数
    uint32_t stack_magic;           // 栈的边界标记, 用于检测栈的溢出
};

魔数之上的那一行就是新增的内容,表示进程的退出状态

kernel/memory.c

内存释放的函数,用来释放进程结束后的那一页PCB

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

thread/thread.c

// pid 的位图, 最大支持 1024 个 pid
uint8_t pid_bitmap_bits[128] = {0};

// pid 池
struct pid_pool {
    struct bitmap pid_bitmap; // pid 位图
    uint32_t pid_start; // 起始pid
    struct lock pid_lock; // 分配 pid 锁
}pid_pool;

// 初始化 pid 池
static void pid_pool_init(void) {
    pid_pool.pid_start = 1;
    pid_pool.pid_bitmap.bits = pid_bitmap_bits;
    pid_pool.pid_bitmap.btmp_bytes_len = 128;
    bitmap_init(&pid_pool.pid_bitmap);
    lock_init(&pid_pool.pid_lock);
}

// 分配 pid
static pid_t allocate_pid(void) {
    lock_acquire(&pid_pool.pid_lock);
    int32_t bit_idx = bitmap_scan(&pid_pool.pid_bitmap, 1);
    bitmap_set(&pid_pool.pid_bitmap, bit_idx, 1);
    lock_release(&pid_pool.pid_lock);
    return (bit_idx + pid_pool.pid_start);
}

// 释放 pid
void release_pid(pid_t pid) {
    lock_acquire(&pid_pool.pid_lock);
    int32_t bit_idx = pid - pid_pool.pid_start;
    bitmap_set(&pid_pool.pid_bitmap, bit_idx, 0);
    lock_release(&pid_pool.pid_lock);
}

// 回收 thread_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);
    return pthread->pid == pid;
}

// 根据 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;
}
// 初始化线程环境
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_thread = thread_start("idle", 10, idle, NULL);

    put_str("thread_init done\n");
}

使用位图管理pid,新增了pid分配与释放的函数,在初始化线程环境的时候,初始化pid

thread_exit 函数:回收 thread_over 的 pcb 和页表, 并将其从调度队列中去除

pid_check 函数:比对任务的 pid,看看当前pid和指定的pid是不是一样的

pid2thread 函数:根据 pid 找 pcb, 若找到则返回该 pcb, 否则返回 NULL

基础工作完成

实现 wait 和 exit

userprog/wait_exit.c

/* 释放用户进程资源: 
 * 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++;
   }
}

释放用户进程资源:

  1. 页表中对应的物理页
  2. 虚拟内存池占物理页框
  3. 关闭打开的文件
/* list_traversal的回调函数,
 * 查找pelem的parent_pid是否是ppid,成功返回true,失败则返回false */
static bool find_child(struct list_elem* pelem, int32_t ppid) {
   /* elem2entry中间的参数all_list_tag取决于pelem对应的变量名 */
   struct task_struct* pthread = elem2entry(struct task_struct, all_list_tag, pelem);
   if (pthread->parent_pid == ppid) {     // 若该任务的parent_pid为ppid,返回
      return true;   // list_traversal只有在回调函数返回true时才会停止继续遍历,所以在此返回true
   }
   return false;     // 让list_traversal继续传递下一个元素
}

/* list_traversal的回调函数,
 * 查找状态为TASK_HANGING的任务 */
static bool find_hanging_child(struct list_elem* pelem, int32_t ppid) {
   struct task_struct* pthread = elem2entry(struct task_struct, all_list_tag, pelem);
   if (pthread->parent_pid == ppid && pthread->status == TASK_HANGING) {
      return true;
   }
   return false; 
}

/* list_traversal的回调函数,
 * 将一个子进程过继给init */
static bool init_adopt_a_child(struct list_elem* pelem, int32_t pid) {
   struct task_struct* pthread = elem2entry(struct task_struct, all_list_tag, pelem);
   if (pthread->parent_pid == pid) {     // 若该进程的parent_pid为pid,返回
      pthread->parent_pid = 1;
   }
   return false;		// 让list_traversal继续传递下一个元素
}

三个回调函数:

find_child 函数,找父进程pid为ppid的进程

find_hanging_child 函数,找状态为HANGING的进程

init_adopt_a_child 函数,将子进程过继给init进程,其实就是把父进程pid写成init进程的

/* 等待子进程调用exit,将子进程的退出状态保存到status指向的变量.
 * 成功则返回子进程的pid,失败则返回-1 */
pid_t sys_wait(int32_t* status) {
   struct task_struct* parent_thread = running_thread();

   while(1) {
      /* 优先处理已经是挂起状态的任务 */
      struct list_elem* child_elem = list_traversal(&thread_all_list, find_hanging_child, parent_thread->pid);
      /* 若有挂起的子进程 */
      if (child_elem != NULL) {
	 struct task_struct* child_thread = elem2entry(struct task_struct, all_list_tag, child_elem);
	 *status = child_thread->exit_status; 

	 /* thread_exit之后,pcb会被回收,因此提前获取pid */
	 uint16_t child_pid = child_thread->pid;

	 /* 2 从就绪队列和全部队列中删除进程表项*/
	 thread_exit(child_thread, false); // 传入false,使thread_exit调用后回到此处
	 /* 进程表项是进程或线程的最后保留的资源, 至此该进程彻底消失了 */

	 return child_pid;
      } 

      /* 判断是否有子进程 */
      child_elem = list_traversal(&thread_all_list, find_child, parent_thread->pid);
      if (child_elem == NULL) {	 // 若没有子进程则出错返回
	 return -1;
      } else {
      /* 若子进程还未运行完,即还未调用exit,则将自己挂起,直到子进程在执行exit时将自己唤醒 */
	 thread_block(TASK_WAITING); 
      }
   }
}

/* 子进程用来结束自己时调用 */
void sys_exit(int32_t status) {
   struct task_struct* child_thread = running_thread();
   child_thread->exit_status = status; 
   if (child_thread->parent_pid == -1) {
      PANIC("sys_exit: child_thread->parent_pid is -1\n");
   }

   /* 将进程child_thread的所有子进程都过继给init */
   list_traversal(&thread_all_list, init_adopt_a_child, child_thread->pid);

   /* 回收进程child_thread的资源 */
   release_prog_resource(child_thread); 

   /* 如果父进程正在等待子进程退出,将父进程唤醒 */
   struct task_struct* parent_thread = pid2thread(child_thread->parent_pid);
   if (parent_thread->status == TASK_WAITING) {
      thread_unblock(parent_thread);
   }

   /* 将自己挂起,等待父进程获取其status,并回收其pcb */
   thread_block(TASK_HANGING);
}

sys_wait 函数,等待子进程调用 exit, 将子进程的退出状态保存到 status 指向的变量,成功则返回子进程的 pid, 失败则返回 -1

  • 获取当前进程pid,进入循环
    • 如果有挂起的子进程,就把退出状态和pid获取了,然后从线程队列中删除pcb,(init循环使用wait可用来消除僵尸进程)
    • 如果有子进程,将自己挂起
    • 如果无子进程,返回-1

sys_exit 函数,子进程用来结束自己时调用

  • 获取当前PCB和退出状态
  • 将进程的子进程过继给init进程
  • 回收进程的资源
  • 如果父进程正在等待,则将父进程唤醒,然后将自己挂起

实现 cat 命令

这里实现一个简单的cat,在此之前,先将 wait 和 exit 添加到系统调用里,修改 Start.S

command/start.S

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

	; 将 main 的返回值通过栈传给 exit, gcc 用 eax 存储返回值, 这是 ABI 规定的
	push eax
	call exit

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

先判断参数个数,只支持1个参数,申请空间获取参数,将路径拼接成绝对路径,通过open的只读模式打开文件,循环读取文件进行输出(将文件内容read读取到缓冲区,然后使用write输出到标准输出)

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

BIN="cat"
CFLAGS="-Wall -c -fno-builtin -W -Wstrict-prototypes \
      -Wmissing-prototypes -Wsystem-headers"
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="../hd60M.img" 

nasm -f elf ./start.S -o ./start.o
ar rcs simple_crt.a $OBJS start.o
gcc $CFLAGS $LIBS -o $BIN".o" $BIN".c"
ld $BIN".o" simple_crt.a -o $BIN
SEC_CNT=$(ls -l $BIN|awk '{printf("%d", ($5+511)/512)}')

if [[ -f $BIN ]];then
   dd if=./$DD_IN of=$DD_OUT bs=512 \
   count=$SEC_CNT seek=300 conv=notrunc
fi

shell/shell.c

有了wait和exit,可以把相关的while(1)给去掉了

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

kernel/main.c

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

/*************    写入应用程序    *************/
   uint32_t file_size = 5352; 
   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("[selph@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");
}

在main函数里,除了向向文件系统写入cat程序以外,在原先下面while(0)处进行了修改,改成了exit退出进程

在init进程里,修改while(0)部分,改为循环回收僵尸进程

子进程通过exit唤醒父进程并让父进程销毁其PCB,但是父进程没通过wait接收子进程退出信息,导致父进程没法销毁其PCB,导致该PCB称为空进程,也就是僵尸进程,死不瞑目进程

僵尸进程会在init进程中被循环的wait函数消除,wait中回遍历线程列表,找状态为退出的线程,然后代替父进程接收退出状态和pid然后释放其PCB(PCB是它留存于世间最后的灵魂)

孤儿进程会被exit函数进程处理,进程退出的时候,如果有子进程,就把子进程过继给init进程,然后自己再退出,自己离开前好给子孙一个交代,如果有父进程,就将父进程唤醒。

运行 Bochs

编译,运行:

image-20210303235841479

管道

管道系统,用于进程间通信

管道的原理

进程间通信方式有很多种,有消息队列、共享内存、socket网络通信等,还有一种就是管道。

Linux 中一切皆文件,因此管道也被视为文件,只是该文件不存在于文件系统上,而存在于内存中

既然是文件,管道就要按照文件操作的函数来使用,因此也要使用open、close、read、write等方法来操作管道。管道通常被多个进程共享, 而且存在于内存之中,管道其实就是内核空间中的内存缓冲区。管道是个环形缓冲区。

当某个进程往管道中写入数据后,该数据很快就会被另一个进程读取,之后可以用新的数据覆盖老数据,继续被别的进程读取

管道有两端,一端用于从管道中读入数据,另一端用于往管道中写入数据。这两端使用文件描述符的方式来读取,故进程创建管道实际上是内核为其返回了用千读取管道缓冲区的文件描述符,一个描述符用于读,另一个描述符用于写。

通常的用法是进程在创建管道之后,马上调用 fork,克隆出一个子进程,子进程完全继承了父进程的一切,也继承了管道的描述符,这为父子进程通信提供了保证。

一般情况下,父子进程中都是一个读数据,一个写数据,并不会存在一方又读又写的情况,因此在父子进程中会分别关掉不使用的管道描述符。

管道分为两种:匿名管道和命名管道。以上说的是匿名管道,只能用于父子进程间的通信。

命名管道在 Linux 中通过命令 mkfifo 来创建,成功创建之后便会在文件系统上存在个管道文件,使得该管道对任何进程都可见,因此多个进程即使没有父子关系也都通过访问该管道文件进行通信。

image-20210304004100265

管道的设计

Linux 上,使用文件系统的中间层 VFS,Virtual File System,虚拟文件系统来抽象各种实现细节,用户只和VFS打交道

Linux 利用现有文件结构和VFS索引节点实现管道:

image-20210304004850782

我们这里效仿Linux的思路来实现一个简单的管道

管道也是文件,但是需要与普通文件和目录文件有区分,可以复用原来的文件结构:

  • fd_pos:记录管道的打开数
  • fd_flags:0xFFFF,标识为管道
  • fs_inode:指向内存缓冲区

文件通过文件描述符访问,文件描述符是指向文件表数组的下标,数组元素的值指向文件表的下标,下标对应文件结构,要让任意进程进行通信,就得让他们都访问同一个公共的空间,只要让任意文件所指向的位于文件表中的文件是同一个就行

实现管道的思路:

image-20210304005918679

接下来会实现管道操作符|,上级命令的输出作为下级命令的输入,两两相邻的命令共享1个管道

当上级命令将管道缓冲区占满之后仍未结束,后面的命令没法继续执行,从而导致进程无限休眠的情况,为了避免这种情况并且简化实现难度,这里采取的方法就是让生产者和消费者用到的数据小于缓冲区的大小

管道的实现

Linux 创建管道的方法是调用 pipe

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

新增函数 ioq_length,返回环形缓冲区数据的长度

shell/pipe.c

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

/* 创建管道,成功返回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;
}

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

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

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

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

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

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

在pipe.h中定义了PIPE_FLAG为0xFFFF

is_pipe 函数,根据flag判断该文件是否是管道

sys_pipe 函数,接收数组作为参数,数组有两个成员,0表示读取,1表示写入

  • 先获取文件结构空位下标,为其fd_inode分配一页内核内存空间作为缓冲区
  • 初始化缓冲区为环形缓冲区
  • 将fd_flag设置为管道标志0xFFFF
  • 将fd_pos复用为管道的打开数,为 2
  • 接下来让两个进程的文件描述符都指向同一个文件结构

pipe_read 函数,获取指定的文件描述符对应的文件表中的文件结构,获取环形缓冲区,和缓冲区剩余长度,读取数据到buffer

pipe_write 函数,获取指定的文件描述符对应的文件表中的文件结构,获取环形缓冲区,和缓冲区剩余长度,写入数据到buffer

读写函数很常规,基本都是一回事

fs/fs.c

管道用到了文件系统,就需要对原来的文件系统进行一些更变

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

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

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

sys_close 函数新增对管道的判断,如果是管道,则释放环形缓冲区,关闭管道上的文件描述符

sys_write 函数也新增对管道的判断,标准输出可能是输出到管道,就要用管道的方法,如果只是管道输出,则也用管道方法输出

sys_read 函数同理

userprog/fork.c

新增函数:

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

管道和普通文件的结构对应的功能不一样,所以需要先判断一下是不是管道再更新inode打开数

userprog/wait_exit.c

static void release_prog_resource(struct task_struct* release_thread) {
...
/* 关闭进程打开的文件 */
   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++;
   }
}

新增对管道的处理,如果程序退出忘记关闭打开的管道,在这里要关掉它

pipe要记得添加系统调用里

利用管道实现进程间通信

这里开始编写用户进程,来验证父子进程间通信的功能

command/prog_pipe.c

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

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

BIN="prog_pipe"
CFLAGS="-Wall -c -fno-builtin -W -Wstrict-prototypes \
      -Wmissing-prototypes -Wsystem-headers"
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/work/my_workspace/bochs/hd60M.img" 

nasm -f elf ./start.S -o ./start.o
ar rcs simple_crt.a $OBJS start.o
gcc $CFLAGS $LIBS -o $BIN".o" $BIN".c"
ld $BIN".o" simple_crt.a -o $BIN
SEC_CNT=$(ls -l $BIN|awk '{printf("%d", ($5+511)/512)}')

if [[ -f $BIN ]];then
   dd if=./$DD_IN of=$DD_OUT bs=512 \
   count=$SEC_CNT seek=300 conv=notrunc
fi

kernel/main.c

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

/*************    写入应用程序    *************/
   uint32_t file_size = 5352; 
   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("[selph@localhost /]$ ");
   thread_exit(running_thread(), true);
   return 0;
}

运行 Bochs 功能验证

编译,运行:

image-20210304014722467

父进程给子进程说完话先退出了,这里子进程变孤儿进程了,子进程收到消息后输出了出来,然后自己也退出了。

在 shell 中支持管道

管道利用了输入输出重定向。利用输入输出重定向的原理,可以将一个命令的输出作为另一个命令的输入。命令行中若包括管道符,则将管道符左边命令的输出作为管道符右边命令的输出。把输入和输出都重定向到了文件。

管道的核心就是输入输出的重定向

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

将数组中原来的下标换成新的下标,这样访问这个文件的时候就访问到新的文件了,重定向到新的文件了

如果管道位于标准输入输出,则直接用,否则需要先获取管道的位置再给文件描述符赋值

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

char* argv[MAX_ARG_NR] = {NULL};
int32_t argc = -1;
/* 简单的shell */
void my_shell(void) {
   cwd_cache[0] = '/';
   while (1) {
      print_prompt(); 
      memset(final_path, 0, MAX_PATH_LEN);
      memset(cmd_line, 0, MAX_PATH_LEN);
      readline(cmd_line, MAX_PATH_LEN);
      if (cmd_line[0] == 0) {	 // 若只键入了一个回车
	 continue;
      }

      /* 针对管道的处理 */
      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");
}

修改之前的shell.c,新增的cmd_execute函数,这个函数实际上是把原来my_shell函数里的命令执行给单独封装出来了

然后修改了my_shell函数新增了对管道的处理:

  • 从左向右先找管道符|
  • 找到了就生成管道,将当前进程的标准输出重定向到管道fd[1],执行第一个命令将输出写入环形缓冲区
  • 跨过|,处理下一个命令
  • 将标准输入重定向到fd[0],循环执行中间的命令,中间的命令输入输出都是环形缓冲区(标准输入输出的改变体现在命令的读写函数会使用不同的函数,而对于此处是透明的)
  • 最后一个命令将标准输出恢复到屏幕,执行最后一条命令
  • 执行完毕之后,将标准输入恢复成键盘
  • 关闭管道
  • 如果无管道就直接解析命令执行

使用管道执行命令本质上就是重定向输入输出,在管道左边就修改输出,在右边就修改输入,在中间就都改,具体重定向了之后输入输出是什么,都在输入输出相关函数上进行体现

command/cat.c

为了测试管道效果,修改cat.c

#include "syscall.h"
#include "stdio.h"
#include "string.h"
int main(int argc, char** argv) {
   if (argc > 2) {
      printf("cat: argument error\n");
      exit(-2);
   }

   if (argc == 1) {
      char buf[512] = {0};
      read(0, buf, 512);
      printf("%s",buf);
      exit(0);
   }   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;
}

修改为:如果没有参数的时候,从键盘获取数据,此时键盘输入会被重定向为环形缓冲区,所以实际上当没有参数的时候从环形缓冲区中读取数据,打印完成之后,以0的结束状态退出,编译脚本还是用原来的,kernel/main.c还是老样子,把文件读取到文件系统中

这里顺便再添加一下help系统调用吧

新增 help 系统调用

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

添加系统调用即可

运行 Bochs 功能验证

编译,运行:

image-20210304023537457

这里通过管道,cat从管道中获取输入,会以0的状态返回

help命令示例如下:

image-20210304023645869

感想

到此,关于本书《操作系统真象还原》的学习终于完成了,终于告一段落了,太不容易了,挺辛苦的哈哈哈

接下来我会单独对整本书各个章节内容进行梳理,试图让整本书的知识更加连贯起来

参考资料


评论