帶您進入內核開發的大門 | 內核中的線程

內核線程是直接由內核自己啓動的進程。內核線程其實是將內核函數委託給獨立的進程,它與內核中的其餘進程」並行」執行。內核線程常常被稱之爲內核守護進程。內核線程是被調度的實體,它被加入到某種數據結構中,調度程序根據實際狀況進行線程的調度。 內核線程與用戶態線程的做用相似,一般用於執行某些週期性的計算任務,或者在後臺執行須要大量計算的任務。 node

linux kernel
本文主要介紹一下 內核線程操做相關的API的使用,以及內核線程的實現基本原理,更深刻的內容在後續文章中介紹。

內核線程操做函數

內核線程操做涉及的函數(API)主要是建立、調度和中止等函數。操做起來也是比較簡單的。下面分別介紹一下這些接口的定義。 建立線程 建立線程的函數爲kthread_create,以下是函數的原型,該函數其實是函數kthread_create_on_node的一個宏定義。後者則是在某個CPU上建立一個線程。該函數的前兩個參數分別是線程主函數指針和函數的參數,然後面的參數經過變參數的方式爲線程命名。linux

#define kthread_create(threadfn, data, namefmt, arg...) \ kthread_create_on_node(threadfn, data, NUMA_NO_NODE, namefmt, ##arg)
複製代碼

喚醒線程算法

經過該函數建立的線程處於非運行狀態,須要調用wake_up_process函數將其喚醒後才能夠在CPU上運行。緩存

int wake_up_process(struct task_struct *p) 複製代碼

建立並運行線程數據結構

在內核的API中有另一個接口能夠直接建立一個處於運行狀態的線程,其定義以下。這裏其實就是調用了上文描述的兩個函數。socket

#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; \ })
複製代碼

中止線程函數

線程也能夠被中止,此時主函數將會退出,固然須要主函數的實現考慮該問題。以下是中止線程的函數接口。ui

int kthread_stop(struct task_struct *k) 複製代碼

線程的調度spa

內核線程建立完成後將一直運行下去,除非遇到了阻塞事件或者本身將本身調度出去。經過下面函數,線程能夠將本身調度出去。調度出去的含義就是將CPU讓給其它線程操作系統

asmlinkage __visible void __sched schedule(void) 複製代碼

簡單內核線程使用

前面介紹了內核線程基本原理及相關的API,下面咱們將開發一個內核線程的基本實例。 這個實例是在一個內核模塊中啓動一個內核線程。內核線程的做用很簡單,就是定時的向系統日誌中輸出一個字符串。本例的目的主要是介紹如何建立、使用和銷燬一個內核線程。

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/mm.h>

#include <linux/in.h>
#include <linux/inet.h>
#include <linux/socket.h>
#include <net/sock.h>
#include <linux/kthread.h>
#include <linux/sched.h>

#define BUF_SIZE 1024
struct task_struct *main_task;

/* 這個函數用於將內核線程置於休眠狀態,也就是將其調度出 * 隊列。*/
static inline void sleep(unsigned sec) {
        __set_current_state(TASK_INTERRUPTIBLE);
        schedule_timeout(sec * HZ);
}

/* 線程函數, 這個是線程執行的主體 */
static int multhread_server(void *data) {
        int index = 0;

        /* 在線程沒有被中止的狀況下,循環向系統日誌輸出 * 內容, 完成後休眠1秒。*/
        while (!kthread_should_stop()) {
                printk(KERN_NOTICE "thread run %d\n", index);
                index ++; 
                sleep(1);
        }

        return 0;
}


static int multhread_init(void) {
        ssize_t ret = 0;

        printk("Hello, thread! \n");
        /* 建立並啓動一個內核線程, 這裏參數爲線程函數, * 函數的參數(NULL),和線程名稱。 */
        main_task = kthread_run(multhread_server,
                                  NULL,
                                  "multhread_server");
        if (IS_ERR(main_task)) {
                ret = PTR_ERR(main_task);
                goto failed;
        }

failed:
        return ret;
}

static void multhread_exit(void) {
        printk("Bye thread!\n");
        /* 中止線程 */
        kthread_stop(main_task);

}

module_init(multhread_init);
module_exit(multhread_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("SunnyZhang<shuningzhang@126.com>");
複製代碼

基本實現原理

建立線程

不管是用戶態的進程仍是內核線程,在內核態都是線程。在Linux操做系統,建立線程實質是是對父進程(線程)進行克隆的過程。 目前,在3.x之後的版本中,內核線程的建立都有一個名爲kthreadd的後臺線程操做完成。建立線程的接口只是用於建立任務,並加到任務列表中,並等待後臺線程的具體處理。 前文中建立線程的函數kthread_create或者kthread_run調用的函數是__kthread_create_on_node,也就是在某個CPU上建立線程。該函數其實只是建立一個建立線程的請求,以下是裁剪的代碼,核心內容以下:

struct task_struct *__kthread_create_on_node(int (*threadfn)(void *data), void *data, int node, const char namefmt[], va_list args) {
        DECLARE_COMPLETION_ONSTACK(done);
        struct task_struct *task;
        struct kthread_create_info *create = kmalloc(sizeof(*create), GFP_KERNEL);

        if (!create)
                return ERR_PTR(-ENOMEM);
        create->threadfn = threadfn;
        create->data = data;
        create->node = node;
        create->done = &done;

        spin_lock(&kthread_create_lock);
        /* 將建立任務添加到鏈表中 */
        list_add_tail(&create->list, &kthread_create_list);
        spin_unlock(&kthread_create_lock);

        wake_up_process(kthreadd_task);
        ... ...
}
複製代碼

具體建立工做在名爲kthreadd的後臺線程中進行,該線程會從隊列中獲取建立請求,並逐個建立線程。建立線程調用的接口爲kernel_thread,該函數實現從父線程克隆子線程的操做,並創建父子線程的關聯關係。

線程調度

Linux的線程管理和調度是一個很是複雜的話題,很難用一篇文章說清楚,咱們這裏只是介紹一下基本原理。目前Linux操做系統默認使用的是CFS調度算法,該算法是基於優先級和時間片的算法,這個算法包含4部分的內容:

  • 時間記帳
  • 進程選擇
  • 調度器入口
  • 睡眠和喚醒 時間記帳用於記錄進程運行的虛擬時間,而進程選擇則是根據策略選擇應該將那個進程調度到CPU上運行。進程選擇使用的數據結構是紅黑樹,紅黑樹是一個自平衡二叉樹,也就是其中的數據是有序的,這樣能夠很容易的找到目的數據。Linux內核在具體實現的時候又使用了一個技巧,也就是將下一個要調度的進程放入緩存中,這樣就能夠直接找到該進程進行調度,下降了檢索時間。 Linux內核的調度入口是schedule函數,當線程調用該函數時將觸發線程調度。這個函數實現自己很簡單,但其內部調用context_switch函數實現真正的調度,在調用該函數以前會經過調度類獲取目的進程。
static __always_inline struct rq * context_switch(struct rq *rq, struct task_struct *prev, struct task_struct *next, struct rq_flags *rf) 複製代碼

這樣,經過context_switch函數就能夠將當前進程調度出去,而將新的進程調度進來。context_switch最終會調度到一個平臺相關的函數,而這個函數是彙編語言實現的,主要實現寄存器和堆棧的處理,並最終完成進程的切換。

相關文章
相關標籤/搜索