第五章 文件讀寫
5.1 文件總覽
libedk文件對象一覽。
- transfer 代表一個傳輸任務,一個傳輸任務通常只有一個文件。原始ed2k不支持目錄下載
- piece_picker 分片選擇器
- piece_manager 分片管理器
- storage_interface 文件操作接口
- disk_io_thread 異步讀寫文件線程
- file_storage 文件對象抽象
- transfer_info
- file_storage 文件對象抽象 (以引用方式與piece_manager共享對象)
- std::vector<md4_hash> m_piece_hashes 此文件所有分片哈希值
- md4_hash m_info_hash 此文件總哈希值
與文件操作有關對象,及其部分的重要方法和成員:
transfer
|---boost::scoped_ptr<piece_picker> m_picker
###########################################
|---piece_manager* m_storage
|---async_write()
|---async_read()
|---async_xxxx() {create a job, set job.storage=`this` and add it to m_io_thread.}
|---file_storage const& m_files; //注:由transfer::transfer_info::file_storage m_files初始化
|---disk_io_thread& m_io_thread;
|---void disk_io_thread::thread_fun()
|---job.storage->write_impl //if job type is "write" call piece_manager::write_impl
|---storage_interface::writev
|---boost::scoped_ptr<storage_interface> m_storage; //instance of class `default_storage`
|---virtual int readv(...);
|---virtual int writev(...);
|---default_storage::write(...)
|---default_storage::writev(...)
|--- default_storage::readwritev(...)
|---find the FILE iterator and file offset from file_storage const& m_files;
|---default_storage::open_file(FILE)
|---libed2k::copy_bufs
|---libed2k::advance_bufs
|---file::writev() or default_storage::write_unaligned
|---boost::scoped_ptr<file_storage> m_mapped_files;
|---file_storage const& m_files; <== const&
|---std::vector<boost::uint8_t> m_file_priority;
|---std::string m_save_path;
|---file_pool& m_pool; std::map<std::pair<void*, int>, lru_file_entry> file_set;
|---//from session::m_filepool
|---int m_page_size;
|---bool m_allocate_files;
|---boost::intrusive_ptr<piece_manager> m_owning_storage
###########################################
|---boost::intrusive_ptr<transfer_info> m_info
|---file_storage m_files <== 代表文件存儲,見下方成員
|---std::vector<internal_file_entry> m_files
|---name
|---offset
|---symlink_index
|---size
|---name_len
|---pad_file
|---hidden_attribute
|---executable_attribute
|---symlink_attribute
|---path_index
|---size_type m_total_size;
|---int m_num_pieces
|---int m_piece_length
|---std::string m_name;在一個file_storage添加多個文件時有點迷,ed2k原不能下載整個目錄
|---std::vector<std::string> m_symlinks
|---std::vector<time_t> m_mtime
|---std::vector<std::string> m_paths
|---std::vector<md4_hash> m_piece_hashes
|---md4_hash m_info_hash
5.2 文件分片
5.2.1 文件分片以及文件分片哈希值
文件的分片信息存儲在transfer_info::m_piece_hashes,transfer_info在創建一個transfer任務時,在transfer對象的構造函數中創建。transfer_info提供了讀寫分片哈希值的接口:
- const std::vector<md4_hash>& piece_hashses() const { return m_piece_hashes; }
- void piece_hashses(const std::vector<md4_hash>& hs) { m_piece_hashes = hs; }
在首次創建一個下載任務(而非從臨時文件中恢復一個下載任務)時,很明顯此時是沒有碎片哈希值信息的而只能通過詢問其他用戶獲取(見4.3.4),當獲取到以後將保存到transfer_info對象中:
- 文件分片的大小是固定的(const size_type PIECE_SIZE = 9728000ull)
- 文件大小在我們開始一個文件傳輸任務時就可以確定的(詢問服務器或者P2P消息)
- 文件碎片數等於文件大小除以分片大小(除法結果向上取整如3.1=>4)
設置分片大小和數量的操作都是在transfer_info對象的初始化函數中完成:
transfer_info::transfer_info(
md4_hash const& info_hash,
const std::string& filename,
size_type filesize,
const std::vector<md4_hash>& piece_hashses)
: m_info_hash(info_hash) //文件hash
, m_piece_hashes(piece_hashses) //碎片hash(s)
{
m_files.set_num_pieces(div_ceil(filesize, PIECE_SIZE));
m_files.set_piece_length(PIECE_SIZE);
m_files.add_file(filename, filesize);
//文件只有1個分片時,碎片hash必然等於文件hash.
if (m_piece_hashes.empty() && filesize < PIECE_SIZE)
m_piece_hashes.push_back(info_hash);
}
可以看到file_storage transfer_info::m_files這個文件抽象對象管理:
- 分片數
- 分片大小
- 文件名
- 文件大小
而file_storage並沒有碎片hash數組,它另外保存到了transfer_info::m_piece_hashes中。在後續的章節中我們將詳細介紹file_storage這個文件抽象類,在這裏我們先跳過它。
5.2.2 文件分片哈希及臨時文件格式分析
4.3.4中曾經說明,ed2k網絡裏的每一個參與者即使是隻擁有一個文件的部分文件片都必須知道此文件的所有分片哈希值,因此有必要保存從其他用戶中獲取的哈希值以便分享給其他用戶。在libed2k中,這個值和其他信息一起保存到臨時文件(resume-file)中,在這一節中我們分析一下臨時文件對應的數據結構。
首先臨時文件長這樣:
struct transfer_resume_data
{
// 完整文件的哈希值,等同於transfer_add_param中的file_hash
md4_hash m_hash;
// utf-8 file name
container_holder<boost::uint16_t, std::string> m_filename;
// 完整文件的大小
size_type m_filesize;
// 是否處於做種模式
bool m_seed;
// 臨時文件中保存的文件信息(其中之一就是所有碎片的hash值)
tag_list<boost::uint8_t> m_fast_resume_data;
// 用於保存的構造函數
transfer_resume_data(const md4_hash& hash,
const std::string& filename,
size_type size,
bool seed,
const std::vector<char>& fr_data) :
m_hash(hash)
, m_filename(filename)
, m_filesize(size)
, m_seed(seed) {
if (!fr_data.empty())
m_fast_resume_data.add_tag(make_blob_tag(fr_data, FT_FAST_RESUME_DATA, true));
}
// 用於載入的構造函數
transfer_resume_data() : m_filesize(0), m_seed(false)
{}
// 序列化到文件和從文件反序列化。
template<typename Archive>
void serialize(Archive& ar)
{
ar & m_hash;
ar & m_filename;
ar & m_filesize;
ar & m_seed;
ar & m_fast_resume_data;
}
};
從上面的序列化函數中可以知道臨時文件的結構是:
名字 |
大小 |
值 |
說明 |
哈希值 |
16字節 |
文件總哈希值 |
|
文件名長度 |
2字節 |
文件名長度 |
boost::uint16_t類型 |
文件名 |
變長 |
文件名 |
utf-8字符串 |
文件大小 |
8字節 |
libed2k::size_type 類型 |
|
是否做種模式 |
1字節 |
0或者1 |
bool類型 |
其他信息 |
變長 |
tag_list類型 |
在conn項目中cc_store命令實現了從臨時文件中創建任務,我們看一下它的實現:
- 反序列化臨時文件數據到libed2k::transfer_resume_data
- 從m_fast_resume_data成員中取得tag爲libed2k::FT_FAST_RESUME_DATA的二進制數據並賦予add_transfer_params::resume_data(它爲std::vector<char>*類型)
APP("restore " << vpaths[n]);
std::ifstream ifs(
vpaths[n].c_str(), //這是臨時文件的全路徑。
std::ios_base::in | std::ios_base::binary);
if (ifs)
{
libed2k::transfer_resume_data trd;
libed2k::archive::ed2k_iarchive ia(ifs);
ia >> trd; //反序列化到libed2k::transfer_resume_data對象
libed2k::add_transfer_params params;
params.seed_mode = false;
params.file_path = trd.m_filename.m_collection;
params.file_size = trd.m_filesize;
if (trd.m_fast_resume_data.size() > 0)
{
params.resume_data = const_cast<std::vector<char>*>(
&trd.m_fast_resume_data.getTagByNameId(
libed2k::FT_FAST_RESUME_DATA)->asBlob());
}
params.file_hash = trd.m_hash;
ses.add_transfer(params);
}
首先從上面的代碼知transfer_info::resume_data的來源是tag_list<boost::uint8_t> transfer_resume_dat中NameID = libed2k::FT_FAST_RESUME_DATA的數據塊,現在我們需要知道這個數據塊的構成。
在session添加任務時ses.add_transfer會調用到transfer::start(),在這個函數中會對transfer::m_resume_data(即上面的Blob數據塊,它在transfer構造函數中傳入)進行bdecode解析,結果將存到lazy_entry transfer::m_resume_entry:
error_code ec;
if (!m_resume_data.empty() &&
lazy_bdecode(&m_resume_data[0],
&m_resume_data[0] + m_resume_data.size(),
m_resume_entry, ec) != 0)
{
std::vector<char>().swap(m_resume_data);
m_ses.m_alerts.post_alert_should(fastresume_rejected_alert(handle(), errors::fast_resume_parse_error));
}
可以看到數據是通過調用lazy_bdecode解析到m_resume_entry,lazy_bdecode函數用於將一段以bdecode編碼的數據解析到lazy_entry對象(一個類似map類型的數據結構)中。
在void transfer::start()隨後調用的void transfer::init()裏,對lazy_entry對象做如下解析:
//特別注意seed模式不做fast_resume檢查
if (!m_seed_mode){
set_state(transfer_status::checking_resume_data);
if (m_resume_entry.type() == lazy_entry::dict_t){
DBG("read resume data: {hash: " << hash() << ", file: " << name() << "}");
int ev = 0;
if (m_resume_entry.dict_find_string_value("file-format") != "libed2k resume file")
ev = errors::invalid_file_tag;
std::string info_hash = m_resume_entry.dict_find_string_value("transfer-hash");
if (!ev && info_hash.empty())
ev = errors::missing_transfer_hash;
if (!ev && (md4_hash::fromString(info_hash) != hash()))
ev = errors::mismatching_transfer_hash;
if (ev){
std::vector<char>().swap(m_resume_data);
lazy_entry().swap(m_resume_entry);
m_ses.m_alerts.post_alert_should(
fastresume_rejected_alert(handle(), error_code(ev, get_libed2k_category())));
}else{
//從bedcode dict節點lazy_entry中讀取各種信息。
read_resume_data(m_resume_entry);
m_need_save_resume_data = false;
}
}
m_storage->async_check_fastresume(
&m_resume_entry,
boost::bind(&transfer::on_resume_data_checked,
shared_from_this(), _1, _2));
}
通過對read_resume_data實現細節的探索可知當前fastresume臨時文件可能有以下數據:
名字 |
類型 |
值 |
說明 |
file-format |
字符串 |
"libed2k resume file" |
|
transfer-hash |
字符串 |
文件哈希值 |
|
total_uploaded |
int |
||
total_downloaded |
int |
||
upload_rate_limit |
int |
可選 |
|
download_rate_limit |
int |
可選 |
|
num_seeds |
int |
已下載的數量 |
保留未用,總是爲0 |
num_downloaders |
int |
未完成下載的數量 |
保留未用,總是爲0 |
sequential_download |
int |
按順序下載 |
將轉爲bool值 |
paused |
int |
任務是否處於暫停狀態 |
將轉爲bool值 |
hashset-values |
字符串數組 |
一個字符串數組,每一個元素都是一個哈希值。 |
|
blocks per piece |
整形 |
一個分片中的塊數,等於分片長度/塊大小 |
|
mapped_files |
字符串數組 |
用於重命名文件? 喵喵喵? |
見default_storage::verify_resume_data |
file_priority |
int數組 |
||
file sizes |
二維int數組 |
第二維有兩個元素,a[0]是 size_type,a[1]是std::time_t |
|
slots |
二維數組 |
數組元素>0時代表對應分片已完成 |
元素個數等於分片數 |
pieces |
字符串類型 |
如果str[i] & 1 == 1表示對應分片已完成。 |
字符串長度等於分片數。slot和pieces只需要設置一個即可。 |
allocation |
字符串 |
"compact"或其他值 |
|
pieces |
字符串 |
每個字符代表一個文件分片,元素值&1==0表示這個文件分片不存在。 |
可以看做是字符串形式的bitmap |
unfinished |
dict數組 |
每個dict由兩個成員組成:int類型的"piece"和string類型的"bitmask"。其中piece是文件分片序號,每個"bitmask"字符串的長度等於一個分片所擁有的塊數除以8,如果一個字符(0~7)對應位=1,那麼代表這塊的已經完成下載。 |
默認情況下一個分片大小=9728000,一個塊大小=256*1024。那麼一片有37.1塊,向上取整後=38塊。一個字符有8位可以代表8塊,那麼需要5個字符才能代表一片。 |
5.2.3 文件分片的校驗
文件分片校驗的總體流程
在void transfer::init()中調用了read_resume_data,它的作用是實現如下:
void transfer::read_resume_data(lazy_entry const& rd)
{
m_total_uploaded = rd.dict_find_int_value("total_uploaded");
m_total_downloaded = rd.dict_find_int_value("total_downloaded");
set_upload_limit(rd.dict_find_int_value("upload_rate_limit", -1));
set_download_limit(rd.dict_find_int_value("download_rate_limit", -1));
m_complete = rd.dict_find_int_value("num_seeds", -1);
m_incomplete = rd.dict_find_int_value("num_downloaders", -1);
int sequential_ = rd.dict_find_int_value("sequential_download", -1);
if (sequential_ != -1) set_sequential_download(sequential_);
int paused_ = rd.dict_find_int_value("paused", -1);
if (paused_ != -1) m_paused = paused_;
}
在read_resume_data將執行對fastresume數據的檢查(m_storage->async_check_fastresume),在檢查完成後的回調transfer::on_resume_data_checked中有如下代碼:
lazy_entry const* hv = m_resume_entry.dict_find_list("hashset-values");
std::vector<md4_hash> piece_hashses;
piece_hashses.resize(hv->list_size());
for (int n = 0; n < hv->list_size(); ++n)
{
piece_hashses[n] = md4_hash::fromString(hv->list_at(n)->string_value());
}
m_info->piece_hashses(piece_hashses);
以上代碼的作用就是在對transfer對應的臨時文件進行檢查後重寫transfer_info中的碎片哈希值。所以一般流程是:
- 創建transfer_info對象
- 載入transfer對應的臨時文件
- 檢查臨時文件數據
- 複寫碎片哈希值到transfer_info::m_piece_hashes
文件分片校驗的細節
檢查臨時文件數據transfer::async_check_fastresume的具體實現如下:
- 從臨時文件讀取數據到m_resume_entry
- 調用piece_manager::async_check_fastresume
- 在IO線程中調用piece_manager::check_fastresume
piece_manager::check_fastresume主要做以下事情(下面的lazy_entry即爲從臨時文件反序列化後的bdecode對象):
- 檢查塊大小,每個文件片的塊個數(lazy_entry["blocks per piece"])
- 調用default_storage::verify_resume_data,獲取列表lazy_entry["mapped_files"] =>[string, string, ... string]
- 獲取列表lazy_entry["file_priority"]=>[uint8_t,uint8_t,... uint8_t]
- 獲取列表lazy_entry["file sizes"] =>[[size_type,time_t],[size_type,time_t],...[size_type,time_t]]
- 獲取列表lazy_entry["slots"] => [size_type,size_type,...size_type] 如果隊列中所有的值都大於0則seed=true
- 如"slots"未找到則獲取字符串lazy_entry["pieces"],如果字符串裏每個字符p[i] & 1 == 1,那麼seed=true。
- 如果是seed=true,那麼檢查default_storage::files().num_files,並逐項檢查default_storage::files()數組,與lazy_entry["file sizes"]取得的值對比。
- 獲取字符串lazy_entry["allocation"], 如果等於"compact"則設置full_allocation_mode=true
- 調用match_filesizes(files(), m_save_path, file_sizes, flags, error)其中file_sizes參數由lazy_entry["file sizes"]取得。
5.2.4 分片選擇介紹
分片選擇的邏輯被封裝在了piece_picker類中,它的職責是根據既定策略從P2P連接的對等客戶端中挑選出我們需要且對方擁有的文件分片。分片選擇在piece_picker::pick_pieces成員函數中實現。
分片選擇有多種模式,常見的是“稀有優先”和“順序下載”,默認爲稀有優先。在稀有優先時需要使用一套優先級算法,這個算法十分晦澀難懂分析不動在這裏先跳過不寫。詳見"piece_picker::update_pieces"、“piece_picker::add_blocks”和“piece_picker::pick_pieces”函數。這個分片算法和libtorrent的是相同的,如果需要尋找資料可以嘗試搜索libtorrent分片算法。
piece_picker類提供了一系列“分片狀態”讀寫接口,以分片爲單位的接口:
- int num_pieces()
- void we_have(int piece_index)
- void we_dont_have(int piece_index)
- bool have_piece(int index)
- void piece_info(int index, piece_picker::downloading_piece& st) const;
- piece_pos const& piece_stats(int index) const
- void restore_piece(int index);
- bool is_piece_finished(int index) const
- int blocks_in_piece(int index) const;
以piece_block爲單位的接口:
- bool is_requested(piece_block block)
- bool is_downloaded(piece_block block)
- bool is_finished(piece_block block)
- bool mark_as_downloading(piece_block block, void* peer, piece_state_t s)
- bool mark_as_writing(piece_block block, void* peer);
- void mark_as_finished(piece_block block, void* peer);
- void write_failed(piece_block block);
- int num_peers(piece_block block) const;
以上接口中使用的數據結構:
- piece_block,文件塊索引,內部包含文件分片索引和文件塊在文件分片中的索引。
- block_info,塊詳情信息,包含擁有此塊的peer數量,當前正在傳輸的peer對象,以及塊狀態信息(none、已請求、正在寫入、已經完成)。
- downloading_piece,正在下載的一個分片,包含了分片序號、分片狀態、完成塊數,正在寫入塊數,已請求塊數,以及指向這個分片所擁有的block_info(塊詳情)數組的頭個元素的指針。
- piece_pos,分片細節信息,包括擁有此分片的peer的數量、是否正在下載、是否已經擁有了這個分片、優先級。
以上數據結構的組織:
- std::vector<piece_pos> m_piece_map;
- std::vector<downloading_piece> m_downloads;
- std::vector<block_info> m_block_info;
m_piece_map數組存放了所有的分片細節信息,它的大小等於下載任務中文件的總分片數。如果某個數組中的元素的piece_pos::downloading = 1,那麼這個元素對應的分片也一定存在於m_downloads數組中。m_downloads存放了正在下載的分片,這個數組中每個元素都對應了m_blocks_per_piece個m_block_info數組中的元素,即m_downloads[i] => m_block_info[i*m_blocks_per_piece],不過需要注意對於最後一個文件分片,這個數組的有效大小爲m_blocks_in_last_piece,它一般小於佔用空間m_blocks_per_piece。
小結:從源碼分析的結果看piece_picker具有分片狀態管理的功能,當然爲了挑選下一個下載的分片必須如此。piece_picker可以管理:
- 分片數以及是否擁有分片。
- 各分片和塊的優先級(用於挑選下個需要下載的塊)。
- 指定分片和塊是否正在下載。
- 擁有指定分片和塊的peer數量。
- 當前transfer文件正在下載,已發請求,已經下載,尚未開始下載的分片和塊數。
注意piece_picker並未涉及具體分片文件該如何打開、讀取、校驗和寫入,這是下一節介紹的piece_manager的職責。
5.3 分片文件系統的實現
piece_manager是分片文件系統的高級抽象,是ed2k文件系統的高級接口。它提供了以下功能:
- 保存下載任務進度信息到臨時文件
- 異步寫文件
- 異步讀文件
- 文件分片的校驗(計算分片哈希)
- 臨時文件的校驗
- 重命名文件,以及移動文件保存路徑
- 緩存分片以及清除分片緩存
- 釋放文件和刪除文件
- 取消文件讀寫IO。
以上接口通過以下核心組件實現:
- disk_io_thread 異步工作線程對象,用於實現異步操作的執行體。
- file_storage 實體文件的抽象。
- storage_interface 操作文件對象的接口。
5.3.1 piece_manager接口說明
piece_manager類主要提供了以下接口,簡介如下:
- abort_disk_io用於停止一個文件傳輸任務(transfer),當這個接口調用後內部IO線程將丟棄所有與當前這個piece_manager相關的可丟棄的IO任務。這裏注意IO線程依舊會繼續運行,這是因爲IO線程屬於session與文件傳輸是一對多的關係,即一個session內可以有N個transfer任務而IO線程只有一個。
- async_release_files用於刷新文件寫緩衝區的塊到文件並關閉文件句柄。這個函數在刪除、暫停、完成一個傳輸任務以及寫入任務到臨時文件後調用。
- async_clear_read_cache這個接口如接口名用於清除disk_io_thread對象中與當前piece_manager有關的讀緩衝區。這個接口在暫停一個文件傳輸任務時被調用。
- async_delete_files在disk_io_thread內部寫緩衝區列表中中刪除與當前piece_manager有關的寫緩衝區並釋放與之對應的內存、關閉文件句柄、然後將文件從硬盤中刪除。
- async_hash接口用於校驗一個文件分片(piece)的哈希值,根據輸入一個分片ID和hash值檢查結果。同時還會將對應的分片寫緩存區同步到實際文件(文件flush),如果校驗失敗了還會調用對應piece_manager的mark_failed函數將這個分片標記爲傳輸失敗。
- async_check_fastresume用於檢查反序列化後的臨時文件對象,見5.2.3中的說明。
- async_save_resume_data接口保存當前transfer基本信息和進度等保存到一個libed2k::entry對象(類似JSON對象),完成後將這個entry對象通過回調函數返回給調用者。這個對象返回後將封裝到一個alert中,使用者在alert監聽函數中決定是否保存以及怎麼保存到文件中。
- async_check_files用於檢查一個文件所有分片的hash值,在check_fastresume發生piece/slot校驗錯誤後可能會調用到它。
- get_storage_impl返回內置的storage_interface指針
- async_read用於讀取文件分片,它的第一個參數peer_request中指定了分片ID、位移和長度,當發送數據給其他Peer時需要用到。
- async_write將指定的內存數據寫入到piece_manager的內部寫緩衝區中,當內部寫緩衝區超過了設計的最大緩存大小時將寫緩衝區中的數據寫入到實際文件中(用於合併多個寫操作)。這個接口在接收其他peer發送的數據時需要用到。
- async_move_storage移動當前transfer/piece_manager的所有文件到參數指定的目錄中,修改下載路徑並返回給調用者,整個操作僅適用於當前transfer不會應用到整個session。
- async_rename_file調用storage_interface進行文件重命名操作。需要指定兩個參數,第一個參數是需要重命名的文件在文件集(file_storage)中的ID,另一個參數是重命名後的文件名(僅需要文件名,file_storage已經保存了目錄路徑)。
- async_finalize_file實際不做任何事情。
- async_read_and_hash當前未用。這個接口將第一個參數peer_request中指定的整個piece讀取到內部緩存並計算哈希值。另外注意它會調用transfer_info::hash_for_piece(piece_id)將得到的結果與上一步計算的值進行對比,如果兩個值不一致則會報告errors::failed_hash_check錯誤,也因此這個接口只能在已經fast-resume的transfer任務上使用。
- async_cache當前未用。這個接口讀取分片ID對應的數據到內部緩衝區列表並返回已緩存的字節數。
5.3.2 異步操作的實現
在libed2k中文件的異步操作使用disk_io_thread這個對象實現,它繼承了一個disk_buffer_pool類,這個類是對boost::pool的封裝,它以BLOCK_SIZE爲單位在boost::pool內存池中申請和釋放內存。通過繼承這個類disk_io_thread獲得了以下幾個接口用於緩衝區管理:
- allocate_buffer() - 從內存池中申請一塊內存。
- free_buffer() - 從內存池中釋放一塊內存。
- free_multiple_buffers() - 釋放多塊內存(輸入參數是char**二維指針和個數)。
- block_size() - 在libed2k的當前實現中總是等於BLOCK_SIZE。
- release_memory() - 讓內存池釋放所有未被分配的內存,但已分配的內存塊不受影響 。
- in_use() - 返回已分配的內存塊數量。
disk_io_thread的構造函數:
disk_io_thread(io_service& ios ,
boost::function<void()> const& queue_callback ,
file_pool& fp ,
int block_size = BLOCK_SIZE);
第一個參數是boost::io_service的實例;第二個參數是一個回調函數,將一個任務添加到disk_io_thread實例後將調用它;第三個參數是file_pool對象;第四個參數是緩存塊大小(BLOCK_SIZE)。
disk_io_thread的實例在session的構造函數中創建,各參數對應如下如下:
m_disk_thread(m_io_service,
boost::bind(&session_impl::on_disk_queue, this), m_filepool, BLOCK_SIZE),
其中session_impl::on_disk_queue是一個空函數。disk_io_thread中定義的其他接口簡介如下:
- abort() - 取消所有可放棄的任務並設置m_abort標誌爲true,包括以下任務disk_io_job::read/check_files/read_and_hash/cache_piece/update_settings。在接下來的循環中將刷新寫緩衝區到緩存,然後清理所有線程使用的內部緩衝區列表,最後退出線程函數中的任務循環。
- join() - 對內部線程對象執行join操作,需要先執行abort接口。
- stop() - 參數指定了一個文件傳輸任務t。遍歷任務列表取消所有任務=t的(可放棄的)任務。然後提交一個disk_io_job::abort_torrent任務,在這個任務的處理函數中將釋放所有與t有關的內部緩衝區。
- add_job() - 添加一個disk_io_job對象到內部的job隊列中,disk_io_job中至少要有disk_io_job中至少要對“action_t action”賦值,它代表了異步類型,在線程循環中將根據它的值分別處理。關於對disk_io_job的詳細介紹見本節下文。
- queue_buffer_size() - 當前寫緩存的大小,每當添加一個write任務時就增加任務中的字節數,當超過m_settings.max_queued_disk_bytes設定的上限時disk_io_thread不可寫入,直到寫入操作完成後減少寫緩存的當前大小,當下載速度超過磁盤的寫入速度時就會發生這種情況。
- can_write() - 當前寫緩存的大小是否超過了m_settings.max_queued_disk_bytes。
- get_cache_info() - [沒有用到]從寫緩衝區列表和讀緩衝區列表中根據獲取輸入參數md4_hash指定的傳輸對象(transfer)進行篩選,符合條件的緩衝區被封裝到cached_piece_info中,並將它添加到輸出參數vector<cached_piece_info>中。
- status() - [沒有用到]返回disk_io_thread的一些統計信息。
- thread_fun() - 工作線程函數,處理add_job添加的任務列表,根據不同類型作不同處理。詳情見本節後續介紹。
異步工作任務disk_io_job
disk_io_thread的處理模型很簡單:add_job將一個disk_io_job對象添加到一個內部列表,這個對象封裝操作類型以及操作所對應的參數。然後線程函數thread_fun逐個取出這些對象,然後按照job的類型調用不同的函數進行處理。
disk_io_job包含了以下成員:
- action_t action,任務類型
- char* buffer; 指向緩衝區指針,當讀文件時結果存放在這裏,寫緩衝區時指向寫入內容。注意當讀文件時緩衝區指向的是內部的內存池,爲了對這個緩衝區的管理特意引進了緩衝區自釋放disk_buffer_holder類。
- int buffer_size; buffer的大小。
- int piece, offset; 通常用於指明分片序號以及偏移地址,即文件讀寫時需要從那裏開始。特別注意當重命名文件時piece的意義是文件在“file pool”中的ID而與分片無關。
- std::string str; 用於指明操作中的文件名和路徑名,在move_storage操作時用於指明下載文件將移動到哪個目錄,在用於rename_file是則指名新文件名(不包含目錄)。
- std::string error_file;通常用於指明發生IO錯誤的文件路徑(當前似乎未用)
- int max_cache_line;【當前總是爲0】僅用於讀操作,與讀緩衝區有關。
- int cache_min_time;【當前總是爲0】僅用於讀操作,與讀緩衝區有關。
- boost::shared_ptr<entry> resume_data;僅用於save_resume_data操作,用於臨時存放save resume data的結果。注意下面這幾個對象都有一個相同的寫臨時文件對象的函數,default_storage::write_resume_data/piece_manager::write_resume_data/transfer::write_resume_data,它們依次往resume_data裏寫入各自擁有的信息,全部寫完後向session拋出alert。
- error_code error; 用於指明IO操作的錯誤碼。
- boost::function<void(int, disk_io_job const&)> callback;執行操作成功後,這個回調函數對象將被打包塞到io_service中執行,這個io_service和session中是同一個,總之:
- 異步函數piece_manager::async_xxx通常是在io_service線程中運行
- 然後對應的piece_manager::xxxx是在disk_io_thread的線程函數(threadfun)中被調用
- 最終這些類似on_xxxx的callback又被丟回io_service線程中執行(io_service::post)。
- libed2k::ptime start_time;
disk_io_thread讀寫緩衝區管理(僅內部使用的非public接口)
在disk_io_thread中存在兩個緩衝區,寫緩衝區m_pieces和讀緩衝區m_read_pieces,注意它們的每個元素都是以分片爲單位,然後每個分片中又包含了分塊信息(見cached_piece_entry)。
當執行disk_io_job::write操作時優先使用寫緩衝區m_pieces而不是實際文件。當寫入文件時首先調用find_cached_piece()函數在寫緩衝區m_pieces中查找,如果找到則更新緩衝區數據(包括回調),否則調用cache_block()函數在寫緩衝區m_pieces中插入一個分片緩存。
寫緩衝區有關的函數:
- flush_cache_blocks(),按照session_settings.disk_cache_algorithm的策略將函數參數指定數量個數的N個塊寫入到文件中。
- flush_expired_pieces(),將緩存超時的(session_settings.cache_expiry)文件分片寫入到實際文件中。這個函數在disk_io_thread線程函數的每次循環中都會執行。
- flush_contiguous_blocks(),在緩存中尋找一塊最大塊的在邏輯上連續的緩存,如果它的大小大於session_settings.write_cache_line_size,那麼將它寫入到文件中。
- flush_range(),以上幾個flush函數的底層實現,用於將一個分片中塊ID從start到end範圍內的塊寫入到文件中。
- cache_block(),新建一片緩存並寫入一塊數據。爲參數disk_io_job.piece新建一個分片緩存,並根據參數disk_io_job.offset指定的位置計算塊ID並在塊ID相應位置寫入一個塊的數據。在使用這個函數前必須使用find_cached_piece()函數查找是否已經又分片緩存,如果則需要在這個分片緩存上更新而不是新建一個重複的分片緩存。
讀緩衝區有關的函數:
- clear_oldest_read_piece(),清理超時時間理現在最近的分片,在超時時間設定相同的情況下也就是最早塞到讀緩衝區裏的分片。如果第一個超時最近的分片還未超時那麼不做任何事情直接返回0,否則釋放參數指定的前num_blocks個塊,如果分片中已分配的塊數小於num_blocks則釋放這個分片所有的block緩存,函數返回釋放的塊個數。
- read_into_piece(),將參數【int start_block】位置開始的【int num_blocks】個塊讀入到參數【cached_piece_entry& p】指定的緩衝區中。
- cache_read_block(),嘗試讀取最少3塊從參數[disk_io_job.piece,disk_io_job.offset]指定的開始位置的文件塊到緩存。讀取數量的上限不超過當前分片剩餘塊數如果剩餘塊數小於3則讀取剩餘塊數,上限值同時和m_settings.cache_size、m_cache_stats.read_cache_size、當前已使用塊(in_use()函數)、m_settings.read_cache_line_size幾個參數有關。注意disk_io_thread的內存池緩存塊數不能超過m_settings.cache_size,在最差情況下會嘗試同步寫緩衝區的部分內容到文件以便騰出部分空間。
- free_piece(),從內存池中釋放一個分片緩存中所有的塊。cached_piece_entry分片緩存對象自身並不從multi_index_container中移除。函數返回釋放的塊數。
- drain_piece_bufs(),收集一個分片緩存中所有的塊內存指針,並將塊指針設置成NULL。這個函數通常作爲釋放塊內存的子函數。
- try_read_from_cache(),嘗試從讀緩存獲取分片數據,如果分片不存在那麼調用cache_read_block嘗試載入分片到緩存中,然後再次嘗試從緩衝中讀取數據。如果讀取失敗了則直接返回失敗錯誤碼(負數),否則修改讀片緩存的expire值並返回。特別注意如果這片讀緩存的塊數等於0時(即沒有爲這個分片緩存任何塊)將從讀緩存中移除這一個分片。
- read_piece_from_cache_and_hash(),緩存一整個分片並計算hash值。
- cache_piece(),從硬盤中讀取一整個分片到硬盤,即使這個分片已經在讀緩存中存在。如果已經存在則更新expire,否則新建一個cached_piece_entry加入到讀緩存中。
thread_fun函數
線程函數依順序主要做以下事情:
- 執行m_queued_completions中的callback。執行完action_t操作後對應的callback被保存到m_queued_completions,如果【這個callback隊列的大小超過了30】或者【已經沒有需要執行的的job】那麼將隊列裏的回調POST到io_service中執行。總之:
- 異步函數piece_manager::async_xxx通常是在io_service線程中運行
- 對應的piece_manager::xxxx是在disk_io_thread的線程函數(threadfun)中被調用
- 最終這些類似on_xxxx的callback又被丟回io_service線程中執行(io_service::post)
- 等待m_signal事件觸發以繼續循環。
- 檢查abort標誌,如果已設置則處理abort事件的後續(disk_io_job::abort在下面switch (j.action)中處理),當abort標誌被設置時:
- 把寫緩衝區的數據同步到硬盤上。
- 清理讀寫緩衝區(內存池)
- 清理任務隊列
- 退出線程。
- 處理m_sorted_read_jobs(僅m_settings.allow_reordered_disk_operations=true時)。如果從m_jobs中取出的事件類型是讀事件,而且該讀事件未命中緩存,則這個任務會被添加到m_sorted_read_jobs隊列然後跳過後續步驟。由於m_sorted_read_jobs這個隊列是有序的,使得從頭開始處理文件讀取(每次讀取能緩存N個連續的塊)可以增加讀緩存的命中率和優化磁盤讀取速度(注:對固態硬盤無意義,對於固態硬盤來說分散讀取反而更快)。注意噹噹達到以下兩個條件之一時將彈出m_sorted_read_jobs隊列中的任務繼續執行。
- m_jobs任務列表爲空
- 使用m_settings.read_job_every算法輪到了處理m_sorted_read_jobs。這個算法防止全部磁盤IO被文件寫入佔用。
- 檢查是否有超時的寫緩衝區,如果有則寫入硬盤。
- 根據不同的action做不同處理【switch (j.action)】,多數情況下都是調用piece_manager的非異步版本處理。
- 事件完成後,將回調用post_callback添加到m_queued_completions中,在接下來的循環中將處理這個隊列。
5.3.3 文件列表file_storage
libed2k::file_storage類表示文件列表和片段大小,用於解釋常規bittorrent存儲文件結構所需的一切,包括:
- 文件屬性libed2k::file_entry列表,包括讀寫列表中指定index元素屬性的接口
- name,文件名
- offset,通常是0
- symlink_index
- size,文件大小
- name_len,文件名長度,如果是0則需要調用者用戶自己管理。
- pad_file,通常是false
- hidden_attribute,通常是false
- executable_attribute,通常是false,在windows下無意義。
- symlink_attribute,通常是false
- path_index,通常是-1
- 哈希列表(與文件列表對應),各文件下載完整後的哈希值。
- transfer任務名,以第一個添加的任務爲準。
- 所有文件總大小
- 所有分片的總數
- 單分片長度
- 指定分片ID=i時,獲取分片i的長度。
具有以下接口:
void add_file(std::string const& p, size_type size, int flags = 0, std::time_t mtime = 0, std::string const& s_p = "");
往列表中添加一個文件名爲p,大小爲size的文件(libed2k中添加torrent時後面的參數都是默認的),文件名不帶路徑,格式像“BBC荒野求生.mp4”,執行後構造一個新的file_entry添加到列表中。
void rename_file(int index, std::string const& new_filename);
重命名列表中ID=index的文件名爲new_filename,僅適用於列表並不實際修改的文件名。修改實際文件的文件名的操作在storage_interface::rename_file中執行,當然在storage_interface修改完文件的名字後也要調用這個接口以同步修改到libed2k::file_storage對象。
iterator begin() const { return m_files.begin(); }
iterator end() const { return m_files.end(); }
reverse_iterator rbegin() const { return m_files.rbegin(); }
reverse_iterator rend() const { return m_files.rend(); }
返回std::vector<internal_file_entry>::const_iterator迭代器指針
file_entry at(int index) const;
file_entry at(iterator i) const;
從文件信息數組中根據ID或者迭代器獲取file_entry。
iterator file_at_offset(size_type offset) const;
返回offset所處的文件,libed2k::file_storage對象可能會具有多個文件,但是所有文件的大小被累加到m_total_size中,因此每個文件都有一個起始地址(保存在內部的m_file_base數組中),第一個文件的起始地址爲0,第二各文件的起始地址就是第一個文件的大小,依此類推。
size_type file_base(int index) const;
void set_file_base(int index, size_type off);
獲取和設置索引爲index的文件在[0,m_total_size-1]這個區間中的起始地址。
sha1_hash hash(int index) const;
std::string const& symlink(int index) const;
time_t mtime(int index) const;
int file_index(int index) const;
std::string file_path(int index) const;
size_type file_size(int index) const;
獲取索引爲index的文件的各項屬性。
std::vector<file_slice> map_block(int piece, size_type offset, int size) const;
根據輸入的piece id,偏移和大小得到文件file_slice信息,如果輸入參數指定的返回跨越了多個文件則會返回多個file_slice對象。file_slice定義如下:
// 文件碎片,定義一小塊文件。在map_block中返回時,一個文件只會有一個file_slice struct LIBED2K_EXPORT file_slice
{
int file_index; //< 文件在file_storage::m_files中的索引
size_type offset; //< 文件偏移
size_type size; //< 文件片的大小
};
peer_request map_file(int file, size_type offset, int size) const;
和map_block相反,這是根據文件索引,偏移和大小得到piece id, offset和length並填充到peer_request相應的域中。
size_type total_size() const { return m_total_size; }
void set_num_pieces(int n) { m_num_pieces = n; }
int num_pieces() const
{
LIBED2K_ASSERT(m_piece_length > 0); return m_num_pieces;
}
void set_piece_length(int l) { m_piece_length = l; }
int piece_length() const
{
LIBED2K_ASSERT(m_piece_length > 0); return m_piece_length;
}
int piece_size(int index) const;
這幾個接口用於獲取分片數量和大小數據。注意piece_length()返回固定大小的分片字節數,piece_size()返回指定分片ID的字節數,只有最後一個分片的piece_size()不等於piece_length()。
5.3.4 storage_interface 接口說明
storage_interface 是一個純虛擬類,可以實現該類以自定義torrent數據的存儲方式和位置。默認的存儲實現使用文件系統中的常規文件,以一種將torrent保存到磁盤的方式映射torrent中的文件。實現自己的存儲接口可以將所有數據存儲在RAM中,或者以某種優化的順序存儲在磁盤上(例如,接收片段的順序),或者將多文件種子保存在單個文件中,以便能夠處理優化的磁盤I / O的優勢。
也可以編寫一個使用默認存儲但修改某些特定行爲的瘦類,例如在將數據寫入磁盤之前對其進行加密,並在再次讀取該數據時對其進行解密。
storage_interface基於片段。每個讀取和寫入操作都發生在分片空間中。每一個分片都是“ piece_size”個字節。所有訪問都通過寫入和讀取分片的全部或部分內容來完成。
libtorrent帶有兩個內置的存儲實現; default_storage和Disabled_storage。它們的構造函數分別稱爲default_storage_constructor()和Disabled_storage_constructor。Disabled_storage_constructor功能就像它名字說明的一樣,它丟棄已寫入的數據並讀取垃圾。它主要用於基準測試和性能分析。
virtual bool initialize(bool allocate_files) = 0;
當要初始化磁盤上的存儲時,將調用此函數。 此時默認存儲將創建目錄和空文件。 如果allocate_files爲true則還將截斷所有文件至其目標大小(即當)。
可以在單個實例上多次調用此函數。 強制重新檢查種子文件時,將重新初始化存儲以從頭開始重新檢查。
該函數不一定在其他成員函數之前調用。 例如,has_any_files()和verify_resume_data()會提前調用以確定我們是否必須檢查所有文件。 如果我們要對文件進行全面檢查,則每個文件都會被散列,從而導致readv()也被調用。
任何需要初始化的必需內部構件都應在構造函數中完成。 在種子文件開始下載之前,將調用此函數。
如果發生錯誤,應設置storage_error以反映該錯誤。
virtual bool has_any_file (storage_error& ec) = 0;
第一次檢查(或重新檢查)torrent的存儲時,將調用此函數。 如果磁盤上存在此存儲中使用的任何文件,則應返回true。 如果是這樣,將在開始下載之前檢查存儲中是否存在現有的片段。
如果發生錯誤,應設置storage_error以反映該錯誤。
virtual int readv(file::iovec_t const* bufs, int slot, int offset, int num_bufs);
virtual int writev(file::iovec_t const* bufs, int slot, int offset, int num_bufs);
這些函數應該以給定的偏移量讀取數據或將數據寫入給定的部分。它應該順序讀取或寫入num_bufs個緩衝區,其中每個緩衝區的大小在緩衝區數組bufs中指定。 iovec_t類型具有以下成員:struct iovec_t {void * iov_base; size_t iov_len; };
可能會從多個線程同時調用這些函數,所以應確保它們是線程安全的。當libtorrent中的文件可以回退到pread,preadv或Windows等效文件時,它是線程安全的。在無法進行線程安全讀取的目標(即必須先查找然後讀取)的目標上,僅使用一個磁盤線程。
大多數情況下,偏移量都與16 kiB邊界對齊,但很少有例外。特別是在讀取緩存被禁用/或已滿並且對等方請求未對齊數據的情況下。大多數客戶端要求對齊的數據。
應該返回讀取或寫入的字節數,如果出錯則返回-1。如果有錯誤,則必須填寫storage_error以表示發生的錯誤。
virtual int read(char* buf, int slot, int offset, int size) = 0;
virtual int write(const char* buf, int slot, int offset, int size) = 0;
讀寫緩衝區的另一種表現,在默認的內部實現是將buf和size封裝爲一個file::iovec_t,然後調用readv和writev版本
virtual size_type physical_offset(int slot, int offset) = 0;
返回slot和offset在物理文件中的偏移,因爲一個任務可能由多個文件組成。這個用於計算讀寫位置很方便,在libed2k中僅用於disk_io_thread的resort_read,見5.3.2中"thread_fun函數"一節第四點的介紹。
virtual void hint_read(int, int, int) {}
執行操作系統層面上的文件預讀,這個對於windows來說沒有意義因爲windows的文件緩存預讀是自動進行的且未提供任何可調用的接口。
virtual int sparse_end(int start) const;
稀疏文件,這是UNIX類和NTFS等文件系統的一個特性。開始時,一個稀疏文件不包含用戶數據,也沒有分配到用來存儲用戶數據的磁盤空間。當數據被寫入稀疏文件時,NTFS逐漸地爲其分配磁盤空間。一個稀疏文件有可能增長得很大。
這個函數根據start這個分片ID查找稀疏文件的結束位置後的一個分片ID,如果start這個分片不在文件的稀疏區域中則直接返回start。這個函數在piece_manager::check_one_piece中有用到。
virtual bool move_storage(std::string const& save_path) = 0;
此函數應將屬於該存儲的所有文件移動到新的save_path。 默認存儲將移動單個文件或torrent目錄。
在移動文件之前,可能必須關閉所有打開的文件句柄,例如release_files()。
如果發生錯誤,應設置storage_error以反映該錯誤。
virtual bool verify_resume_data(lazy_entry const& rd, error_code& error) = 0;
他的功能應使用磁盤上的文件來驗證恢復數據。 如果簡歷數據似乎是最新的,則返回true。 如果不是,則將error設置爲對不匹配項的描述,然後返回false。
默認存儲可以比較文件的大小和文件的時間戳。
如果發生錯誤,應設置serror以反映該錯誤。
此功能應使用磁盤上的文件來驗證恢復數據。 如果resume數據是最新的,則返回true。 如果不是,則將error設置爲對不匹配項的描述,然後返回false。
virtual bool write_resume_data(entry& rd) const = 0;
寫入和storage有關的fast resume項目。在默認實現中包含了"file sizes"列表,這個列表的每個元素是一對[文件大小,最後修改時間]。
virtual bool move_slot(int src_slot, int dst_slot) = 0;
從源slot複製文件內容到目標slot。
virtual bool swap_slots(int slot1, int slot2) = 0;
交換兩個slot的文件內容,讀取slot1和slot2的內容然後交換寫入。
virtual bool swap_slots3(int slot1, int slot2, int slot3) = 0;
交換三個slot的文件內容,將slot1的數據搬到slot2,將slot2的數據搬到slot3,將slot3的數據搬到slot1。
virtual bool release_files() = 0;
關閉所有已以寫入模式打開的句柄,這個在transfer任務完成下載的時候。
virtual bool rename_file(int index, std::string const& new_filename) = 0;
重命名一個文件,以文件集合中的序號定位一個文件。
virtual bool delete_files() = 0;
關閉所有已打開的文件並刪除所有文件。
disk_buffer_pool* disk_pool()
返回硬盤緩衝區內存池對象的指針,這個指針在libed2k中指向的是上一節介紹的disk_io_thread。這個函數多數時候用在LIBED2K_ALLOCATE_BLOCKS和LIBED2K_FREE_BLOCKS宏中,用於從硬盤緩衝區內存池中申請和釋放內存。
session_settings const& settings()
返回只讀的SESSION設置。disk_io_thread的線程中設置session_setting。
void set_error(std::string const& file, error_code const& ec) const;
error_code const& error() const { return m_error; }
std::string const& error_file() const { return m_error_file; }
virtual void clear_error() { m_error = error_code(); m_error_file.resize(0); }
設置、獲取和清除錯誤碼,設置和獲取錯誤文件。
在libed2k中default_storage的實現基於libed2k::file類和libed2k::file_storage類,如:
- default_storage::writev 的底層實現是file::writev
- default_storage::readv的底層實現是調用file::readv
- default_storage::has_any_file的底層實現是在file_storage const& m_files;中查找文件。
等等
5.3.5 default_storage的實現
上節提到libed2k使用的default_storage底層依賴於libed2k::file類和libed2k::file_storage(見5.3.3),這一節將通過分析幾個主要函數探索default_storage的實現細節。
首先看default_storage::writev的實現:
libed2k::piece_manager::write_impl(file::iovec_t* bufs, int piece_index, int offset, int num_bufs)
|---slot = allocate_slot_for_piece(piece_index)
|---libed2k::default_storage::writev(bufs, slot, offset, num_bufs);
|---fileop op = { &file::writev, &default_storage::write_unaligned, ..., file::read_write };
|--- libed2k::default_storage::readwritev(bufs, slot, offset, num_bufs, op);
|---size_type start = slot * (size_type)m_files.piece_length() + offset;
|---file_storage::iterator file_iter = $find_file_by_start(start)
|---while(file_iter & start & bufsize) //當讀寫長度跨越多個文件時逐個文件進行操作
|---file_handle = open_file(file_iter, op.mode, ec); //file_pool::open_file
|---create_directories(parent_path(path), ec); //當不存在時
|---adj_offset = files().file_base(*file_iter) + file_offset;
|---bytes = libed2k::file::writev(adj_offset, tmp_bufs,num_tmp_bufs,ec)//fileop
|---advance_bufs(current_buf, bytes);
|---$update_hash(libed2k::piece_manager::m_piece_hasher)
大致脈絡如下:
- 首先piece_manager::write_impl傳入num_bufs個內存緩衝區file::iovec_t,並指定了要將這些數據寫入到分片ID=piece_index且偏移爲offset的位置。
- 在write_impl函數中,爲piece_index分片分配slot(slot是storage_interface的概念,slot大小等於piece大小)。
- 在write_impl函數中調用default_storage::writev,將num_bufs個bufs寫入到slot:offset
- default_storage::readwritev提供了一個讀寫共用的流程,用fileop op區分。在default_storage::writev的實現中,這個文件操作是寫操作&file::writev。default_storage::readwritev的流程如下:
- 根據輸入的slot ID(slot)和偏移(offset)計算它在file_storage中的絕對偏移AO。。
- 根據絕對偏移AO尋找文件FE(file_storage::file_entry),計算相對於此文件起始地址的相對偏移RO(在file_storage中保存了文件列表、所有文件總長度、各文件的長度、分片數、分片大小等,可以根據這些值簡單計算出文件和偏移值)。現在我們知道了要將數據寫入文件FE中RO開始的位置。
- 接下來要解決一個問題,當輸入緩衝區的長度大於文件FE剩餘大小FE_LEFT時需要怎麼做?這裏的做法是打開文件FE寫入FE_LEFT(使用libed2k::file::writev),接下來取下一個文件,從緩衝區的FE_LEFT位置開始寫入下一個文件,當然下一個文件FE++的相對偏移就是0。如此循環往復直到緩衝區的數據全部寫完。
注意上面default_storage::writev中:
- 文件打開的操作default_storage::open_file的實現是default_storage::m_pool.open_file (file_pool::open_file)
- 文件寫入的操作是file::writev
- 由boost::intrusive_ptr<file> file_handle實現文件自動關閉。
file_pool的定義在include\libed2k\file_pool.hpp
這個類用於緩存已打開文件的句柄(boost::intrusive_ptr<libed2k::file>),storage_interface接口實例通過這個類打開文件。這個緩存的作用是避免多個文件重複打開關閉,當一個文件以一種方式打開時就會存放到緩存中,直到文件傳輸任務完成或者需要更換"讀/寫"打開模式。
緩存的具有一個上限,即保證不會打開比指定數量更多的文件句柄。 在多線程環境下每個線程都具有(通過智能指針)鎖定文件句柄的能力。只要保證文件的打開操作只通過file_pool接口完成就可以保證這個類是線程安全的。
在Session中只有一個file_pool實例,即session_impl類中的file_pool m_filepool;其他對象持有的file_pool對象都是它的引用。
主要的成員如下:
boost::intrusive_ptr<file> open_file(void* st, std::string const& p
, file_storage::iterator fe, file_storage const& fs, int m, error_code& ec);
返回一個已打開的句柄,打開模式由m參數指定。文件的位置是在file_storage fs的file_storage::iterator fe。
如果尚無此文件句柄的緩存則以m指定的模式打開文件全路徑即參數p指定的文件,然後新建一個句柄緩存(即一對<std::pair<void*, int>, lru_file_entry>)它以輸入參數<st,fs.file_index(*fe)>爲鍵, 以一個lru_file_entry值,在lru_file_entry保存了新打開文件的句柄boost::intrusive_ptr<file>、輸入參數st、打開模式(即本函數的參數m)以及最後使用時間。
如果已經有了此文件句柄的緩存,且緩存中的"打開文件模式"與輸入參數m一致則直接返回緩存中的句柄,否則以新的模式打開文件並更新緩存。如果緩存數超過了最大數量(默認40),那麼就會移除最後使用的文件句柄緩存,當文件緩存被移除時對應文件會自動關閉(由boost::intrusive_ptr<file>提供自動關閉機制)。
注:通過libed2k::file::open打開文件。
void release(void* st);
void release(void* st, int file_index);
關閉緩存中存儲接口對象等於st以及文件索引等於file_index的文件(參數對應內部map的鍵std::pair<void*, int>),如果st爲NULL則關閉所有緩存中的句柄。
內部緩存的實現如下:
/**
* \brief 句柄緩存
*/
struct lru_file_entry
{
lru_file_entry()
: key(0), last_use(libed2k::time_now()), mode(0)
{}
// 已打開的文件句柄
mutable boost::intrusive_ptr<file> file_ptr;
// 在當前default_storage的實現是指向default_storage對象的指針
void* key;
// 最後一次使用這個句柄的時間
libed2k::ptime last_use;
// 這個文件句柄的打開模式,由file_pool::open_file的m參數指定。
int mode;
};
// maps storage pointer, file index pairs to the
// lru entry for the file
//
// 在當前default_storage的實現中:
// std::pair<void*, int>的第一個成員是一個指向default_storage對象的指針
// 第二個成員是文件在一個file_storage中的0開始的索引。
//
typedef std::map<std::pair<void*, int>, lru_file_entry> file_set;
libed2k::file類的接口
bool open(std::string const& p, int m, error_code& ec);
打開文件,p可以是在運行目錄下的相對目錄,也可以是絕對路徑。在libed2k中default_storage使用的是絕對路徑。m是打開模式。在Windows下使用CreateFileA這個API打開文件:
WINBASEAPI __out HANDLE WINAPI
CreateFileA(
__in LPCSTR lpFileName,
__in DWORD dwDesiredAccess,
__in DWORD dwShareMode,
__in_opt LPSECURITY_ATTRIBUTES lpSecurityAttributes,
__in DWORD dwCreationDisposition,
__in DWORD dwFlagsAndAttributes,
__in_opt HANDLE hTemplateFile
);
,(以下加重字體部分是libed2k::file的文件打開模式)參數m的值中,基礎模式有三種:【read_only = 0,write_only = 1,read_write = 2】
這三種打開方式對應CreateFileA的dwDesiredAccess和dwCreationDisposition參數:
- read_only : {dwDesiredAccess = GENERIC_READ, dwCreationDisposition = OPEN_EXISTING}
- write_only :{dwDesiredAccess = GENERIC_WRITE, dwCreationDisposition = OPEN_ALWAYS}
- read_write :{dwDesiredAccess = GENERIC_WRITE | GENERIC_READ, dwCreationDisposition = OPEN_ALWAYS}
lock_file標誌影響CreateFileA的dwShareMode參數:
- 當未設置時:dwShareMode=0
- 當設置時,取決於基礎模式
- read_only :{FILE_SHARE_READ | FILE_SHARE_WRITE}
- write_only :{FILE_SHARE_READ}
- read_write :{FILE_SHARE_READ}
[attribute_hidden, attribute_executable, random_access, no_buffer]這幾個數值影響dwFlagsAndAttributes:
- 當1 == m & attribute_hidden時FlagsAndAttributes |= FILE_ATTRIBUTE_HIDDEN
- 當0 == m & attribute_hidden時FlagsAndAttributes |= FILE_ATTRIBUTE_NORMAL
- attribute_executable在windows上不起作用。
- 當1 == m & random_access時FlagsAndAttributes |= FILE_FLAG_SEQUENTIAL_SCAN
- 當1 == m & no_buffer時
- FlagsAndAttributes |= FILE_FLAG_OVERLAPPED
- FlagsAndAttributes |= FILE_FLAG_NO_BUFFERING
當1 == m & file::sparse且打開模式爲write_only或者read_write時在打開的文件句柄上執行:
::DeviceIoControl(m_file_handle, FSCTL_SET_SPARSE, 0, 0, 0, 0, &temp, 0);
FSCTL_SET_SPARSE聲明這個文件是一個稀疏文件。
bool set_size(size_type size, error_code& ec);
這個函數將設置文件大小。如果文件已經存在並且大小大於size參數指定的值,那麼它將被截斷到size大小。如果文件不存在,那麼文件將被創建且文件大小被初始化爲size(即填充0).
size_type get_size(error_code& ec) const;
獲取文件的大小,在Windows下調用的是GetFileSizeEx。
void finalize();
在Windows下,這個函數的意義是清除FSCTL_SET_SPARSE。如下:
FILE_SET_SPARSE_BUFFER b;
b.SetSparse = FALSE;
:DeviceIoControl(m_file_handle, FSCTL_SET_SPARSE, &b, sizeof(b) , 0, 0, &temp, 0);
int pos_alignment() const;
當以禁用系統緩存時(session_settings::disk_io_write_mode和session_settings::disk_io_read_mode被設置爲disable_os_cache),這是file_offsets需要的對齊方式,file_offset & pos_alignment() == 0是讀寫操作的前提條件。在windows下這個值由
BOOL WINAPI GetDiskFreeSpaceA(
__in_opt LPCSTR lpRootPathName,
__out_opt LPDWORD lpSectorsPerCluster,
__out_opt LPDWORD lpBytesPerSector,
__out_opt LPDWORD lpNumberOfFreeClusters,
__out_opt LPDWORD lpTotalNumberOfClusters )
API取得的lpBytesPerSector即扇區內字節數決定,在這個API調用失敗時設置的默認值是512。當使用FILE_FLAG_NO_BUFFERING打開文件進行工作時,程序必須達到下列要求:
- 文件的存取開頭的字節偏移量必須是扇區尺寸的整倍數.
- 文件存取的字節數必須是扇區尺寸的整倍數.例如,如果扇區尺寸是512字節.程序就可以讀或者寫512,1024或者2048字節,但不能夠是335,981或者7171字節.
- 進行讀和寫操作的地址必須在扇區的對齊位置,在內存中對齊的地址是扇區.尺寸的整倍數.一個將緩衝區與扇區尺寸對齊的途徑是使用VirtualAlloc函數.它分配與操作系統內存頁大小的整倍數對齊的內存地址.因爲內存頁尺寸和扇區尺寸--2都是它們的冪.這塊內存在地址中同樣與扇區尺寸大小的整倍數對齊.程序可以通過調用GetDiskFreeSpace來確定扇區的尺寸.
默認情況下ession_settings::disk_io_write_mode和session_settings::disk_io_read_mode被設置enable_os_cache。
int buf_alignment() const;
當禁用系統緩存時,這是內存緩衝區的起始地址需要的對齊。
int file::size_alignment() const
在windows下這個值等於GetSystemInfo返回的dwPageSize,即系統分頁大小。在禁用系統緩存時寫入大小必須時這個的整數倍。
size_type writev(size_type file_offset, iovec_t const* bufs, int num_bufs, error_code& ec);
在使用系統緩存時用SetFilePointerEx定位到file_offset,然後用WriteFile循環寫入num_bufs個bufs。在禁用系統緩存時用WriteFileGather寫入。
size_type readv(size_type file_offset, iovec_t const* bufs, int num_bufs, error_code& ec);
在使用系統緩存時用SetFilePointerEx和ReadFile從文件中循環讀入num_bufs個bufs。在禁用系統緩存時調用ReadFileScatter。
void hint_read(size_type file_offset, int len);
調用操作系統API對文件進行預讀,在Windows下無意義。
size_type sparse_end(size_type start) const;
返回稀疏文件中從start位置開始已分配空間區域的位置,內部調用的函數是DeviceIoControl(FSCTL_QUERY_ALLOCATED_RANGES)這個函數的返回值:
- 如果調用發生錯誤返回start。
- 如果start之後的文件中沒有已分配的稀疏區域,那麼返回文件大小(get_size())。
- 如果start在已分配的稀疏區域內,那麼返回start。
- 否則返回下一個已分配區域的起始位置。
size_type phys_offset(size_type offset);
返回文件offset在硬盤上的實際位置用於優化文件讀寫,對固態硬盤沒有意義。在Windows下調用的是DeviceIoControl(FSCTL_GET_RETRIEVAL_POINTERS)。對這個偏移的應用請見disk_io_thread::m_sorted_read_jobs,這個隊列根據這個物理偏移對讀文件請求進行排序。
5.3.6 piece、slot和儲存模式
piece和slot的同與不同:
- piece是file_storage概念,而slot是storage_interface中的概念。
- 在當寫入一個piece時,由piece_manager爲這個piece分配slot(allocate_slot_for_piece)
- 在使用稀疏文件時,piece[n] != slot[n]
transfer的存儲模式在add_transfer_params::storage_mode中設置,默認爲storage_mode_sparse;儲存模式的定義如下:
enum storage_mode_t
{
storage_mode_allocate = 0,
storage_mode_sparse,
// this is here for internal use
internal_storage_mode_compact_deprecated,
#ifndef LIBED2K_NO_DEPRECATE
storage_mode_compact = internal_storage_mode_compact_deprecated
#endif
};
三種儲存模式的異同點:
- storage_mode_allocate 在開始時就分配和完整文件一樣大小的硬盤空間
- storage_mode_sparse 創建一個稀疏文件,它的文件大小和完整文件大小一樣,但是佔用空間隨寫入數據增長。
- storage_mode_compact 未知