先说我们操作系统的管道实现:
上述图中,管道缓冲区就是一页内存,这一页内存被我们当成了环形缓冲区结构,
当这页管道被创建出来后,全局打开文件表中某个表项记录会着这个管道,其中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用于读,另一个用于写
#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:选择适量的数据,写入环形缓冲区,避免阻塞。
/* 返回环形缓冲区中的数据长度 */
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;
}
/* 关闭文件描述符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;
}
}
}
/* 更新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++;
}
}
这里我思考了一下,相比sys_close,他还少了一句
running_thread()->fd_table[fd] = -1; // 使该文件描述符位可用
release_prog_resource的作用之一就是子进程结束时回收资源,关闭文件,管道也被当成了一种文件,占用了打开文件表的一项,sys_close里的判断处理是更全面的,需要恢复管道占用的表项。
因此不修改。
syscall.c
syscall.h
syscall_init.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;
}
}
同compile3.sh
修改名字cat为prog_pipe即可
写入Seven.img磁盘共5344字节
修改5344字节,cat改成prog_pipe
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就执行标准输出。
重点就是下面这条命令的理解
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标准输出无需重定向。
结合代码弄懂上述,最后一节的代码理解就没问题了。
/* 将文件描述符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;
}
}
* 执行命令 */
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 */
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处理内部还是外部命令。这非常之合理。
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,
首先child_pid 2这句话大家必然是知道的,my_shell的子进程执行cat执行完毕后exit(0),my_shell的父进程打印的,只不过重定向后,输出给了管道。
我试了很久,管道只能打印5行数据,我不知道为什么这样。
如果你输入3个cat,那么按道理就会打印6行在屏幕,管道可能装不下,程序就会一直悬停,
由于重定向的原因,我的printf调试大法无效,所以我不知道为什么有这个bug,可能是管道只能输出输入适量数据,具体以后有时间再来分析。
总之完结撒花,谢谢作者郑钢,寄!