關於 Linux 進程的睡眠和喚醒 ,來看這篇就夠了~

2021-03-02 Linux學習

作者:chumojing

連結:http://blog.chinaunix.net/uid-12461657-id-3178775.html

在 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 ... */

在第一個語句中,程序存儲了一份進程結構指針 sleeping_task,current 是一個宏,它指向正在執行的進程結構。set_current_state() 將該進程的狀態從執行狀態 TASK_RUNNING 變成睡眠狀態TASK_INTERRUPTIBLE。 如果 schedule() 是被一個狀態為TASK_RUNNING 的進程調度,那麼 schedule() 將調度另外一個進程佔用 CPU;如果 schedule() 是被一個狀態為 TASK_INTERRUPTIBLE 或 TASK_UNINTERRUPTIBLE 的進程調度,那麼還有一個附加的步驟將被執行:當前執行的進程在另外一個進程被調度之前會被從運行隊列中移出,這將導致正在運行的那個進程進入睡眠,因為 它已經不在運行隊列中了。

我們可以使用下面的這個函數將剛才那個進入睡眠的進程喚醒。


wake_up_process(sleeping_task);


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

幾乎在所有的情況下,進程都會在檢查了某些條件之後,發現條件不滿足才進入睡眠。可是有的時候進程卻會在 判定條件為真後開始睡眠,如果這樣的話進程就會無限期地休眠下去,這就是所謂的無效喚醒問題。在作業系統中,當多個進程都企圖對共享數據進行某種處理,而 最後的結果又取決於進程運行的順序時,就會發生競爭條件,這是作業系統中一個典型的問題,無效喚醒恰恰就是由於競爭條件導致的。


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


A 進程:

1 spin_lock(&list_lock);

2 if(list_empty(&list_head)) {

3 spin_unlock(&list_lock);

4 set_current_state(TASK_INTERRUPTIBLE);

5 schedule();

6 spin_lock(&list_lock);

7 }

8

9 /* Rest of the code ... */

10 spin_unlock(&list_lock);

B 進程:

100 spin_lock(&list_lock);

101 list_add_tail(&list_head, new_node);

102 spin_unlock(&list_lock);

103 wake_up_process(processa_task);

這裡會出現一個問題,假如當 A 進程執行到第 3 行後第 4 行前的時候,B 進程被另外一個處理器調度投入運行。在這個時間片內,B 進程執行完了它所有的指令,因此它試圖喚醒 A 進程,而此時的 A 進程還沒有進入睡眠,所以喚醒操作無效。在這之後,A 進程繼續執行,它會錯誤地認為這個時候鍊表仍然是空的,於是將自己的狀態設置為 TASK_INTERRUPTIBLE 然後調用 schedule() 進入睡 眠。由於錯過了 B 進程喚醒,它將會無限期的睡眠下去,這就是無效喚醒問題,因為即使鍊表中有數據需要處理,A 進程也還是睡眠了。

如何避免無效喚醒問題呢?我們發現無效喚醒主要發生在檢查條件之後和進程狀態被設置為睡眠狀態之前, 本來 B 進程的 wake_up_process() 提供了一次將 A 進程狀態置為 TASK_RUNNING 的機會,可惜這個時候 A 進程的狀態仍然是 TASK_RUNNING,所以 wake_up_process() 將 A 進程狀態從睡眠狀態轉變為運行狀態的努力 沒有起到預期的作用。要解決這個問題,必須使用一種保障機制使得判斷鍊表為空和設置進程狀態為睡眠狀態成為一個不可分割的步驟才行,也就是必須消除競爭條 件產生的根源,這樣在這之後出現的 wake_up_process () 就可以起到喚醒狀態是睡眠狀態的進程的作用了。
找到了原因後,重新設計一下 A 進程的代碼結構,就可以避免上面例子中的無效喚醒問題了。


A 進程:

1 set_current_state(TASK_INTERRUPTIBLE);

2 spin_lock(&list_lock);

3 if(list_empty(&list_head)) {

4 spin_unlock(&list_lock);

5 schedule();

6 spin_lock(&list_lock);

7 }

8 set_current_state(TASK_RUNNING);

9

10 /* Rest of the code ... */

11 spin_unlock(&list_lock);

可以看到,這段代碼在測試條件之前就將當前執行進程狀態轉設置成 TASK_INTERRUPTIBLE 了,並且在鍊表不為空的情況下又將自己置為 TASK_RUNNING 狀態。這樣一來如果 B 進程在 A 進程進程檢查了鍊表為空以後調用 wake_up_process(),那麼 A 進程的狀態就會自動由原來 TASK_INTERRUPTIBLE變成 TASK_RUNNING,此後即使進程又調用了 schedule(),由於它現在的狀態是 TASK_RUNNING,所以仍然不會被從運行隊列中移出,因而不會錯誤的進入睡眠,當然也就避免了無效喚醒問題。

在 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() 將自己的狀態設置為就緒,這樣同樣保證了進程不會存在錯誤的進入睡眠的傾向,當然也就不會導致出現無效喚醒問題。


下面讓我們用 linux 內核中的實例來看看 Linux 內核是如何避免無效睡眠的,這段代碼出自 Linux2.6 的內核 (linux-2.6.11/kernel/sched.c: 4254):

4253 /* Wait for kthread_stop */

4254 set_current_state(TASK_INTERRUPTIBLE);

4255 while (!kthread_should_stop()) {

4256 schedule();

4257 set_current_state(TASK_INTERRUPTIBLE);

4258 }

4259 __set_current_state(TASK_RUNNING);

4260 return 0;

上面的這些代碼屬於遷移服務線程 migration_thread,這個線程不斷地檢查 kthread_should_stop(),

直 到 kthread_should_stop() 返回 1 它才可以退出循環,也就是說只要 kthread_should_stop() 返回 0 該進程就會一直睡 眠。從代碼中我們可以看出,檢查 kthread_should_stop() 確實是在進程的狀態被置為 TASK_INTERRUPTIBLE 後才開始執行 的。因此,如果在條件檢查之後但是在 schedule() 之前有其他進程試圖喚醒它,那麼該進程的喚醒操作不會失效。

通過上面的討論,可以發現在 Linux 中避免進程的無效喚醒的關鍵是在進程檢查條件之前就將進程的狀態置為 TASK_INTERRUPTIBLE 或 TASK_UNINTERRUPTIBLE,並且如果檢查的條件滿足的話就應該將其狀態重新設置為 TASK_RUNNING。這樣無論進程等待的條件是否滿足, 進程都不會因為被移出就緒隊列而錯誤地進入睡眠狀態,從而避免了無效喚醒問題。

●編號464,輸入編號直達本文

●輸入m獲取到文章目錄

黑客技術與網絡安全

更多推薦18個技術類公眾微信

涵蓋:程序人生、算法與數據結構、黑客技術與網絡安全、大數據技術、前端開發、Java、Python、Web開發、安卓開發、iOS開發、C/C++、.NET、Linux、資料庫、運維等。

相關焦點

  • 設置Linux進程的睡眠和喚醒
    無效喚醒 幾乎在所有的情況下,進程都會在檢查了某些條件之後,發現條件不滿足才進入睡眠。可是有的時候 進程卻會在 判定條件為真後開始睡眠,如果這樣的話進程就會無限期地休眠下去,這就是所謂的無效喚醒問題。
  • 框架篇:見識一下linux高性能網絡IO+Reactor模型
    當程序打開一個現有文件或者創建一個進程,socket套接字時,內核都會向進程返回一個文件描述符Linux進程運行中可以接受來自系統或者進程的信號值,然後根據信號值去運行相應捕捉函數;信號相當於是硬體中斷的軟體模擬在零拷貝機制篇章已介紹過 用戶空間和內核空間和緩衝區,這裡就省略了網絡IO的讀寫過程當在用戶空間發起對socket
  • Linux 自旋鎖spinlock,教你如何把ubuntu弄死鎖
    自旋鎖的使用在linux kernel的實現中,經常會遇到這樣的場景:共享數據被中斷上下文和進程上下文訪問,該如何保護呢?,沒 有一個task_struct(這點對於softirq和tasklet一樣),因此真的休眠了,比如調用了會導致block的例程,內核幾乎肯定會死。
  • 從linux源碼看epoll
    和select、poll等系統調用相比,epoll在需要監視大量文件描述符並且其中只有少數活躍的時候,表現出無可比擬的優勢。epoll能讓內核記住所關注的描述符,並在對應的描述符事件就緒的時候,在epoll的就緒鍊表中添加這些就緒元素,並喚醒對應的epoll等待進程。
  • Linux內核同步機制之(四):spin lock
    如果只有進程上下文的訪問,那麼可以考慮使用semaphore或者mutex的鎖機制,但是現在中斷上下文也參和進來,那些可以導致睡眠的lock就不能使用了,這時候,可以考慮使用spin lock。本文主要介紹了linux kernel中的spin lock的原理以及代碼實現。
  • 關於Android 進程保活,你所需要知道的一切
    關於 Android 平臺的進程保活這一塊,想必是所有 Android 開發者矚目的內容之一。你到網上搜 Android 進程保活,可以搜出各種各樣神乎其技的做法,絕大多數都是極其不靠譜。前段時間,Github還出現了一個很火的「黑科技」進程保活庫,聲稱可以做到進程永生不死。
  • linux下進程和線程狀態查看
    ps -eLf |grep pid|grep -v greppstree -p `ps -aux | grep server | awk '{print $2}'` | wc -l查看線程其實linux
  • Linux怎樣查詢出當前系統的所有進程
    本經驗已linux發行版Ubuntu為例,Linux下使用PS命令結合相關參數可以查看linux當前系統下的所有進程、所有運行中的進程、所有非root運行的進程、所有指定用戶運行的進程。使用搜索功能搜索「Terminal」,打開Ubuntu命令行終端。
  • Linux進程調度器-基礎
    概述從這篇文章開始,將開始Linux調度器的系列研究了。本文也會從一些基礎的概念及數據結構入手,先打造一個粗略的輪廓,後續的文章將逐漸深入。2. 概念2.1 進程從教科書上,我們都能知道:進程是資源分配的最小單位,而線程是CPU調度的的最小單位。
  • 「正點原子Linux連載」第五十二章Linux阻塞和非阻塞IO實驗
    *q)參數q就是要喚醒的等待隊列頭,這兩個函數會將這個等待隊列頭中的所有進程都喚醒。wake_up函數可以喚醒處於TASK_INTERRUPTIBLE和TASK_UNINTERRUPTIBLE狀態的進程,而wake_up_interruptible函數只能喚醒處於TASK_INTERRUPTIBLE狀態的進程。
  • 嵌入式Linux系統中MMC卡驅動管理技術研究
    這時,當前讀寫進程進入睡眠狀態,等待中斷處理程序的喚醒。(2)中斷響應和處理 MMC卡在數據傳輸請求、內部時鐘關閉、命令發布完畢、數據傳輸完畢的情況下都會產生中斷,但足MMC卡的控制器只通過1裉GPIO23的引腳與CPU相連,用於中斷信號線的復用;因此在中斷處理程序中,必須首先判斷到底是哪種原因產生的中斷,然後再進行相應的處理。
  • Linux系統Systemd實戰篇
    CentOS7.3學習筆記總結(六十九)- Systemd實戰篇Systemd是linux系統工具,是用來守護進程的,是大多數發行版的標準配置。啟動服務,httpd的啟動配置文件已經生成:ls -l /usr/lib/systemd/system/httpd.service重新啟動httpd軟體:systemctl restart httpd停止httpd軟體:systemctl stop httpd#有時執行此命令無響應,可以執行systemctl kill httpd殺死進程
  • 助眠加喚醒,優質睡眠好開始——Sleepace享睡喚醒燈評測
    隨著科技的不斷進步,知識的豐富,人們對如何保證優質的睡眠越來越渴求,在這樣的背景下以「創造優質睡眠」為宗旨的智能睡眠品牌Sleepace享睡誕生了,經過多年的專注於睡眠科技領域的研發其產品在業內獲得了良好的聲譽和知名度,今天筆者就給大家介紹一下Sleepace享睡一款新產品---Sleepace享睡喚醒燈。
  • Linux進程管理命令:nohup、&、jobs、fg、bg、ps、kill
    jobs命令一般和-l搭配使用,可以顯示後臺執行進程的進程號。這裡介紹一些常見的快捷鍵和進程命令:ctrl+c 停止當前正在執行的進程,相當於直接kill掉。ctrl+z 將當前正在執行的進程放到後臺,並且暫停執行,此時進程處於stop狀態。
  • linux kernel工作隊列及源碼詳細講解
    以下代碼的linux內核版本為2.6.19.2, 原始碼文件主要為kernel/workqueue.c.2.操作函數4.1 創建工作隊列一般的創建函數是create_workqueue, 但這其實只是一個宏:/* include/linux/workqueue.h */#define create_workqueue(name) __create_workqueue((name), 0)在workqueue的初始化函數中, 定義了一個針對內核中所有線程可用的事件工作隊列
  • 心理|是否應該喚醒夢遊者?淺談睡眠、夢境和夢遊
    這其中的奧秘是什麼呢?還要先從睡眠說起。睡眠充足的睡眠是非常必要的,在生理方面,它可以讓我們的機體從白天的運作中暫停下來,得到一定的休息。在心理方面,睡眠的時候我們的大腦可以整合和加工白天接收的信息和知識。
  • vxworks和linux有什麼區別
    自己目前開發的嵌入式開發所用的作業系統是VxWorks,以前讀大學的時候用的最多的是linux作業系統,但是,對於這兩種作業系統之間到底有什麼區別,還真沒有真正去細心的總結過,被別人問起時,難免有些尷尬的感覺 Linux是一類Unix計算機作業系統的統稱。Linux作業系統的內核的名字也是「Linux」。
  • Linux 下的進程間通信:共享存儲 | Linux 中國
    (之前推送的本篇中代碼存在一些錯誤,重新推送一遍)本篇是 Linux 下進程間通信(IPC)系列的第一篇文章。這個系列將使用 C 語言代碼示例來闡明以下 IPC 機制:在聚焦上面提到的共享文件和共享內存這兩個機制之前,這篇文章將帶你回顧一些核心的概念。
  • Linux內核源碼do_fork分析
    我們都知道進程是Linux內核中最為重要的一個抽象概念,那麼我們平時在fork一個進程時,該進程究竟是怎麼產生的呢? 本篇推送會淺談一下在進程創建過程中扮演著重要角色的do_fork函數。
  • 你不知道的車載乙太網睡眠喚醒姿勢!
    而基於安全考慮,往往汽車的相關產品和協議規範會將工程師們繞的暈頭轉向。小編在這就給各位看官解鎖汽車乙太網PHY睡眠和喚醒的正確姿勢。新命令包括LPS、WUR和WUP,這三個命令在PHY晶片手冊十分常見,故而理解這些命令是十分必要的。低功耗睡眠(LPS)是某節點向鏈路夥伴指示睡眠請求的命令。