linux文件系统-文件的打开与关闭

用户进程在能够读写一个文件之前必须先要打开这个文件。对文件的读写从概念上说是一种进程与文件系统之间的一种有连接通信,所谓打开文件实质上就是在进程与文件之间建立起连接,而打开文件号就唯一地标识着这样一个连接。不过,严格意义上的连接意味着一个独立的上下文,如果一个进程与某个目标之间重复建立起多个连接,则每个连接都应该是互相独立的。在文件系统的处理中,每当一个进程重复打开同一个文件时就建立起一个由file数据结构代表的独立的上下文。通常,一个file数据结构,即一个读写文件的上下文,都由一个打开文件号加以标识,但是通过系统调用dup或dup2却可以使同一个file结构对应到多个打开文件号。

打开文件的系统调用时open,在内核中通过sys_open实现,代码如下:


asmlinkage long sys_open(const char * filename, int flags, int mode)
{
	char * tmp;
	int fd, error;

#if BITS_PER_LONG != 32
	flags |= O_LARGEFILE;
#endif
	tmp = getname(filename);
	fd = PTR_ERR(tmp);
	if (!IS_ERR(tmp)) {
		fd = get_unused_fd();
		if (fd >= 0) {
			struct file *f = filp_open(tmp, flags, mode);
			error = PTR_ERR(f);
			if (IS_ERR(f))
				goto out_error;
			fd_install(fd, f);
		}
out:
		putname(tmp);
	}
	return fd;

out_error:
	put_unused_fd(fd);
	fd = error;
	goto out;
}

调用参数filename实际上是文件的路径名(绝对路径名或相对路径名);mode表示打开的模式,如只读等等;而flag则包含了许多标识位,用以表示打开模式以外的一些属性和要求。函数通过getname从用户空间把文件的路径名拷贝到系统空间,并通过get_unused_fd从当前进程的打开文件表中找到一个空闲的表项,该表项的下标则为打开文件号。然后,根据文件名通过filp_open找到或者创建一个连接,或者说读写该文件的上下文。文件读写的上下文是由file数据结构代表和描绘的,其定义如下:


struct file {
	struct list_head	f_list;
	struct dentry		*f_dentry;
	struct vfsmount         *f_vfsmnt;
	struct file_operations	*f_op;
	atomic_t		f_count;
	unsigned int 		f_flags;
	mode_t			f_mode;
	loff_t			f_pos;
	unsigned long 		f_reada, f_ramax, f_raend, f_ralen, f_rawin;
	struct fown_struct	f_owner;
	unsigned int		f_uid, f_gid;
	int			f_error;

	unsigned long		f_version;

	/* needed for tty driver, and maybe others */
	void			*private_data;
};

数据结构中不但有指向文件的dentry结构的指针f_dentry,有指向将文件所在的设备安装在文件系统中的vfsmount结构的指针,有共享计数f_count,还有一个在文件中的当前读写的位置f_pos,这就是上下文。找到或者创建了代表着目标文件的file结构以后,就通过fd_install将指向这个结构的指针填入当前进程的打开文件表,即由其task_struct结构中的指针files所指向的files_struct数组中并返回其数组中的下标。

函数get_unused_fd的代码如下:

sys_open=>get_unused_fd


/*
 * Find an empty file descriptor entry, and mark it busy.
 */
int get_unused_fd(void)
{
	struct files_struct * files = current->files;
	int fd, error;

  	error = -EMFILE;
	write_lock(&files->file_lock);

repeat:
 	fd = find_next_zero_bit(files->open_fds, 
				files->max_fdset, 
				files->next_fd);

	/*
	 * N.B. For clone tasks sharing a files structure, this test
	 * will limit the total number of files that can be opened.
	 */
	if (fd >= current->rlim[RLIMIT_NOFILE].rlim_cur)
		goto out;

	/* Do we need to expand the fdset array? */
	if (fd >= files->max_fdset) {
		error = expand_fdset(files, fd);
		if (!error) {
			error = -EMFILE;
			goto repeat;
		}
		goto out;
	}
	
	/* 
	 * Check whether we need to expand the fd array.
	 */
	if (fd >= files->max_fds) {
		error = expand_fd_array(files, fd);
		if (!error) {
			error = -EMFILE;
			goto repeat;
		}
		goto out;
	}

	FD_SET(fd, files->open_fds);
	FD_CLR(fd, files->close_on_exec);
	files->next_fd = fd + 1;
#if 1
	/* Sanity check */
	if (files->fd[fd] != NULL) {
		printk("get_unused_fd: slot %d not NULL!\n", fd);
		files->fd[fd] = NULL;
	}
#endif
	error = fd;

out:
	write_unlock(&files->file_lock);
	return error;
}

进程的task_struct结构中有个指针files,指向本进程的files_struct数据结构。与打开文件有关的信息都保存在这个数据结构中,其定义如下:


/*
 * Open file table structure
 */
struct files_struct {
	atomic_t count;
	rwlock_t file_lock;
	int max_fds;
	int max_fdset;
	int next_fd;
	struct file ** fd;	/* current fd array */
	fd_set *close_on_exec;
	fd_set *open_fds;
	fd_set close_on_exec_init;
	fd_set open_fds_init;
	struct file * fd_array[NR_OPEN_DEFAULT];
};

这个数据结构中最主要的成分是一个file结构指针数组fd_array,这个数组的大小是固定的,即32,其下标即为打开文件号。另外,结构中还有个指针fd,最初指向fd_array,结构中还有两个位图close_on_exec_init和open_fds_init,这些位图大致对应着file结构指针数组的内容,但是比fd_array的大小要大得多。同时,又有两个指针close_on_exec和open_fds,最初时分别指向上述两个位图。每次打开文件分配一个打开文件号时就将由open_fds所指向位图中的相应位设成1.此外,该数据结构中还有两个参数max_fds和max_fdset,分别反映着当前file结构指针数组与位图容量。一个进程可以有多少个已打开文件取决于该进程的task_struct结构中关于可用资源的限制(见上面代码中的701行)。在这个限制以内,如果超出了其file结构指针数组的容量,就通过expand_fd_array扩充该数组的容量,并让指针fd指向新的数组;如果超出了位图的容量就通过expand_fdset扩充两个位图的容量,并使两个指针也分别指向新的位图。这样,就克服了早期Unix因只采用固定大小的file结构指针数组而使每个进程可以同时打开文件数量受到限制的缺陷。

打开文件时,更确切的说是分配空闲打开文件号时,通过宏定义FD_SET将open_fds所指向的位图中的相应位设成1,表示这个打开文件号已不再空闲,这个位图代表着已经在使用中的打开文件号。同时,还通过FD_CLR将由指针close_on_exec所指向的位图中的相应位清零,表示如果当前进程通过exec系统调用执行一个可执行文件的话无需将这个文件关闭。这个位图的内容可以通过ioctl系统调用来设置。

动态地调整可同时打开的文件数量对于目前的环境具有重要意义,因为在这些环境下常常要求同时打开数量众多(但是每个文件很小)的文件。

显然,sys_open的主体是filp_open,其代码入下:

sys_open=>filp_open


/*
 * Note that while the flag value (low two bits) for sys_open means:
 *	00 - read-only
 *	01 - write-only
 *	10 - read-write
 *	11 - special
 * it is changed into
 *	00 - no permissions needed
 *	01 - read-permission
 *	10 - write-permission
 *	11 - read-write
 * for the internal routines (ie open_namei()/follow_link() etc). 00 is
 * used by symlinks.
 */
struct file *filp_open(const char * filename, int flags, int mode)
{
	int namei_flags, error;
	struct nameidata nd;

	namei_flags = flags;
	if ((namei_flags+1) & O_ACCMODE)
		namei_flags++;
	if (namei_flags & O_TRUNC)
		namei_flags |= 2;

	error = open_namei(filename, namei_flags, mode, &nd);
	if (!error)
		return dentry_open(nd.dentry, nd.mnt, flags);

	return ERR_PTR(error);
}

这里的参数flags就是系统调用open传下来的,它遵循open界面上对flags的约定,但是这里调用的open_namei却对这些标志位有不同的约定(见600行至613行中的注释),所以要在调用open_namei前先加以转换,对于i386处理器,用于open界面上的标志位定义如下:


/* open/fcntl - O_SYNC is only implemented on blocks devices and on files
   located on an ext2 file system */
#define O_ACCMODE	   0003
#define O_RDONLY	     00
#define O_WRONLY	     01
#define O_RDWR		     02
#define O_CREAT		   0100	/* not fcntl */
#define O_EXCL		   0200	/* not fcntl */
#define O_NOCTTY	   0400	/* not fcntl */
#define O_TRUNC		  01000	/* not fcntl */
#define O_APPEND	  02000
#define O_NONBLOCK	  04000
#define O_NDELAY	O_NONBLOCK
#define O_SYNC		 010000
#define FASYNC		 020000	/* fcntl, for BSD compatibility */
#define O_DIRECT	 040000	/* direct disk access hint - currently ignored */
#define O_LARGEFILE	0100000
#define O_DIRECTORY	0200000	/* must be a directory */
#define O_NOFOLLOW	0400000 /* don't follow links */

对于flags中最低两位所在的变换是620-621行完成的,具体的变换如下:

00 表示无写要求,也就是只读 变换成:01,表示要求读访问权限。

01 表示只写, 变换成:10,表示要求写访问权。

10 表示读和写,变换成:11,表示读和写访问权限。

11 特殊(O_RDONLY|O_WRONLY)变换成:11,表示要求读和写权限。

此外,如果O_TRUNC标志位为1(表示要求截尾)则意味着要求写访问权限。这些代码确实是很精炼的。

下面就是调用open_namei,其代码很长,我们分段来看:

sys_open=>filp_open=>open_namei

/*
 *	open_namei()
 *
 * namei for open - this is in fact almost the whole open-routine.
 *
 * Note that the low bits of "flag" aren't the same as in the open
 * system call - they are 00 - no permissions needed
 *			  01 - read permission needed
 *			  10 - write permission needed
 *			  11 - read/write permissions needed
 * which is a lot more logical, and also allows the "no perm" needed
 * for symlinks (where the permissions are checked later).
 * SMP-safe
 */
int open_namei(const char * pathname, int flag, int mode, struct nameidata *nd)
{
	int acc_mode, error = 0;
	struct inode *inode;
	struct dentry *dentry;
	struct dentry *dir;
	int count = 0;

	acc_mode = ACC_MODE(flag);

	/*
	 * The simplest case - just a plain lookup.
	 */
	if (!(flag & O_CREAT)) {
		if (path_init(pathname, lookup_flags(flag), nd))
			error = path_walk(pathname, nd);
		if (error)
			return error;
		dentry = nd->dentry;
		goto ok;
	}

	/*
	 * Create - we need to know the parent.
	 */
	if (path_init(pathname, LOOKUP_PARENT, nd))
		error = path_walk(pathname, nd);
	if (error)
		return error;

	/*
	 * We have the parent and last component. First of all, check
	 * that we are not asked to creat(2) an obvious directory - that
	 * will not do.
	 */
	error = -EISDIR;
	if (nd->last_type != LAST_NORM || nd->last.name[nd->last.len])
		goto exit

调用参数flag中的O_CREAT标志位表示如果要打开的文件不存在就创建这个文件。所以,如果这个标志位为0,就仅仅是在文件系统中寻找目标节点,这就是通过path_init和path_walk根据目标节点的路径名找到该节点的dentry结构(以及inode结构)的过程,那已经在前面介绍过了。这里唯一值得一提的是在调用path_init时的参数flag还要通过lookup_flags进行一些处理,其代码如下:

sys_open=>filp_open=>open_namei=>lookup_flags


/* 
 * Special case: O_CREAT|O_EXCL implies O_NOFOLLOW for security
 * reasons.
 *
 * O_DIRECTORY translates into forcing a directory lookup.
 */
static inline int lookup_flags(unsigned int f)
{
	unsigned long retval = LOOKUP_FOLLOW;

	if (f & O_NOFOLLOW)
		retval &= ~LOOKUP_FOLLOW;
	
	if ((f & (O_CREAT|O_EXCL)) == (O_CREAT|O_EXCL))
		retval &= ~LOOKUP_FOLLOW;
	
	if (f & O_DIRECTORY)
		retval |= LOOKUP_DIRECTORY;

	return retval;
}

可见,这里为上面953行的path_init设置了其参数flags中反映着搜索准则的标志位,但是最多只有LOOKUP_DIRECTORY或者LOOKUP_FOLLOW两位有可能为1.另一方面,在open_namei中对这些标志位的设置,即对lookup_flags的调用,是有条件的,仅在原先的参数flag中O_CREAT标志为0时才进行,所以我们知道这里889行中的条件一定是不成立的。如果O_CREAT标志位为0,那么要是找不到目标节点就失败返回(而不是创建这个节点)。

找到了目标节点的dentry结构以后,还要对其进行很多检查,等一下我们再回到这个话题。

如果O_CREAT标志位为1,那就要复杂多了。首先也是通过path_init和path_walk沿着路径搜索,不过这一次寻找的不是目标节点的本身,而是其父节点,也就是目标文件所在的目录,所以在调用path_init时的标志位为LOOKUP_PARENT。如果在搜索的过程中出了错,例如某个中间节点不存在或者不允许当前进程访问,那就出错返回了。否则,那就是找到了这个父节点。但是,找到了父节点并不表示整个路径就没有问题了。在正常的路径名中,路径的终点是一个文件名,此时nameidata结构中的last_type由path_walk设置成LAST_NORM。但是,也有可能路径名的终点为'.',"..",也就是说路径的终点实际上是一个目录,此时path_walk将last_type设置成LAST_DOT、LAST_DOTDOT(见linux文件系统--从路径名到目录节点博客),那就应该视为出错而返回出错代码-EISDIR。这是为什么呢?因为O_CREAT标志位为1表示若目录节点不存在就创建该节点,可是open只能创建文件而不能创建目录,目录要由另一个系统调用mkdir来创建。同时,目标节点必须是以'\0'结尾的,那才是个正常的文件名。否则说明在目标节点名后面还有个作为分隔符的'/'字符,那么这还是个目录节点。注意虽然寻找的是目标节点名的父节点,但是path_walk将nameidata结构中的qstr结构last设置成含有目标节点的节点名字符串长度,只不过没有去寻找目标节点的dentry结构(以及inode结构)。可以回过去重温下path_walk的代码。

通过了这些检查,才说明真的找的了目标文件所在的目录的dentry结构,可以往下执行了。继续往下看代码:

sys_open=>filp_open=>open_namei

	dir = nd->dentry;
	down(&dir->d_inode->i_sem);
	dentry = lookup_hash(&nd->last, nd->dentry);

do_last:
	error = PTR_ERR(dentry);
	if (IS_ERR(dentry)) {
		up(&dir->d_inode->i_sem);
		goto exit;
	}

	/* Negative dentry, just create the file */
	if (!dentry->d_inode) {
		error = vfs_create(dir->d_inode, dentry, mode);
		up(&dir->d_inode->i_sem);
		dput(nd->dentry);
		nd->dentry = dentry;
		if (error)
			goto exit;
		/* Don't check for write permission, don't truncate */
		acc_mode = 0;
		flag &= ~O_TRUNC;
		goto ok;
	}

	/*
	 * It already exists.
	 */
	up(&dir->d_inode->i_sem);

	error = -EEXIST;
	if (flag & O_EXCL)
		goto exit_dput;

	if (d_mountpoint(dentry)) {
		error = -ELOOP;
		if (flag & O_NOFOLLOW)
			goto exit_dput;
		do __follow_down(&nd->mnt,&dentry); while(d_mountpoint(dentry));
	}
	error = -ENOENT;
	if (!dentry->d_inode)
		goto exit_dput;
	if (dentry->d_inode->i_op && dentry->d_inode->i_op->follow_link)
		goto do_link;

	dput(nd->dentry);
	nd->dentry = dentry;
	error = -EISDIR;
	if (dentry->d_inode && S_ISDIR(dentry->d_inode->i_mode))
		goto exit;
ok:

我们已经找到了目标文件所在的目录的dentry结构,并让指针dir指向这个数据结构,下一步就是通过lookup_hash寻找目标文件的dentry结构(980行)。函数lookup_hash的代码如下:

sys_open=>filp_open=>open_namei=>lookup_hash


/*
 * Restricted form of lookup. Doesn't follow links, single-component only,
 * needs parent already locked. Doesn't follow mounts.
 * SMP-safe.
 */
struct dentry * lookup_hash(struct qstr *name, struct dentry * base)
{
	struct dentry * dentry;
	struct inode *inode;
	int err;

	inode = base->d_inode;
	err = permission(inode, MAY_EXEC);
	dentry = ERR_PTR(err);
	if (err)
		goto out;

	/*
	 * See if the low-level filesystem might want
	 * to use its own hash..
	 */
	if (base->d_op && base->d_op->d_hash) {
		err = base->d_op->d_hash(base, name);
		dentry = ERR_PTR(err);
		if (err < 0)
			goto out;
	}

	dentry = cached_lookup(base, name, 0);
	if (!dentry) {
		struct dentry *new = d_alloc(base, name);
		dentry = ERR_PTR(-ENOMEM);
		if (!new)
			goto out;
		lock_kernel();
		dentry = inode->i_op->lookup(inode, new);
		unlock_kernel();
		if (!dentry)
			dentry = new;
		else
			dput(new);
	}
out:
	return dentry;
}

这个函数先在dentry杂凑表队列中寻找,找不到就先创建一个新的dentry数据结构。再到目标文件所在的目录中寻找一次,如果找到了就把已经创建的dentry结构归还,找不到才采用它。为什么要采用这样的次序呢?这是因为在执行d_alloc的过程中有可能进入睡眠,这样就存在一种可能,就是当睡眠醒过来的时候情况已经变化了。类似的情景我们在前几篇博客中已经看过多次。这样,从lookup_hash返回时,不管这目标文件存在与否,总是返回一个dentry数据结构指针(除非系统中的缓冲区已经用完)。如果目标文件的dentry结构原来就存在,那么结构中的d_inode指针指向该文件的inode结构:而如果这个dentry结构是新创建的,则其d_inode指针为NULL,因为此时它还没有inode结构。

先看目标文件尚不存在的情况(见990行),那就是通过vfs_create创建。这个函数的代码比较长,却相对独立。并且受到多处调用,所以我们把它推迟到后面再阅读,这里只要暂时知道这个函数的作用是创建文件就行了。

要是目标文件原来就存在呢?首先,在系统调用open的参数中标志位O_CREAT和O_EXCL同时为1表示目标文件在此之前必须不存在,所以如果已经存在就只好返回错误了,出错代码为-EEXIST。其次,这个目标文件可能是一个安装点,那就要通过__follow_down跑到所安装的文件系统中去,而且要通过一个do-while循环一直跑到头。此外,这个目标文件也有可能只是一个符号链接,而连接的对象有可能悬空,就是连接的对象在另一个设备上,但是那个设备却没有安装。所以代码中先检验目标文件是否为符号链接,是的话就goto到do_link处,顺着连接前进到其目标节点,如果链接对象悬空则返回出错代码-ENOENT,否则便又goto回到do_last处对目标节点展开新一轮的检查。这一段代码在open_namei的最后,我们顺着程序的流程把它提到前面来阅读。

sys_open=>filp_open=>open_namei

do_link:
	error = -ELOOP;
	if (flag & O_NOFOLLOW)
		goto exit_dput;
	/*
	 * This is subtle. Instead of calling do_follow_link() we do the
	 * thing by hands. The reason is that this way we have zero link_count
	 * and path_walk() (called from ->follow_link) honoring LOOKUP_PARENT.
	 * After that we have the parent and last component, i.e.
	 * we are in the same situation as after the first path_walk().
	 * Well, almost - if the last component is normal we get its copy
	 * stored in nd->last.name and we will have to putname() it when we
	 * are done. Procfs-like symlinks just set LAST_BIND.
	 */
	UPDATE_ATIME(dentry->d_inode);
	error = dentry->d_inode->i_op->follow_link(dentry, nd);
	dput(dentry);
	if (error)
		return error;
	if (nd->last_type == LAST_BIND) {
		dentry = nd->dentry;
		goto ok;
	}
	error = -EISDIR;
	if (nd->last_type != LAST_NORM)
		goto exit;
	if (nd->last.name[nd->last.len]) {
		putname(nd->last.name);
		goto exit;
	}
	if (count++==32) {
		dentry = nd->dentry;
		putname(nd->last.name);
		goto ok;
	}
	dir = nd->dentry;
	down(&dir->d_inode->i_sem);
	dentry = lookup_hash(&nd->last, nd->dentry);
	putname(nd->last.name);
	goto do_last;
}

读过path_walk的读者对这段代码不应该感到陌生。找到链接目标的操作主要是由具体文件系统在其inode_operations结构中的函数指针follow_link提供的。对于ext2文件系统这个函数为ext2_follow_link,而最后这个函数又会通过vfs_follow_link调用path_walk,这读者已经看到过了。注意前面通过path_init设置在nameidata数据结构中的标志位并未改变,仍是LOOKUP_PARENT。

对于目标节点是符号链接的情况,如果说在follow_link之前的目标节点是视在目标节点,那么follow_link以后的nd->last就是真实目标节点的节点名了,而nd->dentry则仍旧指向其父节点的dentry结构。

搜索到了真实的目标节点的节点以后,同样还要检查这个节点是否只是个目录而不是文件(1136行和1138行),如果是目录就要出错返回。至于搜索的结果为LAST_BIND,则只发生于/proc或类似的特殊文件系统中,我们在此并不关心。

最后,还要通过lookup_hash找到或创建真实目录节点dentry结构,接着就转回前面的do_last标号处,在那里又要重复对目标节点的一系列检查。检查的结果可能会发现我们所以为的真实目标节点实际上又是一个视在目标节点,那就又会转到这里的do_link。为了防止陷入无限循环,这里用了一个计数器count加以控制(见1142行),如果计数值达到了32就果断结束而转到标号ok处,在那里还要做进一步的检查,若发现仍是链接节点就会返回出错代码-ELOOP。我们以前在path_walk的代码中看到调用一个函数do_follow_link,在那里也有对链接长度的限制(通过task_struct结构中的link_count字段),为什么这里还要另搞一套呢?这是因为path_walk对于旨在

LOOKUP_PARENT的搜索只处理到目标节点的父节点为止,对目标节点本身不不做处理的。

只要链接链的长度不超过合理的范围,最终总会找到真正的目标节点。我们继续往下看:

sys_open=>filp_open=>open_namei

ok:
	error = -ENOENT;
	inode = dentry->d_inode;
	if (!inode)
		goto exit;

	error = -ELOOP;
	if (S_ISLNK(inode->i_mode))
		goto exit;
	
	error = -EISDIR;
	if (S_ISDIR(inode->i_mode) && (flag & FMODE_WRITE))
		goto exit;

	error = permission(inode,acc_mode);
	if (error)
		goto exit;

	/*
	 * FIFO's, sockets and device files are special: they don't
	 * actually live on the filesystem itself, and as such you
	 * can write to them even if the filesystem is read-only.
	 */
	if (S_ISFIFO(inode->i_mode) || S_ISSOCK(inode->i_mode)) {
	    	flag &= ~O_TRUNC;
	} else if (S_ISBLK(inode->i_mode) || S_ISCHR(inode->i_mode)) {
		error = -EACCES;
		if (IS_NODEV(inode))
			goto exit;

		flag &= ~O_TRUNC;
	} else {
		error = -EROFS;
		if (IS_RDONLY(inode) && (flag & 2))
			goto exit;
	}
	/*
	 * An append-only file must be opened in append mode for writing.
	 */
	error = -EPERM;
	if (IS_APPEND(inode)) {
		if  ((flag & FMODE_WRITE) && !(flag & O_APPEND))
			goto exit;
		if (flag & O_TRUNC)
			goto exit;
	}

	/*
	 * Ensure there are no outstanding leases on the file.
	 */
	error = get_lease(inode, flag);
	if (error)
		goto exit;

	if (flag & O_TRUNC) {
		error = get_write_access(inode);
		if (error)
			goto exit;

		/*
		 * Refuse to truncate files with mandatory locks held on them.
		 */
		error = locks_verify_locked(inode);
		if (!error) {
			DQUOT_INIT(inode);
			
			error = do_truncate(dentry, 0);
		}
		put_write_access(inode);
		if (error)
			goto exit;
	} else
		if (flag & FMODE_WRITE)
			DQUOT_INIT(inode);

	return 0;

exit_dput:
	dput(dentry);
exit:
	path_release(nd);
	return error;

do_link:
	error = -ELOOP;
	if (flag & O_NOFOLLOW)
		goto exit_dput;
	/*
	 * This is subtle. Instead of calling do_follow_link() we do the
	 * thing by hands. The reason is that this way we have zero link_count
	 * and path_walk() (called from ->follow_link) honoring LOOKUP_PARENT.
	 * After that we have the parent and last component, i.e.
	 * we are in the same situation as after the first path_walk().
	 * Well, almost - if the last component is normal we get its copy
	 * stored in nd->last.name and we will have to putname() it when we
	 * are done. Procfs-like symlinks just set LAST_BIND.
	 */
	UPDATE_ATIME(dentry->d_inode);
	error = dentry->d_inode->i_op->follow_link(dentry, nd);
	dput(dentry);
	if (error)
		return error;
	if (nd->last_type == LAST_BIND) {
		dentry = nd->dentry;
		goto ok;
	}
	error = -EISDIR;
	if (nd->last_type != LAST_NORM)
		goto exit;
	if (nd->last.name[nd->last.len]) {
		putname(nd->last.name);
		goto exit;
	}
	if (count++==32) {
		dentry = nd->dentry;
		putname(nd->last.name);
		goto ok;
	}
	dir = nd->dentry;
	down(&dir->d_inode->i_sem);
	dentry = lookup_hash(&nd->last, nd->dentry);
	putname(nd->last.name);
	goto do_last;
}

找到或者创建了目标文件之后,还要对代表目标文件的数据结构进行了一系列的检查,包括访问权限的检验以及一些处理(如截尾)。我们可能会对头三项的检验感到奇怪,不是刚刚已经检查过了吗?回过去看一下就知道了,那只是在特殊条件下进行的,而且并不完整。而现在,则是进行综合、完整的检验。

函数permission的代码以后再讲。

下面还有一些对文件模式(由inode结构中的i_mode表示)与操作要求(由flag表示)之间的一致性的检验和处理。这些检验和处理大多数比较简单,例如FIFO和socket文件就无所谓截尾,所以把O_TRUNC标志位清零等等。我们在这里不关心磁盘空间配额(见1093行),所以集中看文件截尾。打开文件时,将O_TRUNC标志位设成1表示要将文件中原有的内容清除,称之为截尾,这是由代码中的第1083行至1100行完成的。在此之前还调用了一个函数get_lease,这是与文件租借有关的操作,我们在此并不关心。

在文件的inode数据结构中有个计数器i_writecount,用来对正在写访问该文件的进程计数。另一方面,有些文件可能已经通过mmap系统调用映射到某个进程的虚存空间,这个计数器就用于通过正常的文件操作和通过内存映射这两种写访问之间的互斥。当整个计数为负数时表示有进程可以通过虚存管理对文件进行写操作,为正值表示某个或者某些进程正在对文件进行写访问。内核中提供了get_write_access和deny_write_access两个函数来保证这种互斥访问,这两个函数的代码如下:

/*
 * get_write_access() gets write permission for a file.
 * put_write_access() releases this write permission.
 * This is used for regular files.
 * We cannot support write (and maybe mmap read-write shared) accesses and
 * MAP_DENYWRITE mmappings simultaneously. The i_writecount field of an inode
 * can have the following values:
 * 0: no writers, no VM_DENYWRITE mappings
 * < 0: (-i_writecount) vm_area_structs with VM_DENYWRITE set exist
 * > 0: (i_writecount) users are writing to the file.
 *
 * Normally we operate on that counter with atomic_{inc,dec} and it's safe
 * except for the cases where we don't hold i_writecount yet. Then we need to
 * use {get,deny}_write_access() - these functions check the sign and refuse
 * to do the change if sign is wrong. Exclusion between them is provided by
 * spinlock (arbitration_lock) and I'll rip the second arsehole to the first
 * who will try to move it in struct inode - just leave it here.
 */
static spinlock_t arbitration_lock = SPIN_LOCK_UNLOCKED;
int get_write_access(struct inode * inode)
{
	spin_lock(&arbitration_lock);
	if (atomic_read(&inode->i_writecount) < 0) {
		spin_unlock(&arbitration_lock);
		return -ETXTBSY;
	}
	atomic_inc(&inode->i_writecount);
	spin_unlock(&arbitration_lock);
	return 0;
}
int deny_write_access(struct file * file)
{
	spin_lock(&arbitration_lock);
	if (atomic_read(&file->f_dentry->d_inode->i_writecount) > 0) {
		spin_unlock(&arbitration_lock);
		return -ETXTBSY;
	}
	atomic_dec(&file->f_dentry->d_inode->i_writecount);
	spin_unlock(&arbitration_lock);
	return 0;
}

这里要注意,注释中所说的"gets write permission"和通过permission检验当前进程对文件的写访问权限是两码事。前者是指对文件的常规写操作和对经过内存映射对文件内容的写操作之间的互斥,而后者则是出于安全性考虑的文件访问权限的验证。

通过get_write_access得到了写操作的许可后,还要考虑目标文件是否已经被其他进程锁住了。从进程的角度考虑,对文件的每次读或写可以认为是原子性的,因为每次读或写都只要通过一次性系统调用就完成。但是,如果考虑连续几次的读、写操作,那么这些相继的读、写操作从总体上看就显然不是原子性的了。对于数据库一类的应用,这就成为一个问题。因此就发展起了对文件或文件中的某一部分内容加锁的技术。

加锁技术有两种。一种是由进程之间自己协调的,称为协调锁(advisory lock或 cooperative lock)。对于这一种锁,内核只是提供加锁以及检测文件是否已经加锁的手段,即系统调用flock,但是内核并不参与锁的实施。也就是说,如果有进程不遵守“游戏规则”,不检查目标文件是否已经由别的进程加了锁就往里写,内核是不加阻拦的,另一种则是内核强制实施的,称为“强制锁”(mandatory lock),即使有进程不遵守游戏规则,不管三七二十一就往加了锁的文件中写,内核时会加以阻拦的。这种锁是对协调锁的改进与加强,具体通过fcntl系统调用实现。但是,在有些应用中并不适合使用强制锁,所以要给文件加上一个像开关一样的东西,这样才可以有选择的允许或不允许对一个文件使用强制锁。在inode结构的i_flag字段中定义的一个标志为MS_MANDLOCK,就是起开关作用。这标志不仅用于inode结构,也用于super_block结构, 对于整个文件系统子系统也可以在安装时将参数中的这个标志位设成1或0,使整个设备上的文件全部允许或不允许使用强制锁。对于要求截尾的打开文件操作,内核应检查目标文件或者目标文件所在的设备是否允许使用强制锁,并且已经加了锁;如果已经加了强制锁便应该出错返回。这就是调用inline函数locks_verify_locked的原因。代码如下:

sys_open=>filp_open=>open_namei=>locks_verify_locked

/*
 * Candidates for mandatory locking have the setgid bit set
 * but no group execute bit -  an otherwise meaningless combination.
 */
#define MANDATORY_LOCK(inode) \
	(IS_MANDLOCK(inode) && ((inode)->i_mode & (S_ISGID | S_IXGRP)) == S_ISGID)

static inline int locks_verify_locked(struct inode *inode)
{
	if (MANDATORY_LOCK(inode))
		return locks_mandatory_locked(inode);
	return 0;
}

有关的宏操作定义为:

/*
 * Note that nosuid etc flags are inode-specific: setting some file-system
 * flags just means all the inodes inherit those flags by default. It might be
 * possible to override it selectively if you really wanted to with some
 * ioctl() that is not currently implemented.
 *
 * Exception: MS_RDONLY is always applied to the entire file system.
 *
 * Unfortunately, it is possible to change a filesystems flags with it mounted
 * with files in use.  This means that all of the inodes will not have their
 * i_flags updated.  Hence, i_flags no longer inherit the superblock mount
 * flags, so these have to be checked separately. -- [email protected]
 */
#define __IS_FLG(inode,flg) ((inode)->i_sb->s_flags & (flg))

#define IS_RDONLY(inode) ((inode)->i_sb->s_flags & MS_RDONLY)
#define IS_NOSUID(inode)	__IS_FLG(inode, MS_NOSUID)
#define IS_NODEV(inode)		__IS_FLG(inode, MS_NODEV)
#define IS_NOEXEC(inode)	__IS_FLG(inode, MS_NOEXEC)
#define IS_SYNC(inode)		(__IS_FLG(inode, MS_SYNCHRONOUS) || ((inode)->i_flags & S_SYNC))
#define IS_MANDLOCK(inode)	__IS_FLG(inode, MS_MANDLOCK)

在一些老的版本中(从Unix系统V开始),曾经用inode结构中的mode字段中S_ISGID和S_IXGRP两个标志位的结合起来MS_MANDLOCK标志位的作用。标志位S_ISGID与S_ISUID相似,表示在启动一个可执行文件时将进程的组号设置成该文件所属的组号。可是,如果这个文件对于所属的组根本就没有可执行属性(由S_IXGRP代表),那S_ISGID就失去了意义。所以,在正常情况下S_ISGID为1而S_IXGRP为0 是自相矛盾的而不应该出现。既然如此,Unix系统版本V就利用了这种组合来控制强制锁使用。所以,只有在inode结构中或者super_block结构中MS_MANDLOCK为1,并且inode结构中的S_ISGID为1而S_IXGRP为0时才允许使用强制锁,这就是901行所表达的意思。

如果目标文件是允许使用强制锁的,那就需要进一步检查是否已经加了锁。函数locks_mandatory_locked的代码如下:

sys_open=>filp_open=>open_namei=>locks_verify_locked=>locks_mandatory_locked

int locks_mandatory_locked(struct inode *inode)
{
	fl_owner_t owner = current->files;
	struct file_lock *fl;

	/*
	 * Search the lock list for this inode for any POSIX locks.
	 */
	lock_kernel();
	for (fl = inode->i_flock; fl != NULL; fl = fl->fl_next) {
		if (!(fl->fl_flags & FL_POSIX))
			continue;
		if (fl->fl_owner != owner)
			break;
	}
	unlock_kernel();
	return fl ? -EAGAIN : 0;
}

每个文件的inode结构中都有个file_lock数据结构队列i_flock,每当一个进程对一个文件中的一个区间加锁时,就创建一个file_lock数据结构并挂入文件的inode结构中。与file_lock结构有关的定义如下:

/*
 * The POSIX file lock owner is determined by
 * the "struct files_struct" in the thread group
 * (or NULL for no owner - BSD locks).
 *
 * Lockd stuffs a "host" pointer into this.
 */
typedef struct files_struct *fl_owner_t;

struct file_lock {
	struct file_lock *fl_next;	/* singly linked list for this inode  */
	struct list_head fl_link;	/* doubly linked list of all locks */
	struct list_head fl_block;	/* circular list of blocked processes */
	fl_owner_t fl_owner;
	unsigned int fl_pid;
	wait_queue_head_t fl_wait;
	struct file *fl_file;
	unsigned char fl_flags;
	unsigned char fl_type;
	loff_t fl_start;
	loff_t fl_end;

	void (*fl_notify)(struct file_lock *);	/* unblock callback */
	void (*fl_insert)(struct file_lock *);	/* lock insertion callback */
	void (*fl_remove)(struct file_lock *);	/* lock removal callback */

	struct fasync_struct *	fl_fasync; /* for lease break notifications */

	union {
		struct nfs_lock_info	nfs_fl;
	} fl_u;
};

一个file_lock结构就是一把锁,结构中的指针fl_file指向目标文件的file结构,而fl_start和fl_end就确定了该文件中的一个区间,如果fl_start为0而fl_end为OFFSET_MAX就表示整个文件。此外,fl_type表示锁的性质,如读写;fl_flags中是一些标志位,这些标志位的定义也在该文件中。定义如下:

#define FL_POSIX	1
#define FL_FLOCK	2
#define FL_BROKEN	4	/* broken flock() emulation */
#define FL_ACCESS	8	/* for processes suspended by mandatory locking */
#define FL_LOCKD	16	/* lock held by rpc.lockd */
#define FL_LEASE	32	/* lease held on this file */

标志位FL_FLOCK为1表示这个锁是通过传统的flock系统调用加上去的,这种锁一定是协调锁,并且只能是对整个文件的。标志位FL_POSIX则表示通过fcntl系统调用加上的锁,它支持对文件中的区间加锁。由于在POSIX标准中规定了这种对文件中部分内容所加的锁,所以又称为POSIX锁。POSIX锁可以是协调锁,也可以是强制锁,具体取决于前面所述的条件。这里,在locks_mandatory_locked的代码中检测的是强制锁,所以只关心FL_POSIX标志位为1的那些数据结构。

回到open_namei的代码中,如果目标文件并未加上强制锁,就可以通过do_truncate执行截尾了,其代码如下:

sys_open=>filp_open=>open_namei=>do_truncate


int do_truncate(struct dentry *dentry, loff_t length)
{
	struct inode *inode = dentry->d_inode;
	int error;
	struct iattr newattrs;

	/* Not pretty: "inode->i_size" shouldn't really be signed. But it is. */
	if (length < 0)
		return -EINVAL;

	down(&inode->i_sem);
	newattrs.ia_size = length;
	newattrs.ia_valid = ATTR_SIZE | ATTR_CTIME;
	error = notify_change(dentry, &newattrs);
	up(&inode->i_sem);
	return error;
}

参数length为截尾后残留的长度,从open_namei中传下来的参数值为0,表示全部切除。代码中先准备一个iattr结构,然后通过notify_change来完成操作。函数notify_change的代码如下:

sys_open=>filp_open=>open_namei=>do_truncate=>notify_change
 


int notify_change(struct dentry * dentry, struct iattr * attr)
{
	struct inode *inode = dentry->d_inode;
	int error;
	time_t now = CURRENT_TIME;
	unsigned int ia_valid = attr->ia_valid;

	if (!inode)
		BUG();

	attr->ia_ctime = now;
	if (!(ia_valid & ATTR_ATIME_SET))
		attr->ia_atime = now;
	if (!(ia_valid & ATTR_MTIME_SET))
		attr->ia_mtime = now;

	lock_kernel();
	if (inode->i_op && inode->i_op->setattr) 
		error = inode->i_op->setattr(dentry, attr);
	else {
		error = inode_change_ok(inode, attr);
		if (!error)
			inode_setattr(inode, attr);
	}
	unlock_kernel();
	if (!error) {
		unsigned long dn_mask = setattr_mask(ia_valid);
		if (dn_mask)
			inode_dir_notify(dentry->d_parent->d_inode, dn_mask);
	}
	return error;
}

这里的目的是改变inode结构中的一些数据,以及执行伴随着这些改变的操作。这些操作常常是因文件系统而异的,所以具体的文件系统可以通过其inode_operations数据结构提供用于这个目的的函数(指针)。就ext2文件系统而言,它并未提供这样的函数,所以通过inode_change_ok和inode_setattr这两个函数来完成这个工作的。函数inode_change_ok主要是对权限的检验,其代码如下:

sys_open=>filp_open=>open_namei=>do_truncate=>notify_change=>inode_change_ok


/* Taken over from the old code... */

/* POSIX UID/GID verification for setting inode attributes. */
int inode_change_ok(struct inode *inode, struct iattr *attr)
{
	int retval = -EPERM;
	unsigned int ia_valid = attr->ia_valid;

	/* If force is set do it anyway. */
	if (ia_valid & ATTR_FORCE)
		goto fine;

	/* Make sure a caller can chown. */
	if ((ia_valid & ATTR_UID) &&
	    (current->fsuid != inode->i_uid ||
	     attr->ia_uid != inode->i_uid) && !capable(CAP_CHOWN))
		goto error;

	/* Make sure caller can chgrp. */
	if ((ia_valid & ATTR_GID) &&
	    (!in_group_p(attr->ia_gid) && attr->ia_gid != inode->i_gid) &&
	    !capable(CAP_CHOWN))
		goto error;

	/* Make sure a caller can chmod. */
	if (ia_valid & ATTR_MODE) {
		if ((current->fsuid != inode->i_uid) && !capable(CAP_FOWNER))
			goto error;
		/* Also check the setgid bit! */
		if (!in_group_p((ia_valid & ATTR_GID) ? attr->ia_gid :
				inode->i_gid) && !capable(CAP_FSETID))
			attr->ia_mode &= ~S_ISGID;
	}

	/* Check for setting the inode time. */
	if (ia_valid & (ATTR_MTIME_SET | ATTR_ATIME_SET)) {
		if (current->fsuid != inode->i_uid && !capable(CAP_FOWNER))
			goto error;
	}
fine:
	retval = 0;
error:
	return retval;
}

函数inode_setattr的代码也在同一个文件中:

sys_open=>filp_open=>open_namei=>do_truncate=>notify_change=>inode_setattr


void inode_setattr(struct inode * inode, struct iattr * attr)
{
	unsigned int ia_valid = attr->ia_valid;

	if (ia_valid & ATTR_UID)
		inode->i_uid = attr->ia_uid;
	if (ia_valid & ATTR_GID)
		inode->i_gid = attr->ia_gid;
	if (ia_valid & ATTR_SIZE)
		vmtruncate(inode, attr->ia_size);
	if (ia_valid & ATTR_ATIME)
		inode->i_atime = attr->ia_atime;
	if (ia_valid & ATTR_MTIME)
		inode->i_mtime = attr->ia_mtime;
	if (ia_valid & ATTR_CTIME)
		inode->i_ctime = attr->ia_ctime;
	if (ia_valid & ATTR_MODE) {
		inode->i_mode = attr->ia_mode;
		if (!in_group_p(inode->i_gid) && !capable(CAP_FSETID))
			inode->i_mode &= ~S_ISGID;
	}
	mark_inode_dirty(inode);
}

我们这里关心只是文件大小的改变,这个改变及其伴随操作的是通过vmtruncate完成的。函数vmtruncate所完成的操作以及它的代码都与文件的读写关系更为密切,所以我们打算把这个函数留到文件的读写博客中再深入阅读。

最后,由于inode结构的内容改变了,所以要通过mark_inode_dirty把它挂入到所属super_block结构中的s_dirty队列中。

至此对目标文件所进行的操作都已完成,但是代表着当前进程与目标文件的连接的file结构却尚未建立。返回到filp_open的代码中以后,下一个要调用的函数是dentry_open,它的任务就是建立起目标文件的一个上下文,即file数据结构,并让它与当前进程的task_struct结构挂上钩,成为该文件驻在当前进程task_struct结构中的一个代表。函数dentry_open的代码如下:

sys_open=>filp_open=>dentry_open


struct file *dentry_open(struct dentry *dentry, struct vfsmount *mnt, int flags)
{
	struct file * f;
	struct inode *inode;
	int error;

	error = -ENFILE;
	f = get_empty_filp();
	if (!f)
		goto cleanup_dentry;
	f->f_flags = flags;
	f->f_mode = (flags+1) & O_ACCMODE;
	inode = dentry->d_inode;
	if (f->f_mode & FMODE_WRITE) {
		error = get_write_access(inode);
		if (error)
			goto cleanup_file;
	}

	f->f_dentry = dentry;
	f->f_vfsmnt = mnt;
	f->f_pos = 0;
	f->f_reada = 0;
	f->f_op = fops_get(inode->i_fop);
	if (inode->i_sb)
		file_move(f, &inode->i_sb->s_files);
	if (f->f_op && f->f_op->open) {
		error = f->f_op->open(inode,f);
		if (error)
			goto cleanup_all;
	}
	f->f_flags &= ~(O_CREAT | O_EXCL | O_NOCTTY | O_TRUNC);

	return f;

cleanup_all:
	fops_put(f->f_op);
	if (f->f_mode & FMODE_WRITE)
		put_write_access(inode);
	f->f_dentry = NULL;
	f->f_vfsmnt = NULL;
cleanup_file:
	put_filp(f);
cleanup_dentry:
	dput(dentry);
	mntput(mnt);
	return ERR_PTR(error);
}

顾名思义,get_empty_filp的作用是分配一个空闲的file数据结构。内核中有一个空闲file结构的队列free_list,需要file结构时就从该队列中摘下一个,并将其暂时挂入一个中间队列中anon_list。在确认了对该文件可以进行写操作以后,就对这个空闲file结构进行初始化,然后通过file_move将其从中间队列中脱链而挂入该文件所在设备的super_block结构中的file结构队列s_files。

函数get_write_access一方面检查该文件是否因内存映射而不允许常规的写访问,另一方面如果允许常规的写访问就递增inode结构中的计数器i_writecount,以此来保证常规的文件写访问与内存映射的文件写访问之间的互斥,其代码读者已在前面看到过。

读者已经熟知,file结构中的指针f_op指向所属文件系统的file_operations数据结构。这个指针就是在这里设置的,而具体的file_operations结构指针则来自相应的inode结构。但是,如果相应文件系统是由可安装模块支持的就需要递增该模块的使用计数。这里的fops_get这个宏操作,定义如下:


/* Alas, no aliases. Too much hassle with bringing module.h everywhere */
#define fops_get(fops) \
	(((fops) && (fops)->owner)	\
		? ( try_inc_mod_count((fops)->owner) ? (fops) : NULL ) \
		: (fops))

具体的文件系统还可能对打开文件有一些特殊的附加操作,如果有就通过其file_operations结构中的函数指针open提供。就ext2文件系统而言,这个函数就是ext2_open_file,其代码如下:

sys_open=>filp_open=>dentry_open=>ext2_open_file


/*
 * Called when an inode is about to be open.
 * We use this to disallow opening RW large files on 32bit systems if
 * the caller didn't specify O_LARGEFILE.  On 64bit systems we force
 * on this flag in sys_open.
 */
static int ext2_open_file (struct inode * inode, struct file * filp)
{
	if (!(filp->f_flags & O_LARGEFILE) &&
	    inode->i_size > 0x7FFFFFFFLL)
		return -EFBIG;
	return 0;
}

大家知道,inode结构中有个union,对于ext2文件系统这个union被用作ext2_inode_info数据结构。在这个数据结构中有个字段i_high_size,当文件的大小超过32位整数(2GB)时这个字段的值为文件大小的值的高32位。如果文件的大小真的超过32位,则在系统调用open的参数中要将一个标志位O_LARGEFILE置为1,这是为64位系统结构考虑而设置的。在inode结构中另一个表示文件大小的字段i_size,其类型为loff_t,实际上是long long,即64位整数。如果O_LARGEFILE为0,则文件大小必须小于2GB。

最后,调用参数中的O_CREAT,O_EXCL等标志位已经不再需要了,因为它们只是对打开文件有作用,而现在文件已经打开,所以就把这些标志位清0。

函数dentry_open返回指向新建立的file数据结构的指针,每进行一次成功的open系统调用就为目标文件建立起一个由file结构代表的上下文,而与该文件是否已经有其他的file结构无关。一个文件在内核中只有一个inode结构,却可以有多个file结构。

这样,filp_open的操作就完成了。回到sys_open的代码中,下面还有个inline函数。fd_install的作用是将新建立的file数据结构的指针安装到当前进程的files_struct结构中,确切地说是里面的已打开的文件指针数组中,其位置即下标fd,已经在前面分配好。该inline函数的代码如下:

sys_open=>fd_install


/*
 * Install a file pointer in the fd array.  
 *
 * The VFS is full of places where we drop the files lock between
 * setting the open_fds bitmap and installing the file in the file
 * array.  At any such point, we are vulnerable to a dup2() race
 * installing a file in the array before us.  We need to detect this and
 * fput() the struct file we are about to overwrite in this case.
 *
 * It should never happen - if we allow dup2() do it, _really_ bad things
 * will follow.
 */

static inline void fd_install(unsigned int fd, struct file * file)
{
	struct files_struct *files = current->files;
	
	write_lock(&files->file_lock);
	if (files->fd[fd])
		BUG();
	files->fd[fd] = file;
	write_unlock(&files->file_lock);
}

从此,当前进程与目标文件之间就建立了连接,可以通过这个特定的上下文访问该目标文件了。

看完了sys_open的主体,我还要回头看一下vfs_create的代码,当系统调用open的参数O_CREAT标志位为1,而目标文件又不存在时,就要通过这个函数来创建,其代码如下:

sys_open=>filp_open=>open_namei=>vfs_create


int vfs_create(struct inode *dir, struct dentry *dentry, int mode)
{
	int error;

	mode &= S_IALLUGO & ~current->fs->umask;
	mode |= S_IFREG;

	down(&dir->i_zombie);
	error = may_create(dir, dentry);
	if (error)
		goto exit_lock;

	error = -EACCES;	/* shouldn't it be ENOSYS? */
	if (!dir->i_op || !dir->i_op->create)
		goto exit_lock;

	DQUOT_INIT(dir);
	lock_kernel();
	error = dir->i_op->create(dir, dentry, mode);
	unlock_kernel();
exit_lock:
	up(&dir->i_zombie);
	if (!error)
		inode_dir_notify(dir, DN_CREATE);
	return error;
}

参数dir指向所在目录的inode结构,而dentry则指向待创建文件的dentry结构。但是此时待创建文件尚无inode结构,所以其dentry结构中的d_inode指针为0,这样的dentry结构称为是negative的dentry结构。

每个进程都有个文件访问权限屏蔽掩码 umask,记录在其fs_struct结构中(task_struct结构中的指针fs指向这个数据结构)。这是一些对于文件访问权限的屏蔽位,其格式与表示文件访问权限的mode相同。如果umask中的某一位为1,则由此进程所创建的文件就把相应的访问权限屏蔽掉。例如:如果一个进程的umask为0777,则由它所创建的文件只能由文件主使用,因为对同组人以及其他用户的访问权限全给屏蔽掉了。进程的umask代代相传,但是可以通过系统调用umask加以改变。代码中第902行和第903行说明了怎样根据调用参数mode和进程的umask确定所创建文件的访问模式。这里的常数S_IALLUGO定义如下:

#define S_ISUID  0004000
#define S_ISGID  0002000
#define S_ISVTX  0001000

#define S_IRWXUGO	(S_IRWXU|S_IRWXG|S_IRWXO)
#define S_IALLUGO	(S_ISUID|S_ISGID|S_ISVTX|S_IRWXUGO)

这些标志的意思会在“文件系统的访问权限与安全性”一节中介绍。

不言而喻,创建文件时要改变其所在目录的内容。这个过程不容许其他进程干扰,所以要放在临界区中完成。为此目的,在inode数据结构中提供了一个信号量i_zombie。进入临界区后,先要检查当前进程的权限,看看是否允许在所在的目录中创建文件。代码中的may_create是个inline函数,其代码如下:

sys_open=>filp_open=>open_namei=>vfs_create=>may_create


/*	Check whether we can create an object with dentry child in directory
 *  dir.
 *  1. We can't do it if child already exists (open has special treatment for
 *     this case, but since we are inlined it's OK)
 *  2. We can't do it if dir is read-only (done in permission())
 *  3. We should have write and exec permissions on dir
 *  4. We can't do it if dir is immutable (done in permission())
 */
static inline int may_create(struct inode *dir, struct dentry *child) {
	if (child->d_inode)
		return -EEXIST;
	if (IS_DEADDIR(dir))
		return -ENOENT;
	return permission(dir,MAY_WRITE | MAY_EXEC);
}

这里IS_DEADDIR检查目标文件所在的目录是否实际上已被删除,其定义如下:

#define S_DEAD		32	/* removed, but still open directory */


#define IS_DEADDIR(inode)	((inode)->i_flags & S_DEAD)

删除文件时,要将所在目录inode结构中i_flags字段的S_DEAD标志位设为1。但是,如果当时其dentry结构和inode结构的共享计数不能递减到0则不能将这两个数据结构释放。所以,存在着这样一种可能性,就是在当前进程通过path_walk找到了所在目录的dentry结构和inode结构之后,在试图进入由信号量i_zombie结构的临界区时进入了睡眠;而此时正在临界区的另一个进程却删除了这个目录。不过,一旦当前进程进入了临界区以后,就再不会发生这种情况了。

通过了访问权限的检验以后,具体创建文件的操作则因文件系统而异,每种文件系统都通过其inode_operations结构提供用于创建文件的函数。就ext2文件系统而言,这个函数为ext2_create,代码如下:

sys_open=>filp_open=>open_namei=>vfs_create=>ext2_create


/*
 * By the time this is called, we already have created
 * the directory cache entry for the new file, but it
 * is so far negative - it has no inode.
 *
 * If the create succeeds, we fill in the inode information
 * with d_instantiate(). 
 */
static int ext2_create (struct inode * dir, struct dentry * dentry, int mode)
{
	struct inode * inode = ext2_new_inode (dir, mode);
	int err = PTR_ERR(inode);
	if (IS_ERR(inode))
		return err;

	inode->i_op = &ext2_file_inode_operations;
	inode->i_fop = &ext2_file_operations;
	inode->i_mapping->a_ops = &ext2_aops;
	inode->i_mode = mode;
	mark_inode_dirty(inode);
	err = ext2_add_entry (dir, dentry->d_name.name, dentry->d_name.len, 
			     inode);
	if (err) {
		inode->i_nlink--;
		mark_inode_dirty(inode);
		iput (inode);
		return err;
	}
	d_instantiate(dentry, inode);
	return 0;
}

简言之,就是通过ext2_new_inode创建目标文件在存储设备上的索引节点和在内存中的inode结构,然后通过ext2_add_entry把目标文件的文件名与索引节点号写入其所在的目录(也是一个文件)中,最后由d_instantiate将目标文件的dentry结构和inode结构联系在一起。注意inode结构中的i_op和i_fop两个重要指针都在这里设置的,还有其a_ops所指的address_space结构的指针a_ops也是在这里设置的。

先看ext2_new_inode,代码比较长分段来看。其代码如下:

sys_open=>filp_open=>open_namei=>vfs_create=>ext2_create=>ext2_new_inode

/*
 * There are two policies for allocating an inode.  If the new inode is
 * a directory, then a forward search is made for a block group with both
 * free space and a low directory-to-inode ratio; if that fails, then of
 * the groups with above-average free space, that group with the fewest
 * directories already is chosen.
 *
 * For other inodes, search forward from the parent directory\'s block
 * group to find a free inode.
 */
struct inode * ext2_new_inode (const struct inode * dir, int mode)
{
	struct super_block * sb;
	struct buffer_head * bh;
	struct buffer_head * bh2;
	int i, j, avefreei;
	struct inode * inode;
	int bitmap_nr;
	struct ext2_group_desc * gdp;
	struct ext2_group_desc * tmp;
	struct ext2_super_block * es;
	int err;

	/* Cannot create files in a deleted directory */
	if (!dir || !dir->i_nlink)
		return ERR_PTR(-EPERM);

	sb = dir->i_sb;
	inode = new_inode(sb);
	if (!inode)
		return ERR_PTR(-ENOMEM);

参数dir指向所在目录的inode结构,这个结构中的i_nlink表示有几个目录项与这个inode结构相联系。

相对而言,在内存中分配一个inode结构时比较简单的,这里的new_inode是个inline函数,定义如下:

static inline struct inode * new_inode(struct super_block *sb)
{
	struct inode *inode = get_empty_inode();
	if (inode) {
		inode->i_sb = sb;
		inode->i_dev = sb->s_dev;
	}
	return inode;
}

函数get_empty_inode将分配的空白inode结构挂入内核中的inode_in_user队列,其代码就不看了。

下一步要做的是目标文件在存储设备上分配一个索引节点。函数ext2_new_inode并不是单纯用来创建普通文件的,它也是用来创建目录(当然,目录实际上是文件,但是毕竟有些不同,目录是通过mkdir系统调用创建的)。实际上,调用这个函数的地方不光是ext2_create,还有ext2_mknod、ext2_mkdir以及ext2_symlink。代码的作者在函数的前面加了注释,说对于目录和普通文件采取了不同的分配策略。下面就会具体看到。

以前讲过,现在的块设备通常是很大的。为了提供访问效率,就把存储介质划分成许多块组。一般来说,文件就应该与其所在目录存储在同一个块组中,这样才能提高效率。另一方面,文件的内容和文件的索引节点也应存储在同一个块组中,所以在创建文件系统(格式化)时已经注意到了每个块组在索引节点和记录块也应该存储在同一个块组中,所以在创建文件系统(格式化)时已经注意到了每个块组在索引节点和记录块数量之间的比例,这个比例是从统计信息得来的,取决于平均的文件大小。此外,根据统计,每个块组中平均有多少个目录,也就是说每个目录中平均有多少个文件,也大致上有个比例。所以,如果要创建的是文件,就应该首先考虑将它的索引节点分配在其所在目录所在的块组中。而如果要创建的是目录,则要考虑将来是否能将其属下的文件都容纳在同一个块组中,所以应该找一个其空闲索引节点的数量超过整个设备上的平均值这么一个块组,而不惜离开其父节点所在的块组另起炉灶。了解了这些背景,我们应该可以看懂下面这段程序了。注意282行使指针es指向该文件系统超级块的缓冲区。沿着ext2_new_inode继续往下看:

sys_open=>filp_open=>open_namei=>vfs_create=>ext2_create=>ext2_new_inode

	lock_super (sb);
	es = sb->u.ext2_sb.s_es;
repeat:
	gdp = NULL; i=0;
	
	if (S_ISDIR(mode)) {
		avefreei = le32_to_cpu(es->s_free_inodes_count) /
			sb->u.ext2_sb.s_groups_count;
/* I am not yet convinced that this next bit is necessary.
		i = dir->u.ext2_i.i_block_group;
		for (j = 0; j < sb->u.ext2_sb.s_groups_count; j++) {
			tmp = ext2_get_group_desc (sb, i, &bh2);
			if (tmp &&
			    (le16_to_cpu(tmp->bg_used_dirs_count) << 8) < 
			     le16_to_cpu(tmp->bg_free_inodes_count)) {
				gdp = tmp;
				break;
			}
			else
			i = ++i % sb->u.ext2_sb.s_groups_count;
		}
*/
		if (!gdp) {
			for (j = 0; j < sb->u.ext2_sb.s_groups_count; j++) {
				tmp = ext2_get_group_desc (sb, j, &bh2);
				if (tmp &&
				    le16_to_cpu(tmp->bg_free_inodes_count) &&
				    le16_to_cpu(tmp->bg_free_inodes_count) >= avefreei) {
					if (!gdp || 
					    (le16_to_cpu(tmp->bg_free_blocks_count) >
					     le16_to_cpu(gdp->bg_free_blocks_count))) {
						i = j;
						gdp = tmp;
					}
				}
			}
		}
	}
	else 
	{
		/*
		 * Try to place the inode in its parent directory
		 */
		i = dir->u.ext2_i.i_block_group;
		tmp = ext2_get_group_desc (sb, i, &bh2);
		if (tmp && le16_to_cpu(tmp->bg_free_inodes_count))
			gdp = tmp;
		else
		{
			/*
			 * Use a quadratic hash to find a group with a
			 * free inode
			 */
			for (j = 1; j < sb->u.ext2_sb.s_groups_count; j <<= 1) {
				i += j;
				if (i >= sb->u.ext2_sb.s_groups_count)
					i -= sb->u.ext2_sb.s_groups_count;
				tmp = ext2_get_group_desc (sb, i, &bh2);
				if (tmp &&
				    le16_to_cpu(tmp->bg_free_inodes_count)) {
					gdp = tmp;
					break;
				}
			}
		}
		if (!gdp) {
			/*
			 * That failed: try linear search for a free inode
			 */
			i = dir->u.ext2_i.i_block_group + 1;
			for (j = 2; j < sb->u.ext2_sb.s_groups_count; j++) {
				if (++i >= sb->u.ext2_sb.s_groups_count)
					i = 0;
				tmp = ext2_get_group_desc (sb, i, &bh2);
				if (tmp &&
				    le16_to_cpu(tmp->bg_free_inodes_count)) {
					gdp = tmp;
					break;
				}
			}
		}
	}

	err = -ENOSPC;
	if (!gdp)
		goto fail;

	err = -EIO;
	bitmap_nr = load_inode_bitmap (sb, i);
	if (bitmap_nr < 0)
		goto fail;

对于所创建目标为目录的情景,代码作者(不知是否是原作者)把290行至301行暂时注释了,说不相信这个是有必要的。不过,依我们看这倒是有好处的;如果一个块组目录的数量与空闲索引节点的数量之比小于1/256(见293行),则所创建目录(不包括其子目录)能够容纳在这个块组里的概率应该是很高的。这样做也有利于减少另起炉灶的次数,而让子目录尽量留在父目录所在的块组里。

确定了将索引节点分配在哪一个块组中以后,就要从该块组的索引节点位图中分配一个节点了。

sys_open=>filp_open=>open_namei=>vfs_create=>ext2_create=>ext2_new_inode


	bh = sb->u.ext2_sb.s_inode_bitmap[bitmap_nr];
	if ((j = ext2_find_first_zero_bit ((unsigned long *) bh->b_data,
				      EXT2_INODES_PER_GROUP(sb))) <
	    EXT2_INODES_PER_GROUP(sb)) {
		if (ext2_set_bit (j, bh->b_data)) {
			ext2_error (sb, "ext2_new_inode",
				      "bit already set for inode %d", j);
			goto repeat;
		}
		mark_buffer_dirty(bh);
		if (sb->s_flags & MS_SYNCHRONOUS) {
			ll_rw_block (WRITE, 1, &bh);
			wait_on_buffer (bh);
		}
	} else {
		if (le16_to_cpu(gdp->bg_free_inodes_count) != 0) {
			ext2_error (sb, "ext2_new_inode",
				    "Free inodes count corrupted in group %d",
				    i);
			/* Is it really ENOSPC? */
			err = -ENOSPC;
			if (sb->s_flags & MS_RDONLY)
				goto fail;

			gdp->bg_free_inodes_count = 0;
			mark_buffer_dirty(bh2);
		}
		goto repeat;
	}

在前一节中提到过,super_block内的ext2_sb_info结构中有一个索引节点位图缓冲区的指针数组,用来缓冲存储若干个块组的位图。当需要使用某个块组的索引节点位图时,就先在这个数组中找,若找不到再从设备上把这个块组的位图读入缓冲区中,并让该数组中的某个指针指向这个缓冲区。这是由load_inode_bitmap完成的,其代码不再分析了。

取得了目标块组的索引节点位图以后,就通过ext2_find_first_zero_bit从位图中找到一位仍然为0的位,也就是找到一个空闲的索引节点。一般情况下,这是不会失败的,因为该块组的描述结构已经告诉我们有空闲节点。

所谓从位图中分配一个索引节点,就是通过ext2_set_bit将其对应位设置成1。这是一个宏操作,定义如下:

#define ext2_set_bit(nr, addr) test_and_set_bit((nr), (addr))

也就是说,ext2_set_bit一方面将位图中的某一位设成1,另一方面还检查这一位原来是否为1,如果是就说明了冲突,因而要goto转回标号repeat处另起寻找。否则,如果一切顺利,索引节点的分配就成功了,此时要立即把索引节点所在记录块的缓冲区标志成脏。如果super_block结构的s_flags中的MS_SYNCHRONOUS标志位为1,则立即要通过ll_rw_block把改变了的记录块写回磁盘并等待其完成。函数ll_rw_block的代码属于设备驱动层的内容了,在后面的博客中会讲解。

但是,尽管块组的描述结构告诉我们有空闲节点,ext2_find_first_zero_bit还是有可能失败,因为块组的描述结构有可能已经损坏了,这往往发生在及其在运行时中途断电,或者用户不按规定程序关机的情况下。通常在这种情况发生后再次开机时系统会检测到文件系统不干净而强制进行一次文件系统检测(fsck)。并且,作为安全措施,在一个文件系统顺利安装了一定次数之后也要进行一次例行检查。但尽管这样还是可能会有漏网之鱼,所以通过块组描述结构中的信息与索引节点位图不一致时便说明块组已经损坏,该文件系统已经不一致了。如果发生了这样的情况(在位图中找不到空闲的索引节点),那就只好再找其他块组,所以(400行)通过goto语句转回标号repeat处再来一次。

至此,变量i表示块组号,而j表示所分配的索引节点在本块组中的序号,根据这二者可以算出该节点在整个设备(文件子系统)中的索引节点号。我们继续往下看:

sys_open=>filp_open=>open_namei=>vfs_create=>ext2_create=>ext2_new_inode

	j += i * EXT2_INODES_PER_GROUP(sb) + 1;
	if (j < EXT2_FIRST_INO(sb) || j > le32_to_cpu(es->s_inodes_count)) {
		ext2_error (sb, "ext2_new_inode",
			    "reserved inode or inode > inodes count - "
			    "block_group = %d,inode=%d", i, j);
		err = -EIO;
		goto fail;
	}
	gdp->bg_free_inodes_count =
		cpu_to_le16(le16_to_cpu(gdp->bg_free_inodes_count) - 1);
	if (S_ISDIR(mode))
		gdp->bg_used_dirs_count =
			cpu_to_le16(le16_to_cpu(gdp->bg_used_dirs_count) + 1);
	mark_buffer_dirty(bh2);
	es->s_free_inodes_count =
		cpu_to_le32(le32_to_cpu(es->s_free_inodes_count) - 1);
	mark_buffer_dirty(sb->u.ext2_sb.s_sbh);
	sb->s_dirt = 1;
	inode->i_mode = mode;
	inode->i_uid = current->fsuid;
	if (test_opt (sb, GRPID))
		inode->i_gid = dir->i_gid;
	else if (dir->i_mode & S_ISGID) {
		inode->i_gid = dir->i_gid;
		if (S_ISDIR(mode))
			mode |= S_ISGID;
	} else
		inode->i_gid = current->fsgid;

	inode->i_ino = j;
	inode->i_blksize = PAGE_SIZE;	/* This is the optimal IO size (for stat), not the fs block size */
	inode->i_blocks = 0;
	inode->i_mtime = inode->i_atime = inode->i_ctime = CURRENT_TIME;
	inode->u.ext2_i.i_new_inode = 1;
	inode->u.ext2_i.i_flags = dir->u.ext2_i.i_flags;
	if (S_ISLNK(mode))
		inode->u.ext2_i.i_flags &= ~(EXT2_IMMUTABLE_FL | EXT2_APPEND_FL);
	inode->u.ext2_i.i_faddr = 0;
	inode->u.ext2_i.i_frag_no = 0;
	inode->u.ext2_i.i_frag_size = 0;
	inode->u.ext2_i.i_file_acl = 0;
	inode->u.ext2_i.i_dir_acl = 0;
	inode->u.ext2_i.i_dtime = 0;
	inode->u.ext2_i.i_block_group = i;
	if (inode->u.ext2_i.i_flags & EXT2_SYNC_FL)
		inode->i_flags |= S_SYNC;
	insert_inode_hash(inode);
	inode->i_generation = event++;
	mark_inode_dirty(inode);

	unlock_super (sb);
	if(DQUOT_ALLOC_INODE(sb, inode)) {
		sb->dq_op->drop(inode);
		inode->i_nlink = 0;
		iput(inode);
		return ERR_PTR(-EDQUOT);
	}
	ext2_debug ("allocating inode %lu\n", inode->i_ino);
	return inode;

fail:
	unlock_super(sb);
	iput(inode);
	return ERR_PTR(err);
}

分配了空闲索引节点后,还要对其节点号做一次检查。ext2文件系统可能保留最初的若干索引节点不用,此外超级块中的s_inodes_count也可能与各块组中索引节点的总和不一致(通常发生在用户使用工具对超级块进行了某种修补之后)。

下面就是对块组描述结构和超级块中数据的调整,以及对新建立的inode结构的初始化了。读者应该注意对新创建文件(或目录)的用户号uid和组号gid的设置。首先,新创文件的UID并不是当前进程的uid,而是它的fsuid。也就是说,如果当前进程是因为执行一个suid可执行程序而成为超级用的,那么它所创建的文件属于超级用户(uid为0)。或者,如果当前进程通过设置进程的用户号转到了另一个用户的名下,那么它所创建的文件也就属于当前进程此时实际使用的用户号,即fsuid。组号的情况也与此类似。但是安装文件系统时可以设置一个GRPID标志位,使得在该文件系统中新创建文件时使用其所在目录的gid,而不管当前进程的fsuid是什么。或者,如果虽然GRPID标志位为0,但是,所在目录的模式中的S_ISGID标志为1,也就继承其所在目录的gid。

然后将新的inode结构链入到inode_hashtable中的某个杂凑队列中,insert_inode_hash的代码如下:

sys_open=>filp_open=>open_namei=>vfs_create=>ext2_create=>ext2_new_inode=>insert_inode_hash


/**
 *	insert_inode_hash - hash an inode
 *	@inode: unhashed inode
 *
 *	Add an inode to the inode hash for this superblock. If the inode
 *	has no superblock it is added to a separate anonymous chain.
 */
 
void insert_inode_hash(struct inode *inode)
{
	struct list_head *head = &anon_hash_chain;
	if (inode->i_sb)
		head = inode_hashtable + hash(inode->i_sb, inode->i_ino);
	spin_lock(&inode_lock);
	list_add(&inode->i_hash, head);
	spin_unlock(&inode_lock);
}

  索引节点号只在同一个设备上保持唯一性,所以在杂凑阶段时将所在设备的super_block结构的地址也一起计算进去,以保证其全系统范围的唯一性。

由于我们并不关心设备上存储空间的配额问题,ext2_new_inode的操作就完成了。回到ext2_create的代码中,接着是设置新创的inode结构中的inode_operations结构的指针和file_operations结构指针,还有用于文件映射(至虚存空间中)

的address_space_operations结构指针,使它们一一指向由ext2文件系统提供的相应数据结构。这样,对这个新建文件,VFS层与ext2层之间的界面就设置好了。这些指针决定了对该文件系统所做一些文件操作要通过由ext2文件系统所提供的函数来完成。

至此,新文件的索引节点已经分配。内核中的inode数据结构也已经建立并设置好。由于新的inode已经通过mark_inode_dirty设置成脏,并从杂凑队列转移到了super_block结构中的s_dirty队列里,这样内核就会(在适当的时机)把这个inode结构的内容写回设备上的索引节点,因此可以认为文件本身的创建已经完成了。但是,尽管如此,这个文件还只是一个孤岛,通向这个文件的路径并不存在。所以,回到ext2_create中,下一步就是要在该文件所在的目录中增加一个目录项,使新文件的文件名与其索引节点号挂上钩并出现在目录中,从而建立通向这个文件的路径,这是由ext2_add_entry完成的。如前所述,目录实际上也是文件,所以在目录中增加一个目录项的操作就与普通文件的读写很类似,等我们看了后面的文件的读和写博客之后,可以回过头来看这段代码。

最后,还要让新建文件的dentry结构(在open_namei中由lookup_hash中创建的)与inode结构之间也挂上钩,这是由d_instantiate完成的,其代码前面已经读过了。

函数ext2_create执行完毕之后,vfs_create的任务也就完成了。

看完了文件的打开,再来看文件的关闭。系统调用close是由内核中的sys_close来实现的。

代码如下:


/*
 * Careful here! We test whether the file pointer is NULL before
 * releasing the fd. This ensures that one clone task can't release
 * an fd while another clone is opening it.
 */
asmlinkage long sys_close(unsigned int fd)
{
	struct file * filp;
	struct files_struct *files = current->files;

	write_lock(&files->file_lock);
	if (fd >= files->max_fds)
		goto out_unlock;
	filp = files->fd[fd];
	if (!filp)
		goto out_unlock;
	files->fd[fd] = NULL;
	FD_CLR(fd, files->close_on_exec);
	__put_unused_fd(files, fd);
	write_unlock(&files->file_lock);
	return filp_close(filp, files);

out_unlock:
	write_unlock(&files->file_lock);
	return -EBADF;
}

代码中FD_CLR以及有关的宏操作定义如下:

#define FD_CLR(fd,fdsetp)	__FD_CLR(fd,fdsetp)

#define __FD_CLR(fd,fdsetp) \
		__asm__ __volatile__("btrl %1,%0": \
			"=m" (*(__kernel_fd_set *) (fdsetp)):"r" ((int) (fd)))

它将位图files->close_on_exec的序号为fd的那一位清零。

函数__put_unused_fd的代码如下:

sys_close=>__put_unused_fd


static inline void __put_unused_fd(struct files_struct *files, unsigned int fd)
{
	FD_CLR(fd, files->open_fds);
	if (fd < files->next_fd)
		files->next_fd = fd;
}

代码的作者在sys_close的注释中讲述了在释放打开文件号之前先检查与其对应的file结构指针filp是否为0的重要性。一方面是因为在打开文件时分配打开文件号在前,而安装file结构指针在最后(见sys_open的代码)。另一方面,一个进程在fork子进程时可以选择让子进程共享而不是继承它的资源,包括其files_struct结构。这样,如果两个进程共享同一个files_struct结构,其中一个进程正在打开文件,已经分配了打开文件号,但是尚未安装file结构指针,而另一个进程却在中途挤进来关闭这个已打开文件而释放了这个打开文件号,那当然会出问题。

然后,就像sys_open的主体是filp_open一样,sys_close的主体是filp_close,其代码如下:

sys_close=>filp_close


/*
 * "id" is the POSIX thread ID. We use the
 * files pointer for this..
 */
int filp_close(struct file *filp, fl_owner_t id)
{
	int retval;

	if (!file_count(filp)) {
		printk("VFS: Close: file count is 0\n");
		return 0;
	}
	retval = 0;
	if (filp->f_op && filp->f_op->flush) {
		lock_kernel();
		retval = filp->f_op->flush(filp);
		unlock_kernel();
	}
	fcntl_dirnotify(0, filp, 0);
	locks_remove_posix(filp, id);
	fput(filp);
	return retval;
}

有些文件系统安排在关闭文件时冲刷文件的内容,即把文件已经改变过的内容写回设备上,并因而在其file_operations数据结构中提供相应的函数指针flush。不过,ext2并不做这样的安排,其函数指针flush为空指针,这一来关闭文件的操作就变得简单了。此外,当前进程可能对欲关闭的文件加了POSIX锁,但是忘了在关闭前把锁解除,所以调用locks_remove_posix试一下,以防万一。

最后,就是fput了。它递减file结构中的共享计数,如果递减后达到了0就释放该file结构有关的代码在include/linux/fs.h和fs/file_table.c中。我们应该注意到从sys_close开始我们并未见到与fput配对的fget。其实,这个计数是当初在打开文件时在get_empty_filp中设置成1的,所以这里的递减与此遥相呼应。至于这一次fput是否能使计数达到0,则取决于此时是否还有其他活动或进程在共享这个数据结构。例如,要是当初打开这个文件的进程,clone了一个线程,那就会在clone的时候递增这个计数,如果所创建的线程尚未关闭这个文件,则因共享计数大于1让它不会递减至0。

sys_close=>filp_close=>fput


void fput(struct file * file)
{
	struct dentry * dentry = file->f_dentry;
	struct vfsmount * mnt = file->f_vfsmnt;
	struct inode * inode = dentry->d_inode;

	if (atomic_dec_and_test(&file->f_count)) {
		locks_remove_flock(file);
		if (file->f_op && file->f_op->release)
			file->f_op->release(inode, file);
		fops_put(file->f_op);
		file->f_dentry = NULL;
		file->f_vfsmnt = NULL;
		if (file->f_mode & FMODE_WRITE)
			put_write_access(inode);
		dput(dentry);
		if (mnt)
			mntput(mnt);
		file_list_lock();
		list_del(&file->f_list);
		list_add(&file->f_list, &free_list);
		files_stat.nr_free_files++;
		file_list_unlock();
	}
}

在fput中又来处理当前进程可能已经对目标文件加上而为来得及解除的锁,但是这一次关心的是FL_FLOCK锁。如前所述,这种锁一定是协调锁;而刚才处理的是POSIX锁,它可以是协调锁也可以是强制锁。

代码中的fops_put是个宏操作:

sys_close=>filp_close=>fput=>fops_put

#define fops_put(fops) \
do {	\
	if ((fops) && (fops)->owner) \
		__MOD_DEC_USE_COUNT((fops)->owner);	\
} while(0)

显然,这里关心的是动态安装模块的使用计数。

此外,每种文件系统可以对file结构的释放规定一些附加操作,通过其file_operations结构中的函数指针release提供相应的操作,如果这个指针非0就表示需要调用这个函数。就ext2文件系统而言,这个函数就是ext2_release_file,其代码如下:

sys_close=>filp_close=>fput=>ext2_release_file

/*
 * Called when an inode is released. Note that this is different
 * from ext2_file_open: open gets called at every open, but release
 * gets called only when /all/ the files are closed.
 */
static int ext2_release_file (struct inode * inode, struct file * filp)
{
	if (filp->f_mode & FMODE_WRITE)
		ext2_discard_prealloc (inode);
	return 0;
}

操作简单,只是把预分配的数据块(见下一篇博客)释放掉而已。

把file结构释放以后,目标文件的dentry结构以及所在设备的vfsmount结构就少了一个用户,所以还要调用dput和mntput递减它们的共享计数。同样,如果递减后达到了0就要将数据结构释放。还有,如果当初打开这个文件的模式为写访问,则还要通过put_write_access递减其inode结构中的i_writecount计数,如前所述,这个计数用于按普通文件操作与内存映射访问文件这两种途径间的互斥。

最后,所谓释放file结构,就是把它从inode_hashtable中杂凑队列里脱链,退还到free_list中。

你可能感兴趣的:(ext2,linux内核,数据结构,linux)