Quantcast
Channel: 蜗窝科技
Viewing all 218 articles
Browse latest View live

Linux TTY framework(1)_基本概念

$
0
0

1. 前言

由于串口的缘故,TTY是Linux系统中最普遍的一类设备,稍微了解Linux系统的同学,对它都不陌生。尽管如此,相信很少有人能回到这样的问题:TTY到底是什么东西?我们常常挂在嘴边的终端(terminal)、控制台(console)等概念,到底是什么意思?

本文是Linux TTY framework分析文章的第一篇,将带着上述疑问,介绍TTY有关的基本概念,为后续的TTY软件框架的分析,以及Linux serial subsystem的分析,打好基础。

2. 终端(terminal)

2.1 基本概念

在计算机或者通信系统中,终端是一个电子(或电气)设备,用于向系统输入数据(input),或者将系统接收到的数据显示出来(output),即我们常说的“人机交互设备”。

关于终端最典型的例子,就是电传打字机(Teletype)[1][2]----一种基于电报技术的远距离信息传送器械。电传打字机通常由键盘、收发报器和印字机构等组成。发报时,按下某一字符键,就能将该字符的电码信号自动发送到信道(input);收报时,能自动接收来自信道的电码信号,并打印出相应的字符(output)。

2.2 Unix终端

在计算机的世界里,键盘和显示器,是最常用的终端设备,一个用于向计算机输入信息,一个用于显示计算机的输出信息。

在大型机(mainframe)和小型机(minicomputer)的时代里,终端设备和计算机主机都同属一个整体。但到PC时代,情况发生了变化。Unix创始人肯•汤普逊和丹尼斯•里奇想让Unix成为一个多用户系统。多用户系统意味着要给每个用户配置一个终端,每个用户都要有一个显示器、一个键盘。但当时所有的计算机设备(包括显示器)价格都非常昂贵,而且键盘和主机是集成在一起的,根本没有独立的键盘。

最后他们找到了一样东西,那就是ASR33电传打字机。虽然电传打字机的用途是在电报线路上收发电报,但是它也可以作为人与计算机的接口,而且价格低廉。ASR33打字机的键盘用来输入信息,打印纸用来输出信息。所以他们把ASR33电传打字机作为终端,很多个ASR33连接到同一个主机,每个用户都可以在终端输入用户名和密码登录主机。这样他们创造了计算机历史上的第一个真正的多用户系统Unix,而ASR33成为第一个Unix终端。

2.3 TTY设备

由上面的介绍可知,第一个Unix终端是一个名字为ASR33的电传打字机,而电传打字机的英文单词为Teletype(或Teletypewritter),缩写为TTY。因此,该终端设备也被称为TTY设备。这就是TTY这个名称的来源,当然,在现在的Unix/Linux系统中,TTY设备已经演变为不同的意义了,后面我们会介绍演变的过程。

注1:读到这里,希望读者再仔细思索一下“设备”的概念。ASR33的电传打字机本身是一个硬件设备,在Unix/Linux系统中,这个硬件设备被抽象为“TTY设备”。

2.4 串口终端(Serials Terminal)

早期的TTY终端(这里暂时特指电传打字机),一般通过串口和Unix设备连接的,如下所示:

+------------------+
|            |
|            |
|            |          +------------+
|            |          
|        |
|  H/W Device run | Serial line  |        +<--->Keyboard
|  Unix/Linux OS  +<------------>+  Teletype |
|    (PC, etc.) |          |        +<--->Monitor
|            |          |        |
|            |          +------------+
|            |
|            |
+------------------+

然后,正如你我所熟知的,我们可以把上面红色部分(电传打字机),替换为任意的具有键盘、显示器、串口的硬件设备(如另一台PC),如下:

+------------------+
|            |
|            |
|            |          +------------+
|            |          |        |
|  H/W Device run | Serial line  | Any Device +<--->Keyboard
|  Unix/Linux OS  +<------------>+        |
|    (PC, etc.) |          | (PC, etc) +<--->Monitor
|            |          |        |
|            |          +------------+
|            |
|            |
+------------------+

因此,对Unix/Linux系统来说,只要是通过串口连接的设备,都可以作为终端设备,因而不再需要关注具体的终端形态。久而久之,终端设备、TTY设备、串口设备等概念,逐渐混在一起,就不再区分了,总结来说,在当今的Linux系统中:

1)TTY设备就是终端设备,终端设备就是TTY设备,无需区分。

2)所有的串口设备都是TTY设备。

3)当然,除了串口设备,也发展出来了其它形式的TTY设备,例如虚拟终端(VT)、伪终端(Pseudo Terminal)等等,这些概念本文就不展开描述了,后续会使用专门的文章分析。

3. 控制台(console)

了解了终端和TTY的概念之后,再来看看另一个比较熟悉的概念:console。

回到Unix系统刚刚支持多用户(2.2小节的描述)的时代,此时的PC有一个自带的、昂贵的终端(自身的键盘、显示器等),另外为了支持多用户,可以通过串口线连接多个TTY终端(Teletype)。为了彰显自带终端崇高的江湖地位,人们称它为console。

当然,“江湖地位”之说,纯属玩笑,不过从console的中文翻译-----控制台,可以看出,自带终端(console)有别于TTY终端的地方如下:

1)控制台(console)是昂贵的。

2)控制台(console)比TTY终端拥有更多的权限,例如用户建立、密码更改、权限分配等等,这也是“控制”的意义所在。

3)系统的运行日志、出错信息等内容,通常只会输出到控制台(console)终端中,以方便管理员进行“控制”和“管理”。

不过,随着计算机技术的发展、操作系统的改进,控制台(console)终端和普通TTY终端的界限越来越模糊,console能做的事情,普通终端也都能做了。因此,console逐渐退化,以至于在当前的Linux系统中,它仅仅保留了第三点“日志输出”的功能,这就是Linux TTY framework中console的概念(具体可参考后续文章的分析)。

4. 参考文章

[1] https://en.wikipedia.org/wiki/Teleprinter

[2] 电传打字机(Teletype),http://baike.baidu.com/view/1773688.htm

[3] 你真的知道什么是终端吗?

[4] 串口通信技术浅析

 

原创文章,转发请注明出处。蜗窝科技www.wowotech.net


TLB flush操作

$
0
0

一、前言

Linux VM subsystem在很多场合都需要对TLB进行flush操作,本文希望能够把这个知识点相关的方方面面描述清楚。第二章描述了一些TLB的基本概念,第三章描述了ARM64中TLB的具体硬件实现,第四章描述了linux中和TLB flush相关的软件接口。内核版本依然是4.4.6版本。

二、基本概念

1、什么是TLB?

TLB的全称是Translation Lookaside Buffer,我们知道,处理器在取指或者执行访问memory指令的时候都需要进行地址翻译,即把虚拟地址翻译成物理地址。而地址翻译是一个漫长的过程,需要遍历几个level的Translation table,从而产生严重的开销。为了提高性能,我们会在MMU中增加一个TLB的单元,把地址翻译关系保存在这个高速缓存中,从而省略了对内存中页表的访问。

2、为什么有TLB?

TLB这个术语有些迷惑,但是其本质上就是一种cache,既然是一种cache,那么就没有什么好说的,当然其存在就是为了更高的performance了。不同于instruction cache和data cache,它是Translation cache。对于instruction cache,它是解决cpu获取main memory中的指令数据(地址保存在PC寄存器中)的速度比较慢的问题而设立的。同样的data cache是为了解决数据访问指令比较慢而设立的。但是实际上这只是事情的一部分,我们来仔细看看程序中的数据访问指令(例如说是把)的执行过程,这个过程可以分成如下几个步骤:

(1)将PC中的虚拟地址翻译成物理地址

(2)从memory中获取数据访问指令(假设该指令需要访问地址x)

(3)将虚拟地址x翻译成物理地址y

(4)从location y的memory中获取具体的数据

instruction cache解决了step (2)的性能问题,data cache解决了step (4)中的性能问题,当然,复杂的系统会设立了各个level的cache用来缓存main memory中的数据,因此,实际上unified cache同时可以加快step (2)和(4)的速度。Anyway,这只是解决了部分的问题,IC设计工程师怎么会忽略step (1)和step (2)呢,这也就是TLB的由来。如果CPU core发起的地址翻译过程能够在TLB(translation cache)中命中(cache hit),那么CPU不需要访问慢速的main memory从而加快了CPU的performance。

3、TLB工作原理

大概的原理图如下(图片来自Computer Organization and Design 5th):

当需要转换VA到PA的时候,首先在TLB中找是否有匹配的条目,如果有,那么我们称之TLB hit,这时候不需要再去访问页表来完成地址翻译。不过TLB始终是全部页表的一个子集,因此也有可能在TLB中找不到。如果没有在TLB中找到对应的item,那么称之TLB miss,那么就需要去访问memory中的page table来完成地址翻译,同时将翻译结果放入TLB,如果TLB已经满了,那么还要设计替换算法来决定让哪一个TLB entry失效,从而加载新的页表项。简单的描述就是这样了,我们可以对TLB entry中的内容进行详细描述,它主要包括:

(1) 物理地址(更准确的说是physical page number)。这是地址翻译的结果。

(2) 虚拟地址(更准确的说是virtual page number)。用cache的术语来描述的话应该叫做Tag,进行匹配的时候就是对比Tag。

(3) Memory attribute(例如:memory type,cache policies,access permissions)

(4) status bits(例如:Valid、dirty和reference bits)

(5) 其他相关信息。例如ASID、VMID,下面的章节会进一步描述。

三、ARMv8的TLB

我们选择Cortex-A72 processor来描述ARMv8的TLB的组成结构以及维护TLB的指令。

1、TLB的组成结构。下图是A72的功能block:

A72实现了2个level的TLB,绿色是L1 TLB,包括L1 instruction TLB(48-entry fully-associative)和L1 data TLB(32-entry fully-associative)。黄色block是L2 unified TLB,它要大一些,可以容纳1024个entry,是4-way set-associative的。当L1 TLB发生TLB miss的时候,L2 TLB是它们坚强的后盾。

通过上图,我们还可以看出:对于多核CPU,每个processor core都有自己的TLB。

2、如何确定TLB match

整个地址翻译过程并非简单的VA到PA的映射那么简单,其实系统中的虚拟地址空间有很多,而每个地址空间的翻译都是独立的:

(1)操作系统中的每一个进程都有自己独立的虚拟地址空间。在各个进程不同的虚拟地址空间中,相同的VA被翻译成不同的PA。

(2)如果支持虚拟化,系统中存在一个host OS和多个guest OS,不同OS之间,地址翻译是不同的,而对于一个guest OS内部,其地址空间的情况请参考(1)。

(3)如果支持TrustZone,secure monitor、secure world以及normal world是不同的虚拟地址空间。

当然,我们可以在TLB匹配过程中,不考虑上面的复杂情况,比如在进程切换的时候,在切换虚拟机的时候,或者在切换secure/normal world的时候,将TLB中的所有内容全部flush掉(全部置为无效),这样的设计当然很清爽,但是性能会大打折扣。因此,实际上在设计TLB的时候,往往让TLB entry包括了和虚拟地址空间context相关的信息。在A72中,只有满足了下面的条件,才能说匹配了一个TLB entry:

(1)请求进行地址翻译的VA page number等于TLB entry中的VA page number

(2)请求进行地址翻译的memory space identifier等于TLB entry中的memory space identifier。所谓memory space identifier其实就是区分请求是来自EL3 Exception level、Nonsecure EL2 Exception level或者是Secure and Non-secure EL0还是EL1 Exception levels。

(3)如果该entry被标记为non-Global,那么请求进行地址翻译的ASID(保存在TTBRx中)等于TLB entry中的ASID。

(4)请求进行地址翻译的VMID(保存在VTTBR寄存器中)等于TLB entry中的VMID

3、进程切换和ASID(Address Space Identifier)

如果了解OS的基本知识,那么我们都知道:每个进程都有自己独立的虚拟地址空间。如果TLB不标识虚拟地址空间,那么在进程切换的时候,虚拟地址空间也发生了变化,因此TLB中的所有的条目都应该是无效了,可以考虑invalidate all。但是,这么做从功能上看当然没有问题,但是性能收到了很大的影响。

一个比较好的方案是区分Global pages (内核地址空间)和Process-specific pages(参考页表描述符的nG的定义)。对于Global pages,地址翻译对所有操作系统中的进程都是一样的,因此,进程切换的时候,下一个进程仍然需要这些TLB entry,因而不需要flush掉。对于那些Process-specific pages对应的TLB entry,一旦发生切换,而TLB又不能识别的话,那么必须要flush掉上一个进程虚拟地址空间的TLB entry。如果支持了ASID,那么情况就不一样了:对于那些nG的地址映射,它会有一个ASID,对于TLB的entry而言,即便是保存多个相同虚拟地址到不同物理地址的映射也是OK的,只要他们有不同的ASID。

切换虚拟机和VMID的概念是类似的,这里就不多说了。

4、TLB的一致性问题

TLB也是一种cache,有cache也就意味着数据有多个copy,因此存在一致性(coherence)问题。和数据、指令cache或者unified cache不同的是,硬件并不维护TLB的coherence,一旦软件修改了page table,那么软件也需要进行TLB invalidate操作,从而维护了TLB一致性。

5、TLB操作过程

我们以一个普通内存访问指令为例,说明TLB的操作过程,在执行该内存访问指令的过程中,第一件需要完成的任务就是将要访问的虚拟地址翻译成物理地址,具体操作步骤如下:

(1)首先在L1 data TLB中寻找匹配的TLB entry(如果是取指操作,那么会在L1 instruction TLB中寻找),如果运气足够好,TLB hit,那么一切都结束了,否者进入下一步

(2)在L2 TLB中寻找匹配的TLB entry。如果不能命中,那么就需要启动hardware translation table walk了

(3)在执行hardware translation table walk的时候,是直接访问main memory还是通过L2 cache 访问呢?其实都可以的,这和系统配置有关(具体参考TCR_ELx)。如果配置的是Normal memory, Inner Write-Back Cacheable,那么可以在L2 cache中来寻找page table。如果配置的是Normal memory, Inner Write-Through Cacheable或者Non-cacheable,那么hardware translation table walk将直接和external main memory。

6、维护TLB的指令

我们将在下一章,配合linux的标准cache flush接口来描述。

四、TLB flush API

和TLB flush操作相关的接口API主要包括:

1、 void flush_tlb_all(void)。

这个接口用来invalidate TLB cache中的所有的条目。执行完毕了该接口之后,由于TLB cache中没有缓存任何的VA到PA的转换信息,因此,调用该接口API之前的所有的对page table的修改都可以被CPU感知到。注:该接口是大杀器,不要随便使用。

对于ARM64,flush_tlb_all接口使用的底层命令是:tlbi vmalle1is。Tlbi是TLB Invalidate指令,vmalle1is是参数,指明要invalidate那些TLB。vm表示本次invalidate操作对象是当前VMID,all表示要invalidate所有的TLB entry,e1是表示要flush的TLB entry的memory space identifier是EL0和EL1,regime stage 1的TLB entry。is是inner shareable的意思,表示要invalidate所有inner shareable内的所有PEs的TLB。如果没有is,则表示要flush的是local TLB,其他processor core的TLB则不受影响。

flush_tlb_all接口有一个变种:local_flush_tlb_all。flush_tlb_all是invalidate系统中所有的TLB(各个PEs上的TLB),而local_flush_tlb_all仅仅是invalidate本CPU core上的TLB。local_flush_tlb_all对应的底层接口是:tlbi vmalle1,没有is参数。

2、 void flush_tlb_mm(struct mm_struct *mm)。

这个接口用来invalidate TLB cache中所有和mm这个进程地址空间相关的条目。执行完毕了该接口之后,由于TLB cache中没有缓存任何的mm地址空间中VA到PA的转换信息,因此,调用该接口API之前的所有的对mm地址空间的page table的修改都可以被CPU感知到。

对于ARM64,flush_tlb_mm接口使用的底层命令是:tlbi aside1is, asid。is是inner shareable的意思,表示该操作要广播到inner shareable domain中的所有PEs。asid表示该操作范围是根据asid来进行的。

3、void flush_tlb_page(struct vm_area_struct *vma, unsigned long addr)。

flush_tlb_page接口函数对addr对应的TLB entry进行flush操作。这个函数类似于flush_tlb_range,只不过flush_tlb_range操作了一片VA区域,涉及若干TLB entry,而flush_tlb_page对range进行了限定(range的size就是一个page),因此,也就只是invalidate addr对应的tlb entry。

对于ARM64,flush_tlb_page接口使用的底层命令是:tlbi vale1is, addr。va参数是virtual address的意思,表示本次flush tlb的操作是针对当前asid中的某个virtual address而进行的,addr给出具体要操作的地址和ASID信息。l表示last level,也就是说用户修改了last level Translation table的内容(一般而言,PTE就是last level,在某些情况下,例如section map,last level是PMD),那么我们仅仅需要flush最后一级的页表(page table walk可以有多级)。

4、 void flush_tlb_range(struct vm_area_struct *vma, unsigned long start, unsigned long end)。

flush_tlb_range接口是一个flush强度比flush_tlb_mm要弱的接口,flush_tlb_range不是invalidate整个地址空间的TBL,而是针对该地址空间中的一段虚拟内存(start到end-1)在TLB中的entry进行flush。

ARM64并没有直接flush一个range的硬件操作接口,因此,在ARM64的代码中,flush一个range是通过flush一个个的page来实现的。

 

五、参考文献

1、Cortex A72 TRM

2、ARM ARM

3、Documentation/cachetlb.txt

 

原创文章,转发请注明出处。蜗窝科技

Linux TTY framework(2)_软件架构

$
0
0

1. 前言

由“Linux TTY framework(1)_基本概念”的介绍可知,在Linux kernel中,TTY就是各类终端(Terminal)的简称。为了简化终端的使用,以及终端驱动程序的编写,Linux kernel抽象出了TTY framework:对上,向应用程序提供使用终端的统一接口;对下,提供编写终端驱动程序(如serial driver)的统一框架。

本文是Linux TTY framework分析的第二篇文章,将从整体架构的角度,介绍Linux TTY framework,以便分解出功能相对独立的子模块,以便后续的分析。

2. 软件架构

Linux kernel TTY framework位于“drivers/tty”目录中,其软件框架如下面图片1所示:

tty_arch

图片1 Linux TTY framework框架

和Linux其它的framework类似,TTY framework通过TTY core屏蔽TTY有关的技术细节,对上以字符设备的形式向应用程序提供统一接口,对下以TTY device/TTY driver的形式提供驱动程序的编写框架。具体请参考后续章节介绍。

2.1 TTY Core

TTY core是TTY framework的核心逻辑,功能包括:

1)以字符设备的形式,向用户空间提供访问TTY设备的接口,例如:

设备号(主, 次)        字符设备                                   备注
(5, 0)                     /dev/tty                                     控制终端(Controlling Terminal)
(5, 1)                     /dev/console                             控制台终端(Console Terminal)
(4, 0)                     /dev/vc/0 or /dev/tty0                  虚拟终端(Virtual Terminal)
(4, 1)                     /dev/vc/1 or /dev/tty1                  同上
…                         …                                             …
(x, x)                     /dev/ttyS0                                 串口终端(名称和设备号由驱动自行决定)
…                         …                                             …
(x, x)                     /dev/ttyUSB0                            USB转串口终端
…                         …                                             …

注1:控制终端、控制台终端、虚拟终端等概念,比较抽象,我会在后续的文章中详细介绍。

2)通过设备模型中的struct device结构抽象TTY设备,并通过struct tty_driver抽象该设备的驱动,并提供相应的register接口。TTY驱动程序的编写,简化为填充并注册相应的struct tty_driver结构。

注2:TTY framework弱化了TTY设备(图片1中使用虚线框标注)的概念,通常情况下,可以在注册TTY驱动的时候,自动分配并注册TTY设备。

3)使用struct tty_struct、struct tty_port等数据结构,从逻辑上抽象TTY设备及其“组件”,以实现硬件无关的逻辑。

4)抽象出名称为线路规程(Line Disciplines)的模块,在向TTY硬件发送数据之前,以及从TTY设备接收数据之后,进行相应的处理(如特殊字符的转换等)。

2.2 System Console Core

Linux kernel的system console主要有两个功能:

1)向系统提供控制台终端(Console Terminal) ,以便让用户登录进行交互操作。

2)提供printk功能,以便kernel代码进行日志输出。

System console core模块使用struct console结构抽象system console功能,具体的driver不需要关心console的内部逻辑,填充该接口并注册给kernel即可。

2.3 TTY Line Disciplines

线路规程(Line Disciplines)在TTY framework中是一个非常优雅的设计,我们可以把它看成设备驱动和应用接口之间的一个适配层。从字面意思理解,就是辅助TTY driver,将我们通过TTY设备键入的字符转换成一行一行的数据[3],当然,实际情况远比这复杂,例如在蜗窝x project所使用的kernel版本中,存在如下的Line Disciplines(以n_为前缀,我们后续的文章会更为详细的介绍):

pengo@DESKTOP-CH8SB7C:~/work/xprj/linux$ ls drivers/tty/n_*
drivers/tty/n_gsm.c   drivers/tty/n_r3964.c        drivers/tty/n_tracesink.c  drivers/tty/n_tty.c
drivers/tty/n_hdlc.c  drivers/tty/n_tracerouter.c  drivers/tty/n_tracesink.h

2.4 TTY Drivers以及System Console Drivers

最后,对内核以及驱动工程师来说,更关注的还是具体的TTY设备驱动。在kernel为我们搭建的如此beauty的框架下面,编写相应的driver就成为一件比较简单的事情了。当然的kernel中,主要的TTY driver有两类:

1)虚拟终端(Virtual Terminal,VT)驱动,位于drivers/tty/vt中,负责实现VT(后续文章会详细介绍)有关的功能。

2)串口终端驱动,也即我们所熟知的serial subsystem(话说终于到重点了,哈哈),位于drivers/tty/serial中。

3. 总结

本文对Linux TTY framework的软件框架作了一个简单的介绍,目的是从整体上了解Linux TTY有关的软件实现。基于本文的描述,后续计划从如下角度继续TTY framework的分析:

控制终端、控制台终端、虚拟终端等概念的理解及解释;

TTY core的分析;

System Console Core的分析;

Serial subsystem(串口子系统)的分析;

虚拟终端(VT)的分析;

常用线路规程(Line Disciplines)的介绍和分析;

等等。

4. 参考文档

[1] TTY驱动分析

[2] 控制终端(controlling terminal),https://linux.die.net/man/4/tty

[3] https://utcc.utoronto.ca/~cks/space/blog/unix/TTYLineDisciplineWhy

 

原创文章,转发请注明出处。蜗窝科技www.wowotech.net

X-012-KERNEL-serial early console的移植

$
0
0

1. 前言

对Linux kernel工程师来说,最依赖的工具非printk莫属(不多解释,大家都懂)。因此,在Linux kernel移植的初期阶段,如果能够尽快地实现printk功能,将会为后续的工作带来极大的帮助。

在众多可用作printk输出的终端里面(串口、屏幕、USB、网络、等等),串口终端(也即串口驱动)无疑是实现起来最简单一种,因此也是嵌入式linux开发过程中(特别是早期阶段)最普遍使用的。

但是,受限于Linux TTY框架的复杂性[1],长久以来,在Kernel移植的初期阶段(各种功能都不ready,缺乏有效的调试手段),快速的实现serial driver也是一个不小的挑战。不过,随着serial subsystem中的early console功能的出现,这种状况得到了极大的改善。

本文将借助“X Project” kernel的开发过程,介绍serial early console功能的移植过程。

注1:博客中“Linux TTY子系统”的分析正在缓慢推进,不过还未涉及console、serial subsystem、earlycon等模块。因此我一直在纠结先写理论(分析),还是先写实践(移植说明)。结论是先写本文,理由是:虽然earlycon功能涉及到TTY子系统的复杂知识,如console driver、tty driver、serial driver等等,但从移植的角度看,我们可以什么都不懂。这种优雅的抽象和封装,是优秀软件(如Linux kernel)所必需具备的特性,也是我们软件人需要竭力追求的神圣目标。

2. 移植步骤

2.1 串口驱动

early console是linux serial subsystem的一个子功能,因此需要依附于具体平台的serial driver之下(放心,本文不涉及任何串口驱动的知识)。以“X Project” 所使用的bubblegum-96平台为例,我们需要在“drivers/tty/serial/”下新建一个serial driver,并修改kernel serial subsystem的Kconfig和Makefile文件,将其添加到kernel的编译系统中, 步骤如下:

1)新建serial driver

touch drivers/tty/serial/owl-serial.c

其中“owl”是bubblegum-96平台所使用SOC的代号,大家可以根据实际情况,使用和自己平台匹配的名称。

2)修改drivers/tty/serial/Kconfig和drivers/tty/serial/Makefile,将新建的serial driver加入到编译框架中

diff --git a/drivers/tty/serial/Kconfig b/drivers/tty/serial/Kconfig
index 13d4ed6..bbf4b69 100644
--- a/drivers/tty/serial/Kconfig
+++ b/drivers/tty/serial/Kconfig
@@ -1624,6 +1624,14 @@ config SERIAL_MVEBU_CONSOLE
          and warnings and which allows logins in single user mode)
          Otherwise, say 'N'.

+config SERIAL_OWL
+       tristate "Actions OWL serial port support"
+       depends on ARCH_OWL
+       select SERIAL_CORE
+       select SERIAL_CORE_CONSOLE
+       help
+         If you have a machine based on an Actions OWL CPU you
+         can enable its onboard serial ports by enabling this option.

endmenu

config SERIAL_MCTRL_GPIO
diff --git a/drivers/tty/serial/Makefile b/drivers/tty/serial/Makefile
index 8c261ad..3f75d73 100644
--- a/drivers/tty/serial/Makefile
+++ b/drivers/tty/serial/Makefile
@@ -91,6 +91,7 @@ obj-$(CONFIG_SERIAL_MEN_Z135) += men_z135_uart.o
obj-$(CONFIG_SERIAL_SPRD) += sprd_serial.o
obj-$(CONFIG_SERIAL_STM32)     += stm32-usart.o
obj-$(CONFIG_SERIAL_MVEBU_UART)        += mvebu-uart.o
+obj-$(CONFIG_SERIAL_OWL)       += owl-serial.o

 

我们为新建的serial driver指定了“SERIAL_OWL”配置项,该配置项依赖ARCH_OWL[2]。与此同时,我们需要选中“SERIAL_CORE”和“SERIAL_CORE_CONSOLE”两个配置项,以支持earlycon功能。

3)配置kernel,开启TTY、serial等功能,并使能我们新加入的serial driver

cd ~/work/xprj/build
make kernel-config

#选中如下的配置项
   Device Drivers  --->
      Character devices  --->
           [*] Enable TTY
           Serial drivers  --->
               [*] Actions OWL serial port support

完成后make kernel重新编译即可。

2.2 early console的移植

实现early console的移植过程非常简单,包括:

1)实现一个early console的setup接口,并调用serial core提供的注册接口(EARLYCON_DECLARE)将其注册到kernel中,如下:

int __init earlycon_owl_setup(struct earlycon_device *device, const char *opt)
{
        /* TODO */
        uart5_base = early_ioremap(UART5_BASE, 4);

        device->con->write = earlycon_owl_write;
        return 0;
}
EARLYCON_DECLARE(owl_serial, earlycon_owl_setup);

其中“owl_serial”是early console的名称,earlycon_owl_setup是开始使用之前的初始化接口,需要在这个初始化接口中做两件事情:

进行一些必要的初始化,如例子中的寄存器map;

为printk输出制定一个write接口----earlycon_owl_write。

注2:“EARLYCON_DECLARE”是实现early console的一种方法,另外我们也可以使用device tree的方式,为了不增加复杂度,本文就不提及相关的实现了。

注3:为了简单,early console和u-boot[3]使用相同的串口,并且使用相同的配置,因此不需要额外的初始化操作。

注4:虽然配置不变,我们还是需要访问串口寄存器输出字符串,因此需要map相应的IO地址,由于early console需要在很早的时候使用,此时mm还没有初始化,因此我们可以用early_ioremap[4]进行map。

2)earlycon_owl_write的实现

console输出需要字符串处理,可以借用serial core的标准接口----uart_console_write:

static void earlycon_owl_write(struct console *con, const char *s, unsigned n)
{
        /* TODO */
        uart_console_write(NULL, s, n, owl_serial_putc);
}

我们需要做的就是提供一个字符输出的API----owl_serial_putc,具体可参考代码以及“X-004-UBOOT-串口驱动移植(Bubblegum-96平台)[3]”中有关的描述。

3)移植完成后重新编译kernel即可

3. 使用说明

移植完成后,可以通过kernel的命令行参数,告诉kernel在启动的时候使用该early console,参数的格式如下:

earlycon=owl_serial

其中“earlycon”是关键字,“owl_serial”是early console的名字。

命令行参数可以通过u-boot传入,为了测试方便,可以暂时加到kernel的配置项中[5],如下:

#
# Boot options
#
-CONFIG_CMDLINE=""
+CONFIG_CMDLINE="earlycon=owl_serial"

以上改动具体可参考下面的patch:

https://github.com/wowotechX/linux/commit/1285ab5e6e5c5bb263a1d715fe04cca2d217bd98

4. 测试和调试

移植完成后,按照“README.bubblegum96[6]”中的步骤,编译并运行kernel,发现printk信息没有如约出现,没关系,兵来将挡,水来土掩,开始调试了。

4.1 使用点LED的方式,定位问题所在位置

该方法在u-boot移植的初期,也是用过,应该是轻车熟路了[7]。不过在kernel中有点稍微的不一样,我们需要将GPIO有关的寄存器map成虚拟地址才能使用,代码如下(只为暂时调试使用,因而没有上传到代码仓库):

1)在init/main.c中添加LED debug有关的函数

+void __init xprj_earlyyyy_debug(void)
+{
+       static void __iomem *gpioa_outen = NULL;
+       static void __iomem *gpioa_outdat = NULL;
+
+       if (gpioa_outen == NULL) {
+               gpioa_outen = early_ioremap(0xe01b0000, 4);
+               gpioa_outdat = early_ioremap(0xe01b0008, 4);
+       }
+
+       writel(0xffffffff, gpioa_outen);
+       writel(0xffffffff, gpioa_outdat);
+}

注5:为了可以尽早(在kernel内存管理模块初始化之前)使用,我们使用early_ioremap获取寄存器的虚拟地址[4]

注6:为了简单,这里粗暴的把GPIOA的所有GPIO都输出高电平了。

2)根据earlycon的初始化逻辑,在earlycon初始化的路径上,调用debug API,点亮LED。第一个地方是setup_earlycon(earlycon的具体流程,会在下一篇文章中分析):

+extern void __init xprj_earlyyyy_debug(void);
int __init setup_earlycon(char *buf)
{
        const struct earlycon_id *match;
+#if 1
+       xprj_earlyyyy_debug();
+       while (1);
+#endif

+

无法点亮!

接着往前跟踪,放到param_setup_earlycon中:

@@ -204,6 +210,11 @@ static int __init param_setup_earlycon(char *buf)

{
        int err;
+#if 1
+       xprj_earlyyyy_debug();
+       while (1);
+#endif

+

还是无法点亮!

怀疑earlycon的代码压根没有执行,检查drivers/tty/serial/Makefile,发现由CONFIG_SERIAL_EARLYCON控制:

obj-$(CONFIG_SERIAL_EARLYCON) += earlycon.o

仿照drivers/tty/serial/Kconfig中其它serial driver,在OWL_SERIAL中select该配置项,如下:

@@ -1629,6 +1629,7 @@ config SERIAL_OWL
        depends on ARCH_OWL
        select SERIAL_CORE
        select SERIAL_CORE_CONSOLE
+       select SERIAL_EARLYCON
        help
          If you have a machine based on an Actions OWL CPU you
          can enable its onboard serial ports by enabling this option.

配置kernel后,再次编译测试,得到如下的执行过程:

param_setup_earlycon          OK
setup_earlycon                     OK
earlycon_owl_setup              OK
earlycon_owl_write               FAIL

看来初始化OK了,没有人调用console的write接口?

注7:上面debug的过程中,下面的代码会导致编译错误:

extern void __init xprj_earlyyyy_debug(void);
static void earlycon_owl_write(struct console *con, const char *s, unsigned n)
{
#if 1
        xprj_earlyyyy_debug();
        while (1);
#endif

错误信息为:

WARNING: modpost: Found 2 section mismatch(es).
To see full details build your kernel with:
make CONFIG_DEBUG_SECTION_MISMATCH=y'
FATAL: modpost: Section mismatches detected.
Set CONFIG_SECTION_MISMATCH_WARN_ONLY=y to allow them.
make[3]: *** [vmlinux.o] Error 1
make[2]: *** [vmlinux] Error 2

原因是我们在非__init的section中(earlycon_owl_write)调用了__init section(xprj_earlyyyy_debug)的代码,按照提示说的,打开CONFIG_SECTION_MISMATCH_WARN_ONLY配置项即可编译通过:

    Kernel hacking  --->
           Compile-time checks and compiler options  ---> 
                 [*] Make section mismatch errors non-fatal

4.2 使能printk的配置项

依稀记得linux kernel的printk有配置项可以控制使能与否,检查kernel的相关代码,果然如此。重新配置kernel使能该配置项:

    General setup  --->
          [*] Enable support for printk

再次运行,串口有输出了:

Starting kernel ...
flushing dcache successfully.
Booting Linux on physical CPU 0xrFailed to find device node for bpDBPDentry cacInode-cache hash table entries: software IO TLB [mem 0x79cb4000-Memory: 1993624K/2097152K availa0ffSsaK  

觉得奇奇怪怪的?没有换行?没有格式化输出?

4.3 检查earlycon驱动

再仔细检查一下drivers/tty/serial/owl-serial.c中owl_serial_putc的实现,有个地方写错了(从u-boot抄过来的时候大意了),修改一下:

static void owl_serial_putc(struct uart_port *port, int ch)
{
-       if (readl(uart5_base + UART_STAT) & UART_STAT_TFFU)
-               return;
+       /* wait for TX FIFO untill it is not full */
+       while (readl(uart5_base + UART_STAT) & UART_STAT_TFFU)
+               ;
        writel(ch, uart5_base + UART_TXDAT);
}

终于OKAY了:

Booting Linux on physical CPU 0x0
Linux version 4.6.0-rc5+ (pengo@ubuntu) (gcc version 4.8.3 20131202 (prerelease) (crosstool-NG linaro-1.13.1-4.8-2013.12 - Linaro GCC 2013.11) ) #8 SMP Mon Oct 3 23:36:39 PDT 2016
Boot CPU: AArch64 Processor [410fd032]
earlycon: owl_serial0 at I/O port 0x0 (options '')
bootconsole [owl_serial0] enabled
Failed to find device node for boot cpu
missing boot CPU MPIDR, not enabling secondaries
percpu: Embedded 14 pages/cpu @ffffffc07ffdd000 s28032 r0 d29312 u57344
Detected VIPT I-cache on CPU0
Built 1 zonelists in Zone order, mobility grouping on.  Total pages: 516096
Kernel command line: earlycon=owl_serial
PID hash table entries: 4096 (order: 3, 32768 bytes)
Dentry cache hash table entries: 262144 (order: 9, 2097152 bytes)
Inode-cache hash table entries: 131072 (order: 8, 1048576 bytes)
software IO TLB [mem 0x79cb4000-0x7dcb4000] (64MB) mapped at [ffffffc079cb4000-ffffffc07dcb3fff]
Memory: 1993624K/2097152K available (1028K kernel code, 78K rwdata, 120K rodata, 116K init,
201K bss, 103528K reserved, 0K cma-reserved)
Virtual kernel memory layout:
    modules : 0xffffff8000000000 - 0xffffff8008000000   (   128 MB)
    vmalloc : 0xffffff8008000000 - 0xffffffbdbfff0000   (   246 GB)
      .text : 0xffffff8008080000 - 0xffffff8008181000   (  1028 KB)
    .rodata : 0xffffff8008181000 - 0xffffff80081a0000   (   124 KB)
      .init : 0xffffff80081a0000 - 0xffffff80081bd000   (   116 KB)
      .data : 0xffffff80081bd000 - 0xffffff80081d0800   (    78 KB)
    fixed   : 0xffffffbffe7fd000 - 0xffffffbffec00000   (  4108 KB)
    PCI I/O : 0xffffffbffee00000 - 0xffffffbfffe00000   (    16 MB)
    memory  : 0xffffffc000000000 - 0xffffffc080000000   (  2048 MB)
SLUB: HWalign=64, Order=0-3, MinObjects=0, CPUs=1, Nodes=1
Hierarchical RCU implementation.
        Build-time adjustment of leaf fanout to 64.
        RCU restricting CPUs from NR_CPUS=64 to nr_cpu_ids=1.
RCU: Adjusting geometry for rcu_fanout_leaf=64, nr_cpu_ids=1
NR_IRQS:64 nr_irqs:64 0
Kernel panic - not syncing: No interrupt controller found.
---[ end Kernel panic - not syncing: No interrupt controller found.

终于从蛮荒地世界中走出来了,庆祝吧~~!!

注8:上述过程可参考如下patch:

https://github.com/wowotechX/linux/commit/1e18b4d2a5df61e5f495dbc071fe2ad2740d7061

5. 参考文档

[1] Linux TTY framework(2)_软件架构

[2] X-009-KERNEL-Linux kernel的移植(Bubblegum-96平台)

[3] X-004-UBOOT-串口驱动移植(Bubblegum-96平台)

[4] Fix-Mapped Addresses

[5] Linux kernel内核配置解析(5)_Boot options(基于ARM64架构)

[6] README.bubblegum96,https://github.com/wowotechX/doc/blob/master/README.bubblegum96

[7] 通过点亮LED的方法调试嵌入式代码

 

原创文章,转发请注明出处。蜗窝科技,www.wowotech.net

X-013-UBOOT-使能autoboot功能

$
0
0

1. 前言

通过“X-012-KERNEL-serial early console的移植”,早期的串口控制台已经ready,kernel的printk可以正确输出,“X Project”由此进入“文明”时代。基于此,后续的开发工作将会focus在linux kernel上,而u-boot,可以蜕化为其原始目标:boot kernel。

在之前的测试和调试过程中,都是先进入u-boot的命令行,手动输入bootm命令,boot linux kernel。为了简化这个动作,有必要将u-boot的autoboot功能用起来。

所谓的autoboot,是指u-boot run起来之后,自动加载并执行linux kernel image的 过程。该功能非常简单,之所以写一篇文章,权当“X Project”开发过程的一个记录。

2. 使能autoboot

2.1 u-boot autoboot功能简介

u-boot autoboot功能的具体介绍可参考“README.autoboot[1]”,总结来说:

1)通过CONFIG_BOOTDELAY配置是否使用autoboot功能,是否在autoboot之前等待一段时间以便让用户输入从而进入命令行:

Delay before automatically booting the default image;

set to -1 to disable autoboot.

set to -2 to autoboot with no delay and not check for abort (even when CONFIG_ZERO_BOOTDELAY_CHECK is defined).

2)通过CONFIG_BOOTCOMMAND配置boot kernel所使用的命令。

3)通过CONFIG_BOOTARGS配置命令行参数。

4)其它等等。

2.2 基于bubblegum-96配置并使能autoboot

以“X Project”目前的情况来说,我们暂时只使用CONFIG_BOOTDELAY和CONFIG_BOOTCOMMAND两个即可,如下:

/* include/configs/bubblegum.h */

#define CONFIG_BOOTDELAY -2

#define CONFIG_BOOTCOMMAND "bootm 0x6400000"

其中CONFIG_BOOTDELAY为-2,表示不需要任何delay;CONFIG_BOOTCOMMAND即为我们在u-boot命令行敲入的用于boot kernel的指令。

注1:由于当前bubblegum-96的u-boot没有移植timer驱动,CONFIG_BOOTDELAY为正值的时候无法正确使用(没有计时,也就没有timeout了)。

2.3 简单的测试

为了方便测试,我在build makefile中添加了几个辅助命令,如下:

#
# some help commands
#
spl-run:
    sudo $(DFU_DIR)/dfu $(BOARD_NAME) $(SPL_BASE) $(TOOLS_DIR)/$(BOARD_VENDOR)/splboot.bin 1

uimage-load:
    sudo $(DFU_DIR)/dfu $(BOARD_NAME) $(FIT_UIMAGE_BASE) $(UIMAGE_ITB_FILE) 0

uboot-run:
    sudo $(DFU_DIR)/dfu $(BOARD_NAME) $(UBOOT_BASE) $(OUT_DIR)/u-boot/u-boot-dtb.bin 1

kernel-run: uimage-load uboot-run

按住ADFU键开机,执行make spl-run初始化DDR,然后执行make kernel-run,就可以直接启东到linux kernel了。具体可参考”README.bubblegum96[2]”。

以上改动可参考如下patch:

https://github.com/wowotechX/u-boot/commit/59553f4c7583c002e9eb5e2ac7402a25699d51c6

3. 参考文档

[1] README.autoboot

[2] README.bubblegum96

 

原创文章,转发请注明出处。蜗窝科技,www.wowotech.net

内存初始化(上)

$
0
0

一、前言

一直以来,我都非常着迷于两种电影拍摄手法:一种是慢镜头,将每一个细节全方位的展现给观众。另外一种就是快镜头,多半是反应一个时代的变迁,从非常长的时间段中,截取几个典型的snapshot,合成在十几秒的镜头中,可以让观众很快的了解一个事物的发展脉络。对应到技术层面,慢镜头有点类似情景分析,把每一行代码都详细的进行解析,了解技术的细节。快镜头类似数据流分析,勾勒一个过程中,数据结构的演化。本文采用了快镜头的方法,对内存初始化部分进行描述,不纠缠于具体函数的代码实现,只是希望能给大家一个概略性的印象(有兴趣的同学可以自行研究代码)。BTW,在本文中我们都是基于ARM64来描述体系结构相关的内容。

 

二、启动之前

在详细描述linux kernel对内存的初始化过程之前,我们必须首先了解kernel在执行第一条语句之前所面临的处境。这时候的内存状况可以参考下图:

bootloader有自己的方法来了解系统中memory的布局,然后,它会将绿色的kernel image和蓝色dtb image copy到了指定的内存位置上。kernel image最好是位于main memory起始地址偏移TEXT_OFFSET的位置,当然,TEXT_OFFSET需要和kernel协商好。kernel image是否一定位于起始的main memory(memory address最低)呢?也不一定,但是对于kernel而言,低于kernel image的内存,kernel是不会纳入到自己的内存管理系统中的。对于dtb image的位置,linux并没有特别的要求。由于这时候MMU是turn off的,因此CPU只能看到物理地址空间。对于cache的要求也比较简单,只有一条:kernel image对应的cache必须clean to PoC,即系统中所有的observer在访问kernel image对应内存地址的时候是一致性的。

 

三、汇编时代

一旦跳转到linux kernel执行,内核则完全掌控了内存系统的控制权,它需要做的事情首先就是要打开MMU,而为了打开MMU,必须要创建linux kernel正常运行需要的页表,这就是本节的主要内容。

在体系结构相关的汇编初始化阶段,我们会准备二段地址的页表:一段是identity mapping,其实就是把物理地址mapping到物理地址上去,打开MMU相关的代码需要这样的mapping(别的CPU不知道,但是ARM ARCH强烈推荐这么做的)。第二段是kernel image mapping,内核代码欢快的执行当然需要将kernel running需要的地址(kernel txt、dernel rodata、data、bss等等)进行映射了。具体的映射情况可以参考下图:

turn on MMU相关的代码被放入到一个特别的section,名字是.idmap.text,实际上对应上图中物理地址空间的IDMAP_TEXT这个block。这个区域的代码被mapping了两次,做为kernel image的一部分,它被映射到了__idmap_text_start开始的虚拟地址上去,此外,假设IDMAP_TEXT block的物理地址是A地址,那么它还被映射到了A地址开始的虚拟地址上去。虽然上图中表示的A地址似乎要大于PAGE_OFFSET,不过实际上不一定需要这样的关系,这和具体处理器的实现有关。

编译器感知的是kernel image的虚拟地址(左侧),在内核的链接脚本中定义了若干的符号,都是虚拟地址。但是在内核刚开始,没有打开MMU之前,这些代码实际上是运行在物理地址上的,因此,内核起始刚开始的汇编代码基本上是PIC的,首先需要定位到页表的位置,然后在页表中填入kernel image mapping和identity mapping的页表项。页表的起始位置比较好定(bss段之后),但是具体的size还是需要思考一下的。我们要选择一个合适的size,确保能够覆盖kernel image mapping和identity mapping的地址段,然后又不会太浪费。我们以kernel image mapping为例,描述确定Tranlation table size的思考过程。假设48 bit的虚拟地址配置,4k的page size,这时候需要4级映射,地址被分成9(level 0 or PGD) + 9(level 1 or PUD) + 9(level 2 or PMD) + 9(level 3 or PTE) + 12(page offset),假设我们分配4个page分别保存Level 0到level 3的translation table,那么可以建立的最大的地址映射范围是512(level 3中有512个entry) X 4k = 2M。2M这个size当然不理想,无法容纳kernel image的地址区域,怎么办?使用section mapping,让PMD执行block descriptor,这样使用3个page就可以mapping 512 X 2M = 1G的地址空间范围。当然,这种方法有一点副作用就是:PAGE_OFFSET必须2M对齐。对于16K或者64K的page size,使用section mapping就有点不合适了,因为这时候对齐的要求太高了,对于16K page size,需要32M对齐,对于64K page size,需要512M对齐。不过,这也没有什么,毕竟这时候page size也变大了,不使用section mapping也能覆盖很大区域。例如,对于16K page size,一个16K page size中可以保存2K个entry,因此能够覆盖2K X 16K = 32M的地址范围。对于64K page size,一个64K page size中可以保存8K个entry,因此能够覆盖8K X 64K = 512M的地址范围。32M和512M基本是可以满足需求的。最后的结论:swapper进程(内核空间)需要预留页表的size是和page table level相关,如果使用了section mapping,那么需要预留PGTABLE_LEVELS - 1个page。如果不使用section mapping,那么需要预留PGTABLE_LEVELS 个page。

上面的结论起始是适合大部分情况下的identity mapping,但是还是有特例(需要考虑的点主要和其物理地址的位置相关)。我们假设这样的一个配置:虚拟地址配置为39bit,而物理地址是48个bit,同时,IDMAP_TEXT这个block的地址位于高端地址(大于39 bit能表示的范围)。在这种情况下,上面的结论失效了,因为PGTABLE_LEVELS 是和虚拟地址的bit数、PAGE_SIZE的定义相关,而是和物理地址的配置无关。linux kernel使用了巧妙的方法解决了这个问题,大家可以自己看代码理解,这里就不多说了。

一旦设定完了页表,那么打开MMU之后,kernel正式就会进入虚拟地址空间的世界,美中不足的是内核的虚拟世界没有那么大。原来拥有的整个物理地址空间都消失了,能看到的仅仅剩下kernel image mapping和identity mapping这两段地址空间是可见的。不过没有关系,这只是刚开始,内存初始化之路还很长。

 

四、看见DTB

虽然可以通过kernel image mapping和identity mapping来窥探物理地址空间,但终究是管中窥豹,不了解全局,那么内核是如何了解对端的物理世界呢?答案就是DTB,但是问题来了,这时候,内核还没有为DTB这段内存创建映射,因此,打开MMU之后的kernel还不能直接访问,需要先创建dtb mapping,而要创建address mapping,就需要分配页表内存,而这时候,还没有了解内存布局,内存管理模块还没有初始化,如何来分配内存呢?

下面这张图片给出了解决方案:

整个虚拟地址空间那么大,可以被平均分成两半,上半部分的虚拟地址空间主要各种特定的功能,而下半部分主要用于物理内存的直接映射。对于DTB而言,我们借用了fixed-mapped address这个概念。fixed map是被linux kernel用来解决一类问题的机制,这类问题的共同特点是:(1)在很早期的阶段需要进行地址映射,而此时,由于内存管理模块还没有完成初始化,不能动态分配内存,也就是无法动态分配创建映射需要的页表内存空间。(2)物理地址是固定的,或者是在运行时就可以确定的。对于这类问题,内核定义了一段固定映射的虚拟地址,让使用fix map机制的各个模块可以在系统启动的早期就可以创建地址映射,当然,这种机制不是那么灵活,因为虚拟地址都是编译时固定分配的。

好,我们可以考虑创建第三段地址映射了,当然,要创建地址映射就要创建各个level中描述符。对于fixed-mapped address这段虚拟地址空间,由于也是位于内核空间,因此PGD当然就是复用swapper进程的PGD了(其实整个系统就一个PGD),而其他level的Translation table则是静态定义的(arch/arm64/mm/mmu.c),位于内核bss段,由于所有的Translation table都在kernel image mapping的范围内,因此内核可以毫无压力的访问,并创建fixed-mapped address这段虚拟地址空间对应的PUD、PMD和PTE的entry。所有中间level的Translation table都是在early_fixmap_init函数中完成初始化的,最后一个level则是在各个具体的模块进行的,对于DTB而言,这发生在fixmap_remap_fdt函数中。

系统对dtb的size有要求,不能大于2M,这个要求主要是要确保在创建地址映射(create_mapping)的时候不能分配其他的translation table page,也就是说,所有的translation table都必须静态定义。为什么呢?因为这时候内存管理模块还没有初始化,即便是memblock模块(初始化阶段分配内存的模块)都尚未初始化(没有内存布局的信息),不能动态分配内存。

 

五、early ioremap

除了DTB,在启动阶段,还有其他的模块也想要创建地址映射,当然,对于这些需求,内核统一采用了fixmap的机制来应对,fixmap的具体信息如下图所示:

从上面这个图片可以看出fix-mapped虚拟地址分成两段,一段是permanent fix map,一段是temporary fixmap。所谓permanent表示映射关系永远都是存在的,例如FDT区域,一旦完成地址映射,内核可以访问DTB之后,这个映射关系一直都是存在的。而temporary fixmap则不然,一般而言,某个模块使用了这部分的虚拟地址之后,需要尽快释放这段虚拟地址,以便给其他模块使用。

你可能会很奇怪,因为传统的驱动模块中,大家通常使用ioremap函数来完成地址映射,为了还有一个early IO remap呢?其实ioremap函数的使用需要一定的前提条件的,在地址映射过程中,如果某个level的Translation tabe不存在,那么该函数需要调用伙伴系统模块的接口来分配一个page size的内存来创建某个level的Translation table,但是在启动阶段,内存管理的伙伴系统还没有ready,其实这时候,内核连系统中有多少内存都不知道的。而early io remap则在early_ioremap_init之后就可以被使用了。更具体的信息请参考mm/early_ioremap.c文件。

结论:如果想要在伙伴系统初始化之前进行设备寄存器的访问,那么可以考虑early IO remap机制。

 

六、内存布局

完成DTB的映射之后,内核可以访问这一段的内存了,通过解析DTB中的内容,内核可以勾勒出整个内存布局的情况,为后续内存管理初始化奠定基础。收集内存布局的信息主要来自下面几条途径:

(1)choosen node。该节点有一个bootargs属性,该属性定义了内核的启动参数,而在启动参数中,可能包括了mem=nn[KMG]这样的参数项。initrd-start和initrd-end参数定义了initial ramdisk image的物理地址范围。

(2)memory node。这个节点主要定义了系统中的物理内存布局。主要的布局信息是通过reg属性来定义的,该属性定义了若干的起始地址和size条目。

(3)DTB header中的memreserve域。对于dts而言,这个域是定义在root node之外的一行字符串,例如:/memreserve/ 0x05e00000 0x00100000;,memreserve之后的两个值分别定义了起始地址和size。对于dtb而言,memreserve这个字符串被DTC解析并称为DTB header中的一部分。更具体的信息可以参考device tree基础文档,了解DTB的结构。

(4)reserved-memory node。这个节点及其子节点定义了系统中保留的内存地址区域。保留内存有两种,一种是静态定义的,用reg属性定义的address和size。另外一种是动态定义的,只是通过size属性定义了保留内存区域的长度,或者通过alignment属性定义对齐属性,动态定义类型的子节点的属性不能精准的定义出保留内存区域的起始地址和长度。在建立地址映射方面,可以通过no-map属性来控制保留内存区域的地址映射关系的建立。更具体的信息可以阅读参考文献[1]。

通过对DTB中上述信息的解析,其实内核已经基本对内存布局有数了,但是如何来管理这些信息呢?这也就是著名的memblock模块,主要负责在初始化阶段用来管理物理内存。一个参考性的示意图如下:

内核在收集了若干和memory相关的信息后,会调用memblock模块的接口API(例如:memblock_add、memblock_reserve、memblock_remove等)来管理这些内存布局的信息。内核需要动态管理起来的内存资源被保存在memblock的memory type的数组中(上图中的绿色block,按照地址的大小顺序排列),而那些需要预留的,不需要内核管理的内存被保存在memblock的reserved type的数组中(上图中的青色block,也是按照地址的大小顺序排列)。要想了解进一步的信息,请参考内核代码中的setup_machine_fdt和arm64_memblock_init这两个函数的实现。

 

七、看到内存

了解到了当前的物理内存的布局,但是内核仍然只是能够访问部分内存(kernel image mapping和DTB那两段内存,上图中黄色block),大部分的内存仍然处于黑暗中,等待光明的到来,也就是说需要创建这些内存的地址映射。

在这个时间点上,创建内存的地址映射有一个悖论:创建地址映射需要分配内存,但是这时候伙伴系统没有ready,无法动态分配。也许你会说,memblock不是已经ready了吗,不可以调用memblock_alloc进行物理内存的分配吗?当然可以,memblock_alloc分配的物理内存仍然需要通过虚拟地址访问,而这些内存都还没有创建地址映射,因此内核一旦访问memblock_alloc分配的物理内存,悲剧就会发生了。

怎么办呢?内核采用了一个巧妙的办法:那就是控制创建地址映射,memblock_alloc分配页表内存的顺序。也就是说刚开始的时候创建的地址映射不需要页表内存的分配,当内核需要调用memblock_alloc进行页表物理地址分配的时候,很多已经创建映射的内存已经ready了,这样,在调用create_mapping的时候不需要分配页表内存。更具体的解释参考下面的图片:

我们知道,在内核编译的时候,在BSS段之后分配了几个page用于swapper进程地址空间(内核空间)的映射,当然,由于kernel image不需要mapping那么多的地址,因此swapper进程translation table的最后一个level中的entry不会全部的填充完毕。换句话说:swapper进程页表可以支持远远大于kernel image mapping那一段的地址区域,实际上,它可以支持的地址段的size是SWAPPER_INIT_MAP_SIZE。为(PAGE_OFFSET,PAGE_OFFSET+SWAPPER_INIT_MAP_SIZE)这段虚拟内存创建地址映射,mapping到(PHYS_OFFSET,PHYS_OFFSET+SWAPPER_INIT_MAP_SIZE)这段物理内存的时候,调用create_mapping不会发生内存分配,因为所有的页表都已经存在了,不需要动态分配。

一旦完成了(PHYS_OFFSET,PHYS_OFFSET+SWAPPER_INIT_MAP_SIZE)这段物理内存的地址映射,这时候,终于可以自由使用memblock_alloc进行内存分配了,当然,要进行限制,确保分配的内存位于(PHYS_OFFSET,PHYS_OFFSET+SWAPPER_INIT_MAP_SIZE)这段物理内存中。完成所有memory type类型的memory region的地址映射之后,可以解除限制,任意分配memory了。而这时候,所有memory type的地址区域(上上图中绿色block)都已经可见,而这些宝贵的内存资源就是内存管理模块需要管理的对象。具体代码请参考paging_init--->map_mem函数的实现。

 

八、结束语

目前为止,所有为内存管理做的准备工作已经完成:收集了整个内存布局的信息,memblock模块中已经保存了所有需要管理memory region的信息,同时,系统也为所有的内存(reserved除外)创建了地址映射。虽然整个内存管理系统没有ready,但是通过memblock模块已经可以在随后的初始化过程中进行动态内存的分配。 有了这些基础,随后就是真正的内存管理系统的初始化了,我们下回分解。

 

参考文献:

1、Documentation/devicetree/bindings/reserved-memory/reserved-memory.txt

2、linux4.4.6内核代码

Linux TTY framework(3)_从应用的角度看TTY设备

$
0
0

1. 前言

可以毫不夸张的说,我们在使用Linux系统的过程中,每时每刻都在和TTY打交道,显示输出、键盘输入、用户登录、shell终端、等等。

与此同时,作为软件工程师的我们,也会或多或少的困惑:这些习以为常的行为,怎么和kernel中的这些冷冰冰的代码联系起来的?

因此,在Linux TTY framework分析工作正式开始之前,让我们带着上面的疑问,以这些熟悉的应用场景为视角,进一步理解TTY有关的概念。这就是本文的目的。

2. 再谈终端(Terminal)设备

我们在“Linux TTY framework(1)_基本概念”中简单的提过,终端设备是指那些帮助我们和计算机进行人机交互的设备,所谓的人机交互,可简单的总结为:

输入(input),向计算机发送指令;

输出(output),计算机将执行结果显示出来。

与此同时,关于输入/输出,可提出如下疑问(为了简单,将输入、输出看作一个统一的整体,即终端):

输入、输出设备是什么?

计算机怎么接收和输出?

人怎么接收和输出(这个就不用回答了,大家都知道,呵呵)。

关于这两个疑问,下面我们分别讨论。

2.1 终端的类型

根据不同的输入、输出设备类型,我们常见的终端有如下几类:

1)控制台终端(console)

这类终端的输入设备通常是键盘,输出设备通常是显示器;

输入、输出设备通过各类总线直接和计算机相连;

“终端”其实是这些设备的一个逻辑抽象。

2)虚拟终端(VT)

控制台终端的输出设备(显示器)一般只有一个,同一时刻由一个应用程序独占;

但在多任务的操作环境中,有时需要在将终端切换给另一个应用程序之前,保留当前应用在终端上的输出,以方便后面查看;

因此Unix/Linux系统在控制台终端的基础上,又虚拟出来6个终端----称作虚拟终端,不同的应用程序可以在这些虚拟终端上独立的输出,在需要的时候,可以通过键盘的组合键(CTRL+ALT+ F1~F6)将某一个虚拟终端调出来在屏幕上显示。

3)串口终端(TTY)

这是正牌的TTY设备

输入设备和输出设备集成在一个独立的硬件上(称作TTY设备),这个硬件和计算机通过串口连接;

输入设备(键盘)的输入动作,将会转换为串口上的RX数据包(以计算机为视角),发送给计算机;

计算机的输出会以TX数据包的形式发送给TTY设备,TTY设备转换后在输出设备(屏幕)上显示。

4)软件终端

这是我们现在最常用的终端:

既然人机交互的数据流可以封装后经过串口传输,那么终端设备的形式就不再受限了,只要可以接收用户的输入并打包通过串口发送给计算机,以及接收计算机从串口发来的输出并显示出来,任何设备都可以变成终端设备,例如另一台计算机;

当另一台计算机被当作终端设备时,通常不会把它的所有资源都用来和对端进行人机交互,常用的方法是,在这个计算机上利用软件,模拟出来一个“终端设备”。该软件就像一个中间商:从键盘接收用户输入,然后控制串口发送给对端;从串口接收对端的输出,然后在软件界面上显示出来;

平时大家经常使用的PuTTY、SecureCRT、Windows超级终端、等等,都是“软件终端”。

5)USB、网络等终端

既然串口可以作为人机交互数据的传说媒介,其它通信接口一样可以,例如USB、Ethernet、等等,其原理和串口终端完全一样,这里不再过多说明。

6)图形终端

前面所介绍的那些终端,人机交互的输出界面都是字符界面,随着计算机技术的发展,GUI界面慢慢出现并成为主流,这些通过GUI交互的形式,也可以称作图形终端。不过这已经超出了TTY framework系列文章的讨论范围了,因为TTY的势力范围只涵盖字符界面。

2.2 输入和输出

首先声明,这里的输入和输出,都是针对计算机设备而言。

另外,计算机是一个硬件设备,它本身没有输入和输出的自主意识,因此,输入和输出,都是指运行在计算机中的软件,所谓的人机交互,其实是人和一个个的软件交互。

以Linux系统为例:

kernel会通过控制台终端输出日志信息(printk);

kernel启动之后,init进程会打开控制台终端,以便和我们交互(接收一些指令并应答、输出日志信息、等等);

随后,getty应用可以打开任意的终端设备,和我们交互,以便我们可以登入系统;

登入系统之后,getty将终端的控制权交给shell(bash、sh等等),我们与之交互,进行着愉快的人机对话,巴拉巴拉……

当然,在人机对话的过程中,我们可以命令shell启动其它的应用,例如地图应用,该应用可能会打开另一个TTY设备(例如GPS UART),并与之交互(由此可见,上面提到的人机交互其实是狭隘的概念,也可以机机交互~~);

等等,等等。

3. 软件视角

讲完概念,我们还是要回到软件上来,厘清Linux TTY framework中的那些奇怪概念。

注1:建议读者结合“Linux TTY framework框架[1]”阅读本节内容。

3.1 console driver

Linux系统中可以存在多个种类各异的终端设备,工程师会使用一个个的TTY driver(struct tty_driver)驱动它们。如果某些我们需要让某些终端作为控制台终端,可以基于TTY driver,创建对应的console driver,并注册给kernel。

关于console driver,kernel有如下的策略:

1)可以同时注册多个console driver,并有选择的使用。

2)可以在kernel启动的时候,通过命令行(或者后来的device tree),告诉kernel使用哪个或者哪些控制台终端。例如:

console=”/dev/ttyS0”, console=”/dev/ttyUSB0”

3)对kernel的日志输出来说,可以在所有被选中的控制台终端上输出。

4)后续可用作人机交互的控制台终端只能有一个(后指定的那个)。

3.2 控制台终端(/dev/console)

前面提到过,控制台终端只能有一个(最后指定的那个),那么“/dev/console”又是怎么回事呢?

了解linux kernel启动过程的同学都知道,kernel启动的后期,会在kernel_init线程(最后会退化为init进程)中打开控制台终端。但是由上面3.1小节的介绍可知,控制台终端的类型、名称是五花八门的,怎么让kernel的核心代码无视这些差异呢?这就是“/dev/console”的存在意义:

由“Linux TTY framework(2)_软件架构[2]”中的介绍可知,/dev/console的设备号固定为(5, 1) ,当init线程打开该设备的时候,TTY core会问system console core:喂,哪一个终端适合做控制台终端啊?

因此,最终打开的是那个具体的、可以当作控制台终端的设备,而“/dev/console”,仅仅是一个占位坑,如下图所示:

dev_console

3.3 虚拟终端(/dev/ttyN)

正如其名,虚拟终端是虚拟出来的一个终端,不对应具体的设备(屏幕和键盘)。应用程序可以打开某一个虚拟终端,以便和人进行交互。

对应用程序而言,这个终端和具体的物理终端,没有任何区别(应用程序也无法区分)。而对整个系统来说,由于物理资源(键盘和屏幕)只有一套,因此同一时刻只能和某一个虚拟终端对接。从另一个角度看,各个虚拟终端轮流使用物理资源和人进行交互,如下所示:

vt

3.4 伪终端(Pseudo Terminal,pty)

前面提到过,既然串口(serial)可用作终端和计算机之间的数据传说通道,那么其它诸如Ethernet的通信介质,也可以实现类型的功能。但这里面有一个问题,以网络为例:

Linux系统中中的网络驱动,并不是以TTY的形式提供API(众所周知,网络使用socket接口)。因此,系统中的应用程序,无法直接打开网络设备,进而和对应的终端设备通信。怎么办?

解决方案就是伪终端(英文为Pseudo Terminal,简称pty)。字面上理解,伪终端根本不是终端(虚拟终端好歹还是),是为了让应用程序可以从网络等非串口类的接口上,和终端设备交互而伪造出来的一种东西。它的原理如下:

pty由pts(pseudo-terminal slave)和ptmx(pseudo-terminal master)两部分组成。

pts伪造出一个标准的TTY设备,应用程序可以直接访问。应用程序向pts写入的数据,会直接反映到ptmx上,同样,应用程序从pts读数据,则相当于直接从ptmx读取。

而pymx,则根据具体情况,具体实现。例如要通过网络接口和终端设备交互,则pymx需要打开对应的socket,将pts写来的数据,从socket送出,将从socket读取的数据,送回给pts。

示意图图下:

pty

3.5 控制终端(control terminal,/dev/tty)

讲到控制终端(control terminal),就不得不提Unix/Linux的Job control[3]功能。有关job control,感兴趣的读者可以参考其它文章(如[3]),这里简单总结如下:

1)job control是Unix/Linux shell(记住,shell是一个应用程序)中的概念,是shell用来管理、控制jobs的一种方法。

2)job是进程组(process group[4])在shell中的体现。换句话说,shell借用进程组实现了job。

3)进程组(或者job,在本文中可以等同,后面不在区分)是多个进程的组合。

基于上面简单的知识(不一定准确,但不影响我们对控制终端的理解),我们引入控制终端的概念:

1)通常情况下,Linux启动后,终端(以后都用TTY指代)的控制权会交给shell(一种应用程序)。所谓的控制权,就是指shell程序可以通过TTY读取终端的输入,以及通过TTY向终端输出。

2)通过shell,可以启动其它的应用程序,相应地,应用程序在需要的时候也会获得TTY的控制权。

3)同一时刻,只能有一个应用可以占有TTY,即只有一个应用可以通过TTY输入、输出。

4)那个占有TTY、可以进行输入输出的应用,称作前台应用。相应的,不能进行输入输出的应用,称作后台应用。因此,shell中只有一个前台应用,可以有多个后台应用。

5)然后,问题就来了:如果某个后台应用,就是想输入输出,怎么办?有一个办法,就是通过控制终端(control terminal)

6)控制终端在Linux中的名称固定为/dev/tty, 设备号为(5, 0),作用和/dev/console类似,进程可以通过TTY core提供的ioctl,选择控制终端所对应的实际的终端设备。

7)暂且抛开前台应用不谈(因为人家有TTY设备),对于那些后台应用,如果想输入输出,可以读取或者写入控制终端。此时,一般情况下,TTY core会向后台应用发送SIGTTIN(读取控制终端时) 或者 SIGTTOU(写入控制终端时)信号,这会终止该后台应用。

8)不过,shell会重设收到 SIGTTOU信号时的行为,于是,后台应用写入的内容,可以通过控制终端显示出来。

4. 参考文档

[1] Linux TTY framework框架

[2] Linux TTY framework(2)_软件架构

[3] https://en.wikipedia.org/wiki/Job_control_(Unix)

[4] http://www.wowotech.net/process_management/process_identification.html

 

原创文章,转发请注明出处。蜗窝科技www.wowotech.net

DRAM 原理 5 :DRAM Devices Organization

$
0
0

随着系统对内存容量、带宽、性能等方面的需求提高,系统会接入多个 DRAM Devices。而多个 DRAM Devices 不同的组织方式,会带来不同的效果。本文将对不同的组织方式及其效果进行简单介绍。

相关文章:

DRAM 原理 1 :DRAM Storage Cell

DRAM 原理 2 :DRAM Memory Organization

DRAM 原理 3 :DRAM Device

DRAM 原理 4 :DRAM Timing

1. Single Channel DRAM Controller 组织方式

Single Channel 指 DRAM Controller 只有一组控制和数据总线。 在这种场景下,DRAM Controller 与单个或者多个 DRAM Devices 的连接方式如下所示:

1.1 连接单个 DRAM Device

Single Channel 连接单个 DRAM Device 是最常见的一种组织方式。 由于成本、工艺等方面的因素,单个 DRAM Device 在总线宽度、容量上有所限制,在需要大带宽、大容量的产品中,通常接入多个 DRAM Devices。

1.2 连接多个 DRAM Devices

上图中,多个 DRAM Devices 共享控制和数据总线,DRAM Controller 通过 Chip Select 分时单独访问各个 DRAM Devices。此外,在其中一个 Device 进入刷新周期时,DRAM Controller 可以按照一定的调度算法,优先执行其他 Device 上的访问请求,提高系统整体内存访问性能。

NOTE:
CS0 和 CS1 在同一时刻,只有一个可以处于使能状态,即同一时刻,只有一个 Device 可以被访问。

上述的这种组织方式只增加总体容量,不增加带宽。下图中描述的组织方式则可以既增加总体容量,也增加带宽。

上图中,多个 DRAM Devices 共享控制总线和 Chip Select 信号,DRAM Controller 同时访问每个 DRAM Devices,各个 Devices 的数据合并到一起,例如 Device 1 的数据输出到数据总线的 DATA[0:7] 信号上,Device 2 的数据输出到数据总线的 DATA[8:15] 上。这样的组织方式下,访问 16 bits 的数据就只需要一个访问周期就可以完成,而不需要分解为两个 8 bits 的访问周期。

2. Multi Channel DRAM Controller 组织方式

Multi Channel 指 DRAM Controller 只有多组控制和数据总线,每一组总线可以独立访问 DRAM Devices。 在这种场景下,DRAM Controller 与 DRAM Devices 的连接方式如下所示:

2.1 连接 Single Channel DRAM Devices

这种组织方式的优势在于多个 Devices 可以同时工作,DRAM Controller 可以对不同 Channel 上的 Devices 同时发起读写请求,提高了读写请求的吞吐率。

NOTE:
CS0 和 CS1 在同一时刻,可以同时处于使能状态,即同一时刻,两个 Devices 可以同时被访问。

2.2 连接 Multi Channel DRAM Device

在一些 DRAM 产品中,例如 LPDDR3、LPDDR4 等,引入了 Multi Channel 的设计,即一个 DRAM Devices 中包括多个 Channel。这样就可以在单个 Device 上达成 Multi Channel 同时访问的效果,最终带来读写请求吞吐率的提升。


Linux TTY framework(4)_TTY driver

$
0
0

1. 前言

本文将从驱动工程师的角度去看TTY framework:它怎么抽象、管理各个TTY设备?它提供了哪些编程接口以方便TTY driver的开发?怎么利用这些接口编写一个TTY driver?等等。

注1:话说介绍各个framework的时候,我一直比较喜欢用provider、consumer等概念,因为非常生动、易懂。不过在TTY framework的官方俗语中,压根没有provider、consumer等概念,为了不混淆试听,就算了吧。

注2:TTY framework在Linux kernel中算得上一个比较繁琐、庞杂的framework了,再加上现在很少有人会直接去写一个TTY driver,因此本文只是介绍一些概念性的东西,以加深对TTY及其driver的理解,为后续学习serial framework打基础。一些细节的东西,大家可参考callme_friend同学写的"TTY驱动分析[2]”,特别是其中的一些图示,很清晰!

注3:本文所使用的kernel版本为“X Project”初始的“Linux 4.6-rc5”版本。

2. 关键数据结构

注4:阅读本章内容时可对照callme_friend画的的TTY个数据结构的关系图[3]以加深理解。

2.1 TTY device

Linux TTY framework的核心功能,就是管理TTY设备,以方便应用程序使用。于是,问题来了,Linux kernel是怎么抽象TTY设备的呢?答案很尴尬,kernel并不认为TTY device是一个设备,这很好理解:

比如,我们熟悉的串口终端,串口控制器(serial controller)是一个实实在在的硬件设备,一个控制器可以支持多个串口(serial port),软件在串口上收发数据,就相当于在驱动“串口终端”。此处的TTY device,就是从串口控制器中抽象出来的一个数据通道;

再比如,我们常用的网络终端,只有以太网控制器(或者WLAN控制器)是实实在在的设备,sshd等服务进程,会基于网络socket,虚拟出来一个数据通道,软件在这个通道上收发数据,就相当于在驱动“网络终端”。

因此,从kernel的角度看,TTY device就是指那些“虚拟的数据通道”。

另外,由于TTY driver在linux kernel中出现的远比设备模型早,所以在TTY framework中,没有特殊的数据结构用于表示TTY设备。当然,为了方便,kernel从设备模型和字符设备两个角度对它进行了抽象:

1)设备模型的角度

为每个“数据通道”注册了一个stuct device,以便可以在sysfs中体现出来,例如:

/sys/class/tty/tty

/sys/class/tty/console

/sys/class/tty/ttyS0

2)字符设备的角度

为每个“数据通道”注册一个struct cdev,以便在用户空间可以访问,例如:

/dev/tty

/dev/console

/dev/ttyS0

2.2 TTY driver

从当前设备模型的角度看,TTY framework有点奇怪,它淡化了device的概念(参考2.1的介绍),却着重突出driver。由struct tty_driver所代表的TTY driver,几乎大包大揽了TTY device有关的所有内容,如下:

struct tty_driver {
        int     magic;          /* magic number for this structure */
        struct kref kref;       /* Reference management */
        struct cdev **cdevs;
        struct module   *owner;
        const char      *driver_name;
        const char      *name;
        int     name_base;      /* offset of printed name */
        int     major;          /* major device number */
        int     minor_start;    /* start of minor device number */
        unsigned int    num;    /* number of devices allocated */
        short   type;           /* type of tty driver */
        short   subtype;        /* subtype of tty driver */
        struct ktermios init_termios; /* Initial termios */
        unsigned long   flags;          /* tty driver flags */
        struct proc_dir_entry *proc_entry; /* /proc fs entry */
        struct tty_driver *other; /* only used for the PTY driver */

        /*
         * Pointer to the tty data structures
         */
        struct tty_struct **ttys;
        struct tty_port **ports;
        struct ktermios **termios;
        void *driver_state;

        /*
         * Driver methods
         */

        const struct tty_operations *ops;
        struct list_head tty_drivers;
}

原则上来说,在编写TTY driver的时候,我们只需要定义一个struct tty_driver变量,并根据实际情况正确填充其中的字段后,注册到TTY core中,即可完成驱动的设计。当然,我们不需要关心struct tty_driver中的所有字段,下面我们捡一些重点的字段一一说明。

1)需要TTY driver关心的字段

driver_name,该TTY driver的名称,在软件内部使用;

name,该TTY driver所驱动的TTY devices的名称,会体现到sysfs以及/dev/等文件系统下;

major、minor_start,该TTY driver所驱动的TTY devices的在字符设备中的主次设备号。因为一个tty driver可以支持多个tty device,因此次设备号只指定了一个start number;

num,该driver所驱动的tty device的个数,可以在tty driver注册的时候指定,也可以让TTY core自行维护,具体由TTY_DRIVER_DYNAMIC_DEV flag决定(可参考“”中的介绍);

type、subtype,TTY driver的类型,具体可参考“include/linux/tty_driver.h”中的定义;

init_termios,初始的termios,可参考2.5小节的介绍;

flags,可参考2.6小节的介绍;

ops,tty driver的操作函数集,可参考2.7小节的介绍;

driver_state,可存放tty driver的私有数据。

2)内部使用的字段

ttys,一个struct tty_struct类型的指针数组,可参考2.3小节的介绍;

ports,一个struct tty_port类型的指针数组,可参考2.4小节的介绍;

termios,一个struct ktermios类型的指针数组,可参考2.5小节的介绍。

2.3 TTY struct(struct tty_struct)

TTY struct是TTY设备在TTY core中的内部表示。

从TTY driver的角度看,它和文件句柄的功能类似,用于指代某个TTY设备。

从TTY core的角度看,它是一个比较复杂的数据结构,保存了TTY设备生命周期中的很多中间变量,如:

dev,该设备的struct device指针;

driver,该设备的struct tty_driver指针;

ops,该设备的tty操作函数集指针;

index,该设备的编号(如tty0、tty1中的0、1);

一些用于同步操作的mutex锁、spinlock锁、读写信号量等;

一些等待队列;

write buffer有关的信息;

port,该设备对应的struct tty_port(可参考2.4小节的介绍)

等等。

由于编写TTY driver的时候不需要特别关心struct tty_struct的内部细节,这里不再详细介绍。

2.4 TTY port(struct tty_port)

在TTY framework中TTY port是一个比较难理解的概念,因为它和TTY struct类似,也是TTY device的一种抽象。那么,既然有了TTY struct,为什么还需要TTY port呢?先看一下kernel代码注释的解释:

/* include/linux/tty.h */

/*
* Port level information. Each device keeps its own port level information
* so provide a common structure for those ports wanting to use common support
* routines.
*
* The tty port has a different lifetime to the tty so must be kept apart.
* In addition be careful as tty -> port mappings are valid for the life
* of the tty object but in many cases port -> tty mappings are valid only
* until a hangup so don't use the wrong path.
*/

我的理解是:

TTY struct是TTY设备的“动态抽象”,保存了TTY设备访问过程中的一些临时信息,这些信息是有生命周期的:从打开TTY设备开始,到关闭TTY设备结束;

TTY port是TTY设备固有属性的“静态抽象”,保存了该设备的一些固定不变的属性值,例如是否是一个控制台设备(console)、打开关闭时是否需要一些delay操作、等等;

另外(这一点很重要),TTY core负责的是逻辑上的抽象,并不关心这些固有属性。因此从层次上看,这些属性完全可以由具体的TTY driver自行维护;

不过,由于不同TTY设备的属性有很多共性,如果每个TTY driver都维护一个私有的数据结构,将带来代码的冗余。所以TTY framework就将这些共同的属性抽象出来,保存在struct tty_port数据结构中,同时提供一些通用的操作接口,供具体的TTY driver使用;

因此,总结来说:TTY struct是TTY core的一个数据结构,由TTY core提供并使用,必要的时候可以借给具体的TTY driver使用;TTY port是TTY driver的一个数据结构,由TTY core提供,由具体的TTY driver使用,TTY core完全不关心。

2.5 termios(struct ktermios)

说实话,在Unix/Linux的世界中,终端(terminal)编程是一个非常繁琐的事情,为了改善这种状态,特意制订了符合POSIX规范的应用程序编程接口,称作POSIX terminal interface[3]。POSIX terminal interface操作的对象,就是名称为termios的数据结构(在用户空间为struct termios,内核空间为struct ktermios)。以kernel中的struct ktermios为例,其定义如下:

/* include/uapi/asm-generic/termbits.h */

struct ktermios {
        tcflag_t c_iflag;               /* input mode flags */
        tcflag_t c_oflag;               /* output mode flags */
        tcflag_t c_cflag;               /* control mode flags */
        tcflag_t c_lflag;               /* local mode flags */
        cc_t c_line;                    /* line discipline */
        cc_t c_cc[NCCS];                /* control characters */
        speed_t c_ispeed;               /* input speed */
        speed_t c_ospeed;               /* output speed */
};

说实话,要理解上面的数据结构,真的不是一件容易的事情,这里只能简单介绍一些我们常用的内容,更为具体的,如有需要,后面会用单独的文章去分析:

c_cflag,可以控制TTY设备的一些特性,例如data bits、parity type、stop bit、flow control等(例如串口设备中经常提到的8N1);

c_ispeed、c_ospeed,可以分别控制TTY设备输入和输出的速度(例如串口设备中的波特率);

其它,暂不介绍。

2.6 tty driver flags

TTY driver在注册struct tty_driver变量的时候,可以提供一些flags,以告知TTY core一些额外的信息,例如(具体可参考include/linux/tty_driver.h中的定义和注释,写的很清楚):

TTY_DRIVER_DYNAMIC_DEV:如果设置了该flag,则表示TTY driver会在需要的时候,自行调用tty_register_device接口注册TTY设备(相应地回体现在字符设备以及sysfs中);如果没有设置,TTY core会在tty_register_driver时根据driver->num信息,自行创建对应的TTY设备。

2.7 TTY操作函数集

TTY core将和硬件有关的操作,抽象、封装出来,形成名称为struct tty_operations的数据结构,具体的TTY driver不需要关心具体的业务逻辑,只需要根据实际的硬件情况,实现这些操作接口即可。

听着还挺不错啊(framework的中心思想一贯如此,大家牢记即可),不过不能高兴的太早,看过这个数据结构之后,估计心会凉半截:

struct tty_operations {
        struct tty_struct * (*lookup)(struct tty_driver *driver,
                        struct inode *inode, int idx);
        int  (*install)(struct tty_driver *driver, struct tty_struct *tty);
        void (*remove)(struct tty_driver *driver, struct tty_struct *tty);
        int  (*open)(struct tty_struct * tty, struct file * filp);
        void (*close)(struct tty_struct * tty, struct file * filp);
        void (*shutdown)(struct tty_struct *tty);
        void (*cleanup)(struct tty_struct *tty);
        int  (*write)(struct tty_struct * tty,
                      const unsigned char *buf, int count);
        int  (*put_char)(struct tty_struct *tty, unsigned char ch);
        void (*flush_chars)(struct tty_struct *tty);
        int  (*write_room)(struct tty_struct *tty);
        int  (*chars_in_buffer)(struct tty_struct *tty);
        int  (*ioctl)(struct tty_struct *tty,
                    unsigned int cmd, unsigned long arg);
        long (*compat_ioctl)(struct tty_struct *tty,
                             unsigned int cmd, unsigned long arg);
        void (*set_termios)(struct tty_struct *tty, struct ktermios * old);
        void (*throttle)(struct tty_struct * tty);
        void (*unthrottle)(struct tty_struct * tty);
        void (*stop)(struct tty_struct *tty);
        void (*start)(struct tty_struct *tty);
        void (*hangup)(struct tty_struct *tty);
        int (*break_ctl)(struct tty_struct *tty, int state);
        void (*flush_buffer)(struct tty_struct *tty);
        void (*set_ldisc)(struct tty_struct *tty);
        void (*wait_until_sent)(struct tty_struct *tty, int timeout);
        void (*send_xchar)(struct tty_struct *tty, char ch);
        int (*tiocmget)(struct tty_struct *tty);
        int (*tiocmset)(struct tty_struct *tty,
                        unsigned int set, unsigned int clear);

        int (*resize)(struct tty_struct *tty, struct winsize *ws);
        int (*set_termiox)(struct tty_struct *tty, struct termiox *tnew);
        int (*get_icount)(struct tty_struct *tty,
                                struct serial_icounter_struct *icount);
#ifdef CONFIG_CONSOLE_POLL
        int (*poll_init)(struct tty_driver *driver, int line, char *options);
        int (*poll_get_char)(struct tty_driver *driver, int line);
        void (*poll_put_char)(struct tty_driver *driver, int line, char ch);
#endif
        const struct file_operations *proc_fops;
};

这也太复杂了吧?确实如此,好在终端设备正在慢慢的退出历史舞台,看这篇文章的同学们,可能这一辈子都不会直接去写一个TTY driver。所以,这里我也不打算详细介绍,仅仅出于理解TTY framework的目的,做如下说明:

这些操作函数的操作对象,基本上都是struct tty_struct类型的指针,这也印证了我们在2.3中所说的----TTY struct是TTY设备的操作句柄;

当然,具体的TTY driver,可以从struct tty_struct指针中获取足够多的有关该TTY设备的信息,例如TTY port等;

TTY core会通过“.write“接口,将输出信息送给终端设备并显示。因此具体的TTY driver需要实现该接口,并通过硬件操作将数据送出;

你一定会好奇:既然有“.write“接口,为什么没有相应的read接口?TTY设备上的输入信息,怎么经由TTY core送给Application呢?具体可以参考"TTY驱动分析[2]”,本文不再详细介绍了。

3. 提供的用于编写TTY driver的API

在提供了一系列的数据结构的同时,TTY framework向下封装了一些API,以方便TTY driver的开发,具体如下。

3.1 TTY driver有关的API

用于struct tty_driver数据结构的分配、初始化、注册等:

/* include/linux/tty_driver.h */

extern struct tty_driver *__tty_alloc_driver(unsigned int lines,
                struct module *owner, unsigned long flags);
extern void put_tty_driver(struct tty_driver *driver);
extern void tty_set_operations(struct tty_driver *driver,
                        const struct tty_operations *op);
extern struct tty_driver *tty_find_polling_driver(char *name, int *line);

extern void tty_driver_kref_put(struct tty_driver *driver);

/* Use TTY_DRIVER_* flags below */
#define tty_alloc_driver(lines, flags) \
                __tty_alloc_driver(lines, THIS_MODULE, flags)

/* include/linux/tty.h */

extern int tty_register_driver(struct tty_driver *driver);
extern int tty_unregister_driver(struct tty_driver *driver);

tty_alloc_driver,分配一个struct tty_driver指针,并初始化那些不需要driver关心的字段:

lines,指明该driver最多能支持多少个设备,TTY core会根据该参数,分配相应个数的ttys、ports、termios数组;

flags,请参考2.6小节的说明。

tty_set_operations,设置TTY操作函数集。

tty_register_driver,将TTY driver注册给TTY core。

3.2 TTY device有关的API

如果TTY driver设置了TTY_DRIVER_DYNAMIC_DEV flag,就需要自行注册TTY device,相应的API包括:

/* include/linux/tty.h */

extern struct device *tty_register_device(struct tty_driver *driver,
                                          unsigned index, struct device *dev);
extern struct device *tty_register_device_attr(struct tty_driver *driver,
                                unsigned index, struct device *device,
                                void *drvdata,
                                const struct attribute_group **attr_grp);
extern void tty_unregister_device(struct tty_driver *driver, unsigned index);

tty_register_device,分配并注册一个TTY device,最后将新分配的设备指针返回给调用者:

driver,对应的TTY driver;

index,该TTY设备的编号,它会决定该设备在字符设备中的设备号,以及相应的设备名称,例如/dev/ttyS0中的‘0’;

dev,可选的父设备指针。

tty_register_device_attr,和tty_register_device类似,只不过可以额外指定设备的attribute。

3.3 数据传输有关的API

当TTY core有数据需要发送给TTY设备时,会调用TTY driver提供的.write或者.put_char回调函数,TTY driver在这些回调函数中操作硬件即可。

当TTY driver从TTY设备收到数据并需要转交给TTY core的时候,需要调用TTY buffer有关的接口,将数据保存在缓冲区中,并等待Application读取,相关的API有:

/* include/linux/tty_flip.h */

static inline int tty_insert_flip_char(struct tty_port *port,
                                        unsigned char ch, char flag)
{
        ….
}

static inline int tty_insert_flip_string(struct tty_port *port,
                const unsigned char *chars, size_t size)
{
        return tty_insert_flip_string_fixed_flag(port, chars, TTY_NORMAL, size);
}

注5:本文没有涉及TTY buffer、TTY flip有关的知识,大家知道有这么回事就行了。

4. TTY driver的编写步骤

说实话,TTY driver的编写步骤,就和“把大象放进冰箱”一样“简单”:

步骤1:实现TTY设备有关的操作函数集,并保存在一个struct tty_operations变量中。

步骤2:调用tty_alloc_driver分配一个TTY driver,并根据实际情况,设置driver中的字段(包括步骤1中的struct tty_operations变量)。

步骤3:调用tty_register_driver将driver注册到kernel。

步骤4:如果需要动态注册TTY设备,在合适的时机,调用tty_register_device或者tty_register_device_attr,向kernel注册TTY设备。

步骤5:接收到数据时,调用tty_insert_flip_string或者tty_insert_flip_char将数据交给TTY core;TTY core需要发送数据时,会调用driver提供的回调函数,在那里面访问硬件送出数据即可。

好吧,相信任何人看到上面的步骤之后,都没办法去写一个完整的TTY driver(我也是)。好在这是一个幸福的时代,我们很少需要直接去写这样的一个driver。那么本文的目的又是什么呢?权当是一个科普吧,如果你真的打算去写一个TTY driver,不至于无从下手。

5. 参考文档

[1] Linux TTY framework(2)_软件架构

[2] TTY驱动分析

[3] TTY各数据结构关系图,http://www.wowotech.net/content/uploadfile/201505/eb451430746679.gif

[4] POSIX terminal interface,https://en.wikipedia.org/wiki/POSIX_terminal_interface#CITEREFZlotnick1991

[5] Linux设备模型(4)_sysfs

 

原创文章,转发请注明出处。蜗窝科技www.wowotech.net

进程管理和终端驱动:基本概念

$
0
0

一、前言

对于任何一种OS,终端部分的内容总是令人非常的痛苦和沮丧,GNU/linux也是如此。究其原因主要有两个,一是终端驱动和终端相关的系统软件承载了太多的内容:各种虚拟终端、 伪终端、串口通信、modem、printer等。其次可能是终端和信号处理、进程关系等耦合在一起加大了理解终端驱动的难度。本文的目标是希望能够理清这些内容。

在第二章,本文会简单介绍终端的一些基础知识,这些知识在wowo的终端基本概念文档中也有描述,我这里也重复一次,加深印象(呵呵,其实这一章的内容一年前就写了,后来暂停了,这次重新组织一下,发表出来)。随后的第三章主要描述的是和进程管理相关的TTY(终端)的一些基础知识,希望能够帮助广大人民群众理解进程和终端驱动之间的交互关系。主要的概念包括:session、前台进程组、controlling terminal、Job control等。最后一章是结合实际的应用场景来描述进程管理和终端驱动相关的概念。

 

二、终端基础知识

本章分成两个部分,第一节描述了终端的概念,其他小节则介绍了形形色色的终端驱动。

1、什么是TTY,什么是终端?

TTY是TeleTYpe的缩写,直译成中文就是电传打字机。我们都熟悉telephone,phone本身是听筒或者那些发声或使用声音的工具,tele的含义则是指距离较远,需要通过电信号传递。同样的概念可以应用到teletype:type是指打字或者是印字的机构,tele则说明打字和印字机构相距比较远,需要通过电线传递信号。要理解Teletype这个概念需要回去过去的通信时代,最初发明的是电报,把message编成摩斯码,通过操作员将长长短短的tone音发送出去,在接收端,也会设立一个操作员,收听长长短短的tone音,还原message。这种通讯方式对人的要求很高,需要专门训练才能上岗,这严重阻碍了广大人民群众对交换信息的需求,因此teletype这样的机器被发明出来,它包括键盘模组、收发信息模组和字符打印模组(有些模组是机械的,因此,用现代语言,teletype也是一种机械电子设备)。发message时,需要不断的按下字符键,这些字符会被自动发送到电线上。收message的时候,接收器能自动接收来对端的字符信号,并打印出相应的字符。

计算机系统发明是比较靠后的事情了,肯定是比teletype晚。计算机系统需要输入和输出设备,而teletype包括了键盘和printer,可以作为计算机系统的输入和输出设备,因此,早期的计算机使用teletype作为终端设备(有些teletype设备也支持穿孔纸带作为输入或者用穿孔的纸带作为输出)。随着计算机工业的发展,teletype设备中的印字机构或者穿孔机构模块被显示器取代。其实,当teletype被应用到计算机系统中的时候,tty(teletype)这个术语已经不适用了(这时候tty没有传统的通信功能了,更多的是一个计算机系统的输入输出设备,也就是computer terminal),所以现在把tty称为终端会比较合适。

时代总是向前的,不论你愿不愿意。实际上目前的年轻的工程师都不太可能真正使用到终端设备,因为它被淘汰了。我在大学期间的计算机课程都是使用一台DEC VAX的大型机(mainframe computer),当然,我不是独占,这台大型机接出了30多台终端,每个同学拥有一个终端。而终端设备会从每个同学那里接收键盘输入,并且将这些输入通过串行通信的手段发送给VAX大型机,VAX大型机会处理每个用户的键盘输入和命令,然后输出返回并显示在每个终端的屏幕上。终端基本上都是text terminal,因此,主机和终端之间的串行通信都是基于字符的,终端收到字符后会显示在屏幕上。当进入微机(microcomputer)时代,每台主机都有自己的显示设备和键盘设备(多谢大规模集成电路的发展),图形显示器显示的都是点阵信息而不是基于字符的显示,LCD和键盘也不是通过慢速的串行通信设备和主CPU系统相连,从这个角度看,目前我们接触到的终端都不是真正的终端设备了,应该称之”终端仿真设备“。例如:我们可以通过windows系统中的一个窗口加上键盘设备组成一个终端,登录到本机或者远程主机。

2、什么是虚拟终端(Virtual Terminal)

虽然传统的终端设备随着时代的进步而消失了,但是我们仍然可以通过个人PC上的图形显示系统(SDRAM中的frame buffer加上LCD controller再加上display panel)和键盘来模拟传统的字符终端设备(所以称为虚拟终端设备)。整个系统的概念如下图所示:

由于已经不存在物理的终端设备了,因此Vitual Terminal不可能直接和硬件打交道,它要操作Display和Keyboard的硬件必须要通过Framebuffer的驱动和Keyboard driver。

虚拟终端不是实际的物理终端设备,之所以称为Virtual是因为可以在一个物理图形终端(键盘加上显示器)上同时运行若干个虚拟终端。虚拟终端也称为虚拟控制台(Virtual console)。对于linux系统而言,内核支持64个虚拟终端。用户可以通过Alt+Fx进行切换虚拟控制台。目前,GNU/linux操作系统在启动的时候支持6个虚拟终端,用户用Alt+F1 ~ F6进行切换,Alt+F7将切换到图形界面环境。一旦进入了图形桌面环境,可以通过Ctrl+Alt+Fx切回到字符界面的虚拟终端。关于图形界面下的终端相关的软件架构图我们会在后面的章节描述。

虚拟终端的主设备号是4,字符设备,前64个设备是虚拟控制台设备,具体的次设备号分配如下:

4 char    TTY devices

         0 = /dev/tty0        Current virtual console
          1 = /dev/tty1        First virtual console
            ...
         63 = /dev/tty63          63rd virtual console

虽然内核支持了64个Virtual terminal设备,但是具体整个系统是否支持还是要看配置。一般而言,GNU/linux操作系统在启动的时候支持6个虚拟终端(使用tty1~tty6)。次设备号等于0的tty设备比较特殊,代表当前的虚拟控制台。如果不启动GUI系统,那么缺省tty0指向tty1,之后可以根据用户的动作进行切换,但是无论如何,tty0指向了当前的Virtual terminal。

3、什么是控制台(Console)

对于linux系统而言,控制台(console)或者系统控制台(system console)也是一种终端设备,但是又有其特殊性,如下:

(1)可以支持single user mode进行登录

(2)接收来自内核的日志信息和告警信息

在linux系统中,我们一般将系统控制台设备设定为其中一个虚拟终端(一般是当前的virtual terminal,也就是/dev/tty0),这样,我们可以在某一个virtual terminal上看到系统的message。在嵌入式的环境中,我们一般会将系统控制台设备设定为串口终端或者usb串口终端。

可以通过kernel启动的command line来设定系统控制台,例如:

root=/dev/mtdblock0 console=ttyS2,115200 mem=64M

通过这样的设置可以选择串口设备做为系统控制台。

4、什么是伪终端(pseudo terminal或者PTY)

在linux中,伪终端是在无法通过常规终端设备(显示器和keyboard)登录系统的情况下(例如本主机的显示器和keyboard被GUI程序控制,疑惑本主机根本没有显示器和keyboard设备,只能通过网络登录),提供一种模拟终端操作的机制,它包括master和slave两部分。Slave device的行为类似物理终端设备,master设备被进程用来从slave device读出数据或者向slave device写入数据。通过这样的方法模拟了一个终端。那么为什么要有这样的伪终端设备,又为什么要有主设备和从设备配对呢?下面的图片可以给出解释:

当然,大部分的用户在使用GNU/Linux系统的时候都是使用图形用户界面(GUI)系统,在这样的系统里,整个显示屏和键盘(也就是传统的虚拟终端设备)都在一个视窗管理进程的控制之下。从使用层面看,用户一般都是启动一个终端窗口程序来模拟命令行终端的场景。用户可以启动多个这样的窗口,每个这样的窗口都是一个进程,接收用户输入,期望和shell通过终端设备进行沟通。但是这时候,所有的窗口进程其实是无法打开并使用display+key board终端设备的(GUI管理进程控制了实际的物理终端资源),怎么办呢?这就要使用伪终端设备了。 每一对伪终端设备连接着显示器上的一个终端窗口进程和一个shell进程。当视窗管理进程从键盘接收到一个字符时,该字符会被送到获得当前焦点的那个终端窗口进程,而终端窗口进程会将改字符送往与之对应的伪终端"主设备"。因此,对于键盘输入,视窗管理进程起着中转、分发的作用。而写入伪终端"主设备"一端的字符马上就到达了其"从设备"一端,在那里,对于与"从设备"关联的shell进程来说,其动作就跟从普通终端设备读入字符一模一样了。另外一个方向的通信也是类似的,这里就不再赘述了。

当然,和shell进程通过伪终端对进行连接的不一定非得是终端窗口进程,也可能是其他的进程,例如网络服务进程,这时候就是网络终端登录的场景了,其结构图也类似,大家可以自行补脑。

5、串口终端

串口终端好象没有什么好说的,大家都熟悉的很,这里就顺便说一下串口终端的设备节点吧,如下:


主设备号是4的字符设备是TTY devices,虚拟终端占据了0~63的minor设备号,之后的minor是串口设备使用
         64 = /dev/ttyS0         First UART serial port
            ...
        255 = /dev/ttyS191    192nd UART serial port

USB串口终端的主设备号是188的字符设备,这是主机侧的USB serial converters devices,具体如下:
          0 = /dev/ttyUSB0    First USB serial converter
          1 = /dev/ttyUSB1    Second USB serial converter
            ...
主设备号是127的字符设备是gadget侧的USB serial devices,具体如下:
0 = /dev/ttyGS0      First USB serial converter
          1 = /dev/ttyGS1     Second USB serial converter
            ...

 

三、进程管理中和终端相关的概念

1、什么是shell,什么是登录系统?

如果OS kernel是乌龟的身体,,那么shell就是保护乌龟身体的外壳。shell提供访问OS kernel服务的用户接口,用户通过shell可以控制整个系统的运行(例如文件移动、启动进程,停止进程等)。目前的shell主要有两种,一种就是大家熟悉的桌面环境,另外一种就是基于文本command line类型的,主要for业内人事使用。command line类型的shell虽然需要用户记住很多复杂的命令和脚本格式等等,但是其效率非常高,速度非常快。GUI类型的shell操作简单,用户容易上手,但效率为人所诟病。

上面说过了,shell是人类访问、控制计算机服务的接口,不过这个接口有些特殊,shell自己有自己的要求:用户不能通过任意的设备和shell交互,必须是一个终端设备。我们现在网络设备比较发达,可以通过网络设备直接和shell通信吗?不行,网络设备不是终端设备,想要通过网络设备访问shell,必须通过伪终端(后面会讲)。shell和人类交互的示意图如下:

对于系统工程师,我们更关注command line类型的shell。在系统的启动过程中,init进程会根据/etc/inittab中的设定进行系统初始化(这里还是说的传统的系统启动过程,如果使用systemd那么启动过程将会不一样,但是概念类似),和tty相关的内容包括:

1:2345:respawn:/sbin/getty 38400 tty1
2:23:respawn:/sbin/getty 38400 tty2
3:23:respawn:/sbin/getty 38400 tty3
4:23:respawn:/sbin/getty 38400 tty4
5:23:respawn:/sbin/getty 38400 tty5
6:23:respawn:/sbin/getty 38400 tty6

init进程会创建6个getty进程,这六个getty进程会分别监听在tty1~tty6这六个虚拟终端上,等待用户登录。用户登录之后会根据用户的配置文件(/etc/passwd)启动指定shell程序(例如/bin/bash)。因此,shell其实就是一个命令解析器,运行在拥有计算资源的一侧,通过tty驱动和对端tty设备(可能是物理的终端设备,也可能是模拟的)进行交互。

2、什么是Job control

和终端一样,Job control在现代操作系统中的需求已经不是那么明显,以至于很多工程师都不知道它的存在,不理解Job control相关的概念。在过去,计算机还是比较稀有的年代,每个工程师不可能拥有自己的计算机,工程师都是通过字符终端来登录计算机系统(当然,那个年代没有GUI系统),来分享共同的计算资源。在自己的终端界面上,任务一个个的串行执行,有没有可能让多个任务一起执行呢?(例如后台运行科学运算相关的程序,前台执行了小游戏放松一下)这就是job control的概念了。Job control功能其实就是在一个terminal上可以支持多个job(也就是进程组,后面会介绍)的控制(启动,结束,前后台转换等)。当然,在引入虚拟终端,特别是在GUI系统流行之后,Job control的需求已经弱化了,工程师在有多个任务需求的时候,可以多开一些虚拟终端,或者直接多开几个Termianl窗口程序就OK了,但是,Job control是POSIX标准规定的一个feature,因此,各种操作系统仍然愿意服从POSIX标准,这也使得job control这样的“历史功能”仍然存在现代的操作系统中。

如果不支持Job control,那么登录之后,可以通过终端设备和shell进行交互,执行Job A之后,用户可以通过终端和该进程组(也就是Job A)进行交互,当执行完毕之后,终端的控制权又返回给shell,然后通过用户在终端的输入,可以顺序执行Job B、Job C……在引入Job control概念之后,用户可以并行执行各种任务,也就是说Job A、Job B、Job C……都可以并行执行,当然前台任务(Job)只能有一个,所有其他的任务都是在后台运行,用户和该前台任何进行交互。

通过上面的描述,我们已经了解到了,在用户的操作下,多个Job可以并行执行,但是终端只有一个,而实际上每一个运行中的Job都是渴望和user进行交互,而要达成这个目标,其实核心要解决的问题就是终端的使用问题:

(1)终端的输入要递交给谁?

(2)各个进程是否能够向终端输出?

对于第一个问题,答案比较简单,用户在终端的输入当然是递交给前台Job了,但是那些后台任务需要终端输入的时候怎么办呢?这时候就需要任务控制(Job control)了,这里会涉及下面若干的动作:

(1)后台Job(后者说是后台进程组)对终端进行读访问的时候,终端驱动会发送一个SIGTTIN的信号给相应的后台进程组。

(2)该后台进程组的所有的进程收到SIGTTIN的信号会进入stop状态

(3)做为父进程的shell程序可以捕获该后台进程组的状态变化(wait或者waitpid),知道它已经进入stopped状态

(4)用户可以通过shell命令fg讲该后台进程组转入前台,从而使得它能够通过终端和用户进行交互。这时候,原来的前台任务则转入后台执行。

对于输入,我们严格限定了一个进程组做为接收者,但是对于输出的要求则没有那么严格,你可以有两种选择:

(1)前台任务和后台任务都可以向终端输出,当然,大家都输出,我估计这时候输出屏幕有些混乱,呵呵~~~

(2)前台任务可以输出,但是后台任务不可以。如果发生了后台任务对终端的写访问动作,终端驱动将发送SIGTOUT信号给相应的后台进程组。

除此之外,为了支持job control,终端驱动还需要支持信号相关的特殊字符,包括:

a) Suspend key(缺省control-Z),对应SIGTSTP信号

b) Quit key(缺省control-\),对应SIGQUIT信号

c) Interrupt key(缺省control-C),对应SIGINT信号,

当终端驱动收到用户输入的这些特殊字符的时候,会转换成相应的信号,发送给session中(后面会介绍该术语)的前台进程组,当然,前提是该终端是session的控制终端。因此,为了让那么多的进程组(Job)合理的、有序的使用终端,我们需要软件模块协同工作,具体包括:

(1)支持Job control的shell

(2)终端驱动需要支持Job control

(3)内核必须支持Job control的信号

3、什么是进程组?

简单的说,进程组就是一组进程的集合,当然,我们不会无缘无故的把他们组合起来,一定是有共同的特性,一方面,这些进程属于同一个Job,来自终端的信号可以送达这一进程组的每一个成员(是为了job control)。此外,我们可以通过killpg接口向一个进程组发送信号。任何一个进程都不是独立存在的,一定是属于某个进程组的,当创建的时候,该进程归入创建者进程所属的进程组。比如说shell程序属于进程组A,当用户输入aaa程序启动一个新进程的时候(是shell创建了该进程,没有&符号,是前台进程),aaa进程则归属与进程组A(和shell程序属于同一个进程组)。如果用户输入aaa &,启动一个后台进程的时候,shell会创建一个新的进程aaa,同时创建一个新的进程组,aaa会加入该进程组,成为该后台进程组的唯一一个进程。

通过上面的描述,很多工程师可能认为Shell可以同时运行一个前台进程和任意多个后台进程,其实不然,shell其实是以进程组(也就是Job了)来控制前台和后台的。我们给一个具体的例子:用户通过终端输入的一组命令行,命令之间通过管道连接,这些命令将会形成一个进程组,例如下面的命令:

$ proc1 | proc2 &
$ proc3 | proc4 | proc5

对于第一行命令,shell创建了一个新的后台进程组,同时会创建两个进程proc1和proc2,并把这两个进程放入那个新创建的后台进程组(不能访问终端)。执行第二行命令的时候,shell创建了proc3、proc4和proc5三个进程,并把这三个进程加入shell新创建的前台进程组(可以访问终端)。

如何标识进程组呢?这里借用了进程ID来标识进程组。对于任何一个进程组,总有一个最开始的加入者,第一个加入者其实就是该进程组的创始者,我们称之为该进程组的Leader进程,也就是进程ID等于进程组ID的那个进程,或者说,我们用process leader的进程ID来做为process group的ID。当然,随着程序的执行,可能会有进程加入该进程组,也可能程序执行完毕,退出该进程组,对于进程组而言,即便是process group leader进程退出了,process group仍然可以存在,其生命周期直到进程组中最后一个进程终止, 或加入其他进程组为止。

通过getpgid接口函数,我们可以获取一个进程的进程组ID。通过setpgid接口函数,我们可以加入或者新建一个进程组。当然,进程组也不是任意创建或者加入的,一个进程只能控制自己或者它的子进程的进程组。而一旦子进程执行了exec函数之后,其父进程也无法通过setpgid控制该子进程的进程组。还需要注意的是:调用setpgid的进程、设定process group的进程和指定的process group必须在一个sesssion中。最后需要说的一点是:根据POSIX标准,我们不能修改session leader的进程组ID。

4、什么是Session?

从字面上看,session其实就是用户和计算机之间的一次对话,通俗的概念是这样的:你想使用计算机资源,当然不能随随便便的使用,需要召开一个会议,参加的会议双方分别是用户和计算机,用户把自己的想法需求告诉计算机,而计算机接收了用户的输入并把结果返回给用户。就这样用户和计算机之间一来一去,不断进行交互,直到会议结束。用户和计算机如何交互呢?用户是通过终端设备和计算机交互,而代表计算机和用户交互则是shell进程。每次当发生这种用户和计算机交互过程的时候,操作都会创建一个session,用来管理本次用户和计算机之间的交互。

如果支持job control,那么用户和计算机之间的session可能并行执行多个Job,而Job其实就是进程组的抽象,因此,session其实就是进程组的容器,其中容纳了一个前台进程组(只能有一个)和若干个后台进程组(当然,没有连接的控制终端的情况下,session也可能没有前台进程组,由若干后台进程组组成)。创建session的场景有两个:

(1)一次登录会形成一个session(我们称之login session,大部分的场景都是描述login session的)。

(2)系统的daemon进程会在各自的session中(我们称之daemon session)。

无论哪一个场景,都是通过setsid函数建立一个新的session(注意:调用该函数的进程不能是进程组的leader),由于创建了session,该进程也就成为这个新创建session的leader。之后,session leader创建的子进程,以及进程组也都属于该session。为何process group leader不能调用setsid来创建session呢?我们假设没有这个限制,process group leader(ID等于A)通过setsid把把自己加入另外一个新建的session,有了session就一定要有进程组,创建了sesssion的那个process group leader就成了该session中的第一个进程组leader,标识这个新建的进程组的ID就是A。而其他进程组中的成员仍然在旧的session中,在旧的session中仍然存在A进程组,这样一个进程组A的成员,部分属于新的session,部分属于旧的session,这是不符合session-process group2级拓扑结构的。我了满足这个要求,我们一般会先fork,然后让父进程退出,子进程执行setsid,fork之后,子进程不可能是进程组leader,因此满足上面的条件。

因此,setsid创建了新的session,同时也创建了新的process group,创建session的那个进程ID被用来标识该process group。新创建的sesssion没有控制终端,如果调用setsid的进程有控制终端,那么调用setsid之后,新的session和那个控制终端失去连接关系。如何标识session呢?我们往往使用session leader的process ID来标识。

5、控制终端(controlling terminal)

通过上面的描述,我们已经知道了:为了进行job control,我们把若干的进程组放入到了session这个容器进行管控。但是,用户如何来管控呢?必须要建立一个连接的管道,我们把和session关联的那个终端称为控制终端,把建立与控制终端连接的session首进程(session Leader)叫做控制进程(controlling process)。session可以有一个控制终端,不能有多个,当然也可以没有。而一个终端也只能和一个session对应,不能和多个session连接。

对于login session,我们登录的那个终端设备基本上就是该session的controlling terminal,而shell程序就是该session的leader,也是该session的controlling process。之所以会有controlling process的概念主要用来在终端设备断开的时候(例如网络登录的场景下,网线被拔出),终端驱动会把hangup signal送到对应session的controlling process。

session可以有一个前台进程组和若干个后台进程组(很好理解,占有控制终端的就是前台,没有的就是后台。当然可以一个前台进程组都没有,例如daemon session)。对于前台进程组,共同占有控制终端。在控制终端键入ctrl+c产生终止信号(或者其他可以产生信号的特殊字符组合)会被递交给前台进程组所有的进程(不会递交给后台进程)。虽然终端被前台进程组掌管,但是通过shell、内核和终端驱动的交互,后台进程组可以被推入到前台,也就是说:所有的session内的进程组可以分时复用该controlling terminal。

当创建一个session的时候,往往没有controlling terminal,当session leader open了一个终端设备,除非在open的时候指明O_NOCTTY,否则该terminal就会称为该session的controlling terminal,当然,该终端也不能是其他session的controlling terminal,否则就会有一个控制终端对应两个session的状况发生了。一旦拥有了控制终端之后,session leader的子进程都会继承这个controlling terminal。除了上面说的隐含式的设定,程序也可以通过ioctl来显示的配置(TIOCSCTTY)controlling terminal,或者通过TIOCNOTTY来解除该终端和session的联系,变成一个普通的终端。

对于login session,session leader会建立和终端的连接,同时把标准输入、输出和错误定向到该终端。因此,对于后续使用shell运行的普通程序而言,我们不需要直接访问控制终端,一般是直接访问标准输入、输出和错误。如果的确是有需要(例如程序的标准输入、输出被重定向了),那么可以通过打开/dev/tty设备节点(major=5,minor=0)来访问控制终端,/dev/tty就是当前进程的控制终端。

 

四、应用

1、系统初始化

在系统启动的时候,swapper进程(或者称之idle进程)的信息整理如下:

PID PGID SID TTY
0 0 0 NO

swapper进程在启动过程中会创建非常多的内核线程,这些内核线程的job control相关的信息如下:

PID PGID SID TTY
x 0 0 NO

由此可见,所有内核线程都是在一个session中,属于一个进程组,随着内核线程的不断创建,其process ID从2开始,不断递增。Process ID等于1的那个进程保留给了init进程,这也是内核空间转去用户空间的接口,刚开始,init进程继承了swapper进程的sid和pgid,不过,init进程会在启动过程中调用setsid,从而创建新的session和process group,基本信息如下:

PID PGID SID TTY
1 1 1 NO

 

2、虚拟终端登录

对于linux而言,/etc/inittab包括了登录信息,也就是说,init进程需要在哪些终端设备上执行fork动作,并执行(exec)getty程序,因此getty拥有自己的Process ID,并且是一个普通进程,不是session leader,也不是process group leader,在getty程序中会调用setsid,创建新的session和process group,同时,该程序会打开做为参数传递给它的终端设备(对应这个场景应该是ttyx),因此ttyx这个虚拟终端就成了该session的controlling terminal,gettty进程也就是controlling process。一旦正确的打开了终端设备,文件描述符0、1、2都定向到该终端设备。完成这些动作之后,通过标准输出向用户提示登录信息(例如login:)。在用户输入用户名之后,getty进程已经完成其历史使命,它会调用exec加载login程序(PID、PGID、SID都没有变化)。

login进程最重要的任务当然是鉴权了,如果鉴权失败,那么login进程退出执行,而其父进程(也就是init进程)会侦听该事件并重复执行getty。如果鉴权成功,那么login进程会fork子进程并执行shell。刚开始,login进程和shell进程属于一个session,同时也属于同一个前台进程组,共享同一个虚拟终端,也就是说两个进程的controlling terminal都是指向该登录使用的那个虚拟终端。shell进程并非池中之物,最终还是要和login进程分道扬镳的。shell进程会执行setpgid来创建一个新的进程组,然后调用tcsetpgrp将shell所在的进程组设定为前台进程组。

注:上面的描述是基于Debian 8系统描述的。

2、shell执行命令

在虚拟终端登陆后,我们可以执行下面的命令:

#ps –eo stat,comm,sid,pid,pgid,tty | grep tty | more

shell执行这一条命令的动作示意图如下:

在/dev/tty1完成登录之后,系统存在两个进程组,前台进程组是shell,后台进程组是login,两个进程组都属于一个session,所有进程的控制终端都是虚拟终端tty1。执行上述命令之后,shell创建了3个进程ps、grep和more,并将这三个进程放到一个新创建的进程组中(ps是进程组leader),同时把该进程组推向前台,shell自己隐居幕后。一旦程序执行完毕,后台的shell收到子进程的信号后,又把自己推到前台来,等待用户输入的下一条命令。

我们假设用户输入下面的命令:

#ps –eo stat,comm,sid,pid,pgid,tty | grep tty | more &

&符号其实就是后台执行的命令,shell执行这条命令的过程和上图类似,不过这时候并不把新建立的进程组推到前台,shell自己仍然是前台进程组。由于有pipe,输出信息沿着ps—>grep--->more的路径来到了more进程,more进程输出到标准输出,也就是tty1这个虚拟终端的时候,悲剧发生了,后台进程组不能write控制终端,从而引发了SIGTOUT被发送到该后台进程组的每一个进程组,因此,ps,grep和more都进入stop状态。这时候只要在shell执行fg的操作,就可以把ps那个后台进程组推到前台,这时候虚拟终端的屏幕才会打印出相关的进程信息。

3、GUI系统

TODO (对GUI系统不熟悉,只能暂时TODO了,哈哈)

 

五、参考文献

1、Unix高级环境编程

2、POSIX标准

3、The Linux Programming Interface - A Linux and UNIX System Programming Handbook

 

原创文章,转发请注明出处。蜗窝科技

Linux TTY framework(5)_System console driver

$
0
0

1. 前言

由[1]中的介绍可知,Linux kernel的console框架,主要提供“控制台终端”的功能,用于:

1)kernel日志信息(printk)的输出。

2)实现基础的、基于控制台的人机交互。

本文将从console driver开发者的视角,介绍:console有关的机制;编写一个console驱动需要哪些步骤;从用户的角度怎么使用;等等。

2. 设计思路介绍

不知道大家是否有这样的疑问:既然已经有了TTY框架,为什么要多出来一个console框架,为什么不能直接使用TTY driver的接口实现console功能?

确实,由[2]中的介绍可知,TTY框架的核心功能,就是管理TTY设备,并提供访问TTY设备的API(如数据收发)。而console的两个功能需求,“日志输出”就是向TTY设备发送数据,“控制台人机交互”就是标准的TTY功能。因此从功能上看,完全可以直接使用TTY框架的API啊。

不过,既然存在,一定有其意义。内核之所以要抽象出console框架,思路如下:

1)Linux kernel有一个很强烈的隐性规则----内核空间的代码不应该直接利用用户空间接口访问某些资源,例如kernel代码不应该直接使用文件系统接口访问文件(虽然它可以)。回到本文的场景里面,TTY框架通过字符设备(也即文件系统)向用户空间提供接口,那么kernel的代码(如printk),就不能直接使用TTY的接口访问TTY设备,怎么办呢?开一个口子,从kernel里面再拉出一套接口,这就是console框架,如下图所示:

tty_console

2)console框架构建在TTY框架之上,大部分的实现(特别是访问硬件的部分)都和TTY框架复用。

3)系统中可以有多个TTY设备,只有那些附加了console驱动的设备,才有机会成为kernel日志输出的目的地,有机会成为控制台终端。因此,console框架变相的成为管理TTY设备的一个框架。

4)驱动工程师在为某个TTY设备编写TTY driver的时候,会根据实际的需求,评估该TTY设备是否可能成为控制台设备,如果可能,则同时为其编写system console driver,使其成为候选的控制台设备。系统工程师在系统启动的时候,可以通过kernel命令行参数,决定printk会在哪些候选设备上输出,那个候选设备最终会成为控制台设备。示意图如下:

console_overview

3. 核心数据结构

理解了console框架的设计思路之后,再来看它的实现,就很简单了。其核心数据结构为struct console,如下:

/* include/linux/console.h */

struct console {
        char    name[16];
        void    (*write)(struct console *, const char *, unsigned);
        int     (*read)(struct console *, char *, unsigned);
        struct tty_driver *(*device)(struct console *, int *);
        void    (*unblank)(void);
        int     (*setup)(struct console *, char *);
        int     (*match)(struct console *, char *name, int idx, char *options);
        short   flags;
        short   index;
        int     cflag;
        void    *data;
        struct   console *next;
};

该数据结构中经常被使用的字段有:

name,该console的名称,配合index字段,可用来和命令行中的“console=xxx”中的“xxx”匹配,例如:

如果name为“ttyXS”,index为大于等于0的数字(例如2),则可以和“console=ttyXS2”匹配;

如果name为“ttyXS”,index为小于0的数字(例如-1),则可以和“console=ttySXn“(n=0,1,2…)任意一个匹配。

write,如果某个console被选中作为printk的输出,则kernel printk模块会调用write回调函数,将日志信息输出到。

device,获取该console对应的TTY driver,用于将console和对应的TTY设备绑定,这样控制台终端就可以和console共用同一个TTY设备了。

setup,用于初始化console的回调函数,console driver可以在该回调函数中对硬件做出现动作。可以不实现,如果实现,则必须返回0,否则该console不可用。

flags,指示属性的flags,常用的包括:

CON_BOOT,该console是一个临时console,只在启动的时候使用,kernel会在真正的console注册后,把它注销掉。

CON_CONSDEV,表示该console会被用作控制台终端(和/dev/console对应),对应命令行中的最后一个,例如“console=ttyXS0 console=ttyUSB2”中的ttyUSB2。

CON_PRINTBUFFER,如果设置了该flag,kernel在该console被注册的时候,会将那些被缓存到buffer中的之前的日志,统统输出到该console上。通常注册的console,如串口console,都会设置该flag,以便可以看到console注册前的日志输出。

CON_ENABLED,表示该console正在被使用。

4. 接口说明

4.1 console driver的注册接口

对具体的console driver来说,只需要关心console的注册/注销接口即可:

/* include/linux/console.h */

extern void register_console(struct console *);
extern int unregister_console(struct console *);

在正确填充struct console变量之后,通过register_console接口将其注册到kernel中即可。该接口将会完成如下的事情:

检查该console的name和index,确认之前没有注册过,否则注册失败;

如果该console为boot console(CON_BOOT),确认是第一个注册的boot console,否则注册失败;

如果系统中从来没有注册过console,则将第一个被注册的、可以setup成功(.setup为NULL或者返回0)的console作为正在使用的console,并使能之(CON_ENABLED);

和command line中的“console=xxx”最对比,使能那些在命令行中指定的console;

查找在command line中指定的最后一个console,并置位其CON_CONSDEV flag,表明选择它为控制台console。

4.2 用户层面接口

系统console的使用控制,主要由命令行参数(一般都是bootloader传递而来的)指定,总结如下:

1)如果某个console只需要在启动的时候使用,则需要在注册console的时候,置位其CON_BOOT标志,例如[3]中介绍的early console。

2)如果系统中注册了多个console,可以通过命令行参数指定使用哪个或者哪些,kernel的日志将会输出到所有在命令行指定了的console上面。

3)命令行中指定的最后一个console,将会作为控制台console,应用程序打开/dev/console将会打开该console对应的TTY设备(由.device回调返回的tty driver指定)。

5. console driver的编写步骤

理解了system console的原理之后,编写一个console driver就比较简单了,包括:

1)如果希望该console可以作为系统控制台(/dev/console),则必须先实现该console对应的TTY设备的TTY driver。

2)定义一个console变量,并根据实际情况填充对应的字段,包括name、index、setup(可选)、write、device(可选)等。

3)调用register_console将其注册到kernel中即可。

6. 参考文档

[1] Linux TTY framework(3)_从应用的角度看TTY设备

[2] Linux TTY framework(4)_TTY driver

[3] X-012-KERNEL-serial early console的移植

 

原创文章,转发请注明出处。蜗窝科技www.wowotech.net

X-013-KERNEL-ARM GIC driver的移植

$
0
0

1. 前言

X Project”完成“X-012-KERNEL-serial early console的移植”之后,终止在如下的kernel panic中:

NR_IRQS:64 nr_irqs:64 0
Kernel panic - not syncing: No interrupt controller found.
---[ end Kernel panic - not syncing: No interrupt controller found.

结果很明显,系统中没有注册中断控制器。因此,本文将以“Bubbugum-96”平台为例,介绍ARM GIC驱动的移植步骤,顺便继续加深对device tree的理解和认识。

注1:由于“Bubbugum-96”的GIC符合ARM标准,Linux kernel中相关的驱动是现成的,因此GIC驱动的移植就非常简单了,只要配置一下device tree即可。

2. GIC硬件说明

由[1]中的关键字可知,bubblegum-96所使用的SOC(S900)符合GIC_400规范。根据[3]中GIC driver的分析可知,配置GIC驱动需要知道如下的信息:

Distributor address range
CPU interface address range
Virtual interface control block
Virtual CPU interfaces

不过S900的公开资料却没有这方面的信息,没关系,我们可以从它们开放出来的源代码反推,如下[4]

gic: interrupt-controller@e00f1000 {
      compatible = "arm,cortex-a15-gic", "arm,cortex-a9-gic";
      #interrupt-cells = <3>;
      #address-cells = <0>;
      interrupt-controller;
      reg = <0 0="" 0xe00f1000="" 0x1000="">,
               <0 0="" 0xe00f2000="" 0x1000="">,
               <0 0="" 0xe00f4000="" 0x2000="">,
               <0 0="" 0xe00f6000="" 0x2000="">;
               interrupts = ;
       };

即:

Distributor address range:0xe00f1000 ~ 0xe00f1000 + 0x1000
CPU interface address range:0xe00f2000 ~ 0xe00f1000 + 0x1000
Virtual interface control block:0xe00f4000 ~ 0xe00f4000 + 0x2000
Virtual CPU interfaces:0xe00f6000 ~ 0xe00f6000 + 0x2000

3. GIC driver移植

移植过程很简单,修改dts文件,加入gic的节点即可(可以抄上面的~~):

diff --git a/arch/arm64/boot/dts/actions/s900-bubblegum.dts b/arch/arm64/boot/dts/actions/s900-bubblegum.dts
index fb1351a..a3af064 100644
--- a/arch/arm64/boot/dts/actions/s900-bubblegum.dts
+++ b/arch/arm64/boot/dts/actions/s900-bubblegum.dts
@@ -10,4 +10,16 @@
/ {
        model = "Bubblegum-96 Development Board";
        compatible = "actions,s900-bubblegum", "actions,s900";
+
+       #address-cells = <2>;
+       #size-cells = <2>;

+
+       gic: interrupt-controller@e00f1000 {
+               compatible = "arm,gic-400";
+               #interrupt-cells = <3>;
+               #address-cells = <0>;
+               interrupt-controller;
+               reg = <0 0="" 0xe00f1000="" 0x1000="">,  /* dist_base */
+                     <0 0="" 0xe00f2000="" 0x1000="">;  /* cpu_base */

+       };
};

简单说明如下:

1)#address-cells和#size-cells

目前为止,我们的dts文件已经裸奔了很久了(因为没有任何的节点),不过现在就不行了。因为有了第一个节点----interrupt-controller@e00f1000,该节点下面有“reg”关键字需要解析,解析的依据是什么呢?也就是说:

reg关键字中,地址信息有多长?size信息又有多长呢?

这两个信息需要由#address-cells和#size-cells告诉kernel,这里都设置为2,含义是:

address信息占用两个word的长度,即“0 0xe00f1000”组合成0x00000000e00f1000(Distributor address),“0 0x1000”组合成0x0000000000001000(Distributor length)。

2)compatible名称为“arm,gic-400”,具体可参考“drivers/irqchip/irq-gic.c”中有关的定义。

3)interrupt-controller用于指示该节点是一个中断控制器。

4)#interrupt-cells用于指明该中断控制器怎么编码某一个中断源,这里的3是ARM GIC driver定义的,意义如下:

例如:interrupts = ;

第一个cell用于指示中断的类型,0是SPI,1是PPI;

第二个cell用于指示中断号;

第三个cell用于指示中断的flag,包括: bits[3:0]指示中断的触发类型,1 edge triggered、4 level triggered。

5)reg字段我没有写完全,剩下的后续真正用的时候再说了。

4. 编译验证

根据“X Project”的开发指南,编译并运行新的kernel:

make kernel uImage

make spl-run

make kernel-run

出现如下的打印:

clocksource_probe: no matching clocksources found
Kernel panic - not syncing: Unable to initialise architected timer.

---[ end Kernel panic - not syncing: Unable to initialise architected timer.

panic的对象换了,看来GIC成功了,简单吧?

5. GIC的debug和使用

虽然编译、运行通过了,但能否正确使用呢?我们需要一个测试对象,因此,留到下一篇Generic Timer的移植里面,一起介绍吧。

6. 参考文档

[1] bubblegum-96/SoC_bubblegum96.pdf

[2] bubblegum-96/HardwareManual_Bubblegum96.pdf

[3] http://www.wowotech.net/linux_kenrel/gic_driver.html

[4] https://github.com/96boards-bubblegum/linux/blob/bubblegum96-3.10/arch/arm64/boot/dts/s900.dtsi

 

原创文章,转发请注明出处。蜗窝科技,www.wowotech.net

Linux kernel debug技巧----开启DEBUG选项

$
0
0

kernel的source code中有很多使用pr_debug/dev_dbg输出的日志信息(例如device tree解析的代码,drivers/of/fdt.c)。默认情况下,kernel不会将这些日志输出到控制台上,除非:

1)开启了DEBUG宏,并且

2)kernel printk的默认日志级别大于7

看似简单,不过我相信每个人都问过这样的问题(不管是问自己还是问别人,特别是在调试kernel启动过程的时候,例如device tree的匹配、device probe等):怎么开启DEBUG选项?

之所以有这篇短文,是因为我也问过(不止一次),于是就记录如下:

1)开启了DEBUG宏

其实开启DEBUG宏的方法很简单,在需要pr_debug/dev_dbg输出的模块开头,直接#define DEBUG即可,kernel中有一个例子:

/* init/main.c */

#define DEBUG           /* Enable initcall_debug */

不过这种方法有个缺点:我们必须准确的知道需要debug那个C文件,如果想大网撒鱼(例如,想debug为什么新修改的DTS文件没有起作用,而又对kernel fdt的代码不是很熟悉),就麻烦了。这里我给一个大杀器:在编译kernel的时候,通过KCFLAGS直接传递,这样可以全局生效,如下(以本站的“X Project”为例):

diff --git a/Makefile b/Makefile
index 0835b1c..59625f4 100644
--- a/Makefile
+++ b/Makefile
@@ -83,7 +83,7 @@ kernel-config:
kernel:
        mkdir -p $(KERNEL_OUT_DIR)
        make -C $(KERNEL_DIR) CROSS_COMPILE=$(CROSS_COMPILE) KBUILD_OUTPUT=$(KERNEL_OUT_DIR) ARCH=$(BOARD_ARCH) $(KERNEL_D
-       make -C $(KERNEL_DIR) CROSS_COMPILE=$(CROSS_COMPILE) KBUILD_OUTPUT=$(KERNEL_OUT_DIR) ARCH=$(BOARD_ARCH) $(KERNEL_T
+       make -C $(KERNEL_DIR) CROSS_COMPILE=$(CROSS_COMPILE) KBUILD_OUTPUT=$(KERNEL_OUT_DIR) KCFLAGS=-DDEBUG ARCH=$(BOARD_
 

2)设置kernel printk的默认日志级别为8

修改printk的默认日志级别的方法有多种,例如直接修改printk.c(新kernel为printk.h)中的CONSOLE_LOGLEVEL_DEFAULT宏定义。不过修改kernel原生代码的方式稍显粗暴,我们还有优雅一些的手段,例如通过命令行参数的loglevel变量传递,如下:

diff --git a/arch/arm64/configs/xprj_defconfig b/arch/arm64/configs/xprj_defconfig
index 5d0d591..9335d3f 100644
--- a/arch/arm64/configs/xprj_defconfig
+++ b/arch/arm64/configs/xprj_defconfig
@@ -320,7 +320,7 @@ CONFIG_FORCE_MAX_ZONEORDER=11
#
# Boot options
#
-CONFIG_CMDLINE="earlycon=owl_serial"
+CONFIG_CMDLINE="earlycon=owl_serial loglevel=8"
CONFIG_CMDLINE_FORCE=y
# CONFIG_EFI is not set

3)修改完之后,编译并启动kernel看看效果吧(是不是很爽?)

Starting kernel ...

flushing dcache successfully.
[    0.000000] Booting Linux on physical CPU 0x0
[    0.000000] Linux version 4.6.0-rc5+ (pengo@ubuntu) (gcc version 4.8.3 20131202 (prerelease) (crosstool-NG linaro-1.13.1-4.8-2013.12 - Linaro GCC 2013.11) ) #17 SMP Tue Nov 1 03:52:32 PDT 2016
[    0.000000] Boot CPU: AArch64 Processor [410fd032]
[    0.000000] earlycon: owl_serial0 at I/O port 0x0 (options '')
[    0.000000] bootconsole [owl_serial0] enabled
[    0.000000] On node 0 totalpages: 524288
[    0.000000]   DMA zone: 8192 pages used for memmap
[    0.000000]   DMA zone: 0 pages reserved
[    0.000000]   DMA zone: 524288 pages, LIFO batch:31
[    0.000000]  -> unflatten_device_tree()
[    0.000000] Unflattening device tree:
[    0.000000] magic: d00dfeed
[    0.000000] size: 00001000
[    0.000000] version: 00000011
[    0.000000]   size is cb0, allocating...
[    0.000000]   unflattening ffffffc07ffed1c8...
[    0.000000] fixed up name for  ->
[    0.000000] fixed up name for memory -> memory
[    0.000000] fixed up name for chosen -> chosen
[    0.000000] fixed up name for interrupt-controller@e00f1000 -> interrupt-controller
[    0.000000] fixed up name for timer -> timer
[    0.000000]  <- unflatten_device_tree()
[    0.000000] Failed to find device node for boot cpu
[    0.000000] missing boot CPU MPIDR, not enabling secondaries
[    0.000000] mask of set bits 0x0
[    0.000000] MPIDR hash: aff0[0] aff1[8] aff2[16] aff3[32] mask[0x0] bits[0]
[    0.000000] percpu: Embedded 14 pages/cpu @ffffffc07ffdd000 s28032 r0 d29312 u57344
[    0.000000] pcpu-alloc: s28032 r0 d29312 u57344 alloc=14*4096
[    0.000000] pcpu-alloc: [0] 0
[    0.000000] Detected VIPT I-cache on CPU0
[    0.000000] Built 1 zonelists in Zone order, mobility grouping on.  Total pages: 516096
[    0.000000] Kernel command line: earlycon=owl_serial loglevel=8
[    0.000000] doing Booting kernel, parsing ARGS: 'earlycon=owl_serial loglevel=8'
[    0.000000] doing Booting kernel: earlycon='owl_serial'
[    0.000000] doing Booting kernel: loglevel='8'
[    0.000000] PID hash table entries: 4096 (order: 3, 32768 bytes)
[    0.000000] Dentry cache hash table entries: 262144 (order: 9, 2097152 bytes)
[    0.000000] Inode-cache hash table entries: 131072 (order: 8, 1048576 bytes)
[    0.000000] software IO TLB [mem 0x79cb3000-0x7dcb3000] (64MB) mapped at [ffffffc079cb3000-ffffffc07dcb2fff]
[    0.000000] Memory: 1993604K/2097152K available (1032K kernel code, 78K rwdata, 128K rodata, 120K init, 201K bss, 103548K reserved, 0K cma-reserved)
[    0.000000] Virtual kernel memory layout:
[    0.000000]     modules : 0xffffff8000000000 - 0xffffff8008000000   (   128 MB)
[    0.000000]     vmalloc : 0xffffff8008000000 - 0xffffffbdbfff0000   (   246 GB)
[    0.000000]       .text : 0xffffff8008080000 - 0xffffff8008182000   (  1032 KB)
[    0.000000]     .rodata : 0xffffff8008182000 - 0xffffff80081a3000   (   132 KB)
[    0.000000]       .init : 0xffffff80081a3000 - 0xffffff80081c1000   (   120 KB)
[    0.000000]       .data : 0xffffff80081c1000 - 0xffffff80081d4800   (    78 KB)
[    0.000000]     fixed   : 0xffffffbffe7fd000 - 0xffffffbffec00000   (  4108 KB)
[    0.000000]     PCI I/O : 0xffffffbffee00000 - 0xffffffbfffe00000   (    16 MB)
[    0.000000]     memory  : 0xffffffc000000000 - 0xffffffc080000000   (  2048 MB)
[    0.000000] SLUB: HWalign=64, Order=0-3, MinObjects=0, CPUs=1, Nodes=1
[    0.000000] Hierarchical RCU implementation.
[    0.000000]  Build-time adjustment of leaf fanout to 64.
[    0.000000]  RCU restricting CPUs from NR_CPUS=64 to nr_cpu_ids=1.
[    0.000000] RCU: Adjusting geometry for rcu_fanout_leaf=64, nr_cpu_ids=1
[    0.000000] NR_IRQS:64 nr_irqs:64 0
[    0.000000] of_irq_init: init /interrupt-controller@e00f1000 (ffffffc07ffed800), parent           (null)
[    0.000000] OF: ** translation for device /interrupt-controller@e00f1000 **
[    0.000000] OF: bus is default (na=2, ns=2) on /
[    0.000000] OF: translating address: 00000000 e00f1000
[    0.000000] OF: reached root node
[    0.000000] OF: ** translation for device /interrupt-controller@e00f1000 **
[    0.000000] OF: bus is default (na=2, ns=2) on /
[    0.000000] OF: translating address: 00000000 e00f2000
[    0.000000] OF: reached root node
[    0.000000] OF: ** translation for device /interrupt-controller@e00f1000 **
[    0.000000] OF: bus is default (na=2, ns=2) on /
[    0.000000] OF: translating address: 00000000 e00f2000
[    0.000000] OF: reached root node
[    0.000000] irq: Added domain (null)
[    0.000000] of_irq_parse_one: dev=/timer, index=0
[    0.000000]  intspec=1 intlen=12
[    0.000000]  intsize=3 intlen=12
[    0.000000] of_irq_parse_raw:  /interrupt-controller@e00f1000:00000001,0000000d,00000f08
[    0.000000] of_irq_parse_raw: ipar=/interrupt-controller@e00f1000, size=3
[    0.000000]  -> addrsize=0
[    0.000000]  -> got it !
[    0.000000] of_irq_parse_one: dev=/timer, index=1
[    0.000000]  intspec=1 intlen=12
[    0.000000]  intsize=3 intlen=12
[    0.000000] of_irq_parse_raw:  /interrupt-controller@e00f1000:00000001,0000000e,00000f08
[    0.000000] of_irq_parse_raw: ipar=/interrupt-controller@e00f1000, size=3
[    0.000000]  -> addrsize=0
[    0.000000]  -> got it !
[    0.000000] of_irq_parse_one: dev=/timer, index=2
[    0.000000]  intspec=1 intlen=12
[    0.000000]  intsize=3 intlen=12
[    0.000000] of_irq_parse_raw:  /interrupt-controller@e00f1000:00000001,0000000b,00000f08
[    0.000000] of_irq_parse_raw: ipar=/interrupt-controller@e00f1000, size=3
[    0.000000]  -> addrsize=0
[    0.000000]  -> got it !
[    0.000000] of_irq_parse_one: dev=/timer, index=3
[    0.000000]  intspec=1 intlen=12
[    0.000000]  intsize=3 intlen=12
[    0.000000] of_irq_parse_raw:  /interrupt-controller@e00f1000:00000001,0000000a,00000f08
[    0.000000] of_irq_parse_raw: ipar=/interrupt-controller@e00f1000, size=3
[    0.000000]  -> addrsize=0
[    0.000000]  -> got it !
[    0.000000] Architected cp15 timer(s) running at 24.00MHz (phys).
[    0.000000] clocksource: arch_sys_counter: mask: 0xffffffffffffff max_cycles: 0x588fe9dc0, max_idle_ns: 440795202592 ns
[    0.000031] sched_clock: 56 bits at 24MHz, resolution 41ns, wraps every 4398046511097ns
[    0.007999] Registered 0xffffff800816bf78 as sched_clock source
[    0.013906] Calibrating delay loop (skipped), value calculated using timer frequency.. 48.00 BogoMIPS (lpj=96000)
[    0.024093] pid_max: default: 4096 minimum: 301
[    0.028687] Mount-cache hash table entries: 4096 (order: 3, 32768 bytes)
[    0.035281] Mountpoint-cache hash table entries: 4096 (order: 3, 32768 bytes)
[    0.042468] kobject: 'fs' (ffffffc079802c00): kobject_add_internal: parent: '', set: ''
[    0.051687] No CPU information found in DT
[    0.055499] CPU0: cluster 0 core 0 thread -1 mpidr 0x00000080000000
[    0.061718] ASID allocator initialised with 65536 entries
[    0.067687] Brought up 1 CPUs
[    0.070031] SMP: Total of 1 processors activated.
[    0.074718] CPU: All CPU(s) started at EL2
[    0.079281] kobject: 'devices' (ffffffc079823a98): kobject_add_internal: parent: '', set: ''
[    0.088249] kobject: 'devices' (ffffffc079823a98): kobject_uevent_env
[    0.094656] kobject: 'devices' (ffffffc079823a98): kobject_uevent_env: attempted to send uevent without kset!
[    0.104531] kobject: 'dev' (ffffffc079823b00): kobject_add_internal: parent: '', set: ''
[    0.113624] kobject: 'block' (ffffffc079823b80): kobject_add_internal: parent: 'dev', set: ''
[    0.122656] kobject: 'char' (ffffffc079823c00): kobject_add_internal: parent: 'dev', set: ''
[    0.131562] kobject: 'bus' (ffffffc079823c98): kobject_add_internal: parent: '', set: ''
[    0.140687] kobject: 'bus' (ffffffc079823c98): kobject_uevent_env
[    0.146749] kobject: 'bus' (ffffffc079823c98): kobject_uevent_env: attempted to send uevent without kset!
[    0.156281] kobject: 'system' (ffffffc079823d18): kobject_add_internal: parent: 'devices', set: ''
[    0.165718] kobject: 'system' (ffffffc079823d18): kobject_uevent_env
[    0.172062] kobject: 'system' (ffffffc079823d18): kobject_uevent_env: attempted to send uevent without kset!
[    0.181843] kobject: 'class' (ffffffc079823d98): kobject_add_internal: parent: '', set: ''
[    0.191124] kobject: 'class' (ffffffc079823d98): kobject_uevent_env
[    0.197343] kobject: 'class' (ffffffc079823d98): kobject_uevent_env: attempted to send uevent without kset!
[    0.207062] kobject: 'firmware' (ffffffc079823e00): kobject_add_internal: parent: '', set: ''
[    0.216593] device: 'platform': device_add
[    0.220656] kobject: 'platform' (ffffff80081d2680): kobject_add_internal: parent: 'devices', set: 'devices'
[    0.230374] kobject: 'platform' (ffffff80081d2680): kobject_uevent_env
[    0.236874] kobject: 'platform' (ffffff80081d2680): kobject_uevent_env: filter function caused the event to drop!
[    0.247093] kobject: 'platform' (ffffffc079822818): kobject_add_internal: parent: 'bus', set: 'bus'
[    0.256124] kobject: 'platform' (ffffffc079822818): kobject_uevent_env
[    0.262624] kobject: 'platform' (ffffffc079822818): fill_kobj_path: path = '/bus/platform'
[    0.270843] kobject: 'devices' (ffffffc079823e98): kobject_add_internal: parent: 'platform', set: ''
[    0.280468] kobject: 'devices' (ffffffc079823e98): kobject_uevent_env
[    0.286874] kobject: 'devices' (ffffffc079823e98): kobject_uevent_env: filter function caused the event to drop!
[    0.297031] kobject: 'drivers' (ffffffc079823f18): kobject_add_internal: parent: 'platform', set: ''
[    0.306656] kobject: 'drivers' (ffffffc079823f18): kobject_uevent_env
[    0.313062] kobject: 'drivers' (ffffffc079823f18): kobject_uevent_env: filter function caused the event to drop!
[    0.323187] bus: 'platform': registered
[    0.326999] kobject: 'cpu' (ffffffc079822a18): kobject_add_internal: parent: 'bus', set: 'bus'
[    0.335593] kobject: 'cpu' (ffffffc079822a18): kobject_uevent_env
[    0.341656] kobject: 'cpu' (ffffffc079822a18): fill_kobj_path: path = '/bus/cpu'
[    0.349031] kobject: 'devices' (ffffffc079823f98): kobject_add_internal: parent: 'cpu', set: ''
[    0.358218] kobject: 'devices' (ffffffc079823f98): kobject_uevent_env
[    0.364624] kobject: 'devices' (ffffffc079823f98): kobject_uevent_env: filter function caused the event to drop!
[    0.374749] kobject: 'drivers' (ffffffc079820d18): kobject_add_internal: parent: 'cpu', set: ''
[    0.383937] kobject: 'drivers' (ffffffc079820d18): kobject_uevent_env
[    0.390374] kobject: 'drivers' (ffffffc079820d18): kobject_uevent_env: filter function caused the event to drop!
[    0.400499] bus: 'cpu': registered
[    0.403874] device: 'cpu': device_add
[    0.407531] kobject: 'cpu' (ffffffc079822c10): kobject_add_internal: parent: 'system', set: 'devices'
[    0.416718] kobject: 'cpu' (ffffffc079822c10): kobject_uevent_env
[    0.422781] kobject: 'cpu' (ffffffc079822c10): kobject_uevent_env: filter function caused the event to drop!
[    0.432562] kobject: 'container' (ffffffc079822e18): kobject_add_internal: parent: 'bus', set: 'bus'
[    0.441656] kobject: 'container' (ffffffc079822e18): kobject_uevent_env
[    0.448249] kobject: 'container' (ffffffc079822e18): fill_kobj_path: path = '/bus/container'
[    0.456656] kobject: 'devices' (ffffffc079820d98): kobject_add_internal: parent: 'container', set: ''
[    0.466374] kobject: 'devices' (ffffffc079820d98): kobject_uevent_env
[    0.472781] kobject: 'devices' (ffffffc079820d98): kobject_uevent_env: filter function caused the event to drop!
[    0.482937] kobject: 'drivers' (ffffffc079820e18): kobject_add_internal: parent: 'container', set: ''
[    0.492624] kobject: 'drivers' (ffffffc079820e18): kobject_uevent_env
[    0.499031] kobject: 'drivers' (ffffffc079820e18): kobject_uevent_env: filter function caused the event to drop!
[    0.509187] bus: 'container': registered
[    0.513093] device: 'container': device_add
[    0.517249] kobject: 'container' (ffffffc079824010): kobject_add_internal: parent: 'system', set: 'devices'
[    0.526937] kobject: 'container' (ffffffc079824010): kobject_uevent_env
[    0.533531] kobject: 'container' (ffffffc079824010): kobject_uevent_env: filter function caused the event to drop!
[    0.543843] kobject: 'devicetree' (ffffffc079820f98): kobject_add_internal: parent: 'firmware', set: ''
[    0.553718] kobject: 'devicetree' (ffffffc079820f98): kobject_uevent_env
[    0.560406] kobject: 'devicetree' (ffffffc079820f98): kobject_uevent_env: attempted to send uevent without kset!
[    0.570531] doing early, parsing ARGS: 'earlycon=owl_serial loglevel=8'
[    0.577124] doing early: earlycon='owl_serial'
[    0.581562] doing early: loglevel='8'
[    0.585187] doing core, parsing ARGS: 'earlycon=owl_serial loglevel=8'
[    0.591687] doing core: earlycon='owl_serial'
[    0.596031] doing core: loglevel='8'
[    0.599593] kobject: 'kernel' (ffffffc079825000): kobject_add_internal: parent: '', set: ''
[    0.608937] clocksource: jiffies: mask: 0xffffffff max_cycles: 0xffffffff, max_idle_ns: 7645041785100000 ns
[    0.618656] doing postcore, parsing ARGS: 'earlycon=owl_serial loglevel=8'
[    0.625499] doing postcore: earlycon='owl_serial'
[    0.630156] doing postcore: loglevel='8'
[    0.634062] device class 'bdi': registering
[    0.638218] kobject: 'bdi' (ffffffc079824218): kobject_add_internal: parent: 'class', set: 'class'
[    0.647156] kobject: 'bdi' (ffffffc079824218): kobject_uevent_env
[    0.653218] kobject: 'bdi' (ffffffc079824218): fill_kobj_path: path = '/class/bdi'
[    0.660781] kobject: 'mm' (ffffffc079825100): kobject_add_internal: parent: 'kernel', set: ''
[    0.669781] kobject: 'amba' (ffffffc079824418): kobject_add_internal: parent: 'bus', set: 'bus'
[    0.678437] kobject: 'amba' (ffffffc079824418): kobject_uevent_env
[    0.684593] kobject: 'amba' (ffffffc079824418): fill_kobj_path: path = '/bus/amba'
[    0.692124] kobject: 'devices' (ffffffc079825198): kobject_add_internal: parent: 'amba', set: ''
[    0.701406] kobject: 'devices' (ffffffc079825198): kobject_uevent_env
[    0.707812] kobject: 'devices' (ffffffc079825198): kobject_uevent_env: filter function caused the event to drop!
[    0.717968] kobject: 'drivers' (ffffffc079825218): kobject_add_internal: parent: 'amba', set: ''
[    0.727249] kobject: 'drivers' (ffffffc079825218): kobject_uevent_env
[    0.733656] kobject: 'drivers' (ffffffc079825218): kobject_uevent_env: filter function caused the event to drop!
[    0.743781] bus: 'amba': registered
[    0.747249] device class 'tty': registering
[    0.751406] kobject: 'tty' (ffffffc079824618): kobject_add_internal: parent: 'class', set: 'class'
[    0.760343] kobject: 'tty' (ffffffc079824618): kobject_uevent_env
[    0.766406] kobject: 'tty' (ffffffc079824618): fill_kobj_path: path = '/class/tty'
[    0.773937] doing arch, parsing ARGS: 'earlycon=owl_serial loglevel=8'
[    0.780437] doing arch: earlycon='owl_serial'
[    0.784781] doing arch: loglevel='8'
[    0.788343] vdso: 2 pages (1 code @ ffffff8008186000, 1 data @ ffffff80081c8000)
[    0.795874] DMA: preallocated 256 KiB pool for atomic allocations
[    0.801781] of_platform_bus_create() - skipping /memory, no compatible prop
[    0.808687] of_platform_bus_create() - skipping /chosen, no compatible prop
[    0.815656] OF: ** translation for device /interrupt-controller@e00f1000 **
[    0.822562] OF: bus is default (na=2, ns=2) on /
[    0.827156] OF: translating address: 00000000 e00f1000
[    0.832281] OF: reached root node
[    0.835562] OF: ** translation for device /interrupt-controller@e00f1000 **
[    0.842499] OF: bus is default (na=2, ns=2) on /
[    0.847093] OF: translating address: 00000000 e00f2000
[    0.852218] OF: reached root node
[    0.855499] of_irq_parse_one: dev=/interrupt-controller@e00f1000, index=0
[    0.862249] OF: ** translation for device /interrupt-controller@e00f1000 **
[    0.869187] OF: bus is default (na=2, ns=2) on /
[    0.873781] OF: translating address: 00000000 e00f1000
[    0.878906] OF: reached root node
[    0.882187] OF: ** translation for device /interrupt-controller@e00f1000 **
[    0.889124] OF: bus is default (na=2, ns=2) on /
[    0.893718] OF: translating address: 00000000 e00f2000
[    0.898843] OF: reached root node
[    0.902124] OF: ** translation for device /interrupt-controller@e00f1000 **
[    0.909062] OF: bus is default (na=2, ns=2) on /
[    0.913656] OF: translating address: 00000000 e00f1000
[    0.918781] OF: reached root node
[    0.922062] of_dma_get_range: no dma-ranges found for node(/interrupt-controller@e00f1000)
[    0.930312] platform e00f1000.interrupt-controller: device is not dma coherent
[    0.937499] platform e00f1000.interrupt-controller: device is not behind an iommu
[    0.944937] device: 'e00f1000.interrupt-controller': device_add
[    0.950843] kobject: 'e00f1000.interrupt-controller' (ffffffc079824820): kobject_add_internal: parent: 'platform', set: 'devices'
[    0.962437] bus: 'platform': add device e00f1000.interrupt-controller
[    0.968874] kobject: 'e00f1000.interrupt-controller' (ffffffc079824820): kobject_uevent_env
[    0.977187] kobject: 'e00f1000.interrupt-controller' (ffffffc079824820): fill_kobj_path: path = '/devices/platform/e00f1000.interrupt-controller'
[    0.990218] of_irq_parse_one: dev=/timer, index=0
[    0.994874]  intspec=1 intlen=12
[    0.998062]  intsize=3 intlen=12
[    1.001281] of_irq_parse_raw:  /interrupt-controller@e00f1000:00000001,0000000d,00000f08
[    1.009343] of_irq_parse_raw: ipar=/interrupt-controller@e00f1000, size=3
[    1.016093]  -> addrsize=0
[    1.018781]  -> got it !
[    1.021281] of_irq_parse_one: dev=/timer, index=1
[    1.025968]  intspec=1 intlen=12
[    1.029187]  intsize=3 intlen=12
[    1.032374] of_irq_parse_raw:  /interrupt-controller@e00f1000:00000001,0000000e,00000f08
[    1.040437] of_irq_parse_raw: ipar=/interrupt-controller@e00f1000, size=3
[    1.047218]  -> addrsize=0
[    1.049906]  -> got it !
[    1.052406] of_irq_parse_one: dev=/timer, index=2
[    1.057093]  intspec=1 intlen=12
[    1.060281]  intsize=3 intlen=12
[    1.063499] of_irq_parse_raw:  /interrupt-controller@e00f1000:00000001,0000000b,00000f08
[    1.071562] of_irq_parse_raw: ipar=/interrupt-controller@e00f1000, size=3
[    1.078312]  -> addrsize=0
[    1.080999]  -> got it !
[    1.083531] of_irq_parse_one: dev=/timer, index=3
[    1.088187]  intspec=1 intlen=12
[    1.091406]  intsize=3 intlen=12
[    1.094624] of_irq_parse_raw:  /interrupt-controller@e00f1000:00000001,0000000a,00000f08
[    1.102687] of_irq_parse_raw: ipar=/interrupt-controller@e00f1000, size=3
[    1.109437]  -> addrsize=0
[    1.112124]  -> got it !
[    1.114624] of_irq_parse_one: dev=/timer, index=4
[    1.119312]  intspec=1 intlen=12
[    1.122531]  intsize=3 intlen=12
[    1.125718] of_irq_parse_one: dev=/timer, index=0
[    1.130406]  intspec=1 intlen=12
[    1.133624]  intsize=3 intlen=12
[    1.136812] of_irq_parse_raw:  /interrupt-controller@e00f1000:00000001,0000000d,00000f08
[    1.144874] of_irq_parse_raw: ipar=/interrupt-controller@e00f1000, size=3
[    1.151624]  -> addrsize=0
[    1.154312]  -> got it !
[    1.156843] of_irq_parse_one: dev=/timer, index=1
[    1.161531]  intspec=1 intlen=12
[    1.164718]  intsize=3 intlen=12
[    1.167937] of_irq_parse_raw:  /interrupt-controller@e00f1000:00000001,0000000e,00000f08
[    1.175999] of_irq_parse_raw: ipar=/interrupt-controller@e00f1000, size=3
[    1.182749]  -> addrsize=0
[    1.185437]  -> got it !
[    1.187968] of_irq_parse_one: dev=/timer, index=2
[    1.192624]  intspec=1 intlen=12
[    1.195843]  intsize=3 intlen=12
[    1.199031] of_irq_parse_raw:  /interrupt-controller@e00f1000:00000001,0000000b,00000f08
[    1.207093] of_irq_parse_raw: ipar=/interrupt-controller@e00f1000, size=3
[    1.213874]  -> addrsize=0
[    1.216562]  -> got it !
[    1.219062] of_irq_parse_one: dev=/timer, index=3
[    1.223749]  intspec=1 intlen=12
[    1.226968]  intsize=3 intlen=12
[    1.230156] of_irq_parse_raw:  /interrupt-controller@e00f1000:00000001,0000000a,00000f08
[    1.238218] of_irq_parse_raw: ipar=/interrupt-controller@e00f1000, size=3
[    1.244968]  -> addrsize=0
[    1.247656]  -> got it !
[    1.250187] of_dma_get_range: no dma-ranges found for node(/timer)
[    1.256343] platform timer: device is not dma coherent
[    1.261437] platform timer: device is not behind an iommu
[    1.266812] device: 'timer': device_add
[    1.270624] kobject: 'timer' (ffffffc079824a20): kobject_add_internal: parent: 'platform', set: 'devices'
[    1.280156] bus: 'platform': add device timer
[    1.284499] kobject: 'timer' (ffffffc079824a20): kobject_uevent_env
[    1.290749] kobject: 'timer' (ffffffc079824a20): fill_kobj_path: path = '/devices/platform/timer'
[    1.299593] doing subsys, parsing ARGS: 'earlycon=owl_serial loglevel=8'
[    1.306249] doing subsys: earlycon='owl_serial'
[    1.310749] doing subsys: loglevel='8'
[    1.314499] device: 'cpu0': device_add
[    1.318218] kobject: 'cpu0' (ffffffc07ffe1468): kobject_add_internal: parent: 'cpu', set: 'devices'
[    1.327218] bus: 'cpu': add device cpu0
[    1.331031] kobject: 'cpu0' (ffffffc07ffe1468): kobject_uevent_env
[    1.337187] kobject: 'cpu0' (ffffffc07ffe1468): fill_kobj_path: path = '/devices/system/cpu/cpu0'
[    1.346156] device class 'misc': registering
[    1.350281] kobject: 'misc' (ffffffc079824e18): kobject_add_internal: parent: 'class', set: 'class'
[    1.359281] kobject: 'misc' (ffffffc079824e18): kobject_uevent_env
[    1.365437] kobject: 'misc' (ffffffc079824e18): fill_kobj_path: path = '/class/misc'
[    1.373156] device class 'power_supply': registering
[    1.378093] kobject: 'power_supply' (ffffffc079803018): kobject_add_internal: parent: 'class', set: 'class'
[    1.387812] kobject: 'power_supply' (ffffffc079803018): kobject_uevent_env
[    1.394656] kobject: 'power_supply' (ffffffc079803018): fill_kobj_path: path = '/class/power_supply'
[    1.403749] doing fs, parsing ARGS: 'earlycon=owl_serial loglevel=8'
[    1.410093] doing fs: earlycon='owl_serial'
[    1.414249] doing fs: loglevel='8'
[    1.417656] clocksource: Switched to clocksource arch_sys_counter
[    1.423718] device class 'mem': registering
[    1.427843] kobject: 'mem' (ffffffc079827018): kobject_add_internal: parent: 'class', set: 'class'
[    1.436781] kobject: 'mem' (ffffffc079827018): kobject_uevent_env
[    1.442843] kobject: 'mem' (ffffffc079827018): fill_kobj_path: path = '/class/mem'
[    1.450374] device: 'null': device_add
[    1.454124] kobject: 'virtual' (ffffffc079826080): kobject_add_internal: parent: 'devices', set: ''
[    1.463656] kobject: 'mem' (ffffffc079826100): kobject_add_internal: parent: 'virtual', set: '(null)'
[    1.472843] kobject: 'null' (ffffffc079827210): kobject_add_internal: parent: 'mem', set: 'devices'
[    1.481843] kobject: 'null' (ffffffc079827210): kobject_uevent_env
[    1.487999] kobject: 'null' (ffffffc079827210): fill_kobj_path: path = '/devices/virtual/mem/null'
[    1.496937] device: 'zero': device_add
[    1.500656] kobject: 'zero' (ffffffc079827410): kobject_add_internal: parent: 'mem', set: 'devices'
[    1.509656] kobject: 'zero' (ffffffc079827410): kobject_uevent_env
[    1.515812] kobject: 'zero' (ffffffc079827410): fill_kobj_path: path = '/devices/virtual/mem/zero'
[    1.524749] device: 'full': device_add
[    1.528468] kobject: 'full' (ffffffc079827610): kobject_add_internal: parent: 'mem', set: 'devices'
[    1.537468] kobject: 'full' (ffffffc079827610): kobject_uevent_env
[    1.543624] kobject: 'full' (ffffffc079827610): fill_kobj_path: path = '/devices/virtual/mem/full'
[    1.552562] device: 'random': device_add
[    1.556468] kobject: 'random' (ffffffc079827810): kobject_add_internal: parent: 'mem', set: 'devices'
[    1.565656] kobject: 'random' (ffffffc079827810): kobject_uevent_env
[    1.571968] kobject: 'random' (ffffffc079827810): fill_kobj_path: path = '/devices/virtual/mem/random'
[    1.581249] device: 'urandom': device_add
[    1.585249] kobject: 'urandom' (ffffffc079827a10): kobject_add_internal: parent: 'mem', set: 'devices'
[    1.594499] kobject: 'urandom' (ffffffc079827a10): kobject_uevent_env
[    1.600937] kobject: 'urandom' (ffffffc079827a10): fill_kobj_path: path = '/devices/virtual/mem/urandom'
[    1.610374] device: 'kmsg': device_add
[    1.614093] kobject: 'kmsg' (ffffffc079827c10): kobject_add_internal: parent: 'mem', set: 'devices'
[    1.623124] kobject: 'kmsg' (ffffffc079827c10): kobject_uevent_env
[    1.629249] kobject: 'kmsg' (ffffffc079827c10): fill_kobj_path: path = '/devices/virtual/mem/kmsg'
[    1.638187] device: 'tty': device_add
[    1.641843] kobject: 'tty' (ffffffc079826280): kobject_add_internal: parent: 'virtual', set: '(null)'
[    1.651031] kobject: 'tty' (ffffffc079827e10): kobject_add_internal: parent: 'tty', set: 'devices'
[    1.659937] kobject: 'tty' (ffffffc079827e10): kobject_uevent_env
[    1.665999] kobject: 'tty' (ffffffc079827e10): fill_kobj_path: path = '/devices/virtual/tty/tty'
[    1.674781] device: 'console': device_add
[    1.678749] kobject: 'console' (ffffffc079829010): kobject_add_internal: parent: 'tty', set: 'devices'
[    1.688031] kobject: 'console' (ffffffc079829010): kobject_uevent_env
[    1.694437] kobject: 'console' (ffffffc079829010): fill_kobj_path: path = '/devices/virtual/tty/console'
[    1.703937] doing device, parsing ARGS: 'earlycon=owl_serial loglevel=8'
[    1.710562] doing device: earlycon='owl_serial'
[    1.715062] doing device: loglevel='8'
[    1.718812] bus: 'platform': add driver alarmtimer
[    1.723562] kobject: 'alarmtimer' (ffffffc079828600): kobject_add_internal: parent: 'drivers', set: 'drivers'
[    1.733437] kobject: 'alarmtimer' (ffffffc079828600): kobject_uevent_env
[    1.740124] kobject: 'alarmtimer' (ffffffc079828600): fill_kobj_path: path = '/bus/platform/drivers/alarmtimer'
[    1.750156] Registering platform device 'alarmtimer'. Parent at platform
[    1.756843] device: 'alarmtimer': device_add
[    1.761093] kobject: 'alarmtimer' (ffffffc079829220): kobject_add_internal: parent: 'platform', set: 'devices'
[    1.771062] bus: 'platform': add device alarmtimer
[    1.775812] kobject: 'alarmtimer' (ffffffc079829220): kobject_uevent_env
[    1.782499] kobject: 'alarmtimer' (ffffffc079829220): fill_kobj_path: path = '/devices/platform/alarmtimer'
[    1.792218] bus: 'platform': driver_probe_device: matched device alarmtimer with driver alarmtimer
[    1.801124] bus: 'platform': really_probe: probing driver alarmtimer with device alarmtimer
[    1.809437] devices_kset: Moving alarmtimer to end of list
[    1.814906] driver: 'alarmtimer': driver_bound: bound to device 'alarmtimer'
[    1.821937] bus: 'platform': really_probe: bound device alarmtimer to driver alarmtimer
[    1.829999] workingset: timestamp_bits=60 max_order=19 bucket_order=0
[    1.836312] Failed to find cpu0 device node
[    1.840468] Unable to detect cache hierarchy from DT for CPU 0
[    1.846281] bus: 'platform': add driver gpio-clk
[    1.850874] kobject: 'gpio-clk' (ffffffc079828900): kobject_add_internal: parent: 'drivers', set: 'drivers'
[    1.860593] kobject: 'gpio-clk' (ffffffc079828900): kobject_uevent_env
[    1.867093] kobject: 'gpio-clk' (ffffffc079828900): fill_kobj_path: path = '/bus/platform/drivers/gpio-clk'
[    1.876781] doing late, parsing ARGS: 'earlycon=owl_serial loglevel=8'
[    1.883281] doing late: earlycon='owl_serial'
[    1.887624] doing late: loglevel='8'
[    1.891187] device: 'cpu_dma_latency': device_add
[    1.895843] kobject: 'misc' (ffffffc079826780): kobject_add_internal: parent: 'virtual', set: '(null)'
[    1.905124] kobject: 'cpu_dma_latency' (ffffffc079829610): kobject_add_internal: parent: 'misc', set: 'devices'
[    1.915187] kobject: 'cpu_dma_latency' (ffffffc079829610): kobject_uevent_env
[    1.922281] kobject: 'cpu_dma_latency' (ffffffc079829610): fill_kobj_path: path = '/devices/virtual/misc/cpu_dma_latency'
[    1.933218] device: 'network_latency': device_add
[    1.937874] kobject: 'network_latency' (ffffffc079829810): kobject_add_internal: parent: 'misc', set: 'devices'
[    1.947937] kobject: 'network_latency' (ffffffc079829810): kobject_uevent_env
[    1.955031] kobject: 'network_latency' (ffffffc079829810): fill_kobj_path: path = '/devices/virtual/misc/network_latency'
[    1.965968] device: 'network_throughput': device_add
[    1.970906] kobject: 'network_throughput' (ffffffc079829a10): kobject_add_internal: parent: 'misc', set: 'devices'
[    1.981218] kobject: 'network_throughput' (ffffffc079829a10): kobject_uevent_env
[    1.988593] kobject: 'network_throughput' (ffffffc079829a10): fill_kobj_path: path = '/devices/virtual/misc/network_throughput'
[    2.000031] device: 'memory_bandwidth': device_add
[    2.004781] kobject: 'memory_bandwidth' (ffffffc079829c10): kobject_add_internal: parent: 'misc', set: 'devices'
[    2.014937] kobject: 'memory_bandwidth' (ffffffc079829c10): kobject_uevent_env
[    2.022124] kobject: 'memory_bandwidth' (ffffffc079829c10): fill_kobj_path: path = '/devices/virtual/misc/memory_bandwidth'
[    2.033437] Warning: unable to open an initial console.
[    2.038531] Freeing unused kernel memory: 120K (ffffff80081a3000 - ffffff80081c1000)
[    2.046124] This architecture does not have kernel memory protection.
[    2.052593] Kernel panic - not syncing: No working init found.  Try passing init= option to kernel. See Linux Documentation/init.txt for guidance.
[    2.065624] Kernel Offset: disabled
[    2.069093] Memory Limit: none
[    2.072124] ---[ end Kernel panic - not syncing: No working init found.  Try passing init= option to kernel. See Linux Documentation/init.txt for guidance.


原创文章,转发请注明出处。蜗窝科技,www.wowotech.net

X-014-KERNEL-ARM generic timer driver的移植

$
0
0

1. 前言

本文将基于“Linux时间子系统之(十七):ARM generic timer驱动代码分析[1]”,以bubblegum-96平台为例,介绍ARM generic timer的移植步骤。

另外,我们在[2]中完成了ARM GIC驱动的移植,但还没有测试是否可用。刚好借助timer驱动,测试GIC是否可以正常工作,顺便理解Interrupt的使用方法。

2. Generic timer硬件说明

有关ARM generic timer的技术细节,可参考[1]。本文所使用的bubblegum-96平台,其SOC包含了4个Cortex A53的core,支持CP15 type的Generic timer。为了驱动它,我们需要如下两个信息:

1)System counter的输入频率。

2)Per-CPU的timer和GIC之间的接口(即这些Timer的中断源以及中断触发方式)。参考[2]中的介绍,对于支持virtualization extension的SOC,每个cpu有四个timer:Non-secure PL1 physical timer,Secure PL1 physical timer,Non-secure PL2 physical timer和virtual timer ,因此将会有四个中断源。

不过,和我们移植GIC驱动[2]时遇到的问题一样,我们找不到bubblegum-96平台有关的信息(大家在开发自己的平台时,应该没有这些困扰),只能从开源出来的代码中[5]反推,结论如下:

1)System counter的输入频率为24MHz,这一点可以从[4]中推测出来,因为bubblegum-96开发板的晶振是24MHz,一般system counter直接使用晶振为输入时钟。

2)4个Per-CPU timer的中断源分别是:Non-secure PL1 physical timer----PPI 13,Secure PL1 physical timer----PPI 14,Non-secure PL2 physical timer----PPI 11,virtual timer----PPI 10。 它们都是低电平触发的方式。

3. Generic timer移植

3.1 u-boot中的移植

记得我们在“X-013-UBOOT-使能autoboot功能”中调试u-boot的autoboot功能的时候,由于没有timer驱动,无法正确使用CONFIG_BOOTDELAY(因为无法计时)。既然本文要在kernel中移植generic timer,就顺便提一下,在u-boot中支持ARM generic timer的方法。

其实很简单,对于ARM64平台来说,支持generic timer只需要知道System counter的输入频率,并通过COUNTER_FREQUENCY宏定义告诉u-boot即可,如下(我们同时修改boot delay为5s,以验证timer功能):

/* include/configs/bubblegum.h */

@@ -74,7 +74,8 @@
#define CONFIG_CMD_BOOTI

-#define CONFIG_BOOTDELAY -2
+#define CONFIG_BOOTDELAY 5

#define CONFIG_BOOTCOMMAND "bootm 0x6400000"

+#define COUNTER_FREQUENCY (24000000) /* 24MHz */
#endif

修改完成后,编译u-boot并启动kernel,就可以看到自动boot之前的倒计时了,说明timer添加成功:

cd ~/work/xprj/build
make u-boot uImage
make spi-run

make kernel-run

3.2 kernel中的移植

在kernel中的移植也很简单,只需要修改dts文件,添加generic timer的节点,并提供第2章所描述的硬件信息即可:

/* ./arch/arm64/boot/dts/actions/s900-bubblegum.dts */

 
+#include
+
/ {
        model = "Bubblegum-96 Development Board";
        compatible = "actions,s900-bubblegum", "actions,s900";
+
+       timer {
+               compatible = "arm,armv8-timer";
+               interrupts = ,
+                            ,
+                            ,
+                            ;
+               clock-frequency = <24000000>;
+       };

};

说明如下:

1)为了使用GIC有关的信息,我们需要包含“include/dt-bindings/interrupt-controller/arm-gic.h”头文件。

2)timer节点的compatible字段为"arm,armv8-timer",表明我们是armv8 generic timer。

3)通过clock-frequency字段指定system counter的输入频率,这里为24000000(24MHz)。

4)interrupts字段指定了该设备所使用的中断源,可以是一个,也可以是多个,按照第2章的介绍,这里共有4个。参考[2]中的介绍,GIC的interrupt-cell为3,分别是:

第一个cell:PPI或SPI,此处均为PPI;

第二个cell,中断号,这里分别为13,14,11,10;

第三个cell,终端flag,这里需要包含一个GIC强制要求的----GIC_CPU_MASK_SIMPLE(4),另外就是中断触发类型,此处全是低电平触发。

4. 测试和验证

4.1 编译及debug

修改完dts文件,编译并运行kernel,GIC移植[4]时最后的kernel panic(如下)不见了:

clocksource_probe: no matching clocksources found
Kernel panic - not syncing: Unable to initialise architected timer.

---[ end Kernel panic - not syncing: Unable to initialise architected timer.

取而代之的是:

NR_IRQS:64 nr_irqs:64 0
arch_timer: No interrupt available, giving up
sched_clock: 64 bits at 250 Hz, resolution 4000000ns, wraps every 9007199254000000ns
Calibrating delay loop (skipped), value calculated using timer frequency.. 48.00 BogoMIPS (lpj=96000)
pid_max: default: 4096 minimum: 301
Mount-cache hash table entries: 4096 (order: 3, 32768 bytes)
Mountpoint-cache hash table entries: 4096 (order: 3, 32768 bytes)
No CPU information found in DT
ASID allocator initialised with 65536 entries
Brought up 1 CPUs
SMP: Total of 1 processors activated.
CPU: All CPU(s) started at EL2
clocksource: jiffies: mask: 0xffffffff max_cycles: 0xffffffff, max_idle_ns: 7645041785100000 ns
vdso: 2 pages (1 code @ ffffff8008185000, 1 data @ ffffff80081c4000)
DMA: preallocated 256 KiB pool for atomic allocations
workingset: timestamp_bits=60 max_order=19 bucket_order=0
Failed to find cpu0 device node
Unable to detect cache hierarchy from DT for CPU 0
Warning: unable to open an initial console.
Freeing unused kernel memory: 116K (ffffff80081a0000 - ffffff80081bd000)
This architecture does not have kernel memory protection.
Kernel panic - not syncing: No working init found.  Try passing init= option to kernel. See Linux Documentation/init.txt for guidance.
Kernel Offset: disabled
Memory Limit: none
---[ end Kernel panic - not syncing: No working init found.  Try passing init= option to kernel. See Linux Documentation/init.txt for guidance.

arch_timer: No interrupt available, giving up”好像还有哪里不对劲,好像我们在第3章dts中指定的interrupts字段,没有被正常解析。没关系,根据[6]的介绍,打开所有的DEBUG输出,然后根据更为详细的日志,检查代码(过程略掉……),如下(从drivers/clocksource/arm_arch_timer.c开始):

       CLOCKSOURCE_OF_DECLARE(armv8_arch_timer, "arm,armv8-timer", arch_timer_of_init);
              irq_of_parse_and_map(drivers/of/irq.c)
                     of_irq_parse_one
                            of_irq_find_parent

好像是找不到interrupt parent?原来是我们的dts还没有添加interrupt-parent字段,按照下面的代码增加:

--- a/arch/arm64/boot/dts/actions/s900-bubblegum.dts
+++ b/arch/arm64/boot/dts/actions/s900-bubblegum.dts

@@ -12,6 +12,8 @@

        model = "Bubblegum-96 Development Board";
        compatible = "actions,s900-bubblegum", "actions,s900";
+       interrupt-parent = <&gic>;
+

        #address-cells = <2>;
        #size-cells = <2>;

再次编译执行,异常消失了,算是移植完成了吧。

4.2 测试

测试的方法很简单,我们把printk的时间戳打开,如果可以正确显示时间戳,则说明移植成功。打开printk时间戳的方法如下:

cd ~/work/xprj/build;
make kernel-config

    Kernel hacking  --->
        printk and dmesg options  --->
            [*] Show timing information on printks

然后编译并运行kernel,输出的日志如下:

[    0.093281] Failed to find cpu0 device node

[    0.097437] Unable to detect cache hierarchy from DT for CPU 0

[    0.103562] Warning: unable to open an initial console.

[    0.108531] Freeing unused kernel memory: 116K (ffffff80081a0000 - ffffff80081bd000)

[    0.116156] This architecture does not have kernel memory protection.

[    0.122593] Kernel panic - not syncing: No working init found.  Try passing init= option to kernel. See Linux Documentation/init.txt for guidance.

[    0.135656] Kernel Offset: disabled

[    0.139124] Memory Limit: none

[    0.142156] ---[ end Kernel panic - not syncing: No working init found.  Try passing init= option to kernel. See Linux Documentation/init.txt for guidance.

成功了。

5. 参考文档

[1] Linux时间子系统之(十七):ARM generic timer驱动代码分析

[2] X-013-KERNEL-ARM GIC driver的移植

[3] bubblegum-96/SoC_bubblegum96.pdf

[4] bubblegum-96/HardwareManual_Bubblegum96.pdf

[5] https://github.com/96boards-bubblegum/linux/blob/bubblegum96-3.10/arch/arm64/boot/dts/s900.dtsi

[6] Linux kernel debug技巧----开启DEBUG选项

 

原创文章,转发请注明出处。蜗窝科技,www.wowotech.net

蓝牙协议分析(8)_BLE安全机制之白名单

$
0
0

1. 前言

在万物联网的时代,安全问题将会受到非常严峻的挑战(相应地,也会获得最大的关注度),因为我们身边的每一个IOT设备,都是一个处于封印状态的天眼,随时都有被开启的危险。想想下面的场景吧:

凌晨2点,x米手环的闹钟意外启动,将你从睡梦中惊醒,然后床头的灯光忽明忽暗……

你的心率、血压、睡眠质量等信息,默默地被竞争对手收集着,并通过大数据分析你的情绪、健康等,随时准备给你致命一击……

我知道你家里有几盏灯、几台电器、几个人,知道你几点睡觉几时醒来,知道你一周做过几顿饭,甚至知道你有一个xx棒、一周使用几次、每次使用多久……

……

算了,不罗列了,有时间的话可以建个iot eyes的站点,专门收集、整理物联网安全有关的内容。这里就先言归正传。

经过前面几篇的蓝牙协议分析,我们对蓝牙(特别是蓝牙低功耗)已经有了一个比较全面的了解。随后几篇文章,我会focus在BLE的安全机制上。毕竟,知己知彼,才能攻防有度。

话说,蓝牙SIG深知物联网安全的水有多深,因此使用了大量的篇幅,定义BLE安全有关的机制,甚至可以不夸张的说,BLE协议中内容最多、最难理解的部分,非安全机制莫属。本文先从介绍最简单的----白名单机制(White list)。

2. 白名单机制

白名单(white list)是BLE协议中最简单、直白的一种安全机制。其原理很简单,总结如下(前面的分析文章中都有介绍):

所谓的白名单,就是一组蓝牙地址;

通过白名单,可以只允许特定的蓝牙设备(白名单中列出的)扫描(Scan)、连接(connect)我们,也可以只扫描、连接特定的蓝牙设备(白名单中列出的)。

例如,如果某个BLE设备,只需要被受信任的某几个设备扫描、连接,我们就可以把这些受信任设备的蓝牙地址加入到该设备的白名单中,这样就可以有效避免其它“流氓设备”的骚扰了。

不过呢,该机制只防君子不防小人,因为它是靠地址去过滤“流氓”的,如果有些资深流氓,伪装一下,将自己的设备地址修改为受信任设备的地址,那就惨了……

3. 白名单有关的HCI命令

注1:本文主要从HCI的角度分析、介绍,如非必要,不再会涉及HCI之下的BLE协议(后续的分析文章,也大抵如此)。

3.1 白名单维护相关的命令

BLE协议在HCI层定义了4个和白名单维护有关的命令,分别如下:

1)LE Read White List Size Command,获取controller可保存白名单设备的个数

该命令的格式为:

OCF Command parameters Return Parameters
0x000F   Status
White_List_Size

Status,命令执行的结果,0为success。

White_List_Size,size,范围是1~255。

注2:由此可知,白名单是保存在controller中,由于size的范围是1~255,因此controller必须实现白名单功能(最少保存一个)。

2)LE Clear White List Command,将controller中的白名单清空

该命令的格式为:

OCF Command parameters Return Parameters
0x0010   Status

Status,命令执行的结果,0为success。

3)LE Add Device To White List Command,将指定的设备添加到白名单

该命令的格式为:

OCF  Command parameters Return Parameters
0x0011 Address_type(1 byte)
Address(6 bytes)
Status

Address_type,设备的地址类型[1],0为Public Device Address,1为Random Device Address。

Address,设备的地址。

Status,命令执行的结果,0为success。

4)LE Remove Device From White List Command,将指定的设备从白名单中移除的命令

该命令的格式为:

OCF Command parameters Return Parameters
0x0012 Address_type(1 byte)
Address(6 bytes)
Status

Address_type,设备的地址类型[1],0为Public Device Address,1为Random Device Address。

Address,设备的地址。

Status,命令执行的结果,0为success。

最后需要说明的是,当controller处于以下三个状态的时候,以上命令除“LE Read Resolving List Size Command”外,均不能执行:

正在advertising;

正在scanning;

正在connecting。

3.2 白名单使用策略有关的命令

BLE设备在发起Advertising、Scanning或者Connecting操作的时候,可以通过Set Advertising Parameters、Set Scan Parameters或者LE Create Connection Command,设置Advertising、Scanning或者Connecting的过滤策略(Filter_Policy),具体如下:

1)Advertising时的白名单策略

LE Set Advertising Parameters Command的命令格式为:

OCF Command parameters Return Parameters
0x0006
Advertising_Filter_Policy(1 byte)
Status

该命令的其它参数请参考[2],Advertising_Filter_Policy的含义如下:

0x00,禁用白名单机制,允许任何设备连接和扫描。

0x01,允许任何设备连接,但只允许白名单中的设备扫描(scan data中有敏感信息?)。

0x02,允许任何设备扫描,但只允许白名单中的设备连接。

0x03,只允许白名单中的设备扫描和连接。

2)Scanning时的白名单策略

LE Set Scan Parameters Command的命令格式为:

OCF Command parameters Return Parameters
0x000B
Scanning_Filter_Policy(1 byte)
Status

该命令的其它参数请参考[2],Scanning_Filter_Policy的含义如下:

0x00,禁用白名单机制,接受所有的广播包(除了那些不是给我的directed advertising packets)。

0x01,只接受在白名单中的那些设备发送的广播包(除了那些不是给我的directed advertising packets)。

0x02,和白名单无关,不再介绍。

0x03,接受如下的广播包:在白名单中的那些设备发送的广播包;广播者地址为resolvable private address的directed advertising packets;给我的给我的directed advertising packets。

注3:Scanning时的白名单策略有点奇怪,既然是主动发起的,要白名单的意义就不大了吧?

3)Connecting时的白名单策略

LE Create Connection Command的命令格式为:

OCF Command parameters Return Parameters
0x000D
Initiator_Filter_Policy(1 byte)
Status

该命令的其它参数请参考[4],Initiator_Filter_Policy的含义如下:

0x00,禁用白名单机制,使用Peer_Address_Type and Peer_Address指定需要连接的设备。

0x01,连接那些在白名单中的设备,不需要提供Peer_Address_Type and Peer_Address参数。

4. 使用示例

4.1 准备工作

后续的测试需要用到如下的设备和软件:

1)蓝牙设备A,作为Advertiser,发送广播数据,接受连接。

2)蓝牙设备B,作为Scanner,扫描设备A的广播数据,发起连接。

上述的1)和2)可以是如下一种:

一个具有bluez(hcitool等工具)的Android手机,可能需要较旧的android版本才行;

带有蓝牙功能的树莓派,允许Debian、Ubuntu等系统(只要不是Android就行);

Linux PC(或者虚拟机)加上一个具有BLE功能的蓝牙适配器。

3)bluez工具集,我们需要使用其中的hcitool命令。

4.2 相关的hcitool命令说明

hcitool中的一些命令,和白名单机制有关,总结如下。

1)hcitool lewlsz,获取controller白名单的size,对应3.1中的LE Read White List Size Command,该命令不需要参数,可直接使用,如下:

root@android:/ # hcitool lewlsz
hcitool lewlsz
White list size: 26

2)hcitool lewlclr,情况controller的白名单,对应3.1中的LE Clear White List Command,该命令也不需要参数,可直接使用,如下:

root@android:/ # hcitool lewlclr
hcitool lewlclr

3)hcitool lewladd,将指定设备添加到白名单中,对应3.1中的LE Add Device To White List Command,其格式如下:

root@android:/ # hcitool lewladd --help
hcitool lewladd --help
Usage:
        lewladd [--random]

其中是必选项,为要添加的蓝牙设备的地址。地址有public和random两种,默认是public,如果需要添加random类型的地址,则要指定--random参数,例如:

root@android:/ # hcitool lewladd 22:22:21:CD:F4:58
hcitool lewladd 22:22:21:CD:F4:58

root@android:/ # hcitool lewladd --random 11:22:33:44:55:66
hcitool lewladd --random 11:22:33:44:55:66

4)hcitool lewlrm,将指定设备从白名单中移除,对应3.1中的LE Remove Device From White List Command,该命令只需要蓝牙地址作为参数,如下:

root@android:/ # hcitool lewlrm --help
hcitool lewlrm --help
Usage:
        lewlrm

5)hcitool lecc,连接BLE设备的命令,对应3.2中的LE Create Connection Command,可以连接指定地址的设备,也可以直接连接白名单中的设备:

root@android:/ # hcitool lecc --help
hcitool lecc --help
Usage:
        lecc [--random]
        lecc --whitelist

一般情况下,我们都是通过hcitool lecc 的方式连接蓝牙设备,不过如果我们需要连接白名单中的设备,可直接使用如下命令:

hcitool lecc --whitelist

6)hcitool cmd,对于其它没有直接提供hcitool命令的HCI操作,我们可以使用hcitool cmd直接发送命令,其使用方法如下:

root@android:/ # hcitool cmd --help
hcitool cmd --help
Usage:
        cmd [parameters]
Example:
        cmd 0x03 0x0013 0xAA 0x0000BBCC 0xDDEE 0xFF

其中ogf、ocf和parameters可以去蓝牙spec的“HCI COMMANDS AND EVENTS”章节查询。需要注意的是,parameters可以使用各种类型(8位、16位、32位),还是很方便的。

4.3 测试步骤

这里仅仅罗列一个简单的测试,步骤包括:

1)设备A作为Advertising设备,不使用白名单,发送正常的ADV_IND(可连接、可扫描)广播包。

2)设备B扫描并连接设备A(应该可以正常连接)。

3)设备A作为Advertising设备,启用白名单,设置Advertising_Filter_Policy为0x2(只允许白名单中的设备连接),且没有把B的地址添加到白名单中。

4)设备B扫描并连接设备A(应该不可以正常连接)。

5)设备A把设备B添加到白名单中,其它策略保持不变。

6)设备B扫描并连接设备A(应该可以正常连接)。

详细步骤如下(我没有测试,有问题的请大家留言告诉我):
1)设备A作为Advertising设备,不使用白名单,发送正常的ADV_IND(可连接、可扫描)广播包。

# disable BLE advertising
hcitool cmd 0x08 0x000A 0x00

# 设置广播参数和广播策略
# Advertising_Interval_Min=0x0800 (1.28 second, default)
# Advertising_Interval_Max=0x0800 (1.28 second, default)
# Advertising_Type=0x00(ADV_IND, default)
# Own_Address_Type=0x00(Public Device Address, default)
# Peer_Address_Type=0x00(Public Device Address, default)
# Peer_Address=00 00 00 00 00 00 (no use)
# Advertising_Channel_Map=0x07(all channels enabled, Default)
# Advertising_Filter_Policy=0x0(禁用白名单)
hcitool -i hci0 cmd 0x08 0x0006 0x0800 0x0800 0x00 0x00 0x00 00 00 00 00 00 00 0x07 0x00

# enable BLE advertising
hcitool cmd 0x08 0x000A 0x01

# set advertising data to Eddystone UUID(可参考[3]中的介绍)
hcitool -i hci0 cmd 0x08 0x0008 1e 02 01 06 03 03 aa fe 17 16 aa fe 00 -10 00 01 02 03 04 05 06 07 08 09 0a 0b 0e 0f 00 00 00 00

2)设备B扫描并连接设备A(应该可以正常连接)。

hcitool lescan
hcitool lecc [bdaddr of A]

3)设备A作为Advertising设备,启用白名单,设置Advertising_Filter_Policy为0x2(只允许白名单中的设备连接),且没有把B的地址添加到白名单中。

# disable BLE advertising
hcitool cmd 0x08 0x000A 0x00

# 设置广播参数和广播策略
# …
# Advertising_Filter_Policy=0x2(只允许白名单中的设备连接)
hcitool -i hci0 cmd 0x08 0x0006 0x0800 0x0800 0x00 0x00 0x00 00 00 00 00 00 00 0x07 0x02

# 清空白名单
hcitool lewlclr

# 随便加一个地址到白名单
hcitool lewladd 11:22:33:44:55:66


# enable BLE advertising
hcitool cmd 0x08 0x000A 0x01

# set advertising data to Eddystone UUID(可参考[3]中的介绍)
hcitool -i hci0 cmd 0x08 0x0008 1e 02 01 06 03 03 aa fe 17 16 aa fe 00 -10 00 01 02 03 04 05 06 07 08 09 0a 0b 0e 0f 00 00 00 00

4)设备B扫描并连接设备A(应该不可以正常连接)。

hcitool lescan
hcitool lecc [bdaddr of A]

5)设备A把设备B添加到白名单中,其它策略保持不变。

# disable BLE advertising
hcitool cmd 0x08 0x000A 0x00

# 设置广播参数和广播策略
# …
# Advertising_Filter_Policy=0x2(只允许白名单中的设备连接)
hcitool -i hci0 cmd 0x08 0x0006 0x0800 0x0800 0x00 0x00 0x00 00 00 00 00 00 00 0x07 0x02

# 将B添加到白名单中
hcitool lewladd [bdaddr of B]


# enable BLE advertising
hcitool cmd 0x08 0x000A 0x01

# set advertising data to Eddystone UUID(可参考[3]中的介绍)
hcitool -i hci0 cmd 0x08 0x0008 1e 02 01 06 03 03 aa fe 17 16 aa fe 00 -10 00 01 02 03 04 05 06 07 08 09 0a 0b 0e 0f 00 00 00 00

6)设备B扫描并连接设备A(应该可以正常连接)。

hcitool lescan
hcitool lecc [bdaddr of A]

5. 参考文档

[1] 蓝牙协议分析(6)_BLE地址类型

[2] 蓝牙协议分析(5)_BLE广播通信相关的技术分析

[3] 玩转BLE(1)_Eddystone beacon

[4] 蓝牙协议分析(7)_BLE连接有关的技术分析

[5] Core_v4.2.pdf

 

原创文章,转发请注明出处。蜗窝科技,www.wowotech.net


ARM64的__create_page_tables代码走读

$
0
0

一、前言

本文没有什么框架性的东西,就是按照__create_page_tables代码的执行路径走读一遍,记录在初始化阶段,内核是如何创建内核运行需要的页表过程。想要了解一些概述性的、框架性的东西可以参考内存初始化文档。

本文的代码来自ARM64,内核版本是4.4.6,此外,阅读本文最好熟悉ARMv8中翻译表描述符的格式。

 

二、create_table_entry

这个宏定义主要是用来创建一个中间level的translation table中的描述符。如果用linux的术语,就是创建PGD、PUD或者PMD的描述符。如果用ARM64术语,就是创建L0、L1或者L2的描述符。具体创建哪一个level的Translation table descriptor是由tbl参数指定的,tbl指向了该translation table的内存。virt参数给出了要创建地址映射的那个虚拟地址,shift参数以及ptrs参数是和具体在哪一个entry中写入描述符有关。我们知道,在定位页表描述的时候,我们需要截取虚拟地址中的一部分做为offset(index)来定位描述符,实际上,虚拟地址右移shift,然后截取ptrs大小的bit field就可以得到entry index了。tmp1和tmp2是临时变量。create_table_entry的代码如下:

.macro    create_table_entry, tbl, virt, shift, ptrs, tmp1, tmp2
lsr    \tmp1, \virt, #\shift
and    \tmp1, \tmp1, #\ptrs - 1    // table index-------------------(1)
add    \tmp2, \tbl, #PAGE_SIZE-------------------------(2)
orr    \tmp2, \tmp2, #PMD_TYPE_TABLE---------------------(3)
str    \tmp2, [\tbl, \tmp1, lsl #3]--------------------------(4)
add    \tbl, \tbl, #PAGE_SIZE---------------------------(5)
.endm

(1)tmp1中保存virt地址对应在Translation table中的entry index。

(2)初始阶段的页表页表定义在链接脚本中,如下:

BSS_SECTION(0, 0, 0)

. = ALIGN(PAGE_SIZE);
idmap_pg_dir = .;
. += IDMAP_DIR_SIZE;
swapper_pg_dir = .;
. += SWAPPER_DIR_SIZE;

初始阶段的页表(PGD/PUD/PMD/PTE)都是排列在一起的,每一个占用一个page。也就是说,如果create_table_entry当前操作的是PGD,那么tmp2这时候保存了下一个level的页表,也就是PUD了。

(3)这一步是合成描述符的数值。光有下一级translation table的地址不行,还要告知该描述符是否有效(bit 0),该描述符的类型是哪一种类型(bit 1)。对于中间level的页表,该描述符不可能是block entry,只能是table type的描述符,因此该描述符的最低两位是0b11。

#define PMD_TYPE_TABLE        (_AT(pmdval_t, 3) << 0)

(4)这是最关键的一步,将描述符写入页表中。之所以有“lsl #3”操作,是因为一个描述符占据8个Byte。

(5)将translation table的地址移到next level,以便进行下一步设定。

 

三、create_pgd_entry

从字面上看,create_pgd_entry似乎是用来在PGD中创建一个描述符,但是,实际上该函数不仅仅创建PGD中的描述符,如果需要下一级的translation table,例如PUD、PMD,也需要同时建立,最终的要求是能够完成所有中间level的translation table的建立(其实每个table中都是只建立了一个描述符),仅仅留下PTE,由其他代码来完成。该函数需要四个参数:tbl是pgd translation table的地址,具体要创建哪一个地址的描述符由virt指定,tmp1和tmp2是临时变量,create_pgd_entry具体代码如下:

    .macro    create_pgd_entry, tbl, virt, tmp1, tmp2
    create_table_entry \tbl, \virt, PGDIR_SHIFT, PTRS_PER_PGD, \tmp1, \tmp2-------(1)
#if SWAPPER_PGTABLE_LEVELS > 3------------------------(2)
    create_table_entry \tbl, \virt, PUD_SHIFT, PTRS_PER_PUD, \tmp1, \tmp2--------(3)
#endif
#if SWAPPER_PGTABLE_LEVELS > 2------------------------(4)
    create_table_entry \tbl, \virt, SWAPPER_TABLE_SHIFT, PTRS_PER_PTE, \tmp1, \tmp2
#endif
    .endm

(1)create_table_entry 在上一节已经描述了,这里通过调用该函数在PGD中为虚拟地址virt创建一个table type的描述符。

(2)SWAPPER_PGTABLE_LEVELS这个宏定义和ARM64_SWAPPER_USES_SECTION_MAPS相关,而这个宏在蜗窝已经有一篇文章描述,这里就不说了。SWAPPER_PGTABLE_LEVELS其实定义了swapper进程地址空间的页表的级数,可能3,也可能是2,具体中间的Translation table有多少个level是和配置相关的,如果是section mapping,那么中间level包括PGD和PUD就OK了,PMD是最后一个level。如果是page mapping,那么需要PGD、PUD和PMD这三个中间level,PTE是最后一个level。当然,如果整个page level是3或者2的时候,也有可能不存在PUD或者PMD这个level。

(3)当SWAPPER_PGTABLE_LEVELS > 3的时候,需要创建PUD这一级的Translation table。

(4)当SWAPPER_PGTABLE_LEVELS > 2的时候,需要创建PMD这一级的Translation table。

上面太枯燥了,我们给出一些实例:

例子1:当虚拟地址是48个bit,4k page size,这时候page level等于4,映射关系是PGD(L0)--->PUD(L1)--->PMD(L2)--->Page table(L3)--->page,但是如果采用了section mapping(4k的page一定会采用section mapping),映射关系是PGD(L0)--->PUD(L1)--->PMD(L2)--->section。在create_pgd_entry函数中将创建PGD和PUD这两个中间level。

例子2:当虚拟地址是48个bit,16k page size(不能采用section mapping),这时候page level等于4,映射关系是PGD(L0)--->PUD(L1)--->PMD(L2)--->Page table(L3)--->page。在create_pgd_entry函数中将创建PGD、PUD和PMD这三个中间level。

例子3:当虚拟地址是39个bit,4k page size,这时候page level等于3,映射关系是PGD(L1)--->PMD(L2)--->Page table(L3)--->page。由于是4k page,因此采用section mapping,映射关系是PGD(L1)--->PMD(L2)--->section。在create_pgd_entry函数中将创建PGD这一个中间level。

 

四、create_block_map

create_block_map的名字起得不错,该函数就是在tbl指定的Translation table中建立block descriptor以便完成address mapping。具体mapping的内容是将start 到 end这一段VA mapping到phys开始的PA上去,代码如下:

    .macro    create_block_map, tbl, flags, phys, start, end
    lsr    \phys, \phys, #SWAPPER_BLOCK_SHIFT
    lsr    \start, \start, #SWAPPER_BLOCK_SHIFT
    and    \start, \start, #PTRS_PER_PTE - 1    // table index
    orr    \phys, \flags, \phys, lsl #SWAPPER_BLOCK_SHIFT    // table entry
    lsr    \end, \end, #SWAPPER_BLOCK_SHIFT
    and    \end, \end, #PTRS_PER_PTE - 1        // table end index
9999:    str    \phys, [\tbl, \start, lsl #3]        // store the entry
    add    \start, \start, #1            // next entry
    add    \phys, \phys, #SWAPPER_BLOCK_SIZE        // next block
    cmp    \start, \end
    b.ls    9999b
    .endm

 

五、__create_page_tables

1、准备阶段

__create_page_tables:
    adrp    x25, idmap_pg_dir------------------------(1)
    adrp    x26, swapper_pg_dir
    mov    x27, lr

    mov    x0, x25-----------------------------(2)
    add    x1, x26, #SWAPPER_DIR_SIZE
    bl    __inval_cache_range

    mov    x0, x25-----------------------------(3)
    add    x6, x26, #SWAPPER_DIR_SIZE
1:    stp    xzr, xzr, [x0], #16
    stp    xzr, xzr, [x0], #16
    stp    xzr, xzr, [x0], #16
    stp    xzr, xzr, [x0], #16
    cmp    x0, x6
    b.lo    1b

    ldr    x7, =SWAPPER_MM_MMUFLAGS-----------------(4)

(1)取idmap_pg_dir这个符号的物理地址,保存到x25。取swapper_pg_dir这个符号的物理地址,保存到x26。这段代码没有什么特别要说明的,除了adrp这条指令。adrp是计算指定的符号地址到run time PC值的相对偏移(不过,这个offset没有那么精确,是以4K为单位,或者说,低12个bit是0)。在指令编码的时候,立即数(也就是 offset)占据21个bit,此外,由于偏移计算是按照4K进行的,因此最后计算出来的符号地址必须要在该指令的-4G和4G之间。由于执行该指令的 时候,还没有打开MMU,因此通过adrp获取的都是物理地址,当然该物理地址的低12个bit是全零的。此外,由于在链接脚本中 idmap_pg_dir和swapper_pg_dir是page size aligned,因此使用adrp指令也是OK的。

(2)这段代码是要进行invalid cache的操作了,具体要操作的范围就是identity mapping和kernel image mapping所对应的页表区域,起始地址是idmap_pg_dir,结束地址是swapper_pg_dir+SWAPPER_DIR_SIZE。

为什么要调用__inval_cache_range来invalidate idmap_pg_dir和swapper_pg_dir对应页表空间的cache呢?根据boot protocol,代码执行到此,对于cache的要求是kernel image对应的那段空间的cache line是clean到PoC的,不过idmap_pg_dir和swapper_pg_dir对应页表空间不属于kernel image的一部分,因此其对应的cacheline很可能有一些旧的,无效的数据,必须要清理掉。

(3)将idmap和swapper页表内容设定为0是有意义的。实际上这些translation table中的大部分entry都是没有使用的,PGD和PUD都是只有一个entry是有用的,而PMD中有效的entry数目是和mapping的地 址size有关。将页表内容清零也就是意味着将页表中所有的描述符设定为invalid(描述符的bit 0指示是否有效,等于0表示无效描述符)。

(4)要创建mapping除了需要VA和PA,还需要memory attribute的参数,这个参数定义如下:

#if ARM64_SWAPPER_USES_SECTION_MAPS
#define SWAPPER_MM_MMUFLAGS    (PMD_ATTRINDX(MT_NORMAL) | SWAPPER_PMD_FLAGS)
#else
#define SWAPPER_MM_MMUFLAGS    (PTE_ATTRINDX(MT_NORMAL) | SWAPPER_PTE_FLAGS)
#endif

为了理解这些定义,需要理解block type和page type的描述符的格式,大家自行对照ARMv8文档,这里就不贴图了。SWAPPER_MM_MMUFLAGS这个flag其实定义了要映射地址的memory attribut。对于kernel image这一段内存,当然是普通内存,因此其中的MT_NORMAL就是表示后续的地址映射都是为normal memory而创建的。其他的flag定义如下:

#define SWAPPER_PTE_FLAGS    (PTE_TYPE_PAGE | PTE_AF | PTE_SHARED)
#define SWAPPER_PMD_FLAGS    (PMD_TYPE_SECT | PMD_SECT_AF | PMD_SECT_S)

PMD_SECT_AF(PTE_AF)中的AF是access flag的缩写,这个bit用来表示该entry是否第一次使用(当程序访问对应的page或者section的时候,就会使用该entry,如果从来没有被访问过,那么其值等于0,否者等于1)。该bit主要被操作系统用来跟踪一个page是否被使用过(最近是否被访问),当该page首次被创建的时候,AF等于0,当代码第一次访问该page的时候,会产生MMU fault,这时候,异常处理函数应该设定AF等于1,从而阻止下一次访问该page的时候产生MMU Fault。在这里,kernel image对应的page,其描述符的AF bit都设定为1,表示该page当前状态是actived(最近被访问),因为只有用户空间进程的page才会根据AF bit来确定哪些page被swap out,而kernel image对应的page是always actived的。

PMD_SECT_S(PTE_SHARED)对应shareable attribute bits,这个两个bits定义了该page的shareable attribute。那么是shareable attribute呢?shareable attribute定义了memory location被多个系统中的bus master共享的属性。具体定义如下:

SH[1:0] Normal memory
00 Non-shareable
01 无效
10 outer shareable
11 inner shareble

这里memory attribute中SH被设定为0b11,即inner shareable。如果一个page被标注为inner shareable,那么在inner shareable domain中,所有的bus mast访问该page中的memory都是coherent的(HW会处理cache coherence问题),软件不需要考虑cache。一般而言,所有cpu core组成了inner shareable domain,也就是说,对于kernel direct mapping而言,其对应的内存对所有的cpu core的访问都是coherent的。

memory attribute中其他的flag都没有显式指定,也就是说它们的值都是0,我们可以简单过一下。AP的值是0,表示该page对kernel mode(EL1)是read/write的,对于userspace(EL0),是不允许访问的。nG bit是0,表示该地址翻译是全局的,不是process-specific的,这也合理,内核page的映射当然是全局的了。

2、建立identity mapping

    mov    x0, x25                -------------------------(1)
    adrp    x3, __idmap_text_start        --------------------(2)

#ifndef CONFIG_ARM64_VA_BITS_48---------------------(3)
#define EXTRA_SHIFT    (PGDIR_SHIFT + PAGE_SHIFT - 3)-----------(4)
#define EXTRA_PTRS    (1 << (48 - EXTRA_SHIFT)) ---------------(5)


#if VA_BITS != EXTRA_SHIFT-------------------------(6)
#error "Mismatch between VA_BITS and page size/number of translation levels"
#endif

    adrp    x5, __idmap_text_end-------------------------(7)
    clz    x5, x5
    cmp    x5, TCR_T0SZ(VA_BITS)    ----------------------(8)
    b.ge    1f

    adr_l    x6, idmap_t0sz---------------------------(9)
    str    x5, [x6]
    dmb    sy
    dc    ivac, x6

    create_table_entry x0, x3, EXTRA_SHIFT, EXTRA_PTRS, x5, x6--------(10)
1:
#endif

    create_pgd_entry x0, x3, x5, x6-----------------------(11)
    mov    x5, x3                // __pa(__idmap_text_start)
    adr_l    x6, __idmap_text_end        // __pa(__idmap_text_end)
    create_block_map x0, x7, x3, x5, x6---------------------(12)

(1)x0保存了idmap_pg_dir变量的物理地址,也就是identity mapping的PGD。

(2)x3保存了__idmap_text_start的物理地址,对于identity mapping而言,x3也保存了虚拟地址,因为虚拟地址是等于物理地址的。

(3)基本上创建identity mapping是没有什么大问题的,但是,如果物理内存的地址位于非常高的位置,那么在进行identity mapping就有问题了,因为有可能你配置的VA_BITS不够大,超出了虚拟地址的范围。这时候,就需要扩展virtual address range了。当然,如果配置了48bits的VA_BITS就不存在这样的问题了,因为ARMv8最大支持的VA BITS就是48个,根本不可能扩展了。

(4)在虚拟地址地址不是48 bit,而系统内存的物理地址又放到了非常非常高的位置,这时候,为了完成identity mapping,我们必须要扩展虚拟地址,那么扩展多少呢?扩展到48个bit。扩展之后,增加了一个EXTRA的level,地址映射关系是EXTRA--->PGD--->……,其中EXTRA_SHIFT等于(PGDIR_SHIFT + PAGE_SHIFT - 3)。

(5)扩展之后,地址映射多个一个level,我们称之EXTRA level,该level的Translation table中有多少个entry呢?EXTRA_PTRS给出了答案。

(6)其实现行的linux kernel中,对地址映射是有要求的,即要求PGD是满的。例如:48 bit的虚拟地址,4k的page size,对应的映射关系是PGD(9-bit)+PUD(9-bit)+PMD(9-bit)+PTE(9-bit)+page offset(12-bit),对于42bit的虚拟地址,64k的page size,对应的映射关系是PGD(13-bit)+ PTE(13-bit)+ page offset(16-bit)。这两种例子有一个共同的特点就是PGD中的entry数目都是满的,也就是说索引到PGD的bit数目都是PAGE_SIZE-3。如果不满足这个关系,linux kernel会认为你的配置是有问题的。注意:这是内核的要求,实际上ARM64的硬件没有这么要求。

正因为正确的配置下,PGD都是满的,因此扩展之后EXTRA_SHIFT一定是等于VA_BITS的,否则一定是你的配置有问题。我们延续上一个实例来说明如何扩展虚拟地址的bit数目。对于42bit的虚拟地址,64k的page size,扩展之后,虚拟地址是48个bit,地址映射关系是EXTRA(6-bit)+ PGD(13-bit)+ PTE(13-bit)+ page offset(16-bit)。

(7)x5保存了__idmap_text_end的物理地址,之所以这么做是因为需要确定identity mapping的最高的物理地址,计算该物理地址的前导0有多少个,从而可以判断该地址是否是位于物理地址空间中比较高的位置。

(8)宏定义TCR_T0SZ可以计算给定虚拟地址数目下,前导0的个数。如果虚拟地址是48的话,那么前导0是16个。如果当前物理地址的前导0的个数(x5的值)还有小于当前配置虚拟地址的前导0的个数,那么就需要扩展。

(9)OK,现在进入需要扩展的分支,当然,具体要配置虚拟地址是通过TCR_EL1寄存器中的T0SZ域进行的,现在还不是时候(具体的设定在__cpu_setup函数中),这里,我们只要设定idmap_t0sz这个变量值就OK了,在__cpu_setup函数中会从该变量取值并设定到TCR_EL1寄存器中的。代码中,x6是idmap_t0sz变量的物理地址,x5是物理地址前导0的个数,将其保存到idmap_t0sz变量中。

(10)创建extra translation table的entry。具体传递的参数如下:

x0:页表地址idmap_pg_dir

x3:准备映射的虚拟地址(虽然x3保存的是物理地址,但是identity mapping嘛,VA和PA都是一样的)

EXTRA_SHIFT:正常建立最高level mapping的时候, shift是PGDIR_SHIFT,但是,由于物理地址位置太高,需要额外的映射,因此这里需要再加上一个level的mapping,因此shift需要PGDIR_SHIFT + (PAGE_SHIFT - 3)。

EXTRA_PTRS:增加了一个level的Translation table,我们需要确定这个增加level的Translation table中包含的描述符的数目,EXTRA_PTRS给出了这个参数。

(11)create_pgd_entry这个函数上面解释过了,建立各个中间level的table描述符。

(12)创建最后一个level translation table的entry。该entry可能是page descriptor,也可能是block descriptor,具体传递的参数如下:

x0:指向最后一个level的translation table

x7:要创建映射的memory attribute

x3:物理地址

x5:虚拟地址的起始地址(其实和x3一样)

x6:虚拟地址的结束地址

 

3、创建kernel direct mapping

mov    x0, x26                ------------------------(1)
mov    x5, #PAGE_OFFSET-----------------------(2)
create_pgd_entry x0, x5, x3, x6---------------------(3)
ldr    x6, =KERNEL_END            // __va(KERNEL_END)
mov    x3, x24                // phys offset
create_block_map x0, x7, x3, x5, x6 -------------------(4)


mov    x0, x25
add    x1, x26, #SWAPPER_DIR_SIZE
dmb    sy
bl    __inval_cache_range

mov    lr, x27
ret

(1)swapper_pg_dir其实就是swapper进程(pid等于0的那个,其实就是idle进程)的地址空间,这时候,x0指向了内核地址空间的PGD的基地址。

(2)PAGE_OFFSET是kernel image的首地址,对于48bit的VA而言,该地址是0xffff8000-00000000

(3)创建PAGE_OFFSET(即kernel image首地址,虚拟地址)对应中间level的table描述符。

(4)创建PAGE_OFFSET~KERNEL_END之间地址映射的最后一个level的描述符。

 

参考文献:

1、ARMv8技术手册

2、Linux 4.4.6内核源代码

 

原创文章,转发请注明出处。蜗窝科技

X-015-KERNEL-串口驱动开发之驱动框架

$
0
0

1. 前言

在过去的一段时间里,蜗窝上发表了一系列的关于内核各个子系统的分析文章,设备模型、device tree、中断子系统、clock framework、电源管理、GPIO、pinctrl等等,这些文章重理论、轻实践。随着“X Project”的进行,我们渐渐有机会把这些缺失的实践慢慢补回来。

串口驱动是进入Linux kernel之后最先遭遇的一个驱动,虽然不是很复杂,但要素齐全,使用到了kernel的各个子系统。因此我希望能借助串口驱动的开发过程,将这些子系统的使用一一串联起来,并记录为一份份单独的文章。

本文是这一系列文章的第一篇,借助串口驱动开发,学习Linux设备模型之下的驱动框架。

2. 驱动开发框架

Linux kernel中大部分设备可以归结为平台设备,因此大部分的驱动是平台驱动(patform driver)。得益于设备模型,Linux kernel平台驱动的开发有了一套非常固定的框架,总结如下(具体可参考[1][2]):

1)模块的入口和出口

用于注册/注销platform driver,这一部分的代码基本固定,包括函数和变量的命名方式也可固定,如下:

static int __init AAA_BBB_init(void)
{
    int ret;

    pr_info("%s\n", __func__);

    ret = platform_driver_register(&AAA_BBB_platform_driver);
    if (ret < 0) {
        pr_err("AAA_BBB_platform_driver register failed, ret = %d\n", ret);
        return ret;
    }
    return 0;
}

static void __exit AAA_BBB_exit(void)
{
    pr_info("%s\n", __func__);

    platform_driver_unregister(&AAA_BBB_platform_driver);
}

module_init(AAA_BBB_init);
module_exit(AAA_BBB_exit);

MODULE_ALIAS("platform driver: AAA_BBB");
MODULE_DESCRIPTION("BBB driver for xxxx");
MODULE_AUTHOR("wowo");
MODULE_LICENSE("GPL v2");

其中AAA一般可以是厂商名、芯片名、芯片的系列名、等等,例如TI OMAP系列的芯片,AAA可以是omap。BBB一般是模块名,以本文的串口驱动为例,可以是serial或者uart。因此AAA_BBB可以替换为omap_serial。编写驱动的时候,直接将上面的copy出来,查找替换即可。

2)platform driver

基本的platform driver包含三要素:struct platform_driver变量、probe/remove函数、用于和device tree匹配的match table,如下:

static const struct of_device_id AAA_BBB_of_match[] = {
    {
        .compatible    = "xxx,xxx-BBB",
    },
    {
    },
};
MODULE_DEVICE_TABLE(of, AAA_BBB_of_match);

static int AAA_BBB_probe(struct platform_device *pdev)
{
    int ret = 0;

    const struct of_device_id *match;

    dev_info(&pdev->dev, "%s\n", __func__);

    match = of_match_device(AAA_BBB_of_match, &pdev->dev);
    if (!match) {
        dev_err(&pdev->dev, "Error: No device match found\n");
        return -ENODEV;
    }

    return ret;
}

static int AAA_BBB_remove(struct platform_device *pdev)
{
    dev_info(&pdev->dev, "%s\n", __func__);

    return 0;
}

static struct platform_driver AAA_BBB_platform_driver = {
    .probe        = AAA_BBB_probe,
    .remove        = AAA_BBB_remove,
    .driver        = {
        .name    = "AAA_BBB",
        .of_match_table = AAA_BBB_of_match,
    },
};

同理,上面的AAA_BBB可以直接替换。另外有一点需要注意,AAA_BBB_of_match中的.compatible需要和DTS文件中的compatible对应,一般格式是“厂商名称,芯片系列-模块名”,例如“actions,s900-serial”    。

3)头文件包含

上面的代码,使用到了很多kernel API,需要包含如下头文件:

#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/platform_device.h>
#include <linux/module.h>
#include <linux/of.h>
#include <linux/of_device.h>

4)设备描述

需要在dts文件中,增加platform driver所要驱动设备的描述,例如:

    BBBx: BBB@xxxxxxxx {
        compatible = "xxx,xxx-BBB";
    };

其中BBBx为dts节点的别名,用于方便在它处引用,为可选项。@xxxxxxxx一般为该模块的I/O起始地址,如果没有,可以写任意名称(或者留空),compatible需要和上面AAA_BBB_of_match对应,例如“actions,s900-serial” 。

3. 串口驱动

基于上面描述,我们以Bubblegum-96平台的串口为例,编写如下代码(基本上都是使用查找替换操作,%s/AAA/owl/g, %s/BBB/serial/g):

https://github.com/wowotechX/linux/commit/ff4955f391cd211ff85588f46b05099065ddeb07

注1:由于代码非常简单,这里就不解释了,有疑问的同学,可以直接在github上指定的代码行添加评论。

注2:这里的串口驱动和serial early console[3]共用一份代码,为了方便调试,在串口驱动ready之前,我们暂时保留early console原封不动。

注3:开始写代码的时候,我们一般会为这个工作新建一个分支,在这个分支上工作OK的话,再merge回主分支,这也是代码开发的一个基本规则

pengo@DESKTOP-CH8SB7C:~/work/xprj/linux$ git fetch origin
pengo@DESKTOP-CH8SB7C:~/work/xprj/linux$ git branch -a
* x_integration
  remotes/origin/HEAD -> origin/x_integration
  remotes/origin/x_integration
pengo@DESKTOP-CH8SB7C:~/work/xprj/linux$ git rebase origin/x_integration
pengo@DESKTOP-CH8SB7C:~/work/xprj/linux$ git checkout -b serial_dev
Switched to a new branch 'serial_dev'
pengo@DESKTOP-CH8SB7C:~/work/xprj/linux$ git branch
* serial_dev
  x_integration

完成代码后,编译并运行kernel,通过earlycon的打印,查看probe函数是否被执行。

4. 参考文档

[1] Linux设备模型(8)_platform设备

[2] Linux设备模型(5)_device和device driver

[3] X-012-KERNEL-serial early console的移植

 

原创文章,转发请注明出处。蜗窝科技,www.wowotech.net

X-016-KERNEL-串口驱动开发之uart driver框架

$
0
0

1. 前言

本文是“X Project”串口驱动开发的第二篇,将以“bubblegum-96”开发板为例,介绍在linux serial framework的框架下,编写串口driver以及console driver的方法和步骤(暂不涉及实现细节)。

注1:有关串口、TTY、console等概念,可参考本站“TTY子系统[1]”的文章。Linux serial framework的分析,会在后续的文档中补充(这里故意颠倒,以便让大家理解kernel framework的妙处)。

2. 硬件说明

我们在前面“u-boot串口驱动移植[2]”以及"linux serial early console移植[3]”的时候,主要以“bubblegum-96”开发板的UART5为例。为了简单,本文也基于UART5,并做到和其它串口兼容。有关“bubblegum-96”开发板UART5的硬件介绍,可以参考[2],这里不再详细说明了。

3. 串口驱动的移植步骤

3.1 定义并注册uart driver

在linux serial framework中,uart driver是一个平行于platform driver的概念,用于驱动“虚拟”的“串口”设备。举例来说:

假如一个soc中有5个串口控制器(也可称作uart控制器,后面我们不再区分),每个uart控制器都可引出一个串口(uart port)。那么:

每个uart控制器,都是一个platform device,由[5]中介绍的dts文件的一个node描述。而这5个platform device,可由同一个driver驱动,即[5]介绍的platform driver。

相对于uart控制器实实在在的存在,我们更为熟悉的串口(uart port),则是虚拟的设备,它们由“struct uart_port”描述(后面会介绍),并在platform driver的probe接口中,注册到kernel。它们可由同一个driver驱动,即这里所说的uart driver。

uart driver一般长这个样子(AAA和BBB的含义可参考[5]):

#define aaa_bbb_MAXIMUM            5

static struct uart_driver AAA_BBB_driver = {
    .owner        = THIS_MODULE,
    .driver_name    = "AAA_BBB",
    .dev_name    = "ttyS",
    .cons        = NULL,
    .nr        = aaa_bbb_MAXIMUM,
};

其中.cons表示该driver对应的console driver,第4章会补充介绍。.nr表示该uart driver最大支持的uart port个数,根据硬件情况,我们这里定义为5。

uart driver的注册和注销和[5]中介绍的platform driver一样,具体可参考后面的patch文件。

3.2 注册uart port

前面提到过,platform device代表uart控制器,是实体抽象。对应的,uart port代表“串口”,是虚拟抽象。因此,我们需要在platform device probe的时候(platform driver的probe接口),动态分配并注册一个uart port(struct uart_port)。

由于在后续的串口操作中,都是以uart port指针为对象,所以在通常情况下,为了保存一些自定义的信息,我们会定义一个包含struct uart_port的数据结构----struct AAA_uart_port,如下:

struct AAA_uart_port {
        struct uart_port                port;

        /* others, TODO */
};

在probe的时候,会分配struct AAA_uart_port类型的指针,然后初始化并注册其中的port变量(这是driver开发的惯用伎俩,大家慢慢就会熟悉),例如:

struct AAA_uart_port *Aup;
struct uart_port *port;

Aup = devm_kzalloc(&pdev->dev, sizeof(*Aup), GFP_KERNEL);
if (Aup == NULL) {
        dev_err(&pdev->dev, "Failed to allocate memory for Aup\n");
        return -ENOMEM;
}
platform_set_drvdata(pdev, Aup);
port = &Aup->port;

port->dev = &pdev->dev;
port->type = PORT_AAA;
port->ops = &AAA_uart_ops;

注2:这里使用devm_kzalloc,有关devm_xxx的说明,可参考[6]。另外我们会调用platform_set_drvdata将新申请的Aup指针保持在pdev的drvdata中,以便后续需要使用的时候再取出(例如remove的时候),这也是driver开发的惯用伎俩。

struct uart_port结构在抽象一个虚拟的“串口”的同时,也保存了该“串口”的一些常用属性,我们需要在probe的时候将这些属性保存在新申请的指针中,包括:

port->dev,对应的struct device指针;

port->type,该串口的类型,是以PORT_为前缀的一个宏定义,大家可以根据需要在include/uapi/linux/serial_core.h中定义;

port->ops,该串口的操作函数集,后面会介绍;

port->iotype,该串口的IO类型,我们常用的通过寄存器访问的uart控制器,选用UPIO_MEM32即可;

port->membase,对应MEM类型的串口,保持它的寄存器基址,一般是从DTS中解析得到的;

port->irq,该串口对应的中断号, 一般是从DTS中解析得到的;

port->line,该串口的编号,和串口字符设备的次设备号等有关,后面文章会重点介绍;

等等。

本文先定义一些必要的,其它的会随着功能的完善,逐步添加,具体可参考后面的patch文件[8]

初始化完之后,直接调用uart_add_one_port接口,将该port添加到kernel serial core即可,如下(第一个参数为上面3.1注册的uart driver指针):

ret = uart_add_one_port(&AAA_BBB_driver, port);
if (ret < 0) {
        dev_err(&pdev->dev, "Failed to add uart port, err %d\n", ret);
        return ret;
}

3.3 定义并实现uart ops

struct uart_ops结构包含了各式各样的uart端口的操作函数,需要在添加uart port的时候提供。开始移植的时候,我们可以只实现部分接口(暂时留空都可以),至少应包括:

static struct uart_ops AAA_uart_ops = {
        .startup = AAA_BBB_startup,
        .shutdown = AAA_BBB_shutdown,
        .start_tx = AAA_BBB_start_tx,
        .stop_tx = AAA_BBB_stop_tx,
        .stop_rx = AAA_BBB_stop_rx,
        .tx_empty = AAA_BBB_tx_empty,
        .set_mctrl = AAA_BBB_set_mctrl,
        .set_termios = AAA_BBB_set_termios,
};

有关这些操作函数的具体含义,我会在下一篇文章中介绍。

4. console驱动的移植步骤

在嵌入式开发的过程中,我们通常会从SOC上众多串口中选择一个,当作console设备,以方便开发和调试。在Linux serial framework的框架下,编写一个console驱动是非常简单的。步骤如下:

1)定义struct console变量

有关struct console结构的描述可参考[7],下面是一个例子:

static struct console AAA_console = {
        .name   = "ttyS",
        .write  = AAA_console_write,
        .device = uart_console_device,
        .setup  = AAA_console_setup,
        .flags  = CON_PRINTBUFFER,
        .index  = -1,
        .data   = &AAA_BBB_driver,
};

重点字段的解释如下:

.name字段决定console的名称,command line传入的时候,需要和该名称匹配,例如“console=ttyS0”;

.index用来指定该console使用哪一个uart port(对应3.2小节中的port->line),这里使用-1,kernel会自动选择第一个uart port。后面有需要的时候我们再改;

.data可以用来保存console的私有数据,这里我们把struct uart_driver指针保存下来,后面有用;

.setup和.write是两个需要实现的回调函数,我们可以先留空,具体请参考[8]中的patch。

2)将console变量的指针保存在struct uart_driver变量的.cons字段中,如下:

static struct console AAA_console;

static struct uart_driver AAA_BBB_driver = {
        …
        .cons           = &AAA_console,
        …
};

3)OK,移植完成。当我们执行3.2小节所介绍的add port的时候,serial core会自动比较uart driver---->cons---->index和uart port---->line,如果匹配,则调用register_console帮忙注册console驱动。

5. 参考文档

[1] Linux kernel TTY子系统

[2] X-004-UBOOT-串口驱动移植(Bubblegum-96平台)

[3] X-012-KERNEL-serial early console的移植

[4] Documentation/serial/driver

[5] X-015-KERNEL-串口驱动开发之驱动框架

[6] Linux设备模型(9)_device resource management

[7] Linux TTY framework(5)_System console driver

[8] 和本文对应的patch文件,https://github.com/wowotechX/linux/commit/d072d177c9a88e57eb7c5f18c424b96f0ce6d2d5

 

原创文章,转发请注明出处。蜗窝科技,www.wowotech.net

内存布局代码走读

$
0
0

一、前言

同样的,本文是内存初始化文章的一份补充文档,希望能够通过这样的一份文档,细致的展示在初始化阶段,Linux 4.4.6内核如何从device tree中提取信息,完成内存布局的任务。具体的cpu体系结构选择的是ARM64。

 

二、memory type region的构建

memory type是一个memblock模块(内核初始化阶段的内存管理模块)的术语,memblock将内存块分成两种类型:一种是memory type,另外一种是reserved type,分别用数组来管理系统中的两种类型的memory region。本小节描述的是系统如何在初始化阶段构建memory type的数组。

1、扫描device tree

在完成fdt内存区域的地址映射之后(fixmap_remap_fdt),内核会对fdt进行扫描,以便完成memory type数组的构建。具体代码位于setup_machine_fdt--->early_init_dt_scan--->early_init_dt_scan_nodes中:

void __init early_init_dt_scan_nodes(void)
{
    of_scan_flat_dt(early_init_dt_scan_chosen, boot_command_line); ------(1)
    of_scan_flat_dt(early_init_dt_scan_root, NULL); 
    of_scan_flat_dt(early_init_dt_scan_memory, NULL);-------------(2)
}

(1)of_scan_flat_dt函数是用来scan整个device tree,针对每一个node调用callback函数,因此,这里实际上是针对设备树中的每一个节点调用early_init_dt_scan_chosen函数。之所以这么做是因为device tree blob刚刚完成地址映射,还没有展开,我们只能使用这种比较笨的办法。这句代码主要是寻址chosen node,并解析,将相关数据放入到boot_command_line。

(2)概念同上,不过是针对memory node进行scan。

2、传统的命令行参数解析

int __init early_init_dt_scan_chosen(unsigned long node, const char *uname, int depth, void *data)
{
    int l;
    const char *p;

    if (depth != 1 || !data ||
        (strcmp(uname, "chosen") != 0 && strcmp(uname, "chosen@0") != 0))
        return 0; -------------------------------(1)

    early_init_dt_check_for_initrd(node); --------------------(2)

    /* Retrieve command line */
    p = of_get_flat_dt_prop(node, "bootargs", &l);
    if (p != NULL && l > 0)
        strlcpy(data, p, min((int)l, COMMAND_LINE_SIZE)); ------------(3)


#ifdef CONFIG_CMDLINE
#ifndef CONFIG_CMDLINE_FORCE
    if (!((char *)data)[0])
#endif
        strlcpy(data, CONFIG_CMDLINE, COMMAND_LINE_SIZE);
#endif /* CONFIG_CMDLINE */ -----------------------(4)


    return 1;
}

(1)上面我们说过,early_init_dt_scan_chosen会为device tree中的每一个node而调用一次,因此,为了效率,不是chosen node的节点我们必须赶紧闪人。由于chosen node是root node的子节点,因此其depth必须是1。这里depth不是1的节点,节点名字不是"chosen"或者chosen@0和我们毫无关系,立刻返回。

(2)解析chosen node中的initrd的信息

(3)解析chosen node中的bootargs(命令行参数)并将其copy到boot_command_line。

(4)一般而言,内核有可能会定义一个default command line string(CONFIG_CMDLINE),如果bootloader没有通过device tree传递命令行参数过来,那么可以考虑使用default参数。如果系统定义了CONFIG_CMDLINE_FORCE,那么系统强制使用缺省命令行参数,bootloader传递过来的是无效的。

3、memory node解析

int __init early_init_dt_scan_memory(unsigned long node, const char *uname, int depth, void *data)
{
    const char *type = of_get_flat_dt_prop(node, "device_type", NULL);
    const __be32 *reg, *endp;
    int l; 
    if (type == NULL) {
        if (!IS_ENABLED(CONFIG_PPC32) || depth != 1 || strcmp(uname, "memory@0") != 0)
            return 0;
    } else if (strcmp(type, "memory") != 0)
        return 0; -----------------------------(1)

    reg = of_get_flat_dt_prop(node, "linux,usable-memory", &l);
    if (reg == NULL)
        reg = of_get_flat_dt_prop(node, "reg", &l);---------------(2)
    if (reg == NULL)
        return 0;

    endp = reg + (l / sizeof(__be32)); --------------------(3)

    while ((endp - reg) >= (dt_root_addr_cells + dt_root_size_cells)) {
        u64 base, size;

        base = dt_mem_next_cell(dt_root_addr_cells, ®);
        size = dt_mem_next_cell(dt_root_size_cells, ®); ----------(4)

        early_init_dt_add_memory_arch(base, size);--------------(5)
    }

    return 0;
}

(1)如果该memory node是root node的子节点的话,那么它一定是有device_type属性并且其值是字符串”memory”。不是的话就可以返回了。不过node没有定义device_type属性怎么办?大部分的平台都可以直接返回了,除了PPC32,对于这个平台,如果memory node是更深层次的节点的话,那么它是没有device_type属性的,这时候可以根据node name来判断。当然,目标都是一致的,不是自己关注的node就赶紧闪人。

(2)该memory node的物理地址信息保存在"linux,usable-memory"或者"reg"属性中(reg是我们常用的)

(3)l / sizeof(__be32)是reg属性值的cell数目,reg指向第一个cell,endp指向最后一个cell。

(4)memory node的reg属性值其实就是一个数组,数组中的每一个entry都是base address和size的二元组。解析reg属性需要两个参数,dt_root_addr_cells和dt_root_size_cells,这两个参数分别定义了root节点的子节点(比如说memory node)reg属性中base address和size的cell数目,如果等于1,基地址(或者size)用一个32-bit的cell表示。对于ARMv8,一般dt_root_addr_cells和dt_root_size_cells等于2,表示基地址(或者size)用两个32-bit的cell表示。

注:dt_root_addr_cells和dt_root_size_cells这两个参数的解析在early_init_dt_scan_root中完成。

(5)针对该memory mode中的每一个memory region,调用early_init_dt_add_memory_arch向系统注册memory type的内存区域(实际上是通过memblock_add完成的)。

4、解析memory相关的early option

setup_arch--->parse_early_param函数中会对early options解析解析,这会导致下面代码的执行:

static int __init early_mem(char *p)
{

    memory_limit = memparse(p, &p) & PAGE_MASK;

    return 0;
}
early_param("mem", early_mem);

在过去,没有device tree的时代,mem这个命令行参数传递了memory bank的信息,内核根据这个信息来创建系统内存的初始布局。在ARM64中,由于强制使用device tree,因此mem这个启动参数失去了本来的意义,现在它只是定义了memory的上限(最大的系统内存地址),可以限制DTS传递过来的内存参数。

 

三、reserved type region的构建

保留内存的定义主要在fixmap_remap_fdt和arm64_memblock_init函数中进行,我们会按照代码顺序逐一进行各种各样reserved type的memory region的构建。

1、保留fdt占用的内存,代码如下:

void *__init fixmap_remap_fdt(phys_addr_t dt_phys)
{……

    memblock_reserve(dt_phys, size);

……}

fixmap_remap_fdt主要是为fdt建立地址映射,在该函数的最后,顺便就调用memblock_reserve保留了该段内存。

2、保留内核和initrd占用的内容,代码如下:

void __init arm64_memblock_init(void)
{
    memblock_enforce_memory_limit(memory_limit); ----------------(1)
    memblock_reserve(__pa(_text), _end - _text);------------------(2)
#ifdef CONFIG_BLK_DEV_INITRD
    if (initrd_start)
        memblock_reserve(__virt_to_phys(initrd_start), initrd_end - initrd_start);------(3)
#endif
……}

(1)我们前面解析了DTS的memory节点,已经向系统加入了不少的memory type的region,当然reserved memory block也会有一些,例如DTB对应的memory就是reserved。memory_limit可以对这些DTS的设定给出上限,memblock_enforce_memory_limit函数会根据这个上限,修改各个memory region的base和size,此外还将大于memory_limit的memory block(包括memory type和reserved type)从列表中删掉。

(2)reserve内核代码、数据区等(_text到_end那一段,具体的内容可以参考内核链接脚本)

(3)保留initital ramdisk image区域(从initrd_start到initrd_end区域)

3、通过early_init_fdt_scan_reserved_mem函数来分析dts中的节点,从而进行保留内存的动作,代码如下:

void __init early_init_fdt_scan_reserved_mem(void)
{
    int n;
    u64 base, size;

    if (!initial_boot_params)------------------------(1)
        return;

    /* Process header /memreserve/ fields */
    for (n = 0; ; n++) {
        fdt_get_mem_rsv(initial_boot_params, n, &base, &size);--------(2)
        if (!size)
            break;
        early_init_dt_reserve_memory_arch(base, size, 0);-----------(3)
    }

    of_scan_flat_dt(__fdt_scan_reserved_mem, NULL);------------(4)
    fdt_init_reserved_mem();
}

(1)initial_boot_params实际上就是fdt对应的虚拟地址。在early_init_dt_verify中设定的。如果系统中都没有有效的fdt,那么没有什么可以scan的,return,走人。

(2)分析fdt中的 /memreserve/ fields ,进行内存的保留。在fdt的header中定义了一组memory reserve参数,其具体的位置是fdt base address + off_mem_rsvmap。off_mem_rsvmap是fdt header中的一个成员,如下:

struct fdt_header {
……
    fdt32_t off_mem_rsvmap;------/memreserve/ fields offset
……};

fdt header中的memreserve可以定义多个,每个都是(address,size)二元组,最后以0,0结束。

(3)保留每一个/memreserve/ fields定义的memory region,底层是通过memblock_reserve接口函数实现的。

(4)对fdt中的每一个节点调用__fdt_scan_reserved_mem函数,进行reserved-memory节点的扫描,之后调用fdt_init_reserved_mem函数进行内存预留的动作,具体参考下一小节描述。

4、解析reserved-memory节点的内存,代码如下:

static int __init __fdt_scan_reserved_mem(unsigned long node, const char *uname,
                      int depth, void *data)
{
    static int found;
    const char *status;
    int err;

    if (!found && depth == 1 && strcmp(uname, "reserved-memory") == 0) { -------(1)
        if (__reserved_mem_check_root(node) != 0) {
            pr_err("Reserved memory: unsupported node format, ignoring\n"); 
            return 1;
        }
        found = 1; ---------------------------------(2)
        return 0;
    } else if (!found) { 
        return 0; ----------------------------------(3)
    } else if (found && depth < 2) { -------------------------(4)
        return 1;
    }

    status = of_get_flat_dt_prop(node, "status", NULL); ----------------(5)
    if (status && strcmp(status, "okay") != 0 && strcmp(status, "ok") != 0)
        return 0;

    err = __reserved_mem_reserve_reg(node, uname); ----------------(6)
    if (err == -ENOENT && of_get_flat_dt_prop(node, "size", NULL))
        fdt_reserved_mem_save_node(node, uname, 0, 0); ---------------(7)

    /* scan next node */
    return 0;
}

(1)found 变量记录了是否搜索到一个reserved-memory节点,如果没有,我们的首要目标是找到一个reserved-memory节点。reserved-memory节点的特点包括:是root node的子节点(depth == 1),node name是"reserved-memory",这可以过滤掉一大票无关节点,从而加快搜索速度。

(2)reserved-memory节点应该包括#address-cells、#size-cells和range属性,并且#address-cells和#size-cells的属性值应该等于根节点对应的属性值,如果检查通过(__reserved_mem_check_root),那么说明找到了一个正确的reserved-memory节点,可以去往下一个节点了。当然,下一个节点往往是reserved-memory节点的subnode,也就是真正的定义各段保留内存的节点。更详细的关于reserved-memory的设备树定义可以参考Documentation\devicetree\bindings\reserved-memory\reserved-memory.txt文件。

(3)没有找到reserved-memory节点之前,of_scan_flat_dt会不断的遍历下一个节点,而在__fdt_scan_reserved_mem函数中返回0表示让搜索继续,如果返回1,表示搜索停止。

(4)如果找到了一个reserved-memory节点,并且完成了对其所有subnode的scan,那么是退出整个reserved memory的scan过程了。

(5)如果定义了status属性,那么要求其值必须要是ok或者okay,当然,你也可以不定义该属性(这是一般的做法)。

(6)定义reserved memory有两种方法,一种是静态定义,也就是定义了reg属性,这时候,可以通过调用__reserved_mem_reserve_reg函数解析reg的(address,size)的二元数组,逐一对每一个定义的memory region进行预留。实际的预留内存动作可以调用memblock_reserve或者memblock_remove,具体调用哪一个是和该节点是否定义no-map属性相关,如果定义了no-map属性,那么说明这段内存操作系统根本不需要进行地址映射,也就是说这块内存是不归操作系统内存管理模块来管理的,而是归于具体的驱动使用(在device tree中,设备节点可以定义memory-region节点来引用在memory node中定义的保留内存,具体可以参考reserved-memory.txt文件)。

(7)另外一种定义reserved memory的方法是动态定义,也就是说定义了该内存区域的size(也可以定义alignment或者alloc-range进一步约定动态分配的reserved memory属性,不过这些属性都是option的),但是不指定具体的基地址,让操作系统自己来分配这段memory。

5、预留reserved-memory节点的内存

device tree中的reserved-memory节点及其子节点静态或者动态定义了若干的reserved memory region,静态定义的memory region起始地址和size都是确定的,因此可以立刻调用memblock的模块进行内存区域的预留,但是对于动态定义的memory region,__fdt_scan_reserved_mem只是将信息保存在了reserved_mem全局变量中,并没有进行实际的内存预留动作,具体的操作在fdt_init_reserved_mem函数中,代码如下:

void __init fdt_init_reserved_mem(void)
{
    int i;

    __rmem_check_for_overlap(); -------------------------(1)

    for (i = 0; i < reserved_mem_count; i++) {--遍历每一个reserved memory region
        struct reserved_mem *rmem = &reserved_mem[i];
        unsigned long node = rmem->fdt_node;
        int len;
        const __be32 *prop;
        int err = 0;

        prop = of_get_flat_dt_prop(node, "phandle", &len);---------------(2)
        if (!prop)
            prop = of_get_flat_dt_prop(node, "linux,phandle", &len);
        if (prop)
            rmem->phandle = of_read_number(prop, len/4);

        if (rmem->size == 0)----------------------------(3)
            err = __reserved_mem_alloc_size(node, rmem->name,
                         &rmem->base, &rmem->size);
        if (err == 0)
            __reserved_mem_init_node(rmem);--------------------(4)
    }
}

(1)检查静态定义的 reserved memory region之间是否有重叠区域,如果有重叠,这里并不会对reserved memory region的base和size进行调整,只是打印出错信息而已。

(2)每一个需要被其他node引用的node都需要定义"phandle", 或者"linux,phandle"。虽然在实际的device tree source中看不到这个属性,实际上dtc会完美的处理这一切的。

(3)size等于0的memory region表示这是一个动态分配region,base address尚未定义,因此我们需要通过__reserved_mem_alloc_size函数对节点进行分析(size、alignment等属性),然后调用memblock的alloc接口函数进行memory block的分配,最终的结果是确定base address和size,并将这段memory region从memory type的数组中移到reserved type的数组中。当然,如果定义了no-map属性,那么这段memory会从系统中之间删除(memory type和reserved type数组中都没有这段memory的定义)。

(4)保留内存有两种使用场景,一种是被特定的驱动使用,这时候在特定驱动的初始化函数(probe函数)中自然会进行处理。还有一种场景就是被所有驱动或者内核模块使用,例如CMA,per-device Coherent DMA的分配等,这时候,我们需要借用device tree的匹配机制进行这段保留内存的初始化动作。有兴趣的话可以看看RESERVEDMEM_OF_DECLARE的定义,这里就不再描述了。

6、通过命令行参数保留CMA内存

arm64_memblock_init--->dma_contiguous_reserve函数中会根据命令行参数进行CMA内存的保留,本文暂不描述之,留给CMA文档吧。

 

四、总结

物理内存布局是归于memblock模块进行管理的,该模块定义了struct memblock memblock这样的一个全局变量保存了memory type和reserved type的memory region list。而通过这两个memory region的数组,我们就知道了操作系统需要管理的所有系统内存的布局情况。

 

原创文章,转发请注明出处。蜗窝科技

X-017-KERNEL-串口驱动开发之serial console

$
0
0

1. 前言

本文是“X Project”串口驱动开发的第三篇,在第二篇“uart driver框架[1]”的基础上,实现console驱动,并借助这个过程,理解如下知识:

1)从DTS regs字段中获取设备的I/O基址,并map出来供driver访问。这是device tree最基本的使用场景。

2)从DTS aliases中获取串口的索引号。这是device tree aliases功能的一个应用场景。

3)uart port—>line和console—>index之间的关系。

2. 从DTS regs字段中获取设备的I/O基址

大家应该记得我们在移植串口的“early console[2]”的时候,需要使用UART5的寄存器地址,那时直接在代码中用宏定义出来了,如下:

#define UART5_BASE (0xE012a000)

这种做法有一个明显的缺点:driver不具有通用性,如果寄存器地址换了怎么办?如果换个串口呢?如果同时支持多个串口呢?这时device tree的优势就体现出来了,driver可以在probe的时候动态的从dts中获取当前设备的寄存器地址,步骤如下:

1)在dts文件中增加regs字段

serial5: serial@e012a000 { 
     compatible = "actions,s900-serial"; 
+    reg = <0 0xe012a000 0 0x2000>;    /* UART5_BASE */ 
};

其格式由dts文件的#address-cells和 #size-cells决定,它们都是2的时候,表示UART5的寄存器从0x00000000e012a000开始,size为0x0000000000002000。

2)在driver probe接口中,以IORESOURCE_MEM为参数,调用platform_get_resource接口,以struct resource的形式,将上面定义的寄存器信息取出来,并保存在uart port的mapbase指针中

+    resource = platform_get_resource(pdev, IORESOURCE_MEM, 0); 
+    if (!resource) { 
+        dev_err(&pdev->dev, "No IO memory resource\n");
+        return -ENODEV; 
+    } 
+    port->mapbase = resource->start;

其中“IORESOURCE_MEM”表示要获取的资源类型是memory资源,0表示是第一个(如果有多个,可以以此传入1、2等等)。

3)调用devm_ioremap_resource接口,将资源map出来,driver就可以使用了

+    port->membase = devm_ioremap_resource(&pdev->dev, resource); 
+    if (IS_ERR(port->membase)) { 
+        dev_err(&pdev->dev, "Failed to map memory resource\n"); 
+        return PTR_ERR(port->membase); 
+    }
+    port->iotype = UPIO_MEM32;

同样,map出来之后,可以保存在uart port的membase指针中,同时需要把iotype设置为UPIO_MEM32。

3. 从DTS aliases中获取串口的索引号

由于linux serial framework允许一个serial driver支持多个uart port,为了方便,我们会为这些串口编号,具体体现在struct uart_port的line变量中,那用什么方法为port->line赋值呢?最好通过device tree,因为这样灵活啊。下面介绍一种通过aliases的方法。

以本文的UART5为例,dts文件中的节点名为:serial@e012a000,为了方便,我们给它添加了一个别名:serial5,这样就可以在别处引用。为了让driver可以获得该串口的编号(5),我们可以在dts中定义一个aliases,如下:

+    aliases { 
+        serial5 = &serial5; 
+    };

然后,在driver的probe中,调用of_alias_get_id,就可以获得该编号:

+    port->line = of_alias_get_id(pdev->dev.of_node, "serial");
+    if (port->line < 0) { 
+        dev_err(&pdev->dev, "failed to get alias id, errno %d\n", 
+            port->line); 
+        return port->line; 
+    }

of_alias_get_id的参数为“serial”,就是上面aliases左边除了数字的部分。

注1:大家可以思考一下这种方法的好处。

4. 完善console driver并测试之

1)把console driver的index也改成5

     .flags    = CON_PRINTBUFFER,
-    .index    = -1, 
+    .index    = 5, 
     .data    = &owl_serial_driver, 

2)实现console driver的write接口

实现方法和[2]中的类似,只不过寄存器的基址是动态map出来的(具体可参考后面的patch文件[3])。另外需要说明的是,driver中的操作对象都是struct uart_port指针,但console的write接口是个例外,它使用struct console指针。也就是说,我们需要一个办法从struct console指针中得到对应的struct uart_port指针。不着急,有办法(代码如下,大家自行理解即可):

static void owl_console_write(struct console *con, const char *s, unsigned n) 

+    struct uart_driver *driver = con->data; 
+    struct uart_port *port = driver->state[con->index].uart_port;
 
 
+    uart_console_write(port, s, n, owl_console_putchar); 
}

3)在kernel command line选中我们新增的console

-CONFIG_CMDLINE="earlycon=owl_serial loglevel=8" 
+CONFIG_CMDLINE="earlycon=owl_serial console=ttyS5 loglevel=8"

4)编译并运行,获得如下打印(early console被禁止了,ttyS5被使能了),console移植成功

[    0.087281] workingset: timestamp_bits=60 max_order=19 bucket_order=0
[    0.093281] owl_serial_init
[    0.096062] owl_serial e012a000.serial: owl_serial_probe
[    0.101343] e012a000.serial: ttyS5 at MMIO 0xe012a000 (irq = 0, base_baud = 0) is a unknown
[    0.109656] console [ttyS5] enabled
[    0.109656] console [ttyS5] enabled
[    0.116593] bootconsole [owl_serial0] disabled
[    0.116593] bootconsole [owl_serial0] disabled

[    0.125468] Failed to find cpu0 device node
[    0.129593] Unable to detect cache hierarchy from DT for CPU 0
[    0.135874] Freeing unused kernel memory: 116K (ffffff80081a0000 - ffffff80081bd000)
[    0.143093] This architecture does not have kernel memory protection.
[    0.149562] Kernel panic - not syncing: No working init found.  Try passing init= option to kernel. See Linux Documentation/init.txt for guidance.
[    0.162593] Kernel Offset: disabled
[    0.166062] Memory Limit: none
[    0.169093] ---[ end Kernel panic - not syncing: No working init found.  Try passing init= option to kernel. See Linux Documentation/init.txt for guidance.

注2:以上过程可参考[3]中的patch文件。

5. 参考文档

[1] X-016-KERNEL-串口驱动开发之uart driver框架

[2] X-012-KERNEL-serial early console的移植

[3] patch文件,https://github.com/wowotechX/linux/commit/9f4d974605fbc9312333f95a0cd2ae1e4f709941

 

原创文章,转发请注明出处。蜗窝科技,www.wowotech.net

Viewing all 218 articles
Browse latest View live


<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>