操作系统真象还原实验记录之实验三十四:实现管道

操作系统真象还原实验记录之实验三十四:实现管道

1.管道相关知识总结

先说我们操作系统的管道实现:

操作系统真象还原实验记录之实验三十四:实现管道_第1张图片
上述图中,管道缓冲区就是一页内存,这一页内存被我们当成了环形缓冲区结构,
操作系统真象还原实验记录之实验三十四:实现管道_第2张图片

当这页管道被创建出来后,全局打开文件表中某个表项记录会着这个管道,其中fd_inode记录着这页地址,fd_flag=PIPE_FLAG表示管道,fd_pos记录着此管道打开数。
此表项会被安装到进程的fd_table中,占用两个fd用于读写,pipefd[0]、pipefd[1]分别保存这两个fd。

由此看来,管道是一页环形缓冲区内存,但依然被当作文件处理。只是管道的读写不能再使用file_write、file_read,毕竟不是读写磁盘,所以创建了新的函数pipe_write、pipe_read来读写环形缓冲区。

由于管道被当成了文件,依靠进程的文件描述符数组来访问,所以系统调用sys_close、sys_read、sys_write都需要增加判断管道的逻辑。此外,更新inode数函数也要增加判断管道逻辑,因为指向管道的表项各字段逻辑与指向文件的表项不同,需要新的代码。

我们的管道只用于fork产生的父子进程间的通信,也就是先创建一片共享内存区即管道,这样父进程的pcb->fd_table就有了两个fd指向此管道,再调用fork,复制产生子进程,这样子进程的fd_table里也有了同样的两个fd。这样规定一个fd用于读,另一个用于写

2.利用管道实现父子进程间的通信

pipe.c

#include "pipe.h"
#include "memory.h"
#include "fs.h"
#include "file.h"
#include "ioqueue.h"
#include "thread.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;
}

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

sys_pipe:接受一个参数pipe_fd[2]创建管道,给进程的fd_table安装两个fd,这两个fd保存在pipe_fd中。

is_pipe:判断给的fd是否指向管道。

pipe_read:选择适量的数据,读出环形缓冲区,避免阻塞

pipe_write:选择适量的数据,写入环形缓冲区,避免阻塞。

ioqueue.c增加ioq_length


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

fs.c的sys_close、sys_write、sys_read增加管道处理


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

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


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

fork.c的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) {
	 if (is_pipe(local_fd)) {
	    file_table[global_fd].fd_pos++;
	 } else {
	    file_table[global_fd].fd_inode->i_open_cnts++;
	 }
      }
      local_fd++;
   }
}

wait_exit.c之release_prog_resource增加管道处理

这里我思考了一下,相比sys_close,他还少了一句

running_thread()->fd_table[fd] = -1; // 使该文件描述符位可用

release_prog_resource的作用之一就是子进程结束时回收资源,关闭文件,管道也被当成了一种文件,占用了打开文件表的一项,sys_close里的判断处理是更全面的,需要恢复管道占用的表项。

因此不修改。

增加pipe系统调用

syscall.c
syscall.h
syscall_init.c

command/prog_pipe.c验证父子进程间通信

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

}

compile4.sh

同compile3.sh
修改名字cat为prog_pipe即可

写入Seven.img磁盘共5344字节

main.c

修改5344字节,cat改成prog_pipe

实验结果

操作系统真象还原实验记录之实验三十四:实现管道_第3张图片
流程和cat一样

3.在shell中支持管道

cat.c增加

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

略。。。
}

之前cat file1 就是将file1的内容读到屏幕。
现在cat增加了一个功能,如果cat不接参数,那么便是标准输入再标准输出,从键盘缓冲区读一个扇区到局部变量buf,再调用printf打印在屏幕,然后提前退出,返回状态status。
标准输入就是从键盘读,标准输出就是向屏幕输出。

每个进程pcb里的fd_table[0]=0、fd_table[1]=1,
但是全局打开表file_table[0]、file_table[1]是没有意义的,不指向任何一个inode。
sys_read接受的fd如果等于0就执行标准输入。
sys_write接受的fd如果等于1就执行标准输出。

shell中支持管道

重点就是下面这条命令的理解

ls -l|/cat|/cat|/cat

这属于cmd1 | cmd2 | cmd3 | cmd4模式
cmd1即 ls -l :向屏幕输出当前目录信息,原本他会调用sys_write,传入的文件描述符fd值为1,那么当前进程pcb->fd_table[fd]的内容是1,由于file_table[1]无意义,所以sys_write的代码逻辑自然不能走file_write,而是走console_put_str,完成向屏幕输出。

现在利用管道,于是进行了本节核心,重定向:
我们假设创建管道后,file_table[3]指向管道
当前进程pcb->fd_table[3]=3,当前进程pcb->fd_table[4]=3
在执行cmd1前,我们将当前进程pcb->fd_table[1]的值修改成3,
这样pcb->fd_table[1]便指向了管道,通俗一点,
就是is_pipe(1)为true了。从而往写屏幕变成了管道里写。

那么原本打印到屏幕上的目录信息就会保存到管道。
cmd2是无参数cat,原本的cat先执行标准输入,再执行标准输出。
同理重定向后,变成了读写管道。

cmd3、cmd4同理。但是cmd4的标准输出不需要重定向,所以最后可以通过屏幕打印的东西来验证理论。

cmd1标准输入无需重定向,cmdn标准输出无需重定向。

结合代码弄懂上述,最后一节的代码理解就没问题了。

pipe.c增加sys_fd_redirect


/* 将文件描述符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.c之cmd_execute

* 执行命令 */
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);
	 }
      }
   }
}

shell.c之my_shell

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

内部、外部命令的判断封装在cmd_execute,管道命令的逻辑实现在my_shell,先处理管道命令,再调用cmd_execute处理内部还是外部命令。这非常之合理。

加两个系统调用fd_redirect、help

fs.c之sys_help


/* 显示系统支持的内部命令 */
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");
}

syscall.c
syscall.h
syscall_init.c

实验结果

到了最后一节的最后时刻,思路还是要清晰,别忘了删除之前的cat
修改的最终cat还是用compile3.sh脚本
写入Seven硬盘共5703字节
main自然也要改一改。

ls -l | ../cat | /cat

命令不要打错了,…/cat和/cat都是绝对路径。因为cat在根目录,cat文件的绝对路径是/cat,
操作系统真象还原实验记录之实验三十四:实现管道_第4张图片
首先child_pid 2这句话大家必然是知道的,my_shell的子进程执行cat执行完毕后exit(0),my_shell的父进程打印的,只不过重定向后,输出给了管道。

我试了很久,管道只能打印5行数据,我不知道为什么这样。
如果你输入3个cat,那么按道理就会打印6行在屏幕,管道可能装不下,程序就会一直悬停,

由于重定向的原因,我的printf调试大法无效,所以我不知道为什么有这个bug,可能是管道只能输出输入适量数据,具体以后有时间再来分析。

总之完结撒花,谢谢作者郑钢,寄!

你可能感兴趣的:(操作系统,os,操作系统,linux)