[作業系統]

Duancf發表於2024-07-05

IO多路複用

程序

程序間通訊

六種方式

管道/訊息佇列/訊號/訊號量/共享記憶體/socket/

管道

管道分為命名管道和無名管道,在核心中申請一塊固定大小的緩衝區,程式擁有寫入和讀取的權利,都可以看成一種特殊的檔案,具有固定的讀端和寫端,也可以使用普通的read、write 等函式。但是它不是普通的檔案,並不屬於其他任何檔案系統,並且只存在於記憶體中;無名管道一般使用fork函式實現父子程序的通訊,命名管道用於沒有血緣關係的程序也可以程序間通訊;面向位元組流、自帶同步互斥機制、半雙工,單向通訊,兩個管道實現雙向通訊。

訊息佇列

在核心中建立一佇列,佇列中每個元素是一個資料包,不同的程序可以透過控制代碼去訪問這個佇列;訊息佇列獨立於傳送與接收程序,可以透過順序和訊息型別讀取,也可以fifo讀取;訊息佇列可實現雙向通訊。

訊號量

在核心中建立一個訊號量集合(本質是個陣列),陣列的元素(訊號量)都是1,使用P操作進行-1,使用V操作+1,透過對臨界資源進行保護實現多程序的同步

共享記憶體

將同一塊實體記憶體一塊對映到不同的程序的虛擬地址空間中,實現不同程序間對同一資源的共享。目前最快的IPC形式,不用從使用者態到核心態的頻繁切換和複製資料,直接從記憶體中讀取就可以,共享記憶體是臨界資源,所以需要操作時必須要保證原子性。使用訊號量或者互斥鎖都可以。

socket

是應用層與TCP/IP協議族通訊的中間軟體抽象層,它是一組介面,把複雜的TCP/IP協議族隱藏在Socket介面後面,對使用者來說,一組簡單的介面就是全部,讓Socket去組織資料。socket起源於UNIX,在Unix一切皆檔案哲學的思想下,socket是一種”開啟—讀/寫—關閉”模式的實現,伺服器和客戶端各自維護一個”檔案”,在建立連線開啟後,可以向自己檔案寫入內容供對方讀取或者讀取對方內容,通訊結束時關閉檔案。是一種可以網間通訊的方式。

程序和執行緒的區別

  • 根本區別

    • 程序是作業系統(OS)資源分配(記憶體,檔案,網路,裝置)的基本單位,
    • 執行緒是處理器(CPU)任務排程和執行的基本單位。
  • 資源開銷:

    • 每個程序都有獨立的程式碼和資料空間(程式上下文),程式之間的切換會有較大的開銷;

    • 執行緒可以看做輕量級的程序,同一類執行緒共享程式碼和資料空間,每個執行緒都有自己獨立的執行棧和程式計數器(PC),執行緒之間切換的開銷小。

  • 包含關係:
    如果一個程序內有多個執行緒,則執行過程不是一條線的,而是多條線
    同完成的;執行緒是程序的一部分,所行過程不是一條線的,而是多條線(線耗)其被稱為輕權程序或者輕量級程序。

  • 記憶體分配:
    同一程序的執行緒共享本程序的空間和資源,而程序之間的地址空間和資源是相互獨立的。

  • 影響關係:
    一個程序崩潰後,在保護模式下不會對其他程序產生影響,但是一個執行緒崩潰整個程序都死掉。所以多程序要比多執行緒健壯。

  • 執行過程:
    每個獨立的程序有程式執行的入口、順序執行序列和程式出口。但是執行緒不能獨立執行,必須依存在應用程式中,由應用程式提供多個執行緒執行控制,兩者均可併發執行。

為什麼執行緒切換比程序快?

首先我們需要了解一個概念:上下文切換
直接說概念可能有點看不懂,我們來舉個例子:比如我們正在洗衣服,這時候突然有人打來電話,那麼我們就需要放下手中的衣服去接電話,這就好比是程序切換,從洗衣服的程序換到打電話的程序,打完電話,我們還需要接著洗衣服,那麼此時我們有什麼呢?有洗了一半的衣服,有開啟的洗衣液,有水,衣服還處在剛剛洗了一半的狀態,這一系列的狀態就可以理解為上下文。

回到計算機的世界裡,核心為每一個程序維持一個上下文,上下文就是重新啟動一個程序所需的狀態,包含以下內容:

  • 通用目的暫存器

  • 浮點暫存器

  • 程式計數器

  • 使用者棧

  • 狀態暫存器

  • 核心棧

  • 各種核心資料結構:比如描繪地址空間的頁表,包含有關當前程序資訊的程序表,包含程序已開啟檔案資訊的檔案表

程序的切換實際上就是上下文切換

那麼為什麼執行緒切換比程序快呢?這裡我們還需要理解一個概念:虛擬記憶體

虛擬記憶體是作業系統為每個程序提供的一種抽象的,私有的,連續地址的虛擬記憶體空間,但是我們都知道實際上程序的資料以及程式碼必然要放到實體記憶體上,那麼我們怎麼知道虛擬空間中的陣列實際上存放的具體位置呢?答案就是頁表

每個程序都有屬於自己的虛擬記憶體空間,程序中的所有執行緒共享程序的虛擬記憶體空間,所以程序之間互不影響,執行緒之間可能會相互影響

現在我們可以來回答這個問題了
執行緒切換比程序塊的主要原因就是程序切換涉及虛擬記憶體地址空間的切換而執行緒不會。因為每個程序都有自己的虛擬記憶體地址空間,而執行緒之間的虛擬地址空間是共享的,因此同一個程序之中的執行緒切換不涉及虛擬地址空間的轉換,然而將虛擬地址轉化為實體地址需要查詢頁表,查詢頁表是一個很慢的過程,所以執行緒切換自然就比程序快了。

死鎖

死鎖是指在併發系統中,兩個或多個程序因為互相等待對方釋放資源而無法繼續執行的狀態。

死鎖發生的條件通常包括以下四個條件:

  • 互斥條件(Mutual Exclusion):至少有一個資源被標記為只能被一個程序佔用,即一次只能有一個程序使用該資源。

  • 請求與保持條件(Hold and Wait):一個程序在持有至少一個資源的同時,又請求其他程序佔用的資源。

  • 不可剝奪條件(No Preemption):已經分配給一個程序的資源不能被強制性地剝奪,只能由持有該資源的程序主動釋放

  • 迴圈等待條件(Circular Wait):存在一個程序資源的迴圈鏈,每個程序都在等待下一個程序所佔用的資源。

當這四個條件同時滿足時,就可能發生死鎖。為了避免死鎖的發生,可以採取一些策略,如資源預分配、避免迴圈等待、引入資源剝奪等。

記憶體

在程式中得到邏輯地址,先透過地址轉換機構得到實體地址,

也就是透過查詢頁表找到對應的實體地址,請注意,這裡有一個TLB用於加快查詢頁表的過程

頁表中儲存的是邏輯頁和物理頁的對映。頁地址加上頁內地址才是完整的地址

TLB中儲存的是最近訪問的頁表項,如果TLB命中就停止去記憶體中查詢頁表項

如果TLB和頁表都沒有這個頁的實體地址說明這一頁沒有還沒載入到記憶體中,發生缺頁中斷。

得到實體地址後,先去查詢cache判斷這一塊是否在cache中,如果在直接命中,cache中不僅包含資料塊還包含了資料標記,也就是一塊的地址值

如果不在,再去記憶體中載入這一塊到cache。

虛擬記憶體

TLB

一、程序、執行緒、協程的概念
程序:是併發執行的程式在執行過程中分配和管理資源的基本單位,是一個動態概念,競爭計算機系統資源的基本單位。
執行緒:是程序的一個執行單元,是程序內科排程實體。比程序更小的獨立執行的基本單位。執行緒也被稱為輕量級程序。
協程:是一種比執行緒更加輕量級的存在。一個執行緒也可以擁有多個協程。其執行過程更類似於子例程,或者說不帶返回值的函式呼叫。

二、程序和執行緒的區別
地址空間:執行緒共享本程序的地址空間,而程序之間是獨立的地址空間。
資源:執行緒共享本程序的資源如記憶體、I/O、cpu等,不利於資源的管理和保護,而程序之間的資源是獨立的,能很好的進行資源管理和保護。
健壯性:多程序要比多執行緒健壯,一個程序崩潰後,在保護模式下不會對其他程序產生影響,但是一個執行緒崩潰整個程序都死掉。
執行過程:每個獨立的程序有一個程式執行的入口、順序執行序列和程式入口,執行開銷大。但是執行緒不能獨立執行,必須依存在應用程式中,由應用程式提供多個執行緒執行控制,執行開銷小。
可併發性:
兩者均可併發執行。
切換時:
程序切換時,消耗的資源大,效率高。所以涉及到頻繁的切換時,使用執行緒要好於程序。同樣如果要求同時進行並且又要共享某些變數的併發操作,只能用執行緒不能用程序。
其他:執行緒是處理器排程的基本單位,但是程序不是。

三、協程和執行緒的區別
協程避免了無意義的排程,由此可以提高效能,但程式設計師必須自己承擔排程的責任。同時,協程也失去了標準執行緒使用多CPU的能力。
執行緒相對獨立有自己的上下文切換受系統控制;協程相對獨立有自己的上下文切換由自己控制,由當前協程切換到其他協程由當前協程來控制。

四、何時使用多程序,何時使用多執行緒?
對資源的管理和保護要求高,不限制開銷和效率時,使用多程序。要求效率高,頻繁切換時,資源的保護管理要求不是很高時,使用多執行緒。

五、為什麼會有執行緒?
每個程序都有自己的地址空間,即程序空間,在網路或多使用者換機下,一個伺服器通常需要接收大量不確定數量使用者的併發請求,為每一個請求都建立一個程序顯然行不通(系統開銷大響應使用者請求效率低),因此作業系統中執行緒概念被引進。

六、*python多執行緒的問題(面試問題)
存在問題:
python由於歷史遺留的問題,嚴格說多個執行緒並不會同時執行(沒法有效利用多核處理器,python的併發只是在交替執行不同的程式碼)。多執行緒在Python中只能交替執行,即使100個執行緒跑在100核CPU上,也只能用到1個核。所以python的多執行緒併發並不能充分利用多核,併發沒有java的併發嚴格。

原因:
原因就在於GIL ,在Cpython 直譯器(Python語言的主流直譯器)中,有一把全域性解釋鎖(GIL, Global Interpreter Lock),在直譯器解釋執行Python 程式碼時,任何Python執行緒執行前,都先要得到這把GIL鎖。這個GIL全域性鎖實際上把所有執行緒的執行程式碼都給上了鎖。這意味著,python在任何時候,只可能有一個執行緒在執行程式碼。其它執行緒要想獲得CPU執行程式碼指令,就必須先獲得這把鎖,如果鎖被其它執行緒佔用了,那麼該執行緒就只能等待,直到佔有該鎖的執行緒釋放鎖才有執行程式碼指令的可能。多個執行緒一起執行反而更加慢的原因:同一時刻,只有一個執行緒在執行,其它執行緒只能等待,即使是多核CPU,也沒辦法讓多個執行緒「並行」地同時執行程式碼,只能是交替執行,因為多執行緒涉及到上線文切換、鎖機制處理(獲取鎖,釋放鎖等),所以,多執行緒執行不快反慢。什麼時候GIL 被釋放?當一個執行緒遇到I/O 任務時,將釋放GIL。計算密集型(CPU-bound)執行緒執行100次直譯器的計步(ticks)時(計步可粗略看作Python 虛擬機器的指令),也會釋放GIL。即,每執行100條位元組碼,直譯器就自動釋放GIL鎖,讓別的執行緒有機會執行。Python雖然不能利用多執行緒實現多核任務,但可以透過多程序實現多核任務。多個Python程序有各自獨立的GIL鎖,互不影響。

本條參考部落格:http://www.sohu.com/a/230407177_99992472

七、程序通訊方式(選讀)
管道:速度慢,容量有限,只有父子程序能通訊
FIFO:任何程序間都能通訊,但速度慢
訊息佇列:容量受到系統限制,且要注意第一次讀的時候,要考慮上一次沒有讀完資料的問題
訊號量:不能傳遞複雜訊息,只能用來同步
共享記憶體區:能夠很容易控制容量,速度快,但要保持同步,比如一個程序在寫的時候,另一個程序要注意讀寫的問題,相當於執行緒中的執行緒安全,當然,共享記憶體區同樣可以用作執行緒間通訊,不過沒這個必要,執行緒間本來就已經共享了同一程序內的一塊記憶體
本條參考部落格:https://blog.csdn.net/weixin_40283480/article/details/82155704

八、舉例說明程序、執行緒、協程
程式:例如main.py這是程式,是一個靜態的程式。
python程序:一個程式執行起來後,程式碼+用到的資源 稱之為程序,它是作業系統分配資源的基本單元。multiprocessing.Process實現多程序
程序池:如果要啟動大量的子程序,可以用程序池的方式批次建立子程序。multiprocessing.Pool
程序間通訊:各自在獨立的地址空間,並不能直接進行全域性的資料共享,在建立子程序的時候會將父程序的資料複製到子程序中一份。程序間通訊 Python的multiprocessing模組包裝了底層的機制,提供了Queue、Pipes等多種方式來交換資料。
python執行緒:thread是比較低階,底層的模組,threading是高階模組,對thread進行了封裝,可以更加方便的被使用。
python協程:執行緒和程序的操作是由程式觸發系統介面,最後的執行者是系統;協程的操作則是程式設計師,當程式中存在大量不需要CPU的操作時(例如 I/O),適用於協程。
例如yield其中 yield 是python當中的語法。當協程執行到yield關鍵字時,會暫停在那一行,等到主執行緒呼叫send方法傳送了資料,協程才會接到資料繼續執行。但是,yield讓協程暫停,和執行緒的阻塞是有本質區別的。
協程的暫停完全由程式控制,執行緒的阻塞狀態是由作業系統核心來進行切換。因此,協程的開銷遠遠小於執行緒的開銷。最重要的是,協程不是被作業系統核心所管理,而完全是由程式所控制(也就是在使用者態執行)。這樣帶來的好處就是效能得到了很大的提升,不會像執行緒切換那樣消耗資源。python可以透過 yield/send 的方式實現協程。在python 3.5以後,async/await 成為了更好的替代方案。

優先順序佇列

資料結構是堆

一. PriorityQueue

PriorityQueue 簡介

繼承關係

PriorityQueue 示例

二. Comparable 比較器

Compare 介面

三. Comparator 比較器

Comparator 介面

四. 底層原理

一. PriorityQueue
PriorityQueue 簡介
PriorityQueue ,即優先順序佇列。優先順序佇列可以保證每次取出來的元素都是佇列中的最小或最大的元素<Java優先順序佇列預設每次取出來的為最小元素>。

大小關係:元素的比較可以透過元素本身進行自然排序,也可以透過構造方法傳入比較器進行比較。

繼承關係

透過繼承關係圖可以知道 PriorityQueue 是 Queue 介面的一個實現類,而 Queue 介面是 Collection 介面的一個實現類,因此其擁有 Collection 介面的基本操作,此外,佇列還提供了其他的插入,移除和查詢的操作。每個方法存在兩種形式:一種是丟擲異常(操作失敗時),另一種是返回一個特殊值(null 或 false)。

PriorityQueue 的 peek 和 element 操作的時間複雜度都為常數,add,offer,remove 以及 poll 的時間複雜度是 log(n)。

PriorityQueue 示例
impot java.util.PriorityQueue;

public class PriorityQueueTest{
public static void main(String[] args){
PriorityQueue queue = new PriorityQueue<>();
queue.add(11);
queue.add(22);
queue.add(33);
queue.add(55);
queue.add(44);
System.out.println(queue.remove());
System.out.println(queue.remove());
System.out.println(queue.remove());
System.out.println(queue.remove());
System.out.println(queue.remove());
}
}
執行結果:

程式碼中我們依次新增11,22,33,55,44五個資料,然後進行刪除,透過結果我們發現,每次刪除的都為佇列中最小元素,即體現了優先順序佇列。

結論:優先順序佇列預設每次獲取佇列最小的元素,也可以透過 comparator 比較器來自定義每次獲取為最小還是最大。

注意:優先順序佇列中不可以儲存 null。

二. Comparable 比較器
Compare 介面
public interface Comparable{
public int compareTo(T o);
}
該介面只存在一個 public int compareTo(T o); 方法,該方法表示所在的物件和 o 物件進行比較,返回值分三種:

1:表示該物件大於 o 物件

0:表示該物件等於 o 物件

-1:表示該物件小於 o 物件

需求:在優先順序佇列中儲存物件學生,每個學生有 id,name 兩個屬性,並且使優先順序佇列每次按照學生的 id 從小到大取出。

程式碼示例:

Student 類:當前類實現了 Comparable 介面,即當前類提供了預設的比較方法。

public class Student implements Comparable{
    private int id;
    private String name;
    
    public Student(int id,String name,int age){
        this.id = id;
        this.name = name;
    }
    
    public int getId(){
        return id;
    }
    
    @Override
    public String toString(){
        return "Student{" +
                "id=" + id +
                ", name='" + name + '\'' +
                '}';
    }
    
    @Override
    public int compareTo(Object o){
        Student o1 = (Student)o;
        return this.id - o1.id;
    }
}

PriorityQueueTest 類:

public class PriorityQueueTest {
public static void main(String[] args) {
PriorityQueue queue = new PriorityQueue<>();
queue.add(new Student(2,"Jack"));
queue.add(new Student(1,"Mary"));
queue.add(new Student(5,"Mcan"));
queue.add(new Student(4,"Scolt"));
queue.add(new Student(3,"Tina"));
System.out.println(queue.remove());
System.out.println(queue.remove());
System.out.println(queue.remove());
System.out.println(queue.remove());
System.out.println(queue.remove());
}
}
執行結果:

三. Comparator 比較器
新需求:如果使優先順序佇列按照學生 id 從大到小取出呢?我們很快就會想到修改 Student 類的compareTo 方法,使 return o1.id - this.id;這樣當然可以實現我們的新需求。但是有很多時候類的compareTo 方法是不能修改的,比如 JDK 給我們提供的原始碼,在不修改 compareTo 方法的前提下實現需求,只能用 comparator 比較器了。

Comparator 介面
public interface Comparator{
int compare(T o1,T o2);
}
該介面只存在一個 int compare(T o1,T o2);方法,該方法需要引數是兩個待比較的物件,返回結果是 int 型別:

1:表示 o1物件 大於 o2 物件

0:表示 o1物件 等於 o2 物件

-1:表示 o1物件 小於 o2 物件

public class PriorityQueueTest {
public static void main(String[] args) {
PriorityQueue queue = new PriorityQueue<>(new Comparator() {

        @Override
        public int compare(Student o1, Student o2) {
            return o2.getId() - o1.getId();
    }
})
queue.add(new Student(2, "Jack"));
queue.add(new Student(1, "Mary"));
queue.add(new Student(5, "Mcan"));
queue.add(new Student(4, "Scolt"));
queue.add(new Student(3, "Tina"));
System.out.println(queue.remove());
System.out.println(queue.remove());
System.out.println(queue.remove());
System.out.println(queue.remove());
System.out.println(queue.remove());

}
}

執行結果:

四. 底層原理
優先順序佇列是如何保證每次取出的是佇列中最小(最大)的元素的呢?檢視原始碼,底層的儲存結構為一個陣列

transient Object[] queue;

表面上是一個陣列結構,實際上優先佇列採用的是堆的形式來進行儲存的,透過調整小堆或大堆來保證每次取出的元素為佇列中的最小或最大。

相關文章