Linux:文件与IO

发布于:2024-12-08 ⋅ 阅读:(128) ⋅ 点赞:(0)

✨✨所属专栏:Linux✨✨

✨✨作者主页:嶔某✨✨

理解文件

  • ⽂件在磁盘⾥
  • 磁盘是永久性存储介质,因此⽂件在磁盘上的存储是永久性的
  • 磁盘是外设(即是输出设备也是输⼊设备)
  • 磁盘上的⽂件本质是对⽂件的所有操作,都是对外设的输⼊和输出简称 IO
  • 文件 = 文件属性 + 文件内容
  • 0 kb的空文件也是在磁盘中占用空间的
  • 对文件的操作无非都是进程对属性的操作和对内容的操作
  • 磁盘的管理者是操作系统
  • ⽂件的读写本质不是通过 C 语⾔ / C++ 的库函数来操作的(这些库函数只是为⽤户提供⽅便),⽽是通过⽂件相关的系统调⽤接⼝来实现的

广义上对文件的理解:可以认为在Linux下一切皆文件(键盘,显示器,网卡,磁盘……)Linux下都把它们做了抽象。

回顾C语言的文件操作

 回顾C语言文件操作接口:C语言中的文件和文件操作

文件操作函数

    功能
fopen 打开文件
fclose 关闭文件
fputc 写入一个字符
fgetc 读取一个字符
fputs 写入一个字符串
fgets 读取一个字符串
fprintf 格式化写入数据
fscanf 格式化读取数据
fwrite 向二进制文件写入数据
fread 从二进制文件读取数据
fseek 设置文件指针的位置
ftell 计算当前文件指针相对于起始位置的偏移量
rewind 设置文件指针到文件的起始位置
ferro 判断文件操作过程中是否发生错误
feof 判断文件指针是否读取到文件末尾

 打开一个文件的模式:

文件使用方式 含义 如果指定文件不存在
“r”(只读)    为了输⼊数据,打开⼀个已经存在的⽂本⽂件    出错
“w”(只写) 为了输出数据,打开⼀个⽂本⽂件 建⽴⼀个新的⽂件
“a”(追加)    向⽂本⽂件尾添加数据   建⽴⼀个新的⽂件
“rb”(只读) 为了输⼊数据,打开⼀个⼆进制⽂件 出错
“wb”(只写) 为了输出数据,打开⼀个⼆进制⽂件 建⽴⼀个新的⽂件
“ab”(追加) 向⼀个⼆进制⽂件尾添加数据 建⽴⼀个新的⽂件
“r+”(读写) 为了读和写,打开⼀个⽂本⽂件 出错
“w+”(读写) 为了读和写,建议⼀个新的⽂件 建⽴⼀个新的⽂件
“a+”(读写) 打开⼀个⽂件,在⽂件尾进⾏读写 建⽴⼀个新的⽂件
“rb+”(读写) 为了读和写打开⼀个⼆进制⽂件 出错
“wb+”(读写) 为了读和写,新建⼀个新的⼆进制⽂件 建⽴⼀个新的⽂件
“ab+”(读写) 打开⼀个⼆进制⽂件,在⽂件尾进⾏读和写 建⽴⼀个新的⽂件

示例

#include<stdio.h>
int main()
{
    FILE*fp=fopen("log.txt","w");
    if(fp==NULL)
    {
        perror("fopen fail:");
        return 1;
    }
    //open success
    const char*msg="hello Qin!\n";
    int count=5;
    while(count--)
    {
        fputs(msg,fp);
    }
    fclose(fp);
    return 0;
}

进程在当前目录下新建文件并写入 

一般而言如果没有定义对应log.txt文件,系统会在当前路径自动创建该文件。并且当前路径并不是指可执行程序所处的路径,而是指该可执行程序运行成为进程时我们所处的路径。并且当前路径并不是指可执行程序所处的路径,而是指该可执行程序运行成为进程时我们所处的路径。比如我们可以在上级目录执行test 文件:

 打开⽂件,本质是进程打开,所以,进程知道⾃⼰在哪⾥(环境变量cwd 等等),即便⽂件不带路径,进程也知道。由此OS 就能知道要创建的⽂件放在哪⾥。

可以使⽤ ls /proc/[进程id] -l 命令查看当前正在运⾏进程的信息:  

然后我们可以看见两个软连接

  • cwd指向当前进程运⾏⽬录的⼀个符号链接。
  • exe指向启动当前进程的可执⾏⽂件(完整路径)的符号链接。

 三个默认打开流

我们常说Linux下一切皆文件,那么我们的键盘与显示器自然也是文件。我们向键盘输入数据,本质就是操作系统向键盘文件中读取数据;我们能从显示器看见数据,本质就是操作系统向显示器文件写入数据。但是我们在使用键盘与显示器时并没有手动进行任何文件相关的读写操作,那我们又是如何对键盘文件与显示器文件进行读写的呢?

答案自然是操作系统自动帮我们打开的,任何进程在运行时,操作系统都会默认打开三个输入输出流,分别为:标准输入流,标准输出流以及标准错误流。对于C语言分别就是:stdin、stdout以及stderr。对于C++分别就是:cin、cout和cerr,自然其他语言也会有相似的概念,因为这是操作系统所支持的,而不是某个语言所独有的。

我们可以在Linux中的man查看对应的声明:

其中标准输入流对应的就是我们的键盘,而标准输出流与标准错误流对应的就是我们显示器。

其中我们也可以通过fputs 函数验证一下:

#include<stdio.h>
int main()
{
  //向显示器打印
  fputs("hello QinMou!\n",stdout);
  fputs("hello QinMou!\n",stdout);
  fputs("hello QinMou!\n",stdout);
  fputs("hello QinMou!\n",stdout);
  return 0;
}

系统调用文件接口 

在前面我们学习操作系统时知道,为了方便用户使用,一般我们会对系统接口进行封装。我们的文件操作也不例外,像fopenfclose 等接口本质其实对操作系统提供的文件接口的封装。接下来我们就来学习一下系统提供的文件接口。 

open函数

首先我们来介绍文件打开操作的系统接口。

  • pathname:表示打开或者创建的目标文件,若pathname以路径的方式给出,则当需要创建该文件时,就在pathname路径下进行创建。若pathname以文件名的方式给出,则当需要创建该文件时,默认在当前路径下进行创建。
  • flags:表示打开文件的方式。
  • mode:表示创建文件的默认权限(八进制数)。

 其中常用文件打开方式有如下几个:

参数选项 含义
O_RDONLY 以只读的方式打开文件
O_WRNOLY 以只写的方式打开文件
O_APPEND 以追加的方式打开文件
O_RDWR 以读写的方式打开文件
O_CREAT 当目标文件不存在时,创建文件

参数 flags 使用了位图的传参方式,如果想同时兼具多个打开方式,可以使用逻辑与 | 链接两个选项。比如说我们想打开文件并且文件不存在时创建文件,可以写成:

O_WRNOLY | O_CREAT

所以我们也可以使用按位与&操作来检测是否设置某个选项:

if (flags&O_RDONLY){
    //设置了O_RDONLY选项
}
if (flags&O_WRONLY){
    //设置了O_WRONLY选项
}
if (flags&O_RDWR){
    //设置了O_RDWR选项
}
if (flags&O_CREAT){
    //设置了O_CREAT选项
}
//...

并且如果我们打开的文件已存在就使用两个参数的接口,如果打开的文件不存在就需要使用三个参数的接口,即需要为创建的文件设置默认权限。

Linux:权限-CSDN博客 

如果我们要为文件设置默认权限,就需要考虑文件默认掩码umask的影响。我们之前讲过文件的默认权限为:mode&(~mask),我们除了可以在命令行通过指令umask 八进制数来修改默认的掩码umask(默认为002)外,还能在程序中调用umask函数进行修改。比如我们将umask设置为0:

umask(0); // 将文件默认掩码设置为0

最后再来探究一下open 的返回值,也就是文件描述符 fd

#include<stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
    umask(0);//设置文件掩码为0
    int fd1 = open("log1.txt", O_RDONLY | O_CREAT, 0666);
	int fd2 = open("log2.txt", O_RDONLY | O_CREAT, 0666);
	int fd3 = open("log3.txt", O_RDONLY | O_CREAT, 0666);
	int fd4 = open("log4.txt", O_RDONLY | O_CREAT, 0666);
	int fd5 = open("log5.txt", O_RDONLY | O_CREAT, 0666);
	printf("fd1:%d\n", fd1);
	printf("fd2:%d\n", fd2);
	printf("fd3:%d\n", fd3);
	printf("fd4:%d\n", fd4);
	printf("fd5:%d\n", fd5);
    return 0;
}

 

运行之后我观察到文件描述符是从3开始的,并且依次递增,这起始并不是偶然。0 1 2系统默认给了标准输入,标准输出,标准错误。

当然这只是文件成功返回的情况,如果文件打开失败,那将返回-1。

close函数 

我们可以调用系统接口close 来关闭指定文件,其原型为: 

使用close 函数时传入需要关闭文件的文件描述符即可,若关闭文件成功则返回0,若关闭文件失败则返回-1。 

 write函数

同样我们也能通过系统接口write 对文件进行写入,其原型为: 

其中fd 指的是文件描述符,buf 为用户缓冲区,而count 期望写的字节数。如果写入成功返回实际写入的字节数,若写入失败则返回-1。

注意:ssize_t其实就是一个有符号整型,具体来说就是被typedef重新定义过:typedef int ssize_t

以下我们可以利用write函数对一个log.txt文件进行写入:

#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<string.h>
int main()
{
    int fd = open("log.txt",O_WRONLY | O_CREAT);
    if(fd < 0)
    {
        //open error
        perror("open fail:");
        return 1;
    }
    const char* msg = "hello QinMou!\n";
    for(int i = 0; i < 8; i++)
    {
        write(fd, msg, strlen(msg));
    }
    close(fd);
    return 0;
}

read函数 

同样我们也能通过系统接口read 对文件进行读写,其原型为: 

其中fd 指的是文件描述符,buf 为用户缓冲区,而count 为期望读的字节数。如果读出成功返回实际读出的字节数,若读出失败则返回 -1。

以下我们可以利用read 函数对一个log.txt 文件进行读出:

#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<string.h>
int main()
{
    int fd = open("log.txt", O_RDONLY);
    if(fd < 0)
    {
        perror("open fail:");
        return 1;
    }
    char buf[1024] = {'\0'};
    ssize_t ret = read(fd, buf, 1023);
    if(ret > 0) printf("%s",buf);
    
    close(fd);
    return 0;
}

 

文件描述符——fd 

在我们的操作系统中,文件是由我们进程所打开的,存在大量进程就意味着存在大量被打开的文件。为了方便我们对文件进行管理,我们就将每个文件struct file 链入我们的双向链表之中。 

struct File
{
  //包含了打开文件的相关属性
  //链接属性
};

而一个文件也可能被多个进程所读写,为了让操作系统能够准确识别每个进程对应的文件,我们就一定要让进程与我们的文件建立联系。事实也是如此,我们的进程控制块task_struct 中就存在一个指针指向一个名为struct file_struct 的结构体,这个结构体中存在一个结构体指针数组struct file*fd_array[] 分别存放着着每个文件struct file 的地址。这样我们的进程就与文件建立起了联系。

 

 

一般我们的指针数组struct file*fd_array[] 0,1,2下标分别对应我们的标准输入流,标准输出流,标准错误流这三个文件,而这些下标就是我们所说的文件描述符——fd。这也解释了我们打开文件的描述符为什么从3开始,并且依次递增。并且,通过对应的文件描述符,进程只需要找到对应的指针数组fd_array 就能访问对应的文件,这也是为什么我们文件的系统调用接口的参数一定会有fd 的原因。

当然如果我们在中途关掉某个文件,操作系统就会为该下标重新分配对应的文件。 

#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
int main()
{
    close(0);
    close(2);
	int fd1 = open("log1.txt", O_RDONLY | O_CREAT, 0666);
	int fd2 = open("log2.txt", O_RDONLY | O_CREAT, 0666);
	int fd3 = open("log3.txt", O_RDONLY | O_CREAT, 0666);
	int fd4 = open("log4.txt", O_RDONLY | O_CREAT, 0666);
	int fd5 = open("log5.txt", O_RDONLY | O_CREAT, 0666);
	printf("fd1:%d\n", fd1);
	printf("fd2:%d\n", fd2);
	printf("fd3:%d\n", fd3);
	printf("fd4:%d\n", fd4);
	printf("fd5:%d\n", fd5);
	return 0;
}

 

我们也知道,当一个程序运行起来时,操作系统会将该程序的代码和数据加载到内存,然后为其创建对应的task_struct、mm_struct、页表等相关的数据结构,并通过页表建立虚拟内存和物理内存之间的映射关系。如果与我们的文件管理联系起来,就是一个磁盘文件log.txt 加载进内存形成内存文件,最后加入对应双向链表中管理起来。

当文件存储在磁盘上时,我们称之为磁盘文件。而当磁盘文件被加载到内存中后,就变成了内存文件。磁盘文件与内存文件的关系,恰似程序和进程的关系。程序在运行起来后成为进程,同样,磁盘文件在加载到内存后成为内存文件。磁盘文件主要由两部分构成,即文件内容和文件属性。文件内容指的是文件中存储的数据,而文件属性则是文件的一些基本信息,包括文件名、文件大小以及文件创建时间等。这些文件属性也被称为元信息。在文件加载到内存的过程中,一般会先加载文件的属性信息。这是因为在很多情况下,我们可能只需要了解文件的基本属性,而不一定立即需要对文件内容进行操作。当确实需要对文件内容进行读取、输入或输出等操作时,才会延后式地加载文件数据。这样的设计可以提高系统的效率,避免在不必要的时候浪费资源加载大量的文件数据。

 本期博客到这里就结束了,如果有什么错误,欢迎指出,如果对你有帮助,请点个赞,谢谢!


网站公告

今日签到

点亮在社区的每一天
去签到