大狗哥传奇

  • 首页

  • 关于

  • 标签

  • 分类

  • 归档

  • 搜索

有限状态机

发表于 2020-06-15 更新于 2020-06-30 分类于 计算机基础

概念

有限状态机简称状态机,状态机表示有限个状态以及在这些状态之间的转移和动作等行为的数学模型。通俗的描述就是状态机定义了一套状态变更的流程:状态机包括一个状态集合,定义当状态机处于某一个状态的时候它所能接收的事件以及可执行的行为,执行完成后状态机所处的状态。状态机包含以下几个重要的元素:

  • State:状态。一个标准的状态机最少包括两个状态:初态和终态。初态是状态机初始化后所处的状态,而终态就是状态机结束时所处的状态。其他的状态都是一些流转中停留的状态。
  • Event:事件。执行某个操作的触发器或者口令
  • Action:行为。状态变更所要执行的具体行为
  • Transition:变更。一个状态接收一个事件执行了某些行为到达了一个状态的过程。它表示状态机的运转流程。

应用场景

状态机主要的应用场景就是流程控制。一个状态机定义以后,在某个状态下就只接收固定的 Event,也就是执行指定的操作,这样流程就能按照预期定义的那样流转,不会出现乱入的情况,执行了一些在某个状态下不允许执行的操作。

示例

编写一个程序,以每行一个单词的形式打印其输入

我们使用状态机的思想来解决这个问题

状态集:当前字符在单词内记做 IN,当前字符不在单词内记做 OUT,初态为 OUT 当前状态为 IN 时:若当前字符为空白字符(空格,制表,换行符),则输出换行并改变状态为 OUT,否则输出字符并保持状态为 IN 当前状态为 OUT 时:若当前字符为空白字符(空格,制表,换行符),则保持状态为 OUT,否则输出字符并改变状态为 IN

voicexml解析器实现相关

发表于 2020-06-10 更新于 2020-06-30 分类于 ivr

概述

IVR 系统的测试一般需要电话或软电话,拨号经由呼叫中心平台进入 IVR 系统进行测试,这种测试方式比较慢,且无法进行自动化测试。呼叫中心平台与 IVR 系统的交互使用VoiceXML标准协议,通过编写一个简易的 VoiceXML 解析器,实现对 IVR 系统的模拟请求与根据返回报文自动执行。

指令集

通过模仿计算机组成原理的相关知识,我们将每个标签转化为一个由操作码,操作数(一个或多个)的基本操作指令。

精简的 VoiceXML 报文示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<vxml version="2.1">
<var name="_avayaExitReason" expr="''"/>
<var name="_avayaExitInfo1" expr="''"/>
<var name="_avayaExitInfo2" expr="''"/>
<var name="_avayaExitCustomerId" expr="''"/>
<var name="_avayaExitPreferredPath" expr="'1'"/>
<var name="_avayaExitTopic" expr="''"/>
<var name="_avayaExitParentId" expr="''"/>
<catch event="error.runtime">
<goto next="example"/>
</catch>
<form>
<block>
<throw event="error.runtime.Exception"/>
</block>
</form>
<block>
</block>
</vxml>

上述报文,我们可以定义出以下操作指令

  • [ "var" , "_avayaExitReason" , "''" ]
  • [ "catch" , "error.runtime" , 0 ]
  • [ "goto" , "example" ]
  • [ "form" , 0 ]
  • [ "block" ]
  • [ "throw" , "error.runtimeException" ]

vxml 中标签从上向下顺序执行,若子标签内有其他标签,会先进入子标签内执行,即以深度优先遍历的方式生成指令集。有些标签执行需要满足条件,若catch,需要当前有对应的事件抛起时,才会进入标签内执行,所以我们还需要在catch处,计算当条件不满足时,下一条指令的位置。按照上文的 demo 报文,我们可以生成如下报文;

  1. var _avayaExitReason ''
  2. var _avayaExitInfo1 ''
  3. var _avayaExitInfo2 ''
  4. var _avayaExitCustomerId ''
  5. var _avayaExitPreferredPath '1'
  6. var _avayaExitTopic ''
  7. var _avayaExitParentId ''
  8. catch error.runtime 9
  9. goto example
  10. form 12
  11. block 12
  12. throw error.runtime.Exception
  13. end

当报文比较复杂时,我们很难从生成的指令集中很好的观察流程走向,且难以观察程序的层级结构,一些特殊标签,例如 catch(捕获当前标签内抛出的事件,若没有合适的 catch 去处理,则转交父标签处理) 标签的功能难以实现。

广度遍历优先

所以我们使用广度优先遍历的方式去生成指令集,在遇到有子标签的情况,我们插入一条Call指令,以调用子程序的方式去解释执行子标签,同时在子标签指令集尾部,插入 Return指令,使其返回调用子程序处。通过这种方式产生的报文如下:

  1. var _avayaExitReason ''
  2. var _avayaExitInfo1 ''
  3. var _avayaExitInfo2 ''
  4. var _avayaExitCustomerId ''
  5. var _avayaExitPreferredPath '1'
  6. var _avayaExitTopic ''
  7. var _avayaExitParentId ''
  8. catch error.runtime
  9. call 12
  10. form
  11. call 14
  12. end
  13. goto example
  14. return 9
  15. block 12
  16. call 17
  17. return 11
  18. throw error.runtime.Exception
  19. return 16

这样的报文结构清晰,容易理解,且可以使用栈来实现子程序的调用。例如:我们可以在执行 call 指令时,向下扫描所有 catch,直到 return,这样我们就可以得到当前作用域的所有 catch 事件

下面是示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
private void scan(List<Object[]> cmd, DOMElement tag, int callPC) {
List<Runnable> delayScans = new ArrayList<>();
List<DOMElement> elements = tag.elements();
for (DOMElement element : elements) {

Object[] current = new Object[]{element.getTagName()};
cmd.add(current);
List<DOMElement> child = element.elements();
if (child.size() > 0) {
Object[] call = new Object[]{"call", 0};
cmd.add(call);
//记录子程序入口指针位置
int after = cmd.size();
//子程序先不扫描,先遍历当前层级的标签
delayScans.add(() -> {
call[1] = cmd.size();
scan(cmd, element, after);

});
}

}
//当前标签遍历解释,插入一条返回call的指令
cmd.add(new Object[]{"return", callPC});
//继续子程序的扫描
delayScans.forEach(Runnable::run);
}

List<Object[]> operators = new ArrayList<>();
operators.add(new Object[]{"call", 2});
operators.add(new Object[]{"end",});
scan(operators, root, 1);

中断系统

为了及时处理事件或 I/O 工作,我们需要在解析器在出现抛出事件,需要打印或者输入参数时,暂时中断现行程序,转而去执行中断服务程序,那么就要求我们设定的指令执行周期尽可能小,在一条原子性指令执行结束后去判断是否需要响应中断事件。

计算机组成笔记

发表于 2020-06-07 更新于 2020-06-30 分类于 计算机组成原理

学习计算机组成原理,是为了了解计算机底层解决各种问题的思路,以供模仿这些思路写出更好的代码。重点关注指令执行的整个过程。

名词解释

位,字节,字

  • 位 在计算机中,数据只用 0 和 1 两种表现形式,(这里只表示一个数据点,不是数字),一个 0 或者 1 占一个“位”。
  • 字节 而系统中规定 8 个“位”为一个“字节”.
  • 字长 而一个字的位数,是由机器字长决定的【系统硬件(总线、cpu 命令字位数等)】
    1. 在 16 位的系统中(比如 8086 微机) 1 字 (word)= 2 字节(byte)= 16(bit)
    2. 在 32 位的系统中(比如 win32) 1 字(word)= 4 字节(byte)=32(bit)
    3. 在 64 位的系统中(比如 win64)1 字(word)= 8 字节(byte)=64(bit)

总线

总线是连接计算机多个部件的信息传输线,是各个部件共享数据的传输介质。当多个部件与总线相连时,如果出现两个或两个以上部件同时向总线发送信息,势必导致信号冲突,传输无效。因此,在某一时刻,只允许有一个部件向总线发送信息,而多个部件可以同时从总线上接收相同的信息。

总线实际上是由许多传输线或通路组成,每条线可一位一位的传输二进制码,一串二进制码可在同一段时间内在一条传输线上逐一传输,也可同时在多条传输线同时传输,例如 16 条传输线组成的总线可以同时传输 16 位二进制代码。

根据总线传输信息的不同,可分为三类:

  1. 数据总线 数据总线用来传输各功能部件之间的数据信息,它是双向传输总线,其位数与机器字长,存储字长有关,一般为 8 位,16 位或 32 位。数据总线的位数称为数据总线宽度。如果数据总线的宽度为 8 位,指令字长为 16 位,那么 CPU 在取指阶段必须两次访问主存。
  2. 地址总线 地址总线主要用来指出数据总线上的源数据或目的数据在主存单元的地址或者 I/O 设备的地址。例如,欲从存储器读出一个数据,则 CPU 要将此数据所在存储单元的地址送到地址线上。地址总线上的代码总是用来指明 CPU 欲访问的存储单元或者 I/O 端口的地址,由 CPU 输出,单向传输。地址线的位数与存储单元的个数有关。如地址线为 20 根,则对应的存储单元个数为2202^{20}220
  3. 控制总线 由于数据总线,地址总线都是被挂载总线上的所有部件共享的,如何使各部件能在不同时刻占用总线使用权,需要依靠控制总线来完成。因此控制总线是用来发出各种控制信号的传输线。每根控制线的控制信号时固定的,它的传输是单向的。但对于控制总线整体来说,又可以认为是双向的。 常见的控制信号如下
    • 时钟:用来同步各种操作
    • 复位:初始化所有部件
    • 总线请求:表示某部件需获得总线控制权
    • 总线允许: 表示需要获得总线控制权的部件已经获得了控制权
    • 中断请求: 表示某个部件提出中断请求
    • 中断响应: 表示中断请求已被接收
    • 存储器写: 将数据总线上的额数据写至存储器的指定单元地址内
    • 存储器读: 将指定存储单元中的数据读到数据总线上
    • I/O 读:从指定 I/O 端口将数据读到数据总线上
    • I/O 写:将数据总线上的数据输出到指定的 I/O 端口内
    • 传输响应: 表示数据已被接收,或已将数据送到数据总线上

众多部件共享总线,在争夺总线使用权时,应按各部件的优先等级来解决。在通信时间上,则应按分时方式来处理,即以获得总线使用权的先后顺序分时占用总线,即哪一个部件获得使用权,此刻就由它传送,下一个部件获得使用权,接着下一时刻传送。这样一个接一个轮流交替传送。

存储器

存储器的层级结构 计算机组成笔记_2020-06-09-21-16-32.png 缓存-主存层次主要解决 CPU 和主存速度不匹配的问题

主存

计算机为了实现能按地址访问主存,主存中还必须配置两个寄存器 MAR 和 MDR。MAR(Memory Address Register)是存储地址的寄存器,其位数对于存储单元的个数(如 MAR 为 10 位,则有210=10242^{10}=1024210=1024个存储单元,记做 1K)。MDR(Memory Data Register)是存储数据的寄存器,其位数与存储字长相等。主存的工作方式是根据存储单元的地址号来实现对存储字各位的存(写入),读(取出)。

当要从存储器读出某一个信息字时,首先由 CPU 将该字的地址送到 MAR,经地址总线送至主存,然后发出读命令。主存接到读命令后,得知需将该地址单元的内容读出,变完成读操作,将该单元的内容读至数据总线上,至于该信息由 MDR 送到什么地方,这已经不是主存的任务,而是有 CPU 决定的。若要想主存存入一个信息字时,首先 CPU 将该字所在主存单元的地址经 MARD 送到地址总线,并将该信息送入 MDR,然后向主存发出写命令,主存接到写命令后,便将数据线上的信息写入到对应地址线指出的主存单元中。

主存各存储单元的空间位置是由单元地址号来表示的,而地址总线时用来指出存储单元地址号的,根据该地址号可读出或写入一个存储字,不同的机器存储字长也不同,为了满足字符处理的需要,常用 8 位二进制表示一个字节,因此存储字长都取 8 的倍数。

缓存

主存有2n2^{n}2n个可编制的字组成,每个字有唯一的 n 位地址。为了与 Cache 映射,将主存与缓存都分为若干块,每块内又包含若干个字,并使他们的快大小相同(即块内的自述相同)。这就将主存的地址分为两段:高 m 位表示主存的块地址,低 b 位表示块内地址,则2m=M2^{m}=M2m=M表示主存的块数。同样缓存的地址也分为两段:高 c 位表示主存的块地址,低 b 位表示块内地址,则2c=C2^{c}=C2c=C表示缓存块数,且 C 远小于 M。主存与缓存地址中都用 b 为表示其块内自出,即B=2bB=2{b}B=2b反映了块的大小,称 B 为块长。 任何时刻都有一些主存块处于缓存块中。CPU 欲读取主存某字时,有两种可能:一种是所需要的字已在缓存 zhong ,即可直接访问 cache(CPU 与 Cache 之间通常一次传送一个字);另外一种是所需要的字不在 cache 内,此时将该字所在准成整个字块一次调用 cache 中(cache 与主存直接时 字块 传送)。如果主存块已调用缓存块,则成该主存块与缓冲块建立了对应关系。

cache 的基本结构 计算机组成笔记_2020-06-09-21-46-46.png

cache 的模块组成

  1. cache 存储体以块为单位与主存交换信息,现代计算机一班提供多级缓存。
  2. 地址映射变换机制,将 CPU 送来的主存地址转换为 cache 地址
  3. 替换机制,当缓存内容已满时,使用替换算法决定那块缓存需要被替换掉
  4. 读写操作,读时优先从 cache 读取。写现在主要有两种方式:
    • 写直达法,即写操作时既写入 cache,也写入主存
    • 写回法,即写操作时只把数据写入 cache,但数据被替换时才写入主存

机器指令

机器语言是由一条条语句构成的,每一条语句又能准确表达某种语义。计算机就是连续执行每一机器语句而实现全自动工作的。

指令字长

指令字长取决于操作码的长度,操作数地址的长度和操作数地址的个数。通常把常用的指令(如数据传送指令,算逻辑运算指令等)设计成单子长或者短字长格式的指令。

指令格式

指令是由操作码和地址码两部分组成的,基本格式如下 计算机组成笔记_2020-06-09-23-09-06.png

  1. 操作码 用来指定指明该指令所要完成的操作,如加法,减法,传送,移位,转移等。通常,其位数反映了机器的操作种类,也即机器允许的指令条数,如操作码占 7 位,则该机器最多包含 27=1282^{7}=12827=128条指令。

  2. 地址码 地址码用来指出该指令的源操作数的地址(一个或两个),结果的地址以及下一条指令的地址。这里的地址可以时主存的地址,也可以是寄存器的地址,甚至可以时 I/O 设备的地址。

指令格式集中体现了指令系统的功能,因此在确定指令格式时,必须从以下几个方面综合考虑

  1. 操作类型:包括指令数及操作的难以程度
  2. 数据类型:确定哪些数据可以参与操作
  3. 指令格式:包括指令字长,操作码位数,地址码位数,地址个数,寻址方式类型,以及指令字长和操作码是否可变等
  4. 寻址方式: 包括指令和操作数具有哪些寻址方式
  5. 寄存器个数:寄存器的多少直接影响指令的执行时间

操作数与操作类型

数据在存储器中的存放方式

通常计算机的数据存放在存储器或寄存器中,而寄存器的位数便可反映机器字长。一般机器字长可取字节的 1、2、4、8 倍,这样便于字符处理,现代计算机机器字长发展到 32 位和 64 位。为了便于硬件实现,通常对多字节的数据在存储器的存放方式能满足 边界对齐 的要求,即所存的数据总是整数倍(半字地址是 2 的整数倍,字地址是 4 的整数倍,双字地址是 8 的整数倍),当所存数据不满足此要求时,可填充一个或多个空白字节。

操作类型

  1. 数据传送:各个存储单元之间进行读写操作
  2. 算术逻辑操作:加减乘除,与或非等
  3. 移位:算术移位,逻辑移位等
  4. 转移:
    • 无条件转移:不受任何条件的约束,可直接将程序转移到下一条需要执行指令的地址
    • 条件转移:根据当前指令的执行结果来决定是否需要转移,若条件满足,则翻译,若条件不满足,则继续按顺序执行
    • 调用与返回:在编写程序时,有些具有特定功能的程序段会被反复使用。为避免重复编写,可将这些程序段设定为独立子程序,当需要执行子程序时,只需要用子程序调用指令即可。
  5. 输入输出:对于 I/O 单独编制的计算机而言,通常舍友输入输出指令,它完成从外设中的寄存器读入一个数据到 CPU 的寄存器内,或将数据从 CPU 的寄存器输出到某外设的寄存器中。
    • 陷阱指令
  6. 其他包括等待指令,停机指令,空操作指令,开中断指令,关中断指令等。

调用子程序细节

调用指令(CALL)一般与返回指令(RETURN)配合使用。CALL 用于从当前的程序位置转至子程序的入口,RETURN 用于子程序执行完成后重新返回到源程序的断点。下图示意了调用(CALL)与返回(RETURN)指令在程序执行中的流程 计算机组成笔记_2020-06-09-23-52-28.png

需要注意以下几点

  1. 子程序可在多处被调用
  2. 每个 CALL 指令都对应一条 RETURN 指令
  3. CPU 必须记住返回地址,返回地址可存放在以下三处
    • 专用寄存器内
    • 子程序的入口地址内
    • 栈顶内。现代计算机都设有堆栈,执行 RETURN 指令后,变可自动从栈顶取出相应的返回地址

操作数类型

指令中常用的操作数类型有

  1. 地址:地址实际上也是一种数据,在许多情况下需要计算操作数的地址。这时地址可以被认为是一个无符号的整数。
  2. 数字:计算机常见的数字有定点数,浮点数和十进制数。
  3. 字符:文本或字符串是一种常见的数据类型,计算机在处理信息过程中普遍才有 ASCII 码存储字符
  4. 逻辑数据: 用于进行逻辑运算的布尔类型的数据。

寻址方式

寻址方式是指在确定本条指令的数据地址以及下一条将要执行的指令地址的方法,它与硬件结构紧密相关,寻址方式分为指令寻址和数据寻址两大类。

指令寻址

  1. 顺序寻址:通过程序计数器 PC(指令的在内存中的地址) 加 1,自动形成下一条指令的地址
  2. 跳跃寻址:将程序计算器 PC 跳跃到指定的指令地址

数据寻址

种类较多,在指令字中必须设一字段来表面当前属于哪一种寻址方式。指令的地址码字段通常都不代表操作数的真实地址,把他称作为形式地址记作 A,操作数的真实地址称为有效地址记作 EA,它是由寻址方式和寻址形式地址共同来确定的 计算机组成笔记_数据寻址方式.png

  1. 立即寻址:特点是操作数本身设在指令字内,即形式地址 A 不是操作数的地址,而是操作数本身,又称之为立即数
  2. 直接寻址:特点是指令字中的形式地址 A 就是操作数的真实地址 EA
  3. 隐含地址:是指指令字中不明显给出操作数的地址,其操作数的地址隐含在操作码或某个寄存器中。隐含地址有利于缩短指令字长
  4. 间接寻址:是指指令的 形式地址不直接指出操作数的地址,而是指出操作数的有效地址所在的存储单元地址,也就是说有效地址是由形式地址间接提供的。间接寻址可很方便的完成子程序返回。
  5. 寄存器寻址
  6. 寄存器间接寻址
  7. 基址寻址:设有基址寄存器 BR,其操作的有效地址 EA 等于指令字中的形式地址和基址寄存器的内容相加,即 EA=A+(BR)EA = A + (BR)EA=A+(BR)。操作系统控制基址寄存器的值,在执行过程中基址寄存器的值不变,基址寻址主要用于多道程序,程序无需关注实际物理地址,只需指定使用哪一个寄存器作为基址寄存器即可。
  8. 变址寻址:其有效地址等于 EA 等于形式地址 A 与变址寄存器 IX 相加,EA=A+(IX)EA = A + (IX)EA=A+(IX),指令在的 A 不可变,变址寄存器的内容由用户设定,在执行过程中其值可变,主要用于为程序或数据分配连续存储空间,特别适合数组等循环程序.基址寻址和变址寻址本质上是一样的,只是表现形式不同。
  9. 相对寻址:相对寻址的有效地址是将程序计数器 PC 的值(即当前指令的地址)与形式地址 A 相加而成。$ EA = (PC) +A $,相对寻址的最大特点是转移地址不固定,它可随 PC 值的变化而变。
  10. 堆栈寻址:堆栈寻址要求计算机中设有堆栈,堆栈即可用寄存器组(硬堆栈)来实现,也可利用主存的一部分空间作为堆栈(软堆栈)。以软堆栈为例,可用堆栈指针 SP(stack point)指出栈顶地址,操作数只能从栈顶地址指示的存储单元存或取。堆栈寻址也可视为一种隐含寻址。

CPU 结构

根据 CPU 的功能不难设想

  1. 要取指令,必须有一个寄存器专用于存放当前指令的地址。
  2. 要分析指令,必须有存放当前指令和对指令操作码进行转译的部件
  3. 要执行指令,必须有一个能发出各种操作命令序列的控制部件 CU
  4. 要完成算术运算和逻辑运算,必须有存放操作数的寄存器和实现逻辑运算的部件 ALU
  5. 为了处理异常情况和特殊请求还必须有中断系统。

CPU 中有一类寄存器用于控制 CPU 的操作或运算。

  1. MAR:存储器地址寄存器,用于存放将要被访问的存储单元的地址
  2. MDR:存储器数据寄存器,用于存放欲存入存储器的数据或最近从存储器中读出的数据。
  3. PC:程序计数器,存放现行指令的地址,通常具有计数功能。当遇到转移类指令时,PC 的值可被修改。
  4. IR:指令寄存器,存放当前欲执行的指令。

指令周期

CPU 每取出并执行一条指令所需要的全部时间成为指令周期,也即 CPU 完成一条指令的时间。指令周期包括

  1. 取指周期:完成取指令和分析指令的操作。PC 中存放当前指令的地址,将该地址送到 MAR 并送至地址总线,然后由控制部件 CU 向存储器发读命令,使对于 MAR 所指单元的内容(指令)经数据总线送至 MDR,再送至 IR,并且 CU 控制 PC 内容加 1,形成下一条指令的地址。
  2. 间址周期:取操作数有效地址。一旦取指周期结束,CU 便检查 IR 中的内容,以确定是否有间址操作,如果需要间址操作,则 MDR 中指示形式地址的右 N 位(记做 Ad(MDR))将被送至 MAR,又送至地址总线,此后 CU 向存储器发出读命令,以获取有效地址并存至 MDR
  3. 执行周期:完成执行指令的操作
  4. 中断周期:执行周期结束时刻,CPU 要查询是否有请求中断的事件发生,如有则进入中断周期。

一个指令周期通常用若干个机器周期表示,一个机器周期又包含若干个时钟周期 cpu时钟周期_2020-06-07-15-57-33.png

时钟周期

同步 CPU,使用时钟发生器不断产生稳定间隔的电压脉冲,CPU 中所有的组件将随着这个时钟来同步进行运算动作。 cpu时钟周期_2020-06-07-15-23-23.png 如图,时钟发生器发出的脉冲信号做出周期变化的最短时间称之为震荡周期,也称为 CPU 时钟周期。它是计算机中最基本的、最小的时间单位。每一次脉冲(即一个震荡周期)到来,芯片内的晶体管就改变一次状态,让整个芯片完成一定任务。一个震荡周期内,晶体管只会改变一次状态。由此,更小的时钟周期就意味着更高的工作频率。 一秒内,震荡周期的个数称为时钟频率,俗称主频。

时钟周期数

指运行单个程序所包含的所有的指令总共所需要的时钟周期数

中断系统

在实时处理系统中,必须及时处理某个事件或现象。此时计算机暂时中断现行程序,转而去处理中断服务程序,以解决各种情况。在多道程序运行时,可以通过分配给每道程序一个固定的时间片,利用时钟定时发中断进行程序切换。在多处理器系统中,各处理器之间的信息交流和任务切换也可以通过中断来实现。

引起中断的各种因素

  1. 人为设置的中断,一般称作自愿中断
  2. 程序性事故,如定点溢出,浮点溢出,操作码不能识别等。
  3. 硬件故障
  4. I/O 设备请求
  5. 外部事件

将引起中断的各个因素称为中断源,中断分为量大类:一类为不可屏蔽中断,这类中断不能禁止响应,如电源掉电;另一类为可屏蔽中断,对可屏蔽中断源的请求,CPU 可根据该中断源是否被屏蔽来确定是否给与响应。

中断请求标记

为了判断哪个中断源发出请求,在中断系统中必须设置中断请求标记触发器,简称,记做 INTR,当其状态为 1 时,表示中断请求触发器。 计算机组成笔记_中断请求触发器.png

中断判优逻辑

任何一个中断系统,在任一时刻,只能响应一个中断源的请求。但许多中断源提出请求都是随机的,当某一个时刻有多个中断源提出请求时,中断系统必须按照优先顺序予以响应。

中断服务程序入口地址的寻找

一般情况下使用无条件转移指令,将 PC 指向当前中断服务对应的中断服务程序入口地址

响应中断的条件

中断触发器的状态为 1,且允许中断触发器状态为 1

响应中断的时间

指令周期的执行周期后进入中断周期,统一向所有中断源发出中断信号,只有此时,CPU 才能获知哪个中断源有请求。

中断隐指令

CPU 响应中断后,即进入中断周期。在中断周期内,CPU 要自动完成一系列操作,具体如下

  1. 保护程序断点:将当前程序计数器 PC 的内容(程序断点)保存到存储器中。
  2. 寻找中断服务程序的入口地址
  3. CPU 进入中断周期,意味着 CPU 响应了某个中断源的请求,为了确保 CPU 响应后所需要做的一系列操作不至于受到新的中断请求干扰,在中断周期内必须自动关中断,以禁止 CPU 再次响应新的中断请求。

java-工具类

发表于 2020-06-04 更新于 2020-06-30 分类于 java

EventBus

maven

1
2
3
4
5
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>21.0</version>
</dependency>

post事件,注册的 listener 中注解了@Subscribe的方法会被执行,该方法的参数的类型需要与event类型一致,若没有类型一致的@Subscribe,则由参数类型DeadEvent的listener统一处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class EventBusTest {
@Test
public void test() {
EventBus eventBus = new EventBus();
EventListener eventListener = new EventListener();
eventBus.register(eventListener);
eventBus.post("hello");
eventBus.post(123);
}
public static class EventListener {
@Subscribe
public void stringEvent(String event) {
System.out.println("event = " + event);
}
@Subscribe
public void handleDeadEvent(DeadEvent deadEvent) {
System.out.println("deadEvent = " + deadEvent);
}
}
}

junit 断言异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Student {
public boolean canVote(int age) {
if (i<=0) throw new IllegalArgumentException("age should be +ve");
if (i<18) return false;
else return true;
}
}
public class TestStudent{

@Rule
public ExpectedException thrown= ExpectedException.none();

@Test
public void canVote_throws_IllegalArgumentException_for_zero_age() {
Student student = new Student();
thrown.expect(IllegalArgumentException.class);
thrown.expectMessage("age should be +ve");
student.canVote(0);
}
}

poi

在生成excel时,当为单元格填充内容为数字时,生成的 excel 的数字单元格左上角提示绿色小三角。可在填充单元格值时使用Double类型

1
2
3
4
5
6
7
XSSFCell cell = row.createCell(cellNum);
cell.setCellType(Cell.CELL_TYPE_STRING);
if(value.matches("\\d+")){
cell.setCellValue(Double.valueOf(value));
}else{
cell.setCellValue(value);
}

java-配置相关

发表于 2020-06-04 更新于 2020-06-30 分类于 java

1. 加载资源文件

一般加载资源文件可以使用如下方法

1
2
ClassLoader.getSystemResourceAsStream("xxx.properties")
SomeClass.class.getResourceAsStream("xxx.properties")

但是注意,第一种方法是有缺陷的,因为不同的类加载器会造成读取不到文件的情况,典型的就是tomcat的类加载读取不到应用路径下的文件

2. 查看 java 启动变量

1
2
3
System.getenv();//系统级别环境变量,可以在~/.bash_profile中配置
System.getProperties();//java环境变量,一般可以用-Dkey=value来指定
System.setProperty(key,value) //临时指定java环境变量

3. InetAddress.getLocalHost() java.net.UnknownHostException 异常

问题原因是在系统的 /etc/Hostname 中配置了“zw_65_43” 作为主机名,而在/etc/hosts 文件中没有 相应的“zw_65_43”。简单的解决办法是

对应关系配好就可以,甚至删除/etc/Hostname 这个文件也可以。

深层的原因: 在大多数 Linux 操作系统中,都是以/etc/hosts 中的配置查找主机名的,但是 Detian based system 用/etc/Hostname 文件中的配置做主机名。 结论:

  1. 设置本机名称:hostname mName xxx 最好不是写 IP 地址的形式,若写则必须是本机的完全 IP 形式(不要只写一半)
  2. 在/etc/hosts 里加一行 本机 IP mName
  3. 用 InetAddress.getLocalHost().getHostAddress()测试一下结果是否是与本机 IP 一致

也可以在shell中执行echo $HOSTNAME查看主机名,通过ping $HOSTNAME查看是否问题已经解决了

4. logback 日志

logback 日志在开始阶段会输出一些自身的日志,通过配置NopStatusListener即可以清除

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?xml version="1.0" encoding="UTF-8"?>
<configuration>

<!-- Stop output INFO at start -->
<statusListener class="ch.qos.logback.core.status.NopStatusListener" />

<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<layout class="ch.qos.logback.classic.PatternLayout">
<Pattern>
%d{yyyy-MM-dd HH:mm:ss} %-5level %logger{36} - %msg%n
</Pattern>
</layout>
</appender>

<root level="error">
<appender-ref ref="STDOUT"/>
</root>

</configuration>

5. tomcat使用java启动变量作为端口

tomcat 默认会加载bin目录下新建setenv.sh作为启动环境,若无则新建即可

1
2
3
4
#!/bin/sh
#JAVA-OPTIONS

JAVA_OPTS="$JAVA_OPTS -Dtomcat.port=9999"

tomcat的端口配置文件conf/server.xml中,将默认端口替换为如下

1
2
3
4
 <Service name="Catalina">

<Connector port="{tomcat.port}" protocol="HTTP/1.1"
...

6. 控制台console乱码

java启动参数添加 -Dfile.encoding=utf-8

shell-tips

发表于 2020-06-04 更新于 2020-12-02 分类于 linux

多命令

多个命令用;分割,可以一起执行

curl

参数

  • -X 请求方法 -X POST
  • -d 请求报文 -d '{"name":"li"}'
  • -H 请求头 -H "Content-type:application/json"

显示请求详情

1
curl -v http://example.com

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
~$ curl -v http://centos7:8888
* Rebuilt URL to: http://centos7:8888/
* Trying 10.211.55.5...
* Connected to centos7 (10.211.55.5) port 8888 (#0)
> GET / HTTP/1.1
> Host: centos7:8888
> User-Agent: curl/7.43.0
> Accept: */*
>
< HTTP/1.1 502 Bad Gateway
< Server: nginx/1.19.0
< Date: Fri, 19 Jun 2020 13:33:46 GMT
< Content-Type: text/html
< Content-Length: 157
< Connection: keep-alive
<
<html>
<head><title>502 Bad Gateway</title></head>
<body>
<center><h1>502 Bad Gateway</h1></center>
<hr><center>nginx/1.19.0</center>
</body>
</html>
* Connection #0 to host centos7 left intact

重定向

当使用curl或者在 java 中请求一个远程接口时,当服务器将请求转发即redirect时,将无法得到返回报文。 服务器的返回头中,会有 redirect 的目标地址。 若使用 curl,我们可以使用curl -iL --max-redirs 1 http://example.com,将返回头打印出来

1
2
3
4
5
HTTP/1.1 301 Moved Permanently
Date: Thu, 18 Apr 2019 02:39:59 GMT
Transfer-Encoding: chunked
Connection: keep-alive
Location: https://example.com/about

返回头中的Location,即重定向的地址。我们再次请求重定向的地址,即可得到想要的结果

获取 http 返回码

1
2
response=$(curl --write-out %{http_code} --silent --output /dev/null servername)
echo $response

死循环

无线循环并睡眠 1 秒

1
2
3
4
5
6

#!/bin/bash
while [ 1 ]
do
sleep 1s
done

后台进程

nohup可以用来将脚本在后台运行,默认会将脚本的输出信息打印到nohup.out中

若需要不输出日志信息可以使用

1
nohup ./program >/dev/null 2>&1 &

ls

按文件大小顺序显示

ls -LS

nl

将输出的每一行加上行号。例如:'cat 1.txt | nl',输出1.txt的文件并加上行号

rm

在使用cd dir && rm -rf file时需要注意,当dir不存在时,rm会直接删除当前目录的文件,因此rm后跟文件绝对路径

向远程服务器文件写入

使用管道符基本用法如

1
<command> | ssh user@remote-server "cat > output.txt"

例如

1
2
echo "qwerty" | ssh user@Server-2 "cat > output.txt"
ssh user@Server-1 "<command>" | ssh user@Server-2 "cat > output.txt"

ssh免密及执行远程命令

操作机上生成秘钥ssh-keygen -t rsa,将会生成一对秘钥,将公钥内容追加到服务器的~/.ssh/authorized_keys中, 可通过远程命令cat ~/.ssh/id_rsa.pub |ssh user@example.com 'cat >> ~/.ssh/authorized_keys'去执行,可以简单的使用ssh-copy-id user@example.com,这种方式 采用的是默认的22端口,拷贝的公钥是默认的id_rsa.pub

确保服务器的文件及目录权限

  1. 设置 authorized_keys 权限
    chmod 600 authorized_keys
  2. 设置.ssh 目录权限
    chmod 700 -R .ssh
  3. 设置用户目录权限
    chmod go-w ~
  4. 检查 AuthorizedKeysFile 配置是否启用 authorized_keys

    1
    2
    $ cat /etc/ssh/sshd_config |egrep AuthorizedKeysFile
    #AuthorizedKeysFile .ssh/authorized_keys

    把下面几个选项打开

    1
    AuthorizedKeysFile  .ssh/authorized_keys

后续再执行ssh操作,或者scp等操作,则不需要再输入密码

通过系统日志文件我们可以查看无法登陆远程服务器的原因

1
2
3
tail /var/log/secure -n 20
#也可以在使用ssh时将详情打印出来
ssh -vvv root@192.168.0.1

默认情况下,ssh 去~/.ssh/目录下去找私钥,有时候无法,可以使用ssh-agent将私钥加载到内存中

1
2
3
4
# 启动ssh代理
$ eval `ssh-agent`
# 将私钥注册到agent
$ ssh-add ~/.ssh/id_rsa

XARGS

传递参数

1
ls *.jar|xargs -I {} jadx {} -d src

top

使用top命令查看进程占用情况,可配合grep来实现查看想要的信息 top|grep java

svn

通过 svn info判断服务器和本地的版本号是否相同,可使用grep和awk组合

WGET

用wget递归下载

wget -r -np --reject=html www.download.example 或者可以把reject换做 --accept=iso,c,h,表示只接受以此结尾的文件,分隔符为逗号(comma-separated)

AWK

默认情况下awk以空格进行分割字符串,-F,可以指定分割符
‘{print $1}’,输出第几个分割字符

截取除第一位之后的所有元素

1
echo  1 2 3 4 5|awk '{first = $1; $1 = ""; print $0 }'

示例:

1
more 1.txt|awk -F ',' '{print $2}'

使用条件判断筛选数据

1
awk 'length($2) ==12 && $2 > 20190101 && $2 <= 20191212 {print $0}'

history

设置历史记录不重复

1
2
3
4
5
6
7
8
9
10
export HISTIGNORE='ls:bg:fg:history'
shopt -s histappend # append new history items to .bash_history
export HISTCONTROL=ignoreboth:erasedups
export HISTFILESIZE=10000 # increase history file size (default is 500)
export HISTSIZE=${HISTFILESIZE} # increase history size (default is 500)
# ensure synchronization between Bash memory and history file
#export PROMPT_COMMAND="history -a; history -n; ${PROMPT_COMMAND}"
shopt -s cmdhist
# Append new history lines, clear the history list, re-read the history list, print prompt.
export PROMPT_COMMAND="history -n; history -w; history -c; history -r;history -a; $PROMPT_COMMAND"

uniq

去重uniq,uniq默认仅会比较相邻的字符串

会统计重复的次数

1
uniq -c

显示所有系统变量

1
env

显示内存占用较多的进程

1
ps -aux --sort=-rss|head 10

type

查看命令的详情

  • -a 列出包含别名(alias)在内的指定命令名的命令
  • -p 显示完整的文件名称
  • -t 显示文件类型,其文件类型主要有两种,一种是 builtin,为 bash 的内置命令;另一种是 file,为外部命令

计算 shell 执行时间

time [command]

读取文件内容到变量

1
content=`cat file.txt`

echo

不换行输出

1
`echo -n 'hello:'

判断当前是否为 root 用户执行

1
2
3
4
5
#!/bin/bash
if [[ $EUID -eq 0 ]]; then
echo "this is root"
exit 1
fi

获取时间戳

使用date --help查看具体语法

节选部分

1
2
3
4
5
6
%p   locale's equivalent of either AM or PM; blank if not known
%P like %p, but lower case
%r locale's 12-hour clock time (e.g., 11:11:04 PM)
%R 24-hour hour and minute; same as %H:%M
%s seconds since 1970-01-01 00:00:00 UTC
%S second (00..60)
1
2
3
4
timestamp() {
date +"%s" # seconds since 1970-01-01 00:00:00 UTC

}

mktemp

可以使用 mktemp 命令创建临时文件,文件名会自动生成

1
2
3
4
5
6
#根据后面的X个数随机生成n个字符的名称
tempfile=`mktemp -t tmp.XXXXXX`
echo $tempfile
tempdir=`mktemp -d tmp.XXXXXX`
cd $tempdir
#/tmp/tmp.dOH3ir

判断是否连接网络

1
2
3
4
5
6
ping -o -t 2 www.baidu.com >&/dev/null;
if (( $? == 0 )) ;then #判断ping命令执行结果 为0表示ping通
echo ok
else
echo no
fi

java-stream

发表于 2020-06-04 更新于 2020-12-02 分类于 java

groupingBy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
 List<String> items =
Arrays.asList("apple", "apple", "banana",
"apple", "orange", "banana", "papaya");
Map<String, Long> result =
items.stream().collect(
Collectors.groupingBy(
Function.identity(), Collectors.counting()
)
);
System.out.println("result = " + result);
Map<Object, Set<String>> collect = items.stream().collect(Collectors.groupingBy(String::length, Collectors.mapping(e -> e,
Collectors.toSet())));

System.out.println("collect = " + collect);

join

1
String join = items.stream().collect(Collectors.joining(","));

reduce

递归执行所有元素

1
2
3
Stream<Integer> stream = Stream.of(1, 2, 3);
int count = stream.reduce(Integer::sum).orElse(0);
System.out.println("count " + count);

依次与初始值identity进行运算

1
2
3
4
Stream<Integer> stream = Stream.of(2, 0, 3);
int identity = 6;
short value = stream.reduce(identity, (a, b) -> a * b).shortValue();
System.out.println("value = " + value);

相当于

1
2
3
4
T result = identity;
for (T element : this stream)
result = accumulator.apply(result, element)
return result;

依次与初始值identity进行运算,运行返回其他类型

1
2
3
Stream<Integer> stream = Stream.of(2, 2, 3);
String reduce = stream.reduce("", (a, b) -> a + b, (u, u2) -> "");
System.out.println("reduce = " + reduce);

模拟 join

1
2
3
 List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
String join= numbers.stream().map(String::valueOf).reduce((total, element) -> total + element+"").get();
System.out.println("join= " + join);

流快速删除,Collection提供了方法

1
2
List<String> list = new ArrayList<>();
list.removeIf(Predicate<? super E> filter)

Collectors.toMap()问题

Collectors.toMap()要求生成的 map 的 value 不能为 null,否则会报 nullPoint 异常。且 key 不能重复,否则会报 duplicate key 异常

java-字符串

发表于 2020-06-04 更新于 2020-06-30 分类于 java

切割字符串

1
2
3
4
5
String value = "Notice:4001";
StringTokenizer st = new StringTokenizer(value, ":");
if(st.hasMoreElements()){
System.out.println(st.nextToken());
}

String 替换 正则匹配组

1
2
String s = "HelloWorldMyNameIsCarl".replaceAll("(.)([A-Z])", "$1_$2");
String s = "1.1".replaceAll("(\\.\\d)$", "$10");//$1表示前面正则表达式组1所捕获到的字符

java 字符串占位符

1
2
3
String msg = "hello{0},hello{1}";
String format = MessageFormat.format(msg, new ArrayList<>(), 100);
System.out.println("format = " + format);

可以使用 Apache Common 工具类

1
2
3
4
5
6
7
8
9
10
11
12
import org.apache.commons.lang.text.StrSubstitutor;
...

String template = "Welcome to {theWorld}. My name is {myName}.";

Map<String, String> values = new HashMap<>();
values.put("theWorld", "Stackoverflow");
values.put("myName", "Thanos");

String message = StrSubstitutor.replace(template, values, "{", "}");

System.out.println(message)

输出结果

format = hello[],hello100

html 特殊字符转译

1
2
3
import org.apache.commons.lang3.StringEscapeUtils;
...
StringEscapeUtils.unescapeHtml4(str)

URL 中文转议

1
2
URLEncoder.encode("中文", StandardCharsets.UTF_8.name())
URLDecoder.decode("%E4%B8%AD%E6%96%87", StandardCharsets.UTF_8.name())

规范输出数字

当数字位数不够时,自动在前段补 0

1
String.format("%03d",num)

kafka

发表于 2020-06-03 更新于 2020-12-02 分类于 code

安装

1
2
wget https://downloads.apache.org/kafka/2.6.0/kafka_2.13-2.6.0.tgz
tar -xvf kafka_2.13-2.6.0.tgz

启动

1
2
3
4
#启动zookeeper
$ bin/zookeeper-server-start.sh config/zookeeper.properties
#启动kafka
$ bin/kafka-server-start.sh config/server.properties

相关命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

#创建topic --partitions 20 使用20个分区 --replication-factor 3 备份服务器
$ bin/kafka-topics.sh --create --topic quickstart-events --partitions 20 --replication-factor 3 --bootstrap-server localhost:9092
Created topic quickstart-events.

#查看topic
$ bin/kafka-topics.sh --describe --topic --bootstrap-server localhost:9092
Topic: quickstart-events PartitionCount: 1 ReplicationFactor: 1 Configs: segment.bytes=1073741824
Topic: quickstart-events Partition: 0 Leader: 0 Replicas: 0 Isr: 0

#可以筛选,quicks既可以看到
$ bin/kafka-topics.sh --describe --topic quicks --bootstrap-server localhost:9092
Topic: quickstart-events PartitionCount: 1 ReplicationFactor: 1 Configs: segment.bytes=1073741824
Topic: quickstart-events Partition: 0 Leader: 0 Replicas: 0 Isr: 0

## 删除topic
$ bin/kafka-topics.sh --delete --topic quickstart-events --bootstrap-server localhost:9092

#进入交行界面发送消息,ctrl+c退出
$ bin/kafka-console-producer.sh --topic quickstart-events --bootstrap-server localhost:9092
> This is my first event
> This is my second event


#进入交行界面读取消息,ctrl+c退出,--from-beginning表示从头开始读取消息
#--consumer-property group.id=group1 表示已组group1进行消费
$ bin/kafka-console-consumer.sh --topic quickstart-events --from-beginning --bootstrap-server localhost:9092 --consumer-property group.id=group1

This is my first event
This is my second event

# 查看消费组
$ bin/kafka-consumer-groups.sh --bootstrap-server localhost:9092 --list

# 查看消费组消费情况
# --members 显示组成员
$ bin/kafka-consumer-groups.sh --bootstrap-server localhost:9092 --describe --group my-group --members

简介

kafka 是一个分布式数据流处理系统。

  • kafka 以集群的方式运行在一台或多台服务器上
  • kafka 集群用 topic 来分类存储数据流
  • 每个数据都包含 key,value,timestamp

partition

topic 是发布消息的一个通道,topic 可被多个客户端订阅,topic 可向多个客户端发送订阅信息。对于每一个 topic,可能包含多个 partition kafka简述_2020-06-03-22-18-02.png

partition 的规则

  • 如果没有指定 key 值并且可用 partition 个数大于 0 时,在就可用 partition 中做轮询决定消息分配到哪个 partition
  • 如果没有指定 key 值并且没有可用 partition 时,在所有 partition 中轮询决定小心分配到哪个 partition
  • 如果指定 key,对 key 值做 hash 分配到指定的 partion

在 java 中,我们可以指定 partition 规则来确保消息全部发送至一个 partition

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class MyPartion implements Partitioner {

public int partition(String s, Object o, byte[] bytes, Object o1, byte[] bytes1, Cluster cluster) {
return 1;
}

public void close() {

}

public void configure(Map<String, ?> map) {

}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public class PartionProducer {

public static void main(String[] args) {

Properties properties = new Properties();
properties.put("bootstrap.servers", "106.15.37.147:9092");
properties.put("acks", "all");
properties.put("retries", "3");
properties.put("batch.size", "16384");
properties.put("linger.ms", 1);
properties.put("buffer.memory", 33554432);
//key和value的序列化
properties.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
properties.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");

//添加自定义partition器
properties.put("partitioner.class", "com.congge.partion.MyPartion");

//构造生产者对象
KafkaProducer<String, String> producer = new KafkaProducer<String, String>(properties);
//发送消息
for (int i = 0; i < 10; i++) {
producer.send(new ProducerRecord<String, String>("second", "congge-self ", "val = " + i)
, new ProducerCallBackV3());
}
//关闭连接资源
producer.close();

}
}

/**
* 生产者回调消息
*/
class ProducerCallBackV3 implements Callback {

public void onCompletion(RecordMetadata metadata, Exception e) {
if (e == null) {
System.out.println("offset : " + metadata.offset());
System.out.println("partition : " + metadata.partition());
System.out.println("topic : " + metadata.topic());
System.out.println("===============================");
}
}

}

partition 详情

partition 是一个有序日志,在 partition 上每个 consumer 有唯一 offset 表示当前消费的位置。partition 日志在一段时间内是持久保存在服务器上的,不管 consumer 是否消费,可通过配置数据的保留策略设置消息的过期时间。 kafka简述_2020-06-03-22-41-41.png 通过操作 offset,我们可以读取还未过期的历史数据,也可以跳过当前数据读取已经写入 kafka 的未来数据

消费者

在 kafka 中 consumer 隶属于 consumer group.

  • topic 中的一个 partition 只能被同一个 group 的一个 consumer 消费。即发布到该 topic 的记录会根据 partition 的分区规则推送到指定的 consumer 去消费,而不是所有 consumer 都会受到消息。若订阅该 topic 的同一个组的 consumer 数量大于 partition 数量时,将会有 consumer 空闲
  • topic 中的一个 partition 可以被其他 group 的一个 consumer 消费。即发布到该 topic 的记录会广播给所有订阅该 topic 的 group 中的一个 consumer 去消费

例如 kafka简述_2020-06-03-22-49-47.png

代码规范检测表

发表于 2020-06-03 更新于 2020-06-30 分类于 code

命名

  • 易于理解
  • 清晰的表达意思
  • 即时更新

测试

  • 编写必要的 junit 测试

结构

  • 层次分明
  • 清晰优于简洁

断言

  • 警示
  • 申明

方法

  • 参数尽可能少
  • 通过已有参数间接查询
  • 分离修改和查询
  • 方法内赋值变量使用临时变量
  • 方法使用的变量作用域越近越好

其他

  • 常量值应该统一管理,而不是分散在各地
  • 返回集合和类的深拷贝
1…456…15

lijun

与其感慨路难行,不如马上就出发
145 日志
30 分类
117 标签
© 2019 – 2020 lijun