【Linux】文件

Linux 文件

  • 什么叫文件
  • C语言视角下文件的操作
    • 文件的打开与关闭
    • 文件的写操作
    • 文件的读操作 & cat命令模拟实现
  • 文件操作的系统接口
    • open & close
    • write
    • read
  • 文件描述符
  • 进程与文件的关系
  • 重定向问题
  • Linux下一切皆文件的认识
  • 文件缓冲区
    • 缓冲区的刷新策略
  • stuout & stderr

什么叫文件

狭义的文件:普通的磁盘文件
广义的文件:几乎所有的外设,都可以称作文件
站在系统的角度,那些能够被读取(input),或者能够被写出(output)的设备就叫做文件。

C语言视角下文件的操作

文件的打开与关闭

在这里插入图片描述
path是文件的路径,mode的选择如下。
在这里插入图片描述

// 此时是打开(或创建)当前路径下的文件 log.txt
FILE* pf = fopen("log.txt", "w");

所谓当前路径,准确来说是:当一个进程运行起来的时候,这个进程所处的工作路径。
在这里插入图片描述
w的方式打开,如果文件存在,会将文件清空(是在对文件读写操作之前);如果文件不存在,则会创建。
这里可以get到一个tips,就是如何将文件清空。
这里给出更直接的使用方法:> yourfile>表示输入重定向。
在这里插入图片描述
a方式打开,是为了open for appending,与其对应的有>>追加重定向。
文件的关闭使用fclose,传入文件指针就可以了。
在这里插入图片描述

文件的写操作

文件的写操作有多种,下面代码中给出部分示例。

const char* s[] = {"hello fwrite\n","hello fprintf\n","hello fputs\n"};fwrite(s[0], strlen(s[0]), 1, pf);
fprintf(pf, "%s", s[1]);
fputs(s[2], pf);

在这里插入图片描述

文件的读操作 & cat命令模拟实现

下面再以读的方式r模拟实现cat命令
读取信息的就用fgets函数,
在这里插入图片描述
在这里插入图片描述

void Test1(int argc, char* argv[])
{if(argc != 2){printf("argv error!");exit(1);}FILE* pf = fopen(argv[1], "r");if(pf == NULL){perror("fopen");exit(2);}char line[64] = {0};while(fgets(line, sizeof(line), pf)){fprintf(stdout, "%s", line);}fclose(pf);
}

在这里插入图片描述

文件操作的系统接口

访问文件本质其实是进程通过调用接口访问,下面就来学习一下文件的调用接口。

open & close

在这里插入图片描述
open的接口使用比较复杂。
如果想实现w方式打开,需要如下的传参。

/*
* 宏定义,代表一个 bit 数据
* O_WRONLY 代表只写
* O_CREAT 代表创建文件
* O_TRUNC 代表清空文件
*/
int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC)

因为对于是否只写,是否创建,是否清空,这种非此即彼的选择很符合二进制位0与1的选择,open接口内部会对传输的flags做位数据的判断来决定是否执行对应操作,这样可以简化接口的传参和节省空间。
上面O_*的传参其实是宏定义,每一个这样的定义都对应一个bit位上的数据。
在这里插入图片描述
参数mode是完成对文件权限属性的设置,用来设置文件权限的。

int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666); // rw-rw-rw-

在这里插入图片描述
上面对文件权限的设置应该是-rw-rw-rw-,可创建之后为什么是-rw-rw-r--呢?
这和权限掩码umask有关。
在这里插入图片描述

umask(0);
int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666); // -rw-rw-rw-

在这里插入图片描述
umask设置0后,就看到了预期的-rw-rw-rw-了。
文件关闭传入文件描述符(file descriptor)就行了。
在这里插入图片描述
对于文件描述符,open函数创建文件成功就会返回这个文件的文件描述符,这里我们接收就好。

write

write是用于文件写入的操作。
在这里插入图片描述
在这里插入图片描述

void Test2()
{int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);if (fd < 0){perror("open");exit(1);}// 写操作const char* s = "hello world\n";write(fd, s, strlen(s));close(fd);
}

在这里插入图片描述

read

read是用于文件的读取操作。
在这里插入图片描述
在这里插入图片描述

void test3()
{// O_RDONLY 只读int fd = open("./log.txt", O_RDONLY);if(fd < 0){perror("open");exit(1);}// 读操作char buffer[64] = {0};read(fd, buffer, sizeof(buffer));// 打印读取结果printf("%s", buffer);
}

在这里插入图片描述

文件描述符

上面对文件的操作都用到了一个东西,叫做文件描述符fd,下面就来了解一下fd是什么样一个东西吧。

void Test4()
{int fd1 = open("log1.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);assert(!(fd1 < 0));printf("open success, fd1: %d\n", fd1);int fd2 = open("log2.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);assert(!(fd1 < 0));printf("open success, fd2: %d\n", fd2);int fd3 = open("log3.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);assert(!(fd1 < 0));printf("open success, fd3: %d\n", fd3);int fd4 = open("log4.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);assert(!(fd1 < 0));printf("open success, fd4: %d\n", fd4);close(fd1);close(fd2);close(fd3);close(fd4);
}

在这里插入图片描述
为什么我们所创建的文件的文件描述符fd是从 3 开始的呢?为什么不从 0 ,从 1 开始呢?
这是因为在运行C/C++程序是,会默认打开三个文件流:
stdin 标准输入;stdout 标准输出;stderr 标准错误。
在这里插入图片描述
这三个流的类型都是FILE*的,FILE*类型是结构体指针类型(FILE结构体是C标准库提供的),文件描述符fd也只是这个结构体中的一个成员变量。而默认打开的这三个文件,它们已经占据了0,1,2三个fd的值。
在这里插入图片描述

进程与文件的关系

文件大致可以分成两类:

  1. 磁盘文件(没有被打开,文件=内容+属性)
  2. 内存文件(进程在内存中打开)

进程要访问文件,必须先打开文件。一个进程可以打开多个文件(文件要被访问,必须是先加载到内存中)。
当多个进程都要访问文件时,系统中会存在大量的被打开的文件。
面对如此之多的被打开的文件,操作系统就要采用先描述,再组织的方式将这些被打开的文件进行管理。
Linux内核中,面对每一个被打开的文件都要构建一份struct file结构体(包含了一个被打开的文件的几乎所有的内容,不仅仅包含属性)
通过创建struct file对象来充当一个被打开的文件,并通过双链表的结构组织起来。
在这里插入图片描述
上面进程与文件的关系在内核代码中的体现如下图。
在这里插入图片描述
每个进程都有一个指针*files, 指向一张表files_struct,该表最重要的部分就是包含的一个指针数组,数组中每个元素都是一个指向打开文件的指针!所以,本质上,文件描述符就是该数组的下标。所以,只要拿着文件描述符,就可以找到对应的文件。
在这里插入图片描述

重定向问题

void Test5()        
{                                close(1);                              // fd的分配规则是:优先分配最小的,没有被占用的文件描述符 int fd = open("./log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);if(fd < 0)   {                                    perror("open");          exit(1);}          // 应该往显示器(标准输出)上写入的// 但是都写入到了 log.txt// 这是什么呢?-> 输出重定向      printf("fd: %d\n", fd);fprintf(stdout, "hello fprintf\n");const char* s = "hello fwrite\n";fwrite(s,strlen(s), 1, stdout);fflush(stdout);close(fd);   
}  

代码中首先关闭了fd为1的文件stdout,体现在底层就是,文件描述符表中下标为1的元素不再指向标准输出流了。
这时立即创建了log.txt文件,而fd的分配规则是:优先分配最小的,没有被占用的文件描述符。于是给log.txt分配文件描述符fd是1。
然后再调用文件写入操作的一些接口向stdout写入。默认是向fd为1的文件进行写入。
这时就将本应显示到显示器上的内容写入到了log.txt中。
在这里插入图片描述
同理,可以演示输入重定向的问题。

void Test6()    
{    close(0);    int fd = open("./log.txt", O_RDONLY);    if(fd < 0)    {    perror("open");    exit(1);    }    printf("fd: %d\n", fd);    // 输入重定向// 本来从键盘读取数据,变成从log.txt读取    char buffer[64] = {0};    fgets(buffer, sizeof buffer, stdin);    // 将读取的信息进行打印printf("%s", buffer);    close(fd);    
}    

在这里插入图片描述
从上面的式样中可以看出,重定向问题本质其实是在操作系统内部,更改fd对应的内容的指向。
但是上面的重定向操作需要需要先手动关闭默认打开的文件流,这里介绍一个dup2函数来更好的完成重定向操作。
在这里插入图片描述
在这里插入图片描述
dup2接口中oldfdnewfd的拷贝不是单纯的int的拷贝,反应在底层上是文件描述符所对应的数组元素中file*指针的拷贝。
oldfd拷贝给newfd,最后newfd所指向的内容是要和oldfd一样的。
这里利用dup2接口将>重定向和>>重定向的功能模拟实现一下。

void Test7()
{if(argc != 2){exit(1);}// 输入重定向int fd = open("./log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);// 追加重定向//int fd = open("./log.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);if(fd <  0){perror("open");exit(2);}dup2(fd, 1);fprintf(stdout, "%s\n", argv[1]);close(fd);
}

在这里插入图片描述

Linux下一切皆文件的认识

LInux下一切皆文件,是体现在Linux操作系统的软件设计层面上的设计哲学。
当操作系统面对底层不同的硬件,一定对应的是不同的操作方法。
但是对于硬件这种设备都是外设,所以面对每一个设备的核心访问方法,都可以是 read/write(I/O),但是基于不同的设备可以有自己不同的 read/write,即在代码实现上是有差别的。
而且有些设备只能read或者只能write,对应实现的功能也会又所差异(比如键盘只能read)。
但是操作系统将所有这些设备都看待成struct file结构体,每一个设备都对应一个这样的struct file,结构体里面包含了这些设备的属性和方法(方法其实是函数指针)。
当操作系统上层要调用哪个设备,就可以通过找到对应的struct file,然后使用对应的操作方法使用设备了。
也就是说,在操作系统以上,就可以用一切皆文件的视角看待这些不同的设备了。
在这里插入图片描述
下面是内核代码中文件操作方法的函数指针。
在这里插入图片描述

文件缓冲区

缓冲区,简单说就是一段内存空间。它是由语言层提供的。它的存在可以提高整机效率,主要还是为了提高用户的响应速度(是一种写回模式)。

缓冲区的刷新策略

一般情况下有三种刷新策略:

  1. 立即刷新:写入缓冲区就立即刷新。
  2. 行刷新:写完一行内容再刷新。
  3. 满刷新:缓冲区写满再刷新。

特殊情况也会刷新:

  1. 用户强制刷新(fflush)。
  2. 进程退出。

其实,所有的设备,永远都倾向于全缓冲策略(缓冲区满了才刷新),这样做可以更少次地对外设进行访问,提高效率(和外部设备进行IO的时候,预备IO的过程是最费时间的)。
一般而言,行缓冲的设备文件有显示器,全缓冲的设备文件有磁盘文件。显示器的行刷新策略也是对效率和用户体验的折中处理。

void Test8()
{// C语言提供printf("hello printf\n");fprintf(stdout, "hello fprintf\n");const char* s1 = "hello fputs\n";fputs(s1, stdout);// OS提供const char* s2 = "hello write\n";write(1, s2, strlen(s2));// 调用fork时,上面的函数已经被执行完了,但并不代表数据已经刷新了fork();
}

在这里插入图片描述
Test8程序在执行向显示器打印时输出4行文本,向普通文件(磁盘上)打印却变成输出7行。这是为什么呢?
其实很明显是fork()的原因,那fork()又是如何作用出现这种情况呢?
细心观察发现,打印的内容中,系统接口的打印始终是一次,C语言接口相比会多打印一次。(从这里也可以猜到缓冲区是语言层提供的)
其实,如果是向显示器打印,刷新策略是行刷新,那么最后执行fork的时候,之前的函数一定是执行完了,且数据已经被刷新了(行刷新)的,此时fork就无意义了。
当程序重定向后,向磁盘文件打印,刷新策略隐形地变成了满刷新,此时fork的时候,之前的函数一定是执行完了,但数据还没有刷新(在当前进程对应的C标准库中的缓冲区),这部分数据是属于父进程的数据的。而fork之后,程序就要退出了,同时会强制将缓冲区的数据进行刷新。而父子进程的数据被刷新势必会影响到另一方,所以发生写时拷贝,父子进程各刷新一份数据到文件,就出现C接口的打印出现两份的情况了。

在这里插入图片描述
缓冲区是语言层提供的,封装的struct FILE结构体内部就有所打开文件对应的语言层的缓冲区结构。
下面通过Test9可以对上面的解释做一个验证。

void Test9()
{// C语言提供printf("hello printf\n");fprintf(stdout, "hello fprintf\n");const char* s1 = "hello fputs\n";fputs(s1, stdout);// OS提供const char* s2 = "hello write\n";write(1, s2, strlen(s2));// fork之前,进行强制刷新fflush(stdout);// 调用fork时,上面的函数已经被执行完了,但并不代表数据已经刷新了fork();
}

在这里插入图片描述

stuout & stderr

stdout对应的文件描述符是1,stderr对应的文件描述符是2。
它们对应的都是显示器文件,但却是不同的。可以认为,同一个显示器文件,被打开了两次。

void Test10()
{// stdout -> 1printf("hello printf 1\n");fprintf(stdout, "hello fprintf 1\n");const char* s1 = "hello write 1\n";write(1, s1, strlen(s1));std::cout << "hello cout 1" << std::endl;// stderr -> 2    perror("hello perror 2");const char* s2 = "hello write 2\n";write(2, s2, strlen(s2));std::cerr << "hello cerr 2" << std::endl;
}

在这里插入图片描述
从这里发现,重定向的是1号文件描述符对应的文件。
所以可以通过重定向把标准输入和标准输出的内容输入到不同的文件中进行分开查看。
![在这里插入图片描述](https://img-blog.csdnimg.cn/e6143fe26eaa4f91b5604c946427517b.png =x150x)
一般而言,如果程序运行有,可能有错误的话,建议使用stderr,或者cerr来打印。如果是常规文本内容,建议使用coutstdout打印。
当然也是可以将标准输入和标准输出的内容都输入到一个文件中的。
在这里插入图片描述
打印的内容中,有一个细节是perror的打印包含有一段信息,其实就是错误码errno对应的错误信息。
这里根据perror的功能可以模拟实现一个进行理解。

void my_perror(const char* msg)                                                                           
{    fprintf(stderr, "%s: %s\n", msg, strerror(errno));
}  

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/95238.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

已解决module ‘pip‘ has no attribute ‘pep425tags‘报错问题(如何正确查看pip版本、支持、32位、64位方法汇总)

本文摘要&#xff1a;本文已解决module ‘pip‘ has no attribute ‘pep425tags‘的相关报错问题&#xff0c;并总结提出了几种可用解决方案。同时结合人工智能GPT排除可能得隐患及错误。并且最后说明了如何正确查看pip版本、支持、32位、64位方法汇总 &#x1f60e; 作者介绍&…

一篇文章教会你什么是二叉搜索树

二叉搜索树 二叉搜索树概念二叉搜索树操作1.二叉搜索树的查找2.二叉搜索树的插入3.二叉搜索树的删除4.二叉搜索树的遍历 二叉搜索树的实现1.二叉搜索树节点结构2.二叉搜索树类3.二叉搜索树的构造及析构4.二叉搜索树的拷贝构造及赋值重载5.二叉搜索树插入6.二叉搜索树查找7.二叉…

【Ubuntu】Ubuntu常用软件部署

1.安装jdk1.8 (1).apt方式安装 1).安装 1.在终端中输入以下命令&#xff0c;以更新软件包列表 sudo apt-get update2.在终端中输入以下命令&#xff0c;以安装JDK 1.8 sudo apt-get install openjdk-8-jdk3.将Java 1.8设置为默认版本。在终端中输入以下命令 sudo update-…

ElasticSearch安装为Win11服务

在windows的环境下操作是Elasticsearch,并且喜欢使用命令行 &#xff0c;启动时通过cmd直接在elasticsearch的bin目录下执行elasticsearch ,这样直接启动的话集群名称会默elasticsearch&#xff0c;节点名称会随机生成。 停止就直接在cmd界面按CtrlC 其实我们也可以将elasticse…

在抖音中使用语聚AI,实现自动回复用户视频评论、私信问答

您可以通过集简云数据流程&#xff0c;将语聚AI助手集成到抖音视频评论、抖音私信&#xff0c;实现自动回复用户视频评论、私信问答&#xff0c;大大提升账号互动与运营效率。 效果如下&#xff1a; 自动化流程&#xff1a; ● 抖音普通号评论对接语聚AI&#xff08;点击可一…

springboot实战(二)之将项目上传至远程仓库

目录 环境&#xff1a; 背景&#xff1a; 操作&#xff1a; 1.注册码云账号 2.创建仓库 步骤&#xff1a; 1.注册完码云账号后&#xff0c;点击加号&#xff0c;新建仓库 2.输入项目名称和介绍&#xff0c;点击创建 3.复制仓库地址&#xff0c;你可以选择https协议或者…

数据结构】二叉树篇|超清晰图解和详解:后序篇

博主简介&#xff1a;努力学习的22级计算机科学与技术本科生一枚&#x1f338;博主主页&#xff1a; 是瑶瑶子啦每日一言&#x1f33c;: 你不能要求一片海洋&#xff0c;没有风暴&#xff0c;那不是海洋&#xff0c;是泥塘——毕淑敏 目录 一、核心二、题目 一、核心 我们清楚…

统一网关Gateway

文章目录 概览网关的作用搭建网关断言工厂路由过滤器全局过滤器案例 过滤器执行顺序跨域问题 概览 网关的作用 搭建网关 断言工厂 路由过滤器 全局过滤器 案例 过滤器执行顺序 跨域问题

隧道结构健康监测系统,保障隧道稳定安全运行

隧道是地下隐蔽工程&#xff0c;会受到潜在、无法预知的地质因素影响&#xff0c;早期修建的隧道经常出现隧道拱顶开裂、地表沉降、隧道渗漏水、围岩变形、附近建筑物倾斜等隧道的健康问题变得日益突出&#xff0c;作为城市生命线不可或缺的一部分&#xff0c;为了确保隧道工程…

​7.3 项目3 贪吃蛇(控制台版) (A)​

C自学精简实践教程 目录(必读) 主要考察 模块划分 / 文本文件读取 UI与业务分离 / 模块划分 控制台交互 / 数据抽象 需求 用户输入字母表示方向&#xff0c;实现贪吃蛇游戏 规则&#xff1a;碰到边缘和碰到蛇自己都算游戏结束 输入文件 data.txt data.txt 内容如下&am…

Go 官方标准编译器中所做的优化

本文是对#102 Go 官方标准编译器中实现的优化集锦汇总[1] 内容的记录与总结. 优化1-4: 字符串和字节切片之间的转化 1.紧跟range关键字的 从字符串到字节切片的转换&#xff1b; package mainimport ( "fmt" "strings" "testing")var cs10086 s…

【强化学习】贝尔曼公式 - bellman equation

return作用 还是用这个迷宫游戏说。 首先明确&#xff0c;不撞墙到终点比撞墙到终点好。路径越短到终点越好。 不撞墙到终点比撞墙到终点好。你可以把撞墙这个reward设置成负数&#xff0c;不撞墙设置成0。那么在最终return进行累加的时候&#xff0c;不撞墙的return就会大。路…