操作系統學習筆記(一)

進程和線程

進程的常見狀態以及狀態轉換

就緒:進程已處於準備好運行的狀態,即進程已分配到除CPU外的所有必要資源後,只要再獲得CPU,便可立即執行。
執行:進程已經獲得CPU,程序正在執行狀態。
阻塞:正在執行的進程由於發生某事件(如I/O請求、申請緩衝區失敗等)暫時無法繼續執行的狀態。
在這裏插入圖片描述

線程的狀態和轉換

線程從最初的創建到最終的消亡,要經歷若干個狀態:創建(new)、就緒(runnable/start)、運行(running)、阻塞(blocked)、等待(waiting)、時間等待(time waiting) 和 消亡(dead/terminated)。

  1. 新建狀態(New):
    當用new操作符創建一個線程時, 例如new Thread®,線程還沒有開始運行,此時線程處在新建狀態。 當一個線程處於新生狀態時,程序還沒有開始運行線程中的代碼

  2. 就緒狀態(Runnable)
    一個新創建的線程並不自動開始運行,要執行線程,必須調用線程的start()方法。當線程對象調用start()方法即啓動了線程,start()方法創建線程運行的系統資源,並調度線程運行run()方法。當start()方法返回後,線程就處於就緒狀態。
    處於就緒狀態的線程並不一定立即運行run()方法,線程還必須同其他線程競爭CPU時間,只有獲得CPU時間纔可以運行線程。因爲在單CPU的計算機系統中,不可能同時運行多個線程,一個時刻僅有一個線程處於運行狀態。因此此時可能有多個線程處於就緒狀態。對多個處於就緒狀態的線程是由Java運行時系統的線程調度程序(thread scheduler)來調度的。

  3. 運行狀態(Running) :當線程獲得CPU時間後,它才進入運行狀態,真正開始執行run()方法.

  4. 阻塞狀態(Blocked)
    線程運行過程中,可能由於各種原因進入阻塞狀態:

    • 線程通過調用sleep方法進入睡眠狀態;
    • 線程調用一個在I/O上被阻塞的操作,即該操作在輸入輸出操作完成之前不會返回到它的調用者;
    • 線程試圖得到一個鎖,而該鎖正被其他線程持有;
    • 線程在等待某個觸發條件;

      所謂阻塞狀態是正在運行的線程沒有運行結束,暫時讓出CPU,這時其他處於就緒狀態的線程就可以獲得CPU時間,進入運行狀態。
  5. 死亡狀態(Dead):有兩個原因會導致線程死亡:

    • run方法正常退出而自然死亡,
    • 一個未捕獲的異常終止了run方法而使線程猝死。

爲了確定線程在當前是否存活着(就是要麼是可運行的,要麼是被阻塞了),需要使用isAlive方法。如果是可運行或被阻塞,這個方法返回true; 如果線程仍舊是new狀態且不是可運行的, 或者線程死亡了,則返回false.

在這裏插入圖片描述

進程同步

進程同步的主要任務:是對多個相關進程在執行次序上進行協調,以使併發執行的諸進程之間能有效地共享資源和相互合作,從而使程序的執行具有可再現性。

同步機制遵循的原則:

(1)空閒讓進;

(2)忙則等待(保證對臨界區的互斥訪問);

(3)有限等待(有限代表有限的時間,避免死等);

(4)讓權等待,(當進程不能進入自己的臨界區時,應該釋放處理機,以免陷入忙等狀態)。

進程的通信方式有哪些?

進程通信,是指進程之間的信息交換(信息量少則一個狀態或數值,多者則是成千上萬個字節)。因此,對於用信號量進行的進程間的互斥和同步,由於其所交換的信息量少而被歸結爲低級通信。

所謂高級進程通信指:用戶可以利用操作系統所提供的一組通信命令傳送大量數據的一種通信方式。操作系統隱藏了進程通信的實現細節。或者說,通信過程對用戶是透明的。

高級通信機制可歸結爲三大類:

  1. 共享存儲器系統(存儲器中劃分的共享存儲區);實際操作中對應的是“剪貼板”(剪貼板實際上是系統維護管理的一塊內存區域)的通信方式,比如舉例如下:word進程按下ctrl+c,在ppt進程按下ctrl+v,即完成了word進程和ppt進程之間的通信,複製時將數據放入到剪貼板,粘貼時從剪貼板中取出數據,然後顯示在ppt窗口上。

  2. 消息傳遞系統(進程間的數據交換以消息(message)爲單位,當今最流行的微內核操作系統中,微內核與服務器之間的通信,無一例外地都採用了消息傳遞機制。應用舉例:郵槽(MailSlot)是基於廣播通信體系設計出來的,它採用無連接的不可靠的數據傳輸。郵槽是一種單向通信機制,創建郵槽的服務器進程讀取數據,打開郵槽的客戶機進程寫入數據。

  3. 管道通信系統(管道即:連接讀寫進程以實現他們之間通信的共享文件(pipe文件,類似先進先出的隊列,由一個進程寫,另一進程讀))。實際操作中,管道分爲:匿名管道、命名管道。匿名管道是一個未命名的、單向管道,通過父進程和一個子進程之間傳輸數據。匿名管道只能實現本地機器上兩個進程之間的通信,而不能實現跨網絡的通信。命名管道不僅可以在本機上實現兩個進程間的通信,還可以跨網絡實現兩個進程間的通信。

  • 管道:管道是單向的、先進先出的、無結構的、固定大小的字節流,它把一個進程的標準輸出和另一個進程的標準輸入連接在一起。寫進程在管道的尾端寫入數據,讀進程在管道的道端讀出數據。數據讀出後將從管道中移走,其它讀進程都不能再讀到這些數據。管道提供了簡單的流控制機制。進程試圖讀空管道時,在有數據寫入管道前,進程將一直阻塞。同樣地,管道已經滿時,進程再試圖寫管道,在其它進程從管道中移走數據之前,寫進程將一直阻塞。
    注1:無名管道只能實現父子或者兄弟進程之間的通信,有名管道(FIFO)可以實現互不相關的兩個進程之間的通信。
       注2:用FIFO讓一個服務器和多個客戶端進行交流時候,每個客戶在向服務器發送信息前建立自己的讀管道,或者讓服務器在得到數據後再建立管道。使用客戶的進程號(pid)作爲管道名是一種常用的方法。客戶可以先把自己的進程號告訴服務器,然後再到那個以自己進程號命名的管道中讀取回復。

  • 信號量:信號量是一個計數器,可以用來控制多個進程對共享資源的訪問。它常作爲一種鎖機制,防止某進程正在訪問共享資源時,其它進程也訪問該資源。因此,主要作爲進程間以及同一進程內不同線程之間的同步手段。

  • 消息隊列:是一個在系統內核中用來保存消 息的隊列,它在系統內核中是以消息鏈表的形式出現的。消息隊列克服了信號傳遞信息少、管道只能承載無格式字節流以及緩衝區大小受限等缺點。

  • 共享內存:共享內存允許兩個或多個進程訪問同一個邏輯內存。這一段內存可以被兩個或兩個以上的進程映射至自身的地址空間中,一個進程寫入共享內存的信息,可以被其他使用這個共享內存的進程,通過一個簡單的內存讀取讀出,從而實現了進程間的通信。如果某個進程向共享內存寫入數據,所做的改動將立即影響到可以訪問同一段共享內存的任何其他進程。共享內存是最快的IPC方式,它是針對其它進程間通信方式運行效率低而專門設計的。它往往與其它通信機制(如信號量)配合使用,來實現進程間的同步和通信。
  • 套接字:套接字也是一種進程間通信機制,與其它通信機制不同的是,它可用於不同機器間的進程通信。
多線程互斥和同步
  • 同步和互斥
    當有多個線程的時候,經常需要去同步(注:同步不是同時刻)這些線程以訪問同一個數據或資源。例如,假設有一個程序,其中一個線程用於把文件讀到內存,而另一個線程用於統計文件中的字符數。當然,在把整個文件調入內存之前,統計它的計數是沒有意義的。但是,由於每個操作都有自己的線程,操作系統會把兩個線程當作是互不相干的任務分別執行,這樣就可能在沒有把整個文件裝入內存時統計字數。爲解決此問題,你必須使兩個線程同步工作。

  • 所謂同步,是指在不同進程之間的若干程序片斷,它們的運行必須嚴格按照規定的某種先後次序來運行,這種先後次序依賴於要完成的特定的任務。如果用對資源的訪問來定義的話,同步是指在互斥的基礎上(大多數情況),通過其它機制實現訪問者對資源的有序訪問。在大多數情況下,同步已經實現了互斥,特別是所有寫入資源的情況必定是互斥的。少數情況是指可以允許多個訪問者同時訪問資源。

  • 所謂互斥,是指散佈在不同進程之間的若干程序片斷,當某個進程運行其中一個程序片段時,其它進程就不能運行它們之中的任一程序片段,只能等到該進程運行完這個程序片段後纔可以運行。如果用對資源的訪問來定義的話,互斥某一資源同時只允許一個訪問者對其進行訪問,具有唯一性和排它性。但互斥無法限制訪問者對資源的訪問順序,即訪問是無序的。

多線程同步和互斥有幾種實現方法

  • 用戶模式下的方法有:原子操作(例如一個單一的全局變量),臨界區。

  • 內核模式下的方法有:事件,信號量,互斥量。

    • 臨界資源:通過對多線程的串行化來訪問公共資源或一段代碼,速度快,適合控制數據訪問。
    • 互斥量:爲協調共同對一個共享資源的單獨訪問而設計的。
    • 信號量:爲控制一個具有有限數量用戶資源而設計。
    • 事 件:用來通知線程有一些事件已發生,從而啓動後繼任務的開始。
臨界資源

在操作系統中,進程是佔有資源的最小單位(線程可以訪問其所在進程內的所有資源,但線程本身並不佔有資源或僅僅佔有一點必須資源)。但對於某些資源來說,其在同一時間只能被一個進程所佔用。這些一次只能被一個進程所佔用的資源就是所謂的臨界資源。典型的臨界資源比如物理上的打印機,或是存在硬盤或內存中被多個進程所共享的一些變量和數據等(如果這類資源不被看成臨界資源加以保護,那麼很有可能造成丟數據的問題)。

對於臨界資源的訪問,必須是互斥進行。也就是當臨界資源被佔用時,另一個申請臨界資源的進程會被阻塞,直到其所申請的臨界資源被釋放。而進程內訪問臨界資源的代碼被成爲臨界區。

Semaphore(信號量) Vs Mutex(互斥鎖)

當用戶創立多個線程/進程時,如果不同線程/進程同時讀寫相同的內容,則可能造成讀寫錯誤,或者數據不一致。此時,需要通過加鎖的方式,控制臨界區(critical section)的訪問權限。對於semaphore而言,在初始化變量的時候可以控制允許多少個線程/進程同時訪問一個臨界區,其他的線程/進程會被堵塞,直到有人解鎖。

Mutex相當於只允許一個線程/進程訪問的semaphore。此外,根據實際需要,人們還實現了一種讀寫鎖(read-write lock),它允許同時存在多個閱讀者(reader),但任何時候至多隻有一個寫者(writer),且不能於讀者共存。

進程和線程的關係

(1)一個線程只能屬於一個進程,而一個進程可以有多個線程,但至少有一個線程。線程是操作系統可識別的最小執行和調度單位。

(2)資源分配給進程,同一進程的所有線程共享該進程的所有資源。 同一進程中的多個線程共享代碼段(代碼和常量),數據段(全局變量和靜態變量),擴展段(堆存儲)。但是每個線程擁有自己的棧段,棧段又叫運行時段,用來存放所有局部變量和臨時變量。

(3)處理機分給線程,即真正在處理機上運行的是線程。

(4)線程在執行過程中,需要協作同步。不同進程的線程間要利用消息通信的辦法實現同步。

進程與線程的區別

進程是具有一定獨立功能的程序關於某個數據集合上的一次運行活動,進程是系統進行資源分配和調度的一個獨立單位。
線程是進程的一個實體,是CPU調度和分派的基本單位,它是比進程更小的能獨立運行的基本單位。

(1)進程有自己的獨立地址空間,線程沒有

(2)進程是資源分配的最小單位,線程是CPU調度的最小單位

(3)進程和線程通信方式不同(線程之間的通信比較方便。同一進程下的線程共享數據(比如全局變量,靜態變量),通過這些數據來通信不僅快捷而且方便,當然如何處理好這些訪問的同步與互斥正是編寫多線程程序的難點。而進程之間的通信只能通過進程通信的方式進行。)

(4)進程上下文切換開銷大,線程開銷小

(5)一個進程掛掉了不會影響其他進程,而線程掛掉了會影響其他線程

(6)對進程進程操作一般開銷都比較大,對線程開銷就小了

進程上下文切換和線程上下文切換
  • 上下文切換
    對於單核單線程CPU而言,在某一時刻只能執行一條CPU指令。上下文切換(Context Switch)是一種將CPU資源從一個進程分配給另一個進程的機制。從用戶角度看,計算機能夠並行運行多個進程,這恰恰是操作系統通過快速上下文切換造成的結果。在切換的過程中,操作系統需要先存儲當前進程的狀態(包括內存空間的指針,當前執行完的指令等等),再讀入下一個進程的狀態,然後執行此進程。

進程切換分兩步:

1.切換頁目錄以使用新的地址空間

2.切換內核棧和硬件上下文

對於linux來說,線程和進程的最大區別就在於地址空間,對於線程切換,第1步是不需要做的,第2是進程和線程切換都要做的。

切換的性能消耗:

  1. 線程上下文切換和進程上下問切換一個最主要的區別是線程的切換虛擬內存空間依然是相同的,但是進程切換是不同的。這兩種上下文切換的處理都是通過操作系統內核來完成的。內核的這種切換過程伴隨的最顯著的性能損耗是將寄存器中的內容切換出。

  2. 另外一個隱藏的損耗是上下文的切換會擾亂處理器的緩存機制。簡單的說,一旦去切換上下文,處理器中所有已經緩存的內存地址一瞬間都作廢了。還有一個顯著的區別是當你改變虛擬內存空間的時候,處理的頁表緩衝(processor’s Translation Lookaside Buffer (TLB))或者相當的神馬東西會被全部刷新,這將導致內存的訪問在一段時間內相當的低效。但是在線程的切換中,不會出現這個問題。

首先來一句概括的總論:進程和線程都是一個時間段的描述,是CPU工作時間段的描述。

下面細說背景:
CPU+RAM+各種資源(比如顯卡,光驅,鍵盤,GPS, 等等外設)構成我們的電腦,但是電腦的運行,實際就是CPU和相關寄存器以及RAM之間的事情。

一個最最基礎的事實:CPU太快,太快,太快了,寄存器僅僅能夠追的上他的腳步,RAM和別的掛在各總線上的設備完全是望其項背。那當多個任務要執行的時候怎麼辦呢?輪流着來?或者誰優先級高誰來?不管怎麼樣的策略,一句話就是在CPU看來就是輪流着來。

一個必須知道的事實:執行一段程序代碼,實現一個功能的過程介紹 ,當得到CPU的時候,相關的資源必須也已經就位,就是顯卡啊,GPS啊什麼的必須就位,然後CPU開始執行。這裏除了CPU以外所有的就構成了這個程序的執行環境,也就是我們所定義的程序上下文。當這個程序執行完了,或者分配給他的CPU執行時間用完了,那它就要被切換出去,等待下一次CPU的臨幸。在被切換出去的最後一步工作就是保存程序上下文,因爲這個是下次他被CPU臨幸的運行環境,必須保存。

串聯起來的事實:前面講過在CPU看來所有的任務都是一個一個的輪流執行的,具體的輪流方法就是:先加載程序A的上下文,然後開始執行A,保存程序A的上下文,調入下一個要執行的程序B的程序上下文,然後開始執行B,保存程序B的上下文。。。。

========= 重要的東西出現了========
進程和線程就是這樣的背景出來的,兩個名詞不過是對應的CPU時間段的描述,名詞就是這樣的功能。
進程就是包換上下文切換的程序執行時間總和 = CPU加載上下文+CPU執行+CPU保存上下文
線程是什麼呢?
進程的顆粒度太大,每次都要有上下的調入,保存,調出。如果我們把進程比喻爲一個運行在電腦上的軟件,那麼一個軟件的執行不可能是一條邏輯執行的,必定有多個分支和多個程序段,就好比要實現程序A,實際分成 a,b,c等多個塊組合而成。那麼這裏具體的執行就可能變成:

程序A得到CPU =》CPU加載上下文,開始執行程序A的a小段,然後執行A的b小段,然後再執行A的c小段,最後CPU保存A的上下文。

這裏a,b,c的執行是共享了A的上下文,CPU在執行的時候沒有進行上下文切換的。這裏的a,b,c就是線程,也就是說線程是共享了進程的上下文環境,的更爲細小的CPU時間段。

再一個總結:
進程和線程都是一個時間段的描述,是CPU工作時間段的描述,不過是顆粒大小不同。

進程(process)與線程(thread)最大的區別是進程擁有自己的地址空間,某進程內的線程對於其他進程不可見,即進程A不能通過傳地址的方式直接讀寫進程B的存儲區域。進程之間的通信需要通過進程間通信(Inter-process communication,IPC)。與之相對的,同一進程的各線程間之間可以直接通過傳遞地址或全局變量的方式傳遞信息。

進程作爲操作系統中擁有資源和獨立調度的基本單位,可以擁有多個線程。通常操作系統中運行的一個程序就對應一個進程。在同一進程中,線程的切換不會引起進程切換。在不同進程中進行線程切換,如從一個進程內的線程切換到另一個進程中的線程時,會引起進程切換。相比進程切換,線程切換的開銷要小很多。線程於進程相互結合能夠提高系統的運行效率。

線程可以分爲兩類:

用戶級線程(user level thread):對於這類線程,有關線程管理的所有工作都由應用程序完成,內核意識不到線程的存在。在應用程序啓動後,操作系統分配給該程序一個進程號,以及其對應的內存空間等資源。應用程序通常先在一個線程中運行,該線程被成爲主線程。在其運行的某個時刻,可以通過調用線程庫中的函數創建一個在相同進程中運行的新線程。用戶級線程的好處是非常高效,不需要進入內核空間,但併發效率不高。

內核級線程(kernel level thread):對於這類線程,有關線程管理的所有工作由內核完成,應用程序沒有進行線程管理的代碼,只能調用內核線程的接口。內核維護進程及其內部的每個線程,調度也由內核基於線程架構完成。內核級線程的好處是,內核可以將不同線程更好地分配到不同的CPU,以實現真正的並行計算。

事實上,在現代操作系統中,往往使用組合方式實現多線程,即線程創建完全在用戶空間中完成,並且一個應用程序中的多個用戶級線程被映射到一些內核級線程上,相當於是一種折中方案。

進程調度

調度種類

高級調度:(High-Level Scheduling)又稱爲作業調度,它決定把後備作業調入內存運行;

低級調度:(Low-Level Scheduling)又稱爲進程調度,它決定把就緒隊列的某進程獲得CPU;

中級調度:(Intermediate-Level Scheduling)又稱爲在虛擬存儲器中引入,在內、外存對換區進行進程對換。

非搶佔式調度與搶佔式調度

非搶佔式:分派程序一旦把處理機分配給某進程後便讓它一直運行下去,直到進程完成或發生進程調度進程調度某事件而阻塞時,才把處理機分配給另一個進程。

搶佔式:操作系統將正在運行的進程強行暫停,由調度程序將CPU分配給其他就緒進程的調度方式。

調度策略的設計

響應時間: 從用戶輸入到產生反應的時間

週轉時間: 從任務開始到任務結束的時間

CPU任務可以分爲交互式任務和批處理任務,調度最終的目標是合理的使用CPU,使得交互式任務的響應時間儘可能短,用戶不至於感到延遲,同時使得批處理任務的週轉時間儘可能短,減少用戶等待的時間。

調度算法:

FIFO或First Come, First Served (FCFS)先來先服務

  • 調度的順序就是任務到達就緒隊列的順序。

  • 公平、簡單(FIFO隊列)、非搶佔、不適合交互式。

  • 未考慮任務特性,平均等待時間可以縮短。

Shortest Job First (SJF):

  • 最短的作業(CPU區間長度最小)最先調度。

  • SJF可以保證最小的平均等待時間。

Shortest Remaining Job First (SRJF)

  • SJF的可搶佔版本,比SJF更有優勢。

  • SJF(SRJF): 如何知道下一CPU區間大小?根據歷史進行預測: 指數平均法。

優先權調度

  • 每個任務關聯一個優先權,調度優先權最高的任務。

  • 注意:優先權太低的任務一直就緒,得不到運行,出現“飢餓”現象。

Round-Robin(RR)輪轉調度算法

  • 設置一個時間片,按時間片來輪轉調度(“輪叫”算法)

  • 優點: 定時有響應,等待時間較短;缺點: 上下文切換次數較多;

  • 時間片太大,響應時間太長;吞吐量變小,週轉時間變長;當時間片過長時,退化爲FCFS。

多級隊列調度

  • 按照一定的規則建立多個進程隊列

  • 不同的隊列有固定的優先級(高優先級有搶佔權)

  • 不同的隊列可以給不同的時間片和採用不同的調度方法

  • 存在問題1:沒法區分I/O bound和CPU bound;

  • 存在問題2:也存在一定程度的“飢餓”現象;

多級反饋隊列

  • 在多級隊列的基礎上,任務可以在隊列之間移動,更細緻的區分任務。

  • 可以根據“享用”CPU時間多少來移動隊列,阻止“飢餓”。

  • 最通用的調度算法,多數OS都使用該方法或其變形,如UNIX、Windows等。

多級反饋隊列調度算法描述:

在這裏插入圖片描述

  • 進程在進入待調度的隊列等待時,首先進入優先級最高的Q1等待。

  • 首先調度優先級高的隊列中的進程。若高優先級中隊列中已沒有調度的進程,則調度次優先級隊列中的進程。例如:Q1,Q2,Q3三個隊列,只有在Q1中沒有進程等待時纔去調度Q2,同理,只有Q1,Q2都爲空時纔會去調度Q3。

  • 對於同一個隊列中的各個進程,按照時間片輪轉法調度。比如Q1隊列的時間片爲N,那麼Q1中的作業在經歷了N個時間片後若還沒有完成,則進入Q2隊列等待,若Q2的時間片用完後作業還不能完成,一直進入下一級隊列,直至完成。

  • 在低優先級的隊列中的進程在運行時,又有新到達的作業,那麼在運行完這個時間片後,CPU馬上分配給新到達的作業(搶佔式)。

一個簡單的例子
假設系統中有3個反饋隊列Q1,Q2,Q3,時間片分別爲2,4,8。現在有3個作業J1,J2,J3分別在時間 0 ,1,3時刻到達。而它們所需要的CPU時間分別是3,2,1個時間片。

  1. 時刻0 J1到達。 於是進入到隊列1 ,運行1個時間片 ,時間片還未到,此時J2到達。

  2. 時刻1 J2到達。 由於時間片仍然由J1掌控,於是等待。J1在運行了1個時間片後,已經完成了在Q1中的2個時間片的限制,於是J1置於Q2等待被調度。現在處理機分配給J2。

  3. 時刻2 J1進入Q2等待調度,J2獲得CPU開始運行。

  4. 時刻3 J3到達,由於J2的時間片未到,故J3在Q1等待調度,J1也在Q2等待調度。

  5. 時刻4 J2處理完成,由於J3,J1都在等待調度,但是J3所在的隊列比J1所在的隊列的優先級要高,於是J3被調度,J1繼續在Q2等待。

  6. 時刻5 J3經過1個時間片,完成。

  7. 時刻6 由於Q1已經空閒,於是開始調度Q2中的作業,則J1得到處理器開始運行。 J1再經過一個時間片,完成了任務。於是整個調度過程結束。

死鎖

定義:如果一組進程中的每一個進程都在等待僅由該組進程中的其他進程才能引發的事件,那麼該組進程就是死鎖的。或者在兩個或多個併發進程中,如果每個進程持有某種資源而又都等待別的進程釋放它或它們現在保持着的資源,在未改變這種狀態之前都不能向前推進,稱這一組進程產生了死鎖。通俗地講,就是兩個或多個進程被無限期地阻塞、相互等待的一種狀態。

產生死鎖的必要條件
  • 互斥條件(Mutual exclusion):資源不能被共享,只能由一個進程使用。

  • 請求與保持條件(Hold and wait):已經得到資源的進程可以再次申請新的資源。

  • 非搶佔條件(No pre-emption):已經分配的資源不能從相應的進程中被強制地剝奪。

  • 循環等待條件(Circular wait):系統中若干進程組成環路,該環路中每個進程都在等待相鄰進程正佔用的資源。

死鎖預防

死鎖預防的基本思想是動態地檢測資源分配狀態,以確保循環等待條件不成立,從而確保系統處於安全狀態。所謂安全狀態是指:如果系統能按某個順序爲每個進程分配資源(不超過其最大值),那麼系統狀態是安全的,換句話說就是,如果存在一個安全序列,那麼系統處於安全狀態。資源分配圖算法和銀行家算法是兩種經典的死鎖避免的算法,其可以確保系統始終處於安全狀態。其中,資源分配圖算法應用場景爲每種資源類型只有一個實例(申請邊,分配邊,需求邊,不形成環才允許分配),而銀行家算法應用於每種資源類型可以有多個實例的場景。
   具體方法包括:

  • 打破互斥條件:允許進程同時訪問某些資源。但是,有些資源是不能被多個進程所共享的,這是由資源本身屬性所決定的,因此,這種辦法通常並無實用價值。

  • 打破佔有並等待條件:可以實行資源預先分配策略(進程在運行前一次性向系統申請它所需要的全部資源,若所需全部資源得不到滿足,則不分配任何資源,此進程暫不運行;只有當系統能滿足當前進程所需的全部資源時,才一次性將所申請資源全部分配給該線程)或者只允許進程在沒有佔用資源時纔可以申請資源(一個進程可申請一些資源並使用它們,但是在當前進程申請更多資源之前,它必須全部釋放當前所佔有的資源)。但是這種策略也存在一些缺點:在很多情況下,無法預知一個進程執行前所需的全部資源,因爲進程是動態執行的,不可預知的;同時,會降低資源利用率,導致降低了進程的併發性。

  • 打破非搶佔條件:允許進程強行從佔有者哪裏奪取某些資源。也就是說,但一個進程佔有了一部分資源,在其申請新的資源且得不到滿足時,它必須釋放所有佔有的資源以便讓其它線程使用。這種預防死鎖的方式實現起來困難,會降低系統性能。

  • 打破循環等待條件:實行資源有序分配策略。對所有資源排序編號,所有進程對資源的請求必須嚴格按資源序號遞增的順序提出,即只有佔用了小號資源才能申請大號資源,這樣就不回產生環路,預防死鎖的發生。

死鎖解除

死鎖解除的常用兩種方法爲進程終止和資源搶佔。所謂進程終止是指簡單地終止一個或多個進程以打破循環等待,包括兩種方式:終止所有死鎖進程和一次只終止一個進程直到取消死鎖循環爲止;所謂資源搶佔是指從一個或多個死鎖進程那裏搶佔一個或多個資源,此時必須考慮三個問題:

  1. 選擇一個犧牲品
  2. 回滾:回滾到安全狀態
  3. 飢餓(在代價因素中加上回滾次數,回滾的越多則越不可能繼續被作爲犧牲品,避免一個進程總是被回滾)

內存池、進程池、線程池

首先介紹一個概念“池化技術 ”。池化技術就是:提前保存大量的資源,以備不時之需以及重複使用。池化技術應用廣泛,如內存池,線程池,連接池等等。內存池相關的內容,建議看看Apache、Nginx等開源web服務器的內存池實現。
  由於在實際應用當做,分配內存、創建進程、線程都會設計到一些系統調用,系統調用需要導致程序從用戶態切換到內核態,是非常耗時的操作。因此,當程序中需要頻繁的進行內存申請釋放,進程、線程創建銷燬等操作時,通常會使用內存池、進程池、線程池技術來提升程序的性能。

線程池:線程池的原理很簡單,類似於操作系統中的緩衝區的概念,它的流程如下:先啓動若干數量的線程,並讓這些線程都處於睡眠狀態,當需要一個開闢一個線程去做具體的工作時,就會喚醒線程池中的某一個睡眠線程,讓它去做具體工作,當工作完成後,線程又處於睡眠狀態,而不是將線程銷燬。

進程池與線程池同理。

內存池:內存池是指程序預先從操作系統申請一塊足夠大內存,此後,當程序中需要申請內存的時候,不是直接向操作系統申請,而是直接從內存池中獲取;同理,當程序釋放內存的時候,並不真正將內存返回給操作系統,而是返回內存池。當程序退出(或者特定時間)時,內存池纔將之前申請的內存真正釋放。

程序從開始運行到結束的四個過程

  1. 預處理:條件編譯,頭文件包含,宏替換的處理,生成.i文件。

  2. 編譯:將預處理後的文件轉換成彙編語言,生成.s文件

  3. 彙編:彙編變爲目標代碼(機器代碼)生成.o的文件

  4. 鏈接:連接目標代碼,生成可執行程序

動態鏈接庫與靜態鏈接庫的區別
  1. 靜態庫:靜態庫是一個外部函數與變量的集合體。靜態庫的文件內容,通常包含一堆程序員自定的變量與函數,其內容不像動態鏈接庫那麼複雜,在編譯期間由編譯器與鏈接器將它集成至應用程序內,並製作成目標文件以及可以獨立運作的可執行文件。而這個可執行文件與編譯可執行文件的程序,都是一種程序的靜態創建(static build)。
    在這裏插入圖片描述

  2. 動態庫:靜態庫很方便,但是如果我們只是想用庫中的某一個函數,卻仍然得把所有的內容都鏈接進去。一個更現代的方法則是使用共享庫,避免了在文件中靜態庫的大量重複。
    動態鏈接可以在首次載入的時候執行(load-time linking),這是 Linux 的標準做法,會由動態鏈接器ld-linux.so 完成,比方標準 C 庫(libc.so) 通常就是動態鏈接的,這樣所有的程序可以共享同一個庫,而不用分別進行封裝。
    動態鏈接也可以在程序開始執行的時候完成(run-time linking),在 Linux 中使用 dlopen()接口來完成(會使用函數指針),通常用於分佈式軟件,高性能服務器上。而且共享庫也可以在多個進程間共享。

鏈接使得我們可以用多個對象文件構造我們的程序。可以在程序的不同階段進行(編譯、載入、運行期間均可),理解鏈接可以幫助我們避免遇到奇怪的錯誤。
在這裏插入圖片描述

區別:

  • 使用靜態庫的時候,靜態鏈接庫要參與編譯,在生成執行文件之前的鏈接過程中,要將靜態鏈接庫的全部指令直接鏈接入可執行文件中。而動態庫提供了一種方法,使進程可以調用不屬於其可執行代碼的函數。函數的可執行代碼位於一個.dll文件中,該dll包含一個或多個已被編譯,鏈接並與使用它們的進程分開儲存的函數。
  • 靜態庫中不能再包含其他動態庫或靜態庫,而在動態庫中還可以再包含其他動態或者靜態庫。
    靜態庫在編譯的時候,就將庫函數裝在到程序中去了,而動態庫函數必須在運行的時候才被裝載,所以使用靜態庫速度快一些。

虛擬內存

內存的發展歷程

  1. 沒有內存抽象(單進程,除去操作系統所用的內存之外,全部給用戶程序使用)
  2. 有內存抽象(多進程,進程獨立的地址空間,交換技術(內存大小不可能容納下所有併發執行的進程)
  3. 連續內存分配(固定大小分區(多道程序的程度受限),可變分區(首次適應,最佳適應,最差適應),碎片)
  4. 不連續內存分配(分段,分頁,段頁式,虛擬內存)

虛擬內存
  虛擬內存允許執行進程不必完全在內存中。虛擬內存的基本思想是:每個進程擁有獨立的地址空間,這個空間被分爲大小相等的多個塊,稱爲頁(Page),每個頁都是一段連續的地址。這些頁被映射到物理內存,但並不是所有的頁都必須在內存中才能運行程序。當程序引用到一部分在物理內存中的地址空間時,由硬件立刻進行必要的映射;當程序引用到一部分不在物理內存中的地址空間時,由操作系統負責將缺失的部分裝入物理內存並重新執行失敗的命令。這樣,對於進程而言,邏輯上似乎有很大的內存空間,實際上其中一部分對應物理內存上的一塊(稱爲幀,通常頁和幀大小相等),還有一些沒加載在內存中的對應在硬盤上,如圖所示。
  在這裏插入圖片描述
  由圖中可以看出,虛擬內存實際上可以比物理內存大。當訪問虛擬內存時,會訪問MMU(內存管理單元)去匹配對應的物理地址(比如圖5的0,1,2)。如果虛擬內存的頁並不存在於物理內存中(如圖5的3,4),會產生缺頁中斷,從磁盤中取得缺的頁放入內存,如果內存已滿,還會根據某種算法將磁盤中的頁換出。

與傳統存儲器比較虛擬存儲器有以下三個主要特徵:

  • 多次性,是指無需在作業運行時一次性地全部裝入內存,而是允許被分成多次調入內存運行。
  • 對換性,是指無需在作業運行時一直常駐內存,而是允許在作業的運行過程中,進行換進和換出。
  • 虛擬性,是指從邏輯上擴充內存的容量,使用戶所看到的內存容量,遠大於實際的內存容量。

虛擬內存的應用與優點

虛擬內存很適合在多道程序設計系統中使用,許多程序的片段同時保存在內存中。當一個程序等待它的一部分讀入內存時,可以把CPU交給另一個進程使用。虛擬內存的使用可以帶來以下好處:

  • 在內存中可以保留多個進程,系統併發度提高

  • 解除了用戶與內存之間的緊密約束,進程可以比內存的全部空間還大

虛擬內存的實現方式

  • 請求分頁存儲管理。
  • 請求分段存儲管理。
局部性原理
  • 時間上的局部性:最近被訪問的頁在不久的將來還會被訪問;

  • 空間上的局部性:內存中被訪問的頁周圍的頁也很可能被訪問。

分頁和分段的區別
  • 段式存儲管理是一種符合用戶視角的內存分配管理方案。在段式存儲管理中,將程序的地址空間劃分爲若干段(segment),如代碼段,數據段,堆棧段;這樣每個進程有一個二維地址空間,相互獨立,互不干擾。段式管理的優點是:沒有內碎片(因爲段大小可變,改變段大小來消除內碎片)。但段換入換出時,會產生外碎片(比如4k的段換5k的段,會產生1k的外碎片)

  • 頁式存儲管理方案是一種用戶視角內存與物理內存相分離的內存分配管理方案。在頁式存儲管理中,將程序的邏輯地址劃分爲固定大小的頁(page),而物理內存劃分爲同樣大小的幀,程序加載時,可以將任意一頁放入內存中任意一個幀,這些幀不必連續,從而實現了離散分離。頁式存儲管理的優點是:沒有外碎片(因爲頁的大小固定),但會產生內碎片(一個頁可能填充不滿)。

兩者的不同點:

  • 目的不同:分頁是由於系統管理的需要而不是用戶的需要,它是信息的物理單位;分段的目的是爲了能更好地滿足用戶的需要,它是信息的邏輯單位,它含有一組其意義相對完整的信息;

  • 大小不同:頁的大小固定且由系統決定,而段的長度卻不固定,由其所完成的功能決定;

  • 地址空間不同: 段向用戶提供二維地址空間;頁向用戶提供的是一維地址空間;

  • 信息共享:段是信息的邏輯單位,便於存儲保護和信息的共享,頁的保護和共享受到限制;

  • 內存碎片:頁式存儲管理的優點是沒有外碎片(因爲頁的大小固定),但會產生內碎片(一個頁可能填充不滿);而段式管理的優點是沒有內碎片(因爲段大小可變,改變段大小來消除內碎片)。但段換入換出時,會產生外碎片(比如4k的段換5k的段,會產生1k的外碎片)。

內部碎片與外部碎片

在內存管理中,內部碎片是已經被分配出去的的內存空間大於請求所需的內存空間。

外部碎片是指還沒有分配出去,但是由於大小太小而無法分配給申請空間的新進程的內存空間空閒塊。

固定分區存在內部碎片,可變式分區分配會存在外部碎片;

頁式虛擬存儲系統存在內部碎片;段式虛擬存儲系統存在外部碎片

爲了有效的利用內存,使內存產生更少的碎片,要對內存分頁,內存以頁爲單位來使用,最後一頁往往裝不滿,於是形成了內部碎片。

爲了共享要分段,在段的換入換出時形成外部碎片,比如5K的段換出後,有一個4k的段進來放到原來5k的地方,於是形成1k的外部碎片。

頁面置換算法

操作系統將內存按照頁面進行管理,在需要的時候才把進程相應的部分調入內存。當產生缺頁中斷時,需要選擇一個頁面寫入。如果要換出的頁面在內存中被修改過,變成了“髒”頁面,那就需要先寫會到磁盤。頁面置換算法,就是要選出最合適的一個頁面,使得置換的效率最高。頁面置換算法有很多,簡單介紹幾個,重點介紹比較重要的LRU及其實現算法。

  1. 最優頁面置換算法

最理想的狀態下,我們給頁面做個標記,挑選一個最遠纔會被再次用到的頁面調出。當然,這樣的算法不可能實現,因爲不確定一個頁面在何時會被用到。

  1. 先進先出頁面置換算法(FIFO)及其改進

這種算法的思想和隊列是一樣的,該算法總是淘汰最先進入內存的頁面,即選擇在內存中駐留時間最久的頁面予淘汰。實現:把一個進程已調入內存的頁面按先後次序鏈接成一個隊列,並且設置一個指針總是指向最老的頁面。缺點:對於有些經常被訪問的頁面如含有全局變量、常用函數、例程等的頁面,不能保證這些不被淘汰。

  1. 最近最少使用頁面置換算法LRU(Least Recently Used)
    根據頁面調入內存後的使用情況做出決策。LRU置換算法是選擇最近最久未使用的頁面進行淘汰。
    1. 爲每個在內存中的頁面配置一個移位寄存器。(P165)定時信號將每隔一段時間將寄存器右移一位。最小數值的寄存器對應頁面就是最久未使用頁面。
    2. 利用一個特殊的棧保存當前使用的各個頁面的頁面號。每當進程訪問某頁面時,便將該頁面的頁面號從棧中移出,將它壓入棧頂。因此,棧頂永遠是最新被訪問的頁面號,棧底是最近最久未被訪問的頁面號。
顛簸

顛簸本質上是指頻繁的頁調度行爲,具體來講,進程發生缺頁中斷,這時,必須置換某一頁。然而,其他所有的頁都在使用,它置換一個頁,但又立刻再次需要這個頁。因此,會不斷產生缺頁中斷,導致整個系統的效率急劇下降,這種現象稱爲顛簸(抖動)。

內存顛簸的解決策略包括:

  • 如果是因爲頁面替換策略失誤,可以修改替換算法來解決這個問題;

  • 如果是因爲運行的程序太多,造成程序無法同時將所有頻繁訪問的頁面調入內存,則要降低多道程序的數量;

否則,還剩下兩個辦法:終止該進程或增加物理內存容量。

守護、殭屍、孤兒進程的概念

  • 守護進程:運行在後臺的一種特殊進程,獨立於控制終端並週期性地執行某些任務。

  • 殭屍進程:一個進程 fork 子進程,子進程退出,而父進程沒有wait/waitpid子進程,那麼子進程的進程描述符仍保存在系統中,這樣的進程稱爲殭屍進程

  • 孤兒進程:一個父進程退出,而它的一個或多個子進程還在運行,這些子進程稱爲孤兒進程。(孤兒進程將由 init 進程收養並對它們完成狀態收集工作)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章