Linux操作系统~系统文件IO,什么是文件描述符fd?什么是vfs虚拟文件系统_操作系统fd和fds的区别-程序员宅基地

技术标签: c++  Linux  linux  操作系统  服务器  microsoft  开发语言  

目录

1.open()

(1).第二个参数flags—通过比特位传多组标记

2.文件描述符fd(open函数的返回值)

(1).fd的本质

(2).vfs-虚拟文件系统(一切皆文件)

(3).调用read方法执行流程

 3.文件描述符的分配规则

输出重定向(追加重定向,输入重定向)

printf为什么会向标准输入中输入

4.dup2系统调用

Q:执行exec*程序替换的时候,会不会影响我们曾经打开的所有的文件! !

Q:子进程创建的时候,file_struct中的数据会被拷贝过来吗,指针指向的文件呢?

5.缓冲区

(1).看一个问题(刷新策略)

用户->OS :刷新策略:

(2).write是系统调用,不会用C语言缓冲区


tips:为什么要学习使用系统调用接口?

我们的输入输出最终都是访问硬件,OS是操作系统的管理者

我们使用的都是语言层面上的接口,所以的“语言”上的操作,都必须贯穿OS。

然而操作系统不相信任何人,所以访问操作系统都是需要通过系统调用的接口的。

        因此几乎所有的语言fopen,fclose,fread,fwrite,fgets,fputs,fgetc,fputc等底层一定需要使用OS提供的系统调用

上面的 fopen fclose fread fwrite 都是C标准库当中的函数,我们称之为库函数(libc)。

而, open close read write lseek 都属于系统提供的接口,称之为系统调用接口

系统调用接口和库函数的关系,一目了然。

所以,可以认为,f#系列的函数,都是对系统调用的封装,方便二次开发。


1.open()

1.实际上C语言的fopen函数底层调用的就是系统的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: 打开文件时,可以传入多个参数选项,用下面的一个或者多个常量进行“或”运算,构成flags。

 O_RDONLY: 只读打开

 O_WRONLY: 只写打开(默认是覆盖写)

 O_RDWR : 读,写打开

 这三个常量,必须指定一个且只能指定一个

 O_CREAT : 若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限

 O_APPEND: 追加写

  • 返回值:

 成功:新打开的文件描述符

 失败:-1

mode:控制创建文件的权限,以8进制的方式传入,这里我们传入的是0644

int main()
{
    //fopen("./log.txt", "w")
    
    int fd = open("./log.txt", O_WRONLY | O_CREAT, 0644);

    if(fd < 0){
        printf("open error\n");
    }

    printf("fd: %d\n", fd);
    close(fd);
}

(1).第二个参数flags—通过比特位传多组标记

        我们能想到的是通过传123456,来分别对应不同的标志位,但是操作系统这个地方的参数是按位传递的,每一个bit代表一个标志像O_RDONLY,O_WRONLY都是只有一个比特位是1的数,所以它们可以按位|在一起,传入系统调用以后,操作系统可以将传入的flag的值和对应的O_CREAT,O_WRONLY按位&,这样就可以判断当前的文件的打开方式是否是只写,如果文件不存在是否要创建

if(O_WRONLY & flag)
{
    //如果与一下结果为真,则表示flag传入的标志位里面有O_WRONLY,执行对应操作
}

2.文件描述符fd(open函数的返回值)

        文件不打开之前,存放在磁盘中打开文件后被加载到内存。一个进程可以打开多个文件,也就是说,进程被打开后,操作系统需要管理比进程数量更多的文件,这个时候就需要先描述再组织。

struct file 
{
    //包含了打开文件的相关属性信息
    //文件操作指针集合
}

        打开的时候,就是把文件的属性加载到struct file中,所以文件 = 内容+属性。不只是内容,属性也是文件的数据。在文件没有被打开之前,文件的内容和属性都放在磁盘里面。

(1).fd的本质

        fd:本质是操作系统内核中一个指针数组的下标,用于关联进程及其对应的文件的一个指针数组的下标。

        而现在知道,文件描述符就是从0开始的小整数。当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件。于是就有了file结构体。表示一个已经打开的文件对象。而进程执行open系统调用,所以必须让进程和文件关联起来。每个进程都有一个指针*files, 指向一个结构体files_struct,该结构体最重要的部分就是包涵一个指针数组每个元素都是一个指向打开文件的指针(这个文件file所指向的也是一个结构体file,里面有文件的属性等信息)!所以,本质上,文件描述符就是该数组的下标。所以,只要拿着文件描述符,就可以找到对应的文件。

        file结构如下所示,通过f_inode可以找到文件对应的inode结构体(在我之后的文章中会讲到,里面存放了文件的属性信息)。除此之外,该结构中还有f_op,通过它我们可以访问到一个结构体,里面有对文件进行读写操作的接口。write和read函数会调用到这里面的读写接口,从而继续调用统一磁盘驱动中的读写接口,完成对磁盘的读写。

(2).vfs-虚拟文件系统(一切皆文件)

        我们的外设,都可以调用read和write函数,只是像键盘这样的外设,提供的写方法可能是空,然后我们在硬件层的上方有一个vfs,实现了类似多态的功能。

        操作系统中一切皆文件,所以把所有的外设都看作是文件,文件需要用struct file来组织,每个struct file里面有两个函数指针,分别指向不同文件的读方法和写方法。从而在上层看来,我要读就调用文件struct file里面的读方法,写就调用写方法,而不需要关心你这个文件是什么。上层可以将所有文件都看成是struct file类型。


(3).调用read方法执行流程

结合前面的图

        整体流程:调用read方法,进程打开文件以后,现在进程的PCB里面找到一个files struct的结构体指针,找到这个files struct,这个结构体中有一个指针数组,其下标就是文件描述符,对应的元素都是一个个file类型的指针,对应进程打开的文件(前三个下标对应的分别是标准输入,输出,以及标准错误,对应的外设是键盘,显示器,显示器,操作系统默认会为进程打开这三个文件,在操作系统中,一切皆文件),file也是一个结构体,里面存放的是文件的属性等信息,还有对文件进行read和write的函数指针(放在一个函数表里面),调用read方法对文件进行读操作。 


 3.文件描述符的分配规则

在files_struct的指针数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符。

        我们打开用open打开一个文件,文件的信息就会被加载到内存中,会产生一个file对象(保存文件的信息),此时就需要在一个file_struct的指针数组中分配一个空间存放指向这个文件对象,这个位置对应的下标就是文件描述符,0,1,2分别被标准输入,输出,错误给占用了,所以打开的第一个文件的文件标识符是3。

        如果我们close关闭文件,管理文件用的file也就会被回收,所以其对应的文件描述符也就空出来可以给别的文件使用了。

  • 我们这里关闭0或者2,也就是标准输入或者标准错误的话,0和2会被分配给我们新打开的这个文件

输出重定向(追加重定向,输入重定向)

  • 如果我们这里关闭1,也就是关闭掉标准输入的话,此时再打开一个文件,文件标识符1就会被配给这个文件,此时这个文件就相当于是这个进程的标准输出,所有原本标准输出的内容(比如printf)都会输出都这个文件中,这就叫做输出重定向。
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
int main()
{
    close(1);
    int fd = open("myfile", O_WRONLY | O_CREAT, 00644);
    if (fd < 0)
    {
        perror("open");
        return 1;
    }
    printf("fd: %d\n", fd);
    fflush(stdout);
    close(fd);
    exit(0);
}

        echo”hello” > log.txt输出重定向的原理就是把echo进程的1关掉,然后把log.txt这个文件打开,文件表示符1也就会被分配给log.txt,此时log.txt就作为标准输出了。

追加重定向的原理

close(1);
int fd = open("./log.txt",O_CREAT | O_WRONLY | O_APPEND, 0644);

输入重定向的原理:

close(0);
int fd = open("./log.txt", O_RDONLY);
// fd == 0
printf("fd: %d\n", fd);
char line[128];
while (fgets(line, sizeof(line) - 1, stdin))
{ // stdin -> FILE * -> FILE 是一个结构体 -> fd == 0
    printf("%s", line);
}

printf为什么会向标准输入中输入

        printf会向stdout里面打印,而stdout是一个File类型的指针,对应的File是一个结构体,存放当前文件的信息,其中肯定会存有文件描述符fd(printf是向标准输出输出内容的,所以这里的fd是1),给到操作系统,操作系统根据fd找到对应要写入的文件,也就是显示器。

  • stdout是C语言层面上的,其中包含文件描述符fd = 1,对应的文件是显示器
  • stdin包含文件描述符fd = 0,对应的文件是键盘
  • stderr包含文件描述符fd = 2,对应的文件是显示器

        C语言上层的这些对文件的操作中,一定要通过系统层来实现,所以C语言层面的File结构体中一定会包含有对应要写入或者读取文件的fd,给到操作系统,操作系统根据这个fd再去找到这个文件进行对应的读写

这些操作最终一定是通过fd文件操作符来找到对应文件并完成操作的。

甚至可以打印出来看看

printf("stdin -> %d\n", stdin->_fileno);
printf("stdout -> %d\n", stdout->_fileno);
printf("stderr -> %d\n", stderr->_fileno);

4.dup2系统调用

        实际上,重定向只需要将fd对应的指针进行拷贝覆盖就可以(因为fd表示的是指针数组的下标,我们之前关闭1,实际上就是把1下标对应的指针置空,然后在创建文件,也就是让1下标的指针指向这个文件)。比如,要让fd为3的文件,输出需要重定向到这个文件,我们只需要把fd = 3对应的指针,拷贝到fd = 1的位置,这样就完成了输出重定向。

#include <unistd.h>

int dup2(int oldfd, int newfd);

read函数从标准输入中读取,然后输出重定向到.log文件

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
int main()
{
    int fd = open("./log", O_CREAT | O_RDWR);
    if (fd < 0)
    {
        perror("open");
        return 1;
    }
    close(1);  //关不关都行,反正标准输出用不到了
    dup2(fd, 1);
    for (;;)
    {
        char buf[1024] = {0};
        ssize_t read_size = read(0, buf, sizeof(buf) - 1);
        if (read_size < 0)
        {
            perror("read");
            break;
        }
        printf("%s", buf);
        fflush(stdout);
    }
    return 0;
}

Q:执行exec*程序替换的时候,会不会影响我们曾经打开的所有的文件! !

不会,因为exec进行程序替换只会替换代码或者数据,不会影响打开的文件

Q:子进程创建的时候,file_struct中的数据会被拷贝过来吗,指针指向的文件呢?

        会的,以文件描述符为下标的那个数组都会被拷贝一份。子进程创建的时候,task_struct,file_struct都是要重新创建一个的。

        但是文件的部分并不会被拷贝一份,所以会出现父进程和子进程中文件描述符对应的指针可能会指向同一个文件(这里的说的文件实际上是一个FILE类型的对象)。

        父进程打开的标准输入,标准错误和标准输出都会默认被打开,子进程都会继承(因为文件描述符对应的指针都被子进程继承了),这也就是为什么所有进程的标准输入,输出,错误都默认被打开,因为bash打开了,其他的进程都是bash的子进程。


5.缓冲区

(1).看一个问题(刷新策略)

如果我们最后关闭了fd,为什么printf的内容没有输出到文件中?不关闭就会输出到文件中。

        printf实际上是把字符串写到了C语言的缓冲区中,要把C语言缓冲区中的内容刷新到对应文件的内核缓冲区中,必须需要文件描述符fd。因为我们关闭了1,所以这里的fd实际上是1。

        本来我们是向显示器上打印,刷新策略是行缓冲,每隔一个printf都有\n,会将C语言中缓冲区的内容刷新到对应文件的内核缓冲区中,这样最后就能输出到对应的文件中。

        现在是将输出重定向到文件中,刷新策略就变成了全缓冲(此时有\n也没用了,刷新策略已经不是行缓冲了)。此时如果我们最后不关闭fd,那在程序退出的时候,C语言中缓冲区的内容会被刷新到对应文件的内核缓冲区中,最后也能成功输出到文件中。但是如果我们关闭了fd,此时程序退出时,数据被遗留在C语言中的缓冲区中,所以也就无法正常输出到文件中。(因为想把C语言缓冲区的内容刷新到对应文件的内核缓冲区中,必须要文件描述符fd

用户->OS :刷新策略:

  1. 立即刷新(不缓冲)
  2. 行刷新(行缓冲\n),比如,显示器打印
  3. 缓冲区满了,才刷新(全缓冲),比如,往磁盘文件(普通文件)中写入

FILE类里面有与C语言缓冲区相关的内容

解决方法:在最后close前面加上fflush(stdout),在fd被关闭之前,根据fd把C语言缓冲区中的内容刷新到内核缓冲区。


(2).write是系统调用,不会用C语言缓冲区

        write是系统调用,写消息的时候是直接往文件的内核缓冲区里面写的,不会使用C语言的缓冲区,所以最后的close对write的内容没有影响。但是printf是要用到C语言缓冲区的,重定向以后导致刷新策略发生变化,close1以后无法根据文件描述符将C语言缓冲区中的内容刷新到文件内核缓冲区中。

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/qq_24016309/article/details/127899201

智能推荐

while循环&CPU占用率高问题深入分析与解决方案_main函数使用while(1)循环cpu占用99-程序员宅基地

文章浏览阅读3.8k次,点赞9次,收藏28次。直接上一个工作中碰到的问题,另外一个系统开启多线程调用我这边的接口,然后我这边会开启多线程批量查询第三方接口并且返回给调用方。使用的是两三年前别人遗留下来的方法,放到线上后发现确实是可以正常取到结果,但是一旦调用,CPU占用就直接100%(部署环境是win server服务器)。因此查看了下相关的老代码并使用JProfiler查看发现是在某个while循环的时候有问题。具体项目代码就不贴了,类似于下面这段代码。​​​​​​while(flag) {//your code;}这里的flag._main函数使用while(1)循环cpu占用99

【无标题】jetbrains idea shift f6不生效_idea shift +f6快捷键不生效-程序员宅基地

文章浏览阅读347次。idea shift f6 快捷键无效_idea shift +f6快捷键不生效

node.js学习笔记之Node中的核心模块_node模块中有很多核心模块,以下不属于核心模块,使用时需下载的是-程序员宅基地

文章浏览阅读135次。Ecmacript 中没有DOM 和 BOM核心模块Node为JavaScript提供了很多服务器级别,这些API绝大多数都被包装到了一个具名和核心模块中了,例如文件操作的 fs 核心模块 ,http服务构建的http 模块 path 路径操作模块 os 操作系统信息模块// 用来获取机器信息的var os = require('os')// 用来操作路径的var path = require('path')// 获取当前机器的 CPU 信息console.log(os.cpus._node模块中有很多核心模块,以下不属于核心模块,使用时需下载的是

数学建模【SPSS 下载-安装、方差分析与回归分析的SPSS实现(软件概述、方差分析、回归分析)】_化工数学模型数据回归软件-程序员宅基地

文章浏览阅读10w+次,点赞435次,收藏3.4k次。SPSS 22 下载安装过程7.6 方差分析与回归分析的SPSS实现7.6.1 SPSS软件概述1 SPSS版本与安装2 SPSS界面3 SPSS特点4 SPSS数据7.6.2 SPSS与方差分析1 单因素方差分析2 双因素方差分析7.6.3 SPSS与回归分析SPSS回归分析过程牙膏价格问题的回归分析_化工数学模型数据回归软件

利用hutool实现邮件发送功能_hutool发送邮件-程序员宅基地

文章浏览阅读7.5k次。如何利用hutool工具包实现邮件发送功能呢?1、首先引入hutool依赖<dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.7.19</version></dependency>2、编写邮件发送工具类package com.pc.c..._hutool发送邮件

docker安装elasticsearch,elasticsearch-head,kibana,ik分词器_docker安装kibana连接elasticsearch并且elasticsearch有密码-程序员宅基地

文章浏览阅读867次,点赞2次,收藏2次。docker安装elasticsearch,elasticsearch-head,kibana,ik分词器安装方式基本有两种,一种是pull的方式,一种是Dockerfile的方式,由于pull的方式pull下来后还需配置许多东西且不便于复用,个人比较喜欢使用Dockerfile的方式所有docker支持的镜像基本都在https://hub.docker.com/docker的官网上能找到合..._docker安装kibana连接elasticsearch并且elasticsearch有密码

随便推点

Python 攻克移动开发失败!_beeware-程序员宅基地

文章浏览阅读1.3w次,点赞57次,收藏92次。整理 | 郑丽媛出品 | CSDN(ID:CSDNnews)近年来,随着机器学习的兴起,有一门编程语言逐渐变得火热——Python。得益于其针对机器学习提供了大量开源框架和第三方模块,内置..._beeware

Swift4.0_Timer 的基本使用_swift timer 暂停-程序员宅基地

文章浏览阅读7.9k次。//// ViewController.swift// Day_10_Timer//// Created by dongqiangfei on 2018/10/15.// Copyright 2018年 飞飞. All rights reserved.//import UIKitclass ViewController: UIViewController { ..._swift timer 暂停

元素三大等待-程序员宅基地

文章浏览阅读986次,点赞2次,收藏2次。1.硬性等待让当前线程暂停执行,应用场景:代码执行速度太快了,但是UI元素没有立马加载出来,造成两者不同步,这时候就可以让代码等待一下,再去执行找元素的动作线程休眠,强制等待 Thread.sleep(long mills)package com.example.demo;import org.junit.jupiter.api.Test;import org.openqa.selenium.By;import org.openqa.selenium.firefox.Firefox.._元素三大等待

Java软件工程师职位分析_java岗位分析-程序员宅基地

文章浏览阅读3k次,点赞4次,收藏14次。Java软件工程师职位分析_java岗位分析

Java:Unreachable code的解决方法_java unreachable code-程序员宅基地

文章浏览阅读2k次。Java:Unreachable code的解决方法_java unreachable code

标签data-*自定义属性值和根据data属性值查找对应标签_如何根据data-*属性获取对应的标签对象-程序员宅基地

文章浏览阅读1w次。1、html中设置标签data-*的值 标题 11111 222222、点击获取当前标签的data-url的值$('dd').on('click', function() { var urlVal = $(this).data('ur_如何根据data-*属性获取对应的标签对象

推荐文章

热门文章

相关标签