基础IO 1

一. 引入

写一个C文件

#include <stdio.h>

int main(){
    //第一个参数为可选路径+文件名进行创建,如果没有路径,默认在当前路径下创建
    //第二个参数为选项
	FILE* fp = fopen("log.txt","w");
	if(NULL == fp){
		perror("fopen error!\n");
	}
	else{
		char c = 'A';
		for(; c <= 'Z'; ++c){
			fputc(c,fp);
		}
		fclose(fp);
	}
	return 0;
}

上面程序运行,在当前目录下创建log.txt的文件,并将A…Z的字符全部写进去

1. 站在进程角度,它是怎么去访问该文件的

文件=内容+属性
Linux下一切皆文件(键盘,显示器也可当作文件来看待)

为什么会存在这些输入输出流

因为语言设计出来执行指令,肯定需要与人进行交互,为了交互,需要有这些输入输出

写入文件与显示器
size_t fwrite(const void* ptr,size_t size,size_t count,FILE* stream);
//返回值:返回实际写入基本单位个数
//参数:
//  1.写入内容地址
//  2.写入数据的基本单位大小
//  3.写入几个基本单位
//  4.往哪儿写

如:

const char* msg = "hello world\n";
fwrite(msg,strlen(msg),1,stdout);
//这里不用strlen(msg)+1把\n写入,以\n作为结尾是字符串要求,文件不会要求以\n结尾,写入\n会出问题

这里的例子我们可以直接往显示器写,也就是说,运行这段代码程序,会把hello world打印出来。
所以普通文件与显示器实际上没有什么区别,都可以被fwrite写入

2. 总

  • 除了上面库函数接口,我们还有一套系统调用接口,如:open,close,read,write
  • 库函数相当于对系统调用进行封装,调用库函数 == 调用系统调用
  • 系统调用只有一套,就被封装成了各种语言的库函数
  • 显示器,往硬盘中写数据都是给硬件写,所有的I/O操作都是往硬件上写数据,怎么写?都是OS帮我们写

二. 系统文件I/O

1. open

#include <sys/types.h>
#include <sys/stat.h>
#include <fcnt1.h>

int open(const char* pathname,int flags);
int open(const char* pathname,int flage,mode_t mode);

pathname:要打开或创建的目标文件
flags:打开文件时,可以传入多个参数选项,用下面的一个或者多个常量进行”或“运算,构成flags
参数:
	O_RDONLY:只读打开
	O_WRONLY:只写打开
	O_RDWR:读,写打开,这三个常量,必须指定一个且只能指定一个
	O_CREAT:若文件不存在,则创建它,需要使用mode选项,来指明新文件的访问权限
	O_APPEND:追加写
	O_TRUNC:将文件内容清空,再重新重定向写入
返回值:
	成功:新打开的文件描述符
	失败:-1

注:

  • O_RDONLY | O_WRONLY这样相当于按位或,将不同功能组合在一起,传入更多功能
  • O_RDONLY,O_WRONLY这些参数都是宏参数,二进制中只有1位或几位为1,代表不同的功能
  • O_APPEND与O_TRUNC的区别,O_APPEND是不清空,直接写入,O_TRUNC是将文件内容清空,再重新重定向写入

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

返回值,成功返回新打开的文件描述符,在OS层面,其就是一个整数,从0-n;0表示标准输入,1表示标准输出,2表示标准错误。

2. 系统文件结构

因为OS一定要把打开的文件管理起来
在Linux描述文件用struct file结构

struct file{
	//文件的属性信息
	//文件的缓冲与存储位置
}
//这些信息在文件和硬盘上都有

同时打开文件要与进程关联,一个进程又可以打开多个文件,所以整个结构如下:
在这里插入图片描述

由上图可知,task_struct中有一个指针指向struct file_struct,在file_struct中有一个fd_array[]数组,其中每个位置指向一个struct file

所以就靠这样,将文件与进程关联起来,先用链表将文件信息与文件之间管理起来,然后进程用一个数组将多个文件管理起来

注:fd_array[] 默认大小为32(内核3.6.32版本)
如果超过32会升级,让你继续大,但也会有限制

如:write(4,“hello”,5);
OS找到当前进程pid,进入进程内部结构体,系统在其指向文件数组中搜4,就可以找到对应文件,找文件中内容

3. open总

  • 结合后面与前面知识,open不仅处于底层是系统封装的,我们open是进程去调用,然后遍历struct file fd_array[]数组,找到最小下标没有被用过的数组空间,把新打开的文件地址填到对应的数组空间中
    open把其在数组中的下标返回,这就是文件描述符

我们在一个进程中打开的第一个文件,其文件描述符为3,因为0,1,2分别为标准输入,标准输出,标准错误

4. 一些小细节

我们close(0)后,再打开第一个文件的文件描述符为0,而不为3。

close(1)与close(2)也类似

对于进程来讲,第一个文件的进程描述符为3,但如果有更小的,会把更小下标的数组空间给其使用,返回更小的数组下标

当我们close(1)后,再打开一个文件,我们在printf等打印时,发现打印不出来

因为1文件描述符之前其为标准输出所用,我们关闭其后,在fd_array[] 中下标为1的数组空间就空闲出来了;我们再打开一个文件,会将下标为1的数组空间给其使用,也就是我们新打开文件的文件描述符变成了1

但上层并不知道,它依然认为1为标准输出,依然往占用1文件描述符的文件中输入

这也是重定向的原理:

echo "hello world" > my.txt
//就是把”hello world"输入到了my.txt中

echo "hello world" >> mytxt
//相当于加了O_APPEND的选项,可追加输入

5. write与read

#include <unistd.h>

size_t write(int fd,const void* buf,size_t count);

参数:
1.写入文件的文件描述符
2.写入的字符串
3.期望写多少字节数

返回值:
成功写入的字节数,出错返回-1并设置error
stdout vs 1(文件描述符)

stdout属于C的库中。其类型是FILE*的,其中有文件描述符的信息
文件描述符1属于底层OS结构
所以文件描述符1是更靠近底层

read
#include <unistd.h>    
ssize_t read(int fd, void *buf, size_t count); 

返回值:成功读取的字节数,出错返回-1并设置error

参数:
1.从哪儿读
2.读向哪儿
3.期望读多少字节数

6. 缓冲区

int main(){
	close(1);
	int fd = open("log.txt",O_CREAT | O_APPEND | O_WRONLY,0644);
	if(fd < 0){
		perror("open error");
		return 1;
	}
	const char* str = "hello world:write\n";
	const char* str2 = "hello world:printf\n";
	const char* str3 = "hello world:fprintf\n";
	write(1,str,strlen(str);
	printf("%s",str2);
	fprintf(stdout,"%s",str3);
	fork();
	fflush(stdout);
	close(fd);
	return 0;
}

fflush是为了把缓冲区的内容刷新出来,
上面代码的结果为:

当使用close(1)再打开文件,将标准输出重定向后,这些输出都输入了log.txt,当我们cat log.txt时,会显示出如下内容:

hello world:write
hello world:printf
hello world:printf
hello world:fprintf
hello world:fprintf

可发现printffprintf打印了两遍

当我们将close(1)注释掉

所有内容打进标准输出,发现三个输出都打印了一遍

由上面的例子我们可知
  • fprintf、printf为库函数,write为系统调用。
  • 缓冲区,显示器:行缓存,遇到\n才刷新出来。文件:全缓存,只有把缓冲区写满,才刷新出来
  • 所以,重定向可能会影响缓冲方式
  • 系统调用不受缓冲方式的影响,直接刷新出来,库函数受缓冲方式的影响
所以上面的例子为什么会出现这样的现象

但close(1)还存在时,此时缓冲方式为全缓冲,printf 与 fprintf 中的数据还存在缓冲区中,讲过fork() 之后,缓冲区中的内容为进程中的数据,父子进程各自私有,经过fflush() ,父子进程都将缓冲区的内容刷新一次,所以打了两次

此时的缓冲区是由库函数提供的(也就是语言提供的),为用户级别的缓冲区。这就是为什么系统调用不受缓冲区的影响。
如果系统提供,系统调用也要受缓冲区的影响

一个文件可被多个进程打开,可有多个fd与其对应’

7. 重定向操作

如果要实现追加重定向,打开文件时,加上O_APPEND即可

方法一
先关闭一个文件
close(fd);int fd = open(...)打开一个文件进行重定向
方法二
#include <unistd.h>
int dup(int oldfd);
int dup2(int oldfd,int newfd);

如:想把输入文件描述符1的内容输入文件描述符fd的文件,怎么做?

dup2(fd,1);

此时文件描述符1不再指向标准输出,而是指向文件描述符fd所指向的文件,这样就完成了重定向
此时文件描述符1与文件描述符fd都指向该文件,我们可以close(fd),将fd关闭

因为维护的fd_array[]大小是有限的,也就是说打开的文件数量是有上限的,能关掉就关掉

8. 对于struct file{}

struct file{
	//属性
	//方法,函数指针
	int (*write)(int fd,char* buf,int size);
	......
}

对于不同的硬件或文件,如硬盘,显示器,键盘,其实我们要使用不同的方法进行写入和读取。但我们通过函数指针,虽然函数指针指向的方法不同,但是在上层看来,我们调用同一个接口write,read即可,相当于在上层做了一层虚拟层(这也是为什么Linux下一切可当作文件看待),这样会使我们的使用更便捷、高效

三. inode

文件=属性+内容,属性也会占空间

1. 属性

我们把文件属性集合在一起给其定结构叫做inode

ls -i file查看file文件的inode号

一个文件只有一个inode

2. 内容

内容在Linux中一般是纯内容,被保存在block内存块中
一个文件可能有多个block

3. filesystem

描述分区的情况

filesystem{
	//基本情况
	//空间一共是多大
	//有多少已经使用  ||  多少没有被使用
	//inode
	//block
	//group

	//方法,后面解析
}

我们使用ls -l时看到了很多文件元数据

-rwxr-xr-x. 1 root root 7438 "9月 13 14:56" a.out

1. 模式
2. 硬链接数
3. 文件所有者
4.5. 大小
6. 最后修改的时间
7. 文件名
磁盘文件系统图

OS去管理
在这里插入图片描述

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

inode:文件的属性 文件 :inode = 1 :1
Data blocks:数据 文件 :inode :data block = 1 :1 :1(orn)

inode必须包含datablocks对应的映射关系
找一个文件,先找inode

  • 我们怎么去寻找inode
    每一个inode id标识一个inode

  • 如何去找文件
    先找区,块,组,在inode table中找到它inode信息,根据映射,去找数据

  • 如何删除
    在位图中,将inode对应的编号删除
    (删没有清除数据,其是可以有办法恢复)

目录也是文件 = inode+数据块
目录中存的是文件名(inode id)到inode的映射关系,文件名是给人看到的,inode id是OS中辨别inode的
所以我们去找目录文件信息,是先在目录中找到inode id及其对应关系,去inode表中找具体信息

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

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

四. 软链接和硬链接

1. 软链接

具有独立的inode,是一个独立的文件(相当于windows中的快捷方式)

如何建立:

ln -s 文件路径文件名 新文件名
//在此目录下,可直接./运行

2. 硬链接

和指向的文件共享同一个inode,不是一个独立的文件

硬链接是什么:在目录下对于一个文件名及其inode的链接关系,又加了一个不同文件名与其inode链接关系

如何建立:

ln 文件名 新文件名

当我们mkdir创建一个目录,其硬链接数为2。因为目录中有一个文件 . 指向当前目录

当我们在一个目录x 下创建一个新目录y 时,x 的硬链接数为3。因为在y中有一个 . .指向上级目录x

3. 删除文件

硬链接:

  • 在目录中,将文件名(inode id)与inode的关联记录删除
  • 将硬链接数-1,如果为0,则直接将其对于的磁盘释放

软链接:
软链接文件是一个独立的文件,有自己的inode节点。这个文件保存的是源文件路径,通过访问的路径放问源文件
如果源文件被删除,则找不到源文件,这时软链接失效

对于一个目录运用 chmod u-r x/ 去除x目录的读权限
我们再次ls x/时将会报错,我们不能看该目录下的文件
而我们可以 ls x/y -al(y为x中的文件)去访问该文件的信息

因为我们去除了目录的读权限,我们不能去读目录数据块的所有内容
但我们对该 x 目录有进入权限,并且我们对文件 y 有读取权限。但我们还是需要找文件inode,但我们指明了文件名,系统中可以通过其他方式去找到。
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值