帶你寫一個Linux 下的打包軟件 Tar
相信你對 linux 的 .tar.gz 有點熟悉,這就是先 tar 打包(.tar 后綴),再對此 tar 文件用 gzip 壓縮(.tar.gz)的后綴名。
值得注意的是, tar 不是壓縮軟件,它只做把一堆文件/文件夾打包到一個文件(tar 文件)里的事情,而文件聯系,文件權限,相對的路徑等都會給你保存好。
一開始設計是 tar 跟 gzip 只做一件事情,各司其事,后來發現太麻煩了,于是就把壓縮功能整合到 tar 里了。
- - Create a gzipped archive:
- tar czf target.tar.gz file1 file2 file3
最近學習 OS 時寫了一個類似 tar 的項目,那么今天就趁熱打鐵簡單說一下如何寫一個打包軟件,這個軟件會將重復的文件內容通過 md5 比較,復用舊的內容。
基本單位 block
block 可以理解為文件系統的最小單位,分別有以下類型:
- directory block,文件夾 block,存儲文件夾 meta 信息;
- file block,文件 block,存儲文件 meta 信息;
- data block,只用來存文件內容;
Directory block,注意的是 entry 里要有 fileindex 來存儲重復文件的 name 的下標。
同時,給 項目一個 root dir。
- typedef struct {
- char name[SIFS_MAX_NAME_LENGTH]; // name of the directory
- time_t modtime; // time last modified <- time()
- uint32_t nentries;// 文件夾內的文件/文件夾數量
- struct {
- SIFS_BLOCKID blockID; // subdirectory 或者 file 的 blockID
- uint32_t fileindex; // 重復文件的不同名字
- } entries[SIFS_MAX_ENTRIES];
- } SIFS_DIRBLOCK;
文件 Block,length 就是有多少 bytes 的文件內容,之后用來算有多少個 data block,firstblockID 記錄第一個數據 block 的 id,nfiles 記錄有多少重復內容的文件數量了,filenames 就是重復此文件 block 的文件內容的文件名字。
- typedef struct {
- time_t modtime; // time first file added <- time()
- size_t length; // length of files' contents in bytes
- unsigned char md5[MD5_BYTELEN];//the MD5 cryptographic digest (a summary) of the files' contents
- SIFS_BLOCKID firstblockID;// the block number (blockID) of the files' first data-block
- uint32_t nfiles; // n files with identical contents
- char filenames[SIFS_MAX_ENTRIES][SIFS_MAX_NAME_LENGTH];// an array of each same file's name and its modification time.
- } SIFS_FILEBLOCK;
bitmaps數組,記錄了每個 block 的類型,有:文件、文件夾以及data block 三種類型。
通用函數
就讓大家看看關鍵函數好了:
讀 tar 后的文件的 meta 頭,記錄了 block 的大小( blocksize) 以及多少個 blocks。
- void read_vol_header(FILE *vol, SIFS_VOLUME_HEADER *header) {
- fread(header, sizeof(SIFS_VOLUME_HEADER), 1, vol);
- printf("header->blocksize %zu, header->nblocks %u\n", header->blocksize , header->nblocks);
- }
bitmap,每次操作 tar 文件都要讀的。
- void read_bitmap(FILE *vol, SIFS_BIT *bitmap, int nblocks) {
- int size = nblocks * sizeof(SIFS_BIT);
- fread(bitmap, size, 1, vol);
- }
root_block 同理,讀和寫啥東西都要從 root block、root dir 出發。
- void read_root_block(FILE *vol, SIFS_DIRBLOCK *dirblock){
- fread(dirblock, sizeof(SIFS_DIRBLOCK), 1, vol);
- printf("read_root_block finish, dirblock.name: %s, dirblock.entrieds: %d, dirblock.modtime %ld\n", dirblock->name, dirblock->nentries,dirblock->modtime);
- }
路徑嘛,你懂的,./sifs_put volumn ~/res.txt /dirB/subdirB/subsubdir/newfileB,要讀的內容可以靠 read 函數解決,但是寫到 tar 文件里的就要手動解析遞歸查路徑了。
- void read_route_names(char* pathname, char** route_names, int *route_cnt) {
- char *dir;
- char *pathname_to_split = copyStr(pathname);
- strcpy(pathname_to_split, pathname);
- while ((dir = strsep(&pathname_to_split, "/")) != NULL) {
- route_names[*route_cnt] = copyStr(dir);
- (*route_cnt)++;
- }
- }
以上幾乎是 mkdir,rmdir,writefile,readfile,putfile 等等操作都要做的。
實現
然后,應該舉一個 readfile 的例子就可以做代表了。
- int recursive_dirinfo(SIFS_DIRBLOCK *cur_dir_block, char **route_names, int route_name_p, int route_cnt);
實現:
- int recursive_dirinfo(SIFS_DIRBLOCK *cur_dir_block, char **route_names, int route_name_p, int route_cnt) {
- for(int i=0; i<cur_dir_block->nentries ; i++) {
- int blockid = cur_dir_block->entries[i].blockID;
- if(bitmap[blockid]==SIFS_DIR) {
- SIFS_DIRBLOCK dirblock;
- int start = sizeof(SIFS_VOLUME_HEADER) + header.nblocks*sizeof(SIFS_BIT);
- read_dir_block(vol, &dirblock, blockid * blocksize, start);
- if(strcmp(dirblock.name, route_names[route_name_p]) == 0) {
- if(route_name_p+2 == route_cnt) {
- return do_read_file(cur_dir_block, route_names[route_name_p+1], blockid);
- }
- return recursive_dirinfo(&dirblock, route_names, route_name_p+1, route_cnt);
- }
- }
- }
- return 1;
- }
以``./sifs_put volumn ~/res.txt /dirB/subdirB/subsubdir/newfileB 為例子,如果遞歸找到 subsubdir`這個文件夾 block,進行相應操作:
- 寫文件就往 bitmap 一直找沒有用過的 block,夠寫文件就寫進去,文件夾更新一下信息。
- 讀文件就是根據此文件夾 block,找里面的 newfileB
- int do_read_file(SIFS_DIRBLOCK *parent_dir, char *filename, int parent_dir_block) {
- printf("do_find_file_info, filename %s\n", filename);
- for(int i=1; i<header.nblocks ; i++) {
- SIFS_FILEBLOCK fileblock;
- if(bitmap[i]==SIFS_FILE) {
- int start = sizeof(SIFS_VOLUME_HEADER) + header.nblocks*sizeof(SIFS_BIT);
- read_file_block(vol, &fileblock, i * blocksize, start);
- *nbytes = fileblock.length;
- int need_data_blocks = *nbytes / header.blocksize;
- if(strcmp(fileblock.filenames[0], filename) == 0) {
- for(int d_block_id = fileblock.firstblockID; d_block_id - i -1 < need_data_blocks; d_block_id++) {
- read_data_block(vol, (char*)(*data)+(d_block_id - i -1), blocksize, d_block_id * header.blocksize, start);
- }
- return 0;
- }
- }
- }
- return 1;
- }
而真實的 tar 自然更復雜,還要記錄用戶權限、用戶、group文件等等:
- struct posix_header
- { /* byte offset */
- char name[100]; /* 0 */ 文件名
- char mode[8]; /* 100 */ 用戶權限
- char uid[8]; /* 108 */ user id
- char gid[8]; /* 116 */ group id
- char size[12]; /* 124 */ 文件大小
- char mtime[12]; /* 136 */ 修改時間
- char chksum[8]; /* 148 */ 校驗值
- char typeflag; /* 156 */ 文件類型標志
- char linkname[100]; /* 157 */ 符號鏈接指向
- char magic[6]; /* 257 */
- char version[2]; /* 263 */
- char uname[32]; /* 265 */ user name
- char gname[32]; /* 297 */ group name
- char devmajor[8]; /* 329 */ 設備文件 major
- char devminor[8]; /* 337 */ 設備文件 minor
- char prefix[155]; /* 345 */
- /* 500 */
- };
- 文件類型標志定義,包含了所有 Unix 系統中的文件類型
- #define REGTYPE '0' /* regular file */
- #define LNKTYPE '1' /* link */
- #define SYMTYPE '2' /* reserved */
- #define CHRTYPE '3' /* character special */
- #define BLKTYPE '4' /* block special */
- #define DIRTYPE '5' /* directory */
- #define FIFOTYPE '6' /* FIFO special */
- #define CONTTYPE '7' /* reserved */
概覽如此,寫起來其實有點煩 - = -,有興趣的讀者可以寫寫。