contributed by < fennecJ
>
HenryChaing
學到很多之前曾未想過得串列實作方式,其他留言放在內文。
q_insert_head
commit 56af0a
對照 Dictionary.com 對 implement 一詞的解釋:
「實作」會是符合上述 "implement" 語意的譯詞,取「起」以「創作」的意思,而「製造」本質上是固定輸入並通常產生固定輸出的行為。
一開始我的作法如下:
不要寫錯重要軟體的名稱。
但這樣在嘗試 commit 的時候會一直被 Cppcheck 擋下來:
一開始嘗試查閱 Cppcheck manual ,但卻沒能解決我的問題。
花了一些時間後我猜 ele->list
如果沒有初始化的話 list->next
, list->prev
不知道會指到哪邊造成潛在的錯誤。因此一開始以 INIT_LIST_HEAD 初始化 ele->list
的方式通過 Cppcheck。 但仔細檢視 list_add 後留意到它會明確將 list->next
, list->prev
指到原本的 head->next
和 head
,不符前述假設。
之後做了許多嘗試,最後發現問題出在最後面的 list_add(&(ele->list), head)
,將其改為 list_add(&ele->list, head)
(移除 ele->list 前後的括號)後就能通過 Cppcheck 的靜態檢查。
q_remove_head
看到 remove 這個字的時候,一開始直覺認為會需要 free 記憶體空間,直到檢視 queue.h
中的註解:
才知道只需 unlink 即可。
檢視 List Management Functions ,留意到 list_del
這個 API ,到 list.h
中可看到對應實作僅有將 node 進行 unlink:
同時可以留意裡面的 LIST_POISONING 會將 node 的前後指向非法的記憶體位址,用來防止 access via deleted node's next/prev,為何不將 node's next/prev 直接指向 NULL,這是為了能幫助我們更好的 debug ,可參考 What is the role of LIST_POISON1 and LIST_POISON2 的討論,若我們知道 page fault 的 addr 是 LIST_POISON1/2 ,我們能更容易定位到可能出錯的程式碼有哪些。
授課老師有建議可以多使用中文專有名詞,如節點。
HenryChaing
q_merge
commit 285041
不斷的將第二個以後的 queue merge 到第一個 queue 中即可。而在 merge 二個已排序的雙向鏈結串列時,為了方便處理迴圈結束的判斷條件,會將二個鏈結串列都改為 singular list
最後透過一個 helper function rebuild_list_link
將全部合併完的 list 回復為雙向鏈結串列。
值得一提的是在合併比較時用了一個 bitwise 的小技巧:
直接用 xor descend 來判斷接下來的 node 應該是接 l1 的 node 或是 l2 的 node ,但會導致 sort unstable 的問題。
可以了解互斥或操作,但想請教您為何結果會 unstable
HenryChaing
您好,stable sort 的定義為若存在元素 A, B 滿足兩者鍵值相等,則 sort 後 A, B相對位置不變。現在我們檢視上述透過互斥或的操作,假設 ele1->value 和 ele2->value 相等且 ele1 在 ele2 前面,若要滿足 stability , ele1 在排序後也需要在 ele2 前面。若此時 descend = ture ,則 (strcmp(…) < 0 ) ^ descend 會等於 (0) ^ 1 = 1 。導致 ele2 先於 ele1 被放進 sorting 後的佇列中, ele1, ele2 兩者的前後關係發生改變。造成結果 unstable 。 在這裡用 ^ descend 的問題為,在 ele1, ele2 兩者相等時也會達成交換順序的條件。
fennecJ
q_delete_mid
commit e51e73
改進你的漢語表達。
我的作法是用兩個指標 forward, backward 以對向、相同移動速度來存取節點。由於這裡處理的是雙向鏈結串列,因此當兩個指標以相反等速的方向移動時,他們最終會相遇,下圖是一個簡單的示意:
為了說明為何他們相遇在中間點,現在我們將他們的起點切開,把圓攤平:
此時我們假設 Backward 和 Forward 相遇時, Backward 移動了 K 個距離。由於 Forward 以相同的速度移動,因此 Forward 的移動距離亦為 K 。我們可以得到
因此他們相遇的地方就在中間點。取出中間點後把它刪除即可。
q_reverse
commit b7150e
將每個 node 的 prev 和 next 對調即可。
不過要注意 list_for_each 系列的 MACRO 都是從 head->next 開始 traverse ,所以要記得另外更新 head 的 prev 和 next
q_reverseK
commit 1e64e3e
q_reverseK
的處理步驟如下:
q_descend
, q_ascend
commit 1e64e3e
由於處理 q_descend
和 q_ascend
的主要邏輯相同,因此這裡只介紹 q_decend 的作法:
從最後一個 node head->prev
開始往前面的 node 看,並紀錄目前看過得最大 string max_str 。若往前看時該 node 的值小於 max_str ,則移除該 node。
研讀 整合網頁伺服器 時,學到了一個系統呼叫 select() 。
目前的理解如下:
使用者可將多個 file descriptor 傳入同一個集合 set 中,而 select 會同時 monitor 這些 file descriptor ,並透過 return value 告知使用者本次讀到的 file descriptor 是哪一個 file descriptor。
用數位邏輯裡頭的 MUX 解釋。
Mux (多工器)在數位邏輯中指的是可根據 sel 訊號從多個 input 中選擇一個訊號傳送到 output 的硬體,一個常見的 2-to-1 mux 構造如下:
以硬體描述語言 (HDL) 展現其行為:
而 select()
系統呼叫可被視為:
FD_ISSET()
得知 select 訊號 S0 為何Demux (解多工器) 在數位邏輯中指的是可根據 sel 訊號將一個訊號傳送到指定的接收端的硬體,一個常見的 1-to-2 demux 構造如下:
我們透過 FD_ISSET()
得知 select 訊號 S0 之後,可以根據 S0 進行後續的處理,就像是 Demux 的行為一樣:
可對照 console.c
中 cmd_select 的實做
:
在 linenoise.c
中,主要讀取使用者輸入的 function 是 line_edit()
這個 function :
這裡的 l.ifd 會是 stdin 的 fd ,透過 read 從 stdin 讀取一個 char 到 c 在交由後續進行處理。 若要讓 linenoise 也支援透過網頁伺服器接收命令,我們可以用 select() function 做 I/O multiplexing ,並依據收到的輸入是經由網路還是 stdin 做對應的處理。
file 的翻譯是「檔案」,而非「文件」(document)
檢視 console.c
,可以看到 do_web
這個 function ,透過字串查找卻沒有看到有哪個文件 檔案有透過 do_web(
呼叫這個 function 。然而透過 ./qtest 開啟程式後輸入命令 web
可以看到它會去執行 do_web
函式。仔細檢視和 web 相關的程式碼,發現 console.c 中有使用到一個巨集:
ADD_COMMAND
巨集展開如下:
可以看到它會藉由 do_##cmd
將 ADD_COMMAND(web,...)
展開為
add_cmd(web, do_web, ...)
當 console 接收到 web 命令時,就會執行對應的 do_web() ,這裡我們簡單檢視其實作(詳見 github :
改進上方描述
可以注意到其中的 use_linenoise = false
,這是因為藉由 curl 透過網頁伺服器傳命令到 console 中必須要傳輸完整的命令,用不上自動補全,所以當啟用 web 命令後會將 use_linenoise
設為 false
檢視 run_console()
可得知 console 處理 input 的流程:
先判斷是否有指定輸入檔案,若無 (i.e. stdin or via web) 則接著判斷是否能用 use_linenoise ,並透過 cmd_select 處理 input 。
cmd_select 是一個根據不同的 fd (stdin or web_fd) 進行對應處理的 function ,其實作可見 github
當開啟 web server 後,便再也不能使用 linenoise 的功能。有沒有辦法能同時支援 web server 又支援 linenoise 呢?
commit ed8a2ad
由於使用 web
命令後便會把 linenoise 的功能關掉,因此我決定新增一個命令 web_close
讓使用者能在開啟 web server 後把它關起來,而在關掉 web server 的同時會再次將 linenoise 的功能打開。
millaker's commit
增加 web_close
讓使用者能夠重新開啟 linenoise 的功能,但在 web server 運作時仍會導致 linenoise 失效。可否進一步使 web server 運作的同時 console 中的 linenoise 也能同時運作呢?
強者我朋友 millaker 提出了一個解決思路:
無論何時都 call 到 linenoise 中,在 line_edit 內才分辨目前的 fd 是 stdin 還是 web_fd ,並根據對應的 fd 做後續的處理:
只要是 std_in 就進行 linenoise 的自動補全;只要是 web_fd 就直接處理 web_fd 傳來的命令。
但這樣還是會有一個缺陷,就是當使用者在 console 中使用了自動補全之後,經由 curl 傳到 web server 的命令沒辦法立刻被收到,要等到使用者在 console 中按下 enter 後才會被接收到。和 millaker 討論後,我們發現 linenoise 中的 complete_line() 有一個 while loop ,直到使用者按下 enter 後才會離開這個迴圈,這時程式才有機會注意到 web_fd 中有尚未讀取的 data 的原因導致這個現象。我們可以很直接的修改 complete_line() 中處理的流程來解決 curl 被 linenoise 阻塞住的問題,但這會導致 linenoise 套件的運作邏輯被大幅改動,不利於後續的維護。
查資料的過程中,大致看了 SIGIO/poll/epoll 等等可以用來處理 socket data 的作法,但無論是哪個 function 似乎都不能很好 / 優雅的處理上述的問題。
如果要作到兩邊都能即時有反應,我目前只想到透過 context switch 的方式輪流處理 stdin 和 web_fd ,不知道還有什麼好方法?
vax-r 同學也對同時使用網頁伺服器和 linenoise 提出他的 解決方案 ,我十分贊同他 「盡可能對 linenoise 做最小改動」的這個想法,由於 linenoise 是額外整合在作業中的套件,日後若要將其 sync 到 upstream ,較小的改動能讓我們更容易進行更新的維護以及減少 bug 發生的機會。
我嘗試了 vax-r 同學的 repo 但仍會出現前述問題。
為了清楚說明,我將開啟兩個 terminal 並分成兩個名字: term_stdin, term_web
其中 term_stdin 用來透過終端機藉由 linenoise 直接和 qtest 互動
term_web 僅會透過 curl 將命令傳入 qtest 中的網頁伺服器。
依照下列步驟執行可重現我的問題:
tab
使其自動補全為 new( ▮ 為游標符號 )
http://localhost:9999/it/a
並按下 enter ,會發現該命令被 block 住, qtest 不會處理它我認為先送出的命令 (it a 先被 enter 送出) 應先被處理較合理,但顯然 output 不符合先送出先處理的原則。(若先處理 it a 預期應可見到 Warning: Calling insert tail on null queue
等訊息)
I-HSIN Cheng
經過測試後,上述問題的發生是由於函式complete_line
當中的read
函式呼叫引起的阻塞問題,當我們按下 tab (line_edit
當中的 main loop 監聽) 進行自動補全後,程式行為實際上是交給complete_line
來進行自動補全,並在補全後依然停留在complete_line
當中的read
函式等待使用者進一步輸入,造成此./qtest
這個 process 阻塞在此處。
同時您在 term_web 下達的curl
命令則被寫入 socket fd 指向的 file 當中等待讀取,直到您在 term_stdin 按下ENTER
使得./qtest
跳出complete_line
的迴圈並完成對應命令後,才去處理 socket fd 當中的指令。
fennecJ
原來如此,謝謝您耐心解釋程式流程和前述現象發生的原因。
I-HSIN Cheng
此處我認為有兩個解決方法。第一種是同時間處理多個請求的話,利用多執行緒的方法分開處理,畢竟linenoise
原本的設計就建立在面對單一命令輸入的情形。第二種是同樣利用web_eventmux
在complete_line
內做監聽,將term_web
傳送進來的命令暫存,並在complete_line
回傳時讓line_edit
有兩個命令必須執行。我認為第一個方法較為恰當,第二個方法會大幅改動到linenoise
套件預設行為(試想如果同時開啟 n 個 terminal 並對./qtest
發送請求,此方法就要暫存n
條指令)。 此處應請教 Jserv 老師何種變更較為合適。
我認為還有一種較暴力的可能方法是在web_eventmux
當中若收到網路封包請求就直接執行interpret_cmd
將該命令直接執行,執行完後回到linenoise
中的complete_line
或line_edit
繼續監聽 standard input 的命令,但此方法同樣涉及大量改動與變更web
預設行為,我認為能讓功能執行但不適當。
fennecJ
原先我和 millaker 討論的解法也是您提到的第二點 - 在 complete_line 中進行 I/O 的監聽,但正如您提到的,此種方法將大幅改動linenoise
原先的行為,可能導致後續維護上的困難。
I-HSIN Cheng
我使用一個替代方案,依據我上次提交的 PR ,如果在開啟web
命令後,在 command-line 寫入任何指令(不按 tab ),並在另一個 terminal 傳入 command ,則程式會將 command buffer 當中的指令替換成 web request 傳送進來的指令,於是原本 command-line 的命令會被丟棄。如果此行為是可接受的,那只要在complete_line
當中進行以下更動,即可做到相同的行爲。如果此行為是可接受的,我將於近日提交新的 PR 以修復此問題。
當初設計這項作業時,希望引導學員引入 coroutine 來解決問題,例如 Coroutines in C,工程領域本來就常見殊途同歸,很難說一個方法就絕對優於另一者,尚有其他的權衡 (trade-off) 因素,如程式碼的維護成本、持續擴充的考量。
commit 7002a4
照著教授提供的 Fisher–Yates shuffle 演算法可很容易的實作出來,比較特別的是和 404allen404 討論時他指出其實不用特別花心思處理要交換的 node->prev
, next
等 link 的資料,而是可以直接交換 node 中 element 存的值。我覺得這個想法很好,因此用這個方法進行交換。
研讀指定的論文
為了方便後續的維護,我將 ttt 獨立為一個目錄並整合進 lab0-c 中,詳見 commit 613374
我將 ttt 的運作過程分為幾個並行程式:
task 的 schedule 則參考 preempt_sched 中的實做,同時在必要的地方直接呼叫 schedule()
將 cpu 使用權交出去增進使用體驗。
要注意的是,由於任一邊( O 或 X )下完之後都有可能滿足獲勝或平手的條件,因此實做上需要在 player_X_move/player_O_move 後都呼叫一次 game_manager 。另外也記得要分別呼叫 update_screen ,避免
此外,為了方便不同並行程式間的溝通,我定義了一個結構 game_arg
:
而 player_X_move
和 player_O_move
運作上會分別呼叫 agentX
和 agentO
來決定要下在哪裡,如此只要替換掉 更改 agentX 和 agentO 指向的 function 便能很簡單的替換不同的 ai 邏輯。
commit 2628ae
commit 4a2111
我留意到大部分同學在進行 ai vs human 的作業時無法同時顯示出當前的時間(精確到秒),而想要達到這樣的效果需要解決一個問題,就是若直接透過 read()
去捕捉使用者的輸入,則因為 nonblocking read 以及處理 IO 時會關閉 preemption 的關係,若使用者完全沒有在進盤上進行任何輸入,則畫面會直接卡在那邊無法透過 scheduler 跳到別的 coroutine 執行。
這裡我嘗試透過 poll()
系統呼叫,當 poll 偵測到 stdin 有新的 input ,則會直接由 read 讀取一個字元並寫入 input buffer ,直到偵測到使用者按下 enter 後才根據 input buffer 內容執行對應操作。若 poll 尚未偵測到 stdin 有新的 input ,則會直接呼叫 scheduler 進行排程讓畫面時間能夠刷新。而由於只有在明確知道有字元需要讀取時才會呼叫 read()
,此時由於 STDIN 有尚未讀取的字元存在所以立刻會處理完往後執行,而不會卡在那邊等新的輸入,如此便能造成即時反應的效果達到顯示出當前的時間(精確到秒)的功能。
commit 76d90c
為了讓 ai vs human 也能順利顯示出當前的時間(精確到秒),我們仍需要 scheduler 的介入適時切換到印出時間資訊的 task 。
我在 listen_keyboard 中額外處理了 ENTER 和 BACKSPACE 事件,讓使用者能藉由 listen_keyboard 輸入座標並顯示在終端機上,同時透過 shceduler 來呼叫 update_screen 使時間能順利更新。
以下為 demo 影片:
實做主要參考 Clay S. Turner 提出的 A Fast Binary Logarithm Algorithm 以及對應的實現 程式碼
使用牛頓法進行開根號運算:
假設我們要以牛頓法求 val 之根:
令方程式
我們要求的根 a 滿足
用牛頓法進行迭代,假設 a 初始值為 , 可得
已知
則我們可以得出過點 () 之切線方程 L1 為
令 為 的根,則可求出
之後我們便能再用 a_1 進行後續的迭代。
加速牛頓法的迭代
若初始值足夠靠近目標點則可以用更少的迭代次數取得足夠的精度。
已知 ,則我們可以知道 必定滿足
因此我們可將初始值設為
而 MSB 的計算可由
得出。
然而這種估計方式只對整數域有效,假設傳入的定點數小數域共有 PRECISION 個 bits ,則
會變成
且在計算 MSB 時,也要把 PRECISION 佔的 bit 數剪掉:
最後將其整理為初始值的計算公式:
目前取 PRECISION 為 20 bits ,經過實驗,只要進行四次牛頓法就能收斂到可接受的誤差範圍。
之所以取 20 的原因是因為 mcts 演算法中的 ITERATION 次數為 而計算 ust 的公式為
因此小數部份需要能有 的精度,因此取 20 bits 。
最後我們可以實做出對應的開根號運算:
直接進行乘除運算後記得 div 要左移; mult 要右移 PRECISION 位即可。