linux0.11源码分析-打开与读取文件

1,167 阅读11分钟

open

如果打开的文件不存在并且flag带有O_CREAT标志,就会创建一个新的inode添加到目录项中。如果打开的文件已经存在,就会把指定的inode信息读取到缓冲区。open还会向内核的文件表中申请一个空间的file,在自己的进程空间的文件表中申请一个空位给file,并返回文件描述符(就是数组的下标)。

lib/open.c

int open(const char * filename, int flag, ...)
{
	register int res;
	va_list arg;

	va_start(arg,flag);
	__asm__("int $0x80"
		:"=a" (res)
		:"0" (__NR_open),"b" (filename),"c" (flag),
		"d" (va_arg(arg,int)));
	if (res>=0)
		return res;
	errno = -res;
	return -1;
}

调用中断80,就会调用system_call函数,该函数位于kernel/system_call.s 中,间接就会调用sys_open函数,注意此时已经从用户态转变成内核态了。

sys_open

fs/open.c

flag是打开文件标志,它可取值:O_RDONLY(只读)、O_WRONLY(只写)或O_RDWR(读写),以及O_EXCL(被创建文件必须不存在)、O_APPEND(在文件尾添加数据)等其他一些标志的组合。这些标志位于include/fcntl.h文件中。

当执行execve函数时,进程此时将完全被新程序替换掉,那么从父进程那里继承来的文件描述符也就没有意义了,close_on_exec就是这是这样的一个标志位。只要该标志位被置位1,执行execve就会把对应的文件描述符关闭。

int sys_open(const char * filename,int flag,int mode)
{
	struct m_inode * inode; //i-节点
	struct file * f;
	int i,fd;

	mode &= 0777 & ~current->umask; // 当前进程对该文件的模式,就是可读、可写、可执行这些
    
	for(fd=0 ; fd<NR_OPEN ; fd++)  //NR_OPEN是20,也就是说一个进程最多打开20个文件
		if (!current->filp[fd]) // filp是数组,fd就是文件描述符,也就是数组的索引
			break;
            
	if (fd>=NR_OPEN)  //检查有没有超过20个文件
		return -EINVAL;
       
	current->close_on_exec &= ~(1<<fd); // 执行execve时需要关闭的文件描述符
    
	f=0+file_table;
	for (i=0 ; i<NR_FILE ; i++,f++) //在文件表中寻找一个空闲结构项
		if (!f->f_count) break;
	if (i>=NR_FILE)
		return -EINVAL;
      
       //进程对应文件句柄fd的文件结构指针指向搜索到的文件结构,并令文件用计数递增1
       //再进程文件表中占据了一个位置,但是此时还不知道inode是多少
	(current->filp[fd]=f)->f_count++;  
    
        //根据文件名找到inode,找不到就是失败
	if ((i=open_namei(filename,flag,mode,&inode))<0) {
		current->filp[fd]=NULL;
		f->f_count=0;
		return i;
	}
    
    // 根据已打开文件的i节点的属性字段,我们可以知道文件的具体类型。对于不同类
    // 型的文件,我们需要操作一些特别的处理。如果打开的是字符设备文件,那么对于
    // 主设备号是4的字符文件(例如/dev/tty0),如果当前进程是组首领并且当前进程的
    // tty字段小于0(没有终端),则设置当前进程的tty号为该i节点的子设备号,并设置
    // 当前进程tty对应的tty表项的父进程组号等于当前进程的进程组号。表示为该进程
    // 组(会话期)分配控制终端。对于主设备号是5的字符文件(/dev/tty),若当前进
    // 程没有tty,则说明出错,于是放回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;
			}
	}
    // 如果打开的是块设备文件,则检查盘片是否更换过。若更换过则需要让高速缓冲区
    // 中该设备的所有缓冲块失败。
	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;
	f->f_pos = 0;
	return (fd);
}

先从进程中找到一个可用的文件描述符fd,然后在文件表中找到一个引用数为0的file,让这个fd的位置指向file。

open_namei

fs/namei.c

O_TRUNC表示若文件存在,则长度被截为0,属性不变。 O_ACCMODE的作用是取出flag的低2位,0_RDONLY(00)只读打开,O_WRONLY(01)只写打开,O_RDWR(02)读写打开。所以if ((flag & O_TRUNC) && !(flag & O_ACCMODE))就表示如果O_TRUNC标志打开,那么就一定要有写标志。

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;  //添上普通文件标志
    
    	//找到该文件的最顶层目录
	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;
	}
    
        // 从目录项中找到指定名字的inode,并将其读取到缓冲区
	bh = find_entry(&dir,basename,namelen,&de);
    
       //如果该高速缓冲指针为NULL,则表示没有找到对应文件名的目录项,因此只可能是创建文件操作
	if (!bh) {
		if (!(flag & O_CREAT)) {  // 如果不是创建文件,则释放该目录的i 节点
			iput(dir);
			return -ENOENT;
		}
		if (!permission(dir,MAY_WRITE)) {  // 如果用户在该目录没有写的权力,则释放该目录的i 节点
			iput(dir);
			return -EACCES;
		}
        
        	//在目录节点对应的设备上申请一个新i 节点,若失败,则释放目录的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;  //新创建的inode,需要回写到磁盘
        
		bh = add_entry(dir,basename,namelen,&de); //在指定目录dir 中添加一新目录项
        
   
   	        // 如果返回的应该含有新目录项的高速缓冲区指针为NULL,则表示添加目录项操作失败。于是将该
        	// 新i 节点的引用连接计数减1;并释放该i 节点与目录的i 节点
		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;
	}
    
    
    // 若上面在目录中取文件名对应目录项结构的操作成功(即bh不为NULL),则说明指定打开的文件已
    // 经存在。于是取出该目录项的i节点号和其所在设备号,并释放该高速缓冲区以及放回目录的i节点
    // 如果此时堵在操作标志O_EXCL置位,但现在文件已经存在,则返回文件已存在出错码退出。
	inr = de->inode;
	dev = dir->i_dev;
	brelse(bh);
	iput(dir);
	if (flag & O_EXCL)
		return -EEXIST;
        
    // 然后我们读取该目录项的i节点内容。若该i节点是一个目录i节点并且访问模式是只写或读写,或者
    // 没有访问的许可权限,则放回该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;
	}
    // 接着我们更新该i节点的访问时间字段值为当前时间。如果设立了截0标志,则将该i节点的文件长度
    // 截0.最后返回该目录项i节点的指针,并返回0(成功)。
	inode->i_atime = CURRENT_TIME;
	if (flag & O_TRUNC)
		truncate(inode);
	*res_inode = inode;
	return 0;
}

dir_namei文件分析看这里

sys_read

fd就是使用open返回的文件描述符,buf在用户空间,count读取多少字节。

fs/read_write.c

int sys_read(unsigned int fd,char * buf,int count)
{
	struct file * file;
	struct m_inode * inode;

	if (fd>=NR_OPEN || count<0 || !(file=current->filp[fd]))
		return -EINVAL;
	if (!count)
		return 0;
        
    // 然后验证存放数据的缓冲区内存限制。并取文件的i节点。用于根据该i节点的属性,分
    // 别调用相应的读操作函数。若是管道文件,并且是读管道文件模式,则进行读管道操作,
    // 若成功则返回读取的字节数,否则返回出错码,退出。如果是字符型文件,则进行读
    // 字符设备操作,并返回读取的字符数。如果是块设备文件,则执行块设备读操作,并
    // 返回读取的字节数。
	verify_area(buf,count);
	inode = file->f_inode;
	if (inode->i_pipe)
		return (file->f_mode&1)?read_pipe(inode,buf,count):-EIO;
	if (S_ISCHR(inode->i_mode))
		return rw_char(READ,inode->i_zone[0],buf,count,&file->f_pos);
	if (S_ISBLK(inode->i_mode))
		return block_read(inode->i_zone[0],&file->f_pos,buf,count);
        
    // 如果是目录文件或者是常规文件,则首先验证读取字节数count的有效性并进行调整(若
    // 读去字节数加上文件当前读写指针值大于文件长度,则重新设置读取字节数为文件长度
    // -当前读写指针值,若读取数等于0,则返回0退出),然后执行文件读操作,返回读取的
    // 字节数并退出。
	if (S_ISDIR(inode->i_mode) || S_ISREG(inode->i_mode)) {
		if (count+file->f_pos > inode->i_size)
			count = inode->i_size - file->f_pos;
		if (count<=0)
			return 0;
		return file_read(inode,file,buf,count);
	}
    // 执行到这里,说明我们无法判断文件的属性。则打印节点文件属性,并返回出错码退出。
	printk("(Read)inode->i_mode=%06o\n\r",inode->i_mode);
	return -EINVAL;
}

verify_area

对于80386 CPU,在执行特权级0代码时不会理会用户空间中的页面是否是也保护的, 因此在执行内核代码时用户空间中数据页面来保护标志起不了作用,写时复制机制也就失去了作用。verify_area()函数就用于此目的。但对于80486或后来的CPU,其 控制寄存器CRO中有一个写保护标志WP(位16),内核可以通过设置该标志来禁止特权级0的代码向用户空间只读页面执行写数据,否则将导致发生写保护异常。从而486以上CPU可以通过设置该标志来达到本函数的目的。该函数对当前进程逻辑地址从addr到addr+size这一段范围以页为单位执行写操作前的检测操作。由于检测判断是以页面为单位进行操作,因此程序首先需要找出addr所在页面开始地址start,然后start加上进程数据段基址,使这个start变成CPU 4G线性 空间中的地址。最后循环调用write_verify()对指定大小的内存空间进行写前验证。 若页面是只读的,则执行共享检验和复制页面操作。

kernel/fork.c

void verify_area(void * addr,int size)
{
	unsigned long start;

    // 首先将起始地址start调整为其所在左边界开始位置,同时相应地调整验证区域
    // 大小。下句中的start& 0xfff 用来获得指定起始位置addr(也即start)在所在
    // 页面中的偏移值,原验证范围size加上这个偏移值即扩展成以addr所在页面起始
    // 位置开始的范围值。因此在下面也需要把验证开始位置start调整成页面边界值。
	start = (unsigned long) addr;
	size += start & 0xfff;
	start &= 0xfffff000;            // 此时start是当前进程空间中的逻辑地址。
    // 下面start加上进程数据段在线性地址空间中的起始基址,变成系统整个线性空间
    // 中的地址位置。对于linux-0.11内核,其数据段和代码在线性地址空间中的基址
    // 和限长均相同。
	start += get_base(current->ldt[2]);
	while (size>0) {
		size -= 4096;
		write_verify(start);
		start += 4096;
	}
}

file_read

file中保存着文件的读取偏移地址,使用这个算出文件的逻辑块号nr,将该块号的信息读取到缓冲区,然后一个一个字节的复制到用户空间。

int file_read(struct m_inode * inode, struct file * filp, char * buf, int count)
{
	int left,chars,nr;
	struct buffer_head * bh;

    // 首先判断参数的有效性。若需要读取的字节数count小于等于0,则返回0.若还需要读
    // 取的字节数不等于0,就循环执行下面操作,直到数据全部读出或遇到问题。在读循环
    // 操作过程中,我们根据i节点和文件表结构信息,并利用bmap()得到包含文件当前读写
    // 位置的数据块在设备上对应的逻辑块号nr。若nr不为0,则从i节点指定的设备上读取该
    // 逻辑块。如果读操作是吧则退出循环。若nr为0,表示指定的数据块不存在,置缓冲块
    // 指针为NULL。(filp->f_pos)/BLOCK_SIZE用于计算出文件当前指针所在的数据块号。
    
	if ((left=count)<=0)  //若需要读取的字节数count小于等于0,则返回0
		return 0;
        
	while (left) {
		if ((nr = bmap(inode,(filp->f_pos)/BLOCK_SIZE))) {
			if (!(bh=bread(inode->i_dev,nr)))
				break;
		} else
			bh = NULL;
        // 接着我们计算文件读写指针在数据块中的偏移值nr,则在该数据块中我们希望读取的
        // 字节数为(BLOCK_SIZE-nr)。然后和现在还需读取的字节数left做比较。其中小值
        // 即为本次操作需读取的字节数chars。如果(BLOCK_SIZE-nr) > left,则说明该块
        // 是需要读取的最后一块数据。反之还需要读取下一块数据。之后调整读写文件指针。
        // 指针前移此次将读取的字节数chars,剩余字节计数left相应减去chars。
		nr = filp->f_pos % BLOCK_SIZE;
		chars = MIN( BLOCK_SIZE-nr , left );
		filp->f_pos += chars;
		left -= chars;
        // 若上面从设备上读到了数据,则将p指向缓冲块中开始读取数据的位置,并且复制chars
        // 字节到用户缓冲区buf中。否则往用户缓冲区中填入chars个0值字节。
		if (bh) {
			char * p = nr + bh->b_data;
			while (chars-->0)
				put_fs_byte(*(p++),buf++);
			brelse(bh);
		} else {
			while (chars-->0)
				put_fs_byte(0,buf++);
		}
	}
    // 修改该i节点的访问时间为当前时间。返回读取的字节数,若读取字节数为0,则返回
    // 出错号。CURRENT_TIME是定义在include/linux/sched.h中的宏,用于计算UNIX时间。
    // 即从1970年1月1日0时0分0秒开始,到当前的时间,单位是秒。
	inode->i_atime = CURRENT_TIME;
	return (count-left)?(count-left):-ERROR;
}