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