linux核心“任務”之非強制中斷、tasklet、工作者隊列__linux

來源:互聯網
上載者:User
一. 非強制中斷和Tasklets
1.中斷的延遲處理

在中斷的處理中,有一些不緊急不關鍵的任務在需要的時候可以被延遲一段時間來執行。對於中斷服務程式來說,一般情況下如果它不結束執行,就不應該產生新的中斷;而這些延遲的任務可以在開啟中斷的情形下執行,因而把它們從中斷服務程式中提取出來可以降低核心的回應時間。linux支援兩種非緊迫的、可中斷的核心功能: 可延遲函數(包括非強制中斷和tasklets )和通過工作隊列執行的函數。 非強制中斷和takslet是緊密相關的,tasklet在非強制中斷之上實現。但是二者也有區別:
非強制中斷是靜態分配的,而tasklet的分配和初始化可以在運行時進行。 非強制中斷可以並發的運行在多個CPU上,因而非強制中斷中的代碼應該是可重新進入的並且必須使用自旋鎖保護其資料結構。而相同類型的tasklet總是被串列的執行,也就是說不能在兩個CPU上同時運行相同類型的tasklet。但是不同類型的tasklet可以在不同的CPU上並發運行。由於tasklet的這個特性,因而tasklet中的函數不必是可重新進入的。 一般來說可延遲函數有以下幾種操作:
初始化:定義一個新的可延遲函數。一般這個工作在核心初始化或者模組載入時完成。 啟用:標記一個可延遲函數為pengding以便它在可延遲函數的下一輪調度中被執行。啟用可以在任何時候被執行。 屏蔽:有選擇的屏蔽一個可延遲函數,這樣即便它被啟用,核心也不執行它。 執行:執行一個pending的可延遲函數和同類型的其它所有的pending的可延遲函數。執行是在特定的時間進行的。 2.非強制中斷 Linux使用了有限的非強制中斷。在大多數情況下tasklets是足夠用的,而且它更容易編寫。

linux定義了如下的非強制中斷: HI_SOFTIRQ  TIMER_SOFTIRQ NET_TX_SOFTIRQ NET_RX_SOFTIRQ BLOCK_SOFTIRQ  BLOCK_IOPOLL_SOFTIRQ TASKLET_SOFTIRQ SCHED_SOFTIRQ HRTIMER_SOFTIRQ RCU_SOFTIRQ

這些值都是枚舉變數,從0開始,最大值為NR_SOFTIRQS,NR_SOFTIRQS表示系統中當前支援的非強制中斷的數目。這些枚舉變數也定義了對應的非強制中斷的優先順序,值越小優先順序越高。非強制中斷都存放在softirq_vec數組中,每個元素包括一個handler指標和一個當作參數的通用資料指標。

但是需要注意的是非強制中斷的優先順序只是定義了它們的執行順序,而不會影響它們相對於其它“任務”的優先順序,也不會影響它們被執行的頻度。 1.preempt_count

preempt_count欄位用於跟蹤核心搶佔和核心控制路徑的嵌套,它存放在每個進程描述父的thread_info欄位中。它分為不同的位元子欄位,每個子欄位有不同的含義: 0-7  : 核心搶佔計數器 (max value = 255) 8-15  :非強制中斷計數器Softirq counter (max value = 255). 16-27 :硬體中斷計數器Hardirq counter (max value = 4096) 28 :PREEMPT_ACTIVE標記 第一個子欄位記錄了顯式禁用本地CPU核心搶佔的次數,第二個子欄位記錄了可延遲函數被禁用的程度,第三個子欄位記錄了本地CPU上中斷處理常式的嵌套數。當不允許搶佔時,或者正處於中斷上下文時,必須禁止核心搶佔。有了該欄位後核心只需要檢查該欄位即可獲得當前核心的狀態。
2.非強制中斷的處理

每個CPU包含一個描述處於pending狀態的非強制中斷的32位元掩碼。它位於irq_cpustat_t資料結構的_ _softirq_pending域。核心會周期性的檢查是否有pengding的中斷需要處理,如果有就會進行處理,這種檢查是在特點的位置進行的(典型的位置是退出中斷處理時irq_exit以及ksoftirqd中)。在非強制中斷處理中,由於在執行一個非強制中斷函數時可能出現新的非強制中斷,因而為了保證可延遲函數的低延遲性,非強制中斷處理函數會一直運行到執行完所有pending的非強制中斷或者已經在非強制中斷處理中運行了一定的時間(3.9.4中是2ms)。如果在非強制中斷處理完成後仍有未處理的非強制中斷,ksoftirqd將會處理它(每個CPU都有一個ksoftirqd核心線程)。 非強制中斷運行時硬體中斷是開啟的,但是會在本地禁止軟體中斷,結果就是do_softirq在每個CPU上只能進入一次。在處理非強制中斷時,會在關閉中斷的情況下將本地CPU的pending的非強制中斷儲存到局部變數並且將本地CPU的非強制中斷掩碼清0,然後遍曆每個pending的非強制中斷並且先開啟本地中斷再執行非強制中斷函數,再關閉本地中斷,也就是說只有在調用中斷處理函數時本地中斷是開啟的(但是本地的非強制中斷十關閉的)。當把pending的處理完後,會重新讀取本地CPU的非強制中斷pending狀態,如果仍有pending的就繼續處理,直到處理完所有的pending的非強制中斷或已經在非強制中斷處理中運行了一定的時間。
3.非強制中斷相關函數 void open_softirq(int nr, void (*action)(struct softirq_action *))
該函數用於處理非強制中斷的初始化,action為非強制中斷處理函數,nr為非強制中斷號
void raise_softirq(unsigned int nr)
該函數用於啟用非強制中斷,nr為非強制中斷號
asmlinkage void do_softirq(void) 
該函數用於執行非強制中斷處理函數
2. Tasklets Tasklets是I/O驅動中實現可延遲函數的首選方法。tasklets是建立在兩個非強制中斷HI_SOFTIRQ 和TASKLET_SOFTIRQ之上的。多個tasklets可以和一個非強制中斷關聯起來,每個tasklet有自己的函數。除了HI_SOFTIRQ的 tasklets比TASKLET_SOFTIRQ的tasklets先被執行外,兩個類型的tasklet之間沒有別的區別。
1.tasklet的表示

Tasklets以及高優先順序的tasklets分別存放於tasklet_vec和tasklet_hi_vec數組中。這兩個數組都包含NR_CPUS個類型為tasklet_head的元素, 每一個元素都包含一個指向由tasklet描述符組成的鏈表的指標。tasklet描述符的欄位: next:指向列表中的下一個taskletPointer to next descriptor in the list state:tasklet的狀態 count:atomic_t類型的變數,如果其值不為0,則接下來執行待執行的tasklet時就不會再執行它。它用於保證tasklet在所有的CPU上只有一個執行個體在運行 func:tasklet的執行函數 data:tasklet執行函數的參數 Tasklet描述父的state域包含兩個標誌:
TASKLET_STATE_SCHED:設定了該標記時表示tasklet是處於pending狀態的 TASKLET_STATE_RUN:設定了該標誌表示該tasklet正在被執行 2.tasklet的執行

tasklet 是一個特殊的函數, 它在非強制中斷上下文被調度。它可能被調度運行多次,但是tasklet調度不累積,也就是即使在tasklet被執行之前請求了多次來執行該tasklet,它也只運行一次。不會有同一個tasklet的多個執行個體同時運行。但是tasklet可以與SMP系統上的其他tasklet並行運行。因此, 如果多個tasklet會使用相同的資源, 它們必須採取某類加鎖來避免彼此衝突。除非tasklet重新啟用自己,否則每次tasklet啟用只會運行一次。 在HI_SOFTIRQ 和TASKLET_SOFTIRQ的處理函數中,與當前CPU相關聯的tasklet_vec或tasklet_hi_vec元素會被儲存到局部變數,並且元素本身會被設定為NULL(在關閉本地中斷的情況下)。然後遍曆鏈表中的每一個元素,檢查是否已經是運行狀態(在其它CPU上),是否是禁止狀態,如果都不是,並且tasklet被啟用,tasklet就會被運行。
3. Tasklet相關API

API對應的標頭檔為linux/interrupt.h
DECLARE_TASKLET(name, func, data);聲明並定義一個Tasklet
DECLARE_TASKLET_DISABLED(name, func, data);聲明並定義一個Tasklet,且其初始狀態為禁止的
void tasklet_init(struct tasklet_struct *t, void (*func)(unsigned long), unsigned long data);
為了使用tasklet必須首選分配一個tasklet_struct的資料結構並使用tasklet_init來初始化它。
static inline void tasklet_disable(struct tasklet_struct *t)
它用于禁止tasklet
static inline void tasklet_enable(struct tasklet_struct *t)
它用於啟用tasklet
static inline void tasklet_schedule(struct tasklet_struct *t)
static inline void tasklet_hi_schedule(struct tasklet_struct *t)
它們用於啟用tasklet,一個用於高優先順序非強制中斷相關聯的tasklet,一個用於正常的非強制中斷相關聯的tasklet。
void tasklet_kill(struct tasklet_struct *t); 用於確保tasklet不再被調度執行,通常用在裝置要關閉或模組要退出是。如果tasklet正在被調度執行,則該函數會先等待其執行完成,然後再開始自己的動作。如果tasklet會重新調度自己,則應該在重新調度時做某些判斷,以防止永遠無法kill掉。tasklet_kill的代碼如下:

void tasklet_kill(struct tasklet_struct *t){        if (in_interrupt())                printk("Attempt to kill tasklet from interrupt\n");        while (test_and_set_bit(TASKLET_STATE_SCHED, &t->state)) {                do {                        yield();                } while (test_bit(TASKLET_STATE_SCHED, &t->state));        }        tasklet_unlock_wait(t);        clear_bit(TASKLET_STATE_SCHED, &t->state);}

tasklet執行時的核心代碼如下:

                if (tasklet_trylock(t)) {                        if (!atomic_read(&t->count)) {                                if (!test_and_clear_bit(TASKLET_STATE_SCHED, &t->state))                                        BUG();                                trace_irq_tasklet_low_entry(t);                                t->func(t->data);                                trace_irq_tasklet_low_exit(t);                                tasklet_unlock(t);                                continue;                        }                        tasklet_unlock(t);                }

二、workqueue

1.工作隊列的概念 工作隊列用於替換任務隊列。它們都允許核心功能被啟用,之後由特殊的稱為工作者線程的特殊核心線程執行。
工作隊列和可延遲函數非常相似,但是也有不同:
可延遲函數運行於中斷上下文,工作隊列運行於進程上下文 工作隊列可以阻塞,但是可延遲函數不能,因為只有在進程上下文運行時才能執行可阻塞函數(中斷上下文不可能發生進程切換) 類似於可延遲函數,工作隊列中的函數也不能訪問進程的使用者地址空間。而且由於工作隊列由核心線程來執行,因而也不存在使用者態地址空間。
使用工作隊列時需要先定義工作隊列,然後將需要順延強制的函數插入到工作隊列。每個工作者線程在worker_thread函數內部迴圈執行,大部分時間該線程都處於睡眠狀態並等待某些工作被插入到隊列。背景工作執行緒一旦被喚醒就調用run_workqueue()函數,該函數從工作者線程的工作隊列鏈表中刪除所有的work_struct描述父並執行相應的函數。工作者線程可以阻塞,並且可以睡眠。
系統預定義了一些工作隊列,大部分情況下都可以直接使用它:
預定義工作隊列函數                  等價的標準工作隊列函數
schedule_work(w)                   queue_work(keventd_wq,w)
schedule_delayed_work(w,d)         queue_delayed_work(keventd_wq,w,d) (on any CPU)
schedule_delayed_work_on(cpu,w,d)  queue_delayed_work(keventd_wq,w,d) (on a given CPU)
flush_scheduled_work( )            flush_workqueue(keventd_wq)
但是需要注意的是如果你的函數會長期阻塞則不要使用預定義工作隊列,因為這會對使用預定義隊列的其它函數造成影響。

從執行的角度上說,workqueue中的函數是由核心線程調用的。

資料結構workqueue_struct用於表示工作隊列,work_struct用於定義需要由工作隊列(的線程)執行的任務。 2.相關API

API對應的標頭檔為 linux/workqueue.h 1.工作者隊列API

create_workqueue(name)
這實際上是一個宏,它返回新建立的工作隊列的描述父地址。該函數還建立n個工作者線程(n是系統中CPU數目),並根據name為工作者線程命名。
create_singlethread_workqueue(name)
這也是一個宏,它完成和create_workqueue類似的工作,但是只建立一個工作者線程。
void destroy_workqueue(struct workqueue_struct *wq);
該函數用於銷毀工作者隊列,其參數為指向工作者隊列資料結構的指標。
2.工作相關API DECLARE_WORK(name, void (*function)(void *), void *data);
INIT_WORK(struct work_struct *work, void (*function)(void *), void *data);
PREPARE_WORK(struct work_struct *work, void (*function)(void *), void *data); 以上三個API用於初始化“工作”結構
int queue_work(struct workqueue_struct *wq, struct work_struct *work);
該函數用於將函數插入到工作隊列,wq為工作隊列資料結構指標,work為“工作”。
int queue_work_on(int cpu, struct workqueue_struct *wq, struct work_struct *work);
類似於queue_work,但是指定了該函數由那個CPU執行。
int queue_delayed_work(struct workqueue_struct *wq, struct delayed_work *work, unsigned long delay);
該函數在指定的時間後將函數插入到工作者隊列
int queue_delayed_work_on(int cpu, struct workqueue_struct *wq, struct delayed_work *work, unsigned long delay);
類似於queue_delayed_work,但是指定了該函數由哪個CPU執行
void flush_workqueue(struct workqueue_struct *wq);
該函數強制工作隊列中的在調用該函數之前就被加入到其中的所有函數都被執行並且在它們都執行完之前一直阻塞

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.