contributed by < willwillhi1
>
make check
遇到的問題Change Secure Boot State
perfomance.sh
,讓 cpu 只注重效率,將頻率固定在其支持的最高運行頻率上。
address space layout randomization (ASLR)
,ASLR
全名為 Address Space Layout Randomization
(地址空間佈局隨機化),它可以將 process 的某些內存空間地址進行隨機化來增加入侵者預測目的地址的難度,從而降低被入侵的風險。
/etc/default/grub
內的 GRUB_CMDLINE_LINUX_DEFAULT
為 GRUB_CMDLINE_LINUX_DEFAULT="quiet splash isolcpus=7"
,修改完成後需 sudo update-grub
來更新設定,重開機後即生效cat /sys/devices/system/cpu/isolated
taskset -cp 1
,來確認是否生效
taskset -c 7 command
執行測試程式輸出結果代表註冊的 fibdrv
的 Major and Minor Numbers
509
代表動態申請的主設備號
也可以用 ls -l /dev | grep fib
找到
linux kernel moudle
fibdrv.c
後,用 make all
來編譯make unload
移除之前的模組make load
載入新編譯後的模組以下為原始版本的演算法:
用作業提示的 fast-doubling
實作 fibonacci
,參考作業說明和 Calculating Fibonacci Numbers by Fast Doubling 的解說。
公式列在下方:
修改過後的程式碼:
以 n = 0b101
為例,
當迴圈走到最左邊的 1 時(也就是目前總共是 0b1
),會先計算 F(2K)
和 F(2K+1)
,之後在用這兩個結果算出 a = F(2K+1)
和 b = F(2K+2) = F(2K) + F(2K+1)
。
接著往右移一位遇到 0,相當於把先前的結果乘二即可(0b1
-> 0b10
),也就是 6、7 行。
最後遇到 1,就是將上一步的結果乘二 (0b10
-> 0b100
),再加 1 (9 ~ 11 行) 得到 0b101
。
其中
可以用 __builtin_clz
加速
這麼做可以不需要每次迴圈都從 31
開始,透過 clz
可以直接從最高位 1
開始做,接下來用 gunplot
來製圖
if-else
的部份
可以用以下程式代替
n & (1UL << i)
表示只保留 n
的第 i
個 bit,其餘 bits
都是零,而 -!!
這個操作可以讓 mask
:
mask = -1
,如果 n 的第 i
個 bit
為 1
mask = 0
,如果 n 的第 i
個 bit
為 0
最後的程式會是:
接下來測試不同實作的時間分佈
write
實作 naive
版本
read
實作 fast doubling
版本
然後用 gunplot
製圖
圖例應為 naive vs. fast doubling
jservImage Not Showing Possible ReasonsLearn More →
- The image file may be corrupted
- The server hosting the image is unavailable
- The image path is incorrect
- The image format is not supported
使用作業說明提及的 ktime
在核心模組中測量執行時間,實作在 write
函式裡,並回傳 kernel space
執行時間。
user space
在呼叫 write
前後用 clock_gettime
計算 user space
的時間,最後可以把 kernel time
, user time
相減得到 system call
的時間。
先看懂 作業說明的大數運算部份
使用以下結構體來進行大數運算
number
是儲存整個數值的陣列,以 unsigned int
為單位也就是 32 bits
,由低位元依序存到高位元。size
代表該陣列的長度。sign
代表該數為正或負。計算 src
的陣列的 leading zero
。
for
迴圈要是主要處理轉成二進位轉成十進位字串的地方,每 32 bits
處理一次。
src->number
的最高位(msb)逐一放入字串的最高位(msb)並處理。carry
來代表是否進位,初始化為 !!(d & src.number[i])
,代表該 bit
是否為 0
或 1
。for (int j = len - 2; j >= 0; j--)
,每次累加一個 bit
時都要跑整個字串的長度的迴圈。s[j] += s[j] - '0' + carry
,原本的數值乘以二再加上 carry
,最後還要處理進位問題。bn_add
是先用判斷正負號後再依照案例使用 bn_do_add
和 bn_do_sub
進行無號整數的計算。
進行無號整數加法 (c = |a| + |b|)
DIV_ROUNDUP(x, len)
的定義為 (((x) + (len) -1) / (len))
,簡單來說就是除法無條件進位。!d
應該不用加,因為 d
不可能是 0
。for
迴圈部份則是執行簡單的加法。|a| > |b|
條件一定要成立。carry
計算時可能會需要與 1LL << 32
相加,超過 unsigned int
大小,所以 carry
型別要宣告為 long long int
。(long long int) tmp1 - tmp2 - carry
結果若小於 0
就需要借位。bn_resize(c, c->size - d)
,把多餘的零去掉。邏輯就像一般的乘法一樣,慢慢累加上去
另外處理 c
有可能是 a
或 b
的情況,會先用 tmp
來存 c
原本的位址避免 a
或 b
的值被改變,等最後做完乘法再 swap
回來。
原本的版本程式碼如下,會限制 shift
在 31
以下,然後根據 shift
是否大於 leading zero
來判斷重新 resize dest
的大小。
後來我在進行 10110001000110010010010011100001
(剛好 32 bits) 向左 shift 1
時會出錯輸出 01100010001100100100100111000010
,很明顯是最高位的 1
被吃掉了,後來我發現如果 dest
被 resize
成大小為 src->size + 1
,需要額外處理 dest
陣列的最後一個 unsigned int
(第三行)。
這麼一來 fast doubling
需要用到的函式幾乎都有了,接下來就是拿作業說明提供的 bn_fib_fdoubling
函式來跑即可。
原始版本的 bn_fib_fdoubling
為以下:
後來利用 __builtin_clz
硬體指令可以直接從最高位的 1
開始計算,減少迴圈數。
接著為了避免執行 bn_mult
的 (c == a || c == b)
會用 tmp
來儲存 c
的情況所以用以下邏輯來避免這個情況。
fib v0
代表修改前,fib v1
代表修改後,由此可見 bn_alloc()
的成本是很高的。
realloc
次數為了避免每次呼叫 bn_resize
時都會執行 realloc
造成對記憶體頻繁的修剪。
在 bn
結構體多加入了一個變數 capacity
,用來事先配置額外的記憶體,所以在每次呼叫 resize
時都會先判斷是否還有空間可以使用 (capacity > resize size)
,若是則可以直接回傳,避免再次執行 realloc
。
用 perf
比較前後結果,計算 fib(1) ~ fib(10000)
,可以發現 cache-references
確實有明顯的下降。
verify.py
驗證verify.py
會產生 fib(0) ~ fib(想要測量到的範圍)
並存放在 expect
裡,隨後讀取 out
並存在 result
,經過處理後將答案存在 dice
,
最後去跟 expect
做比較,如果有錯就會輸出類似以下的訊息:
目前測試到 1000
為止答案都是正確的。
觀察 make check
時會執行的命令,
發現 expected.txt
不會變更到,所以
就用 verify.py
來驗證,改為以下:
且 verify.py
裡的 exit()
改為 exit(1)
,代表執行期間有錯誤發生 (與預期答案不符)。
原本的大數運算方式是直接在 kernel space
執行 bn_to_string
將 bn->number
轉成字串後通過 copy_to_user
將答案傳回 user space
。
但是實作的大數結構是用一個 unsigned int
陣列來表示
0xffffffff
為例,其所需要的空間是 4 Byte
。4294967295
,因為這邊是用字元表示所以所需空間是 10 byte
,大於二進位表示時所需的空間。所以我將傳送的方式改成傳送 bn->number
到 user space
而不是空間佔用較大的字串,
直接將計算結果透過 copy_to_user
傳送。
接著由 client
接收,並用修改過後的 bn_to_str
將其轉成字串。
最後對 copy_to_user
的執行時間做比較,可以發現傳送所花費的時間確實有差,而且數字越大越明顯。
但是這麼做會造成將 bn
轉成十進位需要在 user space
做相對於在 kernel space
還要花時間,原因推測是因為 bn_to_string
會呼叫到 malloc
需要切換到 Supervisor mode
,導致時間花費會比較高。
malloc
是 library function
,底下實際的實作是透過 sbrk
或 mmap
。
malloc
不是系統呼叫!請善用 perf 一類的工具分析執行成本。
後來發現編譯時用 -O3
,之前沒用的時候導致修改後的 client
的 instructions
數過多導致執行時間變長。
接著用 perf
做比較可以發現兩個方法所花費的時間基本上差不多,但是 pass_number_array
相對減少了 copy_to_user
的時間,也就是傳送 bn->number
到 user space
,然後在 user space
執行 bn_to_string
。
原本以為會有明顯差距,但實際上沒有,
從以上結果可以看到 malloc
再執行時沒有呼叫 system call
因為沒有做 context switch
,
所以我後來去查了一些可能的因素,發現 kmalloc
和 malloc
它們實作上的不同,
kmalloc
配置的記憶體是 physically contiguous
的,通常用於配置 128 KB
以下的空間。
而 malloc
拿到的記憶體是 virtual memory
(也就是物理上不連續的記憶體),所以當程式去修改這片記憶體時,就有可能會發生 page fault
。
由以上兩點來看,剛好可以呼應前面的 perf
分析的 page fault
的結果。
進一步優化 copy_to_user
所需要複製的資料大小,目前的算法是陣列多大就複製多大,可優化的地方是最後一個元素的 leading zero
是不需要的,所以就只傳送最少所需要的位元組即可。
sysfs
界面為了取得比用 printk
更準確的時間,所以透過 sysfs
的界面來存取 fibdrv
的執行時間。
sysfs
描述:
The sysfs filesystem is a pseudo-filesystem which provides an interface to kernel data structures.(More precisely, the files and directories in sysfs provide a view of the kobject structures defined internally within the kernel.)The files under sysfs provide information about devices, kernel modules, filesystems,and other kernel components.
簡單來說 sysfs
可以提供一個界面 (kobject structures
) 讓 user space
可以存取 kernel
內的資料結構。
reference count
linux
風格的 linked list
,嵌入在別的資料結構中使用user space
(透過 sysfs
)kobject
在 sysfs
會以目錄的形式呈現。attribute
)會以檔案的方式表示。kobject
都會有一到多個屬性,每個屬性都代表核心內的某資訊attribute
中的 show
和 store
callback 函式來取得核心資訊attribute
進行讀取操作(ex: cat)時會執行 show
attribute
進行寫入操作(ex: echo)時會執行 store
以下依照kobject-example.c的實作
attribute
__ATTR
定義在 linux/sysfs.h
中:show
, store
store
: 利用寫入的方式傳入要算第幾個費氏數列,並將執行時間紀錄在 kt
。show
: 將 fibdrv
內的 kt
轉為奈秒後輸出。attribute
kobject_create_and_add
在 /sys/kernel/
下建立 fib_ktime
目錄。sysfs_create_group
把剛剛建立的屬性放到這個目錄下。最後用 kobject_put
減少 reference count
。
fib(1000)
的執行時間