linux内核相关视频解析:
手写一个用户态网络协议栈,瞬间提升你网络功底
初识Linux内核,**通信能这么玩
一、中断概述
中断是指在CPU正常运行期间,由于内外部事件或由程序预先安排的事件引起的CPU暂时停止正在运行的程序,转而为该内部或外部事件或预先安排的事件服务的程序中去,服务完毕后再返回去继续运行被暂时中断的程序。
1.1中断类型
同步中断由CPU本身产生,又称为内部中断。这里同步是指中断请求**与代码指令之间的同步执行,在一条指令执行完毕后,CPU才能进行中断,不能在执行期间。所以也称为异常(exception)。
异步中断是由外部硬件设备产生,又称为外部中断,与同步中断相反,异步中断可在任何时间产生,包括指令执行期间,所以也被称为中断(interrupt)。
异常又可分为可屏蔽中断(Maskable interrupt)和非屏蔽中断(Nomaskable interrupt)。而中断可分为故障(fault)、陷阱(trap)、终止(abort)三类。
从广义上讲,中断又可分为四类:中断、故障、陷阱、终止。这些类别之间的异同点请参考 表 1。
有些参考资料中按照中断来源进行分类,如下图所示(个人建议不采用这种**):
图1-1
1.2区分中断号与中断向
I/O设备把中断**发送给中断控制器(8259A)时与之相关联的是一个中断号,当中断控制器把中断**发送给CPU时与之关联的是一个中断向量。换个角度**就是中断号是从中断控制器层面划分,中断向量是从CPU层面划分,所以中断号与中断向量之间存在一对一映射关系。在Intel X86中最大支持256种中断,从0到255开始编号,这个8位的编号就是中断向量。其中将0到31保留用于异常处理和不可屏蔽中断。
【文章福利】需要C/C++ Linux服务器架构师学习资料加群812855908(资料包括C/C++,Linux,golang技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg等)
二、中断数据处理结构
Linux内核中处理中断主要有三个数据结构,irq_desc,irq_chip和irqaction。
在\include\linux\ irq.h中定义了
1)irq_desc用于描述IRQ线的属性与状态,被称为中断描述符。
/** * struct irq_desc - interrupt descriptor * @irq: interrupt number for this descriptor * @timer_rand_state: pointer to timer rand state struct * @kstat_irqs: irq stats per cpu * @irq_2_iommu: iommu with this irq * @handle_irq: highlevel irq-events handler [if NULL, __do_IRQ()] * @chip: low level interrupt hardware access * @msi_desc: MSI descriptor * @handler_data: per-IRQ data for the irq_chip methods * @chip_data: platform-specific per-chip private data for the chip * methods, to allow shared chip implementations * @action: the irq action chain * @status: status information * @depth: disable-depth, for nested irq_disable() calls * @wake_depth: enable depth, for multiple set_irq_wake() callers * @irq_count: stats field to detect stalled irqs * @last_unhandled: aging timer for unhandled count * @irqs_unhandled: stats field for spurious unhandled interrupts * @lock: locking for SMP * @affinity: IRQ affinity on SMP * @node: node index useful for balancing * @pending_mask: pending rebalanced interrupts * @threads_active: number of irqaction threads currently running * @wait_for_threads: wait queue for sync_irq to wait for threaded handlers * @dir: /proc/irq/ procfs entry * @name: flow handler name for /proc/interrupts output */ struct irq_desc{ unsigned int irq; struct timer_rand_state *timer_rand_state; unsigned int *kstat_irqs; #ifdef CONFIG_INTR_REMAP struct irq_2_iommu *irq_2_iommu; #endif irq_flow_handler_t handle_irq; struct irq_chip *chip; struct msi_desc *msi_desc; void *handler_data; void *chip_data; struct irqaction *action; /* IRQ action list */ unsigned int status; /* IRQ status */ unsigned int depth; /* nested irq disables */ unsigned int wake_depth; /* nested wake enables */ unsigned int irq_count; /* For detecting broken IRQs */ unsigned long last_unhandled; /* Aging timer for unhandled count */ unsigned int irqs_unhandled; spinlock_t lock; #ifdef CONFIG_SMP cpumask_var_t affinity; unsigned int node; #ifdef CONFIG_GENERIC_PENDING_IRQ cpumask_var_t pending_mask; #endif #endif atomic_t threads_active; wait_queue_head_t wait_for_threads; #ifdef CONFIG_PROC_FS struct proc_dir_entry *dir; #endif const char *name; }
2)irq_chip用于描述不同类型的中断控制器。
/** * struct irq_chip - hardware interrupt chip descriptor * * @name: name for /proc/interrupts * @startup: start up the interrupt (defaults to ->enable if NULL) * @shutdown: shut down the interrupt (defaults to ->disable if NULL) * @enable: enable the interrupt (defaults to chip->unmask if NULL) * @disable: disable the interrupt (defaults to chip->mask if NULL) * @ack: start of a new interrupt * @mask: mask an interrupt source * @mask_ack: ack and mask an interrupt source * @unmask: unmask an interrupt source * @eoi: end of interrupt - chip level * @end: end of interrupt - flow level * @set_affinity: set the CPU affinity on SMP machines * @retrigger: resend an IRQ to the CPU * @set_type: set the flow type (IRQ_TYPE_LEVEL/etc.) of an IRQ * @set_wake: enable/disable power-management wake-on of an IRQ * * @bus_lock: function to lock access to slow bus (i2c) chips * @bus_sync_unlock: function to sync and unlock slow bus (i2c) chips * * @release: release function solely used by UML * @typename: obsoleted by name, kept as migration helper */ struct irq_chip { const char *name; unsigned int (*startup)(unsigned int irq); void (*shutdown)(unsigned int irq); void (*enable)(unsigned int irq); void (*disable)(unsigned int irq); void (*ack)(unsigned int irq); void (*mask)(unsigned int irq); void (*mask_ack)(unsigned int irq); void (*unmask)(unsigned int irq); void (*eoi)(unsigned int irq); void (*end)(unsigned int irq); int (*set_affinity)(unsigned int irq, const struct cpumask *dest); int (*retrigger)(unsigned int irq); int (*set_type)(unsigned int irq, unsigned int flow_type); int (*set_wake)(unsigned int irq, unsigned int on); void (*bus_lock)(unsigned int irq); void (*bus_sync_unlock)(unsigned int irq); /* Currently used only by UML, might disappear one day.*/ #ifdef CONFIG_IRQ_RELEASE_METHOD void (*release)(unsigned int irq, void *dev_id); #endif /* * For compatibility, ->typename is copied into ->name. * Will disappear. */ const char *typename; }
在\include\linux\ interrupt.h中定义了 irqaction用来描述特定设备所产生的中断描述符。
/** * struct irqaction - per interrupt action descriptor * @handler: interrupt handler function * @flags: flags (see IRQF_* above) * @name: name of the device * @dev_id: cookie to identify the device * @next: pointer to the next irqaction for shared interrupts * @irq: interrupt number * @dir: pointer to the proc/irq/NN/name entry * @thread_fn: interupt handler function for threaded interrupts * @thread: thread pointer for threaded interrupts * @thread_flags: flags related to @thread */ struct irqaction { irq_handler_t handler; unsigned long flags; const char *name; void *dev_id; struct irqaction *next; int irq; struct proc_dir_entry *dir; irq_handler_t thread_fn; struct task_struct *thread; unsigned long thread_flags; };三、Linux中断机制
Linux中断机制由三部分组成:
1、中断子系统初始化:内核自身初始化**中对中断处理机制初始化,例如中断的数据结构以及中断请求等。
2、中断或异常处理:中断整体处理**。
3、中断API:为设备驱动提供API,例如注册,释放和激活等。
3.1中断子系统初始化
3.1.1中断描述符表(IDT)初始化
中断描述符表初始化需要经过两个**:
第一个**在内核引导**。由两个步骤组成,首先给分配IDT分配2KB空间(256中断向量,每个向量由8bit组成)并初始化;然后把IDT起始地址存储到IDTR寄存器中。
第二个**内核在初始化自身的start_kernal函数中使用trap_init初始化系统保留中断向量,使用init_IRQ完成其余中断向量初始化。
3.1.2中断请求队列初始化
init_IRQ调用pre_intr_init_hook,进而最终调用init_ISA_irqs初始化中断控制器以及每个IRQ线的中断请求队列。
3.2中断或异常处理
中断处理**:设备产生中断,并通过中断线将中断**送往中断控制器,如果中断没有被屏蔽则会到达CPU的INTR引脚,CPU立即停止当前工作,根据**中断向量号从IDT中找出门描述符,并执行相关中断程序。
异常处理**:异常是由CPU内部发生所以不会通过中断控制器,CPU直接根据中断向量号从IDT中找出门描述符,并执行相关中断程序。
图3-1
中断控制器处理主要有5个步骤:1.中断请求 2.中断相应 3.优先级比较 4.提交中断向量 5.中断结束。这里不再赘述5个步骤的具体流程。
CPU处理流程主要有6个步骤:1.确定中断或异常的中断向量 2.通过IDTR寄存器找到IDT 3.特权检查 4.特权级发生变化,进行堆栈切换 5.如果是异常将异常代码压入堆栈,如果是中断则关闭可屏蔽中断 6.**中断或异常服务程序执行。这里不再赘述6个步骤的具体流程。
3.3中断API
内核提供的API主要用于驱动的**。
注册IRQ:
int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev);
释放IRQ:
void free_irq(unsigned int, void *);
注:IRQ线资源非常宝贵,我们在使用时必须先注册,不使用时必须释放IRQ资源。
激活当前CPU中断:
local_irq_enable();
禁止当前CPU中断:
local_irq_disable();
激活指定中断线:
void enable_irq(unsigned int irq);
禁止指定中断线:
void disable_irq(unsigned int irq);
禁止指定中断线:
void disable_irq_nosync(unsigned int irq);
注:此函数调用irq_chip中disable禁止指定中断线,所以不会保证中断线上执行的中断服务程序已经退出。
3.4中断机制划分
由于中断会打断内核中**的正常调度运行,所以要求中断服务程序尽可能的短小精悍;但是在实际系统中,当中断到来时,要完成工作往往进行大量的耗时处理。因此期望让中断处理程序运行得快,并想让它完成的工作量多,这两个目标相互制约,诞生——顶/底半部机制。
中断处理程序是顶半部——接受中断,它就立即开始执行,但只有做严格时限的工作。能够被允许稍后完成的工作会推迟到底半部去,此后,在**的时机,底半部会被开终端执行。顶半部简单快速,执行时禁止一些或者全部中断。
底半部稍后执行,而且执行期间可以响应所有的中断。这种设计可以使系统处于中断屏蔽状态的时间尽可能的短,以此来提高系统的响应能力。顶半部只有中断处理程序机制,而底半部的实现有软中断,tasklet和工作队列实现。
图3-2 注:登记中断,将底半部处理程序挂到该设备的低半部执行队列中。
3.4.1顶/底半部划分原则:
1) 如果一个任务对时间非常**,将其放在顶半部中执行;
2) 如果一个任务和硬件有关,将其放在顶半部中执行;
3) 如果一个任务要保证不被其他中断打断,将其放在顶半部中执行;
4) 其他所有任务,考虑放置在底半部执行。
3.4.2底半部实现机制
图3-3
软中断:
软中断作为下半部机制的代表,是随着SMP(share memory processor)的出现应运而生的,它也是tasklet实现的基础(tasklet实际上只是在软中断的基础上添加了一定的机制)。软中断一般是“可延迟函数”的总称,有时候也包括了tasklet(请读者在遇到的时候根据上下文推断是否包含tasklet)。它的出现就是因为要**上面所提出的上半部和下半部的区别,使得对时间不**的任务延后执行,软中断执行中断处理程序留给它去完成的剩余任务,而且可以在多个CPU上并行执行,使得总的系统效率可以更高。它的特性包括:
a)产生后并不是马上可以执行,必须要等待内核的调度才能执行。软中断不能被自己打断,只能被硬件中断打断(上半部)。
b)可以并发运行在多个CPU上(即使同一类型的也可以)。所以软中断必须设计为可重入的函数(允许多个CPU同时操作),因此也需要使用自旋锁来保护其数据结构。
内核中定义了几种软中断的用途:
enum { HI_SOFTIRQ=0, TIMER_SOFTIRQ, NET_TX_SOFTIRQ, NET_RX_SOFTIRQ, BLOCK_SOFTIRQ, BLOCK_IOPOLL_SOFTIRQ, TASKLET_SOFTIRQ, SCHED_SOFTIRQ, HRTIMER_SOFTIRQ, RCU_SOFTIRQ, /* Preferable RCU should always be the last softirq */ NR_SOFTIRQS };
Tasklet:
tasklet是通过软中断实现的,所以它本身也是软中断。
软中断用轮询的**处理。假如正好是最后一种中断,则必须循环完所有的中断类型,才能最终执行对应的处理函数。显然当年**人员为了保证轮询的效率,于是限制中断个数为32个。
为了提高中断处理数量,顺道改进处理效率,于是产生了tasklet机制。
Tasklet采用无差别的队列机制,有中断时才执行,免去了循环查表之苦。Tasklet作为一种新机制,显然可以承担更多的优点。正好这时候SMP越来越火了,因此又在tasklet中加入了SMP机制,保证同种中断只能在一个cpu上执行。在软中断时代,显然没有这种考虑。因此同一种软中断可以在两个cpu上同时执行,很可能造成冲突。
总结下tasklet的优点:
(1)无类型数量限制;
(2)效率高,无需循环查表;
(3)支持SMP机制;
它的特性如下:
1)一种特定类型的tasklet只能运行在一个CPU上,不能并行,只能串行执行。
2)多个不同类型的tasklet可以并行在多个CPU上。
3)软中断是静态分配的,在内核编译好之后,就不能改变。但tasklet就灵活许多,可以在运行时改变(比如添加模块时)。
工作队列:
上面我们介绍的可延迟函数运行在中断上下文中,于是导致了一些问题,说明它们不可挂起,也就是说软中断不能睡眠、不能阻塞,原因是由于中断上下文出于内核态,没有**切换,所以如果软中断一旦睡眠或者阻塞,将无法退出这种状态,导致内核会整个僵死。因此,可阻塞函数不能用软中断来实现。但是它们往往又具有可延迟的特性。而且由于是串行执行,因此只要有一个处理时间较长,则会导致其他中断响应的延迟。为了完成这些不可能完成的任务,于是出现了工作队列,它能够在不同的**间切换,以完成不同的工作。
工作队列能运行在**上下文,它将工作给一个内核线程,作为中断守护线程来使用。多个中断可以放在一个线程中,也可以每个中断分配一个线程。我们用结构体workqueue_struct表示工作者线程,工作者线程是用内核线程实现的。而工作者线程是如何执行被推后的工作——有这样一个链表,它由结构体work_struct组成,而这个work_struct则描述了一个工作,一旦这个工作被执行完,相应的work_struct对象就从链表上移去,当链表上不再有对象时,工作者线程就会继续休眠。因为工作队列是线程,所以我们可以使用所有可以在线程中使用的**。
如何选择下半部机制:
软中断和tasklet运行在中断上下文,工作队列运行在**上下文。如果需要休眠则选择工作队列,否则选择tasklet;如果对性能要求较高则选择软中断。从易用性考虑,首选工作队列,然后tasklet,最后是软中断,因为软中断需要静态创建。从代码安全考虑,如果对底半部代码保护不够安全,则选择tasklet,因为相对软中断,tasklet对锁要求低,上面也简述它们工作**以及运用场景。四、多处理器系统中断相关概念
4.1处理器间中断
在多处理器系统中,操作系统需要在多个处理器中协调操作,所以需要处理器中断(Inter-Processor-Interrupt,IPI)实现,IPI是一种特殊硬件中断,由CPU送出,其他CPU接收,处理CPU之间通信和同步操作。以下是x86中SMP定义的IPI,中断向量号用十六进制表示:
SPURIOUS_APIC_VECTOR 0xffERROR_APIC_VECTOR 0xfeRESCHEDULE_VECTOR 0xfdCALL_FUNCTION_VECTOR 0xfcCALL_FUNCTION_SINGLE_VECTOR 0xfbTHERMAL_APIC_VECTOR 0xfaTHRESHOLD_APIC_VECTOR 0xf9REBOOT_VECTOR 0xf8INVALIDATE_TLB_VECTOR_END 0xf7INVALIDATE_TLB_VECTOR_START 0xf0
4.2中断亲和力
将一个或多个中断服务程序绑定到特定的CPU上处理,这就是中断亲和力(SMP IRQ affinity)。我们可以使用中断亲和力来均衡各个CPU的负载,提高系统处理能力。