核心驅動程式內部原理¶
Surface 系統聚合器模組 (SSAM) 核心和 Surface 序列 Hub (SSH) 驅動程式的架構概述。有關 API 文件,請參閱
概述¶
SSAM 核心實現結構分層,在某種程度上遵循 SSH 協議結構
底層資料包傳輸在資料包傳輸層 (PTL)中實現,直接構建在核心的序列裝置 (serdev) 基礎設施之上。顧名思義,此層處理資料包傳輸邏輯,並處理諸如資料包驗證、資料包確認 (ACKing)、資料包(重傳)超時以及將資料包有效負載中繼到更高級別層之類的事情。
在此之上是請求傳輸層 (RTL)。此層以命令型別的資料包有效負載為中心,即請求(從主機發送到 EC)、EC 對這些請求的響應以及事件(從 EC 傳送到主機)。它專門區分事件與請求響應,將響應與其對應的請求相匹配,並實現請求超時。
控制器層構建在此之上,並且基本上決定了如何處理請求響應,尤其是事件。它提供了一個事件通知程式系統,處理事件啟用/停用,為事件和非同步請求完成提供了一個工作佇列,並且還管理構建命令訊息所需的 訊息計數器 (SEQ, RQID)。基本上,此層為 SAM EC 提供了一個基本介面,供其他核心驅動程式使用。
雖然控制器層已經為其他核心驅動程式提供了一個介面,但客戶端匯流排擴充套件了此介面,以透過 struct ssam_device 和 struct ssam_device_driver 提供對本機 SSAM 裝置(即未在 ACPI 中定義且未實現為平臺裝置的裝置)的支援,從而簡化了客戶端裝置和客戶端驅動程式的管理。
有關客戶端裝置/驅動程式 API 以及其他核心驅動程式的介面選項的文件,請參閱 編寫客戶端驅動程式。建議在繼續閱讀下面的架構概述之前,先熟悉該章節和 Surface 序列 Hub 協議。
資料包傳輸層¶
資料包傳輸層由 struct ssh_ptl 表示,並且圍繞以下關鍵概念構建
資料包¶
資料包是 SSH 協議的基本傳輸單元。它們由資料包傳輸層管理,資料包傳輸層本質上是驅動程式的最低層,並由 SSAM 核心的其他元件構建。要由 SSAM 核心傳輸的資料包由 struct ssh_packet 表示(相反,核心接收的資料包沒有任何特定結構,並且完全透過原始 struct ssh_frame 管理)。
此結構包含在傳輸層內管理資料包所需的欄位,以及對包含要傳輸的資料(即包裝在 struct ssh_frame 中的訊息)的緩衝區的引用。最值得注意的是,它包含一個內部引用計數,用於管理其生命週期(可透過 ssh_packet_get() 和 ssh_packet_put() 訪問)。當此計數器達到零時,將執行透過其 struct ssh_packet_ops 引用提供給資料包的 release() 回撥,然後可以釋放資料包或其封閉結構(例如,struct ssh_request)。
除了 release 回撥之外,struct ssh_packet_ops 引用還提供了一個 complete() 回撥,該回調在資料包完成後執行,並提供此完成的狀態,即成功時為零,如果發生錯誤,則為負 errno 值。一旦資料包已提交到資料包傳輸層,始終保證在 release() 回撥之前執行 complete() 回撥,即資料包始終會在釋放之前完成,無論是成功、發生錯誤還是由於取消而完成。
資料包的狀態透過其 state 標誌 (enum ssh_packet_flags) 管理,該標誌還包含資料包型別。特別是,以下位值得注意
SSH_PACKET_SF_LOCKED_BIT:當即將完成(透過錯誤或成功)時,會設定此位。它表示不應再獲取資料包的任何進一步引用,並且應儘快刪除任何現有引用。設定此位的程序負責從資料包佇列和掛起集中刪除對此資料包的任何引用。SSH_PACKET_SF_COMPLETED_BIT:此位由執行complete()回撥的程序設定,用於確保此回撥僅執行一次。SSH_PACKET_SF_QUEUED_BIT:當資料包在資料包佇列中排隊時,會設定此位;當資料包從資料包佇列中出隊時,會清除此位。SSH_PACKET_SF_PENDING_BIT:當資料包新增到掛起集中時,會設定此位;當資料包從掛起集中刪除時,會清除此位。
資料包佇列¶
資料包佇列是資料包傳輸層中的兩個基本集合中的第一個。它是一個優先順序佇列,其中各個資料包的優先順序基於資料包型別(主要)和嘗試次數(次要)。有關優先順序值的更多詳細資訊,請參閱 SSH_PACKET_PRIORITY()。
所有要由傳輸層傳輸的資料包都必須透過 ssh_ptl_submit() 提交到此佇列。請注意,這包括由傳輸層本身傳送的控制資料包。在內部,由於超時或 EC 傳送的 NAK 資料包,資料資料包可以重新提交到此佇列。
掛起集¶
掛起集是資料包傳輸層中的兩個基本集合中的第二個。它儲存對已傳輸但等待 EC 確認(例如,相應的 ACK 資料包)的資料包的引用。
請注意,如果由於資料包確認超時或 NAK 而重新提交了資料包,則該資料包可能同時處於掛起和排隊狀態。在這種重新提交時,不會從掛起集中刪除資料包。
發射器執行緒¶
發射器執行緒負責有關資料包傳輸的大部分實際工作。在每次迭代中,它(等待並)檢查佇列中的下一個資料包(如果有)是否可以傳輸,如果可以,則從佇列中刪除它並增加其傳輸嘗試次數的計數器,即嘗試次數。如果資料包是排序的,即需要 EC 的 ACK,則將資料包新增到掛起集中。接下來,資料包的資料將提交到 serdev 子系統。如果在提交期間發生錯誤或超時,則發射器執行緒會完成資料包,並相應地設定回撥的狀態值。如果資料包是未排序的,即不需要 EC 的 ACK,則發射器執行緒會完成資料包並顯示成功。
排序的資料包的傳輸受到併發掛起資料包數量的限制,即限制可以並行等待 EC 的 ACK 的資料包數量。此限制當前設定為 1(有關此限制背後的原因,請參閱 Surface 序列 Hub 協議)。控制資料包(即 ACK 和 NAK)始終可以傳輸。
接收器執行緒¶
從 EC 接收的任何資料都會放入 FIFO 緩衝區以供進一步處理。此處理發生在接收器執行緒上。接收器執行緒將接收到的訊息解析和驗證到其 struct ssh_frame 和相應的有效負載中。它為接收到的訊息準備並提交必要的 ACK(以及驗證錯誤或無效資料 NAK)資料包。
此執行緒還處理進一步的處理,例如將 ACK 訊息與相應的掛起資料包(透過序列 ID)匹配並完成它,以及啟動重新提交接收到 NAK 訊息時所有當前掛起的資料包(在 NAK 的情況下重新提交類似於由於超時而重新提交,有關詳細資訊,請參見下文)。請注意,排序的資料包的成功完成始終會在接收器執行緒上執行(而任何指示失敗的完成都將在發生失敗的程序中執行)。
任何有效負載資料都透過回撥轉發到下一個上層,即請求傳輸層。
超時清理程式¶
資料包確認超時是排序的資料包的每個資料包超時,在各個資料包開始(重新)傳輸時啟動(即,此超時在發射器執行緒上的每次傳輸嘗試時都會啟用)。它用於觸發重新提交,或者,當超過嘗試次數時,觸發相關資料包的取消。
此超時透過專用的清理程式任務處理,該任務本質上是一個工作項,在設定為超時的下一個資料包時(重新)計劃執行。然後,工作項檢查掛起資料包集中是否有任何資料包已超過超時時間,並且,如果還有剩餘資料包,則將自身重新計劃到下一個適當的時間點。
如果清理程式檢測到超時,則如果資料包仍有一些剩餘嘗試次數,則會重新提交資料包,否則會以 -ETIMEDOUT 作為狀態完成。請注意,在這種情況下以及由接收 NAK 觸發的重新提交,意味著資料包已新增到佇列中,並且嘗試次數現在已遞增,從而產生更高的優先順序。在下一次傳輸嘗試之前,將停用資料包的超時,並且資料包將保留在掛起集中。
請注意,由於傳輸和資料包確認超時,資料包傳輸層始終保證取得進展,即使只是透過超時資料包,也不會完全阻塞。
併發和鎖定¶
資料包傳輸層中有兩個主要鎖:一個鎖保護對資料包佇列的訪問,另一個鎖保護對掛起集的訪問。只能在各個鎖下訪問和修改這些集合。如果需要訪問兩個集合,則必須在佇列鎖之前獲取掛起鎖,以避免死鎖。
除了保護集合之外,在初始資料包提交之後,某些資料包欄位只能在一個鎖下訪問。具體來說,只能在保持佇列鎖時訪問資料包優先順序,並且只能在保持掛起鎖時訪問資料包時間戳。
資料包傳輸層的其他部分是獨立保護的。狀態標誌由原子位操作管理,如果必要,則由記憶體屏障管理。對超時清理程式工作項和到期日期的修改由它們自己的鎖保護。
資料包到資料包傳輸層的引用 (ptl) 有點特殊。它要麼在提交上層請求時設定,要麼在首次提交資料包時設定(如果沒有上層請求)。設定後,它不會更改其值。可能與提交併發運行的函式(即取消)不能依賴於 ptl 引用設定為設定。在這些函式中對它的訪問由 READ_ONCE() 保護,而 ptl 的設定同樣受到 WRITE_ONCE() 的對稱性保護。
某些資料包欄位可以在保護它們的各個鎖之外讀取,特別是用於跟蹤的優先順序和狀態。在這些情況下,透過採用 WRITE_ONCE() 和 READ_ONCE() 來確保正確的訪問。僅當陳舊值不重要時,才允許此類只讀訪問。
關於更高級別層的介面,資料包提交 (ssh_ptl_submit())、資料包取消 (ssh_ptl_cancel())、資料接收 (ssh_ptl_rx_rcvbuf()) 和層關閉 (ssh_ptl_shutdown()) 始終可以相對於彼此併發執行。請注意,資料包提交不能與同一資料包本身併發執行。同樣,關閉和資料接收也不能與它們自己併發執行(但可以彼此併發執行)。
請求傳輸層¶
請求傳輸層由 struct ssh_rtl 表示,並構建在資料包傳輸層之上。它處理請求,即由主機發送的包含 struct ssh_command 作為幀有效負載的 SSH 資料包。此層將對請求的響應與事件分開,事件也由 EC 透過 struct ssh_command 有效負載傳送。雖然響應在此層中處理,但事件透過相應的回撥中繼到下一個上層,即控制器層。請求傳輸層圍繞以下關鍵概念構建
請求¶
請求是具有命令型別有效負載的資料包,從主機發送到 EC 以從 EC 查詢資料或觸發其上的操作(或同時執行兩者)。它們由 struct ssh_request 表示,包裝底層 struct ssh_packet 儲存其訊息資料(即帶有命令有效負載的 SSH 幀)。請注意,所有頂級表示形式(例如,struct ssam_request_sync)都構建在此結構之上。
由於 struct ssh_request 擴充套件了 struct ssh_packet,因此其生命週期也由資料包結構中的引用計數器管理(可以透過 ssh_request_get() 和 ssh_request_put() 訪問)。一旦計數器達到零,將呼叫請求的 struct ssh_request_ops 引用的 release() 回撥。
請求可以有一個可選的響應,該響應同樣透過帶有命令型別有效負載的 SSH 訊息傳送(從 EC 到主機)。構造請求的一方必須知道是否期望響應,並在提供給 ssh_request_init() 的請求標誌中標記這一點,以便請求傳輸層可以等待此響應。
與 struct ssh_packet 類似,struct ssh_request 也有一個透過其請求操作引用提供的 complete() 回撥,並且保證在透過 ssh_rtl_submit() 提交到請求傳輸層後先完成然後再釋放。對於沒有響應的請求,一旦底層資料包已由資料包傳輸層成功傳輸(即,從資料包完成回撥中),將發生成功完成。對於有響應的請求,一旦已接收到響應並透過其請求 ID 將響應與請求匹配(這發生在資料包層的資料接收回調(在接收器執行緒上執行)中),將發生成功完成。如果請求因錯誤而完成,則狀態值將設定為相應的(負)errno 值。
請求的狀態再次透過其 state 標誌 (enum ssh_request_flags) 管理,該標誌還編碼請求型別。特別是,以下位值得注意
SSH_REQUEST_SF_LOCKED_BIT:當即將完成(透過錯誤或成功)時,會設定此位。它表示不應再獲取請求的任何進一步引用,並且應儘快刪除任何現有引用。設定此位的程序負責從請求佇列和掛起集中刪除對此請求的任何引用。SSH_REQUEST_SF_COMPLETED_BIT:此位由執行complete()回撥的程序設定,用於確保此回撥僅執行一次。SSH_REQUEST_SF_QUEUED_BIT:當請求在請求佇列中排隊時,會設定此位;當請求從佇列中移除時,會清除此位。SSH_REQUEST_SF_PENDING_BIT:當請求新增到掛起集合時,會設定此位;當請求從掛起集合中移除時,會清除此位。
請求佇列¶
請求佇列是請求傳輸層中的兩個基本集合中的第一個。與資料包傳輸層的資料包佇列相反,它不是優先順序佇列,而是遵循簡單的先進先出原則。
所有要由請求傳輸層傳輸的請求必須透過 ssh_rtl_submit() 提交到此佇列。提交後,請求不得重新提交,也不會在超時時自動重新提交。而是,請求以超時錯誤完成。如果需要,呼叫者可以建立並提交一個新請求進行另一次嘗試,但不得再次提交同一請求。
掛起集合¶
掛起集合是請求傳輸層中的兩個基本集合中的第二個。此集合儲存對所有掛起請求的引用,即等待 EC 響應的請求(類似於資料包傳輸層的掛起集合對資料包所做的事情)。
傳送器任務¶
當有新的請求可供傳輸時,會排程傳送器任務。它檢查請求佇列中的下一個請求是否可以傳輸,如果可以,則將其底層資料包提交給資料包傳輸層。此檢查確保同一時間只有有限數量的請求可以處於掛起狀態,即等待響應。如果請求需要響應,則在提交其資料包之前,會將該請求新增到掛起集合中。
資料包完成回撥¶
一旦請求的底層資料包完成,就會執行資料包完成回撥。如果發生錯誤完成,則相應的請求將以該回調中提供的錯誤值完成。
成功完成資料包後,後續處理取決於請求。如果請求期望得到響應,則將其標記為已傳輸,並啟動請求超時。如果請求不期望得到響應,則以成功完成。
資料接收回調¶
資料接收回調通知請求傳輸層,底層資料包傳輸層透過資料型別幀接收到資料。一般來說,這應該是指令型別的有效負載。
如果指令的請求 ID 是為事件保留的請求 ID 之一(1 到 SSH_NUM_EVENTS,包括這兩個值),則將其轉發到請求傳輸層中註冊的事件回撥。如果請求 ID 指示對請求的響應,則在掛起集合中查詢相應的請求,如果找到並標記為已傳輸,則以成功完成。
超時清理器¶
請求-響應-超時是每個請求的超時,用於期望得到響應的請求。它用於確保請求不會無限期地等待 EC 的響應,並在底層資料包成功完成後啟動。
此超時與資料包傳輸層上的資料包確認超時類似,是透過專用的清理器任務處理的。此任務本質上是一個工作項,(重新)排程為在下一個請求設定為超時時執行。然後,該工作項掃描掛起請求的集合,查詢任何已超時的請求,並以 -ETIMEDOUT 作為狀態完成它們。請求不會自動重新提交。而是,請求的釋出者必須構造並提交一個新請求(如果需要)。
請注意,此超時與資料包傳輸和確認超時相結合,可確保請求層始終能夠取得進展,即使只是透過超時資料包,也永遠不會完全阻塞。
併發和鎖定¶
與資料包傳輸層類似,請求傳輸層中有兩個主要鎖:一個保護對請求佇列的訪問,另一個保護對掛起集合的訪問。這些集合只能在各自的鎖下訪問和修改。
請求傳輸層的其他部分是獨立保護的。狀態標誌(再次)由原子位運算和(如果需要)記憶體屏障管理。對超時清理器工作項和到期日期的修改由其自己的鎖保護。
某些請求欄位可以在保護它們的各自鎖之外讀取,特別是用於跟蹤的狀態。在這些情況下,透過使用 WRITE_ONCE() 和 READ_ONCE() 來確保正確的訪問。只有在陳舊的值不重要時,才允許這種只讀訪問。
關於更高層的介面,請求提交(ssh_rtl_submit())、請求取消(ssh_rtl_cancel())和層關閉(ssh_rtl_shutdown())可以始終相對於彼此併發執行。請注意,請求提交不能與同一請求本身併發執行(並且每個請求也只能呼叫一次)。同樣,關閉也不能與自身併發執行。
控制器層¶
控制器層擴充套件了請求傳輸層,為客戶端驅動程式提供了一個易於使用的介面。它由 struct ssam_controller 和 SSH 驅動程式表示。雖然較低級別的傳輸層負責傳輸和處理資料包和請求,但控制器層承擔更多的管理角色。具體來說,它處理裝置初始化、電源管理和事件處理,包括透過(事件)完成系統(struct ssam_cplt)進行事件傳遞和註冊。
事件註冊¶
通常,在 EC 傳送事件之前,主機必須顯式請求事件(或者是一類事件)(HID 輸入事件似乎是例外)。這是透過事件啟用請求完成的(同樣,一旦不再需要事件,應透過事件停用請求停用事件)。
用於啟用(或停用)事件的特定請求是透過事件登錄檔給出的,即此事件的管理機構(可以這樣說),由 struct ssam_event_registry 表示。作為此請求的引數,必須提供要啟用的事件的目標類別,以及(取決於事件登錄檔)例項 ID。如果登錄檔不使用此(可選)例項 ID,則該 ID 必須為零。目標類別和例項 ID 共同構成事件 ID,由 struct ssam_event_id 表示。簡而言之,事件登錄檔和事件 ID 都是唯一標識相應類別的事件所必需的。
請注意,必須為啟用事件請求提供另一個請求 ID 引數。此引數不影響要啟用的事件類別,而是設定為 EC 傳送的此類別的每個事件上的請求 ID (RQID)。它用於標識事件(因為僅為事件保留了有限數量的請求 ID,特別是 1 到 SSH_NUM_EVENTS,包括這兩個值),並將事件對映到其特定類別。當前,控制器始終將此引數設定為 struct ssam_event_id 中指定的目標類別。
由於多個客戶端驅動程式可能依賴於相同(或重疊)的事件類別,並且啟用/停用呼叫是嚴格的二進位制(即開/關),因此控制器必須管理對這些事件的訪問。它透過引用計數來實現,將計數器儲存在基於 RB 樹的對映中,並將事件登錄檔和 ID 作為鍵(沒有已知的有效事件登錄檔和事件 ID 組合的列表)。有關詳細資訊,請參見 struct ssam_nf、ssam_nf_refcount_inc() 和 ssam_nf_refcount_dec()。
此管理與透過頂級 ssam_notifier_register() 和 ssam_notifier_unregister() 函式進行的通知程式註冊(在下一節中描述)一起完成。
事件傳遞¶
要接收事件,客戶端驅動程式必須透過 ssam_notifier_register() 註冊事件通知程式。這會增加該特定類別的事件的引用計數器(如上一節所述),在 EC 上啟用該類別(如果尚未啟用),並安裝提供的通知程式回撥。
通知程式回撥儲存在列表中,每個目標類別都有一個 (RCU) 列表(透過事件 ID 提供;注意:目標類別的數量是固定的)。除了透過事件 ID 給定的目標類別和例項 ID 之外,沒有從事件登錄檔和事件 ID 的組合到事件類別可以提供的命令資料(目標 ID、目標類別、命令 ID 和例項 ID)的已知關聯。
請注意,由於儲存通知程式的方式(或者更確切地說,必須儲存的方式),客戶端驅動程式可能會收到它們未請求的事件,並且需要對此負責。具體來說,預設情況下,它們將收到來自同一目標類別的所有事件。為了簡化處理此問題,可以在註冊通知程式時請求按目標 ID(透過事件登錄檔提供)和例項 ID(透過事件 ID 提供)過濾事件。在執行通知程式時迭代通知程式時,會應用此篩選。
所有通知程式回撥都在專用的工作佇列(所謂的完成工作佇列)上執行。在透過請求層中安裝的回撥(在資料包傳輸層的接收器執行緒上執行)收到事件後,它將被放入其各自的事件佇列(struct ssam_event_queue)。從此事件佇列中,該佇列的完成工作項(在完成工作佇列上執行)將拾取該事件並執行通知程式回撥。這樣做是為了避免阻塞接收器執行緒。
每個目標 ID 和目標類別的組合都有一個事件佇列。這樣做是為了確保對於相同目標 ID 和目標類別的事件,按順序執行通知程式回撥。對於具有不同目標 ID 和目標類別組合的事件,可以並行執行回撥。
併發和鎖定¶
控制器的大多數與併發相關的安全保證由較低級別的請求傳輸層提供。除此之外,事件(取消)註冊由其自己的鎖保護。
對控制器狀態的訪問由狀態鎖保護。此鎖是讀/寫訊號量。讀取器部分可用於確保狀態在依賴於狀態保持不變的函式(例如 ssam_notifier_register()、ssam_notifier_unregister()、ssam_request_sync_submit() 及其派生函式)執行時不會更改,並且這種保證尚未透過其他方式(例如透過 ssam_client_bind() 或 ssam_client_link())提供。寫入器部分保護任何將更改狀態的轉換,即初始化、銷燬、掛起和恢復。
可以在狀態鎖之外訪問(只讀)控制器狀態,以針對無效的 API 使用進行冒煙測試(例如在 ssam_request_sync_submit() 中)。請注意,此類檢查不應(也不會)防止所有無效用法,而是旨在幫助捕獲它們。在這些情況下,透過使用 WRITE_ONCE() 和 READ_ONCE() 來確保正確的變數訪問。
假設已經滿足了狀態不發生變化的任何先決條件,則所有非初始化和非關閉函式都可以彼此併發執行。這包括 ssam_notifier_register()、ssam_notifier_unregister()、ssam_request_sync_submit() 以及所有構建在其之上的函式。