用键盘输入一条命令

0626440fbbaa3fa6fd2d570411ea6aaf.gif

作者 | 闪客

来源 | 低并发编程

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

name:flash
age:28
language:java

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

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

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

我们先从最初始的状态开始说起。

最初始的状态,电脑屏幕前只有这么一段话。

[root@linux0.11]

然后,我们按下按键 'c',将会变成这样。

[root@linux0.11] c

我们再按下 'a'

[root@linux0.11] ca

接下来,我们再依次按下 't'、空格、'i' 等等,才变成了这样。

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

我们今天就要解释这个看起来十分"正常"的过程。

凭什么我们按下键盘后,屏幕上就会出现如此的变化呢?老天爷规定的么?

我们就从按下键盘上的 'c' 键开始说起。

首先,得益于 第16回 | 控制台初始化 tty_init 中讲述的一行代码。

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

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

我们来到 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 队列里。

fc2458899e0d59350e3928d09bd24b96.png

否则,就唤醒等待这个辅助队列 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 标准,具体可以参见:

https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap11.html#tag_11

239e006e37ab8af6569a1afc96e29d3c.png

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

cc1def34324442ce0a11b6f5cfd81602.png

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

一、读队列 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 队列,进行入队操作。

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

212776a6af9e525c6a51f8bea02be217.png

二、放入 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.
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) {...
}

最终会配合显卡,在我们的屏幕上输出我们给出的字符。

ecbc46ae57a5490cca05fdc2a93dd131.png

那我们的图又可以补充了。

4bbe89ff3e2dc4a546de09d730cc6bd1.png

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

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

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

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

这就完成了从键盘输入到显示器输出的一个循环,也就是本回所讲述的内容。

好了,现在我们已经成功做到可以把这样一个字符串输入并回显在显示器上了。

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

那么接下来,shell 程序具体是如何读入这个字符串,读入后又是怎么处理的呢?

4d55c4286454b57e252893566c8c7d21.gif

往期推荐

read 文件一个字节实际会发生多大的磁盘IO?

Docker 容器为什么傲娇?全靠镜像撑腰!

Redis 内存满了怎么办?这样置才正确!

云原生的本手、妙手和俗手

ab6777d37b4300b15185d0c5e02c8722.gif

点分享

0f15a53252aa08fa0ceb6934689a29c7.gif

点收藏

b41c338173dceae34451307208230710.gif

点点赞

fef44a66c40f745bc6547091ca700f46.gif

点在看

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/511447.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Redis 7.0 Multi Part AOF的设计和实现

简介:本文将详解Redis中现有AOF机制的一些不足以及Redis 7.0中引入的Multi Part AOF的设计和实现细节。 Redis 作为一种非常流行的内存数据库,通过将数据保存在内存中,Redis 得以拥有极高的读写性能。但是一旦进程退出,Redis 的数…

面向B端算法实时业务支撑的工程实践

简介:在营销场景下,算法同学会对广告主提供个性化的营销工具,帮助广告主更好的精细化营销,在可控成本内实现更好的ROI提升。我们在这一段时间支持了多个实时业务场景,比如出价策略的实时化预估、关键词批量服务同步、实…

中间表是如何被消灭的?

作者 | 不吃西红柿来源 | CSDN博客中间表的产生中间表是数据库中专门存放中间计算结果的数据表,往往是为了前端查询统计更快或更方便而在数据库中建立的汇总表,由于是由原始数据加工而成的中间结果,因此被称为中间表。在某些大型机构中&#…

自定义控件android.r,Android控件架构与自定义控件

前言最近在开发的路上越走越远了,每天在看各位大神公众号更新内容是自定义View的时候,一些小的内容有点模具,决定回过头来温习一下过往的内容。此篇也是根据android群英传来总结的一篇文章。1 Android控件架构Android的每个控件都是占一块矩形…

基于 PTS 压测轻松玩转问题诊断

简介:性能测试 PTS(Performance Testing Service)是具备强大的分布式压测能力的 SaaS 压测平台,可模拟海量用户的真实业务场景,全方位验证业务站点的性能、容量和稳定性。 作者:智云 为什么要做压测的问题…

阿里云开源业内首个应用多活项目 AppActive,与社区共建云原生容灾标准

简介:继高可用架构团队的 Sentinel、Chaosblade 开源后,第三个重磅高可用产品:应用多活 AppActive 正式开源,形成高可用的三架马车,帮助企业构建稳定可靠的企业级生产系统,提高企业面对容灾、容错、容量等问…

清晰还原31年前现场,火山引擎超清修复Beyond经典演唱会

7月3日晚,抖音携手环球音乐旗下厂牌宝丽金,直播经过火山引擎超清修复的Beyond Live1991生命接触演唱会及纪念音乐会精选内容,吸引了超1.4亿人次观看。 Beyond是一支成立于1983年的摇滚乐队,随着粤语音乐的兴起,Beyond…

如何定位并修复 HttpCore5 中的 HTTP2 流量控制问题

简介:开篇吹一波阿里云性能测试服务 PTS,PTS 在 2021 年 5 月份已经上线了对 HTTP2 协议的支持(底层依赖 httpclient5),在压测时会通过与服务端协商的结果来决定使用 HTTP1.1 或者 HTTP2 协议。 作者:风起…

全链路灰度之 RocketMQ 灰度

简介:本文将以上次介绍过的《如何用 20 分钟就能获得同款企业级全链路灰度能力?》中的场景为基础,来进一步介绍消息场景的全链路灰度。 作者:亦盏 之前的系列文章中,我们已经通过全链路金丝雀发布这个功能来介绍了 M…

普洛斯数据中心发布DC Brain系统,科技赋能智慧化运营管理

7月5日,普洛斯数据中心发布了DC Brain智慧化运营管理系统。该系统由普洛斯历时两年自主研发,契合现代化数据中心平台的发展趋势。目前已应用于普洛斯旗下数据中心,并有对外输出的成功案例,面向行业,赋能中小规模运营商…

mi6 android版本,小米6:我依旧是王,MIUI10.4.2稳定版与AndroidP同时到来

原标题:小米6:我依旧是王,MIUI10.4.2稳定版与AndroidP同时到来小米6作为小米数字系列最受欢迎的机型之一,从上市到下架热度一直未减,它也是众多米粉心目中小米数字系列最成功的机型没有之一。但是,再怎么讲…

如何利用 AHAS 保障 Web 服务稳如磐石?

简介:应用高可用服务 AHAS (Application High Availability Service) 是经阿里巴巴内部多年高可用体系沉淀下来的云产品,基于阿里开源流控降级组件 Sentinel,以流量与容错为切入点,从流量控制、不稳定调用隔离、熔断降级、热点流量…

KubeDL HostNetwork:加速分布式训练通信效率

简介:ubeDL 为分布式训练作业带来了 HostNetwork 网络模式,支持计算节点之间通过宿主机网络相互通信以提升网络性能,同时适应 RDMA/SCC 等新型高性能数据中心架构的网络环境,此外,KubeDL 针对 HostNetwork 模式带来的 …

阿里云容器服务差异化 SLO 混部技术实践

简介:阿里巴巴在“差异化 SLO 混合部署”上已经有了多年的实践经验,目前已达到业界领先水平。所谓“差异化 SLO”,就是将不同类型的工作负载混合运行在同一节点,充分利用工作负载对资源 SLO 需求特征的不同,提升资源整…

鸿蒙系统被烧毁,华为鸿蒙操作系统再次被质疑 国产是原罪

国产是原罪,国际驰名双标现象严重,为何对待国产的东西要格外刻薄?华为手机版鸿蒙系统正式发布,但却引来一片嘲讽,这些人简直是刷新三观。如果一个产品是相同的价格,国产的用料更足但是还不够成熟&#xff1…

云原生落地大爆发,企业和开发者如何把握先机?

简介:回顾 2021 年,云原生有哪些重大技术突破?云原生时代下开发模式、技术标准等不断变化,企业应该如何落地云原生?开发者应掌握哪些能力?本文将为你一一解说。 作者:伍杏玲 随着云计算产业走…

Gartner发布中国人工智能软件市场指南,激烈竞争下走向差异化

作者 | Gartner高级研究总监 方琦 供稿 | Gartner 人工智能(AI)软件是中国企业投资和关注的重点。中国的AI软件市场在持续快速增长,竞争非常激烈。AI软件企业使用一种或多种AI技术(见图1),帮助企业解读事件…

华为Mate是鸿蒙系统,华为mate30概念新机:4500mAh+5G网络+“鸿蒙”系统 这才是华为...

对于现在的华为手机来说,很多消费者的态度不一样,有的觉得华为手机性价比不高,有的则觉得华为手机质量很好,配置也非常的高。而我觉得,华为手机最值得敬佩,敬佩的是华为不屈的精神,敬佩的是远见…

平安保险基于 SPI 机制的 RocketMQ 定制化应用

简介:本文讲讲述平安保险为何选择 RocketMQ,以及在确定使用消息中间件后,又是如何去选择哪款消息中间件的。 作者:孙园园|平安人寿资深开发 为什么选用 RocketMQ 首先跟大家聊聊我们为什么会选用 RocketMQ&#xff…

Redis 内存优化神技,小内存保存大数据

作者 | 码哥呀来源 | 码哥字节这次跟大家分享一些优化神技,当你面试或者工作中你遇到如下问题,那就使出今天学到的绝招,一招定乾坤!❝如何用更少的内存保存更多的数据?我们应该从 Redis 是如何保存数据的原理展开&…