科普文,计算机CPU是什么

科普文,如何理解CPU

把考试试卷想象成电脑内存(RAM),把你自己想象成中央处理器(CPU)。
试卷上有很多题目,对应:内存里存满了计算机指令。
你一步一步 读题, 解题,写答案
分别对应:
CPU从内存 取指令,分析指令,执行指令

CPU当然能做最简单的加减乘除算术运算,这个很容易理解。另外,我们看到显示器上的图象会不断变化,磁盘会不停读写数据,等等,就是cpu按照内存里存的如何指挥显卡和磁盘的指令在操作显示器和磁盘。这些指挥硬件的程序叫驱动程序,驱动程序是操作系统的一部分; 还有QQ,吃鸡游戏这样的叫应用程序。

这是最简化的CPU原理。实际上,现代的计算机很多硬件可以自己独立完成很多工作,减轻了很多CPU的负担。比如,现代显卡的功能越来越强,把图形相关的很多工作都分到显卡来完成了。

中学生理解到这个程度就够了。可以在网上搜一些科普视频看看,帮助理解。实际上很多计算机专业科班毕业的人也只理解到这个程度,因为他们已经早忘了大学学的课程了。

再深入的理解的话,就需要看专业的教材了。大学里讲CPU的课程,联系最紧密的是《微机原理》。《微机原理》这门课一般会用因特尔公司在80年代出的代号为 8086/8088 处理器做为例子讲解。因为8086/8088处理器比较简单,容易用来教学。现代cpu的主要概念,8086/8088处理器都有。比如: 寄存器,快速缓冲(cache),时钟,寻址,数据总线,控制总线,中断。 理解了这些概念,就算对CPU有了专业级的,基础认识。我觉得要完会理解这门课,需要《数字电路》作为基础,理解总线的时候需要用到。

实际生活中的CPU已经非常复杂了,大概分为两个流派:
1. Intel, AMD公司为代表的复杂指令CPU。主要用在电脑和服务器上面。
2. 英国ARM公司主导的精简指令CPU。主要用在手机等移动设备上面。
只有做相关的专业工作,才需要更加深入理解现代CPU。比如要破解软件,需要用工具把软件转成汇编代码(这个过程叫反编译),这样就能看到软件是如何用机器指令指挥CPU工作的。碰到不懂的地方,还需要查找CPU的官方手册。

什么是Tokio

什么是Tokio

Tokio是一个事件驱动的运行时(官网用词runtime),用来编写rust异步代码。所谓异步代码,即和同步代码对应而言。比如同步读磁盘上的文件:应用代码调用读文件的函数,函数封装了操作系统的系统调用,通知操作系统读入文件内容,操作系统把文件内容全部读出后才回到应用代码,继续执行之后的逻辑。在操作系统读文件内容的过程中,应用程序无所事事,默默地等待。而异步读入文件内容,异步调用通知操作系统读文件后立即返回,应用程序有机会在操作系统读文件时干其他事情,当操作系统读完文件内容后会以某种形式通知应用程序读文件已经完成

Tokio为异步编程提供了这些东西:
1. 一些基本工具。比如同步原语(synchronization primitives),管道(channels),计时器,延时,intervals(不知道这是什么东西,描述一个时间间隔么?)。
2. API。网络相关的tcp,udp异步函数,异步文件操作函数,异步的进程和信号管理。
3. 调度器,用来调度tasks(类似其他语言的异步框架的绿色线程或协程的概念)。
4. I/O驱动(官网用语IO driver),使用原生操作系统的事件队列接口。比如:linux下的epoll,freeBSD下的kqueue,windows下的IOCP。
5. 高性能的计时器。

快速 Fast

Tokio使用的是Rust编程语言,当然有资格有能力做到快速。Tokio要设计时也把速度放在非常重要的位置。

零开销抽象 Zero-cost abstractions

Tokio广泛使用Future这个异步概念(类似node.js中的promise,据我理解,不同语言的异步框架中使用的future和promise是非常近似的概念)。官方文档声称tokio的future和其他语言的future实现不一样。它是独一无二的(unique)。Tokio中的future会被编译器编译成一个状态机。做异步事件的同步处理时,分配内存,及其他future的实现中有开销的地方,tokio都是零开销。(我觉得协程类的异步框架都要维护状态机用来记录栈的信息么,状态机并不unique。unique的是零开销抽象)。

零开销抽象并不意味着tikio自身没有开销,而指是不可能再用其他什么方法减少开销,开销已经减到最少。

并发 Concurrency

Tokio提供了一个多线程,work-stealing(不知用哪个中文合适)的调度器。Tokio是开箱即用的,意味着,当你使用tokio运行时的时候,你就可能充分利用电脑上所有的cpu核心。

现代计算机通过增加中央处理器的核心来增加性能。所以,能够利用好多核心对于高性能的应用来说是至关重要的。

非阻塞I/O Non-blocking I/O

tokio使用操作系统原生的多路复用技术(linux的epoll,freeBSD的kqueue,windows的IOCP),一个线程可以同时管理多个socket。这样能减少系统调用(system call),提高应用的性能。

可靠 Reliable

Tokio在设计时就竭尽所能避免应用程序因使用tokio不当产生BUG,但tokio当然不可能完全做到这点。Tokio的API设计得不易于用错。这样,在项目的最后一天,你就可以信心满满地交付了。

所有权和类型系统 Ownership and type system

Tokio籍由Rust语言特有的所有权系统和严格的类型安全,能避免很多内存安全方面的错误。它能避免绝大多数常见的内存出错:访问未初始化内存,访问释放后内存,内存重复释放(Double Free)。并且,做到这些并不需要付出运行时的额外开销。(回忆自己用C/C++写的复杂应用时追踪偶发的内存出错BUG真是无比痛苦。)

另外,严格的类型安全系统也使得难以错误使用API。比如,Tokio中的互斥锁并不需要开发者显式释放。(这一点并不稀奇,C++的RAII,Go的defer,python的with也能做到,稍微现代的语言都有类似机制。)

反向压力传递 Backpressure

Tokio自带了压力反向传递的功能,这真是真是一个让人称赞的功能,真香。所谓反向压力传递,可以这么理解:当消费者消费的速度小于生产者生产的速度时,数据会在内存中越堆越多,最终把内存撑暴。反向压消费力传递指的是,消费者的压力会反向传递给生产者,让生产者减慢生产的速度以匹配消费者的消费速度。很多其他的库并没有提供这个功能,于是应用需要自己实现一个。但要实现一个高性能的类似功能并不是一件容易的事情。(回忆起自己有段时间用C++的asio异步网络库写的服务器。我的实现是当存放任务的队列到达一个阈值时就让生产者线程sleep很短一段时间,再从网络中读取数据,生产任务,结果性能大降。我相信tokio一定优雅高效地提供了这个功能。)

tokio官方文档中表示,Tokio中的生产者天生是lazy的,它们会轮循消费者,只有当消费者充许增加数据时,生产者才生产数据。

取消 Cancellation

应用的业务代码持有一个future,它描述了异步计算的结果。如果当业务代码认为并不需要这个结果时,则可以不再持有这个future(让它的生命期结束)。这样,异步计算就会及时结束,不再执行不必要的计算。官方文档表示这主要受益于Tokio的轮循设计。

多谢了Rust的权限模型,异步执行部分能通过实现drop这个特征(trait,类似c++,c#,java的接口),及时感知到future已经被丢弃了。

轻量级 Lightweight

Tokio的伸缩性很好,且伸缩时不给应用增加额外负担。这样,tokio能在资源受限的环境下发展得不错。

没有垃圾回收 No garbage collector

tokio使用的是rust语言,所有没有垃圾回收机制,也就避免了在有垃圾回收的语言中普遍存在的“世界暂停”问题。应用会周期性的启动垃圾回收,在极限性能要求的情况下,这个问题就会暴露出来。Tokio的这个特性使得它适合中实时环境中使用。

模块化 Modular

尽管Tokio提供了非常多开箱即用的功能,并且是用模块式的方式组织的。每一个component都使用一个独立的库library。用户可以精确的设定需要使用哪些特性而只导入相应的库,其他不需要使用的库则不会被编译进最终的应用程序中。很多其他著名的rust库也使用了tokio,比如hyper和actix。

Rust语言的宏编程极简教程Macro

Rust的宏和c/c++的异同

Rust语言的宏设计得比较复杂,当然也可以说功能非常强大,跟c/c++语言的宏非常不一样。
相同点 当然是宏能够让代码更精简,码农可以少敲很多样板代码(boilerplate code)。当然你说函数和类不也可以抽象代码,使代码精简吗?但宏能够获取很多在编译期的信息,函数和类却不能。比如,代码所在的文件和行数,这个信息只有宏才能获得。
不同点 是,C/C++的宏只是在预编译期简单地做模式替换,预编译后再交由编译器。c/c++的include头文件展开也是在预编译阶段。但Rust宏的展开不是发生在预编译期,而是编译期。于是Rust能够获得更复杂的更详细的编译器的信息。这里有两个概念Token Tree和AST。如果有大学阶段学习过的编译原理课程的背景就很容易理解。

Token Tree

Token Tree简写成TT。编译器拿到源代码后先做词法分析,即把源代码字节流分成一个一个的token。token和token之间的逻辑关系也记录下来。比如下面的一个简单语句:

a + b + (c + d[0]) + e

的Token Tree就长这个样子:

«a» «+» «b» «+» «(   )» «+» «e»
         ╭────────┴──────────╮
          «c» «+» «d» «[   ]»
                       ╭─┴─╮
                        «0»

AST

AST的全称是Abstract Syntax Tree,抽象语法树。编译器把Token Tree翻译成AST,这是一个更有利于编译器理解源代码的结构。上面的TT翻译成对应的AST后长这个样子:

              ┌─────────┐
              │ BinOp   │
              │ op: Add │
            ┌╴│ lhs: ◌  │
┌─────────┐ │ │ rhs: ◌  │╶┐ ┌─────────┐
│ Var     │╶┘ └─────────┘ └╴│ BinOp   │
│ name: a │                 │ op: Add │
└─────────┘               ┌╴│ lhs: ◌  │
              ┌─────────┐ │ │ rhs: ◌  │╶┐ ┌─────────┐
              │ Var     │╶┘ └─────────┘ └╴│ BinOp   │
              │ name: b │                 │ op: Add │
              └─────────┘               ┌╴│ lhs: ◌  │
                            ┌─────────┐ │ │ rhs: ◌  │╶┐ ┌─────────┐
                            │ BinOp   │╶┘ └─────────┘ └╴│ Var     │
                            │ op: Add │                 │ name: e │
                          ┌╴│ lhs: ◌  │                 └─────────┘
              ┌─────────┐ │ │ rhs: ◌  │╶┐ ┌─────────┐
              │ Var     │╶┘ └─────────┘ └╴│ Index   │
              │ name: c │               ┌╴│ arr: ◌  │
              └─────────┘   ┌─────────┐ │ │ ind: ◌  │╶┐ ┌─────────┐
                            │ Var     │╶┘ └─────────┘ └╴│ LitInt  │
                            │ name: d │                 │ val: 0  │
                            └─────────┘                 └─────────┘

AST图有排版有点乱,我懒得改了。这两人个图,TT和AST都是从 https://danielkeep.github.io/tlborm/book/mbe-syn-source-analysis.html 抄来的。您可以移步到那个页面。
重点来了,Rust语言的宏展开就发生在编译器生成了源代码的AST的时候。Rust语言的宏可以从AST获得非常丰富的信息,并操作AST。

Rust的宏有两种

Rust语言设计了两种宏,一种叫Declarative Macros(声明式宏),以前的版本也有Macros by example这个名字,旧的名字怪怪的。另一种是Procedure Macros(过程宏)我不明白为什么取这个名字。Declarative Macro相对Procedure Macros要简单一些,而过程宏则可以玩出另复杂的花样。

Declarative Macro

常见的这样一个定义vector的Rust语句:

let v: Vec<u32> = vec![1, 2, 3];

就使用了声明式宏。这个宏的定义如下:

#[macro_export]
macro_rules! vec {
    ( $( $x:expr ),* ) => {
        {
            let mut temp_vec = Vec::new();
            $(
                temp_vec.push($x);
            )*
            temp_vec
        }
    };
}

#[macro_export]表示将定义的宏将导出crate,这样不仅在定义了这个macro的crate的内部可以使用该宏,导入该crate的其它crate亦可使用这个宏。
然后看这个声明宏的定义,非常像rust语中的匹配表达式(match expression)。$(x:expr)$()* 语法表,表示展开为0条或者多条表达式,其他的都比较好理解。
示匹配一个表达式并把匹配到的表达式存在变量$x里。 $(x:expr)被$(),* 包裹。这里有点像正则式的描述,它表示匹配0个或者多个被逗号,分隔的表达式。 =>后面的部分就是描述怎么展开宏了,除了也用到类似正则式的$()* 语法,表示展开为0条或者多条表达式,其他的都比较好理解。
另外,这个宏的定义只使用了一条规则,实际上可以像匹配表达式一样定义多个规则。

Procedure Macros

过程宏可以为一个类自动生成特性trait(类似c++中的纯虚函数,java/c#中的interface)的实现;还能实现类似python中的decoration概念,如下代码所示:

#[route(GET, "/")]
fn index() {
// ...
}

这就跟python的http框架flask用来定义路由的decoration的写法几乎一样了。
不过,编写procedure macros需要使用两个辅助的crate用来操作AST,它们是sync和quote。挺复杂的,你们还是去看官方方档吧。

hygiene是什么

在读官方的Rust Macro相关的资料时,时不时看到hygiene这个詞。一开始不懂这是个什么玩艺儿。后来,才知道原来是rust的宏机制避免了c/c++简单的宏经常出现的副作用。在Rust的话术里,把这个东东称为Hygiene。

就写到这吧,写文章好累呀,还是写代码轻松有意思一些。

Rust语言中的单位类型unit type是什么

每个学习Rust语言的读者碰到单位类型unit type ()的时候都非常疑惑这究竟是个什么鬼?
本文总结了stackoverflow上面的贴子 What is the purpose of the unit type in Rust?加上我自己的理解。

  1. unit type是一个类型,有且仅有一个值,都写成小括号()
  2. 单元类型()类似c/c++/java语言中的void。当一个函数并不需要返回值的时候,c/c++/java中函数返回void,rust则返回()。但语法层面上,void仅仅只是一个类型,该类型没有任何值;而单位类型()既是一个类型,同时又是该类型的值。
  3. 单元类型()也类似c/c++/java中的null,但却有很大不同。 null是一个特殊值,可以赋给不可类型的值,例如java中的对象,c中指向struct实例的指针,c++中的对象指针。但在rust中,()不可以赋值给除单元类型外的其它的类型的变量,()只能赋值给()。
  4. Rust标准库中使用单元类型()的一个例子是HashSet。一个HashSet只不过是HashMap的一个非常简单地包裹,写作:HashMap<T, ()>。HashMap的第二个泛型类型参数即用了单元类型()
  5. 可以用Result<(), MyErrorType>代替Option,某些开发者认为Result<(), MyErrorType>语义上能更简明地表示一个“结果”。

You could argue that Option is ‘better’, but I disagree; the Result’s naming conventions mean it’s explicitly clear what’s going on.

使用Prettier,Husky和lint-staged,在提交javascript代码前自动格式化代码

目标

为防止提交不合格的代码排版到仓库中,我们希望在提交前格式化代码,使之符合项目的代码格式规范,而且尽可能保证逻辑正确。即目标有两点:
1. 对git staged的代码强行格式化,使符合规范。对于没有被staged的代码或文件,将完全忽略。
2. 跑单元测试。只有通过了单元测试才能提交代码。

安装依赖

$ npm install --save-dev husky lint-staged

先安装husky和lint-staged。注间在安装husky之前需要确保工程目录已经是一个合法的git仓库。因为,安装husky时需要给git仓库安装预提交钩子(precommit hook)。
Lint-staged提供了一个关键的功能,它能找到所有被git staged的文件,而不是工程下面所有的文件,然后通过管道交给Prettier做格式化。

修改npm脚本

对npm script做如下改动:

  "scripts": {
    "precommit": "lint-staged"
  },
  "lint-staged": {
    "*.{js,jsx,json}": ["prettier --write", "git add"]
  }

scripts.precommit由Husky处理,它会依照配置在把代码正式提交到仓库前运行lint-staged(当然我们也可以配置其他命令)。
Lint-staged再找到属于它自己的配置部分,即lint-staged键下面的配置。
在此例中,所有staged状态的,且扩展名是.js, .jsx, 和.json文件都会自动交由Prettier做格式化,然后再提交。

为何一定要用lint-staged

You might wonder why we don’t just use Husky to run a few npm scripts.
你可以会疑惑,为什么一定要罗里罗嗦地用lint-staged,用Husky直接调用npm脚本不更简单么。
这是因为我们用lint-staged拿到staged状态的文件,而不是所有的文件。
你的node工程文件中可能已经有如下做代码格式化的脚本

"format": "prettier --write '**/*.{js,jsx}'"

你可能会因此如下配置

"lint-staged": {
  "*.{js,jsx,json}": ["npm run format", "git add"]
}

如果是这样配置的话,就没有必要用lint-staged了。因为,这样的配置会把prettier应用到所有匹配.js,.jsx.json的文件上,而不是仅staged状态的文件。

把跑单元测试加入预提交检查

Husky还能够帮你把跑单元测试加入到预提交检查中,保证只有通过了单元测试才正式提交代码到仓库。
如下是一个典型的配置:

"scripts": {
    "test": "exit 0",
    "precommit": "lint-staged && npm test"
  },
  "lint-staged": {
    "*.{js,jsx,json}": ["prettier --write", "git add"]
  }

实际项目中要修改scripts.test部分,视工程使用的单元测试框架Jest, 或Mocha ,本示例简单的返回0,表示无条件通过。

python用multiprocessing起子进程,父进程被杀掉时,如何让子进程也自动退出

最近用python3写了一段代码,代码会起一个子进程做一些辅助的事情。但发现父进程被杀死后,这个子进程变成了孤儿进程,仍然在运行。
起进程的代码大概如下:

from multiprocessing import Process, Queue    
__q = Queue() 
__p = Process(target=__run, args=(__q,))                                                                                                           
__p.start()   

很不方便,那怎样让父进程退出后,子进程也跟着退出呢。
一番google之后,原来可以用一行代码解决:

from multiprocessing import Process, Queue    
__q = Queue() 
__p = Process(target=__run, args=(__q,))   
__p.daemon = True    # 加上这一行代码
__p.start()   

设置子进程的daemon属性为True。这样,当在控制台上用ctrl-c,退发送SIGTERM杀掉父进程后,子进程也会跟着退出,不会成为孤儿进程。
参考python官方的解释:

daemon
The process’s daemon flag, a Boolean value. This must be set before start() is called.
The initial value is inherited from the creating process.
When a process exits, it attempts to terminate all of its daemonic child processes.

官方解释的重点是daemon flag必须在start()前被设置。
还有一个坑人的地方,貌似父进程必须被SIGTERM信号杀死,或者自行自然退出,子进程才会退出,否则还是变成了孤儿进程。在stackoverflow上有人提到了这点:
https://stackoverflow.com/questions/25542110/kill-child-process-if-parent-is-killed-in-python

I want to mention, that if parent process was killed using SIGKILL (kill -9) then daemon processes won’t stop.

我想,这或许应该是由于-9(SIGKILL)会立刻中止父进程,于是父进程没有机会通知子进程,比如用atexit设置的进程退出时调用的hook函数就没机会执行了。