核心驱动程序内部原理¶
表面系统聚合模块 (SSAM) 核心和表面串行集线器 (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 和其他内核驱动程序的接口选项的文档,请参阅 编写客户端驱动程序。建议在继续阅读下面的架构概述之前,先熟悉该章节和 表面串行集线器协议。
数据包传输层¶
数据包传输层由 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 Serial 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 以从其查询数据或触发其上的操作(或两者同时)。它们由 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 与请求匹配(这发生在数据包层接收线程上运行的数据接收回调中),就会成功完成。如果请求完成时出现错误,状态值将被设置为相应的(负)错误号值。
请求的状态再次通过其 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,由 struct ssam_event_id
表示。简而言之,需要事件注册表和事件 ID 才能唯一标识相应的事件类别。
请注意,必须为启用事件请求提供进一步的请求 ID 参数。此参数不会影响正在启用的事件类别,而是设置为 EC 发送的此类每个事件的请求 ID (RQID)。它用于标识事件(因为有限数量的请求 ID 仅保留用于事件,特别是 1 到 SSH_NUM_EVENTS
,包括 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 上启用该类别(如果尚未启用),并安装提供的通知器回调。
通知器回调存储在列表中,每个目标类别(通过事件 ID 提供;注意:目标类别的数量是固定的)有一个 (RCU) 列表。除了通过事件 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()
以及所有构建在这些之上的函数。