痞子衡嵌入式:利用i.MXRT1xxx系列ROM提供的FlexSPI driver API可輕鬆IAP

  大家好,我是痞子衡,是正經搞技術的痞子。今天痞子衡給大家介紹的是i.MXRT系列ROM中的FlexSPI驅動API實現IAP

  痞子衡的技術交流群里經常有群友提問: i.MXRT中的FlexSPI驅動API到底怎麼用啊?這個問題已經出現過好幾次了,本來痞子衡不打算專門為這個寫文章的,因為這部分內容在芯片手冊System Boot章節里的最後一節ROM APIs里其實介紹得非常詳細了,但是既然還是有不少朋友在問這個,看起來手冊里的內容藏得有點深,這麼好的東西被埋沒太可惜了,那麼今天痞子衡就跟大家再認真聊一聊。

一、ROM API簡介

1.1、API產生背景

  i.MXRT系列都是Flashless(沒有內置NVM)的芯片,所以BootROM必不可少。BootROM是個很特殊的東西,本質上它是一個完整的C代碼寫成的系統級App,這個系統級App專門用於從外部存儲器中加載用戶級App執行。簡單地說,BootROM就是PC機里的BIOS。

  BootROM代碼是存放在專門的ROM區域的(前面講i.MXRT系列沒有內置NVM,其實不夠準確,其實是有內部ROM空間的,只不過這個ROM區域用戶無法下載程序使用,因此等效於沒有NVM),ROM顧名思義Readonly,所以BootROM代碼只能隨着芯片一起Tapeout,代碼無法更改(其實也有ROM patch機制,以後再介紹)。

  ROM空間其實挺大的,從64KB到512KB不等,因芯片啟動功能複雜程度而異。下圖是i.MXRT1050系列的BootROM所佔空間,ROM起始地址是0x200000(起始地址在i.MXRT上都一樣),ROM大小為96KB(這是標準啟動功能所要的代碼長度。在i.MXRT1010上是64KB – 精簡啟動功能,在i.MXRT1170上是256KB – 複雜啟動功能)。

  BootROM代碼其實並沒有佔滿全部ROM空間,總有些剩餘空間(因為工藝原因,ROM空間都是8/16KB倍數),這部分空間浪費了着實可惜。如果我們能把SDK里的一些常用模塊驅動(比如WDOG)順便放進去供用戶調用,既充分利用ROM空間,也為用戶節省Flash空間,豈不是一舉兩得。此外,BootROM功能代碼中也有一些現成模塊驅動(比如各種啟動設備存儲器驅動接口)可以一併導出,這便是API由來。

1.2、API設計實現

  有了API想法,現在就是設計實現了。其實i.MXRT ROM API設計並不是重頭開始的,在這個MCU系列被主推之前,Kinetis系列也曾當紅過,Kinetis中也內置了ROM,並且提供了ROM API,痞子衡之前為此寫過一篇文章 《飛思卡爾Kinetis系列MCU啟動那些事(11)- KBOOT特性(ROM API)》。 i.MXRT ROM API設計思路完全復用了Kinetis ROM API的設計。

  API說到底就是一個個功能函數的結合,我們知道工程代碼都是由鏈接器自動分配的,因此每個函數實際鏈接地址是無法預期的(在鏈接文件里給每個函數分配固定地址鏈接這種方法不在考慮範疇,當函數數量眾多時,這種方法太麻煩),業界上一個比較通用的做法是定義成員是函數指針的結構體,i.MXRT ROM API就是採用的業界通用方式,下面bootloader_api_entry_t便是i.MXRT1060中API原型,g_bootloaderTree就是實例:

typedef struct
{
    const uint32_t version;
    const char *copyright;
    void (*runBootloader)(void *arg);
    const hab_rvt_t *habDriver;

    //!< FlexSPI NOR Flash API
    const flexspi_nor_driver_interface_t *flexSpiNorDriver;

    const nand_ecc_driver_interface_t *nandEccDriver;
    const clock_driver_interface_t *clockDriver;
    const rtwdog_driver_interface_t *rtwdogDriver;
    const wdog_driver_interface_t *wdogDriver;
    const stdlib_driver_interface_t *stdlibDriver;
} bootloader_api_entry_t;

// Bootloader API Tree
const bootloader_api_entry_t g_bootloaderTree = {
    .copyright = "Copyright 2018 NXP",
    .version = MAKE_VERSION(1, 0, 0),
    .runBootloader = run_bootloader,
    .habDriver = &hab_rvt,

    .flexSpiNorDriver = &g_flexspiNorDriverInterface,

    .nandEccDriver = &g_nandEccDriverInterface,
    .clockDriver = &g_clockDriverInterface,
    .rtwdogDriver = &g_rtwdogDriverInterface,
    .wdogDriver = &g_wdogDriverInterface,
    .stdlibDriver = &g_stdlibDriverInterface,
};

  從上面代碼我們可以看出,bootloader_api_entry_t成員好像並不是函數指針,是的,為了分組方便,bootloader_api_entry_t成員還是一個個結構體,它的這些結構體成員(比如flexspi_nor_driver_interface_t)才是真正包含一個個函數指針的結構體。API從功能來分一共提供了7類:HAB、FlexSPI NOR、NAND ECC、Clock、RT-WDOG、WDOG、stdlib。

  設計到這裏,我們通過g_bootloaderTree結構體常量就可以調用所有的API函數了,最後剩下的問題就是如何在ROM里找一個確定的地方保存隨機鏈接的g_bootloaderTree地址(只要4字節即可)。是的,還是Kinetis ROM API用的那個巧妙的方法,下面是BootROM工程的startup文件(Keil版),BootROM將g_bootloaderTree的地址放到了中斷向量表第8個向量的位置處(該向量為ARM Cortex-M未定義的系統向量),因此0x20001c處開始的4bytes便固定是g_bootloaderTree地址。

                PRESERVE8
                THUMB

; Vector Table Mapped to Address 0 at Reset

                AREA    RESET, DATA, READONLY
                EXPORT  __Vectors
                EXPORT  __Vectors_End
                EXPORT  __Vectors_Size
                IMPORT  |Image$$ARM_LIB_STACK$$ZI$$Limit|
                IMPORT  g_bootloaderTree

__Vectors       DCD     |Image$$ARM_LIB_STACK$$ZI$$Limit|
                DCD     Reset_Handler
                DCD     DefaultISR
                DCD     HardFault_Handler
                DCD     DefaultISR
                DCD     DefaultISR
                DCD     DefaultISR
                DCD     g_bootloaderTree
                DCD     0
                DCD     0
                DCD     0
                DCD     SVC_Handler
                DCD     DefaultISR
                DCD     0
                DCD     DefaultISR
                DCD     DefaultISR
		        ;; ...

1.3、API調用方法

  了解了前面介紹的ROM API產生背景與設計實現,它的調用方法就非常簡單了,以WDOG API調用為例,只需要如下簡單3句代碼:

// 找到API根結構體
#define g_bootloaderTree (*(bootloader_api_entry_t **)0x0020001c)
// 定義WDOG模塊配置變量
wdog_config_t config;
// 調用API中WDOG_Init()
g_bootloaderTree->wdogDriver->WDOG_Init(WDOG1, config);

1.4、支持API的i.MXRT型號

  截止目前,i.MXRT1xxx系列一共出了7款型號,但並不是每個型號都開放了ROM API,最早誕生的三款型號(105x、1021、1015)就並沒有開放API(不是沒有API,而是沒有嚴格測試),其餘型號都支持API。

RT芯片型號 是否支持ROM API
i.MXRT117x 支持
i.MXRT1064 支持
i.MXRT106x 支持
i.MXRT105x 未開放
i.MXRT1021 未開放
i.MXRT1015 未開放
i.MXRT1011 支持

二、API之FlexSPI驅動

  前面鋪墊了太多ROM API設計細節,到這裏才算進入正題,本文其實主要是要跟大家聊如何利用API里的FlexSPI NOR驅動實現IAP。痞子衡在前面鋪墊那麼多的原因其實主要是想告訴大家,API里的每個驅動都是經過完善測試的,尤其是這個FlexSPI NOR驅動,更是經過了千錘百鍊,無論是易用性、運行穩定性還是Flash型號的支持度上都是首屈一指的。

  對於JESD216標準下的串行SPI接口Flash驅動,大家知道更多的可能是RT-Thread技術總監朱天龍大神的開源 SFUD 項目,但痞子衡告訴你,i.MXRT ROM API里的這個串行Flash驅動也毫不遜色(持續維護與優化了近6年,歷經多款MCU的ROM,是真正的產品級),只是不如開源項目那麼知名,不過它的源代碼也是開源在SDK里的(\SDK\middleware\mcu-boot\src\drivers\flexspi_nor),BSD-3-Clause許可證。

2.1 FlexSPI驅動原型

  flexspi_nor_driver_interface_t便是FlexSPI NOR驅動的原型,尋常的讀寫擦功能自然不在話下,除此以外,API裏面還有一個非常厲害的xfer()函數,這個函數可以用來實現其他定製化的Flash操作函數,有興趣的朋友可以進一步去研究。

typedef struct
{
    uint32_t version;
    status_t (*init)(uint32_t instance, flexspi_nor_config_t *config);
    status_t (*program)(uint32_t instance, flexspi_nor_config_t *config, uint32_t dst_addr, const uint32_t *src);
    status_t (*erase_all)(uint32_t instance, flexspi_nor_config_t *config);
    status_t (*erase)(uint32_t instance, flexspi_nor_config_t *config, uint32_t start, uint32_t lengthInBytes);
    status_t (*read)(uint32_t instance, flexspi_nor_config_t *config, uint32_t *dst, uint32_t addr, uint32_t lengthInBytes);
    void (*clear_cache)(uint32_t instance);
    status_t (*xfer)(uint32_t instance, flexspi_xfer_t *xfer);
    status_t (*update_lut)(uint32_t instance, uint32_t seqIndex, const uint32_t *lutBase, uint32_t seqNumber);
    status_t (*get_config)(uint32_t instance, flexspi_nor_config_t *config, serial_nor_config_option_t *option);
} flexspi_nor_driver_interface_t;

2.2 FlexSPI驅動使用示例

  FlexSPI驅動使用基本三步走,先調用get_config()獲取完整FlexSPI模塊配置,然後調用init()函數去初始化FlexSPI以及訪問Flash獲取SFDP表信息,最後就是調用Flash操作函數(比如erase())。

// 找到API根結構體
#define g_bootloaderTree (*(bootloader_api_entry_t **)0x0020001c)

// 定義FlexSPI, Flash配置變量
flexspi_nor_config_t config;
serial_nor_config_option_t option;
option.option0.U = 0xC0000008; // QuadSPI NOR, Frequency: 133MHz
uint32_t instance = 0;

// 調用API中get_config()函數
g_bootloaderTree->flexSpiNorDriver->get_config(instance, &config, &option);
// 調用API中init()函數
g_bootloaderTree->flexSpiNorDriver->init(instance, &config);
// 調用API中erase()函數
g_bootloaderTree->flexSpiNorDriver->erase(instance, &config, 0x40000, 0x1000);

2.3 FlexSPI驅動特點

  因為FlexSPI NOR驅動API來自於BootROM,因此其在使用上有一些小小的限制,也算是其特點吧。FlexSPI驅動API里並沒有提供Flash連接的Pinmux配置,其Pinmux配置已經寫死在init()函數中,就是ROM支持啟動的FlexSPI PORTA上的那些pin(片選是SS0)。

  在上面的使用示例代碼中,你會看到option.option0.U = 0xC0000008代碼,這算是FlexSPI驅動最大的特點了,這是一個簡化的option配置word(其原型可在芯片手冊里找到),通過這個簡化的option,用戶可以輕鬆配置來訪問不同廠商的Flash,下面是常用的Flash模式配置值。

• QuadSPI NOR - Quad SDR Read: option0 = 0xc0000008 (133MHz)
• QuadSPI NOR - Quad DDR Read: option0 = 0xc0100003 (60MHz)
• HyperFLASH 1V8: option0 = 0xc0233009 (166MHz)
• HyperFLASH 3V0: option0 = 0xc0333006 (100MHz)
• MXIC OPI DDR (OPI DDR enabled by default): option=0xc0433008(133MHz)
• Micron Octal DDR: option0=0xc0600006 (100MHz)
• Micron OPI DDR: option0=0xc0603008 (133MHz), SPI->OPI DDR
• Micron OPI DDR (DDR read enabled by default): option0 = 0xc0633008 (133MHz)
• Adesto OPI DDR: option0=0xc0803008(133MHz)

2.4 FlexSPI驅動用作IAP

  IAP其實就是在App中實現Flash擦寫,單純從技術上來說並不是一個很難的東西。但i.MXRT上很多時候App代碼本身也在同一片Flash里執行(也叫XIP),而市面上很多Flash都是不支持RWW(Read-While-Write)的,這就導致一個問題,當你調用Flash操作函數去擦寫Flash時,CPU又需要繼續去Flash獲取指令,違反了RWW,因此你只能把Flash相關操作函數全部放在RAM中去執行(這涉及分散加載了,對於初級嵌入式用戶來說稍微有點難)。

  現在我們有了ROM API,FlexSPI驅動代碼體全部都在ROM空間里,並不佔用Flash空間,因此不存在RWW問題,真是天然為IAP而生,再也不用再管什麼分散加載這麼麻煩的事了。

三、FlexSPI API業界應用

  最後再介紹一下i.MXRT FlexSPI API在業界的應用,這個API其實並不小眾,目前已被主流IDE和調試工具用作i.MXRT Flash下載算法。

3.1 用於IAR下載算法

  如果你的IAR版本夠新,能夠支持i.MXRT1060等型號,隨便打開一個i.MXRT1060 SDK工程,在工程Option里找到Debugger,然後進入Flashloader配置,你會看到頁面里有Extra parameters一欄,在下面的解釋里有這個參數的示例,它就是前面2.3節里介紹的option0。有了這種方式設計的Flash下載算法,你再也不用手動更新下載算法文件去支持不同的Flash了,改參數就行了。

3.2 用於J-Link下載算法

  目前最新的Jlink驅動里的下載算法也是基於ROM API的,痞子衡有一個開源項目,收集了i.MXRT所有型號的下載算法源代碼工程,其中jlink算法是最全的,其他IDE算法還在陸續完善中。

https://github.com/JayHeng/imxrt-tool-flash-algo

  至此,i.MXRT系列ROM中的FlexSPI驅動API實現IAP痞子衡便介紹完畢了,掌聲在哪裡~~~

歡迎訂閱

文章會同時發布到我的 博客園主頁、CSDN主頁、微信公眾號 平台上。

微信搜索”痞子衡嵌入式“或者掃描下面二維碼,就可以在手機上第一時間看了哦。

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

【其他文章推薦】

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

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

※台北網頁設計公司全省服務真心推薦

※想知道最厲害的網頁設計公司"嚨底家"!

新北清潔公司,居家、辦公、裝潢細清專業服務

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

寫給大忙人看的死鎖全詳解

前言

計算機系統中有很多獨佔性的資源,在同一時刻只能每個資源只能由一個進程使用,我們之前經常提到過打印機,這就是一個獨佔性的資源,同一時刻不能有兩個打印機同時輸出結果,否則會引起文件系統的癱瘓。所以,操作系統具有授權一個進程單獨訪問資源的能力。

兩個進程獨佔性的訪問某個資源,從而等待另外一個資源的執行結果,會導致兩個進程都被阻塞,並且兩個進程都不會釋放各自的資源,這種情況就是 死鎖(deadlock)

死鎖可以發生在任何層面,在不同的機器之間可能會發生死鎖,在數據庫系統中也會導致死鎖,比如進程 A 對記錄 R1 加鎖,進程 B 對記錄 R2 加鎖,然後進程 A 和 B 都試圖把對象的記錄加鎖,這種情況下就會產生死鎖。

下面我們就來討論一下什麼是死鎖、死鎖的條件是什麼、死鎖如何預防、活鎖是什麼等。

首先你需要先了解一個概念,那就是資源是什麼

資源

大部分的死鎖都和資源有關,在進程對設備、文件具有獨佔性(排他性)時會產生死鎖。我們把這類需要排他性使用的對象稱為資源(resource)。資源主要分為 可搶佔資源和不可搶佔資源

可搶佔資源和不可搶佔資源

資源主要有可搶佔資源和不可搶佔資源。可搶佔資源(preemptable resource) 可以從擁有它的進程中搶佔而不會造成其他影響,內存就是一種可搶佔性資源,任何進程都能夠搶先獲得內存的使用權。

不可搶佔資源(nonpreemtable resource) 指的是除非引起錯誤或者異常,否則進程無法搶佔指定資源,這種不可搶佔的資源比如有光盤,在進程執行調度的過程中,其他進程是不能得到該資源的。

死鎖與不可搶佔資源有關,雖然搶佔式資源也會造成死鎖,不過這種情況的解決辦法通常是在進程之間重新分配資源來化解。所以,我們的重點自然就會放在了不可搶佔資源上。

下面給出了使用資源所需事件的抽象順序

如果在請求時資源不存在,請求進程就會強制等待。在某些操作系統中,當請求資源失敗時進程會自動阻塞,當自資源可以獲取時進程會自動喚醒。在另外一些操作系統中,請求資源失敗並显示錯誤代碼,然後等待進程等待一會兒再繼續重試。

請求資源失敗的進程會陷入一種請求資源、休眠、再請求資源的循環中。此類進程雖然沒有阻塞,但是處於從目的和結果考慮,這類進程和阻塞差不多,因為這類進程並沒有做任何有用的工作。

請求資源的這個過程是很依賴操作系統的。在一些系統中,一個 request 系統調用用來允許進程訪問資源。在一些系統中,操作系統對資源的認知是它是一種特殊文件,在任何同一時刻只能被一個進程打開和佔用。資源通過 open 命令進行打開。如果文件已經正在使用,那麼這個調用者會阻塞直到當前的佔用文件的進程關閉文件為止。

資源獲取

對於一些數據庫系統中的記錄這類資源來說,應該由用戶進程來對其進行管理。有一種管理方式是使用信號量(semaphore) 。這些信號量會初始化為 1 。互斥鎖也能夠起到相同的作用。

這裏說一下什麼是互斥鎖(Mutexes):

在計算機程序中,互斥對象(mutex) 是一個程序對象,它允許多個程序共享同一資源,例如文件訪問權限,但並不是同時訪問。需要鎖定資源的線程都必須在使用資源時將互斥鎖與其他線程綁定(進行加鎖)。當不再需要數據或線程結束時,互斥鎖設置為解鎖。

下面是一個偽代碼,這部分代碼說明了信號量的資源獲取、資源釋放等操作,如下所示

typedef int semaphore;
semaphore aResource;

void processA(void){
  
  down(&aResource);
	useResource();
  up(&aResource);
  
}

上面显示了一個進程資源獲取和釋放的過程,但是一般情況下會存在多個資源同時獲取鎖的情景,這樣該如何處理?如下所示

typedef int semaphore;
semaphore aResource;
semaphore bResource;

void processA(void){
  
  down(&aResource);
  down(&bResource);
	useAResource();
  useBResource();
  up(&aResource);
  up(&bResource);
  
}

對於單個進程來說,並不需要加鎖,因為不存在和這個進程的競爭條件。所以單進條件下程序能夠完好運行。

現在讓我們考慮兩個進程的情況,A 和 B ,還存在兩個資源。如下所示

typedef int semaphore;
semaphore aResource;
semaphore bResource;

void processA(void){
  
  down(&aResource);
  down(&bResource);
	useBothResource();
  up(&bResource);
  up(&aResource);
  
}

void processB(void){
  
  down(&aResource);
  down(&bResource);
	useBothResource();
  up(&bResource);
  up(&aResource);
  
}

在上述代碼中,兩個進程以相同的順序訪問資源。在這段代碼中,一個進程在另一個進程之前獲取資源,如果另外一個進程想在第一個進程釋放之前獲取資源,那麼它會由於資源的加鎖而阻塞,直到該資源可用為止。

在下面這段代碼中,有一些變化

typedef int semaphore;
semaphore aResource;
semaphore bResource;

void processA(void){
  
  down(&aResource);
  down(&bResource);
	useBothResource();
  up(&bResource);
  up(&aResource);
  
}

void processB(void){
  
  down(&bResource); // 變化的代碼 
  down(&aResource); // 變化的代碼
	useBothResource();
  up(&aResource); // 變化的代碼 
  up(&bResource); // 變化的代碼 
  
}

這種情況就不同了,可能會發生同時獲取兩個資源並有效地阻塞另一個過程,直到完成為止。也就是說,可能會發生進程 A 獲取資源 A 的同時進程 B 獲取資源 B 的情況。然後每個進程在嘗試獲取另一個資源時被阻塞。

在這裏我們會發現一個簡單的獲取資源順序的問題就會造成死鎖,所以死鎖是很容易發生的,所以下面我們就對死鎖做一個詳細的認識和介紹。

死鎖

如果要對死鎖進行一個定義的話,下面的定義比較貼切

如果一組進程中的每個進程都在等待一個事件,而這個事件只能由該組中的另一個進程觸發,這種情況會導致死鎖

簡單一點來表述一下,就是每個進程都在等待其他進程釋放資源,而其他資源也在等待每個進程釋放資源,這樣沒有進程搶先釋放自己的資源,這種情況會產生死鎖,所有進程都會無限的等待下去。

換句話說,死鎖進程結合中的每個進程都在等待另一個死鎖進程已經佔有的資源。但是由於所有進程都不能運行,它們之中任何一個資源都無法釋放資源,所以沒有一個進程可以被喚醒。這種死鎖也被稱為資源死鎖(resource deadlock)。資源死鎖是最常見的類型,但不是所有的類型,我們後面會介紹其他類型,我們先來介紹資源死鎖

資源死鎖的條件

針對我們上面的描述,資源死鎖可能出現的情況主要有

  • 互斥條件:每個資源都被分配給了一個進程或者資源是可用的
  • 保持和等待條件:已經獲取資源的進程被認為能夠獲取新的資源
  • 不可搶佔條件:分配給一個進程的資源不能強制的從其他進程搶佔資源,它只能由佔有它的進程显示釋放
  • 循環等待:死鎖發生時,系統中一定有兩個或者兩個以上的進程組成一個循環,循環中的每個進程都在等待下一個進程釋放的資源。

發生死鎖時,上面的情況必須同時會發生。如果其中任意一個條件不會成立,死鎖就不會發生。可以通過破壞其中任意一個條件來破壞死鎖,下面這些破壞條件就是我們探討的重點

死鎖模型

Holt 在 1972 年提出對死鎖進行建模,建模的標準如下:

  • 圓形表示進程
  • 方形表示資源

從資源節點到進程節點表示資源已經被進程佔用,如下圖所示

在上圖中表示當前資源 R 正在被 A 進程所佔用

由進程節點到資源節點的有向圖表示當前進程正在請求資源,並且該進程已經被阻塞,處於等待這個資源的狀態

在上圖中,表示的含義是進程 B 正在請求資源 S 。Holt 認為,死鎖的描述應該如下

這是一個死鎖的過程,進程 C 等待資源 T 的釋放,資源 T 卻已經被進程 D 佔用,進程 D 等待請求佔用資源 U ,資源 U 卻已經被線程 C 佔用,從而形成環。

總結一點:吃着碗里的看着鍋里的容易死鎖

那麼如何避免死鎖呢?我們還是通過死鎖模型來聊一聊

假設有三個進程 (A、B、C) 和三個資源(R、S、T) 。三個進程對資源的請求和釋放序列如下圖所示

操作系統可以任意選擇一個非阻塞的程序運行,所以它可以決定運行 A 直到 A 完成工作;它可以運行 B 直到 B 完成工作;最後運行 C。

這樣的順序不會導致死鎖(因為不存在對資源的競爭),但是這種情況也完全沒有并行性。進程除了在請求和釋放資源外,還要做計算和輸入/輸出的工作。當進程按照順序運行時,在等待一個 I/O 時,另一個進程不能使用 CPU。所以,嚴格按照串行的順序執行並不是最優越的。另一方面,如果沒有進程在執行任何 I/O 操作,那麼最短路徑優先作業會優於輪轉調度,所以在這種情況下串行可能是最優越的

現在我們假設進程會執行計算和 I/O 操作,所以輪詢調度是一種合理的調度算法。資源請求可能會按照下面這個順序進行

下圖是針對上面這六個步驟的資源分配圖。

這裏需要注意一個問題,為什麼從資源出來的有向圖指向了進程卻表示進程請求資源呢?筆者剛開始看也有這個疑問,但是想了一下這個意思解釋為進程佔用資源比較合適,而進程的有向圖指向資源表示進程被阻塞的意思。

在上面的第四個步驟,進程 A 正在等待資源 S;第五個步驟中,進程 B 在等待資源 T;第六個步驟中,進程 C 在等待資源 R,因此產生了環路並導致了死鎖。

然而,操作系統並沒有規定一定按照某種特定的順序來執行這些進程。遇到一個可能會引起死鎖的線程后,操作系統可以乾脆不批准請求,並把進程掛起一直到安全狀態為止。比如上圖中,如果操作系統認為有死鎖的可能,它可以選擇不把資源 S 分配給 B ,這樣 B 被掛起。這樣的話操作系統會只運行 A 和 C,那麼資源的請求和釋放就會是下面的步驟

下圖是針對上面這六個步驟的資源分配圖。

在第六步執行完成后,可以發現並沒有產生死鎖,此時就可以把資源 S 分配給 B,因為 A 進程已經執行完畢,C 進程已經拿到了它想要的資源。進程 B 可以直接獲得資源 S,也可以等待進程 C 釋放資源 T 。

有四種處理死鎖的策略:

  • 忽略死鎖帶來的影響(驚呆了)
  • 檢測死鎖並回復死鎖,死鎖發生時對其進行檢測,一旦發生死鎖后,採取行動解決問題
  • 通過仔細分配資源來避免死鎖
  • 通過破壞死鎖產生的四個條件之一來避免死鎖

下面我們分別介紹一下這四種方法

鴕鳥算法

最簡單的解決辦法就是使用鴕鳥算法(ostrich algorithm),把頭埋在沙子里,假裝問題根本沒有發生。每個人看待這個問題的反應都不同。數學家認為死鎖是不可接受的,必須通過有效的策略來防止死鎖的產生。工程師想要知道問題發生的頻次,系統因為其他原因崩潰的次數和死鎖帶來的嚴重後果。如果死鎖發生的頻次很低,而經常會由於硬件故障、編譯器錯誤等其他操作系統問題導致系統崩潰,那麼大多數工程師不會修復死鎖。

死鎖檢測和恢復

第二種技術是死鎖的檢測和恢復。這種解決方式不會嘗試去阻止死鎖的出現。相反,這種解決方案會希望死鎖盡可能的出現,在監測到死鎖出現后,對其進行恢復。下面我們就來探討一下死鎖的檢測和恢復的幾種方式

每種類型一個資源的死鎖檢測方式

每種資源類型都有一個資源是什麼意思?我們經常提到的打印機就是這樣的,資源只有打印機,但是設備都不會超過一個。

可以通過構造一張資源分配表來檢測這種錯誤,比如我們上面提到的

的算法來檢測從 P1 到 Pn 這 n 個進程中的死鎖。假設資源類型為 m,E1 代表資源類型1,E2 表示資源類型 2 ,Ei 代表資源類型 i (1 <= i <= m)。E 表示的是 現有資源向量(existing resource vector),代表每種已存在的資源總數。

現在我們就需要構造兩個數組:C 表示的是當前分配矩陣(current allocation matrix) ,R 表示的是 請求矩陣(request matrix)。Ci 表示的是 Pi 持有每一種類型資源的資源數。所以,Cij 表示 Pi 持有資源 j 的數量。Rij 表示 Pi 所需要獲得的資源 j 的數量

一般來說,已分配資源 j 的數量加起來再和所有可供使用的資源數相加 = 該類資源的總數。

死鎖的檢測就是基於向量的比較。每個進程起初都是沒有被標記過的,算法會開始對進程做標記,進程被標記后說明進程被執行了,不會進入死鎖,當算法結束時,任何沒有被標記過的進程都會被判定為死鎖進程。

上面我們探討了兩種檢測死鎖的方式,那麼現在你知道怎麼檢測后,你何時去做死鎖檢測呢?一般來說,有兩個考量標準:

  • 每當有資源請求時就去檢測,這種方式會佔用昂貴的 CPU 時間。
  • 每隔 k 分鐘檢測一次,或者當 CPU 使用率降低到某個標準下去檢測。考慮到 CPU 效率的原因,如果死鎖進程達到一定數量,就沒有多少進程可以運行,所以 CPU 會經常空閑。

從死鎖中恢復

上面我們探討了如何檢測進程死鎖,我們最終的目的肯定是想讓程序能夠正常的運行下去,所以針對檢測出來的死鎖,我們要對其進行恢復,下面我們會探討幾種死鎖的恢復方式

通過搶佔進行恢復

在某些情況下,可能會臨時將某個資源從它的持有者轉移到另一個進程。比如在不通知原進程的情況下,將某個資源從進程中強製取走給其他進程使用,使用完后又送回。這種恢復方式一般比較困難而且有些簡單粗暴,並不可取。

通過回滾進行恢復

如果系統設計者和機器操作員知道有可能發生死鎖,那麼就可以定期檢查流程。進程的檢測點意味着進程的狀態可以被寫入到文件以便後面進行恢復。檢測點不僅包含存儲映像(memory image),還包含資源狀態(resource state)。一種更有效的解決方式是不要覆蓋原有的檢測點,而是每出現一個檢測點都要把它寫入到文件中,這樣當進程執行時,就會有一系列的檢查點文件被累積起來。

為了進行恢復,要從上一個較早的檢查點上開始,這樣所需要資源的進程會回滾到上一個時間點,在這個時間點上,死鎖進程還沒有獲取所需要的資源,可以在此時對其進行資源分配。

殺死進程恢復

最簡單有效的解決方案是直接殺死一個死鎖進程。但是殺死一個進程可能照樣行不通,這時候就需要殺死別的資源進行恢復。

另外一種方式是選擇一個環外的進程作為犧牲品來釋放進程資源。

死鎖避免

我們上面討論的是如何檢測出現死鎖和如何恢復死鎖,下面我們探討幾種規避死鎖的方式

單個資源的銀行家算法

銀行家算法是 Dijkstra 在 1965 年提出的一種調度算法,它本身是一種死鎖的調度算法。它的模型是基於一個城鎮中的銀行家,銀行家向城鎮中的客戶承諾了一定數量的貸款額度。算法要做的就是判斷請求是否會進入一種不安全的狀態。如果是,就拒絕請求,如果請求后系統是安全的,就接受該請求。

比如下面的例子,銀行家一共為所有城鎮居民提供了 15 單位個貸款額度,一個單位表示 1k 美元,如下所示

城鎮居民都喜歡做生意,所以就會涉及到貸款,每個人能貸款的最大額度不一樣,在某一時刻,A/B/C/D 的貸款金額如下

上面每個人的貸款總額加起來是 13,馬上接近 15,銀行家只能給 A 和 C 進行放貸,可以拖着 B 和 D、所以,可以讓 A 和 C 首先完成,釋放貸款額度,以此來滿足其他居民的貸款。這是一種安全的狀態。

如果每個人的請求導致總額會超過甚至接近 15 ,就會處於一種不安全的狀態,如下所示

這樣,每個人還能貸款至少 2 個單位的額度,如果其中有一個人發起最大額度的貸款請求,就會使系統處於一種死鎖狀態。

這裏注意一點:不安全狀態並不一定引起死鎖,由於客戶不一定需要其最大的貸款額度,但是銀行家不敢抱着這種僥倖心理。

銀行家算法就是對每個請求進行檢查,檢查是否請求會引起不安全狀態,如果不會引起,那麼就接受該請求;如果會引起,那麼就推遲該請求。

類似的,還有多個資源的銀行家算法,讀者可以自行了解。

破壞死鎖

死鎖本質上是無法避免的,因為它需要獲得未知的資源和請求,但是死鎖是滿足四個條件后才出現的,它們分別是

  • 互斥
  • 保持和等待
  • 不可搶佔
  • 循環等待

我們分別對這四個條件進行討論,按理說破壞其中的任意一個條件就能夠破壞死鎖

破壞互斥條件

我們首先考慮的就是破壞互斥使用條件。如果資源不被一個進程獨佔,那麼死鎖肯定不會產生。如果兩個打印機同時使用一個資源會造成混亂,打印機的解決方式是使用 假脫機打印機(spooling printer) ,這項技術可以允許多個進程同時產生輸出,在這種模型中,實際請求打印機的唯一進程是打印機守護進程,也稱為後台進程。後台進程不會請求其他資源。我們可以消除打印機的死鎖。

後台進程通常被編寫為能夠輸出完整的文件后才能打印,假如兩個進程都佔用了假脫機空間的一半,而這兩個進程都沒有完成全部的輸出,就會導致死鎖。

因此,盡量做到盡可能少的進程可以請求資源。

破壞保持等待的條件

第二種方式是如果我們能阻止持有資源的進程請求其他資源,我們就能夠消除死鎖。一種實現方式是讓所有的進程開始執行前請求全部的資源。如果所需的資源可用,進程會完成資源的分配並運行到結束。如果有任何一個資源處於頻繁分配的情況,那麼沒有分配到資源的進程就會等待。

很多進程無法在執行完成前就知道到底需要多少資源,如果知道的話,就可以使用銀行家算法;還有一個問題是這樣無法合理有效利用資源

還有一種方式是進程在請求其他資源時,先釋放所佔用的資源,然後再嘗試一次獲取全部的資源。

破壞不可搶佔條件

破壞不可搶佔條件也是可以的。可以通過虛擬化的方式來避免這種情況。

破壞循環等待條件

現在就剩最後一個條件了,循環等待條件可以通過多種方法來破壞。一種方式是制定一個標準,一個進程在任何時候只能使用一種資源。如果需要另外一種資源,必須釋放當前資源。對於需要將大文件從磁帶複製到打印機的過程,此限制是不可接受的。

另一種方式是將所有的資源統一編號,如下圖所示

進程可以在任何時間提出請求,但是所有的請求都必須按照資源的順序提出。如果按照此分配規則的話,那麼資源分配之間不會出現環。

儘管通過這種方式來消除死鎖,但是編號的順序不可能讓每個進程都會接受。

其他問題

下面我們來探討一下其他問題,包括 通信死鎖、活鎖是什麼、飢餓問題和兩階段加鎖

兩階段加鎖

雖然很多情況下死鎖的避免和預防都能處理,但是效果並不好。隨着時間的推移,提出了很多優秀的算法用來處理死鎖。例如在數據庫系統中,一個經常發生的操作是請求鎖住一些記錄,然後更新所有鎖定的記錄。當同時有多個進程運行時,就會有死鎖的風險。

一種解決方式是使用 兩階段提交(two-phase locking)。顧名思義分為兩個階段,一階段是進程嘗試一次鎖定它需要的所有記錄。如果成功后,才會開始第二階段,第二階段是執行更新並釋放鎖。第一階段並不做真正有意義的工作。

如果在第一階段某個進程所需要的記錄已經被加鎖,那麼該進程會釋放所有鎖定的記錄並重新開始第一階段。從某種意義上來說,這種方法類似於預先請求所有必需的資源或者是在進行一些不可逆的操作之前請求所有的資源。

不過在一般的應用場景中,兩階段加鎖的策略並不通用。如果一個進程缺少資源就會半途中斷並重新開始的方式是不可接受的。

通信死鎖

我們上面一直討論的是資源死鎖,資源死鎖是一種死鎖類型,但並不是唯一類型,還有通信死鎖,也就是兩個或多個進程在發送消息時出現的死鎖。進程 A 給進程 B 發了一條消息,然後進程 A 阻塞直到進程 B 返迴響應。假設請求消息丟失了,那麼進程 A 在一直等着回復,進程 B 也會阻塞等待請求消息到來,這時候就產生死鎖

儘管會產生死鎖,但是這並不是一個資源死鎖,因為 A 並沒有佔據 B 的資源。事實上,通信死鎖並沒有完全可見的資源。根據死鎖的定義來說:每個進程因為等待其他進程引起的事件而產生阻塞,這就是一種死鎖。相較於最常見的通信死鎖,我們把上面這種情況稱為通信死鎖(communication deadlock)

通信死鎖不能通過調度的方式來避免,但是可以使用通信中一個非常重要的概念來避免:超時(timeout)。在通信過程中,只要一個信息被發出后,發送者就會啟動一個定時器,定時器會記錄消息的超時時間,如果超時時間到了但是消息還沒有返回,就會認為消息已經丟失並重新發送,通過這種方式,可以避免通信死鎖。

但是並非所有網絡通信發生的死鎖都是通信死鎖,也存在資源死鎖,下面就是一個典型的資源死鎖。

當一個數據包從主機進入路由器時,會被放入一個緩衝區,然後再傳輸到另外一個路由器,再到另一個,以此類推直到目的地。緩衝區都是資源並且數量有限。如下圖所示,每個路由器都有 10 個緩衝區(實際上有很多)。

假如路由器 A 的所有數據需要發送到 B ,B 的所有數據包需要發送到 D,然後 D 的所有數據包需要發送到 A 。沒有數據包可以移動,因為在另一端沒有緩衝區可用,這就是一個典型的資源死鎖。

活鎖

你會發現一個很有意思的事情,死鎖就跟榆木腦袋一樣,不會轉彎。我看過古代的一則故事:

如果說死鎖很痴情的話,那麼活鎖用一則成語來表示就是 弄巧成拙

某些情況下,當進程意識到它不能獲取所需要的下一個鎖時,就會嘗試禮貌的釋放已經獲得的鎖,然後等待非常短的時間再次嘗試獲取。可以想像一下這個場景:當兩個人在狹路相逢的時候,都想給對方讓路,相同的步調會導致雙方都無法前進。

現在假想有一對并行的進程用到了兩個資源。它們分別嘗試獲取另一個鎖失敗后,兩個進程都會釋放自己持有的鎖,再次進行嘗試,這個過程會一直進行重複。很明顯,這個過程中沒有進程阻塞,但是進程仍然不會向下執行,這種狀況我們稱之為 活鎖(livelock)

飢餓

與死鎖和活鎖的一個非常相似的問題是 飢餓(starvvation)。想象一下你什麼時候會餓?一段時間不吃東西是不是會餓?對於進程來講,最重要的就是資源,如果一段時間沒有獲得資源,那麼進程會產生飢餓,這些進程會永遠得不到服務。

我們假設打印機的分配方案是每次都會分配給最小文件的進程,那麼要打印大文件的進程會永遠得不到服務,導致進程飢餓,進程會無限制的推后,雖然它沒有阻塞。

總結

死鎖是一類通用問題,任何操作系統都會產生死鎖。當每一組進程中的每個進程都因等待由該組的其他進程所佔有的資源而導致阻塞,死鎖就發生了。這種情況會使所有的進程都處於無限等待的狀態。

死鎖的檢測和避免可以通過安全和不安全狀態來判斷,其中一個檢測方式就是銀行家算法;當然你也可以使用鴕鳥算法對死鎖置之不理,但是你肯定會遭其反噬。

也可以在設計時通過系統結構的角度來避免死鎖,這樣能夠預防死鎖;也可以破壞死鎖的四個條件來破壞死鎖。資源死鎖並不是唯一性的死鎖,還有通信間死鎖,可以設置適當的超時時間來完成。

活鎖和死鎖的問題有些相似,它們都是一種進程無法繼續向下執行的狀態。由於進程調度策略導致嘗試獲取進程的一方永遠無法獲得資源后,進程會導致飢餓的出現。

尾聲

提出一個勘誤,已反饋給出版社

關於我

我自己現在寫了三本 PDF,讀者回復都非常不錯,現在免費分享出來,關注我的公眾號即可領取

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

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

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

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

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

※幫你省時又省力,新北清潔一流服務好口碑

※回頭車貨運收費標準

日本國土百萬年的惡夢 每日數百噸的福島輻射污染水

文:宋瑞文(媽媽監督核電廠聯盟特約撰述)

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

【【其他文章推薦】

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

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

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

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

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

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

印度環境部長:解決首都空污時間 會比北京短

摘錄自2019年11月23日中央通訊社印度報導

印度環境、森林與氣候變遷部部長賈瓦德卡爾昨天(22日)在印度國會下院答詢時聲稱,空污需要大規模運動來解決,「北京花了15年的時間,我們將花費更少時間(解決空污問題)」。

印度包含新德里在內國家首都區每年冬天因農民焚燒農田殘梗、車輛排放、建築工地揚塵、6000家餐廳用炭火燒烤雞肉、窮人焚燒垃圾取暖等諸多因素,陷入嚴重空氣污染,但印度政府一直無法拿出有效解決對策。空污在今年11月更有惡化趨勢,引發民眾不滿。

賈瓦德卡爾指出,政府已針對城市制定監測空氣污染計畫,目前在全印度355個城市監測空污。此外,政府正每天展開工作以消除空污威脅,包括2018年啟用東、西部外環高速公路,且已把碳排放降低了22%;在第6期汽車廢氣排放標準(BS VI)明年4月實施後,汽車廢氣排放量將可減少80%。

他還把空污問題與氣候變遷聯繫起來,並說印度的總發電量在2030年之前,將有40%是來自可再生能源;而且印度的綠地覆蓋率不斷增加,使印度成為可達到綠地覆蓋率標準的少數國家之一,更五度在國家首都區種植更多樹木,以替代因興建地鐵而砍伐掉的樹木。

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

【其他文章推薦】

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

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

※台北網頁設計公司全省服務真心推薦

※想知道最厲害的網頁設計公司"嚨底家"!

新北清潔公司,居家、辦公、裝潢細清專業服務

澳洲森林大火 空污程度空前

摘錄自2019年11月22日中央通訊社澳洲報導

澳洲新南威爾斯省遍布森林野火,飄散出的煙霧使今天(22日)空污程度空前,造成就醫人數激增,並引起駕駛人視線不良等危險。

澳洲人口第一大城雪梨(Sydney)已連續4天被煙霧籠罩,罕見但一再成為世界空污最嚴重的10大城市之一。近幾天來某些時候,全球城市空氣品質監測網站Air Visual排行顯示,雪梨名列世界空污最嚴重的第8大城市,排名在雅加達及深圳之前,位居孟買之後。

隨著強風吹送林火煙霧以及因全澳3年乾旱而堆積的灰塵,波克鎮的空污比安全標準高出15倍。煙霾挾帶著來懸浮微粒污染物,形成官員所說新南威爾斯省紀錄中的最嚴重污染。這種粒子會被人體吸入血液中。

大火迄今仍在新南威爾斯省、維多利亞省(Victoria)、南澳省(South Australia)及昆士蘭省(Queensland)燃燒。澳洲總理莫里森(Scott Morrison)因為這次危機而受到壓力,批評者指稱,莫里森並未盡力處理氣候變遷的衝擊。氣象學家則指出,氣候變遷正導致野火季節的時間延長。

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

【其他文章推薦】

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

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

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

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

※幫你省時又省力,新北清潔一流服務好口碑

一口氣投80億人民幣 比亞迪建長沙、青島兩個新能源車生產基地

比亞迪汽車於5月18日與湖南省長沙市雨花區經開區簽署合作備忘錄,比亞迪將投資50億元人民幣(下同),在長沙比亞迪汽車城二期興建電動卡車及專用車生產基地,計畫年內投產。   長沙比亞迪專案將由電動轎車、電動客車和電動卡車及專用車三部分組成。電動卡車及專用車專案,現已動工建設,是比亞迪電動卡車及專用車全球製造中心,含整車及相應配套零部件,目標向全中國工廠提供電動底盤、車橋等核心部件。比亞迪預計2016年將達產2500輛,規劃至2020年達5000輛年產能,2025年擴建到10000輛年產能。   此外,比亞迪在青島市城陽區投資30億元的新能源汽車專案一期目前已開工建設,這是青島城陽首個新能源汽車產業專案,也是建區以來引進的最大工業專案。根據規劃分三期建設完成,預計2017年全部建成。專案全部建成後,將年產電動汽車5000輛,年可實現銷售收入105億元。   此專案位於棘洪灘軌道交通裝備製造產業園內,規劃佔地1000畝,主要生產純電動轎車、中巴、小巴、物流車等產品,以及從事車輛電池、電機、電控等核心零部件的研發和製造。

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

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

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

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

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

比亞迪新能源車夯!2015年可望售出350億人民幣

根據中國汽車工業協會公佈的統計資料顯示,2015年1至4月份,比亞迪新能源汽車銷量已超過1.2萬輛。同時第一季,比亞迪歸屬於上市公司股東的淨利潤達1.2億元人民幣(下同),同比增幅高達910%。   按4月份單月的銷量資料來看,比亞迪乘用車總體銷量為32260輛,年減29.85%。但新能源汽車銷量為3646輛,其中秦2625輛,e6電動車890輛,騰勢131輛,實現了正向增長。   據比亞迪相關人士介紹,2015年,比亞迪還將推出包括鎖定個人消費者的插電式混合動力汽車「唐」,中型及緊湊型插電式混合動力SUV車型「宋」和「元」,以及新能源商用車「商」等。除此之外,在特種車輛方面,比亞迪也會逐步加快研發並推出應用於礦山、港口、機場及倉儲等領域的車型,以及應用於環衛、城市物流等領域的特種車輛。   而比亞迪副總裁廉玉波在出席內地電動車研討會時透露,預料今年可售出約值300億元至350億元的新能源汽車,料全年可售出約6.7萬部新車,佔公司汽車業務一半收入,並有望達到「自給自足」盈利水準。   他表示,現時比亞迪每月可售出1,000部純電動車e6,純電動巴士每月售約600部。且位於惠州的電池廠3月起投產,有利提升產能;其中較受歡迎的混能車「秦」,6月時產能將增至6,000架。而比亞迪與戴姆勒合資生意的純電動車「騰勢」,每月約售出500部,估計年底時將增至3,000至4,000部。

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

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

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

新北清潔公司,居家、辦公、裝潢細清專業服務

Tesla 挹注擴大 F-貿聯第三季營收季增幅度大

Tesla 日前發表的儲能電池計畫,受到外界關注,法人評估,首波供應商 F-貿聯最快 8 月有機會開始交貨,明年下半年新計畫挹注逐步擴大。法人也預估,該公司本季營收季增個位數、且將挑戰單季新高,第三季起隨 Tesla 新 SUV 車款問世,6 月下旬起零件逐月放量,下季營收季增幅度將拉大,全年營收拚增兩位數。   F-貿聯與 Tesla 的往來關係深厚,早在 Tesla 還未成為全球知名的電動車大廠前,就已相互搭配,因此也成為該客戶的指標核心供應商。一直以來,貿聯較為人熟知是 Tesla 電動車電池管理系統線束的獨家供應商,此次配合與松下合資的超級電池工廠明年下半年量產,電池供應量將大增。   據了解,F-貿聯已是特斯拉除能電池系統 Powerwall 與企業用 Powerpack 計畫的首波零件供應商,供貨品項仍為電池管理系統用線。法人預期,最快 8 月有望正式交貨,雖對今年的營收貢獻不大,但更確認了貿聯在 Tesla 各項計畫共同開發的核心地位,而明年下半年超級電池工廠如果順利量產,該計畫的挹注將明顯增溫。

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

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

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

※幫你省時又省力,新北清潔一流服務好口碑

用Visual C++創建WPF項目的三種主要方法

用Visual C++創建WPF項目的三種主要方法

The problem with using XAML from C++

Because C++ doesn’t support partial class definitions, it isn’t possible to directly support XAML in VC++ projects using this mechanism. That isn’t, however, the core reason why VC++ doesn’t directly support XAML. In addition to using the x:Class attribute, you can also use the x:Subclass attribute so that the XAML gets compiled into the class specified by the x:Class attribute, and the code behind will define the class specified by x:Subclass, which will be derived from the x:Class type. Thus, the lack of partial classes isn’t that big of a block. The main issue is that, right now, no 100-percent CodeDOM support is available to convert

the XAML to C++, and that is the single biggest reason why VC++ doesn’t support XAML intrinsically. I don’t know this for sure, but it’s possible that on a later date, the Visual C++ team may work on their CodeDOM support and provide a fully functional XAML-to-C++ converter. Once that’s available, XAML support can be integrated into VC++ projects. As of today, however, that isn’t an option.

NOTE: CodeDOM is a term used to represent a bunch of types available in the System.

CodeDom namespace that lets you abstract code into an object model. Source code is represented using the CodeDOM tree and can be converted into source code for a specific language using the CodeDOM code generator for that specific language.

Still, the fact that you can’t directly use XAML in a Visual C++ project doesn’t mean that WPF applications can’t be written with Visual C++.

Three ways to write WPF apps using VC++

You can use three different approaches to write WPF applications using Visual C++. Each has its pros and cons, and we’ll cover each of these approaches in the next section:

  • Use procedural code.

For one thing, you can directly use procedural code to

write Avalon-based applications and avoid using XAML. Of course, if you

do that, you automatically give up the advantages of declarative programming

that XAML brings in, but for certain scenarios, procedural code often

serves the purpose well.

  • Dynamically load XAML.

Alternatively, you can dynamically load XAML during runtime to create your Avalon windows, although the disadvantage is that you’d be distributing a bunch of XAML files with your application.

  • Derive from a class in a C# DLL

A third technique uses a C# project to create your XAML-based Avalon controls and have a class (or classes) in your C++ project that derives from the classes in the C#-based Avalon DLL. With that mechanism, the UI is created using XAML in the C# project, and the business logic is kept in the C++ project.

When you’re developing WPF applications with C++, you can use one or more of these approaches to achieve whatever functionality you want. In the next section, you’ll see how to write a simple WPF app with C++/CLI using each of the three techniques mentioned here.

7.2 Using C++/CLI to write a WPF application

If Visual C++ doesn’t have support for XAML, and there are no project templates for building an Avalon application (as of the June 2006 CTP), how much extra effort does it take to write Avalon applications using C++? In this section, you’ll find out. You’ll put the three different techniques I described at the end of section

7.1.2 into action. All three mechanisms have their advantages and disadvantages; you can decide which is most suitable for your specific scenario. First, though, let’s briefly go over how to create a new C++/CLI project for Avalon.

7.2.1 Creating a new C++/CLI Avalon project

Avalon is a managed framework, and as such any Visual C++ project that needs to access and use Avalon needs to have the /clr compilation mode turned on.

Creating a new C++/CLI project with support for Avalon is fortunately not a difficult task. Table 7.1 lists the few simple steps you need to follow each time you create an application (or library, as the case might be) that uses Avalon.

Table 7.1 Steps to create a C++/CLI Avalon project

Step Action How To
1 Generate a new project Using the application wizard, specify the CLR Empty Project template.
2 Set the SubSystem to /SUBSYSTEM:WINDOWS Apply this change in the Project properties, Linker settings, System sub-setting.
3 Set the Entry Point to main From Project properties, choose Linker settings and then the Advanced sub-setting.
4 Add references to the following assemblies: System PresentationCore PresentationFramework WindowsBase Note: Except for System, the other three are required for Avalon.

At this point, your empty project is ready for writing Avalon code. Of course, you don’t have any code yet to compile, but you’ll fix that soon.

7.2.2 Using procedural code

You’ll now write your first Avalon application using C++/CLI, and you’ll do so entirely using procedural code. Think of it as analogous to an instruction book for putting together a table that contains only textual instructions (analogous to the procedural code) and no pictures (analogous to the XAML).

Create a new CLR project using the steps outlined in the previous section, and add an App.cpp file to it (you can call it whatever you want). Listing 7.2 shows the code for the simplest Avalon application that shows a window onscreen.

Listing 7.2 A simple Avalon app in procedural code

If you compile and run the application, you’ll see a window onscreen that can be moved, resized, minimized, maximized, and closed. Avalon requires you to set the COM threading model to single threaded apartment (STA). You do so using the STAThread attribute on the main function . You then create a new instance of the Application object (using gcnew) and invoke the Run method on that instance, passing in a new instance of a Window object (again using gcnew) . The Application class represents an Avalon application and provides the core functionality for running the application. It has a Run method that is called to initiate the application’s main thread. The Run method has an overload that accepts a Window object, which you use in the code. This overload launches the application and uses the specified Window as the main application window. The Window class represents the core functionality of a window and by default provides you with basic windowing functionality such as moving, resizing, and so on, which you verified when you ran the application and saw a fully functional window onscreen.

Note: Those of you who have an MFC background may see a faint similarity between this model and MFC, where the CWinApp class is analogous to the Application class, and the CFrameWnd class is analogous to the Window

class. CWinApp has a Run method that provides the default message loop, and Application::Run does something similar. Of course, you shouldn’t infer too much from these minor similarities because they’re totally different UI programming models, but it’s possible that a similar design model was used by the architects of Avalon.

This little program doesn’t have a lot of functionality; it just uses the default Window object to create and show a window onscreen. Let’s write a more refined application with its own Application-derived object as well as a window with some controls. Figure 7.4 shows a screenshot of what the enhanced application

will look like.

The main steps involved would be to derive two classes-one from the Window class, and the other from the Application class. You’ll start with the Window-derived class.

Figure 7.4

Enhanced WPF app in C++ (procedural code)

Writing the Window-derived class

The first thing you’ll do is add a new class called FirstWindow to your project, which will be derived from the Window class. You’ll also add some member variables for the various controls and set some of the window properties in the constructor. Listing 7.3 shows the code once you’ve done that.

Listing 7.3 A more functional Avalon app in procedural code

using namespace System;

using namespace System::Windows;

using namespace System::Windows::Controls;

It’s much like Windows Forms programming, except that the controls you declare ①. are from the System::Windows::Controls namespace (which contains various WPF controls). You set properties like Title, Width, Height, and so on on the window object in the constructor ②. There’s also a call to a method called InitControls ③, where you initialize the child controls (I put it into a separate method to improve the code’s readability). Listing 7.4 shows the InitControls method. Basically, you instantiate each of the child controls, instantiate a container control, add the child controls to the container controls, and finally set the container control as the main Content of the parent window.

Listing 7.4 Function to initialize the Avalon controls

void InitControls(void)

{
      listbox = gcnew ListBox();
      listbox->Width = 180;

      listbox->Height = 350;

      Canvas::SetTop(listbox, 10);

      Canvas::SetLeft(listbox, 10);

      textbox = gcnew TextBox();

      textbox->Width = 180;

      textbox->Height = 25;
    
      Canvas::SetTop(textbox, 10);

      Canvas::SetLeft(textbox, 200);

      addbutton = gcnew Button();

      addbutton->Width = 80;

      addbutton->Height = 25;

      addbutton->Content = "Add";

      Canvas::SetTop(addbutton, 45);

      Canvas::SetLeft(addbutton, 200);

      addbutton->Click += gcnew RoutedEventHandler(this, &FirstWindow::OnAddButtonClick);

      maincanvas = gcnew Canvas();

      maincanvas->Children->Add(listbox);

      maincanvas->Children->Add(textbox);

      maincanvas->Children->Add(addbutton);

      Content = maincanvas;
}

Again, you probably notice the similarity with Windows Forms programming.

You instantiate the child controls ①, ②, and ③, and set various properties like Width and Height, and you also use the Canvas::SetTop and Canvas::SetLeft methods to position them on their container. For the button control, you also add an event handler for the Click event ④. Then, you instantiate the Canvas control (which is a container control for other child controls) and add the child controls as its children ⑤. Finally, you set the Content property of the window to this Canvas control ⑥.

Now, you need to add the Click event handler for the button control, where you add the text entered into the TextBox to the ListBox:

void OnAddButtonClick(Object^ sender, RoutedEventArgs^ e)
{
​	listbox->Items->Add(textbox->Text);
​	textbox->Text = "";
​	textbox->Focus();
}

Notice that you set the text of the TextBox to an empty string once you’ve added it to the ListBox. You also call the Focus() method so that the user can continue

adding more entries into the ListBox. The Window-derived class is ready. Let’s now write the Application-derived class.

Writing the Application-derived class

You derive a class called FirstApp from Application and add an override for the OnStartup method where you create and show the main window:

#include "FirstWindow.h"

ref class FirstApp : Application
{
public:
FirstApp(void){}

protected:
      virtual void OnStartup(StartupEventArgs^ e) override
      {
          Application::OnStartup(e);
          FirstWindow^ mainwnd = gcnew FirstWindow();
          mainwnd->Show();
      }
};

The OnStartup method is called, not surprisingly, when the application has just started. You override that function so that you can instantiate and show the window.

The base function is responsible for invoking any event handlers associated with the Startup event, and thus you need to call the base method in the override.

Now, all that’s left is to modify the main function to use the custom Application object instead of the default, as shown here:

#include "FirstApp.h"

[STAThread]

int main(array<String^>^ args)
{
	return (gcnew FirstApp())->Run();
}

Notice that you don’t specify a window object to the Run method, because the window object is created in the OnStartup override of your Application-derived class.

Compile and run the application, and try entering some text into the TextBox and clicking the Add button. You should see the text being entered into the ListBox.

When you use procedural code with Avalon, it’s much like using Windows Forms, where you derive classes from the default controls, set some properties, add some event handlers, and are done. Procedural code is all right to develop WPF applications for simple user interfaces, but sometimes it makes better sense

to take advantage of XAML and declarative programming. As I’ve mentioned a few times already, XAML isn’t directly supported in VC++, so you’ll have to look at alternate options to make use of XAML. One such option is to dynamically load the XAML at runtime.

7.2.3 Dynamically loading XAML

In this section, you’ll rewrite the application you wrote in the previous section, using dynamically loaded XAML. This way, you get to leverage the power of XAML and declarative programming (which you couldn’t in the procedural code technique you used in the previous section). Continuing the instruction-book analogy, this will be like one that has textual instructions that refer to pictures (which describe the various steps needed) and are loosely distributed along with the book but not directly printed in the book. You’ll define the UI using XAML instead of procedural code. When you’re done, you’ll have an identical application

to the one you previously created.

Create a new C++/CLI Avalon project using the steps mentioned in the introduction to section 7.2, and call it FirstAvalonDynamic (or whatever you want to call it). The first thing you’ll do is write the XAML (MainWindow.xaml) that represents the UI; see listing 7.5.

Listing 7.5 XAML for the main window

<Window

     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

     Title="First Avalon App (dynamically load XAML)"

     Height="400" Width="400"

     ResizeMode="NoResize"

     > 

     <Canvas>
           <ListBox Canvas.Left="10" Canvas.Top="10"
                 Width="180" Height="350"
                 Name="listbox" />
               <TextBox Canvas.Left="200" Canvas.Top="10"
                 Width="180" Height="25"
                 Name="textbox" />
           <Button Canvas.Left="200" Canvas.Top="45"
                 Width="80" Height="25"
                 Name="addbutton">Add</Button>
     </Canvas>
</Window>

The XAML shown does exactly what you did with the procedural code earlier. For the control elements, you use the same names using the Name attribute as you use for the member variables in the procedural code. Next, you need to hook an event handler to the Button so that the text entered into the TextBox is inserted

into the ListBox. For that, you’ll write a helper class, as shown in listing 7.6.

Listing 7.6 WindowHelper class that implements the event handler

using namespace System;

using namespace System::Windows;

using namespace System::Windows::Controls;

using namespace System::Windows::Markup;

using namespace System::IO;

ref class WindowHelper
{

     ListBox^ listbox;

     TextBox^ textbox;

     Button^ addbutton;



public:
WindowHelper(Window^ window)
{
   addbutton = (Button^)window->FindName("addbutton");

   textbox = (TextBox^)window->FindName("textbox");

   listbox = (ListBox^)window->FindName("listbox");

   addbutton->Click += gcnew RoutedEventHandler(

   this,&WindowHelper::OnAddButtonClick);
}

void OnAddButtonClick(Object^ sender, RoutedEventArgs^ e)
{
   listbox->Items->Add(textbox->Text);

   textbox->Text = "";

   textbox->Focus();
}

};

The WindowHelper constructor accepts a Window argument and uses the FindName method ① to get the control with the specified identifier (which maps to the Name attributes you used in the XAML). You also hook an event handler to the addbutton control ②. Finally, you have the event handler③, which is identical to the one you used in the procedural code project. Listing 7.7 shows the code for the Application-derived class, where you override OnStartup as before, except that you create a window dynamically by loading the XAML file from the disk.

Listing 7.7 The Application-derived class

ref class FirstAppDynamic : Application

{

public:

     FirstAppDynamic(void)
     {

     }

protected:
     virtual void OnStartup(StartupEventArgs^ e) override
     {

           Application::OnStartup(e);

           Stream^ st = File::OpenRead("MainWindow.xaml");

           Window^ mainwnd = (Window^)XamlReader::Load(st);

           st->Close();

           WindowHelper^ mainwndhelper = gcnew WindowHelper(mainwnd);

           mainwnd->Show();

     }

};

You open a file stream to the XAML using File::OpenRead ① and use the overload of XamlReader::Load ② that takes a Stream^ as parameter to create a Window object. This Load method works the magic, by reading and parsing the XAML and building a Window object out of it. You instantiate the WindowHelper object and pass

this Window object as the argument, so that the event handler for the addbutton control is properly set up ③. You then show the window ④ with a call to Show().

The main method is much the same as before, where you instantiate the Application object and call Run on it:

[STAThread]
int main(array<String^>^ args)
{

      return (gcnew FirstAppDynamic())->Run();

}

The advantage of using this technique over using procedural code is that you get to design your UI in XAML, thereby achieving a level of UI/code separation. You can also use Cider or some other XAML designer to quickly design flexible user interfaces, which would involve a good bit of hand-coding in procedural code.

The disadvantage is that you have to distribute the XAML file with your application, and if you have multiple windows, you then need that many XAML files.

There’s always the risk of a loosely-distributed XAML file getting corrupted (accidentally or otherwise) or even being deleted. You can embed all the XAML files as resources in the C++/CLI assembly and load them at runtime, but even that involves a lot of extra work. To avoid distributing XAML files loosely with your

application or embedding them as resources, you may want to use the technique we’ll discuss in the next section: putting the XAML into a C# project and accessing it via a derived class in a C++ project.

7.2.4 Deriving from a class in a C# DLL

You’ll write a third variation of the same application in this section. You’ll use a C# control library project for the XAML, and a C++ project that will utilize that XAML control by deriving a control from it. Using the instruction-book analogy again, this is essentially a picture-based, step-by-step guide with the textual

instructions printed alongside each picture providing some meta-information for the step indicated by that picture. First, use the New Project Wizard to generate a new C# .NET 3.0 Custom Control Library project, and delete the default XAML file generated by the wizard. The default XAML is derived from User-

Control and isn’t what you want. Add a new XAML file to the C# project that represents a Window, and either use Cider or hand-code the XAML from listing 7.8 into that file.

Listing 7.8 The Window class definition using XAML

<Window x:Class="CSXamlLibrary.BaseWindow"
     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
     Title="First Avalon App (dynamically load XAML)"
     Height="400" Width="400"
     ResizeMode="NoResize"
     > 

     <Canvas>
           <ListBox Canvas.Left="10" Canvas.Top="10"
                 Width="180" Height="350"
                 Name="listbox" x:FieldModifier="protected" />

           <TextBox Canvas.Left="200" Canvas.Top="10"
                 Width="180" Height="25"
                 Name="textbox" x:FieldModifier="protected" />

           <Button Canvas.Left="200" Canvas.Top="45"
                 Width="80" Height="25"
                 Name="addbutton" x:FieldModifier="protected">Add</Button>
     </Canvas>

</Window>

The XAML is identical to that used in the previous project (where you dynamically loaded it) except for the x:Class attribute for the Window element, which specifies the name of the class that will be generated, and the x:FieldModifier attributes that are applied to the child control elements so they’re generated as protected members in the class (rather than as private which is the default). Build the C# project, and generate the control library. Once that’s done, create a new C++/CLI Avalon project (using the same steps as before), and then add a reference to this C# project. Now, you can write a new Window class that’s derived from the class in the C# DLL, as shown in listing 7.9.

Listing 7.9 Deriving the main window from the XAML-defined Window class

using namespace System;

using namespace System::Windows;

using namespace System::Windows::Controls;



ref class AppMainWindow : CSXamlLibrary::BaseWindow
{
     public:
           AppMainWindow(void)
           {
                 addbutton->Click += gcnew RoutedEventHandler(this, &AppMainWindow::OnAddButtonClick);
           }

           void OnAddButtonClick(Object^ sender, RoutedEventArgs^ e)
           {

                 listbox->Items->Add(textbox->Text);

                 textbox->Text = "";

                 textbox->Focus();

           }

};

The code is similar to what you’ve seen thus far, except that it’s a lot cleaner.

Unlike the first example, you don’t have a lot of clogged procedural code to create the UI. Unlike the second example, you don’t need a helper class to map the XAML elements to the control variables and event handlers. It’s definitely an improvement over the previous two examples, but you have to bring in the C#

project just for the XAML. The rest of the code needed for the application is more or less similar to what you saw earlier:

ref class FirstAppDerived : Application
{
      protected:
            virtual void OnStartup(StartupEventArgs^ e) override
            {
                  Application::OnStartup(e);
                  AppMainWindow^ mainwnd = gcnew AppMainWindow();
                  mainwnd->Show();
            }

};

[STAThread]

int main(array<String^>^ args)
{
      return (gcnew FirstAppDerived())->Run();
}

In some ways, the third technique is a sort of hybrid of the previous two techniques.

A lot of the code is identical to that in the first technique – as with the declaration of a custom class derived from Window and an Application-derived class with the OnStartup method creating the custom window. But, like the second technique, the UI definition is in the XAML, except that in this case, it’s compiled into the C# DLL. You also reduce lines of code with each successive technique. You had the most lines of code with procedural code (as is to be expected) and improved on that considerably when you moved the UI definition to the XAML in the dynamically-loaded XAML example. In the last example, you saved even further on lines of code, such as the helper class from the second example that had to wire the XAML elements to the member variables. Of course, the total lines of code (LOC) isn’t always the single deciding factor that determines what technique you choose. Table 7.2 shows a comparison of the three techniques; for each factor, the cells with the bold text reflect the technique (or techniques) that offer maximum performance (or convenience).

Table 7.2 Comparison of the three techniques

Procedural code Dynamically load XAML XAML in C# DLL
Cluttered code that generates the UI Yes No No
Dependency on loose XAML files No Yes No
Dependency on C#-based DLL No No Yes
Lines of code Maximum In-between Minimum
UI design convenience Poor Excellent Excellent
UI/business logic separation Poor Good Excellent
Level of Visual C++ project support Total Partial (Not applicable)

It’s hard to pinpoint a specific technique and claim that it’s the best one, because depending on your requirements, each has advantages and disadvantages. Of course, in the future, if Visual C++ has direct support for XAML (as I believe it will), that will be your best option for the majority of scenarios.

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

【其他文章推薦】

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

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

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

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

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

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

如何使用ABP進行軟件開發之基礎概覽

ABP框架簡述

1)簡介

在.NET眾多的技術框架中,ABP框架(本系列中指aspnetboilerplate項目)以其獨特的魅力吸引了一群優秀開發者廣泛的使用。

在該框架的賦能之下,開發者可根據需求通過官方網站【https://aspnetboilerplate.com/Templates】選擇下載例如Vue/AngluarJS/MVC等不同類型的模板項目,輕鬆加入ABP開發者的隊伍中,盡享基於ABP開發帶來的樂趣。

ABP開發框架也提供了豐富的文檔,能夠為開發者帶來許多便捷。目前ABP的文檔網站為:

官方文檔:https://aspnetboilerplate.com/Pages/Documents

文檔庫不可謂不全,加上國內眾多的ABP開發者參与的活躍的技術圈子,使得學習成本只是在第一個項目中比較高,後期將會越來越平滑。

2)現狀

當然,目前ABP的框架開發者和社區已經把更多的精力投入到了ABP.VNEXT開發框架,這個新框架以其DDD+微服務+模塊化的理念獲得了大量擁躉,使ABP框架的開發優先級已經開始逐漸降低。

但這是因為ABP框架的功能已經成熟穩定,且ABP是一種增量式的架構設計,開發者在熟練掌握這種框架后,可以根據自己的需要進行方便的擴展,使其成為小項目架構選型中一種不錯的備選方案。

當然,也存在一些弊端。例如由於ABP被稱為.NET眾多開發框架中面向領域驅動設計的最佳實踐,而囿於領域驅動設計本身不低的門檻,使得學習的過程變得看起來非常陡峭;

除此之外,ABP也廣泛使用了目前Asp.NET/Asp.NET Core框架的大量比較新的特性,對於不少無法由於各種原因無法享受.NET技術飛速發展紅利的傳統開發者來說,無形中也提高了技術門檻。

3)綜述

在這個系列中,本文計劃分成三篇來介紹ABP框架,第一篇介紹ABP的基礎概覽,介紹基礎知識,第二篇介紹ABP的模式實踐,第三篇,試圖介紹如何從更傳統的三層甚至是單層+SQL的單層架構,如何遷移到ABP框架。

(畢竟。。.NET遺留應用實在是太多了,拯救或不拯救?)

代碼結構結構

基本文件夾簡述

當我們通過ABP模板項目的官方網站下載一個項目后,我們所獲得的代碼包的結構如下圖所示,其中:

  • vue為使用iview框架構建的管理系統基本模板,該腳手架使用了yarn作為包管理器,並集成了vuex/axios等常用框架,並提供了用戶,租戶,權限三個基本功能的示例代碼,開發者只需發揮聰明才智就能快速的通過該框架入手前端項目。
  • (當然,該項目廣泛使用了typescript+面向對象的設計,似乎前端開發者。。普遍不擅長面向對象開發?)
  • aspnet-core則是一個完整的asp.netcore項目的快速開發腳手架。該腳手架集成了docker打包於一體,並包含基本的單元測試示例,使用了identity作為權限控制單元,使用swagger作為接口文檔管理工具,集成了efcore、jwt等常用組件,對於開發者來說,基本上算是開箱即用了。

前端vue項目

打開vue文件夾之後,該項目的基本目錄如下圖所示。(src文件夾)

lib文件夾

定義了與abp+vue腳手架項目的基礎組件和常見類庫,封裝了一系列基本方法。例如權限控制,數據請求,菜單操作,SignalR等基礎組件的用法。

router文件夾

定義了vue項目的路由規則,其中index.ts文件是項目的入口,router.ts文件定義了vue文件的路由規則。

store文件夾

由於本項目使用了vuex框架,所以我們可以來看看對於store文件夾的介紹。

在vuex框架中:

每一個 Vuex 應用的核心就是 store(倉庫)。“store”基本上就是一個容器,它包含着你的應用中大部分的狀態 (state)。
Vuex 和單純的全局對象有以下兩點不同:
Vuex 的狀態存儲是響應式的。當 Vue 組件從 store 中讀取狀態的時候,若 store 中的狀態發生變化,那麼相應的組件也會相應地得到高效更新。
你不能直接改變 store 中的狀態。改變 store 中的狀態的唯一途徑就是顯式地提交 (commit) mutation。這樣使得我們可以方便地跟蹤每一個狀態的變化,從而讓我們能夠實現一些工具幫助我們更好地了解我們的應用。

即vuex框架中,將原來的請求鏈路,抽象化為狀態的變化,通過維護狀態,使得數據的管理更加便捷,也易於擴展。

views文件夾

定義了登錄、首頁、用戶、角色、租戶的基本頁面,並提供了新增、查看、編輯、刪除的代碼示例。

綜上,該項目是一個結構清晰,邏輯縝密的前端框架,可以作為常見管理系統的腳手架。

後端項目

簡介

後端項目是一個遵循了領域驅動設計的分層,同時又符合Robert Martin在《代碼整潔之道》提出的【整潔架構】。

領域驅動設計簡介

在領域驅動設計的分層設計中,共有四個功能分層,分別是:

表示層(Presentation Layer):為用戶提供接口,使用應用層實現用戶交互。

應用層(Application Layer):介於用戶層和領域層之間,協調用戶對象,完成對應的任務。

領域層(Domain Layer):包含業務對象和規則,是應用程序的心臟。

基礎設施層(Infrastructure Layer):提供高層級的通用技術功能,主要使用第三方庫完成。

在後文中,基於abp對領域驅動設計的功能分層將進行多次、詳細敘述,本小節不再贅述。

整潔架構簡介

整潔架構是由Bob大叔提出的一種架構模型,來源於《整潔架構》這本書,顧名思義,其目的並不是為了介紹這一種優秀的架構本身,而是介紹如何設計一種整潔的架構,使得代碼結構易於維護。

(整潔架構就是這樣一個洋蔥,所以也有人稱它為“洋蔥”架構)

  1. 依賴規則(Dependency Rule)

用一組同心圓來表示軟件的不同領域。一般來說,越深入代表你的軟件層次越高。外圓是戰術是實現機制(mechanisms),內圓的是核心原則(policy)。

Policy means the application logic.

Mechanism means the domain primitives.

使此體系架構能夠工作的關鍵是依賴規則。這條規則規定軟件模塊只能向內依賴,而裏面的部分對外面的模塊一無所知,也就是內部不依賴外部,而外部依賴內部。同樣,在外面圈中使用的數據格式不應被內圈中使用,特別是如果這些數據格式是由外面一圈的框架生成的。我們不希望任何外圓的東西會影響內圈層

  1. 實體 (Entities)

實體封裝的是整個企業範圍內的業務核心原則(policy),一個實體能是一個帶有方法的對象,或者是一系列數據結構和函數,只要這個實體能夠被不同的應用程序使用即可。

如果你沒有編寫企業軟件,只是編寫簡單的應用程序,這些實體就是應用的業務對象,它們封裝着最普通的高級別業務規則,你不能希望這些實體對象被一個頁面的分頁導航功能改變,也不能被安全機制改變,操作實現層面的任何改變不能影響實體層,只有業務需求改變了才可以改變實體

  1. 用例 (Use case)

在這個層的軟件包含只和應用相關的業務規則,它封裝和實現系統的所有用例,這些用例會混合各種來自實體的各種數據流程,並且指導這些實體使用企業規則來完成用例的功能目標。

我們並不期望改變這層會影響實體層. 我們也不期望這層被更外部如數據庫 UI或普通框架影響,而這也正是我們分離出這一層來的原因所在。

然而,應用層面的操作改變將會影響到這個用例層,如果需求中用例發生改變,這個層的代碼就會隨之發生改變。所以可以看到,這一層是和應用本身緊密相關的

  1. 接口適配器 (Interface Adapters)

這一層的軟件基本都是一些適配器,主要用於將用例和實體中的數據轉換為外部系統如數據庫或Web使用的數據,在這個層次,可以包含一些GUI的MVC架構,表現視圖 控制器都屬於這個層,模型Model是從控制器傳遞到用例或從用例傳遞到視圖的數據結構。

通常在這個層數據被轉換,從用例和實體使用的數據格式轉換到持久層框架使用的數據,主要是為了存儲到數據庫中,這個圈層的代碼是一點和數據庫沒有任何關係,如果數據庫是一個SQL數據庫, 這個層限制使用SQL語句以及任何和數據庫打交道的事情。

  1. 框架和驅動器

最外面一圈通常是由一些框架和工具組成,如數據庫Database, Web框架等. 通常你不必在這個層不必寫太多代碼,而是寫些膠水性質的代碼與內層進行粘結通訊。

這個層是細節所在,Web技術是細節,數據庫是細節,我們將這些實現細節放在外面以免它們對我們的業務規則造成影響傷害

ABP的分層實現

在ABP項目中,層次劃分如下。

1. 應用層(Application項目)

在領域驅動設計的分層式架構中,應用層作為應用系統的北向網關,對外提供業務外觀的功能。在Abp模板項目中,Application項目也是編寫主要用例代碼的位置,開發者們在此定義與界面有關的數據行為,實現面向接口的開發實踐。

應用服務層包含應用服務,數據傳輸單元,工作單元等對象。

  • Application Service

為面向用戶界面層實現業務邏輯代碼。例如需要為某些界面對象組裝模型,通常會定義ApplicationService,並通過DTO對象,實現與界面表現層的數據交換。

  • Data Transfer Object (DTO)

最常見的數據結構為DTO(數據傳輸對象),這是來源於馬丁弗勒在《企業架構應用模式》中提到的名詞,其主要作用為:

是一種設計模式之間傳輸數據的軟件應用系統。 數據傳輸目標往往是數據訪問對象從數據庫中檢索數據。

在ABP的設計中,有兩種不同類型的DTO,分別是用於新增、修改、刪除的Input DTO,和用於查詢的Output DTO。

  • Unit of Work:

工作單元。工作單元與事務類似,封裝了一系列原子級的數據庫操作。

2. 核心層(Core項目)

核心層包含領域實體、值對象、聚合根,以及領域上下文實現。

  • Entity(實體):

實體有別於傳統意義上大家所理解的與數據庫字段一一匹配的實體模型,在領域驅動設計中,雖然實體同樣可能持久化到數據庫,但實體包含屬性和行為兩種不同的抽象。

例如,如果有一個實體為User,其中有一個屬性為Phone,數據為086-132xxxxxxxx,我們有時需要判斷該手機號碼的國際代號,可能會添加一個新的判定 GetNationCode(),可以通過從Phone字段中取出086來實現,這就是一種通俗意義上的行為。

  • Value Object(值對象):

值對象無需持久化到數據庫,往往是從其他實體或聚合中“剝離”出來的與某些聚合具備邏輯相關性或語義相關性的對象,有時值對象甚至只有個別屬性。

例如,上述實體,包含Phone字段,我們可以將整個Phone“剝離”為一個Telephone對象,該對象可包含PhoneNumber和NationCode字段。

public class User
{
     public Telephone Phone{public get;private set;}
}
public class Telephone
{
    public string  PhoneNumber {get;set;}
     public string NationCode  {get;set;}
}
  • Aggregate & Aggregate Root(聚合,聚合根):

聚合是業務的最小工作單元,有時,一個實體就是一個小聚合,而為聚合對外提供訪問機制的對象,就是聚合根。

在領域驅動設計中,識別聚合也是一件非常重要的工作,有一組系統的方法論可以為我們提供參考。

當然,事實上識別領域對象,包括且不限定於識別聚合、值對象、實體識別該對象的行為或(方法)本身是一件需要經驗完成的工作,有時需要UML建模方法的廣泛參与。

有時,我們會習慣於通過屬性賦值完成梭代碼的過程,從而造成領域行為流失在業務邏輯層的問題,那麼或許可以採取這樣的方法:

1、對象的創建,使用構造函數賦值,或工廠方法創建。

2、將所有對於屬性的訪問級別都設置為

public string Phone{public get;private set;}

然後再通過一個綁定手機號碼的方法,來給這個對象設置手機號碼。

public string BindPhone(string phone)
{
}

將所有一切涉及到對Phone的操作,都只能通過規定的方法來賦值,這樣可以實現我們開發過程中,無意識的通過屬性賦值,可能導致的“領域行為”丟失的現象發生。
這種方式可以使得對對象某些屬性的操作,只能通過唯一的入口完成,符合單一職責原則的合理運用,如果要擴展方法,可以使用開閉原則來解決。

但是,採用這種方式,得盡量避免出現:SetPhone(string phone) 這樣的方法出現,畢竟這樣的方法,其實和直接的屬性賦值,沒有任何區別。

  • Repository(倉儲)

倉儲封裝了一系列對象數據庫操作的方法,完成對象從數據庫到對象的轉換過程。在領域驅動設計中,一個倉儲往往會負責一個聚合對象從數據庫到創建的全過程。

  • Domain Service(領域服務)

領域服務就是“實幹家”,那些不適合在領域對象中出現,又不屬於對象數據庫操作的方法,又與領域對象息息相關的方法,都可以放到領域服務中實現。

  • Specification(規格定義)

規範模式是一種特殊的軟件設計模式,通過使用布爾邏輯將業務規則鏈接在一起,可以重新組合業務規則。

實際上,它主要用於為實體或其他業務對象定義可重用的過濾器。

3. 其他基礎設施(EntityFrameworkCore,Web.Core,Web.Host項目)

EntityFrameworkCore負責定義數據庫上下文和對EFCore操作的一系列規則、例如種子數據的初始化等。

Web.Core:定義了應用程序的外觀和接口。雖然從表面上看,Web.Core定義了作為Web訪問入口的控制器方法和登錄驗證的邏輯,看起來像是用戶表現層的東西,但是仔細想想,這些東西,何嘗不是一種基礎設施?

Web.Host:定義WEB應用程序的入口。

總結

本文簡述了ABP框架的前後端項目的分層結構,通過了解這些結構,將有助於我們在後續的實戰中更快入手,為應用開發插上翅膀。

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

【其他文章推薦】

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

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

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

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

新北清潔公司,居家、辦公、裝潢細清專業服務