【Linux】文件描述符

前言:

本章我们将进入Linux文件相关的学习与操作,重点知识如下:

  • 复习C语言文件IO相关操作
  • 认识文件相关系统调用接口
  • 了解虚拟文件系统,内核管理文件的数据结构

目录

    • [1. 文件的预备知识](#1. 文件的预备知识)
    • [2. 复习C语言的文件IO操作](#2. 复习C语言的文件IO操作)
      • [2.1 打开文件(fopen 函数)](#2.1 打开文件(fopen 函数))
      • [2.2 关闭文件(fclose 函数)](#2.2 关闭文件(fclose 函数))
      • [2.3 理论理解:](#2.3 理论理解:)
    • [3. Linux系统文件接口](#3. Linux系统文件接口)
      • [3.1 open函数接口:](#3.1 open函数接口:)
      • [3.2 write函数接口:](#3.2 write函数接口:)
      • [3.3 read函数接口:](#3.3 read函数接口:)
      • [3.4 close函数接口:](#3.4 close函数接口:)
  • [4 文件描述符fd:](#4 文件描述符fd:)
    • [4.1 0 & 1 & 2描述符 :](#4.1 0 & 1 & 2描述符 :)
    • [4.2 内核数据结构详解 :](#4.2 内核数据结构详解 :)
    • [4.3 如何理解Linux下一切皆文件 :](#4.3 如何理解Linux下一切皆文件 :)
  • [5 缓冲区的概念:](#5 缓冲区的概念:)
    • [5.1 什么是缓冲区:](#5.1 什么是缓冲区:)
    • [5.2 缓冲区的刷新策略:](#5.2 缓冲区的刷新策略:)

1. 文件的预备知识

  • 文件 = 文件内容 + 文件属性**(文件属性也是数据,也占用磁盘空间)**
  • 文件操作 = 文件内容的操作 + 文件属性的操作

所谓的"打开"文件,究竟是在干什么?

  • 将文件的属性或内容加载到内存中!(这是由冯·诺依曼体系决定的!CPU 只能在内存中,对文件进行读写操作
  • 打开文件不是目的,访问文件才是目的!(例如:将文件内容中小写字母改成大写字母,是先将文件内容读到内存里,再把buff里面所有的内容改成大写,再写回到文件中。)

程序被加载到内存中后, 磁盘中还有吗?

  • 是的程序被加载到内存中后,磁盘中仍然存在程序文件
  • 程序文件是存储在磁盘上的二进制文件,它包含了程序的代码、数据和资源等信息。

并不是所有的文件,都会处于被打开的状态

通常我们打开文件,访问文件,关闭文件,是谁在进行相关操作?

  • fopen,fclose, fread, fwrite
  • 当我们的进程运行起来的时候,才会执行对应的代码
  • 然后,才是真正的对文件进行相关的操作。
  • 真正的是进程对文件进行操作!

小结:
1. 对文件的操作:

  • 只有将程序编成可执行程序之后,加载到内存里的时候。
  • 变成了一个进程并且被CPU调度,开始执行自己跟文件操作相关的代码(如fclose 、fopen等),这个时候才开始进行文件操作。
  • 通常说对文件的操作这句话,应该准确的说是:程序对应的进程对文件的操作。。。

2. 访问一个文件就得先打开,打开就得在内存里
3. 程序要打开文件,必须先把程序变成进程。
4. 所有文件操作的本质都是在研究进程和打开文件的关系。

2. 复习C语言的文件IO操作

2.1 打开文件(fopen 函数)

  • 返回值是一个指向文件的**文件指针(FILE*)**

FILE结构包括一个缓冲区和一个文件描述符(打开文件系统自己返回给你的)

cpp 复制代码
1. "r":以只读方式打开文件。文件必须已经存在才能成功打开。
2. "w":以写入方式打开文件。如果文件不存在,则创建新文件;如果文件已存在,则清空文件内容。
3. "a":以追加方式打开文件。如果文件不存在,则创建新文件;如果文件已存在,则在文件末尾追加内容。
4. "rb":以二进制只读方式打开文件。类似于 "r",但以二进制模式读取文件。
5. "wb":以二进制写入方式打开文件。类似于 "w",但以二进制模式写入文件。
6. "ab":以二进制追加方式打开文件。类似于 "a",但以二进制模式追加内容。

代码演示:
通过fopen打开文件,r选项,再通过fgets按行读取文件。

cpp 复制代码
#include <stdio.h>
#include <unistd.h>

int main()
{
    //1. 默认这个文件会在哪里形成呢?
    //2. r, w, r+, w+, a, a+
    //(r+ 和 w+ 都叫做既读又写,只不过w+多了一个功能,就是文件不存在会自动创建)
    //3. 关注一下文件清空的问题
    
    FILE* fp = fopen("log.txt", "r"); 
    if(fp == NULL)
    {
        perror("fopen");
        return 1;
    }

    char buffer[64] = { 0 };
    while(fgets(buffer, sizeof(buffer), fp) != NULL)
    {
        printf("echo : %s\n", buffer);
    }

    fclose(fp);

    return 0;
}

2.2 关闭文件(fclose 函数)

cpp 复制代码
FILE* fp = fopen("log.txt", "a"); //写入
    if(fp == NULL)
    {
        perror("fopen");
        return 1;
    }
 fclose(fp);

2.3 理论理解:

  • 当我们向文件写入的时候,最终是不是向磁盘写入?是的!
  • 磁盘就是硬件,只有操作系统才有资格想硬件写入
  • 对硬件对应写入的时候,必须使用操作系统提供的相关系统调用。

为什么要对系统调用接口做封装?

1.原生系统接口,使用成本比较高。

2.语言不具有跨平台性。

所有的跨平台的语言,必须通过自己的方案,对所有的系统接口做相关封装,设计好自己对应语言当中的IO接口。

3. Linux系统文件接口

我们学习的Linux系统文件接口,更接近于操作系统。。。

open、 close、 read、 write四个系统调用接口。

3.1 open函数接口:

cpp 复制代码
#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。
参数:(通过宏来实现的)
O_RDONLY: 只读打开
O_WRONLY: 只写打开
O_RDWR : 读,写打开
这三个常量,必须指定一个且只能指定一个
O_CREAT : 若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限
O_APPEND: 追加写
% 返回值:
成功:新打开的文件描述符
失败:-1
  • mode_t理解:直接 man 手册,比什么都清楚
  • open 函数具体使用哪个,和具体应用场景相关,
  • 如目标文件不存在,需要open创建,则第三个参数表示创建文件
    的默认权限,否则,使用两个参数的open
    文件的权限主要包括rwx 等。。

3.2 write函数接口:

为什么我们write往文件里写的时候,写的长度为什么不带上最后的'\0'呢

这是刻意为之,因为文件是不认是C语言的'\0'。

在写入的时候,算长度要减去'\0'占据的那一位。。。

3.3 read函数接口:

cpp 复制代码
int main()
{
    char buffer[1024];
    ssize_t s = read(0, buffer, sizeof(buffer) - 1);
    if(s > 0)
    {
        buffer[s] = '\0';
        printf("echo: %s", buffer);

    }
    return 0;
}

3.4 close函数接口:

close接口,关闭文件

close是真的将文件销毁了吗?

  • 调用 close 函数后,文件仍然存在,并且可以通过重新打开或使用其他文件操作函数来再次访问
  • close 操作只是将文件与当前程序的连接断开,而不是销毁文件本身

4 文件描述符fd:

  • 通过对open函数返回值的观察,我们知道了文件描述符就是一个小整数
  • 文件描述符,其实是数组下标,是进程中保存的文件描述符指针的下标
  • 文件描述符:在linux系统中打开文件就会获得文件描述符 ,它是个很小的非负整数。每个进程在PCB(Process Control Block)中保存着一份 文件描述符表,文件描述符就是这个表的索引,每个表项都有一个指向已打开文件的指针。
  • 文件指针:C语言中使用文件指针做为I/O的句柄。文件指针指向进程用户区中的一个被称为FILE结构的数据结构。FILE结构包括一个缓冲区和一个文件描述符(打开文件系统自己返回给你的)。而文件描述符是 文件描述符表的一个索引,因此从某种意义上说文件指针就是句柄的句柄(在Windows系统上,文件描述符被称作文件句柄

4.1 0 & 1 & 2描述符 :

  • Linux, 进程默认情况下会有3个缺省打开的文件描述符,分别是标准输入0, 标准输出1, 标准错误2
  • 0,1,2对应的物理设备一般是:stdin,stdout,stderr
cpp 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
int main()
{
char buf[1024];
ssize_t s = read(0, buf, sizeof(buf));
if(s > 0){
buf[s] = 0;
write(1, buf, strlen(buf));
write(2, buf, strlen(buf));
}
return 0;
}

4.2 内核数据结构详解 :

一个进程可以打开多个文件。

在系统在运行中,有可能会存在大量的被打开的文件

操作系统要对这些被打开的文件进行管理,要先描述,再组织

描述的话,使用数据结构struct file{ }


  • 文件描述符的分配规则
    文件描述符的分配规则:在files_struct数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符
cpp 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
close(0);
//close(2);
int fd = open("myfile", O_RDONLY);
if(fd < 0){
perror("open");
return 1;
}
printf("fd: %d\n", fd);
close(fd);
return 0;
}
  • 重定向

本来应该输出到显示器上的内容,输出到了文件 myfile 当中,其中,fd=1。这种现象叫做输出 重定向。常见的重定向有:>, >>, <

那重定向的本质是什么呢?

可使用 dup2 系统调用,进行重定向修改

cpp 复制代码
#include <unistd.h>
int dup2(int oldfd, int newfd);
%% oldfd 想保留的依赖关系
%% newfd 想删除的依赖关系
%%% 最后要指向oldFd的文件描述符代表的那个文件。

dup函数返回新的文件描述符,如果复制成功,则返回的文件描述符与oldfd具有相同的值和属性。如果复制失败,则返回-1,并设置errno来指示错误的原因。

代码演示:

cpp 复制代码
#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;
}

4.3 如何理解Linux下一切皆文件 :

  • 1. 如何知道这些struct file对应的操作方法是不一样的?
  • Linux是C语言写的,虽然不支持结构体中封装函数,但是可以用函数指针
  • 通过函数指针的方式,让函数指针指向特定的方法。
  • 打开struct file的时候识别底层文件的类型拿到底层文件的读写方法,用自己的函数指针指向就可以了。
  • 上层来使用看的时候就 认为是一切皆文件.


补充理解:

** OS内的内存文件系统以统一的视角看待所有的设备!**

  • 如果要打开文件,就在内核给硬件创建struct file
  • 初始化的时候,将函数指针指向具体的设备
  • 但是在内核中存在的永远都是struct file,然后将struct file关联起来
  • 对应的设备,对应的读写方法一定是不一样的

** 有没有任何硬件的差别了?
答:没有了,看待所有的文件的方式,都统一成为了struct file**

什么叫做文件呢?

站在系统的角度,能够被input读取或者能够output写出的设备,就叫做文件!

狭义的文件:普通的磁盘文件

广义的文件:显示器、键盘、网卡、声卡、显卡及磁盘,几乎所有的外设,都可以称之为文件。。

上层使用同一个对象,指针指向不同的对象,最终就能调用不同的方法

5 缓冲区的概念:

缓冲区的作用:主要是为了提高用户的响应速度。

5.1 什么是缓冲区:

就是一段内存空间(这个空间谁提供的?用户char buffer[64] ,scanf(buffer)

缓冲区在哪里? 语言里面会设计,os 系统里面也会涉及

5.2 缓冲区的刷新策略:

  • 1.立即刷新

  • 2.行刷新(行刷新 \n)

  • 3.满刷新(全缓冲)

特殊情况时:

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

    尾声
    看到这里,相信大家对这个Linux 有了解了。
    如果你感觉这篇博客对你有帮助,不要忘了一键三连哦
相关推荐
lzb_kkk8 分钟前
【JavaEE】JUC的常见类
java·开发语言·java-ee
Zfox_9 分钟前
【Linux】进程信号全攻略(二)
linux·运维·c语言·c++
速盾cdn9 分钟前
速盾:vue的cdn是干嘛的?
服务器·前端·网络
安於宿命14 分钟前
【Linux】简易版shell
linux·运维·服务器
丶Darling.17 分钟前
MIT 6.S081 Lab1: Xv6 and Unix utilities翻译
服务器·unix·lab·mit 6.s081·英文翻译中文
黄小耶@25 分钟前
linux常见命令
linux·运维·服务器
叫我龙翔27 分钟前
【计网】实现reactor反应堆模型 --- 框架搭建
linux·运维·网络
古驿幽情29 分钟前
CentOS AppStream 8 手动更新 yum源
linux·运维·centos·yum
BillKu30 分钟前
Linux(CentOS)安装 Nginx
linux·运维·nginx·centos
爬山算法32 分钟前
Maven(28)如何使用Maven进行依赖解析?
java·maven