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

1.管道相关知识总结

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


上述图中,管道缓冲区就是一页内存,这一页内存被我们当成了环形缓冲区结构,

当这页管道被创建出来后,全局打开文件表中某个表项记录会着这个管道,其中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

实验结果


流程和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都是绝对路径。

首先child_pid 2这句话大家必然是知道的,my_shell的子进程执行cat执行完毕后exit(0),my_shell的父进程打印的,只不过重定向后,输出给了管道。

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

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

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

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

  1. 操作系统真象还原实验记录之实验十二:实现ASSERT

    操作系统真象还原实验记录之实验十二:实现ASSERT,通过makefile完成编译 对应书P367 第8.2节 1.相关基础知识 见书 2.实验代码 完成了开关中断函数.实现assert断言函数用于调 ...

  2. 操作系统真象还原实验记录之实验七:加载内核

    操作系统真象还原实验记录之实验七:加载内核 对应书P207 1.相关基础知识总结 1.1 elf格式 1.1.1 c程序如何转化成elf格式 写好main.c的源程序 //main.c int mai ...

  3. 操作系统真象还原实验记录之实验六:内存分页

    操作系统真象还原实验记录之实验五:内存分页 对应书P199页 5.2 1.相关基础知识总结 页目录 页目录项 页表 页表项 物理页 虚拟地址 物理地址 概念略 页目录项及页表项 低12位都是属性.高2 ...

  4. 操作系统真象还原实验记录之实验一:第一次编写mbr

    操作系统真象还原之实验一:第一次编写mbr 对应书中第2.3节:让mbr飞一会 第58页 1.相关基础知识提炼总结 1.1电脑开机前与后 在电脑未开机前,BIOS就被事先写入到内存的F0000~FFF ...

  5. 操作系统真象还原实验记录之实验二十三:硬盘分区,并编写硬盘驱动程序

    操作系统真象还原实验记录之实验二十三:编写硬盘驱动程序 1.硬盘分区 1.1 创建Seven80.img硬盘 ./bximage -mode=create -imgmode=flat -hd=80 - ...

  6. 操作系统真象还原实验记录之实验十一:实现中断处理(二)

    操作系统真象还原实验记录之实验十一:实现中断处理(二) 书p335 7.6.2 改进中断处理程序,并调快时钟 1.实验代码第一次修改 对应 书p335 7.6.2 改进中断处理程序 这次是上一次实验的 ...

  7. 《操作系统真象还原》1-3章 学习记录

    文章目录 前言 一.开始实验前的一些基本问题解答? section的含义? vstart的含义? $ 和 $$区别? 实模式的特点? CPU如何和硬盘进行交互? CPU和IO设备交互方式? 程序载入内 ...

  8. 《操作系统真象还原》从零开始自制操作系统 全流程记录

    文章目录 前引 章节博客链接 实现源码链接 前引 这本<操作系统真象还原>里面一共有十五个章节 大约760页 这些系列博客也是我在做完哈工大操作系统Lab之后 觉得还是有些朦朦胧胧 毅然决 ...

  9. 《操作系统真象还原》第四章 ---- 剑指Loader 刃刺GDT 开启新纪元保护模式 解放32位

    文章目录 专栏博客链接 相关查阅博客链接 本书中错误勘误 看到第四章的一些很有趣的话 想记录下来 修改MBR.S 更新配置文件boot.inc 忽生疑惑(怎么是平坦模型?) 编写Loader.S 调用 ...

最新文章

  1. linux中的vp命令,Linux基础回顾之基础命令五(用户及组)
  2. Loonframwork到SWT的移植测试(JAVA GAME TEST SOURCE)
  3. Winform-DataGridView
  4. 火线妹在线求偶,想找高质量男朋,粉丝:有没有绯闻心里没数?
  5. 最新天猫Java3轮面试题目:虚拟机+并发锁+Sql防注入+Zookeeper
  6. Linux中断处理:上半部和下半部
  7. SOS宣布与融合子公司成立一家合资企业,专注区块链资产和加密货币等业务
  8. 用几小时,零基础也能学会可视化大屏,这百张模板帮了大忙
  9. php url 2f,PHP2(url二次编码)
  10. C++程序设计语言(特别版)pdf
  11. 服务器硬盘整体ghost,ghost备份整个硬盘| 全盘镜像ghost步骤
  12. 2022.03.15 Arcmap栅格数据无法按照拟定范围进行重分类的解决方案
  13. php文本框如何设置高度,更改文本框高度?
  14. 系列篇|一文尽览事件相机原理
  15. 呼叫中心中间件(mod_cti基于FreeSWITCH)-排队(ACD 话务分配)
  16. Jenkins集成GitHub
  17. MT【33】证明琴生不等式
  18. 传输层协议TCP和UDP的区别详解
  19. 资深EMC工程师总结:EMC整改流程及常见问题
  20. 基于stm32的物联网、智能家居控制系统

热门文章

  1. java后端解决跨域问题
  2. 英伟达登录界面卡住_免费!Google Colab现已支持英伟达T4 GPU
  3. 关于Wwise Audio的层级简介
  4. HFSS波端口和集总端口
  5. 博途PLC 1200/1500PLC ModbusTcp通信之状态机编程
  6. 阿里技术专家:详解技术中台/移动中台/研发中台!
  7. 解决变频器干扰低压电子设备的经验
  8. 江西省中小学生学籍管理-非小学新生注册(5)
  9. 4.Git基本命令操作
  10. php 调用百度AI实现图像审核功能