Linux 0.11-shell 程序读取你的命令-43
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相关推荐
- Xp下的程序编译成linux,WinXP下打造自己的linux 0.11简易编译环境(原创)
http://caiwei8888.blog.163.com/blog/static/3017424120101913353856/ 学习赵炯博士的<linux 0.11 内核完全注释>, ...
- Linux 0.11内核分析04:多进程视图
目录 1 进程概念的引入 1.1 使用CPU的直观想法 1.2 直观用法的缺点 1.3 直观用法的改进 1.4 进程的概念 1.4.1 保存程序执行状态 1.4.2 进程与PCB 1.5 Linux ...
- linux 0.11 init/main.c初始化部分
在head设置了页表.GDT和IDT之后,然后就进入了main程序,这里首先介绍一些参数: ORIG_ROOT_DEV,该参数是读取0x901FC的两个byte读取的数据,这两个byte就是boots ...
- LINUX 0.11内核完全剖析学习笔记-第三章内核编程语言和环境
一.编译器 linux 0.11 集成了两种汇编器.一种是能产生16位代码的as86汇编器,使用配套的ld86链接器:另一种是GUN汇编器gas,使用GNU ld链接器俩链接产生的目标文件. 1.1 ...
- 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 ...
- linux 0.11 源码学习(十四)
文件系统综述 linux 文件系统是基于MINIX 1.0文件系统,这部分的代码量是整个内核里最大的,但代码结构对应着MINIX文件系统的构成,还是比较清晰易读的. MINIX文件系统 MINIX的文 ...
- linux 0.11 内核学习 -- bootsect.s, 万里长征第一步
呵呵,终于将linux 0.11 下面的boot文件夹下的三个文件读完,下面是相关注释,没有汇编基础的人也是可以读的.废话少说,下面就是linux的源码了. 参考资料 Linux内核完全注释.pdf ...
- Linux 0.11 fork 函数(二)
Linux 0.11 系列文章 Linux 0.11启动过程分析(一) Linux 0.11 fork 函数(二) Linux0.11 缺页处理(三) Linux0.11 根文件系统挂载(四) Lin ...
- Linux与shell环境,Linux 环境及 Shell 程序
Linux 环境及 Shell 程序 View 98 Download 1 Embed Size (px) 344 x 292429 x 357514 x 422599 x 487 DESCRIPTI ...
- Linux 0.11 实验环境搭建与调试
缘起 之前我写过一篇博文:Linux 0.11 实验环境搭建 本以为有了这个环境(gcc-3.4 & gdb-6.8),就可以调试无忧了.谁知遇到了以下问题: (1)用 gdb 调试 main ...
最新文章
- Revit二次开发之“取得所选元素的族名称”
- 使用curl操作InfluxDB
- 每日一笑 | 3 X 4 = ?
- data在python_python-data-英语单词
- flask中数据库的基本操作-增删改查【备忘】
- backward理解
- java.lang.IncompatibleClassChangeError:
- java的oracle事务回滚_Oracle事务处理
- matlab求最大公约数和最小公倍数
- python 今日头条 控制手机_你知道Python脚本控制安卓手机可以用来做什么吗?
- 阿里云云计算 8 ECS的实例规格
- 干货来袭!几行代码实现pdf添加水印和去除水印
- Silverlight4 如何实现DataContextChanged事件
- 关于zip命令的使用问题
- 微型计算机经历了那几个阶段,微型计算机的发展经历了哪几个阶段,各阶段微处理器的主要特征是什么...
- 将业务做到遍布全球,需要多大的IT运维团队?
- 如何将c语言程序变成应用,C语言代码转换为应用程序
- 歌词欣赏《一程山水一程歌》
- 深度学习笔记(三十一)三维卷积及卷积神经网络
- 闭关修炼——one——struts2
热门文章
- 2022好用的便签记事日程提醒软件有哪些
- arcgis制作瓦片地图_利用ArcGISDesktop制作【地图瓦片包(TPK切片包)】的技术流程及优化...
- 双机热备软件 Pacemaker和Keepalived
- “不限量”只是幌子!流量卡到底哪家最划算?
- 【CyberSecurityLearning 12】数据链路层 及 交换机工作原理与配置
- 无人驾驶学习笔记-NDT 配准
- 初识Kinect之二
- Tornado get/post请求异步处理框架分析
- 这篇文章能让你吃透SVG
- 怎样打开VOIP与SIP