【Linux系统编程】第二十七弹---文件描述符与重定向:fd奥秘、dup2应用与Shell重定向实战

✨个人主页:熬夜学编程的小林

💗系列专栏: 【C语言详解】 【数据结构详解】【C++详解】【Linux系统编程】

目录

1、文件描述符fd

[1.1、0 & 1 & 2](#1.1、0 & 1 & 2)

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

2、重定向

[3、使用 dup2 系统调用](#3、使用 dup2 系统调用)

[3.1、> 输出重定向](#3.1、> 输出重定向)

[3.2、>> 追加重定向](#3.2、>> 追加重定向)

[3.3、< 输入重定向](#3.3、< 输入重定向)

[3.4、shell模拟实现> >> <](#3.4、shell模拟实现> >> <)

4、缓冲区


1、文件描述符fd

  • 通过对open函数的学习,我们知道了文件描述符就是一个小整数

查看open的返回值fd。

会用到的头文件

复制代码
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>

代码演示

复制代码
int main()
{
  // 查看open返回值是什么
  int fda = open("loga.txt",O_WRONLY | O_CREAT | O_TRUNC,0666);
  printf("fda : %d\n",fda);
  int fdb = open("logb.txt",O_WRONLY | O_CREAT | O_TRUNC,0666);
  printf("fdb : %d\n",fdb);
  int fdc = open("logc.txt",O_WRONLY | O_CREAT | O_TRUNC,066);
  printf("fdc : %d\n",fdc);
  int fdd = open("logd.txt",O_WRONLY | O_CREAT | O_TRUNC,0666);
  printf("fdd : %d\n",fdd);
  return 0;
}

运行结果

结果是从3开始,且是依次递增的。

1.1、0 & 1 & 2

fd为什么从3开始呢?0 1 2分别代表什么呢?

  • Linux进程默认情况下会有3个缺省打开的文件描述符,分别是标准输入0, 标准输出1, 标准错误2,根据文件描述符分配规则(后序一个标题详细讲解),找到当前没有被使用的最小的一个下标,作为新的文件描述符
  • 0,1,2对应的物理设备一般是:键盘,显示器,显示器。

补充(使用系统调用读文件):

复制代码
#include <unistd.h>

ssize_t read(int fd, void *buf, size_t count);

尝试从文件描述符 fd 读取最多 count 个字节到从 buf 开始的缓冲区。

验证一

复制代码
int main()
{
  char buf[1024];
  // 从键盘读取sizeof(buf)个字节到buf中
  ssize_t s = read(0, buf, sizeof(buf));
  if(s > 0)
  {
    buf[s] = 0;// 设置结尾\0
    // 将buf的strlen(buf)长度写到显示器中
    write(1, buf, strlen(buf));
    write(2, buf, strlen(buf));
  }
  return 0;
}

运行结果

内核结构

而现在知道,文件描述符就是从0开始的小整数 。当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件。于是就有了file结构体。表示一个已经打开的文件对象。而进程执行open系统调用,所以必须让进程和文件关联起来 。每个进程都有一个指针*files, 指向一张表files_struct,该表最重要的部分就是包涵一个指针数组,每个元素都是一个指向打开文件的指针!所以,本质上,文件描述符就是该数组的下标。所以,只要拿着文件描述符,就可以找到对应的文件。

进一步验证

fd在C语言层面其实是FILE结构体中的一个 _fileno 成员,我们可以打印这个成员的结果来验证0,1,2。

代码演示

复制代码
int main()
{
  //0 1 2 默认打开 
  printf("stdin->fd: %d\n",stdin->_fileno);
  printf("stdout->fd: %d\n",stdout->_fileno);
  printf("stderr->fd: %d\n",stderr->_fileno);
  // 普通文件创建的
  FILE* fp = fopen("log.txt","w");
  if(fp == NULL) return 1;
  printf("fd: %d\n",fp->_fileno);

  FILE* fp1 = fopen("log1.txt","w");
  if(fp == NULL) return 1;
  printf("fd: %d\n",fp1->_fileno);
  
  FILE* fp2 = fopen("log2.txt","w");
  if(fp == NULL) return 1;
  printf("fd: %d\n",fp2->_fileno);
  return 0;
}

运行结果

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

关闭0或者2

复制代码
int main()
{
  close(2);
  //close(0);
  int fd = open("log.txt",O_WRONLY | O_CREAT | O_TRUNC,0666);
  if(fd < 0) 
  {
    perror("open");
    return 1;
  }
  printf("fd: %d\n",fd);
  return 0;
}

运行结果

从关闭0号和2号文件描述符我们可以看到,关闭几号创建新文件的fd就是几号。

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

2、重定向

代码演示

复制代码
int main()
{
  close(1);
  int fd = open("log.txt",O_WRONLY | O_CREAT | O_TRUNC,0666);
  if(fd < 0)
  {
    perror("open");
    return 1;
  }
  printf("fd: %d\n",fd);
  fflush(stdout);
  close(fd);
  return 0;
}

运行结果

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

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

上层用的 fd 不变,在内核中更改 fd 对应的 struct file* 地址。

3、使用 dup2 系统调用

复制代码
#include <unistd.h>

int dup2(int oldfd, int newfd);

文件描述符下标内容的拷贝,将oldfd拷贝给newfd。

3.1、> 输出重定向

将新文件描述符下标内容拷贝到显示器上。

代码演示

复制代码
int main()
{
  int fd = open("log.txt",O_WRONLY | O_CREAT | O_TRUNC,0666);
  if(fd < 0)
  {
    perror("open");
    return 1;
  }
  dup2(fd,1);
  printf("hello linux\n");
  
  return 0;
}

运行结果

3.2、>> 追加重定向

将新文件描述符下标内容追加拷贝到显示器上。

代码演示

复制代码
int main()
{
  int fd = open("log.txt",O_WRONLY | O_CREAT | O_APPEND,0666);
  if(fd < 0)
  {
    perror("open");
    return 1;
  }
  dup2(fd,1);
  printf("hello linux\n");
  fprintf(stdout,"hello linux\n"); 
  return 0;
}

运行结果

3.3、< 输入重定向

将新文件描述符下标内容拷贝到键盘上。

代码演示

复制代码
int main()
{
    int fd=open("log.txt",O_RDONLY);
    if(fd == -1)
    {
        perror("open");
        return 1;
    }
    //输入重定向
    dup2(fd,0);
    char outbuffer[64];
    while(1)
    {
        // 获取fd = 0中数据,即文件
        if(fgets(outbuffer,sizeof(outbuffer),stdin) == NULL) break;
        printf("<%s",outbuffer);
    }
    return 0;
}

运行结果

3.4、shell模拟实现> >> <

头文件、宏、全局变量

复制代码
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
#include<ctype.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<errno.h>

#define SIZE 512
#define ZERO '\0'
#define SEP " "
#define NUM 32
// 找最后一个/ ,宏是替换可以不用传二级指针,do while 不加分号,为了后面加分号
#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)

char* gArgv[NUM];
int lastcode = 0;
char cwd[SIZE*2];

// "ls -a -l -n > myfile.txt"
#define None_Redir 0
#define In_Redir   1
#define Out_Redir  2
#define App_Redir  3

int redir_type = None_Redir;
char *filename = NULL;

1.CheckRedir

在获取字符串之后检查是否有重定向符号,通过全局变量redir_type赋予不同的值,默认没有重定向符号。

代码演示

复制代码
void CheckRedir(char cmd[])
{
  // > >> <
  // "ls -a -l > myfile.txt"
  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++;
    }
  }
}

2.测试CheckRedir

打印出对应的变量值即可。

复制代码
    printf("cmd: %s\n",usercommand);
    printf("redir: %d\n",redir_type);
    printf("filename: %s\n",filename);

运行结果

4、缓冲区

缓冲区是什么?

缓冲区是一段内存空间。

为什么要有缓冲区?

给上层提供高效的IO体验,间接提高整体的效率。

缓冲区的刷新策略

正常情况

1、立即刷新。fflush(stdout) int fsync(int fd); synchronize a file's in-core state with storage device。

2、行刷新。显示器刷新,为了照顾用户的体验。

3、全缓冲。缓冲区写满才刷新(普通文件)。

特殊情况

1、进程退出,系统自动刷新

2、强制刷新

缓冲器包括?

用户级缓冲区 内核级缓冲区

缓冲区的意义:

1、解耦

2、提高效率,提高用户使用的效率,提高刷新IO的效率

代码演示

复制代码
int main()
{
  printf("hello printf\n");
  fprintf(stdout,"hello fprintf\n");

  const char* msg = "hello write\n";
  write(1,msg,strlen(msg));
  return 0;
}

运行结果

奇怪的代码

复制代码
int main()
{
  printf("hello printf\n");
  fprintf(stdout,"hello fprintf\n");

  const char* msg = "hello write\n";
  write(1,msg,strlen(msg));
  fork();
  return 0;
}

运行结果

我们发现 printf 和 fprintf (库函数)都输出了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

复制代码
在/usr/include/libio.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;
#ifdef _IO_USE_OLD_IO_FILE
};
相关推荐
JustMove0n几秒前
互联网大厂Java面试全流程问答及技术详解
java·jvm·redis·mybatis·dubbo·springboot·多线程
爱倒腾的老唐1 分钟前
02、STM32——嵌入式芯片
linux·stm32·嵌入式硬件
iFlyCai6 分钟前
数据结构与算法之希尔排序
数据结构·算法·排序算法
SimonKing9 分钟前
5分钟学会!把代码从本地推送到 GitHub,就是这么简单
java·后端·程序员
玹外之音9 分钟前
Spring AI 11 种文档切割策略全解析
java·spring·ai编程
lcreek24 分钟前
LeetCode2208. 将数组和减半的最少操作次数、LeetCode2406.将区间分为最少组数
python·算法
AryShaw25 分钟前
macOS 上搭建 RK3568 交叉编译环境
linux·macos
shehuiyuelaiyuehao25 分钟前
算法1,移动零
数据结构·算法·排序算法
Java练习两年半28 分钟前
互联网大厂 Java 求职面试:技术栈与微服务深度解析
java·微服务·面试·技术栈
shehuiyuelaiyuehao30 分钟前
算法2,复写零
数据结构·算法