arm64架构的linux中断分析

文章目录

    • 1. 中断的概念和作用
    • 2. Linux中断处理机制
      • 2.1 中断请求
      • 2.2 中断处理
      • 2.3 中断完成
      • 2.4.中断触发和处理步骤详解
        • 2.4.1 异常向量表的解读
    • 3. GICv3中断控制器
      • 3.1 GICv3中断控制器设备树
      • 3.2 GICv3中断控制器驱动
    • 4. GIC的下一级中断控制器
      • 4.1 设备树
      • 4.2 内核对设备树的处理
        • 4.2.1 irq_domain_translate
        • 4.2.2 irq_domain_alloc_irqs
        • 4.2.3 irq_create_mapping
      • 4.3 链式中断驱动

1. 中断的概念和作用

当计算机的CPU需要在执行任务的同时响应外部事件时,中断是一种重要的机制。中断是异步事件的一种形式,这是指发起事件与处理事件之间的时间间隔没有固定的模式,而是在不同的时间点发生的。

在计算机系统中,中断可分为软件中断和硬件中断两类。在软件中断中,中断是由CPU执行的一条特殊指令造成的,它用于暂停CPU的正常执行流程,然后转而执行一个内部事件或处理程序。硬件中断则是由硬件设备发出的,它与处理器无关,并向CPU发出中断请求信号。

中断的作用主要有以下几个方面:

  1. 响应外部事件:当外部事件发生时,中断能够及时响应并引导CPU执行事件处理程序,保证事件能够得到及时处理。

  2. 提高系统效率:CPU无需主动地去轮询外部设备是否有数据需要处理,中断机制可以使CPU在处理器时间的同时响应必要的事件处理。

  3. 处理复杂任务:某些外部事件处理需要一段复杂的代码,中断处理程序可以协助CPU快速完成这些复杂任务。

  4. 进行设备驱动:硬件设备的驱动需要中断的支持,来达到与CPU通讯和协调工作的目的。

2. Linux中断处理机制

Linux操作系统具有广泛的应用,它的中断处理机制是实时、高效和可扩展的。Linux中断处理机制采用了事件驱动模型,对中断按照优先级进行排队,以确保最优先处理高优先级事件,同时保证低优先级事件不会被阻塞。下面从中断请求、中断处理和中断完成三个方面介绍Linux中断处理机制。

2.1 中断请求

在Linux中,中断被认为是分离的、独立的事件,因此中断的处理必须是实时的,使用优先级表进行调度和排序。Linux内核实现了基于可插拔中断体系结构的抽象机制,从而可以在处理器和设备机制之间提供更好的耦合性和深入性。这种体系结构允许中断请求线共享,提高了系统的可扩展性和可靠性,提升了系统的响应速度和性能表现。
当一个硬件设备需要处理器的处理时,它将通过硬件 Intline 请求一个中断。中断请求将被系统中断控制器传递,并被处理器接收。 Linux 支持多个中断请求,通常使用 PCI 设备接口来实现。当控制器接收到中断请求后,它会将请求传递给 Linux 中的 IRQ 子系统,于是就有了一个中断。

2.2 中断处理

中断处理是指系统响应中断事件的过程,它通常包含以下几个步骤:

  • 中断调度:中断请求被发送到CPU中心处理器上的中断控制器后,由硬件中断控制器将中断信号传递到内核。Linux内核根据中断向量号找到对应的中断处理程序。
  • 应答中断:内核通过设置正确的输入 / 输出引脚响应中断,这样中断控制器就可以发送指令将中断信息传递给 CPU。
  • 中断上下文的保存:当内核处理中断时,必须保存当前进程运行的上下文,为后续恢复工作做好准备。
  • 中断处理程序运行:中断程序处理中断,执行需要的中断服务程序,并最终返回到主线程。
  • 中断恢复工作:恢复中断执行前的上下文,这包括无序通知的应用程序。

2.3 中断完成

当中断处理完成后,处理器会向中断控制器发送完成中断的信号。中断控制器将中断信号发送回硬件设备。在中断完成的过程中,主要包含以下几个步骤:

  1. 中断状态恢复:在中断处理过程中,处理器会保存中断处理程序执行前的所有中断状态,包括进程的上下文、寄存器和状态字等。因此,在中断处理程序执行结束后,处理器需要通过对这些状态进行恢复,来重新运行之前被中断的进程。

  2. 设备控制器响应:当设备控制器接收到中断完成信号后,将在下一个可用的时钟周期中恢复设备操作,以继续处理新的输入或输出请求。此时,处理器将会准备新的请求或服务,以响应下一次中断请求。

中断完成是中断处理机制的最后一个阶段,通过该阶段可以使得设备控制器继续其后续的工作,同时为下一次中断请求做好准备。

2.4.中断触发和处理步骤详解

我们以gpio中断位例子讲解一下arm64中断的完整流程,先看看cpu到gpio的硬件上是怎么样的一个关系:
在这里插入图片描述
看到上面这个图片我们可以看到中断触发的流程:外部设备(比如按键)→GPIO控制器→GIC→CPU。
当触发流程走到cpu后,cpu会跳转到中断异常向量表,执行后面的操作:读取GIC信息,了解到是GPIO触发的中断,再读取GPIO控制器的信息,了解到是某一个GPIO的外设导致的,执行这个外设的中断处理函数。
看到上面这个简单的过程,感觉还是很不清晰。

arm64中断初始化,cpu接受到中断后为什么会自动跳转到异常向量表呢?
这是因为linux初始化的时候,会把异常向量表vectors的地址写入到vbar_el1寄存器中,我们linux内核触发了中断,则会自动跳转到vbar_el1这个这个地址上运行。具体初始化可以查看linux内核启动分析(一)。

我们再看看异常向量表是怎么样的,linux的异常向量表代码在arm64\kernel\entry.S:

SYM_CODE_START(vectors)kernel_ventry	1, sync_invalid			// Synchronous EL1tkernel_ventry	1, irq_invalid			// IRQ EL1tkernel_ventry	1, fiq_invalid			// FIQ EL1tkernel_ventry	1, error_invalid		// Error EL1tkernel_ventry	1, sync				// Synchronous EL1hkernel_ventry	1, irq				// IRQ EL1hkernel_ventry	1, fiq_invalid			// FIQ EL1hkernel_ventry	1, error			// Error EL1hkernel_ventry	0, sync				// Synchronous 64-bit EL0kernel_ventry	0, irq				// IRQ 64-bit EL0kernel_ventry	0, fiq_invalid			// FIQ 64-bit EL0kernel_ventry	0, error			// Error 64-bit EL0#ifdef CONFIG_COMPATkernel_ventry	0, sync_compat, 32		// Synchronous 32-bit EL0kernel_ventry	0, irq_compat, 32		// IRQ 32-bit EL0kernel_ventry	0, fiq_invalid_compat, 32	// FIQ 32-bit EL0kernel_ventry	0, error_compat, 32		// Error 32-bit EL0
#elsekernel_ventry	0, sync_invalid, 32		// Synchronous 32-bit EL0kernel_ventry	0, irq_invalid, 32		// IRQ 32-bit EL0kernel_ventry	0, fiq_invalid, 32		// FIQ 32-bit EL0kernel_ventry	0, error_invalid, 32		// Error 32-bit EL0
#endif
SYM_CODE_END(vectors)

2.4.1 异常向量表的解读

其实,在 ARM64 体系结构中,异常分为同步异常和异步异常。
同步异常是试图执行指令时生成的异常,或是作为指令的执行结果生成的异常。同步异常包括如下。

  1. 系统调用。异常级别 0 使用 svc指令陷入异常级别 1,异常级别1 使用hv指令陷入异常级别2,异常级别 2 使用 smc指令陷入异常级别 3。
  2. 数据中止,即访问数据时的页错误异常,虚拟地址没有映射到物理地址,或者没有写权限。
  3. 指令中止,即取指令时的页错误异常,虚拟地址没有映射到物理地址,或者没有执行权限。
  4. 栈指针或指令地址没有对齐。
  5. 没有定义的指令。
  6. 调试异常。

异步异常不是由正在执行的指令生成的,和正在执行的指令没有关联。异步异常包括以下。

  1. 中断(normal priority interrupt,IRQ),即普通优先级的中断。
  2. 快速中断(fast interrupt,FIQ),即高优先级的中断。
  3. 系统错误(System Error,SError),是由硬件错误触发的异常,例如最常见的是把脏数据从缓存行写回内存时触发异步的数据中止异常。

当异常发生的时候,处理器需要执行异常的处理程序。存储异常处理程序的内存位置称为异常向量,通常把所有异常向量存放在一张表中,称为异常向量表。对于 ARM64 处理器的异常级别 1、2 和 3,每个异常级别都有自己的异常向量表,异常向量表的起始虚拟地址存放在寄存器 VBAR_ELn(向量基准地址寄存器,Vector Based Address Register)中。每个异常向量表有 16 项,分为 4 组,每组 4项,每项的长度是 128 字节(可以存放32 条指令)中。

我们看到有4组,每一组的4项都是分别表示发生了同步异常,irq,firq和系统错误。这4组的区别就是,第一组表示异常发生在EL0,处理异常的特权等级也是EL0。第二组表示异常发生在ELn(n可以为1,2,3),处理异常的特权等级也是ELn(n可以为1,2,3),但是这里是linux内核,所以我们的特权为EL1,我们可以理解为异常发生在EL1,处理异常的特权等级也是EL1。这两组的共同点是异常的发生和处理在同一个特权级别,不需要进行特区那级别的切换;而后面两组则是异常的发生和处理不在同一个特权级别,需要进行特区那级别的切换。在linux这里就是说,第三组和第四组表示异常发生在EL0,但是异常处理却在EL1,而他们的区别就是,第三组表示异常发生在64位环境下,第四组表示异常发生在32位环境下。
我们现在知道这4组,16项的含义了,我们看fiq都是invalid的,是因为linux不支持fiq,所以没有对fiq异常进行处理。我们也看到第一组都是invalid的,是因为EL0发生了异常,不会在EL0处理,会陷入EL1处理,所以第一组也不需要实现。
第二组第一项kernel_ventry 1, sync ,在这里就是b el1_sync ,也就是跳转到el1_sync函数了。到这里,我们把每一项支持的异常向量跳转函数写成表格就是:

异常向量跳转的函数
kernel_ventry 1, syncel1_sync
kernel_ventry 1, irqel1_irq
kernel_ventry 1, errorel1_error
kernel_ventry 0, syncel0_sync
kernel_ventry 0, irqel0_irq
kernel_ventry 0, errorel0_error
kernel_ventry 0, sync_compat, 32el0_sync_compat
kernel_ventry 0, irq_compat, 32el0_irq_compat
kernel_ventry 0, error_compat, 32el0_error_compat

也就是说linux在用户态触发中断会执行el0_irq,在内核态触发中断会执行el1_irq,这两个的代码看下面:

SYM_CODE_START_LOCAL_NOALIGN(el0_irq)kernel_entry 0
el0_irq_naked:el0_interrupt_handler handle_arch_irqb	ret_to_user
SYM_CODE_END(el0_irq)SYM_CODE_START_LOCAL_NOALIGN(el1_irq)kernel_entry 1el1_interrupt_handler handle_arch_irqkernel_exit 1
SYM_CODE_END(el1_irq)

我么可以看到他们都是执行函数handle_arch_irq函数,最后返回各自的状态。handle_arch_irq在下面的gic控制器的初始化过程中会设置的。

3. GICv3中断控制器

GICv3(Generic Interrupt Controller Version 3)是一种基于ARM Cortex-A架构的中断控制器,它提供了高度灵活和可扩展的中断架构,适用于多核系统和虚拟化环境中,它还提供对TrustZone安全性扩展的支持。
GICv3 控制器接受系统级别中断的产生,并可以发信号通知给它所连接的每个内核,从而有可能导致IRQ或FIQ异常发生。GICv3 还支持虚拟中断,允许将虚拟机中的中断映射到真实机器上,并为每个虚拟处理器提供独立的中断处理能力。
GICv3 中,中断处理程序可以以不同的模式运行:Secure EL1、Non-secure EL1、Secure EL2 和 Non-secure EL2。这使得 GICv3 在处理不同的安全级别和特权级别时,有更强的灵活性和可定制性。
在GICv3中,中断被分为三种类型:软件生成中断(SGI)、私有中断(PPI)和共享中断(SPI)。下面对这三种中断类型进行简要的介绍:

  1. 软件生成中断(SGI):SGI是由处理器上的软件或操作系统生成的中断,用于在特定的CPU上触发中断。它们是非可屏蔽中断(NMI),具有固定的优先级和中断号。在ARMv8-A体系结构中,每个处理器都支持4个SGI中断线,其中SGI 0用于调试控制,其他SGI可以用于特定的事件处理。

  2. 私有中断(PPI):PPI是由处理器上的硬件设备或系统外设生成的中断。PPI是由每个处理器的本地中断控制器(LPI)处理并响应的,因此不会发送到网络中的其他处理器。PPI的中断号和优先级是固定的,但不保证在所有处理器上相同,因为它们由LPI分配。

  3. 共享中断(SPI):SPI是由GICv3的分发器(Distributor)处理分配给多个处理器的中断。SPI可以由设备连接到任意处理器中的任意一个或多个中断信号线,因此在多核系统中,分配给不同处理器的SPI可以在生成它的设备上具有不同的优先级和中断号。

GICv3包括了Distributor和Redistributor子系统。Distributor子系统接收来自外部设备的中断请求,进行中断的分类处理,再分配到不同的Redistributor进行处理。Redistributor子系统接收来自Distributor的中断请求,将其分配到不同的CPU处理器进行响应。GICv3 可以通过软件配置来实现中断控制器的定制化和协同工作。这些寄存器包括:

  1. Distributor registers:用于将中断向量路由到正确的 CPU 指令队列和 CPU 核中。也就是GICD_的系列寄存器。
  2. Redistributor registers:用于将来自 Distributor 的中断路由到对应的 CPU 核。也就是GICR_的系列寄存器。
  3. CPU interface registers:用于在 CPU 上启用、禁用、优先级排序和处理中断。也就是GICC_的系列寄存器。

当外部设备发出中断请求时,请求被分配给Distributor子系统,由该子系统根据优先级进行分类。分类后的中断请求发送给不同的Redistributor子系统,再由Redistributor子系统将中断请求发送给对应的CPU处理器进行处理。处理器通过在向量地址映射到对应的中断服务程序,来响应中断请求。

GICv3控制器会记录中断的状态,中断可以处于多种不同状态:
① 非活动状态(Inactive)–这意味着该中断未触发。
② 挂起(Pending)–这意味着中断源已被触发,但正在等待CPU核处理。待处理的中断要通过转发到CPU接口单元,然后再由CPU接口单元转发到内核。
③ 活动(Active)–描述了一个已被内核接收并正在处理的中断。
④ 活动和挂起(Active and pending)–描述了一种情况,其中CPU核正在为中断服务,而GIC又收到来自同一源的中断。

中断的优先级和可接收中断的核都在分发器(distributor)中配置。外设发给分发器的中断将标记为pending状态(或Active and Pending状态,如触发时果状态是active)。distributor确定可以传递给CPU核的优先级最高的pending中断,并将其转发给内核的CPU interface。通过CPU interface,该中断又向CPU核发出信号,此时CPU核将触发FIQ或IRQ异常。
作为响应,CPU核执行异常处理程序。异常处理程序必须从CPU interface寄存器查询中断ID,并开始为中断源提供服务。完成后,处理程序必须写入CPU interface寄存器以报告处理结束。然后CPU interface准备转发distributor发给它的下一个中断。在处理中断时,中断的状态开始为pending,active,结束时变成inactive。中断状态保存在distributor寄存器中。

3.1 GICv3中断控制器设备树

	gic: interrupt-controller@30800000 {compatible = "arm,gic-v3";#interrupt-cells = <3>;#address-cells = <2>;#size-cells = <2>;ranges;interrupt-controller;reg = <0x0 0x30800000 0 0x20000>,	/* GICD */<0x0 0x30880000 0 0x80000>,	/* GICR */<0x0 0x30840000 0 0x10000>,	/* GICC */<0x0 0x30850000 0 0x10000>,	/* GICH */<0x0 0x30860000 0 0x10000>;	/* GICV */interrupts = <GIC_PPI 9 IRQ_TYPE_LEVEL_LOW>;its: gic-its@30820000 {compatible = "arm,gic-v3-its";msi-controller;reg = <0x0 0x30820000 0x0 0x20000>;};};

这是飞腾的gic的设备树,跟瑞芯微的差不多,不过瑞芯微的的reg只有GICD和GICR,虽然飞腾的设备树多了这个多寄存器,实际上驱动也是没有去读取那些寄存器的。its的我们不用看,这是PCIE才用到的。

3.2 GICv3中断控制器驱动

驱动代码在drivers/irqchip/irq-gic-v3.c文件中,首先我们看看compatible 是怎么匹配的:

IRQCHIP_DECLARE(gic_v3, "arm,gic-v3", gic_of_init);

展开后为:

static const struct of_device_id __of_table_gic_v3		\__used __section(__irqchip_of_table)				\= { .compatible = "arm,gic-v3",					\.data = gic_of_init  }

那么gic_of_init 函数是怎么调用到的呢?其调用路径如下:
start_kernel (init\main.c) → init_IRQ (arch\arm64\kernel\irq.c) → irqchip_init (drivers\irqchip\irqchip.c) → of_irq_init(__irqchip_of_table);(drivers\of\irq.c) → desc->irq_init_cb = match->data;和 ret = desc->irq_init_cb(desc->dev,desc->interrupt_parent);
前面的调用路径都好理解,就是start_kernel一直调用到 of_irq_init这个函数,我们的宏申明了__irqchip_of_table这个段,所以在 of_irq_init这个函数里面会遍历整个设备树,找到所有compatible匹配的节点,并且判断是否为interrupt-controller,是否有data成员,如果都有,则把date成员赋值给desc->irq_init_cb,最后调用desc->irq_init_cb。这就会让"arm,gic-v3"的date成员gic_of_init函数调用起来,精简过的of_irq_init函数如下:

void __init of_irq_init(const struct of_device_id *matches)
{...for_each_matching_node_and_match(np, matches, &match) {if (!of_property_read_bool(np, "interrupt-controller") ||!of_device_is_available(np))continue;if (WARN(!match->data, "of_irq_init: no init function for %s\n",match->compatible))continue;...desc = kzalloc(sizeof(*desc), GFP_KERNEL);desc->irq_init_cb = match->data;...}while (!list_empty(&intc_desc_list)) {list_for_each_entry_safe(desc, temp_desc, &intc_desc_list, list) {ret = desc->irq_init_cb(desc->dev,desc->interrupt_parent);...}}
}

现在我们看看gic_of_init函数主要是获取设备树中的信息,然后初始化gic控制器:

static int __init gic_of_init(struct device_node *node, struct device_node *parent)
{void __iomem *dist_base;struct redist_region *rdist_regs;u64 redist_stride;u32 nr_redist_regions;int err, i;dist_base = of_iomap(node, 0);//获取GICD的地址,并且进行了映射if (!dist_base) {pr_err("%pOF: unable to map gic dist registers\n", node);return -ENXIO;}err = gic_validate_dist_version(dist_base);//验证gic版本if (err) {pr_err("%pOF: no distributor detected, giving up\n", node);goto out_unmap_dist;}if (of_property_read_u32(node, "#redistributor-regions", &nr_redist_regions))nr_redist_regions = 1;rdist_regs = kcalloc(nr_redist_regions, sizeof(*rdist_regs),GFP_KERNEL);if (!rdist_regs) {err = -ENOMEM;goto out_unmap_dist;}//redistributor-regions不存在,nr_redist_regions值为1,获取GICR的地址for (i = 0; i < nr_redist_regions; i++) {struct resource res;int ret;ret = of_address_to_resource(node, 1 + i, &res);rdist_regs[i].redist_base = of_iomap(node, 1 + i);if (ret || !rdist_regs[i].redist_base) {pr_err("%pOF: couldn't map region %d\n", node, i);err = -ENODEV;goto out_unmap_rdist;}rdist_regs[i].phys_base = res.start;}if (of_property_read_u64(node, "redistributor-stride", &redist_stride))redist_stride = 0;gic_enable_of_quirks(node, gic_quirks, &gic_data);//初始化GIC控制器,设置GICD等寄存器来配置NMI、SGI、PPI和SPI中断err = gic_init_bases(dist_base, rdist_regs, nr_redist_regions,redist_stride, &node->fwnode);if (err)goto out_unmap_rdist;gic_populate_ppi_partitions(node);if (static_branch_likely(&supports_deactivate_key))gic_of_setup_kvm_info(node);return 0;out_unmap_rdist:for (i = 0; i < nr_redist_regions; i++)if (rdist_regs[i].redist_base)iounmap(rdist_regs[i].redist_base);kfree(rdist_regs);
out_unmap_dist:iounmap(dist_base);return err;
}

gic_of_init主要做了一下几件事:

  1. 获取GICD的地址,并且进行了映射,映射地址存到dist_base
  2. 调用函数gic_validate_dist_version验证gic版本
  3. 获取GICR的地址存到rdist_regs数组中
  4. 调用函数gic_init_bases初始化GIC控制器,设置GICD等寄存器来配置NMI、SGI、PPI和SPI中断

我们再看看gic_init_bases函数:

static int __init gic_init_bases(void __iomem *dist_base,struct redist_region *rdist_regs,u32 nr_redist_regions,u64 redist_stride,struct fwnode_handle *handle)
{u32 typer;int err;if (!is_hyp_mode_available())static_branch_disable(&supports_deactivate_key);if (static_branch_likely(&supports_deactivate_key))pr_info("GIC: Using split EOI/Deactivate mode\n");//设置全局变量gic_datagic_data.fwnode = handle;gic_data.dist_base = dist_base;gic_data.redist_regions = rdist_regs;gic_data.nr_redist_regions = nr_redist_regions;gic_data.redist_stride = redist_stride;/** Find out how many interrupts are supported.*/typer = readl_relaxed(gic_data.dist_base + GICD_TYPER);gic_data.rdists.gicd_typer = typer;gic_enable_quirks(readl_relaxed(gic_data.dist_base + GICD_IIDR),gic_quirks, &gic_data);pr_info("%d SPIs implemented\n", GIC_LINE_NR - 32);pr_info("%d Extended SPIs implemented\n", GIC_ESPI_NR);/** ThunderX1 explodes on reading GICD_TYPER2, in violation of the* architecture spec (which says that reserved registers are RES0).*/if (!(gic_data.flags & FLAGS_WORKAROUND_CAVIUM_ERRATUM_38539))gic_data.rdists.gicd_typer2 = readl_relaxed(gic_data.dist_base + GICD_TYPER2);//创建并且初始化domain,最重要的是gic_irq_domain_ops方法gic_data.domain = irq_domain_create_tree(handle, &gic_irq_domain_ops,&gic_data);gic_data.rdists.rdist = alloc_percpu(typeof(*gic_data.rdists.rdist));gic_data.rdists.has_rvpeid = true;gic_data.rdists.has_vlpis = true;gic_data.rdists.has_direct_lpi = true;gic_data.rdists.has_vpend_valid_dirty = true;if (WARN_ON(!gic_data.domain) || WARN_ON(!gic_data.rdists.rdist)) {err = -ENOMEM;goto out_free;}irq_domain_update_bus_token(gic_data.domain, DOMAIN_BUS_WIRED);gic_data.has_rss = !!(typer & GICD_TYPER_RSS);pr_info("Distributor has %sRange Selector support\n",gic_data.has_rss ? "" : "no ");if (typer & GICD_TYPER_MBIS) {err = mbi_init(handle, gic_data.domain);if (err)pr_err("Failed to initialize MBIs\n");}//设置handle_arch_irq为gic_handle_irq,cpu触发irq就会跑到gic_handle_irqset_handle_irq(gic_handle_irq);gic_update_rdist_properties();gic_dist_init();//初始化GICD分发器,配置控制器的中断路由gic_cpu_init();//初始化GICR,配置PPI,gic_smp_init();//初始化SGI中断gic_cpu_pm_init();//初始化CPU的GIC控制器用于中断的电源管理特性if (gic_dist_supports_lpis()) {its_init(handle, &gic_data.rdists, gic_data.domain);its_cpu_init();} else {if (IS_ENABLED(CONFIG_ARM_GIC_V2M))gicv2m_init(handle, gic_data.domain);}gic_enable_nmi_support();//初始化NMI中断return 0;out_free:if (gic_data.domain)irq_domain_remove(gic_data.domain);free_percpu(gic_data.rdists.rdist);return err;
}

gic_init_bases主要做了一下几件事:

  1. 设置全局变量gic_data,
  2. 创建并且初始化domain,
  3. 调用函数set_handle_irq设置handle_arch_irq为gic_handle_irq,handle_arch_irq就是上面异常向量表的未知函数
  4. 调用函数gic_dist_ini初始化GICD分发器,配置控制器的中断路由
  5. 调用函数gic_cpu_ini初始化GICR,配置PPI,
  6. 调用函数gic_smp_ini初始化SGI中断
  7. 调用函数 gic_cpu_pm_init初始化CPU的GIC控制器用于中断的电源管理特性
  8. 调用函数 gic_enable_nmi_suppor初始化NMI中断

到这里,这个gic控制器的初始化流程就讲完了。

4. GIC的下一级中断控制器

4.1 设备树

		gpio0: gpio0@fdd60000 {compatible = "rockchip,gpio-bank";reg = <0x0 0xfdd60000 0x0 0x100>;interrupts = <GIC_SPI 33 IRQ_TYPE_LEVEL_HIGH>;clocks = <&pmucru PCLK_GPIO0>, <&pmucru DBCLK_GPIO0>;gpio-controller;#gpio-cells = <2>;interrupt-controller;#interrupt-cells = <2>;};

这里是瑞芯微3568的gpio0模块,瑞芯微每个gpio有32个引脚。

4.2 内核对设备树的处理

linux内核的设备树处理函数入口是drivers/of/platform.c文件的of_device_alloc函数,这个函数会在平台设备遍历设备树注册成平台设备的过程中被调用:

struct platform_device *of_device_alloc(struct device_node *np,const char *bus_id,struct device *parent)
{struct platform_device *dev;int rc, i, num_reg = 0, num_irq;struct resource *res, temp_res;//分配平台设备dev = platform_device_alloc("", PLATFORM_DEVID_NONE);if (!dev)return NULL;/* count the io and irq resources */while (of_address_to_resource(np, num_reg, &temp_res) == 0)num_reg++;num_irq = of_irq_count(np);//统计节点使用irq的次数/* Populate the resource table */if (num_irq || num_reg) {res = kcalloc(num_irq + num_reg, sizeof(*res), GFP_KERNEL);if (!res) {platform_device_put(dev);return NULL;}dev->num_resources = num_reg + num_irq;dev->resource = res;for (i = 0; i < num_reg; i++, res++) {//把设备树地址转换为资源rc = of_address_to_resource(np, i, res);WARN_ON(rc);}//根据设备节点中的中断信息, 构造中断资源if (of_irq_to_resource_table(np, res, num_irq) != num_irq)pr_debug("not all legacy IRQ resources mapped for %pOFn\n",np);}dev->dev.of_node = of_node_get(np);//把node节点保存到deb中dev->dev.fwnode = &np->fwnode;dev->dev.parent = parent ? : &platform_bus;//保存父节点if (bus_id)dev_set_name(&dev->dev, "%s", bus_id);elseof_device_make_bus_id(&dev->dev);return dev;
}

of_device_alloc主要做了以下一些工作:

  1. 调用函数platform_device_alloc分配平台设备
  2. 调用函数of_irq_count统计节点使用irq的次数
  3. 遍历reg资源,调用of_address_to_resource函数把设备树地址转换为资源
  4. 调用函数of_irq_to_resource_table根据设备节点中的中断信息, 构造中断资源
  5. 设置平台设备的父节点、节点和名字等信息

我们主要关注of_irq_to_resource_table函数:

int of_irq_to_resource_table(struct device_node *dev, struct resource *res,int nr_irqs)
{int i;for (i = 0; i < nr_irqs; i++, res++)if (of_irq_to_resource(dev, i, res) <= 0)break;return i;
}

of_irq_to_resource_table函数主要是遍历全部irq,然后调用of_irq_to_resource解析节点中的中断信息,和构造中断资源。我们看of_irq_to_resource这个函数:

int of_irq_to_resource(struct device_node *dev, int index, struct resource *r)
{int irq = of_irq_get(dev, index);if (irq < 0)return irq;/* Only dereference the resource if both the* resource and the irq are valid. */if (r && irq) {const char *name = NULL;memset(r, 0, sizeof(*r));/** Get optional "interrupt-names" property to add a name* to the resource.*/of_property_read_string_index(dev, "interrupt-names", index,&name);r->start = r->end = irq;r->flags = IORESOURCE_IRQ | irqd_get_trigger_type(irq_get_irq_data(irq));r->name = name ? name : of_node_full_name(dev);}return irq;
}

of_irq_to_resource主要是调用of_irq_get函数找到对应节点的中断号,同时找到其父节点,创建映射关系,最后保存为resource。我们看看of_irq_get:

int of_irq_get(struct device_node *dev, int index)
{int rc;struct of_phandle_args oirq;struct irq_domain *domain;//解析设备树中的中断信息, 保存在of_phandle_args结构体中rc = of_irq_parse_one(dev, index, &oirq);if (rc)return rc;domain = irq_find_host(oirq.np);if (!domain)return -EPROBE_DEFER;return irq_create_of_mapping(&oirq);//创建中断映射
}

of_irq_get主要做了两件事:

  1. 调用函数of_irq_parse_one解析设备树中的中断信息, 保存在of_phandle_args结构体中
  2. 调用函数irq_create_of_mapping创建中断映射

我们继续看irq_create_of_mapping:

unsigned int irq_create_of_mapping(struct of_phandle_args *irq_data)
{struct irq_fwspec fwspec;//根据irq_data的信息填充irq_fwspec结构体of_phandle_args_to_fwspec(irq_data->np, irq_data->args,irq_data->args_count, &fwspec);//创建从fwspec到IRQ号的映射关系return irq_create_fwspec_mapping(&fwspec);
}

irq_create_of_mapping函数首先根据irq_data的信息填充irq_fwspec结构体,然后调用函数irq_create_fwspec_mapping创建从fwspec到IRQ号的映射关系。irq_create_fwspec_mapping函数:

unsigned int irq_create_fwspec_mapping(struct irq_fwspec *fwspec)
{struct irq_domain *domain;struct irq_data *irq_data;irq_hw_number_t hwirq;unsigned int type = IRQ_TYPE_NONE;int virq;//根据fwspec找到对应的domianif (fwspec->fwnode) {domain = irq_find_matching_fwspec(fwspec, DOMAIN_BUS_WIRED);if (!domain)domain = irq_find_matching_fwspec(fwspec, DOMAIN_BUS_ANY);} else {domain = irq_default_domain;}if (!domain) {pr_warn("no irq domain found for %s !\n",of_node_full_name(to_of_node(fwspec->fwnode)));return 0;}//调用irq_domain->ops的translate或xlate,把设备节点里的中断信息解析为hwirq, typeif (irq_domain_translate(domain, fwspec, &hwirq, &type))return 0;/** WARN if the irqchip returns a type with bits* outside the sense mask set and clear these bits.*/if (WARN_ON(type & ~IRQ_TYPE_SENSE_MASK))type &= IRQ_TYPE_SENSE_MASK;//看看这个hwirq是否已经映射, 如果virq非0,说明找到了就直接返回virq = irq_find_mapping(domain, hwirq);if (virq) {/** If the trigger type is not specified or matches the* current trigger type then we are done so return the* interrupt number.*/if (type == IRQ_TYPE_NONE || type == irq_get_trigger_type(virq))return virq;/** If the trigger type has not been set yet, then set* it now and return the interrupt number.*/if (irq_get_trigger_type(virq) == IRQ_TYPE_NONE) {irq_data = irq_get_irq_data(virq);if (!irq_data)return 0;irqd_set_trigger_type(irq_data, type);return virq;}pr_warn("type mismatch, failed to map hwirq-%lu for %s!\n",hwirq, of_node_full_name(to_of_node(fwspec->fwnode)));return 0;}//来到这里说明没有找到,需要创建映射if (irq_domain_is_hierarchy(domain)) {//如果这个是级联中断//根据domian分配虚拟中断号virq = irq_domain_alloc_irqs(domain, 1, NUMA_NO_NODE, fwspec);if (virq <= 0)return 0;} else {//否则就是链式中断//创建硬中断号和虚拟中断号的映射关系virq = irq_create_mapping(domain, hwirq);if (!virq)return virq;}//通过虚拟中断号查找irq_data,找不到就返回irq_data = irq_get_irq_data(virq);if (!irq_data) {if (irq_domain_is_hierarchy(domain))irq_domain_free_irqs(virq, 1);elseirq_dispose_mapping(virq);return 0;}//保存中断触发类型irqd_set_trigger_type(irq_data, type);return virq;
}

irq_create_fwspec_mapping函数主要做了一下几件事:

  1. 根据fwspec找到对应的domian
  2. 调用函数irq_domain_translate把设备节点里的中断信息解析出hwirq, type
  3. 调用函数irq_find_mapping查看这个hwirq是否已经映射,如果已经映射到某个虚拟中断号,则返回;否则往下走
  4. 调用函数irq_domain_is_hierarchy判断是否为级联中断,如果是则调用函数irq_domain_alloc_irqs根据domian分配虚拟中断号,
  5. 如果不是级联中断,说明是链式中断,则调用函数irq_create_mapping创建硬中断号和虚拟中断号的映射关系
  6. 调用函数irq_get_irq_data通过软件中断号查找irq_data,找不到就返回
  7. 调用函数irqd_set_trigger_type保存中断触发类型,返回虚拟中断号

下面我们分小结讲解irq_domain_translate、irq_domain_alloc_irqs和irq_create_mapping这几个函数

4.2.1 irq_domain_translate

static int irq_domain_translate(struct irq_domain *d,struct irq_fwspec *fwspec,irq_hw_number_t *hwirq, unsigned int *type)
{
#ifdef CONFIG_IRQ_DOMAIN_HIERARCHYif (d->ops->translate)//如果translate方法集存在,就调用它return d->ops->translate(d, fwspec, hwirq, type);
#endifif (d->ops->xlate)//如果xlate方法集存在,就调用它return d->ops->xlate(d, to_of_node(fwspec->fwnode),fwspec->param, fwspec->param_count,hwirq, type);//如果转换方法都不存在,则假定中断号*hwirq = fwspec->param[0];return 0;
}

irq_domain_translate函数做了一下几件事:

  1. /如果translate方法集存在,就调用translate方法后返回
  2. 如果xlate方法集存在,就调用xlate方法后返回
  3. 如果转换方法都不存在,则假定中断号

translate和xlate都是中断控制器驱动注册的时候写好的,后面会详细说。

4.2.2 irq_domain_alloc_irqs

static inline int irq_domain_alloc_irqs(struct irq_domain *domain,unsigned int nr_irqs, int node, void *arg)
{return __irq_domain_alloc_irqs(domain, -1, nr_irqs, node, arg, false,NULL);
}int __irq_domain_alloc_irqs(struct irq_domain *domain, int irq_base,unsigned int nr_irqs, int node, void *arg,bool realloc, const struct irq_affinity_desc *affinity)
{int i, ret, virq;if (domain == NULL) {domain = irq_default_domain;if (WARN(!domain, "domain is NULL; cannot allocate IRQ\n"))return -EINVAL;}if (realloc && irq_base >= 0) {virq = irq_base;} else {//分配和初始化irq_des内存virq = irq_domain_alloc_descs(irq_base, nr_irqs, 0, node,affinity);if (virq < 0) {pr_debug("cannot allocate IRQ(base %d, count %d)\n",irq_base, nr_irqs);return virq;}}//最外层的irq_data被嵌入到结构体irq_desc中if (irq_domain_alloc_irq_data(domain, virq, nr_irqs)) {pr_debug("cannot allocate memory for IRQ%d\n", virq);ret = -ENOMEM;goto out_free_desc;}mutex_lock(&irq_domain_mutex);//调用domain->ops->alloc申请虚拟中断号ret = irq_domain_alloc_irqs_hierarchy(domain, virq, nr_irqs, arg);if (ret < 0) {mutex_unlock(&irq_domain_mutex);goto out_free_irq_data;}for (i = 0; i < nr_irqs; i++) {//在IRQ域中修剪层次结构,并更新IRQ域的继承关系ret = irq_domain_trim_hierarchy(virq + i);if (ret) {mutex_unlock(&irq_domain_mutex);goto out_free_irq_data;}}for (i = 0; i < nr_irqs; i++)//将新的中断描述符加入到IRQ域中,以及将中断描述符与对应的中断控制器进行关联irq_domain_insert_irq(virq + i);mutex_unlock(&irq_domain_mutex);return virq;out_free_irq_data:irq_domain_free_irq_data(virq, nr_irqs);
out_free_desc:irq_free_descs(virq, nr_irqs);return ret;
}

irq_domain_alloc_irqs直接调用函数__irq_domain_alloc_irqs,__irq_domain_alloc_irqs函数做了以下几件事:

  1. 调用函数irq_domain_alloc_descs分配和初始化irq_des内存
  2. 调用函数irq_domain_alloc_irq_data把最外层的irq_data被嵌入到结构体irq_desc中
  3. 调用函数irq_domain_alloc_irqs_hierarchy调用domain->ops->alloc申请虚拟中断号
  4. 遍历每一个虚拟中断号,调用函数irq_domain_trim_hierarchy在IRQ域中修剪层次结构,并更新IRQ域的继承关系
  5. 遍历每一个虚拟中断号,调用函数irq_domain_insert_irq将新的中断描述符加入到IRQ域中,以及将中断描述符与对应的中断控制器进行关联

4.2.3 irq_create_mapping

static inline unsigned int irq_create_mapping(struct irq_domain *host,irq_hw_number_t hwirq)
{//创建映射return irq_create_mapping_affinity(host, hwirq, NULL);
}unsigned int irq_create_mapping_affinity(struct irq_domain *domain,irq_hw_number_t hwirq,const struct irq_affinity_desc *affinity)
{struct device_node *of_node;int virq;pr_debug("irq_create_mapping(0x%p, 0x%lx)\n", domain, hwirq);/* Look for default domain if nececssary */if (domain == NULL)domain = irq_default_domain;if (domain == NULL) {WARN(1, "%s(, %lx) called with NULL domain\n", __func__, hwirq);return 0;}pr_debug("-> using domain @%p\n", domain);of_node = irq_domain_get_of_node(domain);//通过硬中断号找软中断号virq = irq_find_mapping(domain, hwirq);if (virq) {pr_debug("-> existing mapping on virq %d\n", virq);return virq;}//申请软件中断号virq = irq_domain_alloc_descs(-1, 1, hwirq, of_node_to_nid(of_node),affinity);if (virq <= 0) {pr_debug("-> virq allocation failed\n");return 0;}//建立软中断号和硬中断号的映射关系if (irq_domain_associate(domain, virq, hwirq)) {irq_free_desc(virq);return 0;}pr_debug("irq %lu on domain %s mapped to virtual irq %u\n",hwirq, of_node_full_name(of_node), virq);return virq;
}

irq_create_mapping函数直接调用irq_create_mapping_affinity,irq_create_mapping_affinity做了以下几件事:

  1. 调用函数irq_find_mapping通过硬中断号找软中断号,找到了就返回
  2. 调用函数irq_domain_alloc_descs申请软件中断号
  3. 调用函数irq_domain_associate建立软中断号和硬中断号的映射关系

irq_domain_associate主要是通过线性映射或radix tree映射,以硬中断号为索引,建立硬中断号与软中断号的映射关系:

int irq_domain_associate(struct irq_domain *domain, unsigned int virq,irq_hw_number_t hwirq)
{struct irq_data *irq_data = irq_get_irq_data(virq);int ret;if (WARN(hwirq >= domain->hwirq_max,"error: hwirq 0x%x is too large for %s\n", (int)hwirq, domain->name))return -EINVAL;if (WARN(!irq_data, "error: virq%i is not allocated", virq))return -EINVAL;if (WARN(irq_data->domain, "error: virq%i is already associated", virq))return -EINVAL;mutex_lock(&irq_domain_mutex);//初始化硬中断号,软中断号在alloc_desc->desc_set_defaults中初始化irq_data->hwirq = hwirq;irq_data->domain = domain;//调用map方法进行映射if (domain->ops->map) {ret = domain->ops->map(domain, virq, hwirq);if (ret != 0) {/** If map() returns -EPERM, this interrupt is protected* by the firmware or some other service and shall not* be mapped. Don't bother telling the user about it.*/if (ret != -EPERM) {pr_info("%s didn't like hwirq-0x%lx to VIRQ%i mapping (rc=%d)\n",domain->name, hwirq, virq, ret);}irq_data->domain = NULL;irq_data->hwirq = 0;mutex_unlock(&irq_domain_mutex);return ret;}/* If not already assigned, give the domain the chip's name */if (!domain->name && irq_data->chip)domain->name = irq_data->chip->name;}domain->mapcount++;//建立了软硬中断号之间的映射关系irq_domain_set_mapping(domain, hwirq, irq_data);mutex_unlock(&irq_domain_mutex);irq_clear_status_flags(virq, IRQ_NOREQUEST);return 0;
}

irq_domain_associate做了以下几件事:

  1. 初始化硬中断号和domian
  2. 调用调用map方法进行映射
  3. 调用函数irq_domain_set_mapping建立了软硬中断号之间的映射关系

4.3 链式中断驱动

链式中断驱动的初始化代码都在rockchip_interrupts_register中,其调用路径为rockchip_gpio_probe→rockchip_gpiolib_register→rockchip_interrupts_register。

static int rockchip_interrupts_register(struct rockchip_pin_bank *bank)
{unsigned int clr = IRQ_NOREQUEST | IRQ_NOPROBE | IRQ_NOAUTOEN;struct irq_chip_generic *gc;int ret;bank->domain = irq_domain_create_linear(dev_fwnode(bank->dev), 32,&irq_generic_chip_ops, NULL);if (!bank->domain) {dev_warn(bank->dev, "could not init irq domain for bank %s\n",bank->name);return -EINVAL;}ret = irq_alloc_domain_generic_chips(bank->domain, 32, 1,"rockchip_gpio_irq",handle_level_irq,clr, 0, 0);if (ret) {dev_err(bank->dev, "could not alloc generic chips for bank %s\n",bank->name);irq_domain_remove(bank->domain);return -EINVAL;}gc = irq_get_domain_generic_chip(bank->domain, 0);if (bank->gpio_type == GPIO_TYPE_V2) {gc->reg_writel = gpio_writel_v2;gc->reg_readl = gpio_readl_v2;}gc->reg_base = bank->reg_base;gc->private = bank;gc->chip_types[0].regs.mask = bank->gpio_regs->int_mask;gc->chip_types[0].regs.ack = bank->gpio_regs->port_eoi;gc->chip_types[0].chip.irq_ack = irq_gc_ack_set_bit;gc->chip_types[0].chip.irq_mask = irq_gc_mask_set_bit;gc->chip_types[0].chip.irq_unmask = irq_gc_mask_clr_bit;gc->chip_types[0].chip.irq_enable = rockchip_irq_enable;gc->chip_types[0].chip.irq_disable = rockchip_irq_disable;gc->chip_types[0].chip.irq_set_wake = irq_gc_set_wake;gc->chip_types[0].chip.irq_suspend = rockchip_irq_suspend;gc->chip_types[0].chip.irq_resume = rockchip_irq_resume;gc->chip_types[0].chip.irq_set_type = rockchip_irq_set_type;gc->wake_enabled = IRQ_MSK(bank->nr_pins);/** Linux assumes that all interrupts start out disabled/masked.* Our driver only uses the concept of masked and always keeps* things enabled, so for us that's all masked and all enabled.*/rockchip_gpio_writel(bank, 0xffffffff, bank->gpio_regs->int_mask);rockchip_gpio_writel(bank, 0xffffffff, bank->gpio_regs->port_eoi);rockchip_gpio_writel(bank, 0xffffffff, bank->gpio_regs->int_en);gc->mask_cache = 0xffffffff;irq_set_chained_handler_and_data(bank->irq,rockchip_irq_demux, bank);return 0;
}

rockchip_interrupts_register函数主要做了以下几件事:

  1. 调用函数irq_domain_create_linear分配和初始化irq_domain数据
  2. 调用函数irq_alloc_domain_generic_chips分配和初始化irq_chip_generic数据
  3. 调用函数irq_get_domain_generic_chip获取irq_chip_generic指针,
  4. 填充irq_chip_generic->chip_types->chip的方法集
  5. 调用函数irq_set_chained_handler_and_data设置irq_desc[].handle_irq为rockchip_irq_demux,rockchip_irq_demux的功能是分辨是哪一个hwirq, 调用对应的irq_desc[].handle_irq。

gpio中断控制器的初始化就这么简单,这是因为很多工作在设备树被注册为平台设备的时候已经调用GIC的方法完成了初始化工作。当然,我们还要重点关注irq_generic_chip_ops结构体、irq_chip里面的方法和rockchip_irq_demux这个中断处理函数。这个东西有点长,这里不多介绍。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/67156.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

大数据学习:Hive常用函数

Hive常用函数 1. Hive的参数传递 1.1 Hive命令行 查看hive命令的参数 [hadoopnode03 ~]$ hive -help语法结构: hive [-hiveconf xy]* [<-i filename>]* [<-f filename>|<-e query-string>][-S] 说明&#xff1a; -i 从文件初始化HQL。-e从命令行执行指定…

线性代数的学习和整理16:什么是各种空间(类型),向量空间,距离(类型)?

目录 1 空间相关的群&#xff0c;环&#xff0c;域&#xff0c;集合&#xff0c;空间的预备知识 1.1&#xff1a;群&#xff0c;环&#xff0c;域&#xff0c;集合&#xff0c;空间的定义&#xff08;表示不懂&#xff0c;只是做个标记&#xff09; 2 空间 2.1 各种空间概念…

WebRTC-Streamer交叉编译

WebRTC-Streamer交叉编译 flyfish 文章目录 WebRTC-Streamer交叉编译零、前言一、提前准备工作1 安装需要的工具2 可选的交叉编译工具3 默认执行python是python34 获取源码5 使用其他版本的方法 二、非交叉编译编译1 在 src目录执行 安装所需的依赖2 执行命令 三、 交叉编译1 …

【Linux】redhat7.8配置yum在线源【redhat7.8镜像容器内配置yum在线源】通用

&#x1f468;‍&#x1f393;博主简介 &#x1f3c5;云计算领域优质创作者   &#x1f3c5;华为云开发者社区专家博主   &#x1f3c5;阿里云开发者社区专家博主 &#x1f48a;交流社区&#xff1a;运维交流社区 欢迎大家的加入&#xff01; &#x1f40b; 希望大家多多支…

Navicat 强大的数据模型功能 | 面向数据库设计、架构和数据资产梳理等使用场景

数据模型是用来描述数据、组织数据和对数据进行操作的一组概念和定义。根据不同的应用需求&#xff0c;数据模型可以分为概念模型、逻辑模型和物理模型。这些数据模型帮助数据库设计人员设计和管理数据库&#xff0c;以满足用户的需求。 Navicat 强大的数据模型功能主要适用于…

软件定义网络:重新定义云计算网络架构

文章目录 软件定义网络的基本概念软件定义网络的工作原理软件定义网络在云计算中的应用与优势示例&#xff1a;软件定义网络配置未来发展和挑战结论 &#x1f389;欢迎来到AIGC人工智能专栏~软件定义网络&#xff1a;重新定义云计算网络架构 ☆* o(≧▽≦)o *☆嗨~我是IT陈寒&a…

贷款公司如何精准获客,大数据获客

近年来&#xff0c;贷款中介机构在金融服务领域发挥着越来越重要的作用。随着时代的发展&#xff0c;贷款中介机构不仅是贷款服务的提供者&#xff0c;也是能够帮助客户更准确获取客户的服务提供者。 为此&#xff0c;贷款中介机构应把握以下几个方面。 首先&#xff0c;贷款…

基于YOLOV8模型和CCPD数据集的车牌目标检测系统(PyTorch+Pyside6+YOLOv8模型)

摘要&#xff1a;基于YOLOV8模型和CCPD数据集的车牌目标检测系统可用于日常生活中检测与定位车牌目标&#xff0c;利用深度学习算法可实现图片、视频、摄像头等方式的目标检测&#xff0c;另外本系统还支持图片、视频等格式的结果可视化与结果导出。本系统采用YOLOv8目标检测算…

【Unity编辑器扩展】 | 编辑器扩展入门基础

前言 【Unity编辑器扩展】 | 编辑器扩展入门基础一、基本概念二、核心知识点 简述三、相关API 总结 前言 当谈到游戏开发工具&#xff0c;Unity编辑器是一个备受赞誉的平台。它为开发者提供了一个强大且灵活的环境&#xff0c;使他们能够创建令人惊叹的游戏和交互式体验。然而…

Java“牵手”1688商品列表数据,关键词搜索1688商品数据接口,1688API申请指南

1688商城是一个网上购物平台&#xff0c;售卖各类商品&#xff0c;包括服装、鞋类、家居用品、美妆产品、电子产品等。要获取1688商品列表和商品详情页面数据&#xff0c;您可以通过开放平台的接口或者直接访问1688商城的网页来获取商品详情信息。以下是两种常用方法的介绍&…

成都瀚网科技有限公司:抖店的评论会消失吗?

抖店是抖音推出的电子商务平台。很多用户在购物后都会对产品进行评价。但有时用户可能会发现抖店评论缺失&#xff0c;让用户产生一些疑惑和困惑。本文将围绕这个问题提供一些答案和解决方案。 1.为什么抖店评论不见了&#xff1f; 首先需要明确的是&#xff0c;抖店评论消失可…

大数据Flink(七十):SQL 动态表 连续查询

文章目录 SQL 动态表 & 连续查询 一、​​​​​​​SQL 应用于流处理的思路

百度等8家企业首批上线大模型服务;大语言模型微调之道

&#x1f989; AI新闻 &#x1f680; 百度等8家企业首批上线大模型服务 摘要&#xff1a;百度、字节、中科院旗下8家企业/机构的大模型通过备案&#xff0c;正式面向公众提供服务。百度旗下AI大模型产品文心一言率先开放&#xff0c;用户可下载App或登录官网体验。百川智能也…

Blender里复制对象动画

假设在Blender里有2个对象&#xff0c;其中一个添加了动画&#xff0c;另外一个没有添加动画&#xff0c;那么如何把已有的动画拷贝到没有动画的对象上呢&#xff1f; 分为2步&#xff1a; 先选中没有动画的对象&#xff0c;再按shift键选中有动画的对象&#xff0c;此时2个对…

django/CVE-2017-12794XSS漏洞复现

docker搭建漏洞复现环境 漏洞原理看帮助文档 # Django debug page XSS漏洞&#xff08;CVE-2017-12794&#xff09;分析Django发布了新版本1.11.5&#xff0c;修复了500页面中可能存在的一个XSS漏洞&#xff0c;这篇文章说明一下该漏洞的原理和复现&#xff0c;和我的一点点评…

企业电子招投标采购系统源码之电子招投标的组成

​ 功能模块&#xff1a; 待办消息&#xff0c;招标公告&#xff0c;中标公告&#xff0c;信息发布 描述&#xff1a; 全过程数字化采购管理&#xff0c;打造从供应商管理到采购招投标、采购合同、采购执行的全过程数字化管理。通供应商门户具备内外协同的能力&#xff0c;为外…

vscode html使用less和快速获取标签less结构

扩展插件里面搜索 css tree 插件 下载 使用方法 选择你要生成的标签结构然后按CTRLshiftp 第一次需要在输入框输入 get 然后选择 Generate CSS tree less结构就出现在这个里面直接复制到自己的less文件里面就可以使用了 在html里面使用less 下载 Easy LESS 插件 自己创建…

手写一个简单爬虫--手刃豆瓣top250排行榜

#拿到页面面源代码 request #通过re来提取想要的有效信息 re import requests import re url"https://movie.douban.com/top250"headers{"user-agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/11…

WPF C# .NET7 基础学习

学习视频地址&#xff1a;https://www.bilibili.com/video/BV1hx4y1G7C6?p3&vd_source986db470823ebc16fe0b3d235addf050 开发工具&#xff1a;Visual Studio 2022 Community 基础框架&#xff1a;.Net 6.0 下载创建过程略 .Net和.Framework 区别是Net是依赖项&#xff…

docker 笔记6:高级篇 DockerFile解析

目录 1.是什么&#xff1f; 2.构建三步骤 3.DockerFile构建过程解析 3.1 Dockerfile内容基础知识 3.2Docker执行Dockerfile的大致流程 总结 4.DockerFile常用保留字指令 5.案例&#xff1a;自定义镜像 5.1 要求&#xff1a; Centos7镜像具备vimifconfigjdk8 5.2编写 5…