# 2020q2 Homework3 (fibdrv)
contributed by < `eecheng87` >
###### tags: `linux2020`
## 修改 Fibonacci 演算法
原本提供的版本是最直覺的演算法,複雜度為 $O(N)$
```cpp
unsigned long long easy_fib(int n)
{
unsigned long long series[92];
series[0] = 0;
series[1] = 1;
for (int i = 2; i < n + 1; i++) {
series[i] = series[i - 1] + series[i - 2];
}
return series[n];
}
```
接著尋找更快的演算法,藉由作業説明的提示,開始著手以 Fast Doubling 手法實做 fibonacci 。
由作業說明提供的公式和 pesudo code 實做
$$
\begin{align}
&F(2n) = 2F(n)\times F(n+1) - F(n)^2 \\
&F(2n+1) = F(n)^2 + F(n+1)^2
\end{align}
$$
```cpp
unsigned long long double_fib(int n)
{
unsigned long long a = 0, b = 1, t1, t2;
for (int i = 31; i >= 0; i--) {
t1 = a * (b * 2 - a);
t2 = b * b + a * a;
a = t1;
b = t2;
if ((n & (1 << i)) > 0) {
t1 = a + b;
a = b;
b = t1;
}
}
return a;
}
```
這個演算法理論上複雜度 $O(log{N})$,但時實際的速度卻不如預期,在 n 很小的時候速度會比原版本的還慢很多。原因不外乎就是 Fast doubling 每次都會從位元最高位開始找,在本範例即 bit 31 開始。改善的想法很簡單,我們要避免在 n 很小的時候,迴圈計數引數 `i` 仍從 31 開始。 `clz` 函數就是我們要找的,它能算出左起第一個 1 之前 0 的個數,接著再用 `31` 減掉該數字就能知道要從哪個 bit 開始做 fast doubling。
```cpp
int i = clz ? 31 - __builtin_clz(n) : 31;
```
新增的部份是先判斷是否採用 `clz` 來優化原本的演算法,從而給定不同的起始位元數 `i`。
既然函式已做好,那麼馬上開始檢驗不同 n 值的對應的運算時間分佈
```cpp
for (int i = 2; i < 93; i++) {
clock_gettime(CLOCK_REALTIME, &t1);
(void) easy_fib(i);
clock_gettime(CLOCK_REALTIME, &t2);
printf("%d ", i);
printf("%ld ", elapse(t1, t2));
clock_gettime(CLOCK_REALTIME, &t1);
(void) double_fib(i, false);
clock_gettime(CLOCK_REALTIME, &t2);
printf("%ld ", elapse(t1, t2));
clock_gettime(CLOCK_REALTIME, &t1);
(void) double_fib(i, true);
clock_gettime(CLOCK_REALTIME, &t2);
printf("%ld\n", elapse(t1, t2));
}
```
透過 gnuplot 製圖:
![](https://i.imgur.com/UBECCX0.png)
觀察:最初版本模擬出來的確符合 $O(N)$ 的複雜度,此外尚未優化的 fast doubling 在 n 太小時 (從圖中看大概是 70 以前) 效率是會比上一個版本還慢的。不過若添加 `clz` 提前檢查的話,可以得到很好的效率。
[實驗程式碼](https://github.com/eecheng87/fibdrv/tree/master/experiment)
:::warning
TODO: 參照 [A Fairly Fast Fibonacci Function](http://www.oranlooney.com/post/fibonacci/),我們可透過 vector 一類的資料結構 (這也是 quiz4 命題的考量) 充當 cache,只要空間控制得當 (太小沒效果,太大反而會變慢),可達到加速。參考的 JavaScript 實作:
```javascript
var fib = (function(cache){
return cache = cache || {}, function(n){
if (cache[n]) return cache[n];
else return cache[n] = n == 0 ? 0 : n < 0 ? -fib(-n)
: n <= 2 ? 1 : fib(n-2) + fib(n-1);
};
})();
```
:notes: jserv
:::
## 增加 fibonacci 計算範圍
原本透過 `sudo ./client` 可以觀察到輸出到 92 後開始怪怪的,但還是維持正數 ( 這是 `MAX_LENGTH` 設定在 92 下起的保護作用 )。若把值設大就可以明顯觀察到 overflow 的發生。
```
Reading from /dev/fibonacci at offset 92, returned the sequence 7540113804746346429.
Reading from /dev/fibonacci at offset 93, returned the sequence -6246583658587674878.
Reading from /dev/fibonacci at offset 94, returned the sequence 1293530146158671551.
Reading from /dev/fibonacci at offset 95, returned the sequence -4953053512429003327.
```
### 檢查機制
當我們什麼都還沒做,直接對剛 fork 下來的專案執行 `make check` ,如同預期出現綠色 pass 字樣,另外也有指出第 92 項錯誤。先來檢查 `Makefile` 裡面的 `check` 到底做了什麼
```shell
check: all
$(MAKE) unload
$(MAKE) load
sudo ./client > out
$(MAKE) unload
@diff -u out scripts/expected.txt && $(call pass)
@scripts/verify.py
```
首先為了避免已經掛載其他模組,所以得先 `unload`,其實對應的就是 `rmmod` 指令。再來把模組裝上 (`insmod`)。接著執行 `client.c`,但是把輸出導向 `out`,效果類似寫檔案。
接著的下一行就是比較透過 `fibdrv.c` 得到的費氏數列和預期答案 ( expected.txt ) 是否相同。事實上我認為沒有加這行的必要,第一個原因是 `expected.txt` 裡面的答案是錯的,超過 92 項以後都是一樣的數字。所以用這個檔案無法來檢查正確性。第二個原因是,其實下一行的 `verify.py` 就是在幫我們檢查 driver 的正確性。
把 `verify.py` 拿出來看看裡面在做什麼
```python
for i in range(2, 101):
expect.append(expect[i - 1] + expect[i - 2])
```
`python` 有一個非常棒的特性是整數並沒有上限,完全不用考慮 overflow 的問題。所以可以很輕鬆的直接把 101 項以前的答案紀錄在 `expect` 中。
```python
with open('out', 'r') as f:
tmp = f.readline()
while (tmp):
result.append(tmp)
tmp = f.readline()
f.close()
for r in result:
if (r.find('Reading') != -1):
..
```
接著把透過我們實做的 `fib_sequence` 得到的數列存到 `dics` 中 ( 當然要用 `split` 對字串做處理,才能只拿到數字 )。
```python
for i in dics:
fib = i[1]
if (expect[i[0]] != fib):
..
```
最後一個步驟就是檢查 `expect` ( 正確答案 ) 和 `dics` 中的數列是否吻合,只要一有不一樣的,就馬上輸出預期的答案和離開 `verify.py`。
### 增加運算範圍
原本作業說明提供的方法是透過 `struct bigN` 來存共 128 位元的資料。但是這個方法其實比較麻煩 (跨高低位的乘法需要考慮的事情很多,同時也會再次面對空間不夠的問題),且擴充性不好 (需要更多位呢? 總不能一直增加 struct 的成員吧)。於是我選擇用字元來表示數字,並且做出字元運算。
事實上,M H Rasel 已實做 [Big integer](http://billor.chsh.chc.edu.tw/IT/C/BigInteger.htm),我就直接拿來用。以下簡單摘要幾個 operation 如何實做。
首先定義一個新的型態作為運算的物件,當然若只是要用來 fast doubling 其實可以拿掉 `signbit`。因為是在負數減法才需要用到的 bit。
```cpp
typedef struct {
char digits[MAXDIGITS]; /* represent the number */
int signbit; /* 1 if positive, -1 if negative */
int lastdigit; /* index of high-order digit */
} bignum;
```
<br>
以下是加法的實做:
```cpp
int add_bignum(bignum *a, bignum *b, bignum *c)
{
..
if (a->lastdigit < b->lastdigit)
return add_bignum(b, a, c);
int k = c->lastdigit = a->lastdigit + 1;
c->digits[k--] = '\0';
carry = 0;
n_carry = 0;
for (i = b->lastdigit - 1, j = a->lastdigit - 1; i >= 0; i--, j--) {
carry = b->digits[i] - '0' + a->digits[j] - '0' + carry;
c->digits[k--] = (carry % 10) + '0';
carry = carry / 10;
if (carry)
n_carry++;
}
for (; j >= 0; j--) {
carry = a->digits[j] - '0' + carry;
c->digits[k--] = (carry % 10) + '0';
carry = carry / 10;
if (carry)
n_carry++;
}
if (carry)
c->digits[k] = carry + '0';
else {
char string[MAXDIGITS];
strlcpy(string, &c->digits[1], MAXDIGITS);
strlcpy(c->digits, string, MAXDIGITS);
c->lastdigit = c->lastdigit - k - 1;
}
return n_carry;
}
```
省略的部份是判斷是否遇到加負數的情況。若是,則呼叫成 subtract 的形式。接著加法是選定大者為被除數,兩者從尾端開始加。最後判斷是否進位,若進位則需要把 `c->digit[k]` 補上應有的數字。反之,則把結果往前移,填滿預留的空格。
<br>
以下是減法的實做:
```cpp
int subtract_bignum(bignum *a, bignum *b, bignum *c)
{
..
int k = c->lastdigit = MAX(a->lastdigit, b->lastdigit);
n_borrow = 0;
c->digits[k--] = '\0';
for (i = a->lastdigit - 1, j = b->lastdigit - 1; j >= 0; i--, j--) {
temp = a->digits[i] - '0' - (b->digits[j] - '0' + op);
if (temp < 0) {
temp += 10;
op = 1;
n_borrow++;
} else
op = 0;
c->digits[k--] = temp + '0';
}
while (op) {
temp = a->digits[i--] - op - '0';
if (temp < 0) {
temp += 10;
op = 1;
n_borrow++;
} else
op = 0;
c->digits[k--] = temp + '0';
}
for (; i >= 0; i--)
c->digits[k--] = a->digits[i];
for (i = 0; !(c->digits[i] - '0'); i++)
;
c->lastdigit = c->lastdigit - i;
..
}
```
同樣省略前面在判斷**非**正數減正數的情況。首先第一個迴圈負責相減,直到減數最高位被減完,接著只要把被減數多出的位數補上即可。
以下是乘法的實做:
```cpp
void multiply_bignum(bignum *a, bignum *b, bignum *c)
{
register long int i, j, k = 0;
short int num1[MAXDIGITS], num2[MAXDIGITS], of = 0, res[MAXDIGITS] = {0};
for (i = 0, j = a->lastdigit - 1; i < a->lastdigit; i++, j--)
num1[i] = a->digits[j] - 48;
for (i = 0, j = b->lastdigit - 1; i < b->lastdigit; j--, i++)
num2[i] = b->digits[j] - 48;
res[0] = 0;
for (j = 0; j < b->lastdigit; j++) {
for (i = 0, k = j; i < a->lastdigit || of; k++, i++) {
if (i < a->lastdigit)
res[k] += num1[i] * num2[j] + of;
else
res[k] += of;
of = res[k] / 10;
res[k] = res[k] % 10;
}
}
for (i = k - 1, j = 0; i >= 0; i--, j++)
c->digits[j] = res[i] + 48;
c->digits[j] = '\0';
c->lastdigit = k;
c->signbit = a->signbit * b->signbit;
}
```
相較字元相加減,字元並沒有提供**乘法**,所以不得已得先把數字擷取出來,分別存放到 `num1`和`num2`。接著迭代這兩個陣列把乘法結果放到 `res`,最後在填到 `c` 的 `digit` 中。
以上,fast doubling 所需要的運算都已經完成了,接著馬上開始建構 fast doubling。事實上,我們已經在第一個步驟中實做 fast doubling,儘管是用 `int` 的運算來做,但是我們只要把運算元換成對應的字元運算函數,也能達到一樣的效果。
```cpp
bignum a, b;
bignum big_two;
int_to_bignum(0, &a);
int_to_bignum(1, &b);
int_to_bignum(2, &big_two);
for (int i = 31 - __builtin_clz(k); i >= 0; i--) {
bignum t1, t2;
bignum tmp1, tmp2;
multiply_bignum(&b, &big_two, &tmp1);
(void) subtract_bignum(&tmp1, &a, &tmp2);
multiply_bignum(&a, &tmp2, &t1);
multiply_bignum(&a, &a, &tmp1);
multiply_bignum(&b, &b, &tmp2);
(void) add_bignum(&tmp1, &tmp2, &t2);
copy(&a, &t1);
copy(&b, &t2);
if ((k & (1 << i)) > 0) {
(void) add_bignum(&a, &b, &t1);
copy(&a, &b);
copy(&b, &t1);
}
}
return a;
```
可對照 `double_fib` 的 [pesudo code](https://hackmd.io/1UeUSEGCQOKQ1thdlKVd7A?both#%E4%BF%AE%E6%94%B9-fibonacci-%E6%BC%94%E7%AE%97%E6%B3%95) 不難發現兩者根本一樣。
:::warning
上述實作的問題在於,從十進位運算去思考,就會忽略尚可改善效能之處,運算過程如果全是二進位,就可運用 bitwise 去改善,最終再考慮到轉回十進位即可。參照 [bignum](https://github.com/sysprog21/bignum)
:notes: jserv
:::
### client 端如何取得結果
原本的實做是直接在 `fib_read` 中直接回傳 `fib_sequence` ,這是剛好回傳的東西是可以轉形成 `ssize_t` 所以才能這樣寫。但是如今我們使用了自己的型態 `bignum` ,所以無法直接強行轉型成 `ssize_t`。
```cpp
char kbuf[MAXDIGITS] = {0};
bignum res = fib_sequence(*offset);
snprintf(kbuf, MAXDIGITS, "%s", res.digits);
copy_to_user(buf, kbuf, MAXDIGITS);
return 0;
```
不過我們發現 `fib_read` 有傳一個 `buf` 進來,所以我們可以透過這個參數,從 user 端取得答案。這裡需要注意的是在 kernel 與 user space 間無法隨心所欲的交換資料,所以我們要透過 [copy_to_user](https://www.fsl.cs.sunysb.edu/kernel-api/re256.html) 來完成。
### 測試結果
```
Reading from /dev/fibonacci at offset 92, returned the sequence 7540113804746346429.
Reading from /dev/fibonacci at offset 93, returned the sequence 12200160415121876738.
Reading from /dev/fibonacci at offset 94, returned the sequence 19740274219868223167.
Reading from /dev/fibonacci at offset 95, returned the sequence 31940434634990099905.
Reading from /dev/fibonacci at offset 96, returned the sequence 51680708854858323072.
Reading from /dev/fibonacci at offset 97, returned the sequence 83621143489848422977.
Reading from /dev/fibonacci at offset 98, returned the sequence 135301852344706746049.
Reading from /dev/fibonacci at offset 99, returned the sequence 218922995834555169026.
Reading from /dev/fibonacci at offset 100, returned the sequence 354224848179261915075.
```
可查閱[預期答案](http://www.protocol5.com/Fibonacci)對照,發現正確。
## 測量 user space 和 kernel space 的傳遞時間
利用前面完成的 `fib_sequence`(增加計算範圍+ fast doubling)。在 kernel 中稍微修改程式,這次 read 的回傳值改成回傳 `fib_sequence` 的運算時間 ( 在 kernel space ),同時我們也計算在 user space 呼叫 `read` 前後的時間差距,因而得到在 kernel 運算的時間和在 user space 運算的時間。接著,把兩者相減得到傳遞時間 ( user -> kernel + kernel -> user )。
kernel
```cpp
static ssize_t fib_read(struct file *file,
char *buf,
size_t size,
loff_t *offset)
{
ktime_t k1, k2;
k1 = ktime_get();
bignum res = fib_sequence(*offset);
k2 = ktime_sub(ktime_get(), k1);
return (long long)ktime_to_ns(k2);
}
```
client
```cpp
for (int i = 0; i <= offset; i++) {
lseek(fd, i, SEEK_SET);
clock_gettime(CLOCK_REALTIME, &t1);
sz = read(fd, buf, MAXDIGITS);
clock_gettime(CLOCK_REALTIME, &t2);
printf("%d ",i);
// this is transporting time between kernel and user space
printf("%lld ",elapse(t1,t2) - sz);
// this is executing time in user space
printf("%ld ", elapse(t1, t2));
// this is executing time in kernel
printf("%lld \n",sz);
}
```
接著用 gnuplot 畫圖
![](https://i.imgur.com/KPD5xyo.png)
傳遞的時間不會因為 n 的大小而改變
## 自我檢查清單
### 研讀上述費氏數列相關材料 (包含論文),摘錄關鍵手法,並思考 clz / ctz 一類的指令對 Fibonacci 數運算的幫助。請列出關鍵程式碼並解說
已於第一部份- 修改 fibonacci 演算法 - 中討論。
### 複習 C 語言 數值系統 和 bitwise operation,思考 Fibonacci 數快速計算演算法的實作中如何減少乘法運算的成本;
### lsmod 的輸出結果有一欄名為 Used by,這是 “each module’s use count and a list of referring modules”,但如何實作出來呢?模組間的相依性和實際使用次數 (reference counting) 在 Linux 核心如何追蹤呢?
[lsmod](https://man.linuxde.net/lsmod) 可以列出目前系統中的模組有哪些,而下 lsmod 這個命令時就是在執行一支程式。比起找出 lsmod 的原始馬來研究它到底如何辦到追蹤模組的引用狀況,我發現一個更好的工具 [strace](https://medium.com/fcamels-notes/%E4%BD%BF%E7%94%A8-strace-%E4%BA%86%E8%A7%A3%E7%A8%8B%E5%BC%8F%E8%AE%80%E5%8F%96%E8%B3%87%E6%96%99%E7%9A%84%E4%BE%86%E6%BA%90-aaa17ee2df2b),它能夠逐行印出呼叫的 system call。如此一來,我們更能知道程式是透過什麼手段拿到資料。
```shell
$ strace lsmod
```
會得到一堆輸出,擷取重點:
```shell
write(1, "Module Size Us"..., 38Module Size Used by
) = 38
openat(AT_FDCWD, "/sys/module/fibdrv/refcnt", O_RDONLY|O_CLOEXEC) = 3
read(3, "0\n", 31) = 2
read(3, "", 29) = 0
close(3) = 0
```
看起來 lsmod 會到 `sys/module` 底下去拿資料,而 used by 這格欄位的值,看來和檔案 `refcnt` 相當有關係。接著直接找出這個檔案,觀察裡面是什麼。
```shell
$ cat refcnt
0
```
沒錯,得到的是一個數字,而這個數字剛好是 used by 的次數。
接著我們思考的是,什麼時候會讓 `refcnt` 的值變動?應該是 load 和 unload 時,所以開始找相對應的呼叫。
首先,我們要找尋的目標是紀錄 reference counting 的變數,根據 [module.c](https://github.com/torvalds/linux/blob/cc12071ff39060fc2e47c58b43e249fe0d0061ee/kernel/module.c) 中許多模組的初始化或刪除,都牽涉到一個 struct mod 型態的變數。於是我們開始找起這個變數的定義:
[/linux/module.h](https://github.com/torvalds/linux/blob/14cd0bd04907df79b36a31e55f18768172230987/include/linux/module.h)
```cpp=360
struct module {
enum module_state state;
/* Member of list of modules */
struct list_head list;
...
```
我們在 module.h 底下找到 module 的定義,接著往下繼續找,在 510 行找到了 `refcnt`。
```cpp=510
/* Destruction function. */
void (*exit)(void);
atomic_t refcnt;
#endif
```
再透過這個線索,找到 [module.c](https://github.com/torvalds/linux/blob/master/kernel/module.c) 底下的 system call: delete_module ,發現它會呼叫 try_stop_module
```cpp=1019
/* Stop the machine so refcounts can't move and disable module. */
ret = try_stop_module(mod, flags, &forced);
if (ret != 0)
goto out;
```
加上註解的提示,這個函數肯定和 `refcnt` 的維護有關。於是繼續往下找,發現 try_stop_module 會再呼叫 try_release_module_ref
```cpp
/* Try to release refcount of module, 0 means success. */
static int try_release_module_ref(struct module *mod)
{
int ret;
/* Try to decrement refcnt which we set at loading */
ret = atomic_sub_return(MODULE_REF_BASE, &mod->refcnt);
BUG_ON(ret < 0);
if (ret)
/* Someone can put this right now, recover with checking */
ret = atomic_add_unless(&mod->refcnt, MODULE_REF_BASE, 0);
return ret;
}
```
我們找到 try_release_module_ref 後,馬上發現裡面的 `atomic_sub_return(MODULE_REF_BASE, &mod->refcnt);` 就是我們在找的目標,它正在維護 `refcnt`。當 unload 時代表少一個人用,所以要減一。
:::warning
TODO: 從 module 間的相依性去考慮 reference count 的作用,不要只看 load/unload
:notes: jserv
:::
### 注意到 fibdrv.c 存在著 DEFINE_MUTEX, mutex_trylock, mutex_init, mutex_unlock, mutex_destroy 等字樣,什麼場景中會需要呢?撰寫多執行緒的 userspace 程式來測試,觀察 Linux 核心模組若沒用到 mutex,到底會發生什麼問題
mutex_lock 是為了保證 critical section 所需要用的手段,顧名思義,在 lock 間的程式碼即 critical section ,在此區段永遠只有一個人能進來。若你像我只用過 pthread 的話,那 mutex_trylock, mutex_unlock 對應的就是 pthread_mutex_lock 和 pthread_mutex_unlock。
那什麼情境下需要 lock?以下附上一個我曾經寫過的片段程式碼:
```cpp
pthread_mutex_lock(&mutex);
if(head!=NULL)
{
// (A)
// queue is not empty
queue *tmpq = head;
strcpy(query_str,tmpq->element);
send_fd = tmpq->fd;
head = head->next;
free(tmpq);
*(thread_state+offset) = 1;
}
pthread_mutex_unlock(&mutex);
```
以上功能是要把 list 的頭刪掉,若今天沒有鎖起來,同時有兩條 thread 進 if statement,倘若不幸的是 list 只剩一個 node,那麼這樣程式就會錯掉。
簡單寫一個 client 端程式,目的讓多執行緒同時 read 檔案(記得要把 `exit` 註解掉,否則無法觀察)
```cpp
void *task(void* in){
long long sz;
// u128_t sz;
char buf[MAXDIGITS];
char write_buf[] = "testing writing";
int offset = 100; /* TODO: try test something bigger than the limit */
int fd = open(FIB_DEV, O_RDWR);
if (fd < 0) {
perror("Failed to open character device");
// exit(1);
}
for (int i = 0; i <= offset; i++) {
lseek(fd, i, SEEK_SET);
sz = read(fd, buf, MAXDIGITS);
printf("Reading from " FIB_DEV
" at offset %d, returned the sequence "
"%s.\n",
i, buf);
}
}
int main(){
pthread_t t[5];
for(int i = 0; i < 5; i++)
pthread_create(&t[i], NULL, task, NULL);
for(int i = 0; i < 5; i++)
pthread_join(t[i], NULL);
return 0;
}
```
在 `fibdrv.c` 中要改的是:
```cpp
static int fib_open(struct inode *inode, struct file *file)
{
/*if (!mutex_trylock(&fib_mutex)) {
printk(KERN_ALERT "fibdrv is in use");
return -EBUSY;
}*/
return 0;
}
```
比較註解掉和沒註解掉的差異( 保留註解表示有上鎖 )
Ps: Makefile 也要修改
```=27
client: client.c
$(CC) -o $@ $^ -lpthread
```
以下是有上鎖的輸出:
```
Failed to open character device: Device or resource busy
Reading from /dev/fibonacci at offset 0, returned the sequence 0.
Failed to open character device: Device or resource busy
Failed to open character device: Device or resource busy
Reading from /dev/fibonacci at offset 1, returned the sequence 1.
Reading from /dev/fibonacci at offset 0, returned the sequence .
Reading from /dev/fibonacci at offset 1, returned the sequence .
Failed to open character device: Device or resource busy
Reading from /dev/fibonacci at offset 2, returned the sequence .
..
Reading from /dev/fibonacci at offset 10, returned the sequence 55.
Reading from /dev/fibonacci at offset 11, returned the sequence 89.
Reading from /dev/fibonacci at offset 12, returned the sequence 144.
Reading from /dev/fibonacci at offset 13, returned the sequence 233.
```
以下是沒上鎖的輸出:
```
Reading from /dev/fibonacci at offset 99, returned the sequence 218922995834555169026.
Reading from /dev/fibonacci at offset 75, returned the sequence 2111485077978050.
Reading from /dev/fibonacci at offset 100, returned the sequence 354224848179261915075.
Reading from /dev/fibonacci at offset 76, returned the sequence 3416454622906707.
Reading from /dev/fibonacci at offset 77, returned the sequence 5527939700884757.
Reading from /dev/fibonacci at offset 78, returned the sequence 8944394323791464.
Reading from /dev/fibonacci at offset 79, returned the sequence 14472334024676221.
Reading from /dev/fibonacci at offset 80, returned the sequence 23416728348467685.
Reading from /dev/fibonacci at offset 81, returned the sequence 37889062373143906.
Reading from /dev/fibonacci at offset 82, returned the sequence 61305790721611591.
Reading from /dev/fibonacci at offset 83, returned the sequence 99194853094755497.
Reading from /dev/fibonacci at offset 84, returned the sequence 160500643816367088.
Reading from /dev/fibonacci at offset 85, returned the sequence 259695496911122585.
Reading from /dev/fibonacci at offset 86, returned the sequence 420196140727489673.
Reading from /dev/fibonacci at offset 87, returned the sequence 679891637638612258.
Reading from /dev/fibonacci at offset 88, returned the sequence 1100087778366101931.
Reading from /dev/fibonacci at offset 89, returned the sequence 1779979416004714189.
Reading from /dev/fibonacci at offset 90, returned the sequence 2880067194370816120.
Reading from /dev/fibonacci at offset 91, returned the sequence 4660046610375530309.
Reading from /dev/fibonacci at offset 92, returned the sequence 7540113804746346429.
Reading from /dev/fibonacci at offset 93, returned the sequence 12200160415121876738.
Reading from /dev/fibonacci at offset 94, returned the sequence 19740274219868223167.
Reading from /dev/fibonacci at offset 95, returned the sequence 31940434634990099905.
Reading from /dev/fibonacci at offset 96, returned the sequence 51680708854858323072.
Reading from /dev/fibonacci at offset 97, returned the sequence 83621143489848422977.
Reading from /dev/fibonacci at offset 98, returned the sequence 135301852344706746049.
Reading from /dev/fibonacci at offset 99, returned the sequence 218922995834555169026.
Reading from /dev/fibonacci at offset 100, returned the sequence 354224848179261915075.
```
令人訝異的是,反而沒上鎖的表現在某些地方竟然比較好。可以觀察到**上鎖版本**的輸出會因為有上鎖,所以不讓你讀,自然沒辦法算費氏數列,因此看到 `Reading from /dev/fibonacci at offset 1, returned the sequence .` 的輸出。反之,**沒上鎖版本**仍允許同時多個執行緒去開檔案,因此每條 thread 都能算出數字出來。此外也可以觀察到有上鎖版本輸出的次序有由小到大,反之則沒有。
但是話說回來,為什麼沒上鎖也能算出正確的值。我想是因為開檔案**讀**而非**寫**,並不會造成競爭或互斥等現象,所以多個人同時讀也不會造成程式錯誤。
:::warning
由此也不難發現,lock 也該依據適用場域去調整,例如 [Readers–writer lock](https://en.wikipedia.org/wiki/Readers%E2%80%93writer_lock) 針對單一寫入者、多個讀取者的情境去特化同步處理機制。
:notes: jserv
:::
## Reference
* [VFS](http://sp1.wikidot.com/linuxvfs)
* [bigN](https://stackoverflow.com/questions/25095741/how-can-i-multiply-64-bit-operands-and-get-128-bit-result-portably)