1. 程式人生 > >Linux open系統呼叫(二)

Linux open系統呼叫(二)

注:本文分析基於3.10.0-693.el7核心版本,即CentOS 7.4

上回說到根據使用者給的路徑,通過path_init函式設定起始目錄nd->path,那接下來就要遍歷目錄了,我們從link_path_walk函式開始分析。

static int link_path_walk(const char *name, struct nameidata *nd)
{
	struct path next;
	int err;
	
	while (*name=='/')
		name++;//過濾前導"/",即使ls /////home 正常執行
	if (!*name)//只有根目錄,沒有子目錄,直接返回
		return 0;

	/* At this point we know we have a real path component. */
	//開始遍歷子目錄
	for(;;) {
		struct qstr this;
		long len;
		int type;

		err = may_lookup(nd);//許可權校驗
 		if (err)
			break;
		//計算路徑名的雜湊值,並返回路徑長度
		len = hash_name(name, &this.hash);
		this.name = name;
		this.len = len;

		type = LAST_NORM;
		//處理路徑為.和..的情況,設定相應標記
		if (name[0] == '.') switch (len) {
			case 2:
				if (name[1] == '.') {
					type = LAST_DOTDOT;//..的情況
					nd->flags |= LOOKUP_JUMPED;
				}
				break;
			case 1:
				type = LAST_DOT;
		}
		//如果是普通路徑名
		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, &this);
				if (err < 0)
					break;
			}
		}
		//更新子路徑名
		nd->last = this;
		nd->last_type = type;

		if (!name[len])
			return 0;
		/*
		 * If it wasn't NUL, we know it was '/'. Skip that
		 * slash, and continue until no more slashes.
		 */
		do {
			len++;
		} while (unlikely(name[len] == '/'));//過濾掉中間連續的/
		if (!name[len])
			return 0;

		name += len;
		//開始處理子路徑
		err = walk_component(nd, &next, LOOKUP_FOLLOW);
		if (err < 0)
			return err;

		if (err) {
			err = nested_symlink(&next, nd);
			if (err)
				return err;
		}
		if (!d_can_lookup(nd->path.dentry)) {
			err = -ENOTDIR; 
			break;
		}
	}
	terminate_walk(nd);
	return err;
}

處理好“.”和“..”的情況,同時過濾掉多餘的/之後,此時路徑名肯定就是一個目錄或者連結了。這是就要進入walk_component函式處理這些子路徑。

static inline int walk_component(struct nameidata *nd, struct path *path,
		int follow)
{
	struct inode *inode;
	int err;
	/*
	 * "." and ".." are special - ".." especially so because it has
	 * to be able to know about the current root directory and
	 * parent relationships.
	 */
	if (unlikely(nd->last_type != LAST_NORM))//處理.和..的情況
		return handle_dots(nd, nd->last_type);
	...
}

對於這個子路徑,有三種情況,分別是“.”和“..” ,普通目錄以及符號連結。我們先看看“.”和“..”的處理。

static inline int handle_dots(struct nameidata *nd, int type)
{
	//..——上級目錄
	if (type == LAST_DOTDOT) {
		if (nd->flags & LOOKUP_RCU) {
			//處理..,往上一級目錄查詢
			if (follow_dotdot_rcu(nd))
				return -ECHILD;
		} else
			return follow_dotdot(nd);
	}
	//如果是.,那就是當前目錄,不需要處理
	return 0;
}

平時我們認為“..”很簡單,就是上一級目錄而已,但是在核心中並沒有表現出來的這麼簡單。因為往上一級就有可能走到另一個檔案系統中,而且由於涉及到掛載的問題,處理起來還是略顯複雜的。

static int follow_dotdot_rcu(struct nameidata *nd)
{
	set_root_rcu(nd);//設定nd->root為根檔案系統

	while (1) {
		//如果當前目錄已經是預設的根目錄,那到頂了,直接返回
		if (nd->path.dentry == nd->root.dentry && nd->path.mnt == nd->root.mnt) {
			break;
		}
		//如果當前目錄不是預設的根目錄,且不是當前檔案系統的根目錄,那就向上走一級
		if (nd->path.dentry != nd->path.mnt->mnt_root) {
			struct dentry *old = nd->path.dentry;
			struct dentry *parent = old->d_parent;//獲取父目錄
			unsigned seq;

			seq = read_seqcount_begin(&parent->d_seq);
			if (read_seqcount_retry(&old->d_seq, nd->seq))
				goto failed;
			nd->path.dentry = parent;//nd跨越到上一級目錄
			nd->seq = seq;
			if (unlikely(!path_connected(&nd->path)))
				goto failed;
			break;
		}
		//判斷父mount結構是否在另一個檔案系統中,返回0表示在同一個檔案系統中
		//在不同檔案系統時,需要一直往上走,因為可能是多個檔案系統掛載同一個目錄
		if (!follow_up_rcu(&nd->path))
			break;
		nd->seq = read_seqcount_begin(&nd->path.dentry->d_seq);
	}
	//如果此時找到的父目錄也是一個掛載點,需要往上繼續找(核心空間)
	//雖然從路徑名上我們往上一層就可以了,但是在核心裡,
	//當前這個路徑名同樣可能是經過多次掛載呈現出來的,因此需要找到最新的那個掛載點
	while (d_mountpoint(nd->path.dentry)) {
		struct mount *mounted;
		//在散列表裡查詢對應的掛載點
		mounted = __lookup_mnt(nd->path.mnt, nd->path.dentry);
		if (!mounted)
			break;//找到的目錄不是掛載點就可以退出了
		//找到的目錄仍然是掛載點,需要繼續找
		//因為有可能多個檔案系統掛載到同一個目錄,因此需要在連結串列中找到最新的那個目錄
		nd->path.mnt = &mounted->mnt;
		nd->path.dentry = mounted->mnt.mnt_root;
		nd->seq = read_seqcount_begin(&nd->path.dentry->d_seq);
		if (read_seqretry(&mount_lock, nd->m_seq))
			goto failed;
	}
	nd->inode = nd->path.dentry->d_inode;//更新inode
	return 0;

failed:
	nd->flags &= ~LOOKUP_RCU;
	if (!(nd->flags & LOOKUP_ROOT))
		nd->root.mnt = NULL;
	rcu_read_unlock();
	return -ECHILD;//返回使用ref-walk方式查詢
}

可見,如果當前目錄不是根目錄,也不是當前檔案系統的根目錄,也就是誰就是簡簡單單的一個普通目錄,那處理“..”也就是獲取父目錄的索引而已。

但是如果是該目錄掛載了多個檔案系統,那麼跨越到父目錄的時候需要先找到最初掛載的目錄結構,否則獲取到的檔案系統狀態仍然是該目錄層級。

static int follow_up_rcu(struct path *path)
{
	struct mount *mnt = real_mount(path->mnt);
	struct mount *parent;
	struct dentry *mountpoint;
	//獲取上一級mount結構
	parent = mnt->mnt_parent;
	//當前檔案系統的掛載點就是自己,即跨越到根檔案系統(rootfs)的時候
	//在根檔案系統裡,其上一級mount結構指向的就是自己,因此他們的vfsmount結構相同
	if (&parent->mnt == path->mnt)
		return 0;
	//走到這說明上一級mount結構是在另一個檔案系統中
	mountpoint = mnt->mnt_mountpoint;
	path->dentry = mountpoint;//更新掛載點,掛載點的本質也是目錄
	path->mnt = &parent->mnt;//更新vfsmount結構,也就跨越到上一級目錄,完成..的操作
	return 1;
}

如果當前檔案系統的掛載點就是自己,即跨越到根檔案系統(rootfs)的時候。此時follow_up_rcu函式將返回0,之後退出while(1)迴圈。

我們舉個例子說明這種情況,考慮以下場景,當前目錄pwd=/home/a/,檔案路徑path=…/log.txt(即絕對路徑為/home/log.txt),其中目錄home和a都是普通目錄,沒有掛載任何檔案系統。因此我們可以知道此時傳入follow_up_rcu函式的入參nd->path->mnt指向的是根檔案系統(rootfs),因此其上一級mount結構指向的還是自己,follow_up_rcu函式返回0,退出while(1)迴圈。

而需要while(1)迴圈則是因為某個目錄可能被重複掛載多個檔案系統。考慮另一種場景,當前目錄pwd=/home/a/,檔案路徑path=…/log.txt(即絕對路徑為/home/log.txt)。但是在此之前先將某個分割槽,如/dev/sda1,檔案系統為fs1,掛載至/home目錄,原先/home目錄下的檔案將被隱藏。然後再將另一個分割槽,如/dev/sda2,檔案系統為fs2,掛載至/home目錄,此時在/home目錄下再建立目錄a和檔案log.txt,形成開始時的場景。這個時候因為/home目錄被重複掛載,因此在a目錄訪問上級目錄下的log.txt檔案,我們需要一個迴圈體來順著mount結構的連結串列從fs2->fs1找到最初的檔案系統。

如果退出迴圈體,至此已經獲取到了當前目錄項的上一級目錄項(即“…”所代表的父目錄項)。

接下來又是一個while迴圈,這是考慮到這個父目錄項有可能也是一個掛載點,也可能被重複掛載,所以要獲取到最新的那個掛載系統。通過__lookup_mnt()檢查父目錄下掛載的檔案系統是否為最新的檔案系統,如果是則檢查結束;否則,將繼續檢查;

走完上述流程,我們也就處理完“.”和“…”的情況,接下來我們來看下如果是普通目錄的情況,這就是下次要說的了。