linux驱动模型基础(1)

 

在开发中我们并不会真的去写一个总线模型,我们分析总线驱动模型是为了搞清楚总线是如何将driver和device匹配的,即当我们插入我们的设备时,总线是如何找到这个设备对应的驱动,并且调用驱动的probe函数的。不管是先有驱动后有设备、还是先有设备后有驱动。都会调用驱动driver中的probe,而不是设备device。

 

linux驱动模型是分成三个部分的,

1. 设备(结构体device),

2. 驱动(结构体device_driver),

3. 总线(结构体bus_type)

 

 这个模型管理着linux的驱动,让他们在开机之后能正常驱动硬件有序的干活。这一切需要从一棵树讲起,我说的这棵树就是我们常说的dts(device tree source),搞懂搞透设备树是我们可以正确开发硬件驱动的前提,因为设备树是配置板级信息的地方。我们所说的设备树起源于openfirmware,所以linux关于设备树的接口都是以of开头的,存放这些核心API的源代码路径是drivers/of/of.c

首先,我们需要搞清楚一个流程,那就是driver是如何找到硬件的,驱动模型告诉我们是device主动去找driver或者driver主动去找device的,就像一男一女想要通过相亲来解决个人问题,如果没有一个红娘,他们没有机会认识彼此的,bus_type就是我说的这个红娘,她来负责牵线,让两个人开始相识,后面会不会相知相守,她就不会管了。如果想让红娘给你们牵线的前提,就是红娘都要认识你们,那么device和driver是如何让红娘都认识的呢,就是两者需要通过红娘提供的注册接口来注册个人信息,不过bus_type比较勤劳,也比较敬业,只要你一注册,她就会立马触发匹配流程,也就是说,一旦有一方注册,她就会立马把她认识的所有的异性给你介绍一遍,让你挨个去相亲,如果你找到了你心仪的另外一半,就把彼此交给对方,堕入爱河吧。那如果你都不满意,不好意思了,只能等你心仪的另外一半来找你了。接下来我们从device/driver双方角度结合代码分别说明这个匹配流程。

@device端发起匹配:
在linux启动并初始化的地方,会有一个专门解析设备树的地方,他就在著名的start_kernel这个函数里面,让我们来看看这个梦开始的地方,是如何一步一步让万千男女堕入爱河的。
下面是流程图,结合这个流程图再去细究每一个函数模块

asmlinkage __visible void __init start_kernel(void)
{
	char *command_line;
	char *after_dashes;
        。。。。此处省略大量无关代码
	setup_arch(&command_line);//可以看到,这个地方开 始解析设备树
        。。。。此处省略大量无关代码
	/* Do the rest non-__init'ed, we're now alive */
	rest_init();
}

再看看setup_arch(&command_line)是如何写的吧

void __init setup_arch(char **cmdline_p)
{
	
         。。。。此处省去大量无关代码
	setup_machine_fdt(__fdt_pointer);//第一步是把设备树三个重要的节点解析出来
         。。。。此处省去大量无关代码
	

	if (acpi_disabled) {
		unflatten_device_tree();//第二步是把所有子节点解析出来
		psci_dt_init();
	} else {
		psci_acpi_init();
	}
	 。。。。此处省去大量无关代码
}

细究一下这两个函数

static void __init setup_machine_fdt(phys_addr_t dt_phys)
{
	void *dt_virt = fixmap_remap_fdt(dt_phys);

	if (!dt_virt || !early_init_dt_scan(dt_virt)) {
		pr_crit("\n"
			"Error: invalid device tree blob at physical address %pa (virtual address 0x%p)\n"
			"The dtb must be 8-byte aligned and must not exceed 2 MB in size\n"
			"\nPlease check your bootloader.",
			&dt_phys, dt_virt);

		while (true)
			cpu_relax();
	}

	dump_stack_set_arch_desc("%s (DT)", of_flat_dt_get_machine_name());
}
bool __init early_init_dt_scan(void *params)
{
	bool status;

	status = early_init_dt_verify(params);
	if (!status)
		return false;

	early_init_dt_scan_nodes();
	return true;
}
  •  
void __init early_init_dt_scan_nodes(void)
{
	/* Retrieve various information from the /chosen node */
	of_scan_flat_dt(early_init_dt_scan_chosen, boot_command_line);//从chosen节点解析出bootargs并存放到boot_command_line这个全局变量

	/* Initialize {size,address}-cells info */
	of_scan_flat_dt(early_init_dt_scan_root, NULL);//从根节点中解析出address-cells和size-cells并存放在dt_root_addr_cells和dt_root_size_cells两个全局变量中

	/* Setup memory, calling early_init_dt_add_memory_arch */
	of_scan_flat_dt(early_init_dt_scan_memory, NULL);//解析memory节点并初始化内存
}
  • 第一次扫描节点并解析出三个对于系统来说非常重要的三个节点
    接下来是重头戏,解析出root节点下所有的子设备节点,也是我们需要重点了解和掌握的。
void __init unflatten_device_tree(void)
{
	__unflatten_device_tree(initial_boot_params, &of_root,
				early_init_dt_alloc_memory_arch);

	/* Get pointer to "/chosen" and "/aliases" nodes for use everywhere */
	of_alias_scan(early_init_dt_alloc_memory_arch);
}
static void __unflatten_device_tree(const void *blob,
			     struct device_node **mynodes,
			     void * (*dt_alloc)(u64 size, u64 align))
{
	unsigned long size;
	int start;
	void *mem;

	pr_debug(" -> unflatten_device_tree()\n");

	if (!blob) {
		pr_debug("No device tree pointer\n");
		return;
	}

	pr_debug("Unflattening device tree:\n");
	pr_debug("magic: %08x\n", fdt_magic(blob));
	pr_debug("size: %08x\n", fdt_totalsize(blob));
	pr_debug("version: %08x\n", fdt_version(blob));

	if (fdt_check_header(blob)) {
		pr_err("Invalid device tree blob header\n");
		return;
	}

	/* First pass, scan for size */
	start = 0;
	size = (unsigned long)unflatten_dt_node(blob, NULL, &start, NULL, NULL, 0, true);
	size = ALIGN(size, 4);

	pr_debug("  size is %lx, allocating...\n", size);

	/* Allocate memory for the expanded device tree */
	mem = dt_alloc(size + 4, __alignof__(struct device_node));
	memset(mem, 0, size);

	*(__be32 *)(mem + size) = cpu_to_be32(0xdeadbeef);

	pr_debug("  unflattening %p...\n", mem);

	/* Second pass, do actual unflattening */
	start = 0;
	unflatten_dt_node(blob, mem, &start, NULL, mynodes, 0, false);
	if (be32_to_cpup(mem + size) != 0xdeadbeef)
		pr_warning("End of tree marker overwritten: %08x\n",
			   be32_to_cpup(mem + size));

	pr_debug(" <- unflatten_device_tree()\n");
}
  •  
static void * unflatten_dt_node(const void *blob,
                void *mem,
                int *poffset,
                struct device_node *dad,
                struct device_node **nodepp,
                unsigned long fpsize,
                bool dryrun)
{
    const __be32 *p;
    struct device_node *np;
    struct property *pp, **prev_pp = NULL;
    const char *pathp;
    unsigned int l, allocl;
    static int depth;
    int old_depth;
    int offset;
    int has_name = 0;
    int new_format = 0;

    /* 获取node节点的name指针到pathp中 */
    pathp = fdt_get_name(blob, *poffset, &l);
    if (!pathp)
        return mem;

    allocl = ++l;

    /* version 0x10 has a more compact unit name here instead of the full
     * path. we accumulate the full path size using "fpsize", we'll rebuild
     * it later. We detect this because the first character of the name is
     * not '/'.
     */
    if ((*pathp) != '/') {
        new_format = 1;
        if (fpsize == 0) {
            /* root node: special case. fpsize accounts for path
             * plus terminating zero. root node only has '/', so
             * fpsize should be 2, but we want to avoid the first
             * level nodes to have two '/' so we use fpsize 1 here
             */
            fpsize = 1;
            allocl = 2;
            l = 1;
            pathp = "";
        } else {
            /* account for '/' and path size minus terminal 0
             * already in 'l'
             */
            fpsize += l;
            allocl = fpsize;
        }
    }

    /* 分配struct device_node内存,包括路径全称大小 */
    np = unflatten_dt_alloc(&mem, sizeof(struct device_node) + allocl,
                __alignof__(struct device_node));
    if (!dryrun) {
        char *fn;
        of_node_init(np);

        /* 填充full_name,full_name指向该node节点的全路径名称字符串 */
        np->full_name = fn = ((char *)np) + sizeof(*np);
        if (new_format) {
            /* rebuild full path for new format */
            if (dad && dad->parent) {
                strcpy(fn, dad->full_name);
                fn += strlen(fn);
            }
            *(fn++) = '/';
        }
        memcpy(fn, pathp, l);

        /* 节点挂接到相应的父节点、子节点和姊妹节点 */
        prev_pp = &np->properties;
        if (dad != NULL) {
            np->parent = dad;
            np->sibling = dad->child;
            dad->child = np;
        }
    }
    /* 处理该node节点下面所有的property */
    for (offset = fdt_first_property_offset(blob, *poffset);
         (offset >= 0);
         (offset = fdt_next_property_offset(blob, offset))) {
        const char *pname;
        u32 sz;

        if (!(p = fdt_getprop_by_offset(blob, offset, &pname, &sz))) {
            offset = -FDT_ERR_INTERNAL;
            break;
        }

        if (pname == NULL) {
            pr_info("Can't find property name in list !\n");
            break;
        }
        if (strcmp(pname, "name") == 0)
            has_name = 1;
        pp = unflatten_dt_alloc(&mem, sizeof(struct property),
                    __alignof__(struct property));
        if (!dryrun) {
            /* We accept flattened tree phandles either in
             * ePAPR-style "phandle" properties, or the
             * legacy "linux,phandle" properties.  If both
             * appear and have different values, things
             * will get weird.  Don't do that. */

            /* 处理phandle,得到phandle值 */
            if ((strcmp(pname, "phandle") == 0) ||
                (strcmp(pname, "linux,phandle") == 0)) {
                if (np->phandle == 0)
                    np->phandle = be32_to_cpup(p);
            }
            /* And we process the "ibm,phandle" property
             * used in pSeries dynamic device tree
             * stuff */
            if (strcmp(pname, "ibm,phandle") == 0)
                np->phandle = be32_to_cpup(p);
            pp->name = (char *)pname;
            pp->length = sz;
            pp->value = (__be32 *)p;
            *prev_pp = pp;
            prev_pp = &pp->next;
        }
    }
    /* with version 0x10 we may not have the name property, recreate
     * it here from the unit name if absent
     */
    /* 为每个node节点添加一个name的属性 */
    if (!has_name) {
        const char *p1 = pathp, *ps = pathp, *pa = NULL;
        int sz;

        /* 属性name的value值为node节点的名称,取“/”和“@”之间的子串,设备和驱动的别名匹配用的就是这个地方的name */
        while (*p1) {
            if ((*p1) == '@')
                pa = p1;
            if ((*p1) == '/')
                ps = p1 + 1;
            p1++;
        }
        if (pa < ps)
            pa = p1;
        sz = (pa - ps) + 1;
        pp = unflatten_dt_alloc(&mem, sizeof(struct property) + sz,
                    __alignof__(struct property));
        if (!dryrun) {
            pp->name = "name";
            pp->length = sz;
            pp->value = pp + 1;
            *prev_pp = pp;
            prev_pp = &pp->next;
            memcpy(pp->value, ps, sz - 1);
            ((char *)pp->value)[sz - 1] = 0;
        }
    }
    /* 填充device_node结构体中的name和type成员 */
    if (!dryrun) {
        *prev_pp = NULL;
        np->name = of_get_property(np, "name", NULL);
        np->type = of_get_property(np, "device_type", NULL);

        if (!np->name)
            np->name = "<NULL>";
        if (!np->type)
            np->type = "<NULL>";
    }

    old_depth = depth;
    *poffset = fdt_next_node(blob, *poffset, &depth);
    if (depth < 0)
        depth = 0;
    /* 递归调用node节点下面的子节点 */
    while (*poffset > 0 && depth > old_depth)
        mem = unflatten_dt_node(blob, mem, poffset, np, NULL,
                    fpsize, dryrun);

    if (*poffset < 0 && *poffset != -FDT_ERR_NOTFOUND)
        pr_err("unflatten: error %d processing FDT\n", *poffset);

    /*
     * Reverse the child list. Some drivers assumes node order matches .dts
     * node order
     */
    if (!dryrun && np->child) {
        struct device_node *child = np->child;
        np->child = NULL;
        while (child) {
            struct device_node *next = child->sibling;
            child->sibling = np->child;
            np->child = child;
            child = next;
        }
    }

    if (nodepp)
        *nodepp = np;

    return mem;
}
  •  
  •  
  •  

至此,内核已经全部把设备树解析出来并存放在device_node这个结构体指针当中,观察这个结构体的组成,我们可以看出来这个结构体是一个双向链表,因为一旦得到一个设备节点,我们就可以从他的parent和child中得到这个设备树中的每一个设备节点的信息。那么问题来了,设备树是解析出来了,有啥用呢,当然是为了“相亲”。

在setup.c文件中有一个函数被编译进入内核,成为内核img的一部分,这个函数就是为了在设备树被解析出来之后进行驱动匹配的。

static int __init arm64_device_init(void)
{
	if (of_have_populated_dt()) {
		of_iommu_init();
		of_platform_populate(NULL, of_default_bus_match_table,
				     NULL, NULL);
	} else if (acpi_disabled) {
		pr_crit("Device tree not populated\n");
	}
	return 0;
}
arch_initcall_sync(arm64_device_init);

这个函数中的of_platform_puoulate函数就是为了把内核解析出来的设备节点填装到platform_device这个平台设备结构体中并和已经注册到platform总线的驱动进行匹配,让我们看看这个流程

int of_platform_populate(struct device_node *root,
			const struct of_device_id *matches,
			const struct of_dev_auxdata *lookup,
			struct device *parent)
{
	struct device_node *child;
	int rc = 0;

	root = root ? of_node_get(root) : of_find_node_by_path("/");
	if (!root)
		return -EINVAL;

        //遍历根节点下的每一个子设备节点并把device_node的信息填充到创建platform_device中
	for_each_child_of_node(root, child) {
		rc = of_platform_bus_create(child, matches, lookup, parent, true);
		if (rc) {
			of_node_put(child);
			break;
		}
	}
	of_node_set_flag(root, OF_POPULATED_BUS);

	of_node_put(root);
	return rc;
}
  •  
static int of_platform_bus_create(struct device_node *bus,
				  const struct of_device_id *matches,
				  const struct of_dev_auxdata *lookup,
				  struct device *parent, bool strict)
{
	const struct of_dev_auxdata *auxdata;
	struct device_node *child;
	struct platform_device *dev;
	const char *bus_id = NULL;
	void *platform_data = NULL;
	int rc = 0;

        /* 只有包含"compatible"属性的node节点才会生成相应的platform_device结构体 */
	/* Make sure it has a compatible property */
	if (strict && (!of_get_property(bus, "compatible", NULL))) {
		pr_debug("%s() - skipping %s, no compatible prop\n",
			 __func__, bus->full_name);
		return 0;
	}

	auxdata = of_dev_lookup(lookup, bus);
	if (auxdata) {
		bus_id = auxdata->name;
		platform_data = auxdata->platform_data;
	}

	if (of_device_is_compatible(bus, "arm,primecell")) {
		/*
		 * Don't return an error here to keep compatibility with older
		 * device tree files.
		 */
		of_amba_device_create(bus, bus_id, platform_data, parent);
		return 0;
	}

	dev = of_platform_device_create_pdata(bus, bus_id, platform_data, parent);
	if (!dev || !of_match_node(matches, bus))
		return 0;

	for_each_child_of_node(bus, child) {
		pr_debug("   create child: %s\n", child->full_name);
		rc = of_platform_bus_create(child, matches, lookup, &dev->dev, strict);
		if (rc) {
			of_node_put(child);
			break;
		}
	}
	of_node_set_flag(bus, OF_POPULATED_BUS);
	return rc;
}
static struct platform_device *of_platform_device_create_pdata(
					struct device_node *np,
					const char *bus_id,
					void *platform_data,
					struct device *parent)
{
	struct platform_device *dev;

        /* 
     * 针对节点下面得到status = "ok" 或者status = "okay"或者不存在status属性的
     * 节点分配内存并填充platform_device结构体
     */
	if (!of_device_is_available(np) ||
	    of_node_test_and_set_flag(np, OF_POPULATED))
		return NULL;

	dev = of_device_alloc(np, bus_id, parent);
	if (!dev)
		goto err_clear_flag;

	dev->dev.bus = &platform_bus_type;
	dev->dev.platform_data = platform_data;
	of_dma_configure(&dev->dev, dev->dev.of_node);
	of_msi_configure(&dev->dev, dev->dev.of_node);

        //of_device_add函数就是把platform_device用平台总线去匹配驱动了
	if (of_device_add(dev) != 0) {
		of_dma_deconfigure(&dev->dev);
		platform_device_put(dev);
		goto err_clear_flag;
	}

	return dev;

err_clear_flag:
	of_node_clear_flag(np, OF_POPULATED);
	return NULL;
}
int of_device_add(struct platform_device *ofdev)
{
	BUG_ON(ofdev->dev.of_node == NULL);

	/* name and id have to be set so that the platform bus doesn't get
	 * confused on matching */
	ofdev->name = dev_name(&ofdev->dev);
	ofdev->id = -1;

	/*
	 * If this device has not binding numa node in devicetree, that is
	 * of_node_to_nid returns NUMA_NO_NODE. device_add will assume that this
	 * device is on the same node as the parent.
	 */
	set_dev_node(&ofdev->dev, of_node_to_nid(ofdev->dev.of_node));

	return device_add(&ofdev->dev);
}
int device_add(struct device *dev)
{
	struct device *parent = NULL;
	struct kobject *kobj;
	struct class_interface *class_intf;
	int error = -EINVAL;
	struct kobject *glue_dir = NULL;

	dev = get_device(dev);
	if (!dev)
		goto done;

	if (!dev->p) {
		error = device_private_init(dev);
		if (error)
			goto done;
	}

	/*
	 * for statically allocated devices, which should all be converted
	 * some day, we need to initialize the name. We prevent reading back
	 * the name, and force the use of dev_name()
	 */
	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);

	if (!dev_name(dev)) {
		error = -EINVAL;
		goto name_error;
	}

	pr_debug("device: '%s': %s\n", dev_name(dev), __func__);

	parent = get_device(dev->parent);
	kobj = get_device_parent(dev, parent);
	if (IS_ERR(kobj)) {
		error = PTR_ERR(kobj);
		goto parent_error;
	}
	if (kobj)
		dev->kobj.parent = kobj;

	/* use parent numa_node */
	if (parent && (dev_to_node(dev) == NUMA_NO_NODE))
		set_dev_node(dev, dev_to_node(parent));

	/* first, register with generic layer. */
	/* we require the name to be set before, and pass NULL */
	error = kobject_add(&dev->kobj, dev->kobj.parent, NULL);
	if (error) {
		glue_dir = get_glue_dir(dev);
		goto Error;
	}

	/* notify platform of device entry */
	if (platform_notify)
		platform_notify(dev);

	error = device_create_file(dev, &dev_attr_uevent);
	if (error)
		goto attrError;

	error = device_add_class_symlinks(dev);
	if (error)
		goto SymlinkError;
	error = device_add_attrs(dev);
	if (error)
		goto AttrsError;
	error = bus_add_device(dev);
	if (error)
		goto BusError;
	error = dpm_sysfs_add(dev);
	if (error)
		goto DPMError;
	device_pm_add(dev);

	if (MAJOR(dev->devt)) {
		error = device_create_file(dev, &dev_attr_dev);
		if (error)
			goto DevAttrError;

		error = device_create_sys_dev_entry(dev);
		if (error)
			goto SysEntryError;

		devtmpfs_create_node(dev);
	}

	/* Notify clients of device addition.  This call must come
	 * after dpm_sysfs_add() and before kobject_uevent().
	 */
	if (dev->bus)
		blocking_notifier_call_chain(&dev->bus->p->bus_notifier,
					     BUS_NOTIFY_ADD_DEVICE, dev);

	kobject_uevent(&dev->kobj, KOBJ_ADD);
	bus_probe_device(dev);
	if (parent)
		klist_add_tail(&dev->p->knode_parent,
			       &parent->p->klist_children);

	if (dev->class) {
		mutex_lock(&dev->class->p->mutex);
		/* tie the class to the device */
		klist_add_tail(&dev->knode_class,
			       &dev->class->p->klist_devices);

		/* notify any interfaces that the device is here */
		list_for_each_entry(class_intf,
				    &dev->class->p->interfaces, node)
			if (class_intf->add_dev)
				class_intf->add_dev(dev, class_intf);
		mutex_unlock(&dev->class->p->mutex);
	}
done:
	put_device(dev);
	return error;
 SysEntryError:
	if (MAJOR(dev->devt))
		device_remove_file(dev, &dev_attr_dev);
 DevAttrError:
	device_pm_remove(dev);
	dpm_sysfs_remove(dev);
 DPMError:
	bus_remove_device(dev);
 BusError:
	device_remove_attrs(dev);
 AttrsError:
	device_remove_class_symlinks(dev);
 SymlinkError:
	device_remove_file(dev, &dev_attr_uevent);
 attrError:
	kobject_uevent(&dev->kobj, KOBJ_REMOVE);
	glue_dir = get_glue_dir(dev);
	kobject_del(&dev->kobj);
 Error:
	cleanup_glue_dir(dev, glue_dir);
parent_error:
	put_device(parent);
name_error:
	kfree(dev->p);
	dev->p = NULL;
	goto done;
}
EXPORT_SYMBOL_GPL(device_add);
  •  
  •  
  •  
void bus_probe_device(struct device *dev)
{
	struct bus_type *bus = dev->bus;
	struct subsys_interface *sif;

	if (!bus)
		return;

	if (bus->p->drivers_autoprobe)
		device_initial_probe(dev);

	mutex_lock(&bus->p->mutex);
	list_for_each_entry(sif, &bus->p->interfaces, node)
		if (sif->add_dev)
			sif->add_dev(dev, sif);
	mutex_unlock(&bus->p->mutex);
}
void device_initial_probe(struct device *dev)
{
	__device_attach(dev, true);
}
static int __device_attach(struct device *dev, bool allow_async)
{
	int ret = 0;

	struct device_node *of_device_node = dev->of_node;

	printk("[yzr] device name: %s\n",of_device_node->name);

	device_lock(dev);
	if (dev->driver) {
		if (klist_node_attached(&dev->p->knode_driver)) {
			ret = 1;
			goto out_unlock;
		}
		ret = device_bind_driver(dev);
		if (ret == 0)
			ret = 1;
		else {
			dev->driver = NULL;
			ret = 0;
		}
	} else {
		struct device_attach_data data = {
			.dev = dev,
			.check_async = allow_async,
			.want_async = false,
		};

		if (dev->parent)
			pm_runtime_get_sync(dev->parent);
                //遍历总线上所有的platform_driver并调用总线的match函数触发匹配的动作
		ret = bus_for_each_drv(dev->bus, NULL, &data,
					__device_attach_driver);
		if (!ret && allow_async && data.have_async) {
			/*
			 * If we could not find appropriate driver
			 * synchronously and we are allowed to do
			 * async probes and there are drivers that
			 * want to probe asynchronously, we'll
			 * try them.
			 */
			dev_dbg(dev, "scheduling asynchronous probe\n");
			get_device(dev);
			async_schedule(__device_attach_async_helper, dev);
		} else {
			pm_request_idle(dev);
		}

		if (dev->parent)
			pm_runtime_put(dev->parent);
	}
out_unlock:
	device_unlock(dev);
	return ret;
}
static int __device_attach_driver(struct device_driver *drv, void *_data)
{
	struct device_attach_data *data = _data;
	struct device *dev = data->dev;
	bool async_allowed;

	/*
	 * Check if device has already been claimed. This may
	 * happen with driver loading, device discovery/registration,
	 * and deferred probe processing happens all at once with
	 * multiple threads.
	 */
	if (dev->driver)
		return -EBUSY;

	if (!driver_match_device(drv, dev))
		return 0;

	async_allowed = driver_allows_async_probing(drv);

	if (async_allowed)
		data->have_async = true;

	if (data->check_async && async_allowed != data->want_async)
		return 0;

        匹配成功调用platform_driver的probe函数进行硬件的初始化动作
	return driver_probe_device(drv, dev);
}
static inline int driver_match_device(struct device_driver *drv,
				      struct device *dev)
{
	return drv->bus->match ? drv->bus->match(dev, drv) : 1;
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值