leevis.com icon indicating copy to clipboard operation
leevis.com copied to clipboard

linux-0.11打开文件

Open vislee opened this issue 3 years ago • 0 comments

概述

linux中一切兼文件,本文分析的就是存在块设备,准确的说是磁盘中的文件打开过程,也就是open函数的实现。

代码分析

上一篇中就说明了文件系统结构。磁盘块2是超级块,存放了保存了i 节点个数、逻辑块个数、i节点位图所占用的数据块个数、逻辑块位图所占用的数据块个数、第一个数据逻辑块等等。具体参考fs.h中定义的d_super_block结构体。 磁盘上一个文件或目录,对应一个i节点和1个或多个数据块,而目录的数据块保存的就是目录下文件和对应的i节点号,参考fs.h定义的dir_entry 结构体。 在任务1中调用的初始化init函数,调用了 setup((void *) &drive_info); 函数,该函数调用了 mount_root函数挂载根文件系统。 挂载要做的一个工作就是把磁盘超级块读到内存中 super_block[NR_SUPER] 数组中。 初始化工作完成后,我们从open函数跟踪一次文件打开,因为open是用户态的系统调用,因此对应内核函数是sys_open

先看下进程打开一个文件对应的结构体

fs.h

struct file {
	unsigned short f_mode; // 文件操作模式
	unsigned short f_flags;  // 文件打开和控制标志
	unsigned short f_count; // 文件引用计数值
	struct m_inode * f_inode; // 对应的i节点
	off_t f_pos;  // 文件位置
};

struct dir_entry {
	unsigned short inode;  // 目下文件对应的I节点号
	char name[NAME_LEN]; // 目录下文件的名称
};

open.c 文件


// 打开一个文件,对应用户态open的三个参数。
int sys_open(const char * filename,int flag,int mode)
{
	struct m_inode * inode;
	struct file * f;
	int i,fd;

	mode &= 0777 & ~current->umask;
       // 从进程打开文件数组中找一个空位。
	for(fd=0 ; fd<NR_OPEN ; fd++)
		if (!current->filp[fd])
			break;
	if (fd>=NR_OPEN)
		return -EINVAL;
        // 默认是清除执行时关闭标志位。该标志用一个无符号长整型标识,每bit标志一个打开的文件。
	current->close_on_exec &= ~(1<<fd);
        // 从系统打开文件数组获取一个文件表项,也就是一个空闲的file结构体。
	f=0+file_table;
	for (i=0 ; i<NR_FILE ; i++,f++)
		if (!f->f_count) break;
	if (i>=NR_FILE)
		return -EINVAL;
        // fd是进程中文件指针数组的下标,也就是用户态open返回的文件句柄。
	(current->filp[fd]=f)->f_count++;
        // 根据文件名,从磁盘获取对应的i节点
	if ((i=open_namei(filename,flag,mode,&inode))<0) {
		current->filp[fd]=NULL;
		f->f_count=0;
		return i;
	}
/* ttys are somewhat special (ttyxx major==4, tty major==5) */
	if (S_ISCHR(inode->i_mode))
		if (MAJOR(inode->i_zone[0])==4) {
			if (current->leader && current->tty<0) {
				current->tty = MINOR(inode->i_zone[0]);
				tty_table[current->tty].pgrp = current->pgrp;
			}
		} else if (MAJOR(inode->i_zone[0])==5)
			if (current->tty<0) {
				iput(inode);
				current->filp[fd]=NULL;
				f->f_count=0;
				return -EPERM;
			}
/* Likewise with block-devices: check for floppy_change */
	if (S_ISBLK(inode->i_mode))
		check_disk_change(inode->i_zone[0]);  // 检查软盘是否变化,磁盘不存在变化。
	f->f_mode = inode->i_mode;
	f->f_flags = flag;
	f->f_count = 1;
	f->f_inode = inode;  // 打开文件的i节点
	f->f_pos = 0;
	return (fd);
}

namei.c 文件

int open_namei(const char * pathname, int flag, int mode,
	struct m_inode ** res_inode)
{
	const char * basename;
	int inr,dev,namelen;
	struct m_inode * dir, *inode;
	struct buffer_head * bh;
	struct dir_entry * de;

	if ((flag & O_TRUNC) && !(flag & O_ACCMODE))
		flag |= O_WRONLY;
	mode &= 0777 & ~current->umask;
	mode |= I_REGULAR;
        // 查找返回最后一级目录i节点和文件名
	if (!(dir = dir_namei(pathname,&namelen,&basename)))
		return -ENOENT;
	if (!namelen) {			/* special case: '/usr/' etc */
		if (!(flag & (O_ACCMODE|O_CREAT|O_TRUNC))) {
			*res_inode=dir;
			return 0;
		}
		iput(dir);
		return -EISDIR;
	}
        // 从最后一级目录下查找文件
	bh = find_entry(&dir,basename,namelen,&de);
        // 没找到文件
	if (!bh) {
                // 是否要新建
		if (!(flag & O_CREAT)) {
			iput(dir);
			return -ENOENT;
		}
                // 是否有目录写权限
		if (!permission(dir,MAY_WRITE)) {
			iput(dir);
			return -EACCES;
		}
                // 新建i节点
		inode = new_inode(dir->i_dev);
		if (!inode) {
			iput(dir);
			return -ENOSPC;
		}
		inode->i_uid = current->euid;
		inode->i_mode = mode;
		inode->i_dirt = 1;
		bh = add_entry(dir,basename,namelen,&de);
		if (!bh) {
			inode->i_nlinks--;
			iput(inode);
			iput(dir);
			return -ENOSPC;
		}
		de->inode = inode->i_num;
		bh->b_dirt = 1;
		brelse(bh);
		iput(dir);
		*res_inode = inode;
		return 0;
	}
	inr = de->inode;
	dev = dir->i_dev;
	brelse(bh);
	iput(dir);
	if (flag & O_EXCL)
		return -EEXIST;
        // 获取文件i 节点
	if (!(inode=iget(dev,inr)))
		return -EACCES;
	if ((S_ISDIR(inode->i_mode) && (flag & O_ACCMODE)) ||
	    !permission(inode,ACC_MODE(flag))) {
		iput(inode);
		return -EPERM;
	}
        // 更新文件访问时间戳
	inode->i_atime = CURRENT_TIME;
	if (flag & O_TRUNC)
		truncate(inode);
	*res_inode = inode;
	return 0;
}

// 返回最后一级目录的i节点和 文件名,文件名通过name和namelen指定。
static struct m_inode * dir_namei(const char * pathname,
	int * namelen, const char ** name)
{
	char c;
	const char * basename;
	struct m_inode * dir;

	if (!(dir = get_dir(pathname)))
		return NULL;
	basename = pathname;
	while (c=get_fs_byte(pathname++))
		if (c=='/')
			basename=pathname;
	*namelen = pathname-basename-1;
	*name = basename;
	return dir;
}


// 根据参数路径名开始搜索直到达到最顶端目录
// 根据文件名是绝对路径还是相对路径从进程对应i节点开始查找。
// 调用find_entry函数从当前目录i节点 查找下一级目录的目录项。
// 根据目录项中的i节点号,调用iget获取对应i节点号的i节点。
static struct m_inode * get_dir(const char * pathname)
{
	char c;
	const char * thisname;
	struct m_inode * inode;
	struct buffer_head * bh;
	int namelen,inr,idev;
	struct dir_entry * de;

        // 进程根目录
	if (!current->root || !current->root->i_count)
		panic("No root inode");
        // 进程工作目录
	if (!current->pwd || !current->pwd->i_count)
		panic("No cwd inode");
        // 是否是绝对路径
	if ((c=get_fs_byte(pathname))=='/') {
                // 绝对路径是从根目录开始查找
		inode = current->root;
		pathname++;
	} else if (c)
                // 相对路径是从进程工作目录开始查找
		inode = current->pwd;
	else
		return NULL;	/* empty name is bad */
	inode->i_count++;
	while (1) {
		thisname = pathname;
                // 如果不是目录, 没有对目录的访问权限直接返回null。
		if (!S_ISDIR(inode->i_mode) || !permission(inode,MAY_EXEC)) {
			iput(inode);
			return NULL;
		}
		for(namelen=0;(c=get_fs_byte(pathname++))&&(c!='/');namelen++)
			/* nothing */ ;
		if (!c)。// 已经是最顶端目录了,直接返回对应的inode
			return inode;
                // 从目录下查找子目录
		if (!(bh = find_entry(&inode,thisname,namelen,&de))) {
			iput(inode);
			return NULL;
		}
		inr = de->inode;
		idev = inode->i_dev;
		brelse(bh);
		iput(inode);
                // 读取设备上对应i节点号的i节点
		if (!(inode = iget(idev,inr)))
			return NULL;
	}
}

我们以`/etc/rc`为例,再执行一次该函数。
首先判断当前进程是否指定了根目录和工作目录,如果没有则系统异常。
文件是绝对路径,则从当前进程根目录`current->root`指定的i节点遍历。
进入循环后,先进行权限判断。 然后解析文件路径,`thisname`和`namelen` 是下一级目录,例如`etc`、`rc`等。
如果已经解析完文件路径,则返回。在上例中返回的实际是etc对应的i节点。
调用`find_entry`函数从当前目录查找下一级目录, 例子中从根目录查找`etc/`
然后调用`iget`函数获取下一级目录的i 节点,继续循环。


// 从指定目录查找指定文件的目录项,
// 找到对应文件则,res_dir为文件目录项,返回包含文件的数据块的缓冲块。否则返回NULL。
static struct buffer_head * find_entry(struct m_inode ** dir,
	const char * name, int namelen, struct dir_entry ** res_dir)
{
	int entries;
	int block,i;
	struct buffer_head * bh;
	struct dir_entry * de;
	struct super_block * sb;

#ifdef NO_TRUNCATE
	if (namelen > NAME_LEN)
		return NULL;
#else
	if (namelen > NAME_LEN)
		namelen = NAME_LEN;
#endif
        // 目录下有文件+子目录个数
	entries = (*dir)->i_size / (sizeof (struct dir_entry));
	*res_dir = NULL;
	if (!namelen)
		return NULL;
/* check for '..', as we might have to do some "magic" for it */
	if (namelen==2 && get_fs_byte(name)=='.' && get_fs_byte(name+1)=='.') {
/* '..' in a pseudo-root results in a faked '.' (just change namelen) */
		if ((*dir) == current->root)
			namelen=1;
		else if ((*dir)->i_num == ROOT_INO) {
/* '..' over a mount-point results in 'dir' being exchanged for the mounted
   directory-inode. NOTE! We set mounted, so that we can iput the new dir */
			sb=get_super((*dir)->i_dev);
			if (sb->s_imount) {
				iput(*dir);
				(*dir)=sb->s_imount;
				(*dir)->i_count++;
			}
		}
	}
        // 数据块
	if (!(block = (*dir)->i_zone[0]))
		return NULL;
        // 读取第一个数据块内容
	if (!(bh = bread((*dir)->i_dev,block)))
		return NULL;
	i = 0;
        // 目录节点数据块保存的就是目录项和i节点,强制转换成对应的结构体。
	de = (struct dir_entry *) bh->b_data;
        // 遍历第一个数据块的目录项
	while (i < entries) {
                // 一个数据块的目录项都遍历完则再读区一个数据块
		if ((char *)de >= BLOCK_SIZE+bh->b_data) {
			brelse(bh);
			bh = NULL;
			if (!(block = bmap(*dir,i/DIR_ENTRIES_PER_BLOCK)) ||
			    !(bh = bread((*dir)->i_dev,block))) {
				i += DIR_ENTRIES_PER_BLOCK;
				continue;
			}
			de = (struct dir_entry *) bh->b_data;
		}
                // 比较文件名和目录项中的名称,一致则找到,返回对应文件目录项所在数据块的缓冲块
		if (match(namelen,name,de)) {
			*res_dir = de;
			return bh;
		}
		de++;
		i++;
	}
        // 没找到目标文件则返回空,并释放缓冲块
	brelse(bh);
	return NULL;
}

总结

最后总结一下,用户程序调用glibc提供的open函数打开一个文件时,会触发int 0x80中断,调用内核函数sys_opensys_open函数从进程打开文件数组filp中获取一个空项,空项数组下标就是open函数返回的文件句柄fd。然后再从全局打开文件数组file_table中获取一个结构体,作为进程打开文件数组filp[fd]的值。然后调用open_namei函数查找磁盘文件的i节点,赋给打开文件结构体的f_inode,把进程打开文件数组下标fd返回,作为open函数的返回值。

vislee avatar Dec 18 '21 16:12 vislee