目录
- 引入
- 内存级文件
- 重新使用C文件接口 -- 对比重定向
- 写文件
- 读文件
- 文件流
- 认识文件操作的系统接口
- open
- 参数 -- flag
- flag的内容
- 宏的传参方式
- open
- 关闭文件
- 写文件
- 读文件
- 结论
- 引入文件描述符fd、对文件的理解
- 理解一切皆文件
- 方法集
- 文件fd的分配规则
- 重定向
- 代码的重定向
- 输入重定向
- 输出重定向
- 追加重定向
- 指令重定向
- 输入重定向
- 输出重定向
- 追加重定向
- 修改之前的shell,让它支持重定向
- 缓冲区
- 概念
- 在哪里
- 磁盘级文件(文件系统)
- 磁盘
- 磁盘的机械构成
- 磁盘的物理存储
- 磁盘的逻辑存储
- 1、红色部分
- 2、蓝色部分
- 文件的增删查改和路径有关
- 具体的文件系统
- 软硬链接
- 原理
- 应用场景
- 动静态库
- 回顾
- 动态库的制作和使用
- 理解动态库加载
- 系统角度理解
- 编址
- 理解动态库动态链接和加载问题
引入
a、文件 = 内容 + 属性
b、访问文件之前,都得先打开。修改文件,都是通过执行代码的方式完成修改,文件必须被加载到内存中
c、进程在打开文件
d、一个打开多少个文件?可以打开多个
一定时间段内,系统中有多个进程,那么就有更多同时被进程打开的文件,OS要不要管理多个被进程打开的文件?肯定的,先描述再组织
e、进程和文件的关系,struct task_struct和struct xxxx
f、没有被打开的文件在哪?磁盘
内存级文件
重新使用C文件接口 – 对比重定向
写文件
文件创建的路径和进程有着很大的关系
像文件里面写入内容:fputs
#include<stdio.h>int main()
{FILE* fp = fopen("./log.txt", "w");if(fp == NULL){perror("fopen");return 1;}const char* str = "hello linux\n";fputs(str, fp);fclose(fp);return 0;
}
文件打开再关闭就被清空了?
以w方式打开文件,该文件会被清空
echo “xxxxxx” > log.txt
以a的方式打开,不会清空文件
echo “xxxxxx” >> log.txt
fwrite写入数据
第一个参数:写入文件的其实内存;第二个参数:写入数据的大小,每个基本单位的大小;第三个参数:写入几个基本单元;
返回值:写入了多少个基本单位
fputs
:是写入字符串的形式,它是认识字符串,以\0
结尾
fwrite
:是以二进制流写入,不认识字符串
如果没有此文件,当指定绝对路径就会在绝对路径下创建这个文件,否则就在该进程所在目录下创建
int main()
{FILE* fp = fopen("log.txt", "w");if(fp == NULL){perror("fopen");return 1;}char* str = "hello file\n";int cnt = 5;while(cnt){int num = fwrite(str, strlen(str), 1, fp);cnt--;}fclose(fp);return 0;
}
那改变进程的路径,创建的这个文件也会变
读文件
fgets
:一行读出
文件流
程序默认打开的文件流
stdin | 标准输入 | 键盘设备 |
---|---|---|
stdout | 标准输出 | 显示器设备 |
stderro | 标准错误 | 显示器设备 |
向显示器继续打印的方法
printf、fprintf、fwrite、fputs
int main()
{FILE* fp = fopen("./log.txt", "w");if(fp == NULL){perror("fopen");return 1;}//标准写入//默认想显示器写入,写入流是stdoutprintf("hello printf\n");//与printf略有不同,写入到哪里不是默认的fprintf(stdout, "hello fprintf\n");fprintf(fp, "hello fprintf\n");//char* str = "hello fputs";fputs(str, fp);fputs(str, stdout);//char* str2 = "hello fwrite";fwrite(str2, strlen(str2), 1, fp);fwrite(str2, strlen(str2), 1, stdout);fclose(fp);
标准输入的方法
scanf 、 fread、fscanf
FILE* fp2 = fopen("./log.txt", "r");if(fp2 == NULL){perror("fopen");return 1;}char str3[64];scanf("%s", str3);printf("%s\n", str3);fscanf(stdin, "%s", str3);printf("%s\n", str3);fread(str3, strlen(str3), 1, stdin);fclose(fp2);
结论:stdin、stdout、stderro可以直接被使用
认识文件操作的系统接口
访问文件不仅仅有C语言上的文件接口,OS必须提供对应的访问文件的系统调用
w:会清空文件
a:追加文件
r:读取文件
open
返回值:成功–文件描述符, 失败—1
参数:flags
返回值:int类型的数据
参数 – flag
flag的内容
O_RDONY | 只读打开 |
---|---|
O_WRONLY | 只写打开 |
O_RDWR | 读写打开 |
O_CREAT | 如果文件没有就创建 |
O_TRUNC | 当对已有内容文件做写入时,会对文件内容清空 |
O_APPEND | 每次文件打开向文件结尾处写,追加 |
宏的传参方式
如果传五个参,要设置五个参数比较低级,可以设置标志位
设置一个参数,参数类型是int, 每个bit位代表一个flag
#include<stdio.h>
#include<string.h>
#include<unistd.h>#define ONE 1
#define TWO (1<<1)
#define THREE (1<<2)
#define FOUR (1<<3)
#define FIVE (1<<4)void Print(int flag)
{if(flag & ONE) printf("1\n");if(flag & TWO) printf("2\n");if(flag & THREE) printf("3\n");if(flag & FOUR) printf("4\n");if(flag & FIVE) printf("5\n");return;
}int main()
{Print(ONE);printf("----------------------\n");Print(THREE);printf("----------------------\n");Print(ONE|TWO|THREE);printf("----------------------\n");Print(THREE|FOUR|FIVE);return 0;
}
对于open函数中的flag参数也是如上那么使用
open
先学习三个参数的
mode
: 权限设置
普通文件:0666
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>int main()
{int fd = open("log.txt", O_WRONLY|O_CREAT);close(fd);return 0;
}
两个参数的open在没有文件的时候也能创建,但是创建文件的权限是乱的
因此需要三个参数的open的第三个参数mode去设置创建文件的权限
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>int main()
{int fd = open("log.txt", O_WRONLY|O_CREAT, 0666);close(fd);return 0;
}
但是上面创建的文件的属性和我们设置的0666
还是不一样的,它是0644
。那是因为操作系统有自己的掩码umask
不使用系统的umask
,想使用自己的可以在代码中使用umask()
函数进行设置
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>int main()
{umask(0);int fd = open("log.txt", O_WRONLY|O_CREAT, 0666);close(fd);return 0;
}
如果文件存在,使用两个参数的open,如果文件不存在,使用三个参数的open
关闭文件
利用返回值关闭文件
用那个整数文件描述符
返回值
为何默认从3开始?0、1、2已经被用了,stdin – 0、stdout–1、stderror – 2
数组下标?
写文件
write
strlen(str)不要+1 : \0不是字符串的内容,是C语言规定字符串以、0结尾。写入就是乱码
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<string.h>int main()
{//写文件int fd = open("log.txt", O_WRONLY|O_CREAT, 0666);if(fd == -1){perror("open");return 1;}char* str = "hello world";ssize_t i = write(fd, str, strlen(str));if(i < 0){perror("write");return 1;}close(fd);return 0;
}
系统调用写文件,不会清空文件,直接覆盖式的往里写
参数flag使用O_TRUNC
,即可清空文件里的内容再往里面写
读文件
返回值:ssize_t
有符号的整数
fd:要读取文件的描述符
buf:读出的内容存放的缓冲区
count:放入内容最大可以存放多少
=0 | 文件结束 |
---|---|
<0 | 读取错误 |
>0 | 读取成功,读到多少个字符 |
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<string.h>int main()
{int fd = open("log.txt", O_RDONLY);char str[1024];ssize_t s = read(fd, str, sizeof(str)-1);str[s] = '\0';printf("%s\n", str);close(fd);
结论
1、C语言的文件接口,本质是封装了系统调用。不仅在接口进行封装,在类型上进行封装
2、FILE是C标准库自己封装的一个结构体,这个结构体里一定包含open返回的那个fd(0.1.2.3…)
3、为何C语言要封装?为了保证自己的跨平台性和可移植性
4、文件描述符的本质:数组下标
引入文件描述符fd、对文件的理解
文件描述符的本质:数组下标
理解一切皆文件
方法集
对于文件的描述struct file
里面有一块是函数指针,函数指针指向具体的硬件的调用方法
文件fd的分配规则
现象1:
关掉0文件,再打开自己文件,看其文件描述符
tu
最小的没有被使用的数组下标,会分配给最新打开的文件
现象2:
关掉1,打印时并没有在屏幕里打印出来,竟然把信息写入到了文件里面
输出重定向:语言层不变,在操作系统层进行更改
重定向
代码的重定向
输入重定向
输入重定向,从键盘输入的重定向,stdin
– 0
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<string.h>int main()
{close(0);int fd = open("log.txt", O_RDONLY);char buffer[1024];while(1){char* s = fgets(buffer, sizeof(buffer), stdin);if(s == NULL) break;printf("file content:%s", buffer);}close(fd);return 0;
}
输出重定向
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<string.h>int main()
{char* str = "hello world";int fd = open("log.txt", O_WRONLY|O_CREAT|O_APPEND, 0666);dup2(fd, 1);fprintf(stdout, "file %s\n", str);close(fd);return 0;
}
追加重定向
不想利用文件描述符的分配规则,直接打开,打开之后再拷贝到1的位置,也完成了重定向
最终只剩old
dup2(fd, 1);
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<string.h>int main()
{char* str = "hello world";int fd = open("log.txt", O_WRONLY|O_CREAT|O_APPEND, 0666);dup2(fd, 1);fprintf(stdout, "file %s\n", str);close(fd);return 0;
}
指令重定向
输入重定向
没有对应的文件会创建
什么也不做的时候会被清空
输出重定向
追加重定向
修改之前的shell,让它支持重定向
缓冲区
概念
缓冲区是一块内存空间
提高使用者效率
刷新:聚集数据,一次拷贝,提高整体效率
我们一直在说的缓冲区和内核中的缓冲区没有关系,语言层面的缓冲区,C语言自带缓冲区
调用系统调用是有成本的,时间&&空间
单次调用一次系统调用就可以完成工作是效率比较高的
语言层缓冲区刷新
(1)无刷新,无缓冲
(2)行刷新 ----- 显示器,XXXX\nYY
(3)全刷新 ----- 普通文件,缓冲区写满才刷新
例外
强制刷新
进程退出,要自动刷新
内核中的缓冲区
OS自主决定
在哪里
缓冲区在大写的FILE*内部,像stdin、stdout、stderror都是一个FILE里面都有一个缓冲区
如何证明这个缓冲区的存在呢?
子进程先退出,退出时对于缓冲区需要进行刷新,刷新就是修改,修改就要写时拷贝,因此打了两遍
缓冲区还要支持格式化输入输出的实现
键盘显示器的输入和显示都是字符级的,格式化输入输出,就是把字符转换成int、string...
,或者int、string...
转换成字符级的
磁盘级文件(文件系统)
如何让系统快速定位一个文件?
文件系统,通过路径快速定位文件
例如:菜鸟驿站取快递
磁盘
磁盘的机械构成
盘片、磁头(一面一个磁头)、马达、伺服系统(识别指令并控制盘片和磁头)
磁盘的物理存储
磁道/柱面:一圈
扇区:磁道中的某一部分成为扇区,磁盘IO的基本单位,不一定是系统的
当你想要修改某一个扇区中的一个bit位,必须把整个扇区加载到内存里,修改要修改的bit位,之后再将整个扇区的信息写入。一般512字节
磁头/盘面:一个磁头一个盘面,一个盘面一个编号
访问某一个扇区:
CHS定位法
1、通过磁头定位在哪个磁道/柱面 cylinder
2、使用哪一个磁头/盘面head
3、使用哪一个扇区sector
磁盘的逻辑存储
逻辑存储方便软件的编写
LBA地址logical block address
虽然把内容进行了抽象,但是一次管理这么多还是不容易,所有分成几块,分区
大的分区再分成很多很多组
一个文件一个inode(大小128字节)
删除文件:不会删除内容,只要把inode bitmap和block bitmap使用的置为0就好了
Linux磁盘文件的特性:内容+属性
内容和属性分开存储
内容大小不确定,可能很大,可能很小
属性固定大小的
文件名不属于文件属性
ll -i //显示inode
系统中,表示一个文件吧不是用文件名,而是inode
每个inode的大小:128字节
inode Bitmap:知道inode Table的使用情况
Data blocks:数据区
Block Bitmap:块位图
Group Descriptor Table:块组描述符表
系统里面创建一个文件的步骤
在某一个块创建一个文件,首先查Inode Bitmap找一个没有被使用的inode,将该文件的相关属性写到找的那个inode在inode table里面。当对此文件进行写入数据的时候,先查Block Bitmap看看数据区的哪一块可以使用,写入到数据区相应的块。之后在该文件的inode里面的有个int block[15]
写到这里面,让文件知道自己用了哪个块
inode在整个分区内唯一
1、首先确定inode在哪个组里
1、红色部分
对于一个文件,int block只有15块吗,这么小?不是的,有直接索引、间接索引、三级索引
2、蓝色部分
GDT,Group Descriptor Table:块组描述符,对分组进行管理,描述块组属性信息
超级块(Super Block):存放整个分区情况,对分区做管理,一个文件系统对应一个super block。一个区里选n个group里面有super block,内容是一致的
每个分区可以写入相同或不同的文件系统 — super block
目录是不是文件?是。inode(inode编号)+ 目录的内容(文件名和inode的映射)
当目录没有w权限,在该目录里面就没法创建文件,没法删除文件
当目录没有r权限,就读不到文件名和inode的映射信息,也就读不到该目录下的文件信息
创建文件
(1)查询inode bitmap查找哪个inode没有用,在相应位置创建inode
(2)将inode编号和文件名的对应写在相应目录的内容里
删除一个文件
(1)从目录里面找到要删文件名对应的inode
(2)将inode bitmap和block bitmap该文件使用的位置设置为0
(3)删除该文件在相应目录内容里的inode和文件名的对应
那么访问目录的内容也是需要inode的,哪里来的
文件的增删查改和路径有关
根目录的inode,开机OS是知道的
查一个文件:在内核中,都要逆向的递归般得到/,从根目录进行路径解析
struct dentry{ }
每打开一个路径都会缓存路径,缓存成一个dentry
一个被写入文件系统的分区,要被linux使用,必须要先把这个具有文件系统的分期进行==“挂载”==
挂起:一个文件系统所对应的分区,挂载在对于目录里面
每一个分区都会挂载在一个目录下
访问一个文件,可以根据路径前缀,优先区分文件在哪一个分区下
具体的文件系统
软硬链接
原理
软连接
软链接本质:是一个文件,因为它有自己的inode
软件内容放的是目标文件的路径,类似快捷方式
硬链接
硬链接:本质不是一个独立的文件,因为它的inode编号和目标文件的一样。一定没有新建文件
是新的文件名,和目标文件inode号的映射关系,写入到指定的目录的数据库中
硬链接像重命名,引用计数
应用场景
删除一个文件
rm -f 文件名
unlink 文件名
1、软链接
当我们写了一个项目mytest
要传给其他人,不想把原始代码传给他,只传bin、conf、log、myexe
对方要运行myexe就需要./bin/myxe
这样很麻烦,因此可以用一个软连接
2、硬链接
当创建一个空文件的时候,它的硬链接数就是2,为什么?
因为他有一个.
OS不允许用户自己给目录简历硬链接,因为会形成环路问题
OS可以自己建立,eg. ..
动静态库
回顾
1、默认安装的是动态库,云服务器,静态库(c标准库)默认没有安装
2、默认编译程序,用的是动态链接的
动态库的制作和使用
为什么要有库?
提高开发效率
隐藏源代码
1、建立静态库
当没有源代码的时候,只有.o .h
就可以使用里面的方法
假设我有一些写好的方法,user
这个用户想要使用,但我不想给他我的源代码,就可以只给他.o .h
文件
但是当.o
文件很多的时候很麻烦,而且难免会有遗漏的
ar -rc libmyc.a 打包文件 打包文件
库名:myc,去掉前缀、去掉后缀
-l
:链接哪个库
-L
:连接的库在什么地方,因为系统只默认知道lib64
下的,所以这要指明
2、形成动态库并发布
转换成.o文件
fPIC :产生位置无关码
//形成.o文件
gcc -c -fPIC 文件名
动态库打包
shared:表示生产共享库格式
gcc -shared -o libxxx.so xxxx.o xxxx.o
库的名字以so结尾
但此时给使用者动态库,还是需要给动态库、.h等文件,我们可以把它们合并成一个
libmyc.so: mymath1.o mymath2.ogcc -shared -o $@ $^%.o:%.cgcc -c -fPIC $<
#mymath1.o:mymath1.c
# gcc -c -fPIC $<
#mymath2.o:mymath2.c
# gcc -c -fPIC $<.PHONY:clean
clean:rm -f *.o mylib libmyc.so.PHONY:output
output:mkdir -p mylib/includemkdir -p mylib/libcp -rf *.h mylib/includecp -rf *.so mylib/lib
其中
$<
代表依赖的对象从左到右一个一个执行
还可以吧mylib
压缩一下
不想使用-L
:把这个库拷贝到/lib64
//看可执行程序连接了那些库
ldd xxx.out
4、用户下载
下载使用很麻烦,把.h和库拷贝到默认路径下,就可以不用带了
所谓的把库(其他软件)安装到系统中,本质就是把文件拷贝到指定路径
5、卸载
不建议把自己写的不成熟的库写到默认路径里
注意
静态库只需要编译的时候使用,之后运行什么的就不需要了(只影响编译阶段)
动态库:(1)编译时搜索路径 ---- gcc(2)运行时的搜索路径 — 操作系统
对于动态库在编译的时候可以跑通,运行的时候又找不到库了
解决方法
1、把自己的库拷贝到默认路径下/lib64
,既可以支持编译、又可以支持运行
2、将不在系统默认库搜索路径下的库连接,添加到LD_LIBERERY_PATH
:系统运行程序时,动态库查找时的辅助,:为分隔符
export LD_LIBERERY_PATH=$LD_LIBRARY_PATH:具体路径
问题:重新登陆就没有了,但是在家目录里面的.bashrc
里配置
3、对库文件在默认库路径下建立同名软连接
4、配置文件:把路径放到配置文件里就可以
/etc/ld.so.conf.d
在该目录下创建配置文件
ldconfig
使配置文件生效
理解动态库加载
1、同时形成动静态库,默认动态链接
2、非要静态链接必须使用选项-static
3、如果只提供静态库,那我们的可执行程序也没办法,只能对该库进行静态链接,但是程序不一定整体是静态链接
4、如果只提供动态库,默认只能动态链接,非得静态链接,会发生连接报错
系统角度理解
库函数的调用,依旧在进程的地址空间中进行的
动态库加载之后,会映射到进程的共享区
1、谁来决定,哪些库加载了,那些没加载?OS会自动决定
2、系统中可以同时有多个库吗?可以
3、操作系统对这些库先描述再组织
编址
可执行程序加载之前,就有地址
我们进程地址空间里面很多地址数据,是从可执行程序里面来的(因为不同的可执行程序,他的数据段和代码段是不一样的,进程初始化进程地址空间每块多大的时候就是根据可执行程序)
可执行程序编制方式:
绝对编址方式—平坦模式(从0开始编址)
相对地址—逻辑编制(偏移量)
==虚拟地址空间本身不仅OS要遵守,编译器编译程序的时候,也要遵守
理解动态库动态链接和加载问题
1、进程的地址空间既然是一个数据结构对象,谁来用什么数据初始化呢?
可执行程序本身在加载到内存之前,根据自身表头的数据部分初始化进程地址空间的数据段、代码段等的begin end