【Linux】基础IO详解

今天我们来讲解一下 基础IO.

1. C文件接口回顾

在我们进入基础IO的学习之前,我们先来回顾一下我们之前在C语言中学习的一些接口:

如果想要对C语言文件操作相关接口有比较全面的复习的话,可以看下面的博客:
【C语言】文件操作

1.1 写文件与读文件

这里我们只挑选两个来回顾一下:

  1. fwrite函数
//函数声明:
size_t fwrite(const void* ptr,size_t size,size_t count,FILE* stream);

在这里插入图片描述
同时我们在打卡文件的时候,要给文件附加属性:

  • w :写入,每次写入之后会发生覆盖
  • a:append,本质也是写入,但是不清空原始内容,在文件的最后增添内容

另外,默认情况下,文件的路径是与当前进程的路径相同。


1.2 stdin & stdout & stderr

任何C程序,都会默认打开三个“文件”,分别叫:

  1. 标准输入(stdin)
  2. 标准输出 (stdout)
  3. 标准错误 (stderr)

他们的类型都是 FILE*.

他们分别对应着硬件中的 键盘文件,显示器文件,标准错误。
在这里插入图片描述

扫描二维码关注公众号,回复: 14219533 查看本文章

显然,所有的外设硬件,本质对应的核心操作不外乎是read 和write。

我们可以通过曾经的C接口,直接对stdin,stdout,stderr 进行读写!

我们可以使用 stdout 直接将写入的数据打印出来。

#include <stdio.h>
#include <string.h>
int main()
{
    
    
const char *msg = "hello fwrite\n";
fwrite(msg, strlen(msg), 1, stdout);
printf("hello printf\n");
fprintf(stdout, "hello fprintf\n");
return 0;
}

1.3 OS对文件的的管理

虽然在Linux中一切皆文件,且文件的核心就是read与write,但是不同的硬件,抑或是普通的文件,对应的读写方式都是不同的。

那么操作系统将如何进行管理呢?

还记得OS对进程的管理吗?OS对文件的管理也是相似的。
【Linux】进程入门详解

我们使用 结构体 struct file 对文件进行描述,当然,除了描述其属性,还有该文件的操作接口(指针函数实现)。
在这里插入图片描述


也就是说,每个结构体都可以通过指针找到对应的硬件(或者普通文件)中的读写方法
如果有许多个文件,OS又是如何组织的呢?将各个结构体通过指针连接,组成“双向链表”,OS只需要保存其头指针,就可以对所有结构体进行增删查改。
在这里插入图片描述


2. 系统文件IO

2.1 接口介绍

2.1.1 库函数与系统接口的关系

库函数的底层实现都是系统系统接口。根据平台的不同,选择自己底层对应的文件接口

C库函数 系统接口
fopen open
fclose close
fread read
fwrite write

在这里插入图片描述

2.1.2 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。
//-------------------------------------------------------
参数:
O_RDONLY: 只读打开
O_WRONLY: 只写打开
O_RDWR : 读,写打开
这三个常量,必须指定一个且只能指定一个
O_CREAT : 若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限
O_APPEND: 追加写
//----------------------------------------------------------------
返回值:
成功:新打开的文件描述符
失败:-1

在这里插入图片描述

  • 选项标志位

我们发现参数:O_RDONLY,O_WRONLY,O_RDWR …,这些参数实际上选项标志位的一种。
在系统调用中,为了节省空间,通常一个比特位 代表一种选择。
也就是说,系统喜欢用宏定义出来的比特位不重叠的二进制序列,每一个比特位,表示一种标志。

#define O_RDONLY 0x1
#define O_WRONLY 0x2
#define O_APPEND 0x4

这些宏之间使用 “|”(按位或)连接,在系统中再使用按位与判断我们使用了哪些选项。


2.1.3 write

write 写文件
在这里插入图片描述

2.1.4 read

read 读文件

#include<unistd>

ssize_t read(int fd,void *buf,size_t count);

在这里插入图片描述

2.1.5 为什么所有语言都要自己封装?

  1. 兼容自身的语法特征,系统调用使用成本较高,且不具有可移植性
  2. 封装过的接口,可以自动根据平台的不同,选择自己底层对应的文件接口

2.2 理解fd

2.2.1 什么是fd

通过对open函数的学习,我们知道了文件描述符就是一个小整数.

那么这些整数有着怎样的含义与作用呢?

我们来纳许调用几个open函数并查看其返回值:
在这里插入图片描述
我们发现,是一串连续的数字。
这不经让我们想到了一种常见的数据结构:数组。
实际上,fd 也确实与数组有关,用户所看到的fd,本质上是系统中维护进程和文件对应关系的数组的下标。

但是为什么“下标”是从3开始的呢?这是由于我们之前已经进介绍过,任何C程序,都会默认打开三个文件:标准输入(stdin), 标准输出(stdout), 标准错误(stderr)。他们的文件描述符值分别占有了0 ,1, 2。


2.2.2 进程与文件

那么这个数组到底是什么呢?

所有的文件,要想被使用,都要先打开。同时,一个进程是可以打开多个文件的。那么,这些被打开的文件是如何被OS管理起来的?

先描述 ,再组织

我们之前已经学习过了 ,我们使用struct_file来描述 文件。那么进程是如何打开文件的呢?

在这里插入图片描述

观察上图,一个进程,本质是一个PCB,也就是task_struct 。其中存在一个struct file_struct* 类型的指针,它指向哪? 指向一个struct file_struct 结构体,其作用就是构建进程与文件的对应关系,简单来说,就是帮助进程找到想找的文件。该结构体中存在一个 指针数组 struct file fd_array*,其指向struct file(描述文件的结构体)。

于是,我们打开一个文件的过程就是: 先为该文件创建一个file_struct 结构体,再将其地址填入file_struct结构体的fd_array指针数组中。同时填入的原则是 找寻一个空闲的且最小小标的位置填入。最后,我们再将数组下标返回给进程。

当用户得到了fd之后,如果之后我们使用read,write.close的时候,都要传入fd。这样进程通过指针
找到struct file_struct 结构体,再找到其中的文件指针数组 fd_array,通过fd找到对应的struct file结构体,再通过结构体中的函数指针找到方法列表,找到对应的read,write,close方法。

这里还要补充一点,将文件的数据写入磁盘时,或者从磁盘写入文件之中,都会经过内核级缓冲区,OS会在适当的时机将数据刷入磁盘(文件)中。
在这里插入图片描述


我们这样讲可能还会有些抽象,我们直接找到原码来看一下这些结构体之间的关系:
在这里插入图片描述
由此,我们可以清晰的认识到各个部分之间的关系了。


所以,所谓的默认打开文件,标准输入,标准输出,标准错误,其实都是有底层系统支持的。默认一个进程在运行的时候。就打开了0,1,2。
在这里插入图片描述


那么加入我们主动把这几个默认打开文件关闭会发生什么呢?

  1. 关闭stdin
    在这里插入图片描述

  2. 关闭stderr
    在这里插入图片描述

  3. 关闭stdout

由于我们关闭stdout 显示器文件,又重新打开了一个普通文件,此时这个普通文件的地址就会放在原来1号位,按道理来说,原本要被打印在显示器上的内容,被写入了普通文件中。
在这里插入图片描述

**但是,当我们打开log.txt的时候,却发现log.txt中没有"hello bit"。**这该如何解释?
在这里插入图片描述

2.2.3 FILE

在解释上面的问题之前,我们在使用C接口 fopen 的时候,我们也有一个返回值 fp ,且类型是FILE*。(如下图)

那么疑问来了,这个FILE是什么? 它是否与 fd有关? 进一步拓展,C语言接口与系统接口是如何耦合的?

在这里插入图片描述


我们可以在原码中找到FILE,发现它是一个结构体,那么这个结构体里有什么?

如此是FILE的代码:

typedef struct _IO_FILE FILE;/usr/include/stdio.h
/usr/include/libio.h
struct _IO_FILE {
    
    
int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags
//缓冲区相关
/* The following pointers correspond to the C++ streambuf protocol. */
/* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
char* _IO_read_ptr; /* Current read pointer */
char* _IO_read_end; /* End of get area. */
char* _IO_read_base; /* Start of putback+get area. */
char* _IO_write_base; /* Start of put area. */
char* _IO_write_ptr; /* Current put pointer. */
char* _IO_write_end; /* End of put area. */
char* _IO_buf_base; /* Start of reserve area. */
char* _IO_buf_end; /* End of reserve area. */
/* The following fields are used to support backing up and undo. */
char *_IO_save_base; /* Pointer to start of non-current get area. */
char *_IO_backup_base; /* Pointer to first valid character of backup area */
char *_IO_save_end; /* Pointer to end of non-current get area. */
struct _IO_marker *_markers;
struct _IO_FILE *_chain;
int _fileno; //封装的文件描述符
#if 0
int _blksize;
#else
int _flags2;
#endif
_IO_off_t _old_offset; /* This used to be _offset but it's too small. */
#define __HAVE_COLUMN /* temporary */
/* 1+column number of pbase(); 0 is unknown. */
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];
/* char* _save_gptr; char* _save_egptr; */
_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};

只要稍加阅读,我们就会发现一个熟悉的成员变量fileno:

int _fileno; //封装的文件描述符

fileno 其实就是文件描述符 fd,只是叫法不同而已,所以,这也验证了:因为IO相关函数与系统调用接口对应,并且库函数封装系统调用,所以本质上,访问文件都是通过fd访问的

同时,观察代码,还有一大堆字符指针,这是C语言提供的缓冲区。

所以我们所说的缓冲区 不只有一个,为了提升整机性能,OS也会提供相关内核级缓冲区,不过不在我们讨论范围之内

对于上述内容,我们只需要记住:
struct FILE 内部包含:

  1. 底层对应的文件描述符下标(fileno)
  2. 应用层,C语言提供的缓冲区数据

由此我们可以回答上面的问题:“当我们打开log.txt的时候,却发现log.txt中没有"hello bit“
在这里插入图片描述


在这里插入图片描述

我们在使用 C语言的printf接口时,不是直接向OS内核中返回fd,而是先停留在语言层,将要打印内容 先写入 C语言的FILE结构 的缓冲区, 在遇见‘\n’,或者fflush(stdout)刷新 之后,才会通过fd进入内核层,让进程通过file_struct 找到 文件 ,再选择合适的操作将数据写入内核缓冲区,最后在合适的时机刷到磁盘里。

上述程序的问题就处在C语言缓冲区那里,在我们刷新之前,我们就关闭了fd,所以进程也就收不到fd ,也就没有接下来的步骤了。

这里要补充一下,虽然我们的打印内容给出了’\n’,但是这并不会引发刷新,这是由于 我们之前已经关闭了1,现在的1位置指向的是 普通文件。 普通文件是全缓冲策略,与显示器的行缓冲是不同的。

在这里插入图片描述

2.2.4 全缓冲 与 行缓冲

在之前的问题中我们引入了全缓冲 与行缓冲 这两个概念,对二者的区别有了一个初步的印象,这里我们再巩固与总结一下:

在缓冲区内:

  1. 对于显示器(文件)来说,是行缓冲,遇到‘\n’或者程序结束或fllush(stdout)才刷新。
  2. 对于普通文件来说,是全缓冲,只有程序结束或者flush(stdout)才刷新。

我们再举一个例子:

我们调用三个C语言接口,再调用一个系统接口。最后再fork()一次。查看运行结果:
在这里插入图片描述
运行结果:
在这里插入图片描述

此时我们再将./test 重定向到 一个普通文件log.txt之中(./test > log.txt),我们再次运行并查看log.txt的内容:
在这里插入图片描述


对于上面的两种情况,我们该如何做出解释呢?

  1. 为什么重定向之后,三个C接口写入了两遍 ,但是重定向之前,三个C接口只打印了一遍?
  • 重定向之前,向显示器写入,刷新策略是行缓冲。首先,三个接口的内容先存入C语言缓冲区,由于行刷新,在我们fork()之前,三个语句(都加了‘\n’)已经从缓冲区刷新到了显示器。所以,之后fork()的时候子进程进行写时拷贝的时候缓冲区为空,那么最终只打印一遍也就十分合理了。
  • 重定向到log.txt之后,我们不再向显示器写入,而是向普通文件写入,所以缓冲区的刷新策略也变化为全缓冲。与之前一样,三个接口的内容先存入C语言缓冲区,由于全刷新,所以不刷新,之后我们fork,发生写实拷贝,之后程序退出,在这之前会清空缓存区,父子进程的缓冲区内容都被刷新到log.txt中,从而最终有两份数据。
  1. 为什么两种状态下,系统接口write 都是一次?

write 作为系统接口,直接跳过了语言层,只在内核区进行操作,所以不受影响。

写实拷贝主要考虑用户数据,即语言上的缓冲区数据。



2.3 重定向

2.3.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。这种现象叫做输出重定向。常见的重定向有:>, >>, < .


2.3.2 使用 dup2 系统调用

函数声明:

int dup2(int oldfd , int newfd);

使用测试:
在这里插入图片描述


2.4 理解文件系统

2.4.1 打开文件 与 未打开文件

文件有两种状态,一种是打开状态(只能由进程打开),一种是未打开状态。在上面的小节中,我们介绍的都是基于打开文件的操作与原理。这里我们来介绍一下未打开的文件。

2.4.2 磁盘

磁盘是计算机主要的存储介质,可以存储大量的二进制数据,并且断电后也能保持数据不丢失。早期计算机使用的磁盘是软磁盘(Floppy Disk,简称软盘),如今常用的磁盘是硬磁盘(Hard disk,简称硬盘)。
在这里插入图片描述

虽然磁盘是在现实中是一个圆盘,但实际上它的存储模式就是一个大数组。我们将这个数组划分区域,分为C,D,E,F区等。在这基础上,我们还要进行对硬盘分区进行划分 ,硬盘分区被划分为一个个的block。一个block的大小是由格式化的时候确定的,并且不可以更改。
在这里插入图片描述

我们着重看一下block group 中的分区;

  • Super Block

  • Group Descriptor Table
    块组描述符,描述块组属性信息

  • Block Bitmap
    Block Bitmap中记录着Data Block中哪个数据块已经被占用,哪个数据块没有被占用

  • inode Bitmap
    每个bit表示一个inode是否空闲可用

  • Data blocks(数据块)
    存放文件内容


2.4.3 inode

2.3.3.1 文件inode

  1. 基本上,一个文件,一个inode(包括目录)。
  2. inode是一个文件的所有属性的集合(也是数据,也要占空间),这里的属性不包括文件名
  3. 真正标识文件的不是文件名,而是文件的inode编号
  4. inode是可以与特定的数据块产生关联

在这里插入图片描述

2.3.3.2 目录inode

  • 目录也是文件,具有独立的inode,也有自己的数据块,其中保存的是文件名与inode的映射关系

查看当前目录下的文件inode:
在这里插入图片描述


2.3.3.3 bitmap 位图

我们通过位图来确认当前磁盘的使用情况

bit位是0代表当前位置为空,bit位是1代表当前位置已使用。
在这里插入图片描述

  • 请问,我们touch 一个空文件 test.c,文件系统做了什么?
  1. 在inode bitmap 中申请未被使用的空间,将空文件对应的属性信息写入inode中
  2. 把文件的inode 与文件名 建立映射
  • 如果我们想向test.c中写东西呢,文件系统做什么?
  1. 通过当前目录找到文件名,通过映射找到inode
  2. 找到inode, 向其指向的数据块内写入,位置不够,申请新的数据块。

2.3.3.4 删除文件

  1. 将位图中对应的位置清零
  2. 将文件名与inode 的映射关系删除

所以,删除数据是很轻量化的。

Linux之下,属性与内容是分离的,属性由inode保存,内容由data block保存。


2.4.4 软链接

软链接 就是一个普通的正常文件,有自己独立的inode编号,数据块内保存着 其指向文件的路径:
在这里插入图片描述
简单来说,类似于windows下的快捷方式,我们不用找到源程序来启动,直接点击快捷方式就行。这也是软链接的作用之一。


2.4.5 硬链接

硬链接没有自己独立的inode,且与指向的文件 属性完全相同,相当于别名。

只是创建了文件名和inode的映射关系
在这里插入图片描述
我们观察那一列黄色框里的数字,这代表着硬链接数,当我们删除file.c,hard_link的硬链接数也减1.
在这里插入图片描述
那么硬链接的作用是啥?

我们发现发现一个新创建的目录默认就有两个,这是为什么?
在这里插入图片描述
进入当前目录,我们发现"."的inode编号与dir是相同的,所以dir的硬链接数有两个。
在这里插入图片描述
这个”.“我们之前也讲过,是当前路径的索引,这也为我们使用相对路径和其他路径相关操作提供遍历。同理 ,‘..’也是。

所以,硬链接的作用之一是方便建立相对路径的建立


2.5 文件的三个时间- AMC

在这里插入图片描述

2.6 动静态库

为什么我们需要使用别人的代码?为了开发效率和鲁棒性。

2.6.1 动态链接 与 静态链接

我们写一个程序,调用库函数printf:
在这里插入图片描述

在这里插入图片描述

在Linux中,默认情况下,形成的可执行程序,是动态链接的。

而将库中的我的可执行程序中使用的二进制代码,拷贝进我的可执行程序中,这叫做静态链接。

为了更好的支持开发,第三方或者语言库,都必须提供两个库,一个叫做静态库,一个叫做动态库,方便程序员更具需要来进行bin 的生成。


动态链接的特点:体积小,节省资源(磁盘,内存),一旦库丢失,bin不可以执行
静态链接的特点:体积大,浪费资源(磁盘,内存),一旦库丢失,bin不可以执行

2.6.2 动态库 与静态库 的打包

2.6.2.1 静态库的生成与使用

基本步骤如下:

[root@localhost linux]# ls
add.c add.h main.c sub.c sub.h

[root@localhost linux]# gcc -c add.c -o add.o
[root@localhost linux]# gcc -c sub.c -o sub.o

生成静态库
[root@localhost linux]# ar -rc libmymath.a add.o sub.o
ar是gnu归档工具,rc表示(replace and create)

查看静态库中的目录列表
[root@localhost linux]# ar -tv libmymath.a
rw-r--r-- 0/0 1240 Sep 15 16:53 2017 add.o
rw-r--r-- 0/0 1240 Sep 15 16:53 2017 sub.o
t:列出静态库中的文件
v:verbose 详细信息

[root@localhost linux]# gcc main.c -L. -lmymath
-L 指定库路径
-l 指定库名

测试目标文件生成后,静态库删掉,程序照样可以运行。

假设我们想要写一个 包含加法与减法方法的静态库:

动态库生成
  1. 将头文件 与源文件 都写好,放在 lib (自定义即可)目录之下。
    在这里插入图片描述

在这里插入图片描述
3. 生成静态库 libmymath.a
在这里插入图片描述
4. 新建目录lib,将库文件libmymath.a 和 头文件Add.h,Sub.h 拷贝进入 lib 目录。所谓的静态库就是这二者的集合。

在这里插入图片描述
也就是当前目录下,有这些文件:
在这里插入图片描述

动态库使用
  1. 如果有人想使用我们编写的方法,只需要将lib这个目录给他:
    在这里插入图片描述
  2. 编写一个main 方法,写头文件,调用库方法:

在这里插入图片描述.

  1. 编译main.c ,生成可执行文件:

在这里插入图片描述
这里我们发现我们编译时候给gcc 添加了一些附加命令,这里我们解释一下:

  • -I :告诉gcc 除了默认路径以及当前路径,在指定路径也要找一下头文件
    这里的默认路径指【ls/usr/include/】,该路径存储了大量C语言提供的头文件,而./lib 就是我们的指定待搜索路径。
  • -L :告诉gcc 除了默认路径以及当前路径之外,在指定路径也找一下库文件
    这里的默认路径指【ls/ lib64/ libc】,该路径存储了大量C语言的静态库文件
  • -l (L的小写)库名称:具体你要链接哪一个库

  • 但是为什么C语言在编译的时候,从来没有明显调用-L,-I,-l 等选项?
  1. 库函数和头文件,在默认路劲下gcc能够找到
  2. gcc编译C代码,默认就应该链接libc
  • 如果我们也不想使用这些选项呢?
    将头文件与库文件分别念拷贝到默认路径下,这个过程就是 库的安装,但是,作为第三方库,一般也要带上 -lname。

  1. 运行
    在这里插入图片描述

2.6.2.1 动态库的生成与使用

生成动态库

动态库有几点不同:

  • shared: 表示生成共享库格式
  • fPIC:产生位置无关码(position independent code)
  • 库名规则:libxxx.so
  1. 生成动态库文件,将Add.o与gcc.o写入libmymath.so,我们可以直接编写Makefile来完成。
    在这里插入图片描述
    在这里插入图片描述
  2. 打包成动态库
    在这里插入图片描述
    在这里插入图片描述

使用动态库
  1. 如果有人想使用我们编写的方法,只需要将lib这个目录给他:
    在这里插入图片描述
  2. 写一个main.c 并编译(同静态库)

在这里插入图片描述
我们发现,虽然我们编译完成了,此时我们运行mytest却依旧报错说找不到库文件。这是运行问题,不是gcc问题。在运行时,也要能够让系统帮我们找到运行时所需要使用的动态库。

  1. 更改 LD_LIBRARY_PATH (推荐做法)

LD_LIBRARY_PATH是一个环境变量,我们通过将库文件路径加入其中来保证系统运行时可以找到库文件。但是,当我们重启之后,整个环境变量有还原了。
在这里插入图片描述
如果我们想永久加入,也有办法:ldconfig 配置/etc/ld.so.conf.d/,ldconfig更新

在这里插入图片描述


在动静态库都存在的情况下,系统默认使用动态库。
在这里插入图片描述



猜你喜欢

转载自blog.csdn.net/qq_53268869/article/details/123057372