• <ins id="pjuwb"></ins>
    <blockquote id="pjuwb"><pre id="pjuwb"></pre></blockquote>
    <noscript id="pjuwb"></noscript>
          <sup id="pjuwb"><pre id="pjuwb"></pre></sup>
            <dd id="pjuwb"></dd>
            <abbr id="pjuwb"></abbr>
            隨筆 - 17  文章 - 48  trackbacks - 0
            <2025年5月>
            27282930123
            45678910
            11121314151617
            18192021222324
            25262728293031
            1234567

            常用鏈接

            留言簿(3)

            隨筆檔案

            搜索

            •  

            最新評論

            閱讀排行榜

            評論排行榜

            上一篇提到當訪問的頁表和頁不在內存中時會觸發 Page Fault 異常,操作系統需要在異常處理函數中分配內存頁并設置好相應的分頁表項。異常是一種中斷類型,注冊異常處理函數就是注冊中斷處理函數,中斷處理函數注冊在一個叫 IDT(Interrupt Descriptor Table) 的地方。

            IDT

            中斷處理函數在實模式下注冊在 IVT(Interrput Vector Table) 中,在保護模式下注冊在 IDT(Interrupt Descriptor Table) 。IDT是包含 256 項的表,表項的結構如下:
                struct idt_entry
                {
                    uint16_t offset_0;
                    uint16_t selector;
                    uint8_t zero;
                    uint8_t type_attr;
                    uint16_t offset_1;
                };
            其中 selector 是 GDT 的代碼段選擇器,offerset_0 和 offset_1 分別表示中斷處理函數 offset 地址的 0~15bits 和 16~31bits ,type_attr 的結構如下:
                  7                           0
                +---+---+---+---+---+---+---+---+
                | P |  DPL  | S |    GateType   |
                +---+---+---+---+---+---+---+---+
            P表示是否存在,DPL 表示描述符的最低調用權限,GateType 定義了中斷類型,32 位的中斷類型分別是:
            Interrupt Gate 和 Trap Gate 相似,區別在前者執行中斷處理函數前后會自動關閉和開啟中斷。
            準備好 IDT ,設置好 IDTR 寄存器就把 IDT 都設置好了。IDTR 寄存器結構如下:
                struct idtr
                {
                    uint16_t limit;
                    struct idt_entry *base;
                };
            limit 是整個表的大小 -1 字節,base 指向 IDT 表,設置 IDTR 寄存器的指令是 lidt。

            異常和硬件中斷

            了解 IDT 的結構了之后,我們可以設置異常和硬件中斷的 ISR(Interrupt Service Routine)。對于異常,我們只要知道有哪些異常會觸發,觸發的邏輯是什么樣,實現合適的異常處理函數即可(這里是異常列表)。對于硬件中斷,需要通過一個硬件完成—— PIC(Programmable Interrupt Controller)
            PIC 分為 Master 和 Slave ,每個 PIC 都有一個命令端口和一個數據端口,通過這兩個端口可以讀寫 PIC 的寄存器。每個 PIC 都可連 8 個輸入設備,x86下 Slave 需要通過 line 2 連接到 Master 上才能響應輸入設備,連接的輸入設備有中斷請求的時候會產生 IRQ(Interrupt Request),Master 產生 IRQ 0 ~ IRQ 7,Slave 產生 IRQ 8 ~ IRQ 15。保護模式下可以設定 PIC 產生的中斷對應的 ISR 所在 IDT 中的 offset,通常設置為從 0x20 開始,到 0x2F 結束(0x0 到 0x1F 被異常占用)。
            PIC 的端口號如下表:

            PIC 產生的標準 IRQ 如下表:

            PIC 初始化的時候,要設置 Master 和 Slave 通過 line 2 相連,同時設置好 IRQ 對應的 ISR 在 IDT 中的起始中斷號。PIC 提供一個 IMR(Interrupt Mask Register) 寄存器來標識中斷是否屏蔽,設置 bit 位會屏蔽對應的 IRQ。當 IMR 未設置,并且 CPU 的中斷打開,如果有設備中斷請求發生,那么 ISR 將會執行。ISR 執行完畢之后要通知 PIC 中斷處理完成,需要向 PIC 的命令端口寫入一個 EOI(End Of Interrupt) 命令(0x20),中斷請求如果來自 Slave,那么需要先往 Slave 命令端口寫入 EOI,再向 Master 命令端口寫入 EOI。

            Spurious IRQs

            由于 CPU 與 PIC 之間的競爭條件可能會產生 IRQ 7(Master 產生) 和 IRQ 15(Slave 產生) 的 Spurious IRQs。為了處理這種情況,我們要知道什么時候是無效的 IRQ,通過判斷 IRR(Interrupt Request Register) 寄存器的值可以獲知哪些 IRQ 發生了,這個寄存器的每個 bit 表示相應的 IRQ 是否發生。在 IRQ 7 和 IRQ 15 的 ISR 中先讀取 IRR,然后判斷對應的 bit 位是否被設置,如果沒有設置,那么表示當前是一個 Spurious IRQ,不需要處理,也不需要寫入 EOI,直接返回即可(如果是 Slave PIC 產生的,需要往 Master PIC 寫入 EOI,由于 Master 不知道 Slave 產生的 IRQ 是不是 Spurious 的)。

            PIT

            現代操作系統都有搶占式多任務能力,通常是通過設置一個硬件 Timer,一個進程的執行時間到了之后切換成另一個進程執行,這個硬件 Timer 是 PIT(Programmable Interval Timer)。PIT 有多個 channel 和多種工作 mode,其中 channel 0 連接到 PIC 會產生 IRQ 0,mode 2 和 mode 3 是常用的工作模式。操作系統初始化的時候設置好 PIT,同時設置好 PIT 產生的 IRQ 0 的 ISR,在這個 ISR 中操作系統就可以執行多任務的調度。

            中斷處理結束

            IDT 中設置的 ISR 返回時不能使用普通的函數返回指令 ret,需要使用一條特殊的返回指令 iret。在了解了這些之后,我們有了響應外部設備的能力,可以接入外部輸入設備了,下一步接入鍵盤。
            posted @ 2015-05-05 10:03 airtrack 閱讀(2568) | 評論 (0)編輯 收藏
            上一篇從 Bootloader 開始到內核載入使用的都是平坦內存,即所有地址對應實際的物理地址。現代操作系統都使用分頁來管理內存,分頁可以讓每個進程都有完整的虛擬地址空間,進程間的虛擬地址空間相互隔離以提供頁層級的保護。另外分頁可以讓物理內存少于虛擬地址空間,同時可以使用磁盤存儲暫時未使用的內存頁,提供更多的「內存」。

            分頁

            分頁通過 CPU 的 MMU(Memory Management Unit) 完成,MMU 通過當前的分頁表完成虛擬地址到物理地址的轉換。在 x86 下 MMU 通過兩級分頁表(也可以開啟三級)完成地址轉換,這兩級分別是頁目錄(Page Directory)和頁表(Page Table)。在 x86 下,由 cr3 寄存器存儲頁目錄的地址(物理地址),頁目錄和頁表都包含 1024 項,每項 4 字節,因此頁目錄和頁表大小為 4KB ,按照 4KB 一頁的話,剛好占用一頁。
            MMU 將虛擬地址轉換成物理地址的方式是,取虛擬地址的 22~31bits 表示頁目錄的下標,獲得頁目錄項定位到頁表,再取 12~21bits 表示頁表的下標,獲得頁表項定位到頁,最后取 0~11bits 表示頁偏移。頁目錄項和頁表項的下標分別用 10bits 表示,剛好最大 1024 項,頁內偏移用 12bits 表示,剛好 4KB。
            頁目錄項結構如下:
            其中 S 表示頁大小是 4KB 還是 4MB,P 表示頁表是否在內存中,如果在內存中,那么 12~31 bits 存儲了 4KB 對齊的頁表地址(同樣是物理地址),其它 bit 的含義請參考這里
            頁表項結構如下:
            同樣的,P 表示此頁是否在內存中,如果在內存中,12~31 bits 存儲了頁的地址。
            我們知道了頁目錄和頁表的結構,準備好頁目錄和頁表,就可以開啟分頁了,開啟分頁只需把頁目錄地址放到 cr3 寄存器中,并把 cr0 的最高 bit 置 1。通過頁目錄項,我們可以發現頁表不需要都存在內存當中,當訪問一個虛擬地址,它對應的頁表或者頁不存在內存中時會觸發 Page Fault 異常,我們可以在異常處理函數中完成頁表或者頁的分配,理論上開啟分頁只需要準備好頁目錄。

            分頁前后

            準備好頁目錄頁表,設置 cr3 和 cr0,開啟了分頁之后,內核的所有地址都變成了虛擬地址,所有的地址都要通過 MMU 映射到物理地址再訪問內存。這一變化是需要小心注意的,開啟分頁前,訪問的所有地址是物理地址,開啟分頁之后,所有的地址都變成了虛擬地址,因此,如果分頁由內核來完成,那么內核就需要考慮到前后的變化,即有一部分代碼運行在物理地址下,其它代碼都運行在虛擬地址下;如果分頁由 Bootloader 完成,那么 Bootloader 需要注意這個變化,并正確跳轉到內核,讓內核完整運行在虛擬地址下。
            上一篇我把內核展開到從 0x100000 開始的物理內存中,編譯鏈接內核的時候也把代碼段的地址指定到 0x100000 的地址。開啟分頁之后,內核一般運行在高地址(比如 Linux 內核地址從 0x80000000 開始,Windows 從 0xC0000000 開始),而內核同樣是展開到從 0x100000 開始的物理內存中。我選擇把內核的虛擬地址鏈接到從 0xC0100000 開始,并把這個虛擬地址映射到 0x100000 的物理地址,開啟分頁之前運行的代碼,凡是涉及到地址的操作,我都會把虛擬地址調整為物理地址再操作,開啟分頁之后,所有虛擬地址就可以正常運行了。

            物理內存管理

            操作系統采用分頁方式管理內存,因此物理內存的管理也需按照頁的方式管理,在 Page Fault 異常觸發時,在異常處理函數中分配新的物理頁并把它映射到分頁表中。這里牽涉到空閑物理內存頁的分配和釋放,我們很容易想到一種直觀的方法,把所有空閑內存頁用鏈表串聯起來,分配釋放一頁只需對鏈表進行操作。這種方式管理對進程的物理頁分配簡單有效,但是對內核本身使用的內存分配釋放會導致內存利用率不高,因為這種方式管理的最大連續內存是一頁,而內核中經常會分配大對象,連續多頁的物理內存有更好的利用率。Linux 采用 Buddy memory allocation 方式管理物理內存,使用 Slab/Slub 管理內核對象的分配釋放。
            我的實現也采用 Buddy 方式管理物理內存,把空閑內存頁用多層級的 Buddy 方式管理,分別是 order 0 ~ order 10,表示 2^order 頁連續內存頁塊,即 order 0 管理單頁的空閑內存塊,order 10 管理連續 1024 頁的空閑內存塊。分配內存時,算出最佳的 order,在相應的 order 層級里分配一塊內存塊,如果當前 order 中沒有可用的空閑內存塊,就向 order + 1 層級中借一塊,并把借來的空閑內存塊平分成 2 塊 order 層級的空閑內存塊,其中一塊當作分配結果返回,另一塊放入到 order 層級中待以后分配使用。當第 order 塊的內存使用完釋放時,把這塊釋放的內存塊放入 order 層級時,判斷與它相連的同樣大小的內存塊是否在 order 層級中,如果存在,把它和它的 Buddy 合并成一個 order + 1 的內存塊放入到 order + 1的層級中。

            內存管理器初始化之前

            在內存管理初始化之前,內核沒有動態內存分配能力,因此很多時候我們需要使用靜態全局變量。內存管理器初始化時,可能會使用到動態內存分配,這就出現雞與蛋的問題,為了解決這個問題,通常會實現一個簡單的 Boot Allocator 用在內存管理器初始化之前分配動態內存。我的實現是從內核展開的末尾位置開始建立一個只分配不釋放的 Boot Allocator,等到內存管理器初始化完成之后,Boot Allocator 的使命便完成了。
            另外還有一個問題,我們管理物理內存,需要知道安裝了多少物理內存,因此我們要探測安裝了多少物理內存,這里介紹了幾種探測方法,我使用的是 BIOS 的 INT 0x15, EAX = 0xE820 函數,它由 Bootloader 調用完成,最后通過參數把它傳遞給操作系統內核。
            posted @ 2015-04-27 12:53 airtrack 閱讀(3423) | 評論 (0)編輯 收藏
            2014年的最后一個星期用Rust寫了一個Tunnel,代碼放在GitHub上。主要原因是VPN在12月開始極不穩定,其次是VPN用起來不爽,每次下東西都要關VPN,而用ssh -D時偶爾又會斷開,最后干脆自己寫一個(其實年初就想寫,因為買了VPN就不想折騰了)。

            編譯和使用

            現代語言一般都自帶編譯工具,不用折騰make cmake等東西,Rust官方提供了Cargo,所以編譯很簡單,安裝好Cargo,然后到源碼目錄下Cargo build就完成了。

            編譯完成得到兩個可執行文件,分別是:client, server,server啟動在服務器上,client啟動在本機并綁定到地址127.0.0.1:1080,瀏覽器由代理插件通過SOCKS v5協議連接這個地址即可。

            Tunnel邏輯結構

            下面是邏輯圖:
            Client和Server之間通過一條TCP鏈接相連,客戶端每收到一個TCP請求就開一個port處理,同時在Server上有一個port與之對應,這樣就在Client和Server之間建立了一個會話層,這個TCP鏈接的數據全部都在對應的port里傳輸。

            Tunnel本身跟SOCKS v5不相關,為了讓瀏覽器代理能連上,Client提供了SOCKS v5中最簡單的NO AUTHENTICATION TCP方法,即無用戶名和密碼的TCP代理。

            Client和Server之間傳輸的數據都加了密,加密算法是Blowfish,工作模式是Counter Mode,client和server啟動時的參數Key即加密算法的Key。

            Rust的使用感受

            以前雖有關注Rust,卻從沒用Rust寫過代碼,主要是還未發布1.0,語法不穩定,最近1.0快有眉目了,可以用來寫寫小東西了。因為有Haskell的基礎,所以上手Rust對我來說沒什么難度。

            Rust提供了ADT(Algebraic Data Type), Pattern Matching, Traits,語法表達能力很強,同時也提供了macro,可自定擴展語法,進一步加強了語法表達能力。自動內存管理也讓程序更安全,不過由此也帶來一些語法表達能力的削弱,比如需要在函數返回的時候自動調用socket.close_read,通常可以定義一個struct,并讓這個struct impl trait Drop,在結構體銷毀的時候調用socket.close_read,又因為socket.close_read需要mut的socket引用,而mut的引用只能borrow一次,所以這個struct一旦borrow了socket的mut引用,之后再調用這個socket的mut函數就會報錯,一個workaround的方法就是struct保存socket的一份拷貝(socket本身通過引用計數管理),雖然可行,但是總感覺有些重了,僅僅為寫起來方便的一個問題引入了一次引用計數對象的拷貝。同時也會產生一個警告,由于那個struct的對象沒有被使用過。

            Rust編譯器報錯信息很詳細友好,運行時依賴小,Tunnel編譯出來的的client和server都可以在其它機器上直接運行。其它方面主要是API文檔跟不上,最新文檔上有的函數,編譯器編譯可能報錯,函數已經不存在了(剛剛去看了看最新的文檔,std::io變成了std::old_io)。庫方面,雖然Cargo倉庫里有一些第三方庫,但是總體數量還不多。
            posted @ 2015-02-03 21:03 airtrack 閱讀(3495) | 評論 (0)編輯 收藏

            Bootloader

            我們知道計算機啟動是從BIOS開始,再由BIOS決定從哪個設備啟動以及啟動順序,比如先從DVD啟動再從硬盤啟動等。計算機啟動后,BIOS根據配置找到啟動設備,并讀取這個設備的第0個扇區,把這個扇區的內容加載到0x7c00,之后讓CPU從0x7c00開始執行,這時BIOS已經交出了計算機的控制權,由被加載的扇區程序接管計算機。
            這第一個扇區的程序就叫Boot,它一般做一些準備工作,把操作系統內核加載進內存,并把控制權交給內核。由于Boot只能有一個扇區大小,即512字節,它所能做的工作很有限,因此它有可能不直接加載內核,而是加載一個叫Loader的程序,再由Loader加載內核。因為Loader不是BIOS直接加載的,所以它可以突破512字節的程序大小限制(在實模式下理論上可以達到1M)。如果Boot沒有加載Loader而直接加載內核,我們可以把它叫做Bootloader。
            Bootloader加載內核就要讀取文件,在實模式下可以用BIOS的INT 13h中斷。內核文件放在哪里,怎么查找讀取,這里牽涉到文件系統,Bootloader要從硬盤(軟盤)的文件系統中查找內核文件,因此Bootloader需要解析文件系統的能力。GRUB是一個專業的Bootloader,它對這些提供了很好的支持。
            對于一個Toy操作系統來說,可以簡單處理,把內核文件放到Bootloader之后,即從軟盤的第1個扇區開始,這樣我們可以不需要支持文件系統,直接讀取扇區數據加載到內存即可。

            實模式到保護模式

            我們知道Intel x86系列CPU有實模式和保護模式,實模式從8086開始就有,保護模式從80386開始引入。為了兼容,Intel x86系列CPU都支持實模式。現代操作系統都是運行在保護模式下(Intel x86系列CPU)。計算機啟動時,默認的工作模式是實模式,為了讓內核能運行在保護模式下,Bootloader需要從實模式切換到保護模式,切換步驟如下:
            1. 準備好GDT(Global Descriptor Table)
            2. 關中斷
            3. 加載GDT到GDTR寄存器
            4. 開啟A20,讓CPU尋址大于1M
            5. 開啟CPU的保護模式,即把cr0寄存器第一個bit置1
            6. 跳轉到保護模式代碼
            GDT是Intel CPU保護模式運行的核心數據結構,所有保護模式操作的數據都從GDT表開始查找,這里有GDT的詳細介紹。
            GDT中的每一個表項由8字節表示,如下圖:


            其中Access Byte和Flags如下圖:


            這里
            是詳細說明。
            GDTR是一個6字節的寄存器,有4字節表示GDT表的基地址,2字節表示GDT表的大小,即最大65536(實際值是65535,16位最大值是65535),每個表項8字節,那么GDT表最多可以有8192項。
            實模式的尋址總線是20bits,為了讓尋址超過1M,需要開啟A20,可以通過以下指令開啟:
                in al, 0x92
                or al, 2
                out 0x92, al
            把上述步驟完成之后,我們就進入保護模式了。在保護模式下我們要使用GDT通過GDT Selector完成,它是GDT表項相對于起始地址的偏移,因此它的值一般是0x0 0x8 0x10 0x18等。

            ELF文件

            Bootloader程序是原始可執行文件,如果程序由匯編寫成,匯編編譯器編譯生成的文件就是原始可執行文件,也可以使用C語言編寫,編譯成可執行文件之后通過objcopy轉換成原始可執行文件,這篇文章介紹了用C語言寫Bootloader。
            那么內核文件是什么格式的呢?跟Bootloader一樣的當然可以。內核一般使用C語言編寫,每次編譯鏈接完成之后調用objcopy是可以的。我們也可以支持通用的可執行文件格式,ELF(Executable and Linkable Format)即是一種通用的格式,它的維基百科
            ELF文件有兩種視圖(View),鏈接視圖和執行視圖,如下圖:



            鏈接視圖通過Section Header Table描述,執行視圖通過Program Header Table描述。Section Header Table描述了所有Section的信息,包括所在的文件偏移和大小等;Program Header Table描述了所有Segment的信息,即Text Segment, Data Segment和BSS Segment,每個Segment中包含了一個或多個Section。
            對于加載可執行文件,我們只需關注執行視圖,即解析ELF文件,遍歷Program Header Table中的每一項,把每個Program Header描述的Segment加載到對應的虛擬地址即可,然后從ELF header中取出Entry的地址,跳轉過去就開始執行了。對于ELF格式的內核文件來說,這個工作就需要由Bootloader完成。Bootloader支持ELF內核文件加載之后,用C語言編寫的內核編譯完成之后就不需要objcopy了。

            為什么寫操作系統

            首先是興趣,在現在這個時代,寫操作系統幾乎沒有實用價值,只能是一個Toy,在寫一個Toy OS時,可以學習掌握很多知識,并把這些知識貫穿實用起來。操作系統是一個復雜的系統,牽涉到的東西很多,我相信寫操作系統可以幫助理解現代操作系統及其它底層知識。我目前才剛開始寫,代碼放在Github上。
            posted @ 2014-10-30 19:13 airtrack 閱讀(3354) | 評論 (1)編輯 收藏

            去年花了兩三個星期的業余時間實現了基于DFA的正則引擎( ),時間一晃就過去一年,工作繁忙,興趣面廣,前途未卜什么的耗費太多精力,最近兩三個月抽了點時間寫了基于NFA的正則引擎,代碼放在Github

            正則引擎常見的實現方法

            正則的常見實現方式有三種:DFA、Backtracking、NFA:

            • DFA是三種實現中效率最高的,不過缺點也明顯,一是DFA的構造復雜耗時,二是DFA支持的正則語法有限。在早期正則被發明出來時,只有concatenation、alternation、Kleene star,即"ab" "a|b" "a*",DFA可以輕松搞定。隨著計算機的發展,正則像所有其它語言一樣發展出各種新的語法,很多語法在DFA中難以實現,比如capture、backreference(capture倒是有論文描述可以在DFA中實現)。

            • Backtracking是三種實現中效率最低的,功能確是最強的,它可以實現所有后面新加的語法,因此,大多數正則引擎實現都采用此方法。因為它是回溯的,所以在某些情況下會出現指數復雜度,這篇文章有詳細的描述。

            • NFA(Thompson NFA)有相對DFA來說的構造簡單,并兼有接近DFA的效率,并且在面對Backtracking出現指數復雜度時的正則表達式保持良好的性能。

            NFA-based的實現

            這里描述的NFA是指Thompson NFA。Thompson NFA實現的核心是對于正則表達式多個可能的匹配并發的向前匹配,此過程是在模擬DFA運行。比如對于正則表達式"abab|abbb"匹配字符串"abbb":

            • Backtracking的匹配過程是取正則的第一個子表達式"abab"匹配,前兩個字符匹配成功,匹配第三個字符的時候失敗,這時引擎回溯選擇第二個子表達式"abbb"匹配,最終匹配成功。

            • Thompson NFA是同時取兩個子表達式"abab"和"abbb"匹配,前兩個字符匹配時,兩個子表達式都能匹配成功,當匹配第三個字符時,子表達式"abab"匹配失敗,因此丟棄,"abbb"匹配成功接著匹配,最終匹配成功。

            上面是一個簡單的例子,沒有出現"*" "+" "{m,n}"這種復雜的metacharacters,在處理這種repeat的metacharacter時Thompson NFA優勢更加明顯。

            在實際復雜的正則表達式中,NFA構造是必然會產生一堆epsilon邊,這在第二篇文章中有描述。上面描述Thompson NFA實際是在模擬DFA運行,在每個字符匹配完成之后需要跳過epsilon邊得到后面要匹配的并發的狀態集合,這樣持續的并發匹配下去,當字符串匹配完成時只要有一個達到了接受狀態,就是匹配成功,若這個集合為空,那表示匹配失敗。

            在我的實現中,構造了一組狀態和由這組狀態加epsilon邊集合構造的有向圖,每個狀態有自己的狀態類型,分為兩種:

            • 一種是匹配狀態類型,即用來匹配字符的狀態,若字符匹配成功,則進入下一個狀態;

            • 一種是操作狀態類型,即不匹配字符的狀態,在每個字符匹配結束之后若到達這些狀態,則會進行相應的操作,比如repeat狀態,記錄匹配計數,并判斷匹配計數是否完成再決定是否進入的下面的狀態。

            repeat是一種會分化的狀態,達到最小匹配次數時,可以接著往下走,也可以繼續重復匹配repeat的子正則表達式,這樣就分化成兩條線了,并且每條線都帶有自己的狀態數據,因此,我的實現中引入的thread來表示一條匹配線,里面有狀態數據。

            Match和Search

            狀態構造完成了之后,就要開始匹配了。匹配有兩種,一種是match,即一個正則表達式能否完整匹配一個字符串,若完整匹配則匹配成功;另一種是search,要在一個字符串中或者一塊buffer中查找每個滿足的匹配。這里就有個問題,從第一個字符開始匹配,匹配了幾個字符之后發現匹配失敗了怎么辦呢?回退到第二個字符重新匹配?我們知道對于普通的字符串查找,有KMP算法可以保證不回退字符(其實KMP算法的預處理就是構造DFA),或者有Boyer-Moore算法盡量回退少的字符個數。對于正則這種復雜的匹配該怎么辦呢?從上面的Thompson NFA的描述可以知道匹配過程是多條線并發匹配,因此可以構造一個始終產生一條新線的狀態,若匹配在前面的線失敗被丟棄之后,后面的新線始終可以補上,這樣查找的過程就不再需要回退字符了。

            我的實現中,狀態構造完成后是這樣的:

                // |-----|--------|---------------|-----|-------------|
                
            // | any | repeat | capture begin |  | capture end |
                
            // |-----|--------|---------------|-----|-------------|

            用repeat-any來產生新的匹配線。若在match模式下,則從第三個狀態開始匹配,不會產生新的匹配線,一旦匹配過程失敗了就失敗了。

            結語

            正則表達式語法一直在擴展,新的語法有些很難在DFA和NFA中實現,而在Backtracking中的實現又是以犧牲性能為代價。因此有些正則表達式實現會結合多種實現方式,判斷正則表達式的類型選擇不同的引擎,比如普通字符串加上一些簡單的正則語法采用DFA引擎匹配,或者只有普通字符串的匹配可以用Boyer-Moore算法,畢竟Boyer-Moore算法在普通文本查找中要優于KMP算法:),對于復雜的正則表達式采用Backtracking,甚至有些正則引擎使用JIT來加速。

            posted @ 2014-09-15 19:04 airtrack 閱讀(3184) | 評論 (0)編輯 收藏

            GC的分類

            通常情況下GC分為兩種,分別是:掃描GC(Tracing GC)和引用計數GC(Reference counting GC)。其中掃描GC是比較常用的GC實現方法,其原理是:把正在使用的對象找出來,然后把未被使用的對象釋放。而引用計數GC則是對每個對象都添加一個計數器,引用增加一個計數器就加一,引用減少一個計數器就減一,當計數器減至零時,把對象回收釋放。引用計數GC跟C++中的shared_ptr類似,自然也會存在循環引用問題。

            掃描GC(Tracing GC)是廣泛使用的GC方法,最簡單的實現方式是mark-sweep,即掃描所有存活的對象并mark,然后遍歷整個GC對象列表,把所有標記過的對象清除標記,把未標記過的對象釋放。如果GC使用的是mark-sweep方法,程序運行一段時間后觸發了GC,每次GC的時候會把當前程序中的所有對象都掃描一次,然后釋放未使用的對象。這對于分配GC對象少的程序來說沒有什么問題,當程序中存在大量分配GC對象時,每次啟動GC掃描所有對象的代價是很高的,又因為GC的過程通常是stop-the-world,所以高代價的GC會導致整個程序卡頓一段時間。對于這個問題,解決方法有增量GC(Incremental GC)和分代GC(Generational GC)。

            增量GC(Incremental GC)會把整個GC過程分成很多步(phase),每步的執行可以存在一定間隔運行程序本身,這就盡量把stop-the-world的時間變短,使得程序不會因為GC而導致延遲太大。Lua默認采用的是這種實現方法,Lua 5.2中也引入了分代GC作為備選GC方法。

            分代GC(Generational GC)把對象分成幾代(Generation),通常把GC分為兩種:Minor GC和Major GC。剛剛分配出來的對象屬于最年輕的一代,在一次GC過后把年輕代中存活的對象上升到年老的一代中。把只掃描年輕一代的對象以減少掃描對象數量的GC過程稱為Minor GC,只有在特定情況下才會啟動完整的Major GC。分代GC是基于在大多數程序中新創建的對象同時也是最快變成無效的對象的經驗設計的,對年輕代對象GC時,可以釋放大多數無效對象,存活下來的對象一般存活時間也會更長,因此把它們上升到下一代中以減少最這些對象的掃描。

            對于GC內存的管理,有移動和非移動之分。移動的就是把一次GC過后存活的對象compact到一起,使GC管理的內存保持連續,這里增加了一個移動對象的開銷,不過它也同樣帶來不少好處:分配釋放對象快和更快的序列遍歷(在CPU cache中及在同一個Virtual memory page中)。正因為它會把對象compact到一起,對象的地址就會發生變化,這也就導致一個明顯的缺點,不能使用指針引用GC對象。

            其它高級GC方法,比如.NET的background GC,幾乎不需要stop-the-world就可以在GC線程中完成GC,這種高科技的GC對于我這種初級人士基本屬于不可想象。

            初級分代GC設計

            了解了基本的GC方法之后,我為luna第二版實現了一個初級的分代GC,把對象分成三代:GCGen0,GCGen1,GCGen2:

               GCGen0是最年輕的一代,默認所有對象都是分配在這代中。
               GCGen1是年老的一代,在一次GC過后GCGen0代存活的對象會移動到這一代中。
               GCGen2是最老的一代,一般情況下用于存放編譯時分配的會長期存在的對象,比如函數及字符串常量。

            由于我在很多地方直接引用了GC對象的指針,為了簡單起見,我沒有在GC之后移動對象,而是對每個對象單獨分配釋放內存。每個對象都有Generation標記和GC標記以及一個用于指向跟自己屬于同代的GC對象的指針。

            Minor GC對GCGen0代對象mark-sweep,并把存活的對象移動到GCGen1代中。既然需要mark,自然需要對所有GCGen0代存活的對象標記,這通過對root對象的遍歷完成,root是指所有對象的引用入口,比如程序的棧和全局表。對于Minor GC的root對象遍歷最簡單的方法是跟Major GC的root遍歷完全一致,不過這樣的遍歷對于本來就是為了減少遍歷對象的Minor GC來說似乎不合,所以通常只對某一小塊root遍歷,比如只對棧上的對象遍歷,然后再把存活的對象保留不存活的對象釋放。

            Minor GC的root遍歷存在一個問題:假設只把棧上的對象作為root遍歷,會存在一些從GCGen0代分配出來的對象沒有被棧上的對象引用,而被全局表中的某個對象引用,或者其它某個非GCGen0對象引用了,這樣對GCGen0代sweep的時候可能會把這個存活的對象當做無效對象而釋放掉,這種操作自然也就會導致整個程序crash。于是為了控制root遍歷的范圍,又要解決這個問題,對非GCGen0對象引用GCGen0對象的時候,需要把這個非GCGen0的對象也加入到root遍歷列表中去。這時引入了barrier,對于非GCGen0對象引用GCGen0對象時,把這個非GCGen0的對象放到barrier列表中。

            Major GC是一個完整的GC,它遍歷所有的root并mark,并把所有的無效的對象都sweep釋放。

            GC啟動的時機

            GC什么時候啟動是一個需要仔細考慮的問題,由于我實現的GC并沒有自己管理內存(Lua也沒有自己管理內存,所有內存分配都通過realloc),所以我把GCGen0代和GCGen1代的對象數量作為啟動時機的衡量指標,當GCGen0和GCGen1的對象數量大于它們的閾值時,分別啟動Minor GC和Major GC。我覺得對象的數量比起內存占用大小(各種復雜的GC對象導致內存占用很難精確的統計,Lua的內存統計也不夠精確)更能反映GC時間的長短,如果兩者結合也許會更好。

            通過判斷GC對象個數超過閾值時啟動GC,同時需要在GC之后自動調整閾值大小。比如某些程序很快的達到GCGen0的閾值并在Minor GC之后有超過一半的對象還是存活的,這時需要把閾值調大,以減少GC啟動的次數,這個閾值也不能無限擴大,這不僅會導致一段時間內內存占用一直上升,也會導致一旦觸發GC所需掃描的對象數量太多,GC耗時太長,程序運行的延時增加。

            結語

            為了減少stop-the-world的時間,引入的各種方法都會讓GC實現難度加大。GC是一個復雜的東西,網上所能找到的資料文章似乎不太多,而有關GC的書,目前只發現《The Garbage Collection Handbook》(我還沒有看過),而這本書既沒有pdf也沒有kindle版,只能在美國Amazon上買紙質書。另外一個參考資料就是各個語言的實現源碼了。
            posted @ 2013-11-17 22:20 airtrack 閱讀(2634) | 評論 (1)編輯 收藏

            上一篇已經有近兩個月的時間了,這段時間事情煩(多),導致沒心情寫,現在爭取補上。


            生成epsilon-NFA

            epsilon-NFA是包含epsilon邊(空邊)的NFA,把簡單正則表達式轉換成epsilon-NFA的方法如下:

            正則表達式:”ab” 對應的epsilon-NFA是:


            正則表達式:”a|b”對應的epsilon-NFA是:


            正則表達式:”a*” 對應的epsilon-NFA是:


            這是最基本的3種正則表達式的NFA表示,其中a*在實際的正則表達式實現中通常生成的epsilon-NFA不是這樣的,因為有下面這些正則表達式存在:

            a{m}       重復a,m次
            a{m,n}     重復a,m到n次
            a{m,}      重復a,至少m次
            a+         重復a,至少1次
            a?         重復a,0次或1次

            所以對于a*表示重復至少0次的實現可以跟上面這些正則表達式采用相同方法的實現。

            按照這些生成規則就可以把正則表達式轉換成epsilon-NFA,我代碼中即把這些生成規則實現成一個AST的visitor。

             

            epsilon-NFA subset construction to DFA

            在生成了epsilon-NFA之后,通常會有很多epsilon的邊存在,也會有很多無用的state存在,所以通常需要把epsilon邊消除并合并state,這個過程采用的算法是subset construction,如下:

            subset construction:
            start_subset <- epsilon_extend(start_state)    // 把start_state通過epsilon擴展得到起始subset
            subsets <- { start_subset }                    // 初始化subsets
            work_list <- subsets                           // 初始化work_list
            while (!work_list.empty())
            {
                subset <- work_list.pop_front()
                for edge in epsilon-NFA                    // 取出NFA中的每條邊
                {
                    next_subset <- delta(subset, edge)     // 對subset中的每個state通過edge所到達的state的epsilon邊擴展得到next_subset
                    if (!subsets.exist(next_subset))       // 如果next_subset不存在于subsets中,則把這個next_subset加入到work_list中
                        work_list.push_back(next_subset)
                    map[subset, edge] = next_subset        // 構建subset到next_subset的邊映射
                    subsets.merge({next_subset})           // 把next_subset合并到subsets
                }
            }

            delta:
            next_subset <- { }    // 初始化next_subset為空集合
            for state in subset
            {
                // 取出next_state并將它通過epsilon邊擴展得到的subset合并到next_subset中
                next_state <- map[state, edge]
                if (next_state)
                    next_subset.merge(epsilon_extend(next_state))
            }

             

            這里面使用了epsilon_extend,它是把一個state的所有epsilon邊能到達的state構成一個集合,比如上面正則表達式a*對應的epsilon-NFA中的所有state的epsilon_extend是:

            epsilon_extend(1) –> { 1 }
            epsilon_extend(2) –> { 1, 2, 4 }
            epsilon_extend(3) –> { 1, 3, 4 }
            epsilon_extend(4) –> { 4 }

            對于一個epsilon-NFA來說,每個state的epsilon_extend是固定的,因此可以對epsilon-NFA中的每個state都求出epsilon_extend并保存下來,算法如下:

            epsilon_extend_construct:
            work_list <- { }
            // 為每個state初始化epsilon_extend集合
            for state in epsilon-NFA
            {
                epsilon_extend(state) <- { state }
                work_list.push_back(state)
            }
            while (!work_list.empty())
            {
                state <- work_list.pop_front()
                state_epsilon_extend <- epsilon_extend(state)
                // 把state通過epsilon所能到達的state的epsilon_extend
                
            // 合并到當前state的epsilon_extend
                for next_state in map[state, epsilon]
                    state_epsilon_extend.merge(epsilon_extend(next_state))
                // 如果當前state的epsilon_extend變化了之后
                
            // 把所有通過邊epsilon到達state的pre_state都加入到work_list中
                if (state_epsilon_extend.has_changed())
                {
                    for pre_state in epsilon_pre(state)
                        work_list.push_back(state)
                }
            }

             

            epsilon-NFA通過subset construction構造成完之后,并把構造的subsets中的subset轉換成DFA中的state,再把NFA中除epsilon邊之外的所有邊都轉換成DFA的邊,這樣就把DFA構造完成。


            DFA minimization

            從NFA構造完成DFA之后,這時的狀態數量一般不是最少的,為了減少最終生成的狀態機的狀態數量,通常會對DFA的state進行最小化構造,這個算法具體如下:

            minimization:
            // 把所有state劃分成accept的state集合和非accept的state集合
            state_sets <- { {accept_state(DFA)}, {non_accept_state(DFA)} }
            do
            {
                work_list <- state_sets
                old_state_sets_size <- state_sets.size()
                state_sets <- { }
                for state_set in work_list
                {
                    split_success <- false
                    for edge in DFA
                    {
                        // 如果edge可以把state_set拆分成兩個subset,那就把新拆分出來的
                        
            // 兩個subset合并到state_sets里面,并break繼續work_list中取出下一個
                        
            // state_set拆分
                        subset1, subset2, split_success <- split(state_set, edge)
                        if (split_success)
                        {
                            state_sets.merge({subset1, subset2})
                            break
                        }
                    }
                    if (!split_success)
                        state_sets.merge({state_set})
                }
            while (old_state_sets_size != state_sets.size())


            這里面的split是把一個state_set按edge劃分成兩個subset,即對于state_set中的每一個state都通過這條邊edge到達的state屬于不同的state_set時就把state_set拆分成兩個subset。首先把第一個state劃分到subset1中,從第二個state開始通過邊edge到達的state所屬的state_set和第一個state通過邊edge到達的state所屬的state_set為同一個的時候,把這個state劃分到subset1中,否則劃分到subset2中。

            這個算法就這樣依次把最初的兩個state_set(accept的state組成的set和非accept的state組成的set)劃分到不能再劃分為止,此時就把能合并的state都合并到了同一個state_set中,這時只需要把每個state_set轉換成最終狀態機中的state,即可完成DFA的最小化構造并轉換成狀態機。得到狀態機之后,就可以使用狀態機進行字符匹配了。

            posted @ 2013-09-01 23:25 airtrack 閱讀(1788) | 評論 (0)編輯 收藏

            實現正則表達式的想法很早就有,各種原因導致沒有做,最近花了點時間先實現了幾個簡單的正則語法,分別是concatenation、alternation和closure,其他語法及metacharacter等有時間了有想法了之后再擴展。

             

            這三種基本的語法分別是對應這樣的:

            concatenation: abc    表示匹配字符串abc

            alternation: abc|def   表示匹配字符串abc或者def

            closure: a*               表示匹配零個到多個a構成的字符串

             

            我們知道正則表達式最終需要轉換成自動機才能用來匹配字符串,我實現的正則通過如下幾個步驟把正則表達式轉換成自動機:

            正則表達式->Parse成AST->生成邊(字符)集合->生成NFA->NFA subset construction->轉換成DFA->DFA minimization

            最后用DFA minimization之后構造的自動機來匹配字符串。

             

            正則語法的分析

            一個正則表達式寫出來,要讓這個正則表達式匹配字符串等操作之前,我們先需要從正則表達式中提取需要的信息并在正則語法錯誤的時候提示錯誤,這個過程自然少不了parser。一個parser通常是從一個lexer里面獲取一個token,而正則表達式的token都是字符,那么lexer不需要做任何的分詞操作,只需要簡單的把字符返回給parser即可。

            那三種基本的正則語法對應的BNF為:

            re ::= alter
            re_base ::= char | char_range | '(' re ')'
            alter ::= alter_base alter_end
            alter_base ::= concat
            alter_end ::= '|' alter_base alter_end | epsilon
            concat ::= concat_base concat_end
            concat_base ::= re_base | closure
            concat_end ::= concat_base concat_end | epsilon
            closure ::= re_base '*'

            這個parser分析了正則表達式之后產生AST,AST的node類型為:

            class ASTNode
            {
            public:
                ACCEPT_VISITOR() = 0;
                virtual ~ASTNode() { }
            };
             
            class CharNode : public ASTNode
            {
            public:
                explicit CharNode(int c) : c_(c) { }
             
                ACCEPT_VISITOR();
             
                int c_;
            };
             
            class CharRangeNode : public ASTNode
            {
            public:
                struct Range
                {
                    int first_;
                    int last_;

                    explicit Range(int first = 0, int last = 0)
                        : first_(first), last_(last)
                    {
                    }
                };

                CharRangeNode() { }

                void AddRange(int first, int last)
                {
                    ranges_.push_back(Range(first, last));
                }
             
                void AddChar(int c)
                {
                    chars_.push_back(c);
                }
             
                ACCEPT_VISITOR();
             
                std::vector<Range> ranges_;
                std::vector<int> chars_;
            };
             
            class ConcatenationNode : public ASTNode
            {
            public:
                void AddNode(std::unique_ptr<ASTNode> node)
                {
                    nodes_.push_back(std::move(node));
                }
             
                ACCEPT_VISITOR();
             
                std::vector<std::unique_ptr<ASTNode>> nodes_;
            };
             
            class AlternationNode : public ASTNode
            {
            public:
                void AddNode(std::unique_ptr<ASTNode> node)
                {
                    nodes_.push_back(std::move(node));
                }
             
                ACCEPT_VISITOR();
             
                std::vector<std::unique_ptr<ASTNode>> nodes_;
            };
             
            class ClosureNode : public ASTNode
            {
            public:
                explicit ClosureNode(std::unique_ptr<ASTNode> node)
                    : node_(std::move(node))
            {
                }
             
                ACCEPT_VISITOR();
             
                std::unique_ptr<ASTNode> node_;
            };

            其中ASTNode作為AST的基類,并提供接口實現Visitor模式訪問ASTNode類型。

             

            字符(邊)集的構造

            AST構造好了之后,需要把AST轉換成NFA。語法中有[a-zA-Z0-9]這種字符區間表示法,我們可以用最簡單原始的方法轉換,就是把區間中的每個字符都轉化成相應的一條邊(NFA中的邊),這樣一來會導致字符區間越大,對應邊的數量會越多,使得對應的NFA也越大。因此,我們需要構造區間字符集合來減少邊的數量。

            比如正則表達式是:a[x-z]|[a-z]*e

            那么我們希望對應的字符集合是這樣:[a-a] [b-d] [e-e] [f-w] [x-z]

            這需要構造一個字符集,每次插入一個區間的時候,把新插入的區間與已存在的區間進行分割,初始時已存在的區間集為空,那么正則表達式a[x-z]|[a-z]*e的劃分步驟如下:

            已存在區間集合{},插入[a-a],得到{[a-a]}

            已存在區間集合{[a-a]},插入[x-z],得到{[a-a], [x-z]}

            已存在區間集合{[a-a], [x-z]},插入[a-z],得到{[a-a], [b-w], [x-z]}

            已存在區間集合{[a-a], [b-w], [x-z]},插入[e-e],得到{[a-a], [b-d], [e-e], [f-w], [x-z]}

            這個區間構造完成了之后,還需要在后面轉換成NFA邊的時候,根據字符區間查詢出在這個集合中,由哪幾個區間構成,比如:

            查詢區間[a-a],得到[a-a]

            查詢區間[x-z],得到[x-z]

            查詢區間[a-z],得到區間[a-a] [b-d] [e-e] [f-w] [x-z]

            在轉換成NFA時,集合中的每個區間都對應一條邊,這樣相對于每個字符對應一條邊,邊的數量不會太多。

            有了這么一個集合構造的類之后,把正則的AST中的字符信息提取出來構造出這么個集合即可,這樣只需要寫個visitor就完成了:

            class EdgeSetConstructorVisitor : public Visitor
            {
            public:
                explicit EdgeSetConstructorVisitor(EdgeSet *edge_set)
                    : edge_set_(edge_set)
                {
                }
             
                EdgeSetConstructorVisitor(const EdgeSetConstructorVisitor &) = delete;
                void operator = (const EdgeSetConstructorVisitor &) = delete;
             
                VISIT_NODE(CharNode);
                VISIT_NODE(CharRangeNode);
                VISIT_NODE(ConcatenationNode);
                VISIT_NODE(AlternationNode);
                VISIT_NODE(ClosureNode);

            private:
                EdgeSet *edge_set_;
            };

            邊集合構造完成之后,下一步就是生成NFA了。

            posted @ 2013-07-05 13:30 airtrack 閱讀(4415) | 評論 (3)編輯 收藏

            最近幾個月利用上下班的時間在學習Haskell,Haskell有不少讓人開闊思路的東西,也有不少看起來很美好,用起來不錯,但是讀起來費勁的東西。Haskell的語法學的差不多了之后,用Haskell寫了一個簡單的C++代碼行統計工具,寫過幾個版本,留下了兩個,一個是直接用模式匹配寫的,一個是山寨了一個極簡的parse combinator,然后用這個山寨的parse combinator寫了一個版本,代碼估計寫的都比較爛,以后進階學習之后有時間再改。這個統計工具并不是完整的處理C++的語法,也沒對在字符串和宏定義里面的"http://" "/*" "*/"做處理,因此對某些C++程序統計代碼行,可能不完全正確,但是基本可以用。


            data, type, newtype


            Haskell里面用data來定義數據類型,它可以是這樣:
             

            data Mode = ReadMode | WriteMode
            data Some = Some Int String
            data Thing = { a :: Int, b :: String }
            data Func a b = { func :: a -> b }

             

            第一行定義了一個Mode,包含ReadMode和WriteMode;

            第二行定義了一個普通數據類型Some,包含一個Int數據和一個String數據;

            第三行定義了一個普通數據類型Thing,包含類型為Int的a和類型為String的b;

            第四行定義了一個符合數據類型Func,里面有個函數類型為(a -> b)的數據func。


            第一種相當于C++中的enum class,第二種第三種相當于普通的struct數據,第二種和第三種的區別是第二種不能直接取到Int和String的數據,第三種可以通過a,b取到數據,第四種相當于C++的template class(struct),第四種寫成這樣來定義具體的數據類型:

            type IntStringFunc = data Func Int String

             

            type在這里定義了一個別名IntStringFunc類型,包含了一個函數類型是Int -> String的func的數據,這里的type相當于C++ 11的using別名,因為它還可以這樣寫:


            type IntBFunc b = data Func Int b


            在C++ 11中,using包含了typedef的功能,也支持了template class的類型typedef,如下:


            template <typename T, typename P>
            class SomeType;

            template <typename T>
            using SomeTypeInt = SomeType<T, int>;


            newtype定義的數據類型跟type類型,不過type定義的純粹是別名,別名類型跟原始類型是一致的,而newtype則定義的是一個wrapper,是一種新的數據類型,所以是newtype。newtype定義的類型是編譯時期的wrapper,Haskell保證沒有運行時期的開銷,newtype定義的跟data類似:

            newtype NewType a b = NewType { func :: a -> b }


            模式匹配


            上面說道data定義的第二種數據類型,包含Int和String的數據,但是不能直接取到這兩個數據,所以我們需要定義兩個函數來取其中的數據:


            some = Some 0 "test"  -- 定義一個數據,類型為Some

            -- 定義兩個函數用于獲取Some的數據
            getSomeInt Some i _ = i
            getSomeString Some _ s = s

            getSomeInt some  -- 0
            getSomeString some  -- "test"


            這里的getSomeInt和getSomeString函數都是采用模式匹配來實現,模式匹配就是把數據的結構直接寫在代碼中來匹配,然后取出想要使用的數據即可。


            Haskell里常用的Maybe數據類型是這樣定義的:


            data Maybe a = Nothing
                                 | Just a


            如果要取用Maybe里面的值,我們通常使用模式匹配來獲取數據,如下:


            useMaybe maybe =
                 case maybe of
                      Nothing -> …  -- Maybe的值是空
                      Just a -> …  -- 直接使用a即可

            useMaybe (Just 1)


            下面調用useMaybe的函數體內取到的a的值就是1。


            Haskell里的內置數據類型list,比如[1, 2, 3, 4],使用:可以把新的元素添加到list頭部,即:


            0 : [1, 2, 3, 4]  -- [0, 1, 2, 3, 4]


            這樣的特性同樣可以簡單的套用在模式匹配上面,如下:


            useList [] = 
            useList (x:xs) = … -- x是list里面的第一個元素,xs是list的尾部


            模式匹配可以很直觀的匹配數據的原始表示方式,并可以取出其中有用的值做其他操作,它是一個簡單直觀有效的操作數據的方式,甚至可以在嵌套很深的tuple數據里面直接取出想要的數據,而不用像C++那樣調用tuple::get之類的函數來取出其中的值,比如:


            getTupleValue (_, _, _, (_, _, (x:xs))) = … -- 取得x和xs數據


            Visitor模式和C++ template


            王垠說設計模式中值得一提的模式不多,其中之一的visitor模式是在模擬模式匹配。visitor模式通常是訪問獲取某個繼承類層次構成的一個樹形結構中的某個節點的具體類型,并對這種具體類型做某些操作,而模式匹配是可以從復雜的數據結構中直接取出想要的數據。


            C++的template meta-programming也可以看成是一種模式匹配,C++里面著名的Factorial求值是這樣的:


            template <int N>
            struct Factorial
            {
                 enum { value = N * Factorial<N - 1>::value };
            };

            template <>
            struct Factorial<0>
            {
                 enum { value = 1 };
            };

            int v = Factorial<10>::value;


            而這段代碼如果用Haskell寫是這樣的:


            factorial 0 = 1
            factorial n = n * factorial (n - 1)

            v = factorial 10


            C++中的模板參數就是用來做模式匹配的,每特化一種類型就可以匹配某種類型,然后對那種匹配的類型做相應的操作。C++的template meta-programming是編譯時期(編譯器運行期)的行為,所以它只能操作類型以及編譯時期能夠確定的值,而模式匹配是程序本身的運行期的行為。


            Currying


            Haskell的Currying是一個很有用的特性,但是我覺得這個特性濫用的話,也會讓程序代碼的可讀性降低不少。所謂Currying就是可以向一個多參數的函數傳遞比它所需的參數個數更少的參數后返回生成的一個新函數接受剩余的參數的函數。Haskell里的函數默認都是curried的,所以Haskell里面的函數可以隨意currying,比如:


            add :: Int -> (Int -> Int)  -- 一般寫成 Int -> Int -> Int
            add a b = a + b

            addOne :: Int -> Int
            addOne = add 1

            addOne 2 -- result: 3


            Currying的實現是使用的單參數的lambda構成的閉包(closure),add可以看成是接受一個Int參數返回一個函數,這個函數的類型是Int -> Int。


            Partial application


            Currying是一個從左到右部分傳參數的一個過程,也就是不會出現參數a還沒給,就給了具體的參數b的情況。如果確定要先給參數b,那么它是Partial application,如下:


            addTwo a = add a 2

            addTwo 1 -- result: 3

            (+ 2) 1 -- result: 3


            (+ 2)這種類似的用法可能會作為參數傳遞給另外一個函數。Partial application是一種更寬泛的概念,上面的Currying是一種Partial application。


            正如王垠所說的,如果一個函數接受了多個參數,但是這個函數在實際調用中被Currying了很多次,那最后生成的那個函數它到底接受幾個參數是不能很直觀的看明白的,比如:


            func a b c d e f = …

            do1 = func 1
            do2 = do1 2
            do3 = do2 3
            do4 = do3 4
            do5 = do4 5


            那當我們看到do5函數的時候,我們是很難判斷do5到底接受幾個參數,尤其是do5跟前面幾個doN函數不在同一個地方定義,很有可能do5只是傳遞給某個函數的參數,當然如果給每個函數都加上函數類型聲明會清晰許多。當Currying碰到了flip之后,那代碼的可讀性會降低更多,所以我覺得Currying是一個很有用的特性,但是如果被濫用的話,那代碼的可讀性會是一個問題。


            C++: function + bind


            C++中的function + bind其實是一種Partial application實現,比如:


            int Func(int a, int b, int c);

            std::function<int (intint)> f1 = std::bind(Func, 1, std::placeholders::_1, std::placeholders::_2);
            std::function<int (int)> f2 = std::bind(f1, std::placeholders::_1, 3);
            f2(2); // Func(1, 2, 3);


            我覺得C++的function + bind會比Currying的可讀性要好一些,畢竟我們可以完整看到f1和f2的函數類型,知道參數類型及個數和返回值,是有利于代碼的可讀性的,當然這里完全可以不寫出f1和f2的類型,采用auto,我們同樣可以從調用函數bind的placeholder的個數得知bind之后的function的參數個數,這樣我們可以不用看到函數Func的聲明,就知道需要傳幾個參數。function + bind跟Currying一樣會影響代碼的可讀性,如果嵌套的層次越多,可讀性就越差,所以使用這些特性的時候不要過度。


            typeclass


            Haskell用typeclass來表示一個concept,它是一組抽象函數的集合,一個滿足某個typeclass的數據類型,它就可以跟其他使用這個typeclass的函數或者數據類型組合使用。typeclass一般這么定義:


            class Monad m where
                 (>>=) :: m a -> (a -> m b) -> m b
                 (>>) :: m a -> m b -> m b
                 return :: a -> m a
                 fail :: String -> m a


            它定義了一個叫Monad的typeclass,這個typeclass的concept里有四個函數,分別是(>>=), (>>), return和fail,m是一個帶類型參數的數據類型。我們上面知道了Maybe是一個帶類型參數的data類型,它定義如下:


            data Maybe a = Nothing
                                 | Just a


            既然Maybe是一個帶類型參數的data,那它就滿足Monad typeclass中m的需求,因此可以把Maybe定義成Monad,如下:


            instance Monad Maybe where
                 (>>=) maybeA f =
                      case maybeA of
                           Nothing -> Nothing
                           Just a -> f a

                 (>>) maybeA maybeB = maybeA >>= (\_ -> maybeB)

                 return = Just

                 fail = error


            這里(\_ -> maybeB)定義了一個lambda,參數 _ 緊接 \,-> 后面則是函數體。函數(>>)和fail是可以作為默認實現放到class Monad的定義里面,而instance Monad的時候只需要實現(>>=)和return即可。


            class Monad m where
                 (>>=) :: m a -> (a -> m b) -> m b
                 (>>) :: m a -> m b -> m b
                 (>>) ma mb = ma >>= (\_ -> mb)
                 return :: a -> m a
                 fail :: String -> m a
                 fail = error


            對于內置list類型[a],也是帶有一個類型參數a,因此,我們同樣可以把[] instance成為class Monad,如下:


            instance Monad [] where
                 (>>=) (x:xs) f = (f x) ++ (xs >>= f)
                 (>>=) [] _ = []
                 return a = [a]


            函數(>>)和fail我們保留默認的實現即可。


            Monad


            上面實現的定義的typeclass就是Haskell著名的Monad,它是組合其他操作的一個基礎typeclass,是與no pure交互的一個重要媒介。一般情況下Monad有兩種,一種是數據wrapper,一種是action的wrapper。上面定義的Maybe Monad和list Monad都是數據類型的wrapper,它們實現了Monad定義的接口函數,我們還可以將其它data instance成Monad,只需要遵循了Monad的接口即可。


            我們知道Haskell的函數都是pure的,沒有任何狀態的函數,但是與現實世界交互必然需要影響或修改某種狀態,并且會需要順序執行某些操作以完成交互。我們把action操作封裝在一個data里面,并讓它instance Monad,為了讓前一個action的結果值作為某種狀態往下傳遞,Monad的(>>=)就是為了這個目的而存在的,(>>=) 函數的類型是 m a -> (a -> m b) -> m b,它的意思就是執行封裝在m a這個數據里面的action,然后把這個action的結果值做為參數傳遞給(>>=)的第二個參數(a -> m b),第二個參數是一個函數,這函數可以取用第一個參數的結果,再返回一個m b的數據,m b的數據也是一個action的封裝,這樣當一連串的(>>=)放到一起的時候,就可以把一個狀態值作為action的參數和結果值往下傳遞。


            從Monad的函數(>>)的實現我們可以看到,它把m a的action的結果值丟棄直接返回了m b,當一連串的(>>)放到一起的時候,其實就是讓一組action順序執行。通過(>>=)和(>>),可以把一組Monad action data組合起來。


            IO Monad


            IO Monad是一個把IO action封裝的data,我們可以使用IO Monad與外界進行輸入輸出交互,下面是一個"hello world":


            helloWorld = do
                 putStr "hello "
                 putStrLn "world"


            這里do語法糖其實就是用的Monad來實現,展開之后是這樣:


            helloWorld =
                 (putStr "hello ") >>
                 (putStrLn "world")


            由(>>)函數確定(putStr "hello ")和(putStrLn "world")需要是同一個Monad類型,我們可以查詢到putStr和putStrLn的類型是String -> IO (),那么(putStr "hello ")和(putStrLn "world")的類型都是IO (),helloWorld函數把兩個IO ()的action數據順序組合起來生成一個新的IO (),當這個helloWorld IO action被執行的時候,它會依次執行封裝在它里面的IO action。我們可以把helloWorld IO action放到Main函數里面然后編譯執行,也可以直接在ghci里面執行。


            我們可以自己定義某種data再instance Monad,這樣可以構成一組data combination,可以實現任意的action combine。我山寨的極簡的parse combinator的數據類型定義如下:


            newtype Parser a = Parser {
                 runP :: State (ByteString, Context) a
            } deriving (Monad, MonadState (ByteString, Context))


            這里Parser帶一個類型參數a,deriving (Monad, MonadState (ByteString, Context))表示編譯器自動instance Monad和instance MonadState (ByteString, Context)。有了這個Parser之后,可以寫出簡單的幾個combinator,然后使用這幾個combinator組合成更加復雜的,組合的過程就是利用了Monad的組合能力。當所需的combinator都實現了好了之后,可以最終實現一個Parser a來分析整個C++文件:


            file = repeatP $ spaceLine <||> normalLine


            file就把分析整個C++文件所需的操作都combine到了一起,有了這個分析整個文件的Parser a之后,需要把它跑起來,那就需要定義下面這個函數:


            runParse :: Parser a -> ByteString -> (a, (ByteString, Context))
            runParse p b = runState (runP p) $ (b, emptyContext)


            這個函數接受一個Parser a和一個文件內容ByteString作為參數,把整個Parser a封裝的action用于分析文件內容,再產生一個分析結果。


            這里的file,它是一個一個小的combinator構成的,每個combinator是一個action加上它所需數據構成一個“閉包”再存放到Parser a的data里面,其實可以認為實現了Monad的數據類型是一個“閉包”的載體。在其它語言里,我們可以使用閉包來實現combinator,我記得兩年半前,我使用lua的閉包實現了一組游戲副本內容玩法操作的combinator,這些閉包自由組合在一起之后就能完成一個副本中所需的玩法操作。


            Monad transformer


            一種Monad類型只能封裝和組合一種action操作,而與外界交互的時候,很多時候一種Monad類型是不夠的,為了讓多種Monad類型組合在一起,就需要定義Monad transformer,它跟Monad一樣也是一個數據類型,不同的是它接受至少兩種類型參數,其中一種就是Monad的類型,這樣就可以把某個Monad類型嵌套在它里面。


            newtype StateT s m a = StateT {
                 runStateT :: s -> m (a, s)
            }


            這里StateT就是一個Monad transformer,它允許嵌套一個Monad m類型,它是typeclass MonadState的一個instance,MonadState如下:


            class Monad m => MonadState s m | m -> s where
                 get :: m s
                 put :: s -> m ()


            為了讓Monad transformer可以嵌套進StateT,其它類型的Monad transformer就需要instance MonadState,而StateT Monad transformer為了可以嵌套在其它Monad transformer中,就需要對其它Monad transformer抽象出來的typeclass instance,符合這種規則的Monad transformer就可以相互之間嵌套了,嵌套的層次可以任意深,這樣構造出來的Monad里面有個Monad transformer stack,而這個新構造出來的Monad就可以使用多種Monad的action操作組合在一起了。


            Monad transformer會帶來一個問題,如果想定義一個新的Monad transformer,需要先抽象出這個Monad transformer的typeclass,就像MonadState typeclass一樣,然后把其它Monad transformer都instance這個新抽象出來的typeclass,這樣才能讓這個新的Monad transformer嵌套在其它的Monad transformer之中,接著,為了讓其它Monad transformer能夠嵌套在新的Monad transformer之中,需要把新的Monad transformer instance其它Monad transformer抽象的typeclass。


            我覺得其實Haskell為什么會有Monad和Monad transformer的存在,是因為Haskell是一個純函數式語言,它本身沒有順序執行語句的能力,為了能讓Haskell擁有修改外部狀態并能夠順序執行語句的能力,引入了Monad,又為了讓多種action的Monad能夠組合到一起,由于Monad是一個data type,它不能簡單的組合到一起,因為類型不一致,為了讓它們組合到一起,又引入了更一般化的Monad transformer,讓這些Monad transformer嵌套在一起構成一個stack,才能將這些不同類型的Monad組合。


            Lazy evaluation


            Haskell里面使用的是惰性求值方式,王垠說Haskell的惰性求值是一個很嚴重的問題。我目前也覺得惰性求值是一種負擔,因為惰性求值,會使得程序很容易就出現space leak,我寫的那兩個版本的統計C++代碼行工具都有這個問題,因為它是惰性求值,所以它會把整個目錄的數據全部取出來構造存放到內存中,最后再進行求值,這就自然導致統計大量C++代碼文件的目錄時,占用內存會很高(幾百M上G),也許當我進一步學習之后,我能夠避免這種space leak,但這對于一個初學Haskell的人是一個不小的負擔,因為隨便寫一個小程序都有可能耗用幾百M的內存,而用其他語言實現的話,內存很容易很自然的控制在幾M之內。(看完優化章節,只對程序修改了幾行代碼就讓內存使用降到可以接受的程度,看來Lazy evaluation的問題沒之前想像的那么嚴重。)

            posted @ 2013-04-30 20:41 airtrack 閱讀(5502) | 評論 (0)編輯 收藏
                 摘要: 這篇文章是我兩年多前寫給同事看的,當時不少同事對編碼了解甚少,直到現在發現還是很多人對編碼了解甚少,所以我就把這篇文章發出來讓大家參考一下,希望對一些人有幫助,不過這篇文章是當時花了3個小時左右寫的,錯誤在所難免。字符編碼歷史計算機,發明在20世紀中期西方國家。計算機內部使用二進制作為表示任何東西的基礎,為了能夠在計算機中使用整數、浮點數等都要對其進行編碼,只是這個編碼是在硬件層的(CPU指令),...  閱讀全文
            posted @ 2012-12-23 13:44 airtrack 閱讀(4525) | 評論 (0)編輯 收藏
            僅列出標題  下一頁
            精品久久久久一区二区三区| 亚洲精品国产字幕久久不卡| 狠狠色丁香婷婷综合久久来| 狼狼综合久久久久综合网| 久久久精品人妻一区二区三区蜜桃 | 久久精品国产69国产精品亚洲| 久久96国产精品久久久| 久久久久亚洲AV无码专区桃色| 亚洲国产精品无码久久青草| 久久精品水蜜桃av综合天堂| 国产免费久久精品99久久| 无遮挡粉嫩小泬久久久久久久| 91久久九九无码成人网站| 国内精品久久久久久久久电影网 | 国产精品久久久久国产A级| 国产激情久久久久影院小草| 久久精品视频一| 国产真实乱对白精彩久久| 婷婷综合久久中文字幕蜜桃三电影 | 国产叼嘿久久精品久久| 无码专区久久综合久中文字幕 | 欧美久久一级内射wwwwww.| 欧美午夜精品久久久久免费视| 久久亚洲欧洲国产综合| 国产精品美女久久久久网| 青青草原综合久久大伊人| 久久久久亚洲AV无码去区首| 青青国产成人久久91网| 亚洲AV成人无码久久精品老人| 亚洲欧洲中文日韩久久AV乱码| 99热成人精品免费久久| 好属妞这里只有精品久久| 激情伊人五月天久久综合| 亚洲国产另类久久久精品黑人| 伊人久久大香线蕉综合5g| 四虎国产精品免费久久| 久久精品这里只有精99品| 久久精品二区| 亚洲精品综合久久| 伊人久久大香线蕉综合5g| 蜜桃麻豆WWW久久囤产精品|