Linux喚醒搶占----Linux進程的管理與調度(二十三)

来源:https://www.cnblogs.com/linhaostudy/archive/2018/11/09/9933436.html
-Advertisement-
Play Games

1. 喚醒搶占 當在try_to_wake_up/wake_up_process和wake_up_new_task中喚醒進程時, 內核使用全局check_preempt_curr看看是否進程可以搶占當前進程可以搶占當前運行的進程. 請註意該過程不涉及核心調度器. 每個調度器類都因應該實現一個chec ...


1. 喚醒搶占

當在try_to_wake_up/wake_up_process和wake_up_new_task中喚醒進程時, 內核使用全局check_preempt_curr看看是否進程可以搶占當前進程可以搶占當前運行的進程. 請註意該過程不涉及核心調度器.

每個調度器類都因應該實現一個check_preempt_curr函數, 在全局check_preempt_curr中會調用進程其所屬調度器類check_preempt_curr進行搶占檢查, 對於完全公平調度器CFS處理的進程, 則對應由check_preempt_wakeup函數執行該策略.

新喚醒的進程不必一定由完全公平調度器處理, 如果新進程是一個實時進程, 則會立即請求調度, 因為實時進程優先極高, 實時進程總會搶占CFS進程.

2 Linux進程的睡眠

在Linux中,僅等待CPU時間的進程稱為就緒進程,它們被放置在一個運行隊列中,一個就緒進程的狀 態標誌位為TASK_RUNNING. 一旦一個運行中的進程時間片用完, Linux 內核的調度器會剝奪這個進程對CPU的控制權, 並且從運行隊列中選擇一個合適的進程投入運行.

當然,一個進程也可以主動釋放CPU的控制權. 函數schedule()是一個調度函數, 它可以被一個進程主動調用, 從而調度其它進程占用CPU. 一旦這個主動放棄CPU的進程被重新調度占用CPU, 那麼它將從上次停止執行的位置開始執行, 也就是說它將從調用schedule()的下一行代碼處開始執行.

有時候,進程需要等待直到某個特定的事件發生,例如設備初始化完成、I/O 操作完成或定時器到時等. 在這種情況下, 進程則必須從運行隊列移出, 加入到一個等待隊列中, 這個時候進程就進入了睡眠狀態.

Linux 中的進程睡眠狀態有兩種

  • 一種是可中斷的睡眠狀態,其狀態標誌位TASK_INTERRUPTIBLE.

可中斷的睡眠狀態的進程會睡眠直到某個條件變為真, 比如說產生一個硬體中斷、釋放進程正在等待的系統資源或是傳遞一個信號都可以是喚醒進程的條件.

  • 另一種是不可中斷的睡眠狀態,其狀態標誌位為TASK_UNINTERRUPTIBLE.

不可中斷睡眠狀態與可中斷睡眠狀態類似, 但是它有一個例外, 那就是把信號傳遞到這種睡眠 狀態的進程不能改變它的狀態, 也就是說它不響應信號的喚醒. 不可中斷睡眠狀態一般較少用到, 但在一些特定情況下這種狀態還是很有用的, 比如說: 進程必須等待, 不能被中斷, 直到某個特定的事件發生.

在現代的Linux操作系統中, 進程一般都是用調用schedule的方法進入睡眠狀態的, 下麵的代碼演示瞭如何讓正在運行的進程進入睡眠狀態。

sleeping_task = current;
set_current_state(TASK_INTERRUPTIBLE);
schedule();
func1();
/* Rest of the code ... */

3 linux進程的喚醒

當在try_to_wake_up/wake_up_processwake_up_new_task中喚醒進程時, 內核使用全局check_preempt_curr看看是否進程可以搶占當前進程可以搶占當前運行的進程. 請註意該過程不涉及核心調度器.

3.1 wake_up_process

我們可以使用wake_up_process將剛纔那個進入睡眠的進程喚醒, 該函數定義在kernel/sched/core.c, line 2043.

int wake_up_process(struct task_struct *p)
{
    return try_to_wake_up(p, TASK_NORMAL, 0);
}

在調用了wake_up_process以後, 這個睡眠進程的狀態會被設置為TASK_RUNNING,而且調度器會把它加入到運行隊列中去. 當然, 這個進程只有在下次被調度器調度到的時候才能真正地投入運行.

3.2 try_to_wake_up

try_to_wake_up函數通過把進程狀態設置為TASK_RUNNING, 並把該進程插入本地CPU運行隊列rq來達到喚醒睡眠和停止的進程的目的.

例如: 調用該函數喚醒等待隊列中的進程, 或恢復執行等待信號的進程.

static int
try_to_wake_up(struct task_struct *p, unsigned int state, int wake_flags)

該函數接受的參數有: 被喚醒進程的描述符指針(p), 可以被喚醒的進程狀態掩碼(state), 一個標誌wake_flags,用來禁止被喚醒的進程搶占本地CPU上正在運行的進程.

try_to_wake_up函數定義在kernel/sched/core.c, line 1906

3.3 wake_up_new_task

void wake_up_new_task(struct task_struct *p)

該函數定義在kernel/sched/core.c, line 2421

之前進入睡眠狀態的可以通過try_to_wake_up和wake_up_process完成喚醒, 而我們fork新創建的進程在完成自己的創建工作後, 可以通過wake_up_new_task完成喚醒工作, 參見Linux下進程的創建過程分析(_do_fork/do_fork詳解)–Linux進程的管理與調度(八)

使用fork創建進程的時候, 內核會調用_do_fork(早期內核對應do_fork)函數完成內核的創建, 其中在進程的信息創建完畢後, 就可以使用wake_up_new_task將進程喚醒並添加到就緒隊列中等待調度. 代碼參見kernel/fork.c, line 1755

3.4 check_preempt_curr

wake_up_new_task中喚醒進程時, 內核使用全局check_preempt_curr看看是否進程可以搶占當前進程可以搶占當前運行的進程.

    check_preempt_curr(rq, p, WF_FORK);

函數定義在kernel/sched/core.c, line 905

void check_preempt_curr(struct rq *rq, struct task_struct *p, int flags)
{
    const struct sched_class *class;

    if (p->sched_class == rq->curr->sched_class)
    {
        rq->curr->sched_class->check_preempt_curr(rq, p, flags);
    }
    else
    {
        for_each_class(class) {
            if (class == rq->curr->sched_class)
                break;
            if (class == p->sched_class) {
                resched_curr(rq);
                break;
            }
        }
    }

    /*
     * A queue event has occurred, and we're going to schedule.  In
     * this case, we can save a useless back to back clock update.
     */
    if (task_on_rq_queued(rq->curr) && test_tsk_need_resched(rq->curr))
        rq_clock_skip_update(rq, true);
}

4 無效喚醒

4.1 無效喚醒的概念

幾乎在所有的情況下, 進程都會在檢查了某些條件之後, 發現條件不滿足才進入睡眠. 可是有的時候進程卻會在判定條件為真後開始睡眠, 如果這樣的話進程就會無限期地休眠下去, 這就是所謂的無效喚醒問題.

在操作系統中, 當多個進程都企圖對共用數據進行某種處理, 而最後的結果又取決於進程運行的順序時, 就會發生競爭條件, 這是操作系統中一個典型的問題, 無效喚醒恰恰就是由於競爭條件導致的.

設想有兩個進程A 和B, A 進程正在處理一個鏈表, 它需要檢查這個鏈表是否為空, 如果不空就對鏈表裡面的數據進行一些操作, 同時B進程也在往這個鏈表添加節點. 當這個鏈表是空的時候, 由於無數據可操作, 這時A進程就進入睡眠, 當B進程向鏈表裡面添加了節點之後它就喚醒A進程, 其代碼如下:

A進程:

spin_lock(&list_lock);
if(list_empty(&list_head))
{
    spin_unlock(&list_lock);
    set_current_state(TASK_INTERRUPTIBLE);
    schedule();
    spin_lock(&list_lock);
}
/* Rest of the code ... */
spin_unlock(&list_lock);
}

B進程:

spin_lock(&list_lock);
list_add_tail(&list_head, new_node);
spin_unlock(&list_lock);
wake_up_process(A);

在這之後, A進程繼續執行, 它會錯誤地認為這個時候鏈表仍然是空的, 於是將自己的狀態設置為TASK_INTERRUPTIBLE然後調用schedule()進入睡眠. 由於錯過了B進程喚醒, 它將會無限期的睡眠下去, 這就是無效喚醒問題, 因為即使鏈表中有數據需要處理, A進程也還是睡眠了.

4.2 無效喚醒的原因

如何避免無效喚醒問題呢?

我們發現無效喚醒主要發生在檢查條件之後和進程狀態被設置為睡眠狀態之前, 本來B進程的wake_up_process提供了一次將A進程狀態置為TASK_RUNNING的機會,可惜這個時候A進程的狀態仍然是TASK_RUNNING,所以wake_up_process將A進程狀態從睡眠狀態轉變為運行狀態的努力沒有起到預期的作用.

4.3 避免無效搶占

要解決這個問題, 必須使用一種保障機制使得判斷鏈表為空和設置進程狀態為睡眠狀態成為一個不可分割的步驟才行, 也就是必須消除競爭條件產生的根源, 這樣在這之後出現的wake_up_process就可以起到喚醒狀態是睡眠狀態的進程的作用了.

找到了原因後, 重新設計一下A進程的代碼結構, 就可以避免上面例子中的無效喚醒問題了.

A進程

set_current_state(TASK_INTERRUPTIBLE);
spin_lock(&list_lock);
if(list_empty(&list_head))
{
    spin_unlock(&list_lock);
    schedule();
    spin_lock(&list_lock);
}
set_current_state(TASK_RUNNING);
/* Rest of the code ... */
spin_unlock(&list_lock);

可以看到,這段代碼在測試條件之前就將當前執行進程狀態轉設置成TASK_INTERRUPTIBLE了, 並且在鏈表不為空的情況下又將自己置為TASK_RUNNING狀態.

這樣一來如果B進程在A進程進程檢查了鏈表為空以後調用wake_up_process, 那麼A進程的狀態就會自動由原來TASK_INTERRUPTIBLE變成TASK_RUNNING, 此後即使進程又調用了schedule, 由於它現在的狀態是TASK_RUNNING, 所以仍然不會被從運行隊列中移出, 因而不會錯誤的進入睡眠,當然也就避免了無效喚醒問題.

5 Linux內核的例子

5.1 一個最基本的例子

在Linux操作系統中, 內核的穩定性至關重要, 為了避免在Linux操作系統內核中出現無效喚醒問題, Linux內核在需要進程睡眠的時候應該使用類似如下的操作:

/* ‘q’是我們希望睡眠的等待隊列 */
DECLARE_WAITQUEUE(wait,current);
add_wait_queue(q, &wait);
set_current_state(TASK_INTERRUPTIBLE);

/* 或TASK_INTERRUPTIBLE */
while(!condition) /* ‘condition’ 是等待的條件*/
schedule();
set_current_state(TASK_RUNNING);
remove_wait_queue(q, &wait);

上面的操作, 使得進程通過下麵的一系列步驟安全地將自己加入到一個等待隊列中進行睡眠: 首先調用DECLARE_WAITQUEUE創建一個等待隊列的項, 然後調用add_wait_queue()把自己加入到等待隊列中, 並且將進程的狀態設置為 TASK_INTERRUPTIBLE或者TASK_INTERRUPTIBLE.

然後迴圈檢查條件是否為真: 如果是的話就沒有必要睡眠, 如果條件不為真, 就調用schedule

當進程檢查的條件滿足後, 進程又將自己設置為TASK_RUNNING並調用remove_wait_queue將自己移出等待隊列.

從上面可以看到, Linux的內核代碼維護者也是在進程檢查條件之前就設置進程的狀態為睡眠狀態,

然後才迴圈檢查條件. 如果在進程開始睡眠之前條件就已經達成了, 那麼迴圈會退出並用set_current_state將自己的狀態設置為就緒, 這樣同樣保證了進程不會存在錯誤的進入睡眠的傾向, 當然也就不會導致出現無效喚醒問題.

內核中有很多地方使用了避免無效喚醒的時候, 最普遍的地方是內核線程的, 因為內核線程的主要功能是輔助內核完成一定的工作的, 大多數情況下他們處於睡眠態, 當內核發現有任務要做的時候, 才會喚醒它們.

5.2 2號進程的例子-避免無效搶占

下麵讓我們用linux內核中的實例來看看Linux 內核是如何避免無效睡眠的, 我還記得2號進程吧, 它的主要工作就是接手內核線程kthread的創建, 其工作流程函數是kthreadd代碼在kernel/kthread.c, kthreadd函數, line L514

for (;;) {
    set_current_state(TASK_INTERRUPTIBLE);
    if (list_empty(&kthread_create_list))
        schedule();
    __set_current_state(TASK_RUNNING);

    spin_lock(&kthread_create_lock);
    /*  ==do_something start==  */
    while (!list_empty(&kthread_create_list)) {
        struct kthread_create_info *create;

        create = list_entry(kthread_create_list.next,
                    struct kthread_create_info, list);
        list_del_init(&create->list);
        spin_unlock(&kthread_create_lock);

        create_kthread(create);
        /*  ==do_something end == */

        spin_lock(&kthread_create_lock);
    }
    spin_unlock(&kthread_create_lock);

5.2 kthread_worker_fn

kthread_worker/kthread_work是一種內核工作的更好的管理方式, 可以多個內核線程在同一個worker上工作, 共同完成work的工作, 有點像線程池的工作方式.

內核提供了kthread_worker_fn函數一般作為 kthread_create或者 kthread_run函數的 threadfn 參數運行, 可以將多個內核線程附加的同一個worker上面,即將同一個worker結構傳給kthread_run 或者kthread_create當作threadfn的參數就可以了.

其kthread_worker_fn函數作為worker的主函數框架, 也包含了避免無效喚醒的代碼, kernel/kthread.c, kthread_worker_fn函數, line573, 如下所示

int kthread_worker_fn(void *worker_ptr)
{
    /* ......*/
    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;
    }
    /* ......*/
}

此外內核的__kthread_parkme函數中也包含了類似的代碼

6 總結

通過上面的討論, 可以發現在Linux 中避免進程的無效喚醒的關鍵是

  • 在進程檢查條件之前就將進程的狀態置為TASK_INTERRUPTIBLE或TASK_UNINTERRUPTIBLE
  • 並且如果檢查的條件滿足的話就應該將其狀態重新設置為TASK_RUNNING.

這樣無論進程等待的條件是否滿足, 進程都不會因為被移出就緒隊列而錯誤地進入睡眠狀態, 從而避免了無效喚醒問題.

set_current_state(TASK_INTERRUPTIBLE);
spin_lock(&list_lock);
if(list_empty(&list_head))
{
    spin_unlock(&list_lock);
    schedule();
    spin_lock(&list_lock);
}
set_current_state(TASK_RUNNING);
/* Rest of the code ... */
spin_unlock(&list_lock);

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 伺服器win2008server R2 x64 部署.net core到IIS 並解決ASP .NET Core HTTP Error 502.5 – Process Failure 的問題 1、發佈網站 ;2、安裝 vc_redist.x64 (Visual C++ Redistributable ...
  • openssl openssl是個密碼工具集,提供多端介面調用方式 對稱加密 對稱加密主要是用aes,des演算法 需要註意的是解密不要在源文件操作,否則解密失敗源文件也沒有了 單向散列函數加密 使用最多的是sha256,sha512,hmac md5不再推薦使用,推薦使用sha 2 單向散列函數特點 ...
  • 文章的格式也許不是很好看,也沒有什麼合理的順序 完全是想到什麼寫一些什麼,但各個方面都涵蓋到了 能耐下心看的朋友歡迎一起學習,大牛和杠精們請繞道 實驗環境: Kali機器IP:192.168.163.132 Metasploitable機器IP:192.168.163.129 下麵我介紹幾個工具:N ...
  • 架構組合:nginx1.9.10+php7.0.32+mysql5.7.22+zabbix4.0.1 nginx1.9.10 先裝依賴 openssl-1.1.0f pcre-8.40 zlib-1.2.11 nginx-1.9.10 編譯 安裝 php7.0.32 php使用yum方式安裝 更新y ...
  • 2018 2019 1 20189221 《從問題到程式》第 4 周學習總結 第五章 C程式結構 實數類型和整數類型 實數類型共有三個,類型名分別是: float, double, long double 字元類型 :C 語言還有signed char 和unsigned char 兩個字元類型,普 ...
  • 存儲基礎知識 從工作原理區分: 機械 HDD 固態 SSD SSD的優勢: 從磁碟尺寸區分: 3.5 2.5 1.8 從插拔方式區分: 熱插拔 非熱插拔 從硬碟主要介面區分: IDE —— SATA I/II/II 個人電腦 SCSI —— SAS 伺服器 FC PCIE 從存儲連接方式區分: 本 ...
  • mysql5.7忽略大小寫問題 1.1 前言 新安裝mysql5.7版本後,linux環境下預設是大小寫敏感的。 1.2 忽略大小寫敏感步驟 輸入 i 進入編輯模式,找到 [mysqld] ,在其下方增加一行:lower_case_table_names=1 ,(1表示忽略大小寫,0表示解析大小寫) ...
  • Hello! everybody! 記得大三,第一次上我們某主任的課(我是電腦學部的),某主任上課的第一件事,點名,第二件事,忽悠我們。 具體忽悠步驟如下: 某:”同學們,這裡有兩張圖片,大家看看這兩張圖片有什麼不同?“ 同學們:”呃!沒啥不同啊!“ 同學們:”這個顏色深點?呃,好像還是一樣啊!“ ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...