文章目录

  • 用键盘输入一条命令
    • 1、读队列 read_q 里的字符是什么时候放进去的
    • 2、放入 secondary 队列之后
  • shell程序读取命令

用键盘输入一条命令

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

name:flash
age:28
language:java

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

$ cat info.txt | wc -l
3

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

我们先从最初始的状态开始说起。最初始的状态,电脑屏幕前只有这么一段话:

[root@linux] $

然后,我们按下按键 ‘c’,将会变成这样:

[root@linux] $ c

再按下’a’:

[root@linux] $ ca

再依次按下:

[root@linux] $ cat info.txt | wc -l

今天就要解释这个看起来十分"正常"的过程。凭什么我们按下键盘后,屏幕上就会出现如此的变化呢?老天爷规定的么?

我们就从按下键盘上的 ‘c’ 键开始说起

首先,得益于系列之五中控制台初始化讲述的一行代码:

// console.c
void con_init(void) {...set_trap_gate(0x21,&keyboard_interrupt);...
}

我们成功将键盘中断绑定在了 keyboard_interrupt 这个中断处理函数上,也就是说当我们按下键盘 ‘c’ 时,CPU 的中断机制将会被触发,最终执行到这个 keyboard_interrupt 函数中:

// keyboard.s
keyboard_interrupt:...// 读取键盘扫描码inb $0x60,%al...// 调用对应按键的处理函数call *key_table(,%eax,4)...// 0 作为参数,调用 do_tty_interruptpushl $0call do_tty_interrupt...

很简单,首先通过 IO 端口操作,从键盘中读取了刚刚产生的键盘扫描码,就是刚刚按下 ‘c’ 的时候产生的键盘扫描码。随后,在 key_table 中寻找不同按键对应的不同处理函数,比如普通的一个字母对应的字符 ‘c’ 的处理函数为 do_self,该函数会将扫描码转换为 ASCII 字符码,并将自己放入一个队列里,我们稍后再说这部分的细节。

接下来,就是调用 do_tty_interrupt 函数,见名知意就是处理终端的中断处理函数,注意这里传递了一个参数 0。

我们接着探索,打开 do_tty_interrupt 函数:

// tty_io.c
void do_tty_interrupt(int tty) {copy_to_cooked(tty_table+tty);
}void copy_to_cooked(struct tty_struct * tty) {...
}

这个函数几乎什么都没做,将 keyboard_interrupt 传入的参数 0,作为 tty_table 的索引,找到 tty_table 中的第 0 项作为下一个函数的入参,仅此而已。

tty_table 是终端设备表,在 Linux 0.11 中定义了三项,分别是控制台串行终端 1串行终端 2

// tty.h
struct tty_struct tty_table[] = {{{...},0,          /* initial pgrp */0,          /* initial stopped */con_write,{0,0,0,0,""},       /* console read-queue */{0,0,0,0,""},       /* console write-queue */{0,0,0,0,""}        /* console secondary queue */},{...},{...}
};

我们用的往屏幕上输出内容的终端,就是 0 号索引位置处的控制台终端,所以我将另外两个终端定义的代码省略掉了。

tty_table 终端设备表中的每一项结构,是 tty_struct,用来描述一个终端的属性:

struct tty_struct {struct termios termios;int pgrp;int stopped;void (*write)(struct tty_struct * tty);struct tty_queue read_q;struct tty_queue write_q;struct tty_queue secondary;
};struct tty_queue {unsigned long data;unsigned long head;unsigned long tail;struct task_struct * proc_list;char buf[TTY_BUF_SIZE];
};

说说其中较为关键的几个:

termios 是定义了终端的各种模式,包括读模式、写模式、控制模式等,这个之后再说。

void (*write)(struct tty_struct * tty) 是一个接口函数,在刚刚的 tty_table 中我们也可以看出被定义为了 con_write,也就是说今后我们调用这个 0 号终端的写操作时,将会调用的是这个 con_write 函数,这不就是接口思想么。

还有三个队列分别为读队列 read_q写队列 write_q 以及一个辅助队列 secondary

这些有什么用,我们通通之后再说,跟着我接着看

// tty_io.c
void do_tty_interrupt(int tty) {copy_to_cooked(tty_table+tty);
}void copy_to_cooked(struct tty_struct * tty) {signed char c;while (!EMPTY(tty->read_q) && !FULL(tty->secondary)) {// 从 read_q 中取出字符GETCH(tty->read_q,c);...// 这里省略了一大坨行规则处理代码...// 将处理过后的字符放入 secondaryPUTCH(c,tty->secondary);}wake_up(&tty->secondary.proc_list);
}

展开 copy_to_cooked 函数我们发现,一个大体的框架已经有了。

在 copy_to_cooked 函数里就是个大循环,只要读队列 read_q 不为空,且辅助队列 secondary 没有满,就不断从 read_q 中取出字符,经过一大坨的处理,写入 secondary 队列里

否则,就唤醒等待这个辅助队列 secondary 的进程,之后怎么做就由进程自己决定。

我们接着看,中间的一大坨处理过程做了什么事情呢?这一大坨有太多太多的 if 判断,但都是围绕着同一个目的,我们举其中一个简单的例子:

#define IUCLC   0001000
#define _I_FLAG(tty,f)  ((tty)->termios.c_iflag & f)
#define I_UCLC(tty) _I_FLAG((tty),IUCLC)void copy_to_cooked(struct tty_struct * tty) {...// 这里省略了一大坨行规则处理代码if (I_UCLC(tty))c=tolower(c);...
}

简单说,就是通过判断 tty 中的 termios,来决定对读出的字符 c 做一些处理。在这里,就是判断 termios 中的 c_iflag 中的第 4 位是否为 1,来决定是否要将读出的字符 c 由大写变为小写。这个 termios 就是定义了终端的模式

struct termios {unsigned long c_iflag;      /* input mode flags */unsigned long c_oflag;      /* output mode flags */unsigned long c_cflag;      /* control mode flags */unsigned long c_lflag;      /* local mode flags */unsigned char c_line;       /* line discipline */unsigned char c_cc[NCCS];   /* control characters */
};

比如刚刚的是否要将大写变为小写,是否将回车字符替换成换行字符,是否接受键盘控制字符信号如 ctrl + c 等。

这些模式不是 Linux 0.11 自己乱想出来的,而是实现了 POSIX.1 中规定的 termios 标准:

好了,我们目前可以总结出,按下键盘后做了什么事情:

这里我们应该产生几个疑问:

1、读队列 read_q 里的字符是什么时候放进去的

还记不记得最开始讲的 keyboard_interrupt 函数,我们有一个方法没有展开讲:

// keyboard.s
keyboard_interrupt:...// 读取键盘扫描码inb $0x60,%al...// 调用对应按键的处理函数call *key_table(,%eax,4)...// 0 作为参数,调用 do_tty_interruptpushl $0call do_tty_interrupt...

就是这个 key_table,我们将其展开:

// keyboard.s
key_table:.long none,do_self,do_self,do_self  /* 00-03 s0 esc 1 2 */.long do_self,do_self,do_self,do_self   /* 04-07 3 4 5 6 */....long do_self,do_self,do_self,do_self   /* 20-23 d f g h */...

可以看出,普通的字符 abcd 这种,对应的处理函数是 do_self,我们再继续展开:

// keyboard.s
do_self:...// 扫描码转换为 ASCII 码lea key_map,%ebx1: movb (%ebx,%eax),%al...// 放入队列call put_queue

可以看到最后调用了 put_queue 函数,顾名思义放入队列,看来我们要找到答案了,继续展开:

// tty_io.c
struct tty_queue * table_list[]={&tty_table[0].read_q, &tty_table[0].write_q,&tty_table[1].read_q, &tty_table[1].write_q,&tty_table[2].read_q, &tty_table[2].write_q
};
// keyboard.s
put_queue:...movl table_list,%edx # read-queue for consolemovl head(%edx),%ecx...

可以看出,put_queue 正是操作了我们 tty_table 数组中的零号位置,也就是控制台终端 tty 的 read_q 队列,进行入队操作。

答案揭晓了,那我们的整体流程图也可以再丰富起来:

2、放入 secondary 队列之后

按下键盘后,一系列代码将我们的字符放入了 secondary 队列中,然后呢?

这就涉及到上层进程调用终端的读函数,将这个字符取走了。上层经过库函数、文件系统函数等,最终会调用到 tty_read 函数,将字符从 secondary 队列里取走。

// tty_io.c
int tty_read(unsigned channel, char * buf, int nr) {...GETCH(tty->secondary,c);...
}

取走后要干嘛,那就是上层应用程序去决定的事情了。

假如要写到控制台终端,那上层应用程序又会经过库函数、文件系统函数等层层调用,最终调用到 tty_write 函数:

// tty_io.c
int tty_write(unsigned channel, char * buf, int nr) {...PUTCH(c,tty->write_q);...tty->write(tty);...
}

这个函数首先会将字符 c 放入 write_q 这个队列,然后调用 tty 里设定的 write 函数。

终端控制台这个 tty 我们之前说了,初始化的 write 函数是 con_write,也就是 console 的写函数:

// console.c
void con_write(struct tty_struct * tty) {...
}

这个函数在系列之五中控制台初始化提到了,最终会配合显卡,在我们的屏幕上输出我们给出的字符。我们的图又可以补充完整了:

核心点就是三个队列 read_qsecondary 以及 write_q

  1. read_q 是键盘按下按键后,进入到键盘中断处理程序 keyboard_interrupt 里,最终通过 put_queue 函数字符放入 read_q 这个队列。

  2. secondary 是 read_q 队列里的未处理字符,通过 copy_to_cooked 函数,经过一定的 termios 规范处理后,将处理过后的字符放入 secondary。(处理过后的字符就是成"熟"的字符,所以叫 cooked,是不是很形象?)

  3. 进程通过 tty_read 从 secondary 里读字符,通过 tty_write 将字符写入 write_q,最终 write_q 中的字符可以通过 con_write 这个控制台写函数,将字符打印在显示器上。

这就完成了从键盘输入到显示器输出的一个循环,也就是本节所讲述的内容。好了,现在我们已经成功做到可以把这样一个字符串输入并回显在显示器上了:

[root@linux] $ cat info.txt | wc -l

shell程序读取命令

上文讲了如何用键盘将字符串输入到显示器,那 shell 程序具体是如何读入这个字符串,读入后又是怎么处理的呢?

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

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

  2. 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 将线程阻塞,等待被唤醒。其中 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;

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

进程被调度了,什么时候被唤醒呢?当我们再次按下键盘,使得 secondary 队列中有字符时,也就打破了为空的条件,此时就应该将之前的进程唤醒了,这在上节讲过。

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

总体流程:

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

你管这叫操作系统源码(十五)相关推荐

  1. 你管这叫操作系统源码(十四)

    文章目录 shell程序跑起来了 操作系统启动完毕 总结 shell程序跑起来了 shell只是个标准,具体的是实现有很多,比如Ubuntu18.04上,具体的shell实现是bash: ~$ ech ...

  2. Netty源码(十五):Recycler工具类

    1. Recycler工具类的使用 Recycler为了避免我们重复的创建对象,使用对象池将我们使用过的数据保存起来,下一次就可以拿出来使用 public class TestRecycler {// ...

  3. 第6章 RTX 操作系统源码方式移植

    以下内容转载自安富莱电子: http://forum.armfly.com/forum.php 本章教程为大家将介绍 RTX 操作系统源码方式移植,移植工作比较简单,只需要用户添加需要的源码文件即可, ...

  4. 【RTX操作系统教程】第6章 RTX操作系统源码方式移植

    原文来源:http://forum.armfly.com/forum.php?mod=viewthread&tid=16616&highlight=RTX%B2%D9%D7%F7%CF ...

  5. 你管这叫操作系统源码(五)

    你管这叫操作系统源码之五 控制台初始化tty_init 时间初始化time_init 控制台初始化tty_init 按下键盘后为什么屏幕上就会有输出? void main(void) {...mem_ ...

  6. 你管这叫操作系统源码(二)

    文章目录 保护模式前的最后一次折腾内存 段寄存器的历史包袱 进入保护模式 资料 保护模式前的最后一次折腾内存 上篇品读完第一个操作系统源码文件bootsect.s,之后便跳转到0x90200地址开始执 ...

  7. 你管这叫操作系统源码(一)

    文章目录 最开始的两行代码 自己给自己挪个地 做好最基础的准备工作 硬盘里其他部分也放到内存 资料 最开始的两行代码 话不多说,直奔主题.当你按下开机键的那一刻,在主板上提前写死的固件程序 BIOS ...

  8. 你管这叫操作系统源码(七)

    你管这叫操作系统源码之七 新进程诞生全局概述 move_to_user_mode fork init pause 从内核态到用户态 让进程无法逃出用户态 内核态与用户态的本质-特权级 特权级转换方式 ...

  9. 你管这叫操作系统源码(九)

    你管这叫操作系统源码之九 通过fork看一次系统调用 小结 fork中进程基本信息的复制 通过fork看一次系统调用 有了前两篇文章的铺垫,我们终于可以回到主流程看看fork函数了.这个fork函数稍 ...

  10. 敢写操作系统源码系列?我就等着看你笑话!

    闪客同学告诉我说,他要在公众号搞一个系列,带着大家像读小说一样品读Linux 0.11的核心代码,我立马给他泼了一盆冷水: 操作系统这么枯燥的东西,怎么可能写成小说那样? 写起来吃力又不讨好,你哼哧哼 ...

最新文章

  1. mysql 基于集_一种基于记录集查找特定行的方法_MySQL
  2. Python操作git
  3. Huffman(哈夫曼)编码--又称最佳编码(最有效的二进制编码)
  4. SpringBoot_web开发-扩展与全面接管SpringMVC
  5. 英利1500伏光伏组件系列亮相美国
  6. 275. H-Index II 递增排序后的论文引用量
  7. (TI xDM)SSCR Module—Shared Scratch Memory
  8. [翻译]XNA在线俱乐部网站即将开站!
  9. 阻止系统自动睡眠的小软件,附C#制作过程
  10. Xshell6、xftp资源,舒服!!(自行下载)
  11. Java文件操作——简单文件搜索优化版本Lambda优化
  12. 移动拼图游戏(八数码问题)A*版
  13. 令人发指的关于方法重载和方法重写的一些理解(多态)
  14. 一个神奇的分布式计算框架:jini
  15. java wps 二次开发,Wps二次开发(POI)
  16. AcWing238. 银河英雄传说
  17. 北大软微2021计算机考研难度,2021北京大学软微计算机智能科技方向考研报录情况及备考经验分享...
  18. 云计算课程大纲,Linux云计算运维课程视频
  19. 1166 Summit – PAT甲级真题
  20. QQ音乐vip免费下载歌曲链接和全民k歌歌曲下载链接

热门文章

  1. 【R语言】如何进行英文分词统计(以《爱丽丝漫游奇境》词频统计为例)(20年3月22日复习笔记)
  2. [bzoj2827]千山鸟飞绝【splay】
  3. 怎么把记事本内容导出python_怎么把记事本内容导出python
  4. php 中%3cspan%3e,隐藏第三方网站统计图标
  5. Debian 7 安装vim
  6. 游戏美术和策划,你感兴趣吗
  7. python数据分析之pandas
  8. php判断是否submit,submit什么意思 php提交表单时判断 if$_POST[submit]与 ifisset$_POST[submit] 的区别...
  9. 卡拉赞服务器延迟,卡拉赞开荒详细功略(前门)
  10. [转帖]江湖高手专用的“隐身术”:图片隐写技术