MENU

容器环境下宿主机块设备的事件处理

November 4, 2022 • 学习,折腾

TL;DR

  1. 容器启动时设置为特权容器,或赋予网络权限,CAP_NET_ADMINCAP_NET_RAW,或设置为HOST网络模式1
  2. 容器启动时挂载入宿主机的/dev目录。
  3. udev会通过PF_NETLINK套接字获取宿主机的内核设备树变更事件。
  4. 合理的udev配置 + udev规则,实现自动挂载块设备。

背景

边缘设备的需要在K8s管理的容器内向热插拔磁盘拷贝数据,无法在当前容器生命周期内完成块设备的挂载和访问。

调参

udev的守护进程接收由LinuxKernel生成的各类事件,以响应与外围设备相关的物理事件。通俗来说可以检测系统设备的热插拔(当然也可以处理非物理设备在设备树上的操作)。

kernel: usb 1-4: new high-speed USB device number 76 using xhci_hcd
kernel: usb 1-4: New USB device found, idVendor=****, idProduct=****, bcdDevice=**.01
kernel: usb 1-4: New USB device strings: Mfr=1, Product=2, SerialNumber=3
kernel: usb 1-4: Product: External HDD
kernel: usb 1-4: Manufacturer: JMicron
kernel: usb 1-4: SerialNumber: 000000******
kernel: usb 1-4: USB disconnect, device number 76

在大部分的Linux操作系统上,由于systemd的存在,udev守护进程可直接被管理。在容器环境下,PID1的进程通常是业务意义上的守护进程,

在容器环境内的udev,有若干点需要注意。

/sys

若不以特权容器的形式启动,则/sys目录默认不会被挂载,该位置挂载了一个sysfs文件系统,其主要功能是对系统设备进行管理,sysfs可以产生一个包含所有系统硬件层次的视图。
挂载了这个目录,容器内就具备了操作宿主机设备的能力。

因为容器通常设计为与宿主环境隔离,原则上不应当给予容器可以操作宿主机系统设备的权限。这会导致/etc/init.d/udev守护进程启动脚本(aka:服务启动脚本)拒绝(在容器环境)运行。可以通过注释对应代码(暂未验证无/sys是否影响消息传递,原因见下),或使用特权容器。

从udev源码来看,udev通过监听AF_NETLINKPF_NETLINK2的套接字来获得各类所需要的内核消息3
AF_NETLINK一类套接字主要用于内核与用户空间的通信。若想访问该套接字,网上大部分的资料要求设置容器host网络模式,即--net host。这样一来,目的达到了,但容器开放的端口将直接使用主机网络(容器网络和主机网络在同一个命名空间),我认为并不是十分优雅。
从原理上来说,我认为也可以给予容器CAP_NET_ADMINCAP_NET_RAW1

udev挂载命名空间

udev在宿主机测试时,默认配置实际挂载的命名空间和用户可访问的不一致,因此需要调整配置文件。
   目前尚未测试在容器环境中,是否存在挂载命名空间差异,保险起见,还是修改一下。

简单来说需要把PrivateMounts=yes,改为PrivateMounts=no

具体配置文件请点开下拉栏

$ cat /lib/systemd/system/systemd-udevd.service                
#  SPDX-License-Identifier: LGPL-2.1+
#
#  This file is part of systemd.
#
#  systemd is free software; you can redistribute it and/or modify it
#  under the terms of the GNU Lesser General Public License as published by
#  the Free Software Foundation; either version 2.1 of the License, or
#  (at your option) any later version.

[Unit]
Description=udev Kernel Device Manager
Documentation=man:systemd-udevd.service(8) man:udev(7)
DefaultDependencies=no
After=systemd-sysusers.service systemd-hwdb-update.service
Before=sysinit.target
ConditionPathIsReadWrite=/sys

[Service]
Type=notify
# Note that udev also adjusts the OOM score internally and will reset the value internally for its workers
OOMScoreAdjust=-1000
Sockets=systemd-udevd-control.socket systemd-udevd-kernel.socket
Restart=always
RestartSec=0
ExecStart=/lib/systemd/systemd-udevd
ExecReload=udevadm control --reload --timeout 0
KillMode=mixed
TasksMax=infinity
PrivateMounts=yes #此处需要改为no
ProtectHostname=yes
MemoryDenyWriteExecute=yes
RestrictAddressFamilies=AF_UNIX AF_NETLINK AF_INET AF_INET6
RestrictRealtime=yes
RestrictSUIDSGID=yes
LockPersonality=yes
IPAddressDeny=any
WatchdogSec=3min

   部分装有SELinux的系统可能需要增加以下配置,一般容器环境内不需要。

[Service]
MountFlags=shared

udev规则

udev规则通常存放于两个地方。/lib/udev/rules.d//etc/udev/rules.d/

在参考了软件自带的一些规则之后,可以照葫芦画瓢的仿写出以下的一组规则。4

KERNEL!="sd[a-z][0-9]|sd[a-z]", GOTO="media_by_label_auto_mount_end"

IMPORT{program}="/sbin/blkid -o udev -p /mount_host/%N"

ENV{ID_FS_LABEL}!="", ENV{dir_name}="%E{ID_FS_LABEL}"
ENV{ID_FS_LABEL}=="", ENV{dir_name}="usbhd-%k"

ACTION=="add", ENV{mount_options}="relatime"
ACTION=="add", ENV{ID_FS_TYPE}=="vfat|ntfs", ENV{mount_options}="$env{mount_options},utf8,gid=100,umask=002"

ACTION=="add", ENV{ID_FS_TYPE}=="ext3|ext4", SUBSYSTEMS=="usb", SUBSYSTEM=="block", RUN+="/bin/mkdir -p /media/dextercai/%E{dir_name}", RUN+="/usr/bin/mount -o $env{mount_options} /mount_host/dev/%k /media/dextercai/%E{dir_name}"

ACTION=="remove", ENV{ID_FS_TYPE}=="ext3|ext4", SUBSYSTEMS=="usb", SUBSYSTEM=="block", ENV{dir_name}!="", RUN{program}+="/usr/bin/umount /media/dextercai/%E{dir_name}", RUN{program}+="/bin/rmdir /media/dextercai/%E{dir_name}"

LABEL="media_by_label_auto_mount_end"

简单解释一下为什么这么写。

  1. 整个规则大致包含了这几个动作类型,赋值、判断、执行,主要关键词在===!=RUN+=GOTO=。如果你略懂一些开发,相信你不难理解。每一行都可以看作是判断 + 赋值和执行的组合。
  2. %N是什么意思?或在完整系统上使用man udev5
  3. 比较难理解的大概是IMPORT{program}="/sbin/blkid -o udev -p /mount_host/%N"ENV{mount_options}
    blkid提供了一种为udev而设计的输出类型。形如ID_FS_TYPE=ext4,一行一个,可被udev导入。之后就可以在ENV中使用(如同一个map一样)。ENV{dir_name}%E{dir_name}是同一个东西。
  4. 完整系统可以使用systemd-mount,容器环境通常和systemd八字不合,因此容器环境内挂载命令改为了mount。systemd-mount的行为来看,并不会直接执行挂载,而是产生一个挂载点。具体挂载还是由用户层面的进程进行控制的。6

/sbin/blkid -o udev -p /mount_host/%N 输出

$ sudo blkid -o udev -p /dev/nvme0n1p2
ID_FS_UUID=b9c3bb71-e4e8-47c1-9905-************
ID_FS_UUID_ENC=b9c3bb71-e4e8-47c1-9905-************
ID_FS_VERSION=1.0
ID_FS_TYPE=ext4
ID_FS_USAGE=filesystem
ID_PART_ENTRY_SCHEME=gpt
ID_PART_ENTRY_UUID=ff42861a-ff6c-4499-93c2-************
ID_PART_ENTRY_TYPE=0fc63daf-8483-4772-8e79-************
ID_PART_ENTRY_NUMBER=2
ID_PART_ENTRY_OFFSET=1050624
ID_PART_ENTRY_SIZE=999163904
ID_PART_ENTRY_DISK=259:0

调试与监控

与内核打交道的部分总是令人抓狂的。 ——我说的

udev提供了udevadm程序,可以将问题简化。

通过udevadm monitor,可以监听内核和udev事件,可以用于未配置规则前,容器内检查是否可以正常获取到事件,可以看到当你插入不同设备时发生了什么。

udevadm monitor命令运行时,插入一个可移动设备,将看到各种信息在你的屏幕上滚动而出。主要关注ADD事件,当然如果够硬核的话,也可以直接去看内核日志。

udevadm info -a -n /dev/sda 这条命令,可以看到一些关于设备的基本udev信息,这也是上面那一个rules案例一些参数判断的来源。

udevadm control --reload-rules,可以在不重启udev守护进程的前提下,重载规则。当然在容器内我一般直接通过重启守护进程sudo -S service udev restart

挂载上的注意

   不要企图将宿主机的/dev直接挂载到容器内/dev。会变得不幸。

一般来说我们是肯定需要使用tty去进入容器调试的。tty参数设为true才能正常使用bash。k8s启动时会让docker需要使用的/dev/tmux点出现问题。具体原因未深入学习。

因此,既然我们的事件取于宿主机,/sys也属于宿主机,不妨为宿主机的/dev在容器内的挂载点设置一个别名。这也是上述rules规则为什么没有直接使用/dev的原因。

具体K8s配置因涉敏问题,暂不放出。

eg: docker run -v /dev:/from_host/dev ubuntu:20.04 bash

其他参考


  1. 尚未验证,目前实际我正在使用特权容器的方式
  2. AF_XXXX地址簇;PF_XXXX协议簇
  3. 在Linux下,AF_NETLINKPF_NETLINK是一个东西。参考:http://anoty.blogspot.com/2008/12/difference-between-afxxx-and-pfxxx.html
  4. 参考:https://github.com/Ferk/udev-media-automount
  5. man udev参考:https://linux.die.net/man/8/udev
  6. 待后续补充
Archives QR Code
QR Code for this page
Tipping QR Code