【Linux】第二十一站:文件(一)

news2024/11/26 21:21:44

文章目录

  • 一、共识原理
  • 二、C系列文件接口
  • 三、从C过渡到系统:文件系统调用
  • 四、访问文件的本质

一、共识原理

  1. 文件 = 内容 + 属性

  2. 文件分为打开的文件没打开的文件

  3. 打开的文件:是谁打开的?是进程!----所以研究打开的文件本质是研究进程和文件的关系!

  4. 没打开的文件:在哪里放着呢?在磁盘上放着。我们最关注什么问题?没有被打开的文件非常多。文件如何被分门别类的放置好—即我们要快速的进行增删查改—即快速的找到文件

所以上面的问题总结下来就是如何存储?

  1. 一个文件被打开,必须先被加载到内存中!

进程 : 打开的文件 = 1:n(即一个进程可以打开任意个文件)

由以上两点我们可以得到:操作系统内部,一定存在大量的被打开的文件! ----OS要不要管理这些被打开的文件呢? —肯定是要的,那么怎么管理呢?----先描述,在组织 ---- 所以在内核中,一个被打开的文件都必须有自己的文件打开对象,包含文件的很多属性。struct XXX {文件属性; struct XXX* next};

二、C系列文件接口

如下所示,在下面这个函数中

第一个参数是路径,第二个参数是打开方式。返回值是FILE*即文件指针

image-20231125151450730

如下所示,当我们使用如下代码的时候

#include <stdio.h>

int main()
{
    FILE* fp = fopen("log.txt","w");
    if(fp == NULL)
    {
        perror("fopen");
        return 1;
    }
    fclose(fp);
    return 0;
}

因为我们是以写的方式打开文件,所以如果不存在这个文件,他会自动创建一个这样的文件

image-20231125151902300

注意这里的打开文件的路径和文件名,默认在当前路径下新建一个文件。

那么这里当前路径是什么呢?其实是进程的当前路径cwd

比如下面的代码中

#include <stdio.h>
#include <unistd.h>

int main()
{
    printf("PID:%d\n",getpid());
    FILE* fp = fopen("log.txt","w");
    if(fp == NULL)
    {
        perror("fopen");
        return 1;
    }
    fclose(fp);
    sleep(1000);
    return 0;
}

我们可以去查看一下它的当前目录

image-20231125152707828

在这里,如果我们更改了当前进程的cwd,就可以把文件新建到其他目录了

而我们在一个进程中更改当前目录,可以使用chdir

image-20231125152950457

所以,我们可以使用如下代码

#include <stdio.h>
#include <unistd.h>

int main()
{
    chdir("/home/jby_1");
    printf("PID:%d\n",getpid());
    FILE* fp = fopen("log.txt","w");
    if(fp == NULL)
    {
        perror("fopen");
        return 1;
    }
    fclose(fp);
    sleep(1000);
    return 0;
}

我们可以观察一下运行结果。可以发现这个文件去对应的路径创建了。

image-20231125153527696

我们再看一下当前的目录

image-20231125153633149

以上是文件的打开,我们现在重点来看一下文件写入的操作

如下是文件写入的函数

它的作用是将nmemb个size大小的ptr处的数据写入到一个文件中

image-20231125155557844

如下代码所示

#include <stdio.h>
#include <unistd.h>
#include <string.h>
int main()
{
    printf("PID:%d\n",getpid());
    FILE* fp = fopen("log.txt","w");
    if(fp == NULL)
    {
        perror("fopen");
        return 1;
    }
    
    const char* message = "hello linux message";
    fwrite(message,strlen(message),1,fp);

    fclose(fp);
    return 0;
}

运行结果如下所示

image-20231125160850651

image-20231125160903359

但是如果我们将上面代码的message修改了之后

image-20231125161038623

我们在运行一下,里面的内容也随之改变了

image-20231125161321371

这里我们会发现,原来文件的内容全部不见了。所以说w方法写入之前,都会对文件进行清空处理

这就类似于之前的重定向,就是相当于用w的方法打开了文件。然后写入内容

image-20231125161744002

所以,如果我们使用重定向的时候,前面什么也没有,就相当于清空了这个文件。当我们用w的方法打开一个文件以后,里面什么也都不会有了。

那么现在我们再来看一下上面这个代码

我们前面在这里没有+1,不过c语言中,默认会添加上\0,那么这里需要加1吗

image-20231125162847096

我们先运行一下,然后我们就会发现,这个文件里面的内容就变成了这样了,出现了一个乱码

image-20231125162953414

所以说,这里是不需要+1的,因为字符串后加上\0是C语言的规定,与文件有什么关系呢?

不过在打开文件的方式中,有一个方式是a方式,它是在文件的结尾写。如果文件不存在,则创建一个文件

我们试一下下面这个代码

image-20231125163441508

运行结果为

image-20231125163522699

所以说像我们之前的>>追加重定向,其实就是a方式的打开文件

所以w/a都是写入,w清空并从头写,a在文件结尾,追加写!

我们知道Linux下一切皆文件。在C语言中默认会打开三个流,stdin,stdout,stderr。如下图所示,这三个流的类型就是文件指针。

image-20231125164227819

其实类似的,C++中也会默认打开三个流:cin && cout && cerr

如果我们想向显示器打印也是可以的

image-20231125165627108

我们先看下面的代码。使用fprintf,我们也可以实现前面的在文件中打印的操作

image-20231125165424217

image-20231125165539739

对于fprintf,我们也可以将它的第一个流改为stdout

image-20231125165729504

这样的话,运行结果为,就成功的向显示器打印了

image-20231125165747352

如果我们想在stderr流中去写的话

image-20231125165900674

但从运行结果来看,似乎好像没有什么太大的区别

image-20231125165922307

其实

image-20231125170050877

三、从C过渡到系统:文件系统调用

我们知道,文件其实是在磁盘上的,磁盘是外部设备。所以访问文件其实是访问硬件!

我们知道计算机中是分层的

用户

程序 < - std lib / c / c++

系统调用

操作系统

硬件驱动

硬件

我们知道我们是不可直接访问硬件的,必须要自顶向下贯穿访问。而操作系统不相信任何人,所以就需要提供系统调用!

所以几乎所有的库只要是访问硬件设备,必定要封装系统调用。即printf/fprintf/fscanf/fwrite/fread/fgets/gets/…这些都是库函数,他们必定要封装系统调用接口

如下所示,这些就是文件系统调用接口

image-20231125173109123

我们先只考虑这两个open函数。

这两个open系统调用接口,一个有两个参数,一个有三个参数

上面的这个是下面的子集

所以我们先只谈三个参数的open

int open(const char *pathname, int flags, mode_t mode);

在这里,第一个参数是对应文件的路径:可是是绝对/相对都可以。也可以直接是文件名,那么默认当前目录

而第二个参数中,我们可以看到如下所示

image-20231125174502790

即flag就是一个打开的模式。必须包含O_RDONLY,O_WRONLY或者O_RDWR。

这些其实就是比特位的传参方式。

对于它的返回值,如果失败返回-1

我们先看如下代码

image-20231125175648807

运行结果为,打开失败了

image-20231125175754441

这是因为,我们刚刚用到的这个O_WRONLY选项它并不会新建文件。我们得告诉操作系统,如果文件不存在,我们需要新建它。所以我们还得加上O_CREAT选项

image-20231125180012284

运行结果为

image-20231125180029891

但是我们发现这里新创建的文件的权限是完全不对的

这是因为在linux中,要创建一个文件必须得告诉权限是什么。所以就需要第三个参数了。设置好权限

image-20231125180218285

此时新建的文件的权限已经不是刚刚那种乱码的样子了

image-20231125180326962

不过这里我们发现创建的文件它的权限也不是666,而是664,这是因为我们之前所说的,linux创建一个文件有默认的umask。这是由于这个umask是0002,所以最后一个才出现了一些问题

image-20231125180445133

但是如果我们非要创建一个666的文件。我们就需要用这个umask系统调用了

image-20231125180857722

它可以将代码里面的umask给修改掉。这里只影响该进程,不影响系统的

由于就近原则, open就会听进程的umask。

image-20231125181140151

运行结果为。可以看到,权限确实被改为了666

image-20231125181127957

对于这个open函数,它的返回值为一个int,这个整数我们称为file descriptor,即文件描述符,如果打开失败,则为-1。

如果我们想要关闭一个文件,可以用close

image-20231125182748486

它的参数正好就是文件描述符,所以我们可以传入一个文件描述符,就可以关闭对应的文件了

image-20231125182902897

还有一个系统调用是write

image-20231125183108619

它的功能是向fd文件中写入buf的count字节

image-20231125183401275

运行结果为

image-20231125183448003

如果我们紧接着将字符串改短一些

image-20231125184026136

那么最终的结果为

image-20231125184054054

现象就是,原来的内容都保留着,但是会从文件开始覆盖式的写入,但是并不会清空。

那么如果我们也想做到清空操作呢?

在我们打开文件的时候,即open函数中的第二个参数,我们可以使用O_TRUNC,即清空

image-20231125184707096

image-20231125184411874

此时我们就可以看到,原来的就被清空了

image-20231125184500466

那么如果我们想要实现追加写的功能呢?我们可以使用O_APPEND

image-20231125185213226

image-20231125185300323

运行结果为

image-20231125185350859

这样就实现了追加的功能

所以我们得到的结论是

FILE* fp = fopen("log.txt", "a");
//上面的代码下层一定封装了下面的系统调用接口
int fd = open("log.txt", O_WRONLY|O_CREAT|O_APPEND, 0666);
FILE* fp = fopen("log.txt", "w");
//上面的代码下层一定封装了下面的系统调用接口
int fd = open("log.txt", O_WRONLY|O_CREAT|O_TRUNC, 0666);

所以说,无论是什么语言,最终底层一定会采用同样的接口。底层都是open,只是他们的上层有所区别罢了。

不过我们会好奇的是,上层的函数返回值是指针,而下层的系统调用接口的返回值是int,它们是如何联系起来的呢?

四、访问文件的本质

如下图所示,我们知道,文件都是在磁盘中存储着的,并且文件需要由一个进程来打开,那么进程要打开这个文件。就需要为这个文件创建对应的内核数据结构,即struct file。这个结构体里面存储着一个被打开文件的各种信息。当打开了多个文件的时候,这些结构体就会用一个双向链表连接起来。

也就是先描述在组织,此时对文件的管理就变为了对这个链表的增删查改

image-20231125202346970

可是我们的系统存在多个进程。那么哪一个文件是哪一个进程的呢?所以我们需要建立对应关系

如下图所示,我们的进程PCB结构体里面就有一个指针它指向struct files_struct这样的一个结构体,这个结构体里面,有一个struct file* fd_array[]数组,数组里面存储着很多struct file*指针,然后每当这个进程打开一个文件时,要创建一个struct file结构体,然后将这个结构体的地址放入一个没有被使用的下标中。

image-20231125203953104

而这个表就是文件描述符表。而前面的open系统调用中,这个返回值,就是对应文件描述符表中的下标。

image-20231125204956050

所以这个fd,本质就是一个数组的下标。我们使用write这些接口的时候就需要使用文件描述符来进行辨认文件

image-20231125205218613

所以最终,文件管理和进程管理就通过这个下标产生了关联

image-20231125205533973

我们也许会思考,既然都已经让进程管理起来了文件,为什么要让文件用双链表呢?因为进程也可能会崩掉。

现在我们已经了解了访问文件的本质了,open的返回值其实就是文件描述符表的下标,那么既然如此。我们来验证一下

image-20231125210152268

运行结果为

image-20231125210220783

我们可以在多验证一些

image-20231125210537992

运行结果为

image-20231125210555886

这里返回连续的下标我们也能理解,我们也知道失败会返回-1。那么0,1,2这些下标在哪呢?

我们会注意到,0,1,2刚好是3个。在C语言中刚好要打开三个流

image-20231125210804945

C语言认为要打开三个,linux下一切皆文件。

所以每一个被打开的文件,它在底层根本就不存在这个FILE*流,在操作系统中只认fd。

所以我们现在可以验证一下,这三个流就是0,1,2这三个文件

image-20231125211320279

运行结果为

image-20231125211349480

所以这两个分别为stdout,stderr

我们现在在验证一下stdin

对于这个我们可以用read系统调用接口

image-20231125211457674

image-20231125212234495

运行结果为

image-20231125212300357

注意在这里,由于操作系统并不知道我们读取的是字符串,它最后也不会加上\0,所以我们需要自己加上

这样一来也就验证了0是stdin

所以当一个C语言程序启动的时候,会打开三个标准输入输出流,这个是C语言的特性吗?

答案显然不是,是操作系统的特性,进程默认会打开键盘,显示器,显示器

那么为什么操作系统要这么做?

因为我们电脑刚打开,显示器,键盘早就被操作系统打开了。我们在编程的时候,必须得用显示器和键盘输入和查看结果,所以语言默认都能打开。

那么在C语言中这个FILE是什么呢?

这个FILE是C库自己封装的一个结构体,这个结构体里面必须包含文件描述符。因为操作系统只认文件描述符。

我们可以来证明一下

image-20231125213516775

运行结果为

image-20231125213542822

所以现在我们就知道了这里有两种的封装了。

一种是库函数封装了系统调用接口,一种是FILE封装了文件描述符

如果我们直接将1号文件给关了

image-20231125213830203

我们会发现什么也没有了

image-20231125213845164

因为一号就是显示器文件。而printf里面必然调用了这个1号文件描述符。

如果我们将代码改为下面的

image-20231125214442540

那么结果为

image-20231125214457736

因为我们用的是2号文件去写的。我们关的只是一号文件

还有一点是,在struct file结构体里面,其实还有一个信息是引用计数count。因为可能多个文件描述符指向同一个文件。一个文件描述符指向就是1,两个指向就是2.

image-20231125214924406

所以我们关闭文件去调用close的时候,它的工作其实很简单,只需要引用计数减减,然后将这个指针位置置空。然后判断这个引用计数是否为0,如果不为空则什么也不用做到,如果为空,那么就在去回收这个struct file对象。

所以这就是我们刚刚关闭了1号文件,2号文件还能继续打印的原因。因为仅仅只是引用计数减减了。

而C++中的那些fstream中也是一样的包含fd的。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

预制构件二维码如何生成?

PC预制构件张贴二维码标识牌&#xff0c;可实现预制构建基本信息、设计图纸、安装说明书信息展示微信扫一扫即可查看预制件信息&#xff0c;大大提高施工的精度和效率&#xff1b;同时也可以实现预制生成过程管理、运输过程管理、安装过程管理、后期运维管理实现预制件的过程质…

Typescript基础面试题 | 04.精选 ts 面试题

&#x1f90d; 前端开发工程师&#xff08;主业&#xff09;、技术博主&#xff08;副业&#xff09;、已过CET6 &#x1f368; 阿珊和她的猫_CSDN个人主页 &#x1f560; 牛客高级专题作者、在牛客打造高质量专栏《前端面试必备》 &#x1f35a; 蓝桥云课签约作者、已在蓝桥云…

Spring Security 6.1.x 系列(6)—— 显式设置和修改登录态

一、前言 此篇是对上篇 Spring Security 6.1.x 系列&#xff08;5&#xff09;—— Servlet 认证体系结构介绍 中4.9章节显式调用SecurityContextRepository#saveContext进行详解分析。 二、设置和修改登录态 2.1 登录态存储形式 使用Spring Security框架&#xff0c;认证成…

快速解决Navicat连接数据库报错:10061

目录 问题原因&#xff1a; 错误提示&#xff1a; 解决方案&#xff1a; 问题1&#xff1a;如何进入指定目录&#xff1f; 问题2&#xff1a;若出现&#xff1a;“服务名无效” 将MySQL注册到win服务中 问题原因&#xff1a; mysql服务没有开启&#xff08;可能会在更新windows…

337. 打家劫舍III (二叉树)

题目 题解 # Definition for a binary tree node. # class TreeNode: # def __init__(self, val0, leftNone, rightNone): # self.val val # self.left left # self.right right class Solution:def rob(self, root: Optional[TreeNode]) ->…

游戏软件提示xinput1_3.dll缺失如何修复呢?6种常用修复方法总结

在计算机使用过程中&#xff0c;我们常常会遇到一些错误提示&#xff0c;其中之一就是“xinput1_3.dll丢失”。这个问题可能会导致游戏无法正常运行。为了解决这个问题&#xff0c;我们需要了解xinput1_3.dll是什么以及如何找回丢失的文件。本文将详细介绍xinput1_3.dll丢失的解…

鸿蒙HarmonyOS手把手带大家创建第一个项目 并做一个基本的组件结构讲解

上文 鸿蒙HarmonyOS 编辑器 下载 安装带大家了解并下载安装了 HarmonyOS 的编辑器 那么 我们现在双击打开它 在左侧菜单中选择 Create Project 这里有多种模板 我们先选择默认的 点击下一步 这里 我们项目的目录 和 名字可以修改一下 其他的都先用默认的就好了 但是还是要注…

人力资源管理后台 === 组织架构

目录 1.组织架构-树组件应用 2.组织架构-树组件自定义结构 3.组织架构-获取组织架构数据 4.组织架构-递归转化树形结构 5.组织架构-添加子部门-新建弹层组件 6.组织架构-添加子部门-表单结构 7.组织架构-添加子部门-表单基本校验 8.组织架构-添加子部门-表单业务校验 9…

2023.11.25 关于 MyBatis 的配置与使用

目录 引言 MyBatis 介绍 掌握两个点 在框架中的定位 创建数据库 配置 MyBatis 引入依赖 配置文件 创建实体类 构建 Mapper 层代码实现 添加 mapper 接口 添加 UserMapper.xml 实现 Service 层 实现 Controller 层 最终测验 阅读下面文章之前建议了解并创建…

python回溯求解电话号码组合

给定一个仅包含数字 2-9 的字符串&#xff0c;返回所有它能表示的字母组合。答案可以按 任意顺序 返回。 给出数字到字母的映射如下&#xff08;与电话按键相同&#xff09;。注意 1 不对应任何字母。 输入&#xff1a;digits "23" 输出&#xff1a;["ad&qu…

linux shell操作 - 05 进程 与 IO 模型

文章目录 计算机内存分配进程与子进程流IO模型阻塞IO非阻塞IOIO多路复用异步IO网络IO模型简单的socket并发的socket 计算机内存分配 一个32位&#xff0c;4G内存的计算机&#xff0c;内存使用分为两部分&#xff1a; 操作系统内核空间&#xff1b;应用程序的用户空间使用的操…

3.OpenResty系列之Nginx反向代理

1. Nginx简介 Nginx (engine x) 是一款轻量级的 Web 服务器 、反向代理服务器及电子邮件&#xff08;IMAP/POP3&#xff09;代理服务器 什么是反向代理&#xff1f; 反向代理&#xff08;Reverse Proxy&#xff09;方式是指以代理服务器来接受 internet 上的连接请求&#x…

力扣373场周赛题解

第一题&#xff1a; 这个题是一个简单题&#xff0c;数据范围也特别小&#xff0c;所以直接使用模拟方式暴力解答。 直接进行行移动的过程&#xff0c;然后检查移动后的结果是否与移动前相同。 代码&#xff1a; ​ public class Solution {// 将指定行循环右移k次pri…

怎样设置field symbol的断点

今天看到一篇Jerry Wang 2016年写的关于设置 conditional break point的文章&#xff0c;正好解决我多年如何设置根据fs值进行调试&#xff0c;因为watch point是不能对fs进行处理。非常感谢&#xff0c;也非常开心&#xff01; Source code breakpoint in ABAP debugger Crea…

以太网通讯协议小结--持续更新中

一、以太网介绍 以太网是一种产生较早&#xff0c;使用相当广泛的局域网技术&#xff0c;局域网就是一个区域的网络互联&#xff0c;可以使办公室也可以是学校等等&#xff0c;大小规模不一。 目前以太网根据速度等级分类大概分为&#xff1a;标准以太网&#xff08;10Mbit/s…

针对无法创建JDK8版本的SpringBoot项目

自从昨日起&#xff0c;JDK突然改版了&#xff0c;不能在使用JDK8来进行创建Spring Boot项目了&#xff0c;导致目前小编的JDK无从下手~~ 因此&#xff0c;在仔细琢磨了一晚之后&#xff0c;想起来了以下几个方法&#xff0c;与大家共勉。希望能够得到大家的认可&#xff01; …

班级管理五步法

亲爱的教师朋友们&#xff01;今天我要和大家分享一个超级实用的班级管理方法——班级管理五步法&#xff01;用这个方法&#xff0c;轻松掌握班级秩序&#xff0c;一起来看看吧&#xff01; 第一步&#xff1a;建立规矩 我们要和孩子们一起建立规矩。规矩要简单明了&#xff…

【Linux】:信号在内核里的处理

信号的发送和保存 一.内核中的信号处理二.信号集操作函数1.一些信号函数2.sigprocmask3.sigpending4.写代码 三.信号在什么时候处理的四.再谈地址空间 一.内核中的信号处理 1.实际执行信号的处理动作称为信号递达(Delivery )2.信号从产生到递达之间的状态,称为信号未决(Pending…

AI视频生成工具——Runway gen2 全功能超详细使用教程(2)

昨天给大家分享了Runway Gen1的使用教程&#xff0c;一篇文章就能让你轻松掌握使用文字和图像从现有视频生成新的视频技能&#xff0c;还没有看过的同学们可以回看过往文章。 Runway视频生成功能有3大核心成品 Gen1&#xff1a;视频转视频工具Gen2&#xff1a;视频生成编辑工…

2023年09月 Scratch(三级)真题解析#中国电子学会#全国青少年软件编程等级考试

Scratch等级考试(1~4级)全部真题・点这里 一、单选题(共25题,每题2分,共50分) 第1题 运行下面程序后,角色的x坐标值是?( ) A:100 B:90 C:110 D:120 答案:C 利用变量值作为条件,控制循环的次数。变量从0~10的过程中每次角色的x坐标都增加了10,当变量值为1…