Linux(基础IO)
- 前言
- C语言文件IO
- 什么叫当前路径
- stdin/stdout/stderr
- 系统文件IO
- open
- close
- write
- read
- 文件描述符
- 文件描述符的分配规则
- 重定向
- 输出重定向原理
- 追加重定向原理
- 输入重定向原理
- dup2
- 添加重定向功能到minishell
- 缓冲区
- 模拟实现一个缓冲区
- 理解文件系统
- 初识inode
- 磁盘的概念
- 磁盘分区与格式化介绍
- EXT2文件系统的存储方案
- 软硬链接
- 软链接
- 硬链接
- 文件的三个时间
前言
我们知道C,C++中存在各类文件操作,但是我们需要理解,并不是C,C++直接去访问这些文件的,他们通过系统所提供的的接口进行访问,然后进行各种操作。
OS接口只有一套,但是不同语言文件访问接口不一样,所以就需要对系统接口进行封装,因为一旦都使用系统接口,编写文件代码,就无法在其他平台运行,不具备跨平台性了,所以这也是我们学校系统文件接口的主要原因,更好的理解C,C++文件操作的底层结构。
C语言文件IO
我们先来回忆一下在C语言中的各类文件操作:
文件操作函数 | 功能 |
---|---|
fopen | 打开文件 |
fclose | 关闭文件 |
fputc | 写入一个字符 |
fgetc | 读取一个字符 |
fputs | 写入一个字符串 |
fgets | 读取一个字符串 |
fprintf | 格式化写入数据 |
fscanf | 格式化读取数据 |
fwrite | 向二进制文件写入数据 |
fread | 从二进制文件读取数据 |
fseek | 设置文件指针的位置 |
ftell | 计算当前文件指针相对于起始位置的偏移量 |
rewind | 设置文件指针到文件的起始位置 |
ferror | 判断文件操作过程中是否发生错误 |
feof | 判断文件指针是否读取到文件末尾 |
文件打开形式
文件打开方式 | 含义 |
---|---|
“r” | read:打开文件进行输入操作。该文件必须存在。 |
“w” | write:为输出操作创建一个空文件。如果已存在同名文件,则丢弃其内容,并将该文件视为新的空文件。 |
“a” | append:打开文件以在文件末尾输出。输出操作总是在文件末尾写入数据,并对其进行扩展。忽略重新定位操作(fseek、fsetpos、rewind)。如果文件不存在,则创建该文件。 |
“r+” | read/update:打开一个文件进行更新(输入和输出)。文件必须存在。 |
“w+” | write/update:创建一个空文件并打开它以进行更新(输入和输出)。如果同名文件已经存在,则将丢弃其内容,并且该文件将被视为新的空文件。 |
“a+” | append/uptate:打开一个文件进行更新(包括输入和输出),所有输出操作都在文件末尾写入数据。重新定位操作(fseek、fsetpos、rewind)会影响下一个输入操作,但输出操作会将位置移回文件末尾。如果文件不存在,则创建该文件。 |
对文件进程写入操作
1 #include <stdio.h>2 #include <string.h>3 #include <unistd.h>4 5 int main()6 {7 FILE *pf = fopen("log.txt", "w");8 if(pf == NULL)9 {10 perror("fpeon");11 return 1;12 }13 const char *s = "hello world\n";14 fwrite(s, strlen(s), 1, pf); 15 16 fclose(pf);17 return 0;18 }
对文件进行读取操作
1 #include <stdio.h>2 #include <string.h>3 #include <unistd.h>46 int main()7 {8 FILE *pf = fopen("log.txt", "r");9 if(pf == NULL)10 {11 perror("fpeon");12 return 1;13 }14 char buffer[64]; 15 fread(buffer, sizeof(buffer), 1, pf);16 printf("%s\n", buffer); 17 fclose(pf); 18 return 0; 19 }
对文件进程追加字符串操作
1 #include <stdio.h> 2 #include <string.h>3 #include <unistd.h> 4 5 int main() 6 { 7 FILE *pf = fopen("log.txt", "a+");8 if(pf == NULL)9 { 10 perror("fpeon"); 11 return 1; 12 } 13 const char* s = "hello world\n"; 14 fwrite(s, strlen(s), 1, pf); 15 fclose(pf);16 return 0; 17 }
我们需要注意的是fopen以w形式打开文件,默认先清空文件,在fwrite之前,fopen以a+形式打开文件,是不断像文件中追加内容。
什么叫当前路径
我们知道,当fopen以写入的方式打开一个文件时,若该文件不存在,则会自动在当前路径创建该文件,那么这里所说的当前路径指的是什么呢?
我们在study_8_22目录下创建mytest,可执行程序创建的log.txt就会出现在study_8_22目录下:
我们返回上级目录,将可执行程序移动到此目录中,然后运行:
我们会发现,依然在此目录下创建了log.txt文件,并没有在study_8_22目录下创建。
当该可执行程序运行起来变成进程后,我们可以获取该进程的PID,然后根据该PID在根目录下的proc目录下查看该进程的信息。
我们可以看到两个软链接文件cwd和exe,cwd就是进程运行时我们所处的路径,而exe就是该可执行程序的所处路径。
我们就可以得出结论:
我们这里所说的当前路径不是指可执行程序所处的路径,而是指该可执行程序运行成为进程时我们所处的路径。
stdin/stdout/stderr
都说Linux下一切皆文件,也就是说Linux下的任何东西都可以看作是文件,那么显示器和键盘当然也可以看作是文件。我们能看到显示器上的数据,是因为我们向“显示器文件”写入了数据,电脑能获取到我们敲击键盘时对应的字符,是因为电脑从“键盘文件”读取了数据。
那么既然是写入数据和读取数据,就要打开相应的文件,那么为什么我们没有进行相应打开文件操作呢?
打开文件是在进程运行的时候打开的,而对于任何进程来说,运行的时候都会默认打开标准输入流,标准输出流和标准错误流,对应到C语言当中就是stdin、stdout以及stderr。
标准输入流对应的是键盘,标准输出流和标准错误流对应的都是显示器。
我们查看man手册就可以发现,他们三个的返回值都是FILE* 类型的:
当我们的C程序运行起来的时候,操作系统会默认打开这三个流,所以我们才可以进行scanf,printf等一系列的读取与输出的操作,stdin/stdout/stderr与我们打开文件获得的文件指针是一个概念。
1 #include <stdio.h>2 #include <string.h>3 #include <unistd.h>4 5 int main()6 {7 fprintf(stdout, "hello world\n"); 8 return 0;9 }
1 #include <stdio.h>2 #include <string.h>3 #include <unistd.h>4 5 int main()6 {7 int a;8 fscanf(stdin, "%d", &a);9 printf("%d\n", a); 10 return 0;11 }
系统文件IO
操作文件,除了上述C接口(当然,C++也有接口,其他语言也有),我们还可以采用系统接口来进行文件访问。
open
系统文件接口中,使用open打开文件:
int open(const char *pathname, int flags, mode_t mode);
- open第一个参数
open函数的第一个参数是pathname,表示要打开或创建的目标文件。
若pathname以路径的方式给出,则当需要创建该文件时,就在pathname路径下进行创建。
若pathname以文件名的方式给出,则当需要创建该文件时,默认在当前路径下进行创建。
- open第二个参数
open函数的第二个参数是flags,表示打开文件的方式。
其中常用选项有如下几个:
参数选项 | 含义 |
---|---|
O_RDONLY | 以只读的方式打开文件 |
O_WRNOLY | 以只写的方式打开文件 |
O_APPEND | 以追加的方式打开文件 |
O_RDWR | 以读写的方式打开文件 |
O_CREAT | 当目标文件不存在时,创建文件 |
打开文件时,可以传入多个参数选项,当有多个选项传入时,将这些选项用“或”运算符隔开。
例如,若想以只写的方式打开文件,但当目标文件不存在时自动创建文件,则第二个参数设置如下:
O_WRONLY | O_CREAT
系统接口open的第二个参数flags是整型,有32比特位,若将一个比特位作为一个标志位,则理论上flags可以传递32种不同的标志位。
实际上传入flags的每一个选项在系统当中都是以宏的方式进行定义的,我们举个例子:
1 #include <stdio.h>2 #include <string.h>3 #include <unistd.h>4 5 //用int中不重复的一个bit,标识一种状态6 #define ONE 0x1 //0000 00017 #define TWO 0x2 //0000 00108 #define THREE 0x4 //0000 01009 10 void show(int flags)11 {12 if(flags & ONE) printf("hello one\n");13 if(flags & TWO) printf("hello two\n");14 if(flags & THREE) printf("hello three\n"); 15 }16 int main()17 {18 show(ONE); //0000 000119 printf("---------------------------\n");20 show(ONE | TWO); // 0000 001121 printf("---------------------------\n");22 show(ONE | TWO | THREE); // 0000 011123 printf("---------------------------\n");24 show(ONE | THREE); // 0000 010125 return 0;26 }
运行程序:
系统文件接口中也是类似于这种方式实现的:
例如,O_RDONLY、O_WRONLY、O_RDWR
和O_CREAT
在系统当中的宏定义如下:
#define O_RDONLY 00
#define O_WRONLY 01
#define O_RDWR 02
#define O_CREAT 0100
- open第三个参数
open函数的第三个参数是mode,表示创建文件的默认权限。
1 #include <stdio.h>2 #include <string.h>3 #include <unistd.h>4 #include <sys/types.h>5 #include <sys/stat.h>6 #include <fcntl.h>7 8 int main()9 {11 int fd = open("log.txt", O_WRONLY | O_CREAT, 0666);12 if(fd < 0)13 { 14 perror("open"); 15 }16 printf("open sucess:fd:%d\n", fd); 17 return 0;18 }
例如,将mode设置为0666,则文件创建出来的权限如下:
这是因为umask默认值为0002,实际创建出来文件的权限为:mode&(~umask)。当我们设置mode值为0666时实际创建出来文件的权限为0664。
我们此时将umask值设置为0:
umask(0);
- open的返回值
open函数的返回值是新打开文件的文件描述符。
1 #include <stdio.h>2 #include <string.h>3 #include <unistd.h>4 #include <sys/types.h>5 #include <sys/stat.h>6 #include <fcntl.h>7 8 int main()9 {10 umask(0);11 int fd1 = open("log1.txt", O_WRONLY | O_CREAT, 0666);12 int fd2 = open("log2.txt", O_WRONLY | O_CREAT, 0666);13 int fd3 = open("log3.txt", O_WRONLY | O_CREAT, 0666);14 int fd4 = open("log4.txt", O_WRONLY | O_CREAT, 0666); 15 int fd5 = open("log5.txt", O_WRONLY | O_CREAT, 0666);16 printf("open sucess:fd1:%d\n", fd1);17 printf("open sucess:fd2:%d\n", fd2);18 printf("open sucess:fd3:%d\n", fd3);19 printf("open sucess:fd4:%d\n", fd4);20 printf("open sucess:fd5:%d\n", fd5);21 return 0;22 }
运行程序,我们可以发现文件描述符是连续且递增的:
当我们打开一个没有被创建的文件时:
1 #include <stdio.h>2 #include <string.h>3 #include <unistd.h>4 #include <sys/types.h>5 #include <sys/stat.h>6 #include <fcntl.h>7 int main()8 {9 int fd = open("test.txt", O_RDONLY);10 printf("%d\n", fd);11 return 0;12 }
fd的值就为-1。
实际上这里所谓的文件描述符本质上是一个指针数组的下标,指针数组当中的每一个指针都指向一个被打开文件的文件信息,通过对应文件的文件描述符就可以找到对应的文件信息。
当使用open函数打开文件成功时数组当中的指针个数增加,然后将该指针在数组当中的下标进行返回,而当文件打开失败时直接返回-1,因此,成功打开多个文件时所获得的文件描述符就是连续且递增的。
而Linux进程默认情况下会有3个缺省打开的文件描述符,分别就是标准输入0、标准输出1、标准错误2,这就是为什么成功打开文件时所得到的文件描述符是从3开始进程分配的。
close
系统接口中使用close函数关闭文件,close函数的函数原型如下:
使用close函数时传入需要关闭文件的文件描述符即可,若关闭文件成功则返回0,若关闭文件失败则返回-1。
write
系统接口中使用write函数向文件写入信息,write函数的函数原型如下:
我们可以使用write函数,将buf位置开始向后count字节的数据写入文件描述符为fd的文件当中。
- 如果数据写入成功,实际写入数据的字节个数被返回。
- 如果数据写入失败,-1被返回。
操作示例:
1 #include <stdio.h>2 #include <unistd.h>3 #include <sys/types.h>4 #include <sys/stat.h>5 #include <fcntl.h>6 #include <string.h>7 8 int main()9 {10 int fd = open("log.txt", O_WRONLY | O_CREAT, 0666);11 if(fd < 0)12 { 13 perror("open");14 return 1;15 }16 const char* s = "hello world\n";17 write(fd, s, strlen(s));18 close(fd);19 return 0;20 }
read
系统接口中使用read函数从文件读取信息,read函数的函数原型如下:
我们可以使用read函数,从文件描述符为fd的文件读取count字节的数据到buf位置当中。
- 如果数据读取成功,实际读取数据的字节个数被返回。
- 如果数据读取失败,-1被返回。
操作示例:
1 #include <stdio.h>2 #include <unistd.h>3 #include <sys/types.h>4 #include <sys/stat.h>5 #include <fcntl.h>6 #include <string.h>7 8 int main()9 {10 int fd = open("log.txt", O_RDONLY);11 if(fd < 0)12 {13 perror("open");14 return 1;15 }16 17 char buffer[64];18 read(fd, buffer, sizeof(buffer));19 printf("%s\n", buffer); 20 21 close(fd);22 return 0;23 }
文件描述符
文件是在进程运行时打开的,一个进程可以打开多个文件,系统在任何时刻都会存在大量已经打开的文件,而我们操作系统需要对这些已打开的文件进行管理,操作系统会为每个已经打开的文件创建各自的struct file结构体,然后将这些结构体以双链表的形式连接起来,之后操作系统对文件的管理也就变成了对这张双链表的增删查改等操作。
注意: 向文件写入数据时,是先将数据写入到对应文件的缓冲区当中,然后定期将缓冲区数据刷新到磁盘当中。
- 什么叫做进程运行时会默认打开0 ,1,2;
0对应的标准输入流,就是键盘,1对应标准输出流,是显示器,2对应标准错误流,也是显示器,我们进程刚开始运行时,操作系统就会格局键盘,显示器形成相应的struct file并以双链表形式链接起来,并且将它的地址分别填入fd_array指针数组下标0,1,2的位置,由此就默认打开了标准输入流,标准输出流,标准错误流。
磁盘文件和内存文件
我们将被打开的文件称之为内存文件,没有被打开的文件称为磁盘文件,磁盘文件又由两部分构成(文件内容+文件属性),磁盘文件和内存文件就像程序与进程之间的关系一样,当程序运行起来就变成了进程,而磁盘文件被写入内存中就变成了内存文件。
文件描述符的分配规则
1 #include <stdio.h>2 #include <unistd.h>3 #include <sys/types.h>4 #include <sys/stat.h>5 #include <fcntl.h>6 #include <string.h>7 int main()8 {9 umask(0);10 int fd1 = open("log1.txt", O_RDONLY|O_CREAT, 0666);11 int fd2 = open("log2.txt", O_RDONLY|O_CREAT, 0666);12 int fd3 = open("log3.txt", O_RDONLY|O_CREAT, 0666);13 int fd4 = open("log4.txt", O_RDONLY|O_CREAT, 0666);14 int fd5 = open("log5.txt", O_RDONLY|O_CREAT, 0666);15 printf("%d\n", fd1);16 printf("%d\n", fd2);17 printf("%d\n", fd3);18 printf("%d\n", fd4);19 printf("%d\n", fd5); 20 return 0;21 }
运行程序,我们会发现文件描述符按一下规则排列:
其中0,1,2的位置上已经被标准输入流,标准输出流和标准错误流占了,所以从3开始分配。
那么如果我们关掉文件描述符为0的文件呢?
close(0);
我们会发现此时的第一个文件描述符fd是0了。
同样,我们关掉0和2试一试:
由此我们可以得出一个结论:
文件描述符fd是从函数指针数组fd_array下标最小未被使用的开始进行分配的。
重定向
在了解了文件描述符与它的分配原则以后,我们就可以进一步探索重定向的问题了,其实重定向的本质就是修改文件描述符下标对应的struct file*的内容。
输出重定向原理
输出重定向就是我们本该输出到一个文件的数据输出到另一个文件:
我们如果将本该输出到显示器的文件输出到“log.txt”中去,我们可以在打开文件之前将文件描述符为1的文件进行关闭,这样我们后续打开的“log.txt”文件描述符就为1了。
1 #include <stdio.h>2 #include <unistd.h>3 #include <sys/types.h>4 #include <sys/stat.h>5 #include <fcntl.h>6 #include <string.h>7 8 int main()9 {10 close(1);11 int fd = open("log.txt", O_WRONLY|O_CREAT, 0666);12 if(fd < 0)13 {14 perror("open");15 return 1;16 }17 18 printf("hello world\n");19 close(fd);20 return 0; 21 }
运行程序我们会发现,此时“hello world”并没有打印到屏幕上,我们打开log.txt,发现他存在与log.txt中,此时就完成了我们的输出重定向。
追加重定向原理
追加重定向其实就是在输出重定向的基础上更改一下我们flags的条件就可以了:
int fd = open("log.txt", O_WRONLY|O_APPEND|O_CREAT, 0666);
输入重定向原理
输入重定向就是,将我们本应该从一个文件读取数据,现在重定向为从另一个文件读取数据。
我们想让本应该从“键盘文件”读取数据的scanf函数,改为从log.txt文件当中读取数据,那么我们可以在打开log.txt文件之前将文件描述符为0的文件关闭,也就是将“键盘文件”关闭,这样一来,当我们后续打开log.txt文件时所分配到的文件描述符就是0。
1 #include <stdio.h>2 #include <unistd.h>3 #include <sys/types.h>4 #include <sys/stat.h>5 #include <fcntl.h>6 #include <string.h>7 8 int main()9 {10 close(0);11 int fd = open("log.txt", O_RDONLY);12 if(fd < 0)13 {14 perror("open");15 return 1;16 }17 char buffer[64];18 fgets(buffer, sizeof(buffer),stdin); 19 printf("%s", buffer);20 21 close(fd);22 return 0;23 }
dup2
我们像上面那种方式进行重定向比较麻烦,在Linux操作系统中提供了系统接口dup2,我们可以使用该函数完成重定向。dup2的函数原型如下:
int dup2(int oldfd, int newfd);
函数功能: dup2会将fd_array[oldfd]的内容拷贝到fd_array[newfd]当中,如果有必要的话我们需要先使用关闭文件描述符为newfd的文件。
函数返回值: dup2如果调用成功,返回newfd,否则返回-1。
使用dup2时,我们需要注意以下两点:
- 如果oldfd不是有效的文件描述符,则dup2调用失败,并且此时文件描述符为newfd的文件没有被关闭。
- 如果oldfd是一个有效的文件描述符,但是newfd和oldfd具有相同的值,则dup2不做任何操作,并返回newfd。
1 #include <stdio.h>2 #include <unistd.h>3 #include <sys/types.h>4 #include <sys/stat.h>5 #include <fcntl.h>6 #include <string.h>7 8 int main()9 {10 int fd = open("log.txt", O_WRONLY|O_CREAT, 0666);11 if(fd < 0)12 {13 perror("open");14 return 1;15 }16 17 close(1);18 dup2(fd, 1);19 printf("hello world\n");20 fprintf(stdout, "hello Linux\n");21 22 close(fd); 23 return 0;24 }
代码运行后,我们即可发现数据被输出到了log.txt文件当中:
如何理解Linux下一切皆文件
Linux是用C语言写的,大佬在设计的过常利用C语言的struct实现了面向对象和多态的功能。
我们在学习C语言的过程中可以发现,struct中只能定义成员变量,不能定义成员函数,但是可以定义函数指针,通过在struct中定义函数指针,就可以调用函数,从而来实现面向对象的功能。
底层是不同的硬件,一定对应不同的操作方法,但是上层的设备都是外设,每个设备的核心访问函数都可以是read,write,所以,所有的设备都可以有自己的read,write,但是他们的实现代码肯定是不一样的
添加重定向功能到minishell
1 #include<stdio.h>2 #include<stdlib.h>3 #include<string.h>4 #include<unistd.h>5 #include<sys/wait.h>6 #include<assert.h>7 #include<sys/stat.h>8 #include<sys/types.h>9 #include<fcntl.h>10 11 #define NUM 102412 #define SIZE 3213 #define SEP " "14 //保存完整的字符15 char cmd_line[NUM];16 //保存打散之后的字符串17 char* g_argv[SIZE];18 19 //环境变量buffer,用来测试20 char g_myval[64];21 22 #define INPUT_REDIR 123 #define OUTPUT_REDIR 224 #define APPEND_REDIR 325 #define NONE_REDIR 026 27 int redir_status = NONE_REDIR;28 29 char* CheckRedir(char* start)30 {31 assert(start);32 char* end = start + strlen(start) - 1;33 while(end >= start)34 {35 if(*end == '>')36 {37 if(*(end-1) == '>')38 {39 redir_status = APPEND_REDIR;40 *(end-1) = '\0';41 end++;42 break;43 }44 redir_status = OUTPUT_REDIR;45 *end = '\0';46 end++;47 break;48 }49 else if(*end == '<')50 {51 redir_status = INPUT_REDIR;52 *end = '\0';53 end++;54 break;55 }56 else57 {58 end--;59 }60 }61 if(end >= start)62 {63 return end;64 }65 else66 {67 return NULL;68 }69 }70 int main()71 {72 extern char** environ;73 while(1)74 {75 //1.打印出提示信息:"[root@localhost myshell]# "76 printf("[root@localhost myshell]# ");77 fflush(stdout);78 memset(cmd_line, '\0', sizeof cmd_line);79 //2.获取用户输入的各种键盘指令:"ls -a -l -i"80 if(fgets(cmd_line, sizeof cmd_line, stdin) == NULL)81 {82 continue;83 }84 cmd_line[strlen(cmd_line) - 1] = '\0';85 //检查是否有重定向 (ls -a -l>log.txt -> ls -a -l\0log.txt)86 char* sep = CheckRedir(cmd_line);87 //3.命令行字符串解析:"la -a -l -i" -> "ls" "-a" "-l" "-i"88 g_argv[0] = strtok(cmd_line, SEP);89 int index = 1;90 if(strcmp(g_argv[0], "ls") == 0)91 {92 g_argv[index++] = "--color=auto";93 }94 if(strcmp(g_argv[0], "ll") == 0)95 {96 g_argv[0] = "ls";97 g_argv[index++] = "-l";98 g_argv[index++] = "--color=auto";99 }100 while(g_argv[index++] = strtok(NULL, SEP));//第二次解析原始字符串,出入NULL101 if(strcmp(g_argv[0], "export") == 0 && g_argv[1] != NULL)102 {103 strcpy(g_myval, g_argv[1]);104 int ret = putenv(g_myval);105 if(ret == 0)106 {107 printf("%s export sucess\n", g_argv[1]);108 }109 }110 //4.让父进程自己执行命令111 if(strcmp(g_argv[0], "cd") == 0)\112 {113 if(g_argv[1] != NULL)114 {115 chdir(g_argv[1]);116 }117 continue;118 }119 //5. fork()120 pid_t id = fork();121 if(id < 0)122 {123 perror("fork()");124 return 1;125 }126 else if(id == 0)127 {128 if(sep != NULL)129 {130 int fd = -1;131 switch(redir_status)132 {133 case INPUT_REDIR:134 fd = open(sep, O_RDONLY);135 dup2(fd, 0);136 break;137 case OUTPUT_REDIR:138 fd = open(sep, O_WRONLY|O_TRUNC|O_CREAT, 0666);139 dup2(fd, 1);140 break;141 case APPEND_REDIR:142 fd = open(sep, O_WRONLY|O_APPEND|O_CREAT, 0666);143 dup2(fd, 1);144 break;145 default:146 printf("bug\n");147 break;148 }149 }150 151 //printf("下面程序是由子进程运行的\n");152 //printf("child, MYVAL:%s\n", getenv("MYVAL"));153 //printf("child, PATH:%s\n", getenv("PATH"));154 execvp(g_argv[0], g_argv);155 exit(1);156 }157 else158 {159 int status = 0; 160 pid_t ret = waitpid(-1, &status, 0);161 if(ret > 0)162 {163 printf("wait sucesss: exit code:%d\n", WEXITSTATUS(status));164 }165 }166 167 }168 return 0;169 }
缓冲区
接下来来看一段神奇的代码:
1 #include <stdio.h>2 #include <unistd.h>3 #include <stdlib.h>4 #include <string.h>5 6 int main()7 {8 //C语言提供的9 printf("hello printf\n");10 fprintf(stdout, "hello fprintf\n");11 const char *s = "hello puts\n";12 fputs(s, stdout);13 14 const char *ss = "hello write\n";15 //操作系统提供的 16 write(1, ss, strlen(ss));17 18 fork();//创建子进程19 return 0;20 }
运行程序,我们会发现一个很神奇的事情:
我们直接运行程序时,会发现打印出来只有4行,但是我们输出重定向到log.txt文件里面以后,打印的却是7行,那么这是为什么呢?
这里就需要我们来了解缓冲区的概念了,仔细观察我们会发现,只有C语言提供的接口函数我们是打印了两次,操作系统提供的就只打印了一次。
首先我们需要知道缓冲区的刷新方式有三种:
- 立即刷新;
- 行刷新;(行缓冲)
- 满刷新。(全缓冲)
我们平时打印到显示器上的内容就属于行刷新,通过对上述程序进行分析,我们就可以知道我们所谓的缓冲区,并不是操作系统为我们提供的,而是C标准库为我们提供的,因为我们可以看见,只有C语言提供的接口函数我们是打印了两次,操作系统提供就只打印了一次,如果是操作系统所提供的,write接口就不会只打印一次了。
为什么fork以后打印到显示器是3次,重定向到log.txt文件时7次呢?
我们平时打印到显示器上的内容就属于行刷新,出现“\n”,就会进行刷新,在fork之前,函数已经被执行完了并且数据已经被刷新了,fork就已经毫无意义了。但是我们重定向到log.txt文件时,就变成向文件打印,此时的刷新策略也就变成了全缓冲,“\n”就没有意义了;此时我们所需要输出的内容就被放在了C标准库为我们提供的缓冲区中,函数已经执行完毕,但数据并没有刷新,fork以后创建子进程,由于进程之间的独立性,此时的数据就会发生写实拷贝,父子进程各自拥有一份,所以C标准库提供的接口函数输出端内容就会被打印两次,这也就是fork以后打印到显示器是3次,重定向到log.txt文件时7次的原因。
我们所谓的缓冲区在底层实际上是存放在一个FILE结构体中的:
此时,我们也就理解了我们的fopen函数到底干了什么了:
fopen函数在上层为用户申请FILE结构体变量,并返回该结构体的地址(FILE*),在底层通过系统接口open打开对应的文件,得到文件描述符fd,并把fd填充到FILE结构体当中的_fileno变量中,至此便完成了文件的打开操作。
而C语言当中的其他文件操作函数,比如fread、fwrite、fputs、fgets等,都是先根据我们传入的文件指针找到对应的FILE结构体,然后在FILE结构体当中找到文件描述符,最后通过文件描述符对文件进行的一系列操作。
模拟实现一个缓冲区
我们要写入数据为例:
1 #include <stdio.h>2 #include <unistd.h>3 #include <stdlib.h>4 #include <string.h>5 #include <sys/stat.h>6 #include <sys/types.h>7 #include <assert.h>8 #include <fcntl.h>9 10 #define NUM 102411 struct MyFILE_12 {13 int fd;14 char buffer[NUM];15 int end;//当前缓冲区结尾16 };17 18 typedef struct MyFILE_ MyFILE;19 20 MyFILE *_fopen(const char* pathname, const char* mode)21 {22 assert(pathname);23 assert(mode);24 25 MyFILE* fp = NULL;26 if(strcmp(mode, "w") == 0)27 {28 int fd = open(pathname, O_WRONLY | O_TRUNC | O_CREAT, 0666);29 if(fd >= 0)30 {31 fp = (MyFILE*)malloc(sizeof(MyFILE));32 memset(fp, 0 , sizeof(MyFILE));33 fp->fd = fd;34 }35 }36 37 return fp;38 }39 40 void _fputs(const char *message, MyFILE *fp)41 {42 assert(message);43 assert(fp);44 45 strcpy(fp->buffer+fp->end, message);46 fp->end += strlen(message);47 48 printf("%s\n", fp->buffer); 49 if(fp->fd == 0)50 {51 //标准输入流52 }53 else if(fp->fd == 1)54 {55 //标准输出流56 if(fp->buffer[fp->end-1] == '\n')57 {58 //fprintf(stderr,"fflush:%s", fp->buffer);59 write(fp->fd, fp->buffer, fp->end);60 61 fp->end = 0;62 }63 }64 else if(fp->fd == 2)65 {66 //标准错误流67 }68 else69 {70 //其他流71 }72 }73 74 void _fflush(MyFILE *fp)75 {76 assert(fp);77 78 if(fp->end != 0)79 {80 write(fp->fd, fp->buffer, fp->end);81 syncfs(fp->fd);82 fp->end = 0;83 }84 85 }86 87 void _flose(MyFILE *fp)88 {89 assert(fp);90 _fflush(fp);91 close(fp->fd);92 free(fp);93 }94 95 int main()96 {97 //close(1);98 MyFILE* fp = _fopen("./log.txt", "w");99 if(fp == NULL)
100 {
101 perror("_fopen");
102 return 1;
103 }
104
105 _fputs("one:hello puts", fp);
106 _fputs(" two:hello puts", fp);
107 _fputs(" three:hello puts\n", fp);
108 //fork();
109
110 _flose(fp);
111 return 0;
112 }
运行程序就会发现:
未fork之前,我们log.txt文件中所有内容只会打印一次,fork以后,内容均打印两次:
fflush
有了缓冲区的概念,我们就可以很好的理解fflush了:
1 #include<stdio.h>2 #include<sys/types.h>3 #include<sys/stat.h>4 #include<fcntl.h>5 #include<unistd.h>6 7 8 int main()9 {10 close(1);11 int fd = open("log.txt", O_WRONLY|O_TRUNC|O_CREAT, 0666);12 if(fd < 0)13 {14 perror("open");15 return 1;16 }17 18 printf("hello world:%d\n", fd);19 //fflush(stdout); 20 21 close(fd);22 return 0;23 }
当我们关闭close(1)以后,将hello world写入stdout缓冲区中,但是尽管此时存在‘\n’,也不会刷新出来,随后我们在close(fd),数据在缓冲区中,相应的fd已经关了,数据就刷新不出来了,但是我们使用了fflush以后,存储在缓冲区的数据立即会刷新出来,log.txt文件中也就会打印出来。
理解文件系统
初识inode
磁盘文件由两部分构成,分别是文件内容和文件属性。文件内容就是文件当中存储的数据,文件属性就是文件的一些基本信息,例如文件名、文件大小以及文件创建时间等信息都是文件属性,文件属性又被称为元信息。
在命令行当中输入ls -l,即可显示当前目录下各文件的属性信息。
在Linux操作系统中,文件的元信息和内容是分离存储的,其中保存元信息的结构称之为inode,因为系统当中可能存在大量的文件,所以我们需要给每个文件的属性集起一个唯一的编号,即inode号。
也就是说,inode是一个文件的属性集合,Linux中几乎每个文件都有一个inode,为了区分系统当中大量的inode,我们为每个inode设置了inode编号。
磁盘的概念
磁盘是一种永久性存储介质,在计算机中,磁盘几乎是唯一的机械设备。与磁盘相对应的就是内存,内存是掉电易失存储介质,目前所有的普通文件都是在磁盘中存储的。
磁盘在冯诺依曼体系结构当中既可以充当输入设备,又可以充当输出设备。
我们如何来确定信息在磁盘中的读写位置呢?
大概分为三步:
- 先找到在哪一个面上;
- 在找到在哪一个磁道上;
- 最后找到在哪一个扇区上。
磁盘分区与格式化介绍
理解文件系统,首先我们必须将磁盘想象成一个线性的存储介质,想想磁带,当磁带被卷起来时,其就像磁盘一样是圆形的,但当我们把磁带拉直后,其就是线性的。
磁盘分区
磁盘通常被称为块设备,一般以扇区为单位,一个扇区的大小通常为512字节。我们若以大小为512G的磁盘为例,该磁盘就可被分为十亿多个扇区。
计算机为了更好的管理磁盘,于是对磁盘进行了分区。磁盘分区就是使用分区编辑器在磁盘上划分几个逻辑部分,盘片一旦划分成数个分区,不同的目录与文件就可以存储进不同的分区,分区越多,就可以将文件的性质区分得越细,按照更为细分的性质,存储在不同的地方以管理文件。
磁盘格式化
当磁盘完成分区后,我们还需要对磁盘进行格式化。磁盘格式化就是对磁盘中的分区进行初始化的一种操作,这种操作通常会导致现有的磁盘或分区中所有的文件被清除。
简单来说,磁盘格式化就是对分区后的各个区域写入对应的管理信息。
EXT2文件系统的存储方案
计算机为了更好的管理磁盘,会对磁盘进行分区。而对于每一个分区来说,分区的头部会包括一个启动块(Boot Block),对于该分区的其余区域,EXT2文件系统会根据分区的大小将其划分为一个个的块组(Block Group)。
其次,每个组块都有着相同的组成结构,每个组块都由超级块(Super Block)、块组描述符表(Group Descriptor Table)、块位图(Block Bitmap)、inode位图(inode Bitmap)、inode表(inode Table)以及数据表(Data Block)组成。
注意: 启动块的大小是确定的,而块组的大小是由格式化的时候确定的,并且不可以更改。
- Super Block: 存放文件系统本身的结构信息。记录的信息主要有:Data Block和inode的总量、未使用的Data Block和inode的数量、一个Data Block和inode的大小、最近一次挂载的时间、最近一次写入数据的时间、最近一次检验磁盘的时间等其他文件系统的相关信息。Super Block的信息被破坏,可以说整个文件系统结构就被破坏了。
- Group Descriptor Table: 块组描述符表,描述该分区当中块组的属性信息。
- Block Bitmap: 块位图当中记录着Data Block中哪个数据块已经被占用,哪个数据块没有被占用。
- inode Bitmap: inode位图当中记录着每个inode是否空闲可用。
- inode Table: 存放文件属性,即每个文件的inode。
- Data Blocks: 存放文件内容。
注意:
- 其他块组当中可能会存在冗余的Super Block,当某一Super Block被破坏后可以通过其他Super Block进行恢复。
- 磁盘分区并格式化后,每个分区的inode个数就确定了。
创建文件,系统做了什么?
- 通过遍历inode位图的方式,找到一个空闲的inode;
- 在inode表当中找到对应的inode,并将文件的属性信息填充进inode结构中;
- 将该文件的文件名和inode指针添加到目录文件的数据块中。
删除文件,系统做了什么?
- 将该文件对应的inode在inode位图当中置为无效。
- 将该文件申请过的数据块在块位图当中置为无效。
为什么拷贝文件的时候很慢,而删除文件的时候很快?
因为拷贝文件需要先创建文件,然后再对该文件进行写入操作,该过程需要先申请inode号并填入文件的属性信息,之后还需要再申请数据块号,最后才能进行文件内容的数据拷贝,而删除文件只需将对应文件的inode号和数据块号置为无效即可,无需真正的删除文件,因此拷贝文件是很慢的,而删除文件是很快的。
如何理解目录?
Linux下一切皆文件,目录也是文件,他也要自己的inode,data block,它的data block中会存放文件名与相应的inode编号的映射关系,而目录的inode结构中存储了目录的属性信息,比如比如目录的大小、目录的拥有者。
软硬链接
软链接
我们通过以下命令可以来创建一个软链接:
[gtt@VM-28-16-centos study_9_4]$ ln -s mytest mytest-s
我们可以发现,软链接所产生的文件与源文件的inode编号是不同的,软链接产生的文件大小比源文件要小得多
软链接又叫做符号链接,软链接文件相对于源文件来说是一个独立的文件,该文件有自己的inode号,但是该文件只包含了源文件的路径名,所以软链接文件的大小要比源文件小得多。软链接就类似于Windows操作系统当中的快捷方式。
但是软链接文件只是其源文件的一个标记,当删除了源文件后,链接文件不能独立存在,虽然仍保留文件名,但却不能执行或是查看软链接的内容了。
硬链接
我们通过以下命令可以来创建一个硬链接:
[gtt@VM-28-16-centos study_9_4]$ ln mytest mytest-h
我们查看文件inode编号,会发现硬链接产生的文件与源文件的inode编号是一样的,并且硬链接文件的大小与源文件的大小也是相同的,特别注意的是,当创建了一个硬链接文件后,该硬链接文件和源文件的硬链接数都变成了2。
硬链接文件就是源文件的一个别名,一个文件有几个文件名,该文件的硬链接数就是几,这里inode号为1053442的文件有mytest和mytest-h两个文件名,因此该文件的硬链接数为2。
与软连接不同的是,当硬链接的源文件被删除后,硬链接文件仍能正常执行,只是文件的链接数减少了一个,因为此时该文件的文件名少了一个。
我们需要注意的是硬链接就是让多个不在或者同在一个目录下的文件名,同时能够修改同一个文件,其中一个修改后,所有与其有硬链接的文件都一起修改了。
为什么刚刚创建的目录的硬链接数是2?
因为每个目录创建后,该目录下默认会有两个隐含文件.和…,它们分别代表当前目录和上级目录,因此这里创建的目录有两个名字,一个是dir另一个就是该目录下的.,所以刚创建的目录硬链接数是2。通过命令我们也可以看到dir和该目录下的.的inode号是一样的,也就可以说明它们代表的实际上是同一个文件。
软硬链接的区别:
- 软链接是一个独立的文件,有独立的inode,而硬链接没有独立的inode。
- 软链接相当于快捷方式,硬链接本质没有创建文件,只是建立了一个文件名和已有的inode的映射关系,并写入当前目录。
文件的三个时间
在Linux当中,我们可以使用命令stat 文件名来查看对应文件的信息:
这其中包含了文件的三个时间信息:
- Access: 文件最后被访问的时间。
- Modify: 文件内容最后的修改时间。
- Change: 文件属性最后的修改时间。
当我们修改文件内容时,文件的大小一般也会随之改变,所以一般情况下Modify的改变会带动Change一起改变,但修改文件属性一般不会影响到文件内容,所以一般情况下Change的改变不会带动Modify的改变。
我们若是想将文件的这三个时间都更新到最新状态,可以使用命令touch 文件名来进行时间更新。