Linux 0.11-shell 程序读取你的命令-43

  • shell 程序读取你的命令
  • 转载

shell 程序读取你的命令

新建一个非常简单的 info.txt 文件。

name:flash
age:28
language:java

在命令行输入一条十分简单的命令。

[root@linux0.11] cat info.txt | wc -l
3

这条命令的意思是读取刚刚的 info.txt 文件,输出它的行数。

在上一回,我们详细解读了从键盘敲击出这个命令,到屏幕上显示出这个命令,中间发生的事情。

那今天,我们接着往下走,下一步就是,shell 程序如何读取到你输入的这条命令的

这里我们需要知道两件事情。

第一,我们键盘输入的字符,此时已经到达了控制台终端 tty 结构中的 secondary 这个队列里。

第二,shell 程序将通过上层的 read 函数调用,来读取这些字符。

// xv6-public sh.c
int main(void) {static char buf[100];// 读取命令while(getcmd(buf, sizeof(buf)) >= 0){// 创建新进程if(fork() == 0)// 执行命令runcmd(parsecmd(buf));// 等待进程退出wait();}
}int getcmd(char *buf, int nbuf) {...gets(buf, nbuf);...
}char* gets(char *buf, int max) {int i, cc;char c;for(i=0; i+1 < max; ){cc = read(0, &c, 1);if(cc < 1)break;buf[i++] = c;if(c == '\n' || c == '\r')break;}buf[i] = '\0';return buf;
}

看,shell 程序会通过 getcmd 函数最终调用到 read 函数一个字符一个字符读入,直到读到了换行符(\n 或 \r)的时候,才返回。

读入的字符在 buf 里,遇到换行符后,这些字符将作为一个完整的命令,传入给 runcmd 函数,真正执行这个命令。

那我们接下来的任务就是,看一下这个 read 函数是怎么把之前键盘输入并转移到 secondary 这个队列里的字符给读出来的。

read 函数是个用户态的库函数,最终会通过系统调用中断,执行 sys_read 函数。

// read_write.c
// fd = 0, count = 1
int sys_read(unsigned int fd,char * buf,int count) {struct file * file = current->filp[fd];// 校验 buf 区域的内存限制verify_area(buf,count);struct m_inode * inode = file->f_inode;// 管道文件if (inode->i_pipe)return (file->f_mode&1)?read_pipe(inode,buf,count):-EIO;// 字符设备文件if (S_ISCHR(inode->i_mode))return rw_char(READ,inode->i_zone[0],buf,count,&file->f_pos);// 块设备文件if (S_ISBLK(inode->i_mode))return block_read(inode->i_zone[0],&file->f_pos,buf,count);// 目录文件或普通文件if (S_ISDIR(inode->i_mode) || S_ISREG(inode->i_mode)) {if (count+file->f_pos > inode->i_size)count = inode->i_size - file->f_pos;if (count<=0)return 0;return file_read(inode,file,buf,count);}// 不是以上几种,就报错printk("(Read)inode->i_mode=%06o\n\r",inode->i_mode);return -EINVAL;
}

关键地方我已经标上了注释,整体结构不看细节的话特别清晰。

这个最上层的 sys_read,把读取管道文件字符设备文件块设备文件目录文件普通文件,都放在了同一个方法里处理,这个方法作为所有读操作的统一入口,由此也可以看出 linux 下一切皆文件的思想。

read 的第一个参数是 0,也就是 0 号文件描述符,之前我们在讲第四部分的时候说过,shell 进程是由进程 1 通过 fork 创建出来的,而进程 1 在 init 的时候打开了 /dev/tty0 作为 0 号文件描述符。

// main.c
void init(void) {setup((void *) &drive_info);(void) open("/dev/tty0",O_RDWR,0);(void) dup(0);(void) dup(0);
}

而这个 /dev/tty0 的文件类型,也就是其 inode 结构中表示文件类型与属性的 i_mode 字段,表示为字符型设备,所以最终会走到 rw_char 这个子方法下,文件系统的第一层划分就走完了。

接下来我们看 rw_char 这个方法。

// char_dev.c
static crw_ptr crw_table[]={NULL,       /* nodev */rw_memory,  /* /dev/mem etc */NULL,       /* /dev/fd */NULL,       /* /dev/hd */rw_ttyx,    /* /dev/ttyx */rw_tty,     /* /dev/tty */NULL,       /* /dev/lp */NULL};      /* unnamed pipes */int rw_char(int rw,int dev, char * buf, int count, off_t * pos) {crw_ptr call_addr;if (MAJOR(dev)>=NRDEVS)return -ENODEV;if (!(call_addr=crw_table[MAJOR(dev)]))return -ENODEV;return call_addr(rw,MINOR(dev),buf,count,pos);
}

根据 dev 这个参数,计算出主设备号为 4,次设备号为 0,所以将会走到 rw_ttyx 方法继续执行。

// char_dev.c
static int rw_ttyx(int rw,unsigned minor,char * buf,int count,off_t * pos) {return ((rw==READ)?tty_read(minor,buf,count):tty_write(minor,buf,count));
}

根据 rw == READ 走到读操作分支 tty_read,这就终于快和上一讲的故事接上了。

以下是 tty_read 函数,我省略了一些关于信号和超时时间等非核心的代码。

// tty_io.c
// channel=0, nr=1
int tty_read(unsigned channel, char * buf, int nr) {struct tty_struct * tty = &tty_table[channel];char c, * b=buf;while (nr>0) {...if (EMPTY(tty->secondary) ...) {sleep_if_empty(&tty->secondary);continue;}do {GETCH(tty->secondary,c);...put_fs_byte(c,b++);if (!--nr) break;} while (nr>0 && !EMPTY(tty->secondary));...}...return (b-buf);
}

入参有三个参数,非常简单。

channel 为 0,表示 tty_table 里的控制台终端这个具体的设备。buf 是我们要读取的数据拷贝到内存的位置指针,也就是用户缓冲区指针。nr 为 1,表示我们要读出 1 个字符。

整个方法,其实就是不断从 secondary 队列里取出字符,然后放入 buf 指所指向的内存。

如果要读取的字符数 nr 被减为 0,说明已经完成了读取任务,或者说 secondary 队列为空,说明不论你任务完没完成我都没有字符让你继续读了,那此时调用 sleep_if_empty 将线程阻塞,等待被唤醒。

阻塞怎么做到? —> 设置当前线程状态为阻塞,然后调用进程调度接口,并且将当前线程加入到secondary 队列的线程阻塞列表中
唤醒怎么做到? —> 当键盘输入,触发对应的系统调用时,最终会往secondary 队列塞入元素,此时释放secondary 队列的线程阻塞列表所有线程,并设置每个线程状态为可调度

其中 GETCH 就是个宏,改变 secondary 队列的队头队尾指针,你自己写个队列数据结构,也是这样的操作,不再展开讲解。

#define GETCH(queue,c) \
(void)({c=(queue).buf[(queue).tail];INC((queue).tail);})

同理,判空逻辑就更为简单了,就是队列头尾指针是否相撞。

#define EMPTY(a) ((a).head == (a).tail)

理解了这些小细节之后,再明白一行关键的代码,整个 read 到 tty_read 这条线就完全可以想明白了。那就是队列为空,即不满足继续读取条件的时候,让进程阻塞的 sleep_if_empty,我们看看。

sleep_if_empty(&tty->secondary);// tty_io.c
static void sleep_if_empty(struct tty_queue * queue) {cli();while (!current->signal && EMPTY(*queue))interruptible_sleep_on(&queue->proc_list);sti();
}// sched.c
void interruptible_sleep_on(struct task_struct **p) {struct task_struct *tmp;...tmp=*p;*p=current;
repeat: current->state = TASK_INTERRUPTIBLE;schedule();if (*p && *p != current) {(**p).state=0;goto repeat;}*p=tmp;if (tmp)tmp->state=0;
}

我们先只看一句关键的代码,就是将当前进程的状态设置为可中断等待。

current->state = TASK_INTERRUPTIBLE;

那么执行到进程调度程序时,当前进程将不会被调度,也就相当于阻塞了,不熟悉进程调度的同学可以复习一下 第23回 | 如果让你来设计进程调度。

进程被调度了,什么时候被唤醒呢?

当我们再次按下键盘,使得 secondary 队列中有字符时,也就打破了为空的条件,此时就应该将之前的进程唤醒了,这在上一回 第42回 | 用键盘输入一条命令 一讲中提到过了。

// tty_io.c
void do_tty_interrupt(int tty) {copy_to_cooked(tty_table+tty);
}void copy_to_cooked(struct tty_struct * tty) {...wake_up(&tty->secondary.proc_list);
}

可以看到,在 copy_to_cooked 里,在将 read_q 队列中的字符处理后放入 secondary 队列中的最后一步,就是唤醒 wake_up 这个队列里的等待进程。

而 wake_up 函数更为简单,就是修改一下状态,使其变成可运行的状态。

// sched.c
void wake_up(struct task_struct **p) {if (p && *p) {(**p).state=0;}
}

总体流程就是这个样子的。

当然,进程的阻塞与唤醒是个体系,还有很多细节,我们下一回再仔细展开这部分的内容。

欲知后事如何,且听下回分解。


转载

本文转载至闪客图解操作系统系列文章

Linux 0.11-shell 程序读取你的命令-43相关推荐

  1. Xp下的程序编译成linux,WinXP下打造自己的linux 0.11简易编译环境(原创)

    http://caiwei8888.blog.163.com/blog/static/3017424120101913353856/ 学习赵炯博士的<linux 0.11 内核完全注释>, ...

  2. Linux 0.11内核分析04:多进程视图

    目录 1 进程概念的引入 1.1 使用CPU的直观想法 1.2 直观用法的缺点 1.3 直观用法的改进 1.4 进程的概念 1.4.1 保存程序执行状态 1.4.2 进程与PCB 1.5 Linux ...

  3. linux 0.11 init/main.c初始化部分

    在head设置了页表.GDT和IDT之后,然后就进入了main程序,这里首先介绍一些参数: ORIG_ROOT_DEV,该参数是读取0x901FC的两个byte读取的数据,这两个byte就是boots ...

  4. LINUX 0.11内核完全剖析学习笔记-第三章内核编程语言和环境

    一.编译器 linux 0.11 集成了两种汇编器.一种是能产生16位代码的as86汇编器,使用配套的ld86链接器:另一种是GUN汇编器gas,使用GNU ld链接器俩链接产生的目标文件. 1.1 ...

  5. Linux 0.11内核分析02:系统启动

    目录 1. 内核镜像的构建 1.1 内核源码结构 1.1.1 boot 1.1.2 fs 1.1.3 include 1.1.4 init 1.1.5 kernel 1.1.6 lib 1.1.7 m ...

  6. linux 0.11 源码学习(十四)

    文件系统综述 linux 文件系统是基于MINIX 1.0文件系统,这部分的代码量是整个内核里最大的,但代码结构对应着MINIX文件系统的构成,还是比较清晰易读的. MINIX文件系统 MINIX的文 ...

  7. linux 0.11 内核学习 -- bootsect.s, 万里长征第一步

    呵呵,终于将linux 0.11 下面的boot文件夹下的三个文件读完,下面是相关注释,没有汇编基础的人也是可以读的.废话少说,下面就是linux的源码了. 参考资料 Linux内核完全注释.pdf ...

  8. Linux 0.11 fork 函数(二)

    Linux 0.11 系列文章 Linux 0.11启动过程分析(一) Linux 0.11 fork 函数(二) Linux0.11 缺页处理(三) Linux0.11 根文件系统挂载(四) Lin ...

  9. Linux与shell环境,Linux 环境及 Shell 程序

    Linux 环境及 Shell 程序 View 98 Download 1 Embed Size (px) 344 x 292429 x 357514 x 422599 x 487 DESCRIPTI ...

  10. Linux 0.11 实验环境搭建与调试

    缘起 之前我写过一篇博文:Linux 0.11 实验环境搭建 本以为有了这个环境(gcc-3.4 & gdb-6.8),就可以调试无忧了.谁知遇到了以下问题: (1)用 gdb 调试 main ...

最新文章

  1. Revit二次开发之“取得所选元素的族名称”
  2. 使用curl操作InfluxDB
  3. 每日一笑 | 3 X 4 = ?
  4. data在python_python-data-英语单词
  5. flask中数据库的基本操作-增删改查【备忘】
  6. backward理解
  7. java.lang.IncompatibleClassChangeError:
  8. java的oracle事务回滚_Oracle事务处理
  9. matlab求最大公约数和最小公倍数
  10. python 今日头条 控制手机_你知道Python脚本控制安卓手机可以用来做什么吗?
  11. 阿里云云计算 8 ECS的实例规格
  12. 干货来袭!几行代码实现pdf添加水印和去除水印
  13. Silverlight4 如何实现DataContextChanged事件
  14. 关于zip命令的使用问题
  15. 微型计算机经历了那几个阶段,微型计算机的发展经历了哪几个阶段,各阶段微处理器的主要特征是什么...
  16. 将业务做到遍布全球,需要多大的IT运维团队?
  17. 如何将c语言程序变成应用,C语言代码转换为应用程序
  18. 歌词欣赏《一程山水一程歌》
  19. 深度学习笔记(三十一)三维卷积及卷积神经网络
  20. 闭关修炼——one——struts2

热门文章

  1. 2022好用的便签记事日程提醒软件有哪些
  2. arcgis制作瓦片地图_利用ArcGISDesktop制作【地图瓦片包(TPK切片包)】的技术流程及优化...
  3. 双机热备软件 Pacemaker和Keepalived
  4. “不限量”只是幌子!流量卡到底哪家最划算?
  5. 【CyberSecurityLearning 12】数据链路层 及 交换机工作原理与配置
  6. 无人驾驶学习笔记-NDT 配准
  7. 初识Kinect之二
  8. Tornado get/post请求异步处理框架分析
  9. 这篇文章能让你吃透SVG
  10. 怎样打开VOIP与SIP