Linux 设备驱动开发 —— Tasklets 机制浅析
一 、Tasklets 機(jī)制基礎(chǔ)知識點
1、Taklets 機(jī)制概念
? ? ??Tasklets 機(jī)制是linux中斷處理機(jī)制中的軟中斷延遲機(jī)制。通常用于減少中斷處理的時間,將本應(yīng)該是在中斷服務(wù)程序中完成的任務(wù)轉(zhuǎn)化成軟中斷完成。
? ? ? 為了最大程度的避免中斷處理時間過長而導(dǎo)致中斷丟失,有時候我們需要把一些在中斷處理中不是非常緊急的任務(wù)放在后面執(zhí)行,而讓中斷處理程序盡快返回。在老版本的 linux 中通常將中斷處理分為 top half handler 、 bottom half handler 。利用 top half handler 處理中斷必須處理的任務(wù),而 bottom half handler 處理不是太緊急的任務(wù)。
? ? ? 但是 linux2.6 以后的 linux 采取了另外一種機(jī)制,就是軟中斷來代替 bottom half handler 的處理。而 tasklet 機(jī)制正是利用軟中斷來完成對驅(qū)動 bottom half 的處理。 Linux2.6 中軟中斷通常只有固定的幾種: HI_SOFTIRQ( 高優(yōu)先級的 tasklet ,一種特殊的 tasklet) 、 TIMER_SOFTIRQ (定時器)、 NET_TX_SOFTIRQ (網(wǎng)口發(fā)送)、 NET_RX_SOFTIRQ (網(wǎng)口接收) 、 BLOCK_SOFTIRQ (塊設(shè)備)、 TASKLET_SOFTIRQ (普通 tasklet )。當(dāng)然也可以通過直接修改內(nèi)核自己加入自己的軟中斷,但是一般來說這是不合理的,軟中斷的優(yōu)先級比較高,如果不是在內(nèi)核處理頻繁的任務(wù)不建議使用。通常驅(qū)動用戶使用 tasklet 足夠了。
? ? ? 機(jī)制流程:當(dāng)linux接收到硬件中斷之后,通過 tasklet 函數(shù)來設(shè)定軟中斷被執(zhí)行的優(yōu)先程度從而導(dǎo)致軟中斷處理函數(shù)被優(yōu)先執(zhí)行的差異性。
? ? ? 特點:tasklet的優(yōu)先級別較低,而且中斷處理過程中可以被打斷。但被打斷之后,還能進(jìn)行自我恢復(fù),斷點續(xù)運(yùn)行。
2、Tasklets 解決什么問題?
a -- tasklet是I/O驅(qū)動程序中實現(xiàn)可延遲函數(shù)的首選方法;
b -- tasklet和工作隊列是延期執(zhí)行工作的機(jī)制,其實現(xiàn)基于軟中斷,但他們更易于使用,因而更適合與設(shè)備驅(qū)動程序...tasklet是“小進(jìn)程”,執(zhí)行一些迷你任務(wù),對這些人物使用全功能進(jìn)程可能比較浪費(fèi)。
c -- tasklet是并行可執(zhí)行(但是是鎖密集型的)軟件中斷和舊下半?yún)^(qū)的一種混合體,這里既談不上并行性,也談不上性能。引入tasklet是為了替代原來的下半?yún)^(qū)。
? ? ??軟中斷是將操作推遲到未來時刻執(zhí)行的最有效的方法。但該延期機(jī)制處理起來非常復(fù)雜。因為多個處理器可以同時且獨(dú)立的處理軟中斷,同一個軟中斷的處理程序可以在幾個CPU上同時運(yùn)行。對軟中斷的效率來說,這是一個關(guān)鍵,多處理器系統(tǒng)上的網(wǎng)絡(luò)實現(xiàn)顯然受惠于此。但處理程序的設(shè)計必須是完全可重入且線程安全的。另外,臨界區(qū)必須用自旋鎖保護(hù)(或其他IPC機(jī)制),而這需要大量審慎的考慮。
? ? ?我自己的理解,由于軟中斷以ksoftirqd的形式與用戶進(jìn)程共同調(diào)度,這將關(guān)系到OS整體的性能,因此軟中斷在Linux內(nèi)核中也僅僅就幾個(網(wǎng)絡(luò)、時鐘、調(diào)度以及Tasklet等),在內(nèi)核編譯時確定。軟中斷這種方法顯然不是面向硬件驅(qū)動的,而是驅(qū)動更上一層:不關(guān)心如何從具體的網(wǎng)卡接收數(shù)據(jù)包,但是從所有的網(wǎng)卡接收的數(shù)據(jù)包都要經(jīng)過內(nèi)核協(xié)議棧的處理。而且軟中斷比較“硬”——數(shù)量固定、編譯時確定、操作函數(shù)必須可重入、需要慎重考慮鎖的問題,不適合驅(qū)動直接調(diào)用,因此Linux內(nèi)核為驅(qū)動直接提供了一種使用軟中斷的方法,就是tasklet。
軟中斷和 tasklet 的關(guān)系如下圖:
? ? ? ?上圖可以看出, ksoftirqd 是一個后臺運(yùn)行的內(nèi)核線程,它會周期的遍歷軟中斷的向量列表,如果發(fā)現(xiàn)哪個軟中斷向量被掛起了( pend ),就執(zhí)行對應(yīng)的處理函數(shù),對于 tasklet 來說,此處理函數(shù)就是 tasklet_action ,這個處理函數(shù)在系統(tǒng)啟動時初始化軟中斷的就掛接了。Tasklet_action 函數(shù),遍歷一個全局的 tasklet_vec 鏈表(此鏈表對于 SMP 系統(tǒng)是每個 CPU 都有一個),此鏈表中的元素為 tasklet_struct 。下面將介紹各個函數(shù)
二、tasklet數(shù)據(jù)結(jié)構(gòu)
? ? ? ? tasklet通過軟中斷實現(xiàn),軟中斷中有兩種類型屬于tasklet,分別是級別最高的HI_SOFTIRQ和TASKLET_SOFTIRQ。
? ? ? ?Linux內(nèi)核采用兩個PER_CPU的數(shù)組tasklet_vec[]和tasklet_hi_vec[]維護(hù)系統(tǒng)種的所有tasklet(kernel/softirq.c),分別維護(hù)TASKLET_SOFTIRQ級別和HI_SOFTIRQ級別的tasklet:
[cpp]?view plaincopy
tasklet的核心結(jié)構(gòu)體如下(include/linux/interrupt.h):
[cpp]?view plaincopy
各成員的含義如下:
a -- next指針:指向下一個tasklet的指針。
b -- state:定義了這個tasklet的當(dāng)前狀態(tài)。這一個32位的無符號長整數(shù),當(dāng)前只使用了bit[1]和bit[0]兩個狀態(tài)位。其中,bit[1]=1表示這個tasklet當(dāng)前正在某個CPU上被執(zhí)行,它僅對SMP系統(tǒng)才有意義,其作用就是為了防止多個CPU同時執(zhí)行一個tasklet的情形出現(xiàn);bit[0]=1表示這個tasklet已經(jīng)被調(diào)度去等待執(zhí)行了。對這兩個狀態(tài)位的宏定義如下所示(interrupt.h)
[cpp]?view plaincopy
TASKLET_STATE_SCHED置位表示已經(jīng)被調(diào)度(掛起),也意味著tasklet描述符被插入到了tasklet_vec和tasklet_hi_vec數(shù)組的其中一個鏈表中,可以被執(zhí)行。TASKLET_STATE_RUN置位表示該tasklet正在某個CPU上執(zhí)行,單個處理器系統(tǒng)上并不校驗該標(biāo)志,因為沒必要檢查特定的tasklet是否正在運(yùn)行。
c -- 原子計數(shù)count:對這個tasklet的引用計數(shù)值。NOTE!只有當(dāng)count等于0時,tasklet代碼段才能執(zhí)行,也即此時tasklet是被使能的;如果count非零,則這個tasklet是被禁止的。任何想要執(zhí)行一個tasklet代碼段的人都首先必須先檢查其count成員是否為0。
d -- 函數(shù)指針func:指向以函數(shù)形式表現(xiàn)的可執(zhí)行tasklet代碼段。
e -- data:函數(shù)func的參數(shù)。這是一個32位的無符號整數(shù),其具體含義可供func函數(shù)自行解釋,比如將其解釋成一個指向某個用戶自定義數(shù)據(jù)結(jié)構(gòu)的地址值。
三、tasklet操作接口
? ? ? ?tasklet對驅(qū)動開放的常用操作包括:
a -- 初始化,tasklet_init(),初始化一個tasklet描述符。
b -- 調(diào)度,tasklet_schedule()和tasklet_hi_schedule(),將taslet置位TASKLET_STATE_SCHED,并嘗試激活所在的軟中斷。
c -- 禁用/啟動,tasklet_disable_nosync()、tasklet_disable()、task_enable(),通過count計數(shù)器實現(xiàn)。
d -- 執(zhí)行,tasklet_action()和tasklet_hi_action(),具體的執(zhí)行軟中斷。
e -- 殺死,tasklet_kill()
? ? ? ?即驅(qū)動程序在初始化時,通過函數(shù)task_init建立一個tasklet,然后調(diào)用函數(shù)tasklet_schedule將這個tasklet放在 tasklet_vec鏈表的頭部,并喚醒后臺線程ksoftirqd。當(dāng)后臺線程ksoftirqd運(yùn)行調(diào)用__do_softirq時,會執(zhí)行在中斷向量表softirq_vec里中斷號TASKLET_SOFTIRQ對應(yīng)的tasklet_action函數(shù),然后tasklet_action遍歷 tasklet_vec鏈表,調(diào)用每個tasklet的函數(shù)完成軟中斷操作。
1、tasklet_int()函數(shù)實現(xiàn)如下(kernel/softirq.c)
? ? ?用來初始化一個指定的tasklet描述符
[cpp]?view plaincopy
2、tasklet_schedule()函數(shù)
? ? ? 與tasklet_hi_schedule()函數(shù)的實現(xiàn)很類似,這里只列tasklet_schedule()函數(shù)的實現(xiàn)(kernel/softirq.c),都挺明白就不描述了:
[cpp]?view plaincopy
該函數(shù)的參數(shù)t指向要在當(dāng)前CPU上被執(zhí)行的tasklet。對該函數(shù)的NOTE如下:
a -- 調(diào)用test_and_set_bit()函數(shù)將待調(diào)度的tasklet的state成員變量的bit[0]位(也即TASKLET_STATE_SCHED位)設(shè)置為1,該函數(shù)同時還返回TASKLET_STATE_SCHED位的原有值。因此如果bit[0]為的原有值已經(jīng)為1,那就說明這個tasklet已經(jīng)被調(diào)度到另一個CPU上去等待執(zhí)行了。由于一個tasklet在某一個時刻只能由一個CPU來執(zhí)行,因此tasklet_schedule()函數(shù)什么也不做就直接返回了。否則,就繼續(xù)下面的調(diào)度操作。
b -- 首先,調(diào)用local_irq_save()函數(shù)來關(guān)閉當(dāng)前CPU的中斷,以保證下面的步驟在當(dāng)前CPU上原子地被執(zhí)行。
c -- 然后,將待調(diào)度的tasklet添加到當(dāng)前CPU對應(yīng)的tasklet隊列的首部。
d -- 接著,調(diào)用__cpu_raise_softirq()函數(shù)在當(dāng)前CPU上觸發(fā)軟中斷請求TASKLET_SOFTIRQ。
e -- 最后,調(diào)用local_irq_restore()函數(shù)來開當(dāng)前CPU的中斷。
3、tasklet_disable()函數(shù)、task_enable()函數(shù)以及tasklet_disable_nosync()函數(shù)(include/linux/interrupt.h)
? ? ??使能與禁止操作往往總是成對地被調(diào)用的
4、tasklet_action()函數(shù)在softirq_init()函數(shù)中被調(diào)用:
[cpp]?view plaincopy
tasklet_action()函數(shù)
[cpp]?view plaincopy注釋如下:
①首先,在當(dāng)前CPU關(guān)中斷的情況下,“原子”地讀取當(dāng)前CPU的tasklet隊列頭部指針,將其保存到局部變量list指針中,然后將當(dāng)前CPU的tasklet隊列頭部指針設(shè)置為NULL,以表示理論上當(dāng)前CPU將不再有tasklet需要執(zhí)行(但最后的實際結(jié)果卻并不一定如此,下面將會看到)。
②然后,用一個while{}循環(huán)來遍歷由list所指向的tasklet隊列,隊列中的各個元素就是將在當(dāng)前CPU上執(zhí)行的tasklet。循環(huán)體的執(zhí)行步驟如下:
a -- 用指針t來表示當(dāng)前隊列元素,即當(dāng)前需要執(zhí)行的tasklet。
b -- 更新list指針為list->next,使它指向下一個要執(zhí)行的tasklet。
c -- 用tasklet_trylock()宏試圖對當(dāng)前要執(zhí)行的tasklet(由指針t所指向)進(jìn)行加鎖
? ? ??如果加鎖成功(當(dāng)前沒有任何其他CPU正在執(zhí)行這個tasklet),則用原子讀函atomic_read()進(jìn)一步判斷count成員的值。如果count為0,說明這個tasklet是允許執(zhí)行的,于是:
? ?(1)先清除TASKLET_STATE_SCHED位;
? ?(2)然后,調(diào)用這個tasklet的可執(zhí)行函數(shù)func;
? ?(3)執(zhí)行barrier()操作;
? ?(4)調(diào)用宏tasklet_unlock()來清除TASKLET_STATE_RUN位。
? (5)最后,執(zhí)行continue語句跳過下面的步驟,回到while循環(huán)繼續(xù)遍歷隊列中的下一個元素。如果count不為0,說明這個tasklet是禁止運(yùn)行的,于是調(diào)用tasklet_unlock()清除前面用tasklet_trylock()設(shè)置的TASKLET_STATE_RUN位。
? ??如果tasklet_trylock()加鎖不成功,或者因為當(dāng)前tasklet的count值非0而不允許執(zhí)行時,我們必須將這個tasklet重新放回到當(dāng)前CPU的tasklet隊列中,以留待這個CPU下次服務(wù)軟中斷向量TASKLET_SOFTIRQ時再執(zhí)行。為此進(jìn)行這樣幾步操作:
? (1)先關(guān)CPU中斷,以保證下面操作的原子性。
? (2)把這個tasklet重新放回到當(dāng)前CPU的tasklet隊列的首部;
? (3)調(diào)用__cpu_raise_softirq()函數(shù)在當(dāng)前CPU上再觸發(fā)一次軟中斷請求TASKLET_SOFTIRQ;
? (4)開中斷。
c -- 最后,回到while循環(huán)繼續(xù)遍歷隊列。
5、tasklet_kill()實現(xiàn)
[cpp]?view plaincopy
四、一個tasklet調(diào)用例子
? ? ? ?找了一個tasklet的例子看一下(drivers/usb/atm,usb攝像頭),在其自舉函數(shù)usbatm_usb_probe()中調(diào)用了tasklet_init()初始化了兩個tasklet描述符用于接收和發(fā)送的“可延遲操作處理”,但此是并沒有將其加入到tasklet_vec[]或tasklet_hi_vec[]中:
[cpp]?view plaincopy? ? ? 在其銷毀接口usbatm_destroy_instance()中調(diào)用tasklet_kill()函數(shù),強(qiáng)行將該tasklet踢出調(diào)度隊列。
? ? ?從上述過程以及tasklet的設(shè)計可以看出,tasklet整體是這么運(yùn)行的:驅(qū)動應(yīng)該在其硬中斷處理函數(shù)的末尾調(diào)用tasklet_schedule()接口激活該tasklet;內(nèi)核經(jīng)常調(diào)用do_softirq()執(zhí)行軟中斷,通過softirq執(zhí)行tasket,如下圖所示。圖中灰色部分為禁止硬中斷部分,為保護(hù)軟中斷pending位圖和tasklet_vec鏈表數(shù)組,count的改變均為原子操作,count確保SMP架構(gòu)下同時只有一個CPU在執(zhí)行該tasklet:
總結(jié)
以上是生活随笔為你收集整理的Linux 设备驱动开发 —— Tasklets 机制浅析的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: nagios-3.4.3搭建
- 下一篇: Linux C 数据结构——队列