contributed by < OscarShiang >
linux2020MathEx 如何解析給定字串,從而分離出變數和對應數學表示法呢?MathEx 中,nop 一類使用者自訂的函數如何實作呢?MathEx 對於負數的除法結果不正確、乘法和加法的 overflow 沒有做處理,且缺乏對減法的 underflow 處理機制呢?你預計如何克服?expression.[ch] 裡頭 vec 相關的程式碼,將 quiz4 使用到的技巧予以應用,提出對空間效率更好的實作在 Linux 的核心中為了計算 load average 的數值,使用到了定點數的概念,首先看到 include/linux/sched.h 中相關的 macro :
可以看到在計算時會使用到的精度 FSHIFT 與在使用 Exponential smoothing 會用到的常數 EXP_1, EXP_5 與 EXP_15
而在實作的部分在 kernel/sched/proc.c 中可以看到在 calc_global_load 函式中
為了分別計算 1, 5, 15 秒的 load average,在實作上對 avenrun 三個元素進行運算,利用 calc_load 計算數值
而 calc_load 的函式如下:
可以看到數值使用是 unsigned long 來實作,而為了維持定點數計算的正確性,在結束乘法運算後需要將數值向右 shift 才會是計算的結果
當 load average 被算出來之後,接著就是讓其可以正確地顯示出來。
在 fs/proc/loadavg.c 的實作中
LOAD_INT 與 LOAD_FRAC 的定義如下
LOAD_INT 的實作就是將小數部分捨棄,就可以直接使用。
而 LOAD_FRAC 的話,首先將 x 與 FIXED_1-1 進行 AND 運算,因為 FIXED_1 = 1 << FSHIFT 也就是 ,減 1 後就變成 所以透過遮罩的方式就可以將 x 的小數部分取出。
接著將其乘以 100 再使用 LOAD_INT 就可以取出小數部分的前兩位了
在 Linux Programmer's Manual 的 time(7) 有提到
Real time is defined as time measured from some fixed point, either from a standard point in the past (see the description of the Epoch and calendar time below), or from some point (e.g., the start) in the life of a process (elapsed time).
在 test_simple.c 中可以看到,解析字串的函式是 expr_create,該函式中首先利用 expr_next_token 尋找 token 在字串的位置。
expr_next_token 最主要的功能如下:
# 標注的註解時忽略整行內容\n 時向後尋找其他內容最後將 token 所在的 index 回傳給 n。
回到 expr_create 函式後,依據 token 的類型(數字、運算子、變數等等),分別存入不同的 vec 之中。因為 vec 的 push 與 pop 都是處理 vec 尾端的元素,所以可以知道在 vec 是作為 stack 使用
在遇到 '(' 與 '{' 等等字元時,就會把儲存在 os 與 as 中的運算子與參數進行包裝,利用迴圈的方式重複這樣的過程,讓 result 在傳入 expr_eval 時可以用遞迴的方式將傳入的表示式算出數值。
在分析完所有的字元後,將 es 這個 vec 的元素 pop 到 result 中,並將 result 回傳,結束字串的解析。
在 MathEx/test-simple.c 中有示範使用 user_funcs 陣列包裝函式並在使用 expr_create 時一併傳入。而 struct expr_func 需要的 field data 有二個:
而在測試程式的實作如下:
上述實作中,在填入完函式的名稱與指標後,第三個參數 0 是為了讓編譯器可以將後面的資料初始化為 0。而最後一個 field data 全部都是 0 的元素是用來標記結尾的。
在 expr_create 的分析中,如下列的程式碼:
就會去比對傳入的函式的列表,如果與輸入的字串吻合的話,就會將函式的指標與參數一同 compile 成 bound_func,讓 expr_eval 可以呼叫對應的函式來進行計算。
為了讓 livepatch 能夠找到對應的函式進行替換,我在 main.c 中加入 user_func_fib 作為 MathEx 的自定義函式。
在使用 user_func_fib 分析完參數後,將參數傳入 kfib 函式進行運算
在 livepatch_calc.c 中新增 livepatch_fib 函式作為 kfib 的替代函式
掛載 livepatch_calc 檢查函式是否被替換:
從 fromfixed 指令檢查計算結果,發現因為 livepatch_fib 未按照 fixed-point 的格式計算而產生錯誤
接著著手修改 livepatch_fib 以符合 fixed-point 的格式
目前的 fixed-point 所採取的作法是與 floating-point 類似的計算方式,就是將 32 bits 寬度的數值拆成三個部分: exponent 用以表示次方, mantissa 用以表示尾數以及 sign 來表示正負號
因為在 exponent 的表示方法是以 10 為底數,所以不需要像 IEEE 754 所規範的 32 位元浮點數一樣使用 8 個位元來表示。而在這樣的表示法中可以表示的最大數值為 1342177270000000,最小的數值為 -1342177280000000
但是在數值較大時會產生精度的問題,像是現行的表示法可以表示 1342177270,但卻不能表示 1342177271,因為 1342177271 已經超過 mantissa 所能表示的大小,但卻不到 exponent 可以表示的範圍,就會導致 的情況產生
參考 include/linux/sched.h 中所使用的定點數,並設計以下結構:
也就是使用 32 bit 記錄 「整數」 部分,以及另外 32 bit 記錄 「小數」 部分。
除了擴充可記錄的數值範圍外,更減少在存取整數部分時的計算步驟:在 include/linux/sched.h 中所使用的結構在存取整數部分時需要做位移,但在此結構中只需取出 inte 的 field member 即可;
而在進行加減運算時,不需要考慮 exponent 不同的議題,直接運算即可。在進行乘除時,則可以利用 data 的 field member 來做位移修正。
將字串轉換為 fixed-point 的實作如下:
因為在計算時可能會碰到 NaN, INF 等情況發生,所以需要定義這些特殊值的表示方法:
INF: 調用 fraction 全為 1 的情況記錄NaN: 最小的數值來表示 NaN將 main.c 中,為了符合新的 fixed-point 的實作,將 user_func_fib 進行修改:
因為在新的 fixed-point 的定義下,整數部分實際上能運算到的最大範圍還是 ,所以在這樣的實作下 fib 實際能正確計算出的最大數值是 fib(75)。
因為更改了 fixed-point 的實作,所以為了能檢驗運算的結果,需要一個新的 fixed-point 轉換的程式:
使用定義在 <stdint.h> 中的 UINT32_MAX 將小數部分轉換為浮點數形式,並印出對應的浮點數數值。
接著修改 scripts/test.sh 的轉換機制,將 fromfixed 改成執行編譯好的 scripts/eval:
執行 make check 檢查結果:
從上面的結果可以看到完成 fixed-point 的轉換。