1. 程式人生 > 其它 >執行超時已過期。完成操作之前已超時或伺服器未響應。_作業系統基礎-python版...

執行超時已過期。完成操作之前已超時或伺服器未響應。_作業系統基礎-python版...

技術標籤:執行超時已過期。完成操作之前已超時或伺服器未響應。

1 作業系統

1.1 程序

import 

1.1.1 程序的狀態

1)基本狀態

程序的基本狀態:“就緒”、“執行”、“阻塞”。

  • 就緒:程序已獲得除處理機以外的所需資源,等待分配處理機資源
  • 執行:程序正在佔用處理機資源執行
  • 阻塞:程序等待某種條件,在條件滿足之前無法執行。如發起了 I/O 系統呼叫,會被阻塞,等待 I/O 中斷髮生

2)掛起

“掛起”是指將暫不執行的程序換出到外存,節省記憶體空間。

“掛起”和“阻塞”都是程序暫停執行的狀態,但是這是兩個維度的概念:

  • 阻塞表示程序正在等待一個事件的發生,阻塞狀態下收到收到訊號會切換為就緒狀態
  • 掛起表示程序被換出到外存,掛起狀態下被啟用時會被載入到記憶體,切換為非掛起狀態

綜上所屬,掛起狀態的程序按照是否阻塞可以分為:

  • 掛起就緒狀態:程序在外存中,但是隻要被載入記憶體就可以執行
  • 掛起阻塞狀態:程序在外存中並等待一個事件,即使被載入記憶體(啟用)也無法執行

3)睡眠

Linux 將程序的阻塞狀態進一步細分為:暫停、淺睡眠、深睡眠。其中,若不需要等待資源,則切換為“暫停”;若需要等待資源,切換為“睡眠”;如果睡眠狀態能被訊號喚醒,則是“淺睡眠”,否則是“深睡眠”。

1.1.2 排程演算法

1) 排程演算法的分類

  • 按照 CPU 的分配方式:非搶佔式、搶佔式
  • 按照系統的分時方式:在批處理系統,互動系統或實時系統下的排程

2) 飢餓問題

某個程序無限等待,無法被排程。

3) 批處理系統的排程演算法

排程演算法的目標:

  • 吞吐量(每小時最大作業數):系統每小時完成的作業數,要儘可能多
  • 週轉時間(每作業最小時間):一個作業從提交到完成時的統計平均時間
  • CPU 利用率(CPU 始終忙碌):由於沒有互動,CPU 不會出現等待輸入的情況,因此 CPU 利用率要高

a) 先來先服務(First Come First Serverd,FCFS)

  • 按照請求 CPU 的順序使用 CPU,非搶佔式
  • 優點是易於理解,便於實現,只需一個就緒佇列
  • 缺點是對短作業不公平;對 I/O 密集型程序不利,長時間等待裝置;響應時間不確定

b) 最短作業優先(Shortest Job First,SJF)

  • 預知作業的執行時間,選擇最短時間的優先執行
  • 優點是提高平均週轉時間
  • 缺點是對長作業不公平;可能導致飢餓問題

c) 最短剩餘時間優先(Shortest Remaining Time Next,SRTN)

  • 最短作業優先的搶佔式版本,如果新作業比正在執行的作業剩餘時間短,則它優先執行
  • 缺點是對長作業不公平;可能導致飢餓問題。同“最短作業優先”

d) 最高響應比優先演算法(Highest Response Ratio Next,HRRN)

  • 響應比的定義:作業等待時間/作業執行所需時間
  • 哪個程序的響應比大,哪個程序優先
  • 由響應比的定義可以知道,作業執行所需時間越小、作業等待時間越長,響應比越大
  • 優點:同時考慮了等待時間和執行時間,既優先考慮短作業,也防止長作業無限等待的飢餓

4) 互動系統(分時系統)的排程演算法

排程演算法的目標:

  • 響應時間:要快速響應互動請求
  • CPU 的執行分為若干個時間片,能夠處理不同的運算請求,使每個使用者都能共享主機資源

a) 時間片輪轉(Round Robin,RR)

  • 將所有就緒程序排成一個佇列,按照時間片輪流排程,用完實踐篇的程序排到佇列末尾,屬於搶佔式
  • 優點:沒有飢餓問題
  • 問題:若時間片小,程序切換頻繁,吞吐量低;若時間片長,則響應時間過長,實時性得不到保證

b) 優先順序排程演算法(Priority)

  • 優先順序高的程序先執行,同優先順序的程序輪轉。當高優先順序佇列中沒有程序後,再排程下一級佇列
  • 缺點是可能導致低優先順序程序餓死

引入動態設定優先順序的思想:在優先順序高的程序執行一個時間片後,降低其優先順序,防止其一直佔用 CPU,餓死優先順序低的程序。結合這個思想,可設計出“多級反饋佇列”。

c) 多級反饋佇列(Multilevel Feedback Queue,MFQ)

  • 優先順序高的佇列先執行;優先順序越高,時間片越短;如果一個程序在當前佇列規定的時間片內無法執行完畢,則移動到下一個佇列的隊尾
  • 缺點:也有可能出現飢餓問題,比如不斷有新的更高優先順序的程序加入

d) 彩票法

  • 向程序提供各種系統資源的彩票。排程時隨機抽取彩票,擁有該彩票的程序得到資源
  • 可給重要的程序更多的彩票;協作程序可以交換彩票

e) 公平分享法

  • 每使用者分配一定比例的 CPU 時間,而不是按照程序
  • 各使用者之間按照比例挑選程序

5) 實時系統的排程演算法

排程演算法的目標:滿足任務的截止時間。也就是說,如果有一個任務需要執行,實時作業系統會馬上執行該任務,不會有較長的延時。

a) 最早截止時間優先演算法

先把截止時間早的任務給完成,否則這個任務如果在截止時間後才完成,就沒有意義了。

1.1.3 殭屍程序、孤兒程序、守護程序

  • 殭屍程序:停止執行
  • 孤兒程序:正在執行
  • 守護程序:正在執行

1) 殭屍程序

當一個程序由於某種原因終止時,核心並不是立即把它從系統中清除。程序會保持在一種“已終止”的狀態中,直到被它的父程序回收。當父程序回收已終止的子程序時,核心會拋棄已終止的程序,此時該程序就不存在了。

殭屍程序是指終止但還未被回收的程序。如果子程序退出,而父程序並沒有呼叫 wait()waitpid() 來回收,那麼就會產生殭屍程序。殭屍程序是一個已經死亡的程序,但是其程序描述符仍然儲存在系統的程序表中。

危害:佔用程序號,系統所能使用的程序號是有限的,可能導致不能產生新的程序;佔用一定的記憶體。

如何避免產生殭屍程序:

  • 父程序呼叫 wait 或者 waitpid 等待子程序結束
  • 子程序結束時,核心會發生 SIGCHLD 訊號給父程序。父程序可以註冊一個訊號處理函式,在該函式中呼叫 waitpid,等待所有結束的子程序;也可以用 signal(SIGCLD, SIG_IGN) 忽略 SIGCHLD 訊號,那麼子程序結束後,核心會進行回收
  • 殺死父程序,殭屍程序就會變成孤兒程序,由 Init 程序接管並處理

2) 孤兒程序

如果某個程序的父程序先結束了,那麼它的子程序會成為孤兒程序每個程序結束的時候,系統都會掃描是否存在子程序,如果有則用 Init 程序(pid = 1)接管,並由 Init 程序呼叫 wait 等待其結束,完成狀態收集工作。孤兒程序不會對系統造成危害。

3) 守護程序

守護程序(英語:daemon,英語發音:/ˈdiːmən/或英語發音:/ˈdeɪmən/)是一種在後臺執行的電腦程式。此類程式會被以程序的形式初始化。

1.2 程序間的通訊方式

  • 訊號
  • 管道
  • 訊號量
  • 共享記憶體
  • 訊息佇列
  • 套接字

對比:

方式傳輸的資訊量使用場景關鍵詞

1.2.1 訊號 Signal

訊號是 Linux 系統響應某些條件而產生的一個事件,由作業系統事先定義,接收到該訊號的程序可以採取自定義的行為。這是一種“訂閱-釋出”的模式。

訊號來源分為硬體來源和軟體來源。

  1. 硬體來源。如按下 CTRL+C、除 0、非法記憶體訪問等等
  2. 軟體來源。如 Kill 命令、Alarm Clock 超時、當 Reader 中止之後又向管道寫資料,等等

一般的訊號是都是由一個錯誤產生的。以除 0 為例。在 x86 機器上 DIV 或 IDIV 指令除數為 0 時,會引發 0 號中斷,編號 #DE(Divide Error),即所謂除零異常。這是一個硬體級中斷,會導致陷入核心,執行作業系統預定義在 IDT 中的中斷處理程式。而作業系統處理這個異常的方法,就是向程序傳送一個訊號 SIGFPE。如果程序設定了相應的 signal handler,就執行程序的處理方法。否則,執行作業系統的預設操作,一般這種訊號的預設操作是殺死程序。

同理,溢位、非法記憶體訪問(越界)、非法指令等也都屬於硬體中斷,由作業系統處理。作業系統會將這些硬體異常包裝成“訊號”傳送給程序。如果程序不處理這幾個異常訊號,那麼預設的行為就是掛掉。

但是,訊號也可以作為程序間通訊的一種方式,明確地由一個程序傳送給另一個程序。

程序如何傳送訊號?

  • 作業系統提供傳送訊號的系統呼叫
  • 該系統呼叫會將訊號放到目標程序的訊號佇列中
  • 如果目標程序未處於執行狀態,則該訊號就由核心儲存起來,直到該程序恢復執行並傳遞給它為止。如果一個訊號被程序設定為阻塞,則該訊號的傳遞被延遲,直到其阻塞被取消時才被傳遞給程序

程序如何接收訊號?

  • 每個程序有一個訊號佇列,放其他程序發給它、等待它處理的訊號
  • 程序在執行過程中的特定時刻,檢查並處理自己的訊號佇列。如從系統空間返回到使用者空間之前
  • 傳送訊號時,必須指明發送目標程序的號碼。一般用在具有親緣關係的程序之間

使用者程序對訊號的處理過程有三種:

  1. 處理訊號。定義訊號處理函式,當訊號發生時,執行相應的處理函式
  2. 忽略訊號。當不希望接收到的訊號對程序的執行產生影響,而讓程序繼續執行時,可以忽略該訊號,即不對訊號程序作任何處理
  3. 不處理也不忽略。執行預設操作,linux 對每種訊號都規定了預設操作

有的訊號,使用者程序是無法處理也無法忽略的,比如SIGSTOPSIGKILL 等。

1.2.2 管道 Pipe

管道命令,在 Linux Shell 中經常使用,一般,我們使用管道操作符 | 來表示兩個命令之間的資料通訊。比如:

ps -ef | grep java | xargs echo

管道操作符的內部實現其實就是 Linux 的管道介面。由管道操作符 | 分割的每個命令是獨立的程序,各個程序的標準輸出 STDOUT,會作為下一個程序的標準輸入 STDIN。

1) 定義

管道是一種半雙工的通訊方式,資料只能單向流動,上游程序往管道中寫入資料,下游程序從管道中接收資料。如果想實現雙方通訊,那麼需要建立兩個管道。

管道適合於傳輸大量資訊。管道傳送的內容是以位元組為單位的,沒有格式的位元組流

2) 建立管道

通過 pipe() 系統呼叫來建立並開啟一個管道,當最後一個使用它的程序關閉對他的引用時,pipe 將自動撤銷。

通過 pipe() 建立的是匿名管道,只能用於具有親緣關係的程序之間(父子程序或兄弟程序)。

3) 管道的實現

管道就是一個檔案,是一種只存在於記憶體中的特殊的檔案系統。

在 Linux 中,管道藉助了檔案系統的 File 結構實現。父程序使用 File 結構儲存向管道寫入資料的例程地址,子程序儲存從管道讀出資料的例程地址。這解釋了上文所說的:

  1. 單向流動
  2. 只能用於具有親緣關係的程序之間

管道是由核心管理的一個緩衝區,緩衝區被設計成為環形的資料結構,以便管道可以被迴圈利用(迴圈佇列)。

4) 管道的同步

管道是一個具有特定大小的緩衝區

  • 作業系統會保證讀寫程序的同步
  • 下游程序或者上游程序需要等另一方釋放鎖後才能操作管道。管道就相當於一個檔案,同一時刻只能有一個程序訪問
  • 當管道為空時,下游程序讀阻塞;當管道滿時,上游程序寫阻塞
  • 管道不再被任何程序使用時,自動消失

1.2.3 命名管道 FIFO

Linux 管道包含匿名管道和命名管道。上面說的是匿名管道,只能用在親緣程序中,管道檔案資訊儲存在記憶體裡。

命名管道(FIFO)可用於沒有親緣的程序間。Pipe 和 FIFO 除了建立、開啟、刪除的方式不同外,二者幾乎一模一樣。

通過 mknode() 系統呼叫或者 mkfifo() 函式建立命名管道。一旦建立,任何有訪問權的程序都可以通過檔名將其開啟和進行讀寫,而不侷限於父子程序。

建立命名管道時,會在磁碟中建立一個索引節點,命名管道的名字就相當於索引節點的檔名。索引節點設定了程序的訪問許可權,但是沒有資料塊。命名管道實質上也是通過核心緩衝區來實現資料傳輸。有訪問許可權的程序,可以通過磁碟的索引節點來讀寫這塊緩衝區。

當不再被任何程序使用時,命名管道在記憶體中釋放,但磁碟節點仍然存在。

1.2.4 訊號量 Semaphore

訊號量是一種特殊的變數,對它的操作都是原子的,有兩種操作:V(signal())和 P(wait())。V 操作會增加訊號標 S 的數值,P 操作會減少它。

  • V(S):如果有其他程序因等待 S 而被掛起,就讓它恢復執行,否則 S 加 1
  • P(S):如果 S 為 0,則掛起程序,否則 S 減 1

P、V 來自於荷蘭語:Probeer (try)、Verhoog (increment)。

如果訊號量是一個任意的整數,通常被稱為計數訊號量(Counting semaphore),或一般訊號量(general semaphore);如果訊號量只有二進位制的 0 或 1,稱為二進位制訊號量(binary semaphore)。在 Linux 系統中,二進位制訊號量又稱互斥鎖(Mutex)。訊號量可以用於實現程序或執行緒的互斥和同步。

訊號量在底層的實現是通過硬體提供的原子指令,如 Test And SetCompare And Swap 等。比如 golang 實現互斥量就是使用了 Compare And Swap 指令。

1.2.5 共享記憶體 Shared Memory

共享記憶體顧名思義,允許兩個或多個程序共享同一段實體記憶體。不同程序可以將同一段共享記憶體對映到自己的地址空間,然後像訪問正常記憶體一樣訪問它。不同程序可以通過向共享記憶體端讀寫資料來交換資訊。

一個程序可以通過作業系統的系統呼叫,建立一塊共享記憶體區;其他程序通過系統呼叫把這段記憶體對映到自己的使用者地址空間中;之後各個程序向讀寫正常記憶體一樣,讀寫共享記憶體。共享記憶體區只會駐留在建立它的程序地址空間內。

共享記憶體的優點是簡單且高效,訪問共享記憶體區域和訪問程序獨有的記憶體區域一樣,原因是不需要系統呼叫,不涉及使用者態到核心態的轉換,也不需要對資料不必要的複製。

比如管道和訊息佇列,需要在核心和使用者空間進行四次的資料拷貝(讀輸入檔案、寫到管道;讀管道、寫到輸出檔案),而共享記憶體則只拷貝兩次:一次從輸入檔案到共享記憶體區,另一次從共享記憶體到輸出檔案(圖示)。此外,訊息傳遞的實現經常採用系統呼叫,也就經常需要使用者態和核心態互相轉換;而共享記憶體只在建立共享記憶體區域時需要系統呼叫;一旦建立共享記憶體,所有訪問都可作為常規記憶體訪問,無需藉助核心。

共享記憶體的缺點是存在併發問題,有可能出現多個程序修改同一塊記憶體,因此共享記憶體一般與訊號量結合使用。

1.2.6 訊息佇列 Message Queue

訊息佇列是一個訊息的連結串列,儲存在核心中。訊息佇列中的每個訊息都是一個資料塊,具有特定的格式。作業系統中可以存在多個訊息佇列,每個訊息佇列有唯一的 key,稱為訊息佇列識別符號。

訊息佇列克服了訊號傳遞資訊少、管道只能承載無格式位元組流以及緩衝區大小受限等缺點。和訊號相比,訊息佇列能夠傳遞更多的資訊。與管道相比,訊息佇列提供了有格式的資料,但訊息佇列仍然有大小限制。

訊息佇列允許一個或多個程序向它寫入與讀取訊息。訊息的傳送者和接收者不需要同時與訊息佇列互動。訊息會儲存在佇列中,直到接收者取回它。也就是說,訊息佇列是非同步的,但這也造成了一個缺點,就是接收者必須輪詢訊息佇列,才能收到最近的訊息。

作業系統提供建立訊息佇列、取訊息、發訊息等系統呼叫。

作業系統負責讀寫同步:若訊息佇列已滿,則寫訊息程序排隊等待;若取訊息程序沒有找到需要的訊息,則在等待佇列中尋找。

訊息佇列和管道相比,相同點在於二者都是通過傳送-接收的方式進行通訊,並且資料都有最大長度限制。不同點在於訊息佇列的資料是有格式的,並且取訊息程序可以選擇接收特定型別的訊息,而不是像管道中那樣預設全部接收。

1.2.7 套接字 Socket

  • 不同的計算機的程序之間通過 socket 通訊,也可用於同一臺計算機的不同程序
  • 需要通訊的程序之間首先要各自建立一個 socket,內容包括主機地址與埠號,宣告自己接收來自某埠地址的資料
  • 程序通過 socket 把訊息傳送到網路層中,網路層通過主機地址將其發到目的主機,目的主機通過埠號發給對應程序

作業系統提供建立 socket、傳送、接收的系統呼叫,為每個 socket 設定傳送緩衝區、接收緩衝區。

1.3 程序同步問題

1.3.1 程序同步問題

1) 管程 Monitor

管程將共享變數以及對這些共享變數的操作封裝起來,形成一個具有一定介面的功能模組,這樣只能通過管程提供的某個過程才能訪問管程中的資源。程序只能互斥地使用管程,使用完之後必須釋放管程並喚醒入口等待佇列中的程序。

當一個程序試圖進入管程時,在入口等待佇列等待。若P程序喚醒了Q程序,則Q程序先執行,P在緊急等待佇列中等待。(HOARE管程

WAIT操作:執行wait操作的程序進入條件變數鏈末尾,喚醒緊急等待佇列或者入口佇列中的程序;signal操作:喚醒條件變數鏈中的程序,自己進入緊急等待佇列,若條件變數鏈為空,則繼續執行。(HOARE管程

MESA管程:將HOARE中的signal換成了notify(或者broadcast通知所有滿足條件的),進行通知而不是立馬交換管程的使用權,在合適的時候,條件佇列首位的程序可以進入,進入之前必須用while檢查條件是否合適。優點:沒有額外的程序切換

2) 生產者-消費者問題

問題描述:使用一個緩衝區來存放資料,只有緩衝區沒有滿,生產者才可以寫入資料;只有緩衝區不為空,消費者才可以讀出資料
P(S):①將訊號量S的值減1,即S=S-1;②如果S>0,則該程序繼續執行;否則該程序置為等待狀態,排入等待佇列。
V(S):①將訊號量S的值加1,即S=S+1;②如果S>0,則該程序繼續執行;否則釋放佇列中第一個等待訊號量的程序。
semaphore 

3) 哲學家就餐問題

問題描述:有五位哲學家圍繞著餐桌坐,每一位哲學家要麼思考,要麼吃飯。為了吃飯,哲學家必須拿起兩雙筷子(分別放於左右兩端)不幸的是,筷子的數量和哲學家相等,所以每隻筷子必須由兩位哲學家共享。
""" Three philosohpers, thinking and eating sushi """

4) 讀者-寫者問題

2 執行緒

對於Thread類,它的定義如下:

threading
  • 引數group是預留的,用於將來擴充套件;
  • 引數target是一個可呼叫物件,線上程啟動後執行;
  • 引數name是執行緒的名字。預設值為“Thread-N“,N是一個數字。
  • 引數args和kwargs分別表示呼叫target時的引數列表和關鍵字引數。
import 

2.1 執行緒同步

2.1.1 執行緒同步有哪些方式?

為什麼需要執行緒同步:執行緒有時候會和其他執行緒共享一些資源,比如記憶體、資料庫等。當多個執行緒同時讀寫同一份共享資源的時候,可能會發生衝突。因此需要執行緒的同步,多個執行緒按順序訪問資源。
  • 互斥量 Mutex:互斥量是核心物件,只有擁有互斥物件的執行緒才有訪問互斥資源的許可權。因為互斥物件只有一個,所以可以保證互斥資源不會被多個執行緒同時訪問;當前擁有互斥物件的執行緒處理完任務後必須將互斥物件交出,以便其他執行緒訪問該資源;
  • 訊號量 Semaphore:訊號量是核心物件,它允許同一時刻多個執行緒訪問同一資源,但是需要控制同一時刻訪問此資源的最大執行緒數量。訊號量物件儲存了最大資源計數當前可用資源計數,每增加一個執行緒對共享資源的訪問,當前可用資源計數就減1,只要當前可用資源計數大於0,就可以發出訊號量訊號,如果為0,則將執行緒放入一個佇列中等待。執行緒處理完共享資源後,應在離開的同時通過ReleaseSemaphore函式將當前可用資源數加1。如果訊號量的取值只能為0或1,那麼訊號量就成為了互斥量;
  • 事件 Event:允許一個執行緒在處理完一個任務後,主動喚醒另外一個執行緒執行任務。事件分為手動重置事件和自動重置事件。手動重置事件被設定為激發狀態後,會喚醒所有等待的執行緒,而且一直保持為激發狀態,直到程式重新把它設定為未激發狀態。自動重置事件被設定為激發狀態後,會喚醒一個等待中的執行緒,然後自動恢復為未激發狀態。
  • 臨界區 Critical Section:任意時刻只允許一個執行緒對臨界資源進行訪問。擁有臨界區物件的執行緒可以訪問該臨界資源,其它試圖訪問該資源的執行緒將被掛起,直到臨界區物件被釋放。

2.2 執行緒池

在使用多執行緒處理任務時也不是執行緒越多越好。因為在切換執行緒的時候,需要切換上下文環境,執行緒很多的時候,依然會造成CPU的大量開銷。為解決這個問題,執行緒池的概念被提出來了。預先建立好一個數量較為優化的執行緒組,在需要的時候立刻能夠使用,就形成了執行緒池。

2.3 執行緒鎖

當多個執行緒幾乎同時修改某一個共享資料的時候,需要進行同步控制

Python在threading模組中定義了幾種執行緒鎖類,分別是:

  • Lock 互斥鎖
  • RLock 可重入鎖
  • Semaphore 訊號
  • Event 事件
  • Condition 條件
  • Barrier “阻礙”

2.3.1 互斥鎖Lock

互斥鎖是一種獨佔鎖,同一時刻只有一個執行緒可以訪問共享的資料。使用很簡單,初始化鎖物件,然後將鎖當做引數傳遞給任務函式,在任務中加鎖,使用後釋放鎖。

語法

threading模組中定義了Lock類

如果這個鎖之前是沒有上鎖的,那麼acquire不會堵塞

如果在呼叫acquire對這個鎖上鎖之前 它已經被 其他執行緒上了鎖,那麼此時acquire會堵塞,直到這個鎖被解鎖為止

import 

2.4 死鎖

2.4.1 死鎖概念及產生原理

概念:多個併發程序因爭奪系統資源而產生相互等待的現象。

原理:當一組程序中的每個程序都在等待某個事件發生,而只有這組程序中的其他程序才能觸發該事件,這就稱這組程序發生了死鎖。

本質原因:

  1. 系統資源有限。
  2. 程序推進順序不合理。

2.4.2 死鎖產生的4個必要條件

  1. 互斥:某種資源一次只允許一個程序訪問,即該資源一旦分配給某個程序,其他程序就不能再訪問,直到該程序訪問結束。
  2. 佔有且等待:一個程序本身佔有資源(一種或多種),同時還有資源未得到滿足,正在等待其他程序釋放該資源。
  3. 不可搶佔:別人已經佔有了某項資源,你不能因為自己也需要該資源,就去把別人的資源搶過來。
  4. 迴圈等待:存在一個程序鏈,使得每個程序都佔有下一個程序所需的至少一種資源。

​ 當以上四個條件均滿足,必然會造成死鎖,發生死鎖的程序無法進行下去,它們所持有的資源也無法釋放。這樣會導致CPU的吞吐量下降。所以死鎖情況是會浪費系統資源和影響計算機的使用效能的。那麼,解決死鎖問題就是相當有必要的了。

2.4.3 避免死鎖的方法

1、死鎖預防 ----- 確保系統永遠不會進入死鎖狀態

產生死鎖需要四個條件,那麼,只要這四個條件中至少有一個條件得不到滿足,就不可能發生死鎖了。由於互斥條件是非共享資源所必須的,不僅不能改變,還應加以保證,所以,主要是破壞產生死鎖的其他三個條件。

基本思想是破壞形成死鎖的四個必要條件:

  • 破壞互斥條件:允許某些資源同時被多個程序訪問。但是有些資源本身並不具有這種屬性,因此這種方案實用性有限;
  • 破壞佔有並等待條件:
  • 實行資源預先分配策略(當一個程序開始執行之前,必須一次性向系統申請它所需要的全部資源,否則不執行);
  • 或者只允許程序在沒有佔用資源的時候才能申請資源(申請資源前先釋放佔有的資源);
  • 缺點:很多時候無法預知一個程序所需的全部資源;同時,會降低資源利用率,降低系統的併發性;
  • 破壞非搶佔條件:允許程序強行搶佔被其它程序佔有的資源。會降低系統性能;
  • 破壞迴圈等待條件:對所有資源統一編號,所有程序對資源的請求必須按照序號遞增的順序提出,即只有佔有了編號較小的資源才能申請編號較大的資源。這樣避免了佔有大號資源的程序去申請小號資源。

afd804219b18a26dfb2a361ff9a2cbe0.png

這樣雖然避免了迴圈等待,但是這種方法是比較低效的,資源的執行速度回變慢,並且可能在沒有必要的情況下拒絕資源的訪問,比如說,程序c想要申請資源1,如果資源1並沒有被其他程序佔有,此時將它分配個程序c是沒有問題的,但是為了避免產生迴圈等待,該申請會被拒絕,這樣就降低了資源的利用率

2、避免死鎖 ----- 在使用前進行判斷,只允許不會產生死鎖的程序申請資源

的死鎖避免是利用額外的檢驗資訊,在分配資源時判斷是否會出現死鎖,只在不會出現死鎖的情況下才分配資源。

兩種避免辦法:

1、如果一個程序的請求會導致死鎖,則不啟動該程序

2、如果一個程序的增加資源請求會導致死鎖 ,則拒絕該申請。

避免死鎖的具體實現通常利用銀行家演算法

允許程序動態地申請資源,系統在每次實施資源分配之前,先計算資源分配的安全性,若此次資源分配安全(即資源分配後,系統能按某種順序來為每個程序分配其所需的資源,直至最大需求,使每個程序都可以順利地完成),便將資源分配給程序,否則不分配資源,讓程序等待。

3、死鎖解除

如何檢測死鎖:檢測有向圖是否存在環;或者使用類似死鎖避免的檢測演算法。

死鎖解除的方法:

  • 利用搶佔:掛起某些程序,並搶佔它的資源。但應防止某些程序被長時間掛起而處於飢餓狀態;
  • 利用回滾:讓某些程序回退到足以解除死鎖的地步,程序回退時自願釋放資源。要求系統保持程序的歷史資訊,設定還原點;
  • 利用殺死程序:強制殺死某些程序直到死鎖解除為止,可以按照優先順序進行。

4、鴕鳥策略

直接忽略死鎖。因為解決死鎖問題的代價很高,因此鴕鳥策略這種不採取任務措施的方案會獲得更高的效能。當發生死鎖時不會對使用者造成多大影響,或發生死鎖的概率很低,可以採用鴕鳥策略。

3 協程

協程是一種使用者態的輕量級執行緒,協程的排程完全由使用者控制。協程擁有自己的暫存器上下文和棧。協程排程切換時,將暫存器上下文和棧儲存到其他地方,在切回來的時候,恢復先前儲存的暫存器上下文和棧,直接操作棧則基本沒有核心切換的開銷,可以不加鎖的訪問全域性變數,所以上下文的切換非常快。

  1. 一個執行緒可以擁有多個協程,一個程序也可以單獨擁有多個協程,這樣python中則能使用多核CPU。
  2. 執行緒程序都是同步機制,而協程則是非同步
  3. 協程能保留上一次呼叫時的狀態,每次過程重入時,就相當於進入上一次呼叫的狀態

yield

yield的語法規則是:在yield這裡暫停函式的執行,並返回yield後面表示式的值(預設為None),直到被next()方法再次呼叫時,從上次暫停的yield程式碼處繼續往下執行。當沒有可以繼續next()的時候,丟擲異常,該異常可被for迴圈處理。

總結

  • 使用了yield關鍵字的函式不再是函式,而是生成器。(使用了yield的函式就是生成器)
  • yield關鍵字有兩點作用:
    • 儲存當前執行狀態(斷點),然後暫停執行,即將生成器(函式)掛起
  • 將yield關鍵字後面表示式的值作為返回值返回,此時可以理解為起到了return的作用
  • 可以使用next()函式讓生成器從斷點處繼續執行,即喚醒生成器(函式)
  • Python3中的生成器可以使用return返回最終執行的返回值,而Python2中的生成器不允許使用return返回一個返回值(即可以使用return從生成器中退出,但return後不能有任何表示式)。

頁面置換

有哪些頁面置換演算法?

在程式執行過程中,如果要訪問的頁面不在記憶體中,就發生缺頁中斷從而將該頁調入記憶體中。此時如果記憶體已無空閒空間,系統必須從記憶體中調出一個頁面到磁碟中來騰出空間。頁面置換演算法的主要目標是使頁面置換頻率最低(也可以說缺頁率最低)。

  • 最佳頁面置換演算法OPT(Optimal replacement algorithm):置換以後不需要或者最遠的將來才需要的頁面,是一種理論上的演算法,是最優策略;
  • 先進先出FIFO:置換在記憶體中駐留時間最長的頁面。缺點:有可能將那些經常被訪問的頁面也被換出,從而使缺頁率升高;
  • 第二次機會演算法SCR:按FIFO選擇某一頁面,若其訪問位為1,給第二次機會,並將訪問位置0;
  • 時鐘演算法 Clock:SCR中需要將頁面在連結串列中移動(第二次機會的時候要將這個頁面從連結串列頭移到連結串列尾),時鐘演算法使用環形連結串列,再使用一個指標指向最老的頁面,避免了移動頁面的開銷;
  • 最近未使用演算法NRU(Not Recently Used):檢查訪問位R、修改位M,優先置換R=M=0,其次是(R=0, M=1);
  • 最近最少使用演算法LRU(Least Recently Used):置換出未使用時間最長的一頁;實現方式:維護時間戳,或者維護一個所有頁面的連結串列。當一個頁面被訪問時,將這個頁面移到連結串列表頭。這樣就能保證連結串列表尾的頁面是最近最久未訪問的。
  • 最不經常使用演算法NFU:置換出訪問次數最少的頁面

區域性性原理

  • 時間上:最近被訪問的頁在不久的將來還會被訪問;
  • 空間上:記憶體中被訪問的頁周圍的頁也很可能被訪問。

什麼是顛簸現象

顛簸本質上是指頻繁的頁排程行為。程序發生缺頁中斷時必須置換某一頁。然而,其他所有的頁都在使用,它置換一個頁,但又立刻再次需要這個頁。因此會不斷產生缺頁中斷,導致整個系統的效率急劇下降,這種現象稱為顛簸。記憶體顛簸的解決策略包括:

  • 修改頁面置換演算法;
  • 降低同時執行的程式的數量;
  • 終止該程序或增加實體記憶體容量。

磁碟排程

過程:磁頭(找到對應的盤面);磁軌(一個盤面上的同心圓環,尋道時間);扇區(旋轉時間)。為減小尋道時間的排程演算法:

  • 先來先服務
  • 最短尋道時間優先
  • 電梯演算法:電梯總是保持一個方向執行,直到該方向沒有請求為止,然後改變執行方向。
  • 電梯演算法(掃描演算法)和電梯的執行過程類似,總是按一個方向來進行磁碟排程,直到該方向上沒有未完成的磁碟請求,然後改變方向。

檔案管理

其他

垃圾回收

引用計數器

在refchain中的所有物件內部都有一個ob_refcnt用來儲存當前物件的引用計數器,顧名思義就是自己被引用的次數

age 

上述程式碼表示記憶體中有 18 和 “武沛齊” 兩個值,他們的引用計數器分別為:1、2 。

796235dc52295a382b5772395f21befb.png

當值被多次引用時候,不會在記憶體中重複建立資料,而是引用計數器+1 。 當物件被銷燬時候同時會讓引用計數器-1,如果引用計數器為0,則將物件從refchain連結串列中摘除,同時在記憶體中進行銷燬(暫不考慮快取等特殊情況)。

  • 引用計數增加的情況:
    • 一個物件分配一個新名稱
  • 將其放入一個容器中(如列表、元組或字典)
  • 引用計數減少的情況:
    • 使用del語句對物件別名顯示的銷燬
  • 引用超出作用域或被重新賦值

標記清除

原理:建立特殊連結串列專門用於儲存 列表、元組、字典、集合、自定義類等物件,之後再去檢查這個連結串列中的物件是否存在迴圈引用,如果存在則讓雙方的引用計數器均 -1。

問題:基於引用計數器進行垃圾回收非常方便和簡單,但他還是存在迴圈引用的問題,導致無法正常的回收一些資料,例如:

v1 

對於上述程式碼會發現,執行del操作之後,沒有變數再會去使用那兩個列表物件,但由於迴圈引用的問題,他們的引用計數器不為0,所以他們的狀態:永遠不會被使用、也不會被銷燬。專案中如果這種程式碼太多,就會導致記憶體一直被消耗,直到記憶體被耗盡,程式崩潰。

為了解決迴圈引用的問題,引入了標記清除技術,專門針對那些可能存在迴圈引用的物件進行特殊處理,可能存在迴圈應用的型別有:列表、元組、字典、集合、自定義類等那些能進行資料巢狀的型別。

解決辦法:在Python的底層在維護一個連結串列,連結串列中專門存放那種可能存在迴圈引用的物件(list/tuple/dict/set),在Python內部某種情況下,會去掃描可能存在迴圈引用的連結串列中的每個元素,檢測是否存在迴圈引用,如果有就讓雙方引用計數器-1,如果是0則回收。

分代回收

對標記清除中的連結串列進行優化,將那些可能存在循引用的物件拆分到3個連結串列,連結串列稱為:0/1/2三代,每代都可以儲存物件和閾值,當達到閾值時,就會對相應的連結串列中的每個物件做一次掃描,除迴圈引用各自減1並且銷燬引用計數器為0的物件。

  • 0代:0代中物件個數達到700個掃描一次。
  • 1代:0代掃描10次,則1代掃描一次。
  • 1代:1代掃描10次,則2代掃描一次。

快取機制

池(int)

# 啟動直譯器時,Python內部幫我們建立:-5、-4.....257

free list

從上文大家可以瞭解到當物件的引用計數器為0時,就會被銷燬並釋放記憶體。而實際上他不是這麼的簡單粗暴,因為反覆的建立和銷燬會使程式的執行效率變低。Python中引入了“快取機制”機制。

例如:引用計數器為0時,不會真正銷燬物件,而是將他放到一個名為 free list 的連結串列中,之後會再建立物件時不會在重新開闢記憶體,而是在free list中將之前的物件來並重置內部的值來使用。

float型別,維護的free_list連結串列最多可快取100個float物件。

v1 

總結

在Python中維護了一個refchain的雙向環狀連結串列,這個連結串列中儲存程式建立的所有物件,每種型別的物件中都有一個ob_refcnt引用計數器的值,引用個數+1、-1,最後當引用計數器變為0時會進行垃圾回收(物件銷燬、refchain中移除)。

但是,在Python中對於那些可以有多個元素組成的物件可能會存在迴圈引用的問題,為了解決這個問題Python又引入了標記清除和分代回收,在其內部維護了4個連結串列

  • refchain
  • 2代:10次
  • 1代:10次
  • 0代:700個

在原始碼內部當達到各自的閾值時,就會觸發掃描連結串列進行標記清楚的動作(有迴圈引用則各自-1),並且,原始碼在內部上述的流程中提出了優化機制

總體來說,在Python中,主要通過引用計數進行垃圾回收;通過 “標記-清除” 解決容器物件可能產生的迴圈引用問題;通過 “分代回收” 以空間換時間的方法提高垃圾回收效率。(代指的是一個物件在記憶體中經歷的垃圾清除次數)