这是新开的一个博客专题,将结合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;
}