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

Linux open系統呼叫(一)

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

1、函式原型

int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);

引數說明:
pathname: 表示要開啟的檔案路徑,可以是絕對路徑也可以是相對路徑;

flags: 表示開啟檔案所採用的操作,用於控制可讀、可寫、建立、截斷等

	注:以下模式有且只能指定其中一個
	O_RDONLY:只讀模式開啟
	O_WRONLY:只寫模式開啟
	O_RDWR:可讀可寫開啟

指定讀寫模式後還能位與一些其他控制標誌,來控制開啟的操作。

	O_APPEND:表示追加內容至檔案末尾
	O_CREAT:表示如果指定檔案不存在,則建立這個檔案
	O_TRUNC:表示截斷,如果檔案存在,則將其長度截斷為0
	O_NONBLOCK:表示將 I/O設定為非阻塞模式(nonblocking mode)

以上為常用選項,具體可參考man page使用者手冊。

mode: 表示設定檔案訪問許可權的初始值,可按位與,S_IRWXU、S_IRUSR、S_IWUSR等選項,實際檔案許可權還受umask值影響。

總的來說,open()函式所做的事情就是將傳進去的字串的路徑在核心裡面轉換成相應的inode節點和dentry結構體。執行這一任務的標準過程就是分析路徑名並把它拆分成一個檔名序列,除了最後一個檔名以外,所有的檔名都必定是目錄。

2、核心實現

SYSCALL_DEFINE3(open, const char __user *, filename, int, flags, umode_t, mode)
{	
	//對於64位系統會新增O_LARGEFILE選項,以便能開啟大檔案
	if (force_o_largefile())
		flags |= O_LARGEFILE;
	//第一個引數為AT_FDCWD,表示檔名是以當前路徑作為起始目錄的路徑
	return do_sys_open(AT_FDCWD, filename, flags, mode);
}

long do_sys_open(int dfd, const char __user *filename, int flags, umode_t mode)
{
	struct open_flags op;
	//根據入參構建檔案開啟標誌,比如追加,不存在時建立等標誌
	int lookup = build_open_flags(flags, mode, &op);
	//構建filename結構體,將使用者給的檔名拷貝至核心,
	struct filename *tmp = getname(filename);
	int fd = PTR_ERR(tmp);

	if (!IS_ERR(tmp)) {
		//獲取一個未使用的檔案描述符
		fd = get_unused_fd_flags(flags);
		if (fd >= 0) {
			//重頭戲,開啟檔案的真正操作在這
			struct file *f = do_filp_open(dfd, tmp, &op, lookup);
			if (IS_ERR(f)) {
				//開啟檔案出現錯誤,回收檔案描述符
				put_unused_fd(fd);
				fd = PTR_ERR(f);
			} else {
				//產生notity事件,用於檔案監控
				fsnotify_open(f);
				//將file結構體放入以fd為索引下標的陣列中,讓file和fd相關聯
				fd_install(fd, f);
			}
		}
		putname(tmp);
	}
	return fd;//返回可用的檔案描述符
}

可見,open系統呼叫在程式碼結構上顯得和簡潔直觀,獲取檔案描述符fd->構建file結構體並和需要開啟的檔案關聯->關聯fd和file結構->返回檔案描述符fd。這一流程的重點在於構建file結構體並和需要開啟的檔案關聯,因為這涉及了檔案系統相關操作,正因如此,我們才要來細細分析。

struct file *do_filp_open(int dfd, struct filename *pathname,
		const struct open_flags *op, int flags)
{
	struct nameidata nd;
	struct file *filp;
	//主角是path_openat,三種情況不同的是flags的值
	//核心為了提高效率,會首先在RCU模式(rcu-walk)下進行檔案開啟操作
	//如果在此方式下開啟失敗,則進入普通模式(ref-walk)
	//第三次呼叫比較少用,目前只有在nfs檔案系統才有可能會被使用
	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()之前,我們先來看看struct nameidata這個結構體,它用於儲存本次查詢的結果,因此在逐級查詢目錄項的過程中不斷變化,是一箇中間量。不過定義的nd這個變數最終目的就是儲存使用者所要開啟檔案的最後一個目錄項的資訊。

struct nameidata {
	struct path	path; //當前目錄項
	struct qstr	last; //當前目錄項的名稱及雜湊值
	struct path	root; //根目錄
	struct inode	*inode; //當前目錄項對應的inode,取值來自於path.dentry.d_inode
	unsigned int	flags;
	unsigned	seq;
	int		last_type; //當前目錄項的型別
	unsigned	depth; //符號連結當前的巢狀深度,最大為MAX_NESTED_LINKS(8)
	char *saved_names[MAX_NESTED_LINKS + 1]; //符號連結每個巢狀層級的名稱
	RH_KABI_EXTEND(unsigned  m_seq)
};

其中目錄項的型別有以下幾種,

LAST_NORM:最後一個分量是普通檔名
LAST_ROOT:最後一個分量是“/”
LAST_DOT:最後一個分量是“.”
LAST_DOTDOT:最後一個分量是“..”
LAST_BIND:最後一個分量是符號連結

瞭解了struct nameidata,那我們回過頭來看path_openat函式。

static struct file *path_openat(int dfd, struct filename *pathname,
		struct nameidata *nd, const struct open_flags *op, int flags)
{
	struct file *base = NULL;
	struct file *file;
	struct path path;
	int opened = 0;
	int error;
	//從快取中獲取一個file結構體
	file = get_empty_filp();
	if (IS_ERR(file))
		return file;

	file->f_flags = op->open_flag;//獲取open時的選項
	//初始化起始路徑,即nd->path
	error = path_init(dfd, pathname->name, flags | LOOKUP_PARENT, nd, &base);
	if (unlikely(error))
		goto out;
	//total_link_count用於記錄符號連結的深度,每追蹤一次符號連結,該值就加一
	//目前系統最大允許追蹤40層符號連結
	current->total_link_count = 0;
	//開始遍歷目錄
	error = link_path_walk(pathname->name, nd);
	......
	return file;
}

在逐級查詢目錄項之前,首先得確定起始目錄,根據使用者傳入的引數,檔案路徑可能是絕對路徑,也可能是相對路徑,因此需要先通過path_init函式處理,說白了也就是設定nd->path的值。

static int path_init(int dfd, const char *name, unsigned int flags,
		     struct nameidata *nd, struct file **fp)
{
	int retval = 0;

	nd->last_type = LAST_ROOT; /* if there are only slashes... */
	nd->flags = flags | LOOKUP_JUMPED;
	nd->depth = 0;//跟蹤符號連結的遞迴深度
	//如果flags設定了LOOKUP_ROOT標誌,則表示該函式被open_by_handle_at或sysctl函式呼叫
	//該函式將指定一個路徑作為根,此處暫不分析
	if (flags & LOOKUP_ROOT) {
		...
	}

	nd->root.mnt = NULL;

	nd->m_seq = read_seqbegin(&mount_lock);
	if (*name=='/') {
		//檔名以/開頭,說明是絕對路徑,不關注dfd的值
		if (flags & LOOKUP_RCU) {
			rcu_read_lock();
			set_root_rcu(nd);//設定nd->root為根檔案系統
		} else {
			set_root(nd);
			path_get(&nd->root);
		}
		nd->path = nd->root;//設定起始遍歷路徑nd->path為根檔案系統
	} else if (dfd == AT_FDCWD) {
		//dfd為AT_FDCWD,那麼這個相對路徑是以當前路徑pwd作為起始的
		if (flags & LOOKUP_RCU) {//rcu-walk遍歷
			struct fs_struct *fs = current->fs;
			unsigned seq;

			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 {
		//dfd不是AT_FDCWD,那麼這個相對路徑是使用者設定的
		//需要通過dfd獲取具體相對路徑資訊
		struct fd f = fdget_raw(dfd);
		struct dentry *dentry;

		if (!f.file)
			return -EBADF;

		dentry = f.file->f_path.dentry;

		if (*name) {
			if (!d_can_lookup(dentry)) {
				fdput(f);
				return -ENOTDIR;
			}
		}

		nd->path = f.file->f_path;//獲取到路徑
		if (flags & LOOKUP_RCU) {
			if (f.flags & FDPUT_FPUT)
				*fp = f.file;
			nd->seq = __read_seqcount_begin(&nd->path.dentry->d_seq);
			rcu_read_lock();
		} else {
			path_get(&nd->path);
			fdput(f);
		}
	}
	//當前目錄項對應的inode
	nd->inode = nd->path.dentry->d_inode;
	return 0;
}

設定好遍歷的起始目錄後,就可以開始真正的遍歷目錄了,我們下篇文章再繼續分析。