《Linux內核設計與實現》讀書筆記——下半部和推後執行的工作

中斷處理程序的侷限

中斷處理程序以異步方式執行,有可能打斷其它重要代碼;

  • 需要避免被打斷的代碼停止時間過長;

當前中斷處理程序正在執行時,其它中斷會被屏蔽;

  • 中斷處理程序執行越快越好;
  • 被屏蔽的中斷會在後續激活,而不是放棄;

中斷處理程序往往需要操作硬件;

  • 所以通常有很高的時限要求;

中斷處理程序不在進程上下文中運行;

  • 所以不能阻塞;

 

上半部與下半部

中斷處理程序(也叫上半部):一個快速、異步、簡單的機制負責對硬件做出迅速響應並完成那些要求很嚴格的操作;

  • 如果一個任務對時間非常敏感,將其放在中斷處理程序中執行
  • 如果一個任務和硬件相關,將其放在中斷處理程序中執行;
  • 如果一個任務要保證不被其他中斷(特別是相同的中斷)打斷,將其放在中斷處理程序中執行;

下半部:執行與中斷比密切相關但中斷處理程序本身不執行的工作;

  • 其他所有任務,考慮放在下半部執行。
  • 通常下半部在中斷處理程序一返回就會馬上執行;
  • 下半部的關鍵在於當它運行時允許響應所有的中斷;

 

中斷分類

BH:Bottom Half,老的下半部,不介紹。

任務隊列:老的下半部,不介紹。

軟中斷和tasklet

  • tasklet通過軟中斷實現;
  • 大部分下半部可以使用tasklet,只有對性能要求非常高的情況(比如網絡)才使用軟中斷;

工作隊列

 

軟中斷

軟中斷有softirq_action結構表示(include\linux\interrupt.h):

struct softirq_action
{
    void    (*action)(struct softirq_action *);
};

kernel\softirq.c中定義了一個包含有32個該結構體的數組:

static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;

目前(Linux 2.6.34)只使用了9個:

Linux 2.6.34.1中實際找到了10個(include\linux\interrupt.h):

/* PLEASE, avoid to allocate new softirqs, if you need not _really_ high
   frequency threaded job scheduling. For almost all the purposes
   tasklets are more than enough. F.e. all serial device BHs et
   al. should be converted to tasklets, not to softirqs.
 */
enum
{
    HI_SOFTIRQ=0,
    TIMER_SOFTIRQ,
    NET_TX_SOFTIRQ,
    NET_RX_SOFTIRQ,
    BLOCK_SOFTIRQ,
    BLOCK_IOPOLL_SOFTIRQ,
    TASKLET_SOFTIRQ,
    SCHED_SOFTIRQ,
    HRTIMER_SOFTIRQ,
    RCU_SOFTIRQ,    /* Preferable RCU should always be the last softirq */
    NR_SOFTIRQS
};

相比圖片中多了一個BLOCK_IOPOOL_SOFTIRQ,這也符合書中的說明,即新增的軟中斷可以插在BLOCK_SOFTIRQTASKLET_SOFTIRQ之間。

軟中斷是在編譯期間靜態分配的。

一個註冊的軟中斷必須在被標記後纔會執行,這被稱作觸發軟中斷

中斷處理程序會在返回前標記它的軟中斷,使其在稍後被執行。

之後,在合適的時刻,該軟中斷會被執行。

合適的時刻,比如:

  • 從一個硬件中斷代碼處返回時);
  • 在ksoftirqd內核線程中;
  • 在那些顯式檢查和執行待處理的軟中斷的代碼中,比如網絡子系統中;

不管是用什麼辦法喚起,軟中斷都要在do_softirq()中執行:

asmlinkage void do_softirq(void)
{
    __u32 pending;
    unsigned long flags;
    if (in_interrupt())
        return;
    local_irq_save(flags);
    pending = local_softirq_pending();
    if (pending)
        __do_softirq();
    local_irq_restore(flags);
}

在中斷處理程序中觸發軟中斷是最常見的形式。內核在執行完中斷處理程序之後,馬上就會調用do_softirq()函數,來執行中斷處理程序留給它去完成的剩餘任務。

raise_softirq()將一個軟中斷設置爲掛起狀態,讓它在下次調用do_softirq()函數時投入運行,下面是一個例子:

void run_local_timers(void)
{
    hrtimer_run_queues();
    raise_softirq(TIMER_SOFTIRQ);
    softlockup_tick();
}

對於軟中斷(包括tasklet),內核不會立即處理重新觸發的軟中斷,而作爲改進,當大量軟中斷出現時,內核會喚醒內核線程ksoftirqd來處理這些負載。

 

軟中斷使用

通過open_softirq()函數,往上面的幾種軟中斷中註冊處理函數(稱爲軟中斷處理程序)。以下是一個例子(block\blk-iopoll.c):

static __init int blk_iopoll_setup(void)
{
    int i;
    for_each_possible_cpu(i)
        INIT_LIST_HEAD(&per_cpu(blk_cpu_iopoll, i));
    open_softirq(BLOCK_IOPOLL_SOFTIRQ, blk_iopoll_softirq);
    register_hotcpu_notifier(&blk_iopoll_cpu_notifier);
    return 0;
}

軟中斷處理程序執行時,允許響應中斷,但它自己不能休眠。

在一個軟中斷處理程序運行的時候,當前處理器上的軟中斷被禁止,但是其它處理器仍可以執行別的(別的?但是下面不是說這個也可以嗎?)軟中斷。

如果同一個軟中斷在它被執行的同時再次被觸發,那麼另外一個處理器可以同時運行其處理程序。這意味着共享數據,所以需要做好鎖保護。實際上大部分軟中斷處理程序通過採取單處理器數據或者其它的一些技巧來避免顯式得加鎖。

如果不需要擴展到多個處理器,就使用tasklet,它的同一個處理程序的多個實例不能在多個處理器上同時運行

 

tasklet

tasklet是利用軟中斷實現的一種下半部機制。

相比軟中斷,更應該使用tasklet。

tasklet有兩個軟中斷代表:HI_SOFTIRQ和TASKLET_SOFTIRQ(前面的表中有說明,前者優先級高於後者)。

tasklet結構體:

struct tasklet_struct
{
    struct tasklet_struct *next;
    unsigned long state;
    atomic_t count;
    void (*func)(unsigned long);
    unsigned long data;
};

func成員是tasklet處理程序。

count爲0是tasklet才被激活,並在被設置爲掛起狀態時才能夠被執行。

stateTASKLET_STATE_SCHED時表示tasklet已被調度;TASKLET_STATE_RUN用於判斷tasklet是否在其它處理器上執行。

每個結構體單獨代表一個tasklet。已調度的tasklet(相當於被觸發的軟中斷)存放在兩個單處理器數據結構(前面提到過,它用於避免顯式地枷鎖)tasklet_vec和tasklet_hi_ver(對應兩個軟件中斷代碼),這兩個數據結構是由tasklet結構體構成的鏈表。

tasklet_schedule()和tasklet_hi_schedule()函數用來調度tasklet。

 

tasklet操作

靜態創建tasklet:

#define DECLARE_TASKLET(name, func, data) \
struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(0), func, data }
#define DECLARE_TASKLET_DISABLED(name, func, data) \
struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(1), func, data }

動態創建tasklet:

extern void tasklet_init(struct tasklet_struct *t,
void (*func)(unsigned long), unsigned long data);

調度tasklet:

static inline void tasklet_schedule(struct tasklet_struct *t)

禁止tasklet(會操作count成員):

static inline void tasklet_disable(struct tasklet_struct *t)

激活tasklet(會操作count成員):

static inline void tasklet_enable(struct tasklet_struct *t)

 

工作隊列

工作隊列可以把工作推後,交由一個內核線程去執行

這個下半部總是會在進程上下文中執行

如果推後執行的任務需要睡眠,那麼就需要選擇工作隊列。這樣的下半部可以:

  1. 獲取大量內存;
  2. 需要獲取信號量;
  3. 需要執行阻塞式的IO操作;

使用工作者線程來處理工作隊列。

內核會創建缺省工作者線程events/n,n表示處理器編號。

許多內核驅動程序都把它們的下半部交給缺省的工作線程去做。

處理器密集型和性能要求嚴格的任務會擁有自己的工作者線程。

工作者線程用workqueue_struct結構體表示:

struct workqueue_struct {
    struct cpu_workqueue_struct *cpu_wq;
    struct list_head list;
    const char *name;
    int singlethread;
    int freezeable;     /* Freeze threads during suspend */
    int rt;
#ifdef CONFIG_LOCKDEP
    struct lockdep_map lockdep_map;
#endif
};

由於每個處理器對應一個工作者線程,所以這裏還有一個cpu_workqueue_struct:

struct cpu_workqueue_struct {
    spinlock_t lock;
    struct list_head worklist;
    wait_queue_head_t more_work;
    struct work_struct *current_work;
    struct workqueue_struct *wq;
    struct task_struct *thread;
} ____cacheline_aligned;

worklist對應具體的工作列表,工作用work_struct表示:

struct work_struct {
    atomic_long_t data;
#define WORK_STRUCT_PENDING 0       /* T if work item pending execution */
#define WORK_STRUCT_STATIC  1       /* static initializer (debugobjects) */
#define WORK_STRUCT_FLAG_MASK (3UL)
#define WORK_STRUCT_WQ_DATA_MASK (~WORK_STRUCT_FLAG_MASK)
    struct list_head entry;
    work_func_t func;
#ifdef CONFIG_LOCKDEP
    struct lockdep_map lockdep_map;
#endif
};

工作隊列處理函數func

typedef void (*work_func_t)(struct work_struct *work);

工作者線程通過執行worker_thread()函數來執行具體的工作。

 

工作隊列的使用

靜態創建隊列:

#define DECLARE_WORK(n, f)                  \
    struct work_struct n = __WORK_INITIALIZER(n, f)

動態創建隊列:

#define INIT_WORK(_work, _func)                 \
    do {                            \
        __INIT_WORK((_work), (_func), 0);       \
    } while (0)

對工作進行調度:

extern int schedule_work(struct work_struct *work);

刷新工作隊列:

extern void flush_scheduled_work(void);

 

下半部的比較

 

下半部的禁止和使能

void local_bh_enable(void)
void local_bh_disable(void)

但是這些函數對工作隊列無效。

工作隊列沒有必要禁止,因爲是在進程上下文中執行的,不會涉及到異步執行的問題。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章