Linux中斷處理驅動程序編寫

本章節我們一起來探討一下Linux中的中斷





中斷與定時器:
中斷的概念:指CPU在執行過程中,出現某些突發事件急待處理,CPU暫停執行當前程序,轉去處理突發事件
,處理完後CPU又返回原程序被中斷的位置繼續執行
中斷的分類:內部中斷和外部中斷
內部中斷:中斷源來自CPU內部(軟件中斷指令、溢出、觸發錯誤等)
外部中斷:中斷源來自CPU外部,由外設提出請求

屏蔽中斷和不可屏蔽中斷:
可屏蔽中斷:可以通過屏蔽字被屏蔽,屏蔽後,該中斷不再得到響應
不可平布中斷:不能被屏蔽

向量中斷和非向量中斷:
向量中斷:CPU通常爲不同的中斷分配不同的中斷號,當檢測到某中斷號的中斷到來後,就自動跳轉到與該中斷號對應的地址執行
非向量中斷:多箇中斷共享一個入口地址。進入該入口地址後再通過軟件判斷中斷標誌來識別具體哪個是中斷
也就是說向量中斷由軟件提供中斷服務程序入口地址,非向量中斷由軟件提供中斷入口地址


/*典型的非向量中斷首先會判斷中斷源,然後調用不同中斷源的中斷處理程序*/
irq_handler()
{
...
int int_src = read_int_status();/*讀硬件的中斷相關寄存器*/
switch(int_src){//判斷中斷標誌
case DEV_A:
dev_a_handler();
break;
case DEV_B:
dev_b_handler();
break;
...
default:
break;
}
...
}





定時器中斷原理:
定時器在硬件上也以來中斷,PIT(可編程間隔定時器)接收一個時鐘輸入,
當時鍾脈衝到來時,將目前計數值增1並與已經設置的計數值比較,若相等,證明計數週期滿,產生定時器中斷,並
復位計數值。

如下圖所示:
















    Linux中斷處理程序架構:
Linux將中斷分爲:頂半部(top half)和底半部(bottom half)
頂板部:完成儘可能少的比較緊急的功能,它往往只是簡單的讀取寄存器中的中斷狀態並清除中斷標誌後就進行
“登記中斷”(也就是將底半部處理程序掛在到設備的底半部執行隊列中)的工作
特點:響應速度快

底半部:中斷處理的大部分工作都在底半部,它幾乎做了中斷處理程序的所有事情。
特點:處理相對來說不是非常緊急的事件


小知識:Linux中查看/proc/interrupts文件可以獲得系統中斷的統計信息。

如下圖所示:


第一列是中斷號       第二列是向CPU產生該中斷的次數









介紹完相關基礎概念後,讓我們一起來探討一下Linux中斷編程


Linux中斷編程:
1.申請和釋放中斷
申請中斷:
int request_irq(unsigned int irq,irq_handler_t handler,
unsigned long irqflags,const char *devname,void *dev_id)
參數介紹:irq是要申請的硬件中斷號
handler是向系統登記的中斷處理程序(頂半部),是一個回調函數,中斷髮生時,系統調用它,將
dev_id參數傳遞給它
irqflags:是中斷處理的屬性,可以指定中斷的觸發方式和處理方式:
觸發方式:IRQF_TRIGGER_RISING、IRQF_TRIGGER_FALLING、IRQF_TRIGGER_HIGH、IRQF_TRIGGER_LOW
處理方式:IRQF_DISABLE表明中斷處理程序是快速處理程序,快速處理程序被調用時屏蔽所有中斷
IRQF_SHARED表示多個設備共享中斷,dev_id在中斷共享時會用到,一般設置爲NULL


返回值:爲0表示成功,返回-EINVAL表示中斷號無效,返回-EBUSY表示中斷已經被佔用,且不能共享
頂半部的handler的類型irq_handler_t定義爲
typedef irqreturn_t (*irq_handler_t)(int,void*);
typedef int irqreturn_t;



2.釋放IRQ
有請求當然就有釋放了
void free_irq(unsigned int irq,void *dev_id);
參數定義與request_irq類似



3.使能和屏蔽中斷
void disable_irq(int irq);//等待目前中斷處理完成(最好別在頂板部使用,你懂得)
void disable_irq_nosync(int irq);//立即返回
void enable_irq(int irq);//


4.屏蔽本CPU內所有中斷:
#define local_irq_save(flags)...//禁止中斷並保存狀態
void local_irq_disable(void);//禁止中斷,不保存狀態






下面來分別介紹一下頂半部和底半部的實現機制


底半部機制:
簡介:底半部機制主要有tasklet、工作隊列和軟中斷
1.底半部是想方法之一tasklet
(1)我們需要定義tasklet機器處理器並將兩者關聯
例如:
void my_tasklet_func(unsigned long);/*定義一個處理函數*/
DECLARE_TASKLET(my_tasklet,my_tasklet_func,data);
/*上述代碼定義了名爲my_tasklet的tasklet並將其餘
my_tasklet_func()函數綁定,傳入的參數爲data*/
(2)調度
tasklet_schedule(&my_tasklet);
//使用此函數就能在是當的時候進行調度運行



tasklet使用模板:
/*定義tasklet和底半部函數並關聯*/
void xxx_do_tasklet(unsigned long);
DECLARE_TASKLET(xxx_tasklet,xxx_do_tasklet,0);

/*中斷處理底半部*/
void xxx_do_tasklet(unsigned long)
{
...
}

/*中斷處理頂半部*/
irqreturn_t xxx_interrupt(int irq,void *dev_id)
{
...
tasklet_schedule(&xxx_tasklet);//調度地板部
...
}


/*設備驅動模塊加載函數*/
int __init xxx_init(void)
{
...
/*申請中斷*/
result = request_irq(xxx_irq,xxx_interrupt,
IRQF_DISABLED,"xxx",NULL);
...

return IRQ_HANDLED;
}


/*設備驅動模塊卸載函數*/
void __exit xxx_exit(void)
{
...
/*釋放中斷*/
free_irq(xxx_irq,xxx_interrupt);
...
}


2.底半部實現方法之二---工作隊列
使用方法和tasklet類似
相關操作:
struct work_struct my_wq;/*定義一個工作隊列*/
void my_wq_func(unsigned long);/*定義一個處理函數*/
通過INIT_WORK()可以初始化這個工作隊列並將工作隊列與處理函數綁定
INIT_WORK(&my_wq,(void (*)(void *))my_wq_func,NULL);
/*初始化工作隊列並將其與處理函數綁定*/
schedule_work(&my_wq);/*調度工作隊列執行*/


/*工作隊列使用模板*/

/*定義工作隊列和關聯函數*/
struct work_struct(unsigned long);
void xxx_do_work(unsigned long);


/*中斷處理底半部*/
void xxx_do_work(unsigned long)
{
...
}


/*中斷處理頂半部*/
/*中斷處理頂半部*/
irqreturn_t xxx_interrupt(int irq,void *dev_id)
{
...
schedule_work(&my_wq);//調度底半部
...
return IRQ_HANDLED;
}


/*設備驅動模塊加載函數*/
int xxx_init(void)
{
...
/*申請中斷*/
result = request_irq(xxx_irq,xxx_interrupt,
IRQF_DISABLED,"xxx",NULL);
...
/*初始化工作隊列*/
INIT_WORK(&my_wq,(void (*)(void *))xxx_do_work,NULL);
}



/*設備驅動模塊卸載函數*/
void xxx_exit(void)
{
...
/*釋放中斷*/
free_irq(xxx_irq,xxx_interrupt);
...
}







中斷共享
中斷共享是指多個設備共享一根中斷線的情況

中斷共享的使用方法:
(1).在申請中斷時,使用IRQF_SHARED標識
(2).在中斷到來時,會遍歷共享此中斷的所有中斷處理程序,直到某一個函數返回
IRQ_HANDLED,在中斷處理程序頂半部中,應迅速根據硬件寄存器中的信息參照dev_id參數
判斷是否爲本設備的中斷,若不是立即返回IR1_NONE



/*共享中斷編程模板*/
irqreturn_t xxx_interrupt(int irq,void *dev_id,struct pt_regs *regs)
{
...
int status = read_int_status();/*獲知中斷源*/
if(!is_myint(dev_id,status))/*判斷是否爲本設備中斷*/
return IRQ_NONE;/*不是本設備中斷,立即返回*/

/*是本設備中斷,進行處理*/
...
return IRQ_HANDLED;/*返回IRQ_HANDLER表明中斷已經被處理*/

}


/*設備模塊加載函數*/
int xxx_init(void)
{
...
/*申請共享中斷*/
result = request_irq(sh_irq,xxx_interrupt,
IRQF_SHARE,"xxx",xxx_dev);
...
}


/*設備驅動模塊卸載函數*/
void xxx_exit()
{
...
/*釋放中斷*/
free_irq(xxx_irq,xxx_interrupt);
...
}






內核定時器
內核定時器編程:
簡介:軟件意義上的定時器最終是依賴於硬件定時器實現的,內核在時鐘中斷髮生後檢測各
定時器是否到期,到期後定時器處理函數作爲軟中斷在底半部執行。

Linux內核定時器操作:
1.timer_list結構體
每一個timer_list對應一個定時器
struct timer_list{
struct list_head entry;/*定時器列表*/
unsigned long expires;/*定時器到期時間*/
void (*function)(unsigned long);/*定時器處理函數*/
unsigned long data;/*作爲參數被傳遞給定時器處理函數*/
struct timer_base_s *base;
...
};
當定時器滿的時候,定時器處理函數將被執行



2.初始化定時器
void init_timer(struct timer_list * timer);
//初始化timer_list的entry的next爲NULL,並給base指針賦值。
TIMER_INITIALIZER(_function,_expires,_data);//此宏用來
//賦值定時器結構體的function、expires
、data和base成員
#define TIMER_INITIALIZER(function,_expires,_data){
.entry = {.prev = TIMER_ENTRY_STATIC},\
.function= (_function), \
.expires = (_expire), \
.data = (_data),  \
.base = &boot_tvec_bases,\
}

DEFINE_TIMER(_name,_function,_expires,_data)//定義一個定時器結構體變量
//併爲此變量取名_name

//還有一個setup_timer()函數也可以用於定時器結構體的初始化,此函數大家自己去網上查吧



3.增加定時器
void add_timer(struct timer_list * timer);
//註冊內核定時器,也就是將定時器加入到內核動態定時器鏈表當中

4.刪除定時器
del_timer(struct timer_list *timer);
del_timer_sync()//在刪除一個定時器時等待刪除操作被處理完(不能用於中斷上下文中)


5.修改定時器expires
int mod_timer(struct timer_list * timer,unsigned long expires);
//修改定時器的到期時間




/*內核定時器使用模板*/

/*xxx設備結構體*/
struct xxx_dev{
struct cdev cdev;
...
timer_list xxx_timer;/*設備要使用的定時器*/
};



/*xxx驅動中的某函數*/
xxx_funcl(...)
{
struct xxx_dev *dev = filp->private_data;
...
/*初始化定時器*/
init_timer(&dev->xxx_timer);
dev->xxx_timer.function = &xxx_do_timer;
dev->xxx_timer.data = (unsigned long)dev;
/*設備結構體指針作爲定時器處理函數參數*/
   
dev->xxx_timer.expires = jiffes + delays;
/*添加(註冊)定時器*/
add_timer(&dev->xxx_timer);
...
}

/*xxx驅動中的某函數*/
xxx_func2(...)
{
...
/*刪除定時器*/
del_timer(&dev->xxx_timer);
...
}


/*定時器處理函數*/
static void xxx_do_timer(unsigned long arg)
{
struct xxx_device *dev = (struct xxx_device *)(arg);
...
/*調度定時器再執行*/
dev->xxx_timer.expires = jiffes + delay;
add_timer(&dev -> xxx_timer);
...
}

//定時器到期時間往往是在jiffies的基礎上添加一個時延,若爲HZ
則表示延遲一秒





內核中的延遲工作:
簡介:對於這種週期性的工作,Linux提供了一套封裝好的快捷機制,
本質上利用工作隊列和定時器實現


這其中用到兩個結構體:
(1)struct delayed_work{
struct work_struct work;
struct timer_list timer;
};

(2)struct work_struct{
atomic_long_t data;
...
}


相關操作:
int schedule_delay_work(struct delayed_work *work,unsigned long delay);
//當指定的delay到來時delay_work中的work成員的work_func_t類型成員func()會被執行
work_func_t類型定義如下:
typedef void (*work_func_t)(struct work_struct *work);
//delay參數的單位是jiffes

mescs_to_jiffies(unsigned long mesc);//將毫秒轉化成jiffes單位



int cancel_delayed_work(struct delayed_work *work);
int cancel_delayed_work_sync(struct delayed_work *work);//等待直到刪除(不能用於中斷上下文)






內核延遲:
短延遲:
Linux內核提供瞭如下三個函數分別進行納秒、微妙和毫秒延遲:
void ndelay(unsigned long nsecs);
void udelay(unsigned long usecs);
void mdelay(unsigned long msecs);

機制:根據CPU頻率進行一定次數的循環(忙等待)



注意:在Linux內核中最好不要使用毫秒級的延時,因爲這樣會無謂消耗CPU的資源
對於毫秒以上的延時,Linux提供如下函數
void msleep(unsigned int millisecs);
unsigned long msleep_interruptible(unsigned int millisecs);//可以被打斷
void ssleep(unsigned int seconds);
//上述函數使得調用它的進程睡眠指定的時間



長延遲:
機制:設置當前jiffies加上時間間隔的jiffies,直到未來的jiffies達到目標jiffires


/*實例:先延遲100個jiffies再延遲2s*/
unsigned long delay = jiffies + 100;
while(time_before(jiffies,delay));

/*再延遲2s*/
unsigned long delay =  jiffies + 2*Hz;
while(time_before(jiffies,delay));//循環直到到達指定的時間


與timer_before()相對應的還有一個time_after




睡着延遲:
睡着延遲是比忙等待更好的一種方法
機制:在等待的時間到來之前進程處於睡眠狀態,CPU資源被其他進程使用
實現函數有:
schedule_timeout()
schedule_timeout_uninterruptible()
其實在短延遲中的msleep() msleep_interruptible()
本質上都是依賴於此函數實現的


下面兩個函數可以讓當前進程加入到等待隊列中,從而在等待隊列上睡眠,當超時
發生時,進程被喚醒
sleep_on_timeout(wait_queue_head_t *q,unsigned long timeout);

interruptible_sleep_on_timeout(wait_queue_head_t *q,unsigned long timeout);






在文章的最後附上我修改過的秒字符設備驅動測試程序(可以在ok6410上運行)

/*======================================================================
    A "seond" device driver as an example of kernel timer
    
    The initial developer of the original code is Baohua Song
    <[email protected]>. All Rights Reserved.
======================================================================*/
#include <linux/module.h>
#include <linux/types.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/mm.h>
#include <linux/sched.h>
#include <linux/init.h>
#include <linux/cdev.h>
#include <asm/io.h>
#include <asm/system.h>
#include <asm/uaccess.h>
#include <linux/timer.h> /*包括timer.h頭文件*/
#include <asm/atomic.h> 


#define SECOND_MAJOR 240    /*預設的second的主設備號*/


static int second_major = SECOND_MAJOR;


/*second設備結構體*/
struct second_dev
{
  struct cdev cdev; /*cdev結構體*/
  atomic_t counter;/* 一共經歷了多少秒?(定義爲原子量)*/
  struct timer_list s_timer; /*設備要使用的定時器*/
};








struct second_dev *second_devp; /*設備結構體指針*/


/*定時器處理函數*/
static void second_timer_handle(unsigned long arg)
{
  mod_timer(&second_devp->s_timer,jiffies + HZ);//定義定時器到期時間爲1秒後
  atomic_inc(&second_devp->counter);
  
  printk(KERN_NOTICE "current jiffies is %ld\n", jiffies);
}


/*文件打開函數*/
int second_open(struct inode *inode, struct file *filp)
{
  /*初始化定時器*/
  init_timer(&second_devp->s_timer);
  second_devp->s_timer.function = &second_timer_handle;
  second_devp->s_timer.expires = jiffies + HZ;
  
  add_timer(&second_devp->s_timer); /*添加(註冊)定時器*/
  
  atomic_set(&second_devp->counter,0); //計數清0(原子操作之設置原子量counter爲0)


  return 0;
}
/*文件釋放函數*/
int second_release(struct inode *inode, struct file *filp)
{
  del_timer(&second_devp->s_timer);//刪除定時器
  
  return 0;
}


/*globalfifo讀函數*/
static ssize_t second_read(struct file *filp, char __user *buf, size_t count,
  loff_t *ppos)
{  
  int counter;
  
  counter = atomic_read(&second_devp->counter);//讀取原子量counter的整數值
  if(put_user(counter, (int*)buf))//將counter寫入用戶空間
  return - EFAULT;
  else
  return sizeof(unsigned int);  
}


/*文件操作結構體*/
static const struct file_operations second_fops =
{
  .owner = THIS_MODULE, 
  .open = second_open, 
  .release = second_release,
  .read = second_read,
};






/*初始化並註冊cdev*/
static void second_setup_cdev(struct second_dev *dev, int index)
{
  int err, devno = MKDEV(second_major, index);//組合設備號


  cdev_init(&dev->cdev, &second_fops);//初始化設備結構體
  dev->cdev.owner = THIS_MODULE;
  dev->cdev.ops = &second_fops;
  err = cdev_add(&dev->cdev, devno, 1);//爲設備結構體關聯設備號
  if (err)
    printk(KERN_NOTICE "Error %d adding LED%d", err, index);
}


/*設備驅動模塊加載函數*/
int second_init(void)
{
  int ret;
  dev_t devno = MKDEV(second_major, 0);


  /* 申請設備號*/
  if (second_major)
    ret = register_chrdev_region(devno, 1, "second");
  else  /* 動態申請設備號 */
  {
    ret = alloc_chrdev_region(&devno, 0, 1, "second");
    second_major = MAJOR(devno);
  }
  if (ret < 0)
    return ret;
  /* 動態申請設備結構體的內存*/
  second_devp = kmalloc(sizeof(struct second_dev), GFP_KERNEL);
  if (!second_devp)    /*申請失敗*/
  {
    ret =  - ENOMEM;
    goto fail_malloc;
  }
  
  //清空設備結構
  memset(second_devp, 0, sizeof(struct second_dev));


  //轉載設備
  second_setup_cdev(second_devp, 0);


  return 0;


  fail_malloc: unregister_chrdev_region(devno, 1);
}


/*模塊卸載函數*/
void second_exit(void)
{
  cdev_del(&second_devp->cdev);   /*註銷cdev*/
  kfree(second_devp);     /*釋放設備結構體內存*/
  unregister_chrdev_region(MKDEV(second_major, 0), 1); /*釋放設備號*/
}


MODULE_AUTHOR("Sola");
MODULE_LICENSE("Dual BSD/GPL");


module_param(second_major, int, S_IRUGO);


module_init(second_init);
module_exit(second_exit);


測試程序:

/*======================================================================
    A test program to access /dev/second
    This example is to help understand kernel timer 
    
    The initial developer of the original code is Baohua Song
    <[email protected]>. All Rights Reserved.
======================================================================*/
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/time.h>


main()
{
  int fd;
  int counter = 0;
  int old_counter = 0;
  
  /*打開/dev/second設備文件*/
  fd = open("/dev/second", O_RDONLY);
  if (fd !=  - 1)
  {
    while (1)
    {
      read(fd,&counter, sizeof(unsigned int));//讀目前經歷的秒數
      if(counter!=old_counter)
      {
      printf("seconds after open /dev/second :%d\n",counter);
      old_counter = counter;
      }
    }    
  }
  else
  {
    printf("Device open failure\n");
  }
}


測試結果:



發佈了2 篇原創文章 · 獲贊 4 · 訪問量 10萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章