一.C文件IO相关操作
只有文件名但不带路径的话,默认在当前路径下打开文件(以写的方式打开文件若文件不存在会自动创建文件),那到底什么是当前路径呢?
// test目录下 myproc.内容
#include<stdio.h>
int main()
{
FILE* fp = fopen("test.txt","w");
if(fp == NULL)
{
perror("fopen");
return 1;
}
fclose(fp);
}
可以看出,当前路径和可执行程序所处的路径没有关系,在哪个路径下运行起来可执行程序,该路径就被称为当前路径
为了让大家看的更清楚,稍微修改一下代码,关闭文件后不让进程终止来查看进程的相关信息
// test目录下 myproc.内容
#include<stdio.h>
int main()
{
FILE* fp = fopen("test.txt","w");
if(fp == NULL)
{
perror("fopen");
return 1;
}
fclose(fp);
sleep(10000);
}
cwd(current work directory) : 在哪个路径下运行起来可执行程序,进程创建的临时文件都在当前路径下创建
exe : 可执行程序路径
打开文件,读写文件,关闭文件都是进程运行的时候完成的
#include<stdio.h>
int main()
{
FILE* fp = fopen("test.txt","r");
if(fp == NULL)
{
perror("fopen");
return 1;
}
// 二.
int count = 5;
char buffer[64];
while(count)
{
// 以\n为分隔符,一次读取一行内容
fgets(buffer,sizeof buffer,fp);
printf("%s",buffer);
count--;
}
// 一.
// int ct = 5;
// while(ct)
// {
// fputs("hello bit\n",fp);
// ct--;
// }
fclose(fp);
}
在Linux中,一切皆文件,键盘和显示器也可以将其当作文件,那么为什么在C语言当中我们没有打开显示器文件就可以直接使用printf函数进行写入呢?为什么没有打开键盘文件就可以直接使用scanf函数进行读取呢?,原因如下 :
任何进程在运行的时候,默认打开三个输入输出流
C默认会打开三个输入输出流,分别是stdin, stdout, stderr
仔细观察发现,这三个流的类型都是FILE*,(fopen返回值类型,文件指针),FILE* 是C语言的概念,文件描述符是系统级别的概念
#include<stdio.h>
int main()
{
int count = 5;
char buffer[64];
while(count)
{
fgets(buffer,sizeof buffer,stdin);
printf("%s",buffer);
count--;
}
int ct = 5;
while(ct)
{
fputs("hello world\n",stdout); // strerr
ct--;
}
}
由"w" 和 “a” 可以联想到之前讲到的重定向(>)和追加重定向(>>)
test.txt已经存在5行hello world,使用"a"方式打开文件,向文件末尾追加5行hello lyp
#include<stdio.h>
int main()
{
FILE* fp = fopen("test.txt","a");
if(fp == NULL)
{
perror("fopen");
return -1;
}
int count = 5;
while(count)
{
fputs("hello lyp\n",fp);
count--;
}
fclose(fp);
}
text.txt中已经存在5行hello world,5行hello lyp,使用"w"方式打开文件,清空原始内容,写入5行hehe
#include<stdio.h>
int main()
{
FILE* fp = fopen("test.txt","w");
if(fp == NULL)
{
perror("fopen");
return -1;
}
int count = 5;
while(count)
{
fputs("hehe\n",fp);
count--;
}
fclose(fp);
}
二.系统文件IO
open系统调用接口介绍
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
pathname: 要打开或创建的目标文件
flags: 打开文件时,可以传入多个参数选项,用下面的一个或者多个常量进行“或”运算,构成flags。
参数:
O_RDONLY: 只读打开
O_WRONLY: 只写打开
O_RDWR : 读,写打开
这三个常量,必须指定一个且只能指定一个
O_CREAT : 若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限
O_APPEND: 追加写
mode:
设置文件权限
返回值:
成功:新打开的文件描述符
失败:-1
flags : 系统函数参数传参标志位
传递给flags的这些常量都是宏,通过如下命令可以查看这些宏的定义,可以发现这些宏的二进制序列只有一个1,这些常量进行或运算传递给flags,open函数内部,再通过与运算判断传递了哪些常量
int open(const char *pathname, int flags, mode_t mode)
{
if(flags & X)
{}
if(flags & Y)
{}
}
open(argu1,X | Y,argu3);
grep -E 'O_CREAT|O_RDONLY|O_WRONLY' /usr/include/ -R
下面我们就来使用一下open
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
int main()
{
umask(0);
// O_WRONLY|O_CREAT 等价于 fopen 中的"w"
int fd = open("test.txt",O_WRONLY|O_CREAT,0666);
printf("%d\n",fd);
}
我们想以只写的方式打开test.txt文件,若test.txt文件不存在,会创建一个test.txt文件出来,权限为666
但结果权限为664(默认umask为2),所以我们可以把umask设置为0
#include<stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
umask(0);
int fd1 = open("test.txt",O_WRONLY|O_CREAT,0666);
printf("%d\n",fd1);
int fd2 = open("test.txt",O_WRONLY|O_CREAT,0666);
printf("%d\n",fd2);
int fd3 = open("test.txt",O_WRONLY|O_CREAT,0666);
printf("%d\n",fd3);
int fd4 = open("test.txt",O_WRONLY|O_CREAT,0666);
printf("%d\n",fd4);
int fd5 = open("test.txt",O_WRONLY|O_CREAT,0666);
printf("%d\n",fd5);
}
通过运行结果发现,文件描述符从3开始,这是因为默认打开了标准输入(0),标准输出(1),标准错误(2),所谓的文件描述符是数组下标
write
ssize_t write(int fd, const void *buf, size_t count);
fd : 文件描述符,buf : 写的内容 ,count : 期望写入的字节数
ssize_t : 实际写入的字节数
实际写入的字节数 <= 期望写入的字节数(ssize_t <= count)
// test.txt为空,向test.txt写入5行hello world
#include<stdio.h>
#include<string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
umask(0);
int fd1 = open("test.txt",O_WRONLY|O_CREAT,0666);
printf("%d\n",fd1);
int count = 5;
const char* buf = "hello world\n";
while(count)
{
write(fd1,buf,strlen(buf));
count--;
}
}
read
ssize_t read(int fd, void *buf, size_t count);
fd : 文件描述符,buf : 读取的数据存放的地址 ,count : 期望所读的字节数
ssize_t : 实际所读的字节数
实际所读字节数 <= 期望所读字节数(ssize_t <= count)
// test.txt中有5行hello world,从test.txt中读取字符写入到标准输出中
#include<stdio.h>
#include<string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
umask(0);
int fd1 = open("test.txt",O_RDONLY);
printf("%d\n",fd1);
char c;
while(1)
{
ssize_t s = read(fd1,&c,1);
if(s <= 0)
{
break;
}
write(1,&c,1);
}
}
C语言提供的IO接口底层都封装了系统调用接口
为什么要进行封装 ?
(1).保证可读性以及调用简单
(2).保证跨平台性 : 我们可以发现C语言可以在任一平台上跑,原因在于C语言在设计时,与系统强相关的不可跨平台的接口都被C语言封装了一遍(我们上面用C语言所写的fopen(),fclose()等函数底层都封装了open,close等系统调用)
open函数返回值
在认识返回值之前,先来认识一下两个概念: 系统调用 和 库函数
上面的 fopen fclose fread fwrite fgets fputs都是C标准库当中的函数,我们称之为库函数(libc)。
而 open close read write lseek 都属于系统提供的接口,称之为系统调用接口
回忆一下我们讲操作系统概念时,画的一张图
文件描述符 fd
磁盘文件 vs 内存文件
内存文件(磁盘文件加载而来) : 一个进程可以打开多个文件,在系统中,任何时刻,都可能存在大量的已经打开的文件,对这些文件进行管理依然遵循先描述再组织,struct_file就是描述文件的结构体,以双向链表的方式将其组织起来,对文件的管理就变成了对双链表的增删查改,在内存中有task_struct,struct_file两条双链表,我们想要知道这些被打开的文件哪些属于某一个进程,我们就需要建立进程和文件的对应关系
磁盘文件 : 存在磁盘上的文件并不是只保存了内容,还保存了文件的属性/元信息(文件的名字,创建日期,大小)
文件 = 内容 + 属性
我们将文件打开时,是有两份的,一份在硬盘上,一份在内存里(内存文件更多的是文件的属性信息),当我们想要读写数据时,再延后式的慢慢加载数据到内存(这里会有缓冲区的概念)
// 人和操作系统唯一的交互方式就是进程,而进程和操作系统的交互方式是系统调用接口
open系统调用打开文件的过程
(1). 进程打开一个文件,内存中创建struct_file结构体,存储打开文件的属性信息
(2). 操作系统为该文件分配文件描述符(fd_array数组中未被分配的且最小的下标)
(3). 将struct_file结构体的地址填到fd_array数组对应的位置,返回该文件的文件描述符
由此可知read,write等系统调用为什么要传入fd ?,因为拿到fd可以找到文件对应的struct_file结构体,得到文件的相关信息
重定向
#include<stdio.h>
#include<string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
umask(0);
int fd = open("test.txt",O_WRONLY|O_CREAT,0666);
if(fd < 0)
{
return 1;
}
write(1,"hehe\n",5);
write(1,"hehe\n",5);
write(1,"hehe\n",5);
write(1,"hehe\n",5);
write(1,"hehe\n",5);
close(fd);
}
#include<stdio.h>
#include<string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
umask(0);
close(1);
int fd = open("test.txt",O_WRONLY|O_CREAT,0666);
if(fd < 0)
{
return 1;
}
write(fd,"hehe\n",5);
write(fd,"hehe\n",5);
write(fd,"hehe\n",5);
write(fd,"hehe\n",5);
write(fd,"hehe\n",5);
close(fd);
}
对比这两段代码,我们会发现关闭显示器文件后,fd的值为1,即test.txt文件的文件描述符为1,达到的效果为原本要写入到显示器当中的内容写入到了test.txt中,这种现象叫做输出重定向(>)
重定向的本质 : 修改文件描述符对应的struct_file*的指向
// 输出重定向
cat > test.txt
#include<stdio.h>
#include<string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
umask(0);
close(1);
int fd = open("test.txt",O_WRONLY|O_CREAT,0666);
if(fd < 0)
{
return 1;
}
printf("hello world\n");
fprintf(stdout,"hello fprintf\n");
fputs("hello fputs %d %d %f %c\n",stdout);
// 需要刷新缓冲区
fflush(stdout);
close(fd);
}
解释这段代码前先进行一下知识的铺垫
实际上,C语言的 fopen 函数打开文件主要做了以下三件事情
(1). 创建FILE结构体
(2). 底层调用open函数打开文件,返回fd,将fd填充到FILE结构体当中的 _fileno(封装的文件描述符)
(3). 返回FILE结构体的地址(FILE*)
所以我们C语言中平常所使用的fread,fwrite,fgets,fputs中拿到我们传递的参数stream(FILE* stream),根据stream指针找到struct FILE结构体,struct FILE结构体中封装了文件的fd,拿到fd后,去task_struct->files_struct->fd_array数组中得到文件对应的struct_file结构体,由此得到文件信息进行读取或写入
之前也提到过,任何一个进程,默认会打开3个文件 : 标准输入(键盘文件),标准输出(显示器文件),标准错误(显示器文件),在C语言中创建对应的FILE结构体,调用open函数打开文件,用返回值fd填充 _fileno,返回FILE结构体的地址给stdin,stdout,stderr(FILE*指针)
再来看这段代码,我们首先关闭了1号文件描述符,open打开文件后,1号文件描述符被分配给了test.txt文件,代码中 printf/fprintf/fputs 参数stream都为stdout,stdout->struct FILE结构体->fd->struct_file结构体,此时struct_file结构体对应test.txt文件,因此内容写入到了test.txt文件当中
// 输入重定向
cat < test.txt
#include<stdio.h>
#include<string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
umask(0);
close(0);
int fd = open("test.txt",O_RDONLY);
if(fd < 0)
{
perror("open");
return 1;
}
char buffer[50];
fgets(buffer,50,stdin);
printf("%s\n",buffer);
close(fd);
}
// 追加重定向
cat >> test.txt
#include<stdio.h>
#include<string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
umask(0);
close(1);
int fd = open("test.txt",O_WRONLY|O_APPEND); // 等价于C语言 fopen 中的"a"选项
if(fd < 0)
{
perror("open");
return 1;
}
printf("hello world\n");
fprintf(stdout,"hello fprintf\n");
fputs("hello fputs %d %d %f %c\n",stdout);
fflush(stdout);
}
凡是显示到显示器上面的内容,都是字符
int a = 1024; printf(“%d”,a);
printf在格式化输出时,将1024转换成字符写入到显示器文件
凡是从键盘读取的内容,都是字符
int a; scanf(“%d”,&a);
scanf在格式化输入的时候,将读到的字符根据ascii码转换成整形写入a中
所以,键盘和显示器一般被称之为"字符设备"
缓冲区
(1). 无缓冲
(2). 行缓冲(常见的对显示器进行刷新数据时)
四种刷新方式 : 碰到\n刷新,强制刷新,程序结束刷新,缓冲区满了刷新
(3). 全缓冲(对磁盘文件写入的时候采用全缓冲)
三种刷新方式 : 强制刷新,程序结束刷新,缓冲区满了刷新
我们向显示器,磁盘文件去写入的时候效率是很低的,所以我们把数据积累到内存缓冲区中,定期去刷新,理论上,全缓冲效率最高,无缓冲效率最低
为什么对磁盘文件写入的时候采用全缓冲,对显示器文件写入采用行缓冲 ?
磁盘文件在写入的时候人是不会立即去读的,所以采用全缓冲效率高
显示器文件在写入的时候人是要立即去读的,采用无缓冲的方式可以达到目的,但效率太低,采用全缓冲的方式人无法立即读到信息,因此采用行缓冲在效率和可用性上达到平衡
(1). 这个缓冲区在哪里?
struct FILE结构体里有维护用户缓冲区相关的字段(看一下FILE的结构?)
(2). 这个缓冲区是谁提供的?
C语言自带的缓冲区,每个进程都有自己的缓冲区(进程缓冲区)
(3). OS也是有缓冲的
OS也有自己的缓冲区(内核缓冲区),刷新进程缓冲区的内容并不会将内容直接刷新到文件中,而是刷新到内核缓冲区,由OS定期刷新到文件中(操作系统有自己的刷新机制)
#include<stdio.h>
#include<string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
// C库函数
printf("hello printf\n");
fprintf(stdout,"hello fprintf\n");
// system
const char* msg = "hello write\n";
write(1,msg,strlen(msg));
fork();
}
该程序重定向和非重定向的结果是不一样的
通过运行结果我们可以看出以下两个结果
(1). 重定向还是非重定向会更改进程的缓冲方式
(2). C接口打了两次,系统调用接口打了1次
下面我们来分析一下运行结果
C语言自带缓冲区,系统调用没有缓冲区
(1). 使用printf/fprintf向显示器文件中写入时,采取的是行刷新策略,执行完printf/fprintf后,“hello printf\n” "hello fprintf\n"已经被刷新到了显示器文件当中,write无缓冲,直接写到显示器文件当中,fork()后创建子进程,由于缓冲区中没有数据需要刷新,因此显示器中显示为3行数据
(2). 使用printf/fprintf向文件中写入时,采取的是全缓冲策略,执行完printf/fprintf后,“hello printf\n” "hello fprintf\n"被打印到缓冲区中,write无缓冲,直接写到显示器文件当中,fork()后创建子进程,父进程退出要刷新缓冲区,但因为父子进程具有独立性,改变父进程缓冲区不能影响子进程缓冲区,所以发生写时拷贝,然后子进程再刷新缓冲区
解释一下上面输出重定向的代码最后的fflush(stdout),以输出重定向为例,如果删掉fflush(stdout),最终test.txt中无内容,原因向文件写入采用全缓冲策略,不将缓冲区内容刷新到文件当中,文件中当然没内容,可能这里有人会有疑问,进程结束之后,会自动刷新缓冲区啊,这当然没错,但我们在最后close(fd),将文件关闭了,操作系统没办法把内容刷新到文件中了(可以使用fclose(stdout))
(1). fflush(stdout);
close(fd);
(2). fclose(stdout);
(3). 什么都不做,进程退出自动刷新缓冲区内容
stdout和stderr虽然都可以向显示器文件进行写入,但它们的struct_file结构体是不一样的,可以认为打开同一文件,只是对应的结构体不一样,通过下面代码可以进行验证
#include<stdio.h>
#include<string.h>
int main()
{
printf("hello printf\n");
perror("printf");
fprintf(stderr,"hello fprintf\n");
}
非重定向之前,stdout和strerr都可以向显示器文件写入,但重定向后,stdout无法向显示器写入,stderr仍然可以向显示器写入
如何理解Linux下一切皆文件?
从我们的直觉上来看,磁盘,显示器,网卡,键盘很明显不是文件,系统和这些外设进行沟通只有两种方式I(input)和O(output),内存中存在进程,进程从外设读到信息我们叫做read,向外设写入信息我们叫做write,但是很明显只能向显示器写入(不能读取,read方法为空),只能从键盘中读取(不能写入,write方法为空),即所有的外设硬件都有自己的read,write方法,这些方法绝对不一样,那能不能用同一种方法去处理所有的设备呢?可以的!
在struct file结构体中,存在着函数指针,将来不同的文件打开时,就让函数指针指向不同文件各自对应的read,write方法,换言之,当上层要读写文件时,不需要关心底层打开的是什么文件,只需要调用函数指针所指向的方法即可。所以,在上层读写文件时,我们只需要调用read,write方法就可以完成统一的读写。这种技术在C++中叫做多态
至此,我们站在应用层就可以认为Linux一切皆文件
内核源代码 :
在task_struct中存在 files指针指向 struct files_struct 结构体
在 struct files_struct 结构体中,存在struct file* fd_array[NR_OPEN_DEFAULT]数组,存储 struct file*
NR_OPEN_DEFAULT默认为32,一般可以扩展到1024
在struct file 结构体中,存储打开文件的各种信息,其中上文提到的struct file中的函数指针存在 struct file_operations结构体中
使用dup2系统调用
#include<unistd.h>
int dup2(int oldfd, int newfd);
dup2函数的作用是将oldfd中的struct file* 指针赋值给newfd中的struct file* 指针,我们可以使用dup2完成输出重定向
#include<stdio.h>
#include<string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
umask(0);
int fd = open("test.txt",O_WRONLY|O_CREAT,0666);
if(fd < 0)
{
perror("open");
return 1;
}
dup2(fd,1);
printf("hello printf\n");
fprintf(stdout,"hello fprintf\n");
fputs("hello fputs\n",stdout);
return 0;
}
给简易shell中增加重定向功能
(1). fork()创建出的子进程会拷贝父进程的struct files_struct 结构体,即父子进程共用同一份文件(写时拷贝 ?)
(2). 进程程序替换不会影响文件的打开和关闭
#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<sys/wait.h>
#include<string.h>
#define LEN 1024
#define NUM 32
int main()
{
int type = 0;
char cmd[LEN];
char* argv[NUM];
while(1)
{
printf("[luanyiping@bogon shell]$");
// 从标准输入中读取指令到cmd中
fgets(cmd,LEN,stdin);
char* start = cmd;
while(*start != '\0')
{
if(*start == '>')
{
*start = '\0';
start++;
if(*start == '>')
{
type = 1;
*start = '\0';
start++;
break;
}
break;
}
if(*start == '<')
{
type = 2;
*start = '\0';
start++;
break;
}
start++;
}
if(*start != '\0')
{
while(isspace(*start))
start++;
}
else
{
start = NULL;
}
// 将最后的'\n'变成'\0'
cmd[strlen(cmd) - 1] = '\0';
// 以空格为分隔符分割指令
argv[0] = strtok(cmd," ");
int i = 1;
while(argv[i] != strtok(NULL," "))
{
i++;
}
// 创建子进程去完成任务
pid_t id = fork();
if(id == 0)
{
if(start != NULL)
{
if(type == 0)
{
int fd = open(start,O_WRONLY|O_CREAT,0666);
if(fd < 0)
{
perror("open");
exit(2);
}
dup2(fd,1);
}
else if(type == 1)
{
int fd = open(start,O_WRONLY|O_APPEND);
if(fd < 0)
{
perror("open");
exit(3);
}
dup2(fd,1);
}
else if(type == 2)
{
int fd = open(start,O_RDONLY);
if(fd < 0)
{
perror("open");
exit(4);
}
dup2(fd,0);
}
}
// 进程程序替换
execvp(argv[0],argv);
exit(0);
}
// 阻塞式等待子进程退出
int status = 0;
pid_t ret = waitpid(id,&status,0);
if(ret > 0)
{
printf("child exit code : %d\n",WEXITSTATUS(status));
}
}
}
理解文件系统
查看一个文件的inode编号,可以使用命令 ls -i
上面提到过,磁盘上的文件由内容 + 属性(元信息)构成,所以一个空文件也会占用磁盘的空间
Linux将文件的属性和内容进行分离存储,内容直接在磁盘存储,属性存储在inode中(inode在磁盘中)
inode是任何一个文件的属性集合,Linux中几乎每一个文件都有一个inode,可能存在大量的inode,为了区分inode,用inode编号
(1). 什么是磁盘?
磁盘 : 一种永久性存储介质,笔记本电脑一般装的是ssd(固态硬盘),磁盘几乎是所有硬件设备中唯一的机械设备,目前所有的普通文件都是在磁盘中存储的
首先看下磁盘的整体结构
我们来看下盘片结构,图中的一圈圈灰色同心圆称之为一条条磁道,最外面称为0磁道,往圆心依次加1,即1磁道,2磁道…,从圆心向外画直线,可以将磁道划分成若干个弧段,每个磁道上的一个弧段称为一个扇区(图中绿色部分),扇区是磁盘的最小组成单元,通常为512字节,而我们计算机存储的所有数据,就放在扇区上,磁头划过扇区时根据电流信号的高低,完成数据的0,1转换,这也是计算机唯一能够识别的数字,从而完成数据的读取和写入
这里有几个专有名词
磁头 : 磁头和盘面数是相等的,即每个盘片有2个盘面,2个磁头
磁道 : 盘片中每一个同心圆称为一个磁道
柱面: 所有盘面的不同同心圆组成不同的柱面(数据的读写和磁盘分区都是都是按柱面进行的)
扇区: 每个磁道上的一个弧段为一个扇区
寻道时间: 磁头从开始移动到数据所在磁道所需的时间
旋转延迟: 盘片旋转将请求数据所在扇区移至读写磁头下方所需时间,旋转延迟取决于磁盘转速
为什么数据的读写和磁盘分区都是按柱面进行的?
数据的读写按柱面进行,即磁头读写数据时,首先在同一柱面内从"0"磁头开始进行操作,依次向下在同一柱面的不同盘面上进行操作,只有同一柱面的所有磁头读写完毕后磁头才转移到下一柱面,因为选取磁头只需通过电子切换即可,选取柱面需要机械切换,电子切换相当快,比在机械上磁头向邻近磁道移动快得多,所以数据的读写按柱面进行,而不是按盘面进行,也就是说,一个磁道写满数据后,就在同一柱面的下一盘面来写,一个柱面写满后,才移动到下一柱面的扇区开始写数据,读数据也按这种方式进行,这样就提高了硬盘的读写效率
我们可以把磁盘的圆形存储介质看作线性存储介质,当磁盘很大时,为了方便管理,先将磁盘进行分区,再进行格式化
格式化 : 将管理信息写到每一个分区里,其中管理信息是由文件系统决定的
磁盘分区以后,每个分区可能仍然较大,不好管理,所以每个分区被划分成一个个block,一个block的大小是由格式化的时候确定的,并且不可以更改。例如mke2fs的-b选项可以设定block大小为1024、2048或4096字节。而上图中启动块(Boot Block)的大小是确定的。
分区格式化之后,inode的个数以及block的大小就已经确定了
当我们系统在启动的时候,主板上有一个设备bios,内部有一段存储区域,帮我们找到磁盘的最开始的分区表,根据分区表找到某个分区的最开始有一部分代码(Boot Block)能够帮我们找到操作系统的代码
Block Group:ext2文件系统会根据分区的大小划分为数个Block Group。而每个Block Group都有着相
同的结构组成。
Super Block(整个分区inode和block的使用情况,不一定所有块组都会有,会存在冗余) :存放文件系统本身的结构信息。记录的信息主要有:block 和 inode的总量,未使用的block和inode的数量,一个block和inode的大小,最近一次挂载的时间,最近一次写入数据的时间,最近一次检验磁盘的时间等其他文件系统的相关信息。Super Block的信息被破坏,可以说整个文件系统结构就被破坏了
块位图(Block Bitmap):Block Bitmap中记录着Data Block中哪个数据块已经被占用,哪个数据块没
有被占用,每个bit表示一个block是否空闲可用(如第1个比特位为1,表示第一个block已经被使用,为0表示第一个block未被使用)。
inode Bitmap : 每个bit表示一个inode是否空闲可用。(如第1个比特位为1,表示第一个inode已经被使用,为0表示第一个inode未被使用)
inode Table : 存放inode
Data blocks :存放文件内容
文件 = inode + 内容,inode在Linux中一般占128字节或256字节,inode会被存储到 inode Table中,inode是一个结构体,存储文件的各种属性信息(文件的大小,权限,拥有者,所属组)
文件的内容存储到 Data blocks 中,Data blocks 由一个个块(4KB)组成,文件的内容填到一个个块中,文件的inode中有一个记录文件所使用块的编号的数组,遍历该数组就能得到文件的内容
Super Block 存储 inode 和 block 的使用信息,哪些被使用,哪些没被使用
总结 :
(1). 请描述一下,创建一个文件的过程以及写入1kb数据的过程
1). 遍历inode Bitmap找到第一个未被使用的inode编号,并将该比特位置为1(表明该inode已经被使用)
2). 将各种属性信息填入 inode Table 对应编号的inode结构体中
3). 遍历Block Bitmap找到第一个未被使用的block块,并将该比特位置为1(表明该block已经被使用), 将该block块的编号填入blocks数组,向该block块中写入数据
4). 将文件名和对应文件inode号加入到当前目录的内容中
(2). 删除一个文件,在做什么?
1).将 inode Bitmap 中对应文件inode编号的比特位置为0
2). 将 Block Bitmap 中文件所占用的block编号对应的比特位置为0
(3). 拷贝文件慢,删除文件快,删除文件后,是可以恢复的, 如果误删了文件,最好的做法是什么都不做,防止之前数据被覆盖掉
inode
{
inode ID;
直接块指针
间接块指针
双重间接块指针
三重间接块指针
int ref // 引用计数记录硬链接数
// 一些属性信息
…
}
(4). 如何理解目录?目录创建的过程?
目录也是文件
1). 遍历inode Bitmap找到第一个未被使用的inode编号,并将该比特位置为1(表明该inode已经被使用)
2). 将目录的各种属性信息填入 inode Table 对应编号的inode结构体中
3). 在目录下创建文件时,创建文件的过程和上面描述是一致的
4). 遍历Block Bitmap找到第一个未被使用的block块,并将该比特位置为1(表明该block已经被使用), 将该block块的编号填入blocks数组,向该block块中写入数据,写入的数据是文件名和文件对应的inode号
注意 :
目录的内容是文件名和文件名对应的inode号
inode中并不存储文件名,文件名存储在当前目录的内容中
(5). ls ls -l cat 这些命令在做什么呢?
ls :
找到当前目录的inode,遍历inode中的blocks数组得到目录中的文件名,将其打印出来
ls -l :
找到当前目录的inode,遍历inode中的blocks数组得到目录中的文件名 + inode 编号,由 inode 编号找到各个文件对应的inode,得到各个文件的属性信息,将其打印出来
cat 文件名 :
找到当前目录的inode,遍历inode中的blocks数组找到文件名和对应的inode编号,由文件的inode编号找到inode,遍历inode中的blocks数组将内容打印出来
理解软硬链接
建立软连接(符号链接)
ln -s test test-s
test-s 的 inode 编号和 test 的 inode 编号是不同的,因此软链接形成的文件本质是一个独立文件,文件内容保存的是test文件的路径(软链接相当于Windows中的快捷方式)
建立硬链接
ln test test-h
建立硬链接之后,test 的硬链接数由1变成2,test-h的硬链接数也是2,test-h的 inode 编号和 test 的 inode 编号是一样的,硬链接本质没有创建文件,只是建立了一个文件名和已有的 inode 的映射关系,并写入当前目录,相当于给文件取别名,删掉文件时,在目录中将对应的记录删除,硬链接数 - 1,硬链接为0时,则删掉磁盘上的文件(改变block bitmap inode bitmap)
一个目录的硬链接为2 是因为 目录名 和 . 两组目录名和目录inode有对应关系
在dir目录下再创建一个 newdir 目录,newdir 目录的硬链接数为2,dir 目录的硬链接数变为3
一个普通文件的硬链接为1 是因为只有一组文件名和文件inode有对应关系
因此硬链接的作用是方便目录之间通过相对路径方式进行跳转(也可通过硬链接数 -2 得出目录下有几个子目录)
最后解释一下文件的三个时间:
Access 最后访问时间
Modify 文件内容最后修改时间
Change 属性最后修改时间
stat myproc.c
三. 动态库和静态库
动态库 : 按照ELF格式进行存储,存储的主要内容是程序指令 + 程序数据(objdump -h /lib64/libc.so.6)
使用 nm 指令查看相关符号
动静态库本质是可执行程序的半成品,像我们平常使用的printf,scanf…等函数,函数的具体实现就在C库里面,其实我们并不是想要这些函数形成可执行程序,而是基础模块供人使用
在之前的博客中详细讲过生成可执行程序的步骤(在此不再赘述) :
(1). 预处理 -> 生成后缀为 .i 的文件
(2). 编译 -> 生成后缀为 .s 的文件,生成汇编代码
(3). 汇编 -> 生成后缀为 .o 的文件,生成可重定向的二进制文件
(4). 链接 -> 生成可执行程序
举例 : test1.c test2.c test3.c main.c,这四个文件每个经过上面的4步最终形成 test1.o test2.o test3.o main.o,最后经过链接形成可执行程序,如果将来我们想要 test1.o test2.o test3.o 和 main2.o/main3.o/main4.o …等链接形成可执行程序的话,我们就可以将 test1.o test2.o test3.o 打包成一个库,作为基础模块来使用,这样我们就不需要多次编译 test1.c test2.c test3.c 了,节省了时间
所以所有的库本质是 : 一堆 .o 的集合,不包含main,但是包含了大量的方法
动静态库的相关知识 :
(1). 使用 ldd 命令可以查看可执行程序所依赖的动态库
(2). 在Linux中,以.so结尾的叫动态库,以.a结尾的叫静态库
(3). 在Windows中,以.dll结尾的叫动态库,以.lib结尾的叫静态库
(4). 库名字 : 去掉前缀lib,去掉后缀 .so ,.a 及版本号,剩下的就是库名字
(5). 程序是默认动态链接的,加上-static静态链接
动静态库各自的特点 :
静态库和动态库区别在于链接阶段如何处理库,链接成可执行程序,分别称为静态链接,动态链接
静态库 : 在链接阶段,将汇编生成的.o目标文件和库一起链接到可执行程序中,对应的链接方式为静态链接
静态链接缺点 :
1). 空间浪费大,因为将库和汇编生成的目标文件一起链接形成可执行程序,所以可执行程序占用的磁盘空间大,可执行程序运行起来后,将代码和数据加载到内存中,当多个程序运行起来时,会导致内存中存在大量的重复代码
2). 静态库对程序的更新,部署,发布造成麻烦,当静态库更新时,所有程序都需要重新编译
静态链接的优点:
静态链接将库和汇编生成的目标文件一起链接形成可执行程序,当程序运行的时候不再受静态库的影响,无论静态库存在与否,程序依然可以正常运行
为了解决静态链接的缺点,于是引入了动态链接
动态链接 : 当程序运行起来时,动态库被载入到内存,不同的程序使用相同的动态库,只需要去内存中的动态库里寻找相应的实现即可,这样动态库只需要在内存中存在一份,就可以解决静态链接空间浪费的问题,当动态库更新时,只需要重新编译动态库即可,不需要重新编译我们的程序,解决了静态链接不容易更新的问题
动态链接的缺点:
动态链接是在程序运行时,将动态库加载到内存中,如果动态库不存在,程序就无法正常运行
// mytest.c 内容
#include<stdio.h>
int main()
{
printf("hello world\n");
return 0;
}
// 动态链接
gcc -o mytest mytest.c
// mytest : 动态可执行程序
// 静态链接
gcc -o mytest mytest.c -static
// mytest : 静态可执行程序
制作动静态库
制作静态库
// Makefile
mylib=libcal.a
CC=gcc
$(mylib): add.o sub.o
ar -rc $(mylib) $^
%.o:%.c
$(CC) -c $<
.PHONY:clean
clean:
rm -f $(mylib) *.o
.PHONY:output
output:
mkdir -p mathlib/lib
mkdir -p mathlib/include
cp *.h mathlib/include
cp *.a mathlib/lib
% 为通配符,表示当前目录下所有 .c 要形成 .o
$< : 将所有.c 一个一个的生成 .o
// add.h
#pragma once
#include<stdio.h>
extern int my_add(int x,int y);
// add.c
#include"add.h"
int my_add(int x,int y)
{
return x + y;
}
// sub.h
#pragma once
#include<stdio.h>
extern int my_sub(int x,int y);
// sub.c
#include"sub.h"
int my_sub(int x,int y)
{
return x - y;
}
make 一下我们的静态库 libcal.a 就做好了
静态库做好了,我们怎么样才能将静态库交给别人去使用呢 ?
给别人库的时候,本质上是给别人一份头文件(库的使用说明书) + 一份库文件(.a / .so,库的实现),使用如下命令进行编译就可以使用了
// test.c 内容
#include<stdio.h>
#include<add.h>
int main()
{
int a = 10,b = 20;
printf("%d\n",add(a,b));
return 0;
}
gcc test.c -I mathlib/include/ -L mathlib/lib -lcal
-I : 指定搜索路径的选项(头文件在哪里)
-L : 显示的指定应该去哪个路径下找库的选项(库文件在哪里)
-l : 库名
如果不想这么复杂,可以把我们库和头文件的路径拷贝到系统的路径下,但仍然需要带上库名,拷贝的过程就是安装库的过程,安装软件实际上就是将库拷贝到系统路径下,第三方库使用的时候一般都要指明库的名称
cp mathlib/include/* /usr/include/
cp mathlib/lib/libcal.a /lib64/
拷贝之后执行如下命令就可完成编译,不需要再写 -I -L,因为编译器会去系统默认的路径下去找
gcc test.c -lcal
制作动态库
// Makefile
libcal.so:add.o sub.o
gcc -o $@ $^ -shared
%.o:%.c
gcc -c $< -fPIC
.PHONY:clean
clean:
rm -f *.so *.o
.PHONY:output
output:
mkdir -p mathlib/include
mkdir -p mathlib/lib
cp *.h mathlib/include
cp *.so mathlib/lib
add.h add.c sub.h sub.c test.c 的内容和制作静态库的代码时是一样的
使用这段命令(和静态库一样)
gcc test.c -I mathlib/include/ -L mathlib/lib -lcal
运行起 ./test,会发现系统找不到动态库,但我们不是使用 gcc test.c -I mathlib/include/ -L mathlib/lib -lcal 指定了头文件和库的路径吗? 实际上这只是告诉编译器头文件和库的路径,操作系统却找不到
解决方案 :
(1). LD_LIBRARY_PATH : 程序运行时查找动态库所搜索的路径
默认LD_LIBRARY_PATH是什么都没有的,导出环境变量之后运行./test就可以正常使用了
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/luanyiping/test/10_24/dynamic/mathlib/lib
(2). ldconfig 配置/etc/ld.so.conf.d/,ldconfig更新
echo /home/luanyiping/test/10_24/dynamic/mathlib/lib > bit.conf
cp bit.conf /etc/ld.so.conf.d/
ldconfig
(3). 拷贝.so文件到系统共享库路径下, 一般指/usr/lib
使用外部库
一.
C++ 后缀 :
- .cc
- .cpp
- .cxx
C++的库
二.
在安装gcc/g++时,也把C语言/C++的头文件和库文件下载下来了,然后放在能够让编译器找到的路径下
三.
在Windows中,下载vs时,除了下载编译器本身,实际上也装了C/C++库文件,当我们想使用各种各样的组件,实际上就是把对应的库和头文件拷贝下来安装到vs目录下,使用的时候编译器就能够找到
// include文件夹
// lib文件夹