linux 核心執行緒建立
這個話題乍一聽貌似比較大,其實執行緒建立本身就是一件很平常的事情。
下面將要介紹的是,新版linux中建立核心執行緒的機制做了一些變化(其實本質沒變,最終還是呼叫do_fork()來實現),和控制執行緒的時候需要注意的地方。
本文引用的幾個原始碼檔案:
@ kernel/kernel/kthread.c
@ kernel/include/linux/kthread.h
@ kernel/include/linux/wait.h
@ kernel/kernel/workqueue.c
新版linux中將建立核心執行緒的工作交給了一個專門的核心執行緒kthreadd去做了,該執行緒會檢查全域性連結串列kthread_create_list,如果為NULL,就會調schedule()放棄cpu進入睡眠狀態,否則就取下該連結串列中的一項出來建立對應的執行緒。
kthreadd執行緒何時建立?
kthreadd執行緒在系統啟動階段被建立,建立程式碼如下:
start_kernel() @ kernel/init/main.c
--> rest_init()
pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);
kthreadd_task = find_task_by_pid_ns(pid, &init_pid_ns);
struct task_struct *kthreadd_task; @ kernel/kernel/kthread.c
-- 記錄這執行緒kthreadd的task_struct結構體
該執行緒的執行緒函式是kthreadd() @ kernel/kernel/kthread.c -- 見程式碼
我們平時使用的建立介面?
@ kernel/include/linux/kthread.h
#define kthread_run(threadfn, data, namefmt, ...) \
({ \
struct task_struct *__k \
= kthread_create(threadfn, data, namefmt, ## __VA_ARGS__); \
if (!IS_ERR(__k)) \
wake_up_process(__k); \
__k; \
})
或者直接使用kthread_create()函式來建立,值得注意的是,該函式建立執行緒ok返回時,新建執行緒是休眠的。程式碼裡可以看到休眠的位置。所以如果需要建立執行緒後並馬上執行,kthread_run()是個不錯的介面。
kthread_create()函式?
先看兩個相關的資料結構:
struct kthread_create_info
{
int (*threadfn)(void *data);
void *data;
struct task_struct *result;
struct completion done;
struct list_head list;
};
struct kthread {
int should_stop;
struct completion exited;
};
該函式的實現見程式碼:kernel/kernel/kthread.c
create_kthread()函式時執行緒kthreadd被喚醒之後呼叫的函式,該函式呼叫函式:
pid = kernel_thread(kthread, create, CLONE_FS | CLONE_FILES | SIGCHLD);
其中kthread()是建立的核心執行緒的共用執行緒函式,我們在上層介面中傳遞下來的執行緒函式和引數都包含在引數create中。在函式kthread()才會呼叫我們傳遞進來的函式。
詳見程式碼:kernel/kernel/kthread.c
如何停止某個執行緒?
使用函式kthread_stop(struct task_struct *k)即可停止一個指定的執行緒。但是有時候停止某執行緒的時候需要滿足一定條件才可以成功讓執行緒停止並銷燬資源,這個稍後會提到。這裡先看該函式原始碼: kernel/kernel/kthread.c
注意事項
這個問題是在停止執行緒的時候需要注意的。
如果你的執行緒沒事做的時候是睡眠的,而起睡眠在某個等待佇列頭上等某個條件滿足,那麼在停止該執行緒之前必須讓那個條件滿足,才能呼叫kthread_stop()函式,否則的話,你永遠也停止不掉這個執行緒,除非kill -9結果了它。
因為可睡眠的執行緒通常是呼叫wait_event_interruptible(wq, condition)巨集來等待條件,通常的執行緒被設定成可被訊號中斷地等。
#define __wait_event_interruptible(wq, condition, ret) \
do { \
DEFINE_WAIT(__wait); \
\
for (;;) { \
prepare_to_wait(&wq, &__wait, TASK_INTERRUPTIBLE); \
if (condition) \
break; \
if (!signal_pending(current)) { \
schedule(); \
continue; \
} \
ret = -ERESTARTSYS; \
break; \
} \
finish_wait(&wq, &__wait); \
} while (0)
看的出來,執行緒被喚醒後首先再次去檢查條件是否滿足,條件滿足才會退出睡眠,否則檢查一下如果沒有訊號,會繼續睡下去。所以kthread_stop()中只有一次喚醒這個執行緒的機會,錯過了就錯過了,除非另外的地方滿足了這個條件。所以在停止執行緒之前,滿足一下它等待的條件是非常可靠的。
另外一個情況如下,如果你的執行緒函式是這樣寫的:
struct sched_param param = { .sched_priority = RTPM_PRIO_TPD };
sched_setscheduler(current, SCHED_RR, ¶m);
do{
set_current_state(TASK_INTERRUPTIBLE);
wait_event_interruptible(waiter,條件); // 條件等待 tpd_flag!=0
清除條件 // tpd_flag = 0;
set_current_state(TASK_RUNNING);
if(kthread_should_stop())
continue;
{
資料讀取處理
}
}while(!kthread_should_stop())
紅色部位可能有時候時多餘的,但是針對有些硬體的特點,在沒有讓你去讀取資料的時候你讀資料的時候往往會出錯,因為這一次的喚醒根本就不是硬體說它已經準備好了,而是人為故意滿足了這個條件而已。
如果你的執行緒寫法是資料處理在前,檢查睡眠在後,那麼就沒有這個新增的必要了。
int kthread_should_stop(void)
{
return to_kthread(current)->should_stop;
}
kthread_stop()函式中會做這樣的操作:kthread->should_stop = 1;
最後的一種情況是,你的執行緒函式中沒有等待某個條件,而是主動睡眠,然後其他地方根據你的執行緒的名字來喚醒的話,那麼也就沒有在停止之前人為滿足其條件的動作了。
參考程式碼:kernel/kernel/workqueue.c worker_thread()函式,這個是工作者執行緒的執行緒函式。
linux 核心執行緒 工作佇列 優先順序——總結
今天看了一些有關linux優先順序的問題,使用者態的優先順序那倒是資料很多,但是核心態的執行緒、優先順序就不是很明瞭了。
一、何為linux核心執行緒?
當我搜索這個關鍵字時,查到的資料分為兩類——kthread 和pthread,這裡這兩個執行緒,kthread屬於真正的linux核心執行緒,至於pthread是什麼,可以看下百度的解答:pthread_create是UNIX環境建立執行緒函式,另外,在編譯時注意加上-lpthread引數,以呼叫靜態連結庫。因為pthread並非Linux系統的預設庫。而且從使用上看pthread是位於使用者空間的。
kthread就不多說了,在核心中,常見一個函式kthread_create();看下這個函式在做什麼:
struct task_struct *kthread_create(int(*threadfn)(void*data), void*data, constchar namefmt[], ...) { struct kthread_create_info create;
create.threadfn = threadfn; create.data = data; init_completion(&create.done);
spin_lock(&kthread_create_lock); ————>list_add_tail(&create.list,&kthread_create_list); spin_unlock(&kthread_create_lock);
————>wake_up_process(kthreadd_task); wait_for_completion(&create.done);
if(!IS_ERR(create.result)){ ————>struct sched_param param ={.sched_priority =0}; va_list args;
va_start(args, namefmt); vsnprintf(create.result->comm,sizeof(create.result->comm), namefmt, args); va_end(args); /* * root may have changed our (kthreadd's) priority or CPU mask. * The kernel thread should not inherit these properties. */ ————>sched_setscheduler_nocheck(create.result, SCHED_NORMAL,¶m); set_cpus_allowed_ptr(create.result, cpu_all_mask); } return create.result; }
注意箭頭所指程式碼,這個函式只是把要建立的執行緒加入了一個連結串列,之後喚醒一個kthreadd_task,而kthreadd_task也是一個已經存在的執行緒,它專門負責建立新的執行緒。
還有,我們可以發現,這個函式建立的執行緒是有優先順序策略的——SCHED_NORMAL sched_priority = 0 ,這個策略是普通時間片輪詢,所以在核心中我們用這個函式建立的執行緒是這樣的優先順序,那麼這個優先順序可以改嗎?
可以,核心提供了兩個函式,sched_setscheduler_nocheck和sched_setscheduler,不過這兩個函式在驅動中使用的不多,或者說幾乎不用,我倒是找到一處:在acpi_pad.c 檔案中
sched_setscheduler(current, SCHED_RR, ¶m);
二、工作佇列有優先順序策略嗎?
這個可真不好說,為什麼呢?實際上一個工作佇列一定對應於一個工作者執行緒,這個工作者執行緒當然是有策略的,畢竟他是執行緒,但是工作佇列中的每一個work就沒有什麼策略了,不知我獲得資訊全不全。目前,我看到,工作者執行緒有兩種策略——其中一種是SCHED_FIFO,至於另一種是什麼,很可能是SCHED_NORMAL 。
工作佇列的使用有兩個辦法,一是使用系統預設的工作佇列,它對應一個預設的工作者執行緒,這個執行緒是什麼策略我沒有查過,應該是SCHED_NORMAL ,第二種方法就是自己建立一個佇列,函式為以下4個:
#define create_workqueue(name) __create_workqueue((name),0,0,0) #define create_rt_workqueue(name) __create_workqueue((name),0,0,1) #define create_freezeable_workqueue(name) __create_workqueue((name),1,1,0) #define create_singlethread_workqueue(name) __create_workqueue((name),1,0,0)
這些函式最後呼叫了下邊這個函式:
staticint create_workqueue_thread(struct cpu_workqueue_struct *cwq,int cpu) { ————>struct sched_param param ={.sched_priority = MAX_RT_PRIO-1}; struct workqueue_struct *wq = cwq->wq; constchar*fmt = is_wq_single_threaded(wq)?"%s":"%s/%d"; struct task_struct *p;
p = kthread_create(worker_thread, cwq, fmt, wq->name, cpu); /* * Nobody can add the work_struct to this cwq, * if (caller is __create_workqueue) * nobody should see this wq * else // caller is CPU_UP_PREPARE * cpu is not on cpu_online_map * so we can abort safely. */ if(IS_ERR(p)) return PTR_ERR(p); if(cwq->wq->rt) ————>sched_setscheduler_nocheck(p, SCHED_FIFO,¶m); cwq->thread = p;
trace_workqueue_creation(cwq->thread, cpu);
return0; }
如果我們使用create_rt_workqueue(name),就會建立一個排程策略為SCHED_FIFO的工作佇列。
注意:一個工作佇列只對應一個執行緒,這個執行緒負責執行這個工作佇列上的各種work。這可以檢視 worker_thread 這個函式:
staticint worker_thread(void*__cwq) { struct cpu_workqueue_struct *cwq = __cwq; DEFINE_WAIT(wait);
if(cwq->wq->freezeable) set_freezable();
for(;;){ prepare_to_wait(&cwq->more_work,&wait, TASK_INTERRUPTIBLE); if(!freezing(current)&& !kthread_should_stop()&& list_empty(&cwq->worklist)) schedule(); finish_wait(&cwq->more_work,&wait);
try_to_freeze();
if(kthread_should_stop()) break;
run_workqueue(cwq); }
return0; }
從這裡可以看到工作者執行緒在執行每個work時,是順序執行的。所謂的優先順序策略是這個工作者執行緒的,也就是說,如果有兩個工作佇列,也意味著有兩個工作者執行緒,這兩個執行緒是有優先順序策略的。三、tasklet優先順序?
這個網上資源很多,它擁有兩級優先順序,說明如下:
已排程的tasklet(等同於被觸發的軟中斷)存放在兩個單處理器資料結構:tasklet_vec(普通tasklet)和task_hi_vec高優先順序的tasklet)中,這兩個資料結構都是由tasklet_struct結構體構成的連結串列,連結串列中的每個tasklet_struct代表一個不同的tasklet。Tasklets由tasklet_schedule()和tasklet_hi_schedule()進行排程,它們接收一個指向tasklet_struct結構的指標作為引數。兩個函式非常相似(區別在於一個使用TASKLET_SOFTIRQ而另外一個使用HI_SOFTIRQ).