Linux——基础IO
对文件操作的方式无非有两种:1打开的文件(内核,内存有关);2没有被打开的文件(与磁盘,文件系统相关)没有被打开的文件,保存在磁盘里:内容和属性各自在不同的组中通过块号来保存,形成文件自己的inode,下次通过inode编号来找到文件的所有信息:我们用系统调用open来打开文件时,需要床褥文件路径,从而在磁盘中找到文件,创建struct file,将它的属性与结构体关联起来;将它的内容拷贝到内核级
目录
一C语言层面
在c语言中,我们可以使用fopen,fread,fwrite,fclose等函数来实现对文件的操作:
1 #include<stdio.h>
2 #include<string.h>
3 int main()
4 {
5 FILE *fp = fopen("log.txt", "w");
6 if(NULL == fp)
7 {
8 perror("fopen");
9 return 1;
10 }
11 const char* message = "Hello File!\n";
12 fwrite(message,strlen(message),sizeof(char),fp);
13
14 fclose(fp);
15 return 0;
16 }
我们对文件的操作,前提是我们的代码跑起来了。文件的打开与关闭,本质上是CPU在执行代码:在代码还没有运行时,它在磁盘中保存,等待系统的分配
关于fopen里的“w”:
1如果文件不存在,就会在当前路径下,新建指定的文件
2打开文件时,如果里面有数据,要先进行清空
这跟我们在指令中echo(输出重定向)类似:
如果我们想在语言层面上取理解文件操作是不可能的!!想要深入理解就得到系统层面去理解其中的细节!!
1提炼文件理解
学习完进程的有关知识后,来提炼理解文件操作:
我们知道:我们的代码要去给CPU执行时,从磁盘加载到内存中,OS会帮助我们创建进程来管理;而我们要进行文件操作的前提是CPU有在执行我们写的代码;所以:
打开文件:本质上是进程在打开文件!! 那么进程能打开多个文件吗??可以。
通常情况下,OS内部存在着大量被打开的文件,而OS要进行管理->先描述,再组织
那这样看来:每一个被打开的文件,都应该有描述文件属性的结构体类似PCB来进行管理!(盲猜)
二系统层面
1理解文件
操作文件:本质上是进程在操作文件:那么进程与文件的关系是怎么样的呢??
文件通常是保存在磁盘中,而磁盘是外设,外设是硬件,- >文件的写入,本质是向硬件写入
而我们知道:用户是没有权力访问硬件进行写入;而OS是硬件的管理者->用户要想OS写入
要想进行写入,OS就应该要为用户提供系统接口(OS不相信任何人)
而我们在不同语言中,对文件访问的方式都不同,这实际上都是对系统调用接口的封装!!
所以我们也可以用系统调用接口来进行对文件的访问:
2系统调用接口
先来简单使用下:
1 #include <sys/types.h>
2 #include <sys/stat.h>
3 #include <fcntl.h>
4 #include <stdio.h>
5 #include <string.h>
6 #include<unistd.h>
7 int main()
8 {
9 umask(0);
10 int fd = open("log.txt",O_CREAT|O_WRONLY|O_TRUNC,0666);
11 const char* message = "Hello file!\n";
12 write(fd,message,strlen(message));
13 close(fd);
14 return 0;
15 }
a标记位传参的理解
在open的第二参数中,我们传的是系统的标记位来实现:不存在创建,在打开文件之前先清空数据;那么这个标记位应该怎么来理解呢?
下面用代码来演示:
19 #define ONE 1 // 1 0000 0001
20 #define TWO (1<<1) // 2 0000 0010
21 #define THREE (1<<2) // 4 0000 0100
22 #define FOUR (1<<3) // 8 0000 1000
23
24 void print(int flag)
25 {
26 if(flag&ONE)
27 printf("one\n"); //替换成其他功能
28 if(flag&TWO)
29 printf("two\n");
30 if(flag&THREE)
31 printf("three\n");
32 if(flag&FOUR)
33 printf("four\n");
34 }
35
36 int main()
37 {
38 print(ONE);
39 printf("\n");
40
41 print(TWO);
42 printf("\n");
43
44 print(ONE|TWO);
45 printf("\n");
46
47 print(ONE|TWO|THREE);
48 printf("\n");
49
50 print(ONE|FOUR);
51 printf("\n");
52
53 print(ONE|TWO|THREE|FOUR);
54 printf("\n");
55
56 return 0;
57 }
我们通过1在不同位置进行|运算来自由选择打印的结果,也是falg参数处理的大概过程;这本质上用到位图的思想
b文件描述符
open之后会返回应该整形值。那这个值是什么?该怎么理解呢?
首先解答第一个疑问:返回值这个我们叫做文件描述符,它究竟是什么??
我们试着创建多个文件来打印出这个值出来看看:
1 #include <sys/types.h>
2 #include <sys/stat.h>
3 #include <fcntl.h>
4 #include <stdio.h>
5 #include <string.h>
6 #include<unistd.h>
7 int main()
8 {
9 umask(0);
10 int fd1 = open("log.txt",O_CREAT|O_WRONLY|O_TRUNC,0666);
11 int fd2 = open("log.txt",O_CREAT|O_WRONLY|O_TRUNC,0666);
12 int fd3 = open("log.txt",O_CREAT|O_WRONLY|O_TRUNC,0666);
13 printf("%d\n%d\n%d\n",fd1,fd2,fd3);
14 close(fd1);
15 close(fd2);
16 close(fd3);
17 return 0;
18 }
打印出来的结果是3 4 5,为什么不是0 1 2呢?
在C语言中默认为我们打开三个流:stdin(键盘),stdout(显示器),stderr(显示器);0 1 2 会不会就是被它们所占据的?? 答案是:是的!
要想解决上面的这些问题,我们先来谈谈OS是怎么进行管理的:
文件通常是保存在磁盘中;进程要进行对文件访问时,OS会为文件创建进程(struct_file结构体存储文件属性),将文件数据拷贝到内核级缓冲区(无论你是进行读写);
在要访问文件的进程这边,OS会创建结构体(文件描述符表),其中会开辟一段file* id array【】数组来对文件进程进行管理(填文件进程地址);进程要访问那个文件进程访问文件,就在对应的位置通过映射关系来进行访问,这个访问过程可以是多个位置的访问;并且不会从下标0开始,只会从3开始,因为OS中的3个流:stdout,stdin,stderr是默认已经是打开的!!
上层(语言层)只要在表中拿到fd(下标)就可以访问文件啦!
系统在访问文件时,只让文件描述符!!
我们在想想:open接口在其中是在干什么呢?
1创建file
2开辟文件缓冲区空间,加载文件数据(延后)
3查进程的文件描述符表
4file地址,填入对应的表中
5返回下标
而我们在使用write,read函数时,它的工作不就是在文件内核缓冲区中拷贝数据吗!!
c打通上面的问题
1.fd的0,1,2为什么默认存在
0->标准输入->键盘 1->标准输出->显示器 2->标准错误->显示器
要解决这个问题,我们要先来理解:在Linux中,一切皆文件!
在前面我们说了,用户不能直接进行对硬件的访问,这个管理工作就落在OS身上;OS就将各种硬件通过创建出一个个的结构体来进行统一管理:这些结构体中,包含了基本硬件的属性和对应的操作方法(驱动层);
当上层要用到这些方法时,就只需要调用它对应的stuct file就行了。从上层的视角来看待这样硬件就变成了一切皆文件了!!
所以再过头来看看:键盘,显示器的struct file,它也要开辟对应的内核级缓冲区,将这些读方法,写方法获取的数据拷贝到缓冲区中,而不是在磁盘里;它们的struct file地址也要填入文件描述符表中来进行使用,所以它们是一定在每次进程访问文件都要有的,它们的任务太重要了!!
2理解语言层的文件操作
在C语言中,fopen返回值的类型为:FILE*,这和我们的三个流的类型是一样的:
而这个类型实际上是C语言自己提供的结构体类型,里面就对fd进行了封装!
我们在上面写的open函数的系统标记位:在c语言中也对其进行了封装
3文件描述符fd
有了上面的铺垫,我们就能够很清楚地认识到fd:本质上时文件映射关系数组的下标
4.C语言进行封装的原因
我们访问文件,可以选择系统调用,也可以是语言提供的函数;但是系统不同,接口可能不同;这会导致代码不具备跨平台性;而一个语言是否优秀,一个原因是它能够跨平台性;所以它要进行对不同系统进行语言层上的封装,产生不同的版本,在不同的平台上相同的代码能够跑通!!
三重定向
1对文件属性的操作
我们知道:文件=内容+属性,对文件的操作,不外乎:对内容的操作或者是对属性的操作;前面我们用write演示了对内容的操作,现在来了解一个对属性的操作:
其中最后一个参数是一个输出型参数的结构体
1 #include <sys/types.h>
2 #include <sys/stat.h>
3 #include <unistd.h>
4 #include <stdio.h>
5 const char* message = "log.txt";
6 int main()
7 {
8 struct stat st;
9 int a = stat(message,&st);
10 if(a<0) return 1;
11 printf("%lu\n",st.st_size);
12 return 0;
13 }
将文件的属性就很好的通过stat()来打印出来
2文件符的分配规则
直接看代码:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
int fd = open("myfile", O_RDONLY);
if(fd < 0){
perror("open");
return 1;
}
printf("fd: %d\n", fd);
close(fd);
return 0;
}
打印出来的结果或许你已经猜到了,是:3(0,1,2被占了)
接下来我们关闭0在看:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
close(0);
int fd = open("myfile", O_RDONLY);
if(fd < 0){
perror("open");
return 1;
}
printf("fd: %d\n", fd);
close(fd);
return 0;
}
这时fd的值变为了0,也就是把0(stdin)的位置给了它,为什么呢??
文件描述符的分配规则:
在files_struct数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符
在使用printf打印内容时默认将它们输出到stdout(显示器)上,而fprintf可以是指定向某个流里进行输出;我们知道:stdout的fp对应的是:1,如果将stdout给关闭到,会发生什么现象呢?
3重定向
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
int main()
{
close(1);
int fd = open("myfile", O_WRONLY|O_CREAT, 00644);
if(fd < 0){
perror("open");
return 1;
}
printf("fd: %d\n", fd);
fflush(stdout);
close(fd);
exit(0);
}
本来应该是向stdout(显示器)输出的fd:1,转而向文件里面输出了?
文件的fd竟然变成了1??
让我们通过OS层面来分析分析:
在struct file_struct中,存在文件描述符表:0,1,2位置上默认是被stdin,stdout,stderr所占据并打开的:根据规则:我们要向对文件内容进行操作,就要在空的位置的下标形成对应的fd来进行操作;
但今天我们把下标为1的fd给关了,上层(C语言)要进行文件操作,OS会为它安排一个空的最小的下标的位置(1):现在1位置指向myfile了,不在指向stdout了;
之后我们指向printf想往显示器打印出文件fd看看,但是OS只认文件描述符,它把你的这个指向当成是往1指向的myfile进行写入,从而造成结果没有在显示器打印出来反而在文件!!
这个修改下标的过程我们叫做重定向:
本质上是在内核中修改文件描述符特定下标的内容,和上层无关
3.1重定向相关的函数
dup2函数将现在的fd的内容拷贝到新的fd中来,本质上是进行重定向
4缓冲区
从系统层面上,我们知道:系统内部有个内核级缓冲区来存储文件中的数据来进行对应的处理;但我们之前在c语言中所说到的缓冲区是系统的这个缓冲区吗?
不是的!!两者是不同的概念;C语言在处理文件属性与内容是,会为我们创建stuct file的结构体来进行封装,在这个结构体里面,除了文件描述符_fileno之外,还存在着一段空间来对数据进行临时存放,这个我们叫做语言级缓冲区,我们所说的缓冲区指的就是它!
4.1分析问题
50 #include <stdio.h>
51 #include <sys/types.h>
52 #include <sys/stat.h>
53 #include <fcntl.h>
54 #include <stdlib.h>
55 int main()
56 {
57 close(1);
58 int fd = open("myfile", O_WRONLY|O_CREAT, 00644);
59 if(fd < 0){
60 perror("open");
61 return 1;
62 }
63 printf("fd: %d\n", fd);
64 //fflush(stdout);
65 close(fd);
66 return 0;
67 }
再这里,把stdout关了,open打开文件使的fd=1,fprintf将信息打印到stdout(也就是到myfile文件里写入),但这里的现象是既不在屏幕上,也不在文件里,什么原因?
原因就在于缓冲区上:
fprintf所输出的信息到语言级缓冲区中(stdout)中,在要将这些数据拷贝到内核级缓冲区中进行处理的时候,这是我们close(fp)把它关了,使得数据留在语言级缓冲区中;要想解决:
我们可以:fflush(stdout)将stdout的数据进行刷新
4.2是什么
缓冲区就是一段内存空间来进行数据的临时存储
4.3为什么
有了内核级缓冲区,为什么还要有语言级的缓冲区?
因为系统调用,是有成本的,系统要去对应的位置去找你要的数据再将这些数据给你拷贝回去太费时间了,有了这个语言级缓冲区,避免频繁调用,提高效率;提高使用者的效率
总结:给上层提供高效的IO体验,间接提高整体效率
4.4怎么办
在这里我们只关心语言级缓冲区的刷新策略:
1立即刷新 fflush(stdout)
2行刷新 主要显示器,照顾用户体验
3全缓存 不刷新等到写满才刷新,主要是普通文件
4特殊情况 缓冲区满了强制刷新,进程退出系统自动刷新
4.5见见源代码中的语言级缓冲区
每个文件都会创建对应的进程,即:每个文件都有自己的缓冲区!!
四实现myshell中加入重定向与简单封装库
在上篇Linux文章中,我们基本上实现了shell中指令,现在在来将它进行完善:
#define SkipSpace(cmd, pos) do{\
while(1){\
if(isspace(cmd[pos]))\
pos++;\
else break;\
}\
}while(0)
//检查是否是echo
void CheckRedir(char cmd[])
{
// > >> <
// "ls -a -l -n > myfile.txt"
int pos = 0;
int end = strlen(cmd);
while(pos < end)
{
if(cmd[pos] == '>')
{
if(cmd[pos+1] == '>')
{
cmd[pos++] = 0;
pos++;
redir_type = App_Redir;
SkipSpace(cmd, pos);
filename = cmd + pos;
}
else
{
cmd[pos++] = 0;
redir_type = Out_Redir;
SkipSpace(cmd, pos);
filename = cmd + pos;
}
}
else if(cmd[pos] == '<')
{
cmd[pos++] = 0;
redir_type = In_Redir;
SkipSpace(cmd, pos);
filename = cmd + pos;
}
else
{
pos++;
}
}
}
//指令是echo的处理
void ExecuteCommand()
{
pid_t id = fork();
if(id < 0) Die();
else if(id == 0)
{
//重定向设置
if(filename != NULL){
if(redir_type == In_Redir)
{
int fd = open(filename, O_RDONLY);
dup2(fd, 0);
}
else if(redir_type == Out_Redir)
{
int fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC, 0666);
dup2(fd, 1);
}
else if(redir_type == App_Redir)
{
int fd = open(filename, O_WRONLY | O_CREAT | O_APPEND, 0666);
dup2(fd, 1);
}
else
{}
}
// child
execvp(gArgv[0], gArgv);
exit(errno);
}
else
{
// fahter
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if(rid > 0)
{
lastcode = WEXITSTATUS(status);
if(lastcode != 0) printf("%s:%s:%d\n", gArgv[0], strerror(lastcode), lastcode);
}
}
}
对open,write,close的简单封装为库:->myopen,mywrite,myclose
// stdio.h
#pragma once
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#define LINE_SIZE 1024
#define FLUSH_NOW 1
#define FLUSH_LINE 2
#define FLUSH_FULL 4
struct _myFILE
{
unsigned int flags;
int fileno;
// 缓冲区
char cache[LINE_SIZE];
int cap;
int pos; // 下次写入的位置
};
typedef struct _myFILE myFILE;
myFILE* my_fopen(const char *path, const char *flag);
void my_fflush(myFILE *fp);
ssize_t my_fwrite(myFILE *fp, const char *data, int len);
void my_fclose(myFILE *fp);
// stdio.c
#include "mystdio.h"
myFILE* my_fopen(const char *path, const char *flag)
{
int flag1 = 0;
int iscreate = 0;
mode_t mode = 0666;
if(strcmp(flag, "r") == 0)
{
flag1 = (O_RDONLY);
}
else if(strcmp(flag, "w") == 0)
{
flag1 = (O_WRONLY | O_CREAT | O_TRUNC);
iscreate = 1;
}
else if(strcmp(flag, "a") == 0)
{
flag1 = (O_WRONLY | O_CREAT | O_APPEND);
iscreate = 1;
}
else
{}
int fd = 0;
if(iscreate)
fd = open(path, flag1, mode);
else
fd = open(path, flag1);
if(fd < 0) return NULL;
myFILE *fp = (myFILE*)malloc(sizeof(myFILE));
if(!fp) return NULL;
fp->fileno = fd;
fp->flags = FLUSH_LINE;
fp->cap = LINE_SIZE;
fp->pos = 0;
return fp;
}
void my_fflush(myFILE *fp)
{
write(fp->fileno, fp->cache, fp->pos);
fp->pos = 0;
}
ssize_t my_fwrite(myFILE *fp, const char *data, int len)
{
// 写入操作本质是拷贝, 如果条件允许,就刷新,否则不做刷新
memcpy(fp->cache+fp->pos, data, len); //肯定要考虑越界, 自动扩容
fp->pos += len;
if((fp->flags&FLUSH_LINE) && fp->cache[fp->pos-1] == '\n')
{
my_fflush(fp);
}
return len;
}
void my_fclose(myFILE *fp)
{
my_fflush(fp);
close(fp->fileno);
free(fp);
}
// test.c
#include "mystdio.h"
#include <string.h>
#include <stdio.h>
#include <unistd.h>
#define FILE_NAME "log.txt"
int main()
{
myFILE *fp = my_fopen(FILE_NAME, "w");
if(fp == NULL) return 1;
const char *str = "hello bit";
int cnt = 10;
char buffer[128];
while(cnt)
{
sprintf(buffer, "%s - %d", str, cnt);
my_fwrite(fp, buffer, strlen(buffer)); // strlen()+1不需要
cnt--;
sleep(1);
my_fflush(fp);
}
my_fclose(fp);
return 0;
}
五历史问题
1stderr存在的问题??
我们写的程序,本质上是对数据进行处理(计算,存储);
在这其中,数据从哪来,要去哪,用户要不要看到这个过程引出疑问??
先用代码来演示下:
#include <stdio.h>
int main()
{
fprintf(stdout, "hello fprintf stdout\n");
fprintf(stderr, "hello fprintf stderr\n");
return 0;
}
我们想stdout和stderr里去输入信息,两者本质上都是往显示器中对应文件的:
但我们将打印出的信息重定向到log.txt文件中,发现里面只有stdout的数据到里面,而stderr的数据还是输入到显示器中;
在这里我们使用的 >叫做标准输出重定向,来更改1号fd的内容,与2号fd的内容无关
如果想让stderr也重定向到文件呢?
stderror的出现就是来帮助我们进行程序出现问题时的筛选从而更好找出问题,解决问题!
六磁盘
我们用gcc编译我们写好的代码生成可执行文件;但这个文件还没有被执行时,默认时放在哪呢?
它就以二进制的形式来保存在磁盘中;
要想找到它的位置:1在磁盘中找;2文件路径+文件名
1看看物理磁盘
现在我们使用的笔记本电脑基本上都是右边的这种硬盘了;但在一些公司要建立起服务器就得用到左边这中老式磁盘——存储空间大
2磁盘的存储结构
像这种一圈一圈叠起来的类型光碟的东西我们叫做盘片
盘片:可读可写可擦除;一片有两面都可以写
在盘片中:每一面被划分为一个一个的磁道(柱面);每一面又被划分为一个一个的扇区
磁盘读写的基本单位时扇区:512个字节(4KB),1片=n个磁道 1磁道=m个扇区
每个扇区的大小是相同(不是离的远的扇区就很大)
磁盘中的盘片为什么要旋转? - > 定位扇区
磁头为什么要左右摆动? - > 定位磁道
会移动的前提是 - > 磁盘本质上是一个机械设备
了解了这些,我们来谈论一下如何找到一个指定位置的扇区?
三步走:
a.找到指定的磁头 --> Header
b.找到指定的磁道 --> Cylinder
c.找到指定的扇区 --> Sector
我们把这种方法叫做:CHS定址法
文件的大小转化为就是在磁盘里占据几个扇区的问题!!
3对磁盘的逻辑抽象
上面图片:磁带;相信大家不会陌生;它在进行工作时:将里面的磁带(记录了信息)有顺序的进行移动来进行对数据处理进而进行声音播放;
将磁道从内部拉出:看到一条很长的(不中断)的磁带原型;可以将它像数组一样来看待
类似的道理:我们可以将磁盘上的空间抽象成数组来数组来理解;下标为扇区来标记
得出:文件 = 很多个扇区的下标构成
我们这样的理解也是OS为了对磁盘更好的管理抽象出对数组的管理;如果我们想在这个数组里找文件,还是用CHS定址法吗? --> 不能直接用它,耦合度太高了!!
间接使用CHS:
假设我们得到一个index下标,想知道它具体在磁盘的那个位置
(每一片有10个磁道,1000个扇区)->每一个磁道有100个扇区
3.1计算:
index/1000 找到哪个磁头 ->H
index%1000 = tmp 表面在具体盘片所对应的位置
tmp/100 找到哪个磁道 ->C
tmp%100 找到哪个扇区 ->S
这件事情是由OS来帮助我们进行计算得出的位置的
3.2调整
如果以扇区为单位来实现数组,数组空间也未免太大了;而实际情况为:OS未来在于磁盘进行交互时,它是按8个扇区的大小来进行读的,也就是4KB:因为这是规定出来的!
所以对应的数组也就变成了以块(4KB)为单位来进行读数据;--> 文件 = 很多个以块为构成
只要知道一个块的块号,用它来进行块号*8就得到了一个块的起始下标(扇区),连续读8个下标,在把这些数据和进行计算,C,H,S就都出来了!!
一个块的第一个位置我们把它叫做LBA(逻辑块地址)
七文件系统
我们在使用ls - l的时候,出了看到文件名,还看到了文件元数据:
磁盘的空间这么大,它是怎么来管理这些数据的呢?
1分区与分组
磁盘采用分治的思想:先把总的进行分区,在每个区中在进行分组;然后写入文件系统的管理数据:本质上就是我们俗称的格式化:在磁盘中写入系统。
只要管理好一个组就能管理好每一个组;只要管理好一个分区就能管理好每一个分区(管理方法一样)
2文件系统
在每个分区中,都存在着这么一套系统来对文件(数据)进行管理,我们把它叫做文件系统:
硬盘分区被划分为一个个的block。一个block的大小是由格式化的时候确定的,并且不可以更改
我们知道:文件 = 内容 + 属性 文件在磁盘中存储,本质上:存储着文件的内容和对应的属性数据;而Linux系统特定:文件内容和文件属性分开进行存储
Block Group:ext2文件系统会根据分区的大小划分为数个Block Group。而每个Block Group都有着相同的结构组成。政府管理各区的例子
从右往左:Date blocks(数据块):只存放文件内容(每个块的大小为4kb)
Block Bitmap(块位图):Block Bitmap中记录着Data Block中哪个数据块已经被占用,哪个数据块没有被占用
inode Table(inode表) :存放文件属性 如 文件大小,所有者,最近修改时间等
1.我们在寻找文件的前提:要找到inode号(inode号是以分区为单位,dateblock也是如此)
2.每个inode都会创建出结构体(128字节)来存放文件属性:(其中不包含文件名!!)
3在其中存在着inode number(编号) 和int dateblock[N]数组来找到文件内容(相对映射:映射过去的不一定是文件内容,有可能要再次映射)
inode位图(inode Bitmap):每个bit表示一个inode是否空闲可用
Group Descriptor Table(块组描述符):描述块组属性信息Super Block(超级块):存放文件系统本身的结构信息。记录的信息主要有:bolck 和inode的总量,未使用的block和inode的数量,一个block和inode的大小,最近一次挂载的时间,最近一次写入数据的时间,最近一次检验磁盘的时间等其他文件系统的相关信息。Super Block的信息被破坏,可以说整个文件系统结构就被破坏了(不是每个分组都有的)
3文件名与inode
我们只要拿到inode,就等于拿到了文件的所有数据;但我们在之前找文件与创建文件,一直用的都是文件名啊!!这么处理两者的关系??
谈谈目录:目录 = 文件属性 + 文件内容;
而目录本身也是一个文件,也有自己的inode;
那目录内容里面放什么呢? - > 文件名与inode的映射关系!!
接着我们再来理解文件的增删查改:
1 在同一目录中不能创建同名文件;
2查找文件的顺序:先文件名;再inode编号
3目录的r权限:本质上是是否允许我们读取目录的内容 -->文件的内容
4目录的w权限:新建文件,最后一定要向当前所处的目录内容中写入
4补充
在Linux中,我们要找到指定的文件,就要文件所在的目录去找;打开此目录,根据文件名映射找到文件inode,从而找到文件,这没毛病。
但是,目录也是一个文件,也有自己的inode:在找到目录的前提是我们也要找到它的目录,它是这么做到的呢?
在内部OS会存储曾经打开过的路径结构,形成路径树;但你要访问哪个文件时,OS会为我们进行路径解析,从而找到该文件的目录来找到文件
那OS要不要对访问的路径进行管理的? --> 要!先描述后组织
一个inode在一个分区内有效:通过它来找到对应的分组,找到文件的所有内容和属性;但inode也有一个前提:你得知道你的inode在哪个分区啊!!
OS进行分区时,要先写入文件系统(格式化),再挂载到指定目录下,我们进入该目录(~),在这个目录进行文件操作;我们在Linux中进行文件操作的环境就是OS为我们创建好的一个分区中了:也就是我们已经在目录中了!!
5创建文件
将属性和数据分开存放的想法看起来很简单,但实际上是如何工作的呢?我们通过touch一个新文件来看看如何工作的:
共有4个操作:
1. 存储属性
内核先找到一个空闲的i节点(这里是263466)。内核把文件信息记录到其中。
2. 存储数据
该文件需要存储在三个磁盘块,内核找到了三个空闲块:300,500,800。将内核缓冲区的第一块数据复制到300,下一块复制到500,以此类推。
3. 记录分配情况
文件内容按顺序300,500,800存放。内核在inode上的磁盘分布区记录了上述块列表。
4. 添加文件名到目录
新的文件名abc。linux如何在当前的目录中记录这个文件?内核将入口(263466,abc)添加到目录文件。文件名和inode之间的对应关系将文件名和文件的内容及属性连接起来!
八软硬链接
1见一见
ln -s 用来创建软链接;不加-s用来创建硬链接
我们发现:文件创建硬链接的inode与文件indoe相同,且其中有个数字在创建完硬链接时从1变到2,而软链接则不会,这又代表什么意思呢?
2特征
1软链接是一个独立的文件,所以有自己的inode;连接的是目标路径的字符串:(相当于window中的快捷方式);它对应的文件如果被删除,自己也就失去了它存在的意义
2硬链接不是一个独立的文件,用的是文件的inode;
关于硬链接:硬链接就是一个文件名与inode的映射关系;建立硬链接,就是在指定目录中,添加一个与新的文件名与inode的映射关系(备份)
3在权限属性的后面有一个数字来表示硬链接数:相当于引用计数:有多少个文件字符串通过inode来指向我;但它为1时进行删除,这个文件才是真正的被删除了
所以:定位一个文件我们有两种方式:1通过路径(软链接);2直接找到目标文件的inode
在任何一个场景,新建A目录时它的引用计数一定是2;在该目录内部,新建一个目录,A目录的引用计数自动+1;判断一个A目录中有几个目录:A目录的引用计数-2
我们在构建Linux的路径结构时,我们可以使用硬链接来进行路径定位(其中只能是. ..)
我们想用自己用目录来构建路径定位,将root.hard链接到/;将来OS在要进行文件查找的时候会形成路径环绕;OS不允许我们目录创建硬链接就是我们避免路径环绕的问题
九文件操作总结
对文件操作的方式无非有两种:1打开的文件(内核,内存有关);
2没有被打开的文件(与磁盘,文件系统相关)
没有被打开的文件,保存在磁盘里:内容和属性各自在不同的组中通过块号来保存,形成文件自己的inode,下次通过inode编号来找到文件的所有信息:
我们用系统调用open来打开文件时,需要床褥文件路径,从而在磁盘中找到文件,创建struct file,将它的属性与结构体关联起来;将它的内容拷贝到内核级缓冲区中(不管是写还是读);OS在用户层面上会为用户创建进程,通过文件描述符来进行读或者写......等到一切处理完后,文件的属性与内容该修改的修改,最后将它们在重新拷贝到文件各自的分组里,如果多了就在重新开块号来进行存储,在记录用了多少个快号:更新文件的属性与内容->让用户看到具体的数据
关于文本写入与二进制写入
文本写入这个概念其实是语言层的概念,在OS中全部都是二进制写入;那么文本写入这个工作是谁来完成的呢?
我们要知道:不同的语言,对文件操作的接口都是不同的;但本质上都是对系统接口的封装;
不同语言对文件操作都又一个共性:为用户服务的;其中就要解决文本写入转换成二进制的问题
如:我们用C语言在显示器上打印一个整数1;我们看起来是1,但是实际上打印过程中将整形int转换成字符类型char,实际上这个1是字符1;类似用了int putchar()的接口来进行转换
十静态库与动态库
我们日常中使用C语言函数或者是C++中使用stl容器,我们从来没有关心它们是什么,我们只知道包含对应的头文件我们就能去调用它执行我们想要的某个过程:
但实际上你在使用例如printf/cout打印出消息时都是要去对应的语言库中找到调用方法之后才能实现的:这个过程由我们gcc/g++去库中根据你的头文件与调用函数去把它们进行链接,最终形成a.out可执行程序(==我们写的代码+语言标准库来构成)
而库中又分为静态库(.a)与动态库(.so),它们之间又有什么不同呢?
我们先从怎么办入手,在来一步一步得出是什么,为什么!
1静态库
1.1形成与使用
从两个角度:a.怎么做库(开发者角度) b.怎么用库(使用角度)来理解静态库
在学校里,老师要你们去完成文件操作与数学方法操作相关的代码:你把它们的声明与实现分开,形成各自的文件:
// mymath.h
1 #pragma once
2
3 int myAdd(int,int);
4 int mySub(int,int);
// mymath.c
1 #include"mymath.h"
2
3 int myAdd(int a,int b)
4 {
5 return a+b;
6 }
7
8 int mySub(int a,int b)
9 {
10 return a-b;
11 }
// mystdio.h
1 #pragma once
2 #include <string.h>
3 #include <stdlib.h>
4 #include <sys/types.h>
5 #include <sys/stat.h>
6 #include <fcntl.h>
7 #include <unistd.h>
8
9 #define LINE_SIZE 1024
10 #define FLUSH_NOW 1
11 #define FLUSH_LINE 2
12 #define FLUSH_FULL 4
13
14 struct _myFILE
15 {
16 unsigned int flags;
17 int fileno;
18 // 缓冲区
19 char cache[LINE_SIZE];
20 int cap;
21 int pos; // 下次写入的位置
22 };
23
24 typedef struct _myFILE myFILE;
25
26 myFILE* my_fopen(const char *path, const char *flag);
27 void my_fflush(myFILE *fp);
28 ssize_t my_fwrite(myFILE *fp, const char *data, int len);
29 void my_fclose(myFILE *fp);
// mystdio.c
1 #include "mystdio.h"
2
3 myFILE* my_fopen(const char *path, const char *flag)
4 {
5 int flag1 = 0;
6 int iscreate = 0;
7 mode_t mode = 0666;
8 if(strcmp(flag, "r") == 0)
9 {
10 flag1 = (O_RDONLY);
11 }
12 else if(strcmp(flag, "w") == 0)
13 {
14 flag1 = (O_WRONLY | O_CREAT | O_TRUNC);
15 iscreate = 1;
16 }
17 else if(strcmp(flag, "a") == 0)
18 {
19 flag1 = (O_WRONLY | O_CREAT | O_APPEND);
20 iscreate = 1;
21 }
22 else
23 {}
24
25 int fd = 0;
26 if(iscreate)
27 fd = open(path, flag1, mode);
28 else
29 fd = open(path, flag1);
30
31 if(fd < 0) return NULL;
32
33 myFILE *fp = (myFILE*)malloc(sizeof(myFILE));
34 if(!fp) return NULL;
35
36 fp->fileno = fd;
37 fp->flags = FLUSH_LINE;
38
39 fp->cap = LINE_SIZE;
40 fp->pos = 0;
41
42 return fp;
43 }
44
45 void my_fflush(myFILE *fp)
46 {
47 write(fp->fileno, fp->cache, fp->pos);
48 fp->pos = 0;
49 }
50
51 ssize_t my_fwrite(myFILE *fp, const char *data, int len)
52 {
53 // 写入操作本质是拷贝, 如果条件允许,就刷新,否则不做刷新
54 memcpy(fp->cache+fp->pos, data, len); //肯定要考虑越界, 自动扩容
55 fp->pos += len;
56
57 if((fp->flags&FLUSH_LINE) && fp->cache[fp->pos-1] == '\n')
58 {
59 my_fflush(fp);
60 }
61
62 return len;
63 }
64
65 void my_fclose(myFILE *fp)
66 {
67 my_fflush(fp);
68 close(fp->fileno);
69 free(fp);
70 }
你的舍友这时来问你作业写好没有(言外之意要你的代码):你此时很纠结:如果把代码都给他,到时有可能老师发现我们的作业是一样的,给谁分数还不一定呢!
于是你在不暴露自己的代码方法实现前提下,将它们编译成.o文件与对应的头文件一并交给了你的舍友:
老师要的只是你的实现结果演示,你就跟你的舍友说:按照.h上的方法声明去写出测试代码,到时用它与.o一起链接形成可执行程序演示给老师看看就可以了:
// test.c
1 #include"mystdio.h"
2 #include"mymath.h"
3 #include<stdio.h>
4
5
6 int main()
7 {
8 int a=10;
9 int b=20;
10 printf("%d+%d=%d\n",a,b,myAdd(a,b));
11
12 myFILE *fd=my_fopen("./my.txt","w");
13
14 const char* message="这是我们写的...";
15 my_fwrite(fd,message,strlen(message));
16 my_fclose(fd);
17
18 return 0;
19 }
在这里,头文件相当于一个手册,提供函数的声明告诉用户怎么用
.o文件提供对应的方法实现:我们在main()函数里使用头文件提供的方法,然后和.o文件进行链接,形成可执行:
于是你的舍友就完成在不暴露你的代码的同时完成老师交给的任务:
.o文件打包:
如果是形成的.o文件有很多的话,你的舍友不小心删了一两个.o文件导致程序编不过,这时你就要把所有的.o文件进行打包交给你的舍友来使用:
ar是gnu归档工具,rc表示(replace and create)
但这样是仅仅是其中的一种做法,我们还要从继续来探索:
a安装到系统
如果我想将所有代码完整进行打包:将头文件与.a文件放在不同的目录里,打包放在同一个目录(lib)中:
a.将lib目录上的mylib与mystdio安装在系统目录上进行系统级别进行使用:
(写代码用<>来包含头文件):(此时的lib存不存在没有影响了)
用gcc编译时 用-l告诉gcc我们要用到这个库(这个库的名字要去前缀去后缀)
但这种做法非常不推荐,会’污染‘系统的文件(系统的库与你自己写不是同个级别的!!)
b指定路径
如果不想安装到系统目录上,你也可以在gcc编译的时候指定路径去进行编译:
~-~ 那库文件要进行-lmyc指定,而头文件却不用? ~-~
因为在test.c中#include<mymath.h>中已经帮我们进行指定了!!
而如果不要带 -I头文件路径指定,我们就在test.c中这样指定:#include"./mylib/include/mymath.h"-->去当前目录下找头文件
1.2本质
所谓的库;本质是把.o文件打包:有了库,能够提高开发效率
2动态库
在上面我们说到:指明路径让gcc编译形成可执行程序时我们告诉了它要链接的库是我们之间写的静态库,但我们用ldd查的时候,我们的可执行程序链接的是系统中的动态库,这是我们为什么?
即使有静态库,程序也要链接一个动态库;但是找不到,gcc就把你的静态库拷贝一份到动态库中,才来进行编译;
而我们在开发时,用到的大部分库也基本上是动态库占的较多:现在让我们来见识见识动态库是如如何形成的吧!
2.1形成与使用
-fPIC:产生位置无关码(position independent code)
-shared: 表示生成共享库格式
动态库命名;xxx.so
我们进行gcc 指定路径去编译:
这时编译时通过了,但执行a.out的时候它告诉我们找不到动态库??
因为动态库要在程序运行时候,要找到动态库加载并执行
而静态库在编译期间,已经将静态库中的代码拷贝到我们的可执行程序内部了,加载就和静态库没关系了
解决方法:
a安装到系统
将libmyc.so安装到系统中,执行a.out时就成功了;但还是不建议这样做!!
b.建立同名软链接
在lib64下建立与libmyc.so同名的软连接也是可以的:
c添加环境变量
添加libmyc.so的绝对路径到:LD_LIBRARY_PATH
但下次登陆时环境变量就又变回原来了
d修改 ~/.bashrc的配置文件
让环境变量永久生效:
e新增动态库搜索路径
进入 /etc/ld.so.cof.d,在该目录里添加一个文件(里面写动态库的绝对路径) ;
在执行指令:ldconfig就完成了
这个工作只能由root来做!!
2.2本质
所谓的动态库,本质:将.o文件打包(选项加上 -fPIC);也是为了要提高开发效率!!
3静态库VS动态库
编译器 默认链接的是动态库
如果没有使用-static,并且只提供.a,只能静态连接当前的.a库,其它库正常链接
那-static的意义是什么呢?
将我们程序静态链接,这就要求我们链接的库都必须提供对应的静态库版本!!
而我们的linux的gcc/g++只提供动态库,要静态链接就得自己去下载才能静态链接成功
十一动态库加载
1认识整体轮廓:
可执行程序与库在没有加载到内存之前以文件的形式保存在磁盘中;但要./运行它时,OS在磁盘中找到对应的文件后将它加载在内存中,通过页表映射到虚拟地址空间(本质是mm_struct对象):CPU执行时根据虚拟地址空间与页表映射找到对应在内存中的位置后去执行;而动态库的加载也是如此。
如果再来一个进程也想用同一个动态库,只需将已经加载到内存的库再进行映射即可!
问题:我们可执行程序,在编译成功时,如果没有加载运行,二进制代码中有地址吗??
让我们写段代码,把它通过反汇编来看看:
ELF格式的可执行程序,二进制都是有自己的格式的:包含ELF可执行程序的头部,可执行程序的属性
编译器将我们代码中的执行语句转化成汇编指令:而其中是包含了地址的;那么这个地址是什么地址呢?
这些地址不是内存上的物理地址,而是虚拟地址!!严格上来说:没有加载到内存时这些地址是逻辑地址
那它是如何进行编址的呢?
采用全0到全F的编址方式:称为平坦模式;而上面的也是按这种方法来编址的;编址也要看地址种类来进行的,这里就不展开叙述了
ELF的可执行程序中,
有加载器来获取各个区域的起始代码与结束代码和main函数的入口地址
2具体分析
在前面的学习中,我们知道:进程=内核数据结构+代码和数据;但在OS中,是要先有内核数据结构还是代码和数据呢?
这就有点像你考上某所大学时是你的档案数据先到学校还是要你人先到学校一样:只有是该学校收到了你的志愿信息,在大学的系统中根据你的档案来先注册,再然后等开学是等你来进行确定;
1.创建进程阶段,根据程序的地址和加载器来对mm_struct进行初始化(mm_struct对象中的变量:正文区的起始地址与结束地址;初始化数据区的起始地址与结束地址......)
而要正确无误的完成初始化工作,OS,编译器,加载器要同时都有虚拟地址这个概念!!
2.可执行程序加载到内存后,在每个汇编语句中都有与之对应的物理地址,地址空间在这之前已经初始化完成后,就来开始进行物理地址与虚拟地址之间通过页表进行映射
3.在CPU中有一个PC指针(告诉CPU该执行哪个汇编语句),通过地址空间与加载器找到main函数的入口地址,通过页表进行映射找到内存中的汇编语句进行执行;执行到调用动态库的myAdd函数汇编语句时找不到(此时动态库还没加载进来),会发生缺页中断:把动态库也加载到内存中,映射到页表的共享区中;CPU调用函数时就可以根据库在地址空间中的起始地址+myAdd函数的地址(偏移量)来进行跳转去执行(库被映射到什么位置就不重要了)
进行库的调用,其实只是在我们地址空间中进行来回跳转而已!!
这便是一个可执行程序被CPU执行的大概过程。
那在这里会有一个问题:动态库有没有被加载这件事怎么办??
CPU执行时进程不一定是只有一个,那么如果不同进程要用到同个库,是根据什么条件来判断库就在内存中不用再重新进行加载呢?
进程有多个,库就不一定只有一个库:问题就转化为OS要怎么进行对这些库进行管理?-->先描述,再组织!创建结构体填入库的各种基本属性与是否加载到内存中,将结构体以类似链表的形式进行连接;管理这些库就转化成对链表的增删查改!
进程要用到其中一个动态库,先遍历这个链表一遍,看有没有加载到内存,没有就加载,有就之间找到库的地址进行映射就能进行调用了!!
以上便是我们在基础IO内容中总结的相关内容,希望对你有所帮助。
有问题欢迎在评论区进行指正,谢谢!!
更多推荐
所有评论(0)