Bootstrap

【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以方便我们的使用。

----------------------------------创作不易,觉得有帮助的话就点赞支持一下吧-----------------------------------​​​​​​​

;