|
Linux內核開發(fā)之中斷與時鐘(一) “小王,醒醒,開始上課了,今天咱們開始講中斷,這可是高級東西,錯過不補哈”我使勁推著睡夢中的小王。 “嗯?感情好啊,快點,快點”小王一聽有新東西講,像打了雞血似的興奮,連我都懷疑起她是不是性格中喜新厭舊。 不管那么多了,我講我的,她厭她的… 啥叫中斷?就是指cpu在執(zhí)行過程中,出現了某些突發(fā)事件時CPU必須暫停執(zhí)行當前的程序,轉去處理突發(fā)事件,處理完畢后CPU有返回原程序被中斷的位置并繼續(xù)執(zhí)行。 中斷的分法不懂,分類就不同,向什么內外部中斷,可/不可屏蔽中斷…等等亂七八糟一大堆,我這里要說明的一點是按照中斷入口跳轉方法的不同,可分為向量中 斷和非向量中斷。采用向量中斷的CPU通常為不同的中斷分配不同的中斷號,當檢測到某中斷號的中斷到來后,就自動跳轉到與該中斷號對應的地址執(zhí)行。不同的 中斷號有不同的中斷地址(即入口)。而非向量中斷的多個中斷共享一個入口地址。進入后根據軟件判斷中斷標志來識別具體是哪個中斷。也就是說,向量中斷是由 硬件提供中斷服務程序入口地址,非向量中斷由軟件提供中斷服務程序入口地址。 我們在后邊會說到一個時鐘定時器,它也是通過中斷來實現的。它的原理很簡單,嵌入式微處理器它接入一個時鐘輸入,當時鐘脈沖到來時,就將目前的計數器值加1并和預先設置的計數值比較,若相等,證明計數周期滿,產生定時器中斷并復位目前計數器值。
Linux中斷處理架構 設 備的中斷會打斷內核中進程的正常調度和運行,會影響系統(tǒng)的性能。為了在中斷執(zhí)行時間盡可能短和中斷處理需完成大量工作之間找到一個平衡點,Linux將中 斷處理程序分解成兩個半部:頂半部和底半部。其中頂半部盡可能完成盡可能少的比較緊急的功能。而底半部幾乎做了中斷處理程序所有的事情,而且可以被新的中 斷打斷。 在linux設備驅動中,提供了一系列函數來幫助設備實現中斷的相關操作: 1)設備申請中斷 int request_irq(unsigned int irq, //irq是要申請的中斷號 void (*handler)(int irq, void *dev_id, struct pt_regs * *regs),//回調函數,中斷發(fā)生時,系統(tǒng)會調用該函數, unsigned long irqflags, const char *devname, void *dev_id); 其中irqflags是中斷處理的屬性,若設置為SA_INTERRUPT,則表示中斷處理程序是快速處理程序,它被調用時屏蔽所有中斷。若設置為SA_SHIRQ,則表示多個設備共享中斷,dev_id在中斷共享時會用到,一般設置為這個設備的設備結構體或者NULL. 該函數返回0表示成功,返回-INVAL表示中斷號無效或處理函數指針為NULL,返回EBUSY表示中斷已經被占用且不能共享。 2)釋放中斷 free_irq(unsigned int irq, void *dev_id); 3)使能和屏蔽中斷 void disable_irq(int irq); //這個會立即返回 void disable_irq_nosync(int irq);//等待目前的中斷處理完成再返回。 void enable_irq(int irq); 上述三個函數作用于可編程中斷處理器,因此對系統(tǒng)內所有的CPU都生效。 void local_irq_save(unsigned long flags);//會將目前的中斷狀態(tài)保留在flags中 void local_irq_disable(void);//直接中斷 這兩個將屏蔽本CPU內的所有中斷。對應的上邊兩個中斷的方法如下 void local_irq_restore(unsigned long flags); void local_irq_enable(void);
我們兩邊說了Linux系統(tǒng)中中斷是分為頂半部和底半部的,那么在系統(tǒng)實現方面是具體怎樣實現的呢,這主要有tasklet,工作隊列,軟中斷: 1)tasklet:使用比較簡單,如下: void my_tasklet_function(unsigned long); //定義一個處理函數 DECLARE_TASKLET(my_tasklet, my_tasklet_function, data); //定義了一個名叫my_tasklet的tasklet并將其與處理函數綁定,而傳入參數為data 在需要調度tasklet的時候引用一個tasklet_schedule()函數就能使系統(tǒng)在適當的時候進行調度運行:tasklet_schedule(&my_tasklet); 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_irq);來在系統(tǒng)在適當的時候需要調度時使用運行。 3)軟中斷:使用軟件方式模擬硬件中斷的概念,實現宏觀上的異步執(zhí)行效果,tasklet也是基于軟中斷實現的。 在Linux內核中,用softirq_action結構體表征一個軟中斷,這個結構體中包含軟中斷處理函數指針和傳遞給函數的參數,使用open_softirq()可以注冊軟中斷對應的處理函數,而raise_softirq()函數可以觸發(fā)一個中斷。 軟中斷和tasklet仍然運行與中斷上下文,而工作隊列則運行于進程上下文。因此,軟中斷和tasklet的處理函數不能休眠,但工作隊列是可以的。 local_bh_disable()和local_bh_enable()是內核用于禁止和使能軟中斷和tasklet底半部機制的函數。
下邊咱們再來說說有關中斷共享的相關點:中斷共享即是多個設備共享一根硬件中斷線的情況。Linux2.6內核支持中斷共享,使用方法如下: *共享中斷的多個設備在申請中斷時都應該使用SA_SHIRQ標志,而且一個設備以SA_SHIRQ申請某中斷成功的前提是之前該中斷的所有設備也都以SA_SHIRQ標志申請該終端 *盡管內核模塊可訪問的全局地址都可以作為request_irq(….,void *dev_id)的最后一個參數dev_id,但是設備結構體指針是可傳入的最佳參數。 *在中斷帶來時,所有共享此中斷的中斷處理程序都會被執(zhí)行,在中斷處理程序頂半部中,應迅速根據硬件寄存器中的信息比照傳入的dev_id參數判斷是否是被設備的中斷,如果不是,應迅速返回。
結語:在這次講解中說了三種Linux系統(tǒng)中中斷的頂/底半部機制和中斷共享的先關內容,但礙于頁面空間的原因,沒有給出例子,我在下次博客中會專門來對每個點給出典型的模版. “小濤哥,快醒醒,快醒醒..”小王使勁推著睡夢中的我,“你不是說今天要講昨天有關的典型模板實例嗎…” “???小姐啊,現在才早上8點,還讓人睡覺不,別吵”我一頭鉆進被子里說。 “不管,誰讓你昨天不說完,還賣個小關子,害我昨天晚上都沒睡好,想了一晚上…” 我揉揉蒙蒙的眼說:“行,權當看在你渴求的心情上,但只此一次,下不為例,我還想好好睡懶覺呢..” 昨天我們講了有關中斷方面的東西,鑒于小王你不太懂,我今天就專門拿出一章來說說前邊中斷的使用典型模版,你照抄也方便不是: 1)在中斷分類中,我們說到了有關向量中斷和非向量中斷,向量中斷就是入口地址不同,進不同的地址做不同的事。那非向量中斷則是進同一地址,至于區(qū)分就放在了進去后用條件判斷,請看下邊的模板: 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;
}
}2)在底半部機制中,我們講了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, struct pt_regs *regs) //中斷處理頂半部 { ... tasklet_schedule(&xxx_tasklet); } int __init xxx_init(void) //設備驅動模塊加載函數 { .. result= request_irq(xxx_irq, xxx_interrupt, SA_INTERRUPT, "XXX",NULL); //申請中斷 ... } void __exit xxx_exit(void) //設備驅動卸載模塊 { .. free_irq(xxx_irq, xxx_interrupt); //釋放中斷 .. } struct work_struct xxx_wq; void xxx_do_work(unsigned long); void xxx_do_work(unsigned long) //中斷處理底半部 { ..... } irqreturn_t xxx_interrupt(int irq, void *dev_id, struct pt_regs *regs) //中斷處理頂半部 { ... schedule_work(&xxx_wq); } int xxx_init(void) //設備驅動模塊加載函數 { .. result= request_irq(xxx_irq, xxx_interrupt, SA_INTERRUPT, "XXX",NULL); //申請中斷 ... INIT_WORK(&xxx_wq, (void (*)(void *))xxx_do_work, NULL); ... } void __exit xxx_exit(void) //設備驅動卸載模塊 { .. free_irq(xxx_irq, xxx_interrupt); //釋放中斷 .. } 3)在上節(jié)最后我還給你講了有關中斷共享的東西吧,小王,也把模版給你: 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; } int __init xxx_init(void) //設備驅動模塊加載函數 { .. result= request_irq(xxx_irq, xxx_interrupt, SA_SHIRQ, "XXX",xxx_dev); //申請共享中斷 ... } void __exit xxx_exit(void) //設備驅動卸載模塊 { .. free_irq(xxx_irq, xxx_interrupt); //釋放中斷 .. } 共享中斷中,我們仔細看一下其實也沒什么,不是。就是在和前邊中斷中要修改一下中斷標志,在中斷處理中判斷一下是否是自己本地的中斷,這個我都用紅色的標識出來了。
“小王,看,小濤哥說話算數吧,上邊給出了所有模版,結合前一篇,相信你可以看的很順利的。好了,我要補補剛的覺了,中間不許叫我哈,想我也不行”我打打哈欠說。 晚上7點10分.. “小濤哥,這章不是叫Linux設備驅動程序之中斷與時鐘,前邊你講了中斷,還給了我很多模版,我都看懂了,這次是不是要開始講時鐘了..” “真聰明,越來越喜歡你這聰明的樣子了,說的不錯,今天就要開始一個新的模塊--內核時鐘”我很少夸人,為啥今天夸她呢了,呵呵. 定時器,意思大家都明白,我就不說了,要是不明白,把它想成個鬧鐘總可以吧.. 定時器分為硬件和軟件定時器,軟件定時器最終還是要依靠硬件定時器來完成。內 核在時鐘中斷發(fā)生后檢測各定時器是否到期,到期后的定時器處理函數將作為軟中斷在底半部執(zhí)行。實質上,時鐘中斷處理程序執(zhí)行 update_process_timers函數,該函數調用run_local_timers函數,這個函數處理TIMER_SOFTIRQ軟中斷,運 行當前處理上到期的所有定時器。 Linux內核中定義提供了一些用于操作定時器的數據結構和函數如下: 1)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);經過這個初始化后,entry的next為NULL,并給base賦值 4)刪除定時器:int del_timer(struct timer_list *timer); 說明:del_timer_sync是del_timer的同步版,主要在多處理器系統(tǒng)中使用,如果編譯內核時不支持SMP,del_timer_sync和del_timer等價. 5)修改定時器:int mod_timer(struct timer_list *timer, unsigned long expires); 下邊是一個使用定時器的模版: struct xxx_dev /*second設備結構體*/ { struct cdev cdev; /*cdev結構體*/ ... struct timer_list xxx_timer; /*設備要使用的定時器*/ }; int xxx_func1(...) //xxx驅動中某函數 { struct xxx_dev *dev = filp->private_data; ... /*初始化定時器*/ init_timer(&dev->xxx_timer); dev->xxx_timer.function = &xxx_do_handle; dev->xxx_timer.data = (unsigned long)dev; dev->xxx_timer.expires = jiffies + delay; add_timer(&dev->xxx_timer); /*添加(注冊)定時器*/ ... return 0; } int xxx_func2(...) //驅動中某函數 { ... del_timer(&second_devp->s_timer); ... } static void xxx_do_timer(unsigned long arg) //定時器處理函數 { struct xxx_device *dev = (struct xxx_device *)(arg); ... //調度定時器再執(zhí)行 dev->xxx_timer.expires = jiffies + delay; add_timer(&dev->xxx_timer); } 在定時器函數中往往會在做完具體工作后,延遲expires并將定時器再次添加到內核定時器鏈表中,以便定時器能被再次觸發(fā)(這句話我也是從別處抄來的,別告訴小王哈)。 在內核定時器中,常常少不了要說下內核延遲的事,請接著往下看: 1)短延遲:在linux內核中提供了三個函數來分別實現納秒,微秒,毫秒延遲,原理上是忙等待,它根據CPU頻率進行一定次數的循環(huán) void ndelay(unsigned long nsecs); void udelay(unsigned long usecs); void mdelay(unsigned long msecs); 毫秒延遲已經相當大了,當然更秒延遲當然要小一些,在內核中,為了性能,最好不要用mdelay,這會耗費大量cpu資源,那么咋辦呢,涼拌.. void msleep(unsigned int millisecs); unsigned long msleep_interruptible(unsigned int millisecs); void ssleep(unsigned int seconds); 這三個是內核專門提供該我們用來處理毫秒以上的延遲。上述函數將使得調用它的進程睡眠參數指定的秒數,其中第二個是可以被打斷的,其余的兩個是不可以的。 2)長延遲:內核中進行延遲最常用的方法就是比較當前的jiffies和目標jiffies(當前的加上時間間隔的jiffies),直到未來的jiffies達到目標jiffies。比如: unsigned long delay = jiffies + 100; //延遲100個jiffies while(time_before(jiffies, delay)); 與time_before對應的還有一個time_after().其實就是#define time_before(a,b) time_after(b,a); 另外兩個是time_after_eq(a,b)和time_before_eq(a,b) 3)睡著延遲:這顯然是比忙等待好的方法,因為在未到來之前,進程會處于睡眠狀態(tài),把 CPU空出來,讓CPU可以做別的事情,等時間到了,調用schedule_timeout()就可以喚醒它并重新調度執(zhí)行。msleep和 msleep_interruptible本質上都是依靠包含了schedule_timeout的 schedule_timeout_uninterruptible()和schedule_ timeout_interruptible()實現。就像下邊這樣: void msleep(unsigned int msecs) {
unsigned long timeout = msecs_to_jiffies(msecs) + 1;
while(timeout)
timeout = schedule_timeout_uninterruptible(timeout);
}
unsigned long msleep_interruptible(unsigned int msecs)
{
unsigned long timeout = msecs_to_jiffies(msecs) + 1;
while(timeout && !signal_pending(current))
timeout = schedule_timeout_interruptible(timeout);
return jiffies_to_msecs(timeout);
}
signed long __sched schedule_timeout_interruptible()signed long timeout) { __set_current_state(TASK_INTERRUPTIBLE); return schedule_timeout(timeout); } signed long __sched schedule_timeout_uninterruptible()signed long timeout) { __set_current_state(TASK_UNINTERRUPTIBLE); return schedule_timeout(timeout); } 另外還有如下:
time_on_timeout(wait_queue_head_t *q, unsigned long timeout);
interruptible_sleep_on_timeout(wait_queue_head_t *q, unsigned long timeout);
這兩個將當前進程添加到等待隊列,從而在等待隊列上睡眠,當超時發(fā)生時,進程將被喚醒。
“小王,有關中斷和系統(tǒng)時鐘的咱們也講完了,下次就給你來一個有關系統(tǒng)時鐘的設備驅動例子,鞏固一下吧,你可要抓緊哦..“ Linux內核開發(fā)之中斷與時鐘(四) “小王,小王,今天可是這一章節(jié)最后一節(jié)了,知識點咱們前邊都講過了,今天主要是給你用前邊的東西講一個實際例子---秒字符設備驅動程序” 這個驅動程序會在被打開的時候初始化一個定時器并將其添加到內核定時器鏈表中,每秒輸出一次當前的jiffies,這意味著,定時器處理函數中每次都要修改新的expires。不多說了,看代碼分析: #include …//必要的系統(tǒng)頭文件 #define SECOND_MAJOR 252 /*預設的second的主設備號*/ static int second_major = SECOND_MAJOR; struct second_dev /*second設備結構體*/ { 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); 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 return 0; } int second_release(struct inode *inode, struct file *filp) /*文件釋放函數*/ { del_timer(&second_devp->s_timer); return 0; } static ssize_t second_read(struct file *filp, char __user *buf, size_t count, loff_t *ppos) /*globalfifo讀函數*/ {
int counter;
counter = atomic_read(&second_devp->counter);
if(put_user(counter, (int*)buf))
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,
};
static void second_setup_cdev(struct second_dev *dev, int index) /*初始化并注冊cdev*/
{
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 /* 動態(tài)申請設備號 */
{
ret = alloc_chrdev_region(&devno, 0, 1, "second");
second_major = MAJOR(devno);
}
if (ret < 0)
return ret;
/* 動態(tài)申請設備結構體的內存*/
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("hanyan225"); MODULE_LICENSE("Dual BSD/GPL"); module_param(second_major, int, S_IRUGO); module_init(second_init); module_exit(second_exit); 下面是測試程序: #include ..//必要的頭文件 main()
{
int fd;
int counter = 0;
int old_counter = 0;
fd = open("/dev/second", O_RDONLY); /*打開/dev/second設備文件*/
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");
}
}當我們編譯完驅動程序,并運行了測試程序后,會看到應用程序不斷輸出自打開/dev/second以來經歷的秒數。如下:
#./test
seconds after open /dev/second 1
seconds after open /dev/second 2
..
..
再帶一個中斷,看看內核輸出操作如下:
#tar –f /var/logs/message
current jiffies is 18569
current jiffies is 18669
current jiffies is 18769 ..
“小王,Linux設備驅動之中斷與時鐘也算說完了,告一段落了,也不知道你明白沒,沒明白,也沒關系,不是有我嗎,只是不要一早吵醒我就好,下次我們就要開始系統(tǒng)內存方面的東西了…”我說。 “好,小濤哥,我好好看看,不懂就問你,呵呵..”小王銀鈴般笑著還伴著怪臉,真是讓人… |
|
|