基础IO —— Linux

目录

文件描述符fd 

 write read close lseek ,类比C文件相关接口

  系统接口

C库函数

什么是当前路径

0 & 1 & 2

文件描述符的分配规则

重定向

那重定向的本质是什么呢

使用 dup2 系统调用

注意这里和平时习惯是反的,相当于是用前面的数据去覆盖后面的数据(duplicate 复制)

FILE

缓冲区

标准输出和标准错误的区别(补充)

文件系统 

什么是文件系统

关于磁盘

磁盘的存储结构

CHS地址

磁盘的逻辑结构 

inode

当我们创建一个文件,操作系统做了什么

那么删除一个文件?

软硬链接

软硬连接的创建

软硬连接分析


C语言总结在这常见八大排序在这

作者和朋友建立的社区:非科班转码社区-CSDN社区云💖💛💙

期待hxd的支持哈🎉 🎉 🎉

最后是打鸡血环节:想多了都是问题,做多了都是答案🚀 🚀 🚀

最近作者和好友建立了一个公众号

公众号介绍:

专注于自学编程领域。由USTC、WHU、SDU等高校学生、ACM竞赛选手、CSDN万粉博主、双非上岸BAT学长原创。分享业内资讯、硬核原创资源、职业规划等,和大家一起努力、成长。(二维码在文章底部哈!

文件描述符fd 

先来认识一下两个概念 : 系统调用 库函数

 write read close lseek ,类比C文件相关接口

  系统接口

  write read close lseek

C库函数

#include <stdio.h>
#include <string.h>
 
 
int main()
{
    FILE *fp = fopen("./bite", "wb+");
    if (fp == NULL) {
        perror("fopen error");
        return -1; 
    }   
    fseek(fp, 0, SEEK_SET);
    char *data = "linux so easy!\n";
    //size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
    size_t ret = fwrite(data, 1, strlen(data), fp);
    if (ret != strlen(data)) {
        perror("fwrite error");
        return -1; 
    }   
    //注意读的时候要fseek!!!因为之前写了之后,文件指向位置就已经变了
    fseek(fp, 0, SEEK_SET);//跳转读写位置到,从文件起始位置开始偏移0个字节
    char buf[1024] = {0};
    //size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
    ret = fread(buf, 1, 1023, fp);//因为设置读取块大小位1,块个数为1023因此fread返回值为实际读取到的数据长度
    if (ret == 0) {
        if (ferror(fp)) //判断上一次IO操作是否正确
            printf("fread error\n");
        if (feof(fp)) //判断是否读取到了文件末尾
            printf("read end of file!\n");
        return -1; 
    }   
    printf("%s", buf);
    fclose(fp);
    return 0;
}
通过使用open,我们知道了文件描述符就是一个小整数,用其来操作文件(传参)
fopen fclose fread fwrite 都是 C 标准库当中的函数,我们称之为库函数( libc )。
而, open close read write lseek 都属于系统提供的接口,称之为系统调用接口
回忆一下我们讲操作系统概念时,画的一张图
系统调用接口和库函数的关系,一目了然。
所以,可以认为, f# 系列的函数,都是对系统调用的封装,方便二次开发。
文件 = 文件内容 + 文件属性
文件操作 = 文件内容的操作 + 文件属性的操作
我们平时说的打开文件:就是将文件的属性或内容加载到内存中 -- 冯诺依曼体系结构决定的
通常我们说的打开文件,访问文件是我们写的代码运行起来也就是说是进程去进行操作的
我们常常听说当前路径

什么是当前路径

就是当前进程所处的工作路径
查看进程当前路径
:ls /proc 进程pid
看见一个 cwd 会软连接一个路径那个就是当前路径啦
cwd(Current Working Directory)
PS:Linux下的 FILE 里面的 fileno 就是文件描述符

0 & 1 & 2

我们知道了文件描述符就是一个小整数
Linux进程默认情况下会有3个缺省打开的文件描述符
分别是标准输入0, 标准输出1, 标准错误2.
0,1,2对应的物理设备一般是:键盘,显示器,显示器
所以输入输出还可以采用如下方式:
现在知道,文件描述符就是从 0 开始的小整数。当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件。于是就有了file 结构体。表示一个已经打开的文件对象。而进程执行 open 系统调用,所以必须让进程和文件关联起来。每个进程都有一个指针*files, 指向一张表 files_struct, 该表最重要的部分就是包涵一个指针数组,每个元素都是一个指向打开文件的指针!所以,本质上,文件描述符就是该数组的下标。所以,只要拿着文件描述符,就可以找到对应的文件(重点)

文件描述符的分配规则

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
 int fd = open("myfile", O_RDONLY);
 if(fd < 0){
 perror("open");
 return 1;
 }
 printf("fd: %d\n", fd);
 close(fd);
 return 0;
}
输出发现是 fd: 3
关闭 0 或者 2 ,在看
#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;
}
发现是结果是: fd: 0 或者 fd 2 可见,文件描述符的分配规则:在 fifiles_struct 数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符

重定向

那如果关闭 1 呢?看代码:
#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);
}
此时,我们发现,本来应该输出到显示器上的内容,输出到了文件 myfile 当中,其中, fd 1 。这种现象叫做输出重定向。常见的重定向有:>, >>, <

那重定向的本质是什么呢

其实就是把原来数组中所指向的内容改变了。但是他含义还是那个含义,比如stdout原来是在1的位置,现在用重定向把文件标识符3位置的数据给了1(就是覆盖),但是下次使用stdout的时候,系统还是会去1的位置去使用其数据,外部是并不知道里面进行了重定向的。

使用 dup2 系统调用

注意这里和平时习惯是反的,相当于是用前面的数据去覆盖后面的数据(duplicate 复制)

FILE

因为IO相关函数与系统调用接口对应,并且库函数封装系统调用,所以本质上,访问文件都是通过fd访问的。
所以C库当中的FILE结构体内部,必定封装了fd

缓冲区

来段代码在研究一下:
但如果对进程实现输出重定向呢? ./hello > file , 我们发现结果变成了:
我们发现 printf fwrite (库函数)都输出了 2 次,而 write 只输出了一次(系统调用)。为什么呢?肯定和fork有关!
一般 C 库函数写入文件时是全缓冲的,而写入显示器是行缓冲。
printf fwrite 库函数会自带缓冲区(进度条例子就可以说明),当发生重定向到普通文件时,数据的缓冲方式由行缓冲变成了全缓冲。
而我们放在缓冲区中的数据,就不会被立即刷新,甚至 fork 之后
但是进程退出之后,会统一刷新写入文件当中。
但是 fork 的时候,父子数据会发生写时拷贝,所以当你父进程准备刷新的时候,子进程也就有了同样的一份数据,随即产生两份数据。
write 没有变化,说明没有所谓的缓冲。
综上: printf fwrite 库函数会自带缓冲区,而 write 系统调用没有带缓冲区。另外,我们这里所说的缓冲区, 都是用户级缓冲区。其实为了提升整机性能,OS 也会提供相关内核级缓冲区,不过不再我们讨论范围之内。
那这个缓冲区谁提供呢? printf fwrite 是库函数, write 是系统调用,库函数在系统调用的 上层 , 是对系统调用的“ 封装 ,但是 write 没有缓冲区,而 printf fwrite 有,足以说明,该缓冲区是二次加上的,又因为是C,所以由 C 标准库提供(这里的缓冲区是属于语言级别的缓冲区)。
输出重定向的时候有5条数据(行缓冲变成了全缓冲所以就算加了\n还是不能直接输出)
可以看看FILE结构体:
typedef struct _IO_FILE FILE ; /usr/include/stdio.h

标准输出和标准错误的区别(补充)

 

虽然都是向显示器打印,但是自己的文件描述符是不一样的,重定向只是对 1 重定向了

如何分开 1 2

这么做的意义就是哪些是日常输出,哪些是错误
如何打到一起 

文件系统 

什么是文件系统

上面的所以内容,都是内存中的东西,但是我们知道,大量的文件是在磁盘中的,也就是说我们需要去管理他们!而这就是文件系统(要做的工作)!

关于磁盘

磁盘的存储结构

 每片磁盘有两面,每面有很多磁道,每个磁道被分为n段,每小段称作扇区。

 所有盘的里圆心同距离的磁道组成磁柱。

虽然每个扇区是512byte,但是并不是意味着操作系统去访问的时候也是按1512byte去访问的,文件系统访问磁盘的基本单位是4KB

意义:

1. 提高IO效率(因为一下512byte太小了会频繁访问)

2. 不要让软件(OS)设计和硬件(磁盘)具有强相关性,换句话说,就是解耦合

CHS地址

想找到一个存储单元,我们只要找到他在磁盘上的那个盘面,那个磁道,那个扇区我们就可以找到他!用同样的方法,我们可以找到所有的基本单元!

磁盘的逻辑结构 

还是先描述后组织。虽然磁盘是一盘一盘的进行存储,但是我们可以想象成像数组一样的线性存储结构,也就是LBA(逻辑块地址),这样对磁盘的管理就变成了对数组的管理。然后我们使用的是LBA地址去访问磁盘也就是把LBA地址转换为CHS(Clinder head Sector)地址。

inode

ls -i 可以看文件的 inode

磁盘那么大,我们就要进行管理。

为了能解释清楚 inode 我们先简单了解一下文件系统

上图为磁盘文件系统图(内核内存映像肯定有所不同),磁盘是典型的块设备,硬盘分区被
划分为一个个的 block 。一个 block 的大小是由格式化的时候确定的,并且不可以更改。例如 mke2fs -b 选项可以设定block 大小为 1024 2048 4096 字节。而上图中启动块( Boot Block )的大小是确定的
Block Group ext2 文件系统会根据分区的大小划分为数个 Block Group 。而每个 Block Group 都有着相同的结构组成。政府管理各区的例子
超级块 Super Block ):存放文件系统本身的结构信息。记录的信息主要有: bolck inode 的总量,未使用的block inode 的数量,一个 block inode 的大小,最近一次挂载的时间,最近一次写入数据的时间,最近一次检验磁盘的时间等其他文件系统的相关信息。Super Block 的信息被破坏,可以说整个文件系统结构就被破坏了
GDT Group Descriptor Table :块组描述符,描述块组属性信息
块位图 Block Bitmap ): Block Bitmap 中记录着 Data Block 中哪个数据块已经被占用,哪个数据块没有被占用
inode 位图 inode Bitmap ):每个 bit 表示一个 inode 是否空闲可用。
i节点表 : 存放文件属性 如 文件大小,所有者,最近修改时间等
数据区:存放文件内容(占大部分空间)

SB(超级块):表示整个分区有多少个块,一个块组的inode table 和block 的使用情况,整个分区从磁盘的几号到几号分区是我的等等一些数据,宏观上管理的是整个分区
而GDP是管理分区中的一个组,剩余的段就是管理一个组的文件属性和文件内容
对应SB为什么不单独放出来,而是放在了Block group里面,其实并不是每个Block group里面都有,而是比如10个里面有2-3个有而且完全一样,主要是为了备份,就是怕损坏之后,整个分区全部没用了!(重要)

那一个inode文件如何把自己的文件属性和文件内容关联起来呢?(在这里我们看到是分开存的)

首先 inode table 里面会有一个 inode 来标识 inode(每个 struct inode)的唯一性。然后是struct inode 里面会存有 一个blocks的数组,比如 blocks【15】,比如前12位置直接保存的就是该文件对应的blocks(Date blocks)编号,后面三个位置指向一个date blocks,但是这个 date blocks 不保存有效数据,而保存该文件所适用的其他块的编号。

文件名并不保存在 struct inode 里面

首先文件名是属性吗?答案是是。但是inode里面并不保存文件名,Linux下底层实际都是通过inode编号标识文件的!

那么我们怎么找到文件呢?找文件就要找到inode编号,那么inode编号谁告诉我呢?

其实是目录!目录也是文件,目录文件的内容就是文件名和inode的映射关系!,所以Linux同一个目录下,不可以创建多个同名文件,因为文件名本身就是具有Key值的东西。所以!创建一个文件的时候,一定是在一个目录下!

当我们创建一个文件,操作系统做了什么

首先创建的文件有自己的文件名和inode编号,然后去找到自己所处的目录,根据目录的inode,找到目录的dateblock,然后将文件名和inode编号的映射关系写入到目录的数据块中!

那么删除一个文件?

根据文件名在目录的文件内容里面找到删除文件的inode,然后根据删除文件的inode去找到自己的Blockbitmap和inodebitmap然后置为0就可以了!

上面是理论(重要),下面是实例:

属性数据分开存放的想法看起来很简单,但实际上是如何工作的呢?我们通过 touch 一个新文件来看看如何工作。
当我们touch一个文件时

创建一个新文件主要有一下4个操作:

1. 存储属性
内核先找到一个空闲的 i 节点(这里是 263466 )。内核把文件信息记录到其中。
2. 存储数据
该文件需要存储在三个磁盘块,内核找到了三个空闲块: 300,500 800 。将内核缓冲区的第一块数据复制到300 ,下一块复制到 500 ,以此类推。
3. 记录分配情况
文件内容按顺序 300,500,800 存放。内核在 inode 上的磁盘分布区记录了上述块列表。
4. 添加文件名到目录
新的文件名 abc linux 如何在当前的目录中记录这个文件?内核将入口( 263466 abc )添加到目录文件。文件名和inode 之间的对应关系将文件名和文件的内容及属性连接起来。

软硬链接

软硬连接的创建

带 -s 的是软连接

软硬连接分析

软链接就是一个独立的文件(Linux下的快捷方式),有自己独立的inode和inode编号(既然是独立的文件,那就有其内容,里面存的就是指向文件的所在路径!)。

硬链接不是一个独立文件(单纯在Linux的指定目录下,给文件新增文件名和inode的映射关系),他和目标文件使用的是同一个inode。

最后的最后,创作不易,希望读者三连支持💖

赠人玫瑰,手有余香💖

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

原来45

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值