歡迎光臨
每天分享高質量文章

Shrinking The Kernel With A Hammer

大刀闊斧精簡內核

    這是“討論各種精簡內核大小方法系列文章”的第四篇,旨在讓 Linux 內核能適用於小型的運行環境。精簡內核二進制檔案是有其極限的,而我們已經盡可能地做到極致。但是,我們的標的是將 Linux 完全運行在一個片上微控制器,這個標的還沒有達到。這篇文章會通過使內核和用戶態適用於資源受限系統的角度來總結這個系列文章

    微控制器是一個自包含的系統,它擁有外設,記憶體和CPU。它通常較小,廉價,而且低功耗。微控制器被設計成用於實現單一任務和運行某個特定的程式。因此,微控制器中的動態記憶體通常比靜態記憶體空間小的多。這也就是為何微控制器上的 ROM 普遍比 RAM 大很多倍的原因。

    比如說, ATmega328 (常見的 Arduino 型號)裝載 32KB 的閃存(flash),但只有 2KB 的靜態記憶體(SRAM)。至於可以運行 Linux 的 STM32F767BI 裝載了 2MB 的閃存和 512KB 的靜態記憶體。所以,我們將鎖定使用這些資源來確定如何將最多的內容從 RAM 搬移到 ROM。

 

Kernel XIP

    “就地運行“(eXecute-In-Place XIP)機制允許 CPU 從 ROM 或者閃存中獲取指令,這樣一來可以避免將運行指令儲存和加載到 RAM 中。XIP 引入到微控制器領域是因為它們的 RAM 通常都很小。所以,XIP 在大型系統上很少被使用(因他們擁有充足的記憶體,所以直接在 RAM 上運行所有東西);在 RAM 上運行指令因為高性能快取也會變得更高效。這個也就是為何大部分 Linux 架構不支持 XIP。實際上,內核的 XIP 只在 ARM 平臺上支持,而且早在 Git 出現之前。

    至於內核 XIP, 它需要 ROM 或者閃存能跟記憶體一樣直接通過處理器的記憶體地址來被訪問,且不需要單獨的軟體驅動。NOR 閃存支持隨機訪問而經常被用於這個目的,它不像使用塊地址索引的 NAND 閃存。然後,內核在構建鏈接時特殊處理,這樣一來代碼段和只讀資料段將被分配到閃存的地址空間中。我們只需要開啟 CONFIG_XIP_KERNEL,構建系統會提示輸入預期內核要在閃存上的物理地址。只有可寫的內核資料將被拷貝到 RAM 上。

    因此,我們期望 XIP 內核更可能多地將代碼和資料放置到閃存中。越多空間被放置在閃存中,越少的空間會被拷貝到珍貴的 RAM 上。預設方法和其被 const 標註的資料將會被放置到 flash 上。最近內核開發上為了強化變數用途展開了很多常量化工作,這使得 XIP 內核受益很多。

   用戶態 XIP 和檔案系統

      
 

    用戶空間是個記憶體消耗大戶。但是,正如內核一樣,用戶空間二進制有可讀可寫和只讀段。如果能將只讀段也存放在之前相同的閃存空間,並從閃存中直接運行,這樣就避免被加載到記憶體中。然而,並非完全與內核一樣,內核是一個靜態的二進制,它只被加載或者映射到 ROM 和 RAM 地址一次。用戶空間的程式存在與檔案系統,這樣使得事情變得複雜起來。

    我們能否擯棄檔案系統?當然可以。事實上,這也是大多數小型實時操作系統的做法。他們把程式代碼直接跟內核鏈接到一起,完全繞開檔案系統層。而且這也不會顛覆 Linux 的運行機制,因為內核執行緒本身就可以當作是一個用戶態的程式: 他們擁有自己的執行背景關係,與用戶程式一起被調度,可以被髮送信號,顯示在行程串列中,等等。而且內核執行緒也不需要檔案系統。雖然,將程式運行在內核執行緒中,可能導致整個內核崩潰,但是微控制器中本來也缺少 MMU 設備,它已經是一個純粹的用戶態程式了。

    然而,為用戶態程式搭配一個檔案系統,會有很多我們不想損失的優勢:

  • 兼容全功能的 Linux 系統,這樣我們的程式可以在本地工作站中開發測試

  • 方便整合多個不相關的程式

  • 可以單獨開發和更新內核與用戶態程式

  • 與內核 GPL 協議劃清界限

    也就是說,我們盡可能想要一個最小,最簡單的檔案系統。別忘記我們的閃存容量只有 2MB,而我們的內核已經占用 1MB。因為很多可寫檔案系統有它固有的損耗,我們只能排除這些檔案系統,並且我們不希望寫閃存區域,因為內核代碼存在其中,寫操作可能導致系統崩潰。

    註意:即使閃存通過開啟 CONFIG_MTD_XIP 被用於 XIP,它也是受限可寫的。當前只能在 Intel 和 AMD 的閃存設備實現,而且需要特定的架構支持

所以小型且只讀檔案系統的選擇只有這些:

  • Squiashfs:可高度擴展,預設壓縮,代碼有些複雜,沒有 XIP 支持

  • Romfs: 簡單,精小的代碼,沒有壓縮,部分支持(沒有 MMU 的系統) XIP

  • Cramfs: 簡單,精小的代碼,有壓縮功能,非主幹代碼可以部分支持 (MMU上) XIP

    我選擇 Cramfs,因為只它擁有的壓縮機制能滿足只使用少量的閃存,這些 romfs 所沒有的。而且 Cramfs 的代碼相對 squashfs 比較簡單,可以較容易在沒有 MMU 設備的系統上添加 XIP 特性。並且,Cramfs 只需配置一下可以完全被用在塊設備上。

    然後,添加 XIP 到 cramfs 上的初步嘗試是相當粗魯而缺乏基本原則的。這些嘗試功能要麼能用,要麼完全不能用:比如,每一個檔案在 XIP 下,要麼完全沒壓縮,要麼完全壓縮。實際上,可執行檔案由代碼和資料組成,既然可寫資料得拷貝到 RAM 中,就沒有必要讓它們以非壓縮的方式存在閃存中。所以,我只能靠自己重新設計 cramfs 在 MMU 和非 MMU 的 XIP 支持。我添加了所需的功能以實現混合壓縮和不壓縮任意區間的塊,最終這確實滿足上游合併的標準(主幹 Linux 版本4.15 以後可用)。

    之後,我(再次)發現有10年曆史的 AXFS 檔案系統(仍不在主幹維護)更適用。但,我只能放棄這個想法,畢竟我更願意與主幹代碼打交道。

    有人會奇怪為何 DAX 沒有在這裡被適用。極端上,DAX 有點像 XIP;DAX 被製作於大型可寫檔案系統,並依賴 MMU 來實現頁資料按需讀入和置出。它的文件也提到另一個缺點:”DAX 的代碼不能在虛擬映射快取的架構中正確運行,如 ARM, MIPS 和 SPARC“。因為XIP 的 cramfs 是只讀而且足夠小,可以完全被映射到記憶體中,他的功能完全可以實現所需的結果,而且方式更簡單,這樣一來 DAX 在這個背景關係下就有點太重了。

   用戶空間 XIP 和可執行二進制格式   

 

      

    現在,我們有了 XIP 支持的檔案系統,是時候使用它了。我適用一個靜態構建的 BusyBox 版本以保持簡單。在適用具有 MMU 的架構,我們可以看到程式是如何被映射到記憶體中的。

# cat /proc/self/maps
00010000-000a5000 r-xp 08101000 1f:00 1328 /bin/busybox
000b5000-000b7000 rw-p 00095000 1f:00 1328 /bin/busybox
000b7000-000da000 rw-p 00000000 00:00 0 [heap]
bea07000-bea28000 rw-p 00000000 00:00 0 [stack]
bebc1000-bebc2000 r-xp 00000000 00:00 0 [sigpage]
bebc2000-bebc3000 r–p 00000000 00:00 0 [vvar]
bebc3000-bebc4000 r-xp 00000000 00:00 0 [vdso]
ffff0000-ffff1000 r-xp 00000000 00:00 0 [vectors]

    第一行粗字體的線索就暴露了 XIP。這行代表檔案的偏移與映射的關係。我們可以看到 0x08101000 明顯大於檔案偏移量;實際上,它代表閃存物理地址的偏移量。Cramfs 也可能用在某些場景呼叫 vm_insert_mixed() ,這樣物理機地址彙報將不可用。在任意場景可靠的彙報映射關係將會很有用。

    第二行映射 /bin/busybox (.data 區)被標誌成可讀可寫,並不像第一行的代碼區,是只讀可執行的。可寫段不能映射到閃存中,因此需以正常的方式加載到記憶體中。

    MMU 讓程式很容易使用絕對地址而不用在意它的實際記憶體使用量。在非 MMU 環境下,事情變得複雜起來,用戶執行程式必須可以在任意記憶體地址上運行;因此地址無關代碼(PIC)是一個必須選項。這個功能由 bFLT 平鋪檔案格式支持,該格式被 uClinux 架構支持了很久。然後,這個格式有許多限制,使得 XIP,共享庫,或者兩者合併,不容易操作。

    幸運地是,有一個 ELF 的變體格式,ELF FDPIC,可以解決這些限制。因為 FDPIC 段是地址無關的,不需要先決相對偏移量,因此它可以在多個可執行檔案中共享代碼段,正如 有 MMU 的 ELF 格式一樣。代碼段也可以做成 XIP。ELF FDPIC 支持已經被添加到了 ARM 架構上(同樣主幹 Linux 版本 4.15)

    在我的 STM32 設備上,使用 XIP cramfs 和 ELF FDPIC 用戶態二進制,BusyBox 的地址映射看起來是這樣的:

# cat /proc/self/maps
00028000-0002d000 rw-p 00037000 1f:03 1660       /bin/busybox  
0002d000-0002e000 rw-p 00000000 00:00 0  
0002e000-00030000 rw-p 00000000 00:00 0          [stack]    
081a0760-081d8760 r-xs 00000000 1f:03 1660       /bin/busybox  

    因為缺少 MMU,使用 XIP 的段因為沒有地址轉換,直接可以看到閃存地址。

     砍掉靜態記憶體 
      

    好了,現在我們準備好做一些大動作。我們看到上面 XIP 的 BusyBox 已經省掉 229,376 位元組記憶體,或者說 56 個記憶體頁。如果我們有 512KB,這是 128 個頁的 44%。從現在開始,精確地記錄記憶體被分配到哪,決定這些珍貴的記憶體如何被高效使用,是很重要的。我們先從內核開始看,使用一個之前文章中精簡過的配置,另外加上 XIP 配置,我們得到如下

   text    data     bss     dec     hex filename
1016264   97352  169568 1283184  139470 vmlinux

    1,016,264 位元組的代碼段是被放置在閃存中的,我們先略過這一部分。但是,266,920 位元組的資料段和 BSS 段使用了 51% 的記憶體總量。讓我們通過一些腳本來找出哪些東西占用了這部分空間

    #!/bin/sh
   {
       read addr1 type1 sym1
       while read addr2 type2 sym2; do size=$((0x$addr2 - 0x$addr1))
           case $type1 in b|B|d|D)
               echo -e "$type1 $size\t$sym1"
               ;;
           esac
           type1=$type2
           addr1=$addr2
           sym1=$sym2
       done
   } < System.map | sort -n -r -k 2

前面幾行的輸出:

    B 133953016     _end
   b 131072        __log_buf
   d 8192  safe_print_seq
   d 8192  nmi_print_seq
   D 8192  init_thread_union
   d 4288  timer_bases
   b 4100  in_lookup_hashtable
   b 4096  ucounts_hashtable
   d 3960  cpuhp_ap_states
   [...]

    這裡,我們忽略 _end,因為它使用了超大空間明顯是因為一個情況,記憶體分配的末尾是在閃存前面的。

    然而,我們有一些明顯分配情況用來考量問題。觀察一下 __log_buf 的定義:

    /* record buffer */
   #define LOG_ALIGN __alignof__(struct printk_log)
   #define __LOG_BUF_LEN (1 << CONFIG_LOG_BUF_SHIFT)
   static char __log_buf[__LOG_BUF_LEN] __aligned(LOG_ALIGN);

    這個變數很簡單,因為我們不想將整個 printk() 支持去掉,所以我們將 CONFIG_LOG_BUF_SHIFT 這設置成 12(最小可設置的值)。同時,我們將 CONFIG_PRINTK_SAFE_LOG_BUF_SHIFT 設置成最小值 10. 結果:

   text    data     bss     dec     hex filename
1016220   83016   42624 1141860  116c64 vmlinux

    我們通過一些簡單的配置調節,將記憶體使用量從 266,920 降低到 125,640 位元組。我們再看看符號使用情況:

B 134092280     _end
D 8192  init_thread_union
d 4288  timer_bases
b 4100  in_lookup_hashtable
b 4096  ucounts_hashtable
b 4096  __log_buf
d 3960  cpuhp_ap_states
[...]

    下一個消耗大戶是 init_thread_union 。這個比較有趣,因為它來源自 THREAD_SIZE_ORDER,它用來決定內核行程能擁有多少個棧頁。第一個行程(init 行程)剛巧在資料欄位靜態分配了它的棧,這就是為何我們能在這裡看到它。把這個值從 2 個修改到 1 個就能很好的適用在我們的小型環境里,而且這也能在動態分配棧時每行程能減少 1 個頁。

    我們可以調整 LVL_BITS 從 6 到 4,來減少 timer_bases 的大小。調整 IN_LOOKUP_SHIFT 從 10 到 5. 連同一些其他內核常量。

    搞定動態記憶體分配

 

      

    正如我們所看見的,找出和減少靜態記憶體分配是比較容易的。但是動態分配也需要好好處理,為了這個目的我們得將我們的設備引導起來。當內核的分配器還沒有啟動運行時,第一個動態分配器來自 memblock 分配器。觀察其執行操作的方法是現成的,只要設置 memblock=debug 去啟動它。它會顯示如下:

memblock_reserve: [0x00008000-0x000229f7] arm_memblock_init+0xf/0x48
memblock_reserve: [0x08004000-0x08007dbe] arm_memblock_init+0x1d/0x48

    這裡可以看出靜態記憶體被保留了,它與內核代碼,只讀資料相連,一同存在閃存中(它們被映射在 0x08004000)。如果內核代碼是在 RAM,是有必要保留這部分記憶體。在現在這個場景下,這個保留行為是無用但無害的行為,因為閃存永遠不會被用於分配。

現在看下實際的動態分配:

memblock_virt_alloc_try_nid_nopanic: 131072 bytes align=0x0 nid=0
from=0x0 max_addr=0x0 alloc_node_mem_map.constprop.6+0x35/0x5c
 Normal zone: 32 pages used for memmap
 Normal zone: 4096 pages, LIFO batch:0

    memmap 陣列使用了 131,072 位元組 (32 個頁)去管理 4096 個頁。預設這個設備需要使用 16MB 的主板外置記憶體地址。所以,如果我們把這個數字降低成實際的頁數量,比如說 512KB,那麼這個值會明顯降低。

下一個較大的分配是:

memblock_virt_alloc_try_nid_nopanic: 32768 bytes align=0x1000 nid=-1
from=0xffffffff max_addr=0x0 setup_per_cpu_areas+0x21/0x64
pcpu-alloc: s0 r0 d32768 u32768 alloc=1*32768

    在一個只有小於 1MB 記憶體的單處理器系統預留給每個 CPU 一個 32KB 的記憶體池?沒必要。需要修改 include/linux/percpu.h 檔案來修改成單個頁

-#define PCPU_MIN_UNIT_SIZE             PFN_ALIGN(32 << 10)
+#define PCPU_MIN_UNIT_SIZE             PFN_ALIGN(4 << 10)

-#define PERCPU_DYNAMIC_EARLY_SLOTS     128
-#define PERCPU_DYNAMIC_EARLY_SIZE      (12 << 10)
+#define PERCPU_DYNAMIC_EARLY_SLOTS     32
+#define PERCPU_DYNAMIC_EARLY_SIZE      (4 << 10)

+#undef PERCPU_DYNAMIC_RESERVE
+#define PERCPU_DYNAMIC_RESERVE         (4 << 10)

 值得註意的是,只有 SLOB 記憶體分配器 (CONFIG_SLOB)在這些修改後能繼續工作。

繼續看下一個較大的記憶體分配:

memblock_virt_alloc_try_nid_nopanic: 8192 bytes align=0x0 nid=-1
from=0x0 max_addr=0x0 alloc_large_system_hash+0x119/0x1a4
Dentry cache hash table entries: 2048 (order: 1, 8192 bytes)
memblock_virt_alloc_try_nid_nopanic: 4096 bytes align=0x0 nid=-1
from=0x0 max_addr=0x0 alloc_large_system_hash+0x119/0x1a4
Inode-cache hash table entries: 1024 (order: 0, 4096 bytes)

    誰說這是一個大型系統?是的,目前你應該領悟精簡方法了 ———— 接下來值需要一些類似調整,不過為了讓這篇文章保持合理的篇幅,這些調整被省略了。

    之後,通用的內核記憶體工作將被分配器如 kmalloc() 接管,所有的分配最終落在 __alloc_pages_nodemask(). 類似的跟蹤和調整也適用在這個階段,直到啟動完成。有時就是配置調整的事情,如 sysfs 檔案系統,它使用的記憶體有點超過我們的預算,等等。

     回到用戶空間

 

      

    既然我們已經大幅降低內核的記憶體使用量,我們準備再次引導看看。這次引導成功最低的記憶體所需,我們設定成800KB (設置內核引導命令列”mem=800K“)。讓我們看看這個小小的世界:

BusyBox v1.7.1 (2017-09-16 02:45:01 EDT) hush - the humble shell

# free
            total       used       free     shared    buffers     cached
Mem:           672        540        132          0          0          0
-/+ buffers/cache:        540        132

# cat /proc/maps
00028000-0002d000 rw-p 00037000 1f:03 1660       /bin/busybox
0002d000-0002e000 rw-p 00000000 00:00 0
0002e000-00030000 rw-p 00000000 00:00 0
00030000-00038000 rw-p 00000000 00:00 0
0004d000-0004e000 rw-p 00000000 00:00 0
00061000-00062000 rw-p 00000000 00:00 0
0006c000-0006d000 rw-p 00000000 00:00 0
0006f000-00070000 rw-p 00000000 00:00 0
00070000-00078000 rw-p 00000000 00:00 0
00078000-0007d000 rw-p 00037000 1f:03 1660       /bin/busybox
081a0760-081d8760 r-xs 00000000 1f:03 1660       /bin/busybox

    這裡我們可以看到從 /bin/busybox 的檔案偏移 0x37000 開始有 2 個 4 頁的記憶體。這是兩個資料實體,一個是 shell 行程,一個是 cat 行程。他們共同共享 busybox 從 0x081a0760 開始的 XIP 代碼段。另外,還有兩個匿名的 8 頁記憶體,它們消耗了大量的頁預算。他們相當於一個 32KB 的棧空間。這個頁可以被調整下來:

--- a/fs/binfmt_elf_fdpic.c +++ b/fs/binfmt_elf_fdpic.c @@ -337,6 +337,7 @@ static int load_elf_fdpic_binary(struct linux_binprm *bprm)         retval = -ENOEXEC;
       if (stack_size == 0)
               stack_size = 131072UL; /* same as exec.c's default commit */

+ stack_size = 8192; 
       if (is_constdisp(&interp;_params.hdr))
               interp_params.flags |= ELF_FDPIC_FLAG_CONSTDISP;

    這個是相當又快又髒的做法;在 ELF 二進制頭檔案中合理地修改棧大小才是比較恰當的做法。這頁需要比較細緻的驗證,比如說在有 MMU 的系統上設置一個固定大小的棧,任意的棧上限溢位都能被捕捉到。但是,這反正也不是我們第一次做這種事情了。

在重啟之前,我們看看更多的信息:

# ps
 PID USER       VSZ STAT COMMAND
   1 0          300 S    {busybox} sh
   2 0            0 SW   [kthreadd]
   3 0
ps invoked oom-killer: gfp_mask=0x14000c0(GFP_KERNEL),
nodemask=(null), order=0, oom_score_adj=0
[...]
Out of memory: Kill process 19 (ps) score 5 or sacrifice child

    記憶體不足導致的殺行程行為似乎必會發生。好在記憶體不足時buddy分配器中會提供一些信息:

Normal: 2*4kB (U) 3*8kB (U) 2*16kB (U) 2*32kB (UM)
       0*64kB 0*128kB 0*256kB = 128kB

    ps 行程嘗試使用0階大小(單個 4KB 頁)的記憶體分配,儘管有 128KB 空閑,這個操作仍然失敗了。為什麼?原來是因為頁分配器在低於一定水位後會不執行正常的記憶體分配邏輯,這個水位由 zone_watermark_ok() 來判斷傳回。這樣可以避免死鎖,因為無法分配記憶體後,需要去殺行程,這個殺行程操作又需要記憶體。儘管這個水位很小,但是在我們的小型環境中,這仍然是一個我們無法接受的數值,所以我們稍微降低這個水位

--- a/mm/page_alloc.c +++ b/mm/page_alloc.c @@ -7035,6 +7035,10 @@ static void __setup_per_zone_wmarks(void)                 zone->watermark[WMARK_LOW]  = min_wmark_pages(zone) + tmp;
               zone->watermark[WMARK_HIGH] = min_wmark_pages(zone) + tmp * 2;

+ zone->watermark[WMARK_MIN] = 0; + zone->watermark[WMARK_LOW] = 0; + zone->watermark[WMARK_HIGH] = 0; +                 spin_unlock_irqrestore(&zone-;>lock, flags);
       }

最終,我們能夠用 “mem=768k” 來重啟內核

Linux version 4.15.0-00008-gf90e37b6fb-dirty ([email protected]) (gcc version 6.3.1 20170404
 (Linaro GCC 6.3-2017.05)) #634 Fri Feb 23 14:03:34 EST 2018
   CPU: ARMv7-M [410fc241] revision 1 (ARMv7M), cr=00000000
   CPU: unknown data cache, unknown instruction cache
   OF: fdt: Machine model: STMicroelectronics STM32F469i-DISCO board
   On node 0 totalpages: 192
     Normal zone: 2 pages used for memmap
     Normal zone: 0 pages reserved
     Normal zone: 192 pages, LIFO batch:0
   random: fast init done
   [...]

   BusyBox v1.27.1 (2017-09-16 02:45:01 EDT) hush - the humble shell

   # free
                total       used       free     shared    buffers     cached
   Mem:           644        532        112          0          0         24
   -/+ buffers/cache:        508        136

   # ps
     PID USER       VSZ STAT COMMAND
       1 0          276 S    {busybox} sh
       2 0            0 SW   [kthreadd]
       3 0            0 IW   [kworker/0:0]
       4 0            0 IW<  [kworker/0:0H]
       5 0            0 IW   [kworker/u2:0]
       6 0            0 IW<  [mm_percpu_wq]
       7 0            0 SW   [ksoftirqd/0]
       8 0            0 IW<  [writeback]
       9 0            0 IW<  [watchdogd]
      10 0            0 IW   [kworker/0:1]
      11 0            0 SW   [kswapd0]
      12 0            0 SW   [irq/31-40002800]
      13 0            0 SW   [irq/32-40004800]
      16 0            0 IW   [kworker/u2:1]
      21 0            0 IW   [kworker/u2:2]
      23 0          260 R    ps

   # grep -v " 0 kB" /proc/meminfo
   MemTotal:            644 kB
   MemFree:              92 kB
   MemAvailable:         92 kB
   Cached:               24 kB
   MmapCopy:             92 kB
   KernelStack:          64 kB
   CommitLimit:         320 kB

    看吧! 儘管沒有達到我們 512KB 記憶體的標的,但是 768KB 已經比較接近了。有些微控制器已經有超過這個數量的片上靜態記憶體了。

    不複雜的提升工作仍然存在。我們可以看到在16個行程中,有14個是內核行程,各自使用了 4KB 的棧。其中一些行程肯定可以去除。然後在進行一輪記憶體頁的分析,能透露出更多可以被優化的部分,等等。而且,專用的程式並不需要產生新的子行程,它也只需要更少的記憶體去運行。畢竟,一些流行的控制器用這裡我們剩餘的空閑記憶體就來實現互聯網的連接功能。

 總結

    這裡至少有一個重要的點可以從這個專案中學習到。精簡內核記憶體使用量比精簡內核代碼要容易的多。因為,代碼已經被高度優化過,而且代碼對系統性能有直接的影響,即使在大型系統上。但是記憶體使用量卻是不一樣的。RAM 在大型系統上變得相對偏移,在操作上,即使浪費一些也沒有關係。因此,在優化記憶體使用量的工作中,有很多唾手可得成果。

    除了這些小的調整和快速的修改,其他重要的部分如(XIP 內核,XIP 用戶態,甚至一些設備記憶體使用量精簡)都已經在主幹版本中。但是為了讓 Linux 跑在微小的設備上仍需要進一步工作。這個工作的進度,永遠依賴於人們對使用上的期待和構建一個社區去推動開發的願景。

[感謝 Linaro 組織允許我投入時間在這個專案和寫作這個文章上]

赞(0)

分享創造快樂