一、文件IO的概念
文件I/O称之为不带缓存的IO(unbuffered I/O)。不带缓存指的是每个read,write都调用内核中的一个系统调用。也就是一般所说的低级I/O——操作系统提供的基本IO服务,与os绑定,特定于linix或unix平台。
文件I/O 又称为低级磁盘I/O,遵循POSIX相关标准。任何兼容POSIX标准的操作系统上都支持文件I/O。标准I/O被称为高级磁盘I/O,遵循ANSI C相关标准。只要开发环境中有标准I/O库,标准I/O就可以使用。(Linux 中使用的是GLIBC,它是标准C库的超集。不仅包含ANSI C中定义的函数,还包括POSIX标准中定义的函数。因此,Linux 下既可以使用标准I/O,也可以使用文件I/O)。
通过文件I/O读写文件时,每次操作都会执行相关系统调用。这样处理的好处是直接读写实际文件,坏处是频繁的系统调用会增加系统开销,标准I/O可以看成是在文件I/O的基础上封装了缓冲机制。先读写缓冲区,必要时再访问实际文件,从而减少了系统调用的次数。
文件I/O中用文件描述符表现一个打开的文件,可以访问不同类型的文件如普通文件、设备文件和管道文件等。而标准I/O中用FILE(流)表示一个打开的文件,通常只用来访问普通文件。
- 标准库函数:遵守 ISO 标准,基于流的 I/O,对文件指针(FILE结构体)进行操作
- 系统调用:兼容 POSIX 标准,基于文件描述符的 I/O,对文件描述符进行操作
注意事项:
C库函数的IO 的底层还是调用系统调用 I/O。
需要运行速度很快的时候采用 文件IO系统调用。
FILE 结构体中有 文件描述符成员 fd,标准C的IO依然是通过 fd 来操作文件,系统调用的 文件 IO 直接使用 fd 来操作。
二、文件描述符
Linux系统将所有设备都当作文件来处理,而Linux用文件描述符来标识每个文件对象。其实我们可以想象我们电脑的显示器和键盘在Linux系统中都被看作是文件,而它们都有相应的文件描述符与之对应。
文件描述符 | 缩写 | 描述 |
---|---|---|
0 | STDIN | 标准输入 |
1 | STDOUT | 标准输出 |
2 | STDERR | 标准错误输出 |
其实我们与计算机之间的交互是我可以输入一些指令之后它给我一些输出。那么我们可以把上面表格中的文件描述符0理解为我和计算机交互时的输入,而这个输入默认是指向键盘的; 文件描述符1理解为我和计算机交互时的输出,而这个输出默认是指向显示器的;文件描述符2理解为我和计算机交互时,计算机出现错误时的输出,而这个输出默认是和文件描述符1指向一个位置;
- 对于内核而言,所有打开文件都由文件描述符引用。文件描述符是一个非负整数。当打开一个现存文件或创建一个新文件时,内核向进程返回一个文件描述符。
当读、写一个文件时,用 open 或 creat 返回的文件描述符标识该文件,将其作为参数传送给 read 或 write。 - 在 POSIX 应用程序中,整数0、1、2 被替换成符号常熟 STDIN_FILENO 、STDOUT_FILENO 或STDERR_FILENO。这些常数都定义在头文件 <unistd.h> 中。
文件描述符 0 与 进程的标准输入(standard input)关联
文件描述符 1 与 标准输出(standard output)关联
文件描述符 2 与 标准错误(standard error)关联
在/usr/include/unistd.h
#define STDIN_FILENO 0 /* Standard input. */
#define STDOUT_FILENO 1 /* Standard output. */
#define STDERR_FILENO 2 /* Standard error output. */
- 文件描述符的范围是 0-- OPEN_MAX。Linux为1024(允许每个进程打开1024个文件)
fdopen()函数 – 文件描述符转化为文件指针
#include<stdio.h>
FILE * fdopen(int fd, const char * mode);
- 函数描述:通俗的理解fdopen就是把参数fd的文件描述符转化为文件指针。
- 参数说明:
- fd: 文件描述符
- mode: 文件打开的模式。和fopen中的模式(如r-只读, w-写)相同。
- 函数返回值:
- 返回文件指针。
fileno()函数 – 文件指针转换为文件描述符
#include<stdio.h>
int fileno(FILE *stream)
- 函数描述:fileno()用来取得参数stream 指定的文件流转换成为文件描述符。
- 参数说明:
- stream:指定要转换成文件描述符的文件流
- 函数返回值:
- 返回文件描述符。
三、函数格式定义
1. open()函数 – 打开文件
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
- 函数描述:打开文件
- 参数说明:
- pathname:打开的文件路径字符串
- flags:文件访问模式(即是什么方式打开文件,只读、只写还是可读并可写等)
- mode:设定文件的访问权限
- 函数返回值:
- open建立了一条到文件或设备的访问路径,如果调用成功,返回一个可以被read、write等其他系统调用的函数使用的文件描述符,而且这个文件描述符是唯一的,不与任何其他运行中的进程共享,在失败时返回-1,并设置全局变量errno来指明失败的原因。
- 参数 flags 所能使用的标志:
- O_RDONLY 以只读方式打开文件
- O_WRONLY 以只写方式打开文件
- O_RDWR 以可读写方式打开文件
- 上述三种旗标是互斥的,也就是不可同时使用,但可与下列的标志利用OR(|)运算符组合。
- O_CREAT 若欲打开的文件不存在则按照 mode 参数指定的文件权限来创建文件。
- O_EXCL 如果O_CREAT 也被设置,此指令会去检查文件是否存在。文件若不存在则建立该文件,否则将导致打开文件错误。此外,若O_CREAT与O_EXCL同时设置,并且欲打开的文件为符号连接,则会打开文件失败。在网络文件系统仅从操作时却没有保证
- O_NOCTTY 如果欲打开的文件为终端机设备时,则不会将该终端机当成进程控制终端机。
- O_TRUNC 若文件存在并且以只读或只写的方式打开时,此标志会令文件长度清为0,而原来存于该文件的资料也会消失。
- O_APPEND 当读写文件时会从文件尾开始移动,也就是所写入的数据会以附加的方式加入到文件后面。
- O_NONBLOCK 以不可阻塞的方式打开文件,也就是无论有无数据读取或等待,都会立即返回进程之中。pathname 指的是一个FIFO、一个块特殊文件或一个字符特殊文件。
- O_NDELAY 同O_NONBLOCK。
- O_SYNC 以同步的方式打开文件。
- O_NOFOLLOW 如果参数pathname 所指的文件为一符号连接,则会令打开文件失败。
- O_DIRECTORY 如果参数pathname 所指的文件不是一个目录,则会令打开文件失败。此为Linux2.2以后特有的旗标,以避免一些系统安全问题。
-
参数 mode 则有下列数种组合,只有在建立新文件时才会生效,此外真正建文件时的权限会受到umask值所影响,因此该文件权限应该为(mode-umaks)。
- S_IRWXU,00700 权限,代表该文件所有者具有可读、可写及可执行的权限。
- S_IRUSR 或 S_IREAD,00400 权限,代表该文件所有者具有可读取的权限。
- S_IWUSR 或 S_IWRITE,00200 权限,代表该文件所有者具有可写入的权限。
- S_IXUSR 或 S_IEXEC,00100 权限,代表该文件所有者具有可执行的权限。
- S_IRWXG 00070权限,代表该文件用户组具有可读、可写及可执行的权限。
- S_IRGRP 00040 权限,代表该文件用户组具有可读的权限。
- S_IWGRP 00020权限,代表该文件用户组具有可写入的权限。
- S_IXGRP 00010 权限,代表该文件用户组具有可执行的权限。
- S_IRWXO 00007权限,代表其他用户具有可读、可写及可执行的权限。
- S_IROTH 00004 权限,代表其他用户具有可读的权限
- S_IWOTH 00002权限,代表其他用户具有可写入的权限。
- S_IXOTH 00001 权限,代表其他用户具有可执行的权限。
-
返回值 成功返回文件描述符,失败则返回-1
- 错误代码 EEXIST 参数 pathname 所指的文件已存在,却使用了O_CREAT 和 O_EXCL 标志。
- EACCESS 参数 pathname 所指的文件不符合所要求测试的权限。
- EROFS 欲测试写入权限的文件存在于只读文件系统内
- EFAULT 参数 pathname 指针超出可存取内存空间
- EINVAL 参数 mode 不正确。
- ENAMETOOLONG 参数pathname太长
- ENOTDIR 参数pathname不是目录。
- ENOMEM 核心内存不足
- ELOOP 参数pathname有过多符号连接问题。
- EIO I/O 存取错误。
-
附加说明
- 使用 access() 作用户认证方面的判断要特别小心,例如在 access() 后再作 open()空文件可能会造成系统安全上的问题。
2. create()函数 – 创建文件
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
int creat(const char * pathname, mode_t mode);
- 函数描述:创建文件,creat()相当于调用open(const char * pathname ,(O_CREAT|O_WRONLY|O_TRUNC), mode)
- 参数说明:
- pathname:指向欲建立的文件路径字符串
- mode:设定文件的访问权限
- 函数返回值:
- creat()会返回新的文件描述符,若有错误发生则会返回-1,并把错误代码设给errno。
- 错误代码
- EEXIST 参数 pathname 所指的文件已存在,却使用了O_CREAT 和 O_EXCL 标志。
- EACCESS 参数 pathname 所指的文件不符合所要求测试的权限。
- EROFS 欲测试写入权限的文件存在于只读文件系统内
- EFAULT 参数 pathname 指针超出可存取内存空间
- EINVAL 参数 mode 不正确。
- ENAMETOOLONG 参数pathname太长
- ENOTDIR 参数pathname不是目录。
- ENOMEM 核心内存不足
- ELOOP 参数pathname有过多符号连接问题。
- EMFILE 已达到进程可同时打开的文件数上限
- ENFILE 已达到系统可同时打开的文件数上限
- 附件说明
- creat()无法建立特别的设备文件,如果需要请使用mknod()。
- creat 的一个不足之处是它只以只写的方式打开所创建的文件。
3. close()函数 – 关闭文件
#include<unistd.h>
int close(int fd);
- 函数描述:当使用完文件后若已不再需要则可使用close()关闭该文件,close()会让数据写回磁盘,并释放该文件所占用的资源。
- 参数说明:
- fd:先前由 open() 或 creat() 所返回的文件描述符。
- 函数返回值:
- 若文件顺利关闭则返回0,发生错误时返回-1。
- 错误代码
- EBADF 参数fd 非有效的文件描述词或该文件已关闭。
- 附件说明
- 虽然在进程结束时,系统会自动关闭已打开的文件,但仍建议自行关闭文件,并确实检查返回值。
- 当一个进程终止时,它所有的打开文件都由内核自动关闭。
4. read 函数— 读文件
#include<unistd.h>
ssize_t read(int fd, void * buf, size_t count);
- 函数描述:
- 由已打开的文件读取数据。
- read() 会把参数 fd 所指的文件传送 count 个字节到 buf 指针所指的内存中。
- 若参数 count 为0,则 read() 不会有作用并返回0.
- 参数说明:
- fd:先前由 open() 或 creat() 所返回的文件描述符。
- buf:存放读取数据的缓存
- count:要求读取一次数据的字节数
- 函数返回值:
- 返回值为实际读取到的字节数,如果返回0,表示已到达文件尾或是无可读取的数据,此外文件读写位置会随读取到的字节移动。
- 错误代码
- EINTR 此调用被信号所中断。
- *EAGAIN 当使用不可阻断 I/O 时(O_NONBLOCK),若无数据可读取则返回此值。
- *EBADF 参数 fd 非有效的文件描述词,或该文件已关闭。
- 由多种情况可使实际督导的字节数少于要求读写字节数
- 读普通文件时,在读到要求字节数之前已到达了文件尾端
- 当从终端设备读时,通常一次最多读一行
- 当从网络读时,网络中的缓冲机构可能造成返回值小于所要求读的字节数
- 某些面向记录的设备,例如磁带,一次最多返回一个记录
- 进程由于信号造成中断
- 读操作从文件的当前位移量处开始,在成功返回前,该位移量增加实际读得的字节数。
- 附件说明
- 如果顺利 read() 会返回实际读到的字节数,最好能将返回值与参数 count 做比较,若返回的字节数要比要求读到的字节数少,则有可能读到了文件尾、从管道(pipe)或终端机读取,或者是 read() 被信号中断了读取动作。当有错误发生时,则返回 -1,错误代码存入 errno 中,而文件读写位置则无法预期。
5. write 函数— 写文件
#include<unistd.h>
ssize_t write(int fd, void * buf, size_t count);
- 相关函数: open,read,fcntl,close,lseek,sync,fsync,fwrite。
- 函数描述:
- write()会把参数buf所指的内存写入count个字节到参数fd所指的文件内。当然,文件读写位置也会随之移动。
- write 出错的一个常见原因是:磁盘已写满,或者超过了对一个给定进程的文件长度限制
- 对于普通文件,写操作从文件的当前位移量处开始。如果在打开该文件时,指定了 O_APPEND 选择项,则在每次写操作之前,将文件位移量设置在文件的当前结尾处。在一次成功写之后,该文件位移量增加实际写的字节数。
- 参数说明:
- fd:先前由 open() 或 creat() 所返回的文件描述符。
- buf:存放读取数据的缓存
- count:要求读取一次数据的字节数
- 函数返回值:
- 如果顺利write()会返回实际写入的字节数。当有错误发生时则返回-1,错误代码存入errno中。
- 返回值通常与参数 count 的值不同,如果不一样则表示出错。
- 错误代码
- EINTR 此调用被信号所中断。
- EAGAIN 当使用不可阻断 I/O 时(O_NONBLOCK),若无数据可读取则返回此值。
- EBADF 参数 fd 非有效的文件描述词,或该文件已关闭。
6. lseek 函数— 文件定位
#include<sys/types.h>
#include<unistd.h>
off_t lseek(int fildes,off_t offset ,int whence);
- 相关函数: dup,open,fseek
- 函数描述:定位一个已打开的文件。每一个已打开的文件都有一个读写位置,当打开文件时通常其读写位置是指向文件开头,若是以附加的方式打开文件(如O_APPEND),则读写位置会指向文件尾。当read()或write()时,读写位置会随之增加,lseek()便是用来控制该文件的读写位置。
- 参数说明:
- fildes: 已打开的文件描述符
- offset: 位移量。根据参数whence来移动读写位置的位移数。
- whence: 定位的位置,为下列其中一种:
- SEEK_SET 将该文件的位移量设置为距离文件开始处 offset 个字节
- SEEK_CUR 将该文件的位移量设置为其当前值处 加offset 个字节,offset 可为正或负
- SEEK_END 将该文件的位移量设置为文件长度 加offset 个字节,offset 可为正或负
- 当whence 值为SEEK_CUR 或SEEK_END时,参数offet允许负值的出现
- 函数返回值:
- 若成功则返回新的文件位移量(绝对偏移量),若出错返回-1
- 特别使用方式
- 欲将读写位置移到文件开头时:lseek(int fildes,0,SEEK_SET)
- 欲将读写位置移到文件尾时:lseek(int fildes,0,SEEK_END)
- 想要取得目前文件位置时:lseek(int fildes,0,SEEK_CUR)
- 附件说明
- Linux系统不允许lseek()对tty装置作用,此项动作会令lseek()返回ESPIPE。
- 其他使用方式:
- lseek 也可用来确定所涉及的文件是否可以设置位移量。如果文件描述符引用的是一个管道或FIFO,则 lseek 返回 -1,并将 errno 设置为 EPIPE
- 每个打开文件都由一个与其相关联的“当前文件偏移量”。它是一个非负整数,用以度量从文件开始处计算的字节数。通常,读、写操作都从当前文件偏移量处开始,并使偏移量增加所读或写的字节数。按系统默认,当打开一个文件时,除非指定 O_APPEND 选择项,否则该位移量被设置为0。
四、案例
1. 案例1-- 文件的读写
根据给定的路径,复制一份文件。
io.h
#ifndef _IO_H
#define _IO_h
//定义外部复制函数
extern void copy(int fdin,int fdout);
#endif
io.c
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <errno.h>
#include "io.h"
//定义缓冲区的大小
#define BUFFER_SIZE 1024
//文件的读写拷贝函数
void copy(int fdin,int fdout)
{
char buff[BUFFER_SIZE];//定义缓冲区存放数据
ssize_t size;
//read函数返回值为读取的字节数,返回0表示无数据可读
while((size=read(fdin,buff,BUFFER_SIZE))>0)
{
if(write(fdout,buff,size)!=size)
{
fprintf(stderr,"write data fail:%s\n",strerror(errno));
}
exit(1);
}
if(size<0)//read函数返回值为读取的字节数,如果小于0就相当于读错误了
{
fprintf(stderr,"read data fail:%s\n",strerror(errno));
exit(1);//相当于return 1
}
}
将 io.c 编译成 .o 文件,供其他模块进行调用
gcc -o obj/io.o -Iinclude -c src/io.c
-o:指定输出的目录和文件格式
-Iinclude:指定包含的头文件目录,-I使指定包含头文件,后面的inlcude 即是头文件所在的目录,也可以采用绝对路径
-c:指定源文件
cp.c
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <stdlib.h>
#include "io.h"
int main(int argc,char* argv[])
{
//判断输入的参数是否满足条件,即输入参数为输入2个文件
if(argc!=3)
{
fprintf(stderr,"Usage: %s srcfile destfile\n", argv[0]);
exit(1);
}
int fdin;
int fdout;//定义输入输出的两个文件描述符
//打开只读的文件
fdin=open(argv[1],O_RDONLY);
if(fdin<0)
{
fprintf(stderr,"open file error:%s\n",strerror(errno));
exit(1);
}
else
{
printf("open file:%d\n",fdin);
}
//打开一个待创建的文件,待写入的文件
fdout=open(argv[2],O_WRONLY|O_CREAT|O_TRUNC,0777);
if(fdout<0)
{
fprintf(stderr,"open file error:%s\n",strerror(errno));
exit(1);
}
else
{
printf("open file:%d\n",fdout);
}
//文件复制
copy(fdin,fdout);
//关闭文件
close(fdin);
close(fdout);
return 0;
}
gcc -o bin/cp -Iinclude obj/io.o src/cp.c
运行结果如图所示:
2. 案例2-- 文件的定位
打印要获取文件的大小,每次读完打印读完的位置。
io.c
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <errno.h>
#include "io.h"
//定义缓冲区的大小
#define BUFFER_SIZE 1024
//文件的读写拷贝函数
void copy(int fdin,int fdout)
{
char buff[BUFFER_SIZE];//定义缓冲区存放数据
ssize_t size;
//read函数返回值为读取的字节数,返回0表示无数据可读
while((size=read(fdin,buff,BUFFER_SIZE))>0)
{
//获取当前的的偏移位置
printf("current:%ld\n",lseek(fdin,0L,SEEK_CUR));
if(write(fdout,buff,size)!=size)
{
fprintf(stderr,"write data fail:%s\n",strerror(errno));
exit(1);
}
}
if(size<0)//read函数返回值为读取的字节数,如果小于0就相当于读错误了
{
fprintf(stderr,"read data fail:%s\n",strerror(errno));
exit(1);//相当于return 1
}
}
cp.c
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <stdlib.h>
#include "io.h"
int main(int argc,char* argv[])
{
int ret;
//判断输入的参数是否满足条件,即输入参数为输入2个文件
if(argc!=3)
{
fprintf(stderr,"Usage: %s srcfile destfile\n", argv[0]);
exit(1);
}
int fdin;
int fdout;//定义输入输出的两个文件描述符
//打开只读的文件
fdin=open(argv[1],O_RDONLY);
if(fdin<0)
{
fprintf(stderr,"open file error:%s\n",strerror(errno));
exit(1);
}
else
{
printf("open file:%d\n",fdin);
}
//获取文件的大小,将欲读写的位置移动到文件减尾
ret=lseek(fdin,0L,SEEK_END);
if(ret==-1)
{
fprintf(stderr,"lseek file error:%s\n",strerror(errno));
exit(1);
}
printf("file legth:%d\n",ret);
//获取文件的大小,将欲读写的位置移动到文件减尾
ret=lseek(fdin,0L,SEEK_SET);
if(ret==-1)
{
fprintf(stderr,"lseek file error:%s\n",strerror(errno));
exit(1);
}
//打开一个待创建的文件,待写入的文件
fdout=open(argv[2],O_WRONLY|O_CREAT|O_TRUNC,0777);
if(fdout<0)
{
fprintf(stderr,"open file error:%s\n",strerror(errno));
exit(1);
}
else
{
printf("open file:%d\n",fdout);
}
//文件复制
copy(fdin,fdout);
//关闭文件
close(fdin);
close(fdout);
return 0;
}
运行结果:
3. 案例3-- 空洞文件的制作
新建文件,并写入相关数据,使用lseek函数定位新的位置。
空洞文件就是从文件尾部跳出若干个字节再写入信息,中间空出来的信息就是一个空洞。
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <stdlib.h>
char* buff="0123456789";
int main(int argc,char* argv[])
{
//判断输入的参数是否满足条件,即输入参数为输入2个文件
if(argc<2)
{
fprintf(stderr,"Usage: %s srcfile\n", argv[0]);
exit(1);
}
int fd;
ssize_t size;
//写入多少个字节,一个buff共占多少字节
size=strlen(buff)*sizeof(char);
//打开一个待创建的文件,待写入的文件
fd=open(argv[1],O_WRONLY|O_CREAT|O_TRUNC,0777);
if(fd<0)
{
fprintf(stderr,"open file error:%s\n",strerror(errno));
exit(1);
}
else
{
printf("open file:%d\n",fd);
}
//新创建的文档写入数据
if(write(fd,buff,size)!=size)
{
perror("write error!\n");
exit(1);
}
//定位到文件尾部10个字节后写入
if(lseek(fd,10L,SEEK_END)==-1)
{
perror("lseek error!\n");
exit(1);
}
//再将数据写入到文件中
if(write(fd,buff,size)!=size)
{
perror("write error!\n");
exit(1);
}
//关闭文件
close(fd);
return 0;
}
运行结果: