0%

The Golden Rule of API Design

API设计很难,尤其是大型的设计。如果你正在为成百上千的用户设计一款API,你不得不思考未来你可能作出何种改变,以及这些改变是否会给客户的代码造成破坏。除此之外,你不得不考虑这些API的用户会如何影响你。如果你的API类中有一个类会调用它自己的内部方法,你就不得不推荐用户可以通过继承一个子类并重写此方法,但这可能会成为灾难。你无法改变这个方法,因为用户已经给它赋予了不同的含义。你在之后的内部实现也将受到用户的支配。

API开发者有很多方法解决这个问题,但最简单的方式就是锁定API。如果你用Java实现,可能会在会让很多类和方法加上final。在C#中,你可能会让类和方法加上sealed。不管你用什么语言,你可能都想通过单一模式或静态工厂方法来展示你的API,从而可以保护它们不让他人有覆盖的行为,同时在使用你的代码时,能够限制住它们的选择。这看似全部的原因,但,是真的吗?

过去的十年里,我们逐渐认识到了单元测试是非常重要的一个环节,但这一教训并没有完全渗透到整个行业。其迹象充斥在我们周围。让一个未经测试的类随意用于第三方API,并尝试为它写单元测试。大部分时间,你都会掉坑里。你会发现这些代码所使用的API像是用胶水粘起来的一样。没有办法模拟这些API类,你只能通过“感觉”来和他们交流,或者提供返回值进行测试。

随着时间的推移,这都会变好,但只有在我们设计API时,才开始将测试视为真实用例。不幸的是,它比仅仅测试一下代码要复杂一些。那就是API设计黄金法则仅为你开发的API写测试是不够的;你不得不为使用了你的API的代码写单元测试。当你这么做了,你就能掌握用户在测试他们的代码时不得不面对的困难

没有办法让开发者在测试使用你API的代码时变得容易。staticfinalsealed本身并不是坏的结构。有时它们很有用。但更重要的是意识到测试问题,然后解决,你必须亲身经历。一旦你拥有(这种经验),你就可以像其他设计挑战一样解决它。

  1. 本书我看了大半,不想读了
  2. 暂时不打算对这本书写读书笔记或感悟
  3. 我非常主管地认为这本书是一本学术类报告,不适合当科普

《深度学习,智能时代的核心驱动力量》是微信阅读推送给我的,貌似刚推出,而且看了他人简评说此书可以当作深度学习的入门及其简史的了解。我心想那此书应该是本科普类的书籍吧,然而我想多了,不知是翻译还是原著本身的问题,本书跟像是作者满怀激情地回顾过往,书中的确讲述了计算机如何一步步发展“认知”,但除非你有较好的数学功底和人工智能历史背景,否则基本跟不上作者的节奏(尤其是第二部分,反正我是不行)。因此,仅仅是书本身,我认为写得并不好。当然这并不代表作者专业水平不够,恰恰相反,书中对机器识别发展各个阶段中出现的算法及其原理的透彻介绍,都能看出作者的专业知识那是相当厉害的。

关于深度学习还有另外一本“同名”书《Deep Learning: Adaptive Computation and Machine Learning series》,又叫做“花书”,貌似评价也极高,我对此书的兴趣也很大,但应该从本书中汲取教训,下一次我会选择把它当作技术类书籍来精度,并写学习笔记。

说来惭愧,我个人作为程序员多年,其实对数学和算法都没扎实的基础。我知道历史上很多骨灰级大神其实都是从数学的角度在思考程序的问题,很牛逼,很羡慕,看来我也该认真对待自己的数学了。

采用QEMU制作的最小linux顺利启动后,就该为其写内核模块了,所以这一节就是热热身:

  • 学习如何通过vscode搭建linux驱动开发环境
  • 编写第一个内核模块helloworld
  • 初识linux内核模块机制

警告:在开始阅读下文之前,务必确保自己的linux内核被交叉编译过,并顺利生成zImage。

Ubuntu+VSCode的驱动开发环境搭建

2015年底微软用自己的行动向世人证明,在IDE领域,你大爷还是你大爷,即便是Visual Studio Code这种所谓的代码编辑器,都可以傲视一堆所谓功能强大的集成开发环境。随着近几年的更新迭代,vscode在c/c++的开发方面也不逊色于vs了(我个人感受),在GO、Web、C/C++以及很多非主力语言,它都是我的首选。因此,之后的内核模块代码编写,自然也少不了这利器。(一不小心闲话扯多了)

进入正题,vscode安装好之后,务必安装微软官方扩展——C/C++,这个扩展的好处是提供语法高亮、智能补全、提示、调试等核心功能,剩下的酷炫功能及教程自行看官网。对于驱动来说,主要是方便代码补全和接口提示,以提高编写效率。

vscode及其扩展安装完毕后,开始写代码吧,创建第一个驱动源码——hello.c

1
2
3
$ mkdir ~/varm/drivers/hello
$ cd ~/varm/drivers/hello
$ touch hello.c

先别急着写代码,vscode扩展需要我们告诉它从哪里解析头文件,以分析和自动补全你想要的接口形式,所以:

1
2
~/varm/drivers/hello$ mkdir .vscode # 创建vscode当前项目配置目录
~/varm/drivers/hello$ touch .vscode/c_cpp_properties.json # 创建C/C++扩展的配置

打开刚创建的json配置文件,并把下方的内容拷贝进取,不过要注意includePath部分,里边的路径是我自己的环境,说白了就是要告诉vscode从哪里能够找到linux内核以及arm架构所需的头文件位置,这个需要根据自己的实际情况作调整。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
"configurations": [
{
"name": "Linux",
"includePath": [
"${workspaceFolder}/**",
"/opt/varm/linux-5.0.3/include",
"/opt/varm/linux-5.0.3/arch/arm/include",
"/opt/varm/linux-5.0.3/arch/arm/include/generated",
"/opt/varm/linux-5.0.3/include/uapi"
],
"defines": [
"__KERNEL__",
"__GNUC__"
],
"compilerPath": "/usr/bin/gcc",
"cStandard": "c11",
"cppStandard": "c++17",
"intelliSenseMode": "clang-x64"
}
],
"version": 4
}

好了,现在可以愉快地写代码了,就像下图这样,vscode驱动开发环境配置结束。

第一个linux内核驱动

先看代码!

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
#include <linux/init.h>
#include <linux/module.h>

// 内核加载时触发
static int __init hello_init(void)
{
printk("HELLO_MODULE: hello world\n");
return 0;
}

// 内核卸载时触发
static void __exit hello_exit(void)
{
printk("HELLO_MODULE: goodbye\n");
}

// 真正向内核制定该模块出入口
module_init(hello_init);
module_exit(hello_exit);

// 各种版权、扩展信息声明,无关紧要
MODULE_LICENSE("GPL v2"); // 开源许可证
MODULE_DESCRIPTION("my first kernel module"); // 模块描述
MODULE_ALIAS("HelloWorld"); // 模块别名
MODULE_AUTHOR("Philon"); // 模块作者

上述代码可以说非常简单了,我们定义了两个函数hello_init/hello_exit都只是简单打印了一句话,并通过module_init/module_exit分别告诉内核,当加载/卸载该模块的时候执行对应的函数。

为什么不用printf函数打印

一是因为printf是C的标准库函数,依赖于C的动态库文件,比如/lib/libc.so.6文件,内核就不应该直接去访问这个文件。
二是因为模块是运行在内核层,而非用户层,printf要通过用户文件系统把内容输出到文件,同样是第一条理由,所以printk也就是指通过内核输出。

不光printk,内核开发中还会遇到很多k结尾的函数,比如mallock,都是因为标准C库的原因,不能直接调用,但其功能及接口形式与标准库几乎一样。

两个函数前__init/__exit意味着什么

这两个是修师符,即使不添加也不会有任何表面上的区别,但建议加上!

__init表示该函数是初始化函数,在最终生成的模块文件中,这段代码会被强制放到.init.text段区,当模块加载完毕后,该区所有分配的内存资源都会被释放。
__exit表示该函数是退出函数,如果你的驱动并非“模块”,而是直接编译进内核,那么被此修饰的函数显然是多余的(编译到内核中的驱动是无法卸载的),因此该函数不会被链接,以缩小镜像大小。

关于开源与许可证

这部分会涉及到非常多的法律问题和风险,一般来说如果一个驱动没有声明GPL协议,在加载时会收到内核被污染的警告,逼死强迫症。但是对于我等初学小白而言,可以暂时不用考虑这部分内容。但如果是工作中的代码或涉及商业机密,建议还是谨慎对待,毕竟不是每个程序员都善于打官司。

编译并“运行”驱动

第一步:编译

linux的内核模块编译是必须依赖内核源码的,这就是为什么文章一开始注明必须确保linux内核已经交叉编译过的原因。而内核模块需要通过Makefile来指定是编译到内核中,还是以模块形式存在,也就是下面的第一行obj-m=hello.o

1
2
3
4
5
6
7
8
9
10
11
12
# 模块驱动,必须以obj-m=xxx形式编写
obj-m = hello.o

# 指定内核源码目录及交叉编译环境
KDIR = /opt/varm/linux-5.0.3
CROSS = ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf-

all:
$(MAKE) -C $(KDIR) M=`pwd` $(CROSS) modules

clean:
$(MAKE) -C $(KDIR) M=`pwd` $(CROSS) clean

除了第一行需要留意下以外,其他都是常规Makefile语法,本文主要关注驱动开发,其他语法规则什么的就不废话了。直接make一下就能看到hello.ko的出现,这就是最终内核模块。

第二步:拷入根文件系统

虽然linux内核模块是运行在内核层的,但其加载、卸载、访问、操作等策略性的事物是完全交由用户层来管理,驱动仅仅负责实现和提供设备访问机制。所以现在需要把编译好的ko文件拷贝到之前制作好的根文件系统镜像中——rootfs.ext3。

目前我们的linux最小系统还很简陋,没有网络共享,因此最粗暴的方式就是挂载镜像——复制——启动系统。

1
2
3
$ sudo mount -o loop ~/varm/os/rootfs.ext3 /mnt
$ sudo cp ~/varm/drivers/hello/hello.ko /mnt
$ cd ~/varm/os && ./power_on # 完成拷贝,启动系统

第三步:加载/卸载

记住linux最简单的管理内核命令:

  • insmod,加载内核模块
  • rmmod,卸载内核模块

正如下图所示,hello.ko和预期一样分别在加载和卸载模块时打印出了相应的内容。

关于提示loading out-of-tree module taints kernel.主要是由于该模块并不存在设备树中,想想第一节的make dtbs,所以这个警告无关紧要。关键是第一个内核模块版本的HelloWorld顺利通过验收,鼓掌!

以上是本节的实操,结束!


以下是Linux内核知识,GO!

虚拟文件系统

对于Linux“玩家”而言,文件系统(File System)是非常简单的概念,比如Windows的NTFS、macOS的APFS、Linux的Ext4、U盘的Fat32等等,所以文件系统就是对物理存储空间的逻辑抽象和数据组织管理的机制,根据不同的操作系统或存储介质演化出了如今众多的文件系统类型,但不论如何,我们口中的文件系统泛指对存储介质的数据管理系统。而Linux更是将这种思想发挥到了极致——一切皆文件。

有了文件系统,我们可以非常直观地open、close、read、write任何文件,访问任何目录。但你有没有想过,硬盘里的“文件或目录”归根结底只是一些二进制数据,只要格式化就消失了。而计算机周围的键盘、鼠标、显示屏、打印机、摄像头等,这些可是真实设备啊,每种设备的操作方式都不同,意味着每增加一个设备,Linux内核就要专门为其开发一种API给用户访问,就算Linux社区的黑客们不嫌烦,我等芸芸众猿也学不过来呀。

为此,Linux社区孕育了“把每个设备抽象成一个文件”的想法,这样不论什么新设备都可以用open、close、read、write函数访问,而组织管理这些设备文件的,自然就是设备文件系统,比如/dev、/sys等目录,我暂且把它们称作设备文件系统

此外,操作系统管理着很多进程,这些进程同样千奇百怪,如何让用户非常方便地访问呢?抽象成文件系统啊,每个进程都抽象成文件,直观地摆到用户面前,比如/proc目录,我暂且把它称作进程文件系统

还有,我们的电脑是可以网络通信,通过http、ftp等,网络上的资源也是让人眼花缭乱,怎么能直观呢?浏览器是个不错的选择,但对于只有命令行的黑客呢?抽象成网络文件系统吧。

回首,再看看我们的根文件系统,最多把它称为硬盘文件系统

所以那么多的文件系统摆在我的面前,Windows没有好好珍惜,只是甩给用户一堆ABCDEFG盘。Linux不想这样,它不希望用户觉察到文件系统的存在,让用户觉得“去他妈的文件系统,老子只是打开了一个文件夹”。所以为了解决不同文件系统的“共生”问题,虚拟文件系统诞生了!

重新认识dev、sys、proc目录

上边扯了一堆“废话”介绍有关虚拟文件系统的概念,换而言之,不要简单的认为/dev、/sys、/proc这几个根目录仅仅是个目录,否则接下来的设备驱动开发会看得云里雾里。

挂载proc文件系统

在介绍之前先启动之前用qemu做好的Linux系统,然后尝试用ps看一下进程😁

1
2
3
4
5
6
7
8
9
10
11
12
~/varm/os$ ./power_on.sh
Booting Linux on physical CPU 0x0
Linux version 5.0.3 (philon@philon-matebook) (gcc version 7.3.0 (Ubuntu/Linaro 7.3.0-27ubuntu1~18.04)) #1 SMP Thu Mar 21 20:44:13 CST 2019
...省略各种打印信息...
Please press Enter to activate this console.

/ # ps
PID USER TIME COMMAND
ps: can''t open '/proc': No such file or directory
/ # ls
bin lib lost+found usr
dev linuxrc sbin

看,一个最简单的进程查看命令居然出错了,提示找不到/proc目录,简单啊——创建这个目录再试试:

1
2
3
4
5
6
/ # mkdir /proc
/ # ps -a
PID USER TIME COMMAND

/ # ls /proc/
/ #

错误提示不见了,但依然什么进程都没有,连系统进程都没有,这怎么可能,proc目录下空空如也…

呵呵,根本原因出在我们把/proc当作一个普通目录来处理了,正如前边所说,Linux会把所有的进程抽象为文件来组织管理,所以核心问题是以上命令并没有真正去挂载procfs,所以正确的做法是这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/ # mount -t proc proc /proc # 把proc文件系统挂载到/proc目录下
/ # ls /proc
1 28 815 diskstats net
10 29 828 driver pagetypeinfo
11 3 829 execdomains partitions
... # 这样就可以访问到真正proc文件系统的所以进程ID内容了

/ # ps
PID USER TIME COMMAND
1 0 0:01 init
2 0 0:00 [kthreadd]
3 0 0:00 [rcu_gp]
4 0 0:00 [rcu_par_gp]
... # 系统进程也可以正常查看了

挂载sysfs文件系统

和proc文件系统道理一样,sysfs顾名但不要思义,顾名是指这个文件系统肯定是挂载到/sys目录,不要思义是指sysfs不是为了呈现系统而生的。

sysfs最开始Linux为了更好地以文件/目录形式在用户层展现设备树和驱动的结构及关联才开发的,其实就是从/proc/sys中剥离出来,所以最初其实这个文件系统名为ddfs(Device-Driver-FS)。后来这个文件系统和驱动模型被证明在其他子系统中也大有作为,就干脆叫做sysfs。

说那么多,要搞清楚,/sys这个目录主要是为了清晰呈现设备、驱动、类型之间的关联(如果你不嫌烦可以一个个子目录翻了看看,其中有很多符号链接)。今后写驱动,可能会频繁地访问该目录。

好了,把sysfs也挂载起来吧

1
2
3
4
5
/ # mkdir /sys
/ # mount -t sysfs sysfs /sys
/ # ls /sys
block class devices fs module
bus dev firmware kernel power

/dev与udev

其实如果你不较真儿的话,/dev可以就是个目录,但常见的Linux版本中更喜欢把它挂载到tmpfs文件系统,说白了就是个虚拟硬盘,临时内存区而已。说它可以是个普通目录的意思,是说该目录下的所有文件,理论上来说都是在用户层完成的,非内核在管理。设备和驱动真正被内核管理的地方是刚说的sysfs。这也是为什么在很多嵌入式Linux文件系统的启动脚本里,会有各种mknod命令出现。

/dev目录存在的意义是什么?很简单——设备描述符。任何设备被Linux抽象的符号链接最终都应该放在这里,比如我们要访问串口,不是区/sys目录下找XX设备,而是应该找/dev/ttyXXX设备描述符。

udev是什么?解释之前先了解另一个东东——devfs,这货其实就是最开始内核来管理设备的文件系统了,也就是说刚开始一个u盘插入后各种驱动加载、指定设备号、创建文件描述符等等工作都由它完成。出发点自然是很好,但封闭的弊端就是不灵活。此时udev注意到这些缺点,而且认为什么加载驱动、创建文件符号这些事情完全可以留给用户曾作,画蛇添足干嘛,所以udev才取代了devfs。

简单来说,udev也类似于文件系统管理着/dev目录,但运行在用户态,通过监听uevent,动态为插拔设备做各种驱动加载、创建文件描述符等,但这些行为是根据用户层的匹配规则完成的。

小结一下

  • vscode和c/c++扩展可以很好地支持Linux驱动的代码编辑
  • 内核模块编译依赖于内核源码,并通过obj-m=xxx.o指定模块名称
  • insmod xxx.ko加载一个内核模块
  • rmmod xxx卸载一个内核模块
  • /dev /proc /sys目录其实是三个文件系统

Fulfill Your Ambitions with Open Source

不用你来做开发工作就能实现你雄心勃勃的软件开发白日梦,想必也是极好的。也许你正在为某大型保险公司开发软件时,你宁可在Google、Apple、Microsoft、或你自己的初创公司工作,准备搞一个大动作。你再也不用为那些你根本不关心的系统去开发软件。

幸好,有一个办法可以解决你的问题:开源。那里有成千上万的开源项目,而且其中多数都很活跃,为你提供任何你想得到的软件开发经验。如果你热爱这种开发操作系统的创意,去给其中的某个操作系统项目帮忙吧。如果你想为音乐软件、动画软件、密码学、机器人、PC游戏、大型多人在线游戏、移动电话、或任何(软件)工作,基本上你一定能找到一个符合你兴趣的项目。

当然,天下没有免费的午餐。你不得不放弃你的自由时间,因为你很可能无法在工作日为一款开源的视频游戏工作——你仍然要向你的雇主负责呀。还有,很少有人能通过给开源项目做贡献而获得收入——少部分可以但绝大多数不行。你应该愿意放弃一些自由时间(少玩会视频游戏、少看会电视,你又不会死)。在开源项目上你工作越努力,你就越能尽早意识到作为程序员你真正的理想。但也要注重你的劳动合同——有些老板可能会限制你可以贡献的内容,即使在你的私人时间。此外,你需要小心不要违反与版权、专利、商标、商业机密等相关知识产权法。

开源可以为那些躁动不安的程序员提供巨大的机遇。首先,你可以看到他人是如何对你感兴趣的那些项目实施解决方案的——你会从阅读他人代码中获益匪浅。其次,你可以为这些项目贡献你的代码和想法——不是每个精彩创意都会被接受,但你可以通过研究解决方案和贡献代码学到很多东西。第三,你会遇见对你的软件充满热情的优秀人才——这些开源友谊能持续一辈子。第四,假设你是一个贡献者,你可以在你真正感兴趣的领域积累实际经验。

开源入门实在是太容易了。这里有大量关于你需要的工具的相关文档(比如源码管理、编辑器、编程语言、构建工具等)。首先找出你想要研究的项目,然后学习项目所采用的工具。多数情况下项目文档都比较少,不过最好的学习方式应该是自己查阅代码。如果你想参与,你可以提供帮助文档。或者你可以从义务写测试代码开始。当然这听起来没那么让人兴奋,但实际上你会因为给别人的程序写测试代码而比那些从来不活跃的人学习得更快。写测试代码,真正好的测试代码。找出bug、修复建议、交朋友、为你喜欢的软件工作,实现你软件开发的抱负。

真的是一不小心翻开了此书,然后决定读完的。我之前一直避免去读“创业类”的书籍,一是纸上谈兵居多,二是“老子现在没打算创业”,所以此类书籍大多会给我留下空洞的映像。《创业维艰,如何完成比艰难更难的事》要比我所读过的任何讲述创业的书籍内容都实在的多,也许就是书中说的那个原因——大部分管理书籍都是教你怎么在顺境中当好CEO,而不提怎么应对逆境。不过作者(本·霍洛维茨)就比较苦逼了,在他打理Opsware的8年时间里,只有3天是顺利的,剩下的日子每天都刀口舔血,也正是因此才得以让我们看到光鲜亮丽的CEO身后的孤独与焦虑。

阅读全文 »

问:作为嵌入式软件工程师,你会写驱动吗?
答:会呀,我会控制gpio输出把LED点亮!😂

我想这是我扎根嵌入式软件开发多年的尴尬之处——不太会写驱动!所以,我决定系统地学习一下这个领域,一来是工作需要弥补我在驱动层的空白,二来自己也想全面了解Linux内核机制。基本上我主要通过《Linux设备驱动开发详解,基于最新的Linux4.0内核》以及《Linux设备驱动程序》作为主要参考教材(毕竟手上就这两本书)。

废话结束


为了避免繁琐地硬件环境搭建,学习过程尽可能基于虚拟的arm开发板,既能保证环境统一,又能快速上手👍。

另外,我个人的“战果”全部放在git仓库:github/philon/varm

个人开发环境:

  • 宿主机:Ubuntu 18.04
  • 开发板:qemu+vexpress-a9
  • 编辑器:visual studio code
  • 编译器:arm-linux-gnueabihf-gcc

关于如何安装Linux虚拟机以及交叉编译环境的搭建这里就不写了,网上教程一大堆。所以从现在开始,阅读下文的前提是:Linux、arm-linux-gcc、各种源码编辑器环境已就绪。

好,下面亲手动手,来做一块arm开发板😅。

ARM虚拟机——QEMU

QEMU是一款免费开源且跨平台的虚拟机,可以虚拟各种架构的处理器,和vmware/virtualbox不同,我们更喜欢用它来仿真ARM环境,即一款ARM虚拟机。

第一步:下载并安装qemu

1
2
3
4
5
6
7
# macOS用户
$ brew install qemu

# Ubuntu用户
$ sudo apt-get install qemu

# Windows用户麻烦把门口的小黄车挪一挪

第二步:选择一款arm

装好后可以通过以下命令qemu支持哪些arm处理器(开发板):

1
2
3
4
5
6
7
8
9
10
11
$ qemu-system-arm -M help
akita Sharp SL-C1000 (Akita) PDA (PXA270)
ast2500-evb Aspeed AST2500 EVB (ARM1176)
borzoi Sharp SL-C3100 (Borzoi) PDA (PXA270)
...
vexpress-a15 ARM Versatile Express for Cortex-A15
vexpress-a9 ARM Versatile Express for Cortex-A9 # 👈没错就它了
...
witherspoon-bmc OpenPOWER Witherspoon BMC (ARM1176)
xilinx-zynq-a9 Xilinx Zynq Platform Baseboard for Cortex-A9
z2 Zipit Z2 (PXA27x)

选中自己喜欢的“硬件”环境后,理论上就可以“上电”了,不过且慢——操作系统还没装呢!所以接下来很重要的一步就是制作自己的Linux系统镜像。

制作Linux系统镜像

做嵌入式Linux的人都知道,一个完整的嵌入式Linux操作系统基本分为三大块:

  • u-boot:上电初始化并引导内核
  • kernel:Linux内核
  • rootfs:根文件系统,busybox

由于qemu可以直接引导内核,所以这里暂且略过u-boot的移植。其它两个源码建议选择“稳定版”,别给自己找麻烦。

第一步:移植内核

linux内核官网下载源码并解压(我嘞个去!Linux都步入5.0时代了,果断下载),然后根据以下命令移植:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 0. 可能需要安装flex、bsion,如果之前没装的话,否则编译镜像时会出错
$ sudo apt install flex bison

# 1. 解压源码并进入目录
~/varm/os$ tar xf linux-5.0.3.tar.xz && cd linux-5.0.3

# 2. 将内核设置为vexpress的配置,即vexpress-a9开发板
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- vexpress_defconfig

# 3. 编译内核镜像zImage
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- zImage -j8

# 4. 编译其它驱动模块,制作rootfs时候要用
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- modules -j8

# 5. 编译设备树
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- dtbs -j8

根据上述命令完成kernel移植之后,所有需要的镜像及其相关文件全部放在<kernel_dir>/arch/arm/boot中:

1
2
3
4
5
6
7
8
# zImage和dts就是真正需要的东西
~/varm/os/linux-5.0.3$ ls arch/arm/boot/
Image bootp deflate_xip_data.sh install.sh
Makefile compressed dts zImage

# 为了之后操作方便,我把它们放到varm/os目录下
~/varm/os/linux-5.0.3$ cp -rf arch/arm/boot/zImage .. # 内核镜像
~/varm/os/linux-5.0.3$ cp -f arch/arm/boot/dts/vexpress-v2p-ca9.dtb .. # 设备树描述

第二步:启动内核

既然有了内核镜像文件,就可以先小试牛刀了,qemu走起!

1
2
~/varm/os/linux-5.0.3$ cd ..
~/varm/os$ qemu-system-arm -M vexpress-a9 -m 512M -kernel zImage -dtb vexpress-v2p-ca9.dtb -nographic

可以看到kernel被成功启动了,但由于没有文件系统,内核向你抛出了一个异常。
此外,上述命令比较长,简单解释下:

1
2
3
4
5
6
qemu-system-arm \ # 虚拟机启动
-M vexpress-a9 \ # 指定开发板为vexpress-a9
-m 512M \ # 配置虚拟机内存为512M
-kernel zImage \ # 指定内核镜像文件
-dtb vexpress-v2p-ca9.dtb \ # 指定设备树文件
-nographic \ # 不需要图形界面(LCD)

还有一点,quem启动后是一个独立进程,所有的Ctrl+C和其他中断信号都会被这个进程来接,程序无法关闭,最好的办法是新建一个终端,用kill来杀!

1
2
$ killall qemu-system-arm
# 也可以是ps先看qemu的pid,在用kill <pid>来杀,我只是觉得那样麻烦

第三步:移植busybox

同样先从busybox官网把源码包下载下来,然后开始移植:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 1. 解压源码并进入目录
~/varm/os$ tar xf busybox-1.30.1.tar.bz2 && cd busybox-1.30.1

# 2. 选择默认配置
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- defconfig

# 3. 编译busybox
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -j8

# 4. 安装busybox到./_install目录
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- install

# 顺利完成上述步骤后,可以在`busybox/_install`
# 目录下看到各种`usr lib bin`之类的文件系统结构
# 这就是rootfs的雏形了,现在还需要在此基础上做些加工

# 5. 为了方便之后操作,一样将_install目录放到varm/os下,并重命名为rootfs
~/varm/os/busybox-1.30.1$ mv _install ../rootfs

第四步:制作根文件系统rootfs

busybox已经生成了linux常用的一些命令和简单的目录结构,现在还差两个东西:

  • busybox的命令执行是依赖于交叉编译工具的动态库的,所以要把动态库放入rootfs
  • 需要给文件系统一些默认的设备描述符,否则你想让它往哪输出
1
2
3
4
5
6
7
8
# 1. 拷贝根文件系统的“必需品”到rootfs目录
~/varm/os/busybox-1.30.1$ cd ..
~/varm/os$ mkdir rootfs/lib # 创建系统库文件存放目录
~/varm/os$ cp -P /usr/arm-linux-gnueabihf/lib/*.so* rootfs/lib # 拷贝gcc动态库

# 2. 创建设备目录以及4个tty终端和调试串口
~/varm/os$ mkdir rootfs/dev # 创建设备描述符目录
~/varm/os$ for i in 1 2 3 4; do sudo mknod rootfs/dev/tty$i c 4 $i; done

第五步:创建rootfs镜像

所谓镜像可以理解为一张虚拟的光盘,存放操作系统。但玩过树莓派的同学应该都知道它的存储其实就是一个SD卡,那好,我们就创建一张虚拟的SD卡给qemu加载。

1
2
3
4
5
6
7
8
# 1. 生成镜像文件(虚拟SD卡),且大小为32M
$ dd if=/dev/zero of=rootfs.ext3 bs=1M count=32 # 创建一个32M的空文件
$ mkfs.ext3 rootfs.ext3 # 将该文件格式化为ext3

# 2. 挂在镜像到/mnt目录,并将rootfs目录导入其中
$ sudo mount -o loop rootfs.ext3 /mnt
$ sudo cp -r rootfs/* /mnt
$ sudo umount /mnt

现在整个rootfs.ext3镜像制作完成,再加上zImage内核镜像,可以起飞了可以开机正常加载了,重新调整一下qemu的启动命令:

1
~/varm/os$ qemu-system-arm -M vexpress-a9 -m 512M -kernel zImage -dtb vexpress-v2p-ca9.dtb -sd rootfs.ext3 -nographic -append "root=/dev/mmcblk0 console=ttyAMA0"

上边的命令大体上和内核启动时一样,主要是增加了-sd rootfs.ext3文件系统的SD卡和对应分区,以确保内核能正确加载文件系统。经过一分钟左右的等待,我们的最小Linux系统成功运行起来了😄:

如果顺利的话内核会成功挂在文件系统,从此可以愉快玩耍了。以上就是整个ArmLinux的虚拟机搭建过程,目前位置整个环境基本OK,但在真正写代码之前还有些事情要做,总之等遇到了再查缺补漏。

小结一下

  • QEMU是一款虚拟机,可以虚拟常见的通用处理器架构,包括ARM,支持很多开发板模拟
  • vexpress-a9是ARM官方出的一款开发板
  • QEMU可以直接引导内核,因此可以不用移植u-boot

Floating-point Numbers Aren’t Real

在数学科学种,浮点数不是“实数(real)”,尽管他们在很多编程语言中被称为real,比如Pascal和Fortran。实数是无限精度并连续无损的;浮点数被限制了精度,所以它们是有限的,类似于“表现不好”的整数,因为它们在整个范围没有均匀间隔。

举个例子,把2147483647(32位int的最大值)分配给一个32位float变量(名为x),然后打印。你会得到2147483648。现在打印x - 64,仍然是2147483648。现在打印x - 65你将得到2147483520!为什么?因为这两个浮点数之间的间隔是128,浮点运算会舍入到最近的浮点数。

(译注:类似于分辨率,128是浮点类型在内存中能表现的最小单位——间隔)

IEEE浮点数是基于基数为2的科学记数法的固定精度数:1.d1d2…d(p-1)x2^e,p代表精度(float是24,double是53)。两个连续的数之间的间隔应为2^(1-p+e),可以安全近似于ε|x|,ε表示机器的epsilon(2^(1-p))。

知道相邻浮点数的间隔可以帮你避免数字类型的错误。比如,你要处理迭代计算,例如搜索方程的根,要求找出比超过系统能表达精度更高的数字是没有意义的。确保你要求得的差值没回比间隔更小,否则会进入死循环。

自从浮点数成了实数的近似值以来,就存在不可避免的错误。这种错误被称作四舍五入,会导致意外的结果。当你去减一个近似值时,例如高位数相互抵消,所以在浮点运算的结果,低位的有效数字(被四舍五入的那部分)被提升到了高位,本质上污染了进一步的相关计算(被称作涂抹)。你需要细致地观察你的算法,以阻止这种灾难性的消除。为了说明,考虑用二次公式求解方程x²-100000x+1=0。由于表达式-b+sqrt(b²-4)中的操作数近似相等,你可以先计算出r1=-b+sqrt(b²-4),再求得r2=1/r1,因此任何二次方程,ax2+bx+c=0,这个结果满足r1r2=c/a。

涂抹可以以更精妙的方式运行。假设一个库通过1+x+x²/2+x³/3!+…的公式计算e^x,这只适用于x是正数,但想想当x为一个很大的负数时会发生什么。偶数次方结果是大的正数,减去奇数次方的幅度甚至不会影响结果。这里的问题是,正项在数字中的地位要比真实答案影响更大。结果就是趋向于无穷大!这里的解决方案也很简单:给一个负x,计算e^x=1/e^|x|。

不言而喻,你不应该将浮点数用于金融类应用——就是像python和C#语言中的十进制类。浮点数用于高效的科学计算。但失去了精度,性能就毫无价值,所以请记住舍入错误的来源及相应的代码。

2008年的时候我正在上大学,我所在的城市房价还盘旋在五六千的样子,作为愤青之年的我们,最为津津乐道的一个话题——一个人不吃不喝多少年才能买得起房?那时掐指一算,大概要二十年吧。

毕业后,我本可以向父母伸手凑个首付,从此当个幸福快乐的房奴,但毕竟处在一个桀骜不驯的年纪,励志要凭自己的本事买房。所以我努力工作赚钱,升职加薪,可房价也是个不服输的“励志哥”,我的工资和房价就像赛道里的运动员,一路你追我赶。坚持了多年的“长跑”后,我投降了,不是我没毅力,真的是速度不够快。(虽然攒够了首付钱,但为了减轻房贷压力,还是向家里借钱了)

阅读全文 »

在很多软件开发中,单元测试是最为基础且最有效的软件质量保证手段。我个人是搞C语言开发出身,想当年,printf打天下,从来就没怕过谁。而后来逐步接触C#/Java等企业级的编程语言,才明白单元测试对功能模块和业务的重要性,加之现在IDE的强悍,查错效率也是极高的。

GO语言也提供了相对完善的测试框架——testing包,其实这类内容网上一搜一大把,作者却将其作为本书的最后一章单独提出来,想必他也清楚“测试”对软件开发而言的地位。

所以,就我个人而言,最后一章不难,主要是学习如何全方位地做软件测试,即testing包和go test命令的使用,这些内容很重要,包括:

  • 如何创建单元测试
  • 如何模拟生产环境
  • 如何测试性能

GO语言单元测试

  1. 确保文件名为xxx_test.go的形式
  2. 确保单元测试函数为TestXXX(t *testing.T)的形式
  3. 使用go test直接运行所有的测试文件

此外,本书还按场景提供了不同的测试方法,主要有:

  • 基础单元测试:最常规的,按照预期值测试
  • 表组测试:多个输入值,多个预期值测试
  • 模仿调用:本地模拟服务端,排除网络问题,仅测试业务
  • 端点测试:针对类似RESTful结构,测试某个单一路径功能

为了说明这些测试是如何实现和使用的,我们需要先声明两个全局变量,后续的所有代码中都会调用:

1
2
3
4
5
// 如果测试通过,在行尾打✅
const checkMark = "\u2713"

// 如果测试失败,在行尾打❌
const ballotX = "\u2717"

1.基础单元测试

以下测试是按照书中的举例写的,主要是对某个url发起http请求,正常情况下服务端都会响应200表示OK,但也可能会出现404找不到,或者干脆连接超时的情况出现。这段代码中的url是书中提供的,会出现404或超时

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
func TestSingle(t *testing.T) {
url := "http://www.goinggo.net/feeds/posts/default?alt=rss"
statusCode := 200

t.Log("测试下载: ", url)
{
t.Logf("\t预期收到状态码为: %d", statusCode)
{
resp, err := http.Get(url)
if err != nil {
t.Fatal("\t\t调用http.Get()发起请求失败: ", ballotX, err)
}
t.Log("\t\t调用http.Get()发起请求", checkMark)

defer resp.Body.Close()
if resp.StatusCode == statusCode {
t.Logf("\t\t收到状态码 %v %v", resp.StatusCode, checkMark)
} else {
t.Errorf("\t\t收到状态码 %v %v", resp.StatusCode, ballotX)
}
}
}
}

// ---------- go test -v ----------
=== RUN TestSingle
--- FAIL: TestSingle (3.69s)
unit_test.go:18: 测试下载: http://www.goinggo.net/feeds/posts/default?alt=rss
unit_test.go:20: 预期收到状态码为: 200
unit_test.go:26: 调用http.Get()发起请求 ✓
unit_test.go:32: 收到状态码 404

如上述代码可以看到,所谓单元测试,主要是通过执行某些过程,比对其结果是否符合预期。这里发起http请求只是过程,而断言http.Get()函数会成功以及服务端响应200是预期。从执行结果可以看到,请求成功了,但服务端响应状态码为404,不符合预期,测试失败。

注意t.Log的使用,基本是从log包定制的一套日志实例,主要是能在每行日志前增加测试的源文件及行号,便于错误定位。

另外,源码中每个t.Log后都带有一对大括号,这个不是必须的,因为几乎每个日志内容里都含有\t缩进符,估计作者的本意是为了直观地表示缩进吧。

2.表组测试

很多时候我们都需要用大量且不同的参数来测试某个函数的执行结果是否都符合预期,而这就是表组测试。其实表组测试没有什么特别的地方,无非就是把测试参数装进数组里,通过遍历测试每一个

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
func TestMulti(t *testing.T) {
urls := []struct {
url string
code int
}{
{
"https://www.github.com",
http.StatusOK, // 200
}, {
"https://www.github.com/philon/123",
http.StatusNotFound, // 404
},
}

t.Log("测试访问一组URL,并检查状态码是否正确")
{
for _, u := range urls {
t.Logf("\t对'%s'发起请求,预期状态码为: %d", u.url, u.code)
{
resp, err := http.Get(u.url)
if err != nil {
t.Fatal("\t\t发起请求失败: ", err, ballotX)
}
t.Log("\t\t发起请求成功", checkMark)

defer resp.Body.Close()
if resp.StatusCode == u.code {
t.Log("\t\t收到状态码: ", resp.StatusCode, checkMark)
} else {
t.Log("\t\t收到状态码: ", resp.StatusCode, ballotX)
}
}
}
}
}

// ---------- go test -v ----------
=== RUN TestMulti
--- PASS: TestMulti (1.77s)
unit_test.go:52: 测试访问一组URL,并检查状态码是否正确
unit_test.go:55: 对'https://www.github.com'发起请求,预期状态码为: 200
unit_test.go:61: 发起请求成功 ✓
unit_test.go:65: 收到状态码: 200
unit_test.go:55: 对'https://www.github.com/philon/123'发起请求,预期状态码为: 404
unit_test.go:61: 发起请求成功 ✓
unit_test.go:65: 收到状态码: 404

如上,把多个测试参数放进一个[]struct{}形式的切片中,并通过for-range循环遍历测试,就是表组测试的最基本用法。剩下的内容和第一节的单元测试没什么不同。

3.模仿调用

前两个例子一直是对某些网站发起请求,如果对面的服务器挂了怎么办?或者我们的服务器根本就还没上线怎么办?模仿调用就是模拟服务端的意思,模拟出一个服务器,对其发起请求,主要测试业务逻辑是否正常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// mockServer 创建虚拟http服务
// 默认响应200,并返回一段json数据
func mockServer() *httptest.Server {
f := func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
w.Header().Set("Content-Type", "application/json")
fmt.Fprintln(w, `{"id": 9527, "login": "philon"}`)
}

return httptest.NewServer(http.HandlerFunc(f))
}

func TestMocking(t *testing.T) {
...
// 在测试函数中将模拟服务器启动
server := mockServer()
defer server.Close()
// 通过server.URL可访问到模拟服务器
resp, err := http.Get(server.URL)
...
}

这段代码没有完,因为除了模拟http服务端的部分,其它都会基础单元测试一样,就不重复了。http服务端模拟主要通过httptest包实现的,把这部分用法搞清楚即可。

4.端点测试

众所周知,RESTFul的基本设计思想就是通过URL资源访问,并以GET|POST|PUT|DELETE|PATCH等请求方法区别不同的业务逻辑,比如:

  • GET /users 获取用户列表
  • PATCH /users/philon/profile 更新用户配置
  • DELETE /users/philon 删除指定用户

所谓的端点也就是访问路径的意思,比如/users、/users/philon这样的路径,并针对不同的请求方法进行测试。

自建http服务端

要完成这部分的演示,需要先自建一个RESTFul的服务端,或者说传统的MVC架构的服务,为了简单说明,这里仅仅实现GET /users获取用户列表的功能。

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
// Routes 全局路由映射
func Routes() {
http.HandleFunc("/users", Users)
}

// Users 用户列表控制器
func Users(rw http.ResponseWriter, r *http.Request) {
list := []struct {
ID int `json:"id"`
Name string `json:"username"`
}{
{1234, "张三"},
{4567, "李四"},
{5678, "王五"},
}

rw.Header().Set("Content-Type", "application/json")
rw.WriteHeader(http.StatusOK)
json.NewEncoder(rw).Encode(&list)
}

func main() {
Routes()
log.Println("Http server start listening: ", 4000)
http.ListenAndServe(":4000", nil)
}

上面这段代码通过go run main.go将该服务启动后,可以通过浏览器直接访问http://localhost:4000/users即可看到结果,这里用curl请求也一样:

1
2
3
4
5
6
7
8
$ curl -i localhost:4000/users

HTTP/1.1 200 OK
Content-Type: application/json
Date: Fri, 15 Mar 2019 00:18:13 GMT
Content-Length: 98

[{"id":1234,"username":"张三"},{"id":4567,"username":"李四"},{"id":5678,"username":"王五"}]

如此这般,一个最简单的RESTFul设计风格的http服务端就做好了。但是!!这并不是端点测试的全部,注意Routes()函数里的http.HandleFunc("/users", Users),这才是路由功能的实现,将路径/users只想Users函数。设想一下,如果你把Users相关的服务端的业务代码写完,你会如何测试?搭建http环境——跑服务代码——跑客户端请求代码——看结果?NO,效率太低了。因为只是测试Users函数有没有正确返回json数据,所以可以仿照3.模仿调用的方式虚拟一个http服务器,直接去测试/users路径。

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
func init() {
// 初始化路径,给端点测试用
Routes()
}

func TestController(t *testing.T) {
t.Log("测试服务端点")
{
req, err := http.NewRequest("GET", "/users", nil)
if err != nil {
t.Fatal("\t创建请求对象失败 ", ballotX, err)
}
t.Log("\t创建请求对象成功 ", checkMark)

// httptest创建虚拟服务器
rw := httptest.NewRecorder()
http.DefaultServeMux.ServeHTTP(rw, req)

if rw.Code != http.StatusOK {
t.Fatalf("\t收到状态码 %d 不符合预期 %v", rw.Code, ballotX)
}
t.Log("\t收到状态码", rw.Code, checkMark)

users := []struct {
ID int `json:"id"`
Name string `json:"username"`
}{}

if err := json.NewDecoder(rw.Body).Decode(&users); err != nil {
t.Fatal("响应不是json类型的数据 ", ballotX)
}
t.Log("\tJSON反序列化成功 ", checkMark)

if len(users) == 3 {
t.Log("\t用户列表长度检查 ", checkMark)
} else {
t.Log("\t用户列表长度检查 ", ballotX)
}
}
}

小结一下

  • GO语言自带测试框架testing包
  • go test用来运行测试
  • 测试文件必须以_test.go结尾
  • 测试有单元测试、表组测试、模拟测试、端点测试

因为橙子,我知道了褚时健。

刚步入社会的时候,碰巧公司在玉溪有业务,就订购了一车橙子,给每个员工发一箱当福利。那个时候我带着女友(现在的媳妇)在外租房,饭后我们一人吃了一个看似普通的橙子——简直太好吃了!我清晰记得那个夜晚。本来都睡下把被窝捂热了,由于太回味这橙子的味道,我和老婆冒着寒冷爬起来,又吃了4、5个。后来我在《冬吴相对论》里听到一期关于褚时健的节目,但得知大家都把这橙子叫”褚橙“,而它背后的褚时健可谓一生波折,实在了不起。

上周,朋友圈被褚时健逝世的消息刷屏,媒体也铺天盖地报道,或惋惜、或哀叹。当然,也少不了各种是非争议。我无意参与其中,所以选择读一读他的传记,看一看他的一生,以此祭奠这名时代的勇士。

阅读全文 »