在讀者學習本章以及后續章節之前,最好擁有中斷裸機基礎,可以參考:中斷編程。
一、內核中斷分析
通過裸機系列的學習,我們可以知道異常的觸發分為下面幾個過程:
1. 在規定地址設置異常向量表
2. 保存各種寄存器的值(保存現場)
2. 執行異常處理函數(處理現場)
3. 恢復執行(恢復現場)
如u-boot中arch/arm/cpu/armv7/start.S中代碼:
1 .globl _start 2 _start: b reset 3 ldr pc, _undefined_instruction 4 ldr pc, _software_interrupt 5 ldr pc, _prefetch_abort 6 ldr pc, _data_abort 7 ldr pc, _not_used 8 ldr pc, _irq 9 ldr pc, _fiq 10 11 _undefined_instruction: .word undefined_instruction 12 _software_interrupt: .word software_interrupt 13 _prefetch_abort: .word prefetch_abort 14 _data_abort: .word data_abort 15 _not_used: .word not_used 16 _irq: .word irq 17 _fiq: .word fiq 18 19 ... 20 21 irq: 22 get_irq_stack /* 設置棧 */ 23 irq_save_user_regs /* 保存寄存器的值 */ 24 bl do_irq /* 處理中斷 */ 25 irq_restore_user_regs /* 恢復· */ 26 27 .align 5 28 29 ...
Linux的異常處理其實也和裸機中的流程一樣,只不過Linux要對所有的異常都進行具體分析處理
Linux內核所做的中斷初始化如下,其中b start_kernel代碼在arch/arm/kernel/head-common.S中
b start_kernel ... local_irq_disable(); /* 關中斷 */ ... setup_arch(&command_line); paging_init(mdesc); devicemaps_init(mdesc); early_trap_init(vectors); /* 設置異常向量表 */ ... trap_init(); /* 空函數 */ ... early_irq_init(); /* 初始化irq_desc數組 */ init_IRQ(); /* 芯片相關的中斷的初始化 */ ... local_irq_enable(); /* 開中斷 */
1. 首先關閉中斷,因為異常向量表還沒有設置,如果此時觸發中斷,程序會跑飛
2. 之后設置異常向量表
a. 申請一塊內存,用異常向量表填充這塊內存區域
b. 通過異常向量表的虛擬地址找到對應的物理地址,並把這塊內存區域再次映射到0xFFFF 0000區域
c. 之后檢查內存映射,如果不是高端映射,則映射到0地址(頁表會覆蓋掉前面的高端映射)
3. 初始化irq_desc數組,這個數組用於存儲中斷數據,如中斷號、中斷類型等
4. 芯片相關的中斷初始化、開中斷
當發生中斷時,會跳轉到vector_irq + offset的地址執行代碼,與裸機相同,代碼會進行保存現場、處理現場、恢復現場的操作
處理現場會調用asm_do_IRQ()函數,調用層次如下:
asm_do_IRQ(unsigned int irq, struct pt_regs *regs) -> handle_IRQ(irq, regs); -> generic_handle_irq(irq); -> struct irq_desc *desc = irq_to_desc(irq); /* 將中斷號轉化為irq_desc數組項 */ -> generic_handle_irq_desc(irq, desc); -> desc->handle_irq(irq, desc); /* 最終調用執行初始化階段注冊的通用函數 */ -> handle_level_irq(unsigned int irq, struct irq_desc *desc); -> handle_irq_event(desc); -> handle_irq_event_percpu(desc, action); -> action->handler(irq, action->dev_id) /* 我們需要實現的驅動函數 */ -> action = action->next; /* 共享中斷要執行相同中斷號的cation鏈表的所有中斷 */
asm_do_IRQ()函數除此之外,還會清中斷,因此我們在中斷處理函數中不需要自己清中斷
層次結構中的irq_desc有以下幾個我們需要關注的成員:
struct irq_desc { struct irq_data irq_data; /* 每個irq和芯片數據傳遞給芯片功能 */ ... irq_flow_handler_t handle_irq; /* 通用中斷處理函數 */ ... struct irqaction *action; /* IRQ action鏈表 */ ... }
代碼中的struct irqaction中含有真正的中斷處理函數:
struct irqaction { irq_handler_t handler; /* 中斷處理函數 */ unsigned long flags; /* 標志 */ void *dev_id; /* 中斷函數傳入數據 */ ... int irq; /* 中斷號 */ ... }
代碼中的中斷函數格式和中斷標志位如下:
typedef irqreturn_t (*irq_handler_t)(int, void *); ... #define IRQF_TRIGGER_NONE 0x00000000 #define IRQF_TRIGGER_RISING 0x00000001 /* 上升沿觸發 */ #define IRQF_TRIGGER_FALLING 0x00000002 /* 下降沿觸發 */ #define IRQF_TRIGGER_HIGH 0x00000004 /* 高電平觸發 */ #define IRQF_TRIGGER_LOW 0x00000008 /* 低電平觸發 */ #define IRQF_TRIGGER_MASK (IRQF_TRIGGER_HIGH | IRQF_TRIGGER_LOW | \ IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING) #define IRQF_TRIGGER_PROBE 0x00000010
因此我們需要做的就是向上注冊中斷數據和處理函數
在內核中,注冊中斷函數原型為:
int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev)
函數所做的事情有:
1. 分配、設置、注冊irqaction
2. 設置中斷引腳
3. 使能中斷
釋放申請的中斷函數原型為:
void free_irq(unsigned int, void *);
二、等待隊列
在中斷編程中,中斷通常會和等待隊列一起使用。等待隊列的作用是防止驅動中斷讀或寫過程浪費CPU利用率。
進程通過執行下面步驟將自己加入到一個等待隊列中
1. 定義等待隊列頭部,如wait_queue_head_t my_queue;
2. 調用init_wait_queue_head()初始化等待隊列頭部
3. 調用DECLARE_WAITQUEUE()創建一個等待隊列的項
4. 調用add_wait_queue()把自己加入到等待隊列中,該隊列會在進程等待的條件滿足時喚醒它。在其他地方寫相關代碼,在事件發生時,對等的隊列執行wake_up()操作
5. 調用set_current_state()將進程狀態變更為TASK_INTERRUPTIBLE或TASK_UNINTERRUPTIBLE
6. 如果狀態被置為TASK_INTERRUPTIBLE,則使用中斷喚醒進程
7. 檢查condition是否為真,為真則不休眠,如果為假,則調用scheduled()休眠
8. 當進程被喚醒的時候,它會再次檢查條件是否為真。真就退出循環,否則再次調用scheduled()並一直重復這步操作
9. condition滿足后,進程設置為TASK_RUNNING並通過remove_wait_queue()退出
需要注意的是,如果驅動程序中的read()和write()函數都實現了休眠功能,那么我們需要在read()被喚醒時調用wake_up(wait_queue_head_t *)系列函數喚醒write()函數
這樣是為了防止寫進程和讀進程相互阻塞
wake_up()系列函數聲明有:
/* 喚醒隊列 */ wake_up(queue, condition); // condition為0時解除休眠 wake_up_interruptible(queue, condition); /* 等待事件 */ wait_event(queue, condition); // condition為volatile變量,為1時休眠 wait_event_interruptible(queue, condition); wait_event_timeout(queue, condition); wait_event_interruptible_timeout(queue, condition);
代碼中wait_event()和wait_event_interrupt()的區別是wait_event_interrupt()設置了TASK_INTERRUPTIBLE標記,使得進程處於可中斷(TASK_INTERRUPTIBLE)狀態,從而睡眠進程可以通過接收信號被喚醒
本節代碼中暫時使用不到wait_event()和wait_event_interrupt()函數
wait_event()和wait_event_interrupt()的差別:
wait_event()不能被Ctrl + C和kill -9命令打斷,而wait_event_interrupt()都可以被打斷
示例代碼如下:
1 static ssize_t gm_read(struct file *filp, char __user *buf, size_t len, loff_t * loff) 2 { 3 struct gm_dev *dev = filp->private_data; 4 int count = len; // 考慮邊界條件 5 int ret; 6 7 DECLARE_WAITQUEUE(wait, current); 8 mutex_lock(&dev->lock); 9 add_wait_queue(&dev->r_head, &wait); 10 11 while (dev->current_len == 0) { 12 if (filp->f_flags & O_NONBLOCK) { 13 ret = -EAGAIN; 14 goto out; 15 } 16 17 __set_current_state(TASK_INTERRUPTIBLE); 18 mutex_unlock(&dev->lock); 19 schedule(); 20 21 if (signal_pending(current)) { 22 ret = -ERESTARTSYS; 23 goto out2; 24 } 25 mutex_lock(&dev->lock); 26 } 27 28 if (count > dev->current_len) 29 count = dev->current_len; 30 31 if (copy_to_user(buf, dev->mem, count)) { 32 ret = -EFAULT; 33 goto out; 34 } 35 else { 36 // 把后面的數據放到前面 37 memcpy(dev->mem, dev->mem + count, dev->current_len - count); 38 dev->current_len -= count; 39 ret = count; 40 printk("## GM ## READ %d\n", count); 41 42 // 喚醒可能阻塞的寫進程,注意是寫進程 43 wake_up_interruptible(&dev->w_head); 44 } 45 46 out: 47 mutex_unlock(&dev->lock); 48 49 out2: 50 remove_wait_queue(&dev->r_head, &wait); 51 set_current_state(TASK_RUNNING); 52 53 return ret; 54 }
有了上面的基礎,現在我們可以實現按鍵中斷字符驅動程序
三、按鍵中斷字符驅動程序
key源代碼:
1 #include <linux/module.h> 2 #include <linux/fs.h> 3 #include <linux/init.h> 4 #include <linux/cdev.h> 5 #include <linux/slab.h> 6 #include <linux/device.h> 7 #include <linux/irq.h> 8 #include <linux/interrupt.h> 9 #include <linux/wait.h> 10 #include <linux/timer.h> 11 #include <linux/gpio.h> 12 #include <linux/sched.h> 13 14 #include <asm/uaccess.h> 15 #include <asm/irq.h> 16 #include <asm/io.h> 17 18 #include <mach/gpio.h> 19 20 #define KEY_MAJOR 255 21 22 struct pin_desc { 23 int gpio; 24 int val; 25 char *name; 26 }; 27 28 struct key_device { 29 struct cdev cdev; 30 wait_queue_head_t r_head; 31 wait_queue_head_t w_head; 32 }; 33 34 static struct pin_desc desc[4] = { 35 { EXYNOS4_GPX3(2), 0x01, "KEY0" }, 36 { EXYNOS4_GPX3(3), 0x02, "KEY1" }, 37 { EXYNOS4_GPX3(4), 0x03, "KEY2" }, 38 { EXYNOS4_GPX3(5), 0x04, "KEY3" }, 39 }; 40 41 static int g_major = KEY_MAJOR; 42 module_param(g_major, int, S_IRUGO); 43 44 static struct key_device* dev; 45 static struct class* scls; 46 static struct device* sdev; 47 static unsigned char key_val; 48 49 static irqreturn_t key_interrupt(int irq, void *dev_id) 50 { 51 struct pin_desc *pindesc = (struct pin_desc *)dev_id; 52 unsigned int tmp; 53 54 tmp = gpio_get_value(pindesc->gpio); 55 56 /* active low */ 57 printk(KERN_DEBUG "KEY %d: %08x\n", pindesc->val, tmp); 58 59 if (tmp) 60 key_val = pindesc->val; 61 else 62 key_val = pindesc->val | 0x80; 63 64 set_current_state(TASK_RUNNING); 65 66 return IRQ_HANDLED; 67 } 68 69 static ssize_t key_read(struct file *filp, char __user *buf, size_t len, loff_t * loff) 70 { 71 struct key_device *dev = filp->private_data; 72 73 // 聲明等待隊列 74 DECLARE_WAITQUEUE(rwait, current); 75 add_wait_queue(&dev->r_head, &rwait); 76 77 // 休眠 78 __set_current_state(TASK_INTERRUPTIBLE); 79 schedule(); 80 81 // 有數據 82 copy_to_user(buf, &key_val, 1); 83 84 remove_wait_queue(&dev->r_head, &rwait); 85 set_current_state(TASK_RUNNING); 86 87 return 1; 88 } 89 90 static int key_open(struct inode *nodep, struct file *filp) 91 { 92 struct key_device *dev = container_of(nodep->i_cdev, struct key_device, cdev); 93 // 放入私有數據中 94 filp->private_data = dev; 95 96 int irq; 97 int i, err = 0; 98 99 for (i = 0; i < ARRAY_SIZE(desc); i++) { 100 if (!desc[i].gpio) 101 continue; 102 103 irq = gpio_to_irq(desc[i].gpio); 104 err = request_irq(irq, key_interrupt, IRQ_TYPE_EDGE_BOTH, 105 desc[i].name, (void *)&desc[i]); 106 if (err) 107 break; 108 } 109 110 if (err) { 111 i--; 112 for (; i >= 0; i--) { 113 if (!desc[i].gpio) 114 continue; 115 116 irq = gpio_to_irq(desc[i].gpio); 117 free_irq(irq, (void *)&desc[i]); 118 } 119 return -EBUSY; 120 } 121 122 init_waitqueue_head(&dev->r_head); 123 124 return 0; 125 } 126 127 static int key_release(struct inode *nodep, struct file *filp) 128 { 129 // 釋放中斷 130 int irq, i; 131 132 for (i = 0; i < ARRAY_SIZE(desc); i++) { 133 if (!desc[i].gpio) 134 continue; 135 136 irq = gpio_to_irq(desc[i].gpio); 137 free_irq(irq, (void *)&desc[i]); 138 } 139 140 return 0; 141 } 142 143 static struct file_operations key_fops = { 144 .owner = THIS_MODULE, 145 .read = key_read, 146 .open = key_open, 147 .release = key_release, 148 }; 149 150 static int keys_init(void) 151 { 152 int ret; 153 int devt; 154 if (g_major) { 155 devt = MKDEV(g_major, 0); 156 ret = register_chrdev_region(devt, 1, "key"); 157 } 158 else { 159 ret = alloc_chrdev_region(&devt, 0, 1, "key"); 160 g_major = MAJOR(devt); 161 } 162 163 if (ret) 164 return ret; 165 166 dev = kzalloc(sizeof(struct key_device), GFP_KERNEL); 167 if (!dev) { 168 ret = -ENOMEM; 169 goto fail_alloc; 170 } 171 172 cdev_init(&dev->cdev, &key_fops); 173 ret = cdev_add(&dev->cdev, devt, 1); 174 if (ret) 175 return ret; 176 177 scls = class_create(THIS_MODULE, "key"); 178 sdev = device_create(scls, NULL, devt, NULL, "key"); 179 180 return 0; 181 182 fail_alloc: 183 unregister_chrdev_region(devt, 1); 184 185 return ret; 186 } 187 188 static void keys_exit(void) 189 { 190 dev_t devt = MKDEV(g_major, 0); 191 192 device_destroy(scls, devt); 193 class_destroy(scls); 194 195 cdev_del(&(dev->cdev)); 196 kfree(dev); 197 198 unregister_chrdev_region(devt, 1); 199 } 200 201 module_init(keys_init); 202 module_exit(keys_exit); 203 204 MODULE_LICENSE("GPL");
Makefile:
1 KERN_DIR = /work/tiny4412/tools/linux-3.5 2 3 all: 4 make -C $(KERN_DIR) M=`pwd` modules 5 6 clean: 7 make -C $(KERN_DIR) M=`pwd` modules clean 8 rm -rf modules.order 9 10 obj-m += key.o
測試文件:
1 #include <stdio.h> 2 #include <unistd.h> 3 #include <sys/types.h> 4 #include <sys/stat.h> 5 #include <fcntl.h> 6 #include <string.h> 7 8 int main(int argc, char** argv) 9 { 10 int fd; 11 fd = open("/dev/key", O_RDWR); 12 if (fd < 0) { 13 printf("can't open /dev/key\n"); 14 return -1; 15 } 16 17 unsigned char key_val; 18 19 while (1) { 20 read(fd, &key_val, 1); 21 printf("key_val = 0x%x\n", key_val); 22 } 23 24 close(fd); 25 26 return 0; 27 }
四、中斷的底半部機制
中斷的頂半部(上半部)和底半部(下半部):

如果中斷中需要完成大量代碼,可采用上圖中的分層結構。
頂半部用於完成少量緊急功能,它往往只是簡單的讀取寄存器,然后將底半部掛到執行隊列中去。從而可以服務更多的中斷請求
關於頂半部與底半部的差別:
1. 頂半部代碼短小,底半部代碼較大
2. 頂半部一般不可中斷,底半部可以中斷
Linux實現底半部的機制主要有tasklet、工作隊列、軟中斷和線程化irq,本章只介紹前兩種。
1. tasklet
tasklet的執行上下文是軟中斷,屬於原子上下文操作,不允許休眠,執行時機通常是頂半部返回的時候。表示tasklet的結構體為struct tasklet_struct。
使用模板如下:
1 /* 定義key_tasklet, 底部函數key_tasklet_func,綁定 */ 2 static void key_tasklet_func(unsigned long arg); 3 DECLARE_TASKLET(key_tasklet, key_tasklet_func, 0); 4 5 static void key_tasklet_func(unsigned long arg) 6 { 7 // 底半部代碼 8 } 9 10 static irqreturn_t key_interrupt(int irq, void *dev_id) 11 { 12 /* 頂半部代碼,如示例源碼中的代碼 13 * ... 14 */ 15 16 tasklet_schedule(&key_tasklet); 17 return IRQ_HANDLED; 18 }
2. 工作隊列
工作隊列的執行上下文是內核線程,可以調度和休眠。表示工作隊列的結構體為struct work_struct。
使用模板如下:
1 #include <linux/workqueue.h> 2 3 /* 定義key_tasklet, 底部函數key_tasklet_func,綁定 */ 4 static void key_wq_func(unsigned long arg); 5 struct work_struct key_workqueue; 6 7 static void key_wq_func(unsigned long arg) 8 { 9 // 底半部代碼 10 } 11 12 static irqreturn_t key_interrupt(int irq, void *dev_id) 13 { 14 /* 頂半部代碼,如示例源碼中的代碼 15 * ... 16 */ 17 18 schedule_work(&key_workqueue); 19 return IRQ_HANDLED; 20 } 21 22 static int keys_init(void) 23 { 24 /* 其余初始化代碼 */ 25 26 // 初始化工作隊列 27 INIT_WORK(&key_workqueue, key_wq_func); 28 } 29 30 static void keys_exit(void) 31 { 32 // 注銷工作隊列 33 cancel_work_sync(&key_workqueue); 34 }
五、中斷共享
中斷共享就是多個設備共享一根硬件中斷線的情況。在裸機編程中可以使用寄存器SUBSRCPND和EINTPEND做進一步中斷判斷,這里使用的就是共享中斷。
共享中斷的使用方式如下:
1. 共享中斷的多個設備在申請中斷時,都應該使用IRQF_SHARED標識,如request_irq(..., IRQF_SHAREAD | IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING, ...)
2. 中斷到來時,系統會遍歷此中斷的所有中斷處理程序,直到某個函數返回IRQ_HANDLED。因此我們在中斷函數中需要判斷此中斷是否屬於本設備中斷,若不是,應直接返回IRQ_NONE。
使用模板如下:
1 static irqreturn_t key_interrupt(int irq, void *dev_id) 2 { 3 /* 1. 讀寄存器值或dev_id數據判斷是否為本設備中斷 */ 4 ; 5 /* 2. 如果不是 */ 6 if ( ) 7 return IRQ_NONE; 8 9 /* 3. 如果是,處理 */ 10 11 return IRQ_HANDLED; 12 }
