【Linux】基础IO_文件系统IO_“一切皆文件”_缓冲区

目录

1. 理解"⽂件"

1-1 狭义理解

1-2 ⼴义理解

1-3 ⽂件操作的归类认知

1-4 系统⻆度

访问文件,需要先打开文件!那么是由谁打开文件???

操作系统要不要把被打开的文件管理起来?

2. 回顾C⽂件接⼝

2.1 fopen("文件名","打开方式"); 

snprintf()

打开的myfile⽂件在哪个路径下?

2-2 hello.c写⽂件

2-3 hello.c读⽂件

稍作修改,实现简单cat命令:

2-4 输出信息到显⽰器,你有哪些⽅法

2-5 stdin & stdout & stderr

系统调用接口:

open:

对于系统写入write:

结论:

3.系统读文件:

3.1.open返回值:

那么为什么语言层要进行各自的封装呢?

语言为什么要增加自己的可移植性???

文件描述符是什么???

3-2 ⽂件描述符fd

文件描述符的创建:

重定向:

3.3 在minishell中添加重定向功能(上一章节的后续添加重定向功能)

4.理解一切皆文件

这里就体现了多态的思想:

5.缓冲区

5-1 什么是缓冲区

5-2 为什么要引⼊缓冲区机制

5-3 缓冲类型

5-4 FILE

5-5 简单设计⼀下libc库:

my_stdio.h

my_stdio.c

main.c

总结不易~本章节对我有很大的收获,希望对你也是!!!


1. 理解"⽂件"

文件大小为0,文件要不要再磁盘上占据空间呢???
是要的,文件=内容+属性,存取操作围绕着内容+属性展开的!

1-1 狭义理解

⽂件在磁盘⾥
磁盘是永久性存储介质,因此⽂件在磁盘上的存储是永久性的
磁盘是外设(即是输出设备也是输⼊设备)
磁盘上的⽂件 本质是对⽂件的所有操作,都是对外设的输⼊和输出 简称 IO

1-2 ⼴义理解

Linux 下⼀切皆⽂件(键盘、显⽰器、⽹卡、磁盘…… 这些都是抽象化的过程)(后⾯会讲如何去理解)

1-3 ⽂件操作的归类认知

对于 0KB 的空⽂件是占⽤磁盘空间的
⽂件是⽂件属性(元数据)和⽂件内容的集合(⽂件 = 属性(元数据)+ 内容)
所有的⽂件操作本质是⽂件内容操作和⽂件属性操作

1-4 系统⻆度

对⽂件的操作本质是进程对⽂件的操作
磁盘的管理者是操作系统
⽂件的读写本质不是通过 C 语⾔ / C++ 的库函数来操作的(这些库函数只是为⽤⼾提供⽅便),⽽是通过⽂件相关的系统调⽤接⼝来实现的

访问文件,需要先打开文件!那么是由谁打开文件???

是由进程来打开文件!对文件操作,本质是:进程对文件的操作!

操作系统要不要把被打开的文件管理起来?

要!就是先描述,在组织!!!

2. 回顾C⽂件接⼝

2.1 fopen("文件名","打开方式"); 

虽然这里只传入了一个文件名,但是该调用会将当前路径pwd拼接上当前文件名来进行寻找并打开,在之前的创建文件也是同样如此,获取当前位置的pwd后来创建当前路径的文件!
来认识一下snprintf()这个安全的字符串格式化函数,常用于格式化并将结果写入字符数组中。它可以防止因数组边界溢出导致的安全问题。
int snprintf(char *str, size_t size, const char *format, ...);
  • str:目标缓冲区,用于存储格式化后的字符串。
  • size:缓冲区的大小(包括结尾的空字符 \0)。如果 size 为 0,则 snprintf() 不会向目标缓冲区写入任何字符。
  • format:格式字符串,与 printf 的格式类似,支持 %d%s 等格式说明符。
  • ...:可变参数,提供用于格式化的值。
eg:
#include <stdio.h>int main()
{int cnt = 1;const char* msg = "hello,bit: ";while (cnt < 10) {char buffer[1024];snprintf(buffer, sizeof(buffer), "%s%d",msg, cnt++);printf("%s\n", buffer); // 输出到屏幕}return 0;
}

snprintf()

1.每次都是写入这个局部变量buffer内;2.然后计算输入的大小,保证不会发生越界;3.输入的内容“字符串”;4.可变参数列表

输出为:

hello,bit: 1
hello,bit: 2
hello,bit: 3
hello,bit: 4
hello,bit: 5
hello,bit: 6
hello,bit: 7
hello,bit: 8
hello,bit: 9

size_t fwrite(const void *ptr, size_t size, size_t count, FILE *stream);
  • ptr:指向要写入数据的内存地址。
  • size:每个数据单元的大小(以字节为单位)。
  • count:写入的数据单元数量
  • stream:目标文件的指针(FILE * 类型,通常通过 fopen 获得)。
eg:
fwrite(buffer,strlen(buffer),1,fp);  

size_t fread(void *ptr, size_t size, size_t count, FILE *stream);
  1. ptr:目标缓冲区的指针,用于存储读取到的数据。
  2. size:每个数据单元的大小(以字节为单位)。
  3. count:要读取的数据单元数量。
  4. stream:目标文件指针(FILE * 类型,通常通过 fopen 打开文件获得)。
  • 返回成功读取的 数据单元数量count)。
  • 如果返回值小于请求的 count,可能是因为:
    • 文件结束(EOF)。
    • 发生错误。
  • 可以通过 feof()ferror() 来检查文件状态。

feof()函数可以判断是否读取到文件的末尾

2-1 hello.c打开⽂件
#include <stdio.h>
int main()
{FILE *fp = fopen("myfile", "w");if(!fp){printf("fopen error!\n");
}while(1);fclose(fp);return 0;
}

打开的myfile⽂件在哪个路径下?

在程序的当前路径下,那系统怎么知道程序的当前路径在哪⾥呢?
可以使⽤ ls /proc/[ 进程 id] -l 命令查看当前正在运⾏进程的信息:
[hyb@VM-8-12-centos io]$ ps ajx | grep myProc
506729 533463 533463 506729 pts/249 533463 R+ 1002 7:45 ./myProc
536281 536542 536541 536281 pts/250 536541 R+ 1002 0:00 grep --
color=auto myProc
[hyb@VM-8-12-centos io]$ ls /proc/533463 -l
total 0
......
-r--r--r-- 1 hyb hyb 0 Aug 26 17:01 cpuset
lrwxrwxrwx 1 hyb hyb 0 Aug 26 16:53 cwd -> /home/hyb/io
-r-------- 1 hyb hyb 0 Aug 26 17:01 environ
lrwxrwxrwx 1 hyb hyb 0 Aug 26 16:53 exe -> /home/hyb/io/myProc
dr-x------ 2 hyb hyb 0 Aug 26 16:54 fd
......
其中:
cwd:指向当前进程运⾏⽬录的⼀个符号链接。

exe:指向启动当前进程的可执⾏⽂件(完整路径)的符号链接。
打开⽂件,本质是进程打开,所以,进程知道⾃⼰在哪⾥,即便⽂件不带路径,进程也知道。由此OS就能知道要创建的⽂件放在哪⾥。

2-2 hello.c写⽂件

#include <stdio.h>
#include <string.h>
int main()
{FILE* fp = fopen("myfile", "w");if (!fp) {printf("fopen error!\n");}const char* msg = "hello bit!\n";int count = 5;while (count--) {fwrite(msg, strlen(msg), 1, fp);}fclose(fp);return 0;
}

2-3 hello.c读⽂件

#include <stdio.h>
#include <string.h>
int main()
{FILE* fp = fopen("myfile", "r");if (!fp) {printf("fopen error!\n");return 1;}char buf[1024];const char* msg = "hello bit!\n";while (1) {//注意返回值和参数,此处有坑,仔细查看man⼿册关于该函数的说明size_t s = fread(buf, 1, strlen(msg), fp);if (s > 0) {buf[s] = 0;printf("%s", buf);}if (feof(fp)) {break;}}fclose(fp);return 0;
}

稍作修改,实现简单cat命令:

#include <stdio.h>
#include <string.h>//cat myfile    
int main(int argc, char* argv[])
{if (argc != 2){printf("Usage: %s filename\n", argv[0]);return 1;}FLIE* fp = fopen(argv[1], "r")if (fp == NULL){perror("fopen");return 2;}while (1){char buffer[128];memset(buffer, 0, sizeof(buffer));int n = fread(buffer, sizeof(buffer) - 1, 1, fp);if (n > 0) printf("%s", buffer);if (feof(fp)) break;}fclose(fp);return 0;
}

2-4 输出信息到显⽰器,你有哪些⽅法

#include <stdio.h>
#include <string.h>
int main()
{const char* msg = "hello fwrite\n";fwrite(msg, strlen(msg), 1, stdout);printf("hello printf\n");fprintf(stdout, "hello fprintf\n");return 0;
}

2-5 stdin & stdout & stderr

C默认会打开三个输⼊输出流,分别是stdin, stdout, stderr
仔细观察发现,这三个流的类型都是FILE*, fopen返回值类型,⽂件指针
#include <stdio.h>
extern FILE *stdin;
extern FILE *stdout;
extern FILE *stderr;

系统调用接口:

O_CREAT:创建
O_WRONLY:只写
O_TRUNC:清空

open:

int open(const char *pathname, int flags);
       int open(const char *pathname, int flags, mode_t mode);
可以看出此时的log.txt的权限是随机的:
必须要在open后跟上相应的八进制权限设置:
open指定第三个参数主要就是为了我们的新建文件的权限
但是这里我们设置的是666,可是创建出来的还是664,这是为什么呢?
就是因为存在系统umask的影响!
可以看到这里并没进行清空文件,而是直接进行覆盖式的写入,所以这里仍然缺少一个系统调用,跟语言层面没有关系: O_TRUNC:清空
所以系统调用就要传入以下几个参数:【路径文件名】+【功能:新建,写入,清空】+【权限】
int fd = open("log.txt",O_CREAT | O_WRONLY | O_TRUNC,0666);

所以由上面看到,想要追加就不能清空:O_APPEND

所以语言是不可能直接访问底层的存储系统的,只是封装了操作系统底层的接口来进行访问

对于系统写入write:

有以下两种再语言层面进行封装的二进制写入和字符写入

结论:

再系统层面上,并不关心你是二进制写入还是字符写入,最终系统都会转换成二进制来进行识别
对于语言层进行封装的二进制 或 字符写入的接口都是调用的系统write!

3.系统读文件:

那么就不能再打开的时候进行新建,因为当前文件不存在还要新建再打开读是没有意义的!所以读文件不需要新建文件,也不需要清空文件,不需要写入文件,只需要转换为二进制 O_RDONLY,只读方式打开文件
int fd = open("log.txt",O_RDONLY);  

这也就是为什么write存在两参数调用的接口,就是为读来准备的

int n = read(fd,buffer,sizeof(buffer)-1);

3.1.open返回值:

那为什么返回值是从3开始打印呢?
因为返回值0,1,2是叫做标准输入,标准输出,标准错误!
其中FILE是C语言提供的一个结构体typedef struct FILE{;;;;;}; 
再OS界面,只认fd即文件描述符,那么大胆猜测这个FILE里面一定是封装了文件描述符!!!
所以顶层无论怎么封装,底层都只认识文件描述符

那么为什么语言层要进行各自的封装呢?

就是因为每个OS的实现不同,就是我们上面写的代码放在windows下就跑步过去,因为OS不同,底层接口实现的就不一样,如果我们实现一种语言来将各个OS进行封装,我们写一套C语言代码,就可以实现跨平台移植的作用,在各个OS上只需呀裁掉别的平台的所有代码,只保留当前OS的代码,就是当前OS的接口封装即可,这样就凸显出了语言的可移植性!

语言为什么要增加自己的可移植性???

就是为了能够满足各个平台的人,让更多人去使用,占有市场利用率,防止被淘汰

文件描述符是什么???

对于上面打印的这些数字是什么呢?
是数组下标~

3-2 ⽂件描述符fd

通过对open函数的学习,我们知道了⽂件描述符就是⼀个⼩整数

文件描述符的创建:

FILE是由一个结构体来进行封装的,每一个文件的属性都被封装到一个struct_file内,多个struct_file由一个双链表进行链接,这样先描述,在组织;本质上也就是对一个链表进行增删查改
对于整个链表的管理,还设置了一个文件描述符表 struct file *fd array[] 指针数组,来将整个文件链表的每一个节点存入这个指针数组内,也就有了为什么可以打印出fd的值的下标,这个指针数组再由一个struct files_struct *files指针进行指向
通常我们在申请这个文件描述符的时候,通常就是找到一个最小的,没有被使用过的文件描述符,作为一个被新打开的位置

重定向:

首先进入程序close(1)关掉了stdout标准输出,然后此时的系统就将fd分配给1位置进行指向,然后printf()底层封装的就是原来指向stdout的1,现在又重新指向log.txt位置进行打印,就往文本log.txt内进行打印得到该现象!
重定向dup2(int oldfd,int newfd);
作用就是将当前文件描述符覆盖到newfd这个位置,让newfd也指向oldfd的文件处!dup2()后一般要手动关闭oldfd : close(oldfd)
所以重定向的原理是:打开文件的方式 + dup2()
输入重定向:将打开文件设置为只读方式打开,将当前文件设置到标准输入的位置,dup(fd,0);
fgets(buffer,sizeof(buffer),stdin) fgets()函数用于从指定的流(在这个例子中是 stdin,标准输入流)中读取一行字符,并将其存储到 buffer中。成功了就打印buffer~

设置任意文本的输出重定向: 

⽰例代码
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
int main() {int fd = open("./log", O_CREAT | O_RDWR);if (fd < 0) {perror("open");return 1;}close(1);dup2(fd, 1);for (;;) {char buf[1024] = { 0 };ssize_t read_size = read(0, buf, sizeof(buf) - 1);if (read_size < 0) {perror("read");break;}printf("%s", buf);fflush(stdout);}return 0;
}
printf是C库当中的IO函数,⼀般往 stdout 中输出,但是stdout底层访问⽂件的时候,找的还是fd:1, 但此时,fd:1下标所表⽰内容,已经变成了myfifile的地址,不再是显⽰器⽂件的地址,所以,输出的任何消息都会往⽂件中写⼊,进⽽完成输出重定向。那追加和输⼊重定向如何完成呢?

3.3 在minishell中添加重定向功能(上一章节的后续添加重定向功能)

#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <ctype.h>
using namespace std;
const int basesize = 1024;
const int argvnum = 64;
const int envnum = 64;
// 全局的命令⾏参数表
char* gargv[argvnum];
int gargc = 0;
// 全局的变量
int lastcode = 0;
// 我的系统的环境变量
char* genv[envnum];
// 全局的当前shell⼯作路径
char pwd[basesize];
char pwdenv[basesize];
// 全局变量与重定向有关
#define NoneRedir 0
#define InputRedir 1
#define OutputRedir 2
#define AppRedir 3
int redir = NoneRedir;
char* filename = nullptr;
// " "file.txt
#define TrimSpace(pos) do{\
while(isspace(*pos)){\
pos++;\
}\
}while(0)
string GetUserName()
{string name = getenv("USER");return name.empty() ? "None" : name;
}
string GetHostName()
{string hostname = getenv("HOSTNAME");return hostname.empty() ? "None" : hostname;
}
string GetPwd()
{if (nullptr == getcwd(pwd, sizeof(pwd))) return "None";snprintf(pwdenv, sizeof(pwdenv), "PWD=%s", pwd);putenv(pwdenv); // PWD=XXXreturn pwd;//string pwd = getenv("PWD");//return pwd.empty() ? "None" : pwd;
}
string LastDir()
{string curr = GetPwd();if (curr == "/" || curr == "None") return curr;// /home/whb/XXXsize_t pos = curr.rfind("/");if (pos == std::string::npos) return curr;return curr.substr(pos + 1);
}
string MakeCommandLine()
{// [whb@bite-alicloud myshell]$char command_line[basesize];snprintf(command_line, basesize, "[%s@%s %s]# ", \GetUserName().c_str(), GetHostName().c_str(), LastDir().c_str());return command_line;
}
void PrintCommandLine() // 1. 命令⾏提⽰符
{printf("%s", MakeCommandLine().c_str());fflush(stdout);
}
bool GetCommandLine(char command_buffer[], int size) // 2. 获取⽤⼾命令
{// 我们认为:我们要将⽤⼾输⼊的命令⾏,当做⼀个完整的字符串// "ls -a -l -n"char* result = fgets(command_buffer, size, stdin);if (!result){return false;}command_buffer[strlen(command_buffer) - 1] = 0;if (strlen(command_buffer) == 0) return false;return true;
}
void ResetCommandline()
{memset(gargv, 0, sizeof(gargv));gargc = 0;// 重定向redir = NoneRedir;filename = nullptr;
}
void ParseRedir(char command_buffer[], int len)
{int end = len - 1;while (end >= 0){if (command_buffer[end] == '<'){redir = InputRedir;command_buffer[end] = 0;filename = &command_buffer[end] + 1;TrimSpace(filename);break;}else if (command_buffer[end] == '>'){if (command_buffer[end - 1] == '>'){redir = AppRedir;command_buffer[end] = 0;command_buffer[end - 1] = 0;filename = &command_buffer[end] + 1;TrimSpace(filename);break;}else{redir = OutputRedir;command_buffer[end] = 0;filename = &command_buffer[end] + 1;TrimSpace(filename);break;}}else{end--;}}
}
void ParseCommand(char command_buffer[])
{// "ls -a -l -n"const char* sep = " ";gargv[gargc++] = strtok(command_buffer, sep);// =是刻意写的while ((bool)(gargv[gargc++] = strtok(nullptr, sep)));gargc--;
}
void ParseCommandLine(char command_buffer[], int len) // 3. 分析命令
{ResetCommandline();ParseRedir(command_buffer, len);ParseCommand(command_buffer);//printf("command start: %s\n", command_buffer);// "ls -a -l -n"// "ls -a -l -n" > file.txt// "ls -a -l -n" < file.txt// "ls -a -l -n" >> file.txt//printf("redir: %d\n", redir);//printf("filename: %s\n", filename);//printf("command end: %s\n", command_buffer);
}
void debug()
{printf("argc: %d\n", gargc);for (int i = 0; gargv[i]; i++){printf("argv[%d]: %s\n", i, gargv[i]);}
}
//enum
//{
// FILE_NOT_EXISTS = 1,
// OPEN_FILE_ERROR,
//};
void DoRedir()
{// 1. 重定向应该让⼦进程⾃⼰做!// 2. 程序替换会不会影响重定向?不会// 0. 先判断 && 重定向if (redir == InputRedir){if (filename){int fd = open(filename, O_RDONLY);if (fd < 0){exit(2);}dup2(fd, 0);}else{exit(1);}}else if (redir == OutputRedir){if (filename){int fd = open(filename, O_CREAT | O_WRONLY | O_TRUNC, 0666);if (fd < 0){exit(4);}dup2(fd, 1);}else{exit(3);}}else if (redir == AppRedir){if (filename){int fd = open(filename, O_CREAT | O_WRONLY | O_APPEND, 0666);if (fd < 0){exit(6);}dup2(fd, 1);}else{exit(5);}}else{// 没有重定向,Do Nothong!}
}
// 在shell中
// 有些命令,必须由⼦进程来执⾏
// 有些命令,不能由⼦进程执⾏,要由shell⾃⼰执⾏ --- 内建命令 built command
bool ExecuteCommand() // 4. 执⾏命令
{// 让⼦进程进⾏执⾏pid_t id = fork();if (id < 0) return false;if (id == 0){//⼦进程DoRedir();// 1. 执⾏命令execvpe(gargv[0], gargv, genv);// 2. 退出exit(7);}int status = 0;pid_t rid = waitpid(id, &status, 0);if (rid > 0){if (WIFEXITED(status)){lastcode = WEXITSTATUS(status);}else{lastcode = 100;}return true;}return false;
}
void AddEnv(const char* item)
{int index = 0;while (genv[index]){index++;}genv[index] = (char*)malloc(strlen(item) + 1);strncpy(genv[index], item, strlen(item) + 1);genv[++index] = nullptr;
}
// shell⾃⼰执⾏命令,本质是shell调⽤⾃⼰的函数
bool CheckAndExecBuiltCommand()
{if (strcmp(gargv[0], "cd") == 0){// 内建命令if (gargc == 2){chdir(gargv[1]);lastcode = 0;}else{lastcode = 1;}return true;}else if (strcmp(gargv[0], "export") == 0){// export也是内建命令if (gargc == 2){AddEnv(gargv[1]);lastcode = 0;}else{lastcode = 2;}return true;}else if (strcmp(gargv[0], "env") == 0){for (int i = 0; genv[i]; i++){printf("%s\n", genv[i]);}lastcode = 0;return true;}else if (strcmp(gargv[0], "echo") == 0){if (gargc == 2){// echo $?// echo $PATH
// echo helloif (gargv[1][0] == '$'){if (gargv[1][1] == '?'){printf("%d\n", lastcode);lastcode = 0;}}else{printf("%s\n", gargv[1]);lastcode = 0;}}else{lastcode = 3;}return true;}return false;
}
// 作为⼀个shell,获取环境变量应该从系统的配置来
// 我们今天就直接从⽗shell中获取环境变量
void InitEnv()
{extern char** environ;int index = 0;while (environ[index]){genv[index] = (char*)malloc(strlen(environ[index]) + 1);strncpy(genv[index], environ[index], strlen(environ[index]) + 1);index++;}genv[index] = nullptr;
}
int main()
{InitEnv();char command_buffer[basesize];while (true){PrintCommandLine(); // 1. 命令⾏提⽰符// command_buffer -> outputif (!GetCommandLine(command_buffer, basesize)) // 2. 获取⽤⼾命令{continue;}//printf("%s\n", command_buffer);//ls//"ls -a -b -c -d"->"ls" "-a" "-b" "-c" "-d"//"ls -a -b -c -d">hello.txt//"ls -a -b -c -d">>hello.txt//"ls -a -b -c -d"<hello.txtParseCommandLine(command_buffer, strlen(command_buffer)); // 3. 分析命令if (CheckAndExecBuiltCommand()){continue;}ExecuteCommand(); // 4. 执⾏命令}return 0;
}

其实文件描述符1标准输出,2标准错误,都是指向同一个文件,说明只做了标准输入的重定向,并没有做出标准错误的重定向:

就想把标准错误给重定向到指定文件呢?
为什么非要存在标准错误呢?printf()、perror???cout/cerr
就是因为他们单独占据文件描述符,可以通过重定向能力,把常规消息和错误消息进行分离!方便日志的形成
那如何把标准输出和标准错误都追加到一个文件内呢??
log.txt就相当于是文件描述符3,然后3进行覆盖到1内,然后文件描述符1就指向该文件,进行标准输出写入,然后重定向到log.txt内
2 > &1就是再将文件描述符1里面的内容覆盖的写入2内,此时的2也跟1一样指向log.txt文件,也可以进行重定向写入

4.理解一切皆文件

⾸先,在windows中是⽂件的东西,它们在linux中也是⽂件;其次⼀些在windows中不是⽂件的东西,⽐如进程、磁盘、显⽰器、键盘这样硬件设备也被抽象成了⽂件,你可以使⽤访问⽂件的⽅法访问它们获得信息;甚⾄管道,也是⽂件;将来我们要学习⽹络编程中的socket(套接字)这样的东西, 使⽤的接⼝跟⽂件接⼝也是⼀致的。
这样做最明显的好处是,开发者仅需要使⽤⼀套 API 和开发⼯具,即可调取 Linux 系统中绝⼤部分的资源。举个简单的例⼦,Linux 中⼏乎所有读(读⽂件,读系统状态,读PIPE)的操作都可以⽤read 函数来进⾏;⼏乎所有更改(更改⽂件,更改系统参数,写 PIPE)的操作都可以⽤ write 函数来进⾏。 之前我们讲过,当打开⼀个⽂件时,操作系统为了管理所打开的⽂件,都会为这个⽂件创建⼀个file结构体,该结构体定义在 /usr/src/kernels/3.10.0- 1160.71.1.el7.x86_64/include/linux/ fs.h 下,以下展⽰了该结构部分我们关系的内容:
值得关注的是 struct file 中的 f_op 指针指向了⼀个 file_operations 结构体,这个结构体中的成员除了struct module* owner 其余都是函数指针。该结构和 struct file 都在fs.h下。

这里就体现了多态的思想:

  • 是面向对象编程中的一个重要特性,它允许使用统一的接口来表示不同的行为。在 C++ 中,多态主要通过虚函数(Virtual Function)来实现。
  • 简单来说,多态就是 “多种形态”。例如,对于不同类型的动物(如猫、狗),它们都有 “叫” 这个行为,但叫声不同。多态可以让我们通过一个共同的 “叫” 函数接口,来实现不同动物的不同叫声。
file_operation 就是把系统调⽤和驱动程序关联起来的关键数据结构,这个结构的每⼀个成员都对应着⼀个系统调⽤。读取 file_operation 中相应的函数指针,接着把控制权转交给函数,从⽽完成了Linux设备驱动程序的⼯作。
介绍完相关代码,⼀张图总结:
上图中的外设,每个设备都可以有⾃⼰的read、write,但⼀定是对应着不同的操作⽅法!!但通过struct file 下 file_operation 中的各种函数回调,让我们开发者只⽤file便可调取 Linux 系统中绝⼤部分的资源!!这便是“linux下⼀切皆⽂件”的核⼼理解

5.缓冲区

好比有了菜鸟驿站,就不用让快递员直接跟用户打交道,直接的维护了快递员的效率;而我本身,就不用得到快递员的电话后立马就下去,而是等我有空闲的时间随时去哪

5-1 什么是缓冲区

缓冲区是内存空间的⼀部分。也就是说,在内存空间中预留了⼀定的存储空间,这些存储空间⽤来缓冲输⼊或输出的数据,这部分预留的空间就叫做缓冲区。缓冲区根据其对应的是输⼊设备还是输出设备,分为输⼊缓冲区和输出缓冲区。

5-2 为什么要引⼊缓冲区机制

读写⽂件时,如果不会开辟对⽂件操作的缓冲区,直接通过系统调⽤对磁盘进⾏操作(读、写等),那么每次对⽂件进⾏⼀次读写操作时,都需要使⽤读写系统调⽤来处理此操作,即需要执⾏⼀次系统调⽤,执⾏⼀次系统调⽤将涉及到CPU状态的切换,即从⽤⼾空间切换到内核空间,实现进程上下⽂的切换,这将损耗⼀定的CPU时间,频繁的磁盘访问对程序的执⾏效率造成很⼤的影响。
为了减少使⽤系统调⽤的次数,提⾼效率,我们就可以采⽤缓冲机制。⽐如我们从磁盘⾥取信息,可以在磁盘⽂件进⾏操作时,可以⼀次从⽂件中读出⼤量的数据到缓冲区中,以后对这部分的访问就不需要再使⽤系统调⽤了,等缓冲区的数据取完后再去磁盘中读取,这样就可以减少磁盘的读写次数,再加上计算机对缓冲区的操作⼤ 快于对磁盘的操作,故应⽤缓冲区可⼤ 提⾼计算机的运⾏速度。⼜⽐如,我们使⽤打印机打印⽂档,由于打印机的打印速度相对较慢,我们先把⽂档输出到打印机相应的缓冲区,打印机再⾃⾏逐步打印,这时我们的CPU可以处理别的事情。可以看出,缓冲区就是⼀块内存区,它⽤在输⼊输出设备和CPU之间,⽤来缓存数据。它使得低速的输⼊输出设备和⾼速的CPU能够协调⼯作,避免低速的输⼊输出设备占⽤CPU,解放出CPU,使其能够⾼效率⼯作。

5-3 缓冲类型

标准I/O提供了3种类型的缓冲区。
全缓冲区:这种缓冲⽅式要求填满整个缓冲区后才进⾏I/O系统调⽤操作。对于磁盘⽂件的操作通
常使⽤全缓冲的⽅式访问。
⾏缓冲区:在⾏缓冲情况下,当在输⼊和输出中遇到换⾏符时,标准I/O库函数将会执⾏系统调⽤
操作。当所操作的流涉及⼀个终端时(例如标准输⼊和标准输出),使⽤⾏缓冲⽅式。因为标准
I/O库每⾏的缓冲区⻓度是固定的,所以只要填满了缓冲区,即使还没有遇到换⾏符,也会执⾏
I/O系统调⽤操作,默认⾏缓冲区的⼤⼩为1024。
⽆缓冲区:⽆缓冲区是指标准I/O库不对字符进⾏缓存,直接调⽤系统调⽤。标准出错流stderr通
常是不带缓冲区的,这使得出错信息能够尽快地显⽰出来。
除了上述列举的默认刷新⽅式,下列特殊情况也会引发缓冲区的刷新:
1. 缓冲区满时;
2. 执⾏flush语句;
⽰例如下:
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main() {close(1);int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);if (fd < 0) {perror("open");return 0;}printf("hello world: %d\n", fd);close(fd);return 0;
}
我们本来想使⽤重定向思维,让本应该打印在显⽰器上的内容写到“log.txt”⽂件中,但我们发现,
程序运⾏结束后,⽂件中并没有被写⼊内容:
[hyb@VM-8-12-centos buffer]$ ./myfile
[hyb@VM-8-12-centos buffer]$ ls
log.txt makefile myfile myfile.c
[hyb@VM-8-12-centos buffer]$ cat log.txt
[hyb@VM-8-12-centos buffer]$
这是由于我们将1号描述符重定向到磁盘⽂件后,缓冲区的刷新⽅式成为了全缓冲。⽽我们写⼊的内容并没有填满整个缓冲区,导致并不会将缓冲区的内容刷新到磁盘⽂件中。怎么办呢?可以使⽤fflush强制刷新下缓冲区。
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main() {
close(1);
int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
if (fd < 0) {
perror("open");
return 0;
}
printf("hello world: %d\n", fd);
fflush(stdout);
close(fd);
return 0;
}
还有⼀种解决⽅法,刚好可以验证⼀下stderr是不带缓冲区的,代码如下:
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main() {close(2);int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);if (fd < 0) {perror("open");return 0;}perror("hello world");close(fd);return 0;
}
这种⽅式便可以将2号⽂件描述符重定向⾄⽂件,由于stderr没有缓冲区,“hello world”不⽤fflash
就可以写⼊⽂件:
[hyb@VM-8-12-centos buffer]$ ./myfile
[hyb@VM-8-12-centos buffer]$ cat log.txt
hello world: Success

5-4 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有关!
⼀般C库函数写⼊⽂件时是全缓冲的,⽽写⼊显⽰器是⾏缓冲。
printf fwrite 库函数+会⾃带缓冲区(进度条例⼦就可以说明),当发⽣重定向到普通⽂
件时,数据的缓冲⽅式由⾏缓冲变成了全缓冲。
⽽我们放在缓冲区中的数据,就不会被⽴即刷新,甚⾄fork之后
但是进程退出之后,会统⼀刷新,写⼊⽂件当中。
但是fork的时候,⽗⼦数据会发⽣写时拷⻉,所以当你⽗进程准备刷新的时候,⼦进程也就有了
同样的⼀份数据,随即产⽣两份数据。
write 没有变化,说明没有所谓的缓冲。
综上: printf fwrite 库函数会⾃带缓冲区,⽽ write 系统调⽤没有带缓冲区。另外,我们这
⾥所说的缓冲区,都是⽤⼾级缓冲区。其实为了提升整机性能,OS也会提供相关内核级缓冲区,不过不再我们讨论范围之内。
那这个缓冲区谁提供呢? printf fwrite 是库函数, write 是系统调⽤,库函数在系统调⽤的
“上层”, 是对系统调⽤的“封装”,但是 write 没有缓冲区,⽽ printf fwrite 有,⾜以说
明,该缓冲区是⼆次加上的,⼜因为是C,所以由C标准库提供。
如果有兴趣,可以看看FILE结构体:
typedef struct _IO_FILE FILE; /usr/include/stdio.h

5-5 简单设计⼀下libc库:

my_stdio.h

$ cat my_stdio.h
#pragma once
#define SIZE 1024
#define FLUSH_NONE 0
#define FLUSH_LINE 1
#define FLUSH_FULL 2
struct IO_FILE
{int flag; // 刷新⽅式int fileno; // ⽂件描述符char outbuffer[SIZE];int cap;int size;// TODO
};
typedef struct IO_FILE mFILE;
mFILE* mfopen(const char* filename, const char* mode);
int mfwrite(const void* ptr, int num, mFILE* stream);
void mfflush(mFILE* stream);
void mfclose(mFILE* stream);

my_stdio.c

$ cat my_stdio.c
#include "my_stdio.h"
#include <string.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
mFILE* mfopen(const char* filename, const char* mode)
{int fd = -1;if (strcmp(mode, "r") == 0){fd = open(filename, O_RDONLY);}else if (strcmp(mode, "w") == 0){fd = open(filename, O_CREAT | O_WRONLY | O_TRUNC, 0666);}else if (strcmp(mode, "a") == 0){fd = open(filename, O_CREAT | O_WRONLY | O_APPEND, 0666);}if (fd < 0) return NULL;mFILE* mf = (mFILE*)malloc(sizeof(mFILE));if (!mf){close(fd);return NULL;}mf->fileno = fd;mf->flag = FLUSH_LINE;mf->size = 0;mf->cap = SIZE;return mf;
}
void mfflush(mFILE* stream)
{if (stream->size > 0){// 写到内核⽂件的⽂件缓冲区中!write(stream->fileno, stream->outbuffer, stream->size);// 刷新到外设fsync(stream->fileno);stream->size = 0;}
}
int mfwrite(const void* ptr, int num, mFILE* stream)
{// 1. 拷⻉memcpy(stream->outbuffer + stream->size, ptr, num);stream->size += num;// 2. 检测是否要刷新if (stream->flag == FLUSH_LINE && stream->size > 0 && stream -
> outbuffer[stream->size - 1] == '\n'){mfflush(stream);}return num;
}
void mfclose(mFILE* stream)
{if (stream->size > 0){mfflush(stream);}close(stream->fileno);
}

main.c

$ cat main.c
#include "my_stdio.h"
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main()
{mFILE* fp = mfopen("./log.txt", "a");if (fp == NULL){return 1;}int cnt = 10;while (cnt){printf("write %d\n", cnt);char buffer[64];snprintf(buffer, sizeof(buffer), "hello message, number is : %d", cnt);cnt--;mfwrite(buffer, strlen(buffer), fp);mfflush(fp);sleep(1);}mfclose(fp);
}

总结不易~本章节对我有很大的收获,希望对你也是!!!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.xdnf.cn/news/33951.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

nginx防盗链原理与实践

nginx防盗链的原理是基于http请求头中的referer来限制对资源的访问&#xff08;referer是用来告知浏览器该网页时从哪个页面链接来的&#xff09;&#xff0c;从而防止其他网站胃经授权直接链接资源。 nginx防盗链的作用是节省带宽和资源消耗&#xff0c;保护数据安全&#xf…

UG NX二次开发(Python)-UIStyler-选取点

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录 1、前言2、设计一个UI界面3、创建长方体的代码4、需要引入的库5、测试验证1、前言 采用Python语言进行UG NX二次开发的资料比较少,我本来不是很认可采用Python进行二次开发的,但是近期有读者咨询…

linux环境中后台运行java程序

在生产环境&#xff0c;我们通常需要让java进程后台运行&#xff0c;并且即使会话关闭&#xff0c;进程也依然存在。 使用的命令&#xff1a; nohup java -jar xxx.jar -> aaa.log 2>&1 & 详细介绍下上面这条命令 &#xff08;1&#xff09;nohup&#xff1a;…

算法笔记:力扣15、三数之和

思路&#xff1a; 实现代码 class Solution {public List<List<Integer>> threeSum(int[] nums) {List<List<Integer>> result new ArrayList<>(); Arrays.sort(nums); // 先对数组进行排序 for (int i 0; i < nums.length - 2; i) { /…

java基础语法光速入门

前言 欢迎来到我的博客 个人主页:北岭敲键盘的荒漠猫-CSDN博客 本文整理Java的基础语法部分 适合有编程基础的人快点掌握语法使用 没学过一两门语言的话。。还是不建议看了 极致的浓缩没有一点解释 注释 单行注释 // 多行注释 /**/ 数据类型 布尔型:true false 整型:int,lon…

karmada-descheduler

descheduler规则 karmada-descheduler 定期检测所有部署&#xff0c;通常是每2分钟一次&#xff0c;并确定目标调度集群中无法调度的副本数量。它通过调用 karmada-scheduler-estimator 来完成这个过程。如果发现无法调度的副本&#xff0c;它将通过减少 spec.clusters 的配…

LeetCode 力扣 热题 100道(十四)二叉树的中序遍历(C++)

给定一个二叉树的根节点 root &#xff0c;返回 它的 中序 遍历 。 如下为代码&#xff1a; /*** Definition for a binary tree node.* struct TreeNode {* int val;* TreeNode *left;* TreeNode *right;* TreeNode() : val(0), left(nullptr), right(nullpt…

STM32之ADC采集和DMA传输(八)

STM32F407 系列文章 -内部ADC采集和DMA传输&#xff08;八&#xff09; 目录 前言 一、ADC特性 二、DMA特性 三、ADC采集 1.单通道ADC采集 1.头文件定义 2.函数adc_init() 3.函数HAL_ADC_MspInit() 4.函数adc_channel_set() 5.函数adc_get_result() 6.函数adc_get_r…

三菱人机界面GOT SIMPLE 系列 GS2107\GS2110\GS2512

以客户需求为核心的全面升级!GOT SIMPLE系列新功能 GOT SIMPLE升级版重磅更新&#xff0c;增添了许多期待已久的新功能&#xff0c;帮助用户实现远程维护! 扩充用户存储器容量至15MB&#xff0c;并支持轮廓字体&#xff0c;以实现平滑、靓丽的字体显示。此外&#xff0c;可使用…

VLTVG代码复现并讲解

train.py 在main函数中找到这个构建模型的地方&#xff0c;ctrl&#xff0b;左键点进这个函数中去 来到了这里 又来到了这里&#xff0c;这里就是构建模型的地方&#xff1a; 又来到了这里&#xff0c;还是在VLTVG.py这个文件中&#xff1a; Method The Overall Network Visua…

转换思维是为智

转换思维是为智 2023年11月08日(节选) 我们人的思维分为人间思维&#xff0c;圣人思维&#xff0c;菩萨思维。人间思维讲得通俗一点就是世间智慧&#xff0c;他拥有的是人间的智慧&#xff0c;讲得再简单一点&#xff0c;就是人间的聪明。圣人的思维是什么&#xff0c;是一种脱…

qtcanpool 知 10:包管理雏形

文章目录 前言痛点转机雏形实践后语 前言 曾听闻&#xff1a;C/Qt 没有包管理器&#xff0c;开发起来太不方便。这是一个有过 node.js 开发经验的人对 Qt 的吐槽。 确实&#xff0c;像 python、golang、node.js 这些编程语言都有包管理器&#xff0c;给用户带来了极佳的开发体…

光敏传感器实验

用到 ADC 采集&#xff0c;通过 ADC 采集电压&#xff0c;获取光敏传感器的电阻变化&#xff0c;从而得出环境光线的变化&#xff0c;并在 TFTLCD 上面显示出来。 光敏传感器是最常见的传感器之一&#xff0c;它的种类繁多&#xff0c;主要有&#xff1a;光电管、光电倍增管…

Modbus RTU转Profinet接4台流量器配置案例

Modbus RTU转Profinet是工业自动化领域常见的通讯协议。Modbus RTU因其简单、可靠而被广泛应用于各种设备间的数据传输&#xff0c;而Profinet则以其高速、实时性在现代工业4.0场景中扮演着重要角色。本文将详细解析如何将Modbus RTU转换为Profinet&#xff0c;并通过实际案例来…

【AI系统】推理系统架构

推理系统架构 推理系统架构是 AI 领域中的一个关键组成部分&#xff0c;它负责将训练好的模型应用于实际问题&#xff0c;从而实现智能决策和自动化。在构建一个高效的推理系统时&#xff0c;我们不仅需要考虑其性能和准确性&#xff0c;还需要确保系统的可扩展性、灵活性以及…

家事速配社区新经济与消费新业态创新峰会成功举办,开启多元合作新篇章

2024 年 11 月 28 日&#xff0c;家事速配社区新经济与消费新业态创新峰会在福建福州隆重举行&#xff0c;此次峰会汇聚了各界精英嘉宾&#xff0c;共同见证了一系列具有里程碑意义的合作签约仪式&#xff0c;为社区新经济与消费新业态的融合发展注入强大动力。 上午时分&#…

数字逻辑——二进制

目录 1 信息与编码 1.1 什么是信息&#xff1f; 1.2 什么是编码&#xff1f; 2 数制和码制 2.1 数制 3 一些基本概念 3.1 位&#xff08;bit&#xff09; 3.2 字节&#xff08;byte&#xff09; 3.3 数据量的大小表示符号 4 二进制 4.1 二进制简介 4.2 二进制的…

PyQt信号槽实现页面的登录与跳转 #页面进一步优化

将登录框中的取消按钮使用信号和槽的机制&#xff0c;关闭界面。 将登录按钮使用信号和槽连接到自定义的槽函数中&#xff0c;在槽函数中判断ui界面上输入的账号是否为"admin"&#xff0c;密码是否为"123456",如果账号密码匹配成功&#xff0c;当前界面关…

博客园-添加统计图

&#x1f496;简介 通过WPS在线列表构建博客园每日相关数据统计图。 &#x1f449;效果 &#x1f4d6;实现 前往WPS https://www.kdocs.cn/latest 新建多维表格 创建表格视图 新建仪表盘 新建卡片、折线图 卡片配置示例 折线图配置示例 点击分享获取链接 ⭐链接配置 在co…

浏览器指纹是什么?14种指纹的技术原理

视频版链接&#xff1a; 浏览器指纹是什么&#xff1f;14种指纹背后的技术原理 浏览器指纹简介 这个网站在我没登录的情况下&#xff0c;就能生成一个用户ID。即使我打开了浏览器的无痕模式&#xff0c;生成出来的ID也是一模一样。这背后的技术就是浏览器指纹。即使用户没有登…