操作系統筆記(一):進程和線程

進程、線程

進程(process)和線程(thread)的區別

進程擁有獨立的用戶空間;同一個進程的多個線程共享進程的用戶空間資源,存在安全問題,在多線程併發的情況下,需要對共享資源進行加鎖,以獲得正確的結果。
進程優勢:進程之間相互獨立,可以減少因一個進程異常或者退出帶來的風險;進程不需要加鎖,可以節省鎖帶來的消耗。
線程優勢:fork進程是消耗很大的。fork要把父進程的內存映像拷貝到子進程,並在子進程中複製所有描述符,如此等等。線程提供一種併發能力,可以在一個進程中的同一時刻運行多個線程。每個線程都有自己的硬件寄存器和堆棧。一個進程中的所有線程共享全部虛擬空間地址、所有文件描述符、信號行爲和其他的進程資源。

進程和線程本質上都是CPU的一個工作時間段,進程包括了CPU加載程序上下文、CPU執行、CPU保存程序上下文。線程包含在進程中,一個進程至少有一個線程,也可以有多個線程,進程的不同線程之間共享CPU和程序上下文。
進程是操作系統資源分配(包括CPU、內存、磁盤IO等)的最小單元;線程是CPU調度和分配的基本單元,是最小的執行單元。
PS:不同進程之間的連續虛擬地址空間被MMU映射到不同的離散物理地址,因此不同進程間的用戶空間的數據是不共享的。此外,內核空間的代碼和數據是不同進程之間共享的。

協程(coroutine),協同程序

線程的切換需要進入內核態,協程的切換完全在用戶態進行,效率更高。
協程又稱微線程,是一段子程序。在執行過程中,子程序內部可以中斷,然後轉而執行別的子程序,在適當的時候再返回來接着執行。(在一個子程序中中斷,去執行其他子程序,不是函數調用!)
協程是子程序的切換而不是線程的切換,和多線程相比,節省了線程切換的開銷,執行效率高。
PS:協程切換完全在用戶態進行,它的開銷只有切換CPU上下文;線程切換隻有最高權限的內核態才能完成,它的開銷除了CPU上下文切換,還有用戶態和內核態的切換的開銷以及線程調度算法完成線程調度的開銷。
PS:多線程+協程,即充分用多核,又充分發揮協程的高效率,可以獲得極高的性能。

進程調度策略

  1. 先來先服務(FCFS)調度算法:隊列實現,先進入作業隊列的作業先分配資源、創建進程,然後放入就緒隊列,非搶佔,平均等待時間通常比較長。
  2. 最短作業優先(SJF)調度算法:對一個或若干個短作業或者短進程優先調度,平均等待時間最小,但無法獲知進程下個CPU區間的長度。
  3. 優先級調度算法:優先級越高越先分配,相同優先級按FCFS分配。優先級低的進程無窮等待CPU,會導致無窮阻塞問題,或稱爲飢餓。可以通過老化技術解決該問題,即逐漸增加在系統中等待時間長的進程的優先級。
  4. 時間片輪轉調度算法:專門爲分時系統設計,如果進程的CPU區間超過了一個時間片,那麼該進程就會被搶佔並放回就緒隊列。
  5. 多級隊列調度算法:將就緒隊列分成多個獨立的隊列,每個隊列都有自己的調度算法,隊列之間採用固定優先級搶佔調度。一個進程根據自身的屬性被永久地分配到一個隊列中。
  6. 多級反饋隊列調度算法:相比於多級隊列調度算法,多級反饋隊列調度算法允許進程根據實際情況在隊列之間移動。若進程使用過多的CPU時間,那麼它會被轉移到更低的優先級隊列;在較低優先級隊列等待時間過長的進程會被轉移到更高優先級的隊列,以防止飢餓發生。

進程間、線程間通訊的方式

參考博客
進程間通訊:管道及命名管道、信號/事件、消息隊列、共享內存、信號量、套接字。
管道及命名管道主要用於本地進程間通信,套接字主要用於網絡進程間通信,共享內存和信號量主要用於進程間的同步。
線程間通訊:鎖機制(互斥量、條件變量、讀寫鎖、自旋鎖),信號量機制,信號機制/事件機制。
線程間的通信目的主要是用於線程同步,所以線程沒有像進程通信中的用於數據交換的通信機制。

進程間的通信方式——pipe(管道)
消息隊列的使用場景是怎樣的?

進程、線程的同步方式

進程/線程同步主要是爲解決對共享數據(共享區)的競爭訪問問題,所以同步是指對共享區的訪問同步化(按照既定的先後次序,一個訪問需要阻塞等待前一個訪問完成後才能開始)。

進程同步:信號量(計數器,PV操作)、自旋鎖(調用者循環等待)、管程(管程內定義的操作在同一時刻只被一個進程調用)、會合(會合是適用於不具有公共內存的分佈式系統的同步機制)。

Linux 信號量
信號量有兩種實現:System V信號量和Posix信號量,System V信號量由semget、semop、semctl這幾個函數來實現,Posix信號量由sem_init、sem_destroy、sem_wait(相當於P操作)、sem_post(相當於V操作)這幾個函數來實現。(System V和Posix都是應用於操作系統的接口協議)
信號量的P、V操作:信號量只有兩種操作,等待和發送信號,分別用P(s)、V(s)表示。P、V操作是不可分割的(原子操作)。調用P操作測試消息是否到達,調用V操作發送消息。

  • P(s): 進程申請資源,如果資源已達到最大進程訪問限制(s爲0),則需要等待。如果s的值大於0,那麼P將s的值減1。如果s爲0,那麼就掛起這個進程直到s變爲非零。一個V操作會喚醒這個線程。
  • V(s):釋放資源。將s加1,如果有進程等待s變爲非0,那麼喚醒該進程,該進程將s減1。

詳細解釋:將s設置爲訪問此資源的最大進程數目,每增加一個進程對共享資源的訪問,s減1,只要當s大於0,就可以發出信號量,允許申請資源的進程訪問該共享資源。但當s減小到0時,說明當前佔用資源的進程數已經達到了所允許的最大數目,不允許其他進程訪問該共享資源,此時的信號量將無法發出。進程在處理完共享資源後,s加1。

線程同步:臨界區(Critical Section)、鎖機制【互斥鎖(Mutex)和條件變量(Condition_variable)、讀寫鎖(Read-write lock)、自旋鎖(Spin lock)】、信號量(Semaphore)、信號/事件(Event)。

概括:

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

semaphore和mutex的區別?
信號量與互斥鎖的區別:

  • 互斥鎖:一個二元變量,0-開鎖、1-上鎖,開鎖操作必須由上鎖的線程執行,主要目的是保護共享資源,即保證了共享資源一次只能被一個線程使用(臨界資源);
  • 信號量:一個非負整數值,主要目的是調度線程/進程,允許多個線程/進程同一時刻訪問一個共享資源,但會限制在同一時刻訪問此資源的最大線程/進程數目。如果信號量的值只爲0或1,那麼它就是一個二元信號量,功能就想當於一個互斥鎖。

[多線程] 互斥量和臨界區的區別
臨界區和互斥鎖的區別:

  • 臨界區:臨界區是指一段代碼,這段代碼是用來訪問臨界資源的。臨界資源可以是硬件資源,也可以是軟件資源。但它們有一個特點就是,一次僅允許一個進程或線程訪問。當有多個線程試圖同時訪問,但已經有一個線程在訪問該臨界區了,那麼其他線程將被掛起。臨界區被釋放後,其他線程可繼續搶佔該臨界區。
  • 與互斥鎖區別:臨界區是一種輕量級的同步機制,與互斥和事件這些內核同步對象相比,臨界區是用戶態下的對象,即只能在同一進程中實現線程互斥。因無需在用戶態和核心態之間切換,所以工作效率比較互斥來說要高很多;互斥鎖是可以命名的,因此互斥鎖不僅可以用於同一應用程序不同線程中共享資源訪問的同步,也可以用於不同應用程序的線程之間實現對共享資源訪問的同步。(互斥鎖可以在整個系統中被任意進程的任意線程訪問到,但它嚴格限定只有獲取了互斥量的線程才能釋放該互斥鎖

用戶空間和內核空間、用戶態和內核態:

  • 不同進程之間的內核空間的資源是共享的,用戶空間的資源是獨立的。因此,要實現不同進程之間或者不同進程的線程之間資訪問源的同步,必須進入內核態,由內核中的同步對象實現進程同步。而對於同一進程的不同線程之間用戶空間和內核空間的資源都是共享的,因此可以只用用戶態下的同步對象實現線程的同步。

讀寫鎖:

  • 若當前線程讀數據,則允許其他線程讀數據,但不允許寫;
  • 若當前線程寫數據,則不允許其他線程讀、寫數據。

死鎖產生的條件和處理策略

死鎖產生的四個必要條件

  1. 互斥:至少有一個資源一次只能被一個進程使用。
  2. 佔有並等待:一個進程因請求資源而阻塞時,對已獲得的資源保持不放。
  3. 非搶佔式:進程已獲得的資源在未使用完之前不能被搶佔。
  4. 循環等待:若干進程之間形成一種頭尾相接的循環等待資源關係。

死鎖的處理

  1. 預防死鎖:確保死鎖產生的四個必要條件中至少有一個不成立。打破“非搶佔式”條件,允許進程強行從佔有者那奪取某些資源;打破“佔有並等待”條件,只允許進程在沒有佔用資源時纔可以申請資源。
  2. 避免死鎖:動態地檢測資源分配狀態,以確保循環等待條件不成立。
  3. 檢測、解除死鎖:允許死鎖發生,但可以檢測出死鎖的發生並從系統中將已發生的死鎖清除,常用的解除死鎖的方法爲進程終止和資源搶佔。

生產者消費者模式

生產者和消費者問題是線程模型的經典問題:生產者和消費者在同一時間段內共用同一個存儲空間,生產者往存儲空間中添加產品,消費者從存儲空間中取走產品,當存儲空間爲空時,消費者阻塞,當存儲空間滿時,生產者阻塞
單生產者-單消費者:緩存隊列+互斥量+條件變量實現。(隊列模擬存儲空間;互斥量保證多個讀寫線程之間互斥;條件變量保證隊列爲空時消費者線程會被阻塞,等待隊列非空,隊列爲滿時生產者線程會被組設,等待隊列非滿) -> 應用場景:緩存IO(如TCP套接字的緩衝區)
簡單的實現:
維護兩個位置變量和條件變量:

size_t read_pos; // 消費者讀取產品位置.
size_t write_pos; // 生產者寫入產品位置.
std::mutex mtx; // 互斥量,保護產品緩衝區
std::condition_variable repo_not_full; // 條件變量, 指示產品緩衝區不爲滿.
std::condition_variable repo_not_empty; // 條件變量, 指示產品緩衝區不爲空.

當(write_pos+1)%repository_size == read_pos時表明隊列已滿,要阻塞生產。(repository_size爲存儲空間(緩存)的大小)

...
while(((ir->write_pos + 1) % repository_size) == ir->read_pos) { 
// item buffer is full, just wait here.
    (ir->repo_not_full).wait(lock); // 生產者等待"產品庫緩衝區不爲滿"這一條件發生.
}
... // 寫入產品,寫入位置後移
(ir->repo_not_empty).notify_all(); // 通知消費者產品庫不爲空.

當write_pos == read_pos時,表明隊列爲空,需要阻塞消費者。

...
while(ir->write_pos == ir->read_pos) {
// item buffer is empty, just wait here
    (ir->repo_not_empty).wait(lock); // 消費者等待"產品庫緩衝區不爲空"這一條件發生.
}
... // 讀取產品,讀取位置後移
(ir->repo_not_full).notify_all(); // 通知消費者產品庫不爲滿.

多生產者-多消費者:需要額外維護消費者取走產品的計數器和生產者放入產品的計數器,用於判斷程序是否要結束。
PS:生產者消費者模式還可以用協程實現。改用協程,生產者生產消息後,直接通過yield 跳轉到消費者開始執行,待消費者執行完畢後,切換回生產者繼續生產,效率極高。(參考博客
協程

Nginx

Nginx是一個高性能的HTTP和反向代理web服務器。
在這裏插入圖片描述
面試官常問的Nginx的幾個問題

Nginx高性能服務器,爲啥高性能?

總結:多進程優勢、異步非阻塞I/O(異步主進程由事件驅動+非阻塞socket)、epoll模型(單個進程高效地處理多個連接)。

  • 儘可能限制工作進程的數量,從而減少上下文切換帶來的開銷。默認和推薦配置是讓每個CPU內核對應一個工作進程,從而高效利用硬件資源。
  • 工作進程採用單線程,並以異步非阻塞的事件處理機制來處理多個併發連接,I/O多路複用採用epoll模型

Nginx是如何實現高併發的?

採用了I/O多路複用原理使單個進程能處理多個連接,通過異步非阻塞的事件處理機制,epoll模型,實現輕量級和高併發。
nginx實現高併發的原理
一張圖讀懂nginx多線程高併發

用進程而不用線程的好處

  • 節省鎖帶來的開銷,每個worker進程都是獨立的進程,不共享資源,不需要加鎖。同時,在編程及問題查找時,也會方便很多。
  • 獨立進程,減少風險。採用獨立的進程,可以讓互相之間不會影響,一個進程退出後,其它進程還在工作,服務不會中斷,master進程則會很快啓動新的worker進程。

正向代理和反向代理

  • 正向代理是一個位於客戶端和原始服務器(origin server)之間的服務器,爲了從原始服務器取得內容,客戶端向正向代理髮送一個請求並指定目標(原始服務器)。然後,正向代理向原始服務器轉交請求並將獲得的內容返回給客戶端。客戶端才能使用正向代理。正向代理總結就一句話:代理端代理的是客戶端
  • 反向代理(Reverse Proxy)方式是指以代理服務器來接受internet上的連接請求,然後將請求發給內部網絡上的服務器並將從服務器上得到的結果返回給internet上請求連接的客戶端,此時代理服務器對外就表現爲一個反向代理服務器。反向代理總結就一句話:代理端代理的是服務端

負載均衡

負載均衡即是代理服務器將接收的請求均衡的分發到各服務器中。負載均衡主要解決網絡擁塞問題,提高服務器響應速度,服務就近提供,達到更好的訪問質量,減少後臺服務器大併發壓力。

動態資源和靜態資源

  • 靜態資源:一般客戶端發送請求到web服務器,web服務器從內存中取到相應的文件,返回給客戶端,客戶端解析並渲染顯示出來。
  • 動態資源:一般客戶端請求的動態資源,先將請求交於web容器,web容器連接數據庫,數據庫處理數據之後,將內容交給web服務器,web服務器返回給客戶端解析渲染處理。

nginx實現tomcat動靜分離

以下內容源自:nginx實現tomcat動靜分離詳解

爲什麼要實現動靜分離

  1. Nginx的處理靜態資源能力超強
    主要是nginx處理靜態頁面的效率遠高於tomcat的處理能力,如果tomcat的請求量爲1000次,則nginx的請求量爲6000次,tomcat每秒的吞吐量爲0.6M,nginx的每秒吞吐量爲3.6M,可以說,nginx處理靜態資源的能力是tomcat處理能力的6倍,優勢可見一斑。
  2. 動態資源和靜態資源分開,使服務器結構更清晰。

動靜分離原理

服務端接收來自客戶端的請求中,有一部分是靜態資源的請求,例如html、css、js和圖片資源等等,有一部分是動態數據的請求。因爲tomcat處理靜態資源的速度比較慢,所以我們可以考慮把所有靜態資源獨立開來,交給處理靜態資源更快的服務器,例如nginx處理,而把動態請求交給tomcat處理。

如下圖所示,我們在機器上同時安裝了nginx和tomcat,把所有的靜態資源都放置在nginx的webroot目錄下面,把動態請求的程序都放在tomcat的webroot目錄下面,當客戶端訪問服務端的時候,如果是靜態資源的請求,就直接到nginx的webroot目錄下面獲取資源,如果是動態資源的請求,nginx利用反向代理的原理,把請求轉發給tomcat進行處理,這樣就實現了動靜分離,提高了服務器處理請求的性能。
在這裏插入圖片描述

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