阅读875 返回首页    go 技术社区[云栖]


阿里内核月报2017年03月

内核的跟踪机制主要关注一些事件,这些事件通常与特定代码块的执行相关。但有趣的信息往往发生在事件之间。对于这种类型信息,我们最关心的变化就是监控事件切换时到底花费了多少时间。当然,其他信息也存在这样的问题。目前,对事件间的数值计算可以采用BPF程序或在用户空间处理;而在内核中,一个新的补丁集可能很快将用来执行这些计算。

Tom Zanussi的支持事件间计算的补丁集显然是专注于定时测量。这是他在histogram triggers直方图触发器工作(已经合并到kernel4.6开发周期中)的扩展。这项工作为事件中数据的存储提供了一个机制,但它只能做一件事:生成直方图。这种存储能力具有潜在的其他用途,可用于事件间的跟踪,但也有一些需要解决的问题。

其中的第一个就是如何安排一个事件数据存储在后续的事件中应用。例如有个补丁集包含sched_wakeup事件,该事件发生在内核决定一个睡眠进程变为可运行的,并将其唤醒。考虑以下命令:

   echo hist:keys=pid:ts0=common_timestamp.usecs \
        >> /sys/kernel/debug/tracing/events/sched/sched_wakeup/trigger

这个命令建立一种特殊的“直方图”在sched_wakeup事件上(用直方图机制的数据存储能力)。keys=pid 表示使用进程的ID来指明被唤醒的进程,而该key值与数据是放在一起的。而与key真正关联的数据用ts0=common_timestamp.usecs来指定。它创建了一个新的变量,ts0,记住当前事件被触发的时间。common_timestamp字段也是新的;它使得时间戳信息在任何事件上都可用。

当内核决定唤醒一个进程时上述过程就会记录;那么现在就要计算出这个进程真正在CPU上运行起来花费的时间。这个时间可以认为是这个进程的唤醒延迟时间。通常我们希望这个时间越短越好。在实际的配置当中,这个时间一定不能超过系统容忍的最大限度。进程的唤醒延迟时间可以通过使用sched_swith事件来计算出来,而这个事件一般在一个新的进程被授予CPU执行权时发生。相关的命令如下

   echo 'hist:keys=woken_pid=next_pid:woken_prio=next_prio:\
         wakeup_lat=common_timestamp.usecs-ts0:\
onmatch().trace(wakeup_latency)' \
         >> /sys/kernel/debug/tracing/events/sched/sched_switch/trigger

这里有必要说几件事情。keys=woken_pid=next_pid 字段使得next_pid事件(这个用来指定处理器接下来要处理的进程)可变,并且给他一个新的变量woken_pid,使用这个变量插入到直方图数据中。下一个字段,woken_prio=next_prio,存储了新基恩成的优先级。如果使用下面命令会变得更复杂:

   wakeup_lat=common_timestamp.usecs-ts0

保存在sched_wakeup事件的ts0时间戳会被重新调用,并从当前时间减去这个时间戳,产生了延迟时间。这个值会保存在新的变量里wakeup_lat

上述的onmatch()命令表明了计算出的延迟该如何回报。从两个独立的事件中计算出的延迟和这两个事件并不相关。因为该值不应该回报这两个事件。相反,补丁集给出了一个新的抽象概念叫synthetic event用来汇报这个计算出的、事件间的值。在这种情况下,可以使用以下命令:

 echo 'wakeup_latency lat=sched_switch:wakeup_lat \
                        pid=sched_switch:woken_pid \
                        prio=sched_switch:woken_prio' \
        >> /sys/kernel/debug/tracing/synthetic_events

这个命令创建了一个新的事件叫wakeup_latency。并创建了三个变量用于指向sched_switch事件上的变量。其中,wakeup_lat就是计算出来的延迟。
这里,我们再看下上面命令里的最后一部分

   onmatch().trace(wakeup_latency)

onmatch()函数用于查看直方图key里有没有可匹配到的(在这个例子里,key就是进程的PID),当匹配到的话,就会产生trace()中的synthetic event。这个事件和其他事件一样,它可以输出到用户空间或者用来产生一个直方图。

使用上述的这些命令,就可以监控系统里的唤醒延迟。相关的补丁集已经针对原来的命令进行了简化。想要了解更具体,可以查看the documentation(https://lwn.net/Articles/714516/)
目前这个补丁集已经收到了很多反馈,在这项工作merge前,上述给出的命令可能会改变。另外针对这些反馈,补丁集将会给出新的特性。不久后大家期待新的版本吧!

A resolution on control-group network filters

4.10合并窗口开始允许把BPF连接到一个control group上,BPF可以用做该cgroup网络包的filter。一月份的时候关于BPF API的很多问题被热烈讨论,这篇文章介绍其中一个问题的解决方案赶上了4.10窗口的末班车。

                     A
                      |
       --------|-------
       |                           |
       B                          C

一月份讨论的问题里一个热点是层次cgroup情况下filter如何发挥作用的问题。group A位于顶层,B和C是A的子组。对于B组的网络包来说,如果A和B cgroup上都有BPF filter,会发生什么情况呢?在当前的实现里,最底层的filter会被执行。结果是子组的任何filter能屏蔽掉高层cgroup的配置。有些时候需要这样的设置,有些时候则不是。

因为不是所有人都对这一点很满意,所以在最后时刻Alexei Starovoitov提交了一组对bpf()系统调用的修改,增加了一个新的flag BPF_F_ALLOW_OVERRIDE。带这个flag的filter允许子组的BPF filter覆盖它的效果。如果这个带flag的BPF filter挂在上层cgroup A上,B和C上的filter可以执行。如果cgroup A上的filter不带这个flag,不允许往子组上连接filter。

默认的行为是不设置该flag,这样管理起来更安全。只在必要的时候设置flag,子组才有机会挂起BPF filter。这个问题意见最大的开发者Andy Lutomirski对上述修改提出了一点意见,如果跟组的filter设置了BPF_F_ALLOW_OVERRIDE flag,那么所有的子组都需要带上这个flag。Starovoitov表示认同。

关于这个争议的故事至少在4.10版本到此为止。

Control-group thread mode

cgroup V2的开发已经进行几年了,大多数的controller现在已经可以和新接口共同工作,但在cpu contoller这边仍然进展不畅,主要原因还是cgroup V2提出的新进程模型。为了理解这个问题的本质,就有必要回顾一下cgroup V2对于旧结构到底有哪些不满,以及提出的修改建议是什么

  • 不满之一:目前cgroup controllers的hierarchy支持很混乱,有的controller完全支持(比如cpu),有的controller忽视,框架没有力量去阻止controller的这些混乱行为。
  • 不满之二:cgroup框架的release_agent机制的实现太陈旧,它是依靠usermode_helper去实现的,而做的事情仅仅是想把一个消息(“这个group要被摧毁了“)传递给用户态程序。这可以通过更轻量级的方式去做,特别是目前mainline开发者已经同意未来去除usermode helper了,而cgroup这边还在肆无忌惮地往里加。
  • 不满之三:把cgroup虚拟文件系统和VFS搅在一块,这带来大量同步问题,尤其是在跨group操作时。

这些不满基本无关实质性的功能,对于大多数人来说纯粹是cgroup housekeeping性质的事情。Tejun提出的cgroup V2包括:

  • 去除mutiple hierarchy的支持,所有controler眼中会看到同样的hierarchy,你可以选择在某一结点上打开或者关闭一个
  • 特定的controller,但这不影响层次结构。
  • 所有controller都必须支持hierarchy。
  • cgroup框架管理的最小单元是进程而不是线程,一个进程中的所有线程必须做为一个整体去操作。
  • 不支持结点中同时包含进程和子树,一个结点里要么就只有纯粹的进程(因此这个结点必然是叶子结点),要么就只包含子树

这些修改带来的影响非常大,特别是对于那些原先就在hierarchy上天然地支持得很好的两个控制器,cpu和block来说,相当于在功能上带来了巨大的倒退。我们具体地来考察一下cpu,它支持得好的原因是CFS算法在概念上就是多层次的基于权重的轮转算法,CFS由sched entity组成的树型结构天然地与cgroup原先的线程模型契合,而cgroup V2的做法相当于为了获得工程实现上的稳定和便利,牺牲了一部分功能,强制地要求原本几乎无任何限制的sched entity树必须符合一定的形态要求。 这当然会使得一些用到这部分功能的用户不爽。

由于需要解决的问题并不是实现新的功能,而是对已有的功能做裁剪并找到各种开发者都满意的折衷方案,争论哪里可以多割一刀,哪里不能碰,所以所有的争论对于用户来说其实都是相当无聊的,本周lwn的最新文章就描述了Tejun提出的一种名为thread mode的方案,具体来说就是对于需要精细地在线程粒度做管理的cpu cgroup来说,它可以建立一个虚拟的结点,这个结点对于它来说下边包含各种代表线程的子结点,而对于其他不支持线程粒度管理的controller来说就只是一个代表进程的结点。我们在这里无意展开细聊这一还在讨论中的方案,它很可能和其他正在讨论中的方案一样稍纵即逝,不值得我们浪费时间。

本篇内核月报的编辑只想指出一个基本的观点:cgroup框架做为一个框架,对于用户来说本身其实没有任何功能可言,它的存在只是为了支持上边行使功能的controller。所有为了这个框架本身的便利,就妄图break userspace,甚至妄图劝说应用程序“改一点代码”的方案,都注定失败。

Per-task CPU-frequency control

对于手持设备来说,CPU频率控制一直是一个核心的设计点。过去几年,CPU频率控制与CPU调度器结合愈加紧密,这主要是因为调度器知道CPU当前的工作负载,便于根据负载调节频率。然而,调度器却不知哪些进程对用户最重要。对于特定进程,如手机中的前端进程,用户希望它快速运行;而其它任务,如某些手机中的后端进程,则希望限制其运行速度。遗憾的是,现有的电源管理方案不能识别进程之间的这种差异。

今年二月,Patrick Bellasi基于cpu cgroup,提出一种新的解决方案。添加了两个属性:分别为capacity_min和capacity_max,用于限制CPU频率的变化下界和上界。当cpu cgroup中有任务正在运行时,CPU频率不低于capacity_min,且不超过capacity_max。对于前端进程组,可以调高capacity_min,将capacity_max设置成最大值(1024);对于后端进程组,将capacity_min设置成最小值(0),并调低capacity_max。

当然,设置这两属性时,有一定限制:子组的capacity_min不能小于父组的capacity_min,capacity_max不能大于父组的capacity_max。若两个进程对电源的需求不同时,最好放到不同的cpu cgroup中,若强行将之放到同一个组,capacity_min和capacity_max都应该设置为二者需求的较大值。

必须注意的是,该patch只是众多方案中比较突出的一个,还并没有合入upstream,有待社区的进一步检验。

RCU and the mid-boot dead zone

本文的主要目的是介绍 RCU 和 mid-boot dead zone 的关系,以及一些需要注意的问题,如何让 RCU 可以在dead zone 这段时间也能工作的很好和若干经验分享。

RCU 的基础知识,这里不做介绍了, see: Documentation/RCU/whatisRCU.txt
首先,提出这样一个疑问,内核中各个子系统,会在不同的时间点初始化(see: start_kernel),RCU 子系统也是一样,但是如果其他的子系统在 RCU 初始化之前就已经初始化,并且这些子系统刚好还在使用 RCU 呢?

早期启动的时候,只有一个进程并且关闭抢占,RCU 相关的语义相当于 no-ops,这个阶段在各内核线程被 spawn 之后就宣告结束了。这个期间,我们叫 early boot,在这个阶段使用 RCU 是安全的,但其实并不表达 RCU 语义。

当 RCU 完全被初始化好之后(多个阶段初始化全部完毕),基本时间点是在 RCU kthreads 全部启动之后(early_initcall, 主要是处理 grace period,但实际真正完全工作需要在 core_initcall 的时候),我们管这以后叫,RCU run time,在这个阶段使用 RCU 也是安全的,而且是完整的 RCU 语义。

在 early boot 和 RCU run time 之间,叫 mid-boot dead zone,这段时间相对比较特殊,这个时候 RCU 语义属于半工作状态,在这段时间内使用 RCU 一个很大的区别在于,所有的 updater callbacks 会被推后执行,也就是需要等到 RCU run time 阶段, 其实还与配置有关,比如:CONFIG_PREEMPT_RCU (是否允许 reader 临界区被抢占)等,总之,需要 RCU 子系统自行处理掉各种 ”异常” 情况。

作者给了个在版本演进过程中的例子:synchronize_rcu_expedited 函数在 dead zone 的时候没有任何报错,所以其他子系统的开发者 (比如:ACPI) 很可能就认为它是正常工作的(注:expedited* 的语义主要是减少同步等待时间的,针对实时性要求比较高的场景,减少进程调度延迟,达到几十个微妙的量级,实际上最坏的情况比如:回调非常多,各个子系统大量使用 RCU, 可能延迟会在几百个毫秒或更长时间。原理上主要是通过 IPI 显示的通知,来看每个 CPU 是否上可以达到 quiescent 状态,不过这种方式对于本身在 RCU read 临界区处理慢没办法,一样的死等), 但是在 4.9 内核,作者把调用者进程对等待 grace period 的工作放到了 workqueue 里,主要是想避免 POSIX 信号的干扰 (workqueue kthread 默认是不处理信号的),但是 workqueue 的初始化比 RCU kthreads 的初始化早很多,这样很可能让,synchronize_rcu_expedited 提前工作了(这个时候 RCU 还没初始化完毕),#@$!!!$!$

修复这个问题,首先能想到的,让 RCU 的内核线程在启动进程之前就起来吧(听起来谁也没有它早了),但是这个受内核配置的影响,RCU 内核线程创建的时间点并不统一。第二个想法,用唯一一个 kthread 来 cover 住 expedited的语义,其他的非 expedited 语义至少在 dead zone 阶段直接映射到 expedited上来,这样似乎就都能提前进入工作状态了,作者实际也给了 patch 了,显然,这种方法的缺点也得让这个 kthread 在所有其他的子系统初始化之前就起来。作者最后让 expedited 的语义回到了 4.8 内核以前的情况,还是让 caller 进程自己等待 grace period,但是只是在 dead zone 阶段,之后当 RCU内核线程都起来之后,再切换回 workqueue。

所以看起来 dead zone 不存在了,really??

读者想了解 RCU internals, 需要看原始的一手的资料, i.e. 内核文档:Documentation/RCU/* (包括里面 lwn 的引用),但是注意文档里面的未必是最新的和实现对应的,里面有些我认为是过时的, 想真正了解的话,还是那句话,看代码吧。

Greybus

Linux内核从4.9开始引入了一个新的子系统----Greybus,本文简要介绍它的内部设计实现。
Greybus最初是为了Google的Ara智能手机项目(该项目目前已被终止)设计的,但第一个(也是唯一一个)产品发布则是摩托罗拉的Moto Mods。有一些关于可行性评估的讨论,希望可以将Greybus应用到其它方面,比如IoT和一些内核中平台无关的组件通信。

最初,Greg Kroah-Hartman尝试将Greybus核心代码合并到内核的driver目录下,但随着一些反对的声音,最终将其合并到了staging tree分支中。代码合并时,共计将近2400个补丁,开发周期超过2.5年,包括至少5个组织(Google,Linaro,BayLibre,LeafLabs,MMSolutions)的超过50个开发者贡献代码。有更多的开发者和公司加入开发Ara的软件和硬件的其它部分。Greybus的开发者占据了4.9发布的活跃开发者的top4。

Kroah-Hartman说Greybus的合并确保按照历史提交逐个完成:

因为这是一个长达两年半的工作,很多开发者做出了贡献,我不希望将所有他们的成果简单打包成几个patch,那样对他们来说非常不公平。

所以我重新建了一个git树,所有的更改逐个提交,最终合并入kernel树中,就像btrfs合并进内核一样。
Jonathan Corbet写了一个早期的文章,读者感兴趣可以从中获得更多早期的信息。

UniPro(Unified Protocol)和Greybus子系统的协议

Ara智能手机项目遵循“可定制化”设计。用户可以在一个模块超集中选择一个子集,提供用户感兴趣的功能(如:摄像头、喇叭、电池、显示及各种传感器等),并将这些功能集成到整个手机的框架中。这些模块可以基于UniPro总线与主处理器或其他模块直接通信。这个总线的规格由MIPI(Mobile Industry Processor Interface)联盟管理。UniPro遵循经典的OSI网络模型的体系结构,但没有对应用层进行定义。Greybus在该系统中即为其应用层。

UniPro通信是基于通信实体间的双向连接的,就像Ara智能手机中的模块,通信并不需要通过处理器进行。每个UniPro设备对外提供虚拟端口(Virtual Ports),端口可以视为该设备的子地址。设备包含一些端口,被称之为“连接端口”(Connection Ports或者CPorts)。总线上有一个交换设备,内部配置了设备间路由信息。消息可以以大概10Gb每秒的速率传递。总线同时支持消息优先级、错误处理和消息传递故障的通知机制,但UniPro不支持流和多播。

Greybus规格最初为了Ara智能手机编写的,因此从Ara的设计中吸取很多灵感,模块可以从手机整体框架中动态的插入和移除。为了使Greybus更好的适应其它的应用场景,在规格的的通用性方面做了大量的工作。你还会发现整个实现非常类似于Linux内核中USB框架,因为Greybus在开发过程中参考了USB框架。

Greybus规格中需要设备在系统运行期提供被发现和自描述功能,网络路由和自管理能力,支持类和桥接物理(bridged PHY)协议,设备通过这些功能可以与处理器或者其它设备进行通信。下图给出了内核中不同组件与Greybus子系统的交互。

Greybus框图:

Greybus核心实现了SVC(supervisory controller)协议,应用处理器(application processor,AP-运行Linux的处理器)可以基于SVC进行通信。SVC描述一个Greybus网络的一个实体,该实体进而可以配置和控制这个Greybus(UniPro)网络,这些操作大多数是基于AP完成的。所有模块的插入和删除时间首先被汇报给SVC,然后用SVC协议转发给AP。AP通过SVC管理Greybus网路。

在Ara智能手机开发的初期,还没有SoC可以提供内建的UniPro支持。独立的硬件实体被设计用来连接AP到UniPro网络。这些实体从AP接受消息,然后将其翻译并发送到UniPro网络。另外一个方向也是一样的:从UniPro接受消息,然后翻译后送至AP。这些实体被称为AP桥(APB -- AP Bridge)主控制器。它们可以通过USB接受消息然后发往UniPro,也可以反方向。事实上,AP不是Greybus网络的一部分,所以在上图中并没有AP。Greybus子系统甚至支持内建UniPro的处理器,处理器在上图中由Native UniPro host controllers(本地UniPro主控制器)表示。AP可以不需要USB子系统的情况下与其它实体直接通信。

在模块初始化阶段(Greybus已经完成模块的检测),Greybus对模块提供的描述文件进行解析,从而获得这个模块的最大支持能力,然后创建一个内核可以对外呈现的设备。

整个UniPro网络(包括AP、SVC和所有模块)的电源管理由Greybus负责。在系统挂起时,Greybus设置SVC和各模块进入低功耗状态;当系统需要恢复时,恢复整个Greybus网络。Greybus核心模块对所有的独立实体执行运行期的电源管理。例如:如果一个模块不在应用,Greybus会将其电源关掉,当需要它的时候重新开启。

Greybus同时将自己捆绑到Linux kernel的驱动部分,对外提供一个sysfs接口,接口路径为:/sys/bus/greybus。下图描述了sysfs的层次结构,该结构对应AP连接单个AP桥(APB)。结构中有一个通过APB访问的模块,该模块包含有一个接口,同时包含有两个捆绑设备。框图同时可以看出每个接口包含有一个控制端口(CPort),而每个APB包含有一个SVC,每个实体含有一个属性列表。所有这些实体下面会详细描述。

   greybus/
└── greybus1 (AP Bridge)
    ├── 1-2 (Module)
    │   ├── 1-2.2 (Interface)
    │   │   ├── 1-2.2.1 (Bundle)
    │   │   │   ├── bundle_class
    │   │   │   ├── bundle_id
    │   │   │   └── state
    │   │   ├── 1-2.2.2 (Bundle)
    │   │   │   ├── bundle_class
    │   │   │   ├── bundle_id
    │   │   │   └── state
    │   │   ├── 1-2.2.ctrl (Control CPort)
    │   │   │   ├── product_string
    │   │   │   └── vendor_string
    │   │   ├── ddbl1_manufacturer_id
    │   │   ├── ddbl1_product_id
    │   │   ├── interface_id
    │   │   ├── product_id
    │   │   ├── serial_number
    │   │   └── vendor_id
    │   ├── eject
    │   ├── module_id
    │   └── num_interfaces
    ├── 1-svc (SVC)
    │   ├── ap_intf_id
    │   ├── endo_id
    │   └── intf_eject
    └── bus_id

模块提供的功能通过设备类(device-class)和桥接物理驱动暴露出来。设备类驱动实现的协议,其目的是提供手机上可见的一般性功能的设备抽象,如:摄像头、电池、传感器等。桥接物理驱动实现的协议则是为了支持Greybus网络上的模块通信,这是与设备类协议所不同的。两者都包括集成电路应用不同的物理接口连接到UniPro上,例如:设备通过GPIO、I2C、SPI、USB等。如果一个模块仅仅实现了设备类协议,则被称之为设备类一致(device-class conformant)。如果模块实现了任何一种桥接物理协议,则称之为非设备类一致。设备类协议和桥接物理协议接下来会详细列出。

模块结构

在Greybus中,一个模块被看做是一个可以从该总线上静态或者动态链接/断开的物理硬件实体。一旦模块接入Greybus网络,AP和SVC会枚举模块类型并获取每个接口的描述并学习该模块的各项特性。下图简要描绘了Greybus子系统的结构:

在Linux内核中用struct gb_module来定义模块:

 <source lang='c'> struct gb_module {
   struct device dev;
   u8 module_id;
   size_t num_interfaces;
   struct gb_interface *interfaces[0];
   ...
} </source> 

其中,dev是该模块的设备结构,module_id为一个由SVC指定的8bit唯一数字,interfaces指向该模块支持的相关接口,num_interfaces则是该模块支持接口的数量。

Greybus模块有很多电气接口用以和移动电话基础架构进行连接。这些电气接口统称为“接口块(interface blocks)”,在软件层面用“接口(interface)”来表示。一个模块可以有一个或者多个接口。ID序号最小的接口被作为主接口,而其他接口被作为副接口。module_id即指向主接口。

主接口是作为AP接收模块插入事件的接口,同时模块的卸载也需要通过主接口来实现。而副接口可以用来实现各种不同的功能。Linux内核中使用gb_interface结构来描述接口:

<source lang='c'> struct gb_interface {
   struct device dev;
   struct gb_control *control;
   struct list_head bundles;
   struct list_head manifest_descs;
   u8 interface_id;
   struct gb_module *module;
   ...
}; </source>

其中,dev是设备结构,control用来表示控制连接(后面会详细介绍),bundles是一系列bundle的集合,manifest_descs是接口描述的集合,interface_id是该接口的唯一标识,而module则指向父模块结构。模块ID和接口ID均从0开始,并且在Greybus网络中是唯一的。

Greybus接口包含一个或者多个bundle,每个bundle都通过一个逻辑Greybus设备来表示。比如一个接口包含震动和电池两个功能,则对应会有两个bundle来描述上述的两个功能。每个bundle结构包含一个device结构并与Greybus驱动绑定。每个接口中的bundle ID是唯一的。Linux内核中使用gb_bundle结构来进行描述:

<source lang='c'> struct gb_bundle {
   struct device           dev;
   struct gb_interface     *intf;
   u8                      id;
   u8                      class;
   size_t                  num_cports;
   struct list_head        connections;
   ...
} </source> 

其中dev是bundle的设备结构,intf是指向interface的指针,id为bundle的唯一标识,class描述bundle的类型(如:摄像头,音频),connections是bundle中使用的连接,num_cports是连接的数量。

Greybus驱动使用如下结构来表示,其中的回调函数可以使用bundle结构作为参数:

<source lang='c'> struct greybus_driver {
   const char *name;
   int (*probe)(struct gb_bundle *bundle,
                const struct greybus_bundle_id *id);
   void (*disconnect)(struct gb_bundle *bundle);
   const struct greybus_bundle_id *id_table;
   struct device_driver driver;
}; </source>

其中name是Greybus驱动的名字,probe和disconnect是回调函数,id_table是设备bundle ID表,driver是通用设备驱动结构。

Greybus或者UniPro中的“连接(connection)”是在两个CPort之间的双向连接。一个bundle中可以有一个或者多个CPort。连接中进行的通信协议预先在Greybus协议中进行了规定。每个CPort针对一个特定的协议。每个接口中的CPort号是唯一的。每个接口中的第一个CPort为控制CPort(注意:CPort0不属于任何bundle),CPort从1开始进行编号。CPort0是一个特殊CPort,它被用来进行接口管理操作,由一个特殊控制协议管理。Linux内核中通过gb_connection结构来标识链接:

<source lang='c'> struct gb_connection {
   struct gb_host_device           *hd;
   struct gb_interface             *intf;
   struct gb_bundle                *bundle;
   u16                             hd_cport_id;
   u16                             intf_cport_id;
   struct list_head                operations;
   ...
}; </source>

其中hd表示APB桥接的模块,intf指向对应的接口,bundle指向对应的bundle结构,hd_cport_id表示APB的CPort ID,intf_cport_id表示接口的CPort ID,operations是通过连接执行的操作列表。连接通过hd_cport_id和intf_cport_id建立连接。

Greybus bundle可以用来表示复杂的功能,比如音频、视频。通常情况下一个带有多个组件(如:传感器、DMA控制器、音频、编码等)的复杂设备要通过一个bundle设备来表示是比较困难的。但Greybus确实是这样来表示一个设备的。模块中带有一个固件来让各个组件能够互相配合。通过连接AP可以同bundle进行通信。比如:一个表示摄像头的bundle会有两个连接:数据和管理连接。所有管理指令通过管理连接发送给模块。同时数据则通过数据连接进行传送。而各个组件之间如何协作都被隐藏在Greybus和AP背后了。

当模块和它相关的接口接入到Greybus网络(将模块插入移动电话),AP开始通过CPort0枚举所有接口,并从接口获取数据(称作接口描述,interface manifest),这些数据包含接口描述头和一组描述符。接口描述允许AP了解接口实现的功能。

下面是一个描述支持音频功能的接口描述的简单例子。描述文件通过Manifesto库被转换为二进制数据。下面例子中bundle有两个连接:“管理(management)”和“数据(data)”。描述文件中CPort0可以选择性进行添加。

<source lang='c'>
Simple Audio Interface Manifest
Provided under the three clause BSD license found in the LICENSE file.
[manifest-header] version-major = 0 version-minor = 1
[interface-descriptor] vendor-string-id = 1 product-string-id = 2
Interface vendor string
[string-descriptor 1] string = Project Ara
Interface product string
[string-descriptor 2] string = Simple Audio Interface
Bundle 1
Audio class
[bundle-descriptor 1] class = 0x12
Audio Management protocol on CPort 1
[cport-descriptor 1] bundle = 1 protocol = 0x12
Audio Data protocol on CPort 2
[cport-descriptor 2] bundle = 1 protocol = 0x13 </source>

Greybus消息

AP、SVC和模块之间的信息交换是基于UniPro消息实现的。正常情况,所有的信息流都是双向的,每个请求消息都会对应一个应答消息。具体哪个实体(AP、SVC或者模块)能够初始化请求消息,这决定于各自协议定义的操作。例如,只有AP能够对控制协议的操作进行初始化。也有些操作并不是双向,意味着接收者并不需要有回应消息。

每一个基于UniPro的消息都带有一个短头部,后面跟着操作说明的数据。消息头如下结构体:

<source lang='c'> struct gb_operation_msg_hdr {
   __le16  size;           /* Size in bytes of header + payload */
   __le16  operation_id;   /* Operation unique id */
   __u8    type;           /* E.g GB_I2C_TYPE_TRANSFER */
   __u8    result;         /* Result of request (in responses only) */
   __u8    pad[2];         /* must be zero (ignore when read) */
} </source>

这里面,size是消息头大小(8自己)和荷载数据大小的和。荷载数据大小由不同协议的每个操作定义。operation_id是16位数字的唯一标示,用于请求和应答消息的匹配,这使得很多操作可以在一次连接中同时处理。0是一个特殊的ID,被预留用于指代非双向的操作。type字段8字节,用于描述操作的类型。type数值的具体意义取决于使用的协议。每个给定的协议只有127个操作可用(0x01..0x7f),0x00被预留着。操作类型中最重要的一位(0x80)是用作一个标识,用于区分请求和应答消息。对请求消息,这一位是0,对应答消息,这一位是1。对请求消息,result需要忽略,对应答消息,result里面包含请求操作的结果。

Greybus消息(请求和应答)在linux内核中通过如下结构管理:

<source lang='c'> struct gb_message {
   struct gb_operation             *operation;
   struct gb_operation_msg_hdr     *header;
   void                            *payload;
   size_t                          payload_size;
   ...
}; </source>

这里operation是消息所属者的操作,header是基于UniPro发送消息的头部信息,payload是紧跟header发送的荷载,payload_size则是payload的大小。

一个完整的Greybus操作(一个请求和它对应的应答)在Linux内核中通过如下结构管理:

<source lang='c'> struct gb_operation {
   struct gb_connection    *connection;
   struct gb_message       *request;
   struct gb_message       *response;
   u8                      type;
   u16                     id;
   ...
}; </source>

这里,connection代表Unipro消息发送使用的连接,request和response代表Greybus消息,type和id在之前消息header里面已经描述过。

目前有很多用于发送/接收Greybus消息的帮助接口,单大多数使用者选择的是如下这个:

<source lang='c'> int gb_operation_sync_timeout(struct gb_connection *connection, int type,
                             void *request, int request_size, void *response,
                             int response_size, unsigned int timeout);
</source>

这里,connection表示的是Unipro消息发送使用的连接,type是操作类型,request是请求荷载,request_size是请求荷载的大小,response是给应答荷载的空间,response_size是期望的应答荷载的大小,timeout单位是毫秒,是操作超时时间,通常timeout设置为1ms。

gb_operation_sync_timeout()会创建操作和消息体,将请求荷载拷贝到请求消息里面,然后通过Greybus连接发送请求消息头和荷载。接着等待timeout毫秒,接收另外一端的应答或者接收到出错信息。一旦应答收到了,这个函数会首先检查应答头以确认操作的结果。如果result域表明有错误,gb_operation_sync_timeout()报错返回;否则拷贝应答荷载到response指针指定的内存,并销毁操作和消息结构体。这个函数返回0表示成功,负数表示有错误。

Greybus协议

Greybus协议定义了一个Greybus连接上所有消息的设计和语义。每个Greybus协议定义了一组操作,包含有他们对于的请求和应答消息格式。同时也定义了连接那一边可以初始化每个请求。Greybus协议大致可以划分为3类:专用协议,device class协议和bridged PHY协议。
专用协议是Greybus协议的核心,目前有两类专用协议:SVC和Control。

SVC协议用于AP和SVC之间通信。AP使用这个协议通过SVC控制网络。APB的CPort0用于SVC的连接(不要和每个模块接口的CPort0混淆,那个是用于控制协议)。这个协议的主要目的是帮助AP建立到各个Cports的路由,感知模块的插入和拔除等。Greybus上的模块不需要实现这个。基于这个协议定义的操作包括模块插入和移除消息,路由和连接的建立和销毁,等等。

control协议用于AP和模块接口之间的通信。AP通过这个协议控制各自的接口。这个协议的主要目的是用于帮助AP枚举新接口,并学习它的功能。在这个协议下,只有AP能够初始化操作(发生请求),模块必须回应那些请求。基于这个协议的一些操作允许获取接口的清单,或者控制bundle启动、关闭、休眠和恢复。

正如早前提到的,device-class协议为移动手机常见功能提供了一个设备抽象,例如声音管理协议或者相机管理协议。bridged PHY协议为Greybus网络上面那些不遵从已有的device class协议的模块提供通信,也包括使用修改的物理接口和UniPro之间。

未来

后面还有很多有趣的事情可以推进。首先,Greybus核心代码应该从staging tree移到内核的drivers目录。驱动本身应该移动到他们自己的框架上:例如.../greybus/gpio.c应该是.../gpio/gpio-greybus.c。这个需要花不少时间和努力。

之后,最好能够将Motorala的Moto Mods支持合并到内核,并包含它对Greybus子系统的改进。当然这个主要取决于Motorola团队。由于Ara项目已经没有继续,需要为Greybus子系统找到新的目标(例如物联网),并使得Greybus支持它们。目前已经有一些这方面的讨论正在进行。

最后更新:2017-06-07 19:02:07

  上一篇:go  【AI研究室】OtterTune来了,DBA会失业吗
  下一篇:go  6月7日云栖精选夜读:Spring-beans架构设计原理