Unix标准为程序员提供了一系列的通用IO(无缓冲IO)接口以实现文件读写操作,原书第三章主要所涉及到的系统接口主要针对普通文件为操作目标。通用IO相比于标准IO的最大的区别在于没有缓冲机制,只要调用一次标准IO,就进行IO,而不是像标准IO一样具有缓冲的特点,通用IO的可移植性稍逊色于标准IO (Unix和Windows各有其通用IO接口,但标准IO接口一致)
在开始介绍这些系统接口之前,需要先了解一下Unix环境中对于打开的文件是如何管理的
文件共享
Unix中支持多个进程共享同一个文件,也就是说2个独立的进程打开同一份文件互不影响(在不加文件锁的情况下),但是这仅限于打开文件,对于文件的读写是会有一定的影响的。Unix对打开的文件通过如下三种内核数据结构实现管理。
- 文件描述符表:每一个进程都会维护一张私有的文件描述符表,文件描述符表中记录了进程已经打开的文件,每一个表项由文件描述符标识和文件指针构成,其中文件指针指向内核文件表项
- 文件表项:文件表项是内核为维护每一个进程打开的文件所对应的文件状态信息而定义的一个结构,每一个文件表项由文件状态标识、当前偏移量、v节点指针,文件状态标识保存了对应进程对文件的读写权限信息,偏移量保存对应进程在文件中的’‘光标’'位置
- v节点: v节点是真正意义上的已打开文件的唯一标识,v节点是文件打开时从磁盘读入内存的,其中最重要的信息就是i节点,i节点保存了文件的各种属性信息,包括文件的磁盘地址 (Linux直接用i节点替代v节点)
多进程打开同一份文件时,文件描述符表各个进程独有一份,文件表项与进程一一对应,但是v节点全局只有一份;文件表项与v节点是多对一的关系
系统API
open / openat /close
- open打开相应文件并返回对应文件描述符,通过cmd参数设置打开方式,不定参数用于当cmd中含O_CREAT时新文件的初始权限
- openat用于在指定的目录下打开文件,其余参数与open类似,openat可以改变目标文件的相对路径(open中如果使用相对路径只能时当前路径或上一级路径),在使用openat之前必须使用open打开一个目录文件获得其文件描述符
- close关闭打开的文件
== 下述给出常见的文件打开方式
- 只读 O_RDONLY
- 只写 O_WRONLY
- 读写 O_RDWR
- 不存在则创建 O_CREAT
- 追加 O_APPEND
- 打开目录 O_DIRECTORY
- 重写 O_TRUNC
通过按位或实现多个打开方式的整合,但请不要把冲突的打开方式一起设置(不能只读且只写),更详细的打开方式宏可以参见附录 ==
eg:
#define pi(x) printf("%ld\n",(long int)x)int fd=open("file",O_CREAT|O_RDWR,0666);//file以工作目录为起点检索pi(fd);close(fd);fd=open("dir",__O_DIRECTORY);int _fd=openat(fd,"file",O_CREAT,0666);//file以dir目录为起点检索close(_fd);close(fd);
creat: 以只写方式创建一个文件,是历史遗留物,现在一般不用,可以被open完全代替
read
从文件中读取字节长度(从光标处开始读取),具体内容存入用户缓冲区,返回值为实际读取到个字节数,一般返回值等于指定值,除非已经读到文件末尾(更多情况参见附录)
write
从用户缓冲区中写书指定长度字节到文件,返回值为实际写入的个数
eg:
char buf[32]={0};pi(read(STDIN_FILENO,buf,sizeof buf -1));//从标准输入读取sizeof buf -1字节//! 一定要-1,预留用户缓冲区结束的标识\0//! 否则后续回显会出现乱码pi(write(STDOUT_FILENO,buf,strlen(buf)));//写回标准输出strlen(buf)字节
lseek
移动进程在文件处的光标位置,并返回处理后的相对于文件开头偏移量
whence为三个宏常量
- 文件开始SEEK_SET
- 当前位置SEEK_CUR
- 文件末尾SEEK_END
offset尾相对于whence的偏移量,其值可正可负
当whence为SEEK_SET是offset不可为负,为负数判定为0
文件空洞:
当lseek把文件光标设置到文件末尾后正数位置时,函数不会出错,但是会在中间产生一段空白区域,这个区域成为文件空洞,空洞区域以\0填充,系统并不会为这些空洞区域分配具体的磁盘空间,但是使用ls命令时还是会计算空洞字符
int fd1=open("nohole",O_CREAT|O_RDWR,0666);write(fd1,"hello",5);close(fd1);int fd2=open("hole",O_CREAT|O_RDWR,0666);lseek(fd2,30,SEEK_END);write(fd2,"hello",5);close(fd2);
通过ls -l命令查看文件的属性(30个空洞字符被计算)
通过od -c命令以字符形式显示文件内容(空洞字符为\0)
通过du -s命令查看文件实际分配到的磁盘块数(为了效果更明显,此处创建一个足够大的空洞文件)
两个文件所分配到的磁盘块数一致,这也说明了空洞区域不会被分配盘块
如果完整的拷贝空洞文件,那么空洞字符也会被拷贝并且分配盘块
pread / pwrite
如果多个进程对文件的同一片预取进行读写操作(都是读除外),就容易出现数据不一致的情况,这种现象涉及到了进程调度的问题,为了保证读写的原子性,Unix标准也提供了带有原子性的读写API,pread和pwrite,其使用方法与read和write类似,只不过多了从什么位置开始
dup / dup2
Unix允许进程文件描述符表中存在指向相同文件的文件描述符,这些文件描述符指向相同的文件表项,它们共享文件状态标识和偏移量
- dup 返回一个指向文件表项与参数fd相同的新文件描述符
- dup2 用于文件描述符重定位,将fd2指向fd,会先关闭fd2指向的原文件(dup2具有原子性)
dup图示
fcntl
fcntl有五大功能,具体下述:
- 复制文件描述符F_DUPFD
- 获取/改变文件描述符F_GETFD/F_SETFD
- 获取/改变文件属性F_GETFL/F_SETFL
- 获取/设置同步异步权F_GETOWN/F_SETOWN
- 获取/设置文件锁F_GETLK/F_SETLK
其中最为常用的是通过fcntl获取与设置文件属性
eg:
int fd=creat("file",0666);int flag=fcntl(fd,F_GETFL);//获取文件当前打开属性if(flag&O_ACCMODE==O_WRONLY) printf("write only\n");//判断是否为只写//对于判断只读、只写、读写时需要按位与O_ACCMODE,其他属性判断不需要fcntl(fd,F_SETFL,O_RDONLY);//将文件打开属性变更为只读//!设置之后会关掉原有的只读属性//!如果是增加一个属性,需要使用按位或连接
对于fcntl的其他功能在后续章节会详细讨论,附录中摘录了这些宏的具体描述
附录
图片均来自原著
文件共享:
打开方式
read
IO效率
IO原则:单次IO尽可能的大,IO的次数尽可能小
原子操作
fcntl