Linux系统中,一切皆文件,对所有外部设备的操作,都可以抽象成对文件的读写。
Linux 文件IO
Linux中做文件IO最常用到的5个函数是: open , close , read , write 和 lseek ,这5个函数是不带缓冲的IO,也即每个read和write都调用了内核的一个系统调用。
下面对这5个函数进行介绍。
什么是文件描述符?
对于内核而言,所有打开的文件都通过文件描述符引用。文件描述符是一个非负整数。通常0是标准输入,1是标准输出,2是标准错误。正是有了它们,你的简单程序才可以从控制台读入数据,输出日志,输出错误打印等等。
当打开一个现有文件或创建一个新文件时,内核向进程返回一个文件描述符。当读、写一个文件时。使用open或creat返回的文件描述符标识该文件。将其作为传送给read或write。
打开文件,获取文件描述符
函数原型
#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);
参数
flags
flags字段使用POSIX的几个宏,此时必须包含头文件<fcntl.h>才行。
可以是下面几个宏的逻辑或组合。这些宏共有三种类型:
访问方式 描述
O_RDONLY 只读
O_WRONLY 只写
O_RDWR 可读写
打开时标志 描述
O_CREAT 创建文件,需要指定第餐个参数mode
O_EXCL 与O_CREAT联用,如果文件已存在则返回错误
O_TRUNC 将清空文件的内容,仅对普通文件有用
O_NOCTTY 若打开的文件是终端设备,不让它作为该进程的控制终端
O_NOBLOCK 以非阻塞模式打开
读操作
函数原型
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
参数
参数 描述
fd 文件描述符
buf 读取的数据存放在buf指针指向的缓冲区
count 读取的字节数
这里要说一下关于count:如果buf是一个字符数组名,那么count就用它的sizeof值。若buf是字符指针(字符串)则count用它的strlen值。
返回值:若果函数执行成功,返回读取的字节数,如果遇到EOF,则返回0。出错返回-1,并设置相应errno值。
写操作
函数原型
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
参数
参数同read函数。
参数 描述
fd 文件描述符
buf 读取的数据存放在buf指针指向的缓冲区
count 读取的字节数
如果函数调用成功,返回值为写入的字节数;否则返回值为-1,并设置相应的errno值。
关闭文件
调用close函数即可,它的参数是前面打开的时候获得的文件描述符
#include <unistd.h>
int close(int fd);
成功返回0,失败则返回-1,并且会设置errno。
实例
以上理论太多,还是先看看一些简单的例子:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
int main(int argc, char const *argv[]) {
int fd = -1; //文件描述符
//打开文件
fd = open( "myfile.txt", O_RDWR );
if ( -1 == fd ) {
//perror("open failed:");
printf("open failed:%s\n", strerror(errno));
}else {
printf("open succeed=%d\n", fd );
}
//写文件
char buf[] = "hello,程序猿编码";
ssize_t count = write( fd, buf, sizeof(buf));
if ( -1 == count ) {
perror("write failed");
}else {
printf("文件写入成功,实际写入的字节数目为:%d\n", count);
}
//关闭文件
close( fd );
return 0;
}
编译运行
当open返回-1(很多系统接口类似)时,就会设置errno,这个时候就可以调用perror接口打印对应的错误信息。write也是类似这个意思。这样便于我们定位问题。即:
perror("open failed:");
printf("open failed:%s\n", strerror(errno));
上面两种方式都可以打印出错误信息,区别在于,前者输出到标准错误,后者输出到标准输出。
标准错误是无缓冲的,可以参考《不可不知的Linux中三种缓冲模式》
打开一个存在的文件
在O_RDWR模式下,对于一个已经存在的文件,且有内容,那么写入文件会覆盖对应大小的源文件内容【不是完全覆盖】
int fd = open( "test.txt", O_RDWR );
打开一个文件,不存在时创建
既然不存在时,会打开失败,那么不存在就创建好了,这就用到了O_CREATE标志。因此修改open函数那一行:
int fd = open("test.txt",O_WRONLY | O_CREAT);
注意到了吗,多个标志使用|构成flags参数。
打开一个文件,存在时截断
前面已经实现了文件不存在时,创建,存在时也可以正常打开,如果存在时,又不想要原先的内容?那就需要用到O_TRUNC标志。
int fd = open("test.txt",O_WRONLY | O_CREAT | O_TRUNC);
现在假设test.txt文件存在,且里面有内容,再次运行后,发现打开文件正常,且内容只有新加入的,而没有之前存在的。
在打开的文件后追加内容
如果想在打开的文件后追加内容,那么可以使用O_APPEND标志
int fd = open("test.txt",O_WRONLY | O_CREAT | O_APPEND);
这样如果原来test.txt中有内容,则可以往文件中追加内容。
lseek
功能:移动打开的文件的读写指针的位置。
函数原型:
#include <sys/types.h>
#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);
参数:
fd是文件描述符
offset是偏移量
whence是偏移量的基准位置。它的取值有三个:
SEEK_SET: 开始位置
SEEK_CUR: 当前位置
SEEK_END: 末尾位置
为什么开始位置的后缀是_SET?
实际上,在man手册中可以看出。这三个宏的描述是
SEEK_SET The offset is set to offset bytes.
SEEK_CUR The offset is set to its current location plus offset bytes.
SEEK_END The offset is set to the size of the file plus offset bytes.
fd的偏移量
在内核中对一个文件描述符(fd)的偏移量只维护一个值,也就是说你用读写方式打开一个文件,如果先用read读取了n个字符,紧接着用write写入了n个字符,那么后来写入的n个字符并不是从文件第一个字符位置开始的,而是从n+1个字符位置开始的。所以通常我们需要使用lseek来使fd的偏移量置于文件开始位置。
简单应用:统计文件大小。
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
int main(int argc, char const *argv[]) {
if ( argc != 2 ) {
printf("usage:%s %s\n", argv[0], "filename");
return -1;
}
int fd = -1;
fd = open( argv[1], O_RDWR );
if( -1 == fd ) {
printf("文件打开失败,错误号:%d\n", errno );
perror( "open" );
return -1;
}else {
printf("文件打开成功\n");
}
//把指针移动到文件末尾,就是文件的大小
int count = lseek( fd, 0, SEEK_END );
printf("文件大小为: %d\n", count);
close( fd );
return 0;
}
编译运行
总结
本文对文件I/O只做打开,创建,读写,偏移量操作。虽然本文的I/O函数不带缓冲,但是读写时,一定要选择合适的buf大小是非常关键。要善于参考man手册。
欢迎关注公众号【程序猿编码】,添加本人微信号(17865354792),回复:领取学习资料。进入技术交流群。网盘资料有如下: