最近想通篇了解一下中国历史,无奈市面上讲中国通史的书籍甚少,好评的更是凤毛菱角。故上网搜了一堆,看到“中国社会科学院历史研究所”出品的《中国历史极简本》一书,主观上觉得会是一本很有底蕴的书,而且全是精华。然而事实上本书并不注重章节的连贯性,没有中华上下五千年一气呵成的感觉,而是仅仅按照时间顺序、根据中国不同朝代,挑选军事、经济、政治、文化、人物等不同的点或面进行叙述,当然,书中对中国古代各个方面的点评也相对中肯。不论如何,区区15万字能从炎黄到宣统,把中国古代历史完整咀嚼一遍,此书想必也下了一番功夫。我个人认为,把本书作为一本科普书更适合。
六:并发和竞态
临界资源:是指同一个时段内只允许唯一一个访问者操作的资源。比如打印机、IO模块等,但Linux是多任务的,其内核对资源的管理是抢占式的。多个进程同时运行即所谓的并发,而如果多个进程都同时访问同一个资源就会产生竞态。由于驱动模块的特殊性,它不可避免会存在被多个进程同时“打开、读写、关闭”的情况。设想一下,如果某个驱动的逻辑是open的时候分配一块缓存用于read/write,close的时候又释放缓存,就会存在A进程刚打开的设备节点,B进程就关闭,缓存分配了又释放,最终在读写时导致程序崩溃。
所以,本章主要学习Linux驱动模块有哪些手段可以处理并发时的竞态问题。
原子操作
原子操作就是保证对数据修改的完整性,也就是说a = a + 1
这么简单的表达式也难以避免被编译为多个指令周期,也许在任务A中刚读完表达式右值,又被任务B更新了a
的寄存器,结果一个简单的自加1的操作都可能出现很多诡异的结果。
因此,为了确保i++
就是自加1的操作,内核封装了很多API以实现变量的原子操作:
1 | #include <asm/atomic.h> # 引入原子操作API |
自旋锁
自旋锁是一种对临界资源互斥访问的手段,也就是说在访问资源之前上个锁,访问完成后解锁,如果一个进程在访问资源是发现“锁住”了,就会原地打转——而非进入睡眠!直到锁被解开。就好比一辆车遇到红灯后停了下来但没熄火,发动机一直在空转,直到绿灯。但自旋锁有个很大的弊端——“如果红绿灯刚好坏了,发动机会永远空转下去”。
先来看看自旋锁的简单用法:
1 | #include <linux/spinlock.h> // 引入自旋锁的头 |
上边是最简单的使用方式,但自旋锁还会受到内核中断、底半部(BH)的影响,所以衍生出了更多的“锁定”和“解锁”API。就好比驾驶员在等红灯时跑去尿尿,恰好此时绿灯亮起,该怎么办?答:禁止驾驶员尿尿😄。
这些函数要视情况具体使用:
1 | void spin_lock_irq(spinlock_t* lock); // 禁用中断,并上锁 |
开发驱动时应谨慎使用自旋锁,要直到它“空转”的意思是不放弃CPU,所以在其自旋时会对CPU资源造成浪费,如果不小心锁死了,那就悲催了。
综上,自旋锁只是在访问临界资源前后加了一层排他性的锁,至于锁内的资源操作它完全不关心,然而共享资源在并发访问时往往是这样的需求:可以被同时读,但不允许同时写。也是基于此,内核提供了更多的API来满足这些场景。
- 读写自旋锁
读写自旋锁会区别读和写的资源,满足并发读取,单一写入的要求,但底层也是“自旋”的机制。
1 | // 读写锁定义 |
- 顺序锁
顺序锁是读写锁的优化版,因为读写锁的读和写操作是互斥的,所以使用顺序锁后,当资源正在写入时,依然可以被读取。
1 | // 顺序锁API的定义 |
- RCU: Read-Copy-Update
读——复制——更新的意思是:把要写的部分先读取被拷贝一个副本,然后把内容写入副本,等到何时的时机一把更新到源。
1 | #include <linux/rcupdate.h> |
互斥体
互斥的机制在多线程中是很常见的,Linux内核的互斥体metux
本质是由自旋锁实现的。但与自旋锁不同的是,互斥体会进入默认睡眠,放弃CPU抢占。
1 | #include <linux/mutex.h> |
completion
completion就是指一个执行单元等待另一个执行单元的完成信号,有点多线程同步的意思。
1 | #include <linux/completion.h> |
如何使用Bug跟踪器
不论你是否把它们称作bug、缺陷、设计副作用,你都无法摆脱它们。知道如何提交一个优秀的bug反馈以及在某方面需要什么,是保持项目顺利推进的关键技能。
一个优秀的bug反馈需要做到三件事:
- 如何复现bug,尽可能地清晰描述,以及它出现的频率。
- 理论上应该发生什么,起码在你看来会发生什么。
- 实际上又发生了什么,或者至少记录下你所掌握的更多信息。
关于bug反馈信息的数量和质量,要做到和反馈者所掌握的一样多。愤怒地反馈个简短的bug(“这个函数太烂了!”)除了告诉开发者你此刻很糟糕以外,没什么卵用。一个含有充足上下文的bug可以更容易被复现而赢得大家的尊重,即使它已停止发布。
变更bug的状态,例如打开-关闭,这是你对bug思考的公开声明。花点时间解释为何你觉得bug应该被关闭,可以挽回在恼怒的经理和消费者面前辩解的时间。变更bug的优先级也类似于一份公开声明,仅仅是因为它对你来说微不足道,但并不意味着它会让其他人停止使用该产品。
不要为了你自己的目的就去重载一个bug字段。将“VITAL:”添加为bug主题的字段可能有利于你对一些报告结果做排序,但它也将被其他人复制并难以避免拼写出错,或者在其他报告中将其删除。使用一个新值或新字段来代替,并用文档记录字段该如何使用,以便他人不必重复。
确保每个人都知道如何找出团队认可的bug以被处理。通常可以采用一个公共的查询名称。确保每个人都采用相同的查询方式,在没有事先通知团队你已经改变了某些内容的情况下,不要更新此查询。
最后,记住bug不是工作的一个标准单元,只是一行代码的精确测量。
二:调试技术
古人云:别跟老子说什么断点、跟踪、gdb、lldb…老子调代码从来都是print😂️。调试在程序开发中是非常重要的手段,就像单元测试一样,是保障软件质量的主要手段之一,别不当回事!
言归正传,linux内核提供了多种调试技术,但因为驱动程序不是普通的程序,很多常见的调试工具到内核这一层基本都扑街了,printk反而成了最朴实有效的手段之一,但不论如何,多掌握其他的调试手段和工具,对于今后定位内核模块的错误,总会有帮助的。
打印 - printk
printk
就是常规的打印输出,但与应用成的printf
稍微不同,往往会看到这样的调用printk(KERN_ALERT"hello world")
,其中的KERN_ALERT
表示打印级别,内核源码中定义了多种打印级别,且看定义:
1 | #define KERN_SOH "\001" /* ASCII Start Of Header */ |
从上定义可以看到,内核共提供了0-7个级别,数值越小,优先级越高。
屏蔽其他级别打印
/proc/sys/kernel/printk
文件很重要,可以通过它来屏蔽不同级别的打印输出,我们迅速写一段代码:
1 | static int __init meme_init(void) |
以上代码只是在模块加载的时候打印了7个级别的内容,现在做一件事:
1 | # 查看该文件发现默认打印级别小于7,即除了KERN_DEBUG级别,其他都能显示到终端 |
从上边的指令可以看到,通过修改/proc/sys/kernel/printk文件的值可以直接强制控制台打印的日志级别,这可要比你反复注释+编译的手段高明多了。
通过procfs文件查询调试
printk函数固然简单易用,但除了逼格很low之外还存在个技术上的障碍——大量使用printk会极大地拖累程序的性能,原则上也仅用于常规和错误信息提示,像for、while之类的循环内千万别用它。
然而实际上用户需要时刻掌握各种设备的状态信息,比如cpu当前频率/温度、内存占用率等等,printk显然不能胜任,轮到procfs登场了,我们知道proc是内核的一个虚拟文件系统,能够以文件的形式展现整个系统内部的信息,而通过procfs文件查询调试技术,就是在/proc
目录下创建驱动模块自己的目录和文件——俗称入口,供用户随时访问。
我个人觉得,驱动暴露在proc文件系统的入口其实很少会用于“调试”,而是一些关键信息的展示,比如cpuinfo
、meminfo
等文件,这就是典型的应用场景,cpu和内存驱动创建的文件,可以直接查看相关设备的细节信息。
在proc
文件系统中创建入口文件非常简单,总共有两种形式:
1. 普通proc入口
之前的学习笔记中,我们已经创建了meme驱动模块的设备节点/dev/meme
,现在实现一个新功能,用户只需要通过命令cat /proc/meme/state
即可查看设备节点/dev/meme
的文件访问状态,例如打开、关闭、正在读、正在写等等。
首先,在procfs创建/删除入口文件的API有这么几个:
1 | // proc入口文件的结构体定义 |
然后,通过调用proc_mkdir()
和proc_create()
即可创建出/proc/meme/state
目录和文件了,几乎没什么难度,注意在创建proc的入口文件时需要指定一个file_operations *fops
,其实就是文件访问函数的映射,和字符设备章节中的操作一模一样。而/proc/meme/state
是一个只读状态文件,所以我们只需要实现read
函数的功能——返回状态字符串。
1 | /** |
以上是meme驱动模块在proc入口状态文件的相关实现,既然要监听的是/dev/meme
设备节点的访问状态,上述代码是通过meme_cdev_state
全局变量来实现的,故这边变量自然是在字符设备的代码实现中被修改的。
1 | /** |
如此一来,用户访问/dev/meme
设备节点,meme_cdev_state
值就会被改变,对应的状态自然就暴露到/proc/meme/state
的read触发函数中了,完美~
测试一下:
1 | / # insmod meme.ko |
2. seq_file接口
seq_file
主要是用于处理那些比较“大”的proc入口文件。所谓的大不是说文件体积,举个例子,当我们需要开发一个串口驱动,而这个驱动需要记录串口每一次的收发历史记录,用这种方式再何时不过。seq是序列的意思,即通过迭代的方式,把驱动的某些状态信息按顺序打印出来。
seq_file
提供了4个迭代函数:
1 | struct seq_operations { |
现在再创建一个入口文件/proc/meme/info
,当用户通过命令cat /proc/meme/info
时,打印0-100行计数内容:
1 | /** |
从上边的代码可以看到,除了实现4个迭代的触发函数之外,还需要实现文件的open触发,剩下的内容就和常规的proc入口文件创建机制没什么区别了。测试一下:
1 | / # insmod meme.ko |
代码实现的比较简单,在start
函数中为迭代分配一段内容空间用于计数,而在next
每次迭代时将计数累加1直到100后跳出迭代,每次迭代后都会自动调用show
将记录值打印出来。
通过strace命令监视
strace移植
由于Busybox默认是不带strace的,需要自行移植。
1 | # 下载strace源码并移植 |
参照官方文档测试一下:
1 | / # strace -c ls > /dev/null |
又或者测试一下meme驱动模块
1 | / # strace echo 123 > /dev/meme |
总之strace
是一个非常牛逼的诊断工具,由于本文主要针对的是Linux驱动模块技术,关于这个命令的使用详解就不赘述了,自行Google。
oops消息
oops也就是内核甩了一跤所发出的惨叫,当然导致内核摔跤的绊脚石大概率是我们写出了质量底下的驱动模块导致。举个最简单的例子:
1 | static void __init meme_init(void) |
以上是meme模块加载时的初始化代码,但是我们试图往NULL
地址里赋值,很显然这将引发段错误,如果直接insmod模块,不出意外内核将发出一声惨叫——oops!!!
来看看效果:
1 | / # insmod meme.ko |
多么熟悉的味道,信息量好大,让人眼花缭乱。别着急,主要留意几个地方:
Modules linked in: meme(O+)
说明导致oops的是meme模块驱动。PC is at meme_init+0x18/0x94 [meme]
说明引发oops的函数是meme_init
。Exception stack(0x9ddfbfa8 to 0x9ddfbff0)
以及之后的N个地址表示异常的栈区,读懂这些地址需要比较丰富的经验,刚开始没必要太纠结这部分。Segmentation fault
表示错误类型是段错误
使用gdb、kgdb等调试器
关于gdb调试神器如何应用到内核模块,就不详述了,如果真想了解可以参考IBM这篇《Linux系统内核的调试》。
其实gdb可以提供诸如断点、变量监视、单步执行等非常有用的功能,在追踪bug的效率方面无疑可以碾压上述的几种方式。但是可别忘了,我们写的是Linux内核模块,这些断点、单步执行的功能其实很难应用的内核层,而且驱动本质上是为硬件服务的,你要代码单步执行可以,硬件那边可没有暂停键啊!
所以,在Linux驱动模块的开发过程中,gdb这种工具反而应该尽量避免!
小结一下
printk()
是最朴实有效的调试方式,Linux一共提供了8个打印级别,可以通过echo N > /proc/sys/kernel/printk
来限制级别。- 可以通过创建
/proc/<entry>
入口文件来减少对printk的依赖 seq_file
是procfs入口文件的特殊实现方式,主要用于状态信息庞大的驱动,按顺序迭代输出。strace
是一个非常有用的程序执行跟踪的命令。oops
本质上是内核某些地方执行出错产生的提示信息,可以用于定位问题根源。
高强度工作不会得到回报
作为一个程序员,努力工作往往得不到回报。你和你的少部分同事始终相信你们在办公室为某个项目付出了大量的时间。但事实上比你工作少的人可能得到的更多——有时是非常的多。如果你尝试每周强制高负荷工作超过30小时,你很可能工作强度太高了。应该考虑减少工作量以提高工作效率。
这种说法看似反直觉且有争议,但这是必然的结果,因为程序和软件开发是一种需要全情投入持续学习的过程。当你在做项目时你要理解更多领域方面的问题,找出更多有效方法来实现目标。为了避免无效工作,你必须流出时间来观察正在做的事情的效果,反思所看到的事物,并改变你相应的行为。
职业编程可不像是拼命跑几公里那样,在铺好的跑道尽头可以看到终点。更多的软件工程更像是一场马拉松越野赛。在黑暗中,只有一份粗糙的地图作为指引。如果你只是从一个方向出发,你可以跑得尽可能快,你可能会令人佩服,但你终究不会成功。你需要保持可持续的步伐,当你了解到更多你的位置和前进方向是,你需要调节你的航向。
此外,你总要学习更多有关软件开发的信息,尤其是编程技术。你很可能要读书,开会,和专家交流,尝试新技术的实践,以及从你的工作中掌握强大的工具。作为一个专业程序员,你必须保持在自己的专业领域自我更新——正如脑外科医生和飞行员应该在他们自己的领域保持最佳状态一样。你应该拿出傍晚、周末、假期来自学,因此你不能让你的傍晚、周末、和假期被你项目上的工作占用。你希望让一个本周工作了60小时的脑外科医生给你做手术吗,或者一周飞了60小时的飞行员?当然不想,准备和教育是他们专业的重要组成部分。
专注于项目,通过寻找聪明的解决办法来尽可能多地作出贡献,提高你的能力,反思自己正在做的,并调整你的行为方式。为避免让自己和我们的职业感到尴尬,像仓鼠一样在笼子里快速转动轮子。作为一个职业程序员你应该知道尝试以每周60小时的高负荷工作可不是一种明智的做事方式。请像专业人士一样:做准备、看效果、观察、反思、并改变!
识时务者——《大清相国》书评
我也不记得是从哪里听说的这本书,只是大概记得说里面的主角为人处事很厉害,而且发现国家领导人都在推荐此书,最近似乎还打算拍成电视剧,所以决定拜读一番。
小说讲述康熙年间陈廷敬在朝廷为官五十余载,期间各种政治博弈,你来我往,顷刻间便可断送职业生涯,脑袋搬家的“故事”。而陈廷敬年少有才,考取进士,本人凭借等、稳、忍、狠四个字,一路在官场起起伏伏犹如火中取栗,好在为人正直,大公无私,终究没被奸人所害,晚年领的康熙一句“宽大老城,几近完人”后,因身居要职,看透仕途凶险,便主动装聋,请辞病退,告老还乡,方才全身而退。
书中对各种人物的形象刻画,情节把握,我觉得还是比较好的。因为小说主要讲述朝野上下各种博弈,有点像男版《甄嬛传》(虽然我没看过),很多历史情结其实是一笔带过的。我在读本书之前听到的评价比较高,所以对本书的期待很高,但就我个人而言并没有太多吸睛的地方,但也不至于无聊,因为情节环环相扣,读起来还算有意思,最精彩的地方莫过于陈廷敬最后暗中连环参人,很是过瘾。
四:字符设备
由于Linux中一切皆文件
的思想,字符设备(键盘/鼠标/串口)、块设备(存储器)、网络设备(套接字)作为三种主要设备类型,我们在用户层基本可以通过open close read write
来完成对设备的绝大部分操作。其中字符设备是涵盖面最广的一种,而且其概念相对容易理解,因此以字符设备为切入口,掌握内核与用户层之间的调用机制是再合适不过了。
本章主要学习:
- 字符设备驱动的基本概念
- 如何向内核注册/注销一个字符设备
- 如何通过设备节点、文件API调用驱动的函数
什么是字符设备?
所谓字符设备,泛指那些输入输出以串行方式、按顺序、一个字符一个字符的收发数据的设备。比如键盘,按键数据是一个一个发给系统的,哪怕是组合键;鼠标的坐标;LED灯;串口终端等。而这些数据如果不能及时被系统处理,要么被缓存,要么直接丢弃,但不论如何,当系统去处理这些数据的时候,也是按先来后到的顺序。
你可以设想以下,你的电脑突然卡顿了5秒钟,而这期间你疯狂打了10个字,毫无疑问这10个字的数据被缓存起来了,等系统空闲的时候开始处理它们,现在你是希望这10个字是按照你打的顺序出现在屏幕呢,还是希望它们毫无章法地出现在屏幕。
所以,字符设备,就是数据的IO总是以连续的形式处理的设备类型。
顺带提一句块设备,与字符设备概念做个对比。比如硬盘:你和你的右手出去约会并拍了很多照片回来,然后你把照片导入电脑,而硬盘的存储空间并不是像水杯一样从下往上按顺序填满,而是根据某种算法“随机”存,所以在真实的硬盘中,存放你1号照片的存储区域旁边,并不是2号照片,可能是某份日语学习资料;同理,3号照片可能与去年下载的种子呆在一起。
第一个字符设备 meme
在操作系统中,设备不一定要真实存在,虚拟设备也是设备,可以通过文件函数进行操作。因此为了掌握字符设备的知识,最简单的方法就是自己动手写一个虚拟字符设备。
meme的中文意思是米姆、莫因,表示文化DNA的意思,而作为我学习驱动的实例,它就是“记事本”,用户可以用多种方式读写它,而我给这个虚拟设备取这么个名字没别的意思——just for fun!
先来看看我打算要meme实现哪些内容:
- 向内核注册/注销自己
- 支持open/close/read/write等文件操作函数
字符设备的注册与注销
要像内核注册一个设备,必须提供它的设备号作为唯一标示,字符设备的设备号由主设备号和次设备号共同组成
- 主设备号(major):表示设备的分类编号,比如1代表u盘、2代表显示器、3代表打印机
- 次设备号(minor):表示同一分类的实例编号,比如1号u盘、2号u盘、3号u盘
Linux内核可以静态注册和动态分配的方式注册一个设备号:
1 | // 动态申请,由内核自动扫描空闲设备号给它 |
⚠️注意⚠️:在实际开发过程中,强烈建议仅通过动态分配的方式注册设备,因为说不准你自己规划好的设备编号会在未来被Linux内核给占用了。
字符设备的注册大体分两步:
- 初始化一个
cdev
字符设备的数据结构,向内核说明自己是字符设备 - 分配一个设备号与cdev绑定起来
下面就是一个最纯粹的字符设备注册和注销的实现:
1 | #include <linux/init.h> |
可以看到,字符设备的注册/注销一般是在模块的加载和卸载函数中实现的,整个过程非常简单:
申请设备号 –> 初始化字符设备 –> 内核添加字符设备 –> 内核删除字符设备 –> 释放设备号
关于cdev_init / cdev_add / cdev_del
等函数没有展开说明是因为后续有更好的方法,不要拘泥于此。不过注意cdev_init(&cdev, &fops)
这行,fops是接下来要学习的一个非常重要的数据结构——文件操作。
字符设备的文件操作
因为Linux的一切皆文件,理论上在用户层任何设备都可以通过文件相关的API函数调用设备,而作为设备驱动本身,很重要的一部分工作就是把用户层的各种文件操作关联到各种驱动函数当中,而这离不开一个结构体struct file_operations
,先来看看内核对其定义:
1 | struct file_operations { |
乍一看,很复杂!其实用面向对象的思维可以简单理解为——file_operations就是一个需要继承实现的interface。因此不要去纠结里面的内容有多少,大部分时候我们都是根据需要去实现其中的部分函数即可,等具体实现的时候再来参考对应函数在其中的声明形式。(通过该结构体打通了内核与应用间的任督二脉)
为用户层实现open/close和read/write
- open和close一般都是处理一些设备复位的工作(或者干脆不做任何处理),但如果有逻辑要放在open/close里执行,务必留意应用程序可能会重复多次打开/关闭同一个文件的情况。
- read/write在内核中主要围绕
copy_to_user
和copy_from_user
两个函数来处理,它们负责把“交换”用户空间与内核模块的数据,但要特别小心文件偏移,不是所有设备在读写的时候都需要偏移的!比如读取LED灯的状态,每次都应该从头开始读。
1 | // 对应用户层read读取函数,通常用户接收设备内容,如串口收数据 |
代码很简单,该说的注释都已经说了,除了file_operations里的这句:.owner = THIS_MODULE
。首先要直到THIS_MODULE是一个结构体定义,之所以把它指向owner(模块自身)是为了防止模块正在进行其他操作(如读写)时,被rmmod卸载。每个以模块形式存在的驱动,最好都把这句加上!
设备节点与操作
设备节点是是用户层与内核模块沟通的桥梁!
作为初学者的我曾一度认为/dev目录下的节点就是设备本身,显然这种看法很幼稚,内核真正把设备和驱动暴露在/proc和/sys目录(再说一遍,其实是文件系统!!)而设备节点是在用户层创建的一个特殊文件,它的创建和删除几乎不影响内核对设备本身的管理。
设备节点的创建命令为:mknod <node_name> <type> <major> <minor>
把meme编译好之后实际测试一下:
1 | # 将驱动模块拷贝到镜像 |
从上面的操作中可以看到,meme驱动的基本文件操作功能已经具备,之所以最后读出来的内容有乱码,是因为这是一个设备,不是普通文件,该设备强制有0x100
的存储空间,每次都会被全部读出来。
小结一下
- 字符设备是一种通过顺序读写来操作的设备
- 主设备号表示设备类型,次设备号表示设备实例号
- file_operations定义了文件操作函数的接口
- 设备节点是在用户层创建的特殊文件
大师神话
任何在软件领域工作时间长的人都会听过这样的问题:
我碰到一个XYZ异常,你知道这是什么问题吗?
这些提问很少会包含堆栈跟踪、错误日志、或任何关于问题的上下文。他们似乎认为你是在不同的平面上操作,即便不基于证据的分析,也能找到解决方案。他们认为你是大师。
我们期待那些对软件陌生的人提出这样的问题:对他们而言,系统看上去是有魔力的。但让我担心的是在软件论坛里也看到这种话。类似的问题在程序设计版块中出现,例如“我正在构建库存管理。我是否要用乐观锁?”讽刺是的,提问人往往比回答者能更好的解答这个问题。提问者肯定知道上下文、必备条件,并能读到有利和不利的不同策略。但他们已经寄希望于你,在不要上下文的情况下给予他们智慧的答案。他们想要的是魔法。
是时候为了软件行业驱散这种大师神话了。“大师”是人。他们像我们一样是通过逻辑和系统性地分析问题。他们也利用心里的捷径和直觉。回顾一下你见过的最优秀的程序员:可能在某一点上,他知道的比你现在做的还少。如果一个人看起来像大师,那是他多年的学习和完善思维的过程。一个“大师”只是一个有着无穷无尽好奇心的聪明人。
当然,天资也存在巨大的不确定性。很多黑客都比以往任何时候都更聪明、知识渊博、生产力更高。尽管这样,揭穿大师神话也具有积极的影响。例如,当和一个比我聪明的家伙工作的时候,我会干一些跑腿的活,以提供足够的上下文来有效利用他或她的能力。移除大师神话也意味着移除了感知障碍。没有了魔法屏障,我看到了自己可以持续进步。
最后,软件领域的最大障碍之一是那些坚定传播大师神话的聪明人。可能是出于自尊心,或者是在客户或领导那里增加一点价值的策略。讽刺的是,这种态度反而会导致聪明人贬值,直到他们的贡献成长不如他们的同行。我们不需要大师。我们需要愿意培养他所在领域专家的专家。这是我们所有人的空间。
自然而然——《创始人手记》读书感悟
本周到杭州出差,第一次住全季酒店,在附近一家给人仿佛穿越到80年代的如家酒店的衬托下,全季简直闪瞎了我的狗眼。那装修、那格调、那门面,只能用两个字来形容——内敛😂,但是,全季不是我住过最奢华的酒店,但一定是我目前住过最舒服的连锁酒店,也许如它的老板所说,这是面向新中产的中档型酒店,所以对我等打工一族而言,全季的逼格,很是受用。
说到逼格,我还是第一次见有连锁酒店会在客人的床头柜前放一本书的,端起一看《创始人手记,一个企业家的思想、工作和生活》作者季琦,再看封面:“这特么谁啊?”翻看一下:“哦,这家酒店的老总,难怪…”反正闲来无事,就随便翻了翻,别说,写得挺好!这就是本周我读此书的全部原因。
三:rootfs启动过程详解
话说上回:根文件系统的dev proc sys
三个目录并非普通的目录,它们都是不同的文件系统,并不挂载到硬盘分区,而是由内核专门管理的内存区域,便于以文件的形式让用户层与内核层的交互。而由于我们第一节定制的最小Linux操作系统是个“阉割版”,并不存在proc sys
两个目录,而且dev
目录实质上也是手动创建的,随根目录直接挂载到硬盘,终归不符合“江湖规矩”。
因此为了之后驱动开发做铺垫,本章节主要说明Linux文件系统的启动顺序,解释清楚rootfs加载之后是如何一步步把这些特殊的目录挂载到与之对应的文件系统当中的。所以本回算是一个番外篇!
根文件系统初始化
我们都知道嵌入式操作系统的启动步骤是这样的:u-boot –> kernel –> rootfs
但rootfs被加载之后还有很繁杂的过程,是这样的: rootfs –> /sbin/init –> /etc/inittab –> /etc/rcN.d –> /etc/init.d/rcS
init
是系统启动后的第一个程序,它的进程ID为1,之后的进程都是它的后代,inittab
是初始化表,相当于init的配置文件,根据不同的运行级别执行不同的脚本或命令rcN.d
是不同运行级别的初始化“功能清单”,配合inittab使用rcS
是初始化脚本
关于Linux运行级别
运行级别在嵌入式Linux和PC-Linux中的区别是很大的,因为PC-Linux一般是跑在服务器上,多用户、升级、维护、恢复等场景,面对众多的后台服务,每种场景都需要定制不同的启动策略,所以才孕育除了“运行级别”这个概念。嵌入式Linux(或者说busybox版文件系统)就没那么复杂了,智能终端往往不会有多用户和维护的场景,如果死机了,就用锅铲拍一拍,所以busybox保留了Linux运行级别的机制,但实际上并没有使用。
为了之后讲清楚inittab语法
,了解一点运行级别的知识也不坏。
Linux下有无数的服务要跑,称之为守护进程(Daemon),比如httpd sshd ftpd
等等,正常情况下我们希望这些服务能开机启动,所以网上有很多教程都让你把要开机启动的命令写到/etc/init.d/rcS
文件中。这个固然没错,但忽略了一点,该脚本是“无脑”加载的,假如我们要对系统进行恢复出厂设置,那么前面的三个服务在系统重启后根本没必要运行了;有时候为了排查系统bug,启动一堆多余的服务反而造成障碍等。关于在不同的模式下执行不同的开机启动方案,就是Linux的运行级别。
所谓运行级别就是字面意思,在不同的场景下定制不同的后台服务,Linux一共7种运行级别(0-6):
Level | Mode | Description |
---|---|---|
0 | 关机 | 不要设置为initdefault动作 |
1 | 单用户模式 | root权限,常用于系统恢复/维护 |
2 | 多用户模式(停用网络) | 登陆后进入命令行 |
3 | 多用户模式(启用网络) | 登陆后进入命令行,最常见 |
4 | 未使用/预留 | |
5 | 图形界面模式 | 登陆后进入GUI界面,PC上最常见 |
6 | 重启 | 不要设置为initdefault动作 |
可以通过runlevel
命令查看自己的系统当前处于哪个运行级别:
1 | $ runlevel |
⚠️ 一般而言0、1、6的运行级别都是一样的,但2-5分别表示什么运行级别,不同的Linux发行版不一样。
关于inittab
关于inittab文件需要掌握的只有两点:
- 它是init进程的配置文件,PC-Linux的inittab具体说明可参考👉oracle官方手册
- 它有自己的语法规则,
'<id>:<runlevels>:<action>:<process>'
表示'唯一标示:运行级别:执行动作:命令程序'
(由于busybox中的init机制不同于PC-Linux,以下内容仅以嵌入式端busybox版本为主,具体参考👉busybox inittab)
⚠️ id在busybox里并不作为标示,而是用于指定启动程序的tty,通常为缺省值
⚠️ runlevels在busybox中被完全忽略,总是为缺省值
执行动作<action>
action表示以什么样的方式执行程序,有很多种动作可选:
Action | Description |
---|---|
sysinit |
系统初始化时触发,最先被执行 |
respawn |
如果程序文件不存在,忽略它;如果程序挂了,重启它;如果程序已经在跑了,跳过它 |
askfirst |
相当于respawn ,但执行命令前会先显示”Please press Enter to activate this console.”,等到用户回车后才执行 |
wait |
启动程序并等它运行结束,然后再执行之后的程序 |
once |
启动程序,不等待,死了也不管 |
restart |
当收到重启信号时触发 |
ctrlaltdel |
当按下'ctrl+alt+del' 组合键后触发 |
shutdown |
当关机时触发 |
命令程序<process>
就是常规的命令行,如果有特殊要求,也可以通过exec sh
来执行。
制作一个简单的inittab
1 | # 系统初始化时,执行rcS脚本 |
从配置中的第2行就能看出,为什么我们需要开机启动的程序都必须卸载/etc/init.d/rcS这个地方了😄。rcS就是一个普通的shell脚本,想怎么写都行,达到目的即可。通常我们会在其中完成网络配置、各种后台服务启动、环境初始化等等。
文件系统自动挂载
初始化过程中,在应用程序启动之前,需要先确保Linux环境就绪,这其中就包括很底层的各种文件系统和驱动的挂载。设想一个网络摄像机,在开机后应用程序开始采集图像编解码并通过网络上传到服务端,结果发现/dev下根本没有设备文件,尝试用ps看一下进程列表,结果啥也没有,发现/proc目录是空的,会不会很崩溃,赶紧拿锅铲拍一拍😂。
因此,开机自动挂载文件系统是非常重要的,首先可以明确rootfs已经被内核自动挂载到根目录了,根据上一小节的学习可知,至少还有4个文件系统需要挂载,对应命令为
1 | mount -t proc proc /proc # 进程文件系统挂载 |
我们当然可以把这些命令添加到/etc/init.d/rcS
脚本,如果需要卸载时又一条条的umount,不觉得这样很麻烦么,有没有一种方便点的文件系统挂载方式。
/etc/fstab文件解析
fstab
翻译过来就是文件系统配置表,配合命令mount -a
和umount -a
使用,命令会根据配置表的内容逐条挂载文件系统到指定的挂载点。
fstab同样有自己的格式要求,一行内容表示一个文件系统的挂载,内容为:
1 | # 设备或目标 挂载点 fs类型 选项 是否备份 是否检查 |
如果/etc/fstab已存在,只需手动输入mount -a
,就会根据配置自动把表里的文件系统挂载上。如果想开机自动挂载怎么办,还记得/etc/inittab
么😁?添加两行:
1 | ::sysinit:/bin/mount -a # 开机自动挂载 |
完善varm的rootfs
明白了rootfs的启动过程以及其它文件系统的自动挂载原理,接下来就该完善之前做好的“阉割版”Linux了:
1 | $ cd ~/varm/os/rootfs |
完成rootfs的改造后,重新将其写入镜像文件中:
1 | $ cd ~/varm/os |
小结一下
- 嵌入式Linux的文件系统启动顺序为init –> inittab –> fstab –> rcS
- inittab的基本语法为
'<id>:<runlevels>:<action>:<process>'
- fstab的基本语法为
'device mount-point type options dump pass'
- /etc/init.d/rcS就是一个普通脚本,开机启动的入口