上一篇:https://blog.csdn.net/weixin_42523774/article/details/103341058
· 本文是Linux文件系列的第三篇,上文《系統調用如何進入內核層次,深入glibc尋找open函數真實實現。》,講到了從應用程序到如何通過glibc中進入內核的過程,本文接着上文,講系統調用在內核中如何實現,還是以open函數爲例,後續會介紹read和write函數的內核實現。
· 本文采用linux3.2.1內核代碼[下載處],並推薦使用Source Insight,這會讓這個過程更容易。
· 我想先提醒一下,這些系統調用的操作者是某一個進程,open操作其實是在read和write操作之前的一個預備動作,如果是真實文件,這個操作就是將文件從硬盤(或flash)中讀取到內存中,當然這個過程中要防止多個進程的同時操作,因此會有很多鎖作爲保護,這也是代碼理解的難點。
· 首先將搜尋結構圖展示出來,讓大家先知其全貌,後續在一步一步細講:(前面直接套用的函數就沒寫註釋)
compat_sys_openat
|-->do_sys_open
|-->do_filp_open
|-->do_filp_open
|-->path_openat
|-->path_init # nd初始化。
|-->link_path_walk # 真實尋找。
| |-->for(;;) {
| | |-->may_lookup # 查詢文件權限是否允許訪。
| | |-->hash = init_name_hash(); # 算出該文件名的哈希值,和文件名長度。
| | |-->## 判斷文件名是否使用了"."或者"..",來標明文件類型type ##
| | |-->d_hash # 查詢是否有哈希表存在。
| | |-->walk_component # 依據剛剛識別的類型,做單次搜索。
| | | |-->handle_dots # "." 和 ".." 文件名處理。
| | | |-->do_lookup # 其他文件的搜索。
| | | | |-->__d_lookup_rcu # 不帶rcu搜尋。
| | | | |-->__d_lookup # 帶rcu,可能引起阻塞搜尋。
| | | | |-->d_alloc_and_lookup # 上兩步搜不到,就要通過硬盤文件系統搜尋。
| | | |-->should_follow_link # 查看是否可以繼續鏈接文件,前面提到過,對鏈接次數有限制。
| | |-->nested_symlink # 限制遞歸調用不能超過8次,符號鏈接不能超過40次。
| | |-->can_lookup # 判斷是否可以繼續查找,可以則繼續。
| | |-->terminate_walk(nd); # 查找完成操作,包括解RCU鎖。
| |-->}
|-->do_last # 查找完成,做打開文件操作
1.續接前文
· 書接上文,我們找到了內核代碼位置include\asm-generic\unistd.h的如下語句:
#define __NR_openat 56
__SC_COMP(__NR_openat, sys_openat, compat_sys_openat)
· 根據這個分別找到了如下的宏定義:
#define __SC_COMP(_nr, _sys, _comp) __SYSCALL(_nr, _comp)
#define __SYSCALL(nr, call) [nr] = (call),
· 這裏我們就明白了,我們這個宏定義,將 compat_sys_openat函數的地址賦給了一個變量,然後讓swi指令去調用這個函數,我們在fs/compat.c找到了這個函數:
asmlinkage long
compat_sys_openat(unsigned int dfd, const char __user *filename, int flags, int mode)
{
return do_sys_open(dfd, filename, flags, mode);
}
· asmlinkage 表示這個是通過彙編指令鏈接過來的,do_sys_open這就是open函數的真實實現,入參分別 AT_FDCWD, file, oflag, mode,下面我們看看它做了什麼。
2.do_sys_open
· 本文將整個代碼結構都展示出來,大部分內容通過代碼中加註釋的方式來解釋,關鍵部分在代碼後用文字說明。
long do_sys_open(int dfd, const char __user *filename, int flags, int mode)
{
struct open_flags op; // 創建一個標誌集合
/* 從flags和mode中分離出lookup event bitmap,返回值lookup表示正在做的事件(尋找目錄和結構),op中存有找到之後期望做的事件(執行或創建)*/
int lookup = build_open_flags(flags, mode, &op);
char *tmp = getname(filename); // 將文件名從用戶空間複製到內核空間
int fd = PTR_ERR(tmp);
if (!IS_ERR(tmp)) {
fd = get_unused_fd_flags(flags); // 根據flags獲取一個對應類型的未使用的文件號
if (fd >= 0) {
struct file *f = do_filp_open(dfd, tmp, &op, lookup); // 執行文件打開過程
if (IS_ERR(f)) { // 判斷是否出錯
put_unused_fd(fd); // 釋放剛用get_unused_fd_flags申請的文件號
fd = PTR_ERR(f); // 將指針轉化爲錯誤碼返回
} else {
fsnotify_open(f); // 通知其他相關項,該文件已經打開
fd_install(fd, f); // 安裝文件指針到fd數組
}
}
putname(tmp); // 釋放內核態緩存
}
return fd;
}
· do_sys_open函數首先做了做了設置open_flags標誌和轉換用戶空間的文件名到內核空間,核心功能是調用do_filp_open,我們進一步尋找:
3.do_filp_open
struct file *do_filp_open(int dfd, const char *pathname,
const struct open_flags *op, int flags)
{
struct nameidata nd;
struct file *filp;
/* 核心就是path_openat,就是沿着打開文件名的整個路徑,一層層解析,最後得到文件對象 */
filp = path_openat(dfd, pathname, &nd, op, flags | LOOKUP_RCU);
if (unlikely(filp == ERR_PTR(-ECHILD)))
filp = path_openat(dfd, pathname, &nd, op, flags);
if (unlikely(filp == ERR_PTR(-ESTALE)))
filp = path_openat(dfd, pathname, &nd, op, flags | LOOKUP_REVAL);
return filp;
}
· 這裏的核心就是path_openat,就是沿着打開文件名的整個路徑,一層層解析,最後得到文件對象。但是爲什麼最多會調用3次?
· 由操作系統發展趨勢一文中講過,我們講過當前的操作系統的內存管理系統中,CPU只能間接通過將硬盤數據緩存到內存中才能使用。而用來緩存文件的區域叫dentry cache ,這是一個哈希表。
· 第一次搜索採用rcu-walk方式搜索dentry cache,這種方式不會阻塞等待(RCU纔會阻塞),更高效,但是可能會失敗(因爲文件inode本身還有順序鎖和自旋鎖的保護);
· 第二次搜索則是採用ref-walk方式搜索dentry cache,考慮rcu鎖的情形,但是可能會阻塞。
· 如果這樣仍然失敗,說明內存中沒有該文件,這樣就需要第三次,直接通過硬盤文件系統,進入硬盤慢速搜索,要帶LOOKUP_REVAL標誌。
4.path_openat
static struct file *path_openat(int dfd, const char *pathname,
struct nameidata *nd, const struct open_flags *op, int flags)
{
struct file *base = NULL;
struct file *filp;
struct path path;
int error;
filp = get_empty_filp(); // 找到一個未使用的文件結構返回
if (!filp)
return ERR_PTR(-ENFILE);
/* 初始化nd中的 intent結構的標誌 */
filp->f_flags = op->open_flag;
nd->intent.open.file = filp;
nd->intent.open.flags = open_to_namei_flags(op->open_flag);
nd->intent.open.create_mode = op->mode;
/* 初始化nd中的其他參數,通過開頭是不是"/"和dfd是否是AT_FDCWD,識別是絕對路徑還是相對路徑,設置RCU */
error = path_init(dfd, pathname, flags | LOOKUP_PARENT, nd, &base);
if (unlikely(error))
goto out_filp;
current->total_link_count = 0; // 重置鏈接的次數,後續做判斷
/* 命名解析函數,針對路徑循環搜索,直到找到最後一個不是目錄的文件,次步驟需要細講---> */
error = link_path_walk(pathname, nd);
if (unlikely(error))
goto out_filp;
/* 處理打開文件的最後操作,如果這個文件是個鏈接文件,則需要找到真實文件之後再執行do_last */
filp = do_last(nd, &path, op, pathname);
while (unlikely(!filp)) { /* trailing symlink */
struct path link = path;
void *cookie;
if (!(nd->flags & LOOKUP_FOLLOW)) {
path_put_conditional(&path, nd);
path_put(&nd->path);
filp = ERR_PTR(-ELOOP);
break;
}
nd->flags |= LOOKUP_PARENT;
nd->flags &= ~(LOOKUP_OPEN|LOOKUP_CREATE|LOOKUP_EXCL);
error = follow_link(&link, nd, &cookie);
if (unlikely(error))
filp = ERR_PTR(error);
else
filp = do_last(nd, &path, op, pathname);
put_link(nd, &link, cookie);
}
out:
if (nd->root.mnt && !(nd->flags & LOOKUP_ROOT))
path_put(&nd->root);
if (base)
fput(base);
release_open_intent(nd);
return filp;
out_filp:
filp = ERR_PTR(error);
goto out;
}
· nameidata是搜索用到的主要結構,首先要調用path_init將其初始化,然後調用link_path_walk搜尋文件,找到文件之後調用do_last來做文件打開操作,如果是個鏈接文件還需要找到真實文件,再執行do_last。
· 下面深入介紹 path_init 和 link_path_walk。
5.path_init
static int path_init(int dfd, const char *name, unsigned int flags,
struct nameidata *nd, struct file **fp)
{
int retval = 0;
int fput_needed;
struct file *file;
nd->last_type = LAST_ROOT; /* if there are only slashes... */
nd->flags = flags | LOOKUP_JUMPED;
nd->depth = 0;
/* flags中表明是從根目錄搜尋,然後查看文件權限上是否允許,如果允許,則對nd、RCU和順序鎖進行初始化後就返回 */
if (flags & LOOKUP_ROOT) {
struct inode *inode = nd->root.dentry->d_inode;
if (*name) {
if (!inode->i_op->lookup)
return -ENOTDIR;
retval = inode_permission(inode, MAY_EXEC);
if (retval)
return retval;
}
nd->path = nd->root;
nd->inode = inode;
if (flags & LOOKUP_RCU) {
br_read_lock(vfsmount_lock);
rcu_read_lock();
nd->seq = __read_seqcount_begin(&nd->path.dentry->d_seq);
} else {
path_get(&nd->path);
}
return 0;
}
nd->root.mnt = NULL;
/*通過字符的開頭是"/",也可以表示絕對路徑,則也要做相關初始化*/
if (*name=='/') {
if (flags & LOOKUP_RCU) {
br_read_lock(vfsmount_lock);
rcu_read_lock();
set_root_rcu(nd);
} else {
set_root(nd);
path_get(&nd->root);
}
nd->path = nd->root;
/*開頭不是"/",且dfd == AT_FDCWD表示使用相對路徑*/
} else if (dfd == AT_FDCWD) {
if (flags & LOOKUP_RCU) {
struct fs_struct *fs = current->fs;
unsigned seq;
br_read_lock(vfsmount_lock);
rcu_read_lock();
do {
seq = read_seqcount_begin(&fs->seq);
nd->path = fs->pwd;
nd->seq = __read_seqcount_begin(&nd->path.dentry->d_seq);
} while (read_seqcount_retry(&fs->seq, seq));
} else {
get_fs_pwd(current->fs, &nd->path);
}
/*最後一種特殊情況是依據文件號打開方式,本文不考慮此情況*/
} else {
struct dentry *dentry;
file = fget_raw_light(dfd, &fput_needed);
retval = -EBADF;
if (!file)
goto out_fail;
dentry = file->f_path.dentry;
if (*name) {
retval = -ENOTDIR;
if (!S_ISDIR(dentry->d_inode->i_mode))
goto fput_fail;
retval = inode_permission(dentry->d_inode, MAY_EXEC);
if (retval)
goto fput_fail;
}
nd->path = file->f_path;
if (flags & LOOKUP_RCU) {
if (fput_needed)
*fp = file;
nd->seq = __read_seqcount_begin(&nd->path.dentry->d_seq);
br_read_lock(vfsmount_lock);
rcu_read_lock();
} else {
path_get(&file->f_path);
fput_light(file, fput_needed);
}
}
nd->inode = nd->path.dentry->d_inode;
return 0;
fput_fail:
fput_light(file, fput_needed);
out_fail:
return retval;
}
· 這裏就是依據文件是絕對路徑(從根目錄開始搜尋)還是相對路徑(從當前目錄開始搜索)做區分,對nd、rcu和順序鎖做不同的初始化。AT_FDCWD在這裏終於用到了,就是表示是相對路徑的。
· 下面回到上一步,在看看初始化之後調用的link_path_walk做了什麼:
6.link_path_walk
static int link_path_walk(const char *name, struct nameidata *nd)
{
struct path next;
int err;
while (*name=='/') // 如果是/開頭,這需要將其+到不是"/"爲止
name++;
if (!*name) // 如果字符是無效值,則異常退出
return 0;
/* 到此,我們有了一個依據初始化的nameidata,開始搜尋循環 . */
for(;;) {
unsigned long hash;
struct qstr this;
unsigned int c;
int type;
err = may_lookup(nd); // 查詢文件權限是否允許訪問
if (err)
break;
this.name = name;
c = *(const unsigned char *)name;
/* 算出該文件名的哈希值,和文件名長度. */
hash = init_name_hash();
do {
name++;
hash = partial_name_hash(c, hash);
c = *(const unsigned char *)name;
} while (c && (c != '/'));
this.len = name - (const char *) this.name;
this.hash = end_name_hash(hash);
/* 判斷文件名是否使用了"."或者"..",是則標明type */
type = LAST_NORM;
if (this.name[0] == '.') switch (this.len) {
case 2:
if (this.name[1] == '.') {
type = LAST_DOTDOT;
nd->flags |= LOOKUP_JUMPED;
}
break;
case 1:
type = LAST_DOT;
}
/* 文件名沒有"."或者"..",則是普通文件或目錄,則查看是否有hash緩存表,有表示內存中存在緩存,沒有直接退出 */
if (likely(type == LAST_NORM)) {
struct dentry *parent = nd->path.dentry;
nd->flags &= ~LOOKUP_JUMPED;
if (unlikely(parent->d_flags & DCACHE_OP_HASH)) {
err = parent->d_op->d_hash(parent, nd->inode,
&this);
if (err < 0)
break;
}
}
/* 如果尾部是"/",則open要打開的是目錄,則直接退出 */
if (!c)
goto last_component;
while (*++name == '/'); // 當有多個"/"時,則搜索時去掉
if (!*name)
goto last_component;
/* 依據剛剛識別的類型,做不同的操作,此步驟再細說---> */
err = walk_component(nd, &next, &this, type, LOOKUP_FOLLOW);
if (err < 0)
return err;
/* 限制遞歸調用不能超過8次,符號鏈接不能超過40次 */
if (err) {
err = nested_symlink(&next, nd);
if (err)
return err;
}
/* 判斷是否可以繼續查找,可以則繼續 */
if (can_lookup(nd->inode))
continue;
err = -ENOTDIR;
break;
/* here ends the main loop */
last_component:
nd->last = this;
nd->last_type = type;
return 0;
}
terminate_walk(nd); // 查找完成操作,包括解RCU鎖
return err;
}
· 到這裏我們看到循環了,這就是逐次搜索文件名的循環。
· 循環中,首先判斷文件名有沒有".“或者”…",來設置文件類型;然後查看是否有hash表,有表示內存中存在緩存,沒有直接退出。
· 然後繼續調用walk_component做某一次的搜尋操作。
7.walk_component
static inline int walk_component(struct nameidata *nd, struct path *path,
struct qstr *name, int type, int follow)
{
struct inode *inode;
int err;
/* "." 和 ".." 符號是特殊的,".." 尤其特殊,這是因爲必須知道當前文件所在目錄的父目錄 */
if (unlikely(type != LAST_NORM))
return handle_dots(nd, type); //處理"."和".."文件名,成功則設置nd->path.dentry和nd->inode,如果父目錄是掛載點找到掛載點目錄再處理。
/* 普通文件的搜索 */
err = do_lookup(nd, name, path, &inode);
if (unlikely(err)) {
terminate_walk(nd);
return err;
}
/* 如果沒有找到,返回對應的錯誤碼 */
if (!inode) {
path_to_nameidata(path, nd);
terminate_walk(nd);
return -ENOENT;
}
/* 查看是否鏈接次數超過最大值,前面提到過,對鏈接次數有限制 */
if (should_follow_link(inode, follow)) {
if (nd->flags & LOOKUP_RCU) {
if (unlikely(unlazy_walk(nd, path->dentry))) {
terminate_walk(nd);
return -ECHILD;
}
}
BUG_ON(inode != path->dentry->d_inode);
return 1;
}
path_to_nameidata(path, nd); // 將path中的dentry放到nd->path中
nd->inode = inode;
return 0;
}
· 這裏先判斷文件名是不是".“和”. .",如果是的話就處理退出;
· 如果是普通文件,就執行do_lookup,後面繼續深入分析;
· 然後就是針對搜尋情況做一些異常判斷,請看代碼註釋。
8.do_lookup
static int do_lookup(struct nameidata *nd, struct qstr *name,
struct path *path, struct inode **inode)
{
struct vfsmount *mnt = nd->path.mnt;
struct dentry *dentry, *parent = nd->path.dentry;
int need_reval = 1;
int status = 1;
int err;
/* 文件可能存在RCU鎖,這表示可能有別的進程使用,則有可能被加載到 dcache 中 */
if (nd->flags & LOOKUP_RCU) {
unsigned seq;
*inode = nd->inode;
dentry = __d_lookup_rcu(parent, name, &seq, inode); // 搜索,只使用內存屏障作爲防護手段
if (!dentry)
goto unlazy;
/* 在子目錄的read_seqcount_begin的內存屏障就足夠,這裏判斷父目錄和子目錄的seq是否一致*/
if (__read_seqcount_retry(&parent->d_seq, nd->seq))
return -ECHILD;
nd->seq = seq;
/* 找到的目錄是否需要重新生效,需要就執行denter的對應函數*/
if (unlikely(dentry->d_flags & DCACHE_OP_REVALIDATE)) {
status = d_revalidate(dentry, nd);
if (unlikely(status <= 0)) {
if (status != -ECHILD)
need_reval = 0;
goto unlazy;
}
}
/* 找到的目錄是否需要搜索 */
if (unlikely(d_need_lookup(dentry)))
goto unlazy;
path->mnt = mnt; //將搜索到的mnt和dentry值保存,下次繼續搜索
path->dentry = dentry;
/* 判斷是否此文件是掛載點,是則需要在mount_hashtable中找掛載點,再找子目錄 */
if (unlikely(!__follow_mount_rcu(nd, path, inode)))
goto unlazy;
if (unlikely(path->dentry->d_flags & DCACHE_NEED_AUTOMOUNT))
goto unlazy;
return 0;
unlazy:
if (unlazy_walk(nd, dentry))
return -ECHILD;
} else {
dentry = __d_lookup(parent, name); // 搜索,需要使用RCU鎖,在hash表中搜索。
}
/* 如果搜到的dentry標記了要再搜一次,這種情況就是該文件是另一個文件系統的掛載點 */
if (dentry && unlikely(d_need_lookup(dentry))) {
dput(dentry);
dentry = NULL;
}
retry: // 沒有找到dentry,則需要再搜索一次
if (unlikely(!dentry)) {
struct inode *dir = parent->d_inode;
BUG_ON(nd->inode != dir);
mutex_lock(&dir->i_mutex);
dentry = d_lookup(parent, name); // 再搜索一次
if (likely(!dentry)) {
dentry = d_alloc_and_lookup(parent, name, nd);
if (IS_ERR(dentry)) {
mutex_unlock(&dir->i_mutex);
return PTR_ERR(dentry);
}
/* known good */
need_reval = 0;
status = 1;
} else if (unlikely(d_need_lookup(dentry))) {
dentry = d_inode_lookup(parent, dentry, nd);
if (IS_ERR(dentry)) {
mutex_unlock(&dir->i_mutex);
return PTR_ERR(dentry);
}
/* known good */
need_reval = 0;
status = 1;
}
mutex_unlock(&dir->i_mutex);
}
if (unlikely(dentry->d_flags & DCACHE_OP_REVALIDATE) && need_reval)
status = d_revalidate(dentry, nd);
if (unlikely(status <= 0)) {
if (status < 0) {
dput(dentry);
return status;
}
if (!d_invalidate(dentry)) {
dput(dentry);
dentry = NULL;
need_reval = 1;
goto retry;
}
}
path->mnt = mnt;
path->dentry = dentry;
err = follow_managed(path, nd->flags);
if (unlikely(err < 0)) {
path_put_conditional(path, nd);
return err;
}
if (err)
nd->flags |= LOOKUP_JUMPED;
*inode = path->dentry->d_inode;
return 0;
}
· 這裏首先會調用__d_lookup_rcu 或者 __d_lookup進入dcache中搜索看看是否存在相同文件,如果沒找到,則會調用d_lookup再搜索一遍確認一次(可能在阻塞的這段時間被添加進了dcache)。如果真的沒有,就要調用d_alloc_and_lookup 和 d_inode_lookup 使用硬盤文件系統的接口做慢速搜索。
9. dentry cache搜索函數__d_lookup_rcu
struct dentry *__d_lookup_rcu(struct dentry *parent, struct qstr *name,
unsigned *seq, struct inode **inode)
{
unsigned int len = name->len;
unsigned int hash = name->hash;
const unsigned char *str = name->name;
struct hlist_bl_head *b = d_hash(parent, hash);
struct hlist_bl_node *node;
struct dentry *dentry;
/* hash表的循環查找,實質上是for循環,要經過多次的比較,如果某項不同就跳過,繼續搜索*/
hlist_bl_for_each_entry_rcu(dentry, node, b, d_hash) {
struct inode *i;
const char *tname;
int tlen;
if (dentry->d_name.hash != hash)
continue;
seqretry:
*seq = read_seqcount_begin(&dentry->d_seq);
if (dentry->d_parent != parent)
continue;
if (d_unhashed(dentry))
continue;
tlen = dentry->d_name.len;
tname = dentry->d_name.name;
i = dentry->d_inode;
prefetch(tname);
if (read_seqcount_retry(&dentry->d_seq, *seq))
goto seqretry;
if (unlikely(parent->d_flags & DCACHE_OP_COMPARE)) {
if (parent->d_op->d_compare(parent, *inode,
dentry, i,
tlen, tname, name))
continue;
} else {
if (dentry_cmp(tname, tlen, str, len))
continue;
}
*inode = i;
return dentry;
}
return NULL;
}
· 這裏就是dcache中的搜索函數,dcache hash表採用for循環,每一項要經過多次的比較,如果某項不同就跳過,繼續搜索,直到找到全部符合的文件,否者就是沒找到。搜不到這不代表這個文件就不在dcache中,因爲條件中有順序鎖seq的判斷,在SMP的多核架構中可能會出現這種情況,這就需要回到第3步的do_filp_open中繼續第二次的path_openat,這是就會調用__d_lookup函數,各位可以自行看看,內容也就增加了rcu和自旋鎖的限制,這會引起阻塞。
10.硬盤搜索函數d_alloc_and_lookup
static struct dentry *d_alloc_and_lookup(struct dentry *parent,
struct qstr *name, struct nameidata *nd)
{
struct inode *inode = parent->d_inode;
struct dentry *dentry;
struct dentry *old;
/* Don't create child dentry for a dead directory. */
if (unlikely(IS_DEADDIR(inode)))
return ERR_PTR(-ENOENT);
/* 這裏首先創建一個dentry對象 */
dentry = d_alloc(parent, name);
if (unlikely(!dentry))
return ERR_PTR(-ENOMEM);
/* 然後調用文件系統的lookup函數搜索 */
old = inode->i_op->lookup(inode, dentry, nd);
if (unlikely(old)) {
dput(dentry);
dentry = old;
}
return dentry;
}
· 到這裏就比較簡單了,先分配一個dentry對象,然後調用硬盤文件系統的lookup接口去搜索。
11.總結
· 回過頭來,我們最好再看看這個調用過程,核心就在最後。
· 經過這次的分析,我們實際上深入了虛擬文件系統的核心部分,知道了內核是如何搜尋一個文件的。爽得很!!!!!!!!!!(這麼多感嘆號,是爲了發泄這兩週對我的折磨)
· 但是爽歸爽,其實中間有些地方一帶而過了,其實我們中間忽略了VFS的一些基礎性的架構介紹,需要把這條走過的路鞏固一下,請見下篇《文件系統原理 和 VFS架構》(一股 百家講壇 的感覺 ~~~)。
------------------如果有所收穫的話,請幫忙點個贊吧!
下一篇:https://blog.csdn.net/weixin_42523774/article/details/103739139