十四、Linux驅(qū)動(dòng)程序開發(fā)(10) -
中斷 5.1中斷 中斷本質(zhì)上是一種特殊的電信號(hào),,由硬件設(shè)備發(fā)向處理器,,處理器接收到中斷后,會(huì)馬上向操作系統(tǒng)反映此 信號(hào)的到來(lái),,然后就由OS復(fù)雜處理這些新到來(lái)的數(shù)據(jù),。硬件設(shè)備生成中斷的時(shí)候并不考慮與處理器的時(shí)鐘同步,也就是說(shuō)中斷可以隨時(shí)產(chǎn)生,。 定義: 所謂中斷是指CPU在 執(zhí)行程序的過(guò)程中,,出現(xiàn)了某些突發(fā)事件時(shí)CPU必須暫停執(zhí)行當(dāng)前的程序,轉(zhuǎn)去處理突 發(fā)事件,,處理完畢后CPU又返回原程序被中斷的位置并繼續(xù)執(zhí)行,。 分類: 根據(jù)中斷的來(lái)源:分為內(nèi)部中斷和外部中斷。內(nèi)部中斷的中斷源來(lái)自CPU內(nèi) 部(軟件中斷指令,溢出等),,外部中斷的中斷源來(lái)自CPU外部,,由外設(shè)提出請(qǐng)求。 根據(jù)是否屏蔽:分為可屏蔽中斷與不屏蔽中斷(NMI),, 可屏蔽中斷可通過(guò)屏蔽字屏蔽,,屏蔽后該中斷不再得到響應(yīng)。 根據(jù)中斷入口跳轉(zhuǎn)方式的不同:分為向量中斷和非向量中斷,。采用向量中斷的CPU通 常為不同的中斷分配不同的中斷號(hào),。非向量中斷的多個(gè)中斷共享一個(gè)入口地址。進(jìn)入該入口地址后再通過(guò)軟件判斷中斷標(biāo)志來(lái)識(shí)別具體是哪個(gè)中斷,。就是說(shuō),,向量中 斷是由硬件提供中斷服務(wù)程序入口地址,非向量中斷由軟件提供中斷服務(wù)程序入口地址,。 5.2中斷處理程序 中斷處理程序與其他內(nèi)和函數(shù)的真正區(qū)別在于: 中斷處理程序是被內(nèi)核調(diào)用來(lái)響應(yīng)中斷的,,而它們運(yùn)行于我們稱之為中斷 上下文的特殊上下文中。 前和后半部(上半部和下半部) 中斷處理的一個(gè)主要問(wèn)題是如何在處理中進(jìn)行長(zhǎng)時(shí)間的任務(wù). 常常大量的工作必須響應(yīng)一個(gè)設(shè)備中斷來(lái)完成, 但 是中斷處理需要很快完成并且不使中斷阻塞太長(zhǎng). 這 2 個(gè)需要(工作和速度)彼此沖突,。 Linux (許多其他系統(tǒng)一起)解決這 個(gè)問(wèn)題通過(guò)將中斷處理分為 2 半. 所謂的前半部是實(shí)際響應(yīng)中斷的 函數(shù) -- 你使用 request_irq 注冊(cè) 的那個(gè). 后半部是由前半部調(diào)度來(lái)延后執(zhí)行的函數(shù), 在 一個(gè)更安全的時(shí)間. 最大的不同在前半部處理和后半部之間是所有的中斷在后半部執(zhí)行時(shí)都使能——這就是為什么它在一個(gè)更 安全時(shí)間運(yùn)行. 在典型的場(chǎng)景中, 前半 部保存設(shè)備數(shù)據(jù)到一個(gè)設(shè)備特定的緩存, 調(diào)度它的后半部, 并且退出: 這個(gè)操作非???/span>. 后半部接著進(jìn)行任何其他需要的工作, 例 如喚醒進(jìn)程, 啟動(dòng)另一個(gè) I/O 操 作, 等等. 這種設(shè)置允許前半部來(lái)服務(wù)一個(gè) 新中斷而同時(shí)后半部仍然在工作。 5.3注冊(cè)中斷處理程序 中斷處理程序是管理硬件的驅(qū)動(dòng)程序的組成部分,。每個(gè)設(shè)備都有相關(guān)的驅(qū)動(dòng) 程序,,如果設(shè)備使用中斷,那么相應(yīng)的驅(qū)動(dòng)程序就 注冊(cè)一個(gè)中斷處理程序,。驅(qū)動(dòng)程序可以通過(guò)下面的函數(shù)注冊(cè)并激活一個(gè)中斷處 理程序,,以便處理中斷: int request_irq(unsigned int irq,
irqreturn_t (*handler)(int, void *, struct pt_regs *),
unsigned long flags,
const char *dev_name,
void *dev_id);
(1)第一個(gè)參數(shù)irq表示要分配的中斷號(hào)。對(duì)某些設(shè)備,,這個(gè)值是先定的,,對(duì)大多數(shù)設(shè)備來(lái)
說(shuō),這個(gè)值是可以通過(guò)探測(cè)獲取,,也可以動(dòng)態(tài)確定,。 (2)第二個(gè)參數(shù)handler是一個(gè)指針,指向處理這個(gè)中斷的實(shí)際中斷處理程序,。只要操作系統(tǒng)一接
收到中斷,,該函數(shù)就被調(diào)用。 (3)第三個(gè)參數(shù)irqflags可以為0,,也可能是下列一個(gè)或多個(gè)標(biāo)志的位掩碼:SA_INTERRUPT:此標(biāo)志表明給定的中斷處理程序是一個(gè)快速中斷處理程序,。 SA_SAMPLE_RANDOM:此標(biāo)志表明這個(gè)設(shè)備產(chǎn)生的中斷對(duì)內(nèi)核熵池有貢獻(xiàn)。 SA_SHIRQ:此標(biāo)志表明可以在多個(gè)中斷處理程序之間共享中斷線,。 (4)第四個(gè)參數(shù)devname是與中斷相關(guān)的設(shè)備的ASCII文本表示法,。這些名字會(huì)被/proc/irq和proc/interrupt文件使用,以便與用戶通信。 (5)第五個(gè)參數(shù)dev_id主要用于共享中斷線,。用于區(qū)分共享中斷線上的各個(gè)中斷處理程序,。內(nèi)核 每次調(diào)用中斷處理程序時(shí),都會(huì)把這個(gè)指針傳遞給它,。實(shí)踐中往往會(huì)通過(guò)它傳遞驅(qū)動(dòng)程序的設(shè)備結(jié)構(gòu):這個(gè)指針是唯一的,,而且有可能在中斷處理程序內(nèi)及設(shè)備模式 中被用到。 request_irq()成功執(zhí)行會(huì)返回0,,如果 返回非0值,,就表示有錯(cuò)誤發(fā)生,在這種情況下,,指定的中斷處理程序不會(huì)被注冊(cè),,最常見的錯(cuò)誤是-EBUSY,它表示給定的中斷線已經(jīng)在使用(或者當(dāng)前用戶或者你沒(méi)有指定SA_SHIRQ()),。 注意:request_irq()函數(shù)可能會(huì)睡眠,,因此,不能在中斷上下
文或其它不允許阻塞的代碼中調(diào)用該函數(shù),,在睡眠不安全的上下文中可以安全的調(diào)用request_irq()函數(shù),,是一種常見錯(cuò)誤。 為什么request_irq()會(huì)引起睡眠,? 在注冊(cè)的過(guò)程中,,內(nèi)核需要在/proc/irq文件創(chuàng)建一個(gè)與中斷對(duì)應(yīng)的項(xiàng),函數(shù)proc_mkdir()就是用來(lái)創(chuàng)建這個(gè) 新的procfs項(xiàng)的,,proc_makedir()通 過(guò)調(diào)用函數(shù)proc_create()對(duì)這個(gè)新的profs項(xiàng) 進(jìn)行設(shè)置,,而proc_create()會(huì)調(diào)用函數(shù)kmalloc()來(lái)請(qǐng)求分配內(nèi)存,而我們知道函數(shù)kmalloc()是可以睡眠的,。 釋放中斷處理程序 釋放中斷線,,可以調(diào)用: void free_irq(unsigned int irq, void *dev_id);
如果指定的中斷線不是共享的,那么,,該函數(shù)刪除處理程序的同時(shí)將禁用這條中斷線,如果中斷線是共享的,,則僅刪除dec_id所對(duì)應(yīng)的處理程序,,而這條中斷線本身只有在刪除了最后一個(gè)處理程序才會(huì)被禁用,由此可以看出為什么唯一的dev_id如此重要,,對(duì)于共享的中斷線,,需要一個(gè)唯一的信息來(lái)區(qū)分其上面的多個(gè)處理程序,并通過(guò)它來(lái)保證函數(shù)free_irq()能夠正確的刪除指定的處理程序,,不管在哪種情況下(共享或不共享),,如果dev_id非空,它都必須與需要的刪除的處理程序相匹配。 注意:必須從進(jìn)程上下文中調(diào)用free_irq(),。
5.4編寫中斷處理程序 一個(gè)典型的中斷處理程序聲明:
static
irqreturn_t intr_handle(int irq, void *dev_id, struct pt_regs
*regs 注意,,它的類型與request_irq()參數(shù)中handler所要求的參數(shù)類型相匹配(handler函數(shù)有一個(gè)類型為irqreturn_t的返回值)。第一個(gè)參數(shù)irq就是這個(gè)處理程序要響應(yīng)的中斷的中斷線號(hào),。dev_id是一個(gè)通用的指針,,它與在中斷注冊(cè)時(shí)傳遞給request_irq()的參數(shù)dev_id必須一致。dev_id也有可能指向中斷處理程序使用的一個(gè)數(shù)據(jù)結(jié)構(gòu),,對(duì)每個(gè)設(shè)備而言,,設(shè)備
結(jié)構(gòu)都是唯一的,而且可能在中斷處理程序中也用的到,,因此,,也通常會(huì)把設(shè)備結(jié)構(gòu)傳遞給dev_id。 參數(shù)regs是一個(gè)指向結(jié)構(gòu)的指針,,該結(jié)構(gòu)包含處理中斷之前處理器的寄存器和狀
態(tài),,除了調(diào)試以外,很少用到,。 中斷處理程序的返回值是一個(gè)特殊類型:irqreturn_t,。它可能返回兩個(gè)特殊的值:IRQ_NONE和IRQ_HANDLED。irqreturn_t這個(gè)返回類型實(shí)際上就是一個(gè)int型,,之所以使用這些特殊值是為了與早期的內(nèi)核保持兼容,。 中斷處理程序通常會(huì)標(biāo)記static,因?yàn)樗鼜膩?lái)不會(huì)被別的文件中的代碼直接調(diào)用,。 共享的處理程序與非共享的處理程序在注冊(cè)和運(yùn)行方式上比較相似,,但差異
主要有以下幾點(diǎn): (1)request_irq()的參數(shù)flags必須設(shè)置SA_SHIRQ標(biāo)志。 (2)對(duì)每個(gè)注冊(cè)的中斷處理程序來(lái)說(shuō),,dev_id參數(shù)必須唯一,。而指向任一設(shè)備結(jié)構(gòu)體的指針就可以滿足這一要求;通常
會(huì)選擇設(shè)備結(jié)構(gòu),,因?yàn)樗俏ㄒ坏?,而且中斷處理程序可能?huì)用到它。 (3)中斷處理程序必須能夠區(qū)分它的設(shè)備是否真的產(chǎn)生中斷,。這既需要硬件支
持,,也需要處理程序中有相關(guān)的處理邏輯。如果硬件不支持這一功能,,它就沒(méi)法知道到底是與它對(duì)應(yīng)的設(shè)備發(fā)出了這個(gè)中斷,,還是共享這條中斷線的其它設(shè)備發(fā)出了
這個(gè)中斷。 指定SA_SHIRQ標(biāo)志以調(diào)用request_irq()時(shí),,只有在以下兩種情況下才可能成功,,一是中斷線當(dāng)前未被注冊(cè),,二是在該線上的所有已注冊(cè)處理程序都指定了SA_SHIRQ。 內(nèi)核接收一個(gè)中斷后,,它將依次調(diào)用在該中斷線上注冊(cè)的每一個(gè)處理程序,,
因此,一個(gè)處理程序必須知道它是否應(yīng)該為這個(gè)中斷負(fù)責(zé),,如果與它相關(guān)的設(shè)備并沒(méi)有產(chǎn)生中斷,,那么,處理程序應(yīng)該立即退出,。 當(dāng)執(zhí)行一個(gè)中斷處
理程序或下半部時(shí),,內(nèi)核處于中斷上下文(interrput context)中。讓我們先回憶一下進(jìn)程上下文,,進(jìn)程上下文是一種內(nèi)核所處
的操作模式,,此時(shí)內(nèi)核代表進(jìn)程執(zhí)行——例如,執(zhí)行系統(tǒng)調(diào)用或運(yùn)行內(nèi)核線程,,在進(jìn)程上下文中,,可以通過(guò)current宏去關(guān)聯(lián)當(dāng)前進(jìn)程,此外,,因?yàn)檫M(jìn)程是以進(jìn)程上下文的形式
連接到內(nèi)核中的,,因此,在進(jìn)程上下文可以睡眠,,也可以調(diào)用調(diào)度程序,。 與之相反,中斷上下文和進(jìn)程并沒(méi)有什么瓜葛,,與current宏也是不相干的(盡管它會(huì)指向被中斷的進(jìn)程),,因?yàn)闆](méi)有進(jìn)程的背景,所以中斷上下文不可以睡眠——否則又怎能對(duì)它重新調(diào)度呢,?因此,,不能從中斷 上下文中調(diào)用某些函數(shù),如果一個(gè)函數(shù)睡眠,,就不能在你的中斷處理程序中使用它——這是對(duì)什么樣的函數(shù)可以在中斷處理程序中的使用的限制,。 有一點(diǎn)非常重要:中斷處理程序打斷了其它代碼(甚至可能是打斷了在其 他中斷線上的另一個(gè)中斷處理程序)。正是因?yàn)檫@種異步執(zhí)行的特性,,所以所有的中斷處理程序必須盡可能的迅速,,簡(jiǎn)潔,盡量把工作從中斷處理程序中分離出來(lái),, 放在下半部執(zhí)行,因?yàn)橄掳氩靠梢栽诟线m的時(shí)間運(yùn)行,。 最后,,中斷處理程序并不具有自己的棧,,相反,它共享被中斷進(jìn)程的內(nèi)核棧,,如果沒(méi)有正在運(yùn)行的進(jìn)程,,它 將使用idle進(jìn)程的棧,因?yàn)橹袛嗵幚沓绦蚬蚕韯e人的堆棧所以它們?cè)跅V蝎@取空間時(shí)必須非常節(jié)省,,當(dāng)然,,內(nèi)核棧本 來(lái)就很有限(內(nèi)核棧在32位體系結(jié)構(gòu)上是8KB, 在64位體系結(jié)構(gòu)上是16KB,, 執(zhí)行中的進(jìn)程上下文和產(chǎn)生的所有中斷都共享內(nèi)核棧),。因此所有的內(nèi)核代碼都應(yīng)該謹(jǐn)慎利用它。 5.5中斷處理機(jī)制的實(shí)現(xiàn) 中斷處理系統(tǒng)在Linux中的實(shí)現(xiàn)是非常依賴于體系結(jié)構(gòu)的,。實(shí)現(xiàn)依賴于處理器,、所使用的
中斷控制器的類型、體系結(jié)構(gòu)的設(shè)計(jì)及機(jī)器本身,。 對(duì)于中斷從硬件到內(nèi)核的路由,,設(shè)備產(chǎn)生中斷,通過(guò)總線把電信號(hào)發(fā)送給中斷控制器,。如果中斷是激活的,,那么中斷控制器就會(huì)把中斷發(fā)往處理
器。在大多數(shù)體系結(jié)構(gòu)中,,這個(gè)工作就是通過(guò)電信號(hào)給處理器的特定管腳發(fā)送一個(gè)信號(hào),,處理器會(huì)立即停止它正在做的事情,關(guān)閉中斷系統(tǒng),,然后跳轉(zhuǎn)到內(nèi)存中預(yù)定
義的位置開始執(zhí)行那里的代碼,。這個(gè)預(yù)定以的位置是內(nèi)核設(shè)置的,是中斷處理程序的入口點(diǎn),。 在內(nèi)核中,,中斷的旅程開始于預(yù)定義入口點(diǎn),這類似于系統(tǒng)調(diào)用通過(guò)預(yù)定義
的異常句柄進(jìn)入內(nèi)核,。對(duì)于每一個(gè)中斷線,,處理器都會(huì)跳到對(duì)應(yīng)的一個(gè)唯一的位置。至此,,內(nèi)核知道了所接受的中斷的IRQ號(hào),。初始入口點(diǎn)只是在棧中保存這個(gè)號(hào),并存放當(dāng)前寄存器的值(這些值
屬于被中斷的任務(wù)),,然后,,內(nèi)核調(diào)用函數(shù)do_IRQ()。 5.6中斷控制 Linux內(nèi)核提供了一組接口用于操作機(jī)器上的中斷狀態(tài),。這些接口為我們提供了能夠禁止當(dāng)前處理器的中斷系統(tǒng),,或屏蔽掉整個(gè)機(jī)器的一條中斷線的
能力,,這些例程都是與體系結(jié)構(gòu)相關(guān)的,可以在<asm/system.h>和<asm/irq.h>中找到,。 下面給出一些中斷控制方法: local_irq_disable() 禁止本地中斷傳送 local_irq_enable() 激活本地中斷傳送 local_irq_save() 保存本地中斷傳遞的當(dāng)前狀態(tài),,然后禁止本地中斷傳遞 local_irq_restore() 恢復(fù)本地中斷傳遞到給定的狀態(tài) disable_irq() 禁止給定中斷線,并確保該函數(shù)返回之前在該中斷線上沒(méi)有處理程序在運(yùn)行 disable_irq_nosynoc() 禁止給定中斷線 enable_irq() 激活給定中斷線 irqs_disabled() 如果本地中斷傳遞被禁止,,則返回非0,,否則返回0 in_interrupt() 如果在中斷上下文中,則返回非0,,如果在進(jìn)程上下文中,,則返回0 6.1下半部 下半部的任務(wù)就是執(zhí)行與中斷處理密切相關(guān)但中斷處理程序本身不執(zhí)行的工作。對(duì)于在上半部和下半部之間劃分工作,,盡管不存在某種嚴(yán)格的規(guī)則,,但還是有一些提示可供借鑒:(1)如果一個(gè)任務(wù)對(duì)時(shí)間非常敏感,將其放在中斷處理程序中執(zhí)行,。(2)如果一個(gè)任務(wù)和硬件相關(guān),,將其放在中斷處理程序中執(zhí)行。(3)如果一個(gè)任務(wù)要保證不被其它中斷打斷,,將其放在中斷處理程序中執(zhí)行,。(4)其它所有任務(wù),考慮放在下半部執(zhí)行,。當(dāng)我們 開始嘗試寫自己的驅(qū)動(dòng)程序的時(shí)候,,讀一下別人的中斷處理程序和相應(yīng)的下半部會(huì)令你受益匪淺。現(xiàn)在的問(wèn)題是:下半部具體放到以后的什么時(shí)候去 做呢,?下半部并不需要指明一個(gè)確切時(shí)間,,只要把這些任務(wù)推遲一點(diǎn),讓他們?cè)谙到y(tǒng)不太繁忙并且中斷恢復(fù)后執(zhí)行就可以了,。通常下半部在中斷處理程序一返回就會(huì) 馬上執(zhí)行,。下半部執(zhí)行的關(guān)鍵在于當(dāng)它們運(yùn)行的時(shí)候,允許響應(yīng)所有中斷,。 因?yàn)樵谥袛嗵幚沓绦蜻\(yùn)行的時(shí)候,當(dāng)前的中斷線會(huì)被屏蔽,,如果一個(gè)處理程
序是SA_INTERRUPT類型,,它執(zhí)行的時(shí)候會(huì)禁止所有本地中斷(而且把本地中斷線全局屏蔽掉),再加上中斷處理程序要與其它程序——甚至是其它的中斷處理程序——異
步執(zhí)行,。 具體放到以后什么時(shí)候去做呢,? 在這里,,“以后”僅僅用來(lái)強(qiáng)調(diào)不是“馬上”而已,下半部并不需要指明一個(gè)確
切時(shí)間,,只是把這些任務(wù)推遲一點(diǎn),讓它們?cè)谙到y(tǒng)不太繁忙并且中斷恢復(fù)后執(zhí)行就可以了,,通常下半部在中斷處理程序一返回就會(huì)馬上執(zhí)行,,下半部執(zhí)行的關(guān)鍵在于
當(dāng)它們運(yùn)行的時(shí)候,允許響應(yīng)所有的中斷,。 6.2軟中斷 軟中斷是用軟件方式模擬硬件中斷的概念,,實(shí)現(xiàn)宏觀上的異步執(zhí)行效果,tasklet也是基于軟中斷實(shí)現(xiàn)的,。 異步通知所基于的信號(hào)也類似于中斷,。 硬中斷是外部設(shè)備對(duì)CPU的中斷 軟中斷通常是硬中斷服務(wù)程序?qū)?nèi)核的中斷。 信號(hào)則是由內(nèi)核(或其它進(jìn)程)對(duì)某個(gè)進(jìn)程的中斷,。 軟中斷是在編譯期間靜態(tài)分配的,。不像tasklet那樣能被動(dòng)態(tài)的注冊(cè)或去除。軟中斷由softirq_action結(jié)構(gòu)表示,,它定義在<linux/interrupt.h>中: struct
softirq_action {
void( *action)(struct softirq_action *); /*待執(zhí)行的函數(shù)*/ Void*date; /傳遞給函數(shù)的參數(shù)*/
} ; 在kernel/softirq.c中定義了一個(gè)包含有32個(gè)該結(jié)構(gòu)體的數(shù)組,。 static
strcut softirq_action softirq_vec[32]; 每個(gè)注冊(cè)的軟中斷都占據(jù)該數(shù)組中的一項(xiàng)。 (1) 軟中斷處理程序: 軟中斷處理程序action的函數(shù)原型如下: void
softirq_handler(struct softirq_action *) 當(dāng)內(nèi)核運(yùn)行一個(gè)軟中斷處理程序的時(shí)候,,它就會(huì)執(zhí)行這個(gè)action函數(shù),,其唯一的參數(shù)為指向相應(yīng)的softirq_action結(jié)構(gòu)體的指針。 一個(gè)軟中斷不會(huì)搶占另外一個(gè)軟中斷,,實(shí)際上,,唯一可以搶占軟中斷的是中
斷處理程序,不過(guò),,其它的軟中斷——甚至是相同類型的軟中斷——可以在其它處理器上同時(shí)執(zhí)行,。 (2) 執(zhí)行軟中斷: 一個(gè)注冊(cè)的軟中斷必須在被標(biāo)記后才會(huì)執(zhí)行。這被稱作觸發(fā)軟中斷(raising
the softirq),。通
常,,中斷處理程序會(huì)在返回前標(biāo)記它的軟中斷,使其在稍后被執(zhí)行,。軟中斷被標(biāo)記后,,可以用softirq_pending()檢查到這個(gè)標(biāo)記并按照索引號(hào)將softirq_pending()的返回值的相應(yīng)位置1。 在合適的時(shí)刻,,該軟中斷就會(huì)運(yùn)行,,在下列地方,待處理的軟中斷會(huì)被檢查
和執(zhí)行: 在處理完一個(gè)硬中斷以后 在ksoftirqd內(nèi)核線程中 在那些顯式檢查和執(zhí)行待處理的軟中斷的代碼中,,如網(wǎng)絡(luò)子系統(tǒng)中 不管是用什么辦法喚起,,軟中斷都要在do_softirq()中執(zhí)行,,該函數(shù)很簡(jiǎn)單,如果有待處理的軟中斷,,do_softirq()會(huì)遍歷每一個(gè),,調(diào)用它們的處理程序。 軟中斷在do_softirq()中執(zhí)行,。do_softirq()經(jīng)過(guò)簡(jiǎn)化后的核心部分: u32 pending = sofeirq_pending(cpu); if(pending) { struct softirq_action *h = softirq_vec; softirq_pending(cpu) = 0; do { if(pending&1) h->action(h); //調(diào)用action函數(shù) h++; pending>>=1; }while(pending); } 軟中斷保留給系統(tǒng)中對(duì)時(shí)間要求最嚴(yán)格以及最重要的下半部使用,。內(nèi)核定時(shí)器
和tasklets都
是建立在軟中斷上的,如果你想加入一個(gè)新的軟中斷,,首先要想想為什么用tasklet實(shí)現(xiàn)不了,,tasklet可以動(dòng)態(tài)生成,由于它們對(duì)加鎖的要求不高,,所以使用起來(lái)也很方便,,當(dāng)
然,對(duì)于時(shí)間要求養(yǎng)并能自己高效的完成加鎖工作的應(yīng)用,,軟中斷會(huì)是正確的選擇,。 1、 分配索引:在編譯期間,,可以通過(guò)<linux/interrupt.h>中定義的一個(gè)枚舉類型來(lái)靜態(tài)的聲明軟中斷,。 2、 注冊(cè)處理程序:接著,,在運(yùn)行時(shí)通過(guò)調(diào)用open_softirq()注冊(cè)軟件中斷處理程序,,該函數(shù)有三個(gè)參數(shù):索引號(hào)、處理函數(shù)和data域存放的數(shù)值,。例如網(wǎng)絡(luò)子系統(tǒng),,通過(guò)以下方式注冊(cè)自己的軟中斷: open_softirq(NET_TX_SOFTIRQ,
net_tx_action,NULL); open_softirq(NET_TX_SOFTIRQ,
net_rx_action,NULL),; 軟中斷處理程序的執(zhí)行的時(shí)候,,允許響應(yīng)中斷,但自己不能睡眠,。 3,、 觸發(fā)你的軟中斷: 通過(guò)在枚舉類型的列表中添加新項(xiàng)以及調(diào)用open_softirq()進(jìn)行注冊(cè)以后,新的軟中斷處理程序就能夠運(yùn)行,。raise_softirq()函數(shù)可以將一個(gè)軟中斷設(shè)置為掛起狀態(tài),,讓他在下次調(diào)用do_softirq()函數(shù)時(shí)投入運(yùn)行。一個(gè)例子: raise_softirq(NET_TX_SOFTIRQ); 這會(huì)觸發(fā)NET_TX_SOFTIRQ軟中斷,。它的處理程序net_tx_action()就會(huì)在內(nèi)核下一次執(zhí)行軟中斷時(shí)投入運(yùn)行,。該函數(shù)在觸發(fā)一個(gè)軟中斷前要
禁止中斷,觸發(fā)后再恢復(fù)回原來(lái)的狀態(tài)。在中斷處理程序中觸發(fā)軟中斷是最常見的形式,。這樣,,內(nèi)核在執(zhí)行完中斷處理程序后,馬上就會(huì)調(diào)用do_softirq,。于是軟中斷開始執(zhí)行中斷處理程序留給它去完成的剩余任務(wù),。 6.3 Tasklet tasklet是利用軟中斷實(shí)現(xiàn)的一種下半部機(jī)制。它和進(jìn)程沒(méi)有任何關(guān)系,。它和軟中
斷本質(zhì)上很相似,,行為表現(xiàn)也相近,但是,,它的接口更簡(jiǎn)單,鎖保護(hù)也要求較低,。 軟中斷和tasklet怎樣選擇呢,? 通常你應(yīng)該用tasklet,軟中斷一般用的很少,,它只在那些執(zhí)行頻率很高和連續(xù)性要求很高的情
況下才需要,,而tasklet卻有更廣泛的用途。 因?yàn)?/font>tasklet是通過(guò)軟中斷實(shí)現(xiàn)的,,所以它本身也是軟中斷,。 (1)tasklet結(jié)構(gòu)體:tasklet由tasklet_struct結(jié)構(gòu)表示。每個(gè)結(jié)構(gòu)體單獨(dú)代表一個(gè)tasklet,,它在<linux/interrupt.h>中定義: struct tasklet_struct { struct task_struct *next; /*指向鏈表中的下一個(gè)tasklet
*/ unsigned long state; /* tasklet的狀態(tài)
*/ atomic_t count; /* 引用計(jì)數(shù)器
*/ void (*func) (unsigned long); /* tasklet處理函數(shù)
*/ unsigned long data; /*給tasklet處理函數(shù)的參數(shù)
*/ },; 結(jié)構(gòu)體中的func成員是tasklet的處理程序,data是它唯一的參數(shù),。state成員只能在0,、TASKLET_STATE_SCHED和TASKLET_STATE_RUN之間取值。TASKLET_STATE_SCHED表明tasklet已經(jīng)被調(diào)度,,正準(zhǔn)備投入運(yùn)行,,TASKLET_STATE_RUN表示該tasklet正在運(yùn)行。只有count為0時(shí),,tasklet才被激活,,否則被允許,不允許執(zhí)行,。 調(diào)度tasklet 已調(diào)度的tasklet存放在兩個(gè)單處理器數(shù)據(jù)結(jié)構(gòu):tasklet_vec和task_hi_vec中,。它們都是由tasklet_struct結(jié)構(gòu)體構(gòu)成的鏈表。鏈表中的每個(gè)tasklet_struct代表一個(gè)不同的tasklet,。 tasklet是由tasklet_schedule()和tasklet_hi_schedule()函數(shù)進(jìn)行調(diào)度的,,它們接受一個(gè)指向tasklet_struct結(jié)構(gòu)的指針作為參數(shù)。 Tasklet的實(shí)現(xiàn)通過(guò)軟中斷來(lái)實(shí)現(xiàn)的,tasklet_schedule()調(diào)度函數(shù)執(zhí)行一些初始工作,,緊接著喚起TASKLET_SOFTIRQ或HI_SOFTIRQ軟中斷,,這樣在下一次調(diào)用do_softirq()時(shí)就會(huì)執(zhí)行tasklet。 那么do_softirq()函數(shù)什么時(shí)候執(zhí)行呢,? do_softirq()會(huì)盡可能早的在下一個(gè)合適的時(shí)機(jī)執(zhí)行,,由于大部分tasklet和軟中斷都是在中斷處理程序中被設(shè)置成待處理狀態(tài),所以最近一個(gè)中斷
返回的時(shí)候看起來(lái)就是執(zhí)行do_softirq()的最佳時(shí)機(jī),。因?yàn)?/span>TASKLET_SOFTIRQ和HI_SOFTIRQ已經(jīng)被觸發(fā)了,,所以do_softirq會(huì)執(zhí)行相應(yīng)的軟中斷處理程序。 Tasklet_action()和Tasklet_hi_action()兩個(gè)處理程序就是tasklet處理的核心,。 總結(jié):所有的Tasklets都通過(guò)重復(fù)運(yùn)用TASKLET_SOFTIRQ或HI_SOFTIRQ這兩個(gè)軟中斷實(shí)現(xiàn),,當(dāng)一個(gè)tasklet被調(diào)度時(shí),內(nèi)核就會(huì)喚起這兩個(gè)軟中斷中的一個(gè),,隨后,,該軟中斷會(huì)被特
定的函數(shù)處理,執(zhí)行所有已調(diào)度的tasklet,,這個(gè)函數(shù)保證同一時(shí)間里只有一個(gè)給定類別的tasklet會(huì)被執(zhí)行(但其它不同類型的tasklet可以同時(shí)執(zhí)行),,所有這些復(fù)雜性都被一個(gè)簡(jiǎn)潔的接口隱藏起來(lái)了。 聲明你自己的tasklet 可以靜態(tài)創(chuàng)建,,也可以動(dòng)態(tài)創(chuàng)建,,分別對(duì)應(yīng)直接引用和間接引用。選擇哪種方式取決于你到底是有(或者是想要)一個(gè)對(duì)tasklet的直接引用還是間接引用,,靜態(tài)創(chuàng)建一個(gè)tasklet(也就是有一個(gè)直接引用),,使用下面<linux/interrupt.h>中定義的兩個(gè)宏中的一個(gè): DECLARE_TASKLET(name,
func, data) 實(shí)現(xiàn)了定義名稱為name的tasklet并將其與func這個(gè)函數(shù)綁定,而傳入這個(gè)函數(shù)的參數(shù)為data,。 DECLARE_TASKLET_DISABLED(name, func, data); DECLARE_TASKLET(my_tasklet, my_tasklet_handler, dev);運(yùn)行代碼實(shí)際上等價(jià)于: struct
tasklet_struct my_tasklet = { NULL, 0, ATOMIC_INIT(0),
my_tasklet_handler, dev }; 這樣就創(chuàng)建了一個(gè)名為my_tasklet,處理程序?yàn)?/span>tasklet_handler并且已經(jīng)被激活的tasklet,。 還可以通過(guò)一個(gè)間接引用(一個(gè)指針)賦給一個(gè)動(dòng)態(tài)創(chuàng)建的tasklet_struct結(jié)構(gòu)的方式來(lái)初始化一個(gè)tasklet: Tasklet_init(t,tasklet_handler,dev);/*動(dòng)態(tài)而不是靜態(tài)創(chuàng)建*/ 編寫你自己的tasklet處理程序 必須符合規(guī)定的函數(shù)類型:
void tasklet_handler(unsigned long data) 因?yàn)槭强寇浖袛鄬?shí)現(xiàn),所以tasklet不能睡眠,,這意味著你不能在tasklet中使用信號(hào)量或者其它什么阻塞式的函數(shù),。 調(diào)度你自己的tasklet 通過(guò)調(diào)用task_schedule()函數(shù)并傳遞給它相應(yīng)的tasklet_struct的指針,該tasklet就會(huì)被調(diào)度以便執(zhí)行,。 tasklet_schedule(&my_tasklet); /*把my_tasklet標(biāo)記為掛起*/ 下面我們看一下軟中斷和tasklet的異同: 在前期準(zhǔn)備工作上,,首先要給軟中斷分配索
引,而tasklet則要用宏對(duì)處理程序聲明,。在給軟中斷分配索引后,,還要通過(guò)open_softirq()函數(shù)來(lái)注冊(cè)處理程序。這樣看來(lái),,tasklet是一步到位,,直接到了處理函數(shù),,而軟中斷需要做更多工作。接
下來(lái)軟中斷要等待觸發(fā)(raise_softirq()或raise_softirq_irqoff),而tasklet則是等待tasklet_schedule()和tasklet_hi_schedule()對(duì)其進(jìn)行調(diào)度,。兩者雖然在命名上不同,,但殊途同歸,最終的結(jié)
果都是等待do_softirq()去執(zhí)行處理函數(shù),,即將下半部設(shè)置為待執(zhí)行狀態(tài)以便稍后執(zhí)行,。另外,在tasklet的tasklet_schedule()中,,需要完成的動(dòng)作之一便是喚起(觸發(fā))TASKLET_SOFTIRQ或HI_SOFTIRQ軟中斷,,說(shuō)明tasklet仍然是基于軟中斷的。在進(jìn)入do_softirq()之后,,所做的工作仍然有所不同,,不再論述。 軟中斷和工作隊(duì)列
都是異步發(fā)生的(就是說(shuō),,在中斷處理返回的時(shí)候) 6.4工作隊(duì)列 工作隊(duì)列(work queue)是另外一種將工作推后執(zhí)行的形式,,他和我們前面討論過(guò)的其他形式完全不同。工作隊(duì)列可以把工作推后,,交由一
個(gè)內(nèi)核線程去執(zhí)行——這
個(gè)下半部總是會(huì)在進(jìn)程上下文執(zhí)行。這樣,,通
過(guò)工作隊(duì)列執(zhí)行的代碼能占盡進(jìn)程上下文的所有優(yōu)勢(shì),,最重要的是工作隊(duì)列允許重新調(diào)度甚至是睡眠。 如果你需要用一個(gè)可以重新調(diào)度的實(shí)體來(lái)執(zhí)行你的下半部處理,,你應(yīng)該使用工作
隊(duì)列,,它是唯一能在進(jìn)程上下文運(yùn)行的下半部實(shí)現(xiàn)的機(jī)制,也只有它才可以睡眠,,這意味著你在你需要獲得大量的內(nèi)存時(shí),,在你需要獲取信號(hào)量時(shí),在你需要執(zhí)行阻塞式的IO操作時(shí),,它都會(huì)非常有用,,如果你不需要用一個(gè)內(nèi)核線程來(lái)推后執(zhí)行工作,那么就考慮使用tasklet吧,! 工作隊(duì)列子系統(tǒng)是一個(gè)用于創(chuàng)建內(nèi)核線程的接口,,通過(guò)它創(chuàng)建的進(jìn)程負(fù)責(zé)執(zhí)
行由內(nèi)核其他部分排到隊(duì)列里的其他任務(wù)。它創(chuàng)建的這些內(nèi)核線程被稱作工作者線程,。工作隊(duì)列可以讓驅(qū)動(dòng)程序創(chuàng)建一個(gè)專門的工作者線程來(lái)處理需要推后的工作,。
不過(guò),,工作隊(duì)列子系統(tǒng)提供了一個(gè)默認(rèn)的工作者線程來(lái)處理這些工作。因此,工作隊(duì)列最基本的表現(xiàn)形式就轉(zhuǎn)變成了一個(gè)把需要推后執(zhí)行的任務(wù)交給特定的通用線程
這樣一個(gè)接口,。 表示線程的數(shù)據(jù)
結(jié)構(gòu) 工作者線程用workqueue_struct結(jié)構(gòu)表示: struct workqueue_struct { struct cpu_workqueue_struct cpu_wq[NR_CPUS]; const char *name; struct list_head list; }; 該結(jié)構(gòu)內(nèi)是一個(gè)由cpu_workqueue_struct結(jié)構(gòu)組成的數(shù)組,,定義在kernel/workqueue.c中,數(shù)組的每一項(xiàng)對(duì)應(yīng)一個(gè)系統(tǒng)中的處理器。每個(gè)工作者線程都
對(duì)應(yīng)這樣的cpu_workqueue_struct結(jié)構(gòu)體,。cpu_workqueue_struct是kernel/workqueue.c中的核心數(shù)據(jù)結(jié)構(gòu):
struct cpu_workqueue_struct { spinlock_t lock; /* 鎖定以便保護(hù)該結(jié)構(gòu)體
*/ long romove_sequeue; /* 最近一個(gè)被加上的(下一個(gè)要運(yùn)行的)
*/ long insert_sequeue; /*下一個(gè)要加上的
*/ wait_queue_head_t more_work; wait_queue_head_t work_done; struct workqueue_struct *wq;
/* 有關(guān)聯(lián)的workqueue_struct結(jié)構(gòu)
*/ task_t
*thread; /* 有關(guān)聯(lián)的線程
*/ int
run_depth; /* run_workqueue()循環(huán)深度
*/ }; 由此可以看出,,每個(gè)工作者線程類型關(guān)聯(lián)一個(gè)
自己的workqueue_struct,。在該結(jié)構(gòu)體里面,給每個(gè)線程分配一個(gè)cpu_workqueue_struct,,因而也就是給每個(gè)處理器分配一個(gè),,因?yàn)槊總€(gè)處理器都有一個(gè)
該類型的線程。 表示工作的數(shù)據(jù)
結(jié)構(gòu) 所有工作者線程都是用普通的內(nèi)核線程實(shí)現(xiàn)的,,它們都要執(zhí)行worker_thread()函數(shù),。在它初始化完以后,這個(gè)函數(shù)(worker_thread)開始休眠,。當(dāng)有操作被插入到隊(duì)列的時(shí)候,,線程就會(huì)被喚醒,
以便執(zhí)行這些操作,。當(dāng)沒(méi)有剩余的操作時(shí),,它又會(huì)繼續(xù)睡眠。 工作用<linux/workqueue.h>中定義的work_struct結(jié)構(gòu)體表示:
struct work_struct { unsigned long pending; /* 這個(gè)工作是否正在等待處理
*/ struct list_head entry;
/* l連接所有工作的鏈表
*/ void (* func) (void *);
/* 處理函數(shù)
*/ void *wq_data;
/* 內(nèi)部使用
*/ struct timer_list timer;
/* 延遲的工作隊(duì)列所用到的定時(shí)
器 */ },; 這些結(jié)構(gòu)體被連接成鏈表,,在每個(gè)處理器的每種類型的隊(duì)列都對(duì)應(yīng)這樣一個(gè)
鏈表。當(dāng)一個(gè)工作者線程被喚醒時(shí),,它會(huì)執(zhí)行它的鏈表上的所有工作,。當(dāng)工作完畢時(shí),他會(huì)將相應(yīng)的work_struct對(duì)象從鏈表中移去,當(dāng)鏈表上不再有對(duì)象的時(shí)候,,它就會(huì)繼續(xù)睡眠,。 (1)創(chuàng)建推后的工作 首先要做的是實(shí)際創(chuàng)建一些需要推后執(zhí)行的工
作??梢酝ㄟ^(guò)DECLARE_WORK在編譯時(shí)靜態(tài)的創(chuàng)建該結(jié)構(gòu)體:
DECLARE_WORK(name, void (*func) (void *), void *data); 這樣就會(huì)靜態(tài)的創(chuàng)建一個(gè)名為name,,處理函數(shù)為func,參數(shù)為data的work_struct結(jié)構(gòu)體,。也可以在運(yùn)行時(shí)通過(guò)指針創(chuàng)建一個(gè)工作: INIT_WORK(struct work_struct *work, void (*func)(void *),
void *data); 這樣就動(dòng)態(tài)的初始化了一個(gè)由work指向的工作,。 (2)工作隊(duì)列的處理函數(shù) 原型是:void
work_handler(void *data) 這個(gè)函數(shù)會(huì)由一個(gè)工作者線程執(zhí)行,因此,,函
數(shù)會(huì)運(yùn)行在進(jìn)程上下文中,,默認(rèn)情況下,允許響應(yīng)中斷,,并且不持有任何鎖,,如果需要,,函數(shù)可以睡眠,注意的是,,盡管操作處理函數(shù)運(yùn)行在進(jìn)程上下文中,,但它不能訪問(wèn)用戶空
間,因?yàn)閮?nèi)核線程在用戶空間沒(méi)有相關(guān)的內(nèi)存映射,,通常在系統(tǒng)調(diào)用發(fā)生時(shí),,內(nèi)核會(huì)代表用戶空間的進(jìn)程運(yùn)行,此時(shí)它才能訪問(wèn)用戶空間,,也只有在此時(shí)它才會(huì)映射
用戶空間的內(nèi)存,。 (3)對(duì)工作進(jìn)行調(diào)度 現(xiàn)在工作已經(jīng)創(chuàng)建,我們可以調(diào)度它了,,要把
給定工作的處理函數(shù)提交給默認(rèn)的events工作線程,,只需調(diào)用:
schedule_work(&work); work馬上就會(huì)被調(diào)度,一旦其所在的處理器上的工作者線程被喚醒,,它就會(huì)被執(zhí)行,。 (4)刷新操作 刷新工作隊(duì)列的函數(shù)就是確保在卸載模塊之前,要確保一些操作已經(jīng)執(zhí)行完
畢了,,該函數(shù)如下: Void
flush_scheduled_work(void); 該函數(shù)會(huì)一直等待,,直到隊(duì)列中所有對(duì)象都被執(zhí)行以后才返回,在等待所以
待處理的工作執(zhí)行的時(shí)候,,該函數(shù)會(huì)進(jìn)入休眠狀態(tài),,所以只能在進(jìn)程上下文中使用它。 (5)創(chuàng)建新的工作隊(duì)列 當(dāng)缺省的隊(duì)列不能滿足你的需要時(shí),,你應(yīng)該創(chuàng)建一個(gè)新的工作隊(duì)列和與之對(duì)
應(yīng)的工作者線程。 6.5下半部之間的選擇 1 從設(shè)計(jì)的角度考慮 軟中斷提供的執(zhí)行序列化的保障最少,,這就要求軟中斷必須采取一些步驟確
保共享數(shù)據(jù)的安全,。如果被考察的代碼本身多線索化的工作就做得非常好,軟中斷就很好,,對(duì)于時(shí)間要求嚴(yán)格和執(zhí)行頻率很高的話,,它執(zhí)行的也快。如果代碼本身多
線索化的工作就做得不充分,,就選擇tasklet比較好,,由于兩個(gè)同種類型的tasklet不能同時(shí)執(zhí)行,實(shí)現(xiàn)起來(lái)也很簡(jiǎn)單一些,。 2 如果你需要把任務(wù)推到進(jìn)程上下文中完成,,只能選擇工作隊(duì)列。 如果不需要睡眠,,那么軟中斷和工作隊(duì)列就更合適,。工作隊(duì)列造成的開銷最
大,,因?yàn)樗獱砍兜絻?nèi)核線程甚至是上下文切換。 3 說(shuō)到易用性,,工作隊(duì)列最好,,使用缺省的events隊(duì)列簡(jiǎn)直不費(fèi)吹灰之力。接下來(lái)就tasklet,。他的的接口很簡(jiǎn)單,,最后才是軟中斷,它必須靜態(tài)創(chuàng)建,。 6.6在下半部之間加鎖 使用tasklet的一個(gè)好處是在于它自己負(fù)責(zé)執(zhí)行的序列化保障,,兩個(gè)相同類型的tasklet不允許同時(shí)執(zhí)行,即使在不同的處理器上也不行,,意味著你無(wú)須考慮相同
類型的tasklet內(nèi)部的同步問(wèn)題,。當(dāng)然,tasklet之間的同步(兩個(gè)不同類型的tasklet共享同一數(shù)據(jù)時(shí))需要正確使用鎖機(jī)制,。 因?yàn)檐浿袛喔静槐U蠄?zhí)行序列化,,(即使相同類型的軟中斷也有可能有兩
個(gè)實(shí)例在同時(shí)執(zhí)行)所以所有的共享數(shù)據(jù)都需要合適的鎖。 如果進(jìn)程上下文和一個(gè)下半部共享數(shù)據(jù),,在訪問(wèn)這些數(shù)據(jù)之前,,你需要禁止
下半部的處理并得到鎖的使用權(quán),所做的這些是為了本地和SMP的保護(hù)并且防止死鎖的出現(xiàn),。 如果中斷上下文和一個(gè)下半部共享數(shù)據(jù),,在訪問(wèn)數(shù)據(jù)之前,你需要禁止中斷
并得到鎖的使用權(quán),,所做的這些是為了本地和SMP的保護(hù)并且防止死鎖的出現(xiàn),。 任何在工作隊(duì)列中被共享的數(shù)據(jù)也需要使用鎖機(jī)制,其中有關(guān)鎖的要點(diǎn)和在
一般內(nèi)核代碼中沒(méi)什么區(qū)別,,因?yàn)楣ぷ麝?duì)列本來(lái)就是在進(jìn)程上下文中執(zhí)行的,。 禁止下半部 |
|