分類: 3C資訊

  • PL真有意思(三):名字、作用域和約束

    前言

    這兩篇寫了詞法分析和語法分析,比較偏向實踐。這一篇來看一下語言設計里一個比較重要的部分:名字。在大部分語言里,名字就是標識符,如果從抽象層面來看名字就是對更低一級的內存之類的概念的一層抽象。但是名字還有其它相關的比如它的約束時間和生存周期等等

    約束時間

    約束就是兩個東西之間的一種關聯,例如一個名字和它所命名的事物,約束時間就是指創建約束的時間。有關的約束可以在許多不同的時間作出

    • 語言設計時
    • 語言實現時
    • 編寫程序時
    • 編譯時
    • 鏈接時
    • 裝入時
    • 運行時

    這就是為什麼基於編譯的語言實現通常會比基於解釋器的語言的實現更高效的原因,因為基於編譯的語言在更早的時候就做了約束,比如對於全局變量在編譯時就已經確定了它在內存中的布局了

    對象生存期和存儲管理

    在名字和它們所引用的對象的約束之間有幾個關鍵事件

    • 對象的創建
    • 約束的創建
    • 對變量、子程序、類型等的引用,所有這些都使用了約束
    • 對可能暫時無法使用的約束進行失活或者重新約束
    • 約束的撤銷
    • 對象的撤銷

    對象的生存期和存儲分配機制有關

    • 靜態對象被賦予一個絕對地址,這個地址在程序的整個執行過程中都保持不變
    • 棧對象按照後進先出的方式分配和釋放,通常與子程序的調用和退出同時進行
    • 堆對象可以在任意時刻分配或者釋放,它們要求更通用的存儲管理算法

    靜態分配

    全局變量是靜態對象最顯而易見的例子,還有構成程序的機器語言翻譯結果的那些指令,也可以看作是靜態分配對象。

    還有像每次調用函數都會保持相同的值的局部變量也是靜態分配的。對於數值和字符串這些常量也是靜態分配。

    還有用來支持運行時的各種程序,比如廢料收集和異常處理等等也可以看作是靜態分配

    基於棧的分配

    如果一種語言允許遞歸,那麼局部變量就不能使用靜態分配的方式了,因為在同一時刻,一個局部變量存在的實例個數是不確定的

    所以一般對於子程序,都用棧來保存它相關的變量信息。在運行時,一個子程序的每個實例都在棧中有一個相應的棧幀,保存着它的參數、返回值、局部變量和一些簿記信息

    基於堆的分配

    堆是一塊存儲區域,其中的子存儲塊可以在任意時間分配與釋放。因為堆具有它的動態性,所以就需要對堆空間進行嚴格的管理。許多存儲管理算法都維護着堆中當前尚未使用的存儲塊的一個鏈接表,稱為自由表。

    初始時這個表只有一個塊,就是整個堆,每當遇到分配請求時,算法就在表中查找一個大小適當的塊。所以當請求次數增多,就會出現碎片問題,也需要相應的解決

    所以有廢料收集的語言其實就是對堆的管理

    作用域作用

    一個約束起作用的那一段程序正文區域,稱為這個約束的作用域。

    現在大多數語言使用的都是靜態作用域,也就是在編譯時就確定了。也有少數語言使用動態作用域,它們的約束需要等到運行時的執行流才能確定

    靜態作用域

    在使用靜態作用域的語言,也叫作詞法作用域。一般當前的約束就是程序中包圍着一個給定點的最近的,其中有與該名字匹配的聲明的那個快中建立的那個約束。比如C語言在進入子程序時,如果局部變量和全局變量,那麼當前的約束就是與局部變量關聯,直到退齣子程序才撤銷這個約束

    但是有的語言提供了一種可以提供約束的生存期的機制,比如Fortran的save和C的static

    嵌套子程序

    有許多語言允許一個子程序嵌套在另一個子程序的。這樣有關約束的定義通常來說都是首先用這個名字在當前、最內層的作用域中查找相應的聲明,如果找不到就直接到更外圍的作用域查找當前的約束,直到到達全局作用域,否則就發生一個錯誤

    訪問非局部變量

    上面提到的訪問外圍作用域的變量,但是當前子程序只能訪問到當前的棧幀,所以就需要一個調用幀鏈來讓當前的作用域訪問到外圍作用,通過調用順序形成一個靜態鏈

    聲明的順序

    關於約束還有一個問題,就是在同一作用域里,先聲明的名字是否能使用在此之後的聲明

    在Pascal里有這樣兩條規則:

    1. 修改變量要求名字在使用之前就進行聲明
    2. 但是當前聲明的作用域是整個程序塊

    所以在這兩個的相互作用下,會造成一個讓人吃驚的問題

    const N = 10;
    
    procedure foo;
    const
      M = N; (*靜態語義錯誤*)
      N = 20;

    但是在C、C++和Java等語言就不會出現這個問題,它們都規定標識符的作用域不是整個塊,而是從其聲明到塊結束的那一部分

    並且C++和Java還進一步放寬了規則,免除了使用之前必須聲明的要求

    模塊

    恰當模塊化的代碼可以減少程序員的思維負擔,因為它最大限度的減少了理解系統的任意給定部分時所需的信息量。在設計良好的程序中,模塊之間的接口應盡可能的小,所有可能改變的設計決策都隱藏在某個模塊里。

    模塊作為抽象

    模塊可以將一組對象(如子程序、變量、類型)封裝起來。使得:

    1. 這些內部的對象相互可見
    2. 但是外部對象和內部對象,除非显示的導入,否則都是不可見的

    模塊作為管理器

    模塊使我們很容易的創建各種抽象,但是如果需要多個棧的實例,那麼就需要一個讓模塊成為一個類型的管理器。這種管理器組織方式一般都是要求在模塊中增加創建/初始化函數,並給每一個函數增加一個用於描述被操作的實例

    模塊類型

    對於像這種多實例的問題,除了管理器,在許多語言里的解決方法都是可以將模塊看作是類型。當模塊是類型的時候,就可以將當前的方法認為是屬於這個類型的,簡單來說就是調用方法變化了

    push(A, x) -> A.push(x)

    本質上的實現區別不大

    面向對象

    在更面向對象里的方法里,可以把類看作是一種擴充了一種繼承機制的模塊類型。繼承機制鼓勵其中所有操作都被看作是從屬於對象的,並且新的對象可以從現有對象繼承大部分的操作,而不需要為這些操作重寫代碼。

    類的概念最早應該是起源於Simula-67,像後來的C++,Java和C#中的類的思想也都起源於它。類也是像Python和Ruby這些腳本語言的核心概念

    從模塊到模塊類型再到類都是有其思想基礎,但是最初都是為了更好的數據抽象。但是即使有了類也不能完全取代模塊,所以許多語言都提供了面向對象和模塊的機制

    動態作用域

    在使用動態作用域的語言中,名字與對象間的約束依賴於運行時的控制流,特別是依賴子程序的調用順序

    n : integer
    
    procedure first
      n := 1
    
    procedure second
      n : integer
      first()
    
    n := 2
    if read_integer() > 0
      second()
    else
      first()
    write_integer()

    這裏最後的輸出結果完全取決於read_integer讀入的数字的正負,如果為正,輸出就為2,否則就打印一個1

    作用域的實現

    為了跟蹤靜態作用域程序中的哥哥名字,編譯器需要依靠一個叫做符號表的數據結構。從本質上看,符號表就是一個記錄名字和它已知信息的映射關係的字典,但是由於作用域規則,所以還需要更強大的數據結構。像之前那個寫編譯器系列的符號表就是使用哈希表加上同一層作用域鏈表來實現的

    而對於動態作用域來說就需要在運行時執行一些操作

    作用域中名字的含義

    別名

    在基於指針的數據結構使用別名是很自然的情況,但是使用別名可能會導致編譯器難以優化或者造成像懸空引用的問題,所以需要謹慎使用

    重載

    在大多數語言中都或多或少的提供了重載機制,比如C語言中(+)可以被用在整數類型也可以用在浮點數類型,還有Java中的String類型也支持(+)運算髮

    要在編譯器的符號表中處理重載問題,就需要安排查找程序根據當前的上下文環境返回一個有意義的符號

    比如C++、Java和C#中的類方法重載都可以根據當前的參數類型和數量來判斷使用哪個符號

    內部運算符的重載

    C++、C#和Haskell都支持用戶定義的類型重載內部的算術運算符,在C++和C#的內部實現中通常是將A+B看作是operator+(A, B)的語法糖

    多態性

    對於名字,除了重載還有兩個重要的概念:強制和多態。這三個概念都用於在某些環境中將不同類型的參數傳給一個特定名字的子程序

    強制是編譯器為了滿足外圍環境要求,自動將某類型轉換為另一類型的值的操作

    所以在C中,定義一個計算整數或者浮點數兩個值中的最小值的函數

    double min(double x, double y);

    只要浮點數至少有整數那麼多有效二進制位,那麼結果就一定會是正確的。因為編譯器會對int類型強制轉換為double類型

    這是強制提供的方法,但是多態性提供的是,它使同一個子程序可以不加轉換的接受多種類型的參數。要使這個概念有意義,那麼這多種類型肯定要具有共同的特性

    顯式的參數多態性就叫做泛型,像Ada、C++、Clu、Java和C#都支持泛型機制,像剛才的例子就可以在Ada中用泛型來實現

    generic
      type T is private;
      with function "<" (x, y : T) return Boolean;
    function min(x, y : T) return T;
    
    function min(x, y : T) return T is
    begin
      if x < y then return x;
      else return y;
      end if;
    end min
    
    function string_min is new min(string, "<")
    function date_min is new min(date, date_precedes);

    像List和ML中就可以直接寫

    (define min (lambda (a b) (if (< a b) a b)))

    其中有關類型的任何細節都由解釋器處理

    引用環境的約束

    提到引用環境的約束就有兩種方式:淺約束和深約束

    推遲到調用時建立約束的方式淺約束。一般動態作用域的語言默認是淺約束,當然動態作用域和深約束也是可以組合到一起的。
    執行時依然使用傳遞時的引用環境,而非執行時的引用環境。那麼這種規則稱為深約束,一般靜態作用域的語言默認是深約束

    閉包

    為了實現神約束,需要創建引用環境的一種顯示錶示形式,並將它與對有關子程序的引用捆綁在一起,這樣的捆綁叫做閉包

    總而言之,如果子程序可以被當作參數傳遞,那麼它的引用環境一樣也會被傳遞過去

    一級值和非受限生存期

    一般而言,在語言中,如果一個值可以賦值給變量、可以當作參數傳遞、可以從子程序返回,那麼它被稱為具有一級狀態(和我們在js中說函數是一等公民一個含義)。大多數的語言中數據對象都是一級狀態。二級狀態是只能當作參數傳遞;三級值則是連參數也不能做,比如C#中一些+-*/等符號。

    在一級子程序會出現一個複雜性,就是它的生存期可能持續到這個子程序的作用域的執行期外。為了避免這一問題,大部分函數式語言都表示局部變量具有非受限的生命周期,它們的生命周期無限延長,直到GC能證明這些對象再也不使用了才會撤銷。那麼不撤銷帶來的問題就是這些子程序的存儲分配基於棧幀是不行了,只能是基於堆來分配管理。為了維持能基於棧的分配,有些語言會限制一級子程序的能力,比如C++,C#,都是不允許子程序嵌套,也就從根本上不會存在閉包帶來的懸空引用問題。

    小結

    這一篇從名字入手,介紹了名字與其背後的對象的約束關係、以及約束時間的概念;然後介紹了對象的分配策咯(靜態、棧、堆);緊接着討論了名字與對象之間建立的約束的生命周期,並由此引出了作用域的概念;進一步延伸出多個約束組成的引用環境的相關概念以及問題。

    本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

    USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

    ※評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

    ※智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

  • Kafka冪等性原理及實現剖析

    Kafka冪等性原理及實現剖析

    1.概述

    最近和一些同學交流的時候反饋說,在面試Kafka時,被問到Kafka組件組成部分、API使用、Consumer和Producer原理及作用等問題都能詳細作答。但是,問到一個平時不注意的問題,就是Kafka的冪等性,被卡主了。那麼,今天筆者就為大家來剖析一下Kafka的冪等性原理及實現。

    2.內容

    2.1 Kafka為啥需要冪等性?

    Producer在生產發送消息時,難免會重複發送消息。Producer進行retry時會產生重試機制,發生消息重複發送。而引入冪等性后,重複發送只會生成一條有效的消息。Kafka作為分佈式消息系統,它的使用場景常見與分佈式系統中,比如消息推送系統、業務平台系統(如物流平台、銀行結算平台等)。以銀行結算平台來說,業務方作為上游把數據上報到銀行結算平台,如果一份數據被計算、處理多次,那麼產生的影響會很嚴重。

    2.2 影響Kafka冪等性的因素有哪些?

    在使用Kafka時,需要確保Exactly-Once語義。分佈式系統中,一些不可控因素有很多,比如網絡、OOM、FullGC等。在Kafka Broker確認Ack時,出現網絡異常、FullGC、OOM等問題時導致Ack超時,Producer會進行重複發送。可能出現的情況如下:

     

     

    2.3 Kafka的冪等性是如何實現的?

    Kafka為了實現冪等性,它在底層設計架構中引入了ProducerID和SequenceNumber。那這兩個概念的用途是什麼呢?

    • ProducerID:在每個新的Producer初始化時,會被分配一個唯一的ProducerID,這個ProducerID對客戶端使用者是不可見的。
    • SequenceNumber:對於每個ProducerID,Producer發送數據的每個Topic和Partition都對應一個從0開始單調遞增的SequenceNumber值。

    2.3.1 冪等性引入之前的問題?

    Kafka在引入冪等性之前,Producer向Broker發送消息,然後Broker將消息追加到消息流中后給Producer返回Ack信號值。實現流程如下:

     

    上圖的實現流程是一種理想狀態下的消息發送情況,但是實際情況中,會出現各種不確定的因素,比如在Producer在發送給Broker的時候出現網絡異常。比如以下這種異常情況的出現:

     

    上圖這種情況,當Producer第一次發送消息給Broker時,Broker將消息(x2,y2)追加到了消息流中,但是在返回Ack信號給Producer時失敗了(比如網絡異常) 。此時,Producer端觸發重試機制,將消息(x2,y2)重新發送給Broker,Broker接收到消息后,再次將該消息追加到消息流中,然後成功返回Ack信號給Producer。這樣下來,消息流中就被重複追加了兩條相同的(x2,y2)的消息。

    2.3.2 冪等性引入之後解決了什麼問題?

    面對這樣的問題,Kafka引入了冪等性。那麼冪等性是如何解決這類重複發送消息的問題的呢?下面我們可以先來看看流程圖:

     

     同樣,這是一種理想狀態下的發送流程。實際情況下,會有很多不確定的因素,比如Broker在發送Ack信號給Producer時出現網絡異常,導致發送失敗。異常情況如下圖所示:

     

     當Producer發送消息(x2,y2)給Broker時,Broker接收到消息並將其追加到消息流中。此時,Broker返回Ack信號給Producer時,發生異常導致Producer接收Ack信號失敗。對於Producer來說,會觸發重試機制,將消息(x2,y2)再次發送,但是,由於引入了冪等性,在每條消息中附帶了PID(ProducerID)和SequenceNumber。相同的PID和SequenceNumber發送給Broker,而之前Broker緩存過之前發送的相同的消息,那麼在消息流中的消息就只有一條(x2,y2),不會出現重複發送的情況。

    2.3.3 ProducerID是如何生成的?

    客戶端在生成Producer時,會實例化如下代碼:

    // 實例化一個Producer對象
    Producer<String, String> producer = new KafkaProducer<>(props);

    在org.apache.kafka.clients.producer.internals.Sender類中,在run()中有一個maybeWaitForPid()方法,用來生成一個ProducerID,實現代碼如下:

     private void maybeWaitForPid() {
            if (transactionState == null)
                return;
    
            while (!transactionState.hasPid()) {
                try {
                    Node node = awaitLeastLoadedNodeReady(requestTimeout);
                    if (node != null) {
                        ClientResponse response = sendAndAwaitInitPidRequest(node);
                        if (response.hasResponse() && (response.responseBody() instanceof InitPidResponse)) {
                            InitPidResponse initPidResponse = (InitPidResponse) response.responseBody();
                            transactionState.setPidAndEpoch(initPidResponse.producerId(), initPidResponse.epoch());
                        } else {
                            log.error("Received an unexpected response type for an InitPidRequest from {}. " +
                                    "We will back off and try again.", node);
                        }
                    } else {
                        log.debug("Could not find an available broker to send InitPidRequest to. " +
                                "We will back off and try again.");
                    }
                } catch (Exception e) {
                    log.warn("Received an exception while trying to get a pid. Will back off and retry.", e);
                }
                log.trace("Retry InitPidRequest in {}ms.", retryBackoffMs);
                time.sleep(retryBackoffMs);
                metadata.requestUpdate();
            }
        }

    3.事務

    與冪等性有關的另外一個特性就是事務。Kafka中的事務與數據庫的事務類似,Kafka中的事務屬性是指一系列的Producer生產消息和消費消息提交Offsets的操作在一個事務中,即原子性操作。對應的結果是同時成功或者同時失敗。

    這裏需要與數據庫中事務進行區別,操作數據庫中的事務指一系列的增刪查改,對Kafka來說,操作事務是指一系列的生產和消費等原子性操作。

    3.1 Kafka引入事務的用途?

    在事務屬性引入之前,先引入Producer的冪等性,它的作用為:

    • Producer多次發送消息可以封裝成一個原子性操作,即同時成功,或者同時失敗;
    • 消費者&生產者模式下,因為Consumer在Commit Offsets出現問題時,導致重複消費消息時,Producer重複生產消息。需要將這個模式下Consumer的Commit Offsets操作和Producer一系列生產消息的操作封裝成一個原子性操作。

    產生的場景有:

    比如,在Consumer中Commit Offsets時,當Consumer在消費完成時Commit的Offsets為100(假設最近一次Commit的Offsets為50),那麼執行觸發Balance時,其他Consumer就會重複消費消息(消費的Offsets介於50~100之間的消息)。

    3.2 事務提供了哪些可使用的API?

    Producer提供了五種事務方法,它們分別是:initTransactions()、beginTransaction()、sendOffsetsToTransaction()、commitTransaction()、abortTransaction(),代碼定義在org.apache.kafka.clients.producer.Producer<K,V>接口中,具體定義接口如下:

    // 初始化事務,需要注意確保transation.id屬性被分配
    void initTransactions();
    
    // 開啟事務
    void beginTransaction() throws ProducerFencedException;
    
    // 為Consumer提供的在事務內Commit Offsets的操作
    void sendOffsetsToTransaction(Map<TopicPartition, OffsetAndMetadata> offsets,
                                  String consumerGroupId) throws ProducerFencedException;
    
    // 提交事務
    void commitTransaction() throws ProducerFencedException;
    
    // 放棄事務,類似於回滾事務的操作
    void abortTransaction() throws ProducerFencedException;

    3.3 事務的實際應用場景有哪些?

    在Kafka事務中,一個原子性操作,根據操作類型可以分為3種情況。情況如下:

    • 只有Producer生產消息,這種場景需要事務的介入;
    • 消費消息和生產消息並存,比如Consumer&Producer模式,這種場景是一般Kafka項目中比較常見的模式,需要事務介入;
    • 只有Consumer消費消息,這種操作在實際項目中意義不大,和手動Commit Offsets的結果一樣,而且這種場景不是事務的引入目的。

    4.總結

    Kafka的冪等性和事務是比較重要的特性,特別是在數據丟失和數據重複的問題上非常重要。Kafka引入冪等性,設計的原理也比較好理解。而事務與數據庫的事務特性類似,有數據庫使用的經驗對理解Kafka的事務也比較容易接受。

    5.結束語

    這篇博客就和大家分享到這裏,如果大家在研究學習的過程當中有什麼問題,可以加群進行討論或發送郵件給我,我會盡我所能為您解答,與君共勉!

    另外,博主出書了《》和《》,喜歡的朋友或同學, 可以在公告欄那裡點擊購買鏈接購買博主的書進行學習,在此感謝大家的支持。關注下面公眾號,根據提示,可免費獲取書籍的教學視頻。 

    本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

    台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

    網頁設計公司推薦更多不同的設計風格,搶佔消費者視覺第一線

    ※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

  • 從零開始搭建前後端分離的NetCore2.2(EF Core CodeFirst+Autofac)+Vue的項目框架之十二Swagger(參數)使用二

    從零開始搭建前後端分離的NetCore2.2(EF Core CodeFirst+Autofac)+Vue的項目框架之十二Swagger(參數)使用二

      引言

      在 中提到了 Swagger 的基本使用,僅限於沒有參數,沒有驗證的那種api文檔生成,那麼這篇就連接上篇繼續,在一般具有安全性、權限等驗證的接口上,

      都會在header/url中加上請求者的秘鑰、簽名等,當然也有可能添加到body等其它地方, Swashbuckle.AspNetCore 都支持這些寫法。

      如何使用 — 下面將介紹兩種使用方式

    兩種方式參數設置到何處都是在  In屬性上,屬性對於值如下:    參考

    • query: 參数字段值對應放在url中
    • header: 參數值對應放在header param中
    • body: 參數對應放到請求體中
    • path: 參數應該對應放到請求路徑  // 具體貌似沒用
    • formData: 參數對應放到請求表單中

      第一種:將一個或多個參數保護API的“securityDefinitions”添加到生成的Swagger中。

    這種是直接在文檔的右上方添加一個 Authorize 按鈕,設置了值后,每一個請求都會在設置的位置上加上相應的值,在 上一篇隨筆中的 ConfigureServices 方法中,

    對應位置 services.AddSwaggerGen(options =>{}) 中的  XmlComments  下 添加代碼如下:

                    options.AddSecurityDefinition("token", new ApiKeyScheme
                    {
                        Description = "token format : {token}",//參數描述
                        Name = "token",//名字
                        In = "header",//對應位置
                        Type = "apiKey"//類型描述
                    });
                    options.AddSecurityDefinition("sid", new ApiKeyScheme
                    {
                        Description = "sid format : {sid}",//參數描述
                        Name = "sid",//名字
                        In = "header",//對應位置
                        Type = "apiKey"//類型描述
                    });
                    //添加Jwt驗證設置 設置為全局的,不然在代碼中取不到
                    options.AddSecurityRequirement(new Dictionary<string, IEnumerable<string>> {
                        { "token", Enumerable.Empty<string>() },
                        { "sid", Enumerable.Empty<string>() },
                    });

      添加完成后,運行起來看下效果,效果圖: 

     設置上對應值,調用測試方法,可以在header中取到剛設置到的值,

     這裡能看到,可以取到設置的參數了。這樣一來,在需要驗證的接口上,我們就可以通過接口文檔來測試了。基本不用再藉助postman等接口測試工具了。

    但是,但是,這裡有一個問題,就是只要設置了參數值,每一次訪問都會在請求中帶上參數。

    下面將介紹第二種方式,只給需要驗證用戶的接口上添加驗證參數。

      第二種:使用“filters”擴展Swagger生成器,來實現只在需要添加參數的方法上添加參數。複雜的可以根據自己的需求來添加對應參數

    實現方式就是先新建一個類,名: SwaggerParameter ,實現 IOperationFilter 接口。SwaggerParameter 類代碼如下: 

        /// <summary>
        /// 自定義添加參數
        /// </summary>
        public class SwaggerParameter : IOperationFilter
        {
            /// <summary>
            /// 實現 Apply 方法
            /// </summary>
            /// <param name="operation"></param>
            /// <param name="context"></param>
            public void Apply(Operation operation, OperationFilterContext context)
            {
                if (operation.Parameters == null) operation.Parameters = new List<IParameter>();
                var attrs = context.ApiDescription.ActionDescriptor.AttributeRouteInfo;
                var t = typeof(BaseUserController);
                //先判斷是否是繼承用戶驗證類
                if (context.ApiDescription.ActionDescriptor is ControllerActionDescriptor descriptor && context.MethodInfo.DeclaringType?.IsSubclassOf(t) == true)
                {
                    //再驗證是否允許匿名訪問
                    var actionAttributes = descriptor.MethodInfo.GetCustomAttributes(inherit: true);
                    bool isAnonymous = actionAttributes.Any(a => a is AllowAnonymousAttribute);
                    // 需要驗證的方法添加
                    if (!isAnonymous)
                    {
                        operation.Parameters.Add(new NonBodyParameter()
                        {
                            Name = "sid",
                            In = "header", //query header body path formData
                            Type = "string",
                            Required = true,//是否必選
                            Description = "登錄返回的sid"
                        });
                        operation.Parameters.Add(new NonBodyParameter()
                        {
                            Name = "token",
                            In = "header", //query header body path formData
                            Type = "string",
                            Required = true,//是否必選
                            Description = "登錄返回的token"
                        });
                    }
                }
            }
        }

     運行起來后,進入到  文檔頁面,可以看到右上角的 Authorize 按鈕已經不見了,在不需要驗證的方法上,也找不到相應需要設置參數的輸入框。就只有在需要驗證的接口上才有。

    參考Swagger文檔圖如下: 

    參考代碼圖如下:

     

    效果圖: 

      這樣一來設置也就完成了。從上面就能看出,就只有需要用戶驗證的接口才會有相應參數。 

     

    我的設置方式是先定義了用戶驗證控制器類,讓需要用戶驗證的控制器繼承該控制器,然後在該控制器中不需要用戶驗證的接口上加上 AllowAnonymous 屬性 

    設置fitter時就可以根據上面提到的兩個點來進行判斷是否需要加上參數,如果不是這樣實現的,可以根據自己的需求變更fitter類,來控制文檔的生成。 

     

    以上若有什麼不對或可以改進的地方,望各位指出或提出意見,一起探討學習~ 

    有需要源碼的可通過此 鏈接拉取 覺得還可以的給個 start 和點個 下方的推薦哦~~謝謝!

    本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

    ※帶您來了解什麼是 USB CONNECTOR  ?

    ※自行創業 缺乏曝光? 下一步"網站設計"幫您第一時間規劃公司的門面形象

    ※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

    ※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

    ※廣告預算用在刀口上,網站設計公司幫您達到更多曝光效益

  • (三十五)golang–面向對象之多態

    (三十五)golang–面向對象之多態

    多態:變量具有多種形態,可以用統一的接口來調用不同的實現。

    接口體現多態特徵:

    (1)多態參數:之前所講的Usb接口案例,既可以接受手機變量,也可以接受相機變量,就體現了usb接口的多態;

    (2)多態數組:

    package main
    
    import (
        "fmt"
    )
    
    type usb interface {
        start()
        stop()
    }
    
    type phone struct {
        name string
    }
    
    func (p phone) start() {
        fmt.Println(p.name, "手機開始工作")
    }
    
    func (p phone) stop() {
        fmt.Println(p.name, "手機停止工作")
    }
    
    type camera struct {
        name string
    }
    
    func (c camera) start() {
        fmt.Println(c.name, "相機開始工作")
    }
    
    func (c camera) stop() {
        fmt.Println(c.name, "相機停止工作")
    }
    
    type computer struct {
    }
    
    func (co computer) working(usb usb) {
        usb.start()
        usb.stop()
    }
    
    func main() {
        var usbArr [3]usb
        usbArr[0] = phone{"小米"}
        usbArr[1] = phone{"vivo"}
        usbArr[2] = camera{"尼康"}
        fmt.Println(usbArr)
        for i := 0; i < len(usbArr); i++ {
            usbArr[i].start()
            usbArr[i].stop()
        }
    }

    我們以前講到,數組是只能存儲同一種類型的數據,利用多態數組,就可以存儲不同的類型了;

    如何將一個接口變量賦值給一個自定義類型的變量?使用類型斷言

    類型斷言:由於接口是一般類型,不知道具體類型,如果要轉成具體類型,就需要使用類型斷言;要保持原來空接口指向的數據類型和斷言的數據類型一致;

    為了避免輸出panic報錯,可以進行斷言判斷;

    類型斷言實踐一:

    我們給phone中加入一個方法call(),在調用usb變量時,usb.call(),肯定是不對的,因為usb可能是phone,也可能是camera,而camera是沒有這個函數的,因此,在調用的時候用類型斷言。

    package main
    
    import (
        "fmt"
    )
    
    type usb interface {
        start()
        stop()
    }
    
    type phone struct {
        name string
    }
    
    func (p phone) start() {
        fmt.Println(p.name, "手機開始工作")
    }
    
    func (p phone) call() {
        fmt.Println(p.name,"手機在打電話")
    }
    
    func (p phone) stop() {
        fmt.Println(p.name, "手機停止工作")
    }
    
    type camera struct {
        name string
    }
    
    func (c camera) start() {
        fmt.Println(c.name, "相機開始工作")
    }
    
    func (c camera) stop() {
        fmt.Println(c.name, "相機停止工作")
    }
    
    type computer struct {
    }
    
    func (co computer) working(usb usb) {
        usb.start()
        //如果usb還指向phone的結構體變量,則還需要調用call方法
        if phone, ok := usb.(phone); ok { phone.call() }
        usb.stop()
    }
    
    func main() {
        var usbArr [3]usb
        usbArr[0] = phone{"小米"}
        usbArr[1] = phone{"vivo"}
        usbArr[2] = camera{"尼康"}
        var com computer
        fmt.Println(usbArr)
        for i := 0; i < len(usbArr); i++ {
            com.working(usbArr[i])
        }
    }

    類型斷言實踐2:循環判斷輸入參數的類型

    package main
    
    import (
        "fmt"
    )
    
    type student struct {
        name string
    }
    
    func typeJudge(items ...interface{}) {
        for index, x := range items {
            switch x.(type) {
            case bool:
                fmt.Printf("第%v個參數是bool類型,值是%v\n", index, x)
            case int, int32, int64:
                fmt.Printf("第%v個參數是整數類型,值是%v\n", index, x)
            case float32:
                fmt.Printf("第%v個參數是float32類型,值是%v\n", index, x)
            case float64:
                fmt.Printf("第%v個參數是float64類型,值是%v\n", index, x)
            case string:
                fmt.Printf("第%v個參數是string類型,值是%v\n", index, x)
            case student:
                fmt.Printf("第%v個參數是student類型,值是%v\n", index, x)
            case *student:
                fmt.Printf("第%v個參數是*student類型,值是%v\n", index, x)
            default:
                fmt.Printf("第%v個參數類型不確定,值是%v\n", index, x)
            }
        }
    }
    
    func main() {
        var n1 float32 = 1.1
        var n2 float64 = 1.2
        var n3 int32 = 1
        var name string = "tom"
        var n5 bool = true
    
        stu1 := student{"jack"}
        stu2 := &student{"bob"}
    
        typeJudge(n1, n2, n3, name, n5, stu1, stu2)
    }

    本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

    ※為什麼 USB CONNECTOR 是電子產業重要的元件?

    網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

    ※想要讓你的商品成為最夯、最多人討論的話題?網頁設計公司讓你強力曝光

    ※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

  • 【algo&ds】8.最小生成樹

    【algo&ds】8.最小生成樹

    1.最小生成樹介紹

    什麼是最小生成樹?

    最小生成樹(Minimum spanning tree,MST)是在一個給定的無向圖G(V,E)中求一棵樹T,使得這棵樹擁有圖G中的所有頂點,且所有邊都是來自圖G中的邊,並且滿足整棵樹的邊權值和最小。

    2.prim算法

    和Dijkstra算法很像!!請看如下Gif圖,prim算法的核心思想是對圖G(V,E)設置集合S,存放已被訪問的頂點,然後每次從集合V-S中選擇與集合S的最短距離最小的一個頂點(記為u),訪問並加入集合S。之後,令頂點u為中間點,優化所有從u能到達的頂點v與集合s之間的最短距離。這樣的操作執行n次,直到集合s中包含所有頂點。

    不同的是,Dijkstra算法中的dist是從源點s到頂點w的最短路徑;而prim算法中的dist是從集合S到頂點w的最短路徑,以下是他們的偽碼描述對比,關於Dijkstra算法的詳細描述請

    算法實現:

    #include<iostream>
    #include<vector>
    #define INF 100000
    #define MaxVertex 105
    typedef int Vertex; 
    int G[MaxVertex][MaxVertex];
    int parent[MaxVertex];   // 並查集 
    int dist[MaxVertex]; // 距離 
    int Nv;    // 結點 
    int Ne;    // 邊 
    int sum;  // 權重和 
    using namespace std; 
    vector<Vertex> MST;  // 最小生成樹 
    
    // 初始化圖信息 
    void build(){
        Vertex v1,v2;
        int w;
        cin>>Nv>>Ne;
        for(int i=1;i<=Nv;i++){
            for(int j=1;j<=Nv;j++)
                G[i][j] = 0;  // 初始化圖 
            dist[i] = INF;   // 初始化距離
            parent[i] = -1;  // 初始化並查集 
        }
        // 初始化點
        for(int i=0;i<Ne;i++){
            cin>>v1>>v2>>w;
            G[v1][v2] = w;
            G[v2][v1] = w;
        }
    }
    
    // Prim算法前的初始化 
    void IniPrim(Vertex s){
        dist[s] = 0;
        MST.push_back(s);
        for(Vertex i =1;i<=Nv;i++)
            if(G[s][i]){
                dist[i] = G[s][i];
                parent[i] = s;
            } 
    }
    
    // 查找未收錄中dist最小的點 
    Vertex FindMin(){
        int min = INF;
        Vertex xb = -1;
        for(Vertex i=1;i<=Nv;i++)
            if(dist[i] && dist[i] < min){ 
                min = dist[i];
                xb = i;
            }
        return xb;
    }
    
    void output(){
        cout<<"被收錄順序:"<<endl; 
        for(Vertex i=1;i<=Nv;i++)
            cout<<MST[i]<<" ";
        cout<<"權重和為:"<<sum<<endl; 
        cout<<"該生成樹為:"<<endl; 
        for(Vertex i=1;i<=Nv;i++)
            cout<<parent[i]<<" ";
    }
    
    void Prim(Vertex s){
        IniPrim(s);
        while(1){
            Vertex v = FindMin();
            if(v == -1)
                break;
            sum += dist[v];
            dist[v] = 0;
            MST.push_back(v);
            for(Vertex w=1;w<=Nv;w++)
                if(G[v][w] && dist[w])
                    if(G[v][w] < dist[w]){
                        dist[w] = G[v][w];
                        parent[w] = v;
                    }
        }
    }
    
    
    int main(){
        build();
        Prim(1);
        output();
        return 0;
    } 

    關於prim算法的更加詳細講解請

    3.kruskal算法

    Kruskal算法也可以用來解決最小生成樹的問題,其算法思想很容易理解,典型的邊貪心,其算法思想為:

    • 在初始狀態時隱去圖中所有的邊,這樣圖中每個頂點都是一個單獨的連通塊,一共有n個連通塊
    • 對所有邊按邊權從小到大進行排序
    • 按邊權從小到大測試所有邊,如果當前測試邊所連接的兩個頂點不在同一個連通塊中,則把這條測試邊加入當前最小生成樹中,否則,將邊捨棄。
    • 重複執行上一步驟,直到最小生成樹中的邊數等於總頂點數減一 或者測試完所有邊時結束;如果結束時,最小生成樹的邊數小於總頂點數減一,說明該圖不連通。

    請看下面的Gif圖!

    算法實現:

    #include<iostream>
    #include<string>
    #include<vector>
    #include<queue>
    #define INF 100000
    #define MaxVertex 105
    typedef int Vertex; 
    int G[MaxVertex][MaxVertex];
    int parent[MaxVertex];   // 並查集最小生成樹 
    int Nv;    // 結點 
    int Ne;    // 邊 
    int sum;  // 權重和 
    using namespace std; 
    struct Node{
        Vertex v1;
        Vertex v2;
        int weight; // 權重 
        // 重載運算符成最大堆 
        bool operator < (const Node &a) const
        {
            return weight>a.weight;
        }
    };
    vector<Node> MST;  // 最小生成樹 
    priority_queue<Node> q;   // 最小堆 
    
    // 初始化圖信息 
    void build(){
        Vertex v1,v2;
        int w;
        cin>>Nv>>Ne;
        for(int i=1;i<=Nv;i++){
            for(int j=1;j<=Nv;j++)
                G[i][j] = 0;  // 初始化圖
            parent[i] = -1;
        }
        // 初始化點
        for(int i=0;i<Ne;i++){
            cin>>v1>>v2>>w;
            struct Node tmpE;
            tmpE.v1 = v1;
            tmpE.v2 = v2;
            tmpE.weight = w;
            q.push(tmpE); 
        }
    }
    
    //  路徑壓縮查找 
    int Find(int x){
        if(parent[x] < 0)
            return x;
        else
            return parent[x] = Find(parent[x]);
    } 
    
    //  按秩歸併 
    void Union(int x1,int x2){
        if(parent[x1] < parent[x2]){
            parent[x1] += parent[x2];
            parent[x2] = x1;
        }else{
            parent[x2] += parent[x1];
            parent[x1] = x2;
        }
    } 
    
    void Kruskal(){
        // 最小生成樹的邊不到 Nv-1 條且還有邊 
        while(MST.size()!= Nv-1 && !q.empty()){
            Node E = q.top();  // 從最小堆取出一條權重最小的邊
            q.pop(); // 出隊這條邊 
            if(Find(E.v1) != Find(E.v2)){  // 檢測兩條邊是否在同一集合 
                sum += E.weight; 
                Union(E.v1,E.v2);     // 並起來 
                MST.push_back(E);
            }
        }
        
    } 
    
    
    void output(){
        cout<<"被收錄順序:"<<endl; 
        for(Vertex i=0;i<Nv;i++)
            cout<<MST[i].weight<<" ";
        cout<<"權重和為:"<<sum<<endl; 
        for(Vertex i=1;i<=Nv;i++)
            cout<<parent[i]<<" ";
        cout<<endl;
    }
    
    
    int main(){
        build();
        Kruskal();
        output();
        return 0;
    } 

    關於kruskal算法更詳細的講解

    本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

    USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

    ※評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

    ※智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

  • 【原創】(十一)Linux內存管理slub分配器

    【原創】(十一)Linux內存管理slub分配器

    背景

    • Read the fucking source code! –By 魯迅
    • A picture is worth a thousand words. –By 高爾基

    說明:

    1. Kernel版本:4.14
    2. ARM64處理器,Contex-A53,雙核
    3. 使用工具:Source Insight 3.5, Visio

    1. 概述

    之前的文章分析的都是基於頁面的內存分配,而小塊內存的分配和管理是通過塊分配器來實現的。目前內核中,有三種方式來實現小塊內存分配:slab, slub, slob,最先有slab分配器,slub/slob分配器是改進版,slob分配器適用於小內存嵌入式設備,而slub分配器目前已逐漸成為主流塊分配器。接下來的文章,就是以slub分配器為目標,進一步深入。

    先來一個初印象:

    2. 數據結構

    有四個關鍵的數據結構:

    • struct kmem_cache:用於管理SLAB緩存,包括該緩存中對象的信息描述,per-CPU/Node管理slab頁面等;
      關鍵字段如下:
    /*
     * Slab cache management.
     */
    struct kmem_cache {
        struct kmem_cache_cpu __percpu *cpu_slab;       //每個CPU slab頁面
        /* Used for retriving partial slabs etc */
        unsigned long flags;
        unsigned long min_partial;
        int size;       /* The size of an object including meta data */
        int object_size;    /* The size of an object without meta data */
        int offset;     /* Free pointer offset. */
    #ifdef CONFIG_SLUB_CPU_PARTIAL
        /* Number of per cpu partial objects to keep around */
        unsigned int cpu_partial;
    #endif
        struct kmem_cache_order_objects oo;     //該結構體會描述申請頁面的order值,以及object的個數
    
        /* Allocation and freeing of slabs */
        struct kmem_cache_order_objects max;
        struct kmem_cache_order_objects min;
        gfp_t allocflags;   /* gfp flags to use on each alloc */
        int refcount;       /* Refcount for slab cache destroy */
        void (*ctor)(void *);           // 對象構造函數
        int inuse;      /* Offset to metadata */
        int align;      /* Alignment */
        int reserved;       /* Reserved bytes at the end of slabs */
        int red_left_pad;   /* Left redzone padding size */
        const char *name;   /* Name (only for display!) */
        struct list_head list;  /* List of slab caches */       //kmem_cache最終會鏈接在一個全局鏈表中
        struct kmem_cache_node *node[MAX_NUMNODES];     //Node管理slab頁面
    };
    • struct kmem_cache_cpu:用於管理每個CPU的slab頁面,可以使用無鎖訪問,提高緩存對象分配速度;
    struct kmem_cache_cpu {
        void **freelist;    /* Pointer to next available object */                  //指向空閑對象的指針
        unsigned long tid;  /* Globally unique transaction id */                
        struct page *page;  /* The slab from which we are allocating */     //slab緩存頁面
    #ifdef CONFIG_SLUB_CPU_PARTIAL
        struct page *partial;   /* Partially allocated frozen slabs */
    #endif
    #ifdef CONFIG_SLUB_STATS
        unsigned stat[NR_SLUB_STAT_ITEMS];
    #endif
    };
    • struct kmem_cache_node:用於管理每個Node的slab頁面,由於每個Node的訪問速度不一致,slab頁面由Node來管理;
    /*
     * The slab lists for all objects.
     */
    struct kmem_cache_node {
        spinlock_t list_lock;
    
    #ifdef CONFIG_SLUB
        unsigned long nr_partial;    //slab頁表數量
        struct list_head partial;       //slab頁面鏈表
    #ifdef CONFIG_SLUB_DEBUG
        atomic_long_t nr_slabs;
        atomic_long_t total_objects;
        struct list_head full;
    #endif
    #endif
    };
    • struct page:用於描述slab頁面struct page結構體中很多字段都是通過union聯合體進行復用的。
      struct page結構中,用於slub的成員如下:
    struct page {
        union {
           ...
            void *s_mem;            /* slab first object */
           ...
        };
        
        /* Second double word */
        union {
           ...
            void *freelist;     /* sl[aou]b first free object */
           ...
        };
        
        union {
           ...
            struct {
                union {
                  ...
                    struct {            /* SLUB */
                        unsigned inuse:16;
                        unsigned objects:15;
                        unsigned frozen:1;
                    };
                    ...
                };
           ...
            };       
        };   
        
        /*
         * Third double word block
         */
        union {
           ...
            struct {        /* slub per cpu partial pages */
                struct page *next;  /* Next partial slab */
    #ifdef CONFIG_64BIT
                int pages;  /* Nr of partial slabs left */
                int pobjects;   /* Approximate # of objects */
    #else
                short int pages;
                short int pobjects;
    #endif
            };
    
            struct rcu_head rcu_head;   /* Used by SLAB
                             * when destroying via RCU
                             */
        };
        ...
            struct kmem_cache *slab_cache;  /* SL[AU]B: Pointer to slab */    
        ...
    }

    圖來了:

    3. 流程分析

    針對Slub的使用,可以從三個維度來分析:

    1. slub緩存創建
    2. slub對象分配
    3. slub對象釋放

    下邊將進一步分析。

    3.1 kmem_cache_create

    在內核中通過kmem_cache_create接口來創建一個slab緩存

    先看一下這個接口的函數調用關係圖:

    1. kmem_cache_create完成的功能比較簡單,就是創建一個用於管理slab緩存kmem_cache結構,並對該結構體進行初始化,最終添加到全局鏈表中。kmem_cache結構體初始化,包括了上文中分析到的kmem_cache_cpukmem_cache_node兩個字段結構。

    2. 在創建的過程中,當發現已有的slab緩存中,有存在對象大小相近,且具有兼容標誌的slab緩存,那就只需要進行merge操作並返回,而無需進一步創建新的slab緩存

    3. calculate_sizes函數會根據指定的force_order或根據對象大小去計算kmem_cache結構體中的size/min/oo等值,其中kmem_cache_order_objects結構體,是由頁面分配order值和對象數量兩者通過位域拼接起來的。

    4. 在創建slab緩存的時候,有一個先雞后蛋的問題:kmem_cache結構體來管理一個slab緩存,而創建kmem_cache結構體又是從slab緩存中分配出來的對象,那麼這個問題是怎麼解決的呢?可以看一下kmem_cache_init函數,內核中定義了兩個靜態的全局變量kmem_cachekmem_cache_node,在kmem_cache_init函數中完成了這兩個結構體的初始化之後,相當於就是創建了兩個slab緩存,一個用於分配kmem_cache結構體對象的緩存池,一個用於分配kmem_cache_node結構體對象的緩存池。由於kmem_cache_cpu結構體是通過__alloc_percpu來分配的,因此不需要創建一個相關的slab緩存

    3.2 kmem_cache_alloc

    kmem_cache_alloc接口用於從slab緩存池中分配對象。

    看一下大體的調用流程圖:

    從上圖中可以看出,分配slab對象與Buddy System中分配頁面類似,存在快速路徑和慢速路徑兩種,所謂的快速路徑就是per-CPU緩存,可以無鎖訪問,因而效率更高。

    整體的分配流程大體是這樣的:優先從per-CPU緩存中進行分配,如果per-CPU緩存中已經全部分配完畢,則從Node管理的slab頁面中遷移slab頁per-CPU緩存中,再重新分配。當Node管理的slab頁面也不足的情況下,則從Buddy System中分配新的頁面,添加到per-CPU緩存中。

    還是用圖來說明更清晰,分為以下幾步來分配:

    1. fastpath
      快速路徑下,以原子的方式檢索per-CPU緩存的freelist列表中的第一個對象,如果freelist為空並且沒有要檢索的對象,則跳入慢速路徑操作,最後再返回到快速路徑中重試操作。

    2. slowpath-1
      將per-CPU緩存中page指向的slab頁中的空閑對象遷移到freelist中,如果有空閑對象,則freeze該頁面,沒有空閑對象則跳轉到slowpath-2

    3. slowpath-2
      將per-CPU緩存中partial鏈表中的第一個slab頁遷移到page指針中,如果partial鏈表為空,則跳轉到slowpath-3

    4. slowpath-3
      將Node管理的partial鏈表中的slab頁遷移到per-CPU緩存中的page中,並重複第二個slab頁將其添加到per-CPU緩存中的partial鏈表中。如果遷移的slab中空閑對象超過了kmem_cache.cpu_partial的一半,則僅遷移slab頁,並且不再重複。
      如果每個Node的partial鏈表都為空,跳轉到slowpath-4

    5. slowpath-4
      Buddy System中獲取頁面,並將其添加到per-CPU的page中。

    3.2 kmem_cache_free

    kmem_cache_free的操作,可以看成是kmem_cache_alloc的逆過程,因此也分為快速路徑和慢速路徑兩種方式,同時,慢速路徑中又分為了好幾種情況,可以參考kmem_cache_alloc的過程。

    調用流程圖如下:

    效果如下:

    1. 快速路徑釋放
      快速路徑下,直接將對象返回到freelist中即可。

    2. put_cpu_partial
      put_cpu_partial函數主要是將一個剛freeze的slab頁,放入到partial鏈表中。
      put_cpu_partial函數中調用unfreeze_partials函數,這時候會將per-CPU管理的partial鏈表中的slab頁面添加到Node管理的partial鏈表的尾部。如果超出了Node的partial鏈表,溢出的slab頁面中沒有分配對象的slab頁面將會返回到夥伴系統。

    3. add_partial
      添加slab頁到Node的partial鏈表中。

    4. remove_partial
      從Node的partial鏈表移除slab頁。

    具體釋放的流程走哪個分支,跟對象的使用情況,partial鏈表的個數nr_partial/min_partial等相關,細節就不再深入分析了。

    本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

    台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

    網頁設計公司推薦更多不同的設計風格,搶佔消費者視覺第一線

    ※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

  • .NET高級特性-Emit(1)

    .NET高級特性-Emit(1)

      在這個大數據/雲計算/人工智能研發普及的時代,Python的崛起以及Javascript的前後端的侵略,程序員與企業似乎越來越青睞動態語言所帶來的便捷性與高效性,即使靜態語言在性能,錯誤檢查等方面的優於靜態語言。對於.NETer來說,.NET做為一門靜態語言,我們不僅要打好.NET的基本功,如基本類型/語法/底層原理/錯誤檢查等知識,也要深入理解.NET的一些高級特性,來為你的工作減輕負擔和提高代碼質量。

      ok,咱們今天開始聊一聊.NET中的Emit。

    一、什麼是Emit?

      Emit含義為發出、產生的含義,這是.NET中的一組類庫,命名空間為System.Reflection.Emit,幾乎所有的.NET版本(Framework/Mono/NetCore)都支持Emit,可以實現用C#代碼生成代碼的類庫

    二、Emit的本質

      我們知道.NET可以由各種語言進行編寫,比如VB,C++等,當然絕大部分程序員進行.NET開發都是使用C#語言進行的,這些語言都會被各自的語言解釋器解釋為IL語言並執行,而Emit類庫的作用就是用這些語言來編寫生成IL語言,並交給CLR(公共語言運行時)進行執行。

      我們先來看看IL語言長什麼樣子:

      (1) 首先我們創建一個Hello,World程序

        class Program
        {
            static void Main(string[] args)
            {
                Console.WriteLine("Hello World!");
            }
        }

      (2) 將程序編譯成dll文件,我們可以看到在開發目錄下生成了bin文件夾

      

      (3) 向下尋找,我們可以看到dll文件已經生成,筆者使用netcore3進行開發,故路徑為bin/Debug/netcoreapp3.0

      

      (4) 這時候,我們就要祭出我們的il查看神器了,ildasm工具

      

      如何找到這個工具?打開開始菜單,找到Visual Studio文件夾,打開Developer Command Prompt,在打開的命令行中鍵入ildasm回車即可,筆者使用vs2019進行演示,其它vs版本操作方法均一致

      

     

     

     

     

     

     

       (5) 在dasm菜單欄選擇文件->打開,選擇剛剛生成的dll文件

      

     

     

       (6) 即可查看生成il代碼

      

     

      有了ildasm的輔助,我們就能夠更好的了解IL語言以及如何編寫IL語言,此外,Visual Studio中還有許多插件支持查看il代碼,比如JetBrains出品的Resharper插件等,如果覺得筆者方式較為麻煩可以使用以上插件查看il代碼

    三、理解IL代碼

      在上一章節中,我們理解了Emit的本質其實就是用C#來編寫IL代碼,既然要編寫IL代碼,那麼我們首先要理解IL代碼是如何進行工作的,IL代碼是如何完成C#當中的順序/選擇/循環結構的,是如何實現類的定義/字段的定義/屬性的定義/方法的定義的。

      IL代碼是一種近似於指令式的代碼語言,與彙編語言比較相近,所以習慣於寫高級語言的.NETer來說比較難以理解

      讓我們來看看Hello,World程序的IL代碼:

    IL_0000:  nop
    IL_0001:  ldstr      "Hello World!"
    IL_0006:  call       void [System.Console]System.Console::WriteLine(string)
    IL_000b:  nop
    IL_000c:  ret

      我們可以把IL代碼看成棧的運行

      第一條指令,nop表示不做任何事情,表示代碼不做任何事情

      第二條指令,ldstr表示將字符串放入棧中,字符串的值為“Hello,World!”

      第三條指令,call表示調用方法,參數為調用方法的方法信息,並把返回的結構壓入棧中,使用的參數為之前已經入棧的“Hello World!”,以此類推,如果方法有n個參數,那麼他就會調取棧中n個數據,並返回一個結果放回棧中

      第四條指令,nop表示不做任何事情

      第五條指令,ret表示將棧中頂部的數據返回,如果方法定義為void,則無返回值

      關於Hello,world程序IL的理解就說到這裏,更多的指令含義讀者可以參考微軟官方文檔,筆者之後也會繼續對Emit進行講解和Emit的應用

    四、用Emit類庫編寫IL代碼

      既然IL代碼咱們理解的差不多了,咱們就開始嘗試用C#來寫IL代碼了,有了IL代碼的參考,咱們也可以依葫蘆畫瓢的把代碼寫出來了

      (1) 引入Emit命名空間

    using System.Reflection.Emit;

      (2) 首先我們定義一個Main方法,入參無,返回類型void

    //定義方法名,返回類型,輸入類型
    var method = new DynamicMethod("Main", null, Type.EmptyTypes);

      (3) 生成IL代碼

    //生成IL代碼
    var ilGenerator = method.GetILGenerator();
    ilGenerator.Emit(OpCodes.Nop);
    ilGenerator.Emit(OpCodes.Ldstr,"Hello World!");
    ilGenerator.Emit(OpCodes.Call, typeof(Console).GetMethod("WriteLine", new Type[] { typeof(string) })); //尋找Console的WriteLine方法
    ilGenerator.Emit(OpCodes.Nop);
    ilGenerator.Emit(OpCodes.Ret);

      (4) 創建委託並調用

    //創建委託
    var helloWorldMethod = method.CreateDelegate(typeof(Action)) as Action;
    helloWorldMethod.Invoke();

      (5)運行,即輸出Hello World!

    五、小結

      Emit的本質是使用高級語言生成IL代碼,進而進行調用的的一組類庫,依賴Emit我們可以實現用代碼生成代碼的操作,即編程語言的自舉,可以有效彌補靜態語言的靈活性的缺失。

      Emit的性能非常好,除了第一次構建IL代碼所需要時間外,之後只要將操作緩存在計算機內存中,速度與手寫代碼相差無幾

      有許多著名.NET類庫均依賴於Emit:

      (.NET JSON操作庫)Json.NET/Newtonsoft.Json:

      (輕量ORM)Dapper:

      (ObjectToObjectMapper)EmitMapper:

      (AOP庫)Castle.DynamicProxy:

      學習Emit:

      .NET官方文檔:

      .NET API瀏覽器:

      之後作者將繼續講解.NET Emit的相關內容和應用,感謝閱讀

    本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

    ※想知道網站建置網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計後台網頁設計

    ※不管是台北網頁設計公司台中網頁設計公司,全省皆有專員為您服務

    ※Google地圖已可更新顯示潭子電動車充電站設置地點!!

    ※帶您來看台北網站建置台北網頁設計,各種案例分享

  • 實現 Redis 協議解析器

    本文是 《用 Golang 實現一個 Redis》系列文章第二篇,本文將分別介紹 以及 的實現,若您對協議有所了解可以直接閱讀協議解析器部分。

    Redis 通信協議

    Redis 自 2.0 版本起使用了統一的協議 RESP (REdis Serialization Protocol),該協議易於實現,計算機可以高效的進行解析且易於被人類讀懂。

    RESP 是一個二進制安全的文本協議,工作於 TCP 協議上。客戶端和服務器發送的命令或數據一律以 \r\n (CRLF)結尾。

    RESP 定義了5種格式:

    • 簡單字符串(Simple String): 服務器用來返回簡單的結果,比如”OK”。非二進制安全,且不允許換行。
    • 錯誤信息(Error): 服務器用來返回簡單的結果,比如”ERR Invalid Synatx”。非二進制安全,且不允許換行。
    • 整數(Integer): llenscard等命令的返回值, 64位有符號整數
    • 字符串(Bulk String): 二進制安全字符串, get 等命令的返回值
    • 數組(Array, 舊版文檔中稱 Multi Bulk Strings): Bulk String 數組,客戶端發送指令以及lrange等命令響應的格式

    RESP 通過第一個字符來表示格式:

    • 簡單字符串:以”+” 開始, 如:”+OK\r\n”
    • 錯誤:以”-” 開始,如:”-ERR Invalid Synatx\r\n”
    • 整數:以”:”開始,如:”:1\r\n”
    • 字符串:以 $ 開始
    • 數組:以 * 開始

    Bulk String有兩行,第一行為 $+正文長度,第二行為實際內容。如:

    $3\r\nSET\r\n

    Bulk String 是二進制安全的可以包含任意字節,就是說可以在 Bulk String 內部包含 “\r\n” 字符(行尾的CRLF被隱藏):

    $4
    a\r\nb

    $-1 表示 nil, 比如使用 get 命令查詢一個不存在的key時,響應即為$-1

    Array 格式第一行為 “*”+數組長度,其後是相應數量的 Bulk String。如, ["foo", "bar"]的報文:

    *2
    $3
    foo
    $3
    bar

    客戶端也使用 Array 格式向服務端發送指令。命令本身將作為第一個參數,如 SET key value指令的RESP報文:

    *3
    $3
    SET
    $3
    key
    $5
    value

    將換行符打印出來:

    *3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$5\r\nvalue\r\n

    協議解析器

    我們在 一文中已經介紹過TCP服務器的實現,協議解析器將實現其 Handler 接口充當應用層服務器。

    協議解析器將接收 Socket 傳來的數據,並將其數據還原為 [][]byte 格式,如 "*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$5\r\value\r\n" 將被還原為 ['SET', 'key', 'value']

    本文完整代碼:

    來自客戶端的請求均為數組格式,它在第一行中標記報文的總行數並使用CRLF作為分行符。

    bufio 標準庫可以將從 reader 讀到的數據緩存到 buffer 中,直至遇到分隔符或讀取完畢后返回,所以我們使用 reader.ReadBytes('\n') 來保證每次讀取到完整的一行。

    需要注意的是RESP是二進制安全的協議,它允許在正文中使用CRLF字符。舉例來說 Redis 可以正確接收並執行SET "a\r\nb" 1指令, 這條指令的正確報文是這樣的:

    *3  
    $3
    SET
    $4
    a\r\nb 
    $7
    myvalue

    ReadBytes 讀取到第五行 “a\r\nb\r\n”時會將其誤認為兩行:

    *3  
    $3
    SET
    $4
    a  // 錯誤的分行
    b // 錯誤的分行
    $7
    myvalue

    因此當讀取到第四行$4后, 不應該繼續使用 ReadBytes('\n') 讀取下一行, 應使用 io.ReadFull(reader, msg) 方法來讀取指定長度的內容。

    msg = make([]byte, 4 + 2) // 正文長度4 + 換行符長度2
    _, err = io.ReadFull(reader, msg)

    定義 Client 結構體作為客戶端抽象:

    type Client struct {
        /* 與客戶端的 Tcp 連接 */
        conn   net.Conn
    
        /* 
         * 帶有 timeout 功能的 WaitGroup, 用於優雅關閉
         * 當響應被完整發送前保持 waiting 狀態, 阻止鏈接被關閉
         */
        waitingReply wait.Wait
    
        /* 標記客戶端是否正在發送指令 */ 
        sending atomic.AtomicBool
        
        /* 客戶端正在發送的參數數量, 即 Array 第一行指定的數組長度 */
        expectedArgsCount uint32
        
        /* 已經接收的參數數量, 即 len(args)*/ 
        receivedCount uint32
        
        /*
         * 已經接收到的命令參數,每個參數由一個 []byte 表示
         */
        args [][]byte
    }

    定義解析器:

    type Handler struct {
    
        /* 
         * 記錄活躍的客戶端鏈接 
         * 類型為 *Client -> placeholder 
         */
        activeConn sync.Map 
    
        /* 數據庫引擎,執行指令並返回結果 */
        db db.DB
    
        /* 關閉狀態標誌位,關閉過程中時拒絕新建連接和新請求 */
        closing atomic.AtomicBool 
    }

    接下來可以編寫主要部分了:

    func (h *Handler)Handle(ctx context.Context, conn net.Conn) {
        if h.closing.Get() {
            // 關閉過程中不接受新連接
            _ = conn.Close()
        }
    
        /* 初始化客戶端狀態 */
        client := &Client {
            conn:   conn,
        }
        h.activeConn.Store(client, 1)
    
        reader := bufio.NewReader(conn)
        var fixedLen int64 = 0 // 將要讀取的 BulkString 的正文長度
        var err error
        var msg []byte
        for {
            /* 讀取下一行數據 */ 
            if fixedLen == 0 { // 正常模式下使用 CRLF 區分數據行
                msg, err = reader.ReadBytes('\n')
                // 判斷是否以 \r\n 結尾
                if len(msg) == 0 || msg[len(msg) - 2] != '\r' {
                    errReply := &reply.ProtocolErrReply{Msg:"invalid multibulk length"}
                    _, _ =  client.conn.Write(errReply.ToBytes())
                }
            } else { // 當讀取到 BulkString 第二行時,根據給出的長度進行讀取
                msg = make([]byte, fixedLen + 2)
                _, err = io.ReadFull(reader, msg)
                // 判斷是否以 \r\n 結尾
                if len(msg) == 0 || 
                  msg[len(msg) - 2] != '\r' ||  
                  msg[len(msg) - 1] != '\n'{
                    errReply := &reply.ProtocolErrReply{Msg:"invalid multibulk length"}
                    _, _ =  client.conn.Write(errReply.ToBytes())
                }
                // Bulk String 讀取完畢,重新使用正常模式
                fixedLen = 0 
            }
            // 處理 IO 異常
            if err != nil {
                if err == io.EOF || err == io.ErrUnexpectedEOF {
                    logger.Info("connection close")
                } else {
                    logger.Warn(err)
                }
                _ = client.Close()
                h.activeConn.Delete(client)
                return // io error, disconnect with client
            }
    
            /* 解析收到的數據 */
            if !client.sending.Get() { 
                // sending == false 表明收到了一條新指令
                if msg[0] == '*' {
                    // 讀取第一行獲取參數個數
                    expectedLine, err := strconv.ParseUint(string(msg[1:len(msg)-2]), 10, 32)
                    if err != nil {
                        _, _ = client.conn.Write(UnknownErrReplyBytes)
                        continue
                    }
                    // 初始化客戶端狀態
                    client.waitingReply.Add(1) // 有指令未處理完成,阻止服務器關閉
                    client.sending.Set(true) // 正在接收指令中
                    // 初始化計數器和緩衝區 
                    client.expectedArgsCount = uint32(expectedLine) 
                    client.receivedCount = 0
                    client.args = make([][]byte, expectedLine)
                } else {
                    // TODO: text protocol
                }
            } else {
                // 收到了指令的剩餘部分(非首行)
                line := msg[0:len(msg)-2] // 移除換行符
                if line[0] == '$' { 
                    // BulkString 的首行,讀取String長度
                    fixedLen, err = strconv.ParseInt(string(line[1:]), 10, 64)
                    if err != nil {
                        errReply := &reply.ProtocolErrReply{Msg:err.Error()}
                        _, _ = client.conn.Write(errReply.ToBytes())
                    }
                    if fixedLen <= 0 {
                        errReply := &reply.ProtocolErrReply{Msg:"invalid multibulk length"}
                        _, _ = client.conn.Write(errReply.ToBytes())
                    }
                } else { 
                    // 收到參數
                    client.args[client.receivedCount] = line
                    client.receivedCount++
                }
    
    
                // 一條命令發送完畢
                if client.receivedCount == client.expectedArgsCount {
                    client.sending.Set(false)
    
                    // 執行命令並響應
                    result := h.db.Exec(client.args)
                    if result != nil {
                        _, _ = conn.Write(result.ToBytes())
                    } else {
                        _, _ = conn.Write(UnknownErrReplyBytes)
                    }
    
                    // 重置客戶端狀態,等待下一條指令
                    client.expectedArgsCount = 0
                    client.receivedCount = 0
                    client.args = nil
                    client.waitingReply.Done()
                }
            }
        }
    }

    本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

    網頁設計公司推薦更多不同的設計風格,搶佔消費者視覺第一線

    ※廣告預算用在刀口上,網站設計公司幫您達到更多曝光效益

    ※自行創業 缺乏曝光? 下一步"網站設計"幫您第一時間規劃公司的門面形象

  • I/O多路復用模型

    背景

    在文章中提到了五種I/O模型,其中前四種:阻塞模型、非阻塞模型、信號驅動模型、I/O復用模型都是同步模型;還有一種是異步模型。

    想寫一個系列的文章,介紹從I/O多路復用到異步編程和RPC框架,整個演進過程,這一系列可能包括:

    1. Reactor和Proactor模型
    2. 為什麼需要異步編程
    3. enable_shared_from_this用法分析
    4. 網絡通信庫和RPC

    為什麼有多路復用?

    多路復用技術要解決的是“通信”問題,解決核心在於“同步事件分離器”(de-multiplexer),linux系統帶有的分離器select、poll、epoll網上介紹的比較多,大家可以看看這篇介紹的不錯的文章:。通信的一方想要知道另一方的狀態(以決定自己做什麼),有兩種方法: 一是輪詢,二是消息通知。

    輪詢

    輪詢的一種典型的實現可能是這樣的:當然這裏的epoll_wait()也可以使用poll()或者select()替換。

    whiletrue) {
        active_stream[] = epoll_wait(epollfd)
        for i in active_stream[] {
            read or write till
        }
    }

    輪詢方式主要存在以下不足:

    • 增加系統開銷。無論是任務輪詢還是定時器輪詢都需要消耗對應的系統資源。
    • 無法及時感知設備狀態變化。在輪詢間隔內的設備狀態變化只有在下次輪詢時才能被發現,這將無法滿足對實時性敏感的應用場合。
    • 浪費CPU資源。無論設備是否發生狀態改變,輪詢總在進行。在實際情況中,大多數設備的狀態改變通常不會那麼頻繁,輪詢空轉將白白浪費CPU時間片。

    消息通知

    其實現方式通常是: “阻塞-通知”機制。阻塞會導致一個任務(task_struct,進程或者線程)只能處理一個”I/O流”或者類似的操作,要處理多個,就要多個任務(需要多個進程或線程),因此靈活性上又不如輪詢(一個任務足夠),很矛盾。

     

    select、poll、epoll對比

    矛盾的根源就是”一”和”多”的矛盾: 希望一個任務處理多個對象,同時避免處理阻塞-通知機制的內部細節。解決方案是多路復用(muliplex)。多路復用有3種基本方案,select()/poll()/epoll(),都是來解決這一矛盾的。

    • 通知代理: 用戶把需要關心的對象註冊給select()/poll()/epoll()函數。
    • 一對多: 所有的被關心的對象,只要有一個對象有了通知事件,select()/poll()/epoll()就會結束阻塞狀態。
    • 方便性: 用戶(程序員)不用再關心如何阻塞和被通知,以及哪些情況下會有通知產生。這件事情已經由上述幾個系統調用做了,用戶只需要實現”通知來了我該做什麼”。

     

    那麼上面3個系統調用的區別是什麼呢?
    第一個select(),結合了輪詢和阻塞兩種方式,沒有問題,每次有一個對象事件發生的時候,select()只是知道有事件發生了,具體是哪個對象發生的,不知道,需要從頭到尾輪詢一遍,複雜度是O(n)。poll函數相對select函數變化不大,只是提升了最大的可輪詢的對象個數。epoll函數把時間複雜度降到O(1)。

     

    為什麼select慢而epoll效率高?
    select()之所以慢,有幾個原因: select()的參數是一個FD數組,意味着每次select調用,都是一次新的註冊-阻塞-回調,每次select都要把一個數組從用戶空間拷貝到內核空間,內核檢測到某個對象狀態變化並寫入后,再從內核空間拷貝回用戶空間,select再把這個數組讀取一遍,並返回。這個過程非常低效。

    epoll的解決方案相當於是一種對select()的算法優化: 它把select()一個函數做的事情分解成了3步,首先epoll_create()創建一個epollfd對象(相當於一個池子),然後所有被監聽的fd通過epoll_ctrl()註冊到這個池子,也就是為每個fd指定了一個內部的回調函數(這樣,就沒有了每次調用時的來回拷貝,用戶空間的數組到內核空間只有這一次拷貝)。epoll_wait阻塞等待。在內核態有一個和epoll_wait對應的函數調用,把就緒的fd,填入到一個就緒列表中,而epoll_wait讀取這個就緒列表,做到了快速返回(O(1))。

    詳細的對比可以參考select、poll、epoll之間的區別總結:

     

    有了上面的原理介紹,這裏舉例來說明下epoll到底是怎麼使用的,加深理解。舉兩個例子:

    一個是比較簡單的父子進程通信的例子,單個小程序,不需要跑多個應用實例,不需要用戶輸入。
    一個是比較實戰的socket+epoll,畢竟現實案例中哪有兩個父子進程間通訊這麼簡單的應用場景。

    有了多路復用,難道還不夠?

    有了I/O復用,有了epoll已經可以使服務器併發幾十萬連接的同時,維持高TPS了,難道這還不夠嗎?答案是,技術層面足夠了,但在軟件工程層面卻是不夠的。例如,總要有個for循環去調用epoll,總來處理epoll的返回,這是每次都要重複的工作。for循環體裏面寫什麼—-通知返回之後,做事情的程序最好能以一種回調的機制,提供一個編程框架,讓程序更有結構一些。另一方面,如果希望每個事件通知之後,做的事情能有機會被代理到某個線程裏面去單獨運行,而線程完成的狀態又能通知回主任務,那麼”異步”的進制就必須被引入。

    所以,還有兩個問題要解決,一是”編程框架”,一是”異步”。我們先看幾個目前流行的框架,大部分框架已經包含了某種異步的機制。我們接下來的篇章將介紹“編程框架”和“異步I/O模型”。

    本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

    ※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

    網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

    ※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

  • pod刪除主要流程源碼解析

    本文以v1.12版本進行分析

    當一個pod刪除時,client端向apiserver發送請求,apiserver將pod的deletionTimestamp打上時間。kubelet watch到該事件,開始處理。

    syncLoop

    kubelet對pod的處理主要都是在syncLoop中處理的。

    func (kl *Kubelet) syncLoop(updates <-chan kubetypes.PodUpdate, handler SyncHandler) {
    for {
    ...
            if !kl.syncLoopIteration(updates, handler, syncTicker.C, housekeepingTicker.C, plegCh) {
                break
            }
    ...

    與pod刪除主要在syncLoopIteration中需要關注的是以下這兩個。

    func (kl *Kubelet) syncLoopIteration(configCh <-chan kubetypes.PodUpdate, handler SyncHandler,
        syncCh <-chan time.Time, housekeepingCh <-chan time.Time, plegCh <-chan *pleg.PodLifecycleEvent) bool {
        select {
        case u, open := <-configCh:
    ...
            switch u.Op {
    ...
            case kubetypes.UPDATE:
                handler.HandlePodUpdates(u.Pods)
    ...
        case <-housekeepingCh:
            if !kl.sourcesReady.AllReady() {
            } else {
                if err := handler.HandlePodCleanups(); err != nil {
                    glog.Errorf("Failed cleaning pods: %v", err)
                }
            }
        }

    第一個是由於發送給apiserver的DELETE請求觸發的,增加了deletionTimestamp的事件。這裏對應於kubetypes.UPDATE。也就是會走到HandlePodUpdates函數。

    另外一個與delete相關的是每2s執行一次的來自於housekeepingCh的定時事件,用於清理pod,執行的是handler.HandlePodCleanups函數。這兩個作用不同,下面分別進行介紹。

    HandlePodUpdates

    先看HandlePodUpdates這個流程。只要打上了deletionTimestamp,就必然走到這個流程里去。

    func (kl *Kubelet) HandlePodUpdates(pods []*v1.Pod) {
        for _, pod := range pods {
    ...
            kl.dispatchWork(pod, kubetypes.SyncPodUpdate, mirrorPod, start)
        }
    }

    在HandlePodUpdates中,進而將pod的信息傳遞到dispatchWork中處理。

    func (kl *Kubelet) dispatchWork(pod *v1.Pod, syncType kubetypes.SyncPodType, mirrorPod *v1.Pod, start time.Time) {
        if kl.podIsTerminated(pod) {
            if pod.DeletionTimestamp != nil {
                kl.statusManager.TerminatePod(pod)
            }
            return
        }
        // Run the sync in an async worker.
        kl.podWorkers.UpdatePod(&UpdatePodOptions{
            Pod:        pod,
            MirrorPod:  mirrorPod,
            UpdateType: syncType,
            OnCompleteFunc: func(err error) {
    ...

    這裏首先通過判斷了kl.podIsTerminated(pod)判斷pod是不是已經處於了Terminated狀態。如果是的話,則不進行下面的kl.podWorkers.UpdatePod。

    func (kl *Kubelet) podIsTerminated(pod *v1.Pod) bool {
        status, ok := kl.statusManager.GetPodStatus(pod.UID)
        if !ok {
            status = pod.Status
        }
        return status.Phase == v1.PodFailed || status.Phase == v1.PodSucceeded || (pod.DeletionTimestamp != nil && notRunning(status.ContainerStatuses))
    }

    這個地方特別值得注意的是,並不是由了DeletionTimestamp就會認為是Terminated狀態,而是有DeletionTimestamp且所有的容器不在運行了。也就是說如果是一個正在正常運行的pod,是也會走到kl.podWorkers.UpdatePod中的。UpdatePod通過一系列函數調用,最終會通過異步的方式執行syncPod函數中進入到syncPod函數中。

    func (kl *Kubelet) syncPod(o syncPodOptions) error {
    ...
        if !runnable.Admit || pod.DeletionTimestamp != nil || apiPodStatus.Phase == v1.PodFailed {
            var syncErr error
            if err := kl.killPod(pod, nil, podStatus, nil); err != nil {
    ...

    在syncPod中,調用killPod從而對pod進行停止操作。

    killPod

    killPod是停止pod的主體。在很多地方都會使用。這裏主要介紹下起主要的工作流程。停止pod的過程主要發生在killPodWithSyncResult函數中。

    func (m *kubeGenericRuntimeManager) killPodWithSyncResult(pod *v1.Pod, runningPod kubecontainer.Pod, gracePeriodOverride *int64) (result kubecontainer.PodSyncResult) {
        killContainerResults := m.killContainersWithSyncResult(pod, runningPod, gracePeriodOverride)
    ...
        for _, podSandbox := range runningPod.Sandboxes {
                if err := m.runtimeService.StopPodSandbox(podSandbox.ID.ID); err != nil {
    ...

    killPodWithSyncResult的主要工作分為兩個部分。killContainersWithSyncResult負責將pod中的container停止掉,在停止后再執行StopPodSandbox。

    func (m *kubeGenericRuntimeManager) killContainer(pod *v1.Pod, containerID kubecontainer.ContainerID, containerName string, reason string, gracePeriodOverride *int64) error {
        if err := m.internalLifecycle.PreStopContainer(containerID.ID); err != nil {
            return err
        }
    ...
        err := m.runtimeService.StopContainer(containerID.ID, gracePeriod)

    killContainersWithSyncResult的主要工作是在killContainer中完成的,這裏可以看到,其中的主要兩個步驟是在容器中進行prestop的操作。待其成功后,進行container的stop工作。至此所有的應用容器都已經停止了。下一步是停止pause容器。而StopPodSandbox就是執行這一過程的。將sandbox,也就是pause容器停止掉。StopPodSandbox是在dockershim中執行的。

    func (ds *dockerService) StopPodSandbox(ctx context.Context, r *runtimeapi.StopPodSandboxRequest) (*runtimeapi.StopPodSandboxResponse, error) {
    ...
    if !hostNetwork && (ready || !ok) {
    ...
            err := ds.network.TearDownPod(namespace, name, cID, annotations)
    ...
        }
        if err := ds.client.StopContainer(podSandboxID, defaultSandboxGracePeriod); err != nil {

    StopPodSandbox中主要的部分是先進行網絡卸載,再停止相應的容器。在完成StopPodSandbox后,至此pod的所有容器都已經停止完成。至於volume的卸載,是在volumeManager中進行的。本文不做單獨介紹了。停止后的容器在pod徹底清理后,會被gc回收。這裏也不展開講了。

    HandlePodCleanups

    上面這個流程並不是刪除流程的全部。一個典型的情況就是,如果container都不是running,那麼在UpdatePod的時候都return了,那麼又由誰來處理呢?這裏我們回到最開始,就是那個每2s執行一次的HandlePodCleanups的流程。也就是說比如container處於crash,container正好不是running等情況,其實是在這個流程里進行處理的。當然HandlePodCleanups的作用不僅僅是清理not running的pod,再比如數據已經在apiserver中強制清理掉了,或者由於其他原因這個節點上還有一些沒有完成清理的pod,都是在這個流程中進行處理。

    func (kl *Kubelet) HandlePodCleanups() error {
    ... 
        for _, pod := range runningPods {
            if _, found := desiredPods[pod.ID]; !found {
                kl.podKillingCh <- &kubecontainer.PodPair{APIPod: nil, RunningPod: pod}
            }
        }

    runningPods是從cache中獲取節點現有的pod,而desiredPods則是節點上應該存在未被停止的pod。如果存在runningPods中有而desiredPods中沒有的pod,那麼它應該被停止,所以發送到podKillingCh中。

    func (kl *Kubelet) podKiller() {
    ...
        for podPair := range kl.podKillingCh {
    ...
    
            if !exists {
                go func(apiPod *v1.Pod, runningPod *kubecontainer.Pod) {
                    glog.V(2).Infof("Killing unwanted pod %q", runningPod.Name)
                    err := kl.killPod(apiPod, runningPod, nil, nil)
    ...
                }(apiPod, runningPod)
            }
        }
    }

    在podKiller流程中,會去接收來自podKillingCh的消息,從而執行killPod,上文已經做了該函數的介紹了。

    statusManager

    在最後,statusManager中的syncPod流程,將會進行檢測,通過canBeDeleted確認是否所有的容器關閉了,volume卸載了,cgroup清理了等等。如果這些全部完成了,則從apiserver中將pod信息徹底刪除。

    func (m *manager) syncPod(uid types.UID, status versionedPodStatus) {
    ...
        if m.canBeDeleted(pod, status.status) {
            deleteOptions := metav1.NewDeleteOptions(0)
            deleteOptions.Preconditions = metav1.NewUIDPreconditions(string(pod.UID))
            err = m.kubeClient.CoreV1().Pods(pod.Namespace).Delete(pod.Name, deleteOptions)
    ...

    本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

    ※帶您來了解什麼是 USB CONNECTOR  ?

    ※自行創業 缺乏曝光? 下一步"網站設計"幫您第一時間規劃公司的門面形象

    ※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

    ※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

    ※廣告預算用在刀口上,網站設計公司幫您達到更多曝光效益