searchusermenu
  • 发布文章
  • 消息中心
点赞
收藏
评论
分享
原创

浅谈sysfs,kobject,uevent与udev

2024-07-15 09:44:53
162
0

在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/下创建设备或是执行一些命令。
new_device.PNG

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的操作,流程如下:
sd_probe.PNG

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的流程如下:
scsi_remove.PNG

udev监听netlink事件
udev的守护进程是systemd-udevd,从src/udev/udevd.c:main作为入口,udev会监听文件,获取uevent事件,并结合rules处理uevent中附带的字符串。如果是"remove"事件,除了对应rules外,还要先单独删除文件等:
system-udevd.PNG

0条评论
0 / 1000
N****g
2文章数
2粉丝数
N****g
2 文章 | 2 粉丝
N****g
2文章数
2粉丝数
N****g
2 文章 | 2 粉丝
原创

浅谈sysfs,kobject,uevent与udev

2024-07-15 09:44:53
162
0

在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/下创建设备或是执行一些命令。
new_device.PNG

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的操作,流程如下:
sd_probe.PNG

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的流程如下:
scsi_remove.PNG

udev监听netlink事件
udev的守护进程是systemd-udevd,从src/udev/udevd.c:main作为入口,udev会监听文件,获取uevent事件,并结合rules处理uevent中附带的字符串。如果是"remove"事件,除了对应rules外,还要先单独删除文件等:
system-udevd.PNG

文章来自个人专栏
文章 | 订阅
0条评论
0 / 1000
请输入你的评论
0
0