Thursday, May 24, 2012

Boot to Gnome on Debian

以前剛準備要從 Windows 換到 GNU/Linux 在 Linux 連線板爬文時, 才知道有些事並不是那麼理所當然. 例如這些 Windows 上的常識

  • 系統升級要重灌
  • 系統壞掉要重灌
  • 換主機板要重灌
  • 換硬碟要重灌
  • 用久了要重灌 (那個時代)
  • 更新要重開機
  • 安裝很多軟體後要重開機
  • ...
在 GNU/Linux 上並不一定適用.

當時仿效 vgod 大神, 沒想太多就找了 Debian potato (Linux 2.2!) 來安裝. 用沒多久發現有些套件在 potato 上沒有或太舊, 很快就決定從 stable 升級到 testing; 再從 testing 升到 unstable. 除了最開始安裝 Debian potato 外, 後來都只用到 apt-get. 等到後來買了筆電第二次裝 Debian, 也已經是 7 年後的事了. 而這中間的更新除非有動到 kernel, 不然也大都也沒有重開機. 頂多只是重啟 X server. 即使換了 CPU 跟主機板, 還是舊硬碟的系統繼續用. 甚至換硬碟時也只用到 cp 跟 grub-install 來搬系統. 一切就像 Linux 板說地一樣美好.

期間當然有很多次更新後有東西壞掉或不能開機, 但總是幸運地能夠修好. 能夠做到這點很重要的因素就是對開機流程有基本認識

知道系統是怎麼從 bootloader 進到桌面環境, 系統更新後出了問題才不會束手無策. 雖然有時候這比重灌或重開機更花時間.

Bootloader: grub

在用傳統 BIOS 的環境, grub 由 boot.img 跟 core.img 兩部份組成. 因為 MBR 大小的限制, 只有 boot.img 會被寫到 MBR. 也就是說當 BIOS 把 MBR 讀到記憶體並跳過去執行時, 實際上是在跑 boot.img. 它做的事只是把 core.img 最前頭的部份讀到記憶體就再跳過去. 而這個最前頭的部份才是真正把 core.img 剩餘的部份讀完, 並接著執行後才秀出我們熟悉的開機選單. 換句話說, grub 的主體是 core.img. boot.img 只是為因應 MBR 大小限制, 被設計來載入 core.img 用的.

在系統中, grub-setup 可以用來把 boot.img 寫到 MBR. core.img 則可以用 grub-mkimage 產生. 之所以需要動態產生 core.img 是因為依硬碟的 partition table type 跟 filesystem 不同, 我們需要不同的 core.img. 不過一般只要執行 grub-install 就會幫我們都處理好.

系統更新後如果重開機不能進 grub 或 kernel, 通常是 grub 壞了. 這時得用 Debian 安裝光碟 或 USB 碟 (或其它像是 SystemRescueCd) 開機. 開好機後進 console 把原來的 root mount 起來重跑一次 grub-install 常常就能解決問題. 如果是因為新版 grub 本身的問題, 那要先找舊版的 grub (或許還在 /var/cache/apt/archives ), 裝好再跑 grub-install.

而在 EFI 的環境, grub 只會有 core.img. grub-install 會把 core.img 複製到 EFI system partition 並設定好 EFI boot manager. 要注意的是在執行 grub-install 前需要先把 EFI system partition mount 到 /boot/efi 並且載入 efivars kernel module.

Kernel & initramfs

bootloader 會負責把 kernel 跟 initramfs 載入到記憶體並執行. 詳細的作法規範在 Documentation/x86/boot.txt.

沒特別設定時 kernel image 本身也會帶一份空的 initramfs, 所以這裡的 initramfs 指的是 external initramfs. 它是獨立的一個檔案. bootloader 可以透過舊有的 initrd 機制把 external initramfs 指定給 kernel.

kernel 初始化好系統後會去執行第一個 user space 的程式: init. 在 initramfs 裡有 /init 這個執行檔時 kernel 會去執行它. 否則 kernel 會把 root mount 起來, 並執行 root 裡的 /sbin/init.

initramfs 的 /init 如果存在時, 它在做的通常也是把 root mount 起來, 並執行 root 裡 的 /sbin/init. 之所以會需要 initramfs 是因為有些環境, 例如 root 在遠端時, kernel 做這些事比較麻煩.

kernel 沒辦法進到 user space 常常是因為有缺 driver. 最常見的應該是 SATA 或 filesystem driver 沒有被編進 kernel 或者沒有在 initramfs 裡.

init: sysvinit

sysvinit 之後可能會被 systemd 取代, 不過在這之前還是得熟悉 sysvinit.

sysvinit 的設定檔在 /etc/inittab. 在 Debian 裡它主要會去執行 /etc/rcS.d 跟 /etc/rc2.d 裡所有的 script, 並讓使用者可以從 tty1 到 tty6 登入系統. 這些 script 在做的不外乎是 mount filesystem, 對網路, 系統時間等做基本設定, 還有啟動系統服務. 很多 script 會到 /etc/default 底下讀設定, 所以可以透過修改 /etc/default 裡的檔案來改變這些 script 的行為.

如果開機過程在這個部份出問題時, 可以試著在 kernel command line 加上 single 進入單人模式或 init=/bin/sh 讓 shell 當 init process. 因為這邊都是 script, 有問題時也比較容易解決.

一個多重開機到 Windows 後再回來可能會出現的問題是系統時間差了 8 小時. 這是因為 Debian 在把系統時間寫到 RTC 時會用 UTC 時間, 而 Windows 會把 RTC 的時間當成是 UTC+8. 如果遇到這個問題可以到 /etc/default/rcS 把 UTC 設定 no.

Display Manager: gdm

gdm 會把 X server 跑起來, 並顯示我們熟悉的圖形登入畫面. 它會掃描

/usr/share/gdm/BuiltInSessions 跟
/usr/share/xsessions
裡面的 .desktop 檔找出系統提供的 session. 當我們選好 session 並成功登入時, 它會執行
/etc/gdm/Xsession session-name
/etc/gdm/Xsession 這個 script 會讀入 /etc/X11/Xsession.d 底下所有的 script. 它們在最後通常會執行類似這樣的命令
ssh-agent dbus-launch some-session-manager
讓 session manager 帶出桌面環境來.

如果想要額外設定別的環境變數的話, 可以寫在 ~/.xsessionrc. 或者如果不想跑系統現有的 session 的話, 也可以自己寫 shell script 存成 ~/.xsession. 如此 /etc/gdm/Xsession 最後會改執行 ~/.xsession 而不是 some-session-manager. 這些細節可以在 /etc/X11/Xsession.d 裡看到.

在 Debian 裡輸入法現在是透過 im-config 在管理. 它也是利用 Xsession.d 的機制在登入的過程中被跑起來.

Session Manager: gnome-session

登入後的桌面環境是由 session manager 帶出來的. 一般情況 gnome-session 會參照 /usr/share/gnome-session/sessions/gnome.session 執行 gnome-shell 與 gnome-settings-daemon. 它同時也會執行

~/.config/autostart
/usr/share/gnome/autostart
/etc/xdg/autostart
裡所有沒被 disable 的程式. 後者可以利用 gnome-session-properties 做增減.

gnome-shell 就是眼睛看得到的部分, 這邊不提. gnome-settings-daemon 的功能之一是它會監看某些 GSettings key 並做回應. 例如在 terminal 執行

$ gsettings set org.gnome.desktop.background picture-uri file://<path-to-image>
gnome-settings-daemon 會收到這項改變, 並且把桌面的背景圖片換成新指定的圖片. 或者執行
$ gsettings set org.gnome.desktop.interface gtk-theme <theme-name>
gnome-settings-daemon 會把新的 GTK+ theme name 廣播出去. 所有 GTK+-based 的程式也會自動換用新的 theme.

Friday, December 25, 2009

Android with 3D Effects

年終了。

最近幾個月不知道為什麼,沒辦法專心工作。利用上 ptt 跟看漫畫的空檔,把說很久的特效弄出可以 demo 的樣子



感謝 Erin 的幫忙,這段影片展示了 Cube 跟 Fire 特效。 WindowManagerService 允許的地方都可以接特效,但現在只有接在程式啟動時。

目前的進展可以在 gitorious 上取得。不過很顯然地,還有很多東西要弄。

Sunday, November 29, 2009

Fast bit counting

最近 mesa3d-dev mailing list 有一個討論串在討論 bit counting,也就是在算一個 unsigned int 裡有幾個 bit 是 1。

假設 unsigned int 有 32 bits 寬,最簡單的算法就是一個迴圈跑 32 次,一個一個 bit 去看。想當然這不是有效率的算法。事實上,只要做簡單的修改,就可以讓迴圈跑的次數降到跟 bit 為 1 的 bit 數相同。換句話說,如果 unsigned int 裡只有 3 個 bit 為 1,迴圈只要跑三次就可以。不過像這麼基礎的運算,一定有不少人下過功夫在找最快的算法。一個問題,工程師會去尋找更好的方法;但是身為鄉民,我們感興趣的只有最好的方法,而且我們從不自己找!很幸運地,在最開頭提到的討論串裡頭,就有人提出一個號稱最快的算法。

討論串中提到的方法經過簡化可以寫成

int bitcount(unsigned int v)
{
v = v - ((v >> 1) & 0x55555555);
v = (v & 0x33333333) + ((v >> 2) & 0x33333333);
v = (v + (v >> 4)) & 0x0f0f0f0f;
return (v * 0x01010101) >> 24;
}


在深入去看這段程式碼前,最好可以用抽像點的方式看它想做的事



上圖中,v0 是使用者的輸入。展開成二進位表示法,每個 bit 的值不是 1 就是 0。如果有一個方法可以讓所有的 bit 變成兩個兩個一組 (v1)、4 個 4 個一組 (v2)、8 個 8 個一組 (v3)、一直到 32 個 bit 自己成一組 (v5),那 v5 的值剛好就是我們要的結果。從 v4 看起,假設我們已經有 v4 了,那麼應該不難發現
v5 = (v4 & 0xffff) + (v4 >> 16);


以此類推,很快就可以發現我們需要的是

int bitcount2(unsigned int v)
{
v = (v & 0x55555555) + ((v >> 1) & 0x55555555); /* v1 */
v = (v & 0x33333333) + ((v >> 2) & 0x33333333); /* v2 */
v = (v & 0x0f0f0f0f) + ((v >> 4) & 0x0f0f0f0f); /* v3 */
v = (v & 0x00ff00ff) + ((v >> 8) & 0x00ff00ff); /* v4 */
v = (v & 0x0000ffff) + ((v >> 16) & 0x0000ffff); /* v5 */
return v;
}


這時再回過頭去看 bitcount,可以注意到雖然算法不完全相同,但它前三行在做的也是求 v1、v2 與 v3。但它接下來不是求 v4,而是把 v3 乘上 0x01010101。透過四則運算,這個乘法可以改寫成加法

v = (v3 << 24) + (v3 << 16) + (v3 << 8) + v3


把 v3 的展開式代入,會發現



將這個結果向右 shift 24 bits 得到的也是 v5。這就是 bitcount 的做法。

如果把寫死的 magic number 換成適當型別的除法與補數運算,這個 bitcount 函數對 32/64/128-bits 寬的 unsigned int 都可以適用。寬度的限制是來自於 v4' 的第一個小括號,它不能大於或等於 2^8。不過如果 bitcount 求到 v4 再來做乘法,這個限制也可以被放寬。

Thursday, June 25, 2009

一千零一夜之 GEM Object

這個系列是讀書筆記,作者可能沒有跟主題有關的開發經驗。


GEM object 是一種 buffer,一種可以在 A 程式被配置,在 B 程式被使用的 buffer。當 userspace 產生一個 GEM object 時,以 i915 為例,它會呼叫 drm_gem_object_alloc。從這個函數的頭幾行

struct drm_gem_object *obj;

BUG_ON((size & (PAGE_SIZE - 1)) != 0);

obj = kcalloc(1, sizeof(*obj), GFP_KERNEL);

obj->dev = dev;
obj->filp = shmem_file_setup("drm mm object", size, VM_NORESERVE);
if (IS_ERR(obj->filp)) {
kfree(obj);
return NULL;
}


可以看出,它只是一塊 shared memory。就這個方式來看,GEM object 其實不是新的東西。

但這塊 buffer 除了 A、B 程式會用到外,它還另外有一個使用者,這個使用者是 GPU。之所以把 shared memory 包裝成 GEM object 最主要的原因是,kernel 需要同步 CPU 和 GPU 對這塊 buffer 的存取。很明顯地,如果讓 CPU 與 GPU 同時寫入同一塊記憶體,出來的結果一定無法預期。在 GEM object 裡,處理同步的方法是引入 domain 的機制。所有人在存取 GEM object 前,必須先把它的 domain 設好。如果是 GPU 要讀取或寫入,那就需要先把 read domain 或 write domain 設成 GPU;相對的,如果是 CPU 要讀取或寫入,則需要把 read/write domain 設成 CPU。DRM 保證的是,只要正確地指定 domain,存取到的資料也會是正確的。

為了解 domain 跟同步的關係,我們可以看一下當我們把 read domain 指定為 CPU 時,kernel 會幫我們做什麼處理。這部份完整的程式碼可以在 i915_gem_object_set_to_cpu_domain 看到。

首先,kernel 要確定之前所下的 GPU 指令已經全部執行完畢,而且 GPU cache 要 flush 掉

i915_gem_object_flush_gpu_write_domain(obj);
ret = i915_gem_object_wait_rendering(obj);


GPU 的運作方式是會依序執行某個指定的 ring buffer 裡面的指令,而開發者要做的是把指令放到這個 buffer。為了達到目的,kernel 會在 ring buffer 最後加上 flush 跟 interrupt 指令,並且開始等待中斷。我們可以想像,當 kernel 收到中斷時,也就表示之前的指令都已經執行,包含它自己加上的 flush 指令。

事實上,CPU 除了直接存取 shared memory 外,也可以從 AGP aperture 去存取,這在 i915 裡稱做 GTT domain。所以 kernel 也要確定這邊沒有資料被 cache 住

i915_gem_object_flush_gtt_write_domain(obj);


這些動作可能造成記憶體上的資料被改變,而,這時候 CPU 還不知道這件事。所以只要再確保 CPU 可以知道這些變動

i915_gem_object_set_to_full_cpu_read_domain(obj);
if ((obj->read_domains & I915_GEM_DOMAIN_CPU) == 0) {
i915_gem_clflush_object(obj);
obj->read_domains |= I915_GEM_DOMAIN_CPU;
}


我們就可以說 GEM object 已經被移到 CPU read domain。

GEM object 是 shared memory 的特性,讓 X server 可以直接把 buffer 傳給 client 做 DRI。它同時也可以當做到 OpenGL 裡頭各種 buffer object 的 storage。除了 Intel 外,在 2.6.31 也可以看到初步的 ATI Radeon KMS 支援。雖然從使用者的經驗來看,似乎只是進入另一個黑暗時期。但是在領袖的帶領下,我們好像也可以看到光了!

Wednesday, June 24, 2009

一千零一夜之 Kernel and Firmware

這個系列是讀書筆記,作者可能沒有跟主題有關的開發經驗。

Driver 需要 firmware 時會呼叫 request_firmware,這個函數會在 /sys 底下產生適當的裝置讓 userspace 上傳 firmware 給 driver。它會等到 firmware 上傳完成 (或發生錯誤) 才回傳。除了建立適當的裝置,它也會送一個 uevent,內容類似

KERNEL[1245809267.000297] add      /devices/pci0000:00/0000:00:1c.2/0000:02:00.0/firmware/0000:02:00.0 (firmware)
UDEV_LOG=3
ACTION=add
DEVPATH=/devices/pci0000:00/0000:00:1c.2/0000:02:00.0/firmware/0000:02:00.0
SUBSYSTEM=firmware
FIRMWARE=iwlwifi-3945-2.ucode
SEQNUM=1226


uevent 事件通常是 udev 在處理,在遇到 firmware 事件,也就是 SUBSYSTEM 是 firmware 的事件時,udev 預設會執行 /lib/udev/firmware.agent (在 Debian 上) 來上傳 firmware。簡化過的 firmware.agent 其實只做了

echo 1 > /sys/$DEVPATH/loading # 準備上傳
cat $firmwaredir/$FIRMWARE > /sys/$DEVPATH/data
echo 0 > /sys/$DEVPATH/loading # 上傳完畢


注意到上面大寫的變數內容都是 uevent 提供,像是 $DEVPATH 就是 kernel 剛產生用來接受 firmware 上傳的虛擬裝置。

這是 firmware 擺在 filesystem 上的情形。當 CONFIG_FIRMWARE_IN_KERNEL 打開時,如果 driver 被編進 kernel,firmware 也會一起被編進去。在 request_firmware 中有一段

for (builtin = __start_builtin_fw; builtin != __end_builtin_fw;
builtin++) {
if (strcmp(name, builtin->name))
continue;
dev_info(device, "firmware: using built-in firmware %s\n",
name);
firmware->size = builtin->size;
firmware->data = builtin->data;
return 0;
}


會先檢查 driver 要求的 firmware 是否有內建。這邊的 __start_builtin_fw 與 __end_builtin_fw 是 link time 產生的 symbol,所以不會在任何 C code 找到它們的定義。在 vmlinux.lds (或 include/asm-generic/vmlinux.lds.h) 可以看到,它們的值會是所有 .builtin_fw section 內容的開始位址與結束位址。

.builtin_fw 的內容要再找到 firmware/Makefile。在那份 Makefile 可以看到 kbuild 會為每一個要內建的 firmware 產生一份 assembly code。這些 assembly code 編出來的 object code 會跟 kernel link 在一起,達成 firmware 內建的目的。從 assembly code 可以看出,這些 object code 的內容會是 firmware 本身,以及一個放在 .builtin_fw section 的資料結構,給出 firmware 的檔名、位置、還有大小。

這邊看到內建 firmware 的做法其實是 kernel 常見的手法之一。寫 driver 最開始會加的 module_init(entry_point) 是另一個例子。這些 macro 會產生 initcall,而所有的 initcall 在 link time 也是用類似的方式被收集。在電腦剛開機進 userspace 前它們會依照被收集的順序依序被執行。這邊可以看到一個要注意的小地方。在每個 subsystem (rtc, scsi, ...) 的 Makefile 中,核心的部份必須放在 driver 之前。

Tuesday, June 9, 2009

Android Wave

上篇文章提到的程式碼已上線,有興趣的朋友可以到 Android Eee PC 專案網頁下載。

在做 clean build 的空檔,請 Jeremy 幫忙錄了一段影片,以證明加速是真的 XD



不過錄到後半,我頭暈得差點連瀏覽器 icon 在哪都找不到,所以請注意不要靠太近看。再來兩天要參加 FreedomHEC Taipei 2009,順便休息一下。

Thursday, June 4, 2009

Android 3D acceleration on x86

過去一、二個月的文章都是圍繞著 android x86 與 mesa/dri 在打轉,原因無它,因為這陣子在做的事恰好就是 Android 在 Eee PC 上的 3D 加速。因為 android 跟 mesa 都是第一次接觸,所以確實費了不少工夫在認識它們。

Android 在起來的時候,會試著載入 /system/lib 底下的 libagl.so 與 libhgl.so。前者是 OpenGL ES 與 EGL 的純軟體實作,source code 在 frameworks/base/opengl/libagl/ 底下;後者則與硬體相關,目前沒有開放的實作。所以我做的事基本上就是寫一份 libhgl.so。

之前在 DRI Overview 有提到,只要有適當的 loader,DRI driver 可以在 X 以外的平台使用。目前我的 libhgl.so 實作是基於 eagle,並只支援 intel graphics chipsets。因為 i915_dri.so 實作了 OpenGL,而不是 OpenGL ES,所以定點數與部份 ES 才有的 extension 沒有辦法支援。這個問題最快的解決方式應該是改用 Gallium 搭配 OpenGL ES state tracker,這也是接下來要評估的。

對 Android 本身,主要則有三個部份需要修改。一是 EGLDisplaySurface 要用 kernel modesetting 的實作取代,同時 kernel 也要上 2.6.29;二是 GPUHardware 要改用 i915 的 GEM object 實作;三是 Surface 要能認得 GEM object。

目前整個 Android 系統可以正常運作。SurfaceFlinger 或一些 3D 軟體像是 Android-GL 也可以正確地被加速。不過目前的實作比較接近於驗證這個方式的可行性。程式碼整理好後會放在 Android Eee PC,到時有興趣的開發者都可以一同參與。