对文件的理解:
文件 = 文件内容 + 属性(即数据)
linux认为,一切皆文件。
站在系统的角度而言,能够被input读取或者能够被output写出的设备就叫做文件。
狭义上的文件:普通的磁盘文件
广义上的文件:显示器,键盘,网卡,声卡,显卡等几乎所有的外设,都可以称为文件。
当前路径:当一个程序运行起来的时候,每一个进程都会记录自己当前所处的工作路径,所谓当前路径就是指,该进程所处的工作路径。
一、C标准库IO操作
1.1、写文件
#include <stdio.h>
#include <string.h>
int main()
{
//使用只写的方式打开文件,fp为 FILE* 类型指针
FILE *fp = fopen("myfile", "w");
//如果打开失败,打印错误
if(!fp){
printf("fopen error!\n");
}
const char *msg = "hello bit!\n";
int count = 5;
while(count--){
//使用fwirte将msg写入文件,该接口为c标准库接口
fwrite(msg, strlen(msg), 1, fp);
}
//写入完成后,关闭文件
fclose(fp);
return 0;
}
1.2、读文件
#include <string.h>
int main()
{
//使用只读的方式打开文件,fp为 FILE* 类型指针
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);
rerturn 0;
}
1.3、输出信息到显示器
#include <stdio.h>
#include <string.h>
int main()
{
const char *msg = "hello fwrite\n";
//使用fwirte将字符串打印到stdout中
fwrite(msg, strlen(msg), 1, stdout);
printf("hello printf\n");
fprintf(stdout, "hello fprintf\n");
return 0;
}
1.4、stdin & stdout & stderr
c语言进行运行的时候会默认打开三个输入输出流:stdin ,stdout ,stderr
这三个流的返回值都是FILT* 类型的文件指针
1.5、总结
打开文件的方式如上所示
二、系统文件IO操作
2.1、写入操作
2.1.1、关于open操作的解释
- pathname:要打开的文件的路径名。
- flags:打开文件的标志,指定了打开文件的方式。常用的标志包括:
- O_RDONLY:只读模式,打开文件用于读取。
- O_WRONLY:只写模式,打开文件用于写入。
- O_RDWR:读写模式,打开文件用于读取和写入。
- O_CREAT:如果文件不存在,则创建文件。
- O_TRUNC:如果文件存在且以写入方式打开,则清空文件内容。
- O_APPEND:写入时追加到文件末尾。
- mode:如果使用了O_CREAT标志,指定新建文件的权限。在大多数情况下,这个参数可以被忽略。
2.1.2、写入代码
#include<stdio.h>
#include<sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{
//umask为权限掩码
umask(0);
//open函数参数:第一个为文件路径,第二个参数为打开文件的方式,第三个参数指定文件的权限
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;
}
2.2、读文件
2.2.1、关于read操作
fd
:文件描述符,指定要读取的文件。buf
:用于存储读取数据的缓冲区的指针。count
:要读取的字节数。
2.2.2、代码
#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;
}
2.3、open函数返回值
- 上面的 fopen fclose fread fwrite 都是C标准库当中的函数,我们称之为库函数(libc)。
- 而, open close read write lseek 都属于系统提供的接口,称之为系统调用接口
三、文件描述符(fd)
3.1、fd的引入
如上图代码,可看到如下结果:
为什么文件描述符是从3开始的,理论上应该是从0开始,实际上0,1,2已经被stdio,stdout,stderr占用。
而这些open,fopen等等的接口的返回值都是一个FILE* 类型的,那么FILE*是什么呢,通过查阅底层代码可看到FILE其实是一个结构体,而C文件,库函数内部一定要调用系统调用,在系统的角度,只认fd,那么就可以知道,在FILE这个结构体中一定封装了fd。
3.2、对fd的理解(本质是数组下标)
什么是fd呢:
一个进程想要访问文件,就需要先打开文件,一般而言,一个进程可以打开多个文件,而文件要被访问的前提是要加载到内存中,只有文件被加载到内存中才可以直接被访问。
系统中存在大量的被打开的文件,所以操作系统(OS)需要通过先描述再组织的方法将这些文件管理起来,在内核中,操作系统内部为了管理一个被打开的文件,构建一个struct file结构体,充当一个被打开的文件,如果文件很多,则会使用双链表来进行连接。
struct file
{
struct file* next;
strucr file* prev;
//包含一个被打开文件的几乎所有的内容。
}
四、重定向
4.1、输出重定向
此时运行上述代码可以看到如下结果:
可以发现,本来应该输出到显示器的内容却输出到了文件log.txt中,这种现象叫做输出重定向,常见的重定向方式有>,>>,<.
重定向的本质:在操作系统内部,更改fd对应的内容的指向。
4.2、dup2系统调用重定向
4.2.1、dup2介绍
在这里插入图片描述
oldfd
:表示需要复制的源文件描述符。这个参数是一个整数值,通常是一个已经打开的文件描述符。newfd
:表示目标文件描述符。这个参数也是一个整数值,通常是另外一个已经打开的文件描述符。如果该文件描述符已经被打开,则会在复制之前关闭它。
4.2.2、dup2使用![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/cdec921631794dd68d21c79b5021dadb.png)
4.3、缓冲区、
***缓冲器就是一段内存空间,这个空间有c标准库维护。***该结构被包含在FILE结构体中。
缓冲区的缓冲策略:
-
立即刷新。
-
行刷新(行缓冲)
-
满刷新(全缓冲)
-
特殊情况:用户强制刷新(fflush),进程退出。
关于缓冲区的认识:
一般而言,行缓冲的设备文件为:显示器,全缓冲的设备文件:磁盘文件。
所有的设备都倾向于全缓冲,因为缓冲器满了才刷新,这样只需要更少次的 IO 操作,更少次的外设访问,能提高效率。
显示器:显示器上的内容是直接给用户看的,一方面需要照顾效率,另一方面也需要照顾用户体验,在极端情况下是可以自定义规则的。
在该程序中同样的一个程序,向显示器打印输出4行文本,但是向普通文件(磁盘上),打印的时候,变成了7行,其中:
1.C I0接口是打印了2次的
2.系统接口,只打印一次和向显示器打印一样!
如果向显示器打印,刷新策略是行刷新,那么最后执行fork的时候–一定是函数执行完了 &&数据已经被刷新了! fork也就无意义了!
如果你对应的程序进行了重定向 – 要向磁盘文件打印 – 隐形的刷新策略变成了全缓冲! 边没有意义了fork的时候 --一定是函数一定执行完了,但是数据还没有刷新!!-在当前进程对应的C标准库中的缓冲区中!!–这部分数据是父进程的数据
五、文件系统
5.1、背景知识
在磁盘中还有一类文件,这类文件叫做磁盘级文件,这类文件在进行文件操作的时候未被打开。
5.2、了解磁盘
内存:掉电易失存储介质;磁盘:永久性存储介质—SSD ,U盘,flash卡,光盘,磁带等。磁盘是一个机械设备,是操作系统中唯一一个机械设备,是一个外设。
在物理上,磁盘中分为一个一个的扇区,将数据通过特定方式存储在扇区中,当要找到该数据时候,先找到该数据存储的扇区,然后再在该扇区中寻找想要的数据。那么如何找到该扇区呢。那就需要使用CHS寻址。在哪一个面上对应的就是哪一个磁头,找到磁道,然后找到扇区。对磁盘的管理也就说对一个小分区的管理
5.3、文件系统结构
磁盘的基本单位是扇区(512字节),但是操作系统和磁盘管理的基本单位是:4KB(8*512byte)。如果文件基本单位太小就会导致多次IO 操作,进而使效率降低;如果操作系统使用和磁盘一样大,当磁盘的基本带下改变,就会到子OS的源代码改变,
文件 = 内容 + 属性
- Block Group:ext2文件系统会根据分区的大小划分为数个Block Group。而每个Block Group都有着相 同的结构组成。政府管理各区的例子
- Data block :多个4KB大小的集合,报错的都是特定文件的内容。
- 超级块(Super Block):存放文件系统本身的结构信息。记录的信息主要有:bolck 和 inode的总量, 未使用的block和inode的数量,一个block和inode的大小,最近一次挂载的时间,最近一次写入数据的 时间,最近一次检验磁盘的时间等其他文件系统的相关信息。Super Block的信息被破坏,可以说整个 文件系统结构就被破坏了
- GDT,Group Descriptor Table:块组描述符,描述块组属性信息。
- inod Table:inode是一个大小为128字节的空间,保存的是对应文件的属性,该块组内,所有文件的inode空间的集合,需要标识唯一性,每一个inode块都要有一个inode编号,一般而言,一个inode,一个inode编号。
- 块位图(Block Bitmap):Block Bitmap中记录着Data Block中哪个数据块已经被占用,哪个数据块没有被占用 ,假设有10000+个blocks,10000+比特位:比特位和特定的block是一一对应的,其中比特位为1,代表该block被占用,否则表示可用!
- inode位图(inode Bitmap):每个bit表示一个inode是否空闲可用。
- i节点表:存放文件属性 如 文件大小,所有者,最近修改时间等
- 数据区:存放文件内容
创建一个新文件主要有一下4个操作:
- 存储属性 内核先找到一个空闲的i节点(这里是263466)。内核把文件信息记录到其中。
- 存储数据 该文件需要存储在三个磁盘块,内核找到了三个空闲块:300,500,800。将内核缓冲区的第一块数据 复制到300,下一块复制到500,以此类推。
- 记录分配情况 文件内容按顺序300,500,800存放。内核在inode上的磁盘分布区记录了上述块列表。
- 添加文件名到目录
5.4、软硬链接
5.4.1、软链接
软链接有独立的inode,是一个独立的文件,软链接的文件内容是指向文件对应的路径,相当于windows系统下的快捷方式。
ln -s 源文件 目标文件
如图所示,a.txt就是一个链接到abc.txt的软链接。
5.4.2、硬链接
硬链接没有独立的inode,不是一个独立的文件,创建硬链接只是在指定目录下建立了文件名和指定inode建立了映射关系。
ln 源文件 目标文件
建立了硬链接之后我们发现图示所表示的数由1 变成了2,这个数就是所谓的硬链接数,当创建硬链接这个数就会加1
通过观察inode可以发现,硬链接的inode和源文件的inode相同,说明创建硬链接不会创建新的inode。
通过上图可以看见一个发现一个新目录的硬链接数为2,是因为,该目录下有一个 " . "文件的硬链接,那么这样就可以通过硬链接数来看出该目录下有几层。
六、动静态库
6.1、静态库
ar lib静态库名.a 目标文件.o
静态库的前缀为必须为lib,后缀为.a
6.1.1、静态库的使用
6.1.1.1、直接使用
-I :文件头文件搜索
-L : 库文件搜索路径
-l : 在特定路径下使用的库(去前缀和后缀)
6.1.1.2、静态库安装
自己写的库为第三方库,所以操作系统不会自动连接
C语言默认静态库路径:ls /lib64/libc.a
头文件默认搜索路径为 /usr/include
库文件默认搜索路径为 /usr/lib64
安装后使用静态库:gcc main.c -lhello
6.2、动态库
动态库形成:
形成的 .o 文件需要添加 -fPIC选项,该选项意思为:形成一个与位置无关的目标二进制文件
动态库的前缀为lib 后缀为 .so 需要添加一个-shared选项
6.2.1、动态库使用
6.2.1.1、直接使用
如果只有静态库,那么gcc只能针对该库进行静态链接
如果动态库和静态库同时存在,那么默认使用的是动态库
如果动态库和静态库同时存在,非要使用静态库那么需要添加 -static选项
-static:摒弃默认使用动态库的原则,而是使用动态库的原则
但是该命令生成的可执行文件不可以直接运行,需要在LD_LIBRARY_PATH中添加环境变量
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:库文件目录
6.2.1.2、配置文件
在/etc/ld.so.conf.d目录下创建一个.conf配置文件,将库文件目录放到该文件目录中,再使用ldconfig使该文件生效
6.2.1.3、建立软链接
在lib64目录下建立一个软连接
6.3、库存在的意义
站在使用库的角度:库的存在可以大大减少开发的周期,提高软件本身的质量
M-1720530783950)]
如果只有静态库,那么gcc只能针对该库进行静态链接
如果动态库和静态库同时存在,那么默认使用的是动态库
如果动态库和静态库同时存在,非要使用静态库那么需要添加 -static选项
-static:摒弃默认使用动态库的原则,而是使用动态库的原则
但是该命令生成的可执行文件不可以直接运行,需要在LD_LIBRARY_PATH中添加环境变量
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:库文件目录
6.2.1.2、配置文件
在/etc/ld.so.conf.d目录下创建一个.conf配置文件,将库文件目录放到该文件目录中,再使用ldconfig使该文件生效
6.2.1.3、建立软链接
在lib64目录下建立一个软连接
6.3、库存在的意义
站在使用库的角度:库的存在可以大大减少开发的周期,提高软件本身的质量
站在写库的角度:简单,代码安全