3.4_第二行之_ipipe_init_early()
3.4 第二行之_ipipe_init_early()
3.4.1 IPIPE数据结构
3.4.1.1 IPIPE基础数据结构
先上图,按图索骥,一图抵千言。
前面章节多次提到了Head域(头域)和Root域(根域),它们在IPIPE中抽象成数据结构struct ipipe_domain。一切从根开始,先说Root域(根域)。
Root域的实例是一个全局变量ipipe_root,它里面的成员ipipe_root.name后续会初始化为“Linux”。为方便访问ipipe_root,为它定义了一个全局指针ipipe_root_domain,后面分析时基本都使用ipipe_root_domain代指Root域。
kernel/ipipe/core.c:
struct ipipe_domain ipipe_root;
EXPORT_SYMBOL_GPL(ipipe_root);include/linux/ipipe_domain.h:
#define ipipe_root_domain (&ipipe_root)
Head域的实例化分为两个阶段。第一阶段是IPIPE初始化阶段,定义一个全局指针*ipipe_head_domain指向Root域ipipe_root。第二阶段是Xenomai初始化阶段,调用ipipe_register_head向IPIPE注册名字为“Xenomai”的Head域:xnsched_primary_domain。后面分析时基本都使用ipipe_head_domain代指Head域。
//第一阶段,全局指针指向ipipe_root
kernel/ipipe/core.c:struct ipipe_domain *ipipe_head_domain = &ipipe_root;
EXPORT_SYMBOL_GPL(ipipe_head_domain);//第二阶段,Xenomai注册到Head域
kernel/xenomai/pipeline/init.c:
pipeline_init()->ipipe_register_head(&xnsched_primary_domain, "Xenomai");
IPIPE是支持SMP对称多核CPU的,所以还得为每一个CPU core定义per cpu变量ipipe_percpu,它的数据结构是struct ipipe_percpu_data。这个数据结构里面的成员很多,当前先关注其中的三个变量:root、head、curr!
include/linux/ipipe_domain.hstruct ipipe_percpu_domain_data {unsigned long status; /* <= Must be first in struct. */unsigned long irqpend_0map;
#if __IPIPE_IRQMAP_LEVELS >= 3unsigned long irqpend_1map[IPIPE_IRQ_1MAPSZ];
#if __IPIPE_IRQMAP_LEVELS >= 4unsigned long irqpend_2map[IPIPE_IRQ_2MAPSZ];
#endif
#endifunsigned long irqpend_map[IPIPE_IRQ_MAPSZ];unsigned long irqheld_map[IPIPE_IRQ_MAPSZ];unsigned long irqall[IPIPE_NR_IRQS];struct ipipe_domain *domain;int coflags;
};struct ipipe_percpu_data {struct ipipe_percpu_domain_data root;struct ipipe_percpu_domain_data head;struct ipipe_percpu_domain_data *curr;struct pt_regs tick_regs;int hrtimer_irq;struct task_struct *task_hijacked;struct task_struct *rqlock_owner;struct ipipe_vm_notifier *vm_notifier;unsigned long nmi_state;struct mm_struct *active_mm;
#ifdef CONFIG_IPIPE_DEBUG_CONTEXTint context_check;int context_check_saved;
#endif
};kernel/ipipe/core.c:DEFINE_PER_CPU(struct ipipe_percpu_data, ipipe_percpu) = {.root = {.status = IPIPE_STALL_MASK,.domain = &ipipe_root,},.curr = &bootup_context,.hrtimer_irq = -1,
#ifdef CONFIG_IPIPE_DEBUG_CONTEXT.context_check = 1,
#endif
};
EXPORT_PER_CPU_SYMBOL(ipipe_percpu);
如何理解root和head两个成员?
为了记录Root域ipipe_root_domain和Head域ipipe_head_domain在每个CPU core上留下的身影(即上下文context),struct ipipe_percpu_data为它们各自量身打造了变量root和head,数据结构是struct ipipe_percpu_domain_data。域的运行上下文context有哪些?每个CPU core都要有各自的虚拟中断标志位,就是struct ipipe_percpu_domain_data的成员status。在代码中,ipipe_percpu.root.status被初始化为IPIPE_STALL_MASK。为了方便的找到Root域和Head域,ipipe_percpu.root.domain和ipipe_percpu .head.domain被初始化为ipipe_root_domain和ipipe_head_domain!
如果理解curr成员?
每个CPU core当前处于root和head? struct ipipe_percpu_data又定义了一个curr(current的缩写)指针来指向root或head。
基础数据结构介绍完了,接下来介绍一下各个数据结构相互访问的API。
从struct ipipe_percpu_data到struct ipipe_percpu_domain_data,再到struct ipipe_domain,是逐层包含的关系,利用指针可以非常方便地正向地找到每个想要的数据结构或变量,具体如下:
include/linux/ipipe_domain.h:
//获取 ipipe_percpu.root
static inline struct ipipe_percpu_domain_data * ipipe_this_cpu_root_context(void)//获取 ipipe_percpu.head
static inline struct ipipe_percpu_domain_data * ipipe_this_cpu_head_context(void)//获取 ipipe_percpu.curr
static inline struct ipipe_percpu_domain_data * __ipipe_get_current_context(void)
#define __ipipe_current_context __ipipe_get_current_context()
static inline struct ipipe_percpu_domain_data *ipipe_current_context(void)//获取 ipipe_percpu.curr.domain
static inline struct ipipe_domain *__ipipe_get_current_domain(void)
#define __ipipe_current_domain __ipipe_get_current_domain()
static inline struct ipipe_domain *ipipe_get_current_domain(void)
反向思考,假设当前硬塞(传递)过来一个大冤种ipipe_domain指针,它自己都不知道它指向root域还是head域,如何反向找到准确的找到ipipe_percpu.root或是ipipe_percpu.head?
最简单粗暴的办法,就是判断这个大冤种ipipe_domain的身份,然后正向查询。
如果 ipipe_domain 和 ipipe_root_domain相等ipipe_this_cpu_root_context()
否则ipipe_this_cpu_head_context()
IPIPE Patch里面给了一个巧妙的办法。在root域和head域初始化过程中,为其成员context_offset进行赋值:
//在root域初始化过程中赋值
kernel/ipipe/core.c: __ipipe_init_early ()
ipipe_root_domain->context_offset = offsetof(struct ipipe_percpu_data, root)//在head域初始化过程中赋值
kernel/ipipe/core.c: init_head_stage()
ipipe_head_domain->context_offset = offsetof(struct ipipe_percpu_data, head);//通过context_offset直接拿到struct ipipe_percpu_domain_data结构指针
static inline struct ipipe_percpu_domain_data *
__context_of(struct ipipe_percpu_data *p, struct ipipe_domain *ipd)
{return (void *)p + ipd->context_offset;
}
IPIPE的基本数据结构介绍告一段落,接下来重点介绍为中断新增的数据结构。
3.4.1.2 IPIPE对Linux中断号的改造
在IPIPE domain中,IPIPE_NR_IRQS代表中断总数量,在代码中经常用到,最具代表的就是下图中定义struct ipipe_irqdesc irqs[IPIPE_NR_IRQS].
先列一下宏定义的位置,然后分析一下。
arch/arm64/include/asm/ipipe_base.h:
#define IPIPE_NR_ROOT_IRQS 1024
#define IPIPE_NR_XIRQS IPIPE_NR_ROOT_IRQSinclude/linux/ipipe_domain.h:
#define __bpl_up(x) (((x)+(BITS_PER_LONG-1)) & ~(BITS_PER_LONG-1))
/* Number of virtual IRQs (must be a multiple of BITS_PER_LONG) */
#define IPIPE_NR_VIRQS BITS_PER_LONG
/* First virtual IRQ # (must be aligned on BITS_PER_LONG) */
#define IPIPE_VIRQ_BASE __bpl_up(IPIPE_NR_XIRQS)
/* Total number of IRQ slots */
#define IPIPE_NR_IRQS (IPIPE_VIRQ_BASE+IPIPE_NR_VIRQS)
IPIPE_NR_IRQS由两部分构成,分别分析一下。
第一部分IPIPE_VIRQ_BASE,等同于__bpl_up(IPIPE_NR_XIRQS),即1024。
__bpl_up(x)宏定义的作用,是使得输入的x和BITS_PER_LONG向上对齐,bpl是bits per long三个单词的缩写。假设BITS_PER_LONG是64,那么x返回的值总是和64向上对齐,即总是64的整倍数。用下面的shell可以验证,其中~(64-1) = 0xFFC0。
因为IPIPE_NR_XIRQS(不知道这个X是代指什么)是1024,与64对齐,所以IPIPE_VIRQ_BASE也是1024,等同于IPIPE_NR_XIRQS。
IPIPE_VIRQ_BASE代表第一个virtual IRQ的编号,这就引出了第二部分IPIPE_NR_VIRQS。
第二部分IPIPE_NR_VIRQS,等同于BITS_PER_LONG,即64。
这里的VIRQ和前面讨论的虚拟中断标志位完全不是一个概念哦,千万不能混淆。VIRQ的前10个,分别为7个NR_IPI和3个IPIPE_OOB_IPI_NR,接下来分别展开说一下。
arch/arm64/include/asm/hardirq.h:
#define NR_IPI 7arch/arm64/include/asm/ipipe_base.h:
/** Out-of-band IPIs are directly mapped to SGI1-3, instead of* multiplexed over SGI0 like regular in-band messages.*/
#define IPIPE_IPI_BASE IPIPE_VIRQ_BASE
#define IPIPE_OOB_IPI_NR 3
#define IPIPE_CRITICAL_IPI (IPIPE_IPI_BASE + NR_IPI)
#define IPIPE_HRTIMER_IPI (IPIPE_IPI_BASE + NR_IPI + 1)
#define IPIPE_RESCHEDULE_IPI (IPIPE_IPI_BASE + NR_IPI + 2)
前7个NR_IPI,是Linux原始定义的7个IPI中断,全称是Inter-Processor Interrupt,用于CPU core之间的通信。Linux内核可以通过smp_cross_call->__smp_cross_call-> gic_raise_softirq,从当前CPU core向其它CPU core来触发IPI中断。smp_cross_call调用函数指针__smp_cross_call,而__smp_cross_call由GIC V3中断控制器驱动(irq-gic-v3.c)调用gic_smp_init-> set_smp_cross_call(gic_raise_softirq)初始化为gic_raise_softirq。所以,CPU core之间的IPI中断,是依赖GIC V3中断控制器的,本质是调用gic_raise_softirq。注意这里面的softirq,其实就是GIC V3里面定义的SGI中断,即Software Generated Interrupt.
arch/arm64/kernel/smp.c://Linux原始定义的7个IPI中断
enum ipi_msg_type {IPI_RESCHEDULE,IPI_CALL_FUNC,IPI_CPU_STOP,IPI_CPU_CRASH_STOP,IPI_TIMER,IPI_IRQ_WORK,IPI_WAKEUP
};void (*__smp_cross_call)(const struct cpumask *, unsigned int);void __init set_smp_cross_call(void (*fn)(const struct cpumask *, unsigned int))
{__smp_cross_call = fn;
}static void smp_cross_call(const struct cpumask *target, unsigned int ipinr)
{trace_ipi_raise(target, ipi_types[ipinr]);__smp_cross_call(target, ipinr);
}drivers/irqchip/irq-gic-v3.c:
static void gic_smp_init(void)
{set_smp_cross_call(gic_raise_softirq);cpuhp_setup_state_nocalls(CPUHP_AP_IRQ_GIC_STARTING,"irqchip/arm/gicv3:starting",gic_starting_cpu, NULL);
}
后面3个IPIPE_OOB_IPI_NR,就算是IPIPE自己额外新增的3个IPI中断了。实时内核可以通过ipipe_send_ipi->smp_cross_call->__smp_cross_call->gic_raise_softirq来触发IPI中断。这3个IPIPE_OOB_IPI_NR各自又封装了接口:
ipipe/core.c:
// IPIPE_CRITICAL_IPI
unsigned long ipipe_critical_enter(void (*syncfn)(void))include/xenomai/pipeline/pipeline.h:
//IPIPE_RESCHEDULE_IPI
static inline void pipeline_send_resched_ipi(const struct cpumask *dest)// IPIPE_HRTIMER_IPI
static inline void pipeline_send_timer_ipi(const struct cpumask *dest)
总结起来,IPIPE_NR_IRQS两部分之和:IPIPE_NR_XIRQS(1024)和IPIPE_NR_VIRQS(64)。它们只是干巴巴的数字,实际的意义是什么呢?结合GIC V3来看一下!
参考《GICv3_Software_Overview_Official_Release_B》,下表描述了GIC V3支持的INTID(硬件中断号)的范围。
-
SGI (Software Generated Interrupt):软件触发的中断。Linux内核可以通过写GICD_SGIR寄存器来触发一个中断事件,用于CPU core之间的通信。
-
PPI (Private Peripheral Interrupt):私有外设中断。这是每个核心私有的中断。PPI会送达到指定的CPU上,应用场景有CPU本地时钟。
-
SPI (Shared Peripheral Interrupt):软件触发的中断。软件可以通过写GICD_SGIR寄存器来触发一个中断事件,一般用于核间通信。
-
LPI (Locality-specific Peripheral Interrupt):LPI是GICv3中的新特性,是基于消息的中断。当前GIC V3驱动irq-gic-v3.c的参数gicv3_nolpi默认为0,所以默认是默认支持LPI的。
GIC V3的PPI+SPI+LPI是硬件中断号,在Linux中一般用int hwirq来表示,下面都用hwirq来代指硬件中断号。为了方便管理中断号,在Linux中还定义了逻辑中断号,一般用int virq来表示,每个virq都对应一个struct irq_desc数据结构,下面都用virq来代指逻辑中断号。逻辑中断号virq和硬件中断号hwirq的映射关系,不是简单的相等关系。从CPU硬件的角度来说,hwirq是固定分配好的,但是逻辑中断号virq是通过位图变量allocated_irqs按照先申请先得的规则分配的。二者的映射关系是在内核启动过程中建立,函数调用关系如下,irq_create_fwspec_mapping返回的就是virq,在这里就不具体展开了,还是回到主题来梳理一下逻辑中断号virq的范围。
如刚刚提到的,逻辑中断号virq是通过位图变量allocated_irqs按照先申请先得的规则分配的,所以virq的范围就要看位图变量allocated_irqs到底是多少bit位。
kernel/irq/irqdesc.c:static DECLARE_BITMAP(allocated_irqs, IRQ_BITMAP_BITS);//分配virq时,首先找到空闲的bit位
__irq_alloc_descs-> bitmap_find_next_zero_area(allocated_irqs, IRQ_BITMAP_BITS, from, cnt, 0);
IRQ_BITMAP_BITS具体是多大呢?64或者64+8196,取决于CONFIG_SPARSE_IRQ。
include/asm-generic/irq.h:
/** NR_IRQS is the upper bound of how many interrupts can be handled* in the platform. It is used to size the static irq_map array,* so don't make it too big.*/
#ifndef NR_IRQS
#define NR_IRQS 64
#endifkernel/irq/internals.h:#ifdef CONFIG_SPARSE_IRQ
# define IRQ_BITMAP_BITS (NR_IRQS + 8196)
#else
# define IRQ_BITMAP_BITS NR_IRQS
#endif
CONFIG_SPARSE_IRQ代表如何选择struct irq_desc的管理方式。对于指定的virq,如何快速的找到对应的struct irq_desc结构体?如果CONFIG_SPARSE_IRQ=y,就用radix tree,否则就要静态数组。当前默认是CONFIG_SPARSE_IRQ=y。
kernel/irq/irqdesc.c:#ifdef CONFIG_SPARSE_IRQ
……
static RADIX_TREE(irq_desc_tree, GFP_KERNEL);
……
#else /* !CONFIG_SPARSE_IRQ */struct irq_desc irq_desc[NR_IRQS] __cacheline_aligned_in_smp = {[0 ... NR_IRQS-1] = {.handle_irq = handle_bad_irq,.depth = 1,.lock = __RAW_SPIN_LOCK_UNLOCKED(irq_desc->lock),}
};……
#endif
截止目前,我们分析出了3组和中断相关数字:
IPIPE_NR_IRQS = IPIPE_NR_XIRQS + IPIPE_NR_VIRQS
GIC V3 INTID = SGI + hwirq(PPI + SPI + LPI)
Linux virq的数量 = IRQ_BITMAP_BITS
它们的关系到底是什么呢?接下来用一副图来揭秘!
IPIPE_NR_XIRQS的值默认是1024,如果hwirq的总数超过1024,映射过程就会出错。
IPIPE_NR_VIRQS对应ARM64 GIC V3中断处理器的SGI中断。Linux原始定义的7个IPI中断,会占用7个SGI中断号:SGI0~SGI6。但是IPIPE来了之后,会强制Linux 7个IPI中断共享SGI0。具体是咋做到的,后面会结合代码再分析(参考3.5.1小节)。
3.4.1.3 IPIPE interrupt log数据结构
IPIPE interrupt log的概念,来自于IPIPE中断分发的核心函数__ipipe_dispatch_irq(kernel/ipipe/core.c)里面的注释。这个函数的具体流程会在后面的章节讨论,这里只讨论interrupt log的数据结构是怎么设计的。
第一步,先把中断记录到interrupt log。看一下这个截取自__ipipe_dispatch_irq 函数的图片。
把中断记录到interrupt log,核心函数就是__ipipe_set_irq_pending。从字面很好理解,暂时不处理中断,可不就是让中断处于pending状态嘛。从上图还能看出来,在特殊情况下(IPIPE_IRQF_NOSYNC),head域也不立即处理中断,会记录到interrupt log。下面看一下__ipipe_set_irq_pending的真面目!
这个函数的定义,与include/linux/ipipe_domain.h中的两个宏定义IPIPE_IRQ_MAPSZ/ __IPIPE_IRQMAP_LEVELS相关。根据上一章的分析,IPIPE_NR_IRQS等于1088,所以IPIPE_IRQ_MAPSZ是17,__IPIPE_IRQMAP_LEVELS是2.
在kernel/ipipe/core.c,找到__ipipe_set_irq_pending的定义:
第5行,根据上面的分析,只关注__IPIPE_IRQMAP_LEVELS等于2的情况。
第9行,根据传入的struct ipipe_domain *ipd,找到struct ipipe_percpu_domain_data *p。对于支持SMP的多core CPU,当中断发生时,GIC V3一定是把中断送给某个CPU core来处理。所以,interrupt log也是每个CPU core自己单独记录的,它其实对应的就是ipipe_percpu_domain_data的成员irqpend_map。irqpend_map实际上就是一个数组,类型是unsigned long即每个数组元素提供64个bit位,数组长度是17即总共提供64*17=1088个bit位来标记中断是否pending。
struct ipipe_percpu_domain_data {unsigned long status; /* <= Must be first in struct. */unsigned long irqpend_0map;……unsigned long irqpend_map[IPIPE_IRQ_MAPSZ];unsigned long irqheld_map[IPIPE_IRQ_MAPSZ];unsigned long irqall[IPIPE_NR_IRQS];struct ipipe_domain *domain;int coflags;
};
第10行,数组irqpend_map相当于把1088个bit平分成17个区域(每区域64个bit),用中断号直接对64(BITS_PER_LONG)取整,得到的就是此中断所对应的区域编号。这里,把这个区域编号存入变量l0b,它的名字是level 0 bit的缩写。
第15行,利用linux的位图操作函数,直接把irq在数组irqpend_map中对应的bit置位为1,代表此中断pending。这里提出一个问题哈:置位的位置,属于完数组irqpend_map的哪个元素?答案其实就是第10行的l0b。
第16行,把第10行得出的区域编号l0b作为索引值,在变量irqpend_0map中置位对应的bit。所以,irqpend_0map的每个bit就代表每个区域中是否至少有1个bit处于pending状态。所以,只要irqpend_0map的值不等于0,就代表一定有中断处于pengding状态。
第18行,如果中断irq处于IPIPE_LOCK_FLAG状态,不会把中断记录到irqpend_map中,而是记录到irqheld_map中。只要root domain才会标记irq是否处于IPIPE_LOCK_FLAG状态,这里不做深入讨论,后面在分析中断流程的时候再展开。
第20行,对每个中断irq,记录一下在此CPU core总共发生了多少次。
第二步,从interrupt log中查询出未处理的中断,进行处理,为方便描述简称interrupt log回放。核心函数就是__ipipe_next_irq,它是__ipipe_set_irq_pending的反向操作。从下图的函数调用关系可知,IPIPE总是通过调用__ipipe_sync_stage来完成interrupt log回放。
按照__IPIPE_IRQMAP_LEVELS等于2,来分析一下__ipipe_next_irq。
第11~13行,如之前对__ipipe_set_irq_pending的分析,只要l0m即irqpeng_0map的值为0,代表没有任何中断记录在数组中irqpend_map。
第15~16行,从l0m(level 0 map)即irqpend_0map中找到第一个非0的bit的编号l0b。这个编号l0b作为下标从数组irqpend_map取出一个元素l1m= irqpend_map[l0b],这个l1m就代表一个64bit长的区域,在此区域中一定存在着一个bit不为0.
第17~18行,如果判断成立,肯定是bug。
第20~21行,从l1m(level 1 map)即irqpend_map[l0b]中找到第一个不为0的bit的编号l1b,然后把这个bit位在irqpend_map[l0b]中清零。
第22~23行,如果irqpend_map[l0b]此时已经等于0,代表此64bit长的区域已经没有任何一个中断pend了,把irqpeng_0map中的l0b位清零。
第25行,l0b*64+l1b就是找到的penging的irq编号。
分析起来挺绕的,其实从遍历数组irqpend_map[17]的每个元素,从中找到非0的bit。只不过irqpend_0map的bit位能够标记具体哪个数组元素里面存在非0的bit,所以不再需要用for循环一遍一遍的遍历数组。
3.4.2 __ipipe_init_early之fixup_percpu_data()
这个函数只有在CPU是SMP对称多core的情况下,才会真正运作,否则就是个空函数。
#ifdef CONFIG_SMPstatic inline void fixup_percpu_data(void)
{struct ipipe_percpu_data *p;int cpu;/** ipipe_percpu.curr cannot be assigned statically to* &ipipe_percpu.root, due to the dynamic nature of percpu* data. So we make ipipe_percpu.curr refer to a temporary* boot up context in static memory, until we can fixup all* context pointers in this routine, after per-cpu areas have* been eventually set up. The temporary context data is* copied to per_cpu(ipipe_percpu, 0).root in the same move.** Obviously, this code must run over the boot CPU, before SMP* operations start.*/BUG_ON(smp_processor_id() || !irqs_disabled());per_cpu(ipipe_percpu, 0).root = bootup_context; //这一句感觉是多余的:-)for_each_possible_cpu(cpu) {p = &per_cpu(ipipe_percpu, cpu);p->curr = &p->root;}
}#else /* !CONFIG_SMP */static inline void fixup_percpu_data(void) { }#endif /* CONFIG_SMP */
在Linux处于启动过程中,而实时内核还没有注册到IPIPE中时,ipipe_percpu.curr应该指向ipipe_percpu.root。但是,ipipe_percpu是per cpu变量,在静态定义时是无法让ipipe_percpu.curr指向ipipe_percpu.root的。
为什么?
既然ipipe_percpu是per cpu变量,所以每个CPU core都有一份独有的ipipe_percpu。在Linux没有启动的情况下,你咋知道CPU有多少个core呢?所以,静态定义的per cpu变量在编译阶段只有一份,只有当Linux启动后,才会为每个CPU core分配一个独有的per cpu变量。
怎么办?
IPIPE的解决办法是这样的。静态定义的时候,只要保证通过ipipe_percpu.curr访问到的struct ipipe_percpu_domain_data结构的内容和ipipe_percpu.root的内容一致,不就可以了吗。
于是,IPIPE想到了一个规避的方法,定义了一个bootup_context,它的内容和ipipe_percpu.root完全一致。同时,让ipipe_percpu.curr指向bootup_context。所以,从start_kernel执行到__ipipe_init_early,如果访问ipipe_percpu.curr,得到的内容一定是正确的。
当代码走到__ipipe_init_early->fixup_percpu_data,此时per cpu变量已经根据CPU SMP完成初始化了,是时候最终修复问题了。此时,只需要遍历每个CPU core的ipipe_percpu变量,让ipipe_percpu.curr指向ipipe_percpu.root即可!
3.4.3 __ipipe_init_early之初始化root domain
如下图所示,红框里面的函数当前都是空的,本章还是分析蓝框中的代码片段。
第295行,变量ipd指向了ipipe_root即ipd代表root domain。
第305行,root domain的名字被命名为“Linux”。
第306行,初始化ipd->context_offset变量,详细分析见《3.4.1.1 IPIPE基础数据结构》。
第307行,调用init_stage(ipd)对root domain进行初始化。此函数只要3行,直接在代码里面进行注释如下:
linux/kernel/ipipe/core.c:
static void init_stage(struct ipipe_domain *ipd)
{
//对ipipe_irqdesc irqs[IPIPE_NR_IRQS]数组进行清零操作。
//此数据结构的意义具体见《3.4.1.2 IPIPE对Linux中断号的改造》memset(&ipd->irqs, 0, sizeof(ipd->irqs));//对互斥量进行初始化mutex_init(&ipd->mutex);//(1) 调用__ipipe_ipis_alloc()完成所有IPI中断的分配
//(2) 调用hook_internal_ipi设置IPIPE_CRITICAL_IPI__ipipe_hook_critical_ipi(ipd);
}arch/arm64/kernel/ipipe.c:
void __ipipe_hook_critical_ipi(struct ipipe_domain *ipd)
{__ipipe_ipis_alloc();hook_internal_ipi(ipd, IPIPE_CRITICAL_IPI, __ipipe_do_critical_sync);
}
init_stage最后调用的函数__ipipe_hook_critical_ipi(ipd),里面做了两件事情,需要展开说一下。
- 调用__ipipe_ipis_alloc()完成所有IPI中断的分配
arch/arm64/kernel/smp.c:
void __ipipe_ipis_alloc(void)
{unsigned int virq, ipi;static bool done;if (done)return;/** We have to get virtual IRQs in the range* [ IPIPE_IPI_BASE..IPIPE_IPI_BASE + NR_IPI + IPIPE_OOB_IPI_NR - 1 ],* otherwise something is wrong (likely someone would have* allocated virqs before we do, and this would break our* fixed numbering scheme for IPIs).*/for (ipi = 0; ipi < NR_IPI + IPIPE_OOB_IPI_NR; ipi++) {virq = ipipe_alloc_virq();WARN_ON_ONCE(virq != IPIPE_IPI_BASE + ipi);}done = true;
}
根据《3.4.1.2 IPIPE对Linux中断号的改造》的分析,NR_IPI等于7,IPIPE_OOB_IPI_NR等于3,所以for循环调用ipipe_alloc_virq()函数10次,共申请10个virtual interrupt编号。关于virtual interrupt,之前已经总结过了,它的总数量就是IPIPE_NR_VIRQS(64)。为了管理virtual interrupt,IPIPE定义了全局变量 __ipipe_virtual_irq_map,通过位图来标记是否virtual interrupt是否被占用。
ipipe_alloc_virq()函数的本质,就是在变量__ipipe_virtual_irq_map的64个bit中,找到第一个为0的bit位(ffz就是find first zero)即空闲的bit位,然后把此bit位设置为1即占用此bit位。从__ipipe_virtual_irq_map中找到的bit位存到变量ipos,必须再加上IPIPE_VIRQ_BASE(1024),才能得到最终的virtual interrupt的编号。
回到__ipipe_ipis_alloc,它for循环调用ipipe_alloc_virq()函数10次,得到的10个virq是1024~1033. 注意这个范围哦,下一步的IPIPE_CRITICAL_IPI是1031,就在这个范围内!
linux/kernel/ipipe/core.c:
// 共64 bit位,通过位图来标记是否VIRQ是否被占用
static unsigned long __ipipe_virtual_irq_map;unsigned int ipipe_alloc_virq(void)
{unsigned long flags, irq = 0;int ipos;raw_spin_lock_irqsave(&__ipipe_lock, flags);if (__ipipe_virtual_irq_map != ~0) {ipos = ffz(__ipipe_virtual_irq_map);set_bit(ipos, &__ipipe_virtual_irq_map);irq = ipos + IPIPE_VIRQ_BASE;}raw_spin_unlock_irqrestore(&__ipipe_lock, flags);return irq;
}
EXPORT_SYMBOL_GPL(ipipe_alloc_virq);
- 调用hook_internal_ipi设置IPIPE_CRITICAL_IPI
IPIPE_CRITICAL_IPI是OOB IPI之一,它对应的virq编号是1031。上一步已经把1031分配出来了,hook_internal_ipi就是来设置IPIPE_CRITICAL_IPI对应的中断处理程序为__ipipe_do_critical_sync()!具体这个函数的用处,后面的章节再分析。
arch/arm64/kernel/ipipe.c:
static inline void
hook_internal_ipi(struct ipipe_domain *ipd, int virq,void (*handler)(unsigned int irq, void *cookie))
{ipd->irqs[virq].ackfn = NULL;ipd->irqs[virq].handler = handler;ipd->irqs[virq].cookie = NULL;/* Immediately handle in the current domain but *never* pass */ipd->irqs[virq].control = IPIPE_HANDLE_MASK|IPIPE_STICKY_MASK;
}
3.4.4 __ipipe_init_early之再论虚拟中断
根据《3.4.1.2 IPIPE对Linux中断号的改造》的分析,IPIPE引入的虚拟中断virtual interrupt的概念,其中前10个虚拟中断本质上是利用SGI实现的IPI中断。IPIPE在原来Linux原生的7个NR_IPI的基础上,多定义了3个IPIPE_OOB_IPI_NR。虚拟中断virtual interrupt总共是64个呢,其余的怎么用呢?
在__ipipe_init_early的结尾,初始化了两个virtual interrupt: __ipipe_printk_virq和__ipipe_work_virq。
它们的virtual interrupt编号是多少?
上一节分析,virtual interrupt编号前10个已经分配出去了,即1024~1033。__ipipe_printk_virq和__ipipe_work_virq通过ipipe_alloc_virq()得到的virtual interrupt编号是1034和1035.
它们是如何触发的?
这两个virtual interrupt,都不是经过SGI触发的IPI中断,而是直接在软件层面触发。接下来对它们进行分析。
(1) __ipipe_printk_virq
__ipipe_printk_virq用来干啥?它涉及的代码集中在kernel/printk/printk.c,涉及的commit信息是:printk: ipipe: defer vprintk() output,它最重的修改就是对printk进行hack,让head domain调用printk函数时,实际是把打印内容通过__ipipe_printk_virq送给root domain去打印,避免影响影响head domain的实时性。
正因为如此,root domain需要设置中断响应程序__ipipe_flush_printk来响应来自head domain的__ipipe_printk_virq中断。反过来思考,head domain是发起__ipipe_printk_virq中断的一方,是不需要设置中断响应程序来处理__ipipe_printk_virq中断的哦。
那head domain是如何发起__ipipe_printk_virq中断的? 如果是head domain调用printk函数,核心调用关系如下。
最终调用的是函数__ipipe_dispatch_irq,它之前不都是在硬件中断的调用栈中,怎么会出现在这里呢?非常巧妙,IPIPE可以通过调用ipipe_raise_irq函数,来模拟virtual interrupt的触发,并调用函数__ipipe_dispatch_irq来处理此virtual interrupt。在函数__ipipe_dispatch_irq中,会把__ipipe_printk_virq虚拟中断记录到root domain的interrupt log。直到interrupt log被回放,在root domain中调用虚拟中断响应程序__ipipe_flush_printk来完成打印操作。
(2) __ipipe_work_virq
上面的虚拟中断__ipipe_printk_virq是一个专用的虚拟中断,而__ipipe_work_virq是一个通用的虚拟中断。可以通过触发虚拟中断__ipipe_work_virq,给root domain传递任意的工作项struct ipipe_work_header,定义如下。
include/linux/ipipe.h:struct ipipe_work_header {size_t size;void (*handler)(struct ipipe_work_header *work);
};
如何发起工作项,它的核心调用堆栈如下:
最后一步,是调用__ipipe_set_irq_pending(&ipipe_root, irq)把__ipipe_work_virq虚拟中断记录到root domain的interrupt log,这不是取代了__ipipe_dispatch_irq的工作嘛,非常简单粗暴了。直到interrupt log被回放,在root domain中调用虚拟中断响应程序__ipipe_do_work来完成工作项。
在IPIPE Patch中,并没有运用__ipipe_work_virq的实例。后来在Xenomai patch中找到了一处实例。
至此,对两个虚拟中断的分析结束了。
结合这两个实例,再反过来阅读Documentation/ipipe.rst中关于虚拟中断的章节,感觉理解的更加透彻了!特别是不仅可以利用ipipe_post_irq_root把任务发到in-band即root domain中,还可以利用ipipe_post_irq_head把任务发到out-of-band即head domain中,非常巧妙!
Virtual/Synthetic interrupt vectors
-----------------------------------.. _synthetic:
.. _virtual:
The pipeline introduces an additional type of interrupts, which are
purely software-originated, with no hardware involvement. These IRQs
can be triggered by any kernel code. So-called virtual IRQs are
inherently per-CPU events.Because the common pipeline flow_ applies to virtual interrupts, it
is possible to attach them to out-of-band and/or in-band handlers,
just like device interrupts... NOTE:: virtual interrupts and regular softirqs differ in essence:the latter only exist in the in-band context, and thereforecannot trigger out-of-band activities.Virtual interrupt vectors are allocated by a call to
:c:func:`ipipe_alloc_virq`, and conversely released with
:c:func:`ipipe_free_virq`.For instance, a virtual interrupt can be used for triggering an
in-band activity on the root stage from the head stage as follows::#include <linux/ipipe.h>static void virq_handler(unsigned int virq, void *cookie){do_in_band_work();}void install_virq(void){unsigned int virq;...virq = ipipe_alloc_virq();...ipipe_request_irq(ipipe_root_domain, virq, virq_handler,handler_arg, NULL);}An out-of-band handler can schedule the execution of
:c:func:`virq_handler` like this::ipipe_post_irq_root(virq);Conversely, a virtual interrupt can be handled from the out-of-band
context::static void virq_oob_handler(unsigned int virq, void *cookie){do_oob_work();}void install_virq(void){unsigned int virq;...virq = ipipe_alloc_virq();...ipipe_request_irq(ipipe_head_domain, virq, virq_oob_handler,handler_arg, NULL);}Any in-band code can trigger the immediate execution of
:c:func:`virq_oob_handler` on the head stage as follows::ipipe_post_irq_head(virq);
irq, virq_handler,handler_arg, NULL);}An out-of-band handler can schedule the execution of
:c:func:`virq_handler` like this::ipipe_post_irq_root(virq);Conversely, a virtual interrupt can be handled from the out-of-band
context::static void virq_oob_handler(unsigned int virq, void *cookie){do_oob_work();}void install_virq(void){unsigned int virq;...virq = ipipe_alloc_virq();...ipipe_request_irq(ipipe_head_domain, virq, virq_oob_handler,handler_arg, NULL);}Any in-band code can trigger the immediate execution of
:c:func:`virq_oob_handler` on the head stage as follows::ipipe_post_irq_head(virq);