Linux操作系统之文件(一):重识IO

发布于:2025-07-03 ⋅ 阅读:(22) ⋅ 点赞:(0)

目录

前言:

一、文件

二、操作系统对文件的管理

三、open 

四、文件标识符

五、C语言中的FILE*

结语


前言:

在我们学习C语言的时候,我们都会学到有关C语言的文件操作。包括fopen,fclose等函数的使用。实际上,在操作系统中,文件的操作与管理并没有表面看上去的那么简单。

本篇开始的文件专栏将会与大家更深层次的讨论一下在linux系统下的文件操作。

一、文件

在Windows/Mac中:

  • 文本、图片、视频才是"文件"

  • 键盘、打印机是"设备"

但是在linux中,却发生了一场革命:
 

这就是著名的 "一切皆文件" 哲学,也就是为什么我们经常说:“linux下一切皆文件”的由来:

  • 键盘 → /dev/input(输入文件)

  • 打印机 → /dev/lp0(输出文件)

  • 硬盘分区 → /dev/sda1(存储文件)

  • 甚至CPU信息 → /proc/cpuinfo(虚拟文件)


当你在终端输入:

$ cat hello.txt
Hello Linux World!

你看到的是文件内容——存储在磁盘上的二进制数据,可能是:

  • 文本(ASCII/UTF-8编码)

  • 图片(像素数据)

  • 可执行程序(机器指令)

当你再输入:

$ ls -l hello.txt
-rw-r--r-- 1 tom users 26 Jun 5 10:30 hello.txt

最下面这段神秘代码就是文件的属性

 所以我们可以得出结论:

文件 就是 内容+属性


在访问一个文件前,我们肯定是要先把这个文件打开的,这是天经地义的事情。可是,在没被打开前,文件存储在哪里呢?

答案是磁盘中。

而我们在访问一个文件时,又是依靠谁来访问呢?

答案是进程。

打开一个文件的本质就是把他加载到内存中:一个文件就是由一个进程去访问,由于进程在内存中,所以最后由CPU来实际执行。

一个进程能同时访问多个文件吗?

这是可以的。

那多个进程呢?

照样可以!出现了这么多的文件,我们的操作系统需不需要去对文件进行管理呢?就像操作系统对进程的管理一样?

二、操作系统对文件的管理

 操作系统必须要对加载到内存中的一系列文件进行管理,如何管理呢?

答案依旧是:先描述,再组织

所以在内核中,文件如同进程一般,等于内核数据结构加内容。

已经知道进程在内核中是由task_struct(PCB)进行管理,进程访问文件,所以归根结底就是task_struct与文件结果的关系。


文件可以划分为两类,一种是被打开的文件,它在内存里,另一种是没有被打开的文件,它在磁盘里。

我们今天主要讨论第一种文件。

我们都知道,每个进程默认会打开三个输入输出流:

stdin    标准输入 键盘

stdout  标准输出 显示器

stderr   标准输出 显示器

不只是C语言与C++,其他语言也会有这三个输入输出流。这说明这不是一种语言的特性,而是系统层次上的特性。

那么这三个有什么意义呢?带着这个疑问,我们继续往下看:


在讲文件管理之前,我想先问问大家,平时在C语言中,有几种方法可以打印内容到显示器上。

#include<stdio.h>

int main()
{
    printf("hello\n");
    fprintf(stdout,"hello\n");
    fputs("hello\n",stdout);
    fwrite("hello\n",1,5,stdout);
    return 0;
}

一般我们有这四种方法打印出来,对不对?

他们之间有什么共同点呢?

没错,他们底层其实都是调用的系统接口write,我们所用的C文件接口,底层一定要封装对应的系统文件接口。

包括fopen与fclose也是一样,底层实际上是对我们open与close的封装。

我们就来简单的说一下open函数。


三、open 

open的第一个参数与fopen一样,也是一个路径下的文件,但是从第二个参数开始,就已经有了变化了。flags参数用于控制文件的打开方式,它由多个常量通过按位或(|组合而成。通过这种位图形式的传参,是我们可以达到自己想要的结果。

比如O_RDWR宏常量,他就代表以读写的方式打开:

#include<stdio.h>
#include <fcntl.h>

int main()
{
    int fd = open("./test.txt",O_RDWR);
    if(fd < 0)
    {
        perror("open error");
    }
    return 0;
}

如果是fopen以读写方式打开,哪怕你不存在这个文件都不会失败,因为会给你自动创建一个同名文件,但放在open里你会运行错误,因为我们没有设置专门的创建的宏常量:O_CREAT

所以我们加上该宏:
运行:

怎么回事,大家可以看到,这次运行确实成功了,但txt文件的权限却是乱码的。

这就是open第三个参数负责的事情,创建出的文件的默认权限

int fd = open("./test.txt",O_RDWR | O_CREAT,0666); 

把代码改成这样在运行,就正常了:

 

但有人说这个文件的权限不应该是0666吗,怎么显示的是0664(r权限为4,w为2,x为1),这是因为umask的存在,会导致真正的权限为:mode & ~umask

我们可以在代码中或者指令umask来修改这个值。

 


open的返回值又是什么呢?为什么是一个int?

我们是这把这个返回值打印出来试试:

int main()
{
    int fd = open("./test.txt",O_RDWR | O_CREAT,0666);
    if(fd < 0)
    {
        perror("open error");
    }
    else
    {
        printf("fd = %d\n",fd);
    }
    return 0;
}

 


为什么会是3呢?3又是个什么东西?

我们可以先提前告诉你,这个3是一个文件标识符。open函数成功后,会返回一个文件标识符! 

四、文件标识符

聊到这里,我相信大家已积累了一肚子的疑问了。

别着急,我们一一把这些疑问为大家串联起来解答:

这个文件标识符,是我们访问文件的唯一方式。

我们之前说过,C语言访问文件的接口,都是封装的系统调用,而系统调用,归根结底就是通过文件标识符来找到对应的文件的。如图数组的下标一样(实际上本身就是下标),有了下标,你就能找到在内存中被管理的文件。

 在内存中,进程是由task_struct进行管理的,我们之前说过文件也会被操作系统用一个内核数据结构管理起来,类似的,我们把这个数据结构叫做:struct file。

struct file {
    struct path          f_path;      // 文件的路径(包含 dentry 和 vfsmount)
    struct inode        *f_inode;     // 文件的 inode
    const struct file_operations *f_op;  // 文件操作函数(read/write 等)
    atomic_long_t        f_count;     // 引用计数
    loff_t               f_pos;       // 当前读写位置
    // ...
};

在struct file结构体中包含的struct file的指针,使被加载到内存的多个文件通过指针的方式链接形成链表。我们对文件的管理,就转变为了对链表的增删查改!

而我们说的那三个输入输出流,在linux下也是一种文件,所以无独有偶,也应该被这样的方式进行管理。

我们说文件都是被进程所访问的,进程如何访问呢?就是去寻找task_struct与struct file的联系。

而很不巧,task_struct中就存在这样一个结构体指针,struct files_struct*files指针。

struct task_struct {
    // ...
    struct files_struct *files;  // 指向进程的文件管理结构
    // ...
};

这个指针所指向的结构体中又包含了一个指针数组:struct file* fd_array。

struct files_struct {
    // ...
    struct file __rcu * fd_array[NR_OPEN_DEFAULT];  // 文件描述符数组(默认大小是 64)
    // ...
};

没错,我们上面所说的3,就是这个指针数组的下标元素,那为什么是3呢?

因为我们说过,进程会默认打开三个输入输出流!!

而他们正好占据了数组的前三个位置。

所以,0,1,2的位置早早就被占领了。当我们的代码执行到open的时候,他们先从这个数组里搜索空位,搜到3是空的了,于是把文件加载到内存中形成内核数据结构并连入链表(实际上可能不是链表,会是更加优秀的数据结构比如红黑树之类的)。随后把文件的地址放到3号位置处!


五、C语言中的FILE*

我们看一下C语言中的输入输出流吧:

 #include <stdio.h>

       extern FILE *stdin;
       extern FILE *stdout;
       extern FILE *stderr;

为什么这里是个FILE*类型呢?

答案还是封装,C语言不仅对接口进行封装,还把他们的类型做了封装。

我们可以通过打印来验证一下,首先,FILE*是个结构体指针,他的参数含有一个fileno表示文件标识符,我们打印一下试试:
 

由此可以验证我们上述内容。 


结语

本篇文章就到此结束,由于是文件系统的第一篇,所以篇幅有限,且大部分都是概念,需要同学们好生理解与掌握,尤其是文件被打开的过程。

如果有所疑问或者指正,欢迎评论区留言!!