【文件IO】文件IO学习教程一:系统IO/文件偏移量

一、文件的概念

在Linux系统语境下,文件(file)一般有两个基本含义:

  • 狭义:指普通的文本文件,或二进制文件。包括日常所见的源代码、word文档、压缩包、图片、视频文件等等。
  • 广义:除了狭义上的文件外,几乎所有可操作的设备或接口都可视为文件。包括键盘、鼠标、硬盘、串口、触摸屏、显示器等,也包括网络通讯端口、进程间通讯管道等抽象概念。

在linux下,一切都是文件。除了我们平时常见文件:1.txt/2.jpg/3.mp3…是文件之外,linux系统还会把硬件设备当作是文件,例如:LED灯、触摸屏、LCD液晶屏幕,蜂鸣器等等,这些硬件设备在linux的眼中,都是文件。这句话是站在内核的角度说的,因为在内核中所有的设备(除了网络接口)都一律使用 Linux 独有的虚拟文件系统(VFS) 来管理。这样做的最终目的,是将各种不同的设备用“文件”这个概念加以封装和屏蔽,简化应用层编程的难度。


二、Linux系统中文件的分类

在Linux中,文件总共被分成了7种,他们分别是:

  • 普通文件:存在于外部存储器中,用于存储普通数据。
  • 目录文件:用于存放目录项,是文件系统管理的重要文件类型。
  • 管道文件:一种用于进程间通信的特殊文件,也称为命名管道FIFO。
  • 套接字文件:一种用于网络间通信的特殊文件。
  • 链接文件:用于间接访问另外一个目标文件,相当于Windows快捷方式。
  • 字符设备文件:字符设备在应用层的访问接口。触摸屏 、液晶屏、键盘、鼠标
  • 块设备文件:块设备在应用层的访问接口。硬盘、U盘 、光盘
@ubuntu:~$ ls -l
-rw-r--r-- 1 gec gec  345  Sep  12:38 a.zip
drwxr-xr-x 2 gec gec 1024  Sep  12:38 dir/
prw-r--r-- 1 gec gec    0  Sep  12:38 pipe 
srw-r--r-- 1 gec gec    0  Sep  12:38 socket
lrw-r--r-- 1 gec gec    4  Sep  12:38 link -> a.zip
crw-r--r-- 1 gec gec 1, 3  Sep  12:38 character
brw-r--r-- 1 gec gec 5, 1  Sep  12:38 block

注意到,每个文件信息的最左边一栏,是各种文件类型的缩写,从上到下依次是:

  • -(regular)普通文件
  • d(directory)目录文件
  • p(pipe)管道文件(命名管道)
  • s(socket)套接字文件(Unix域/本地域套接字)
  • l(link)链接文件(软链接)
  • c(character)字符设备文件
  • b(block)块设备文件

三、系统IO与标准IO

1、概念
对文件的操作,基本上就是输入输出,因此也一般称为IO接口。那么我们用户如何实现文件的读取或者写入操作呢?其实是不需要用户写自定义函数,因为在linux下,已经有现成的函数来实现。在操作系统的层面上,这一组专门针对文件的IO接口就被称为系统IO;在标准库的层面上,这一组专门针对文件的IO接口就被称为标准IO,如下图所示:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
2、区别

  • 系统IO:是众多系统调用当中专用于文件操作的一部分接口。存在于man手册中的第二章
  • 标准IO:是众多标准函数当中专用于文件操作的一部分接口。存在于man手册中的第三章
    从图中还能看到,标准IO实际上是对系统IO的封装,系统IO是更接近底层的接口。如果把系统IO比喻为菜市场,提供各式肉蛋菜果蔬,那么标准IO就是对这些基本原来的进一步封装,是品类和功能更加丰富的各类酒庄饭店。

3、如何选择系统IO与标准IO
系统IO:

  • 由操作系统直接提供的函数接口,特点是简洁,功能单一
  • 没有提供缓冲区,因此对海量数据的操作效率较低
  • 套接字Socket、设备文件的访问只能使用系统IO

标准IO:

  • 由标准C库提供的函数接口,特点是功能丰富
  • 有提供缓冲区,因此对海量数据的操作效率高
  • 编程开发中尽量选择标准IO,但许多场合只能用系统IO

四、系统IO基本API

1、open()函数打开文件
在这里插入图片描述
注意:open函数什么时候会打开失败?
(1)你打开的路径名不存在时。
(2)如果文件本身的权限不允许,那么操作权限不对,也会失败。

2、clos()函数关闭文件
在这里插入图片描述
例子: 访问家目录下test.txt,如果访问成功,则输出"open file success",否则,输入"open file error",并关闭文件,如果关闭成功,则输出"close file success",否则输出"close file error"。

#include<stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main()
{
    int fd = open("./test.txt",O_RDONLY);
    if(fd == -1){
        perror("open file error");
        return -1;
    } else{
        printf("open file success\n");
    }
    close(fd);
}

总结:

  • 文件描述符的范围:0-1023
  • 文件描述符的资源是有限的,打开文件之后,后面如果不需要用到该文件了,记得要关闭。

3、标准库函数的错误处理
在所有的库函数中,如果调用过程出错了,那么该函数除了会返回一个特定的数据来告诉用户调用失效之外,还都会去修改一个大家共同的全局错误码变量errno,我们可以通过这个错误码,来进一步确认究竟是什么错误。

关键点:

  • 如果库函数、系统调用出错了,全局错误码 errno 会随之改变
  • 如果库函数、系统调用没出错,全局错误码 errno 不会改变
  • 一个库函数、系统调用出错后,若未及时处理错误码,则错误码可能会被随后的其他函数修改

提取错误码信息的两种办法:

// 1. 使用perror(),直接输出用户信息和错误信息:
if(open("a.txt", O_RDWR) == -1)
{
    perror("打开a.txt失败");
}

// 2. 使用strerror(),返回错误信息交给用户自行处理:
if(open("a.txt", O_RDWR) == -1)
{
    printf("打开a.txt失败:%s\n", strerror(errno));
}

五、文件描述符本质

1、文件描述符概念
文件描述符是open函数的返回值,当open()执行成功后,就会返回一个非负的,最小的,没有使用过的整数。

3 = open("1.txt");  //后面的代码中,3就是代表1.txt这个文件。
4 = open("2.txt");  //后面的代码中,4就是代表2.txt这个文件。

结论:将来处理文件时,我们不需要提供文件名字,只需要提供文件对应的文件描述符就可以。

2、标准输入/输出/出错
访问文件时,发现fd从3开始分配,说明0/1/2已经被占用,究竟是谁使用了呢?其实在程序执行的时候,就会默认打开3个文件,分别是"标准输入"、"标准输出"、"标准出错",他们其实是一个宏定义来的,是被定义在一个头文件中,头文件路径:/usr/include/unistd.h

/* Standard file descriptors.  */
#define    STDIN_FILENO    0    //标准输入设备文件    -> 对象:键盘
#define    STDOUT_FILENO    1    //标准输出设备文件    -> 对象:屏幕
#define    STDERR_FILENO    2    //标准出错设备文件    -> 对象:屏幕

可以理解: 程序刚开始执行的时候,0=open("标准输入");

**3、本质 **
函数 open() 的返回值,是一个整型 int 数据。这个整型数据,实际上是内核中的一个称为 fd_array 的数组的下标
在这里插入图片描述
打开文件时,内核产生一个指向 file{} 的指针,并将该指针放入一个位于 file_struct{} 中的数组 fd_array[] 中,而该指针所在数组的下标,就被 open() 返回给用户,用户把这个数组下标称为文件描述符,如上图所示。

  • 文件描述符从0开始,每打开一个文件,就产生一个新的文件描述符。
  • 可以重复打开同一个文件,每次打开文件都会使内核产生系列结构体,并得到不同的文件描述符
  • 由于系统在每个进程开始运行时,都默认打开了一次键盘、两次屏幕,因此0、1、2描述符分别代表标准输入、标准输出和标准出错三个文件(两个硬件)。

4、拓展参数
flags:可选(0个/多个) -> 如果选了,就使用"|"来添加

flags释义
O_CREAT如果打开的那个文件不存在,那么就会创建;如果flags中有O_CREAT,那么需要填写mode这个参数。如果flags中没有O_CREAT,那么mode这个参数填了也没用
O_TRUNC如果文件存在并且是一个普通文件,而且打开方式是O_WRONLY/O_RDWR,那么这个文件就会被清空。
O_APPEND以追加的方式打开文件,在每一次写之前,文件的定位在末尾。
O_EXCL跟上面的创建配套使用,如果文件存在了则打开失败
O_DIRECTORY判断文件是否是目录,如果不是则打开失败
O_NONBLOCK or O_NDELAY不阻塞的打开文件,读写操作的时候不会卡住(普通文件如果是空的,读取数据,不会阻塞。但是设备文件如果没有数据,读取数据,就会阻塞)
mode释义
0666创建文件的权限
注意:文件的权限在共享文件夹目录下是无法验证,就算mode是 0666 ,创建出来的文件还是0777,因为跟权限掩码有关系。可以在家目录下验证,最终文件权限:mode & (~umask)
    int fd = open("./aa",O_RDONLY|O_DIRECTORY);
    if(fd == -1)
    {
        perror("open error");
    }
  • open函数有两个版本,一个有两个参数,一个有三个参数。
  • 当打开一个已存在的文件时,指定两个参数即可。
  • 当创建一个新文件时,需要用第三个参数指定新文件的权限,否则新文件的权限是随机值。
  • 模式flags,可以使用位或的方式,来同时指定多个模式。

六、文件数据输出/输入

1、read()函数
功能:read - read from a file descriptor。读取一个文件描述符的数据

参数:

  • fd: 想读取的那个文件的文件描述符
  • buf: 数据缓冲区
  • count:想读取的字节数 (愿望值)

返回值:

  • 返回值 > 0 表示成功读取到的字节数
  • 返回值 == 0 表示文件读取到末尾了
  • 返回值 ==-1 读取失败
#include<stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main()
{
    int fd = open("./3.txt",O_RDWR);
    if(fd == -1)
    {
        perror("open error");
    }
    char buf[1024]={0};
    int ret = read(fd,buf,sizeof(buf)-1);
    printf("ret:%d buf:%s\n",ret,buf);
    close(fd);
}

总结:
1)如果一个文件很大,可以写一个循环每次获取指定的字节数,当read返回值 为0时,则表示这个文件读完了,则退出
2)window下文本换行符是 \r\n ,占两个字节。linux下 占1个字节

2、write()函数
功能:write - write to a file descriptor。写入数据到文件描述符中

参数:

  • fd: 想写入的文件的文件描述符
  • buf: 数据缓冲区
  • count: 写入的字节数

返回值:

  • 成功:已经写入的字节数
  • 失败:-1
#include<stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main()
{
    int fd = open("./3.txt",O_RDWR|O_TRUNC|O_CREAT,0777);
    if(fd == -1)
    {
        perror("open error");
        return -1;
    }
    char buf[] = "hello world";
    write(fd,buf,strlen(buf));//有多少写多少
    close(fd);
}

总结:open()打开文件时,文件定位都是在最开头的,文件定位随着读取/写入字节而往后偏移。有多少字节的数据,count就写多少,如果count比真实数据大,那么文本的后面可能有乱码。(文本不够,乱码来凑)


八、文件偏移量

文件偏移量就是文件当前的光标位置,默认打开文件时,文件的光标位置都是在最开头的。
(1)使用读写函数可以使得偏移量发生变化。

fd = open("test.txt");    偏移量:0
write(fd,"hello",5);      偏移量:5 

(2)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: 相对于文件的末尾

返回值:

  • 成功:距离文件开头的偏移量。
  • 失败:-1。
int main(int argc,char **argv)
{
    int fd = open("./1.txt",O_RDWR); //文件内容:hello world
    if(fd == -1)
    {
        printf("open file error\n");
        return -1;
    }
    //基于文件开头向后偏移5个字节
    int ret = lseek(fd,5,SEEK_SET);
    printf("ret:%d\n",ret);
    //字符串123 会直接从光标5的位置直接写入,会覆盖原来的数据
    write(fd,"123",3); 
    
    close(fd);
}

特性:lseek函数不仅可以用来调整当前文件偏移量,而且还可以将文件位置偏移到文件之外,形成一个空洞。这种特性其实是非常重要的,它提供了可以在不同地方同时写一个文件的可能,对于一个较大的文件我们可以在文件中定位到一个指定的地方,让多个进程同时在不同的偏移量处写入文件数据。相当于网络中的多点下载。

  • 19
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值