【Linux】文件IO的系统接口 | 文件标识符
🪐🪐🪐欢迎来到程序员餐厅💫💫💫
主厨的主页:Chef‘s blog
所属专栏:青果大战linux
最近真的任务拉满了,真想一直安安静静的敲代码啊,补药来打扰我了。
我们前面这几篇博客算是把进程这个东西翻了个底朝天,接下来要做的就和文件相关了。为了方便后面对接口的调用,我会把我们要用的接口都在这里讲清楚。
文件接口可以分为两大类,一类是系统接口,一类是语言接口,当然了语言接口都是对系统接口的封装以方便用户使用。
C语言文件接口
fopen
fopen
是 C 语言中的一个标准库函数,用于打开一个文件。它的函数原型在<stdio.h>
头文件中定义,
FILE * fopen ( const char * filename, const char * mode );
-
filename
是一个字符串,表示要打开的文件的路径(绝对路径或相对路径) -
mode
也是一个字符串,用于指定文件的打开模式
mode参数
-
只读模式(
r
)当使用"r"
模式打开一个文件时,程序只能从文件中读取数据。如果文件不存在,fopen
会返回NULL
。 -
只写模式(
w
)以"w"
模式打开文件时,会创建一个新文件(如果文件不存在),或者清空原有文件内容(如果文件已存在),然后用于写入数据。例如: -
追加模式(
a
)使用"a"
模式打开文件时,会在文件末尾追加数据。如果文件不存在,会创建一个新文件用于追加。
返回值是一个FILE*的指针,可以通过该值对文件进行进一步操作,FILE是一个结构体类型,里面存放了关于文件的各种信息,当然了,学C语言时的我们是不会去深入探究的,但学Linux的我们就需要好好研究它了。
fclose
fclose
是 C 语言中用于关闭文件的标准库函数。,定义在<stdio.h>
头文件中。
int fclose(FILE *stream);
stream是我们要关闭的文件的指针
成功关闭文件返回0,失败返回EOF(-1)
Linux的IO接口
在一个进程启动时,会自动打开三个输入输出流,即stdin,stdout,stderr,标准输入流,标准输出流,标准错误流,他们对应的硬件分别的是键盘,显示器,显示器,但是这些硬件被包装成了文件的形式,使得我们可以使用操作文件的方式通过FILE*指针操作他们
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
是一个整数,用于指定文件的打开方式,
flags是以位图的形式被使用的,而下面这些选项其实就是整型所定义的宏,要用哪些选项直接通过按位或传参就行
- 只读方式(
O_RDONLY
)进程只能从指定的文件中读取数据。 - 只写方式(
O_WRONLY
)允许进程向文件中写入数据,但不能读取。 - 读写方式(
O_RDWR
)允许进程对文件进行读取和写入操作。 O_CREAT
(创建文件):如果要打开的文件不存在,就创建一个新的文件。O_APPEND
(追加模式):用于在文件末尾追加数据。当文件以O_APPEND
标志打开后,每次写入操作都会将数据添加到文件的末尾,而不会覆盖已有的内容。O_TRUNC
。用于截断现有文件的内容,在文件以可写方式打开时,将文件长度设置为 0。
如果你想在flags里传入多个宏,那就要以按位或的方式传参
#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
#include<fcntl.h>
#include<sys/stat.h>
int main(){
open("./test.txt",O_WRONLY);
return 0;
}
这段代码在执行后,发现没有test.txt文件所以就什么也不做,而下面这段代码则是创建了该文件
int main(){
open("./test.txt",O_WRONLY |O_CREAT);
return 0;
}
但是这个文件是被标红了的,注意,这个不是我干的是OS干的,原因是他的权限是随机生成的,OS认为有问题所以给他标红,要解决这个问题就要引入第三个参数了
mode
参数主要在创建新文件(O_CREAT
被指定)时使用,用于指定新文件的权限。
int main(){
open("./test.txt",O_WRONLY |O_CREAT,0666);
return 0;
}
此时权限就被设置好了,当然了结果并不是我们传入的0666,而是0664,这是因为我们传入的是初始权限,而最后文件的权限还要在经过掩码修改,对掩码不清楚的可以移步这里Linux的权限讲解
文件描述符fd
好了,关于剩下的文件系统接口都需要一个叫做fd的参数,所以我们插叙一下,先来学fd
开宗明义:所谓的整型fd,其实就是数组下标
文件是等于文件内容+属性(创建时间、所属人等)的,在打开文件之前这些信息都被保存在了磁盘上。
假设我们创建了一份代码A,它里面包含一段打开文件的代码。那么当他被编译链接成可执行文件并且被执行时,就会成为进程A在CPU上执行代码,当CPU执行到了打开文件的代码段时,open函数被执行,根据冯诺依曼体系结构一个文件与CPU交互首先要加载到内存,于是文件就被从磁盘加载到了内存。在此基础上结合进程的知识,我们可以猜想电脑种可能有几十、几百个文件都被打开了,那这么多文件怎么被有效的管理呢?
因此,OS其实为文件设计了一个结构体struct file,去描述文件,之后再通过各种数据结构把这些结构体组织起来。关于这个结构体有什么我们暂且不讨论。
首先我们要明确,一个进程是可以打开多个文件的的,只需要写很多open函数就可以了,那么怎么把打开的文件和进程联系起来呢?很简单,我们在task_struct中加入一个struct file*的数组就可以了
具体是这样的,task_struct中有一个struct files_struct*类型的变量叫做files,它就是负责管理该进程所打开的文件信息的,该结构体中有一个数组struct file* fd_array,而文件描述符fd就是该数组中的下标,所以我们可以通过fd找到对应的文件。
在系统层面文件标识符fd是访问文件的唯一方式
现在我们就理解了为什么open函数的返回值是int类型,因为这个返回值就是被打开的文件fd
我们通过下面的代码展示一下
#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
#include<fcntl.h>
#include<sys/stat.h>
int main(){
int a1= open("./test1.txt",O_WRONLY |O_CREAT,0666);
int a2= open("./test1.txt",O_WRONLY |O_CREAT,0666);
int a3= open("./test1.txt",O_WRONLY |O_CREAT,0666);
int a4= open("./test1.txt",O_WRONLY |O_CREAT,0666);
int a5= open("./test1.txt",O_WRONLY |O_CREAT,0666);
int a6= open("./test1.txt",O_WRONLY |O_CREAT,0666);
printf("%d\n%d\n%d\n%d\n%d\n%d\n",a1,a2,a3,a4,a5,a6);
return 0;
}
可以看到系统在给文件分配fd时,就是递增的,毕竟数组下标本身也是递增的嘛,但是为什么我们打开的第一个文件fd时3呢 ?不应该是0吗,
原因很简单,每个进程在启动时都会默认打开三个输入输出流:stdin(标准输入流),stdout(标准输出流),stderr(标准错误流),他们的fd分别是0,1,2。
在验证这点之前先来点小分析:
现在我们知道c语言的文件接口都是对系统文件接口的封装,而系统文件接口想要被调用去访问文件做IO就需要传入fd(刚说了阿,在系统层面文件标识符fd是访问文件的唯一方式),可是我们随便看一个c语言的文件接口(如下图的fread)发现他的参数只有FILE*这个指针,于是我们就可以猜测fd是包含在FILE这个结构体中的。
#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
#include<fcntl.h>
#include<sys/stat.h>
int main(){
printf("%d\n",stdin->_fileno);
printf("%d\n",stdout->_fileno);
printf("%d\n",stderr->_fileno);
return 0;
}
我们也确实在FILE这个结构体中找到了一个成员fileno,它就是fd!
现在我们理解了fd就可以愉快的学习别的系统文件接口了
close
传入文件fd,即可关闭它
成功返回0,失败返回-1
#include <unistd.h>
int close(int fd);
write
向文件写入
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
-
fd:要被写入的文件
-
buf:要写入的内容
-
count:要写入内容的大小,单位字节
读取成功返回写入内容的字节数,失败返回-1
#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
#include<fcntl.h>
#include<sys/stat.h>
int main(){
int fd1=open("./test1.txt",O_WRONLY);
write(fd1,"HELLO",5);
close(fd1);
return 0;
}
结果是我们确实向test1.txt写入的HELLO
重点:
虽然我们传入的参数是字符串,但是write的buf类型是void,也就是什么参数都可以。
我们看下面的代码
#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
#include<fcntl.h>
#include<sys/stat.h>
int main(){
int a=12345;
write(1,&a,4);
return 0;
}
我们在试着把变量a的值输出到显示器
然而最后的输出结果不是12345,而是90,这是因为write采用的是文本输入!
当我们把a的地址传入进去后 他会以字符的方式分析。我的linux上是小端存储,12345的十六进制是0x00 00 30 39 ,所以他会依次解析一个字节大小的地址,分别是39,30,00,00,换成十进制就是57,48,0,0,在转换成字符就是‘9’、‘0、、’\0'.'\0',所以最后的输出结果是90.
于是我们就知道,当我们要输出的不是char类型时,还要先把要输出的值转化为字符串形式,如下
#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
#include<fcntl.h>
#include<sys/stat.h>
int main(){
int a=12345;
char buff[1024];
snprintf(buff,1024,"%d",a);
write(1,buff,4);
return 0;
}
这样显然是不方便的,于是c语言就封装出了printf等接口。
read
读取成功返回读取内容的字节数,开始读取时就已经是文件末尾则返回0,失败返回-1
ssize_t就是有符号整型
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
-
fd:要用于读取数据的文件
-
buf:要把数据读到哪个指针所指向的空间
-
count:要读取的字节数
#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
#include<fcntl.h>
#include<sys/stat.h>
int main(){
char arr[10];
int fd1=open("./test1.txt",O_RDONLY);
int first=read(fd1,arr,5);
printf("%d\n%s\n",first,arr);
int second=read(fd1,arr,5);
printf("%d\n",second);
close(fd1);
return 0;
}
第一次从test.txt读取了五个字节的字符串,所以返回值是5,第二次开始读取时已经到了文件末尾所以返回值是0
与write一样,read在从文件读取信息时也是文本读入,即读取的是字符或者字符串,假如我们要给一个int变量输入值,就要先把读取进来的字符串格式化
#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
#include<fcntl.h>
#include<sys/stat.h>
int main(){
int a;
char buff[1024];
read(0,buff,1024);
sscanf(buff,"%d",&a);
printf("a的值:%d\n",a);
return 0;
}
于是乎,scanf诞生了,他封装了read以方便我们的使用。
----------------------------------创作不易,觉得有帮助的话就点赞支持一下吧-----------------------------------