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, stderr
	printf("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 0
	int _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;
#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);
    }
  }
  //child
  execvp(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  3

int 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
            {}
        }

        // child
        execvp(gArgv[0], gArgv);
        exit(errno);
    }
    else
    {
        // fahter
        int 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 checkredir
        CheckRedir(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 0
  int _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);
    else
        fd = 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;
}

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

jupyter notebook 设置密码报错ModuleNotFoundError: No module named ‘notebook.auth‘

jupyter notebook 设置密码报错ModuleNotFoundError: No module named ‘notebook.auth‘ 原因是notebook新版本没有notebook.auth 直接输入以下命令即可设置密码 jupyter notebook password

k8s调度原理以及自定义调度器

kube-scheduler 是 kubernetes 的核心组件之一&#xff0c;主要负责整个集群资源的调度功能&#xff0c;根据特定的调度算法和策略&#xff0c;将 Pod 调度到最优的工作节点上面去&#xff0c;从而更加合理、更加充分的利用集群的资源&#xff0c;这也是我们选择使用 kubernete…

Linux---软硬链接

软链接 我们先学习一下怎样创建软链接文件&#xff0c;指令格式为&#xff1a;ln -s 被链接的文件 生成的链接文件名 我们可以这样记忆&#xff1a;ln是link的简称&#xff0c;s是soft的简称。 我们在下面的图片中就是给test文件生成了一个软链接mytest&#xff1a; 我们来解…

数据结构篇其四---栈:后进先出的魔法世界

前言 栈的学习难度非常简单&#xff0c;前提是如果你学过顺序表和单链表的话&#xff0c;我直接说我的观点了&#xff0c;栈就是有限制的顺序表和单链表。 栈只允许一端进行插入删除。栈去除了各种情况复杂的插入删除&#xff0c;只允许一端插入删除的特性&#xff0c;这一种数…

5月4(信息差)

&#x1f384; HDMI ARC国产双精度浮点dsp杜比数码7.1声道解码AC3/dts/AAC环绕声光纤、同轴、USB输入解码板KC33C &#x1f30d; 国铁集团回应高铁票价将上涨 https://finance.eastmoney.com/a/202405043066422773.html ✨ 源代码管理平台GitLab发布人工智能编程助手DuoCha…

【数据结构】您有一份KMP算法教学已到账,请注意查收!!!

KMP算法 导读一、KMP算法1.1 重要术语1.2 部分匹配值1.3 部分匹配值的作用 二、KMP算法原理2.1 从指针的角度理解KMP算法2.2 从匹配的角度理解KMP算法2.3 小结 三、KMP算法的实现3.1 next数组3.2 next数组的计算3.2.1 通过PM值计算next数组3.2.2 通过移位模拟计算next数组3.2.3…

Web Storage 笔记11 网页数据存储

相关内容&#xff1a;Web Storage基本概念、localStorage、sessionStorage、登录注销实例、…… 在制作网页时会希望记录一些信息&#xff0c;例如用户登录状态、计数器或者小游戏等&#xff0c;但是又不希望用到数据库&#xff0c;就可以利用WebStorage技术将数据存储在用户浏…

Kubelet containerd 管理命令 ctr常用操作

镜像常用操作 1. 拉取镜像 ctr images pull docker.io/library/nginx:alpine 指定平台 --all-platforms&#xff1a;所有平台&#xff08;amd64 、arm、386 、ppc64le 等&#xff09;&#xff0c;不加的话下载当前平台架构 --platform&#xff1a;指定linux/amd64平台 ctr …

鸿蒙开发仿咸鱼TabBar

鸿蒙开发自定义TabBar&#xff0c;实现tabBar 上中间按钮凸起效果 第一步、定义数据模型 export default class TabItemData{defaultIcon: ResourceselectedIcon: Resourcetitle: stringisMiddle: booleanconstructor(defaultIcon:Resource, selectedIcon:Resource, title:st…

并发-启动线程的正确姿势

目录 启动线程的正确姿势 Start方法原理解读 Run方法原理解读 常见问题 启动线程的正确姿势 start()与run()方法的比较测试结果可以看出&#xff0c;runnable.run()方法是由main线程执行的&#xff0c;而要子线程执行就一定要先调用start()启动新线程去执行run方法并不能成…

【数据结构】第四讲:双向链表

目录 一、链表的分类 二、双向链表的结构及实现 1.带头双向链表的结构 2.创建节点 3.初始化 4.尾插 5.打印 6.头插 7.尾删 8.头删 9.在pos位置之后插入数据 10.删除pos节点 11.查找 12.销毁 个人主页&#xff1a;深情秋刀鱼-CSDN博客 数据结构专栏&#xff1a;数…

Mac M2 本地下载 Xinference

想要在Mac M2 上部署一个本地的模型。看到了Xinference 这个工具 一、Xorbits Inference 是什么 Xorbits Inference&#xff08;Xinference&#xff09;是一个性能强大且功能全面的分布式推理框架。可用于大语言模型&#xff08;LLM&#xff09;&#xff0c;语音识别模型&…

激动,五四青年节,拿下YashanDB认证YCP

&#x1f4e2;&#x1f4e2;&#x1f4e2;&#x1f4e3;&#x1f4e3;&#x1f4e3; 作者&#xff1a;IT邦德 中国DBA联盟(ACDU)成员&#xff0c;10余年DBA工作经验&#xff0c; Oracle、PostgreSQL ACE CSDN博客专家及B站知名UP主&#xff0c;全网粉丝10万 擅长主流Oracle、My…

中间件之搜索和数据分析组件Elasticsearch

一、概述 1.1介绍 The Elastic Stack, 包括 Elasticsearch、Kibana、Beats 和 Logstash&#xff08;也称为 ELK Stack&#xff09;。 能够安全可靠地获取任何来源、任何格式的数据&#xff0c;然后实时地对数据进行搜索、分析和可视 化。Elaticsearch&#xff0c;简称为 ES&a…

CUDA和显卡驱动

1.安装显卡驱动 https://www.nvidia.com/download/index.aspx?langen-us 由于我的显卡是RTX4060&#xff0c;因此先选择RTX40系列&#xff0c;然后选择RTX4060&#xff0c;进行安装 2.查看显卡对应的CUDA CUDA安装地址&#xff1a;https://developer.nvidia.com/cuda-toolk…

正点原子[第二期]Linux之ARM(MX6U)裸机篇学习笔记-12-蜂鸣器

前言&#xff1a; 本文是根据哔哩哔哩网站上“正点原子[第二期]Linux之ARM&#xff08;MX6U&#xff09;裸机篇”视频的学习笔记&#xff0c;在这里会记录下正点原子 I.MX6ULL 开发板的配套视频教程所作的实验和学习笔记内容。本文大量引用了正点原子教学视频和链接中的内容。…

复旦微JFM7VX690计算后IO接口模块,用于雷达信号处理、数据处理等需要高速密集计算的应用场景

计算后IO接口模块 1 介绍 1.1 产品概述 计算后IO接口模块主要由复旦微JFM7VX690型FPGA、国产以太网收发器YT8521、国产BMC芯片GD32F450、国产CPLD芯片EF2L45BG256B、国产内存颗粒等主要芯片组成&#xff0c;采用标准6U VPX尺寸设计。 本计算后IO接口模块主要用于雷达信号处…

QT+串口调试助手+基本版

一、创建串口调试助手UI界面 1、首先生成串口连接必要参数界面&#xff0c;删除关闭串口控件 2、给参数下拉框添加常见的选项&#xff0c;删除关闭串口控件 3、将串口调试助手参数界面布局整齐&#xff0c;删除关闭串口控件 4、更改控件名字&#xff0c;方便后续编程&#xff…

第五篇:通信脉络:探索计算机外设与总线体系的精髓

通信脉络&#xff1a;探索计算机外设与总线体系的精髓 1 引言 在这个技术日新月异的时代&#xff0c;理解计算机系统的基本构成要素 —— 总线和外设 —— 对于每个从事技术工作的人来说都是至关重要的。这些组件不仅是计算机通信的基石&#xff0c;也直接影响着系统的性能、效…

Universal Thresholdizer:将多种密码学原语门限化

参考文献&#xff1a; [LS90] Lapidot D, Shamir A. Publicly verifiable non-interactive zero-knowledge proofs[C]//Advances in Cryptology-CRYPTO’90: Proceedings 10. Springer Berlin Heidelberg, 1991: 353-365.[Shoup00] Shoup V. Practical threshold signatures[C…