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

news2024/11/17 17:35:32

文章目录

    • 前言
    • 一、文件相关概念与操作
      • 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;
}

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

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

相关文章

Vue 开发中的一些问题简单记录,Cannot find module ‘webpack/lib/RuleSet‘

您好&#xff0c;我是码农飞哥&#xff08;wei158556&#xff09;&#xff0c;感谢您阅读本文&#xff0c;欢迎一键三连哦。 &#x1f4aa;&#x1f3fb; 1. Python基础专栏&#xff0c;基础知识一网打尽&#xff0c;9.9元买不了吃亏&#xff0c;买不了上当。 Python从入门到精…

【C语言】项目实践-贪吃蛇小游戏(Windows环境的控制台下)

一.游戏要实现基本的功能&#xff1a; • 贪吃蛇地图绘制 • 蛇吃食物的功能 &#xff08;上、下、左、右方向键控制蛇的动作&#xff09; • 蛇撞墙死亡 • 蛇撞自身死亡 • 计算得分 • 蛇身加速、减速 • 暂停游戏 二.技术要点 C语言函数、枚举、结构体、动态内存管…

【Java探索之旅】内部类 静态、实例、局部、匿名内部类全面解析

文章目录 &#x1f4d1;前言一、内部类1.1 概念1.2 静态内部类1.3 实例内部类1.4 局部内部类1.5 匿名内部类 &#x1f324;️全篇总结 &#x1f4d1;前言 在Java编程中&#xff0c;内部类是一种强大的特性&#xff0c;允许在一个类的内部定义另一个类&#xff0c;从而实现更好的…

Rust web简单实战

一、使用async搭建简单的web服务 1、修改cargo.toml文件添加依赖 [dependencies] futures "0.3" tokio { version "1", features ["full"] } [dependencies.async-std] version "1.6" features ["attributes"]2、搭…

网络攻击(Cyber Attacks)

目录 1.概念 2.分类 3.总结 1.概念 网络攻击&#xff08;Cyber Attacks&#xff0c;也称赛博攻击&#xff09;是指针对计算机信息系统、基础设施、计算机网络或个人计算机设备的&#xff0c;任何类型的进攻动作。对于计算机和计算机网络来说&#xff0c;破坏、揭露、修改、使…

【C++】STL — List的接口讲解 +详细模拟实现

前言&#xff1a; 本章我们将学习STL中另一个重要的类模板list… list是可以在常数范围内在任意位置进行插入和删除的序列式容器&#xff0c;并且该容器可以前后双向迭代。list的底层是带头双向循环链表结构&#xff0c;双向链表中每个元素存储在互不相关的独立节点中&#xf…

从零到屎山系列-游戏开发(Day2)

简介 这次就来一个比较简单的小游戏贪吃蛇 贪吃蛇 游戏规则就是一串珠子不断的移动&#xff0c;碰到场景里面的食物变长一点&#xff0c;碰到墙壁游戏结束。 开始动手 设计绘制设备 首先我计划从一个控制台游戏开始&#xff0c;需要一个控制台下的绘图机制&#xff0c;希…

基于Sen+MK的多站点不同季节和年尺度的SPEI趋势分析.md

再大的风浪&#xff0c;不过只短暂喧哗。 文章目录 前言1. 概述2.1 问题情景2.2 说明 2. 版本2.1 天津&#xff0c;2024年5月4日&#xff0c;Version1 3. 微信公众号GISRSGeography 一、数据1. 输入数据2. 输出数据 二、程序代码三、参考资料 前言 1. 概述 2.1 问题情景 假…

java-Spring-mvc-(请求和响应)

目录 &#x1f4cc;HTTP协议 超文本传输协议 请求 Request 响应 Response &#x1f3a8;请求方法 GET请求 POST请求 &#x1f4cc;HTTP协议 超文本传输协议 HTTP协议是浏览器与服务器通讯的应用层协议&#xff0c;规定了浏览器与服务器之间的交互规则以及交互数据的格式…

thinkphp6 workerman无法使用框架Db/model等类库方法解决方案

thinkphp6 workerman无法使用框架Db/model相关操作解决 执行安装相关扩展 composer require webman/gateway-worker引入成功后编辑服务类文件,直接展示代码 <?phpnamespace app\server\controller;use GatewayWorker\BusinessWorker; use GatewayWorker\Gateway; use Gate…

java异常.day30(Error,Exception)

Error和Exception说明 Error Error类及其子类表示的是Java虚拟机&#xff08;JVM&#xff09;无法或不应该尝试恢复的严重问题。这些问题通常是由JVM本身的问题、系统资源耗尽、或其他不可控的环境因素引起的。由于Error是不可恢复的&#xff0c;因此应用程序不应该尝试捕获和…

Cisco WLC 2504控制器重启后所有AP掉线故障-系统日期时间

1 故障描述 现场1台WLC 2504控制器掉电重启后&#xff0c;所有AP均无线上线&#xff0c; 正常时共有18个AP在线&#xff0c;而当前为0 AP在线数量为0 (Cisco Controller) >show ap sumNumber of APs.................................... 0Global AP User Name..........…

细胞自动机与森林火灾与燃烧模拟

基于 元胞自动机-森林火灾模拟_vonneumann邻域-CSDN博客 进行略微修改&#xff0c;解决固定方向着火问题&#xff0c;用了一个meshv2数组记录下一状态&#xff0c;避免旧状态重叠数据失效。 参数调整 澳洲森林火灾蔓延数学建模&#xff0c;基于元胞自动机模拟多模式下火灾蔓延…

大语言模型从Scaling Laws到MoE

1、摩尔定律和伸缩法则 摩尔定律&#xff08;Moores law&#xff09;是由英特尔&#xff08;Intel&#xff09;创始人之一戈登摩尔提出的。其内容为&#xff1a;集成电路上可容纳的晶体管数目&#xff0c;约每隔两年便会增加一倍&#xff1b;而经常被引用的“18个月”&#xf…

【C++题解】1659. 是否含有数字5

问题&#xff1a;1659. 是否含有数字5 类型&#xff1a;分支结构 题目描述&#xff1a; 请从键盘读入一个五位整数 n&#xff0c;判断其是否含有数字 5&#xff0c;如果含有数字 5 &#xff0c;请输出这个 5 位数各个位的和&#xff1b;如果不含数字 5 &#xff0c;请直接输出…

IoTDB 入门教程 基础篇⑧——数据库管理工具 | IDEA 连接 IoTDB

文章目录 一、前文二、下载iotdb-jdbc三、IDEA驱动四、IDEA连接数据库五、数据库应用六、其他 一、前文 IoTDB入门教程——导读 二、下载iotdb-jdbc 下载地址org/apache/iotdb/iotdb-jdbc&#xff1a;https://maven.proxy.ustclug.org/maven2/org/apache/iotdb/iotdb-jdbc/ 本…

记录vue报错问题 in ./node_modules/axios/lib/platform/index.js

今天这个问题困扰了我许久 报错内容如下&#xff1a; 最初一直以为是我没装axios&#xff0c;又重新装了一次&#xff0c;后面才发现是axios版本原因&#xff0c;真的总是被版本的原因困住真的很烦 解决方法如下&#xff1a; 将axios的版本改为1.5.0 1、打开项目的文件夹“…

探索LLM在广告领域的应用——大语言模型的新商业模式和新个性化广告的潜力

概述 在网络搜索引擎的领域中&#xff0c;广告不仅仅是一个补充元素&#xff0c;而是构成了数字体验的核心部分。随着互联网经济的蓬勃发展&#xff0c;广告市场的规模已经达到了数万亿美元&#xff0c;并且还在持续扩张。广告的经济价值不断上升&#xff0c;它已经成为支撑大…

C++奇迹之旅:STL初步学习

文章目录 &#x1f4dd;什么是STL&#x1f320; STL的版本&#x1f309;STL的六大组件 &#x1f320;STL的重要性&#x1f309;如何学习STL&#x1f320;STL的缺陷&#x1f6a9;总结 &#x1f4dd;什么是STL STL(standard template libaray-标准模板库)&#xff1a;是C标准库的…

ubuntu安装LVGL/lv_img_conv并在thinkphp中进行调用生成bin文件

项目需求&#xff1a;需要处理图片成为bin文件&#xff0c;并以二进制的方式传给蓝牙设备&#xff0c;当前仅介绍如何安装&#xff0c;对lvgl功能和简介不做过多描述 项目库地址&#xff1a;https://github.com/lvgl/lv_img_conv 安装过程比较简单 一&#xff0c;确保node.j…