【Linux详解】——文件基础(I/O、文件描述符、重定向、缓冲区)
📖 前言:本期介绍文件基础I/O。
目录
- 🕒 1. 文件回顾
- 🕘 1.1 基本概念
- 🕘 1.2 C语言文件操作
- 🕤 1.2.1 概述
- 🕤 1.2.2 实操
- 🕤 1.2.3 OS接口open的使用(比特位标记)
- 🕤 1.2.4 写入操作
- 🕤 1.2.5 追加操作
- 🕤 1.2.6 只读操作
- 🕒 2. 文件的进一步理解
- 🕘 2.1 提出问题
- 🕘 2.2 文件描述符fd
- 🕒 3. 文件fd的分配规则
- 🕒 4. 重定向
- 🕘 4.1 什么是重定向
- 🕘 4.2 dup2(系统调用的重定向)
- 🕤 4.2.1 追加重定向
- 🕤 4.2.2 输入重定向
- 🕤 4.2.3 模拟实现
- 🕒 5. 如何理解Linux一切皆文件
- 🕒 6. 缓冲区
- 🕘 6.1 C接口打印两次的现象
- 🕘 6.2 理解缓冲区问题
- 🕤 6.2.1 为什么要有缓冲区
- 🕤 6.2.2 缓冲区刷新的策略
- 🕤 6.2.3 缓冲区在哪里
- 🕘 6.3 解释打印两次的现象
- 🕘 6.4 模拟实现
- 🕘 6.5 缓冲区与OS的关系
🕒 1. 文件回顾
🕘 1.1 基本概念
- 空文件也要在磁盘占据空间
- 文件 = 内容 + 属性
- 文件操作 = 对内容+对属性+对内容和属性的操作
- 标定一个问题,必须使用:文件路径+文件名【唯一性】
- 如果没有指明对应的文件路径,默认是在当前路径(进程当前路径)进行文件访问
- 当我们把fopen、fclose、fread、fwrite等接口写完之后,代码编译之后,形成二进制可执行程序之后,但是没有运行,文件对应的操作有没有被执行呢?没有,对文件的操作的本质是进程对文件的操作,因此没有运行不存在进程,所以不会被执行。
- 一个文件要被访问,就必须先被打开!(打开的方式:用户进程+OS)
那是不是所有磁盘的文件都被打开呢?显然不是这样!因此我们可以将文件划分成两种:a.被打开的文件;b.没有被打开的文件 。对于文件操作,一定是被打开的文件才能进行操作,本篇文章只会讲解被打开的文件。
文件操作的本质:进程和被打开文件的关系
🕘 1.2 C语言文件操作
🕤 1.2.1 概述
- 语言(如C++、Java、Python)都有文件操作接口,他们实际上的底层都是相同的函数接口,因为都需要通过OS调用。
- 文件在磁盘上,磁盘是硬件,只有操作系统有资格访问,所有人想访问磁盘都不能绕过操作系统,必须使用操作系统调用的接口,即OS会提供文件级别的系统调用接口。
- 所以上层语言无论如何变化,库函数底层必须调用系统调用接口。
- 库函数可以千变万化,但是底层不变,因此这样能够降低学习成本学习不变的东西。
🕤 1.2.2 实操
下面fp按顺序对应以下三个操作依次:写入文件、打印文本信息、追加文本信息到文件中。
#include <stdio.h>
#include <unistd.h>
#include <string.h>int main()
{// 没有指明路径就是在当前路径下创建// r,w, r+(读写,不存在出错),w+(读写, 不存在创建), a(append, 追加), a+()// FILE* fp = fopen(FILE_NAME, "w"); // FILE *fp = fopen(FILE_NAME, "r");FILE *fp = fopen(FILE_NAME, "a"); // if(NULL == fp)// {// perror("fopen"); // return 1;// }//// char buffer[64];// while(fgets(buffer, sizeof(buffer) - 1, fp) != NULL)// {// buffer[strlen(buffer) - 1] = 0; // 消除\n// puts(buffer);// }int cnt = 5;while(cnt){fprintf(fp, "%s:%d\n", "hello world", cnt--);}fclose(fp);return 0;
}
注:以w方式单纯的打开文件,c会自动清空内部的数据。
对于C语言调用的fopen打开文件,实际上底层调用的是操作系统的接口open,其他语言也是这样,只不过语言级别的接口是多了一些特性,接下来就看看手册内容:
对于flag标记位,一般来说对于C语言,一个int类型代表一个标记位,那如果要传10个标记位呢?对于整型来说,实际上有32个比特位,那是不是可以将每一个比特位赋予特定的含义,通过比特位传递选项,从而实现对应的标记呢?一定是可以的。因此在介绍open函数之前,先来介绍一下标记位的实现:
注意:一个比特位一个选项,不能重复。(标记位传参)
#include <stdio.h>
#include <unistd.h>
#include <string.h>
// 每一个宏,对应的数值,只有一个比特位是1,彼此位置不重叠
#define ONE (1<<0) // 0x1
#define TWO (1<<1) // 0x2
#define THREE (1<<2) // 0x4
#define FOUR (1<<3) // 0x8void show(int flags)
{if(flags & ONE) printf("one\n");if(flags & TWO) printf("two\n");if(flags & THREE) printf("three\n");if(flags & FOUR) printf("four\n");
}int main()
{show(ONE);printf("-----------------------\n");show(TWO);printf("-----------------------\n");show(ONE | TWO);printf("-----------------------\n");show(ONE | TWO | THREE);printf("-----------------------\n");show(ONE | TWO | THREE | FOUR);printf("-----------------------\n");}
[hins@VM-12-13-centos file]$ ./myfile
one
-----------------------
two
-----------------------
one
two
-----------------------
one
two
three
-----------------------
one
two
three
four
-----------------------
因此我们再看这个open函数,就明白了是什么含义,就是通过不同的flags,传入不同的标记位,那接下来看看open函数怎么用:
🕤 1.2.3 OS接口open的使用(比特位标记)
int open(const char* pathname, int flags )
int open(const char* pathname, int flags, mode_t mode )
第一个函数是在文件已经存在的基础上使用的,如果不存在源文件,那么就需要用第二个函数,即第二个函数如果文件不存在就会自动创建文件。
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>// 注意并没有指明路径
#define FILE_NAME "log.txt"int main()
{umask(0); // 将系统继承给此进程的掩码设置为0,防止影响此进程int fd = open(FILE_NAME/*文件路径*/, O_WRONLY/*标记位*/ | O_CREAT, 0666/*权限*/); // WRONLY:只写if (fd < 0){perror("open");return 1;}printf("%d\n", fd);close(fd); // close也是系统接口return 0;
}
[hins@VM-12-13-centos file]$ ./myfile
3 # 下节会讲这个值
[hins@VM-12-13-centos file]$ ll
total 20
-rw-rw-rw- 1 hins hins 0 Feb 16 21:53 log.txt
# 权限:0666
🕤 1.2.4 写入操作
对于C语言来讲,除了打开关闭,还有写入fwrite等函数接口,因此对于OS也存在一个接口:write
无论这个buf是什么类别,在OS看来都是二进制!至于这个类别是文本还是图片,都是由语言本身决定的。
int printf(const char *format, ...);
int fprintf(FILE *stream, const char *format, ...); // 将特定的format格式化形成字符串放在str里面
int sprintf(char *str, const char *format, ...);
int snprintf(char *str, size_t size, const char *format, ...);
下面是个例子:
int main()
{umask(0); // 将系统继承给此进程的掩码设置为0,防止影响此进程int fd = open(FILE_NAME/*文件路径*/, O_WRONLY/*标记位*/ | O_CREAT, 0666/*权限*/); // WRONLY:只写if (fd < 0){perror("open");return 1;}int cnt = 5;char outBuffer[64];while(cnt){sprintf(outBuffer, "%s:%d\n", "Hello", cnt--); // outBuffer相当于写入的缓冲区// 以\0作为字符串的结尾,是C语言的规定,与文件无关write(fd, outBuffer, strlen(outBuffer)); //向文件中写入string的时候,长度+1是C语言的规定}printf("%d\n", fd);close(fd); // close也是系统接口return 0;
}
[hins@VM-12-13-centos file]$ ./myfile
[hins@VM-12-13-centos file]$ cat log.txt
Hello:5
Hello:4
Hello:3
Hello:2
Hello:1
可以看出,对于C语言中的w
,封装了文件接口的标识符:O_WRONLY
(写)、O_CREAT
(不存在就创建文件)、O_TRUNC
(清空文件),以及权限。
🕤 1.2.5 追加操作
想要把清空变成追加,只需要将open内部的最后一个清空标识符改成追加的标识符:
int fd = open(FILE_NAME, O_WRONLY | O_CREAT | O_APPEND, 0666);
🕤 1.2.6 只读操作
int fd = open(FILE_NAME, O_RDONLY);
int main()
{umask(0); // 将系统继承给此进程的掩码设置为0,防止影响此进程int fd = open(FILE_NAME, O_RDONLY); if (fd < 0){perror("open");return 1;}char buffer[1024];ssize_t num = read(fd, buffer, sizeof(buffer)-1);if(num > 0) buffer[num] = 0; // 0, '\0', NULL -> 0printf("%s", buffer);close(fd); // close也是系统接口return 0;
}
小结:
系统调用接口 | 对应的C语言库函数接口 |
---|---|
open | fopen |
close | fclose |
write | fwrite |
read | fread |
lseek | fseek |
O_RDONLY //只读打开
O_WRONLY //只写打开
O_RDWR //读写打开
//以上三个常亮,必须且只能指定一个O_CREAT //若文件不存在则创建文件
O_APPEND //追加写入
// 如果想要多个选项一起,那就使用 | 运算符即可。
对应不同功能的打开方式:
文件使用方式 | 含义 | 如果指定的文件不存在 |
---|---|---|
“r”(只读) | 为了输入数据,打开一个已经存在的文本文件 | 出错 |
“w”(只写) | 为了输出数据,打开一个文本文件 | 建立一个新的文件 |
“a”(追加) | 向文本文件尾添加数据 | 建立一个新的文件 |
“rb”(只读) | 为了输入数据,打开一个二进制文件 | 出错 |
“wb”(只写) | 为了输出数据,打开一个二进制文件 | 建立一个新的文件 |
“ab”(追加) | 向一个二进制文件尾添加数据 | 出错 |
“r+”(读写) | 为了读和写,打开一个文本文件 | 出错 |
“w+”(读写) | 为了读和写,建议一个新的文件 | 建立一个新的文件 |
“a+”(读写) | 打开一个文件,在文件尾进行读写 | 建立一个新的文件 |
“rb+”(读写) | 为了读和写打开一个二进制文件 | 出错 |
“wb+”(读写) | 为了读和写,新建一个新的二进制文件 | 建立一个新的文件 |
“ab+”(读写) | 打开一个二进制文件,在文件尾进行读和写 | 建立一个新的文件 |
即库函数接口是封装了系统调用接口的,所有语言的库函数都存在系统调用的影子。
🕒 2. 文件的进一步理解
文件操作的本质:进程和被打开文件的关系
🕘 2.1 提出问题
进程可以打开多个文件,那是不是意味着系统中一定会存在大量的被打开的文件,被打开的文件要不要被操作系统管理起来呢?答案是一定的。
那么OS如何管理呢? 先描述,再组织。因此操作系统为了管理对应的打开文件,必定要为文件创建对应的内核数据结构标识文件:struct file{}
(包含了文件的大部分属性)因此将结构体链式链接,通过找到链表的首地址从而实现对链表内容的增删查改。
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>// 注意并没有指明路径
#define FILE_NAME(number) "log.txt"#number
// 将括号内部的值转换成字符串,两个字符串通过#具有自动连接特性int main()
{umask(0); // 将系统继承给此进程的掩码设置为0,防止影响此进程int fd0 = open(FILE_NAME(1), O_WRONLY | O_CREAT | O_APPEND, 0666);int fd1 = open(FILE_NAME(2), O_WRONLY | O_CREAT | O_APPEND, 0666);int fd2 = open(FILE_NAME(3), O_WRONLY | O_CREAT | O_APPEND, 0666);int fd3 = open(FILE_NAME(4), O_WRONLY | O_CREAT | O_APPEND, 0666);int fd4 = open(FILE_NAME(5), O_WRONLY | O_CREAT | O_APPEND, 0666);printf("fd: %d\n", fd0);printf("fd: %d\n", fd1);printf("fd: %d\n", fd2);printf("fd: %d\n", fd3);printf("fd: %d\n", fd4);close(fd0);close(fd1);close(fd2);close(fd3);close(fd4);}
创建多个文件并打印其返回值:
[hins@VM-12-13-centos file]$ ./myfile
fd: 3
fd: 4
fd: 5
fd: 6
fd: 7
[hins@VM-12-13-centos file]$ ll
total 24
-rw-rw-rw- 1 hins hins 0 Feb 17 11:47 log.txt1
-rw-rw-rw- 1 hins hins 0 Feb 17 11:47 log.txt2
-rw-rw-rw- 1 hins hins 0 Feb 17 11:47 log.txt3
-rw-rw-rw- 1 hins hins 0 Feb 17 11:47 log.txt4
-rw-rw-rw- 1 hins hins 0 Feb 17 11:47 log.txt5
-rw-rw-r-- 1 hins hins 64 Feb 16 21:25 Makefile
-rwxrwxr-x 1 hins hins 8512 Feb 17 11:47 myfile
-rw-rw-r-- 1 hins hins 2295 Feb 17 11:46 myfile.c
现在就有个问题:为什么从3开始连续变化呢?
首先我们需要了解三个标准的输入输出流:stdin
(键盘),stdout
(显示器),stderr
(显示器)
FILE* fp = fopen();
这个FILE实际上是一个结构体,而对于上面的三个输入输出流,实际上也是FILE的结构体:
extern FILE *stdin;
extern FILE *stdout;
extern FILE *stderr;
对于这个结构体必有一个字段–>文件描述符,下面就看一下这个文件描述符的值是什么:
🕘 2.2 文件描述符fd
通过对open函数的学习,我们知道了文件描述符就是一个小整数,即open的返回值。
int main()
{printf("stdin->fd: %d\n", stdin->_fileno);printf("stdout->fd: %d\n", stdout->_fileno);printf("stderr->fd: %d\n", stderr->_fileno);......
[hins@VM-12-13-centos file]$ ./myfile
stdin->fd: 0
stdout->fd: 1
stderr->fd: 2
fd: 3
fd: 4
fd: 5
fd: 6
fd: 7
因此这也就解释了为什么文件描述符默认是从3开始的,因为0,1,2默认被占用。我们的C语言的这批接口封装了系统的默认调用接口。同时C语言的FILE结构体也封装了系统的文件描述符。
那为什么是0,1,2,3,4,5……呢?下面就来解释:
PCB中包含一个files指针,它指向一个属于进程和文件对应关系的一个结构体:struct files_struct
,而这个结构体里面包含了一个数组叫做struct file* fd _array[]
的指针数组,因此如图前三个0、1、2被键盘和显示器调用,这也就是为什么之后的文件描述符是从3开始的,然后将文件的地址填入到3号文件描述符里,此时3号文件描述符就指向这个新打开的文件了。
再把3号描述符通过系统调用给用户返回,所以在一个进程访问文件时,需要传入3,通过系统调用找到对应的文件描述符表,从而通过存储的地址找到对应的文件,文件找到了,就可以对文件进行操作了。因此文件描述符的本质就是数组下标。
而现在知道,文件描述符就是从0开始的小整数。当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件。于是就有了file结构体。表示一个已经打开的文件对象。而进程执行open系统调用,所以必须让进程和文件关联起来。每个进程都有一个指针*files
, 指向一张表files_struct
,该表最重要的部分就是包含一个指针数组,每个元素都是一个指向打开文件的指针。所以,本质上,文件描述符就是该数组的下标。所以,只要拿着文件描述符,就可以找到对应的文件。
🕒 3. 文件fd的分配规则
#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); // fd: 0//close(2); // fd: 2int fd = open("myfile", O_RDONLY);if(fd < 0){perror("open");return 1;}printf("fd: %d\n", fd);close(fd);return 0;
}
因此,我们知道了,文件fd的分配规则就是将这个array数组从小到大,按照循序寻找最小的且没有被占用的fd,这就是fd的分配规则。
🕒 4. 重定向
🕘 4.1 什么是重定向
对于上面的例子,我们关闭了文件描述符0和2对应的文件吗,那么如果关闭1呢?
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
int main()
{umask(0);close(1);int fd = open("log.txt", O_WRONLY|O_CREAT|O_TRUNC, 0666);if(fd < 0){perror("open");return 1;}printf("open fd: %d\n", fd); // 往stdout输出fprintf(stdout, "fd :%d\n", fd);// 与上面打印是一样的功能close(fd);return 0;
}
运行发现什么都没输出,这时我们刷新一下缓冲区,发现输出到文件上了。
fflush(stdout);
明明应该输出到显示器上,为什么会变成文件呢?
如图,可以看到由于文件描述符1所连接的stdout
被关闭,文件的fd发现1的位置是空着的,于是将这个新创建的文件log.txt
与对应的指针进行连接:
重定向的本质:上层用的fd不变,在内核中更改fd对应的struct file*的地址。
常见的重定向有:>(输入), >>(追加), <(输出)。
🕘 4.2 dup2(系统调用的重定向)
int dup2(int oldfd, int newfd);
// newfd的内容最终会被oldfd指向的内容覆盖
dup2的返回值也就是fd的文件描述符,失败返回-1
接下来修改一下刚刚的代码
...
// close(1);
dup2(fd, 1);
...
cat log.txt
,输出:
open fd: 3
open fd: 3
可以发现,这样操作简化了刚才的操作,另外,fd的值也不会被改变。
输出重定向演示完了,那我们就可以实现我们刚才提到的三个重定向剩下的追加、输入重定向了。
🕤 4.2.1 追加重定向
int main()
{umask(0);int fd = open("log.txt", O_WRONLY|O_CREAT|O_APPEND, 0666);if(fd < 0){perror("open");return 1;}dup2(fd, 1);printf("open fd: %d\n", fd); // 往stdout输出fprintf(stdout, "fd :%d\n", fd);// 与上面打印是一样的功能const char *msg= "hello world";write(1, msg, strlen(msg));fflush(stdout);close(fd);return 0;
}
cat log.txt
,输出:
open fd: 3
open fd: 3
hello world
🕤 4.2.2 输入重定向
int main()
{umask(0);int fd = open("log.txt", O_RDONLY);if(fd < 0){perror("open");return 1;}char line[64];while(1){printf("> "); if(fgets(line, sizeof(line), stdin) == NULL) break; //stdin->0printf("%s", line);}fflush(stdout);close(fd);return 0;
}
[hins@VM-12-13-centos file]$ ./myfile
>hello
hello
>world
world
注:在Linux中,Ctrl+D
表示文件结尾。
上面是从键盘中读取,如果不想从键盘读,我们可以重定向到向指定文件中读取:
dup2(fd, 0); // 输入重定向
[hins@VM-12-13-centos file]$ ./myfile
open fd: 3
open fd: 3
hello world
🕤 4.2.3 模拟实现
接下来,我们完善上期的myshell.c
,把这三个重定向加上,完整版本如下:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <ctype.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/wait.h>
#include <assert.h>
#include <errno.h>#define NUM 1024
#define OPT_NUM 64#define NONE_REDIR 0
#define INPUT_REDIR 1
#define OUTPUT_REDIR 2
#define APPEND_REDIR 3#define trimSpace(start) do{\while(isspace(*start)) ++start;\}while(0)char lineCommand[NUM];
char *myargv[OPT_NUM]; //指针数组
int lastCode = 0;
int lastSig = 0;int redirType = NONE_REDIR;
char *redirFile = NULL;// "ls -a -l -i > myfile.txt" -> "ls -a -l -i" "myfile.txt" ->
void commandCheck(char *commands)
{assert(commands);char *start = commands;char *end = commands + strlen(commands);while(start < end){if(*start == '>'){*start = '\0';start++;if(*start == '>'){// "ls -a >> file.log"redirType = APPEND_REDIR;start++;}else{// "ls -a > file.log"redirType = OUTPUT_REDIR;}trimSpace(start); // 过滤空格redirFile = start;break;}else if(*start == '<'){//"cat < file.txt"*start = '\0';start++;trimSpace(start);// 填写重定向信息redirType = INPUT_REDIR;redirFile = start;break;}else{start++;}}
}int main()
{while(1){redirType = NONE_REDIR;redirFile = NULL;errno = 0;// 输出提示符printf("用户名@主机名 当前路径# ");fflush(stdout);// 获取用户输入, 输入的时候,输入\nchar *s = fgets(lineCommand, sizeof(lineCommand)-1, stdin);assert(s != NULL);(void)s;// 清除最后一个\n , abcd\nlineCommand[strlen(lineCommand)-1] = 0; // ?//printf("test : %s\n", lineCommand);// "ls -a -l -i" -> "ls" "-a" "-l" "-i" -> 1->n// "ls -a -l -i > myfile.txt" -> "ls -a -l -i" "myfile.txt" ->// "ls -a -l -i >> myfile.txt" -> "ls -a -l -i" "myfile.txt" ->// "cat < myfile.txt" -> "cat" "myfile.txt" ->commandCheck(lineCommand);// 字符串切割myargv[0] = strtok(lineCommand, " ");int i = 1;if(myargv[0] != NULL && strcmp(myargv[0], "ls") == 0){myargv[i++] = (char*)"--color=auto";}// 如果没有子串了,strtok->NULL, myargv[end] = NULLwhile(myargv[i++] = strtok(NULL, " "));// 如果是cd命令,不需要创建子进程,让shell自己执行对应的命令,本质就是执行系统接口// 像这种不需要让我们的子进程来执行,而是让shell自己执行的命令 --- 内建/内置命令if(myargv[0] != NULL && strcmp(myargv[0], "cd") == 0){if(myargv[1] != NULL) chdir(myargv[1]);continue;}if(myargv[0] != NULL && myargv[1] != NULL && strcmp(myargv[0], "echo") == 0){if(strcmp(myargv[1], "$?") == 0){printf("%d, %d\n", lastCode, lastSig);}else{printf("%s\n", myargv[1]);}continue;}// 测试是否成功, 条件编译
#ifdef DEBUGfor(int i = 0 ; myargv[i]; i++){printf("myargv[%d]: %s\n", i, myargv[i]);}
#endif// 内建命令 --> echo// 执行命令pid_t id = fork();assert(id != -1);if(id == 0){// 因为命令是子进程执行的,真正重定向的工作一定要是子进程来完成// 如何重定向,是父进程要给子进程提供信息的// 这里重定向会影响父进程吗?不会,进程具有独立性switch(redirType){case NONE_REDIR:// 什么都不做break;case INPUT_REDIR:{int fd = open(redirFile, O_RDONLY);if(fd < 0){perror("open");exit(errno);}// 重定向的文件已经成功打开了dup2(fd, 0);}break;case OUTPUT_REDIR:case APPEND_REDIR:{umask(0);int flags = O_WRONLY | O_CREAT;if(redirType == APPEND_REDIR) flags |= O_APPEND;else flags |= O_TRUNC;int fd = open(redirFile, flags, 0666);if(fd < 0){perror("open");exit(errno);}dup2(fd, 1);}break;default:printf("bug?\n");break;}execvp(myargv[0], myargv); // 执行程序替换的时候,会不会影响曾经进程打开的重定向的文件?不会exit(1);}int status = 0;pid_t ret = waitpid(id, &status, 0);assert(ret > 0);(void)ret;lastCode = ((status>>8) & 0xFF);lastSig = (status & 0x7F);}
}
注:文件是共享的,不会因为进程不同而权限不同,因为文件是磁盘上的,与进程之间是独立的。即当子进程被创建并且发生写时拷贝时,原来的文件并不会再次被拷贝一次。
🕒 5. 如何理解Linux一切皆文件
我们利用虚拟文件系统就可以摒弃掉底层设备之间的差别,统一使用文件接口的方式进行文件操作。
文件的引用计数:🔎 Linux文件引用计数的逻辑
🕒 6. 缓冲区
🕘 6.1 C接口打印两次的现象
#include<stdio.h>
#include<string.h>
#include<unistd.h>
int main()
{ //C接口 printf("hello printf"); fprintf(stdout, "hello fprintf\n"); const char* fputsString = "hello fputs\n"; fputs(fputsString, stdout); //系统接口 const char* wstring = "hello write\n"; write(1, wstring, strlen(wstring)); return 0;
}
[hins@VM-12-13-centos buffer]$ ./myfile
hello printf
hello fprintf
hello fputs
hello write
当在代码最后添加一个fork()
后:
[hins@VM-12-13-centos buffer]$ ./myfile > log.txt
[hins@VM-12-13-centos buffer]$ cat log.txt
hello write
hello printf
hello fprintf
hello fputs
hello printf
hello fprintf
hello fputs
直接运行仍是正常的现象,但当重定向到log.txt中,C接口的打印了两次,这是什么原因呢?
🕘 6.2 理解缓冲区问题
缓冲区本质就是一段内存
🕤 6.2.1 为什么要有缓冲区
下面有个场景:
小陈和一个网友相聊甚欢,一天小陈想给对方一份特产,但是小陈人在广州,对方在四川,如果小陈亲自去送,会占用小陈大量的时间,而且也不现实,所以为了不占用小陈自己的时间,就把包裹送到快递公司让其送到对方手里。
现实生活中,快递行业的意义就是节省发送者的时间,而对于这个例子来说,广州就相当于内存,发送者小陈相当于进程,包裹就是进程需要发送的数据,四川就相当于磁盘,对方就是磁盘上的文件,那么可以看成这样:
在冯诺依曼体系中,我们知道内存直接访问磁盘这些外设的速度是相对较慢的,即正如我们所举的例子一样,小陈亲自送包裹会占用他大量的时间,因此顺丰同样属于内存中开辟的一段空间,将我们在内存中已有的数据拷贝到这段空间中,拷贝函数就直接返回了,即小陈接收到顺丰的通知就离开了。在执行你的代码期间,顺丰对应的内存空间的数据也就是包裹就会不断的发送给对方,即发送给磁盘。而这个过程中,顺丰这块开辟的空间就相当于缓冲区。
那么缓冲区的意义是什么呢?——节省进程进行数据IO的时间。
在上述的过程中,拷贝是什么,我们在fwrite的时候没有拷贝啊?因此我们需要重新理解fwrite这个函数,与其理解fwrite是写入到文件的函数,倒不如理解fwrite是拷贝函数,将数据从进程拷贝到“缓冲区”或者外设中!
那我们送的包裹何时会发送出去呢?即我们的数据什么时候会到磁盘中呢?这就涉及到缓冲区刷新策略的问题:
🕤 6.2.2 缓冲区刷新的策略
还是上面的情景,小陈的包裹送到了顺丰,但是当小陈再次来到顺丰邮寄另一个包裹时,发现之前的包裹还在那里放着,于是小陈感到不满,而工作人员此时解释道:我们的快递是通过飞机运的,如果只送你这一件包裹,路费都不够!因此可以看出,快递不是即送即发,也就是说数据不是直接次写入外设的。
那么如果有一块数据,一次写入到外设,还是少量多次的效率高呢?
一定是一次写入最高。一块数据写入到外设,需要外设准备,如果多次写入外设,每一次外设进行的准备都会占用时间,而积攒到一定程度一次发送到外设,外设的准备次数就会大幅减少,效率也会提高。因此,为了在不同设备的效率都是最合适的,缓冲区一定会结合具体的设备,定制自己的刷新策略:
- 立即刷新,无缓冲
- 行刷新,行缓冲(显示器)\n就会刷新,比如_exit和exit
- 缓冲区满,全缓冲 (磁盘文件)
当然还有两种特殊情况
- 用户强制刷新:fflush
- 进程退出 ——>进程退出都要进行缓冲区刷新
🕤 6.2.3 缓冲区在哪里
文章开始时我们提到了C语言接口打印两次的现象,毫无疑问,我们能够从中获得以下信息:
- 这种现象一定和缓冲区有关
- 缓冲区一定不在内核中(如果在内核中,write也应该打印两次)
因此我们之前谈论的所有的缓冲区,都指的是用户级语言层面给我们提供的缓冲区。这个缓冲区在stdout,stdin,stderr->FILE*
,FILE作为结构体,其不仅包括fd,缓冲区也在这个结构体中。所以我们自己要强制刷新的时候,fflush传入的一定是文件指针,fclose也是如此,即:fflush(文件指针),fclose(文件指针)
通过查看:vim /usr/include/libio.h
typedef struct _IO_FILE FILE;
在/usr/include/stdio.h
因此我们所调用的fscanf,fprintf,fclose
等C语言的文件函数,传入文件指针时,都会把相应的数据拷贝到文件指针指向的文件结构体中的缓冲区中。
即缓冲区也可以看做是一块内存,对于内存的申请:无非就是malloc new出来的。
小结:
缓冲区(语言级别)是用户申请的(底层通过malloc/new);缓冲区属于FILE结构体;为了节省进程进行IO的时间
🕘 6.3 解释打印两次的现象
有了缓冲区的理解,现在就足以解释打印两次的现象:
由于代码结束之前,进行创建子进程:
-
如果我们不进行重定向,看到四条消息
stdout默认使用的是行刷新,在进程进行fork之前,三条C函数已经将数据进行打印输出到显示器上(外设),也就是说FILE内部的缓冲区不存在对应的数据。 -
如果进行了重定向>,写入的就不是显示器而是普通文件,采用的刷新策略是全缓冲,之前的三条C显示函数,虽然带了\n,但是不足以将stdout缓冲区写满!数据并没有被刷新,而在fork的时候,stdout属于父进程,创建子进程时,紧接着就是进程退出!无论谁先退出,都一定会进行缓冲区的刷新(就是修改缓冲区)一旦修改,由于进程具有独立性,因此会发生写时拷贝,因此数据最终会打印两份。
-
write函数为什么没有呢?因为上述的过程都与write无关,write没有FILE,用的是fd,没有C对应的缓冲区。
因此如上就是对于现象的解释。
🕘 6.4 模拟实现
那缓冲区和OS有什么关系呢?下面就通过写一个demo实现一下行刷新:touch myStdio.h;touch myStdio.c;touchmain.c
# Makefile
main:main.c myStdio.cgcc -o $@ $^ -std=c99
.PHONY:clean
clean:rm -f main
// myStdio.h
#pragma once
#include<stdio.h>
#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 4typedef 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"int main()
{FILE_ *fp = fopen_("./log.txt", "w");if(fp == NULL){return 1;}int cnt = 10;const char *msg = "hello world ";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;
}
🕘 6.5 缓冲区与OS的关系
我们所写入到磁盘的数据hello world是按照行刷新进行写入的,但并不是直接写入到磁盘中,而是先写到操作系统内的文件所对应的缓冲区里,对于操作系统中的file结构体,除了一些接口之外还有一段内核缓冲区,而我们的数据则通过file结构体与文件描述符对应,再写到内核缓冲区里面,最后由操作系统刷新到磁盘中,而刷新的这个过程是由操作系统自主决定的,而不是我们刚才所讨论的一些行缓冲、全缓冲、无缓冲……,因为我们提到的这些缓冲是在应用层C语言基础之上FILE结构体的刷新策略,而对于操作系统自主刷新策略则比我们提到的策略复杂的多(涉及到内存管理),因为操作系统需要考虑自己的存储情况而定,因此数据从操作系统写到外设的过程和用户毫无关系。
所以一段数据被写到硬件上(外设)需要进行这么长的周期:首先通过用户写入的数据进入到FILE对应的缓冲区,这是用户语言层面的,然后通过我们提到的刷新的策略刷新到由操作系统中struct file*
的文件描述符引导写到操作系统中的内核缓冲区,最后通过操作系统自主决定的刷新策略写入到外设中。如果OS宕机了,那么数据就有可能出现丢失,因此如果我们想及时的将数据刷新到外设,就需要一些其他的接口强制让OS刷新到外设,即一个新的接口:int fsync(int fd)
,调用这个函数之后就可以立即将内核缓冲区的数据刷新到外设中,就比如我们常用的快捷键:ctrl + s
总结:
因此以上我们所提到的缓冲区有两种:用户缓冲区和内核缓冲区,用户缓冲区就是语言级别的缓冲区,对于C语言来说,用户缓冲区就在FILE结构体中,其他的语言也类似;而内核缓冲区属于操作系统层面,他的刷新策略是按照OS的实际情况进行刷新的,与用户层面无关。
OK,以上就是本期知识点“文件基础”的知识啦~~ ,感谢友友们的阅读。后续还会继续更新,欢迎持续关注哟📌~
💫如果有错误❌,欢迎批评指正呀👀~让我们一起相互进步🚀
🎉如果觉得收获满满,可以点点赞👍支持一下哟~
❗ 转载请注明出处
作者:HinsCoder
博客链接:🔎 作者博客主页
相关文章:

【Linux详解】——文件基础(I/O、文件描述符、重定向、缓冲区)
📖 前言:本期介绍文件基础I/O。 目录🕒 1. 文件回顾🕘 1.1 基本概念🕘 1.2 C语言文件操作🕤 1.2.1 概述🕤 1.2.2 实操🕤 1.2.3 OS接口open的使用(比特位标记)…...
HomMat2d
1.affine_trans_region(区域的任意变换) 2.hom_mat2d_identity(创建二位变换矩阵) 3.hom_mat2d_translate(平移) 4.hom_mat2d_scale(缩放) 5.hom_mat2d_rotate(旋转 &…...

Python3 JSON 数据解析
Python3 JSON 数据解析 JSON (JavaScript Object Notation) 是一种轻量级的数据交换格式。 Python3 中可以使用 json 模块来对 JSON 数据进行编解码,它包含了两个函数: json.dumps(): 对数据进行编码。json.loads(): 对数据进行解码。 在 json 的编解码…...

Homebrew 安装遇到的问题
Homebrew 安装遇到的问题 例如:第一章 Python 机器学习入门之pandas的使用 文章目录Homebrew 安装遇到的问题前言一、安装二、遇到的问题1.提示 zsh: command not found: brew三、解决问题前言 使用 Homebrew 能够 安装 Apple(或您的 Linux 系统&#…...

Metasploit框架基础(二)
文章目录前言一、Meatsplooit的架构二、目录结构datadocumentationlibmodulesplugins三、Measploit模块四、Metasploit的使用前言 Metasploit是用ruby语言开发的,所以你打开软件目录,会发现很多.rb结尾的文件。ruby是一门OOP的语言。 一、Meatsplooit的…...

c++容器
1、vector容器 1.1性质 a)该容器的数据结构和数组相似,被称为单端数组。 b)在存储数据时不是在原有空间上往后拓展,而是找到一个新的空间,将原数据深拷贝到新空间,释放原空间。该过程被称为动态拓展。 vec…...
Vue.js如何实现对一千张图片进行分页加载?
目录 vue处理一千张图片进行分页加载 分页加载、懒加载---概念介绍: 思路: 开发过程中,如果后端一次性返回你1000多条图片或数据,那我们前端应该怎么用什么思路去更好的渲染呢? 第一种:我们可以使用分页…...
计算机网络复习(六)
考点:MIME及其编码(base64,quoted-printable)网络协议http是基于什么协议,应用层到网络层基于什么协议6-27.试将数据 11001100 10000001 00111000 进行 base64 编码,并得到最后传输的 ASCII 数据。答:先将 24 比特的二…...

Redis进阶:布隆过滤器(Bloom Filter)及误判率数学推导
1 缘起 有一次偶然间听到有同事在说某个项目中使用了布隆过滤器, 哎呦,我去,我竟然不知道啥是布隆过滤器, 这我哪能忍?其实,也可以忍,但是,可能有的面试官不能忍!&#…...

Java创建对象的方式
Java创建对象的五种方式: (1)使用new关键字 (2)使用Object类的clone方法 (3)使用Class类的newInstance方法 (4)使用Constructor类中的newInstance方法 (5&am…...

dom基本操作
1、style修改样式 基本语法: 元素.style.样式’值‘ 注意: 1.修改样式通过style属性引出 2.如果属性有-连接符,需要转换为小驼峰命名法 3.赋值的时候,需要的时候不要忘记加css单位 4.后面的值必须是字符串 <div></div> // 1、…...
如何将python训练的XGBoost模型部署在C++环境推理
当前环境:Ubuntu,xgboost1.7.4过程介绍:首先用python训练XGBoost模型,在训练完成后注意使用xgb_model.save_model(checkpoint.model)进行模型的保存。找到xgboost的动态链接库和头文件动态链接库:如果你在conda环境下面…...

About Oracle Database Performance Method
bottleneck(瓶颈): a point where resource contention is highest throughput(吞吐量): the amount of work that can be completed in a specified time. response time (响应时间): the time to complete a spec…...
JavaScript 日期和时间的格式化大汇总(收集)
一、日期和时间的格式化 1、原生方法 1.1、使用 toLocaleString 方法 Date 对象有一个 toLocaleString 方法,该方法可以根据本地时间和地区设置格式化日期时间。例如: const date new Date(); console.log(date.toLocaleString(en-US, { timeZone: …...

【Python】缺失值可视化工具库:missingno
文章目录一、前言二、下载二、使用介绍2.1 绘制缺失值条形图2.2 绘制缺失值热力图2.3 缺失值树状图三、参考资料一、前言 在我们进行机器学习或者深度学习的时候,我们经常会遇到需要处理数据集缺失值的情况,那么如何可视化数据集的缺失情况呢࿱…...
【代码随想录二刷】Day18-二叉树-C++
代码随想录二刷Day18 今日任务 513.找树左下角的值 112.路径总和 113.路径总和ii 106.从中序与后序遍历序列构造二叉树 105.从前序与中序遍历序列构造二叉树 语言:C 513.找树左下角的值 链接:https://leetcode.cn/problems/find-bottom-left-tree-va…...

制造业的云ERP在外网怎么访问?内网服务器一步映射到公网
随着企业信息化、智能化时代的到来,很多制造业企业都在用云ERP。用友U 9cloud通过双版本公有云专属、私有云订阅、传统软件购买三种模式满足众多制造业企业的需求,成为一款适配中型及中大型制造业的云ERP,是企业数智制造的创新平台。 用友U 9…...
zookeeper 复习 ---- 练习
zookeeper 复习 ---- 练习在同一节点配置三个 zookeeper,配置正确的是? A: zoo1.cfg tickTime2000 initLimit5 syncLimit2 dataDir/var/lib/zookeeper/zoo1 clientPort2181 server.1localhost:2666:3666 server.2localhost:2667:3667 serv…...
2023年全国最新道路运输从业人员精选真题及答案1
百分百题库提供道路运输安全员考试试题、道路运输从业人员考试预测题、道路安全员考试真题、道路运输从业人员证考试题库等,提供在线做题刷题,在线模拟考试,助你考试轻松过关。 11.在以下选项中关于安全生产管理方针描述正确的是(…...

Java每日一练——Java简介与基础练习
系列文章目录 提示:这里可以添加系列文章的所有文章的目录,目录需要自己手动添加 例如:第一章 Python 机器学习入门之pandas的使用 文章目录 目录 系列文章目录 文章目录 前言 一、简述解释型语言与编译型语言 二、Java语言的执行流程 2.1、…...

MPNet:旋转机械轻量化故障诊断模型详解python代码复现
目录 一、问题背景与挑战 二、MPNet核心架构 2.1 多分支特征融合模块(MBFM) 2.2 残差注意力金字塔模块(RAPM) 2.2.1 空间金字塔注意力(SPA) 2.2.2 金字塔残差块(PRBlock) 2.3 分类器设计 三、关键技术突破 3.1 多尺度特征融合 3.2 轻量化设计策略 3.3 抗噪声…...

使用VSCode开发Django指南
使用VSCode开发Django指南 一、概述 Django 是一个高级 Python 框架,专为快速、安全和可扩展的 Web 开发而设计。Django 包含对 URL 路由、页面模板和数据处理的丰富支持。 本文将创建一个简单的 Django 应用,其中包含三个使用通用基本模板的页面。在此…...

微软PowerBI考试 PL300-选择 Power BI 模型框架【附练习数据】
微软PowerBI考试 PL300-选择 Power BI 模型框架 20 多年来,Microsoft 持续对企业商业智能 (BI) 进行大量投资。 Azure Analysis Services (AAS) 和 SQL Server Analysis Services (SSAS) 基于无数企业使用的成熟的 BI 数据建模技术。 同样的技术也是 Power BI 数据…...

【OSG学习笔记】Day 16: 骨骼动画与蒙皮(osgAnimation)
骨骼动画基础 骨骼动画是 3D 计算机图形中常用的技术,它通过以下两个主要组件实现角色动画。 骨骼系统 (Skeleton):由层级结构的骨头组成,类似于人体骨骼蒙皮 (Mesh Skinning):将模型网格顶点绑定到骨骼上,使骨骼移动…...
【HTTP三个基础问题】
面试官您好!HTTP是超文本传输协议,是互联网上客户端和服务器之间传输超文本数据(比如文字、图片、音频、视频等)的核心协议,当前互联网应用最广泛的版本是HTTP1.1,它基于经典的C/S模型,也就是客…...

深度学习习题2
1.如果增加神经网络的宽度,精确度会增加到一个特定阈值后,便开始降低。造成这一现象的可能原因是什么? A、即使增加卷积核的数量,只有少部分的核会被用作预测 B、当卷积核数量增加时,神经网络的预测能力会降低 C、当卷…...
LangChain知识库管理后端接口:数据库操作详解—— 构建本地知识库系统的基础《二》
这段 Python 代码是一个完整的 知识库数据库操作模块,用于对本地知识库系统中的知识库进行增删改查(CRUD)操作。它基于 SQLAlchemy ORM 框架 和一个自定义的装饰器 with_session 实现数据库会话管理。 📘 一、整体功能概述 该模块…...

AxureRP-Pro-Beta-Setup_114413.exe (6.0.0.2887)
Name:3ddown Serial:FiCGEezgdGoYILo8U/2MFyCWj0jZoJc/sziRRj2/ENvtEq7w1RH97k5MWctqVHA 注册用户名:Axure 序列号:8t3Yk/zu4cX601/seX6wBZgYRVj/lkC2PICCdO4sFKCCLx8mcCnccoylVb40lP...

6.9-QT模拟计算器
源码: 头文件: widget.h #ifndef WIDGET_H #define WIDGET_H#include <QWidget> #include <QMouseEvent>QT_BEGIN_NAMESPACE namespace Ui { class Widget; } QT_END_NAMESPACEclass Widget : public QWidget {Q_OBJECTpublic:Widget(QWidget *parent nullptr);…...
使用python进行图像处理—图像滤波(5)
图像滤波是图像处理中最基本和最重要的操作之一。它的目的是在空间域上修改图像的像素值,以达到平滑(去噪)、锐化、边缘检测等效果。滤波通常通过卷积操作实现。 5.1卷积(Convolution)原理 卷积是滤波的核心。它是一种数学运算,…...