在Linux系统中,接入了系统的设备,比如磁盘设备sda、sdb或是网卡等,会在/sys/devices/下生成对应的设备目录和设备文件,形如
/sys/devices/pci0000:00/0000:00:10.0/host2/target2:0:0/2:0:0:0/block/sda
的格式
而在/sys/block/
等目录下,则是指向上述目录的链接。
[root@localhost block]# ls -l
total 0
lrwxrwxrwx 1 root root 0 May 23 10:02 dm-0 -> ../devices/virtual/block/dm-0
lrwxrwxrwx 1 root root 0 May 23 10:02 sda -> ../devices/pci0000:00/0000:00:10.0/host2/target2:0:0/2:0:0:0/block/sda
lrwxrwxrwx 1 root root 0 May 23 16:31 sdc -> ../devices/pci0000:00/0000:00:10.0/host2/target2:0:1/2:0:1:0/block/sdc
lrwxrwxrwx 1 root root 0 May 23 10:02 sr0 -> ../devices/pci0000:00/0000:00:07.1/ata2/host1/target1:0:0/1:0:0:0/block/sr0
/sys/
目录是内核提供的名为sysfs的一种机制。每一个device在内核中都会创建kobject对象,并由sysfs在/sys/
目录下创建文件。通过对这些文件进行读/写等操作,可以实现对内核设备的变更。这是Linux提供的用户态对内核态操作,与内核态通知用户态的接口。
systemd-udev是用户空间的设备管理。udev在管理设备时,也是在/sys/
目录下。udev通过对sysfs下的文件读/写来实现对设备的控制。
比如udevadm trigger,会对/sys/devices/pci0000:00/../uevent写入'add','remove'或'change'等命令,调用到sysfs的ops接口,最终操纵到对应的device。
同时,当device发生变化时,比如接入/拔出了一个设备,又会通过内核的uevent机制,将这个变化通知到udev。udev根据uevent中的内容,与/lib/udev/rules.d/
下的规则做匹配。匹配到相同类型的device后,可以在/sys/下创建设备或是执行一些命令。
1. kobject
提到sysfs,就离不开kobject。因为sysfs下的文件是基于kobject进行注册和管理的。
kobject用来描述一个内核对象。struct kobject定义在include/linux/kobject.h
,其定义如下:
struct kobject {
const char *name;
struct list_head entry;
struct kobject *parent;
struct kset *kset;
struct kobj_type *ktype;
struct kernfs_node *sd; /* sysfs directory entry */
struct kref kref;
#ifdef CONFIG_DEBUG_KOBJECT_RELEASE
struct delayed_work release;
#endif
unsigned int state_initialized:1;
unsigned int state_in_sysfs:1;
unsigned int state_add_uevent_sent:1;
unsigned int state_remove_uevent_sent:1;
unsigned int uevent_suppress:1;
RH_KABI_RESERVE(1)
RH_KABI_RESERVE(2)
RH_KABI_RESERVE(3)
RH_KABI_RESERVE(4)
};
name:kobject的名字,对应的就是
/sys/
下的一个目录
parent:kobject的父级目录。/sys/
下的目录通常都是复数级,每一级都是一个kobject;通过parent来层层指定最终要在哪个目录下创建kobject
ktype:kobject属于的类型
kset:kobject归属的kset
kobj_type
每一个kobject都应当对应的一个kobj_type
struct kobj_type {
void (*release)(struct kobject *kobj);
const struct sysfs_ops *sysfs_ops; //ops提供读写文件时对应的操作
struct attribute **default_attrs; //该kobj_type类型的目录默认的attrs
const struct kobj_ns_type_operations *(*child_ns_type)(struct kobject *kobj);
const void *(*namespace)(struct kobject *kobj);
RH_KABI_RESERVE(1)
RH_KABI_RESERVE(2)
RH_KABI_RESERVE(3)
RH_KABI_RESERVE(4)
};
struct sysfs_ops {
ssize_t (*show)(struct kobject *, struct attribute *, char *); //sysfs下的读操作
ssize_t (*store)(struct kobject *, struct attribute *, const char *, size_t); //sysfs下的写操作
};
kset
kobject描述的是一个内核对象,kset描述的是一堆内核对象的集合,其定义如下:
struct kset {
struct list_head list;
spinlock_t list_lock;
struct kobject kobj;
const struct kset_uevent_ops *uevent_ops;
RH_KABI_RESERVE(1)
RH_KABI_RESERVE(2)
RH_KABI_RESERVE(3)
RH_KABI_RESERVE(4)
} __randomize_layout;
list:这个集合下的所有kobject,均放在这个list上
kobj:kset本身也是一个内核对象,故内嵌一个kobject用来描述自身
uevent_ops:用来处理uevent事件的操作集
kobj_attribute
kobj_attribute用来描述koject下的文件。比如我们看/sys/block/sda,其中uevent是可写入的:
-r--r--r-- 1 root root 4096 Jun 6 09:53 stat
lrwxrwxrwx 1 root root 0 Jun 6 09:53 subsystem -> ../../../../../../../../class/block
drwxr-xr-x 2 root root 0 Jun 6 09:56 trace
-rw-r--r-- 1 root root 4096 Jun 6 09:53 uevent
大部分情况下,sysfs的同类目录下会存在相同风格命名的可读/写文件,修改这些文件能修改内核配置,这就是由kobj_attribute控制的
struct kobj_attribute {
struct attribute attr;
ssize_t (*show)(struct kobject *kobj, struct kobj_attribute *attr,
char *buf);
ssize_t (*store)(struct kobject *kobj, struct kobj_attribute *attr,
const char *buf, size_t count);
};
attr:表示这个attribute的名字,即文件名
(*show):写操作
(*store):读操作
sysfs_create_groups(struct kobject *kobj, const struct attribute_group **groups)
用于在一个kobject下创建attribute组。
2. /sys/devices/下device的创建与添加
/sys/devices/
属于一个kset对象,以该目录为例介绍一个device如何与koject关联起来
/sys/devices/
的创建在drivers\base\core.c
中。
devices_init
这个函数创建了/sys/devices/这个kset,并创建了/sys/dev/block
与/sys/dev/char
:
devices_kset = kset_create_and_add("devices", &device_uevent_ops, NULL);
if (!devices_kset)
return -ENOMEM;
dev_kobj = kobject_create_and_add("dev", NULL);
if (!dev_kobj)
goto dev_kobj_err;
sysfs_dev_block_kobj = kobject_create_and_add("block", dev_kobj);
if (!sysfs_dev_block_kobj)
goto block_kobj_err;
sysfs_dev_char_kobj = kobject_create_and_add("char", dev_kobj);
device_register
注册一个device到/sys/devices
,由init与add组成(也有的地方,没有直接调用device_register
,而是分开调用的init与add)。
int device_register(struct device *dev)
{
device_initialize(dev);
return device_add(dev);
}
device_initialize
新初始化一个device。初始化的时候,会将这个device的kobject所属的kset指向devices_kset
dev->kobj.kset = devices_kset; //指定所属的kset
kobject_init(&dev->kobj, &device_ktype); //初始化device的kobj,并指定其ktype为device_ktype
device_ktype提供的ops是dev_sysfs_ops
static struct kobj_type device_ktype = {
.release = device_release,
.sysfs_ops = &dev_sysfs_ops,
.namespace = device_namespace,
};
dev_sysfs_ops定义的操作如下:
static const struct sysfs_ops dev_sysfs_ops = {
.show = dev_attr_show,
.store = dev_attr_store,
};
device_add
将device加入到/sys/deivces
下。会调用kobject_add,将device->kobject加入到device->kobject.parent下
1.设置device->kobject的name
/* dev_set_name会为device->kobject设置name */
/* 设置name调用的接口是kobject_set_name_vargs(&dev->kobj, fmt, vargs) */
if (dev->init_name) {
dev_set_name(dev, "%s", dev->init_name);
dev->init_name = NULL;
}
/* subsystems can specify simple device enumeration */
if (!dev_name(dev) && dev->bus && dev->bus->dev_name)
dev_set_name(dev, "%s%u", dev->bus->dev_name, dev->id);
如果是一个pci设备,这里的名字应该是pcixxx。
2.将device添加到devices下,并在/sys/下创建目录、文件以及符号链接等
error = kobject_add(&dev->kobj, dev->kobj.parent, NULL); //添加kobj,会将其添加到parent kobj下
error = device_create_file(dev, &dev_attr_uevent);
error = device_add_class_symlinks(dev); //在parent或是class目录下建立一个指向device的链接。比如/sys/class/block
如下图,创建的链接指向了device所在的目录
[root@localhost block]# cd /sys/class/block/
[root@localhost block]# ls -l
total 0
lrwxrwxrwx 1 root root 0 May 30 10:01 dm-0 -> ../../devices/virtual/block/dm-0
lrwxrwxrwx 1 root root 0 May 30 17:34 sda -> ../../devices/pci0000:00/0000:00:10.0/host2/target2:0:0/2:0:0:0/block/sda
lrwxrwxrwx 1 root root 0 May 30 17:34 sda1 -> ../../devices/pci0000:00/0000:00:10.0/host2/target2:0:0/2:0:0:0/block/sda/sda1
lrwxrwxrwx 1 root root 0 May 30 17:34 sda2 -> ../../devices/pci0000:00/0000:00:10.0/host2/target2:0:0/2:0:0:0/block/sda/sda2
lrwxrwxrwx 1 root root 0 May 30 10:01 sda3 -> ../../devices/pci0000:00/0000:00:10.0/host2/target2:0:0/2:0:0:0/block/sda/sda3
lrwxrwxrwx 1 root root 0 May 30 17:34 sdb -> ../../devices/pci0000:00/0000:00:10.0/host2/target2:0:1/2:0:1:0/block/sdb
lrwxrwxrwx 1 root root 0 May 30 17:34 sr0 -> ../../devices/pci0000:00/0000:00:07.1/ata2/host1/target1:0:0/1:0:0:0/block/sr0
3.添加device的attrs、bus等
error = device_add_attrs(dev); //在device下添加attrs,attrs即设备目录下的各个文件
error = bus_add_device(dev); //在bus目录下添加device
error = dpm_sysfs_add(dev)
4.触发一个add的uevent,并probe设备
kobject_uevent(&dev->kobj, KOBJ_ADD);
bus_probe_device(dev);
以sd_probe
举例一个新的scsi device加入后,sysfs与uevent的操作,流程如下:
3. uevent通知机制
udev作为用户空间设备管理,其与sysfs是通过uevent机制通信的。对于内核的device发生的变化,udev通过.rules匹配后再执行相应的操作。
/* src/udev/udev-rules.c */
/* 从全局变量rules_dirs可以看出,rules文件的路径位置 */
static const char* const rules_dirs[] = {
"/etc/udev/rules.d",
"/run/udev/rules.d",
UDEVLIBEXECDIR "/rules.d",
NULL
};
udev通过.rules在/dev/目录下创建或管理用户空间设备。以下述这条规则举例,当发现新接入了设备是nvme类型时,会在/dev/disk/by-id下创建一个链接:
# NVMe
KERNEL=="nvme*[0-9]n*[0-9]", ATTR{wwid}=="?*", SYMLINK+="disk/by-id/nvme-$attr{wwid}"
KERNEL=="nvme*[0-9]n*[0-9]p*[0-9]", ENV{DEVTYPE}=="partition", ATTRS{wwid}=="?*", SYMLINK+="disk/by-id/nvme-$attr{wwid}-part%n"
# forward scsi device event to corresponding block device
ACTION=="change", SUBSYSTEM=="scsi", ENV{DEVTYPE}=="scsi_device", TEST=="block", ATTR{block/*/uevent}="change"
前面在提及/sys/devices
的初始化函数devices_init时,涉及到了结构体kset
与入参device_uevent_ops
。
kset下有成员uevent_ops,即这个目录对应的uevent操作。device_uevent_ops
就是当/sys/devices
这个目录发生变化时会调用的uevent操作。
static const struct kset_uevent_ops device_uevent_ops = {
.filter = dev_uevent_filter,
.name = dev_uevent_name,
.uevent = dev_uevent, /* 发送uevent的函数 */
};
kobject_uevent_env
kobject_uevent_env
用于发送uevent事件,会调用到.uevent
接口。在发送uevent前会拼装字符串env,env的形式同udev的.rules文件是相同的。
也就是说,发送uevent后,udev就能够用env中的字符串同.rules中的规则进行匹配,匹配到相同规则的device后可以进行处理。
/*
* kobj:kobject对象,通过kobj_to_dev(kobj)能找到相应的device结构
* action:uevent类型,包括add/remove/change等
* envp_ext:传递给uevent的字符串数据,用来描述一个device
*/
int kobject_uevent_env(struct kobject *kobj, enum kobject_action action,
char *envp_ext[])
{
/* 记录本次device发生的ACTION */
const char *action_string = kobject_actions[action];
/* 从kset_uevent_ops的.name中获取device的SUBSYSTEM */
if (uevent_ops && uevent_ops->name)
subsystem = uevent_ops->name(kset, kobj);
else
subsystem = kobject_name(&kset->kobj);
/* 从kobj->parent开始,层层往下拼装device的路径DEVPATH */
devpath = kobject_get_path(kobj, GFP_KERNEL);
/* 将上面获取到的内容与device传入的数据以字符串形式存入env */
/* 这些字符串的形式可以看到同.rules文件是相同的 */
/* default keys */
retval = add_uevent_var(env, "ACTION=%s", action_string);
if (retval)
goto exit;
retval = add_uevent_var(env, "DEVPATH=%s", devpath);
if (retval)
goto exit;
retval = add_uevent_var(env, "SUBSYSTEM=%s", subsystem);
if (retval)
goto exit;
/* keys passed in from the caller */
if (envp_ext) {
for (i = 0; envp_ext[i]; i++) {
retval = add_uevent_var(env, "%s", envp_ext[i]);
if (retval)
goto exit;
}
}
/* 循环找到kobj的最上层parent。 */
/* 对于deivce而言,就是/sys/devices */
top_kobj = kobj;
while (!top_kobj->kset && top_kobj->parent)
top_kobj = top_kobj->parent;
/* 用top_kobj的kset来发送uevent */
/* 对于device而言,就是dev_uevent */
/* kset通知uevent的目的是通知到device的上一层(比如bus/class) */
kset = top_kobj->kset;
uevent_ops = kset->uevent_ops;
retval = uevent_ops->uevent(kset, kobj, env);
/* kset发送uevent后,再网络广播 */
retval = add_uevent_var(env, "SEQNUM=%llu", (unsigned long long)++uevent_seqnum);
retval = kobject_uevent_net_broadcast(kobj, env, action_string,
devpath);
}
/* 通知uevent事件时,kobject_uevent这个接口用的更为广泛 */
int kobject_uevent(struct kobject *kobj, enum kobject_action action)
{
return kobject_uevent_env(kobj, action, NULL);
}
dev_uevent
/sys/devices下新增一个device,触发uevent通知add事件时,首先会调用devices的uevent接口dev_uevent。这个接口的目的是为了通知device的bus/class/type。在/sys下,除了/devices/目录外,还有/bus/与/class/等目录,而里面有些文件是通过链接指向了/devices/的:
[root@localhost sys]# ls -l
total 0
drwxr-xr-x 2 root root 0 May 30 10:01 block
drwxr-xr-x 35 root root 0 May 30 10:01 bus
drwxr-xr-x 58 root root 0 May 30 10:01 class
drwxr-xr-x 4 root root 0 May 30 10:01 dev
drwxr-xr-x 15 root root 0 May 30 10:01 devices
drwxr-xr-x 6 root root 0 May 30 10:01 firmware
drwxr-xr-x 7 root root 0 May 30 10:01 fs
drwxr-xr-x 2 root root 0 May 30 10:01 hypervisor
drwxr-xr-x 14 root root 0 May 30 10:01 kernel
drwxr-xr-x 158 root root 0 May 30 10:01 module
drwxr-xr-x 2 root root 0 May 30 10:01 power
/* 添加dev有关的信息 */
if (dev->type && dev->type->name)
add_uevent_var(env, "DEVTYPE=%s", dev->type->name);
if (dev->driver)
add_uevent_var(env, "DRIVER=%s", dev->driver->name);
/* 如果有对应的bus/class/type,则通过uevent通知 */
/* have the bus specific function add its stuff */
if (dev->bus && dev->bus->uevent) {
retval = dev->bus->uevent(dev, env);
if (retval)
pr_debug("device: '%s': %s: bus uevent() returned %d\n",
dev_name(dev), __func__, retval);
}
/* have the class specific function add its stuff */
if (dev->class && dev->class->dev_uevent) {
retval = dev->class->dev_uevent(dev, env);
if (retval)
pr_debug("device: '%s': %s: class uevent() "
"returned %d\n", dev_name(dev),
__func__, retval);
}
/* have the device type specific function add its stuff */
if (dev->type && dev->type->uevent) {
retval = dev->type->uevent(dev, env);
if (retval)
pr_debug("device: '%s': %s: dev_type uevent() "
"returned %d\n", dev_name(dev),
__func__, retval);
}
kobject_uevent_net_broadcast
通过netlink广播uevent事件。udev监听socket,sysfs通过socket将uevent广播,事件最终被udev接收到。
/* send netlink message */
list_for_each_entry(ue_sk, &uevent_sock_list, list) {
struct sock *uevent_sock = ue_sk->sk;
if (!netlink_has_listeners(uevent_sock, 1))
continue;
if (!skb) {
retval = -ENOMEM;
skb = alloc_uevent_skb(env, action_string, devpath);
if (!skb)
continue;
}
retval = netlink_broadcast(uevent_sock, skb_get(skb), 0, 1,
GFP_KERNEL);
/* ENOBUFS should be handled in userspace */
if (retval == -ENOBUFS || retval == -ESRCH)
retval = 0;
}
4.udev处理uevent
uevent是基于netlink实现的内核态(sysfs)与用户态(udev)通信
内核态在uevent_net_init
创建netlink的socket:
ue_sk->sk = netlink_kernel_create(net, NETLINK_KOBJECT_UEVENT, &cfg); //创建uevent的socket,使用的类型是NETLINK_KOBJECT_UEVENT
list_add_tail(&ue_sk->list, &uevent_sock_list); //将创建的socket添加到全局list uevent_sock_list下
在uevent_net_broadcast_untagged
中遍历uevent_sock_list的所有socket并广播uevent:
list_for_each_entry(ue_sk, &uevent_sock_list, list) {
retval = netlink_broadcast(uevent_sock, skb_get(skb), 0, 1,
GFP_KERNEL);
}
udev监听netlink socket
在netlink_family_table中定义了监听的socket事件类型,其中有NETLINK_KOBJECT_UEVENT:
static const char* const netlink_family_table[] = {
[NETLINK_ROUTE] = "route",
[NETLINK_FIREWALL] = "firewall",
[NETLINK_INET_DIAG] = "inet-diag",
[NETLINK_NFLOG] = "nflog",
[NETLINK_XFRM] = "xfrm",
[NETLINK_SELINUX] = "selinux",
[NETLINK_ISCSI] = "iscsi",
[NETLINK_AUDIT] = "audit",
[NETLINK_FIB_LOOKUP] = "fib-lookup",
[NETLINK_CONNECTOR] = "connector",
[NETLINK_NETFILTER] = "netfilter",
[NETLINK_IP6_FW] = "ip6-fw",
[NETLINK_DNRTMSG] = "dnrtmsg",
[NETLINK_KOBJECT_UEVENT] = "kobject-uevent",
[NETLINK_GENERIC] = "generic",
[NETLINK_SCSITRANSPORT] = "scsitransport",
[NETLINK_ECRYPTFS] = "ecryptfs",
[NETLINK_RDMA] = "rdma",
};
netlink_family_to_string_alloc
将netlink类型(NETLINK_KOBJECT_UEVENT)转换成字符串("kobject-uevent")
netlink_family_from_string
将字符串("kobject-uevent")转换成netlink类型(NETLINK_KOBJECT_UEVENT)
socket_address_parse_netlink
处理netlink的socket,会通过netlink_family_from_string
获取netlink类型并填充netlink的socket
udev处理事件解读
以REMOVE事件为例讲解内核删除一个device到udev收到该事件的流程。
udev的REMOVE事件基本定义在/lib/udev/rules.d/50-udev-default.rules
:
ACTION=="remove", ENV{REMOVE_CMD}!="", RUN+="$env{REMOVE_CMD}"
ACTION=="remove", GOTO="default_end"
当碰到"remove"事件时,如果REMOVE_CMD不为空,就执行REMOVE_CMD;否则跳转到default_end。
而在kernel里,触发"remove"事件时,可以看函数scsi_remove_single_device
scsi_remove_single_device
/* 对/proc/scsi/scsi写操作的函数是proc_scsi_write */
/* 执行echo remove-single-device x x x x > /proc/scsi/scsi可以触发该函数 */
/* 该函数用于remove一个scsi设备 */
/* 接收4个参数,通过这4个参数确定scsi host下的scsi device */
static int scsi_remove_single_device(uint host, uint channel, uint id, uint lun)
{
struct scsi_device *sdev;
struct Scsi_Host *shost;
//通过host找到scsi host
shost = scsi_host_lookup(host);
//在scsi host下,通过channel id lun找到scsi device
sdev = scsi_device_lookup(shost, channel, id, lun);
if (sdev) {
//remove scsi device
scsi_remove_device(sdev);
scsi_device_put(sdev);
error = 0;
}
}
从触发scsi device remove到通知uevent的流程如下:
udev监听netlink事件
udev的守护进程是systemd-udevd,从src/udev/udevd.c:main作为入口,udev会监听文件,获取uevent事件,并结合rules处理uevent中附带的字符串。如果是"remove"事件,除了对应rules外,还要先单独删除文件等: