閱讀875 返回首頁    go 阿裏雲 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架構設計原理