【Linux】文件

目录

 

一、C文件接口

二、系统文件I/O

         1 .接口介绍

         2 .open函数返回值

         3 . 文件描述符fd

         4 . 文件描述符的分配规则

         5 .重定向

         6 .使用 dup2 系统调用

         7 .FILE

三、缓冲区


 

一、C文件接口

写文件: 

#include <stdio.h>
#include <string.h>
int main()
{
	FILE* fp = fopen("myfile", "w");
	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;
}

读文件:

#include <stdio.h>
#include <string.h>
int main()
{
	FILE* fp = fopen("myfile", "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;
}
输出信息到显示器的方法:
#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;
}
stdin & stdout & stderr
C默认会打开三个输入输出流,分别是stdin, stdout, stderr
 
仔细观察发现,这三个流的类型都是FILE*, fopen返回值类型,文件指针
总结
打开文件的方式:
25ca5c29be1d401ab9d8319710c95873.png

 

二、系统文件I/O

文件:
所谓对于文件的操作究其本质,其实它并不关于任何一门语言,而是所有的语言都使用同样的接口,并且对这些接口封装,从而实现我们看到了语言的各种各样的不同的文件操作方式。
 
        文件等于内容加上属性,所以对于文件的操作就是对于内容的操作和对其属性的操作,当我们没有使用文件时,它是保存在磁盘当中。一旦我们对文件操作时,他就会从磁盘加载到内存当中。

        我们在对文件进行操作的时候,文件需要内加载到内存当中,但是是否只有我们一个人在使用呢?也就是这个文件是否有多个人在对其进行打开操作?答案肯定是不可能只有一个人也就是一个进程在使用的,因为就连我们平时操作Linux时,我都能对一个文件进行多次打开。而且我在一个程序当中是可以打开多个文件的,那么我们可以得到一个结论:进程和文件的对应关系是1:n。
 
操作文件,除了C接口(当然,C++也有接口,其他语言也有),我们还可以采用系统调用接口来进行文件访问
 

       并且,在Liunx下,一切皆文件。
       综上,系统会打开多个进程,而一个进程又会操作多个文件,那么系统中会充斥非常多的文件,这些文件是如何被管理的呢?

加载文件:

首先,我们知道文件在没有被操作时是存储在磁盘当中的,只有在被调度时才会从磁盘加载到内存当中,然后呢?内存当中必然到处都是乱七八糟的文件,必须得有一个管理的操作。
        看到管理大家必须的像是触发了关键词一样,那就是先描述,再组织。没错操作系统对于文件的管理如同进程一样,都是先描述再组织,那么它同样是有自己抽象出来的结构体用于装载自己的信息。

struct file

{

        //文件属性

        //文件各种关系

}

我们的文件内容与我们的管理并没有太多的关系,那么我们就让他存储在内存当中,甚至在刚准备打开文件的时候只需要将文件的各种属性告知操作系统都行,内容慢慢的加载。

文件是由操作系统打开的,也就是进程让操作系统打开的,那么这样我们的对于文件的操作也就变为了进程与文件的操作。
        在系统当中,进程和文件都是被组织起来的数据结构,那么他们之间的交互就变成了两个结构体的操作------struct tast_struct和struct file。
        所以整个文件加载到内存当中的过程就相当于把文件指针(struct file*)放进struct tast_struct结构体当中.

文件结构体里面只存有文件已经加载在内存当中的地址,也就是整个文件管理和内存管理的关系只有这样的联系,并且这个联系是随时能够被更改的。

那么这样做之后有什么好处呢?实现了文件管理和内存管理的解耦操作,也就是两者都互相并不关心彼此是如何操作的,两者之间都只需要一个固定的方式进行交互。

 

 

然后我们再认识一些系统调用接口:

写文件:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{
	umask(0);
	int fd = open("myfile", O_WRONLY | O_CREAT, 0644);
	if (fd < 0) {
		perror("open");
		return 1;
	}
	int count = 5;
	const char* msg = "hello bit!\n";
	int len = strlen(msg);
	while (count--) {
		write(fd, msg, len);//fd: 后面讲, msg:缓冲区首地址, len: 本次读取,期望写入多少个字节的数
		据。 返回值:实际写了多少字节数据
	}
	close(fd);
	return 0;
}

读文件:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{
	int fd = open("myfile", O_RDONLY);
	if (fd < 0) {
		perror("open");
		return 1;
	}
	const char* msg = "hello bit!\n";
	char buf[1024];
	while (1) {
		ssize_t s = read(fd, buf, strlen(msg));//类比write
		if (s > 0) {
			printf("%s", buf);
		}
		else {
			break;
		}
	}
	close(fd);
	return 0;
}

 

         1 .接口介绍

55cf28352d78444ea8fd5cbf0cde049a.png

d260b56fadf94273bb59743d86172a22.png
 
pathname: 要打开或创建的目标文件
 
flags: 打开文件时,可以传入多个参数选项,用下面的一个或者多个常量进行“或”运算,构成flags。他是一个位图结构
 
参数:
        O_RDONLY: 只读打开 
 
        O_WRONLY: 只写打开
 
        O_RDWR : 读,写打开
 
这三个常量,必须指定一个且只能指定一个
 
O_CREAT : 若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限
O_APPEND: 追加写
 
返回值:
 
成功: 新打开的文件描述符
 
失败:-1
open 函数具体使用哪个,和具体应用场景相关,如目标文件不存在,需要open创建,则第三个参数表示创建文件的默认权限,否则,使用两个参数的open。

         2 .open函数返回值

fopen fclose fread fwrite 都是C标准库当中的函数,我们称之为库函数(libc)。
而, open close read write lseek 都属于系统提供的接口,称之为系统调用接口

f49440bcd8db4c42b2ab8ac14fed61b7.png

所以,可以认为,f#系列的函数,都是对系统调用的封装,方便二次开发。

         3 . 文件描述符fd

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

通过对open函数的学习,我们知道了文件描述符就是一个小整数
Linux进程默认情况下会有3个缺省打开的文件描述符,分别是标准输入0, 标准输出1, 标准错误2.
 
0,1,2对应的物理设备一般是:键盘,显示器,显示器
所以输入输出还可以采用如下方式:我们可以直接对文件描述符进行操作
#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;
}

d39f48aab5c64d30a5708762866cab41.png

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

         4 . 文件描述符的分配规则

文件描述符的分配规则:在files_struct数组当中,找到当前没有被使用的
最小的一个下标,作为新的文件描述符。

         5 .重定向

常见的重定向有:>, >>, <

86abc3c2c66341a187f65546df600056.png

这里有两种写法:

51815b50177b42b28f36c0324e902321.png

98ec2ab581cb4a6d92f76faba041229f.png 

         6 .使用 dup2 系统调用

函数原型如下:
 
c4cd71a492d748c9b8b207c705ee003e.png

其中两个参数表示:将oldfd去指向newfd,表示了关闭原来的newfd,成为现在的oldfd。也就是说将newfd位置的文件地址改为oldfd的文件地址。

printf是C库当中的IO函数,一般往 stdout 中输出,但是stdout底层访问文件的时候,找的还是fd:1, 但此时,fd:1 下标所表示内容,已经变成了myfile的地址,不再是显示器文件的地址,所以,输出的任何消息都会往文件中写入,进而完成输出重定向。

         7 .FILE

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

我们可以通过下面代码得出FILE结构体里面的fd:

a3af0dea4d344c398603e7b1ba24d977.png

三、缓冲区

研究一下一下代码:
#include <stdio.h>
#include <string.h>
int main()
{
	const char* msg0 = "hello printf\n";
	const char* msg1 = "hello fwrite\n";
	const char* msg2 = "hello write\n";
	printf("%s", msg0);
	fwrite(msg1, strlen(msg0), 1, stdout);
	write(1, msg2, strlen(msg2));
	fork();
	return 0;
}

运行结果:

hello printf
hello fwrite
hello write
但如果对进程实现输出重定向呢? ./hello > file , 我们发现结果变成了:
hello write
hello printf
hello fwrite
hello printf
hello fwrite
我们发现 printf 和 fwrite (库函数)都输出了2次,而 write 只输出了一次(系统调用)。为什么呢?肯定和fork有关!
一般C库函数写入文件时是全缓冲的,而写入显示器是行缓冲。
 
printf fwrite 库函数会自带缓冲区(进度条例子就可以说明),当发生重定向到普通文件时,数据的缓冲方式由行缓冲变成了全缓冲。
 
而我们放在缓冲区中的数据,就不会被立即刷新,甚至fork之后但是进程退出之后,会统一刷新,写入文件当中。
 
但是fork的时候,父子数据会发生写时拷贝,所以当你父进程准备刷新的时候,子进程也就有了同样的一份数据,随即产生两份数据。
 
write 没有变化,说明没有所谓的缓冲。
综上: printf fwrite 库函数会自带缓冲区,而 write 系统调用没有带缓冲区。另外,我们这里所说的缓冲区,都是用户级缓冲区。其实为了提升整机性能,OS也会提供相关内核级缓冲区,不过不再我们讨论范围之内。因此这里认为内核没有缓冲区。那这个缓冲区谁提供呢? printf fwrite 是库函数, write 是系统调用,库函数在系统调用的“上层”, 是对系统调用的“封装”,但是 write 没有缓冲区,而 printf fwrite 有,足以说明,该缓冲区是二次加上的,又因为是C,所以由C标准库提供。
 
总结:
 
缓冲区就是一段内存空间。
他给上层提供高效的IO体验,间接提高整体的效率。
缓冲区的刷新策略:
1. 立即刷新
 
2. 行刷新 ——显示器,照顾了用户的查看习惯
 
3. 全缓冲 ——缓冲区写满,才刷新,用于普通文件
 
特殊情况:
进程退出,系统自动刷新,或者用相关函数进行强制刷新。

缓冲区:每一个文件都有自己对应的语言级缓冲区,在进行数据交换的时候提高效率。不仅提高了用户的效率,还提高了系统IO的效率。

 

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

何陈陈

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

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

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

打赏作者

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

抵扣说明:

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

余额充值