Linux 设备驱动匹配机制

概述

在 Linux 驱动模型里,经常会看到三类对象:

  • bus:总线,负责管理一类设备和驱动,并定义二者如何匹配;
  • device:设备,表示系统里已经存在的某个硬件或虚拟硬件对象;
  • driver:驱动,表示内核里能够驱动某类设备的软件实现。

它们之间最重要的一句话是:

device 和 driver 并不是直接互相寻找对方,而是都挂到同一个 bus 上,由 bus 的 match() 规则负责撮合。

所以 Linux 驱动模型不是简单的"加载一个驱动,然后驱动自己去扫硬件"。更准确地说,内核维护了一套统一的设备模型:设备注册进来,驱动也注册进来;只要二者属于同一个 bus,bus 就会尝试匹配,匹配成功后进入 probe()

本文先从通用 driver core 讲起,然后用 platform device / platform driver 展开,最后结合 dw-apb-uart 串口驱动说明:串口 driver 注册在哪里,串口 device 又是谁创建的。源码示例以本地 Linux 6.5.3 为参考。

1. 三个核心对象

1.1 bus_type:总线负责定义匹配规则

Linux 中的总线由 struct bus_type 描述,典型成员如下:

c 复制代码
struct bus_type {
	const char		*name;
	const char		*dev_name;

	int (*match)(struct device *dev, struct device_driver *drv);
	int (*uevent)(const struct device *dev, struct kobj_uevent_env *env);
	int (*probe)(struct device *dev);
	void (*sync_state)(struct device *dev);
	void (*remove)(struct device *dev);
	...
};

这里最关键的是 match()probe()

match() 决定一个 device 和一个 driver 是否匹配。不同 bus 有不同的匹配规则,比如:

  • PCI bus 根据 vendor/device id 匹配;
  • USB bus 根据 USB id table 匹配;
  • platform bus 可以根据 Device Tree compatible、ACPI id、platform id table、名字匹配;

probe() 则是匹配成功后的入口。很多 bus 的 probe() 只是一个中转函数,最终会调用到具体 driver 提供的 probe 函数。

1.2 device:设备是系统里已经存在的对象

通用设备对象由 struct device 描述:

c 复制代码
struct device {
	struct kobject kobj;
	struct device		*parent;
	struct bus_type		*bus;
	struct device_driver	*driver;
	void			*platform_data;
	...
};

重点是:

  • dev->bus 表示这个设备挂在哪个 bus 上;
  • dev->driver 表示匹配成功后绑定到哪个 driver;
  • dev->parent 表示设备层级关系;
  • platform_dataof_node / fwnode 等字段用于携带固件、板级或平台描述信息。

换句话说,device 不是凭空来的。它一定是某个内核子系统根据硬件枚举结果、固件描述、板级代码或虚拟化设备描述创建出来的。

1.3 device_driver:driver 是可以服务某类设备的实现

通用驱动对象由 struct device_driver 描述:

c 复制代码
struct device_driver {
	const char		*name;
	struct bus_type		*bus;
	struct module		*owner;
	const struct of_device_id	*of_match_table;
	const struct acpi_device_id	*acpi_match_table;
	int (*probe)(struct device *dev);
	void (*sync_state)(struct device *dev);
	int (*remove)(struct device *dev);
	...
};

驱动注册时,需要说明自己属于哪个 bus。比如 platform driver 属于 platform_bus_type,PCI driver 属于 pci_bus_type,I2C driver 属于 i2c_bus_type

driver core 只处理通用的注册、链表、sysfs、绑定和 probe 流程;至于怎么判断这个 driver 支不支持这个 device,则交给具体 bus 的 match()

2. driver 和 device 谁先注册都可以

Linux 的设计里,device 和 driver 没有严格的先后要求。

2.1 device 先注册

如果设备先出现,比如启动阶段 Device Tree 被解析后创建了一个 platform device,那么大致流程是:

text 复制代码
device_register()
  -> device_add()
    -> 把 device 加入所属 bus
    -> 在 sysfs 中创建设备节点
    -> 遍历 bus 上已有 driver
    -> 调用 bus->match(dev, drv)
    -> 匹配成功后调用 probe

也就是说,设备注册进来时,内核会拿它去找当前已经注册过的 driver。

2.2 driver 先注册

如果驱动先加载,比如模块被 modprobe 加载进内核,那么大致流程是:

text 复制代码
driver_register()
  -> bus_add_driver()
    -> 把 driver 加入所属 bus
    -> 遍历 bus 上已有 device
    -> 调用 bus->match(dev, drv)
    -> 匹配成功后调用 probe

也就是说,驱动注册进来时,内核会拿它去匹配当前已经存在的 device。

所以,无论是:

text 复制代码
先有 device,后有 driver

还是:

text 复制代码
先有 driver,后有 device

最终都会收敛到同一件事:

text 复制代码
device 和 driver 位于同一个 bus
  -> bus->match(dev, drv)
  -> 匹配成功
  -> really_probe()
  -> driver probe

3. platform bus 是什么

platform bus 是 Linux driver core 中的一种软件总线,用来承载 platform_deviceplatform_driver

它不是电气意义上的硬件总线,而是给这类设备准备的统一管理入口:

text 复制代码
无法通过硬件总线自动枚举,
但又需要注册成 Linux device,
并参与 driver core 匹配和 probe 流程的板级/SoC 设备。

这类设备通常直接集成在 SoC 或板级硬件中,有固定的寄存器地址、中断号、clock、reset 等资源。硬件本身不会像 PCI/USB 设备那样主动报告"我是谁、我在哪里、我支持什么能力",因此内核需要通过 Device Tree、ACPI、board file,或者父设备驱动创建的子设备来知道它们的存在。

典型的 platform device 包括:

  • UART 控制器;
  • GPIO 控制器;
  • I2C / SPI 控制器;
  • SATA/AHCI 控制器;
  • timer;
  • watchdog;
  • pinctrl;
  • clock controller。

从 Linux 设备模型看,platform bus 的角色很明确:维护 platform 设备和 platform 驱动,并定义二者如何匹配、匹配后如何进入 probe。Linux 6.5.3 中的定义大致如下:

c 复制代码
/* drivers/base/platform.c */
struct bus_type platform_bus_type = {
	.name		= "platform",
	.dev_groups	= platform_dev_groups,
	.match		= platform_match,
	.uevent		= platform_uevent,
	.probe		= platform_probe,
	.remove		= platform_remove,
	...
};

其中:

  • platform_match():判断 platform_deviceplatform_driver 是否匹配;
  • platform_probe():匹配成功后,调用具体 platform_driver 的 probe 函数;
  • platform_remove():设备和驱动解绑时执行清理。

需要注意的是,platform bus 不是 PCI、USB、I2C、SPI、MDIO 这些 bus 的替代品。它通常承载的是"控制器本体",而不是控制器后面挂着的外设。

例如一个 SoC 内部的 I2C 控制器可能是 platform_device,因为这个控制器本身是固定 MMIO 设备;但 I2C 控制器下面的 EEPROM、PMIC、sensor 通常会注册成 i2c_client,挂到 I2C bus 上。SATA 也是类似:SoC 内部的 AHCI 控制器可以是 platform_device,而 AHCI 控制器后面发现的硬盘则进入 libata / SCSI / block 层。

4. platform device 从哪里来

platform_device 是 platform bus 上的设备对象:

c 复制代码
struct platform_device {
	const char	*name;
	int		id;
	bool		id_auto;
	struct device	dev;
	u64		platform_dma_mask;
	struct device_dma_parameters dma_parms;
	u32		num_resources;
	struct resource	*resource;
	...
};

注意这里内嵌了一个通用的 struct device dev。也就是说,platform_device 是对通用 device 的一层封装。

它通常携带几类信息:

  • MMIO 寄存器地址,对应 regstruct resource
  • IRQ 中断号;
  • DMA 信息;
  • clock、reset、pinctrl 等资源;
  • Device Tree node 或 ACPI companion;
  • 设备名字和 id。

4.1 Device Tree 创建 platform device

在使用 Device Tree 的平台上,Device Tree 只描述硬件;是否创建成 platform_device,由内核的 OF/platform 扫描路径决定。这里的 OF 来自 Open Firmware,Linux 内核中很多 Device Tree 相关接口仍沿用 of_ 前缀。

例如一个 DesignWare APB UART 节点:

dts 复制代码
uart0: serial@ff180000 {
	compatible = "snps,dw-apb-uart";
	reg = <0xff180000 0x100>;
	interrupts = <...>;
	clocks = <...>;
	reg-shift = <2>;
	reg-io-width = <4>;
	status = "okay";
};

启动阶段,Device Tree / OF platform 代码会扫描合适的节点,并将其注册到 platform bus:

text 复制代码
of_platform_populate()
  -> of_platform_bus_create()
    -> of_platform_device_create_pdata()
      -> of_device_alloc()
      -> of_device_add()
        -> device_add(&dev->dev)

关键代码在 drivers/of/platform.cdrivers/of/device.c

c 复制代码
/* Linux 6.5.3: drivers/of/platform.c,关键路径摘录 */
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;

	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.coherent_dma_mask = DMA_BIT_MASK(32);
	if (!dev->dev.dma_mask)
		dev->dev.dma_mask = &dev->dev.coherent_dma_mask;
	dev->dev.bus = &platform_bus_type;
	dev->dev.platform_data = platform_data;
	of_msi_configure(&dev->dev, dev->dev.of_node);

	if (of_device_add(dev) != 0) {
		platform_device_put(dev);
		goto err_clear_flag;
	}

	return dev;

err_clear_flag:
	of_node_clear_flag(np, OF_POPULATED);
	return NULL;
}

of_device_alloc() 分配 platform_device,并把 DT node 关联到 dev->dev.of_nodedev->dev.bus = &platform_bus_type 决定该设备进入 platform bus。随后 of_device_add() 调用通用设备注册入口:

c 复制代码
/* Linux 6.5.3: drivers/of/device.c,关键路径摘录 */
int of_device_add(struct platform_device *ofdev)
{
	BUG_ON(ofdev->dev.of_node == NULL);

	/* name and id have to be set before the device enters platform bus */
	ofdev->name = dev_name(&ofdev->dev);
	ofdev->id = PLATFORM_DEVID_NONE;

	...

	/* 
	 * 因为前面已经设置 ofdev->dev.bus = &platform_bus_type,
	 * 这里会把设备加入 platform bus。
	 */
	return device_add(&ofdev->dev);
}

of_device_add() 本身不是重新选择 bus;它是在 ofdev->dev.bus 已经指向 platform_bus_type 的前提下,补齐 platform device 的 name/id,再调用 device_add() 完成设备注册。device_add(&ofdev->dev) 是通用 driver core 入口,会把设备加入 ofdev->dev.bus 指向的 bus。至于加入 bus 后如何匹配 driver,放到第 6 节再展开。

因此,Device Tree 创建 platform device 的核心动作是:

text 复制代码
DT node
  -> platform_device
  -> dev->dev.bus = &platform_bus_type
  -> device_add(&dev->dev)
  -> 设备进入 platform bus

并不是所有 DT node 都会创建成 platform_device。例如 ARM PrimeCell 节点会走 AMBA 设备创建路径:

c 复制代码
/* Linux 6.5.3: drivers/of/platform.c,关键路径摘录 */
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; /* 已按 AMBA device 处理,不再创建 platform_device */
}

/* 非 arm,primecell 节点走默认 OF/platform 路径 */
dev = of_platform_device_create_pdata(bus, bus_id, platform_data, parent);
if (!dev || !of_match_node(matches, bus))
	return 0;

这里的 return 0 很关键:PrimeCell 节点会提前走 AMBA 创建设备路径;只有没有命中这个特殊分支的节点,才继续调用 of_platform_device_create_pdata() 创建 platform_device

这也说明:DT node 本身不声明 Linux bus 类型;扫描它的内核代码决定最终创建哪种 struct device

text 复制代码
Device Tree / OF platform 扫描到的节点 -> platform_device
Device Tree / OF AMBA 特殊处理的节点  -> amba_device
I2C core 扫描的子节点    -> i2c_client
SPI core 扫描的子节点    -> spi_device

4.2 ACPI 创建 platform device

在 x86、服务器平台或部分 ARM64 平台上,设备可能来自 ACPI。

ACPI 表里会描述设备的 _HID_CID、资源窗口、中断等信息。内核 ACPI 子系统解析这些表后,也可以创建相应的 platform device。

对于使用 ACPI 匹配的 platform driver,常见写法是:

c 复制代码
.acpi_match_table = xxx_acpi_match,

这个表的匹配规则放到第 6 节统一说明。

4.3 board file 手动注册 platform device

更老的内核或某些没有使用 Device Tree / ACPI 的平台,可能在板级代码里手动注册:

c 复制代码
static struct resource xxx_uart_resources[] = {
	DEFINE_RES_MEM(...),
	DEFINE_RES_IRQ(...),
};

static struct platform_device xxx_uart_device = {
	.name		= "dw-apb-uart",
	.id		= 0,
	.resource	= xxx_uart_resources,
	.num_resources	= ARRAY_SIZE(xxx_uart_resources),
};

platform_device_register(&xxx_uart_device);

这种方式现在在新平台上少很多,但它最能说明本质:platform device 就是平台代码告诉内核"这里有一个设备"。

4.4 父设备驱动动态创建子设备

还有一种情况是,某个父设备 probe 后,发现自己下面还有子设备,于是动态创建 platform device。

例如一些 MFD、SoC glue driver、特殊总线控制器驱动,可能会在自己的 probe 过程中注册子 platform device。

这说明 platform device 不一定只来自启动阶段,也可以在运行期由其他驱动创建。

5. platform driver 是怎么注册的

platform_driver 是 platform bus 上的驱动对象:

c 复制代码
struct platform_driver {
	int (*probe)(struct platform_device *);
	void (*remove)(struct platform_device *);
	void (*shutdown)(struct platform_device *);
	int (*suspend)(struct platform_device *, pm_message_t state);
	int (*resume)(struct platform_device *);
	struct device_driver driver;
	const struct platform_device_id *id_table;
	bool prevent_deferred_probe;
	...
};

同样,它也内嵌了一个通用的 struct device_driver driver

注册 platform driver 常见写法是(以 dw8250 串口驱动为例):

c 复制代码
static struct platform_driver dw8250_platform_driver = {
	.driver = {
		.name		= "dw-apb-uart",
		.pm		= pm_ptr(&dw8250_pm_ops),
		.of_match_table	= dw8250_of_match,
		.acpi_match_table = dw8250_acpi_match,
	},
	.probe			= dw8250_probe,
	.remove			= dw8250_remove,
};

module_platform_driver(dw8250_platform_driver);

module_platform_driver() 是一个辅助宏,大致展开后就是:

c 复制代码
static int __init xxx_init(void)
{
	return platform_driver_register(&xxx_platform_driver);
}

static void __exit xxx_exit(void)
{
	platform_driver_unregister(&xxx_platform_driver);
}

module_init(xxx_init);
module_exit(xxx_exit);

platform_driver_register() 最终会把这个 driver 注册到 platform_bus_type 上:

text 复制代码
platform_driver_register()
  -> __platform_driver_register()
    -> drv->driver.bus = &platform_bus_type
    -> driver_register(&drv->driver)

之后 driver core 会遍历 platform bus 上已有的 platform device,尝试匹配。

6. platform device 和 platform driver 怎么匹配

platform bus 的匹配函数是 platform_match()。它不是只比较名字,而是按多个来源依次尝试:

c 复制代码
static int platform_match(struct device *dev, struct device_driver *drv)
{
	struct platform_device *pdev = to_platform_device(dev);
	struct platform_driver *pdrv = to_platform_driver(drv);

	/* When driver_override is set, only bind to the matching driver */
	if (pdev->driver_override)
		return !strcmp(pdev->driver_override, drv->name);

	/* Attempt an OF style match first */
	if (of_driver_match_device(dev, drv))
		return 1;

	/* Then try ACPI style match */
	if (acpi_driver_match_device(dev, drv))
		return 1;

	/* Then try to match against the id table */
	if (pdrv->id_table)
		return platform_match_id(pdrv->id_table, pdev) != NULL;

	/* fall-back to driver name match */
	return (strcmp(pdev->name, drv->name) == 0);
}

6.1 Device Tree compatible 匹配

如果 device 来自 Device Tree,那么 platform_device.dev.of_node 会指向对应的设备树节点。

以 dw_8250 串口驱动为例:

c 复制代码
static const struct of_device_id dw8250_of_match[] = {
	{ .compatible = "snps,dw-apb-uart", .data = &dw8250_dw_apb },
	{ .compatible = "cavium,octeon-3860-uart", .data = &dw8250_octeon_3860_data },
	{ .compatible = "marvell,armada-38x-uart", .data = &dw8250_armada_38x_data },
	{ .compatible = "renesas,rzn1-uart", .data = &dw8250_renesas_rzn1_data },
	{ .compatible = "starfive,jh7100-uart", .data = &dw8250_starfive_jh7100_data },
	{ /* Sentinel */ }
};
MODULE_DEVICE_TABLE(of, dw8250_of_match);

然后:

c 复制代码
static struct platform_driver dw8250_platform_driver = {
	.driver = {
		.name		= "dw-apb-uart",
		.pm		= pm_ptr(&dw8250_pm_ops),
		.of_match_table	= dw8250_of_match,
		.acpi_match_table = dw8250_acpi_match,
	},
	.probe			= dw8250_probe,
	.remove			= dw8250_remove,
};

如果 DTS 里的 compatible 属性和 driver 的 of_match_table 中的 compatible 属性对上,就匹配成功。

6.2 ACPI 匹配

ACPI 平台则通过 ACPI id 表匹配,还是以 dw_8250 串口驱动为例:

c 复制代码
static const struct acpi_device_id dw8250_acpi_match[] = {
	{ "80860F0A", (kernel_ulong_t)&dw8250_dw_apb },
	{ "8086228A", (kernel_ulong_t)&dw8250_dw_apb },
	{ "AMD0020", (kernel_ulong_t)&dw8250_dw_apb },
	{ "AMDI0020", (kernel_ulong_t)&dw8250_dw_apb },
	{ "AMDI0022", (kernel_ulong_t)&dw8250_dw_apb },
	{ "APMC0D08", (kernel_ulong_t)&dw8250_dw_apb},
	{ "BRCM2032", (kernel_ulong_t)&dw8250_dw_apb },
	{ "HISI0031", (kernel_ulong_t)&dw8250_dw_apb },
	{ "INT33C4", (kernel_ulong_t)&dw8250_dw_apb },
	{ "INT33C5", (kernel_ulong_t)&dw8250_dw_apb },
	{ "INT3434", (kernel_ulong_t)&dw8250_dw_apb },
	{ "INT3435", (kernel_ulong_t)&dw8250_dw_apb },
	{ },
};
MODULE_DEVICE_TABLE(acpi, dw8250_acpi_match);

然后:

c 复制代码
static struct platform_driver dw8250_platform_driver = {
	.driver = {
		.name		= "dw-apb-uart",
		.pm		= pm_ptr(&dw8250_pm_ops),
		.of_match_table	= dw8250_of_match,
		.acpi_match_table = dw8250_acpi_match,
	},
	.probe			= dw8250_probe,
	.remove			= dw8250_remove,
};

固件里的 ACPI 表大概会描述类似这样的设备对象(_HID、_CID 命中任意一个即可):

c 复制代码
Device (UART)
{
	Name (_HID, "80860F0A")
	Name (_CID, "INT3434")
	Name (_CRS, ResourceTemplate () {
		Memory32Fixed (...)
		Interrupt (...)
	})
}

6.3 platform_device_id 匹配

id_tableplatform_driver 提供的一组传统 platform 设备名。它不匹配 DT 的 compatible,也不匹配 ACPI _HID,而是匹配 platform_device.name

Linux 6.5.3 中的关键逻辑如下:

c 复制代码
/* Linux 6.5.3: drivers/base/platform.c,关键路径摘录 */
static const struct platform_device_id *platform_match_id(
			const struct platform_device_id *id,
			struct platform_device *pdev)
{
	while (id->name[0]) {
		if (strcmp(pdev->name, id->name) == 0) {
			pdev->id_entry = id;
			return id;
		}
		id++;
	}
	return NULL;
}

典型写法是:

c 复制代码
static const struct platform_device_id xxx_ids[] = {
	{ "xxx-device", 0 },
	{ }
};
MODULE_DEVICE_TABLE(platform, xxx_ids);

static struct platform_driver xxx_driver = {
	.probe = xxx_probe,
	.id_table = xxx_ids,
	.driver = {
		.name = "xxx-device",
	},
};

这里真正参与比较的是:

text 复制代码
platform_device.name  <->  platform_device_id.name

这类匹配主要服务于非 DT/ACPI 或兼容旧 board file 的 platform device。对于 DT 创建的 device,如果 driver 提供了 of_match_tablecompatible 能匹配,通常会先在 Device Tree compatible 匹配阶段成功,不会走到 id_table

6.4 name 匹配

如果前面的方式都没有匹配上,最后还可以比较:

text 复制代码
platform_device.name
platform_driver.driver.name

比如 device 的名字是 "dw-apb-uart",driver 的名字也是 "dw-apb-uart",也可以匹配。

不过在现代 DT/ACPI 平台上,更推荐通过 compatible 或 ACPI id 匹配,因为同一个 driver 可能支持多个兼容硬件变体。

7. probe 是怎么被调用到具体驱动的

platform_match() 返回成功后,driver core 会进入通用 probe 流程:

text 复制代码
device_add()
	-> bus_probe_device
		-> __device_attach
			-> driver_probe_device()
				-> really_probe()
    				-> dev->driver = drv
    					-> call_driver_probe()
							-> bus->probe(dev)

对于 platform bus 来说:

text 复制代码
bus->probe(dev)
  -> platform_probe(dev)
    -> platform_driver->probe(platform_device)
c 复制代码
static int platform_probe(struct device *_dev)
{
	struct platform_driver *drv = to_platform_driver(_dev->driver);
	struct platform_device *dev = to_platform_device(_dev);
	int ret;
	......
		if (drv->probe) {
			ret = drv->probe(dev);
			if (ret)
				dev_pm_domain_detach(_dev, true);
		}
	......
}

也就是最终会调用到驱动里写的:

c 复制代码
static int xxx_probe(struct platform_device *pdev)
{
	...
}

9. 小结

bus_type 定义一类设备和驱动的管理方式,尤其是 match()probe()

device 表示系统中已经存在的硬件或虚拟硬件对象,它可能来自硬件枚举、Device Tree、ACPI、board file,或者其他驱动动态创建。

driver 表示可以驱动某类设备的软件实现。driver 注册时会挂到某个 bus 上。

device 和 driver 的匹配不是由二者私下完成的,而是由 bus 统一完成:

text 复制代码
device_register()
driver_register()
  -> 都会触发 bus 上的匹配流程
  -> bus->match(dev, drv)
  -> 匹配成功
  -> bus->probe(dev)
  -> driver->probe(...)

对于 platform 设备来说:

text 复制代码
DTS / ACPI / board file / 父设备驱动
  -> 创建 platform_device
  -> 注册到 platform_bus_type

platform_driver_register()
  -> 注册 platform_driver 到 platform_bus_type

platform_match()
  -> compatible / ACPI id / id_table / name 匹配

platform_probe()
  -> 调用具体 platform_driver->probe()

对于 dw-apb-uart 串口驱动来说:

text 复制代码
dw8250_platform_driver 只是注册串口控制器 driver;
真正的 UART platform_device 通常来自 DTS 或 ACPI;
匹配成功后 driver core 调用 dw8250_probe(pdev)。

所以看驱动代码时,可以先问三个问题:

text 复制代码
1. 这个 driver 注册到了哪个 bus?
2. 对应的 device 是谁创建的?
3. bus 的 match 规则用什么字段把它们匹配起来?

这三个问题想清楚,Linux 里大多数 driver probe 链路就能顺下来了。

相关推荐
卵男(章鱼)1 小时前
第4章:软件架构与SOA
linux·车载系统·汽车
KaifuZeng2 小时前
模电面试问题汇总二
单片机·嵌入式硬件·面试·模电·通信与接口协议
是摆烂第一名呀2 小时前
【RK3506】RGB屏幕与背光调试记录
linux·arm开发·驱动开发·嵌入式硬件
bu_shuo2 小时前
计算机二级学习-软件工程基础
学习·软件工程
浩瀚地学2 小时前
【面试算法笔记】0105-数组-螺旋矩阵
java·开发语言·笔记·算法·面试
qetfw2 小时前
CentOS 7 搭建 DNS 主从服务
linux·运维·centos·dns·bind
KaifuZeng2 小时前
模电面试问题汇总三
单片机·嵌入式硬件·面试·模电
worilb2 小时前
Spring Cloud 学习与实践(15):使用 TraceId 与 MDC 串联微服务日志
学习·spring cloud·微服务