項目 | 內容 |
---|---|
這個作業屬於哪個課程 | 2021春季計算機學院軟件工程(羅傑 任健) |
這個作業的要求在哪里 | 結對作業-第二階段 |
我在這個課程的目標是 | 提升工程能力和團隊意識,熟悉軟件開發的流程 |
這個作業在哪個具體方面幫助我實現目標 | 實踐結對編程 |
項目概要
內容 | |
---|---|
項目地址 | 2021_奧利給_作業/結對項目 gitlab 地址 |
學號后四位 | 3019 3293 |
結對紀實
由於之前的結對方法比較合適,故沒有改變結對方式,仍在線下進行大部分的結對活動,在指導書修改后線上討論並進行編寫和檢查。
至於結對的證據,我們都很熟悉新北二樓的各個有電插座的位置了算一個證據嗎。
3019:不同的感受倒是沒有,不過結對確實可以讓自己全身心投入思考和編碼測試,很難想象自己一個人半天的時間能夠將本次作業寫完。效率的提升是很高的,避免了大量的摸魚無效時間,還規范了作息,兩人商量時間也更加具有默契。有時候編碼走神寫 bug,也會被隊友及時發現並指出,比如對於異常的拋出,exists 和 invalid 搞混;information 測試中 size 判斷錯誤等。另外,還一個好處就是在本地 ubuntu 上跑不出來軟鏈接的一些命令(如 cd 等)隊友可以使用自己的電腦進行實驗。在一些指導書描述不是很清晰的地方,兩人相互討論理解也極大促進了編碼實現的過程,避免了編碼者當局者迷的情況。
3293:本次結對最大的感受就是,看指導書很痛苦。原因在於指導書中大量出現“xx指令也是如此”、“以下不再贅述”,而且充斥着很多需要拋異常的情況。要是個人作業來做,我肯定過不了弱測,這次結對還是得益於隊友清晰的思路以及不斷地梳理,使得復雜的異常情況、指令行為得到了妥善的處理以及測試。希望自己能在下一次作業上不被指導書繞暈,handle各種細節。
設計實現思路
需求分析和設計
需求分析
需要實現用戶和用戶組,因此分別添加兩個類:User
和 Group
。
考慮到文件、目錄、鏈接都需要支持大量相似的方法,抽象出一個 AbstractFile
。

設計
用戶系統相對更易設計,因此先考慮這一部分。由於用戶和組是多對多的關系,需要使用字符串為索引的 HashMap
分別存儲用戶所在的組和組所包含的用戶。另外,用戶具有主組,因此用戶還需要支持對於主組的設置。由於對於權限的檢查較多,對於異常的拋出可以實現為 assertIsRoot
等斷言,增加代碼可讀性。UserSystem
中也對用戶和用戶組分別存儲兩個 HashMap
。
對於文件系統,需要支持更改父親(move
),拷貝(copy
);對於軟鏈接,需要分別支持進入所鏈接目錄以及刪除文件本身,需要存儲指向抽象文件的絕對路徑;對於硬鏈接,需要引用一個常規文件。需要區分何時應當重定向的問題。
實現思路
大致思路
在用戶系統中,由於需要支持和文件系統的交互,將當前用戶和根用戶都設置為 static
變量,並提供 getter
。
由於所有文件和目錄都需要支持 getInfo
,故直接在抽象類 AbstractFile
中實現。
正常情況的具體實現其實比較容易,根據指導書一步一步來即可。不過最開始鏈接的描述令人迷惑,好在后來也改的比較正常可以理解了。關於鏈接的情況,在 HardLink
中重寫關於文件的操作;在 findDirectory
中遇到鏈接到目錄的軟鏈接就進入即可;關於移動和復制,嚴格按照指導書上的順序進行判斷即可。
代碼實現完成后,進行審核和測試設計。由於需要記錄的問題較多,在我們的項目 issue 區對於各種待實現情況發布 issue,之后逐條解決。

有關異常順序
一個難點在於異常的拋出。代碼實現完成后,發現有大量異常順序、內容是不同的,而且具有一定順序要求。比如:move
指令中,根據異常情況 srcpath
先行的規則(指導書第二條),拋出異常的順序為:
srcpath
不存在;srcpath
為當前工作目錄或其上層目錄;srcpath dstpath
指向路徑一樣;srcpath
是dstpath
上層目錄(ln -s
部分提到);src
為文件,目錄dst
下存在名為srcname
的子目錄;src
為目錄,目錄dst
下存在名為srcname
的子文件或非空子目錄;src
為目錄,dst
為文件。
對於每一種異常情況的優先級進行異常 message
內容都需要進行完備的測試。
完整的,包含指導書 3.30 修訂版后對於各種異常情況順序的測試文檔:
ln -s:
src
src 不存在
dst
dst 為文件 (exists)
src=dst
src->dst
dst/srcname 為文件或子目錄 (exists) (dst/srcname)
ln:
src
src 不存在或不為文件
dst
src=dst
src dst 均為文件 (exists)
dst/srcname 為文件或子目錄 (exists) (dst/srcname)
mv:
src
src 不存在
src 是工作目錄或其上層目錄
dst
src=dst
src->dst
src 文件 dst/srcname 目錄 (exists) (dst/srcname)
src 目錄 dst/srcname 文件或非空目錄 (exists) (dst/srcname)
src 目錄 dst 文件 (exists)
cp:
src
src 不存在
dst
src=dst
src->dst
src 文件 dst/srcname 目錄 (exists) (dst/srcname)
src 目錄 dst/srcname 文件或非空目錄 (exists) (dst/srcname)
src 目錄 dst 文件 (exists)
其中,着重注意的一點是對失效軟鏈接拋的異常,根據指導書所寫應當拋出的路徑為軟鏈接所指路徑,因此需要在 findDirectory
內層捕獲異常重新拋出。需要注意的是,如果捕獲到了 Too many levels of symbolic links
則不進行新異常的重新拋出。
測試時,需要對大量異常情況的內容進行測試,我們最開始用的形式如下:
boolean ok = false;
try {
fs.copy("src", "dst");
} catch (FileSystemException e) {
assert (e.getMessage().equals("Path src is invalid"));
ok = true;
}
assert (ok);
但大量這樣的測試堆起來,無論是可讀性還是修改難度都不好。改為了如下形式進行測試。
private String copy(String src, String dst) {
String ret = "";
try {
fs.copy(src, dst);
} catch (FileSystemException e) {
ret = e.getMessage();
}
return ret;
}
assertEquals (copy("src", "dst"), "Path src is invalid");
大大增加了可讀性。
有關創建和修改
對於指導書中大片的信息,總結出表格更容易讓自己和隊友判斷和理解。對於單獨的創建修改信息,可以列出以下表格:
src 為文件
move
情況 | create | modify |
---|---|---|
dst = null | src | yes |
dst = file | src | yes |
dst/srcname = null | src | yes |
dst/srcname = file | src | yes |
copy
(只考慮文件樹根,子樹都被視為新創建了)
情況 | create | modify |
---|---|---|
dst = null | new | yes |
dst = file | dst | yes |
dst/srcname = null | new | yes |
dst/srcname = file | dst/srcname | yes |
src 為目錄
move
情況 | create | modify |
---|---|---|
dst = null | src | yes |
dst/srcname = null | src | yes |
dst/srcname = directory count=0 | dst/srcname | yes |
copy
(只考慮文件樹根,子樹都被視為新創建了)
情況 | create | modify |
---|---|---|
dst = null | new | yes |
dst/srcname = null | new | yes |
dst/srcname = directory count=0 | dst/srcname | yes |
可以更加清晰地設置測試、修改實現。
有關調試
為調試方便,對 tree
進行了進一步的內容輸出(包括軟鏈接的指向路徑、各個抽象文件的 size
、個文件的大小等)。通過 tree
,我們發現了更改父親時重命名為哪個名字的問題、復制文件時對於祖先節點的 size
過度更新的問題等一系列 bug
。從最后的實現時間也可看出,本次對於 tree
的輸出調試直觀化大大縮短了編碼、測試和 debug
的時間。

優化設計
copy on use
由於本次需要支持 copy,所以無論是目錄還是文件都是可以達到指數級。如果能實現“不訪問則不新建”,即每次操作都是線性,可以做到數據范圍內通過所有的極限數據。
當進行從 src 到 dst.fa/dst 的 copy 時,只記錄這一次 copy 操作,並不真的進行拷貝。如果 src 不發生改變,當訪問 dst 時,我可以通過模擬訪問 src 來得到所需結果。重點在於 src 發生了改變。
列出來現在支持的創建刪除修改指令,並分一下類:
創建:mkdir mkdir -p touch fwrite
刪除:rm rm -r
修改:fwrite fappend
改父親:mv cp
將創建刪除視作單元操作,即每次創建刪除一個文件或目錄,可以將指令分解如下:
創建:mkdir touch
刪除:rm rmdir
修改:fwrite fappend
mkdir -p: 一路mkdir rm -r: 對根rmdir
如果想要做出 copy on use
的效果,需要通過修改后的 src 目錄以及存儲下來的信息復原出原有的 src 目錄,這是一個類似於可持久化的過程。按照事件發生先后,需要在 src->dst.fa 的信息通道上記錄下來這一些操作。
這里,信息通道獨立出來 updateInfo
類,其中包含使用 ArrayList
組織的操作類 updateLog
,表示信息通道上按時間順序記錄的操作。
考慮模擬從根目錄向下走的過程。假設現在走到一個目錄 dir
,考慮當前目錄下的是否存在某個子目錄或文件是從 src
copy 過來的。如果存在,那么存在一個從 src
到 dir
的信息通道,故只需在向下走的過程中記錄所有信息通道上存在的操作,在每一個到達的目錄都逆向推出拷貝之前文件或目錄的存在形式(包括創建信息、修改信息、文件內容等),並進行真實拷貝。這樣單次詢問最高進行 \(2048\) 次拷貝,復雜度是可以保證的。
可惜由於各種(時間分配、指導書修改和實現復雜度等)原因,這樣的優化最終僅僅成為了紙上的兵法圖。
lazy tag
由於 mv
指令中,要求對全部子目錄和文件的 modifyTime
進行修改,而暴力遞歸目錄進行修改的復雜度是很高的,故考慮使用懶標記對此指令進行優化。
當目錄 dir
被移動到 dst
時,對移動文件樹根標記為“待下傳”,並存儲。當進行正常的詢問 findDirectory
時,對懶標簽進行下放(pushdown
),和線段樹中的懶標記是一個思路。這樣可以省去不少沒有詢問 modifyTime
的時間。下面壓力測試里也證明了這一點。
壓力測試
NBData(極限數據)
原計划對優化后的代碼進行大壓力測試,生成了一份 nbdata,沒優化的瘋狂 GC,半小時 30 條都跑不完。最后也沒優化成,只扔一份數據生成器在這里了。大致思路就是,生成一份長鏈 cd 進去,然后鏈尾加一個文件和 693 個目錄,然后不停向父目錄 cd 並拷貝子目錄,最后目錄個數在 \(694\times 2^{250}\) 個的數量級,需要實現 copy on use 才有可能通過本測試。
#include<cstdio>
#include<cstring>
#include<cstdlib>
#include<algorithm>
#include<queue>
#include<map>
#include<set>
#include<cmath>
#include<vector>
typedef long long ll;
using namespace std;
#define pii pair<int,int>
#define fi first
#define se second
#define mp make_pair
#define pb push_back
char a[100]={};
int main(){
int i,j;
freopen("nbdata.txt","w",stdout);
for(i=0;i<26;i++)a[i]='a'+i;
for(;i<26+26;i++)a[i]='A'+i-26;
for(;i<52+10;i++)a[i]='0'+i-52;
a[i++]='_';
printf("mkdir -p ");
for(i=0;i<2048;i++)printf("a/");
puts("");
printf("cd ");
for(i=0;i<2048;i++)printf("a/");
puts("");
printf("fwrite 123 file.txt\n");
printf("mkdir -p ");
for(i=0;i<63;i++)if(a[i]<'0'||a[i]>'9')printf("%c/../",a[i]);
for(i=0;i<10;i++)
for(j=0;j<63;j++)
printf("%c%c/../",a[i],a[j]);
puts("");
for(i=0;i<250;i++){
printf("cp ../a ../../a/b\n");
printf("cd ..\n");
}
printf("info /\n");
printf("fappend \"append\" b/a/file.txt\n");
printf("info /\n");
printf("info b/a/G\n");
printf("info ");
for(i=0;i<100;i++){
printf("a/");
}
printf("b/a/_\n");
printf("mkdir -p ");
for(i=0;i<150;i++){
printf("a/");
}
printf("b/a/_/_\n");
printf("mv /a /b\n");
printf("cp /b /c\n");
printf("ls /\n");
return 0;
}
大量 Move 測試
測試懶標簽的性能優化效果。
數據生成器:類似上面,先生成指數級別(這里是 12 次,即 \(694\times 2^{12},3e6\))的目錄和文件,然后對根目錄下的目錄進行 300 次重命名;最后使用 info
指令驗證正確性。
#include<cstdio>
#include<cstring>
#include<cstdlib>
#include<algorithm>
#include<queue>
#include<map>
#include<set>
#include<cmath>
#include<vector>
typedef long long ll;
using namespace std;
#define pii pair<int,int>
#define fi first
#define se second
#define mp make_pair
#define pb push_back
char a[100];
int main(){
int i,j;
freopen("nbdata2.txt","w",stdout);
for(i=0;i<26;i++)a[i]='a'+i;
for(;i<26+26;i++)a[i]='A'+i-26;
for(;i<52+10;i++)a[i]='0'+i-52;
a[i++]='_';
printf("mkdir -p ");
for(i=0;i<2048;i++)printf("a/");
puts("");
printf("cd ");
for(i=0;i<2048;i++)printf("a/");
puts("");
printf("fwrite \"2\" file.txt\n");
printf("mkdir -p ");
for(i=0;i<63;i++)if(a[i]<'0'||a[i]>'9')printf("%c/../",a[i]);
for(i=0;i<10;i++)
for(j=0;j<63;j++)
printf("%c%c/../",a[i],a[j]);
puts("");
for(i=0;i<12;i++){
printf("cp ../a ../../a/b\n");
printf("cd ..\n");
}
printf("cd /\n");
for(i=0;i<150;i++){
printf("mv a b\n");
printf("mv b a\n");
}
printf("cd ");
for(i=0;i<2048-12;i++){
printf("a/");
}
puts("");
printf("info /\n");
printf("info .\n");
printf("info a/a/a/a/a/a/a/a/a/a/a/b/file.txt\n");
printf("info a/a/a/a/a/a/a/a/a/a/a/a/_\n");
return 0;
}
優化前:花費 95.5s,且從 CPU 調用樹中明顯的看出遞歸修改占用巨大。
![]() |
![]() |
優化后:花費 8.5s,其中幾乎全部時間和 CPU 占用都是在 copy
階段使用的,mv
的占用部分甚至不到 0.1%。可見優化效果較為明顯。
![]() |
![]() |
預估和實際耗時
PSP2.1 | Personal Software Process Stages | 預估耗時(分鍾) | 實際耗時(分鍾) |
---|---|---|---|
Planning | 計划 | ||
· Estimate | · 估計這個任務需要多少時間 | 10 | 5 |
Development | 開發 | ||
· Analysis | · 需求分析 (包括學習新技術) | 30 | 22 |
· Design Spec | · 生成設計文檔 | 20 | 20 |
· Design Review | · 設計復審 (和同事審核設計文檔) | 10 | 10 |
· Coding Standard | · 代碼規范 (為目前的開發制定合適的規范) | 0 | 0 |
· Design | · 具體設計 | 10 | 20 |
· Coding | · 具體編碼 | 400 | 420 |
· Code Review | · 代碼復審 | 30 | 30 |
· Test | · 測試(自我測試,修改代碼,提交修改 | 250 | 230 |
Reporting | 報告 | ||
· Test Report | · 測試報告 | 10 | 5 |
· Size Measurement | · 計算工作量 | 10 | 10 |
· Postmortem & Process Improvement Plan | · 事后總結, 並提出過程改進計划 | 60 | 100 |
合計 | 840 | 872 |
和上一次作業一樣,根據指導書的修改也進行了大量的增量測試和開發,就不計入表格中了。