Linux理解文件操作 文件描述符fd 理解重定向 dup2 缓冲区 C语言实现自己的shell

文章目录

    • 前言
    • 一、文件相关概念与操作
      • 1.1 open()
      • 1.2 close()
      • 1.3 write()
      • 1.4 read()
      • 1.4 写入的时候先清空文件内容再写入
      • 1.5 追加(a && a+)
    • 二、文件描述符
      • 2.1 文件描述符 fd 0 1 2 的理解
      • 2.2 FILE结构体:的源代码
    • 三、深入理解文件描述符
    • 四、理解一切皆文件
    • 五、文件描述符的分配规则
    • 六、重定向原理
    • 七、dup2–重定向函数
      • 7.1 使用dup2完成重定向功能
    • 八、极简shell增加重定向的功能
      • 8.1 C语言实现简易shell全部源码
    • 九、缓冲区
    • 十、再次深入理解fd2
    • 十一、封装一个简单的文件接口库

前言

我们在平时使用的C/C++/Java的时候,我们所用的文件操作都是封装系统接口来进行供我们操作,我们在使用这些接口,本质上就是在访问硬件,也就是磁盘

  • 一个硬件设备是如何被函数接口的调用访问到的呢?

当然是通过操作系统,操作系统是管理硬件设备的,在我们学的C/C++/Java等等语言所封装的文件操作接口,都必须通过操作系统的允许,才可以访问到磁盘这个硬件设备,而操作系统是不相信任何用户的,所以为了能够得到操作系统的允许,我们又必须提供一些系统调用接口,供操作系统和用户打交道

  • 当我们在语言层面所使用的文件操作函数接口,本质要访问物理硬件设备磁盘,而访问该磁盘时候,必须要操作系统进行管理,同时操作系统会提供一系列的系统调用供用户去访问操作系统,而这些系统调用接口有很多,我们这里所说的系统调用接口是于文件操作相关的系统调用接口;

一、文件相关概念与操作

  • 我们所要知道的是:文件=文件内容+文件属性

  • 当一个文件的文件内容为空时, 此文件是否占用磁盘空间?

    • 这个答案是肯定的, 即使文件的内容为空, 其实此文件也是占用磁盘空间的, 因为文件并不只有内容, 文件还有属性

关于C语言的文件操作我们这里就不介绍了,下面我直接介绍Linux相关的文件~~

1.1 open()

  • 函数原型

在这里插入图片描述

  • 函数参数解析

    • pathname 所需打开文件的所在路径
    • flags需要传入的就是打开文件的选项
    • mode这个参数指的是打开文件需要修改成什么权限的数值,在我们之前学的权限的时候知道,在Linux下创建文件, 系统会根据umask值来赋予新创建的文件一个默认的文件权限,所以这个mode就是通过mode修改权限
    • open()接口的返回值, 被称为文件描述符fd, 可以看作表示一个打开的文件

  • open 函数具体使用哪个,和具体应用场景相关,如目标文件不存在,需要open创建,则第三个参数,表示创建文件的默认权限,否则,使用两个参数的open。

  • flag的参数

O_RDONLY: 只读打开
O_WRONLY: 只写打开
O_RDWR: 读,写打开
O_CREAT : 若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限
O_APPEND: 追加写
O_TRONC:文件以只读或者只写打开是,清空文件内容;
mode_t:打开文件的权限,以八进制形式写


  • 要实现一个参数实现多个功能就需要位图,flags参数其实需要采用位图的方式传参,也就是说,:Linux操作系统为flags参数提供的各种选项其实是表示一个整数二进制不同的位. 一个整数的比特位表示flags参数中某个选项是否被选中

  • 我们可以打开fcntl.h来查看定义
vim /usr/include/asm-generic/fcntl.h
  • 接下来我们来测试一下open如何使用:
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<stdlib.h>
#include<unistd.h>int main()
{// O_WRONLY 代表只写,如果没有该文件就创建,O_CREAT代表创建文件// 如果不指定创建文件的权限就会乱码int fd = open("log.txt", O_WRONLY | O_CREAT);if(fd < 0){printf("fopen fail!\n");exit(1);}close(fd);return 0;
}

在这里插入图片描述

  • 正确的使用方式是加上第三个参数:
int main()
{int fd = open("log.txt", O_WRONLY | O_CREAT, 0666);if(fd < 0){printf("fopen fail!\n");exit(1);}close(fd);return 0;
}

在这里插入图片描述

  • 这里虽然加上权限了但是怎么不对?少了个w,这是umask在作怪

在这里插入图片描述

  • 在创建文件的时候,OS会将指定的权限 - umask作为实际权限

  • 我们可以在程序的前面加上umask(0)即可解决

int main()
{umask(0);int fd = open("log.txt", O_WRONLY | O_CREAT, 0666);if(fd < 0){printf("fopen fail!\n");exit(1);}close(fd);return 0;
}

在这里插入图片描述

1.2 close()

  • 函数原型

在这里插入图片描述

  • 函数参数解读

    • fd为传入一个文件描述符,什么是文件描述符,我们后面讲

1.3 write()

  • 函数原型

在这里插入图片描述

  • 返回值

    • 写入成功返回写入成功的字节数,返回0为什么也没有写入,返回-1为写入失败
      在这里插入图片描述
  • 函数参数解读:

    • 第一个参数为要传入的文件描述符
    • 第二个参数为要传入的字符串
    • 第三个参数为要写入的长度
  • 函数使用

int main()
{umask(0);int fd = open("log.txt", O_WRONLY | O_CREAT, 0666);if(fd < 0){printf("fopen fail!\n");exit(1);}const char* buffer = "hello world\n";int cnt = 5;while (cnt--) {write(fd, buffer, strlen(buffer));}close(fd);return 0;
}
  • 已经写入指定文件成功~~

在这里插入图片描述

1.4 read()

  • 函数原型

在这里插入图片描述

  • 函数参数解读:

从文件描述符中读取const的字节的数据存入buf

int main()
{umask(0);int fd = open("log.txt", O_RDONLY);if(fd < 0){printf("fopen fail!\n");exit(1);}char buffer[128] = { 0 };// 从文件中读取内容写入buffer, 并输出read(fd, buffer, sizeof(buffer) - 1);printf("%s",buffer);close(fd);return 0;
}
  • 从文件中读取内容写入buffer, 并输出

在这里插入图片描述

1.4 写入的时候先清空文件内容再写入

  • 我们可以再加一个选项:
  • O_TRUNC的作用就是:打开文件时, 先清空文件内容
int main()
{umask(0);// 先清空再写入int fd = open("log.txt", O_CREAT | O_RDWR | O_TRUNC, 0666); if(fd < 0){printf("fopen fail!\n");exit(1);}const char* buffer = "hello linux\n";write(fd, buffer, strlen(buffer));close(fd);return 0;
}

在这里插入图片描述

1.5 追加(a && a+)

  • 使用O_APPEND即可完成文件的追加
int main()
{umask(0);// 先清空再写入int fd = open("log.txt", O_CREAT | O_WRONLY | O_APPEND, 0666); if(fd < 0){printf("fopen fail!\n");exit(1);}const char* buffer = "hello linux~~\n";write(fd, buffer, strlen(buffer));close(fd);return 0;
}

在这里插入图片描述

只传入 O_APPEND 选项, 不传入 O_WRONLYO_RDWR 是无法追加写入的, 因为没有写入打开

二、文件描述符

  • 我们上面所写的fd为open的返回值再次理解一下
  • 我们写写下面的这么一段代码,多次打开文件,查看open返回值
int main()
{umask(0);int fd1 = open("log.txt", O_RDWR | O_CREAT, 0666); int fd2 = open("log.txt", O_RDWR | O_CREAT, 0666); int fd3 = open("log.txt", O_RDWR | O_CREAT, 0666); printf("fd1: %d\n", fd1);printf("fd2: %d\n", fd2);printf("fd3: %d\n", fd3);close(fd1);close(fd2);close(fd3);return 0;
}
  • 这里我们看到返回值是从3开始的,并且递增连续

在这里插入图片描述

  • 那么为什么从3开始,0,1,2呢?

  • 其实在一个进程运行起来的时候默认会给我们打开3个文件流:

    • fd 0:标准输入 –> 键盘

    • fd 1:标准输出 –> 显示器

    • fd 2:标准错误 –> 显示器


2.1 文件描述符 fd 0 1 2 的理解

  • 当我们的程序运行起来后,编程了进程之后,默认情况下,OS会帮我们打开三个标准输入输出~

  • 其中在Linux上:

0:标准输入,键盘
1:标准输出,显示器
2:标准错误,显示器

  • 在C语言上:

stdin:标准输入,键盘
stdout:标准输出,显示器
stderr:标准错误,显示器

  • 在stdio.h头文件就可以看到声明

在这里插入图片描述

  • 本质是 stdinstdout stderr 就是一个变量名,类型为 FILE* 而这个FILE 结构体里面有个成员就是 fd,文件描述符;
  • 就是C语言的 stdinstdout stderr 包含 系统的 0 1 2;

不只是C语言,其他语言都有自己的封装

  • 我们也可以验证一下:
int main() {// C语言会默认打开 stdin, stdout, stderrprintf("stdin-fd: %d\n", stdin->_fileno);printf("stdout-fd: %d\n", stdout->_fileno);printf("stderr-fd: %d\n", stderr->_fileno);return 0;
}

在这里插入图片描述

2.2 FILE结构体:的源代码

typedef struct _IO_FILE FILE; //在/usr/include/stdio.h
struct _IO_FILE {int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags//缓冲区相关/* The following pointers correspond to the C++ streambuf protocol. *//* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */char* _IO_read_ptr; /* Current read pointer */char* _IO_read_end; /* End of get area. */char* _IO_read_base; /* Start of putback+get area. */char* _IO_write_base; /* Start of put area. */char* _IO_write_ptr; /* Current put pointer. */char* _IO_write_end; /* End of put area. */char* _IO_buf_base; /* Start of reserve area. */char* _IO_buf_end; /* End of reserve area. *//* The following fields are used to support backing up and undo. */char *_IO_save_base; /* Pointer to start of non-current get area. */char *_IO_backup_base; /* Pointer to first valid character of backup area */char *_IO_save_end; /* Pointer to end of non-current get area. */struct _IO_marker *_markers;struct _IO_FILE *_chain;int _fileno; //封装的文件描述符
#if 0int _blksize;
#elseint _flags2;
#endif_IO_off_t _old_offset; /* This used to be _offset but it's too small. */
#define __HAVE_COLUMN /* temporary *//* 1+column number of pbase(); 0 is unknown. */unsigned short _cur_column;signed char _vtable_offset;char _shortbuf[1];/* char* _save_gptr; char* _save_egptr; */_IO_lock_t *_lock;
#if

三、深入理解文件描述符

  • 前面我们有一个代码是打开多个文件,它返回的fd值是连续递增的,其实本质上就是数组的下标,所以本质上是文件描述符实际上就是某个数组的下标

  • 一个进程是可以打开多个文件的. 而操作系统中又存在着许多的进程, 其实也就意味着操作系统中存在的大量的被打开的文件

  • 操作系统会对这些大量的被打开的文件进行统一的管理, 会将文件的所有属性描述在一个结构体中, 并将所有的描述着打开文件属性的结构体组织在一起进行管理. 就像管理进程,实际上实在管理进程PCB一样,

  • 在Linux系统中, 描述的打开文件属性的结构体叫做:struct file{};, 每一个打开的文件都由这样一个结构体维护着, 且结构体之间会构成一个数据结构, 方便操作系统进行管理即打开的文件在操作系统中, 实际上都在一个数据结构中维护着若操作系统将这些数据结构以链表的形式连接起来维护, 那么就会存在这样一个维护打开文件的数据结构


  • 其中file指针指向一个 struct file_struct 结构体变量, 而此结构体变量中存储着一个 struct file* fd_array[] 指针数组

  • fd_array[] 指针数组中的每一个空间都存储着一个 struct file* 结构体指针, 指向一个打开的文件

  • 进程的PCB中有一个结构体指针变量 指向了一个结构体变量, 此结构体变量中存储着fd_array[]数组, fd_array[]中存储着 描述了打开文件属性的结构体的指针, 其实也就是指向了打开的文件

  • fd_array[]数组的下标, 就是open()close()等系统接口使用的fd文件描述符. 文件操作的系统接口可以通过fd, 在fd_array[]数组中找到指定下标存储的指针 再找到指针指向的文件

在这里插入图片描述

当你在创建一个新的文件时候,那么操作系统就会给你搞一个 strcut file, 然后把它存放到 fd_array[ ] 数组里,然后把对应的下标返回给上一层用户;那么用户就可以拿到下标,也就是描述符干自己的事了

四、理解一切皆文件

  • 我们的计算机中, 有着非常多的I/O硬件设备:磁盘、键盘、显示器、网卡……

  • 这些I/O设备想要与操作系统交换数据, 一定有它们自己的读写方式, 并且每种硬件的读写方式是独属于此硬件的,各硬件之间的结构不同, 读写方式当然不可能完全相同

  • 每种硬件都有其自己的读写方式, 那么当操作系统需要向这些I/O设备写入数据或需要从这些I/O设备中读取数据时, 操作系统会怎么做呢?

    • 这些打开的I/O设备, 在操作系统中也会以struct file{} 结构体的形式维护着, 并且不同硬件的结构体中还会存在函数指针指向此硬件的各种方法:

在这里插入图片描述

Linux操作系统的内存文件系统会对所有设备和打开的文件以一个统一的视角进行组织和管理, 这就是 Linux下一切皆文件

  • Linux这种将一切设备和文件都以一个统一的视角(file结构体) 进行组织和管理的做法, 被称为 虚拟文件系统(VFS)

五、文件描述符的分配规则

  • 我们可以再次观察下面代码
int main() {int fd = open("log.txt", O_RDONLY);if(fd < 0){perror("open fail!\n");exit(-1);}printf("fd: %d\n",fd);close(fd);return 0;
}
  • 上面也说了,默认是从3开始的012分别被输入输出错误占用了

在这里插入图片描述

  • 那么我们先关闭0再来看一下,这次分配的fd为什么
int main() {close(0); // 关闭0号描述符int fd = open("log.txt", O_RDONLY);if(fd < 0){perror("open fail!\n");exit(-1);}printf("fd: %d\n",fd);close(fd);return 0;
}

在这里插入图片描述

可以观察到,文件描述符的分配规则:files_struct数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符。

六、重定向原理

  • 那么我们先关闭1也就是输出
int main() {umask(0);close(1);int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);if(fd < 0){perror("open fail!\n");exit(-1);}printf("fd: %d\n",fd);const char* str = "hello world\n";write(fd, str, strlen(str));close(fd);return 0;
}
  • 本来要打印到屏幕上的被写入到了文件中

在这里插入图片描述

  • 当我们关闭了 1号文件描述符,断开了 fd_arrary 数组元素1号位置,也就是断开了标准输入 struct file的联系,而当我们再次用open函数打开一个文件为 log.txt时候,文件描述符分配原则告诉我们,就会分配一个数组 1号位置给该文件log.txt;一旦我们使用printf输出时候,就不会显示到屏幕了,而显示到文件;这是因为printf默认是往便准输入输出内容的,而printf的标准输入就是stdout这个变量,而stdout这个变量就是一个FILE类型的结构体指针,而这个结构体指针里面有一个成员就是文件描述符fd,而fd就是1号,而这个1号就是指向struct file 这个结构体,这个结构体就是标准输入

七、dup2–重定向函数

  • 函数原型

在这里插入图片描述

  • 函数参数解读

    • 主要功能是文件描述符的复制

    • 成功返回新文件描述符,失败返回-1

  • oldfd:原先的文件描述符

  • newfd:新的文件描述符

  • 由dup返回的新文件描述符一定是当前可用文件描述符中的最小数值.

  • 用dup 2则可以用newfd参数指定新描述符的数值.如果newfd已经打开,则先将其关闭.如若oldfd等于则dup 2返回newfd,而不关闭它在进程间通信时可用来改变进程的标准输入和标准输出设备

7.1 使用dup2完成重定向功能

int main() {int fd = open("log.txt", O_CREAT | O_WRONLY | O_TRUNC, 0644 );if(fd < 0){perror("open error:");exit(1);}dup2(fd,1); //本应该输出到1的,输出到了fd中printf("printf: hello world\n");fprintf(stdout,"fprintf: hello world\n");fputs("fputs: hello world\n", stdout);close(fd);return 0;
}

在这里插入图片描述

  • 此时,我们发现,本来应该输出到显示器上的内容,输出到了文件log.txt当中,其中,fd=1。这种现象叫做输出重定向。常见的重定向有:>, >>, <

  • 那重定向的本质是什么呢?

    • 原理就是,把oldfd位置的值,复制给了newfd位置的值,这会导致,newfd位置的值和oldfd位置值一样,也就是说,newffd位置的值,不再指向原来的struct file,而是指向了 oldfdstruct file

在这里插入图片描述

八、极简shell增加重定向的功能

  • 实现
char *checkDir(char commandstr[], enum redir* redir_type)
{char* start = commandstr;char* end = commandstr + strlen(commandstr);//1. 检测commandstr内部是否有 > >> <while(start < end){if(*start == '>'){if(*(start + 1) == '>'){                                                                                                                                                                                     *redir_type = REDIR_APPEND;//细节处理为后续命令行分割做铺垫*start = '\0';return start + 2;}else{*redir_type = REDIR_OUTPUT;//细节处理为后续命令行分割做铺垫*start = '\0';return start + 1;}}else if(*start  == '<'){*redir_type = REDIR_INPUT;//细节处理为后续命令行分割做铺垫*start = '\0';return start + 1;}start++;}return NULL;
}
  • 主函数
char *filename = checkDir(commondstr, &redir_type);
  • 子进程的部分:

注意这里一定要将权限先置成0666在执行,要不然可能会出现权限不够写入错误的问题

if(id == 0)
{int fd = -1;if(redir_type != REDIR_NONE){//表示找到了文件,并且重定向类型确定if(redir_type == REDIR_INPUT){fd = open(filename , O_RDONLY);dup2(fd, 0);}else if(redir_type == REDIR_OUTPUT){fd = open(filename , O_CREAT | O_TRUNC | O_WRONLY, 0666);dup2(fd, 1);}else{fd = open(filename , O_CREAT | O_APPEND | O_WRONLY, 0666);dup2(fd, 1);}}//childexecvp(argv[0], argv);exit(0);
}

8.1 C语言实现简易shell全部源码

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <ctype.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <fcntl.h>#define SIZE 512
#define ZERO '\0'
#define SEP " "
#define NUM 32
#define SkipPath(p) do{ p += (strlen(p)-1); while(*p != '/') p--; }while(0)
#define SkipSpace(cmd, pos) do{\while(1){\if(isspace(cmd[pos]))\pos++;\else break;\}\
}while(0)#define None_Redir 0
#define In_Redir   1
#define Out_Redir  2
#define App_Redir  3int redir_type = None_Redir;
char *filename = NULL;char cwd[SIZE*2];
char *gArgv[NUM];
int lastcode = 0;void Die()
{exit(1);
}const char *GetHome()
{const char *home = getenv("HOME");if(home == NULL) return "/";return home;
}const char *GetUserName()
{const char *name = getenv("USER");if(name == NULL) return "None";return name;
}
const char *GetHostName()
{const char *hostname = getenv("HOSTNAME");if(hostname == NULL) return "None";return hostname;
}
// 临时
const char *GetCwd()
{const char *cwd = getenv("PWD");if(cwd == NULL) return "None";return cwd;
}// commandline : output
void MakeCommandLineAndPrint()
{char line[SIZE];const char *username = GetUserName();const char *hostname = GetHostName();const char *cwd = GetCwd();SkipPath(cwd);snprintf(line, sizeof(line), "[%s@%s %s]> ", username, hostname, strlen(cwd) == 1 ? "/" : cwd+1);printf("%s", line);fflush(stdout);
}int GetUserCommand(char command[], size_t n)
{char *s = fgets(command, n, stdin);if(s == NULL) return -1;command[strlen(command)-1] = ZERO;return strlen(command); 
}void SplitCommand(char command[], size_t n)
{(void)n;// "ls -a -l -n" -> "ls" "-a" "-l" "-n" gArgv[0] = strtok(command, SEP);int index = 1;while((gArgv[index++] = strtok(NULL, SEP))); // done, 故意写成=,表示先赋值,在判断. 分割之后,strtok会返回NULL,刚好让gArgv最后一个元素是NULL, 并且while判断结束
}void ExecuteCommand()
{pid_t id = fork();if(id < 0) Die();else if(id == 0){//重定向设置if(filename != NULL){if(redir_type == In_Redir){int fd = open(filename, O_RDONLY);dup2(fd, 0);}else if(redir_type == Out_Redir){int fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC, 0666);dup2(fd, 1);}else if(redir_type == App_Redir){int fd = open(filename, O_WRONLY | O_CREAT | O_APPEND, 0666);dup2(fd, 1);}else{}}// childexecvp(gArgv[0], gArgv);exit(errno);}else{// fahterint status = 0;pid_t rid = waitpid(id, &status, 0);if(rid > 0){lastcode = WEXITSTATUS(status);if(lastcode != 0) printf("%s:%s:%d\n", gArgv[0], strerror(lastcode), lastcode);}}
}void Cd()
{const char *path = gArgv[1];if(path == NULL) path = GetHome();// path 一定存在chdir(path);// 刷新环境变量char temp[SIZE*2];getcwd(temp, sizeof(temp));snprintf(cwd, sizeof(cwd), "PWD=%s", temp);putenv(cwd); // OK
}int CheckBuildin()
{int yes = 0;const char *enter_cmd = gArgv[0];if(strcmp(enter_cmd, "cd") == 0){yes = 1;Cd();}else if(strcmp(enter_cmd, "echo") == 0 && strcmp(gArgv[1], "$?") == 0){yes = 1;printf("%d\n", lastcode);lastcode = 0;}return yes;
}void CheckRedir(char cmd[])
{int pos = 0;int end = strlen(cmd);while(pos < end){if(cmd[pos] == '>'){if(cmd[pos+1] == '>'){cmd[pos++] = 0;pos++;redir_type = App_Redir;SkipSpace(cmd, pos);filename = cmd + pos;}else{cmd[pos++] = 0;redir_type = Out_Redir;SkipSpace(cmd, pos);filename = cmd + pos;}}else if(cmd[pos] == '<'){cmd[pos++] = 0;redir_type = In_Redir;SkipSpace(cmd, pos);filename = cmd + pos;}else{pos++;}}
}int main()
{int quit = 0;while(!quit){// 0. 重置redir_type = None_Redir;filename = NULL;// 1. 我们需要自己输出一个命令行MakeCommandLineAndPrint();// 2. 获取用户命令字符串char usercommand[SIZE];int n = GetUserCommand(usercommand, sizeof(usercommand));if(n <= 0) return 1;// 2.1 checkredirCheckRedir(usercommand);// 3. 命令行字符串分割. SplitCommand(usercommand, sizeof(usercommand));// 4. 检测命令是否是内建命令n = CheckBuildin();if(n) continue;// 5. 执行命令ExecuteCommand();}return 0;
}

九、缓冲区

  • 当时我们在写进度条的时候也提到了缓冲区–输出缓冲区,那么这个缓冲区在哪里?为什么要存在?和struct file[缓冲区],两个是一回事吗?

  • 我们可以再次写下代码观察:

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;
}
  • 分别执行了两次,一次是直接输出,第二次是重定向到了文件里,再查看文件里的内容

在这里插入图片描述

  • 我们发现了奇怪的一幕,为什么通过stdout向屏幕输出的内容在文件中显示了两次,而直接采用文件描述符的方式只有一次

  • 我们发现 printf 和 fwrite (库函数)都输出了2次,而 write 只输出了一次(系统调用)。为什么呢?肯定和fork有关 。

  • 一般C库函数写入文件时是全缓冲的,而写入显示器是行缓冲
  • printf fwrite 库函数会自带缓冲区(进度条例子就可以说明),当发生重定向到普通文件时,数据的缓冲方式由行缓冲变成了全缓冲
  • 而我们放在缓冲区中的数据,就不会被立即刷新,甚至fork之后
  • 但是进程退出之后,会统一刷新,写入文件当中
  • 但是fork的时候,父子数据会发生写时拷贝,所以当你父进程准备刷新的时候,子进程也就有了同样的一份数据,随即产生两份数据write没有变化,说明没有所谓的缓冲
  • 综上: printf fwrite 库函数会自带缓冲区,而 write 系统调用没有带缓冲区。另外,我们这里所说的缓冲区,都是用户级缓冲区。其实为了提升整机性能,OS也会提供相关内核级缓冲区,不过不再我们讨论范围之内。

  • 那这个缓冲区谁提供呢? printf fwrite 是库函数, write 是系统调用,库函数在系统调用的“上层”, 是对系统调用的“封装”,但是write没有缓冲区,而 printf fwrite 有,足以说明,该缓冲区是二次加上的,又因为是C,所以由C标准库提供

  • 其实缓冲区就在FILE结构体中

FILE结构体的代码:

struct _IO_FILE {int _flags;       /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags/* The following pointers correspond to the C++ streambuf protocol. *//* Note:  Tk uses the _IO_read_ptr and _IO_read_end fields directly. */char* _IO_read_ptr;   /* Current read pointer */char* _IO_read_end;   /* End of get area. */char* _IO_read_base;  /* Start of putback+get area. */                                                                 char* _IO_write_base; /* Start of put area. */char* _IO_write_ptr;  /* Current put pointer. */char* _IO_write_end;  /* End of put area. */char* _IO_buf_base;   /* Start of reserve area. */char* _IO_buf_end;    /* End of reserve area. *//* The following fields are used to support backing up and undo. */char *_IO_save_base; /* Pointer to start of non-current get area. */char *_IO_backup_base;  /* Pointer to first valid character of backup area */char *_IO_save_end; /* Pointer to end of non-current get area. */struct _IO_marker *_markers;struct _IO_FILE *_chain;int _fileno;
#if 0int _blksize;
#else                                                                                                                    int _flags2;
#endif_IO_off_t _old_offset; /* This used to be _offset but it's too small.  */#define __HAVE_COLUMN /* temporary *//* 1+column number of pbase(); 0 is unknown. */unsigned short _cur_column;signed char _vtable_offset;char _shortbuf[1];/*  char* _save_gptr;  char* _save_egptr; */_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};

  • 对于缓冲区的理解:

    • 用户级缓冲区
      • 解耦
      • 提高效率(提高使用者的效率,提高IO的效率)
    • 内核级缓冲区
  • 是什么:缓冲区就是一段内存空间

  • 为什么:为上层提高高效的IO体验,间接提高整体效率

  • 怎么办?

    • 刷新策略
      • 立即刷新(fflush(stdout),int fsync(int fd))
      • 行刷新(显示器)
      • 全缓冲。(缓冲区写满,才刷新—>普通文件)
    • 特殊情况
      • 进程退出,系统自动刷新
      • 强制刷新
  • 内核策略并不关心用户


十、再次深入理解fd2

  • 前面我们没有谈到2号描述符有什么作用,我们接下来就来谈一下~
int main()
{perror("error!!!!!!");// 打印错误信息fprintf(stdout, "hello fprintf stdout\n");fprintf(stderr, "hello fprintf stderr\n");fprintf(stdout, "hello fprintf stdout\n");fprintf(stderr, "hello fprintf stderr\n");fprintf(stdout, "hello fprintf stdout\n");fprintf(stderr, "hello fprintf stderr\n");fprintf(stdout, "hello fprintf stdout\n");fprintf(stderr, "hello fprintf stderr\n");fprintf(stdout, "hello fprintf stdout\n");fprintf(stderr, "hello fprintf stderr\n");fprintf(stdout, "hello fprintf stdout\n");fprintf(stderr, "hello fprintf stderr\n");return 0;
}
  • 观察到我们重定向的时候只将标准输出重定向到了文件中了,错误没有

在这里插入图片描述

在这里插入图片描述

  • 那么我们想讲1和2分别重定向到一个文件中,一个为ok.txt一个为err.txt
  1. 重定正确写法
./myfile 1>ok.txt

在这里插入图片描述

  1. 分别重定向到两个文件
./myfile 1>ok.log  2>err.log
  • 将正确的和错误的分开了

在这里插入图片描述

在这里插入图片描述

  1. 那么我们可以将全部的信息重定向到一个文件中
./myfile 1>all.log 2>&1
  • 首先将1里面的内容变成all.log,然后再将这里的2&1也写到2里面

在这里插入图片描述

在这里插入图片描述

有这个标准错误就是为了能将正确信息和错误信息分开,方便我们dbug

十一、封装一个简单的文件接口库

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>#define LINE_SIZE 1024
#define FLUSH_NOW  1 
#define FLUSH_LINE 2 // 行缓冲
#define FLUSH_FULL 4 // 全缓冲#define FILE_NAME "log.txt"struct _myFILE
{unsigned int flags;int fileno;// 缓冲区char cache[LINE_SIZE];int cap;int pos; // 下次写入的位置
};
typedef struct  _myFILE myFILE;myFILE* my_fopen(const char *path, const char *flag);void my_fflush(myFILE *fp);ssize_t my_fwrite(myFILE *fp, const char *data, int len);void my_fclose(myFILE *fp);myFILE* my_fopen(const char *path, const char *flag)
{int flag1 = 0;int iscreate = 0;mode_t mode = 0666;if(strcmp(flag, "r") == 0){flag1 = (O_RDONLY);}else if(strcmp(flag, "w") == 0){flag1 = (O_WRONLY | O_CREAT | O_TRUNC);iscreate = 1;}else if(strcmp(flag, "a") == 0){flag1 = (O_WRONLY | O_CREAT | O_APPEND);iscreate = 1;}else{}int fd = 0;if(iscreate)fd = open(path, flag1, mode);elsefd = open(path, flag1);if(fd < 0) return NULL;myFILE *fp = (myFILE*)malloc(sizeof(myFILE));if(!fp) return NULL;fp->fileno = fd;fp->flags = FLUSH_LINE;fp->cap = LINE_SIZE;fp->pos = 0;return fp;
}void my_fflush(myFILE *fp)
{write(fp->fileno, fp->cache, fp->pos);fp->pos = 0;
}ssize_t my_fwrite(myFILE *fp, const char *data, int len)
{// 写入操作本质是拷贝, 如果条件允许,就刷新,否则不做刷新memcpy(fp->cache+fp->pos, data, len); //肯定要考虑越界, 自动扩容fp->pos += len;if((fp->flags&FLUSH_LINE) && fp->cache[fp->pos-1] == '\n'){my_fflush(fp);}return len;
}void my_fclose(myFILE *fp)
{my_fflush(fp);close(fp->fileno);free(fp);
}int main()
{myFILE *fp = my_fopen(FILE_NAME, "w");if(fp == NULL) return 1;const char *str = "hello bit\n";int cnt = 10;char buffer[128];while(cnt){sprintf(buffer, "%s - %d", str, cnt);my_fwrite(fp, buffer, strlen(buffer)); // strlen()+1不需要cnt--;sleep(1);my_fflush(fp);}my_fclose(fp);return 0;
}

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

DETR类型检测网络---思考和Tricks测试

目录 batch_size的影响辅助损失的作用学习率的影响Decoder层数增多的影响3D检测中, feats位置编码和query位置编码是否共享mpl层背景-关于query的生成方式 利用widthformer类似的方式简化注意力机制 batch_size的影响 batch8: batch20: 由实验结果可知:这里实验有问题,横坐标…

JAVA语言开发的智慧城管系统源码:技术架构Vue+后端框架Spring boot+数据库MySQL

通过综合应用计算机技术、网络技术、现代通信技术等多种信息技术&#xff0c;充分融合RS遥感技术、GPS全球定位技术、GIS地理信息系统&#xff0c;开始建设一个动态可视的、实时更新的、精细量化的城市管理系统。智慧城管将采用云平台架构方式进行建设&#xff0c;基于现有数字…

人工智能大模型应用指南

大家好&#xff0c;我是爱编程的喵喵。双985硕士毕业&#xff0c;现担任全栈工程师一职&#xff0c;热衷于将数据思维应用到工作与生活中。从事机器学习以及相关的前后端开发工作。曾在阿里云、科大讯飞、CCF等比赛获得多次Top名次。现为CSDN博客专家、人工智能领域优质创作者。…

直流屏整流模块HG07A220R电源模块HG10A220R

直流屏整流模块HG07A220R电源模块HG10A220R 其他同类型监控模块PM09T电源模块HG22005/S&#xff0c;HG22010/S&#xff0c;HG11010/S&#xff0c;HG11020/S&#xff0c;HG10A220Z&#xff0c;HG10A220F&#xff0c;HG05A220Z&#xff0c;HG07A220Z&#xff0c;HG10A110Z&#x…

免费可商用字体素材大全,办公设计字体合集打包166款

一、素材描述 这是一套免费可商用字体素材&#xff0c;这些字体一般会在办公与设计的时候用到&#xff0c;比如&#xff0c;Photoshop、illustrator、Coreldraw、AfterEffects、Indesign、WPS、Office&#xff0c;等等&#xff0c;想要更好更快地办公与设计&#xff0c;字体还…

【Java】面向对象核心知识点(二),文章层次分明,内容精益求精,代码简单易懂

目录 一、构造方法 1.1 概念 1.2 作用 1.3 代码 二、抽象 2.1 概念 2.2 作用 2.3 注意 2.4 代码 三、接口 3.1 概念 3.2 作用 3.3 注意 3.4 语法 3.5 代码 四、内部类 4.1 成员内部类 4.2 局部内部类 4.3 静态内部类 4.4 匿名内部类 &#xff08;原创文章&…

WWW‘24 | 课程学习CL+模仿学习IL用于ETF及商品期货交易

WWW24 | 课程学习CL模仿学习IL用于ETF及商品期货交易 原创 QuantML QuantML 2024-05-04 13:47 论文地址&#xff1a;[2311.13326] Curriculum Learning and Imitation Learning for Model-free Control on Financial Time-series (arxiv.org) 本文探讨了在金融时间序列数据上…

W801学习笔记十七:古诗学习应用——上

硬件驱动以及软件架构大体上已经完成&#xff0c;尚存一些遗漏之处&#xff0c;后续会寻找合适的时机进行补充。自此章起&#xff0c;将正式迈入软件应用阶段&#xff0c;尤其是游戏开发领域。 关于第一个应用&#xff0c;此前已有一些构想&#xff1a; 其一&#xff0c;随机…

【跟我学RISC-V】(二)RISC-V的基础知识学习与汇编练习

写在前面&#xff1a; 这篇文章是跟我学RISC-V的第二期&#xff0c;是第一期的延续&#xff0c;第一期主要是带大家了解一下什么是RISC-V,是比较大体、宽泛的概念。这一期主要是讲一些基础知识&#xff0c;然后进行RISC-V汇编语言与c语言的编程。在第一期里我们搭建了好几个环…

FBA头程空运发货流程详解|携手天图通逊,开启高效跨境物流之旅

在众多头程发货方式中&#xff0c;空运以其速度快、时效高的特点&#xff0c;成为许多卖家特别是急需快速补货或应对市场变化的友好选择&#xff0c;那FBA头程空运的发货流程是怎样的呢? 1、发货准备 在开始空运之前&#xff0c;首先需要进行发货准备。这包括将货物送达指定的…

DETR类型检测网络实验2---优化测试

补全reference_point Anchor-DETR提出用预定义的参考点生成query_pos; DBA-DETR提出预定义参考信息由(x,y)增至(x,y,w,h) 那么在3D检测任务中是否可以把预定义参考信息补全为(x,y,z,l,w,h,sint,cost),而query_pos都是使用xy两个维度(因为是bev网络). (这种方法在Sparse-DETR中…

轻松应对数据恢复挑战:雷神笔记本,不同情况不同策略

在数字化时代&#xff0c;数据无疑是我们生活中不可或缺的一部分。无论是重要的工作文件、珍贵的家庭照片&#xff0c;还是回忆满满的视频&#xff0c;一旦丢失&#xff0c;都可能给我们的生活带来诸多不便。雷神笔记本作为市场上备受欢迎的电脑品牌&#xff0c;用户在使用过程…

Adobe-Premiere-CEP 扩展 入门-视频剪辑-去气口插件-Silence Remover

短视频&#xff0c;这两年比较火&#xff0c;不要再问为什么用Premiere&#xff0c;非常难用&#xff0c;为什么不用某影&#xff0c;某些国内软件非常接地气简单&#xff0c;又例如某音资深的视频短编辑就很好用了。。。 Premiere二次开发调试难&#xff0c;不如自己搞个cons…

perl:用 MIDI::Simple 生成midi文件,用 pygame 播放 mid文件

在 csdn.net 下载 strawberry-perl-5.32.1.1-64bit.zip 解压安装在 D:\Strawberry\ 运行 cpan install MIDI::Simple D:\Strawberry\c\bin\gmake.exe test -- OK Running make install for CONKLIN/MIDI-Perl-0.84.tar.gz Installing D:\Strawberry\perl\site\lib\MIDI.pm I…

kubebuilder(6)webhook

operator中的webhook也是很重要的一块功能。也是相对比较独立的模块&#xff0c;所以放在后面讲。 webhook是一个callback&#xff0c;注册到k8s的api-server上。当某个特定的时间发生时&#xff0c;api server就会查询注册的webhook&#xff0c;并根据一些逻辑确认转发消息给…

【ARM Cortex-M3指南】3:Cortex-M3基础

文章目录 三、Cortex-M3基础3.1 寄存器3.1.1 通用目的寄存器 R0~R73.1.2 通用目的寄存器 R8~R123.1.3 栈指针 R133.1.4 链接寄存器 R143.1.5 程序计数器 R15 3.2 特殊寄存器3.2.1 程序状态寄存器3.2.2 PRIMASK、FAULTMASK和BASEPRI寄存器3.2.3 控制寄存器 3.3 操作模式3.4 异常…

使用FPGA实现串-并型乘法器

介绍 其实我们知道&#xff0c;用FPGA实现乘法器并不是一件很简单的事&#xff0c;而且在FPGA中也有乘法器的IP核可以直接调用&#xff0c;我这里完全就是为了熟悉一些FPGA的语法然后写了这样一个电路。 串-并型乘法器模块 从字面上看&#xff0c;串-并乘法器就是其中一个乘数…

Nodejs process.nextTick() 使用详解

还是大剑师兰特&#xff1a;曾是美国某知名大学计算机专业研究生&#xff0c;现为航空航海领域高级前端工程师&#xff1b;CSDN知名博主&#xff0c;GIS领域优质创作者&#xff0c;深耕openlayers、leaflet、mapbox、cesium&#xff0c;canvas&#xff0c;webgl&#xff0c;ech…

OpenCV 为轮廓创建边界框和圆(62)

返回:OpenCV系列文章目录&#xff08;持续更新中......&#xff09; 上一篇:OpenCV检测凸包(61) 下一篇 :OpenCV如何为等值线创建边界旋转框和椭圆(62) ​ 目标 在本教程中&#xff0c;您将学习如何&#xff1a; 使用 OpenCV 函数 cv::boundingRect使用 OpenCV 函数 cv::mi…

数据库(MySQL)—— 事务

数据库&#xff08;MySQL&#xff09;—— 事务 什么是事务事务操作未控制事务测试异常情况 控制事务一查看/设置事务提交方式&#xff1a;提交事务回滚事务 控制事务二开启事务提交事务回滚事务 并发事务问题脏读&#xff08;Dirty Read&#xff09;不可重复读&#xff08;Non…