2020q1 Homework1 (lab0)
contributed by < eric5800602
>
開發環境
作業要求
在 queue.[c/h]
中完成以下實作
q_new
: 建立新的空 queue
q_free
: 釋放 queue
所佔用的記憶體
q_insert_head
: 在 queue
開頭加入給定的新元素,並以LIFO準則
q_insert_tail
: 在 queue
尾部加入給定的新元素,並以FIFO準則
q_remove_head
: 移除在 queue
開頭的元素。
q_size
: 計算 queue
中的元素數量。
q_reverse
: 將 queue
反轉,此函式不能配置或釋放任何記憶體,也就是說只能重新鏈結已經存在的元素;
q_sort
: 以 natural sort來排序鏈結的元素
實作
queue_t
資料結構
為了讓 q_insert_tail
與 q_size
的實作能在時間複雜度為\(O(1)\)的條件下完成,所以根據 linux2020-lab0#牛刀小試 所述,在 queue_t
的結構中增加:
tail
記錄 queue
的尾部元素的記憶體位置
size
儲存 queue
的總元素個數
q_new
q_free
- 因傳入的
queue
有可能為 NULL
,為提高程式碼的穩固程度,所以需要加入判斷
- 為避免出現 memory leak 之情況,須確保每個元素都被釋放掉
- 利用
tmp
儲存當前 head
下一個元素的位置,並釋放 head
元素,再讓 queue
的 head
指到下一個元素(也就是 tmp
),直到下一個元素位置為 NULL
- 最後釋放
queue
本身的記憶體
q_insert_head
- 判斷 input
queue
是否為空,因需插入新的元素,所以也須判斷 malloc
之回傳值
- 新元素的
value
申請空間為本身字數加上最後的'\0'
也就是 (strlen(s) + 1) * sizeof(char)
- 新元素的
value
在 malloc
後也要記得判斷回傳值
- 使用
memset
確保記憶體區塊初始化
- 使用
strncpy
確保不會發生緩衝區溢位之問題
- 讓新創建元素的
next
指向當前 queue
中的 head
,再將 queue
中的 head
指向新創建的元素
q_insert_tail
- 為在時間複雜度為\(O(1)\)的條件下完成,利用
queue
中本身已儲存之 tail
的記憶體位置以達成
- 基本之考慮點與
q_insert_head
同
- 如果在
q_insert_tail
之前 queue
中無元素,需額外更新 queue
中 head
之值
q_remove_head
- 先確認
queue
是否為空,以及其中是否有元素可以被移除
- 初始化
sp
中的內容,以免舊資料未清空造成錯誤,並全部指定為 '\0'
- 同樣使用
strncpy
確保無緩衝區溢位問題,只更新到 bufsize-1
是因為確保字串結尾為原本初始化之 '\0'
- 紀錄原始的
head
位置,並更新 queue
中 head
的位置至下一個
- 釋放原始
head
的記憶體空間
q_reverse
- 原本沒看到不能新增釋放記憶體,使用類似 Reversing a Queue 的方法,結果在
malloc
時一直出現 FATAL ERROR: Calls to malloc disallowed
,最後在看 comment 時才看到不能新增及釋放記憶體,現已修正
- 在
queue
為 NULL
、 queue
中無元素 、 queue
中只有一個元素之狀況無須 reverse
- 先把
tail
改為 head
,並利用 back
及 front
兩 pointer 紀錄下一個以及前一個的元素
- 記得要將
q->tail->next
清除為 NULL
,不然會造成環狀結構
- 利用 while loop 將當前元素的指標方向顛倒,直到
q->head->next
為 NULL
也就是到原本 queue
的尾巴
- 曾試過只利用一個
tmp
來暫時存放前一個元素位置但發現還有原本的 q->head->next
需要存,不然一旦將 q->head->next
改變,就再也找不到它了,所以才有第 25 行錯誤寫法的 comment
q_size
- 因為在
queue.h
中 queue_t 有加入 size 紀錄 queue 的大小,可以直接作為回傳值,因此時間複雜度是\(O(1)\)
q_sort
- 根據 LinkedListSort 中的 Merge Sort 製作 q_sort,用以達成時間複雜度為 \(O(nlogn)\) 並通過自動測試程式
- 因不能新增或釋放記憶體,所以也不能使用 Pseudo Node 實作
- 在 2/17 前實作,所以也沒有用到 natural sort
- 以上之程式碼會造成
Program received signal SIGSEGV, Segmentation fault.
之錯誤,並在 valgrind 中出現ERROR: Time limit exceeded. Either you are in an infinite loop, or your code is too inefficient
及 Address 0x0 is not stack'd, malloc'd or (recently) free'd
- 利用 GDB 測試程式碼在 gdb bt 發現以下情況,一直不斷地呼叫merge,查詢及看到社團中有人有同樣的情況,說明是 stack overflow 造成的
- 為證明此情況為 stack overflow 所造成的另外寫一個程式並同樣利用 GDB 觀察
- 程式碼:
- gdb bt 後也發生同樣的情況,在 valgrind 中也清楚地表示發生
Stack overflow in thread #1: can't grow stack to 0x1ffe801000
之情況
- 為改善上述錯誤情況,將 merge sort 從遞迴形式改寫為迭代的方式,並改用
natsort
中的 strnatcmp
函式做比較
- 將 queue sort 問題 divide and conquer,最後 return 結果之
queue->head
- 利用
q_sort
呼叫 mergeSortList
- 呼叫完成後須變更
q->tail
中的位置,避免 sort
後呼叫 q_insert_tail
造成錯誤
- 根據 natsort 原專案內的 test 寫出類似的 trace file
- 例如:
- 但利用 nasort 卻會出現
ERROR: Not sorted in ascending order
- 我認為這與自動測試程式的測試方式有關係,於是去找了
qtest
中測試 q_sort
的部分
- 其中檢查比較的函式是利用 strcasecmp 與 natsort 不同
- 在下述範例中,可看出在某些情況時兩函式之相異結果
- strcasecmp 之排序結果會是:
pic01 pic02 pic02000 pic02a pic05 pic100 pic100a pic120 pic121 pic2 pic3..
- strnatcmp 之排序結果會是:
pic01 pic02 pic02a pic02000 pic05 pic2 pic3 pic4 pic100 pic100a pic120..
Valgrind 的運用
-
2/22 make valgrind 時出現重大錯誤(2/23 解決)
-
今天在 fetch upstream 後在嘗試 make valgrind 時,出現 Call of 'valgrind /tmp/qtest.lgvfhA -v 1 -f ./traces/trace-01-ops.cmd' failed: [Errno 2] No such file or directory
類似錯誤,檢查發現全部測試檔案都還在 traces 目錄中沒有錯
“directory” 一詞的翻譯是「目錄」,不要跟 folder (檔案夾) 搞混了,後者是 Microsoft Windows 提出的用語。“directory” 這詞彙已行之有年,我們會說 UNIX directory,而非 folder。
:notes: jserv
SymbolWu瞭解,感謝提供觀念釐清
- 而我利用舊的
driver.py
進行 make valgrind
時,就回復正常,研判可能是 driver.py
的問題
- 在此附上正常執行與有錯誤的
dirver.py
import subprocess
import sys
import getopt
class Tracer:
traceDirectory = "./traces"
qtest = "./qtest"
command = qtest
verbLevel = 0
autograde = False
useValgrind = False
traceDict = {
1 : "trace-01-ops",
2 : "trace-02-ops",
3 : "trace-03-ops",
4 : "trace-04-ops",
5 : "trace-05-ops",
6 : "trace-06-string",
7 : "trace-07-robust",
8 : "trace-08-robust",
9 : "trace-09-robust",
10 : "trace-10-malloc",
11 : "trace-11-malloc",
12 : "trace-12-malloc",
13 : "trace-13-perf",
14 : "trace-14-perf",
15 : "trace-15-perf"
}
traceProbs = {
1 : "Trace-01",
2 : "Trace-02",
3 : "Trace-03",
4 : "Trace-04",
5 : "Trace-05",
6 : "Trace-06",
7 : "Trace-07",
8 : "Trace-08",
9 : "Trace-09",
10 : "Trace-10",
11 : "Trace-11",
12 : "Trace-12",
13 : "Trace-13",
14 : "Trace-14",
15 : "Trace-15"
}
maxScores = [0, 6, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7]
def __init__(self, qtest = "", verbLevel = 0, autograde = False, useValgrind = False):
if qtest != "":
self.qtest = qtest
self.verbLevel = verbLevel
self.autograde = autograde
self.useValgrind = useValgrind
def runTrace(self, tid):
if not tid in self.traceDict:
print("ERROR: No trace with id %d" % tid)
return False
fname = "%s/%s.cmd" % (self.traceDirectory, self.traceDict[tid])
vname = "%d" % self.verbLevel
clist = [self.command, self.qtest, "-v", vname, "-f", fname]
try:
retcode = subprocess.call(clist)
except Exception as e:
print("Call of '%s' failed: %s" % (" ".join(clist), e))
return False
return retcode == 0
def run(self, tid = 0):
scoreDict = { k : 0 for k in self.traceDict.keys() }
print("---\tTrace\t\tPoints")
if tid == 0:
tidList = self.traceDict.keys()
else:
if not tid in self.traceDict:
print("ERROR: Invalid trace ID %d" % tid)
return
tidList = [tid]
score = 0
maxscore = 0
if self.useValgrind:
self.command = 'valgrind'
else:
self.command = self.qtest
self.qtest = ''
for t in tidList:
tname = self.traceDict[t]
if self.verbLevel > 0:
print("+++ TESTING trace %s:" % tname)
ok = self.runTrace(t)
maxval = self.maxScores[t]
tval = maxval if ok else 0
print("---\t%s\t%d/%d" % (tname, tval, maxval))
score += tval
maxscore += maxval
scoreDict[t] = tval
print("---\tTOTAL\t\t%d/%d" % (score, maxscore))
if self.autograde:
jstring = '{"scores": {'
first = True
for k in scoreDict.keys():
if not first:
jstring += ', '
first = False
jstring += '"%s" : %d' % (self.traceProbs[k], scoreDict[k])
jstring += '}}'
print(jstring)
def usage(name):
print("Usage: %s [-h] [-p PROG] [-t TID] [-v VLEVEL] [--valgrind]" % name)
print(" -h Print this message")
print(" -p PROG Program to test")
print(" -t TID Trace ID to test")
print(" -v VLEVEL Set verbosity level (0-3)")
sys.exit(0)
def run(name, args):
prog = ""
tid = 0
vlevel = 1
levelFixed = False
autograde = False
useValgrind = False
optlist, args = getopt.getopt(args, 'hp:t:v:A', ['valgrind'])
for (opt, val) in optlist:
if opt == '-h':
usage(name)
elif opt == '-p':
prog = val
elif opt == '-t':
tid = int(val)
elif opt == '-v':
vlevel = int(val)
levelFixed = True
elif opt == '-A':
autograde = True
elif opt == '--valgrind':
useValgrind = True
else:
print("Unrecognized option '%s'" % opt)
usage(name)
if not levelFixed and autograde:
vlevel = 0
t = Tracer(qtest = prog, verbLevel = vlevel, autograde = autograde, useValgrind = useValgrind)
t.run(tid)
if __name__ == "__main__":
run(sys.argv[0], sys.argv[1:])
import subprocess
import sys
import getopt
class Tracer:
traceDirectory = "./traces"
qtest = "./qtest"
command = qtest
verbLevel = 0
autograde = False
useValgrind = False
traceDict = {
1: "trace-01-ops",
2: "trace-02-ops",
3: "trace-03-ops",
4: "trace-04-ops",
5: "trace-05-ops",
6: "trace-06-string",
7: "trace-07-robust",
8: "trace-08-robust",
9: "trace-09-robust",
10: "trace-10-malloc",
11: "trace-11-malloc",
12: "trace-12-malloc",
13: "trace-13-perf",
14: "trace-14-perf",
15: "trace-15-perf",
16: "trace-16-perf",
17: "trace-17-complexity"
}
traceProbs = {
1: "Trace-01",
2: "Trace-02",
3: "Trace-03",
4: "Trace-04",
5: "Trace-05",
6: "Trace-06",
7: "Trace-07",
8: "Trace-08",
9: "Trace-09",
10: "Trace-10",
11: "Trace-11",
12: "Trace-12",
13: "Trace-13",
14: "Trace-14",
15: "Trace-15",
16: "Trace-16",
17: "Trace-17"
}
maxScores = [0, 6, 6, 6, 6, 5, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 5]
def __init__(self,
qtest="",
verbLevel=0,
autograde=False,
useValgrind=False):
if qtest != "":
self.qtest = qtest
self.verbLevel = verbLevel
self.autograde = autograde
self.useValgrind = useValgrind
def runTrace(self, tid):
if not tid in self.traceDict:
print("ERROR: No trace with id %d" % tid)
return False
fname = "%s/%s.cmd" % (self.traceDirectory, self.traceDict[tid])
vname = "%d" % self.verbLevel
clist = [self.command, "-v", vname, "-f", fname]
try:
retcode = subprocess.call(clist)
except Exception as e:
print("Call of '%s' failed: %s" % (" ".join(clist), e))
return False
return retcode == 0
def run(self, tid=0):
scoreDict = {k: 0 for k in self.traceDict.keys()}
print("---\tTrace\t\tPoints")
if tid == 0:
tidList = self.traceDict.keys()
else:
if not tid in self.traceDict:
print("ERROR: Invalid trace ID %d" % tid)
return
tidList = [tid]
score = 0
maxscore = 0
if self.useValgrind:
self.command = 'valgrind ' + self.qtest
else:
self.command = self.qtest
for t in tidList:
tname = self.traceDict[t]
if self.verbLevel > 0:
print("+++ TESTING trace %s:" % tname)
ok = self.runTrace(t)
maxval = self.maxScores[t]
tval = maxval if ok else 0
print("---\t%s\t%d/%d" % (tname, tval, maxval))
score += tval
maxscore += maxval
scoreDict[t] = tval
print("---\tTOTAL\t\t%d/%d" % (score, maxscore))
if self.autograde:
jstring = '{"scores": {'
first = True
for k in scoreDict.keys():
if not first:
jstring += ', '
first = False
jstring += '"%s" : %d' % (self.traceProbs[k], scoreDict[k])
jstring += '}}'
print(jstring)
def usage(name):
print("Usage: %s [-h] [-p PROG] [-t TID] [-v VLEVEL] [--valgrind]" % name)
print(" -h Print this message")
print(" -p PROG Program to test")
print(" -t TID Trace ID to test")
print(" -v VLEVEL Set verbosity level (0-3)")
sys.exit(0)
def run(name, args):
prog = ""
tid = 0
vlevel = 1
levelFixed = False
autograde = False
useValgrind = False
optlist, args = getopt.getopt(args, 'hp:t:v:A', ['valgrind'])
for (opt, val) in optlist:
if opt == '-h':
usage(name)
elif opt == '-p':
prog = val
elif opt == '-t':
tid = int(val)
elif opt == '-v':
vlevel = int(val)
levelFixed = True
elif opt == '-A':
autograde = True
elif opt == '--valgrind':
useValgrind = True
else:
print("Unrecognized option '%s'" % opt)
usage(name)
if not levelFixed and autograde:
vlevel = 0
t = Tracer(qtest=prog,
verbLevel=vlevel,
autograde=autograde,
useValgrind=useValgrind)
t.run(tid)
if __name__ == "__main__":
run(sys.argv[0], sys.argv[1:])
用 diff -up
列出程式差異即可,之後請送 pull request 修正 Valgrind 使用的問題
:notes: jserv
-
在使用 massif
前要將 .valgrindrc
檔案中的 --show-leak-kinds=all
移除才能夠運作
-
下圖為利用 massif-visualizer
對 trace01
做出的記憶體行為視覺化

-
嘗試錯誤:
- 在撰寫
queue.c
的 q_free
過程中曾經忘記將 queue
的節點 value
的記憶體也釋放,因而造成 memory leak
,利用 massif
實驗此情況
- 實驗 command:
- 有將節點的
value
釋放:

- 無將節點的
value
釋放:

- 根據結果圖可看出在無釋放時,最後
memory heap size
為 126B ;有釋放時則回到 0B ,所以根據 valgrind
能確確實實的看到 memory leak,並利用 massif
可以更清楚的觀察
找出 2/22 所發生的問題點 (2/23 解決)
- 先更新到最新的
driver.py
並根據 diff -up
找出程式差異,在處理一些無關緊要的差異後發現可能的錯誤點
- 問題出現在第 28 行的
self.command = 'valgrind ' + self.qtest
使 self.command
值會更動為 'valgrind /tmp/qtest.xxxxxx'
然後放在第 7 行的 clist
中,並在第 9 行時利用 subprocess.call
來進行子進程的呼叫,但這裡的 subprocess.call
會直接 fail 並丟出 Exception,所以我根據這個問題進行以下測試
Test and resolve
- 測試程式:
-
-
- 上述兩程式主要差異在於
subprocess.call
中的參數不同
- 測試結果:
-
-
- 從結果可看出第一種寫法會造成程式不可執行,也就是
driver.py
可能的問題,所以只要將寫法改成第二種就可以執行了
- WHY?
- 在查閱一些資料後得出結論,在
subprocess.py
中定義call
函式的第一個參數 args
,可以是一個字串,可以是一個包含程序參數的 list。要执行的程序一般就是这个列表的第一项,或者是字串本身。
subprocess.call(["ls","-l"])
subprocess.call(["ls -l"])
這兩者中,第二個將不能執行。若參數要為字串時,該字串須為程式的路徑才可以。
但是下面的程式碼可以成功執行
subprocess.call(["ls -l"], shell=True)
原因是,它相當於
subprocess.call(["/bin/sh", "-c", "ls -l"])
當 shell=False 時 (預設值),call 使用 os.execvp() 來執行子行程。args 一般須為一個 list。如果 args 是字元的話,會作為可執行檔案之路徑,無法傳入參數。
但是,在 Windows 下,兩種皆可以運作,由於 windows 下的 CreateProcess
接受的一個字串。即使是 list 型別的參數,也需要先合併成字串再傳遞給 api,都會變成類似下面的程式碼去執行
subprocess.call("notepad.exe tmp.txt" shell=True)
- 結論:
- 完成下述兩點後即可運作:
- 將
clist = [self.command, "-v", vname, "-f", fname]
修改成 clist = [self.command, self.qtest, "-v", vname, "-f", fname]
- 將
self.command = 'valgrind ' + self.qtest
修改成 self.command = "valgrind"
Dude, is my code constant time?
- 根據 Dude, is my code constant time? 所述,
dudect
的運作步驟如下:
- 此首先定義兩種 class 的 input data,一種是 fixed constant value ,另一種是隨機的 random input data ,一種用以測試自己的程式運作時長是否與另一種一樣,來驗證時間複雜度,而另一種則是用以 trigger certain "special" corner-case processing (若是驗證 constant time,就做一定符合的運算式,用以做比對)
- 利用 Modern CPUs 提供的 cycle counters 做為運作時長的標準
- Two sample t-test:比較兩組 data 的平均運作時長是否有差異
- 試著利用 t-test 去證明 null hypothesis 是錯的,反證出兩組 data 的平均運作時長是相同的
- 計算有顯著性差異的數據筆數,若 \(\dfrac{顯著性差異的數據筆數}{總數據筆數}\) 在 0.05 之上,兩組數據具備顯著性差異的可能性為95%。兩個數據所代表的樣本還有5%的可能性是沒有差異的,反之則也可用同樣的觀念表達,而其中5%的可能性是由於隨機誤差造成的
- 在
dudect
中,則是同時檢驗顯著性差異水平達到 0.05 以及 0.01,其中預設總筆數為 10000 ,所以若有 500 筆以上的資料具有顯著性差異則基本可以確認不符合 constant time ,若界在 500 與 10 筆之間代表其實有 99% 的機率該程式符合 contant time
- Simulation 之實作方式
- 基本上的流程與
dudect
相同,主要差別在於如何定義兩種 class 的 input data 以及因為要驗證 constant time,所以需要一個一定符合 constant time 的基本運算式,後續的流程基本上無相異
-
- 在進行 Simulation 時,上面程式之第 5~7 行會執行
q_insert_head
,因為此函式一定為 constant time ,適合用以做實驗與比對,其插入之字串也是 random 產生的。在第 9 行時則做 simulation 的主要目標,也就是測試 q_insert_tail
,並紀錄所使用的 cpucycles ,在總筆數達到 number_measurements
後進行顯著性差異的驗證,若可 disprove null hypothesis 則代表該函式時間複雜度真的為 constant time
select 系統呼叫在本程式的使用方式
-
select 的使用方式
- 在
console.c
中 select 是用以觀察 run_console
函式之參數 infile_name
,等待該檔案描述詞狀態的改變或者直到 timeout
,不過在 console.c
中第 630 行 cmd_select(0, NULL, NULL, NULL, NULL)
的 timeout 設為 NULL 表示 select() 沒有 timeout
- 當 Commandline input available ,也就是
readfds
不為空且 FD_ISSET(infd, readfds)
為 True
就會讓呼叫 readline()
,並在成功時呼叫 interpret_cmd(cmdline)
用以執行該命令
console.c
中處理 fd_set*
的函式:
FD_SET(int fd, fd_set *set)
:新增 fd 至 set 中
FD_CLR(int fd, fd_set *set)
:移除 set 中的 fd
FD_ISSET(int fd, fd_set *set)
:確認 set 中有 fd
除了「舉燭」外,你做了實驗去確認 select 系統呼叫的行為了嗎?
:notes: jserv
-
實驗select運作
- read 與 readline 問題:
- 從鍵盤讀入自己的輸入的文字並輸出,當運作後輸入"short count"之運作結果:
- 改成使用
console.c
中的 readline()
來取代 read()
,並定義 RIO_BUFSIZE
為 10
- 從鍵盤讀入自己的輸入的文字並輸出,當運作後輸入 "short count" 之運作結果:
- 使用
readline
發現不會造成像 read
多出 ort coun
的問題
- select 實際函式運作
- 以一程式做實驗,並預期以下結果
- 可輸入 "dir" 來運作 "ls -l" 並輸出
- 可以藉由輸入 "sleep" 來執行
FD_CLR(keyboard, &readfd)
,並在下一個 loop 時因 FD_ISSET(keyboard, &readfd)
為 false 所以會執行 FD_SET(keyboard, &readfd)
且輸出 readfd is not in set
- 實際輸出結果:
- 輸入
sleep
後執行 dir
:
-
運用 RIO 套件 的考量點
- 避免 short count 造成的 error,在某些情況
read
會 transfer 比 user 請求還少的 bytes
- 若在 read file 時遭遇 EOF 可能會造成 short count 之情況
- 從終端機 Read line 可能會因為 text line 的大小而造成同等的 short count
- 藉由快取到內部記憶體 buffer,使得 read text line 更有效率
- 讀取終止條件:
- 已讀取至
RIO_BUFSIZE
- 遇到 EOF
- 遇到 Newline
- 將 text 存至 buffer
-
整體 console.c
運作情境與流程
- 從
bool run_console(char *infile_name)
進入
- 呼叫
push_file
以 read only 方式開啟 file,並建立新的 rio_ptr
儲存 File descriptor 等資料,assign 給 buf_stack
- 持續運作
cmd_select(0, NULL, NULL, NULL, NULL)
直至 buf_stack
為空或者 quit
指令輸入
- 在
cmd_select
中會先利用 read_ready()
check input buffer 是否真的為一整行 command line,其中就是利用是否有遇到 newline 來判斷的
- 從 input file 中讀取指令,並在成功返回時利用
interpret_cmd()
執行該目標指令
- 持續讀取指令並運作直至 quit command 或者 EOF