1

基础 IO(细节感拉满)

 2 years ago
source link: https://blog.csdn.net/weixin_46873777/article/details/122992418
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

C语言中的文件接口

  C语言中的文件读写方法有很多,这里就稍微说一下。忘记了的看我的C语言文件操作博客链接跳转.
我们打开一个文件进行读写操作,C语言中用的FILE结构体。

//写操作
#include <stdio.h>
#include <string.h>
int main()
{
	FILE *fp = fopen("log.txt", "w");
	//这里可以r w a 对应读、写、追加操作
	if(!fp){
		printf("fopen error!\n");
	}
	const char *msg = "hello bit!\n";
	int count = 5;
	while(count--){
		fwrite(msg, strlen(msg), 1, fp);//二进制写入字符串
	}
	fclose(fp);
	return 0;
}
newCodeMoreWhite.png
//读操作
#include <stdio.h>
#include <string.h>
int main()
{
	FILE *fp = fopen("log.txt", "r");
	if(!fp){
		printf("fopen error!\n");
	}
	char buf[1024];
	const char *msg = "hello bit!\n";
	while(1){
	//注意返回值和参数,此处有坑,仔细查看man手册关于该函数的说明
		ssize_t s = fread(buf, 1, strlen(msg), fp);
		if(s > 0){
			buf[s] = 0;
			printf("%s", buf);
		}
		if(feof(fp)){
			break;
		}
	}
	fclose(fp);
	return 0;
}
newCodeMoreWhite.png

这里有需要注意的一个细节
在这里插入图片描述
这里读写的路径,是当前路径,这里提出一个问题,一个空文件占内存空间嘛?
答案:是肯定的。

每个进程,都有一个内置属性,所以文件 = 内容 + 属性

在这里插入图片描述

任何的C程序都会默认打开三个文件,分别叫做标准输入stdin,标准输出stdout,标准错误stderr,他们分别对应键盘文件、显示器文件、显示器文件,仔细观察发现,这三个流的类型都是FILE*, fopen返回值类型,文件指针。

提到这里,可能就有人会问了,为什么硬件也是文件?

1、其实所有的外设硬件,本质的核心操作都是read和write。
2、不同的硬件,对应的读写方式不一样

底层其实是通过一个双链表链接起来的,都有对应的读写操作,所以在Linux下,一切皆文件。
在这里插入图片描述
在C语言中,我们也可以直接对stdin,stdout,stderr进行读写。比如fprintf、fscanf。

写入
在这里插入图片描述
读取
在这里插入图片描述
显示调用
在这里插入图片描述

在这里插入图片描述

系统文件 IO

操作文件,除了上述C接口(当然,C++也有接口,其他语言也有),我们还可以采用系统接口来进行文件访问。在此之前,我们先站在OS角度,来看进程和文件的关系。

进程和文件的关系

  1. 进程作为文件的使用者,理所当然将要使用的文件记录于自己的进程控制块(PCB),由于进程所对应的程序也是一个文件,文件的相关信息也被记录。
  2. 进程通过调用open()接口打开一个文件时,会把文件封装到一个struct file结构体的实例对象中,也就是上面所画的图,通过一个函数指针进行维护(Linux下一切皆文件)。

我们先来了解如何使用系统调用接口。

man 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
newCodeMoreWhite.png

mode_t理解:文件权限
open 函数具体使用哪个,和具体应用场景相关,如目标文件不存在,需要open创建,则第三个参数表示创建文件的默认权限,否则,使用两个参数的open

  1 #include <stdio.h>  
  2 #include <sys/types.h>  
  3 #include <sys/stat.h>  
  4 #include <fcntl.h>  
  5 #include <unistd.h>  
  6 #include <string.h>  
  7 int main()  
  8 {  
  9   umask(0);  
 10   int fd = open("myfile", O_WRONLY|O_CREAT, 0644);  
 11   if(fd < 0){  
	 12   perror("open");  
	 13   return 1;  
 14   }  
 15   int count = 5;  
 16   const char *msg = "hello bit!\n";  
 17   int len = strlen(msg);  
 18   while(count--){                                                                                                                                                                                    
 19   	write(fd, msg, len);//fd: 后面讲, msg:缓冲区首地址, len: 本次读取,期望写入多少个字节的数据。 返回值:实际写了多少字节数据  
 20   }  
 21   close(fd);  
 22   return 0;  
 23 }  
newCodeMoreWhite.png

在这里插入图片描述
读操作

  1 #include <stdio.h>
  2 #include <sys/types.h>
  3 #include <sys/stat.h>
  4 #include <fcntl.h>
  5 #include <unistd.h>
  6 #include <string.h>
  7 int main()
  8 {
  9   int fd = open("myfile", O_RDONLY);
 10   if(fd < 0){
 11      perror("open");
 12     return 1;
 13   }
 14   char buffer[1024];
 15   const char* msg = "hello bit!\n";
 16   while(1)
 17   {
 18     size_t s = read(fd, buffer, strlen(msg));
 19     if(s > 0) printf("%s", buffer);
 20     else break;                                                                                                                                                                                      
 21   }
 22  // int count = 5;
 23  // const char *msg = "hello bit!\n";
 24  // int len = strlen(msg);
 25  // while(count--){
 26  //   write(fd, msg, len);//fd: 后面讲, msg:缓冲区首地址, len: 本次读取,期望写入多少个字节的数据。 返回值:实际写了多少字节数据
 27  // }
 28   close(fd);
 29   return 0;
 30 }
newCodeMoreWhite.png

在这里插入图片描述
追加操作

第二位的操作增加append即可

在这里插入图片描述

在这里插入图片描述

在这里,我们发现了,使用了或运算,这里解释一下:
在这里插入图片描述

每个语言本身都有自己的语法规则,系统调用使用成本较高,而且不具备可移植性。
在这里插入图片描述

C语言的库函数会自动根据平台,选择自己底层对应的文件接口。
在这里插入图片描述

open函数返回值

在认识返回值之前,先来认识一下两个概念: 系统调用 和 库函数

  • 上面的 fopen fclose fread fwrite 都是C标准库当中的函数,我们称之为库函数(libc)。
  • open close read write lseek 都属于系统提供的接口,称之为系统调用接口
  • 回忆一下我们讲操作系统概念时,画的一张图
    在这里插入图片描述
    系统调用接口和库函数的关系,一目了然。
    所以,可以认为,f#系列的函数,都是对系统调用的封装,方便二次开发。

文件描述符

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

我们可以看一下fd的值是什么 ?
在这里插入图片描述
为什么这里fd是3?

  • Linux进程默认情况下会有3个缺省打开的文件描述符,分别是标准输入0(stdin), 标准输出1(stdout), 标准错误2(stderr).
  • 0,1,2对应的物理设备一般是:键盘,显示器,显示器

0 1 2 已经被占用了,所以新创建的普通文件对应的fd是3。

所有的文件,如果要被使用,首先必须被打开。一个进程可以打开多个文件,系统内被打开的文件,一定是有多个的。那么这些多个文件,也是需要被操作系统管理起来的。先描述,在组织。这里就是一个结构体struct file封装的,里面装了目标文件的基本操作与部分属性(文件名、创建时间、文件大小等等)。

在这里插入图片描述
简略图
在这里插入图片描述

而现在知道,文件描述符就是从0开始的小整数。当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件。于是就有了file结构体。表示一个已经打开的文件对象。而进程执行open系统调用,所以必须让进程和文件关联起来。每个进程都有一个指针files,指向一张表files_struct*,该表最重要的部分就是包涵一个指针数组,每个元素都是一个指向打开文件的指针!所以,本质上,文件描述符就是该数组的下标。所以,只要拿着文件描述符,就可以找到对应的文件

文件描述符的分配规则

将标准输入关闭了之后,此时发现fd的值为0。
在这里插入图片描述

在这里插入图片描述
发现结果是: fd: 0可见,文件描述符的分配规则:在files_struct数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符

探究重定向的本质前,我们先来看一下重定向的现象

  • 输出重定向
    在这里插入图片描述
    这里我们将hello bit 输出重定向到了 log.txt 。
    1 #include <stdio.h>
    2 #include <sys/types.h>
    3 #include <sys/stat.h>
    4 #include <fcntl.h>
    5 #include <stdlib.h>
    6 int main()
    7 {
E>  8   close(1);
    9   int fd = open("myfile", O_WRONLY|O_CREAT, 00644);
   10   if(fd < 0){
   11     perror("open");
   12     return 1;
   13   }
   14   printf("fd: %d\n", fd);
   15   fflush(stdout);
E> 16   close(fd);
   17   exit(0);
   18 }   
newCodeMoreWhite.png

在这里插入图片描述

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

那重定向的本质是什么?
在这里插入图片描述
还有一个问题:我们 printf 的时候,为什么要加fflush(stdout)强制刷新缓冲区?
在这里插入图片描述

使用 dup2 系统调用

函数原型如下:

#include <unistd.h>
int dup2(int oldfd, int newfd);

我们通过man函数看一下使用规则
在这里插入图片描述
newfd是oldfd的一份拷贝,拷贝的不是fd,而是拷贝fd对应数组中的内容,所以oldfd指向新创建的普通文件,

输出重定向:

    1 #include <stdio.h>
    2 #include <string.h>
    3 #include <unistd.h>
    4 #include <fcntl.h>
    5 int main()
    6 {
    7   int fd = open("log.txt", O_CREAT | O_RDWR);
    8   if (fd < 0) {
    9     perror("open");
   10     return 1;
   11   }
   12   //close(1);
   13   dup2(fd, 0);
   14   const char* msg = "hello world\n";
   15   ssize_t write_size = write(1, msg, strlen(msg));
   16   //char buf[1024] = {0};
   17   //ssize_t read_size = read(0, buf, sizeof(buf) - 1);
   18   if (write_size < 0) {
   19     perror("read");                                                                                                                                                                                
E> 20     return;
   21   }
   22 
   23   //printf("%s", buf);
   24   fflush(stdout);
   25 
   26   return 0;
   27 }

   

newCodeMoreWhite.png

在这里插入图片描述

输入重定向:

    1 #include <stdio.h>
    2 #include <unistd.h>
    3 #include <fcntl.h>
    4 #include <string.h>
    5 int main()
    6 {
    7   int fd = open("myfile.txt", O_CREAT | O_RDWR);
    8   if (fd < 0) {
    9     perror("open");
   10     return 1;
   11   }
   12   dup2(fd, 1);                                                                                                                                                                                     
   13   char buf[1024] = {0};
   14   ssize_t read_size = read(0, buf, sizeof(buf) - 1);
   15   if (read_size < 0) {
   16     perror("read");
E> 17     return;
   18   }
   19   printf("%s", buf);
   20   fflush(stdout);
   21 
   22   return 0;
   23 }

newCodeMoreWhite.png

在这里插入图片描述
追加重定向,添加append权限即可

那么这里提一个问题,程序替换的时候,会不会影响重定向对应的数据几个和数据?

当然是不会的,任务控制块task_struct里面的指针分别指向进程地址空间和文件结构体struct files_struct,各执行各的流程,互不影响

在这里插入图片描述

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

在这里插入图片描述
在这里插入图片描述
刚刚上面重定向讲解了为什么需要fflush(stdout),因为printf是自带缓冲区的,fopen、fwrite等函数也是自带缓冲区的,所以FILE结构体里面也存在缓冲区。
在这里插入图片描述

struct FILE 内部包含:
1、底层对应的文件描述符下标
2、应用层,C语言提供的缓冲区数据

在这里插入图片描述
fflush(stdout)函数原型:int fflush(FILE *stream); FILE里面的stdout还存在,stdout->1。

关闭了1,此时指向普通文件,但是数据仍在应用层的缓冲区中待着,所以我们需要强制刷新缓冲区,把缓冲区的数据刷新到对应的普通文件中。

我们再来看一个细节

fprintf(stdout,"hello bit\n");                                                                                                                                                                     
fprintf(stderr, "hello world");

在这里插入图片描述
都是往显示器上打印,可以打印出来,此时我们重定向一下,在来看看现象?
在这里插入图片描述
很神奇吧?为什么log.txt的内容只有hello bit ?
在这里插入图片描述
1指向的stdout,2指向的stderr,此时输出重定向1指向普通文件,改变的是1,当然 2 继续向显示器打印。

  1. 数据不是直接写入到磁盘
  2. 而是通过 fd->task_struct->files_struct->对应文件->系统缓冲区
  3. OS有自己的刷新策略和时机
  4. 为什么用户层不直接和内核打交道,代价太大(王婆介绍媳妇的故事),通过一些列机制访问缓冲区(就像磁盘有自己的磁盘驱动)

在这里插入图片描述
我们结合之前学的进程创建再来看一下系统IO
在这里插入图片描述

  1. 为什么显示器中数据只存在一份?

因为数据此时的刷新方式是行刷新,每行数据后面都有\n,所以就刷新出数据了,此时在fork也就没有了意义。

  1. 为什么输出重定向到普通文件中数据会存在两份?

此时数据的刷新方式由行刷新变成了全缓冲,单独一个\n不能填满缓冲区,数据仍然留在缓冲区中,此时发生了写时拷贝,数据拷贝了一份,此时普通文件中也就两份数据了。

  1. 为什么系统调用的write只有一份?

write是系统调用,没有缓冲区,数据不管在显示器打印还是输出重定向到普通文件中,都只存在一份。

这里结合了:缓冲区 + 写时拷贝 + 数据刷新策略 + 文件差异

定义:打开的文件,其属性与操作的方法就在struct file{},属于内存级文件,进程。普通未打开的文件,在磁盘上面,未被加载到内存,属于程序。打开的文件需要被管理,未打开的文件也需要被管理,这些文件都需要由OS文件系统进行管理。

先来了解文件系统?
  我们知道文件是存在硬盘上面的,也就是所谓的磁盘,我们可以把磁盘看做一个线性的空间,有一个一个的分区(电脑上的C盘、D盘、E盘)。
在这里插入图片描述
我们来看看具体化的文件系统
在这里插入图片描述

  • 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节点表:存放文件属性 如 文件大小,所有者,最近修改时间等
  • 数据区:存放文件内容

从上图我们可以得知一个文件系统

  1. 基本上,一个文件对应一个iNode(包括目录)
  2. iNode是一个文件的所有的属性集合(没有文件名,属性集合也是数据,也要占空间)
  3. 真正表示文件的不是文件名,而是iNode编号
  4. iNode是可以和特定的数据块产生关联的
  5. 程序员按照路径定义一个文件(路径存在目录对应的数据块中)
  6. Linux下属性和内容是分离的,属性iNode保存(iNode编号),内容在data blocks中
  7. 目录也是一个文件,也有对应的iNode编号,其内容保存的是其他文件的文件名和iNode之间的映射关系。
  8. 所以大多数OS同一个目录下,是不允许存在同名文件的(因为一个文件一个iNode,映射关系会错乱,会导致找不到相对应的文件内容)

我们可以通过stat命令来看一下一个文件存储在哪里的,下面是对应的文件系统的内容。
在这里插入图片描述
那么属性和数据分离到底是怎么工作的呢?我们先来创建一个文件。

[root@localhost linux]# touch abc
[root@localhost linux]# ls -i abc
263466 abc

在这里插入图片描述
创建一个新文件主要有一下4个操作:

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

理解硬链接

我们看到,真正找到磁盘上文件的并不是文件名,而是inode。 其实在linux中可以让多个文件名对应于同一个inode。

我们通过ln 命令创建对应硬链接,在通过ll -i -a -l命令可以发现硬链接是对源文件的一份拷贝,iNode的数量+1
在这里插入图片描述
tmp.txt和hard_tmp链接状态完全相同,他们被称为指向文件的硬链接。内核记录了这个连接数,inode656043的硬连接数为2。

我们在删除文件时干了两件事情:1.在目录中将对应的记录删除,2.将硬连接数-1,如果为0,则将对应的磁盘释放。

理解软链接

硬链接是通过inode引用另外一个文件,软链接是通过名字引用另外一个文件,在shell中的做法。

在这里插入图片描述

总结:
1、硬链接相当于是一份拷贝,对应源文件的iNode数+1即可,删除源文件时,该文件不受到影响,对应iNode-1。
2、软连接是一个普通文件,有自己的iNode,内容存的对应源文件的路径,软链接相当于是快捷方式。

静态库与动态库

  • 静态库(.a):程序在编译链接的时候把库的代码链接到可执行文件中。程序运行的时候将不再需要静态库
  • 动态库(.so):程序在运行的时候才去链接动态库的代码,多个程序共享使用库的代码。
  • 一个与动态库链接的可执行文件仅仅包含它用到的函数入口地址的一个表,而不是外部函数所在目标文件的整个机器码
  • 在可执行文件开始运行以前,外部函数的机器码由操作系统从磁盘上的该动态库中复制到内存中,这个过程称为动态链接(dynamic linking)
  • 动态库可以在多个程序间共享,所以动态链接使得可执行文件更小,节省了磁盘空间。操作系统采用虚拟内存机制允许物理内存中的一份动态库被要用到该库的所有进程共用,节省了内存和磁盘空间
生成静态库
[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 详细信息

库搜索路径

  • 从左到右搜索-L指定的目录。
  • 由环境变量指定的目录 (LIBRARY_PATH)
  • 由系统指定的目录
  • /usr/lib
  • /usr/local/lib
    在这里插入图片描述

生成动态库

  • shared: 表示生成共享库格式
  • fPIC:产生位置无关码(position independent code)
  • 库名规则:libxxx.so
  • 依赖关系
    [root@localhost linux]# gcc -fPIC -c sub.c add.c [root@localhost linux]# gcc -shared -o libmymath.so *.o [root@localhost linux]# ls add.c add.h add.o libmymath.so main.c sub.c sub.h sub.o

使用动态库

  • l:链接动态库,只要库名即可(去掉lib以及版本号)
  • L:链接库所在的路径

运行动态库

  • 1、拷贝.so文件到系统共享库路径下, 一般指/usr/lib
  • 2、更改 LD_LIBRARY_PATH(最重要的一步)

库文件名称和引入库的名称
如:libc.so -> c库,去掉前缀lib,去掉后缀.so,.a

在这里插入图片描述

1、二者的不同点在于代码被载入的时刻不同。
2、静态库的代码在编译过程中已经被载入可执行程序,因此体积比较大。
3、动态库(共享库)的代码在可执行程序运行时才载入内存,在编译过程中仅简单的引用,因此代码体积比较小。
4、不同的应用程序如果调用相同的库,那么在内存中只需要有一份该动态库(共享库)的实例。
5、静态库和动态库的最大区别,静态情况下,把库直接加载到程序中,而动态库链接的时候,它只是保留接口,将动态库与程序代码独立,这样就可以提高代码的可复用度,和降低程序的耦合度。
6、静态库在程序编译时会被连接到目标代码中,程序运行时将不再需要该静态库。
7、动态库在程序编译时并不会被连接到目标代码中,而是在程序运行是才被载入,因此在程序运行时还需要动态库存在

这里我们知道区别以及如何封装一个静态库和动态库就可以了。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK