火雞群感染高病原性H7N3 美國農業部:沒有傳染人類案例

摘錄自2020年4月26日ETtoday報導

繼2017年後出現H7N3首例!美國農業部(United States Department of Agriculture,USDA)在9日證實,在南卡羅萊納州(Carolina)的(Chesterfield)市,發現有火雞感染H7N3高病原性(HPAI)禽流感病毒,目前該區域已經下令封鎖,並隔離相關禽類,以確保不進入人類或動物的口中。

根據美國農業部公告,H7N3目前「沒有任何人類感染案例」,因此沒有立即性的健康疑慮,但是為了以防萬一,建議在烹調家禽與雞蛋時應以165˚F(約74℃)溫度殺死病毒與細菌。

根據世界動物衛生組織(OIE)規範,如果「4-8週齡的雞感染後死亡率達75%」即「高病原性禽流感」,通常出現在H5、H7型上。人類如果感染禽流感,可能出現高燒、呼吸急促等症狀,由甲型禽流感(如H5N1、H5N6、H7N9和H10N8病毒)引起的症狀比一般流感嚴重,大多數患者須住院治療。

生活環境
永續發展
土地利用
國際新聞
美國
火雞
禽流感
公共衛生
經濟動物
動物福利
糧食

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※產品缺大量曝光嗎?你需要的是一流包裝設計!

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

※回頭車貨運收費標準

※推薦評價好的iphone維修中心

※超省錢租車方案

台中搬家遵守搬運三大原則,讓您的家具不再被破壞!

※推薦台中搬家公司優質服務,可到府估價

南極地區首次發現青蛙化石碎片

摘錄自2020年4月27日俄羅斯衛星通訊社報導

根據發表在《Scientific Reports》期刊的研究報告,科研人員在南極半島北端的西莫爾島發現了數塊青蛙的頭骨和部分髖骨化石碎片,這種古老的生物是南美地區現代頭盔蛙科的近親。

這一的發現讓科學家對南極大陸的古代氣候有了新的認識。這些化石碎片距今約4000萬年,頭骨形狀可以看出這隻青蛙屬於頭盔蛙科。頭盔蛙科現生種生活在南美安第斯山脈中部的溫暖潮濕山谷中。這表明,至少4000萬年前,南極洲地區也是類似的氣候。

這一發現改變了科學家對南極大陸氣候變化的認識。大多數科學家認為,大約4000萬年前,南極洲與澳洲大陸分離後迅速被冰層覆蓋。但是一些證據表明,在南極大陸與南半球其他現代大陸完全分離前,南極洲冰蓋就已經開始形成。

生態保育
物種保育
生物多樣性
國際新聞
南極
古生物學
化石
青蛙
兩棲類
南極

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※回頭車貨運收費標準

※產品缺大量曝光嗎?你需要的是一流包裝設計!

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

※推薦評價好的iphone維修中心

※教你寫出一流的銷售文案?

台中搬家公司教你幾個打包小技巧,輕鬆整理裝箱!

台中搬家遵守搬運三大原則,讓您的家具不再被破壞!

科學家在北義空污粒子上檢出新冠病毒 傳播距離、病毒是否存活仍待釐清

環境資訊中心綜合外電;姜唯 編譯;林大利 審校

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

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

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

※教你寫出一流的銷售文案?

※別再煩惱如何寫文案,掌握八大原則!

Oracle 11g RAC之HAIP相關問題總結

1 文檔概要

2 禁用/啟用HAIP

  • 2.1 禁用/啟用HAIP資源
  • 2.2 修改ASM資源的依賴關係

3 修改cluster_interconnects參數

  • 3.1 使用grid用戶修改ASM實例的cluster_interconnects參數
  • 3.2 使用oracle用戶修改DB實例的cluster_interconnects參數
  • 3.3 重啟所有實例或集群
  • 3.4 檢查cluster_interconnects參數

1 文檔概要

環境:RHEL 6.4 + GI 11.2.0.4 + Oracle 11.2.0.4 對有關HAIP相關問題的總結,包括禁用/啟用HAIP,修改ASM資源的依賴關係,修改cluster_interconnects參數等。

2 禁用/啟用HAIP

2.1 禁用/啟用HAIP資源

禁用HAIP資源: root用戶執行@all nodes

# /opt/app/11.2.0/grid/bin/crsctl modify res ora.cluster_interconnect.haip -attr "ENABLED=0" -init

啟用HAIP資源:
如果之後想重新使用HAIP資源,可以啟用:

# /opt/app/11.2.0/grid/bin/crsctl modify res ora.cluster_interconnect.haip -attr "ENABLED=1" -init

在實際處理的案例中,發現其中一個節點始終無法啟動HAIP,在另外節點可以啟用HAIP,但這樣是無法啟動集群的,所以在可以啟動HAIP的節點直接禁用HAIP,這樣所有節點都使用真實的私網地址就可以正常啟動成功。但需要注意在有些場景下,單純禁用HAIP會導致ASM無法啟動,這是由於ASM資源的相關依賴關係導致。

2.2 修改ASM資源的依賴關係

在一些實際案例場景,我們直接禁用HAIP資源,再重啟has時可能會無法啟動ASM資源,因為ASM資源對HAIP有依賴關係。這一點我在自己的測試環境也可以驗證。

查看當前ASM資源的關聯關係:

[root@jyrac2 ~]# crsctl stat res ora.asm -p -init
NAME=ora.asm
TYPE=ora.asm.type
ACL=owner:grid:rw-,pgrp:oinstall:rw-,other::r--,user:grid:rwx
ACTION_FAILURE_TEMPLATE=
ACTION_SCRIPT=
ACTIVE_PLACEMENT=0
AGENT_FILENAME=%CRS_HOME%/bin/oraagent%CRS_EXE_SUFFIX%
AUTO_START=restore
CARDINALITY=1
CHECK_ARGS=
CHECK_COMMAND=
CHECK_INTERVAL=1
CHECK_TIMEOUT=30
CLEAN_ARGS=
CLEAN_COMMAND=
DAEMON_LOGGING_LEVELS=
DAEMON_TRACING_LEVELS=
DEFAULT_TEMPLATE=
DEGREE=1
DESCRIPTION="ASM instance"
DETACHED=true
ENABLED=1
FAILOVER_DELAY=0
FAILURE_INTERVAL=3
FAILURE_THRESHOLD=5
GEN_USR_ORA_INST_NAME=+ASM2
HOSTING_MEMBERS=
LOAD=1
LOGGING_LEVEL=1
NOT_RESTARTING_TEMPLATE=
OFFLINE_CHECK_INTERVAL=0
ORA_VERSION=11.2.0.4.0
PID_FILE=
PLACEMENT=balanced
PROCESS_TO_MONITOR=
PROFILE_CHANGE_TEMPLATE=
RESTART_ATTEMPTS=5
SCRIPT_TIMEOUT=600
SERVER_POOLS=
SPFILE=
START_ARGS=
START_COMMAND=
START_DEPENDENCIES=hard(ora.cssd,ora.cluster_interconnect.haip,ora.ctssd)pullup(ora.cssd,ora.cluster_interconnect.haip,ora.ctssd)weak(ora.drivers.acfs)
START_TIMEOUT=600
STATE_CHANGE_TEMPLATE=
STOP_ARGS=
STOP_COMMAND=
STOP_DEPENDENCIES=hard(intermediate:ora.cssd,shutdown:ora.cluster_interconnect.haip)
STOP_TIMEOUT=600
UNRESPONSIVE_TIMEOUT=180
UPTIME_THRESHOLD=1h
USR_ORA_ENV=
USR_ORA_INST_NAME=
USR_ORA_OPEN_MODE=mount
USR_ORA_OPI=false
USR_ORA_STOP_MODE=immediate
VERSION=11.2.0.3.0

可以看到ASM資源和HAIP資源的依賴關係。

修改ASM的關聯關係@all nodes:

crsctl modify resource ora.asm -attr "START_DEPENDENCIES='hard(ora.cssd,ora.ctssd)pullup(ora.cssd,ora.ctssd)weak(ora.drivers.acfs)'" -f -init 

crsctl modify resource ora.asm -attr "STOP_DEPENDENCIES=hard(intermediate:ora.cssd)" -f –init

改回ASM的關聯關係:

crsctl modify resource ora.asm -attr "START_DEPENDENCIES='hard(ora.cssd, ora.cluster_interconnect.haip, ora.ctssd)pullup(ora.cssd, ora.cluster_interconnect.haip, ora.ctssd)weak(ora.drivers.acfs)'" -f -init 

crsctl modify resource ora.asm -attr "STOP_DEPENDENCIES=hard(intermediate:ora.cssd)" -f –init 

3 修改cluster_interconnects參數

3.1 使用grid用戶修改ASM實例的cluster_interconnects參數

修改為具體的私網地址,示例如下:

SQL> alter system set cluster_interconnects='10.10.10.50' scope=spfile sid='+ASM1'; 
SQL> alter system set cluster_interconnects='10.10.10.52' scope=spfile sid='+ASM2';

改回默認值為空,示例如下:

SQL> alter system set cluster_interconnects='' scope=spfile sid='+ASM1'; 
SQL> alter system set cluster_interconnects='' scope=spfile sid='+ASM2';

3.2 使用oracle用戶修改DB實例的cluster_interconnects參數

修改為具體的私網地址,示例如下:

SQL> alter system set cluster_interconnects='10.10.10.50' scope=spfile sid='jyzhao1'; 
SQL> alter system set cluster_interconnects='10.10.10.52' scope=spfile sid='jyzhao2';

改回默認值為空,示例如下:

SQL> alter system set cluster_interconnects='' scope=spfile sid='jyzhao1'; 
SQL> alter system set cluster_interconnects='' scope=spfile sid='jyzhao2';

在實際的一個案例中,客戶是11g版本的GI環境,實際有2塊私網網卡,使用了HAIP特性,同時安裝有11g RAC和10g RAC,11g RAC使用HAIP正常,10g RAC由於無法使用HAIP,所以獲取到的是真實的私網地址,但是數據庫無法在所有節點同時open,這種情況,直接把10g RAC實例的cluster_interconnects參數修改成其中一個網卡的真實私網地址,即可正常在所有節點open。
這類場景是最適合修改參數解決,不影響其他正常使用HAIP的11g環境。

3.3 重啟所有實例或集群

修改參數之後需要重新啟動實例生效,這裏建議直接重啟集群一起驗證修改后的效果:

# /opt/app/11.2.0/grid/bin/crsctl stop has
# /opt/app/11.2.0/grid/bin/crsctl start has

3.4 檢查cluster_interconnects參數

SQL> show parameter cluster_interconnects

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

※別再煩惱如何寫文案,掌握八大原則!

※教你寫出一流的銷售文案?

※超省錢租車方案

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

※產品缺大量曝光嗎?你需要的是一流包裝設計!

※回頭車貨運收費標準

設計模式系列之建造者模式(Builder Pattern)——複雜對象的組裝與創建

說明:設計模式系列文章是讀劉偉所著《設計模式的藝術之道(軟件開發人員內功修鍊之道)》一書的閱讀筆記。個人感覺這本書講的不錯,有興趣推薦讀一讀。詳細內容也可以看看此書作者的博客https://blog.csdn.net/LoveLion/article/details/17517213

模式概述

模式定義

沒有人買車會只買一個輪胎或者方向盤,大家買的都是一輛包含輪胎、方向盤和發動機等多個部件的完整汽車。如何將這些部件組裝成一輛完整的汽車並返回給用戶,這是建造者模式需要解決的問題。建造者模式又稱為生成器模式,它是一種較為複雜、使用頻率也相對較低的創建型模式。建造者模式為客戶端返回的不是一個簡單的產品,而是一個由多個部件組成的複雜產品。

它將客戶端與包含多個組成部分(或部件)的複雜對象的創建過程分離,客戶端無須知道複雜對象的內部組成部分與裝配方式,只需要知道所需建造者的類型即可。它關注如何一步一步創建一個的複雜對象,不同的具體建造者定義了不同的創建過程,且具體建造者相互獨立,增加新的建造者非常方便,無須修改已有代碼,系統具有較好的擴展性。

建造者模式(Builder Pattern):將一個複雜對象的構建與它的表示分離,使得同樣的構建過程可以創建不同的表示。建造者模式是一種對象創建型模式。

建造者模式一步一步創建一個複雜的對象,它允許用戶只通過指定複雜對象的類型和內容就可以構建它們,用戶不需要知道內部的具體構建細節。

模式結構圖

建造者模式結構圖如下所示:

建造者模式結構圖中包含如下幾個角色:

  • Builder(抽象建造者):它為創建一個產品Product對象的各個部件指定抽象接口,在該接口中一般聲明兩類方法,一類方法是buildPartX(),它們用於創建複雜對象的各個部件;另一類方法是getResult(),它們用於返回複雜對象。Builder既可以是抽象類,也可以是接口。

  • ConcreteBuilder(具體建造者):它實現了Builder接口,實現各個部件的具體構造和裝配方法,定義並明確它所創建的複雜對象,也可以提供一個方法返回創建好的複雜產品對象。

  • Product(產品角色):它是被構建的複雜對象,包含多個組成部件,具體建造者創建該產品的內部表示並定義它的裝配過程。

  • Director(指揮者):指揮者又稱為導演類,它負責安排複雜對象的建造次序,指揮者與抽象建造者之間存在關聯關係,可以在其construct()建造方法中調用建造者對象的部件構造與裝配方法,完成複雜對象的建造。客戶端一般只需要與指揮者進行交互,在客戶端確定具體建造者的類型,並實例化具體建造者對象(也可以通過配置文件和反射機制),然後通過指揮者類的構造函數或者Setter方法將該對象傳入指揮者類中。

在建造者模式的定義中提到了複雜對象,那麼什麼是複雜對象?簡單來說,複雜對象是指那些包含多個非簡單類型的成員屬性,這些成員屬性也稱為部件或零件,如汽車包括方向盤、發動機、輪胎等部件,电子郵件包括髮件人、收件人、主題、內容、附件等部件。

模式偽代碼

定義產品角色,典型代碼如下:

public class Product {
    // 定義部件,部件可以是任意類型,包括值類型和引用類型
    private String partA;

    private String partB;

    private String partC;

    // getter、setter方法省略
}

抽象建造者中定義了產品的創建方法和返回方法,其典型代碼如下

public abstract class Builder {
    // 創建產品對象
    protected Product product = new Product();

    public abstract void buildPartA();

    public abstract void buildPartB();

    public abstract void buildPartC();

    // 返回產品對象
    public Product getResult() {
        return product;
    }
}

在抽象類Builder中聲明了一系列抽象的buildPartX()方法用於創建複雜產品的各個部件,具體建造過程在ConcreteBuilder中實現,此外還提供了工廠方法getResult(),用於返回一個建造好的完整產品。

ConcreteBuilder中實現了buildPartX()方法,通過調用ProductsetPartX()方法可以給產品對象的成員屬性設值。不同的具體建造者在實現buildPartX()方法時將有所區別。

在建造者模式的結構中還引入了一個指揮者類Director,該類主要有兩個作用:一方面它隔離了客戶與創建過程;另一方面它控制產品的創建過程,包括某個buildPartX()方法是否被調用以及多個buildPartX()方法調用的先後次序等。指揮者針對抽象建造者編程,客戶端只需要知道具體建造者的類型,即可通過指揮者類調用建造者的相關方法,返回一個完整的產品對象。在實際生活中也存在類似指揮者一樣的角色,如一個客戶去購買電腦,電腦銷售人員相當於指揮者,只要客戶確定電腦的類型,電腦銷售人員可以通知電腦組裝人員給客戶組裝一台電腦。指揮者類的代碼示例如下:

public class Director {

    private Builder builder;

    public Director(Builder builder) {
        this.builder = builder;
    }

    public void setBuilder(Builder builder) {
        this.builder = builer;
    }

    //產品構建與組裝方法
    public Product construct() {

        builder.buildPartA();
        builder.buildPartB();
        builder.buildPartC();

        return builder.getResult();
    }
}

在指揮者類中可以注入一個抽象建造者類型的對象,其核心在於提供了一個建造方法construct(),在該方法中調用了builder對象的構造部件的方法,最後返回一個產品對象。

對於客戶端而言,只需關心具體的建造者即可,代碼片段如下所示:

public static void main(String[] args) {
    Builder builder = new ConcreteBuilder();

    Director director = new Director(builder);

    Product product = director.construct();
}

模式簡化

在有些情況下,為了簡化系統結構,可以將Director和抽象建造者Builder進行合併,在Builder中提供逐步構建複雜產品對象的construct()方法。

public abstract class Builder {
    // 創建產品對象
    protected Product product = new Product();

    public abstract void buildPartA();

    public abstract void buildPartB();

    public abstract void buildPartC();

    // 返回產品對象
    public Product construct() {
        buildPartA();
        buildPartB();
        buildPartC();
        return product;
    }
}

模式應用

模式在JDK中的應用

java.util.stream.Stream.Builder

public interface Builder<T> extends Consumer<T> {
    /**
     * Adds an element to the stream being built.
     */
    default Builder<T> add(T t) {
        accept(t);
        return this;
    }

    /**
     * Builds the stream, transitioning this builder to the built state
     */
    Stream<T> build();
}

模式在開源項目中的應用

看下Spring是如何構建org.springframework.web.servlet.mvc.method.RequestMappingInfo

/**
 * Defines a builder for creating a RequestMappingInfo.
 * @since 4.2
 */
public interface Builder {
  /**
   * Set the path patterns.
   */
  Builder paths(String... paths);

  /**
   * Set the request method conditions.
   */
  Builder methods(RequestMethod... methods);

  /**
   * Set the request param conditions.
   */
  Builder params(String... params);

  /**
   * Set the header conditions.
   * <p>By default this is not set.
   */
  Builder headers(String... headers);

  /**
   * Build the RequestMappingInfo.
   */
  RequestMappingInfo build();
}

Builder接口的默認實現,如下:

private static class DefaultBuilder implements Builder {

  private String[] paths = new String[0];

  private RequestMethod[] methods = new RequestMethod[0];

  private String[] params = new String[0];

  private String[] headers = new String[0];

  public DefaultBuilder(String... paths) {
    this.paths = paths;
  }

  @Override
  public Builder paths(String... paths) {
    this.paths = paths;
    return this;
  }

  @Override
  public DefaultBuilder methods(RequestMethod... methods) {
    this.methods = methods;
    return this;
  }

  @Override
  public DefaultBuilder params(String... params) {
    this.params = params;
    return this;
  }

  @Override
  public DefaultBuilder headers(String... headers) {
    this.headers = headers;
    return this;
  }
  
  @Override
  public RequestMappingInfo build() {
    ContentNegotiationManager manager = this.options.getContentNegotiationManager();

    PatternsRequestCondition patternsCondition = new PatternsRequestCondition(
        this.paths, this.options.getUrlPathHelper(), this.options.getPathMatcher(),
        this.options.useSuffixPatternMatch(), this.options.useTrailingSlashMatch(),
        this.options.getFileExtensions());

    return new RequestMappingInfo(this.mappingName, patternsCondition,
        new RequestMethodsRequestCondition(this.methods),
        new ParamsRequestCondition(this.params),
        new HeadersRequestCondition(this.headers),
        new ConsumesRequestCondition(this.consumes, this.headers),
        new ProducesRequestCondition(this.produces, this.headers, manager),
        this.customCondition);
  }
}

Spring框架中許多構建類的實例化使用了類似上面方式,總結有以下特點:

  1. Builder大多是構建類的內部類,構建類提供了一個靜態創建Builder的方法
  2. Builder返回構建類的實例,大多通過build()方法
  3. 構建過程有大量參數,除了幾個必要參數,用戶可根據自己所需選擇設置其他參數實例化對象

模式總結

建造者模式的核心在於如何一步步構建一個包含多個組成部件的完整對象,使用相同的構建過程構建不同的產品,在軟件開發中,如果我們需要創建複雜對象並希望系統具備很好的靈活性和可擴展性可以考慮使用建造者模式。

建造者模式抽象工廠模式有點相似,但是建造者模式返回一個完整的複雜產品,而抽象工廠模式返回一系列相關的產品;在抽象工廠模式中,客戶端通過選擇具體工廠來生成所需對象,而在建造者模式中,客戶端通過指定具體建造者類型並指導Director類如何去生成對象,側重於一步步構造一個複雜對象,然後將結果返回。如果將抽象工廠模式看成一個汽車配件生產廠,生成不同類型的汽車配件,那麼建造者模式就是一個汽車組裝廠,通過對配件進行組裝返回一輛完整的汽車。

主要優點

  1. 將產品本身與產品的創建過程解耦,使得相同的創建過程可以創建不同的產品對象
  2. 可以更加精細地控制產品的創建過程。將複雜產品的創建步驟分解在不同的方法中,使得創建過程更加清晰,也更方便使用程序來控制創建過程。

適用場景

(1) 需要生成的產品對象有複雜的內部結構,這些產品對象通常包含多個成員屬性。

(2) 需要生成的產品對象的屬性相互依賴,需要指定其生成順序。

(3) 隔離複雜對象的創建和使用,並使得相同的創建過程可以創建不同的產品。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※別再煩惱如何寫文案,掌握八大原則!

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※超省錢租車方案

※教你寫出一流的銷售文案?

網頁設計最專業,超強功能平台可客製化

※產品缺大量曝光嗎?你需要的是一流包裝設計!

台中搬家遵守搬運三大原則,讓您的家具不再被破壞!

Linux的文件系統及文件緩存知識點整理

https://www.luozhiyun.com/archives/291

Linux的文件系統

文件系統的特點

  1. 文件系統要有嚴格的組織形式,使得文件能夠以塊為單位進行存儲。

  2. 文件系統中也要有索引區,用來方便查找一個文件分成的多個塊都存放在了什麼位置。

  3. 如果文件系統中有的文件是熱點文件,近期經常被讀取和寫入,文件系統應該有緩存層。

  4. 文件應該用文件夾的形式組織起來,方便管理和查詢。

  5. Linux內核要在自己的內存裏面維護一套數據結構,來保存哪些文件被哪些進程打開和使用。

    總體來說,文件系統的主要功能梳理如下:

ext系列的文件系統的格式

inode與塊的存儲

硬盤分成相同大小的單元,我們稱為塊(Block)。一塊的大小是扇區大小的整數倍,默認是4K。在格式化的時候,這個值是可以設定的。

一大塊硬盤被分成了一個個小的塊,用來存放文件的數據部分。這樣一來,如果我們像存放一個文件,就不用給他分配一塊連續的空間了。我們可以分散成一個個小塊進行存放。這樣就靈活得多,也比較容易添加、刪除和插入數據。

inode就是文件索引的意思,我們每個文件都會對應一個inode;一個文件夾就是一個文件,也對應一個inode。

inode數據結構如下:

struct ext4_inode {
	__le16	i_mode;		/* File mode */
	__le16	i_uid;		/* Low 16 bits of Owner Uid */
	__le32	i_size_lo;	/* Size in bytes */
	__le32	i_atime;	/* Access time */
	__le32	i_ctime;	/* Inode Change time */
	__le32	i_mtime;	/* Modification time */
	__le32	i_dtime;	/* Deletion Time */
	__le16	i_gid;		/* Low 16 bits of Group Id */
	__le16	i_links_count;	/* Links count */
	__le32	i_blocks_lo;	/* Blocks count */
	__le32	i_flags;	/* File flags */
......
	__le32	i_block[EXT4_N_BLOCKS];/* Pointers to blocks */
	__le32	i_generation;	/* File version (for NFS) */
	__le32	i_file_acl_lo;	/* File ACL */
	__le32	i_size_high;
......
};

inode裏面有文件的讀寫權限i_mode,屬於哪個用戶i_uid,哪個組i_gid,大小是多少i_size_io,佔用多少個塊i_blocks_io,i_atime是access time,是最近一次訪問文件的時間;i_ctime是change time,是最近一次更改inode的時間;i_mtime是modify time,是最近一次更改文件的時間等。

所有的文件都是保存在i_block裏面。具體保存規則由EXT4_N_BLOCKS決定,EXT4_N_BLOCKS有如下的定義:

#define	EXT4_NDIR_BLOCKS		12
#define	EXT4_IND_BLOCK			EXT4_NDIR_BLOCKS
#define	EXT4_DIND_BLOCK			(EXT4_IND_BLOCK + 1)
#define	EXT4_TIND_BLOCK			(EXT4_DIND_BLOCK + 1)
#define	EXT4_N_BLOCKS			(EXT4_TIND_BLOCK + 1)

在ext2和ext3中,其中前12項直接保存了塊的位置,也就是說,我們可以通過i_block[0-11],直接得到保存文件內容的塊。

但是,如果一個文件比較大,12塊放不下。當我們用到i_block[12]的時候,就不能直接放數據塊的位置了,要不然i_block很快就會用完了。

那麼可以讓i_block[12]指向一個塊,這個塊裏面不放數據塊,而是放數據塊的位置,這個塊我們稱為間接塊。如果文件再大一些,i_block[13]會指向一個塊,我們可以用二次間接塊。二次間接塊裏面存放了間接塊的位置,間接塊裏面存放了數據塊的位置,數據塊裏面存放的是真正的數據。如果文件再大點,那麼i_block[14]同理。

這裏面有一個非常顯著的問題,對於大文件來講,我們要多次讀取硬盤才能找到相應的塊,這樣訪問速度就會比較慢。

為了解決這個問題,ext4做了一定的改變。它引入了一個新的概念,叫作Extents。比方說,一個文件大小為128M,如果使用4k大小的塊進行存儲,需要32k個塊。如果按照ext2或者ext3那樣散着放,數量太大了。但是Extents可以用於存放連續的塊,也就是說,我們可以把128M放在一個Extents裏面。這樣的話,對大文件的讀寫性能提高了,文件碎片也減少了。

Exents是一個樹狀結構:

每個節點都有一個頭,ext4_extent_header可以用來描述某個節點。

struct ext4_extent_header {
	__le16	eh_magic;	/* probably will support different formats */
	__le16	eh_entries;	/* number of valid entries */
	__le16	eh_max;		/* capacity of store in entries */
	__le16	eh_depth;	/* has tree real underlying blocks? */
	__le32	eh_generation;	/* generation of the tree */
};

eh_entries表示這個節點裏面有多少項。這裏的項分兩種,如果是恭弘=叶 恭弘子節點,這一項會直接指向硬盤上的連續塊的地址,我們稱為數據節點ext4_extent;如果是分支節點,這一項會指向下一層的分支節點或者恭弘=叶 恭弘子節點,我們稱為索引節點ext4_extent_idx。這兩種類型的項的大小都是12個byte。

/*
 * This is the extent on-disk structure.
 * It's used at the bottom of the tree.
 */
struct ext4_extent {
	__le32	ee_block;	/* first logical block extent covers */
	__le16	ee_len;		/* number of blocks covered by extent */
	__le16	ee_start_hi;	/* high 16 bits of physical block */
	__le32	ee_start_lo;	/* low 32 bits of physical block */
};
/*
 * This is index on-disk structure.
 * It's used at all the levels except the bottom.
 */
struct ext4_extent_idx {
	__le32	ei_block;	/* index covers logical blocks from 'block' */
	__le32	ei_leaf_lo;	/* pointer to the physical block of the next *
				 * level. leaf or next index could be there */
	__le16	ei_leaf_hi;	/* high 16 bits of physical block */
	__u16	ei_unused;
};

如果文件不大,inode裏面的i_block中,可以放得下一個ext4_extent_header和4項ext4_extent。所以這個時候,eh_depth為0,也即inode裏面的就是恭弘=叶 恭弘子節點,樹高度為0。

如果文件比較大,4個extent放不下,就要分裂成為一棵樹,eh_depth>0的節點就是索引節點,其中根節點深度最大,在inode中。最底層eh_depth=0的是恭弘=叶 恭弘子節點。

除了根節點,其他的節點都保存在一個塊4k裏面,4k扣除ext4_extent_header的12個byte,剩下的能夠放340項,每個extent最大能表示128MB的數據,340個extent會使你的表示的文件達到42.5GB。

inode位圖和塊位圖

inode的位圖大小為4k,每一位對應一個inode。如果是1,表示這個inode已經被用了;如果是0,則表示沒被用。block的位圖同理。

在Linux操作系統裏面,想要創建一個新文件,會調用open函數,並且參數會有O_CREAT。這表示當文件找不到的時候,我們就需要創建一個。那麼open函數的調用過程大致是:要打開一個文件,先要根據路徑找到文件夾。如果發現文件夾下面沒有這個文件,同時又設置了O_CREAT,就說明我們要在這個文件夾下面創建一個文件。

創建一個文件,那麼就需要創建一個inode,那麼就會從文件系統裏面讀取inode位圖,然後找到下一個為0的inode,就是空閑的inode。對於block位圖,在寫入文件的時候,也會有這個過程。

文件系統的格式

數據塊的位圖是放在一個塊裏面的,共4k。每位表示一個數據塊,共可以表示$4 * 1024 * 8 = 2{15}$個數據塊。如果每個數據塊也是按默認的4K,最大可以表示空間為$2{15} * 4 * 1024 = 2^{27}$個byte,也就是128M,那麼顯然是不夠的。

這個時候就需要用到塊組,數據結構為ext4_group_desc,這裏面對於一個塊組裡的inode位圖bg_inode_bitmap_lo、塊位圖bg_block_bitmap_lo、inode列表bg_inode_table_lo,都有相應的成員變量。

這樣一個個塊組,就基本構成了我們整個文件系統的結構。因為塊組有多個,塊組描述符也同樣組成一個列表,我們把這些稱為塊組描述符表。

我們還需要有一個數據結構,對整個文件系統的情況進行描述,這個就是超級塊ext4_super_block。裏面有整個文件系統一共有多少inode,s_inodes_count;一共有多少塊,s_blocks_count_lo,每個塊組有多少inode,s_inodes_per_group,每個塊組有多少塊,s_blocks_per_group等。這些都是這類的全局信息。

最終,整個文件系統格式就是下面這個樣子。

默認情況下,超級塊和塊組描述符表都有副本保存在每一個塊組裡面。防止這些數據丟失了,導致整個文件系統都打不開了。

由於如果每個塊組裡面都保存一份完整的塊組描述符表,一方面很浪費空間;另一個方面,由於一個塊組最大128M,而塊組描述符表裡面有多少項,這就限制了有多少個塊組,128M * 塊組的總數目是整個文件系統的大小,就被限制住了。

因此引入Meta Block Groups特性。

首先,塊組描述符表不會保存所有塊組的描述符了,而是將塊組分成多個組,我們稱為元塊組(Meta Block Group)。每個元塊組裡面的塊組描述符表僅僅包括自己的,一個元塊組包含64個塊組,這樣一個元塊組中的塊組描述符表最多64項。

我們假設一共有256個塊組,原來是一個整的塊組描述符表,裏面有256項,要備份就全備份,現在分成4個元塊組,每個元塊組裡面的塊組描述符表就只有64項了,這就小多了,而且四個元塊組自己備份自己的。

根據圖中,每一個元塊組包含64個塊組,塊組描述符表也是64項,備份三份,在元塊組的第一個,第二個和最後一個塊組的開始處。

如果開啟了sparse_super特性,超級塊和塊組描述符表的副本只會保存在塊組索引為0、3、5、7的整數冪里。所以上圖的超級塊只在索引為0、3、5、7等的整數冪里。

目錄的存儲格式

其實目錄本身也是個文件,也有inode。inode裏面也是指向一些塊。和普通文件不同的是,普通文件的塊裏面保存的是文件數據,而目錄文件的塊裏面保存的是目錄裏面一項一項的文件信息。這些信息我們稱為ext4_dir_entry。

在目錄文件的塊中,最簡單的保存格式是列表,每一項都會保存這個目錄的下一級的文件的文件名和對應的inode,通過這個inode,就能找到真正的文件。第一項是“.”,表示當前目錄,第二項是“…”,表示上一級目錄,接下來就是一項一項的文件名和inode。

如果在inode中設置EXT4_INDEX_FL標誌,那麼就表示根據索引查找文件。索引項會維護一個文件名的哈希值和數據塊的一個映射關係。

如果我們要查找一個目錄下面的文件名,可以通過名稱取哈希。如果哈希能夠匹配上,就說明這個文件的信息在相應的塊裏面。然後打開這個塊,如果裏面不再是索引,而是索引樹的恭弘=叶 恭弘子節點的話,那裡面還是ext4_dir_entry的列表,我們只要一項一項找文件名就行。通過索引樹,我們可以將一個目錄下面的N多的文件分散到很多的塊裏面,可以很快地進行查找。

Linux中的文件緩存

ext4文件系統層

對於ext4文件系統來講,內核定義了一個ext4_file_operations。

const struct file_operations ext4_file_operations = {
......
	.read_iter	= ext4_file_read_iter,
	.write_iter	= ext4_file_write_iter,
......
}

ext4_file_read_iter會調用generic_file_read_iter,ext4_file_write_iter會調用__generic_file_write_iter。

ssize_t
generic_file_read_iter(struct kiocb *iocb, struct iov_iter *iter)
{
......
    if (iocb->ki_flags & IOCB_DIRECT) {
......
        struct address_space *mapping = file->f_mapping;
......
        retval = mapping->a_ops->direct_IO(iocb, iter);
    }
......
    retval = generic_file_buffered_read(iocb, iter, retval);
}


ssize_t __generic_file_write_iter(struct kiocb *iocb, struct iov_iter *from)
{
......
    if (iocb->ki_flags & IOCB_DIRECT) {
......
        written = generic_file_direct_write(iocb, from);
......
    } else {
......
		written = generic_perform_write(file, from, iocb->ki_pos);
......
    }
}

generic_file_read_iter和__generic_file_write_iter有相似的邏輯,就是要區分是否用緩存。因此,根據是否使用內存做緩存,我們可以把文件的I/O操作分為兩種類型。

第一種類型是緩存I/O。大多數文件系統的默認I/O操作都是緩存I/O。對於讀操作來講,操作系統會先檢查,內核的緩衝區有沒有需要的數據。如果已經緩存了,那就直接從緩存中返回;否則從磁盤中讀取,然後緩存在操作系統的緩存中。對於寫操作來講,操作系統會先將數據從用戶空間複製到內核空間的緩存中。這時對用戶程序來說,寫操作就已經完成。至於什麼時候再寫到磁盤中由操作系統決定,除非顯式地調用了sync同步命令。

第二種類型是直接IO,就是應用程序直接訪問磁盤數據,而不經過內核緩衝區,從而減少了在內核緩存和用戶程序之間數據複製。

如果在寫的邏輯__generic_file_write_iter裏面,發現設置了IOCB_DIRECT,則調用generic_file_direct_write,裏面同樣會調用address_space的direct_IO的函數,將數據直接寫入硬盤。

帶緩存的寫入操作

我們先來看帶緩存寫入的函數generic_perform_write。

ssize_t generic_perform_write(struct file *file,
				struct iov_iter *i, loff_t pos)
{
	struct address_space *mapping = file->f_mapping;
	const struct address_space_operations *a_ops = mapping->a_ops;
	do {
		struct page *page;
		unsigned long offset;	/* Offset into pagecache page */
		unsigned long bytes;	/* Bytes to write to page */
		status = a_ops->write_begin(file, mapping, pos, bytes, flags,
						&page, &fsdata);
		copied = iov_iter_copy_from_user_atomic(page, i, offset, bytes);
		flush_dcache_page(page);
		status = a_ops->write_end(file, mapping, pos, bytes, copied,
						page, fsdata);
		pos += copied;
		written += copied;


		balance_dirty_pages_ratelimited(mapping);
	} while (iov_iter_count(i));
}

循環中主要做了這幾件事:

  • 對於每一頁,先調用address_space的write_begin做一些準備;
  • 調用iov_iter_copy_from_user_atomic,將寫入的內容從用戶態拷貝到內核態的頁中;
  • 調用address_space的write_end完成寫操作;
  • 調用balance_dirty_pages_ratelimited,看臟頁是否太多,需要寫回硬盤。所謂臟頁,就是寫入到緩存,但是還沒有寫入到硬盤的頁面。

對於第一步,調用的是ext4_write_begin來說,主要做兩件事:

第一做日誌相關的工作。

ext4是一種日誌文件系統,是為了防止突然斷電的時候的數據丟失,引入了日誌(Journal)模式。日誌文件系統比非日誌文件系統多了一個Journal區域。文件在ext4中分兩部分存儲,一部分是文件的元數據,另一部分是數據。元數據和數據的操作日誌Journal也是分開管理的。你可以在掛載ext4的時候,選擇Journal模式。這種模式在將數據寫入文件系統前,必須等待元數據和數據的日誌已經落盤才能發揮作用。這樣性能比較差,但是最安全。

另一種模式是order模式。這個模式不記錄數據的日誌,只記錄元數據的日誌,但是在寫元數據的日誌前,必須先確保數據已經落盤。這個折中,是默認模式。

還有一種模式是writeback,不記錄數據的日誌,僅記錄元數據的日誌,並且不保證數據比元數據先落盤。這個性能最好,但是最不安全。

第二調用grab_cache_page_write_begin來,得到應該寫入的緩存頁。

struct page *grab_cache_page_write_begin(struct address_space *mapping,
					pgoff_t index, unsigned flags)
{
	struct page *page;
	int fgp_flags = FGP_LOCK|FGP_WRITE|FGP_CREAT;
	page = pagecache_get_page(mapping, index, fgp_flags,
			mapping_gfp_mask(mapping));
	if (page)
		wait_for_stable_page(page);
	return page;
}

在內核中,緩存以頁為單位放在內存裏面,每一個打開的文件都有一個struct file結構,每個struct file結構都有一個struct address_space用於關聯文件和內存,就是在這個結構裏面,有一棵樹,用於保存所有與這個文件相關的的緩存頁。

對於第二步,調用iov_iter_copy_from_user_atomic。先將分配好的頁面調用kmap_atomic映射到內核裏面的一個虛擬地址,然後將用戶態的數據拷貝到內核態的頁面的虛擬地址中,調用kunmap_atomic把內核裏面的映射刪除。

size_t iov_iter_copy_from_user_atomic(struct page *page,
		struct iov_iter *i, unsigned long offset, size_t bytes)
{
	char *kaddr = kmap_atomic(page), *p = kaddr + offset;
	iterate_all_kinds(i, bytes, v,
		copyin((p += v.iov_len) - v.iov_len, v.iov_base, v.iov_len),
		memcpy_from_page((p += v.bv_len) - v.bv_len, v.bv_page,
				 v.bv_offset, v.bv_len),
		memcpy((p += v.iov_len) - v.iov_len, v.iov_base, v.iov_len)
	)
	kunmap_atomic(kaddr);
	return bytes;
}

第三步中,調用ext4_write_end完成寫入。這裏面會調用ext4_journal_stop完成日誌的寫入,會調用block_write_end->__block_commit_write->mark_buffer_dirty,將修改過的緩存標記為臟頁。可以看出,其實所謂的完成寫入,並沒有真正寫入硬盤,僅僅是寫入緩存后,標記為臟頁

第四步,調用 balance_dirty_pages_ratelimited,是回寫臟頁。

/**
 * balance_dirty_pages_ratelimited - balance dirty memory state
 * @mapping: address_space which was dirtied
 *
 * Processes which are dirtying memory should call in here once for each page
 * which was newly dirtied.  The function will periodically check the system's
 * dirty state and will initiate writeback if needed.
  */
void balance_dirty_pages_ratelimited(struct address_space *mapping)
{
	struct inode *inode = mapping->host;
	struct backing_dev_info *bdi = inode_to_bdi(inode);
	struct bdi_writeback *wb = NULL;
	int ratelimit;
......
	if (unlikely(current->nr_dirtied >= ratelimit))
		balance_dirty_pages(mapping, wb, current->nr_dirtied);
......
}

在balance_dirty_pages_ratelimited裏面,發現臟頁的數目超過了規定的數目,就調用balance_dirty_pages->wb_start_background_writeback,啟動一個背後線程開始回寫。

另外還有幾種場景也會觸發回寫:

  • 用戶主動調用sync,將緩存刷到硬盤上去,最終會調用wakeup_flusher_threads,同步臟頁;
  • 當內存十分緊張,以至於無法分配頁面的時候,會調用free_more_memory,最終會調用wakeup_flusher_threads,釋放臟頁;
  • 臟頁已經更新了較長時間,時間上超過了設定時間,需要及時回寫,保持內存和磁盤上數據一致性。

帶緩存的讀操作

看帶緩存的讀,對應的是函數generic_file_buffered_read。

static ssize_t generic_file_buffered_read(struct kiocb *iocb,
		struct iov_iter *iter, ssize_t written)
{
	struct file *filp = iocb->ki_filp;
	struct address_space *mapping = filp->f_mapping;
	struct inode *inode = mapping->host;
	for (;;) {
		struct page *page;
		pgoff_t end_index;
		loff_t isize;
		page = find_get_page(mapping, index);
		if (!page) {
			if (iocb->ki_flags & IOCB_NOWAIT)
				goto would_block;
			page_cache_sync_readahead(mapping,
					ra, filp,
					index, last_index - index);
			page = find_get_page(mapping, index);
			if (unlikely(page == NULL))
				goto no_cached_page;
		}
		if (PageReadahead(page)) {
			page_cache_async_readahead(mapping,
					ra, filp, page,
					index, last_index - index);
		}
		/*
		 * Ok, we have the page, and it's up-to-date, so
		 * now we can copy it to user space...
		 */
		ret = copy_page_to_iter(page, offset, nr, iter);
    }
}

在generic_file_buffered_read函數中,我們需要先找到page cache裏面是否有緩存頁。如果沒有找到,不但讀取這一頁,還要進行預讀,這需要在page_cache_sync_readahead函數中實現。預讀完了以後,再試一把查找緩存頁。

如果第一次找緩存頁就找到了,我們還是要判斷,是不是應該繼續預讀;如果需要,就調用page_cache_async_readahead發起一個異步預讀。

最後,copy_page_to_iter會將內容從內核緩存頁拷貝到用戶內存空間。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※教你寫出一流的銷售文案?

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

※回頭車貨運收費標準

※別再煩惱如何寫文案,掌握八大原則!

※超省錢租車方案

※產品缺大量曝光嗎?你需要的是一流包裝設計!

※推薦台中搬家公司優質服務,可到府估價

解讀三組容易混淆的Dockerfile指令

長話短說,今天分享三組容易混淆的Dockerfile指令, 幫助大家編寫更優雅的Dockfile文件、構建更純凈的Docker鏡像。

COPY vs ADD

COPY、ADD主體功能類似:從指定位置拷貝文件到Docker鏡像。

COPY <src>... <dest>
ADD <src>... <dest>

COPY 接收src、dest參數,只允許從Docker Engine主機上拷貝文件到Docker鏡像;
ADD也能完成以上工作,但是ADD支持另外兩種src:

  1. 文件源可以是URL
  2. 可以從src直接解壓tar文件到目的地
ADD http://foo.com/bar.go /tmp/main.go
# 從指定地址下載文件,添加到鏡像文件系統的/tmp/main.go位置
ADD http://foo.com/bar.go /tmp/
# 因為以/結尾,將會引用url中的文件名添加到指定的目錄下


ADD /foo.tar.gz /tmp/
# 自動解壓主機文件到指定目錄

有趣的是,URL下載和自動解壓功能不能同時生效: 任何通過URL下載的壓縮包文件不會自動解壓。

  • 如果拷貝本地文件到鏡像,通常使用COPY,因為含義更明確
  • ADD支持URL文件、自動解壓到指定目錄,這2個特性也很棒

ARG vs ENV

ARG、ENV也讓人很疑惑的,都是Dockerfile中定義變量的指令。

ARG用於鏡像構建階段,ENV用於將來運行的容器

  • 生成鏡像后,ARG值不可用,正在運行的容器將無法訪問ARG變量值。
ARG  VAR_NAME 5
# 構建鏡像時,可提供--build-arg  VAR_NAME=6修改ARG值。
  • ENV主要是為容器環境變量提供默認值,正在運行的容器可訪問環境變量(這是將配置傳遞給應用的好方法):
ENV VAR_NAME_2 6
# 啟動容器時,可通過docker run -e "VAR_NAME_2=7"或docker-compose.yml提供新的環境變量值來覆蓋Dockerfile中設置的ENV值。

一個小技巧: 構建鏡像時不能使用命令行參數重寫ENV,但是你可以使用ARG動態為ENV設置默認值:

# You can set VAR_A while building the image or leave it at the default
ARG VAR_A 5
# VAR_B gets the (overridden) value of VAR_A
ENV VAR_B $VAR_A

RUN vs ENTRYPOINT vs CMD

  1. RUN 在新層中執行命令併產生新鏡像,主要用於安裝新軟件包。
  2. ENTRYPOINT 執行程序的啟動命令,當您想將容器作為可執行文件運行時使用。
  3. CMD和ENTRYPOINT 都可以提供程序的啟動命令;CMD另外一個作用是為執行中的容器提供默認值
  • CMD [“executable”,”param1″,”param2″] (可執行形式,最常見)
  • CMD command param1 param2 (腳本形式)
CMD echo "Hello world"
# run -it <image> 輸出 Hello world

但是當容器以命令啟動,docker run -it /bin/bash, CMD命令會被忽略,bash解析器將會運行:root@98e4bed87725:/#

  • CMD [“param1″,”param2”] (作為ENTRYPOINT指令默認值,此時必須提供ENTRYPOINT指令,且ENTRYPOINT也必須以Json Array形式)
ENTRYPOINT ["/bin/echo", "Hello"]  
CMD ["world"]  

# run -it <image> 將會輸出 Hello world;
# run -it <image> earth 將會輸出 Hello earth

當打算構建一個可執行的且常駐的鏡像,最好選用ENTRYPOINT;
如果需要提供默認命令參數(可在容器運行時從命令行覆蓋),請選擇CMD

Reference

  • https://www.ctl.io/developers/blog/post/dockerfile-add-vs-copy/
  • https://vsupalov.com/docker-arg-vs-env/
  • https://aboullaite.me/dockerfile-run-vs-cmd-vs-entrypoint/

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※超省錢租車方案

※別再煩惱如何寫文案,掌握八大原則!

※回頭車貨運收費標準

※教你寫出一流的銷售文案?

※產品缺大量曝光嗎?你需要的是一流包裝設計!

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

網頁設計最專業,超強功能平台可客製化

淺談鏈接器

目錄

  • 編譯過程簡介
  • 什麼是鏈接器?
  • 鏈接器可操作的元素:目標文件
  • 符號表(Symbol table)
    • 符號決議
  • 庫與可執行文件
    • 靜態庫
    • 動態庫
  • 參考
  • 微信公共號

編譯過程簡介

C語言的編譯過程由五個階段組成:

  • 步驟1:預處理:主要是處理以#開頭的語句,主要工作如下:1)將#include包含的頭文件直接拷貝到.c文件中;2)將#define定義的宏進行替換;3)處理條件編譯指令#ifdef;4)將代碼中的註釋刪除;5)添加行號和文件標示,這樣的在調試和編譯出錯的時候才知道是是哪個文件的哪一行 ;6)保留#pragma編譯器指令,因為編譯器需要使用它們。
gcc -E helloworld.c -o helloworld_pre.c
  • 步驟2: 編譯:將C語言翻譯成彙編,主要工作如下:1)詞法分析;2)語法分析;3)語義分析 4)優化後生成相應的彙編;
gcc -S helloworld.c -o helloworld.s
  • 步驟3: 彙編:將上一步的彙編代碼轉換成機器碼(machine code),這一步產生的文件叫做目標文件;
gcc -c helloworld.c -o helloworld.o
  • 步驟4:鏈接:將多個目標文以及所需的庫文件(.so等)鏈接成最終的可執行文件(executable file)。
gcc helloworld.c -o helloworld

什麼是鏈接器?

鏈接器是一個將編譯器產生的目標文件打包成可執行文件或者庫文件或者目標文件的程序。

鏈接器的作用有點類似於我們經常使用的壓縮軟WinRAR(Linux下是tar),壓縮軟件將一堆文件打包壓縮成一個壓縮文件,而鏈接器和壓縮軟件的區別在於鏈接器是將多個目標文件打包成一個文件而不進行壓縮。

寫C或者C++的u同學經常遇到這樣一個錯誤:

undefined reference to function ABC.

鏈接器可操作的元素:目標文件

鏈接器可操作的最小元素是一個簡單的目標文件
從廣義上來講,目標文件與可執行文件的格式幾乎是一模一樣的,在Linux下,我們把它們統稱為ELF文件。

ELF文件標準裏面把系統中採用ELF格式的文件歸為以下四類:

  • 可重定位文件(Relocatable File):Linux的.o文件,這類文件包含了代碼和數據,可以被用來鏈接成可執行文件或共享目標文件,靜態鏈接庫也歸屬於這一類;

  • 可執行文件(Executable File):比如bin/bash文件,這類文件包含了可以直接執行的程序,它的代表就是ELF文件,他們一般都沒有擴展名;

  • 共享目標文件(shared Object File): 比如Linux的.so文件,這種文件包含了代碼和數據,可以在以下兩種情況下使用,一種是鏈接器可以直接使用這種文件跟其他的可重定位文件和共享目標文件鏈接,產生新的目標文件。第二種是動態鏈接器可以將幾個這樣的共享目標文件與可執行文件結合,作為進程映射的一部分來運行。

  • 核心轉儲文件(Core Dump File): Linux下面的core dump,當進程意外終止時,系統可以將該進程的地址空間的內容及終止時的一些其他信息轉儲到核心轉儲文件中。

符號表(Symbol table)

編譯器在遇到外部定義的全局變量或者函數時只要能在當前文件找到其聲明,編譯器就認為編譯正確。而尋找使用變量定義的這項任務就被留給了鏈接器。鏈接器的其中一項任務就是要確定所使用的變量要有其唯一的定義。雖然編譯器給鏈接器留了一項任務,但為了讓鏈接器工作的輕鬆一點編譯器還是多做了一點工作的,這部分工作就是符號表(Symbol table)。

符號表中保存的信息有兩個部分:

  • 該目標文件中引用的全局變量以及函數;
  • 該目標文件中定義的全局變量以及函數。

編譯器在編譯過程中每次遇到一個全局變量或者函數名都會在符號表中添加一項,最終編譯器會統計一張符號表。

假設C語言源碼如下:

// 定義未初始化的全局變量
int g_x_uninit;

// 定義初始化的全局變量
int g_x_init = 1;

// 定義未初始化的全局私有變量,只能在當前文件中使用
static int g_y_uninit;

// 定義初始化的全局私有變量
static int g_y_init = 2;

// 聲明全局變量,該變量的定義在其它文件
extern int g_z;

// 函數聲明,該函數的定義在其它文件
int fn_a(int x, int y);

// 私有函數定義,該函數只能在當前文件中使用
static int fn_b(int x)
{
    return x + 1;
}

// 函數定義
int fn_c(int local_x)
{
    int local_y_uninit;
    int local_y_init = 3;
    // 對全局變量,局部變量以及函數的使用
    g_x_uninit = fn_a(local_x, g_x_init);
    g_y_uninit = fn_a(local_x, local_y_init);
    local_y_uninit += fn_b(g_z);
    return (g_y_uninit + local_y_uninit);
}

編譯器將為此文件統計出如下一張符號表:

名字 類型 是否可被外部引用 區域
g_z 引用,未定義
fn_a 引用,未定義
fn_b 定義 代碼段
fn_c 定義 代碼段
g_x_init 定義 數據段
g_y_uninit 定義 數據段
g_x_uninit 定義 數據段
g_y_init 定義 數據段

g_z以及fn_a是未定義的,因為在當前文件中,這兩個變量僅僅是聲明,編譯器並沒有找到其定義。剩餘的變量編譯器都可以在當前文件中找到其定義。

本質上整個符號表主要表達兩件事:1)我能提供給其它文件使用的符號; 2)我需要其它文件提供給我使用的符號。

目標文件
數據段
代碼段
符號表

符號決議

有了符號表,鏈接器就可以進行符號決議了。如圖所示,假設鏈接器需要鏈接三個目標文件,如下:

鏈接器會依次掃描每一個給定的目標文件,同時鏈接器還維護了兩個集合,一個是已定義符號集合D,另一個是未定義符合集合U,下面是鏈接器進行符合決議的過程:

  • 對於當前目標文件,查找其符號表,並將已定義的符號並添加到已定義符號集合D中。
  • 對於當前目標文件,查找其符號表,將每一個當前目標文件引用的符號與已定義符號集合D進行對比,如果該符號不在集合D中則將其添加到未定義符合集合U中。
  • 當所有文件都掃描完成后,如果為定義符號集合U不為空,則說明當前輸入的目標文件集合中有未定義錯誤,鏈接器報錯,整個編譯過程終止。

鏈接過程中,只要每個目標文件所引用變量都能在其它目標文件中找到唯一的定義,整個鏈接過程就是正確的。

若鏈接器在查找了所有目標文件的符號表后都沒有找到函數,因此鏈接器停止工作並報出錯誤undefined reference to function A

庫與可執行文件

鏈接器根據目標文件構建出庫(動態庫、靜態庫)或可執行文件。

給定目標文件以及鏈接選項,鏈接器可以生成兩種庫,分別是靜態庫以及動態庫,如下圖所示,給定同樣的目標文件,鏈接器可以生成兩種不同類型的庫。

靜態庫

靜態庫在Windows下是以.lib為後綴的文件,Linux下是以.a為後綴的文件。

靜態庫是鏈接器通過靜態鏈接將其和其它目標文件合併生成可執行文件的,而靜態庫只不過是將多個目標文件進行了打包,在鏈接時只取靜態庫中所用到的目標文件。

目標文件分為三段:代碼段、數據段、符號表,在靜態鏈接時可執行文件的生成過程如下圖所示:

可執行文件的特點如下:

  • 可執行文件和目標文件一樣,也是由代碼段和數據段組成。
  • 每個目標文件中的數據段都合併到了可執行文件的數據段,每個目標文件當中的代碼段都合併到了可執行文件的代碼段。
  • 目標文件當中的符號表並沒有合併到可執行文件當中,因為可執行文件不需要這些字段。

可執行文件和目標文件沒有什麼本質的不同,可執行文件區別於目標文件的地方在於,可執行文件有一個入口函數,這個函數也就是我們在C語言當中定義的main函數,main函數在執行過程中會用到所有可執行文件當中的代碼和數據。main函數是被操作系統調用。

動態庫

靜態庫在編譯鏈接期間就被打包copy到了可執行文件,也就是說靜態庫其實是在編譯期間(Compile time)鏈接使用的。
動態鏈接可以在兩種情況下被鏈接使用,分別是加載時動態鏈接(load-time dynamic linking)運行時動態鏈接 (run-time dynamic linking)

  • 加載時動態鏈接:在這裏我們只需要簡單的把加載理解為程序從磁盤複製到內存的過程,加載時動態鏈接就出現在這個過程。操作系統會查找可執行文件依賴的動態庫信息(主要是動態庫的名字以及存放路徑),找到該動態庫后就將該動態庫從磁盤搬到內存,並進行符號決議,如果這個過程沒有問題,那麼一切準備工作就緒,程序就可以開始執行了,如果找不到相應的動態庫或者符號決議失敗,那麼會有相應的錯誤信息報告為用戶,程序運行失敗。

  • 運行時動態鏈接:run-time dynamic linking 運行時動態鏈接則不需要在編譯鏈接時提供動態庫信息,也就是說,在可執行文件被啟動運行之前,可執行文件對所依賴的動態庫信息一無所知,只有當程序運行到需要調用動態庫所提供的代碼時才會啟動動態鏈接過程。

可以使用特定的API來運行時加載動態庫,在Windows下通過LoadLibrary或者LoadLibraryEx,在Linux下通過使用dlopen、dlsym、dlclose這樣一組函數在運行時鏈接動態庫。當這些API被調用后,同樣是首先去找這些動態庫,將其從磁盤copy到內存,然後查找程序依賴的函數是否在動態庫中定義。這些過程完成后動態庫中的代碼就可以被正常使用了。

在動態鏈接下,可執行文件當中會新增兩段,即dynamic段以及GOT(Global offset table)段,這兩段內容就是是我們之前所說的必要信息。

dynamic 段中保存了可執行文件依賴哪些動態庫,動態鏈接符號表的位置以及重定位表的位置等信息。
當加載可執行文件時,操作系統根據dynamic段中的信息即可找到使用的動態庫,從而完成動態鏈接

參考

  • C語言編譯的4大過程詳解
  • C語言編程透視
  • 徹底理解鏈接器:二,符號決議

微信公共號

NFVschool,關注最前沿的網絡技術。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計最專業,超強功能平台可客製化

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

※回頭車貨運收費標準

※推薦評價好的iphone維修中心

※教你寫出一流的銷售文案?

台中搬家公司教你幾個打包小技巧,輕鬆整理裝箱!

台中搬家公司費用怎麼算?

記一次Docker中Redis連接暴增的問題排查

周六生產服務器出現redis服務器不可用狀態,錯誤信息為:

狀態不可用,等待後台檢查程序恢復方可使用。Unexpected end of stream; expected type ‘Status’

如下圖所示,下圖6300就是我們redis服務器運行的端口。

 

頭一次碰到此類問題,心想難道是redis掛掉了,隨即通過telnet ip+端口。發現運行正常,然後就想着進入redis看下目前連接情況。一看發現竟然高達1903條這麼多。

然後想着應該是代碼創建redis連接過多導致的,查看代碼。

發現redis創建只有這一個地方有,這裏也是服務註冊時才執行。也就是應用程序啟動時才被執行一次。然後整個項目查找,沒有其他地方再有調用redis初始化。

心有不甘,難道是每次在redis讀寫數據時都會創建連接嗎?會和讀寫頻繁有關係嗎?總感覺不會啊,隨即創建測試代碼進行測試一番。

在本地搭建了一個redis環境,測試之前先看看接數多少,目前看只有1個,也就是目前的cmd連接客戶端,這個屬於正常的了。

開始測試,運行程序。代碼是創建一個連接對象,並一共測試1000次寫,和1000次讀。

 

不管我怎麼測試連接都是6個,那麼也就是說我們程序最多創建了5個連接,當然主要有線程池在裏面。

 

所以基本的存儲讀取這塊代碼肯定是沒問題。

但代碼這塊也沒算完全放棄排查,因為生產服務器通過docker運行着大約6個應用程序。都是連接的同一個redis,會不會是其他應用程序導致的?

然後就想直接通過redis 連接列表裡的中隨便一個端口來查詢對應的進程信息就可以知道是哪些應用程序了。

Linux 中通過查詢網絡端口號显示進程信息。

netstat -atunlp | grep 60852 

首先看這端口對應的IP,比如這裏第一個是172.17.0.1。熟悉docker的同學應該知道這個ip是docker網關IP。我們容器中的程序都是通過這個網關IP來和我們宿主主機來通訊的。我們通過ifconfig就能發現docker這個網關IP,第二個172.17.0.3:6379這個一看就是redis的容器IP,

這樣一看確實無法找到具體對應哪個容器中的程序和我們建立連接的。

有一個最笨的辦法就是挨個進入容器裏面。即docker exec –it test /bin/bash 然後查看當前容器的網絡連接情況。這樣非常麻煩,並且需要安裝很多組件才能執行一系列命令。

另外一個辦法lsof命令,如果沒有則需要安裝。我們可以通過進程去找所有網絡連接情況。

比如我們剛發現我們的進程主要是docker,他的pid是582251。

lsof -i |grep 582251或者 lsof -i -p 582251

結果如下圖,右邊其實出現了具體IP,這個IP就是docker容器具體的IP地址。

 

現在知道所有IP和端口了,我們將命令執行結果下載下來。

首先找到自己每個容器對應的IP。

docker inspect name |grep IPAddress    //name 容器名稱或者id

 

找到每個ip后然後根據剛下載的所有網絡連接信息進行統計,看哪個IP連接最多,最多的一個肯定有問題。

然後我就找到這個IP對應的容器部署的程序,然後看redis配置。發現線程池設為200。

另外我通過github,發現CSRedisCore還有個預熱機制,也就是preheat,他默認值就是5個預熱連接。

我們線程池設置的是200加上本身有個預熱機制5個連接,我不知道是不是會創建200*5=1000個。這個有時間再好好研究下源代碼,目前只是猜測。

我現在已經將redis修改為poolsize=5, preheat=false。線程池5個,並且關閉預熱機制。

 

修改我們連接配置,並重啟應用服務器和redis服務器(為了徹底清除已建立的連接)后發現連接數有減少,但沒有很多。後來查詢發現,是redis的idle空閑時長太長,導致連接池維持太多連接,沒有被釋放。

我們設置下超時為30s

執行CONFIG SET timeout 30 (單位是秒,此種方式只是臨時修改,針對當前運行有效。長效記得修改redis配置文件)

然後再看下連接數多少,這樣一下子就減少了很多。

總結:

1、 redis連接暴增,首先從自身應用程序出發去尋找問題,比如我這邊發現的連接池設置過大,加上默認的預熱機制等。還有盡可能的看代碼層面在創建連接是否會被多次觸發,如果有就必須要改正。現在都是通過注入的方式創建實例,要看該地方是存在被多次調用。

2、修改redis服務器配置,比如連接空閑超時時間。包括也可也看下最大連接數多少,默認值。

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※回頭車貨運收費標準

※產品缺大量曝光嗎?你需要的是一流包裝設計!

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

※推薦評價好的iphone維修中心

※教你寫出一流的銷售文案?

台中搬家公司教你幾個打包小技巧,輕鬆整理裝箱!

台中搬家遵守搬運三大原則,讓您的家具不再被破壞!

2020第一季史上第二熱 全年溫度預測、海面上升趨勢一次看

環境資訊中心綜合外電;姜唯 編譯;林大利 審校

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

台北網頁設計公司這麼多該如何選擇?

※智慧手機時代的來臨,RWD網頁設計為架站首選

※評比南投搬家公司費用收費行情懶人包大公開

※回頭車貨運收費標準

網頁設計最專業,超強功能平台可客製化

※別再煩惱如何寫文案,掌握八大原則!