polling vs interrupt
轮询 | 中断 |
---|---|
In polling the CPU keeps on checking all the hardwares of the availablilty of any request | In polling the CPU keeps on checking all the hardwares of the availablilty of any request |
The polling method is like a salesperson. The salesman goes from door to door while requesting to buy a product or service. Similarly, the controller keeps monitoring the flags or signals one by one for all devices and provides service to whichever component that needs its service. | An interrupt is like a shopkeeper. If one needs a service or product, he goes to him and apprises him of his needs. In case of interrupts, when the flags or signals are received, they notify the controller that they need to be serviced. |
中断
中断是来自硬件设备通过电子信号产生,并被引导到中断控制器的引脚上。
An interrupt is produced by electronic signals from hardware devices and directed into input pins on an interrupt controller (a simple chip that multiplexes multiple interrupt lines into a single line to the processor)。
当产生interrupt后,内核中处理流程:
- Upon receiving an interrupt, the interrupt controller sends a signal to the processor.
- The processor detects this signal and interrupts its current execution to handle the interrupt.
- The processor can then notify the operating system that an interrupt has occurred, and the operating system can handle the interrupt appropriately.
不同的设备一般注册了不同的中断号,这样操作系统就可以中断号来区分不同的中断并调用其对应的中断控制程序来处理中断。
中断处理是内核中最为敏感的行为操作,所以其必须足够安全:
-
Hardware devices generate interrupts asynchronously (with respect to the processor clock). That means interrupts can come anytime.
硬件设备异步产生中断,这就意味着随时可能发生中断。
-
Because interrupts can come anytime, the kernel might be handling one of them while another one (of a different type) occurs.
因为随时可能产生中断,所以内核必须有在处理某个中断时再去应付突然又来的其他中断的能力。
-
Some critical regions exist inside the kernel code where interrupts must be disabled. Such critical regions must be limited as much as possible.
某些场景下,内核中必须关中断,这些场景必须尽可能的限制其发生,因为中断具有随机性,不可预测。
interrupts and exceptions
异常exception与中断interrupt不同之处在于:异常exception通常是与processor clock同步产生的,一般也继续call synchronous interrupt(由异常引发的中断)。异常一般是由CPU产生,CPU执行程序指令发生错误(比如除以0)或者一些禁止的操作,这类异常通常来说必须由kernel来处理(比如page fault),此时CPU产生exception异常。在kernel中处理exception与处理interrupt其实很相似。
简单的角度理解:
interrupts – 由硬件产生的异步中断
exceptions – 由CPU产生的同步中断
中断分类
interrupts | |
---|---|
maskable(可屏蔽中断) | 所有的I/O设备产生的IRQs(interrupt request)都为可屏蔽中断类型,maskable类型中断分两种state:1. masked,2. unmasked,masked状态的中断会被控制器忽略直至其变为unmasked状态。 |
non-maskable(不可屏蔽中断) | 只有很少一部分中断(比如硬件错误)为不可屏蔽中断,CPU总会响应处理这类中断。 |
异常分类
exceptions | |
---|---|
falts | 比如除以0之类的错误,page fault,segmentation fault等 |
traps | 执行trapping instrution之后立即会陷入trap,比如breakpoints |
aborts | 用于十分严重的错误,比如硬件错误或者invalid values in system tables等 |
interrupt handler
kernel对interrupt的响应处理函数就叫做中断处理程序ISR(interrupt service routine or interrupt handler)。
- 每个能够产生interrupts的设备都分配了interrupt handler。
- 通常device driver中就包含了该device的interrupt handler。
Linux中,中断处理程序通常是C函数,不过与其他内核函数不同的是,interrupt handler执行时是处于特定的上下文,叫interrupt context。interrupt context中执行代码是不能阻塞的(unable to block),所以也叫atomic context。
因为中断是随时可能发生的,所以中断处理程序迅速执行完毕并返回被打断的code去继续执行是极其重要的:
- 硬件视角:os快速响应中断,没有delay是极其重要;
- 系统视角:interrupt handler执行时间要尽可能的短;
Process Context and Interrupt Context
kernel是通过process context与interrupt context来完成工作。用户程序通过system call进入kernel执行内核代码,这是进程上下文。而中断处理程序是在中断上下文中异步执行。进程上下文不依赖于中断上下文,中断上下文也不依赖与进程上下文。
运行在进程上下文中的内核代码是可被抢占的,而interrupt context,通常执行一直到completion,是不可抢占的。所以,interrupt context中的代码严禁以下操作:
- Go to sleep or relinquish the processor
- Acquire a mutex
- Perform time-consuming tasks
- Access user space virtual memory
基于以上描述,那么ISRs或者interrupt handler就必须是快速执行,不能长时间执行。那么问题来了,假如中断处理程序必须执行大量操作怎么办?,如果中断处理程序长时间执行,占用CPU,那么就有问题:
- 当高优先级的ISR程序正在执行,其他低优先级的中断就不能被响应执行;
- 当一个ISR程序正在执行,其他同类型的中断也无法响应了;
为了解决上面这个问题,将中断处理的过程分为两部分:
- Top halves
- Bottom halves
Top halves and Bottom halves
设计中断处理上半部和下半部的初衷是:
处理中断的过程中还能继续响应新来的中断
Top half
interrupt handler是top half,上半部在中断发生后会立即运行,通常只用于处理对时间要求十分严格的任务,比如确认收到中断、重置硬件等。
Bottom half
下半部一般用于process data处理数据,此时interrupts are enabled,然后上半部可以继续响应新来的中断信号。不过下半部中也是可以关中断的,但是这就有违设计上半部和下半部的初衷了,所以一般不会在下半部中关闭中断。
以网卡为例:
- 当网卡从网络接收到packets时,网卡会立即产生中断(这样会提升throughput和latency性能,避免延时)
- 然后kernel响应中断,执行网卡注册的中断处理程序
- 中断处理程序执行,通知硬件,拷贝new packets到main memory,并且从network card中拷贝更多的packets。这类工作是time-pritical、hardware-specific类型的:
- kernel需要快速响应网卡中断去拷贝packets到内存,因为网卡的buffer和内存相比,通常较小且是固定大小,如果拷贝延迟了,则可能buffer溢出导致丢包
- 当packets拷贝到内存中,中断处理工作则完成,然后它就可以返回到网卡中断打断的原代码处去执行
- 然后对于packets的处理过程则是bottom half的工作
Linux bottom half
Linux中主要有4中bottom half机制:
Linux bottom half机制 |
---|
Workqueue |
threaded IRQs |
Softirq |
Tasklets |
interrupt example
principles
编写中断处理程序,有一些原则:
- 中断处理程序不能进入睡眠,所以不能调用一些函数,比如
sleep
函数 - 当中断处理程序需要处理critical section时,使用自旋锁spinlocks,不能使用mutexes,因为mutex互斥量获取不到的时候,程序会被挂起(sleep)等待直至获取到mutex
- 中断处理程序不能与userspace交互数据
- 中断处理程序必须尽快就执行完毕,最好的办法就是top half 与 bottom half机制,上半部的工作立即执行完毕,下半部的工作可以使用softirq、workqueue、tasklet机制去完成
- 中断处理程序自身不能嵌套调用、不能重复调用,当在执行时,应该关闭该中断号直至该中断处理完毕
- 中断处理程序可以被更高优先级的中断给打断,如果想避免被打断,可以设置为fast handler,但是需要避免设置许多fast handler,这样会增加中断处理延时
functions
与编写中断程序相关的一些函数
request_irq()
request_irq()函数不能在interrupt context中调用(其他不能block的环境都不能调用),因为该函数可能会block阻塞。
irq: 中断号
handler:中断处理程序,如果处理成功,返回IRQ_HANDLED,否则返回IRQ_NONE
flags: 0或者是定义的值(interrupt.h中定义)比如IRQF_SHARED等
name: 产生该中断的设备名称,可以cat /proc/interrupt
查看对应的中断号和设备名
dev: IRQ可能被多个device共享使用,该参数可以提供指定的dev cookie这样内核在移除handler时就可以移除指定的handler
/**
* request_irq - Add a handler for an interrupt line
* @irq: The interrupt line to allocate
* @handler: Function to be called when the IRQ occurs.
* Primary handler for threaded interrupts
* If NULL, the default primary handler is installed
* @flags: Handling flags
* @name: Name of the device generating this interrupt
* @dev: A cookie passed to the handler function
*
* This call allocates an interrupt and establishes a handler; see
* the documentation for request_threaded_irq() for details.
*/
static inline int __must_check
request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,
const char *name, void *dev)
{
return request_threaded_irq(irq, handler, NULL, flags, name, dev);
}
free_irq()
free_irq()只能在进程上下文中调用,不能在中断上下文中调用
如果IRQ没有被share共享,则该函数会删除指定的handler并且disable该IRQ;
如果IRQ被共享,则需要通过dev_id去删除指定的dev对应的handler,当最后一个handler被删除时,也会去disable该IRQ中断。
/**
* free_irq - free an interrupt allocated with request_irq
* @irq: Interrupt line to free
* @dev_id: Device identity to free
*
* Remove an interrupt handler. The handler is removed and if the
* interrupt line is no longer in use by any driver it is disabled.
* On a shared IRQ the caller must ensure the interrupt is disabled
* on the card it drives before calling this function. The function
* does not return until any executing interrupts for this IRQ
* have completed.
*
* This function must not be called from interrupt context.
*
* Returns the devname argument passed to request_irq.
*/
const void *free_irq(unsigned int irq, void *dev_id);
enable_irq()
重新开启被disable_irq()或disable_irq_nosync()关闭的中断。
/**
* enable_irq - enable handling of an irq
* @irq: Interrupt to enable
*
* Undoes the effect of one call to disable_irq(). If this
* matches the last disable, processing of interrupts on this
* IRQ line is re-enabled.
*
* This function may be called from IRQ context only when
* desc->irq_data.chip->bus_lock and desc->chip->bus_sync_unlock are NULL !
*/
void enable_irq(unsigned int irq);
disable_irq()
关闭中断,等待某个事件完成。
/**
* disable_irq - disable an irq and wait for completion
* @irq: Interrupt to disable
*
* Disable the selected interrupt line. Enables and Disables are
* nested.
* This function waits for any pending IRQ handlers for this interrupt
* to complete before returning. If you use this function while
* holding a resource the IRQ handler may need you will deadlock.
*
* This function may be called - with care - from IRQ context.
*/
void disable_irq(unsigned int irq)
{
if (!__disable_irq_nosync(irq))
synchronize_irq(irq);
}
EXPORT_SYMBOL(disable_irq);
intel处理器处理中断
intel处理器通过IDT(interrupt descriptor table)来处理中断。intel支持256个中断(NR_VECTORS变量定义),每个中断向量有8字节,指向handler函数。CPU通过IDTR来定位到IDT,它俩关系如下图所示。
Linux中IRQ映射到vector是在arch/x86/include/asm/irq_vectors.h
中定义:
64位系统有per CPU中断向量表,32位系统则共享一个中断向量表。
/*
* Linux IRQ vector layout.
*
* There are 256 IDT entries (per CPU - each entry is 8 bytes) which can
* be defined by Linux. They are used as a jump table by the CPU when a
* given vector is triggered - by a CPU-external, CPU-internal or
* software-triggered event.
*
* Linux sets the kernel code address each entry jumps to early during
* bootup, and never changes them. This is the general layout of the
* IDT entries:
*
* Vectors 0 ... 31 : system traps and exceptions - hardcoded events
* Vectors 32 ... 127 : device interrupts
* Vector 128 : legacy int80 syscall interface
* Vectors 129 ... LOCAL_TIMER_VECTOR-1
* Vectors LOCAL_TIMER_VECTOR ... 255 : special interrupts
*
* 64-bit x86 has per CPU IDT tables, 32-bit has one shared IDT table.
*
* This file enumerates the exact layout of them:
*/
/* This is used as an interrupt vector when programming the APIC. */
#define NMI_VECTOR 0x02
/*
* IDT vectors usable for external interrupt sources start at 0x20.
* (0x80 is the syscall vector, 0x30-0x3f are for ISA)
*/
#define FIRST_EXTERNAL_VECTOR 0x20
#define LOCAL_TIMER_VECTOR 0xec
在Linux中可以使用int
指令来分配中断号,比如32位系统的system call是通过int 0x80
中断陷入内核(64位系统通过syscall
指令)。
下图是cat /proc/interrupts
查看的我当前系统的中断号分配情况:
注册IRQ中断号时需要做个转换,比如ISA interrupt:
/*
* Vectors 0x30-0x3f are used for ISA interrupts.
* round up to the next 16-vector boundary
*/
#define ISA_IRQ_VECTOR(irq) (((FIRST_EXTERNAL_VECTOR + 16) & ~15) + irq)
irq_vector.h中
vector | 用途 |
---|---|
0 … 31 (0x0 … 0x1F) | system traps and exceptions - hardcoded events |
32 … 127 (0x20 … 0x7F) | device interrupts |
128 (0x80) | legacy int80 syscall interface |
129 … LOCAL_TIMER_VECTOR-1 | |
LOCAL_TIMER_VECTOR … 255 | special interrupts |
外部中断是从0x20开始,所以如果我们想使用IRQ11号中断,需要转换下:
(((FIRST_EXTERNAL_VECTOR + 16) & ~15) + irq)
假如使用IRQ11,则 0x20 + 0x10 + 11 = 0x3B, vector 59
,使用asm("int 0x3B")
命令去注册中断处理函数。
示例
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kdev_t.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/slab.h> //kmalloc()
#include <linux/uaccess.h> //copy_to/from_user()
#include <linux/sysfs.h>
#include <linux/kobject.h>
#include <linux/interrupt.h>
#include <asm/io.h>
#include <asm/hw_irq.h>
#define IRQ_NO 11
//Interrupt handler for IRQ 11.
static irqreturn_t irq_handler(int irq,void *dev_id) {
printk(KERN_INFO "Shared IRQ: Interrupt Occurred");
return IRQ_HANDLED;
}
volatile int irq_test_value = 0;
dev_t dev = 0;
static struct class *dev_class;
static struct cdev irq_test_cdev;
struct kobject *kobj_ref;
static int __init irq_test_driver_init(void);
static void __exit irq_test_driver_exit(void);
/*************** Driver Fuctions **********************/
static int irq_test_open(struct inode *inode, struct file *file);
static int irq_test_release(struct inode *inode, struct file *file);
static ssize_t irq_test_read(struct file *filp,
char __user *buf, size_t len,loff_t * off);
static ssize_t irq_test_write(struct file *filp,
const char *buf, size_t len, loff_t * off);
/*************** Sysfs Fuctions **********************/
static ssize_t sysfs_show(struct kobject *kobj,
struct kobj_attribute *attr, char *buf);
static ssize_t sysfs_store(struct kobject *kobj,
struct kobj_attribute *attr,const char *buf, size_t count);
struct kobj_attribute irq_test_attr = __ATTR(irq_test_value, 0660, sysfs_show, sysfs_store);
static struct file_operations fops =
{
.owner = THIS_MODULE,
.read = irq_test_read,
.write = irq_test_write,
.open = irq_test_open,
.release = irq_test_release,
};
static ssize_t sysfs_show(struct kobject *kobj,
struct kobj_attribute *attr, char *buf)
{
printk(KERN_INFO "Sysfs - Read!!!\n");
return sprintf(buf, "%d", irq_test_value);
}
static ssize_t sysfs_store(struct kobject *kobj,
struct kobj_attribute *attr,const char *buf, size_t count)
{
printk(KERN_INFO "Sysfs - Write!!!\n");
sscanf(buf,"%d",&irq_test_value);
return count;
}
static int irq_test_open(struct inode *inode, struct file *file)
{
printk(KERN_INFO "Device File Opened...!!!\n");
return 0;
}
static int irq_test_release(struct inode *inode, struct file *file)
{
printk(KERN_INFO "Device File Closed...!!!\n");
return 0;
}
static ssize_t irq_test_read(struct file *filp,
char __user *buf, size_t len, loff_t *off)
{
struct irq_desc *desc;
printk(KERN_INFO "Read function\n");
desc = irq_to_desc(11);
if (!desc)
{
return -EINVAL;
}
__this_cpu_write(vector_irq[59], desc);
asm("int $0x3B"); // Corresponding to irq 11
return 0;
}
static ssize_t irq_test_write(struct file *filp,
const char __user *buf, size_t len, loff_t *off)
{
printk(KERN_INFO "Write Function\n");
return len;
}
static int __init irq_test_driver_init(void)
{
/*Allocating Major number*/
if((alloc_chrdev_region(&dev, 0, 1, "irq_test_Dev")) <0){
printk(KERN_INFO "Cannot allocate major number\n");
return -1;
}
printk(KERN_INFO "Major = %d Minor = %d \n",MAJOR(dev), MINOR(dev));
/*Creating cdev structure*/
cdev_init(&irq_test_cdev,&fops);
/*Adding character device to the system*/
if((cdev_add(&irq_test_cdev,dev,1)) < 0){
printk(KERN_INFO "Cannot add the device to the system\n");
goto r_class;
}
/*Creating struct class*/
if((dev_class = class_create(THIS_MODULE,"irq_test_class")) == NULL){
printk(KERN_INFO "Cannot create the struct class\n");
goto r_class;
}
/*Creating device*/
if((device_create(dev_class,NULL,dev,NULL,"irq_test_device")) == NULL){
printk(KERN_INFO "Cannot create the Device 1\n");
goto r_device;
}
/*Creating a directory in /sys/kernel/ */
kobj_ref = kobject_create_and_add("irq_test_sysfs",kernel_kobj);
/*Creating sysfs file for irq_test_value*/
if(sysfs_create_file(kobj_ref,&irq_test_attr.attr)){
printk(KERN_INFO"Cannot create sysfs file......\n");
goto r_sysfs;
}
if (request_irq(IRQ_NO, irq_handler, IRQF_SHARED, "irq_test_device", (void *)(irq_handler))) {
printk(KERN_INFO "my_device: cannot register IRQ ");
goto irq;
}
printk(KERN_INFO "Device Driver Insert...Done!!!\n");
return 0;
irq:
free_irq(IRQ_NO,(void *)(irq_handler));
r_sysfs:
kobject_put(kobj_ref);
sysfs_remove_file(kernel_kobj, &irq_test_attr.attr);
r_device:
class_destroy(dev_class);
r_class:
unregister_chrdev_region(dev,1);
cdev_del(&irq_test_cdev);
return -1;
}
static void __exit irq_test_driver_exit(void)
{
free_irq(IRQ_NO,(void *)(irq_handler));
kobject_put(kobj_ref);
sysfs_remove_file(kernel_kobj, &irq_test_attr.attr);
device_destroy(dev_class,dev);
class_destroy(dev_class);
cdev_del(&irq_test_cdev);
unregister_chrdev_region(dev, 1);
printk(KERN_INFO "Device Driver Remove...Done!!!\n");
}
module_init(irq_test_driver_init);
module_exit(irq_test_driver_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("A simple device driver - Interrupts");
MODULE_VERSION("1.0");
该代码需要内核到处vector_irq[]数组,修改arch/x86/kernel/irq.c
中添加
EXPORT_SYMBOL(vector_irq);
然后重新编译内核,安装新内核,再编译该模块。
diff --git a/arch/x86/kernel/irq.c b/arch/x86/kernel/irq.c
index c5dd50369..bd0e8a3d5 100644
--- a/arch/x86/kernel/irq.c
+++ b/arch/x86/kernel/irq.c
@@ -27,6 +27,7 @@
DEFINE_PER_CPU_SHARED_ALIGNED(irq_cpustat_t, irq_stat);
EXPORT_PER_CPU_SYMBOL(irq_stat);
+EXPORT_SYMBOL(vector_irq);
atomic_t irq_err_count;
安装之后,可以cat /proc/interrupts
查看,中断11之中多了一个irq_test_dev
名字,然后读取/dev/irq_test_dev
则会触发中断函数去执行。当然我看log一直在打印,共享中断号,如果其他设备触发了该中断号,那么我们的中断函数也会被执行。
[ 66.966574] interrupt: loading out-of-tree module taints kernel.
[ 66.966652] interrupt: module verification failed: signature and/or required key missing - tainting kernel
[ 66.967308] Major = 237 Minor = 0
[ 66.967549] Device Driver Insert...Done!!!
[ 68.630689] Shared IRQ: Interrupt Occurred
[ 68.663308] Shared IRQ: Interrupt Occurred
[ 69.630964] Shared IRQ: Interrupt Occurred
[ 69.970339] Shared IRQ: Interrupt Occurred
[ 70.679281] Shared IRQ: Interrupt Occurred
[ 123.129104] Shared IRQ: Interrupt Occurred
[ 125.145217] Shared IRQ: Interrupt Occurred
[ 125.833871] Device File Opened...!!!
[ 125.833898] Read function
[ 125.833944] Shared IRQ: Interrupt Occurred
[ 125.833975] Device File Closed...!!!