linux內核調度的機制 tasklet/workqueue/kthread_worker/kthreadx詳解及示例【轉】

轉自:https://blog.csdn.net/zxpblog/article/details/108539245

前言:
一直就感覺linux下面的任務調度機制太豐富了,由於各種調度機制平時工作中只是要用,理解並不是那麼深刻,所有有時候說不上道道來,只知道這個要用softirq/tasklet/workqueue/thread/, workqueue的優先級要設置成system_wq,system_highpri_wq, system_unbound_wq 或者thread 的SCHED_RR/SCHED_FIFO這樣子,說實話,現在我也不能保證說概述的很全很準確(有不對地方,歡迎大家指出)。就期待後面可以慢慢完善,讀者如果有建議補充的可以提建議,我們一起不斷更新這篇文章,一起努力可以把linux 線程相關的東西喫懂喫透,目的是爭取爲社區貢獻一篇好文章。
閒話少說:先簡述吧

第1章:linux常見的任務調度的機制
1.1 softirq(不允許休眠阻塞,中斷上下文)
軟終端支持SMP,同一個softirq可以在不同CPU同時運行,必須是可重入的,是編譯期間靜態分配的,不想tasklet一樣能被動態註冊,刪除(個人感覺因爲不方便,所以使用較少)。kernel/softirq.c文件中定義了一個包含32個softirq_action結構體的數組,每個被註冊的軟終端都佔據該數組的一項,因此最多可能有32個軟中斷。
特性:
1)一個軟中斷不會搶佔另一個軟中斷
2)唯一可以搶佔軟中斷的是ISR(中斷服務程序)
3)其它軟中斷可以在其它處理器同時執行
4)一個註冊的軟中斷必須被標記後才執行
5)軟中斷不可以自己休眠(不能自己調用sleep,wait等函數)
6)索引號小的軟中斷在索引號大的軟中斷之前執行

1.2 tasklet(不允許休眠阻塞,中斷上下文)
中斷服務程序一般都是在中斷請求關閉的條件下執行的,以避免嵌套而使中斷控制複雜化。但是,中斷是一個隨機事件,它隨時會到來,如果關中斷的時間太長,CPU就不能及時響應其他的中斷請求,從而造成中斷的丟失。因此,Linux內核的目標就是儘可能快的處理完中斷請求,盡其所能把更多的處理向後推遲。例如,假設一個數據塊到達觸發中斷時,中斷控制器接受到這個中斷請求信號時,Linux內核只是簡單地標誌數據到來了,然後讓處理器恢復到它以前運行的狀態,其餘的處理稍後再進行(如把數據移入一個緩衝區,接受數據的進程就可以在緩衝區找到數據)。因此,內核把中斷處理分爲兩部分:上半部(tophalf)和下半部(bottomhalf),上半部(就是中斷服務程序)內核立即執行,而下半部(就是一些內核函數)留着稍後處理,首先,一個快速的“上半部”來處理硬件發出的請求,它必須在一個新的中斷產生之前終止。通常,除了在設備和一些內存緩衝區(如果你的設備用到了DMA,就不止這些)之間移動或傳送數據,確定硬件是否處於健全的狀態之外,這一部分做的工作很少。下半部運行時是允許中斷請求的,而上半部運行時是關中斷的,這是二者之間的主要區別
中斷bai處理的tasklet(小任務)機制
特性:
1)不允許兩個相同的tasklet絕對不會同時執行,即使在不同CPU上。
2)從softirq衍生,但是使用簡單,效率高(較softirq低,較workqueue高),常用
3)在中斷期間運行時, 即使被多次調用也只會執行一次
4)SMP系統上,可以確保在第一個調用它的CPU執行

1.3 workqueue(允許休眠阻塞,進程上下文)
工作隊列(work queue)是另外一種將工作推後執行的形式,它和前面討論的tasklet有所不同。工作隊列可以把工作推後,交由一個內核線程去執行,也就是說,這個下半部分可以在進程上下文中執行。這樣,通過工作隊列執行的代碼能佔盡進程上下文的所有優勢。最重要的就是工作隊列允許被重新調度甚至是睡眠。
1)工作隊列會在進程上下文中執行
2)可以阻塞
3)可以重新調度
4)缺省工作者線程(kthrerad worker && kthread work)
5)在工作隊列和其它內核間用鎖和其它進程上下文一樣
6)默認允許響應中斷
7)默認不持有任何鎖
那麼,什麼情況下使用工作隊列,什麼情況下使用tasklet。如果推後執行的任務需要睡眠,那麼就選擇工作隊列。如果推後執行的任務不需要睡眠,那麼就選擇tasklet。另外,如果需要用一個可以重新調度的實體來執行你的下半部處理,也應該使用工作隊列。它是唯一能在進程上下文運行的下半部實現的機制,也只有它纔可以睡眠。這意味着在需要獲得大量的內存時、在需要獲取信號量時,在需要執行阻塞式的I/O操作時,它都會非常有用。如果不需要用一個內核線程來推後執行工作,那麼就考慮使用tasklet。

1.4 kthread
Linux內核可以看作一個服務進程(管理軟硬件資源,響應用戶進程的種種合理以及不合理的請求)。內核需要多個執行流並行,爲了防止可能的阻塞,支持多線程是必要的。內核線程就是內核的分身,一個分身可以處理一件特定事情。內核線程的調度由內核負責,一個內核線程處於阻塞狀態時不影響其他的內核線程,因爲其是調度的基本單位。這與用戶線程是不一樣的。因爲內核線程只運行在內核態,因此,它只能使用大於PAGE_OFFSET(3G)的地址空間。內核線程和普通的進程間的區別在於內核線程沒有獨立的地址空間,mm指針被設置爲NULL;它只在 內核空間運行,從來不切換到用戶空間去;並且和普通進程一樣,可以被調度,也可以被搶佔。
內核線程(thread)或叫守護進程(daemon),在操作系統中佔據相當大的比例,當Linux操作系統啓動以後,你可以用”ps -ef”命令查看系統中的進程,這時會發現很多以”d”結尾的進程名,確切說名稱顯示裏面加 "[]"的,這些進程就是內核線程。
內核線程和普通的進程間的區別在於內核線程沒有獨立的地址空間,它只在 內核空間運行,從來不切換到用戶空間去;並且和普通進程一樣,可以被調度,也可以被搶佔。讓模塊在加載後能一直運行下去的方法——內核線程。要創建一個內核線程有許多種方法。

第2章:linux常見的任務調度的優先級
2.1 workqueue(include/linux/workqueue.h)與waitequeue協作(include/linux/wait.h)
使用示例

定義workqueue

struct work_struct retreive_frame_work;
1
初始化workqueue和waitqueue及workqueue的callback的定義

INIT_WORK(&retreive_frame_work, retrieve_desc_task_callback);
init_waitqueue_head(&frame_wq);

void retrieve_desc_task_callback(struct work_struct *work){
   add_desc(iav, 0); //生產frame_desc
   wake_up_interruptible_all(&frame_wq); //通知消費者,frame_desc已經生產完畢,可以去取了
}

中斷ISR中:喚醒workqueue

queue_work(system_wq, &retreive_frame_work);
消費由 retrieve_desc_task_callback 生產的 frame_desc(blocking 的方式)

wait_event_interruptible(frame_wq, (desc = find_frame_desc()));

2.2 kthread(include/linux/kthread.h)
常見的優先級如下:SCHED_OTHER、SCHED_RR、SCHED_FIFO
本小節會主要介紹SCHED_RR和SCHED_FIFO這兩種調度策略,因爲SCHED_OTHER其實就是默認的調度機制,也就是說該咋咋滴,能被高優先級搶佔,其實優先級就是最低的,屬於金字塔食物鏈底端,誰都可以欺負它。誰優先級高就誰喫cpu cycle,同優先級就是linux來調度,輪着喫高優先級schedule 出來的cpu cycle。

SCHED_RR、SCHED_FIFO對比
對比才有傷害,是騾子是馬都得見婆婆,醜媳婦也得拉出來溜溜。先上圖再解析,直截了當。
說實話,我是真的懶,看到好圖直接盜圖了,開源精神有。如作者不允許到請聯繫我,我自己畫一個哈。
參考鏈接: 一個披着機器人工程師外套的程序員.
圖參考鏈接: cynchanpin.
SCHED_RR的任務調度時間片如下圖1

SCHED_FIFO任務調度時間片如下圖2

剖析:這裏內核創建了四個優先級相同的線程,對於SCHED_RR而言是每個任務有一個特定的時間片,輪轉依次運行,而SCHED_FIFO是一個任務執行完再去執行下一個任務(優先級高),其執行順序是創建的先後。SCHED_RR是依據時間片來調度線程的,當時間片用完後,無論該線程優先級多高,都不會再執行,而是進入就緒隊列,等待下一個時間片到來。只是圖1顯示,在thread5798時間片用完時,該線程緊接着進行了一次搶佔preemption。又獲得了一個時間片。順便提一句時間片長度的定位是linux憑經驗來的。即選擇儘可能長、同一時候能保持良好對應時間的一個時間片。

SCHED_FIFO:先進先出調度
SCHED_FIFO線程的優先級必須大於0,當它運行時,一定會搶佔正在運行的普通策略的線程(SCHED_OTHER, SCHED_IDLE, SCHED_BATCH);SCHED_FIFO策略是沒有時間片的算法,需要遵循以下規則:
1)如果一個SCHED_FIFO線程被高優先級線程搶佔了,那麼它將會被添加到該優先級等待列表的首部,以便當所有高優先級的線程阻塞的時候得到繼續運行;
2)當一個阻塞的SCHED_FIFO線程變爲可運行時,它將被加入到同優先級列表的尾部;
3)如果通過系統調用改變線程的優先級,則根據不同情況有不同的處理方式:
a)如果優先級提高了,那麼線程會被添加到所對應新優先級的尾部,因此,這個線程有可能會搶佔當前運行的同優先級的線程;
b)如果優先級沒變,那麼線程在列表中的位置不變;
c)如果優先級降低了,那麼它將被加入到新優先級列表的首部;
根據POSIX.1-2008規定,除了使用pthread_setschedprio(3)以外,通過使用其他方式改變策略或者優先級會使得線程加入到對應優先級列表的尾部;
4)如果線程調用了sched_yield(2),那麼它將被加入到列表的尾部;
SCHED_FIFO會一直運行,直到它被IO請求阻塞,或者被更高優先級的線程搶佔,亦或者調用了sched_yield();
5) 處於可運行狀態的SCHED_FIFO級的進程會比任何SCHED_NORMAL級的進程都先得到調用
6) 一旦一個SCHED_FIFO級進程處於可執行狀態,就會一直執行,直到它自己受阻塞或顯式地釋放處理器爲止,它不基於時間片,可以一直執行下去
7) 只有更高優先級的SCHED_FIFO或者SCHED_RR任務才能搶佔SCHED_FIFO任務
如果有兩個或者更多的同優先級的SCHED_FIFO級進程,它們會輪流執行,但是依然只有在它們願意讓出處理器時纔會退出
8) 只要有SCHED_FIFO級進程在執行,其他級別較低的進程就只能等待它變爲不可運行態後纔有機會執行

SCHED_RR:輪轉調度
SCHED_RR是SCHED_FIFO的簡單增強,除了對於線程佔用的時間總量之外,對於SCHED_FIFO適用的規則對於SCHED_RR同樣適用;如果SCHED_RR線程的運行時間大於等於時間總量,那麼它將被加入到對應優先級列表的尾部;如果SCHED_RR線程被搶佔了,當它繼續運行時它只運行剩餘的時間量;時間總量可以通過sched_rr_get_interval()函數獲取;
當SCHED_RR任務耗盡它的時間片時,在同一優先級的其他實時進程被輪流調度
時間片只用來重新調度同一優先級的進程
對於SCHED_FIFO進程, 優先級總是立即搶佔低優先級,但低優先級進程決不能搶佔SCHED_RR任務,即使它的時間片耗盡
SCHED_OTHER:默認Linux時間共享調度
SCHED_OTHER只能用於優先級爲0的線程,SCHED_OTHER策略是所有不需要實時調度線程的統一標準策略;調度器通過動態優先級來決定調用哪個SCHED_OTHER線程,動態優先級是基於nice值的,nice值隨着等待運行但是未被調度執行的時間總量的增長而增加;這樣的機制保證了所有SCHED_OTHER線程調度的公平性

MAX_RT_PRIO:實時優先級
1) 實時優先級範圍從0到MAX_RT_PRIO減1
2) 默認情況下,MAC_RT_RTIO爲100——所以默認的實時優先級範圍從0到99
3) SCHED_NORMAL級進程的noce值共享了這個取值空間。它的取值範圍從MAC_RT_PRIO到(MAX_RT_PRIO+40)。也就是說,在默認情況下,nice值從-20到+19直接對應的是從100到139的實時優先級範圍
4) struct sched_param param = { .sched_priority = MAX_RT_PRIO },最後優先級priority是越小越好,最後的值會轉換爲priority = n - sched_priority,所以sched_priority 越大,則其實優先級越高。

限制實時線程的CPU使用時間
SCHED_FIFO, SCHED_RR的線程如果內部是一個非阻塞的死循環,那麼它將一直佔用CPU,使得其它線程沒有機會運行;
在2.6.25以後出現了限制實時線程運行時間的新方式,可以使用RLIMIT_RTTIME來限制實時線程的CPU佔用時間;Linux也提供了兩個proc文件,用於控制爲非實時線程運行預留CPU時間;
/proc/sys/kernel/sched_rt_period_us
這個文件中的數值指定了總CPU(100%)時間的寬度值,默認值是1,000,000/proc/sys/kernel/sched_rt_runtime_us
這個文件中的數值指定了實時線程可以運行的CPU時間寬度,如果設置爲-1,則認爲不給非實時線程預留任何運行時間,默認值是950,000,因爲第一個文件的總量是1,000,000,也就是說默認配置爲非實時線程預留了5%的CPU時間;

使用示例
現在有一個需求kernel中,共一個SOF(start of frame)中斷喚醒thread A, thread A 去喚醒 thread B, thread A 和 B 執行完就分別進休眠狀態,從中斷到thread B 被喚醒必須在4ms內(即使在arm的loading很重的時候)(具體原因是碼流的idsp/iso/shutter/again/dgain等參數必須要在一幀內下進去,而sensor信號時I2C發送的,如果fps = 120,一幀數據傳輸+I2C stop和start信號之間的間隙約8ms,所以要儘可能間斷其時從SOF 中斷進來,到更新參數thread被喚醒的時間,現預估4ms內,實際應該更小),thread A 去apply參數,thread B 去update參數,具體流程是:

一開始有一個準備好的參數-》 SOF中斷trigger喚醒thread A 去apply 參數-》 thread A 去喚醒thread B 去更新參數:
同時有其它一個線程C一直更新參數,thread B 喚醒時,可以去拿參數。
初始化線程

inline void vin_create_kthreads(struct vin_device *vdev)
{
    struct sched_param sched_param = { .sched_priority = MAX_RT_PRIO / 2 };

    if (!thread_B.kthread) {
        sema_init(&sem_b, 0);
        thread_B.kthread = kthread_run(update_sht_agc_task, vdev, "update_sht_agc");
        sched_setscheduler(thread_B.kthread, SCHED_FIFO, &sched_param);
    }

    if (!thread_A.kthread) {
        sema_init(&sem_a, 0);
        thread_A.kthread = kthread_run(apply_sht_agc, vdev, "apply_sht_agc");
        sched_setscheduler(thread_A.kthread, SCHED_FIFO, &sched_param);
    }
    return ;
}

apply參數線程的結構體

struct apply_sht_agc {
    struct task_struct *kthread;
    struct semaphore sem;
    u32 exit_kthread : 1;
    u32 reserved0 : 31;
};

SOF中斷喚醒apply 參數線程

#include <linux/time.h>
long ns_irq = 0;
struct timespec64  tstart_irq;
idsp_sof_irq {
    atomic_set(&vinc->wait_sof, 0);
    ktime_get_real_ts64(&tstart_irq);
    ns_irq = tstart_irq.tv_nsec;
    up(&vsem_a);
}

apply參數線程,執行結束喚醒更新參數線程

#include <linux/time.h>
extern struct timespec64  tstart_irq;
extern long ns_irq;
long ns_task = 0;
u8 debug_times = 0;
int apply_shutter_agc_task(void *arg)
{
    while (!kthread_should_stop()) {
        if (thread_A.exit_kthread) {
            continue;
        }

        ktime_get_real_ts64(&tstart_irq);
        ns_task = tstart_irq.tv_nsec;
        if (ns_task - ns_irq > 2000000 || debug_times < 2) {
            iav_debug("ns_task = %ld, ns_irq = %ld\n", ns_task, ns_irq);
            debug_times++;
        }

        if (down_interruptible(&sem_a)) {
            continue;
        }
        //apply 參數
        up(&sem_b);
        wake_up_interruptible_all(&a->sht_agc_wq);
        return 0;
}

更新參數線程

int update_sht_agc_task(void *arg)
{
    while (!kthread_should_stop()) {
        if (thread_B->exit_kthread) {
            continue;
        }

        /* wait for wdr setting ioctl */
        if (down_interruptible(&sem_b)) {
            continue;
        }
       //更新參數
        }
    }
    return 0;
}

停止線程

inline void vin_dev_stop_kthreads(struct vin_device *vdev)
{
    if (thread_A.kthread) {
        thread_A.exit_kthread = 1;
        up(&sem_a);
        kthread_stop(thread_A.kthread);
        thread_A.kthread = 0;
        thread_A.exit_kthread = 0;
    }

    if (thread_B.kthread) {
        thread_B.exit_kthread = 1;
        up(&sem_b);
        kthread_stop(thread_B.kthread);
        thread_B.kthread = 0;
        thread_B.exit_kthread = 0;
    }
    return ;
}

2.3 kthread work && kthread worker
kthread worker 和 kthread work機制組合其實如名字所述,比較清晰,就是一個worker可以同時做好幾個work(下圖可以有助於大家理解,用鏈表維護work),其實kthread worker其實就是創建了一個線程,我理解其實類似單片機裏面的單個線程,單片機也能根據timer等機制來實現多任務工作,實際上也是 “宏觀上並行,微觀上串行的“, 這樣看來其實我們的work其實就是宏觀並行,微觀串行的任務了。
圖參考鏈接: 小小城御園
.



結構體分析 (include/linux/kthread.h 文件中)
kthread_worker 結構體

struct kthread_worker {
    unsigned int flags;
    spinlock_t lock;      //保護work_list鏈表的自旋鎖
    struct list_head work_list;    //kthread_work 鏈表,相當於流水線
    struct list_head delayed_work_list;   // 延時工作 list
    struct task_struct *task;      //爲該kthread_worker執行任務的線程對應的task_struct結構
    struct kthread_work *current_work;    //當前正在處理的kthread_work
};

kthread_work 結構體

struct kthread_work {
    struct list_head    node;  //kthread_work鏈表的鏈表元素
    kthread_work_func_t    func;  //執行函數,該kthread_work所要做的事情
    struct kthread_worker    *worker;  //處理該kthread_work的kthread_worker
    /* Number of canceling calls that are running at the moment. */
    int canceling;
};

結構體定義
struct kthread_worker kworker; //聲明一個kthread_worker
struct kthread_work kwork0; //聲明一個kthread_work
struct task_struct *kworker_task;//聲明一個kthread句柄

初始化
kthread_init_worker(&kworker); \\將該worker初始化
/* kthread_worker_fn 會執行一個循環調度看worker裏面work鏈表是否有任務,有任務則調callback執行任務*/
kworker_task = kthread_run(kthread_worker_fn, &kworker, "worker");  \\創建kworker線程並開始運行
kthread_init_work(&kwork0, kwork_callback); //初始化kthread_work,設置work執行函數kwork_callback
sched_setscheduler(bsb_ctx->bsb_kworker_task, SCHED_FIFO, &param);  \\修改kworker線程的調度機制爲FIFO

運行
kthread_queue_work(&kworker, &kwork0); //將work掛到worker上,然後該線程就會去調度執行這個work了
1
銷燬階段
/* 刷新指定 worker 上所有 work,將指定的 worker 上的所有 work 全部執行完,一般會在kthread_stop之前使用 */
kthread_flush_worker(&kworker);
if (kworker_task) {
    /* 停止當前線程一般和kthread_flush_worker一起使用,一般都會在隊列的所有任務都完成之後再停止進程 */
    kthread_stop(kworker_task);
    kworker_task= NULL;
}

kthread_worker_fn 函數剖析
int kthread_worker_fn(void *worker_ptr)
{
    struct kthread_worker *worker = worker_ptr;
    struct kthread_work *work;
    /*
     * FIXME: Update the check and remove the assignment when all kthread
     * worker users are created using kthread_create_worker*() functions.
     */
    WARN_ON(worker->task && worker->task != current);
    worker->task = current;
 
    if (worker->flags & KTW_FREEZABLE)
        set_freezable();
 
repeat:
    set_current_state(TASK_INTERRUPTIBLE);    /* mb paired w/ kthread_stop */
 
    if (kthread_should_stop()) {
        __set_current_state(TASK_RUNNING);
        spin_lock_irq(&worker->lock);
        worker->task = NULL;
        spin_unlock_irq(&worker->lock);
        return 0;
    }
 
    work = NULL;
    spin_lock_irq(&worker->lock);
    if (!list_empty(&worker->work_list)) {
        work = list_first_entry(&worker->work_list,
                    struct kthread_work, node);
        list_del_init(&work->node);
    }
    worker->current_work = work;
    spin_unlock_irq(&worker->lock);
    if (work) {
        __set_current_state(TASK_RUNNING);
        work->func(work);
    } else if (!freezing(current))
        schedule();
    try_to_freeze();
    goto repeat;
}
EXPORT_SYMBOL_GPL(kthread_worker_fn);

代碼邏輯如下:
1、首先是賦值了傳入的 worker->task 爲 current,當前進程,設置狀態爲 TASK_INTERRUPTIBLE;
2、Check 標誌位,看是否需要關閉這個 kthread_worker_fn 內核線程,如果需要關閉,則進程狀態,並清空 worker 下對應的 work
3、判斷當前的 worker 的 work_list 上是否爲空,如果非空,那麼取出它,設置成爲 worker->current_work,即當前的 work
4、執行取出的 work->func,這個最終就會掉用我們之前註冊的回調函數xxx_work_fn
5、如果 worker 的 work_list 上爲空,也就是沒有任務,那麼就會調用 schedule(),這個 kthread_worker_fn 執行線程進入睡眠
6、如果沒有退出,則 goto repeat,繼續執行

2.4 tasklet(include/linux/interrupt.h)
使用示例
中斷:突然有一輛車撞到你了
task A:馬上送去醫院救治(優先級高)
task B:去找司機理論(優先級低)
在中斷中,task A,B兩件事都應該做的對嗎,但是事有輕重緩急,很明顯A 比較重要,B 次之,但是也要做的。

定義tasklet的任務

struct tasklet_struct    A_tasklet;
初始化 tasklet 函數定義

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

初始化tasklet,把task B綁定到tasklet中

tasklet_init(&dsp->vdsp_tasklet, task_B, (unsigned long)dsp);

中斷函數(ISR)中

ISR {
    //do task A
    tasklet_schedule(&dsp->vcap_tasklet); //去調度task,執行vdsp_task ,而vdsp_task 中放task B
}

做不緊急的事情

vdsp_task {
   //do task B
}

文章知識點與
————————————————
版權聲明:本文爲CSDN博主「雨中奔跑的大蒜苗」的原創文章,遵循CC 4.0 BY-SA版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/zxpblog/article/details/108539245

 

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