linux 文件刪除過程淺析

1.Linux文件刪除原理

Linux是通過link的數量控制文件刪除的,只有當文件不存在任何鏈接時,該文件纔會被刪除,一般每個文件有兩個link計數器: i_count 和 i_nlink,從VFS inode結構體中可以找到:

struct inode {
struct hlist_node i_hash; /* hash鏈表的指針 */
struct list_head i_list; /* backing dev IO list */
struct list_head i_sb_list; /* 超級塊的inode鏈表 */
struct list_head i_dentry; /* 引用inode的目錄項對象鏈表頭 */
unsigned long i_ino; /* 索引節點號 */
atomic_t i_count; /* 引用計數器 */
unsigned int i_nlink; /* 硬鏈接數目 */
...

i_count: 引用計數器,文件被一進程引用,i_count數增加 ,可以認爲是當前文件使用者的數量; 
i_nlink: 硬鏈接數目(可以理解爲磁盤的引用計數器),創建硬鏈接對應的 i_nlink 就會增加

對於rm命令來說,實際就是減少磁盤的引用計數 i_nlink 。如果當文件被另外一個進程調用時,用戶執行rm命令刪除文件,再去cat文件內容時就會找不到文件,但是調用該刪除文件的那個進程卻仍然可以對文件進行正常的操作。這就是因爲 i_nlink 爲 0 ,但 i_count 並不爲 0 。只有當 i_nlink 和 i_count 均爲 0 時,文件纔會被刪除(這裏的刪除是指將文件名到 inode 的鏈接刪除了,但文件在磁盤上的block數據塊並未被刪除)。

首先使用strace追蹤rm命令,看看rm具體使用了哪些系統調用:

[root@ty ~]# strace rm test
execve("/bin/rm", ["rm", "test"], [/* 19 vars */]) = 0
brk(0) = 0x175c000
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f1365a2f000
access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
open("/etc/ld.so.cache", O_RDONLY) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=62329, ...}) = 0
mmap(NULL, 62329, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f1365a1f000
close(3) = 0
open("/lib64/libc.so.6", O_RDONLY) = 3
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\360\355\201\2160\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=1922152, ...}) = 0
... ...
brk(0) = 0x175c000
brk(0x177d000) = 0x177d000
open("/usr/lib/locale/locale-archive", O_RDONLY) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=99158576, ...}) = 0
mmap(NULL, 99158576, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f135fb8b000
close(3) = 0
ioctl(0, SNDCTL_TMR_TIMEBASE or TCGETS, {B38400 opost isig icanon echo ...}) = 0
newfstatat(AT_FDCWD, "test", {st_mode=S_IFREG|0644, st_size=0, ...}, AT_SYMLINK_NOFOLLOW) = 0
geteuid() = 0
unlinkat(AT_FDCWD, "test", 0) = 0
close(0) = 0
close(1) = 0
close(2) = 0
exit_group(0) = ?

由上發現最終調用unlinkat系統調用刪除文件。 

2.系統調用unlinkat

unistd.h、unistd_64.h、unistd_32.h中定義系統調用號

#define __NR_link 9
#define __NR_unlink 10
...
#define __NR_unlinkat 456

兩個函數定義在 Namei.c 文件中,如下:

SYSCALL_DEFINE3(unlinkat, int, dfd, const char __user *, pathname, int, flag)
{
if ((flag & ~AT_REMOVEDIR) != 0)
return -EINVAL;
 
if (flag & AT_REMOVEDIR)
return do_rmdir(dfd, pathname);
 
return do_unlinkat(dfd, pathname);
}
 
SYSCALL_DEFINE1(unlink, const char __user *, pathname)
{
return do_unlinkat(AT_FDCWD, pathname);
}

在unlinkat函數定義中會判斷flag標誌,如果有設置AT_REMOVEDIR標誌,說明刪除的是目錄則調用do_rmdir函數,否則調用do_unlinkat函數刪除一個文件。

3.do_unlinkat函數

/*
* 確保文件的實際截斷髮生在其目錄的i_mutex之外,如果有很多寫操作發生截斷需要很長時間
* 在等待I/O發生時,我們不想阻止目錄的訪問
*/
static long do_unlinkat(int dfd, const char __user *pathname)
{
int error;
char *name;
struct dentry *dentry;
struct nameidata nd;
struct inode *inode = NULL;
 
error = user_path_parent(dfd, pathname, &nd, &name); /* 獲取父目錄信息,成功保存在nd結構體中,並返回0*/
if (error)
return error;
 
error = -EISDIR;
if (nd.last_type != LAST_NORM) /*路徑名中最後一個分量類型不是一個普通文件名*/
goto exit1;
 
nd.flags &= ~LOOKUP_PARENT; /*清除LOOKUP_PARENT標誌*/
 
mutex_lock_nested(&nd.path.dentry->d_inode->i_mutex, I_MUTEX_PARENT);
dentry = lookup_hash(&nd); /*返回要刪除文件的dentry結構*/
error = PTR_ERR(dentry);
if (!IS_ERR(dentry)) {
/* Why not before? Because we want correct error value */
if (nd.last.name[nd.last.len]) /*last域代表路徑名中的最後一個分量,在LOOKUP_PARENT標誌設置時使用,因爲上邊已經做了清除標誌操作,此處這樣做可以判斷是否清除成功,否的話,返回正確的錯誤碼*/
goto slashes;
inode = dentry->d_inode;
if (inode)
atomic_inc(&inode->i_count); /*i_count引用計數器(文件當前使用者的數量)原子的加1*/
error = mnt_want_write(nd.path.mnt); /*判斷mnt對象是否有可寫權限,是返回0*/
if (error)
goto exit2;
error = security_path_unlink(&nd.path, dentry); /*檢查權限允許刪除一個到文件的硬鏈接,調用path_unlink是一個函數指針(如果有定義)*/
if (error)
goto exit3;
error = vfs_unlink(nd.path.dentry->d_inode, dentry);
exit3:
mnt_drop_write(nd.path.mnt);
exit2:
dput(dentry); /*釋放dentry結構體,並釋放資源*/
}
mutex_unlock(&nd.path.dentry->d_inode->i_mutex);
if (inode)
iput(inode); /* truncate the inode here */
exit1:
path_put(&nd.path); /*釋放引用的一個path結構體*/
putname(name); /*釋放name*/
return error;
 
slashes:
error = !dentry->d_inode ? -ENOENT :
S_ISDIR(dentry->d_inode->i_mode) ? -EISDIR : -ENOTDIR;
goto exit2;
}

主要分爲以下幾個步驟:

(1) error = user_path_parent(dfd, pathname, &nd, &name);
(2) dentry = lookup_hash(&nd);
(3) atomic_inc(&inode->i_count);
(4) vfs_unlink(nd.dentry->d_inode, dentry);
(5) dput(dentry);
(6) iput(inode); /* truncate the inode here */

該函數中首先使用user_path_parent()函數獲取要刪除文件的父目錄信息,成功會返回0,並且將父目錄信息保存在nameidata類型的結構體nd中,然後通過lookup_hash函數在當前目錄中找尋要刪除文件的目錄項信息,if語句判斷獲取的dentry是否有錯誤,有錯誤就使用path_put()和putname()釋放前幾步獲取到的數據,然後返回,結束;若沒有錯誤,得到該文件的inode,增加其i_count進程引用計數,判斷當前掛載點是否有可寫權限,有可寫權限就調用vfs_unlink函數執行文件dentry的刪除,釋放dentry結構體,並釋放資源,最後調用iput函數截斷inode。

3.1 vfs_unlink函數

int vfs_unlink(struct inode *dir, struct dentry *dentry) /*刪除dentry,即就是硬鏈接*/
{
int error = may_delete(dir, dentry, 0); /*權限檢查,是否有刪除權限*/
 
if (error)
return error;
 
if (!dir->i_op->unlink)
return -EPERM;
 
vfs_dq_init(dir);
 
mutex_lock(&dentry->d_inode->i_mutex);
if (d_mountpoint(dentry))
error = -EBUSY;
else {
error = security_inode_unlink(dir, dentry);
if (!error)
error = dir->i_op->unlink(dir, dentry); /*調用具體文件系統的unlink函數,如ext3_unlink()*/
}
mutex_unlock(&dentry->d_inode->i_mutex);
 
/* We don't d_delete() NFS sillyrenamed files--they still exist. */
if (!error && !(dentry->d_flags & DCACHE_NFSFS_RENAMED)) {
fsnotify_link_count(dentry->d_inode);
d_delete(dentry); /* 若dentry的使用計數爲 1,說明沒有其他進程引用該dentry,就嘗試把該dentry的inode刪除 */
}
return error;
}

在vfs_unlink()函數中會調用具體文件系統的刪除函數。首先檢查要刪除文件所在目錄的權限,判斷是否有刪除權限,有刪除權限,然後判斷具體文件系統是否還有定義自己的unlink函數,沒有就退出vfs_unlink函數,否則調用具體文件系統unlink函數(如ext3_unlink()函數)。

3.2 ext3_unlink函數

static int ext3_unlink(struct inode * dir, struct dentry *dentry)
{
int retval;
struct inode * inode;
struct buffer_head * bh;
struct ext3_dir_entry_2 * de;
handle_t *handle;
......
bh = ext3_find_entry(dir, &dentry->d_name, &de); /*返回該inode對應的buffer_head,並填充其ext3_dir_entry_2結構體*/
if (!bh)
goto end_unlink;
 
inode = dentry->d_inode;
 
retval = -EIO;
if (le32_to_cpu(de->inode) != inode->i_ino)
goto end_unlink;
......
retval = ext3_delete_entry(handle, dir, de, bh); /*從該文件父目錄中刪除該文件目錄項*/
if (retval)
goto end_unlink;
dir->i_ctime = dir->i_mtime = CURRENT_TIME_SEC;
ext3_update_dx_flag(dir);
ext3_mark_inode_dirty(handle, dir);
 
/*drop_nlink()將i_nlink數減1 */
drop_nlink(inode);
/*
* 如果i_nlink爲0,說明該inode再也沒有任何dentry引用它了(即就是硬鏈接數位0)
* 則認爲當前節點爲孤兒節點(準備刪除的inode),加入到orphan inode鏈表中*/
*/
if (!inode->i_nlink)
ext3_orphan_add(handle, inode);
inode->i_ctime = dir->i_ctime; /*更新修改索引節點的時間*/
ext3_mark_inode_dirty(handle, inode);
retval = 0;
 
end_unlink:
ext3_journal_stop(handle);
brelse (bh);
return retval;
}

該函數中最主要的操作是ext3_delete_entry函數,從該文件父目錄中刪除文件目錄項,然後修改父目錄的i_ctime(上次修改文件)和i_mtime(上次寫文件)字段。然後將該文件inode 的硬鏈接計數減一後若爲0,就將其添加到orphan inode(孤兒鏈表,後邊介紹)鏈表中,更新該inode的修改時間。

3.3 dput函數(release a dentry)

void dput(struct dentry *dentry)
{
if (!dentry)
return;
 
repeat:
if (atomic_read(&dentry->d_count) == 1)
might_sleep(); /*提醒用戶調用該函數的函數可能會sleep*/
 
/*
* 將d_count原子的減1,並同時對dcache_lock進行加鎖,若d_count減1後仍非0,則直接返回return
* 這塊就是判斷d_count個數,如果此時d_count大於1,存在多個引用就不能釋放該dentry,直接返回;
* 若爲1,則可以刪除dentry,繼續後邊的釋放語句
**/
if (!atomic_dec_and_lock(&dentry->d_count, &dcache_lock))
return;
 
spin_lock(&dentry->d_lock); /*上一步對dentry->d_count遞減,此處加鎖,防止其他進程進行操作*/
if (atomic_read(&dentry->d_count)) { /**/
spin_unlock(&dentry->d_lock);
spin_unlock(&dcache_lock);
return;
}
 
/*
* AV: ->d_delete() is _NOT_ allowed to block now.
*
*/
/*
* 判斷具體文件系統是否定義了d_op->d_delete接口函數,
* ext2、ext3沒定義,NFS有定義該接口函數,但沒什麼實質內容,基本是直接返回;
* 若定義該接口函數則調用__d_drop()函數,把dentry從哈希鏈上移除,再調用dentry_iput函數嘗試刪除inode
*/
if (dentry->d_op && dentry->d_op->d_delete) {
if (dentry->d_op->d_delete(dentry))
goto unhash_it;
}
/* Unreachable? Get rid of it */
if (d_unhashed(dentry)) /*dentry從dcache hash鏈上移除了,表示該元數據對應的對象已經被刪除,此時可以釋放該元數據*/
goto kill_it;
if (list_empty(&dentry->d_lru)) { /*沒有從dcache哈希鏈移除,表示該元數據對應的對象沒有被刪除,這時把dentry掛到LRU隊列dentry_unused上*/
dentry->d_flags |= DCACHE_REFERENCED;
dentry_lru_add(dentry);
}
spin_unlock(&dentry->d_lock);
spin_unlock(&dcache_lock);
return;
 
unhash_it:
__d_drop(dentry);
kill_it:
/* if dentry was on the d_lru list delete it from there */
dentry_lru_del(dentry);
dentry = d_kill(dentry); /*刪除該dentry,返回父目錄項*/
if (dentry)
goto repeat;
}

dput函數的主要功能是釋放一個dentry結構體,並且將該結構體的使用計數d_count的值減1操作,將該結構體從隊列中刪除,同時,釋放該結構體的資源,無返回值。 
函數中出現repeat字段,每次釋放一個dentry,都要獲取其父目錄項,然後又跳轉到dput()開頭,繼續對父目錄dentry進行釋放操作;是因爲:每次創建一個dentry結構,除了增加自身的使用計數外,還會增加其父目錄dentry的使用計數,所以當釋放了一個dentry後也需要遞減其父目錄dentry的使用計數。才能保證父目錄爲空時能夠被釋放。

4.iput函數(truncate the inode here)

void iput(struct inode *inode)
{
if (inode) {
BUG_ON(inode->i_state == I_CLEAR);
 
if (atomic_dec_and_lock(&inode->i_count, &inode_lock))
iput_final(inode);
}
}

atomic_dec_and_lock宏先對i_count(進程使用計數)加鎖後原子的減一,結果爲0時,返回true,再進行調用iput_final函數進行刪除操作,否則不進行任何操作。

4.1 iput_final函數

static inline void iput_final(struct inode *inode)
{
const struct super_operations *op = inode->i_sb->s_op;
void (*drop)(struct inode *) = generic_drop_inode;
 
if (op && op->drop_inode)
drop = op->drop_inode;
drop(inode);
}

該函數主要是調用generic_drop_inode()函數,其會判斷inode->i_nlink的值,若爲0,則該inode可以被刪除,調用generic_delete_inode()實現。

void generic_drop_inode(struct inode *inode)
{
if (!inode->i_nlink) /*i_nlink硬鏈接數爲 0,刪除 */
generic_delete_inode(inode);
else /* 不爲 0 ,不能刪除*/
generic_forget_inode(inode);
}

在generic_delete_inode()函數中會判斷是否定義具體文件系統的超級塊操作函數delete_inode,若定義的就調用具體的inode刪除函數(如ext3_delete_inode ),否則調用truncate_inode_pages和clear_inode函數(在具體文件系統的delete_inode函數中也必須調用這兩個函數)。

4.2 ext3_delete_inode 函數

void ext3_delete_inode (struct inode * inode)
{
handle_t *handle;
truncate_inode_pages(&inode->i_data, 0);
 
if (is_bad_inode(inode))
goto no_delete;
 
handle = start_transaction(inode);
if (IS_ERR(handle)) {
/* 如果我們要跳過正常清理,我們仍然需要確保內核孤兒鏈表進行適當的清理。*/
ext3_orphan_del(NULL, inode);
goto no_delete;
}
 
if (IS_SYNC(inode))
handle->h_sync = 1;
inode->i_size = 0; /*將索引節點中記錄的文件大小設置爲0*/
if (inode->i_blocks)
ext3_truncate(inode); /*將索引節點中的文件佔用塊數清除*/
 
ext3_orphan_del(handle, inode);
EXT3_I(inode)->i_dtime = get_seconds(); /*修改該文件索引節點中的刪除時間*/
 
if (ext3_mark_inode_dirty(handle, inode))
/* If that failed, just do the required in-core inode clear. */
clear_inode(inode);
else
ext3_free_inode(handle, inode);
ext3_journal_stop(handle);
return;
no_delete:
clear_inode(inode); /* We must guarantee clearing of inode... */
}

該函數會刪除指定的inode,其中主要會調用如下函數(還沒看完):

  • ext3_truncate 截斷磁盤上的索引信息
  • ext3_orphan_del
  • ext3_free_inode 從內存中和磁盤上分別刪除該inode

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章