• <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
            <2011年1月>
            2627282930311
            2345678
            9101112131415
            16171819202122
            23242526272829
            303112345

            常用鏈接

            留言簿(3)

            隨筆檔案

            搜索

            •  

            最新評論

            閱讀排行榜

            評論排行榜

            上一篇提到當(dāng)訪問的頁表和頁不在內(nèi)存中時(shí)會(huì)觸發(fā) Page Fault 異常,操作系統(tǒng)需要在異常處理函數(shù)中分配內(nèi)存頁并設(shè)置好相應(yīng)的分頁表項(xiàng)。異常是一種中斷類型,注冊異常處理函數(shù)就是注冊中斷處理函數(shù),中斷處理函數(shù)注冊在一個(gè)叫 IDT(Interrupt Descriptor Table) 的地方。

            IDT

            中斷處理函數(shù)在實(shí)模式下注冊在 IVT(Interrput Vector Table) 中,在保護(hù)模式下注冊在 IDT(Interrupt Descriptor Table) 。IDT是包含 256 項(xiàng)的表,表項(xiàng)的結(jié)構(gòu)如下:
                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 分別表示中斷處理函數(shù) offset 地址的 0~15bits 和 16~31bits ,type_attr 的結(jié)構(gòu)如下:
                  7                           0
                +---+---+---+---+---+---+---+---+
                | P |  DPL  | S |    GateType   |
                +---+---+---+---+---+---+---+---+
            P表示是否存在,DPL 表示描述符的最低調(diào)用權(quán)限,GateType 定義了中斷類型,32 位的中斷類型分別是:
            Interrupt Gate 和 Trap Gate 相似,區(qū)別在前者執(zhí)行中斷處理函數(shù)前后會(huì)自動(dòng)關(guān)閉和開啟中斷。
            準(zhǔn)備好 IDT ,設(shè)置好 IDTR 寄存器就把 IDT 都設(shè)置好了。IDTR 寄存器結(jié)構(gòu)如下:
                struct idtr
                {
                    uint16_t limit;
                    struct idt_entry *base;
                };
            limit 是整個(gè)表的大小 -1 字節(jié),base 指向 IDT 表,設(shè)置 IDTR 寄存器的指令是 lidt。

            異常和硬件中斷

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

            PIC 產(chǎn)生的標(biāo)準(zhǔn) IRQ 如下表:

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

            Spurious IRQs

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

            PIT

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

            中斷處理結(jié)束

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

            分頁

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

            分頁前后

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

            物理內(nèi)存管理

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

            內(nèi)存管理器初始化之前

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

            編譯和使用

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

            編譯完成得到兩個(gè)可執(zhí)行文件,分別是:client, server,server啟動(dòng)在服務(wù)器上,client啟動(dòng)在本機(jī)并綁定到地址127.0.0.1:1080,瀏覽器由代理插件通過SOCKS v5協(xié)議連接這個(gè)地址即可。

            Tunnel邏輯結(jié)構(gòu)

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

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

            Client和Server之間傳輸?shù)臄?shù)據(jù)都加了密,加密算法是Blowfish,工作模式是Counter Mode,client和server啟動(dòng)時(shí)的參數(shù)Key即加密算法的Key。

            Rust的使用感受

            以前雖有關(guān)注Rust,卻從沒用Rust寫過代碼,主要是還未發(fā)布1.0,語法不穩(wěn)定,最近1.0快有眉目了,可以用來寫寫小東西了。因?yàn)橛蠬askell的基礎(chǔ),所以上手Rust對我來說沒什么難度。

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

            Rust編譯器報(bào)錯(cuò)信息很詳細(xì)友好,運(yùn)行時(shí)依賴小,Tunnel編譯出來的的client和server都可以在其它機(jī)器上直接運(yùn)行。其它方面主要是API文檔跟不上,最新文檔上有的函數(shù),編譯器編譯可能報(bào)錯(cuò),函數(shù)已經(jīng)不存在了(剛剛?cè)タ戳丝醋钚碌奈臋n,std::io變成了std::old_io)。庫方面,雖然Cargo倉庫里有一些第三方庫,但是總體數(shù)量還不多。
            posted @ 2015-02-03 21:03 airtrack 閱讀(3497) | 評論 (0)編輯 收藏

            Bootloader

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

            實(shí)模式到保護(hù)模式

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


            其中Access Byte和Flags如下圖:


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

            ELF文件

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



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

            為什么寫操作系統(tǒng)

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

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

            正則引擎常見的實(shí)現(xiàn)方法

            正則的常見實(shí)現(xiàn)方式有三種:DFA、Backtracking、NFA:

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

            • Backtracking是三種實(shí)現(xiàn)中效率最低的,功能確是最強(qiáng)的,它可以實(shí)現(xiàn)所有后面新加的語法,因此,大多數(shù)正則引擎實(shí)現(xiàn)都采用此方法。因?yàn)樗腔厮莸模栽谀承┣闆r下會(huì)出現(xiàn)指數(shù)復(fù)雜度,這篇文章有詳細(xì)的描述。

            • NFA(Thompson NFA)有相對DFA來說的構(gòu)造簡單,并兼有接近DFA的效率,并且在面對Backtracking出現(xiàn)指數(shù)復(fù)雜度時(shí)的正則表達(dá)式保持良好的性能。

            NFA-based的實(shí)現(xiàn)

            這里描述的NFA是指Thompson NFA。Thompson NFA實(shí)現(xiàn)的核心是對于正則表達(dá)式多個(gè)可能的匹配并發(fā)的向前匹配,此過程是在模擬DFA運(yùn)行。比如對于正則表達(dá)式"abab|abbb"匹配字符串"abbb":

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

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

            上面是一個(gè)簡單的例子,沒有出現(xiàn)"*" "+" "{m,n}"這種復(fù)雜的metacharacters,在處理這種repeat的metacharacter時(shí)Thompson NFA優(yōu)勢更加明顯。

            在實(shí)際復(fù)雜的正則表達(dá)式中,NFA構(gòu)造是必然會(huì)產(chǎn)生一堆epsilon邊,這在第二篇文章中有描述。上面描述Thompson NFA實(shí)際是在模擬DFA運(yùn)行,在每個(gè)字符匹配完成之后需要跳過epsilon邊得到后面要匹配的并發(fā)的狀態(tài)集合,這樣持續(xù)的并發(fā)匹配下去,當(dāng)字符串匹配完成時(shí)只要有一個(gè)達(dá)到了接受狀態(tài),就是匹配成功,若這個(gè)集合為空,那表示匹配失敗。

            在我的實(shí)現(xiàn)中,構(gòu)造了一組狀態(tài)和由這組狀態(tài)加epsilon邊集合構(gòu)造的有向圖,每個(gè)狀態(tài)有自己的狀態(tài)類型,分為兩種:

            • 一種是匹配狀態(tài)類型,即用來匹配字符的狀態(tài),若字符匹配成功,則進(jìn)入下一個(gè)狀態(tài);

            • 一種是操作狀態(tài)類型,即不匹配字符的狀態(tài),在每個(gè)字符匹配結(jié)束之后若到達(dá)這些狀態(tài),則會(huì)進(jìn)行相應(yīng)的操作,比如repeat狀態(tài),記錄匹配計(jì)數(shù),并判斷匹配計(jì)數(shù)是否完成再?zèng)Q定是否進(jìn)入的下面的狀態(tài)。

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

            Match和Search

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

            我的實(shí)現(xiàn)中,狀態(tài)構(gòu)造完成后是這樣的:

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

            用repeat-any來產(chǎn)生新的匹配線。若在match模式下,則從第三個(gè)狀態(tài)開始匹配,不會(huì)產(chǎn)生新的匹配線,一旦匹配過程失敗了就失敗了。

            結(jié)語

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

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

            GC的分類

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

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

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

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

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

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

            初級分代GC設(shè)計(jì)

            了解了基本的GC方法之后,我為luna第二版實(shí)現(xiàn)了一個(gè)初級的分代GC,把對象分成三代:GCGen0,GCGen1,GCGen2:

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

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

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

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

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

            GC啟動(dòng)的時(shí)機(jī)

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

            通過判斷GC對象個(gè)數(shù)超過閾值時(shí)啟動(dòng)GC,同時(shí)需要在GC之后自動(dòng)調(diào)整閾值大小。比如某些程序很快的達(dá)到GCGen0的閾值并在Minor GC之后有超過一半的對象還是存活的,這時(shí)需要把閾值調(diào)大,以減少GC啟動(dòng)的次數(shù),這個(gè)閾值也不能無限擴(kuò)大,這不僅會(huì)導(dǎo)致一段時(shí)間內(nèi)內(nèi)存占用一直上升,也會(huì)導(dǎo)致一旦觸發(fā)GC所需掃描的對象數(shù)量太多,GC耗時(shí)太長,程序運(yùn)行的延時(shí)增加。

            結(jié)語

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

            上一篇已經(jīng)有近兩個(gè)月的時(shí)間了,這段時(shí)間事情煩(多),導(dǎo)致沒心情寫,現(xiàn)在爭取補(bǔ)上。


            生成epsilon-NFA

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

            正則表達(dá)式:”ab” 對應(yīng)的epsilon-NFA是:


            正則表達(dá)式:”a|b”對應(yīng)的epsilon-NFA是:


            正則表達(dá)式:”a*” 對應(yīng)的epsilon-NFA是:


            這是最基本的3種正則表達(dá)式的NFA表示,其中a*在實(shí)際的正則表達(dá)式實(shí)現(xiàn)中通常生成的epsilon-NFA不是這樣的,因?yàn)橛邢旅孢@些正則表達(dá)式存在:

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

            所以對于a*表示重復(fù)至少0次的實(shí)現(xiàn)可以跟上面這些正則表達(dá)式采用相同方法的實(shí)現(xiàn)。

            按照這些生成規(guī)則就可以把正則表達(dá)式轉(zhuǎn)換成epsilon-NFA,我代碼中即把這些生成規(guī)則實(shí)現(xiàn)成一個(gè)AST的visitor。

             

            epsilon-NFA subset construction to DFA

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

            subset construction:
            start_subset <- epsilon_extend(start_state)    // 把start_state通過epsilon擴(kuò)展得到起始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中的每個(gè)state通過edge所到達(dá)的state的epsilon邊擴(kuò)展得到next_subset
                    if (!subsets.exist(next_subset))       // 如果next_subset不存在于subsets中,則把這個(gè)next_subset加入到work_list中
                        work_list.push_back(next_subset)
                    map[subset, edge] = next_subset        // 構(gòu)建subset到next_subset的邊映射
                    subsets.merge({next_subset})           // 把next_subset合并到subsets
                }
            }

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

             

            這里面使用了epsilon_extend,它是把一個(gè)state的所有epsilon邊能到達(dá)的state構(gòu)成一個(gè)集合,比如上面正則表達(dá)式a*對應(yīng)的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 }

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

            epsilon_extend_construct:
            work_list <- { }
            // 為每個(gè)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所能到達(dá)的state的epsilon_extend
                
            // 合并到當(dāng)前state的epsilon_extend
                for next_state in map[state, epsilon]
                    state_epsilon_extend.merge(epsilon_extend(next_state))
                // 如果當(dāng)前state的epsilon_extend變化了之后
                
            // 把所有通過邊epsilon到達(dá)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構(gòu)造成完之后,并把構(gòu)造的subsets中的subset轉(zhuǎn)換成DFA中的state,再把NFA中除epsilon邊之外的所有邊都轉(zhuǎn)換成DFA的邊,這樣就把DFA構(gòu)造完成。


            DFA minimization

            從NFA構(gòu)造完成DFA之后,這時(shí)的狀態(tài)數(shù)量一般不是最少的,為了減少最終生成的狀態(tài)機(jī)的狀態(tài)數(shù)量,通常會(huì)對DFA的state進(jìn)行最小化構(gòu)造,這個(gè)算法具體如下:

            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拆分成兩個(gè)subset,那就把新拆分出來的
                        
            // 兩個(gè)subset合并到state_sets里面,并break繼續(xù)work_list中取出下一個(gè)
                        
            // 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是把一個(gè)state_set按edge劃分成兩個(gè)subset,即對于state_set中的每一個(gè)state都通過這條邊edge到達(dá)的state屬于不同的state_set時(shí)就把state_set拆分成兩個(gè)subset。首先把第一個(gè)state劃分到subset1中,從第二個(gè)state開始通過邊edge到達(dá)的state所屬的state_set和第一個(gè)state通過邊edge到達(dá)的state所屬的state_set為同一個(gè)的時(shí)候,把這個(gè)state劃分到subset1中,否則劃分到subset2中。

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

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

            實(shí)現(xiàn)正則表達(dá)式的想法很早就有,各種原因?qū)е聸]有做,最近花了點(diǎn)時(shí)間先實(shí)現(xiàn)了幾個(gè)簡單的正則語法,分別是concatenation、alternation和closure,其他語法及metacharacter等有時(shí)間了有想法了之后再擴(kuò)展。

             

            這三種基本的語法分別是對應(yīng)這樣的:

            concatenation: abc    表示匹配字符串a(chǎn)bc

            alternation: abc|def   表示匹配字符串a(chǎn)bc或者def

            closure: a*               表示匹配零個(gè)到多個(gè)a構(gòu)成的字符串

             

            我們知道正則表達(dá)式最終需要轉(zhuǎn)換成自動(dòng)機(jī)才能用來匹配字符串,我實(shí)現(xiàn)的正則通過如下幾個(gè)步驟把正則表達(dá)式轉(zhuǎn)換成自動(dòng)機(jī):

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

            最后用DFA minimization之后構(gòu)造的自動(dòng)機(jī)來匹配字符串。

             

            正則語法的分析

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

            那三種基本的正則語法對應(yīng)的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 '*'

            這個(gè)parser分析了正則表達(dá)式之后產(chǎn)生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的基類,并提供接口實(shí)現(xiàn)Visitor模式訪問ASTNode類型。

             

            字符(邊)集的構(gòu)造

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

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

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

            這需要構(gòu)造一個(gè)字符集,每次插入一個(gè)區(qū)間的時(shí)候,把新插入的區(qū)間與已存在的區(qū)間進(jìn)行分割,初始時(shí)已存在的區(qū)間集為空,那么正則表達(dá)式a[x-z]|[a-z]*e的劃分步驟如下:

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

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

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

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

            這個(gè)區(qū)間構(gòu)造完成了之后,還需要在后面轉(zhuǎn)換成NFA邊的時(shí)候,根據(jù)字符區(qū)間查詢出在這個(gè)集合中,由哪幾個(gè)區(qū)間構(gòu)成,比如:

            查詢區(qū)間[a-a],得到[a-a]

            查詢區(qū)間[x-z],得到[x-z]

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

            在轉(zhuǎn)換成NFA時(shí),集合中的每個(gè)區(qū)間都對應(yīng)一條邊,這樣相對于每個(gè)字符對應(yīng)一條邊,邊的數(shù)量不會(huì)太多。

            有了這么一個(gè)集合構(gòu)造的類之后,把正則的AST中的字符信息提取出來構(gòu)造出這么個(gè)集合即可,這樣只需要寫個(gè)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_;
            };

            邊集合構(gòu)造完成之后,下一步就是生成NFA了。

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

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


            data, type, newtype


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

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

             

            第一行定義了一個(gè)Mode,包含ReadMode和WriteMode;

            第二行定義了一個(gè)普通數(shù)據(jù)類型Some,包含一個(gè)Int數(shù)據(jù)和一個(gè)String數(shù)據(jù);

            第三行定義了一個(gè)普通數(shù)據(jù)類型Thing,包含類型為Int的a和類型為String的b;

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


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

            type IntStringFunc = data Func Int String

             

            type在這里定義了一個(gè)別名IntStringFunc類型,包含了一個(gè)函數(shù)類型是Int -> String的func的數(shù)據(jù),這里的type相當(dāng)于C++ 11的using別名,因?yàn)樗€可以這樣寫:


            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定義的數(shù)據(jù)類型跟type類型,不過type定義的純粹是別名,別名類型跟原始類型是一致的,而newtype則定義的是一個(gè)wrapper,是一種新的數(shù)據(jù)類型,所以是newtype。newtype定義的類型是編譯時(shí)期的wrapper,Haskell保證沒有運(yùn)行時(shí)期的開銷,newtype定義的跟data類似:

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


            模式匹配


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


            some = Some 0 "test"  -- 定義一個(gè)數(shù)據(jù),類型為Some

            -- 定義兩個(gè)函數(shù)用于獲取Some的數(shù)據(jù)
            getSomeInt Some i _ = i
            getSomeString Some _ s = s

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


            這里的getSomeInt和getSomeString函數(shù)都是采用模式匹配來實(shí)現(xiàn),模式匹配就是把數(shù)據(jù)的結(jié)構(gòu)直接寫在代碼中來匹配,然后取出想要使用的數(shù)據(jù)即可。


            Haskell里常用的Maybe數(shù)據(jù)類型是這樣定義的:


            data Maybe a = Nothing
                                 | Just a


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


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

            useMaybe (Just 1)


            下面調(diào)用useMaybe的函數(shù)體內(nèi)取到的a的值就是1。


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


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


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


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


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


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


            Visitor模式和C++ template


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


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


            Currying


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


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

            addOne :: Int -> Int
            addOne = add 1

            addOne 2 -- result: 3


            Currying的實(shí)現(xiàn)是使用的單參數(shù)的lambda構(gòu)成的閉包(closure),add可以看成是接受一個(gè)Int參數(shù)返回一個(gè)函數(shù),這個(gè)函數(shù)的類型是Int -> Int。


            Partial application


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


            addTwo a = add a 2

            addTwo 1 -- result: 3

            (+ 2) 1 -- result: 3


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


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


            func a b c d e f = …

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


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


            C++: function + bind


            C++中的function + bind其實(shí)是一種Partial application實(shí)現(xiàn),比如:


            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會(huì)比Currying的可讀性要好一些,畢竟我們可以完整看到f1和f2的函數(shù)類型,知道參數(shù)類型及個(gè)數(shù)和返回值,是有利于代碼的可讀性的,當(dāng)然這里完全可以不寫出f1和f2的類型,采用auto,我們同樣可以從調(diào)用函數(shù)bind的placeholder的個(gè)數(shù)得知bind之后的function的參數(shù)個(gè)數(shù),這樣我們可以不用看到函數(shù)Func的聲明,就知道需要傳幾個(gè)參數(shù)。function + bind跟Currying一樣會(huì)影響代碼的可讀性,如果嵌套的層次越多,可讀性就越差,所以使用這些特性的時(shí)候不要過度。


            typeclass


            Haskell用typeclass來表示一個(gè)concept,它是一組抽象函數(shù)的集合,一個(gè)滿足某個(gè)typeclass的數(shù)據(jù)類型,它就可以跟其他使用這個(gè)typeclass的函數(shù)或者數(shù)據(jù)類型組合使用。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


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


            data Maybe a = Nothing
                                 | Just a


            既然Maybe是一個(gè)帶類型參數(shù)的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)定義了一個(gè)lambda,參數(shù) _ 緊接 \,-> 后面則是函數(shù)體。函數(shù)(>>)和fail是可以作為默認(rèn)實(shí)現(xiàn)放到class Monad的定義里面,而instance Monad的時(shí)候只需要實(shí)現(xiàn)(>>=)和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


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


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


            函數(shù)(>>)和fail我們保留默認(rèn)的實(shí)現(xiàn)即可。


            Monad


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


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


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


            IO Monad


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


            helloWorld = do
                 putStr "hello "
                 putStrLn "world"


            這里do語法糖其實(shí)就是用的Monad來實(shí)現(xiàn),展開之后是這樣:


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


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


            我們可以自己定義某種data再instance Monad,這樣可以構(gòu)成一組data combination,可以實(shí)現(xiàn)任意的action combine。我山寨的極簡的parse combinator的數(shù)據(jù)類型定義如下:


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


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


            file = repeatP $ spaceLine <||> normalLine


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


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


            這個(gè)函數(shù)接受一個(gè)Parser a和一個(gè)文件內(nèi)容ByteString作為參數(shù),把整個(gè)Parser a封裝的action用于分析文件內(nèi)容,再產(chǎn)生一個(gè)分析結(jié)果。


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


            Monad transformer


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


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


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


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


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


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


            我覺得其實(shí)Haskell為什么會(huì)有Monad和Monad transformer的存在,是因?yàn)镠askell是一個(gè)純函數(shù)式語言,它本身沒有順序執(zhí)行語句的能力,為了能讓Haskell擁有修改外部狀態(tài)并能夠順序執(zhí)行語句的能力,引入了Monad,又為了讓多種action的Monad能夠組合到一起,由于Monad是一個(gè)data type,它不能簡單的組合到一起,因?yàn)轭愋筒灰恢拢瑸榱俗屗鼈兘M合到一起,又引入了更一般化的Monad transformer,讓這些Monad transformer嵌套在一起構(gòu)成一個(gè)stack,才能將這些不同類型的Monad組合。


            Lazy evaluation


            Haskell里面使用的是惰性求值方式,王垠說Haskell的惰性求值是一個(gè)很嚴(yán)重的問題。我目前也覺得惰性求值是一種負(fù)擔(dān),因?yàn)槎栊郧笾担瑫?huì)使得程序很容易就出現(xiàn)space leak,我寫的那兩個(gè)版本的統(tǒng)計(jì)C++代碼行工具都有這個(gè)問題,因?yàn)樗嵌栊郧笾担运鼤?huì)把整個(gè)目錄的數(shù)據(jù)全部取出來構(gòu)造存放到內(nèi)存中,最后再進(jìn)行求值,這就自然導(dǎo)致統(tǒng)計(jì)大量C++代碼文件的目錄時(shí),占用內(nèi)存會(huì)很高(幾百M(fèi)上G),也許當(dāng)我進(jìn)一步學(xué)習(xí)之后,我能夠避免這種space leak,但這對于一個(gè)初學(xué)Haskell的人是一個(gè)不小的負(fù)擔(dān),因?yàn)殡S便寫一個(gè)小程序都有可能耗用幾百M(fèi)的內(nèi)存,而用其他語言實(shí)現(xiàn)的話,內(nèi)存很容易很自然的控制在幾M之內(nèi)。(看完優(yōu)化章節(jié),只對程序修改了幾行代碼就讓內(nèi)存使用降到可以接受的程度,看來Lazy evaluation的問題沒之前想像的那么嚴(yán)重。)

            posted @ 2013-04-30 20:41 airtrack 閱讀(5504) | 評論 (0)編輯 收藏
                 摘要: 這篇文章是我兩年多前寫給同事看的,當(dāng)時(shí)不少同事對編碼了解甚少,直到現(xiàn)在發(fā)現(xiàn)還是很多人對編碼了解甚少,所以我就把這篇文章發(fā)出來讓大家參考一下,希望對一些人有幫助,不過這篇文章是當(dāng)時(shí)花了3個(gè)小時(shí)左右寫的,錯(cuò)誤在所難免。字符編碼歷史計(jì)算機(jī),發(fā)明在20世紀(jì)中期西方國家。計(jì)算機(jī)內(nèi)部使用二進(jìn)制作為表示任何東西的基礎(chǔ),為了能夠在計(jì)算機(jī)中使用整數(shù)、浮點(diǎn)數(shù)等都要對其進(jìn)行編碼,只是這個(gè)編碼是在硬件層的(CPU指令),...  閱讀全文
            posted @ 2012-12-23 13:44 airtrack 閱讀(4527) | 評論 (0)編輯 收藏
            僅列出標(biāo)題  下一頁
            97久久香蕉国产线看观看| 无码久久精品国产亚洲Av影片| 亚洲综合熟女久久久30p| 精品99久久aaa一级毛片| 国内精品人妻无码久久久影院| 久久精品国产色蜜蜜麻豆| 综合久久一区二区三区 | 亚洲国产精品久久久久婷婷老年| 精品久久久久久国产| 色妞色综合久久夜夜| 久久99国产精品久久99小说| 亚洲欧洲中文日韩久久AV乱码| 青青热久久国产久精品| 亚洲午夜久久久| 99久久这里只精品国产免费| 超级97碰碰碰碰久久久久最新| 亚洲午夜久久久久妓女影院| 色综合久久中文字幕无码| 国产精品久久久久AV福利动漫| 精品综合久久久久久888蜜芽| 99久久婷婷国产综合亚洲| 久久国产精品久久国产精品| 观看 国产综合久久久久鬼色 欧美 亚洲 一区二区 | 久久精品国产福利国产琪琪| 久久婷婷色综合一区二区| 久久99热这里只有精品66| 人妻无码αv中文字幕久久| 国产精品视频久久久| 国产真实乱对白精彩久久| 亚洲国产婷婷香蕉久久久久久| 色播久久人人爽人人爽人人片AV| 久久精品aⅴ无码中文字字幕重口| 久久国产精品一区二区| 青青热久久国产久精品| 久久亚洲精品人成综合网| 国产福利电影一区二区三区,免费久久久久久久精 | 久久伊人色| 久久精品99久久香蕉国产色戒| 国产福利电影一区二区三区久久久久成人精品综合 | 无码专区久久综合久中文字幕| 国产精品免费看久久久|