Linux操作系统~系统文件IO,什么是文件描述符fd?什么是vfs虚拟文件系统

  • Post author:
  • Post category:linux



目录


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语言缓冲区中的内容刷新到文件内核缓冲区中。



版权声明:本文为qq_24016309原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。