unix编程以及xv6系统浅谈(一)文件I/O

932 阅读10分钟
这是新开的一个博客专题,将结合unix的一些系统库,系统调用等等,以及结合一个小的操作系统xv6的源码来仔细的剖析一些系统调用的用法和其中的实现,由于时间和篇幅的关系,没有办法作特别深入的调研,在这里只写一些浅显的东西,作抛砖引玉只用,若内容有误希望大家多多评论以斧正,谢谢

1.文件I/O

1.1 基本api接口

​ 本章描述的都是不带缓冲的I/O,即都是调用内核中的系统调用

​ 对内核而言,所有打开的文件都通过文件描述符来引用(非负整数),变化范围为0-OPEN_MAX-1(一般63)

​ shell 中将文件描述符0表示为输入,1表示为输出,2为标准错误

​ 同时可以使用宏STDIN_FILENO,STDOUT_FILENO,STDERR_FILENO来表示上述的文件

#include<fcntl.h>
int open(const char* path,int oflag,.../*mode_t mode*/)
    仅当创建新文件时才使用最后这个参数
    返回的文件描述符一定时最小的未用描述符数值
    path:
		要打开或者创建的文件名
	oflag:
		说明此函数的多个选项,可以用|来构成
		1.必须指定并且之能指定一个
				O_RDONLY	只读
				O_WRONLY	只写
				O_RDWR		读写打开
		2.
				O_APPEND		每次写时都追加到文件的尾端,注意是每次,由于该操作为原子操作,这样规避了多进程操作时的写覆盖之类的互斥问题!
				O_CREAT			若文件不存在则创建它,此时mode参数起作用,指定该文件的访问权限位
				O_EXCL			如果同时指定了O_CREAT,文件已经存在,则出错,可以测试文件存在,
								不存在则创建,这是一个完整的原子操作
				O_TRUNC			如果此文件存在,而且为可写打开,则文件长度截断为0
int openat(int fd,const char* path,int oflag,.../*mode_t mode*/)
	fd参数的3种3可能性
		1.path为绝对路径名,则忽略参数fd
		2.path为相对路径名,则fd指出相对路径名在系统中的开始地址,fd参数时通过打开相对路径所在目录获取
		3.fd为AT_FDCWD,则为当前工作目录
int creat(const char* path,mode_t mode)
	等效于open(path,O_WRONLY|O_CREAT|O_TRUNC,mode)
    注意以只写的方式打开所创建的文件,并且从头开始写
int	close(int fd)
    成功则返回0,否则-1
  	关闭一个文件以及在上面的所有记录锁,注意,进程终止时内核会自动关闭它所有的打开文件
off_t lseek(int fd,offset_t offset,int whence)
    每个文件都有自己的当前文件偏移量
    成功则返回新的文件偏移量,否则返回-1(用来判断套接字等不可以设置文件偏移量的文件)
    whence:
			SEEK_SET(0)	文件开始处
			SEEK_CUR(1)	当前文件偏移量所在位置
			SEEK_END(2)	文件结尾处
	offset:
			<0		向前移动
			>0		向后移动
ssize_t read(int fd,void* buf,size_t nbytes)
    返回读到的字节,结尾则为0,出错为-1
    从文件中读取nbytes字节的内容放入buf中
    例:文件还剩30字节,要求读100,则读取30,返回30,并在下一次读取返回0
ssize_t write(int fd,const void* buf,size_t nbytes)
    返回写入的字节数,出错返回-1
    一般情况返回值=nbytes,否则可能磁盘写满or超过了一个给定进程的文件长度限制

​ unix系统支持在不同进程间共享打开文件,下面介绍这种共享的数据结构

​ (1)pcb中拥有的文件描述符表

​ 文件描述符标志

​ 指向一个文件表项的指针

​ (2)内核维持的一张文件表

​ 文件状态标志(读写,添加等等)

​ 当前文件偏移量

​ 指向该文件V节点表项的指针

​ (3)v节点(Linux下面为一个与文件系统无关的i节点)

​ 指向i节点的指针

​ 若两个不同的进程同时用open打开同一个文件,两者拥有不同的vnode但是指向相同的inode

​ 若fork出子进程,则子进程各自的每一个打开文件描述符共享同一个文件表项,即偏移量相同

ssize_t pread(int fd,void* buf,size_t nbytes,off_t offset)
    返回读到的字节,文件尾为0,出错为-1
    调用pread相当于调用lseek后调用read,但是:
    	调用pread无法终端起定位操作
    	不更新当前文件偏移量
ssize_t pwrite(int fd,const void *buf,size_t nbytes,off_t offset)
    与上面的类似

​ 原子操作指的是由多步组成的一个操作,如果该操作原子的执行,则要么执行完所有,要么一步都不执行

//复制现有的文件描述符
int dup(int fd)
    返回当前可用文件描述符中的最小数值
int dup2(int fd,int fd2)
    用fd2指定新描述符的值,如果fd2已经打开,则将其先关闭,如果fd等于fd2,则返回fd2,而不关闭它
//注意,这些函数返回的新文件描述符与原有的共享一个文件表项,即拥有相同的文件偏移量

根据上面的文件操作的一些性质,来实现具体的shell中的重定位操作

case REDIR:	//输入输出重定位的情况 
    rcmd = (struct redircmd*)cmd;
    close(rcmd->fd);	//关闭文件操作符,分别是<或者> 
    if(open(rcmd->file, rcmd->mode) < 0){	//再次打开一个文件将被分配为最小未使用的即上面的fd 
      printf(2, "open %s failed\n", rcmd->file);
      exit();
    }
    runcmd(rcmd->cmd);	//递归的调用拆解后的命令(改过输入输出文件描述符后) 
    break;

同时也可以借助dup函数实现管道命令操作

case PIPE:	//管道|的实现 
    pcmd = (struct pipecmd*)cmd;
    if(pipe(p) < 0)
      panic("pipe");
    if(fork1() == 0){	//子进程1中
      close(1);		//关闭以挂载新的文件表项
      dup(p[1]);
      close(p[0]);	//关闭以防止read的阻塞等待
      close(p[1]);
      runcmd(pcmd->left);	//注意一般会调用exec,exec会替换调用它的进程的内存但是会保留它的文件描述符表
    }
    if(fork1() == 0){	//子进程2中
      close(0);
      dup(p[0]);
      close(p[0]);
      close(p[1]);
      runcmd(pcmd->right);
    }
    close(p[0]);
    close(p[1]);
    wait();
    wait();
    break;

当我们向文件写入数据的时候,内核通常先将数据复制到缓冲区中,然后排入队列,晚些时候再写入磁盘

void sync()
    将所有修改过的块缓冲区排入写队列,然后就返回,并不等待实际写磁盘操作结束
int fsync(int fd)
    只对由文件描述符fd指定的一个文件起作用,并且会等待磁盘操作结束
int fdatasync(int fd)
    只影响文件的数据部分,并不会更新文件的属性
int fcntl(int fd,int cmd,.../*int arg*/)
    返回值依赖于cmd,出错则返回-1
    (1)复制一个已有的描述符(F_DUPFD)
    		复制fd进入>=arg的最小值,与其共享同一文件表项
    (2)获取/设置文件描述符标志(F_GETFD,F_SETFD)
    (3)获取/设置文件状态标志(F_GETFL(arg一般设置0),F_SETFL)
    		getfl后需要与O_ACCMODE求&来获取状态的几个状态字
    			O_RDONLY	只读打开
    			O_WRONLY	只写打开
    			O_RDWR		读写打开
    			O_EXEC		只执行打开
    			O_SEARCH	只搜索打开目录
    		列:(val&O_ACCMODE) == O_RDONLY
    		可以直接与获得的状态字求与判断为真的几个状态字
    			O_APPEND	追加写
    			O_NONBLOCK	非阻塞模式
    			O_SYNC		等待写完成(数据和属性)
    		列:if(val&O_APPEND)
    	注意修改文件状态标志的时候,先获取一下文件 状态标志
    			添加状态:val|=flags;
				去除状态:val&=~flags;
	(4)获取/设置异步I/O所有权(F_GETDOWN,F_SETOWN)
	(5)获取/设置记录锁(F_GETLK,F_SETLK,F_SETLKW)

1.2 详细的实现原理

​ 参照xv6的内核源码,open中的实现是这样的

int sys_open(void)
{
  char *path;
  int fd, omode;
  struct file *f;
  struct inode *ip;
    //若输入的参数有问题则返回-1
  if(argstr(0, &path) < 0 || argint(1, &omode) < 0)
    return -1;
  begin_op();
    //如果设置了O_CREAT关键字则创建文件并且返回其inode
  if(omode & O_CREATE){
    ip = create(path, T_FILE, 0, 0);
    if(ip == 0){
      end_op();
      return -1;
    }
  } else {
      //否则如果输入的文件名没有找到则返回错误,否则返回找到的inode节点
    if((ip = namei(path)) == 0){
      end_op();
      return -1;
    }
    ilock(ip);
      //如果是目录文件则不可以进行除了读取之外的操作
    if(ip->type == T_DIR && omode != O_RDONLY){
      iunlockput(ip);
      end_op();
      return -1;
    }
  }
//通过filealloc()函数来向内核进程申请一个文件表项,fdalloc()函数来从pcb中获得最小的违背使用的文件描述符
  if((f = filealloc()) == 0 || (fd = fdalloc(f)) < 0){
    if(f)
      fileclose(f);
    iunlockput(ip);
    end_op();
    return -1;
  }
  iunlock(ip);
  end_op();
//设置文件表项,主要是设置文件类型为目录或者普通文件,由于管道是pipe创建所以不会出现open情况
    //同时设置文件中的偏移量,这里xv6中没有append关键字所以统一的设置为0
  f->type = FD_INODE;
  f->ip = ip;
  f->off = 0;
  f->readable = !(omode & O_WRONLY);
  f->writable = (omode & O_WRONLY) || (omode & O_RDWR);
  return fd;
}

​ 上述代码中的creat函数在xv6中的实现如下

static struct inode*
create(char *path, short type, short major, short minor)
{
  struct inode *ip, *dp;
  char name[DIRSIZ];
//获取上级目录的inode
  if((dp = nameiparent(path, name)) == 0)
    return 0;
  ilock(dp);
//检查同名文件是否存在
  if((ip = dirlookup(dp, name, 0)) != 0){
    iunlockput(dp);
    ilock(ip);
      //文件名存在并且creat为open调用则成功返回
    if(type == T_FILE && ip->type == T_FILE)
      return ip;
      //其他情况则会返回错误
    iunlockput(ip);
    return 0;
  }
//文件名不存在则会申请一个inode节点
  if((ip = ialloc(dp->dev, type)) == 0)
    panic("create: ialloc");

  ilock(ip);
  ip->major = major;
  ip->minor = minor;
  ip->nlink = 1;
  iupdate(ip);
//如果是目录则初始化.和..
  if(type == T_DIR){  // Create . and .. entries.
    dp->nlink++;  // for ".."
    iupdate(dp);
    // No ip->nlink++ for ".": avoid cyclic ref count.
    if(dirlink(ip, ".", ip->inum) < 0 || dirlink(ip, "..", dp->inum) < 0)
      panic("create dots");
  }
//
  if(dirlink(dp, name, ip->inum) < 0)
    panic("create: dirlink");

  iunlockput(dp);
//返回其inode
  return ip;
}

​ 至于close操作则还是较为简单的

int
sys_close(void)
{
  int fd;
  struct file *f;
 //判断是否存在这个文件描述符
  if(argfd(0, &fd, &f) < 0)
    return -1;
    //关闭pcb中的对应的文件描述符
  myproc()->ofile[fd] = 0;
  fileclose(f);
  return 0;
}
//调用的fileclose函数的源码
void fileclose(struct file *f)
{
  struct file ff;

  acquire(&ftable.lock);
  if(f->ref < 1)
    panic("fileclose");
    //减少文件计数,若该文件的计数为0则关闭这个文件
  if(--f->ref > 0){
    release(&ftable.lock);
    return;
  }
  ff = *f;
  f->ref = 0;
  f->type = FD_NONE;
  release(&ftable.lock);
//关闭文件
  if(ff.type == FD_PIPE)
    pipeclose(ff.pipe, ff.writable);
  else if(ff.type == FD_INODE){
    begin_op();
    iput(ff.ip);
    end_op();
  }
}

​ 下面介绍read和write的实现方式

int
sys_read(void)
{
  struct file *f;
  int n;
  char *p;
//同上
  if(argfd(0, 0, &f) < 0 || argint(2, &n) < 0 || argptr(1, &p, n) < 0)
    return -1;
  return fileread(f, p, n);
}
int
fileread(struct file *f, char *addr, int n)
{
  int r;
//判断是否可以读
  if(f->readable == 0)
    return -1;
    //管道文件则使用管道的读取方法
  if(f->type == FD_PIPE)
    return piperead(f->pipe, addr, n);
    //读取文件信息
  if(f->type == FD_INODE){
    ilock(f->ip);
      //读取到相关字节后则将文件偏移量则加上相关的读取字节数
    if((r = readi(f->ip, addr, f->off, n)) > 0)
      f->off += r;
    iunlock(f->ip);
    return r;
  }
  panic("fileread");
}
int
sys_write(void)
{
  struct file *f;
  int n;
  char *p;
//同上
  if(argfd(0, 0, &f) < 0 || argint(2, &n) < 0 || argptr(1, &p, n) < 0)
    return -1;
  return filewrite(f, p, n);
}
int
filewrite(struct file *f, char *addr, int n)
{
  int r;

  if(f->writable == 0)
    return -1;
  if(f->type == FD_PIPE)
    return pipewrite(f->pipe, addr, n);
  if(f->type == FD_INODE){
    // write a few blocks at a time to avoid exceeding
    // the maximum log transaction size, including
    // i-node, indirect block, allocation blocks,
    // and 2 blocks of slop for non-aligned writes.
    // this really belongs lower down, since writei()
    // might be writing a device like the console.
    int max = ((MAXOPBLOCKS-1-1-2) / 2) * 512;
    int i = 0;
    while(i < n){
      int n1 = n - i;
      if(n1 > max)
        n1 = max;

      begin_op();
      ilock(f->ip);
       //写入后更新文件偏移量
      if ((r = writei(f->ip, addr + i, f->off, n1)) > 0)
        f->off += r;
      iunlock(f->ip);
      end_op();

      if(r < 0)
        break;
      if(r != n1)
        panic("short filewrite");
      i += r;
    }
    return i == n ? n : -1;
  }
  panic("filewrite");
}

​ dup的原理不同于open,是不会申请一个新的文件表项的

int
sys_dup(void)
{
  struct file *f;
  int fd;

  if(argfd(0, 0, &f) < 0)
    return -1;
    //从pcb中的文件描述符表中申请
  if((fd=fdalloc(f)) < 0)
    return -1;
    //增加文件引用计数
  filedup(f);
  return fd;
}