龙空技术网

linux udev详解3-Storage Instantiation Daemon(1)

菜卓001 102

前言:

此刻你们对“linuxnetlink性能”大概比较关切,看官们都需要剖析一些“linuxnetlink性能”的相关内容。那么小编在网络上搜集了一些有关“linuxnetlink性能””的相关资讯,希望各位老铁们能喜欢,你们快快来了解一下吧!

此文章翻译自:Storage Instantiation Daemon (sid-project.github.io)。

强烈推荐看下此文章,不仅对之前的udev内容做了部分补充,更能加强对udev的理解。工作中涉及有udev的道友可以看下原文。

概述

存储实例化守护程序(SID)是一个项目,目的是为了通过监控event来跟踪Linux存储设备状态,包括设备层(device layer)、组(groups)和整个堆栈。基于监控的状态和设备信息,能够触发相应的操作,如激活和停用设备。

SID实现是基于udev的,对uevent事件进行相应的动作。SID与udevd进程关系紧密。udevd进程通过增强的sid udev builtin命令与SID进行通信。同时,SID还监听由udev守护进程触发的uevents,进一步执行后续的行为。

SID核心是为各种设备子系统提供基础框架和API,用以创建和处理特定设备类型及其抽象的模块。因此它对各个uevent进行单独处理,核心部分以及模块可以通过API访问SID数据库,以存储和查询除了udevd数据库功能之外的扩展信息。

SID数据库包含内部(internal)和通用(general-purpose)记录。内部记录只能通过专用的API调用访问,用于跟踪和支持设备依赖关系、分组、触发器等操作。通用记录可以由模块直接存储和查询,并可以自定义关键组件、可见性范围和访问类型。

由于SID跟踪整个存储设备堆栈和分组,它提供了对堆栈、设备和组依赖关系的监听机制,并能够在设备堆栈的发生变化时触发事件进而执行SID内部或者外部的相关操作。

uevents和udev

uevents和udev在Linux的热插拔设备管理中起着重要作用。uevents也称为udev事件,当设备状态发生改变时从Linux内核发送通知消息到用户空间,uevents使用netlink接口发送(NETLINK_KOBJECT_UEVENT类型)。

用户空间可以注册监听器来接收这些事件,如果有多个监听器则以组播方式发送这些事件。

目前支持的uevents的事件类型包括:

add:设备已添加change:设备已更改remove:设备已移除move:设备已移动到新的父级或设备已重命名offline:设备已离线online:设备在离线后重新上线bind:驱动程序绑定到设备(自内核版本4.14起)unbind:驱动程序从设备解绑(自内核版本4.14起)

最常用的是add、change和remove。每个内核uevent都包含一组以KEY=VALUE格式的环境变量。其中内核通用uevent代码添加的最小和基本的键集包括:

ACTION:事件名称DEVPATH:设备在sysfs中的路径SUBSYSTEM:设备所属的子系统SEQNUM:uevent的序列号

Linux内核的驱动程序核心会根据可用的值添加更多的键来扩展基本集:

MAJOR:设备的主设备号MINOR:设备的次设备号DEVNAME:设备的规范内核名称DEVMODE:设备的权限模式DEVUID:设备的用户DEVGID:设备的组ID(如果不是全局根组ID)DEVTYPE:设备的类型名称DRIVER:设备的驱动程序名称

各种设备子系统和内核中的设备驱动程序可以添加更多的KEY=VALUE对。但是uevent的总大小是有限的:KEY=VALUE对的最大数量为32(内核源代码中UEVENT_NUM_ENVP常量决定),从内核发送的整个uevent的总大小限制为2048字节(内核源代码中UEVENT_BUFFER_SIZE常量决定)。

在本文中,将内核发送的uevents称为真正的内核uevents(简称kernel-uevents)。

一般来说,每个块设备的uevent都在其uevent环境中设置了ACTION、DEVPATH、SUBSYSTEM、SEQNUM、MAJOR、MINOR、DEVNAME和DEVTYPE。

除了基于内核和驱动程序生成的真正的kernel-uevents外,还存在合成内核uevents(简称synthetic-uevents)。尽管synthetic-uevents也是内核中生成的,但方式是用户空间通过往/sys/.../uevent文件写入事件(add、change、remove等)触发。这样的uevents与真实的kernel-uevents完全相同,唯一的区别是synthetic-uevents不包含驱动程序添加的其他键,只包含基本键集。synthetic-uevents主要用于用户空间接收后进行具体业务使用。另外还可以直接从用户空间发送uevents到用户空间,实现不同进程间通信,这种uevent称之为udev-uevents。

涵盖所有uevents以及基于这些uevents的用户空间中的动态设备管理逻辑的程序是udev,在用户空间中重要的组件是udev daemon(udevd)。

udev daemon

udev daemon(udevd)是用户空间监听uvent事件的主要角色,已经合并到systemd中,是systemd项目的一部分。

udevd在用户空间中处理uevent的方式由udev规则驱动,这些规则通常放置在/lib/udev/rules.d和/etc/udev/rules.d目录中。上游udevd提供了一组通用的规则(lib路径下),其他规则则由外部工具和系统组件安装(etc或其他路径下)。

当udevd接收到来自内核新的uevent时(包括synthetic-uevents和kernel-uevents),会创建一个新的进程(也称为udevd-worker)来处理该事件,udevd-worker可以重用。如果前一个事件刚刚被处理完毕且队列仍未空,udevd-worker不会被释放,以便立即重用worker执行下一个uevent的规则,从而提升性能。

单个设备发出的所有uevent的处理是串行化的,一次只能处理一个事件,按顺序排队和处理,而针对不同设备的uevent可以并行处理,设备根据在sysfs中的规范设备路径进行区分。

并行处理uevent的worker数量是有限制的,由udevd的--children-max命令行选项或cmdline中udev.children_max参数进行设置。通过这种方式可以控制udevd使用的并行度。源码中默认值的实现是通过一个简单的公式,该公式基于可用的CPU核心数:

cpu_limit = cpu_count * 2 + 16; mem_limit = MAX(physical_memory() / (128UL*1024*1024), 10U); arg_children_max = MIN(cpu_limit, mem_limit); arg_children_max = MIN(WORKER_NUM_MAX, arg_children_max);

udevd的主要作用是收集创建/dev目录下各种符号链接所需的任何其他信息,并根据udev规则中的指令设置权限。除了网络设备外,udevd无法控制设备节点名称。使用devtmpfs文件系统时,设备节点由内核直接创建,udevd只是调整其权限。udev规则还可以访问设备在sysfs中存在的信息。要收集其他信息,udev规则需要设置udevd执行外部命令或以特殊方式收集此信息,可以通过执行以下规则来实现:

IMPORT:执行一个命令,将以KEY=VALUE格式导入到udev上下文中,以供udev规则访问;实际的调用仅在规则匹配时才发生;RUN:添加一个命令到命令列表中,等所有规则处理完后执行;PROGRAM:执行自定义命令,其字符串输出可以与相应的RESULT规则匹配。

IMPORT和RUN规则可以执行外部命令,也可以通过指定IMPORT{builtin}或RUN{builtin}来执行udev的内置命令。内置命令优势在于不需要创建新的进程,并且内置命令在udev启动时就被初始化。但是内置命令需要直接集成到udev的代码库中,不是作为在udev启动时加载的外部模块设计的。

udev对于每个uevent执行所有udev规则的时间有限制,默认值是180秒。可以通过设置udev的event-timeout选项或在cmdline中使用udev.event-timeout参数来修改默认值。计时开始点为worker进程被fork或重用时,结束点为udevd进程接收到worker完成处理消息时。执行udev工作进程的简化步骤如下:

内核uevent被udevd进程接收;udev创建或重用worker进程来处理uevent;udev开始为worker进程启动计时器;worker进程执行并应用udev规则;worker进程更新udev数据库;worker进程执行运行队列(所有由RUN规则设置的命令);worker进程发送udev uevent,其他用户空间进程可以监听;worker进程向udevd进程发送已完成的消息;udevd接收到来worker进程已完成的消息;udev停止worker的计时器。

与kernel-uevent相比,udev-uevent是udev直接发送给所有监听器的uevent,在处理所有规则后才发送,因此udev-uevent包含通过执行和应用udev规则添加的所有以KEY=VALUE格式的环境变量。udevd发送udev-uevent使用的接口和接收kernel-uevent时相同(都是netlink)。udev-uevent和kernel-uevent的监听器使用libudev库监听这些uevent,libudev库将uevent封装在一个结构中,以便更容易地操作和使用各种libudev函数进行进一步处理,同时也将实际的netlink使用抽象出来。

udev数据库是一个简单的基于文件系统的数据库(通常存储在/run/udev目录中)。它包含每个设备的当前环境,即KEY=VALUE键值对和udev记录的其他信息:符号链接列表(links)、符号链接优先级、标签(tags)以及规则中包含了OPTIONS+="watch"的设备监控器(watch,当监听的设备内容发生变化时会收到synthetic-uevents)。

注意:udev规则中OPTIONS+="watch"是使用inotify监听机制实现的。每当/run/udev/watch目录下监听的设备在write操作后closed时,udevd进程会收到inotify事件。然后udevd进程基于inotify事件为设备生成synthetic-uevent。通常情况下,udev规则中使用OPTIONS+="watch"是因为我们期望对设备进行write操作可能会改变其内容,从而触发udev规则,进而可能改变udev数据库内容。

块设备uevent事件

块设备的uevent处理由上游udev提供的规则以及块设备子系统提供的规则驱动。

上游udev提供的规则

60-block.rules:启用多媒体状态轮询;将SCSI事件转发到相应的块设备,并为select的块设备设置OPTIONS+="watch";60-persistent-storage.rules:从udev数据库中导入分区的父信息;调用ata-id、scsi_id、usb_id、path_id、blkid等工具;设置设备符号链接;60-persistent-storage-tape.rules:调用blkid;设置设备符号链接;60-cdrom_id.rules:调用cdrom_id;设置设备符号链接;64-btrfs.rules:调用btrfs_ready内置命令;如果需要将设备标记为未就绪,并适当地设置SYSTEMD_READY变量;99-systemd.rules:基于其他各种变量和sysfs内容设置SYSTEMD_READY变量;还包括处理loop设备。

块设备子系统提供的规则暂时不在这里叙述,可以参考原文。

问题和局限

udev主要设计用于收集特定设备所需的附加信息,然后让udev在/dev中创建附加的符号链接,并根据规则设置设备节点的权限。

尽管处理块设备的规则大多数包含设置设备节点符号链接的规则,但随着时间这些规则中的其他命令数量也增加了。目前规则不仅收集附加信息,还包括其他功能,如用于支持块设备子系统的各个方面辅助调用。因此,这种方法存在各种问题和不足之处,这些问题已经变得相当突出。

这些问题并不完全独立。相反它们之间密切相关,解决其中一个问题通常会减少其他问题部分的影响程度。

下面介绍部署与存储相关的解决方案,并尝试将其与udev集成时发现的各种问题和不足。

多步激活

对于某些块设备当被激活和设备状态改变时,可能具有更复杂的性质。

如DM和MD设备子系统首次被创建时会发送add uevent,但设备可能无法立即使用。通常需要多个步骤来使这些设备ready后才能使用(发送change uevent)。

设备组和堆栈概念

另外需要考虑的最重要的特性之一是某些块设备可以建立在其他设备之上,并且抽象的逻辑块可以组合在一起。

udev在设备组中没有直接的分组或堆栈概念。

设备管理的中间步骤

某些子系统还支持从一种类型转换为另一种类型,过程涉及多个停用和启用步骤,设备会在这些中间过程发生转换。

我们无法在udev规则中进行区分是设备转换的中间过程还是常规设备激活、停用或者变更,除非在内核驱动程序生成的uevent中使用额外的KEY=VALUE标记中间状态,或者使用外部信息或工具来决定当前状态是什么。通常执行规则中命令可能会干扰设备转换或转换过程。

识别uevent、设备状态和负载较高的uevent

所有块设备子系统都是使用udevd处理内核发送的ueven事件,这些uevent可以是内核驱动程序本身发出的,也可以是通过写/sys/.../uevent文件合成的。这些块子系统处理的一些特殊uevent被统一描述成change uevent而没有使用不同uevent描述不同性质的事件。

如果想准确的识别出转换过程的uevent会使得udev规则相当复杂,可能需要将udev变量(KEY=VALUE)与udev数据库中之前保存的变量进行比较。

设计udev目的不是为了解决此问题。即使有规则可以导入之前的udev数据库值(IMPORT{db}规则),也没有办法直接比较之前和当前的某些键的值。只能进行简单的字符串匹配,通过规则中定义ENV{KEY}=="direct_string_to_match"进行匹配,而不是ENV{KEY1}==ENV{KEY2}。另外在udev规则中不支持数值比较,只支持针对显式字符串值的匹配。

udev规则语言及相关限制

正确识别当前状态是驱动程序或udev规则的责任。内核驱动程序可以添加各种额外的KEY=VALUE通过uevent发送出去。也可以直接在udev规则中解决这个问题,需要获取之前的udev数据库状态和当前状态进行比较。

由于udev规则的语言非常简单且受限制,即使是相对简单的设备状态监听或改进跟踪的状态机,也可能使规则变得复杂。

这使得在udev规则中实现状态机来准确跟踪设备状态变得困难。

调试和日志记录

随着规则越来越复杂,当出现问题时进行有效的调试变得复杂,udevd无法获取正在处理的当前环境,也不支持直接在规则中添加额外的日志钩子。因此很难跟踪在处理udev规则时实际的路径以及状态是什么。

标记设备状态ready

udev规则负责根据当前状态在适当时间触发设备激活,如果考虑到设备堆栈(一个块设备子系统层建立在另一个子系统上时),这一点并不容易。

需要有一种标准的方式来标记下层设备对上层设备的就绪状态,目前缺乏这种标准。在udev变量中有各种KEY=VALUE(如DM_ACTIVATION,MD_STARTED,SYSTEMD_READY等)标记各个子系统设备就绪的方式。

当其他地方使用udev进行监控设备状态时,存在同样的问题,它们没有标准化的方式来判断设备是否可以使用。

在udev上下文中的工作量

另一个问题是在处理udev规则时需要完成的工作量。

根据udev的设计应尽量将这些额外的工作最小化,并且应限制为获取信息在/dev下创建的符号链接。这意味着,除了收集基本设备标识和信息我外,其他无关的所有规则和行为都应移出udev上下文并延迟执行,或者与udev并行执行。

超时

udev对每个uevent处理设置了超时时间,在系统负载较重情况下可能会成为一个问题,因为在默认的超时时间内可能完成不了。超时时间无法在运行时设置,udevd中已删除了对OPTIONS="event_timeout"规则的支持。

如果发生超时,udev使用SIGTERM信号终止掉带有其任何子进程的worker进程,因此耗时较长的命令需要在后台执行。在使用systemd的系统上,该类命令是作为一个服务实例化,这不属于udev的上下文和控制组的控制范围。如果发生超时并且worker进程被杀死,udevd只是将其接收到的kernel-uevent作为udev-uevent透传给所有监听器,不会添加任何额外的信息。

如果发生超时,需要让监听器知道或提供定义回退操作,以保持系统运行并让用户修复配置或增加超时时间。

synthetic-uevents

另一个问题领域是uevent的来源。除了直接来自内核的真实uevent外,还存在合成事件(synthetic-uevents)。从用户的角度来看,触发合成uevent的三种常见方式如下:

直接将事件写入/sys/.../uevent文件, 调用udevadm trigger命令(该命令会写入/sys/.../uevent文件), 在udev规则为设备使用OPTIONS="watch"(设备被open进行write操作并closed后,inotify watch会触发udev,然后udev会写入/sys/../uevent文件)。

如果内核驱动程序没有为uevent提供任何附加变量,则kernel-uevent与synthetic-uevent无法区分,导致使用者无法确认设备是否可用。很长一段时间以来,udev的立场是这两个uevent不应该区分,uevent监听器和udev规则的作者应该考虑到这一事实。

然而,我们的论点是这两种类型的uevent确实存在差异。kernel-uevent通知用户空间设备本身的状态变化(如设备add、设备配置change或设备remove)。synthetic-uevent是用户空间触发的,要么用于刷新用户空间中udev的状态(如udev数据库被清除需要更新,或者监听者需要重新获取uevent信息)。

或者,synthetic-uevent也可以用于通知设备内容的变化,设备内容通常不会被内核设备驱动程序跟踪(如将子系统或文件系统签名添加到设备或将其清除)。

目前,这两种类型的uevent被认为是相等的,导致处理uevent时会造成混淆,并且由于过度处理可能导致无用的资源浪费。解决这个问题需要在udev上下文中编写更复杂的udev规则。

此外,synthetic-uevent是完全异步的,目前无法与之同步。如果想单独访问设备(或者使用工具删除设备)实现都相当麻烦,因为synthetic-uevent可能会并行发生。

将设备标记为专用或公共

另一个问题领域是标识对于子系统而言是专用的设备。

此外,设备标记为可用之前需要先进行初始化(或清理),例如对之前使用过的设备清理掉旧签名。或者没有定义如何标记此类设备的标准(如DM设备使用uevent变量(DM_COOKIE)中的标志来处理此问题,而MD则在文件系统中使用临时文件/run/mdadm/creating-<md_device_name>来标记设备尚未完全初始化)。

设备初始化

我们应该能够首选专用模式激活设备,这样可以为用户空间工具提供初始化和清理所需的时间,以便使设备ready后正确使用。

在初始化之后将设备切换到准备就绪状态,用户空间通过发出synthetic-uevent来告知此状态,并把模式从专用切换到公共模式。

最终,解决设备激活期间的初始化和清理工作需要集中处理,而不需要每个子系统提供代码来实现此功能。此方案依赖于框架能够获取到足够的信息以便在特定时间安全地进行初始化和擦除操作。

标签: #linuxnetlink性能