目录
一、重新谈论文件
- 空文件也要在磁盘占据空间
- 文件 = 内容 + 属性
- 文件操作 = 对内容 + 对属性 or 对内容和属性
- 标定一个问题,必须使用:文件路径 + 文件名【唯一性】
- 如果没有指明对应的文件路径,默认是在当前路径进行文件访问
- 当我们把fopen, fclose, fread, fwrite等接口写完之后,代码编译之后,形成二进制可执行程序之后,但是没运行,文件对应的操作有没有被执行呢?没有 — 对文件的操作,本质是进程对文件的操作!
- 一个文件如果没有被打开,可以直接进行文件访问吗?不能!一个文件要被访问,就必须先被打开!— 用户进程 + os
所以,文件操作的本质就是进程和被打开文件的关系。
二、重新谈论文件操作 C
- 文件在哪里-》磁盘-》硬件-》os-》所欲哦人想要访问文件不能绕过os-》使用os提供的接口-》提供文件级别的系统调用接口-》可以,操作系统只有一个
- 操作 C语言
示例代码:
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<string.h>
4 #define FILE_NAME "log.txt"
5
6 int main()
7 {
8 //以w方式单纯的打开文件,c会自动清空内部的数据
9 FILE* fp = fopen(FILE_NAME, "a");//r,w,r+(读写,不存在出错),w+(读写,不存在创建),a(apend, 追加),a+(读+追加)
10 if(fp == NULL)
11 {
12 perror("fopen");
13 return 1;
14 }
15 //char buffer[64];
16 把文件中的数据读取到buffer中
16 //while(fgets(buffer, sizeof(buffer) - 1, fp) != NULL)
17 //{
18 // buffer[strlen(buffer) - 1] = 0;
19 // puts(buffer);
20 //}
21
22 int cnt = 5;
23 while(cnt)
24 {
25 fprintf(fp, "%s:%d\n", "hello world", cnt--);
26 }
27
28 fclose(fp);
29 return 0;
30 }
如上,是我们之前学的文件相关操作。还有 fseek ftell rewind 的函数,在C部分已经有所涉猎。
三、系统文件I/O
操作文件,除了上述C接口(当然,C++也有接口,其他语言也有),我们还可以采用系统接口来进行文件访问,先来直接以代码的形式。
示例代码:
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<string.h>
4 #include<sys/types.h>
5 #include<sys/stat.h>
6 #include<fcntl.h>
7 #define FILE_NAME "log.txt"
8
9 int main()
10 {
11 umask(0);
12 //"w" --> O_WRONLY | O_CREAT | O_TRUNC, 0666
13 //"a" --> O_WRONLY | O_CREAT | O_APPEND, 0666
14 int fd = open(FILE_NAME, O_RDONLY, 0666);
15 if(fd < 0)
16 {
17 perror("open");
18 return 1;
19 }
20 char buffer[1024];
21 ssize_t num = read(fd, buffer, sizeof(buffer) - 1);
22 if(num > 0)
23 {
24 buffer[num] = 0;
25 }
26 printf("%s", buffer);
27 //int cnt = 5;
28 //char outBuffer[64];
29 //while(cnt)
30 //{
31 // sprintf(outBuffer, "%s:%d\n", "aaaaa", cnt--);
32 // //你以\0作为字符串的结尾,是C语言的规定,和我文件有什么关系呢?
33 // write(fd, outBuffer, strlen(outBuffer));
34 //}
35 //printf("fd:%d\n", fd);
36
37 close(fd);
38
39 return 0;
40 }
3.1 接口介绍
- open
man 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: 追加写
返回值:
成功:新打开的文件描述符
失败:-1
write read close lseek ,类比C文件相关接口
3.2 如何理解文件
示例代码:
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<string.h>
4 #include<sys/types.h>
5 #include<sys/stat.h>
6 #include<fcntl.h>
7 #define FILE_NAME(number) "log.txt"#number
8
9 int main()
10 {
11 printf("stdin->fd:%d\n", stdin->_fileno);
12 printf("stdout->fd:%d\n", stdout->_fileno);
13 printf("stderr->fd:%d\n", stderr->_fileno);
14 umask(0);
15 int fd0 = open(FILE_NAME(1), O_WRONLY | O_CREAT | O_APPEND, 0666);
16 int fd1 = open(FILE_NAME(2), O_WRONLY | O_CREAT | O_APPEND, 0666);
17 int fd2 = open(FILE_NAME(3), O_WRONLY | O_CREAT | O_APPEND, 0666);
18 int fd3 = open(FILE_NAME(4), O_WRONLY | O_CREAT | O_APPEND, 0666);
19 int fd4 = open(FILE_NAME(5), O_WRONLY | O_CREAT | O_APPEND, 0666);
20
21 printf("fd:%d\n", fd0);
22 printf("fd:%d\n", fd1);
23 printf("fd:%d\n", fd2);
24 printf("fd:%d\n", fd3);
25 printf("fd:%d\n", fd4);
26
27 close(fd0);
28 close(fd1);
29 close(fd2);
30 close(fd3);
31 close(fd4);
32
33 return 0;
34 }
结果为:
由上代码可知,有三个输入输出流
stdin —>键盘
stdout —>显示器
stderr —>显示器
FILE*是一个结构体,而且必有一个字段是文件描述符,而且文件描述符的本质就是数组的下标。
但是如何理解,键盘,显示器,这些东西也是文件???
由上图可知,系统通过文件指针数组,先描述,在组织,描述文件的属性,在通过数据结构把文件组织起来,数组下标就是文件描述符。
四、文件描述符的分配规则
示例代码:
#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;
}
输出发现是 fd: 3
关闭0或者2,在看
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
close(0);
//close(2);
int fd = open("myfile", O_RDONLY);
if(fd < 0){
perror("open");
return 1;
}
printf("fd: %d\n", fd);
close(fd);
return 0;
发现是结果是: fd: 0
或者 fd 2
。
由上图可知,可见,文件描述符的分配规则:在files_struct数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符。
五、重定向
那如果关闭1呢?看代码:
#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);
//这里输出因为把stdout变成了myfile,输出从显示器变到了myfile,需要刷新缓冲区。
fflush(stdout);
close(fd);
exit(0);
}
此时,我们发现,本来应该输出到显示器上的内容,输出到了文件 myfile 当中,其中,fd=1。这种现象叫做输出重定向。常见的重定向有:>(输出), >>(追加), <(输入)。
由上图可知,重定向的本质就是:上层用的fd不变,在内核中更改fd对应的struct file* 的地址。
六、使用 dup2 系统调用
函数原型如下:
#include <unistd.h>
int dup2(int oldfd, int newfd);//把文件指针数组中文件描述符下标对应的内容做互相拷贝
- 输出重定向
示例代码:
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<string.h>
4 #include<sys/types.h>
5 #include<sys/stat.h>
6 #include<fcntl.h>
7 #define FILE_NAME "log.txt"
8
9 int main()
10 {
11
12 umask(0);
13 int fd = open(FILE_NAME, O_WRONLY | O_CREAT | O_TRUNC, 0666);
14 if(fd < 0)
15 {
16 perror("open");
17 return 1;
18 }
19 dup2(fd, 1);
20
21 printf("fd:%d\n", fd);
22 fflush(stdout);
23 close(fd);
24 return 0;
25 }
输出结果:
2. 输入重定向
示例代码:
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<string.h>
4 #include<sys/types.h>
5 #include<sys/stat.h>
6 #include<fcntl.h>
7 #define FILE_NAME "log.txt"
8
9 int main()
10 {
11
12 umask(0);
13 int fd = open(FILE_NAME, O_RDONLY);
14 if(fd < 0)
15 {
16 perror("open");
17 return 1;
18 }
19 dup2(fd, 0);//输出重定向
20 char line[64];
21
22 while(1)
23 {
24 printf(">");
25 if(fgets(line, sizeof(line), stdin) == NULL)
26 {
27 break;
28 }
29 printf("%s\n", line);
30 }
31 //dup2(fd, 1);
32
33 //printf("fd:%d\n", fd);
34 //fflush(stdout);
35 close(fd);
36 return 0;
37 }
log.txt
输出结果为:
在myshell.c中添加重定向功能:
在上面这份代码中,子进程也会拷贝父进程的文件部分,因为要保证进程间的独立性,进程间不能相互影响。
七、理解 ---- linux一切皆文件
由上图可知,在硬件层,每个外设都有一个结构体,里面有自己的读写方法。操作系统则有自己的描述文件的struct file结构体,里面有描述文件的属性和能够调用不同读写方法的读写函数指针,多个struct file结构体又能够被os组织起来,形成一个内核数据结构,这种struct file结构体在描述不同文件时,调用不同函数的这种调用方式也叫做多态。
八、FILE
- 因为IO相关函数与系统调用接口对应,并且库函数封装系统调用,所以本质上,访问文件都是通过fd访问的。
- 所以C库当中的FILE结构体内部,必定封装了fd。
来段代码研究一下:
#include <stdio.h>
#include <string.h>
int main()
{
const char *msg0="hello printf\n";
const char *msg1="hello fwrite\n";
const char *msg2="hello write\n";
printf("%s", msg0);
fwrite(msg1, strlen(msg0), 1, stdout);
write(1, msg2, strlen(msg2));
fork();
return 0;
}
运行结果为:
hello printf
hello fwrite
hello write
但如果对进程实现输出重定向呢? ./hello > file
, 我们发现结果变成了:
hello write
hello printf
hello fwrite
hello printf
hello fwrite
我们发现 printf
和 fwrite
(库函数)都输出了2次,而 write
只输出了一次(系统调用)。为什么呢?肯定和fork有关。
- 如果我们没有进行>,看到了四条消息,stdout默认使用的行刷新,在进行fork之前,三条c函数已经将数据进行打印到显示器上,你的FILE内部,进程内部不存在对应的数据。
- 如果我们进行了>,写入文件不再是显示器,而是普通文件,采用的刷新策略是全缓冲,之前的三条c显示函数,虽然带了\n,但是不足以stdout缓冲区写满,数据并没有被刷新。执行fork的时候,stdout属于父进程,创建子进程时,紧接着就是进程退出,谁先退出,一定要进行缓冲区刷新(就是修改)。写诗拷贝,数据最终会显示两份。
- write为什么没有呢,上面的过程都和write无关,write没有FIEL,用的是fd,就没有c提供的缓冲区。
为了解释以上现象,我们先了解缓冲区。
缓冲区本质是一段内存。
我们可以把缓冲区类比为快递点。快递行业的意义是节省发送者的时间,缓冲区的意义是节省进程进行数据IO的时间。
缓冲区刷新策略的问题
缓冲区一定会解饿和具体的设备,定制自己的缓冲策略:
a. 立即刷新 – 无缓冲
b. 行刷新 – 行缓冲 – 显示器
c. 缓冲区满 – 全缓冲 – 磁盘文件
- 用户强制刷新
- 进程退出 – 一般都要进行行缓冲区刷新
你所说的缓冲区,在哪里?指的是什么缓冲区?
这种现象,一定和缓冲区有关。
缓冲区一定不在内核中,因为如果在内核中,write也要打印两次。
我们谈论的缓冲区,都指的是用户级语言层面给我们提供的缓冲区。
这个缓冲区,在stdout,stdin,stderr -->FILE* -->FILE结构体 -->fd && 还包括一个缓冲区。
所以我们要强制刷新,fflush(文件指针), fclose(文件指针)。
理解缓冲区
- 实现一个简单的c语言FILE结构体,文件打开,文件读写,文件关闭。
myStdio.h
#pragma once
#include <assert.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#define SIZE 1024
#define SYNC_NOW 1
#define SYNC_LINE 2
#define SYNC_FULL 4
typedef struct _FILE{
int flags; //刷新方式
int fileno;
int cap; //buffer的总容量
int size; //buffer当前的使用量
char buffer[SIZE];
}FILE_;
FILE_ *fopen_(const char *path_name, const char *mode);
void fwrite_(const void *ptr, int num, FILE_ *fp);
void fclose_(FILE_ * fp);
void fflush_(FILE_ *fp);
myStdio.c
#include "myStdio.h"
FILE_ *fopen_(const char *path_name, const char *mode)
{
int flags = 0;
int defaultMode=0666;
if(strcmp(mode, "r") == 0)
{
flags |= O_RDONLY;
}
else if(strcmp(mode, "w") == 0)
{
flags |= (O_WRONLY | O_CREAT |O_TRUNC);
}
else if(strcmp(mode, "a") == 0)
{
flags |= (O_WRONLY | O_CREAT |O_APPEND);
}
else
{
//TODO
}
int fd = 0;
if(flags & O_RDONLY)
fd = open(path_name, flags);
else
fd = open(path_name, flags, defaultMode);
if(fd < 0)
{
const char *err = strerror(errno);
write(2, err, strlen(err));
return NULL; // 为什么打开文件失败会返回NULL
}
FILE_ *fp = (FILE_*)malloc(sizeof(FILE_));
assert(fp);
fp->flags = SYNC_LINE; //默认设置成为行刷新
fp->fileno = fd;
fp->cap = SIZE;
fp->size = 0;
memset(fp->buffer, 0 , SIZE);
return fp; // 为什么你们打开一个文件,就会返回一个FILE *指针
}
void fwrite_(const void *ptr, int num, FILE_ *fp)
{
// 1. 写入到缓冲区中
memcpy(fp->buffer+fp->size, ptr, num); //这里我们不考虑缓冲区溢出的问题
fp->size += num;
// 2. 判断是否刷新
if(fp->flags & SYNC_NOW)
{
write(fp->fileno, fp->buffer, fp->size);
fp->size = 0; //清空缓冲区
}
else if(fp->flags & SYNC_FULL)
{
if(fp->size == fp->cap)
{
write(fp->fileno, fp->buffer, fp->size);
fp->size = 0;
}
}
else if(fp->flags & SYNC_LINE)
{
if(fp->buffer[fp->size-1] == '\n') // abcd\nefg , 不考虑
{
write(fp->fileno, fp->buffer, fp->size);
fp->size = 0;
}
}
else{
}
}
void fflush_(FILE_ *fp)
{
if( fp->size > 0) write(fp->fileno, fp->buffer, fp->size);
fsync(fp->fileno); //将数据,强制要求OS进行外设刷新!
fp->size = 0;
}
void fclose_(FILE_ * fp)
{
fflush_(fp);
close(fp->fileno);
}
main.c
#include "myStdio.h"
#include <stdio.h>
int main()
{
FILE_ *fp = fopen_("./log.txt", "w");
if(fp == NULL)
{
return 1;
}
int cnt = 10;
const char *msg = "hello bit ";
while(1)
{
fwrite_(msg, strlen(msg), fp);
fflush_(fp);
sleep(1);
printf("count: %d\n", cnt);
//if(cnt == 5) fflush_(fp);
cnt--;
if(cnt == 0) break;
}
fclose_(fp);
return 0;
}
- 和os有什么关系
由上图可知,os会根据自己的内存来决定怎么样刷新缓冲区中的数据到外设的,FILE结构体的缓冲区,只是把里面的数据拷贝到内核缓冲区的。
fsync()//将数据,从内核缓冲区强制刷新到外设
九、文件系统
如果一个文件没有被打开呢?该如何被os管理?没有被打开的文件,只能静静的放在磁盘里管理。
磁盘上面有大量文件,也是必须被静态管理,方便我们随时打开。
9.1 磁盘的物理结构
9.2 磁盘的存储结构
9.3 磁盘的逻辑结构
我们如何定位到具体的扇区你?
为什么os要进行逻辑抽象呢?直接用CHS不行吗?
- 便于管理
- 不想让os的代码和硬件强耦合。
9.4 我们的磁盘是怎么管理文件的呢?
因为磁盘中的文件都一样,所以我们管理文件只需要管理一小部分,管理其他文件直接拷贝即可。
了解文件系统
那我们该如何查找文件呢?
查找文件使用的是inode编号!!
由上图可知,我们可以通过inode号找到对应的数据块。
但是我们用的是文件名,那是怎么找的呢?因为文件是在目录下面,目录有自己的inode和自己的数据块,目录的数据块放的是当前目录下的文件名和inode的映射关系。
那我们怎么删呢?
我们也是用inode来删,我们只需要通过inode在inode bitmap中找到对应inode的比特位,把他由1置为0。采用的是惰性删除的方式。还有block bitmap中对应数据的比特位置为0。只需要修改对应的位图就可以把文件删掉。如果想要恢复文件,也可以把对应的位图修改回来就可以了。
9.5 硬链接(没有独立的inode)
我们看到,真正找到磁盘上文件的并不是文件名,而是inode。 其实在linux中可以让多个文件名对应于同一个inode。
建立一个硬链接,究竟是做了什么??
就是在指定路径下,新增文件名和inode编号的映射关系!!
有什么用?
相当于拷贝文件或者叫做文件重命名。
什么时候一个文件算被真正的删除呢?
当一个文件的硬链接变为零的时候。
linux为什么不允许普通用户给目录建立硬链接呢?
1.Linux等大部分系统,从设计原理上就禁止对目录做硬链接。
2.原因就是允许目录的硬链接可能会打破文件系统目录的有向无环图结构,可能创建目录循环,这可能会导致fsck以及其他一些遍历文件树的软件出错。
9.6 软连接(有独立的inode)
硬链接是通过inode引用另外一个文件,软链接是通过名字引用另外一个文件,在shell中的做法。
软连接相当于window的快捷方式。
9.7 acm
下面解释一下文件的三个时间:
- Access 最后访问时间
- Modify 文件内容最后修改时间
- Change 属性最后修改时间
十、动态库和静态库
10.1 静态库与动态库
- 静态库(.a):程序在编译链接的时候把库的代码链接到可执行文件中。程序运行的时候将不再需要静态库。
- 动态库(.so):程序在运行的时候才去链接动态库的代码,多个程序共享使用库的代码。
- 一个与动态库链接的可执行文件仅仅包含它用到的函数入口地址的一个表,而不是外部函数所在目标文件的整个机器码。
- 在可执行文件开始运行以前,外部函数的机器码由操作系统从磁盘上的该动态库中复制到内存中,这个过程称为动态链接(dynamic linking)。
- 动态库可以在多个程序间共享,所以动态链接使得可执行文件更小,节省了磁盘空间。操作系统采用虚拟内存机制允许物理内存中的一份动态库被要用到该库的所有进程共用,节省了内存和磁盘空间。
库的理解
10.2 静态库和静态链接
交付库->库文件.a.so + 匹配的头文件都给别人。
所谓的库的安装就是拷贝!!!
10.2.1 制作静态库
- 写一个简单的库文件
makefile
1 libmymath.a:my_add.o my_sub.o
2 ar -rc $@ $^//ar指的是gun归档工具,rc表示(replace and create),把所有的*.o文件归档在一个文件中
3 my_add.o:my_add.c
4 gcc -c my_add.c -o my_add.o
5 my_sub.o:my_sub.c
6 gcc -c my_sub.c -o my_sub.o
7
8
9 .PHONY:output
//.PHONY:output指的是输出时创建目录或者拷贝文件等等
10 output:
11 mkdir -p mylib/include
12 mkdir -p mylib/lib
13 cp -f *.a mylib/lib
14 cp -f *.h mylib/include
15
16 .PHONY:clean
17 clean:
18 rm -f *.o libmymath.a
my_sub.h
1 #pragma once
2
3 #include<stdio.h>
4
5 extern int Sub(int a, int b);
my_sub.c
1 #include "my_sub.h"
2
3 int Sub(int a, int b)
4 {
5 printf("enter Sub func, %d - %d = ?\n", a, b);
6 return a - b;
7 }
my_add.h
1 #pragma once
2
3 #include<stdio.h>
4
5 extern int Add(int a, int b);
my_add.c
1 #include "my_add.h"
2
3 int Add(int a, int b)
4 {
5 printf("enter Add func, %d + %d = ?\n", a, b);
6 return a + b;
7 }
- 打包并且把包拷贝到其他目录,然后解压
打包
输出结果:
拷贝到其他目录
解压
输出结果
10.2.2 链接静态库
如果要链接第三方库, 必须指明库名称!!!
- 由上面可知,我们已经有头文件和链接文件了,现在我们只需要编译。
由上图可知,-I指的是要包的头文件路径,-L指的是要找的链接文件的路径,-l指的是要你要找的哪个链接文件。
编译之后输出:
由上图可知:
形成一个可执行程序,可能不仅仅依赖一个库!!
gcc默认是动态连接的(建议行为),对于特定的一个库,究竟是动还是静,取决于你提供的是动态库还是静态库!动静态库都给你。
10.2.3 第二种连接第三方库的方法:直接安装到系统的库文件进行使用
- 安装库文件
- 使用
编译后,输出结果为:
- 删除库文件
10.3 生成动态库
- shared: 表示生成共享库格式
- fPIC:产生位置无关码(position independent code)
- 库名规则:libxxx.so
示例:
- 创建动态库
- 打包,拷贝, 解压(同静态链接)
- 编译
可以看出执行的时候系统找不到库文件。虽然我们把库文件告诉了gcc,但是OS和shell不知道,我们的库没有在系统路径下,OS无法找到!!
所以我们需要设置环境变量 - 设置环境变量
输出结果为:
- 第二种方法为通过软连接来进行动态链接的链接
10.4 动态库的加载
由上图可知,动态库和静态库不一样,由上面的形成与地址无关码就可以知道,动态库中指定函数的地址不是绝对编址,动态库中指定的函数地址为start:偏移地址,动态库是寻找相对地址,只需要知道相对于可执行函数的偏移地址,系统会预先知道库lib.so。这样我们执行printf函数就知道他是库libc.so当中的函数。加载库的时候,系统立马就决定了这个库的起始地址。所以当你调用printf的时候,当我们链接的时候,printf里面已经知道在库中的偏移量。只需要知道你调用的函数的偏移量和库的起始地址,就能直接在上下文中调用你所执行的函数。