目录
一. 如何在系统层面上理解文件
二. 语言层面上的文件IO函数
三. Linux操作系统提供的IO接口
3.1 open接口 -- 打开文件
3.2 close接口 -- 关闭文件
3.3 write接口 -- 向文件中写内容
3.4 read接口 -- 从文件中读取内容
四. 总结
一. 如何在系统层面上理解文件
在Linux操作系统层面,可以认为,只要能进行input写入或output读取的任何的任何设备,都可以被理解为文件,我们可以从狭义和广义两个方面认识文件的概念:
- 狭义上的文件:磁盘文件
- 广义上的文件:任何可以进行IO操作的设备,包括显示器、键盘、声卡、网卡、磁盘、光盘、音响、磁带、光盘等。
换句话说就是:Linux下一切皆文件。
文件 = 内容 + 属性,对文件的操作无外乎就是对内容的操作和对属性的操作。因此,就算一个文件的内容为空,那么它的属性信息也要存储在磁盘中,也要占用存储空间。
通过Linux操作系统的ls -l (可简写为ll) 指令,可以查看文件的属性信息,包括文件的类型、权限信息、链接数、拥有者、所属组、文件内容占用空间大小、最近一次修改的时间、文件名。
本质上,只有操作系统有权利对文件进行读写操作。用户对于文件的任何操作,其本质是都是调用操作系统的相关IO接口来完成的,只不过为了方便不同用户的使用,操作系统及各种编程语言,会通过图形化界面、库函数等方式,封装文件的IO接口。
如果我们希望通过代码来访问文件,那么操作流程依次为:写代码 -> 编译源代码 -> 载入内存运行,创建进程 -> 访问文件。因此,我们还可以认为,访问文件的操作本质是由进程来完成的。
二. 语言层面上的文件IO函数
C/C++均提供了相应的库函数/类,来实现的文件得IO操作
- C语言读文件函数:fgets、fgetc、fread、fscanf等。
- C语言写文件函数:fputs、fputc、fwrite、fprintf等。
- C++写文件类:std::ofstream,通过流插入运算符 << 来写文件。
- C++读文件类:std::ifstream,通过流提取运算符 >> 来读文件。
这些语言层面的IO函数,在底层都封装了操作系统提供的IO接口。那么,既然操作系统已经提供了IO接口,为什么各种语言语言还要提供自己的IO函数呢?这可以从以下两个方面来理解:
- 语言层面的IO函数相对于系统接口使用难度较低。
- 语言具有跨平台的性能,而如果直接使用系统接口,那么代码就不具有跨平台性。
提问:库函数怎样针对不同的OS平台,实现不同版本的IO函数呢?答:一般采用条件编译#ifdef/#endif的方法来实现,在库函数中将全部平台下的IO接口都进行封装,实现不同底层原理的IO函数,再根据运行的平台,来对库函数代码选取一部分进行编译。
不同的语言,会提供不同的IO接口函数,但是,在同一操作系统平台下,系统IO的接口都是一样的,无论语言层面的IO接口再怎么变化,在底层都需要封装系统IO接口。
三. Linux操作系统提供的IO接口
3.1 open接口 -- 打开文件
用于打开文件的系统接口(可能创建文件),有下面两种格式:
- int open(const char* file, int flag)
- int open(const char* file, int flag, int mode)
使用open函数,要包含三个头文件:<sys/types.h>、<sys/stat.h>和<sys/fcntl.h>
其中,file的意义为文件打开的路径,flag为打开方式的标识方法,mode为如果要创建文件的话文件的起始权限。
关于file,就是带有路径的文件的文件名。flag类似于C语言的open函数中"w"、"a"、"r"等打开方式控制的标识,主要的flag和对应的功能以下几种:
- O_RDONLY -- 以只读方式打开文件。
- O_WRONLY -- 以只写的方式打开文件。
- O_RDWR -- 读写方式打开文件。
- O_CREAT -- 如果指定的文件不存在,那就创建它。注意O_WRONLY与C语言的"w"不同的是,在open中只声明O_WRONLY并不会在文件不存在的时候创建文件。
- O_APPEND -- 追加写入文件的内容,而不覆盖文件原有内容。
- O_TRUNC -- 清空文件的内容后写文件。如果仅仅声明O_WRONLY,那么仅仅是在文件起始位置开始写数据,写多少内容覆盖文件原来的多少内容,不会情况原有内容。
如果我们同时需要两种flag的功能,那么应该使用按位或操作符,来同时对两个位置进行标记。如:我们希望open以只写的方式打开某个文件的同时在文件不存在的时候创建它,那么flag的传参方式为O_WRONLY|O_CREAT。
mode为文件创建的起始权限,以8进制的方式传参,如果我们希望创建文件的起始权限为拥有者、所属组和其它人都是rw-,那么在调用open时就应该给mode传0666。
但是,实际创建文件的起始权限,要经过系统的文件权限掩码过滤相应权限。Linux系统默认的文件掩码一般为0002,即:其它人不能拥有写文件的权限。那么,我们就可以在创建的进程的文件中,使用umask(0000),将该进程中的文件掩码暂时改为0000,不对任何权限进行过滤,这样就可以按照希望的方式给定文件的起始权限。
open接口的返回值:a.如果文件打开成功,就返回被打开的文件的文件描述符。b.如果文件打开失败,那么就返回-1。
再使用open之后,应当通过if判断open函数的返回值,再确保文件打开成功之后,再使用文件。
代码3.1:文件打开
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<sys/fcntl.h>
int main()
{
int fd1 = open("log1.txt", O_WRONLY|O_CREAT); //以只写的方式打开log1.txt文件,如果文件不存在就创建
if(fd1 < 0) //判断打开是否成功
{
perror("open fd1");
return 1;
}
//下面省略打开文件是否成功的判断
int fd2 = open("log2.txt", O_RDONLY); //以只读方式打开log2.txt文件
int fd3 = open("log3.txt", O_WRONLY|O_APPEND); //以追加写的方式打开log3.txt文件
int fd4 = open("log4.txt", O_WRONLY|O_TRUNC); //清空文件原有内容再写入
umask(0000); //在此之后,权限掩码为0000
int fd5 = open("log5.txt", O_WRONLY|O_CREAT, 0666); //给定权限0666创建文件
close(fd1);
close(fd2);
close(fd3);
close(fd4);
close(fd5);
return 0;
}
3.2 close接口 -- 关闭文件
close用于关闭某个被open接口打开的文件,其函数原型为:
- int close(int fd) -- 其中fd为文件描述符,关闭文件成功返回0,失败返回-1。
使用close函数需要包含的头文件:<unistd.h>
close一般配合open的返回值使用,如果传入错误的文件描述符,那么close就会失败。
3.3 write接口 -- 向文件中写内容
- 函数原型:ssize_t wirte(int fd, const void* ptr, size_t count)
- 函数功能:将从ptr位置处开始的count个字节的内容,写入到fd所对应的文件中去。
- 函数返回值:返回实际写入到文件中的bytes数。
使用write函数需要包含的头文件:<unistd.h>
代码3.2:写文件
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<sys/fcntl.h>
int main()
{
int fd1 = open("log1.txt", O_WRONLY|O_CREAT|O_TRUNC);
if(fd1 < 0)
{
perror("open fd1");
return 1;
}
const char* s1 = "hello world\n";
//这里不需要strlen(s) + 1
//字符串末尾以\0结尾是语言层面的规则,与文件无关
write(fd1, s1, strlen(s1));
write(fd1, s1, strlen(s1));
write(fd1, s1, strlen(s1));
int fd2 = open("log2.txt", O_WRONLY|O_CREAT|O_APPEND|O_TRUNC);
if(fd2 < 0)
{
perror("open fd2");
return 2;
}
const char* s2 = "hello Linux\n";
write(fd2, s2, strlen(s2));
write(fd2, s2, strlen(s2));
write(fd2, s2, strlen(s2));
close(fd1);
close(fd2);
return 0;
}
3.4 read接口 -- 从文件中读取内容
- ssize_t read(int fd, void *buff, size_t count)
- 从fd确定的文件后缀读取count个字符,到buff所指向的内存空间中去。
- 函数返回值:a.如果成功从文件中读取到内容,就返回实际读取到的内容的bytes数 b.如果读到了文件的末尾,就返回0。
代码3.3:read读取文件内容
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<sys/fcntl.h>
int main()
{
int fd = open("log1.txt", O_RDONLY); //只读方式打开文件
if(fd < 0) //检查文件是否打开成功
{
perror("open");
return 1;
}
char buff[100]; //记录存储从文件中读取到的内容
memset(buff, '\0', 100); //空间内容初始化
ssize_t ret = read(fd, buff, 100); //读取100字节的内容到buff中
printf("ret = %d\n", ret);
printf("buff:%s\n", buff);
close(fd);
return 0;
}
四. 总结
- 在操作系统层面,一切可以进行IO操作的设备都被视为文件。从狭义上讲文件就是磁盘文件,从广义上讲文件包含磁盘、显示器、键盘、鼠标、网卡、声卡等诸多设备。
- 每一种编程语言都会提高特定的IO接口,对系统IO接口进行封装,从而方便用户的使用、实现语言的跨平台性能。
- Linux操作系统提供了open、write、read、close等系统调用接口,用于文件IO操作。