【Linux】基础IO(一) :文件描述符,文件流指针,重定向


重新认识文件

  1. 是不是只有C/C++有文件操作呢python、java、go等文件接口操作的方法是不太一样的那如何理解这种现象有没有统一的视角去看待所有的语言文件操作呢—我们今天从系统视角去理解 ---- 实际都是通过系统调用来访问
  2. 文件=内容+属性 — 针对文件的操作对内容的操作对属性的操作对内容和属性的操作
  3. 文件可以分为两大类磁盘文件 和 被打开的文件内存文件
  4. 当文件没有被操作的时候文件一般放在磁盘位置。空文件也在磁盘中占据空间因为文件属性也是数据保存数据就需要空间。
  5. 我们在文件操作的时候文件需要在哪里—内存依据冯诺依曼体系的规定
  6. 所以我们在文件操作的时候文件需要提前load到内存那load是内容还是属性至少有属性吧那是不是只有你一个人在load呢当然不是内存中一定存在大量的不同文件的属性
  7. 所以打开文件的本质就是将需要的文件加载到内存中OS内部一定会同时存在大量的被打开的文件那操作系统需不需要管理呢怎么管理— 先描述在组织
  8. 先描述 — 构建在内存中的文件结构体 struct file 文件从磁盘中来struct file* next连接下一个文件信息。在组织 — struct file结构体利用某种数据结构链接起来。在OS内部对被打开的文件进行管理就转换成了对类似链表的增删查改
  9. 结论文件被打开OS要为被打开的文件创建对应的内核数据结构
  10. 所有文件操作的本质就是进程和被打开文件的关系。 — struct task_struct 和 struct file

系统内部的文件操作

库函数底层必须调用系统调用接口因为无论什么进程想访问文件都必须按照操作系统提供的方式来进行访问所以就算文件操作相关函数千变万化但是底层是不变的这些函数最后都会调用系统调用接口按照操作系统的意愿来合理的访问磁盘上的文件。

我们不能用语言绕过操作系统去操纵硬件所以必须通过系统调用通过操作系统来进行文件操作不管什么编程语言只是不同语言对系统调用进行了各自不同的封装所以对这些文件操作接口的理解其实就要落实到对系统调用接口的理解 也就是说所有的只要要访问硬件或者操作系统内部的资源都要通过系统调用避不开的

我们C语言的文件操作

C语言文件操作接口主要包括以下几类

  • 打开和关闭文件的接口如fopen(), fclose()等。这些接口用于创建或打开一个文件并返回一个FILE类型的指针以及关闭一个已打开的文件并释放相关资源。
  • 顺序读写数据的接口如fgetc(), fputc(), fgets(), fputs(), fprintf(), fscanf()等。这些接口用于从文件中读取或写入字符、字符串或格式化数据并自动移动文件指针。
  • 随机读写数据的接口如fread(), fwrite(), fseek(), ftell()等。这些接口用于从文件中读取或写入二进制数据块并根据指定位置移动或获取文件指针。
  • 其他辅助功能的接口如feof(), ferror(), clearerr()等。这些接口用于检测文件是否到达末尾、是否发生错误、以及清除错误标志。

文件的打开方式
r以只读的方式打开文件若文件不存在就会出错。
w以只写的方式打开文件文件若存在则清空文件内容重新开始写入若不存在则创建一个文件。
a以只写的方式打开文件文件若存在则从文件尾部以追加的方式进行写入若不存在则创建一个文件。
r+以可读写的方式打开文件若文件不存在就会出错。
w+以可读写的方式打开文件其他与w一样。
a+以可读写的方式打开文件其他与a一样。

fopen, fread, fwrite, fseek, fclose等函数的使用

需要注意的是当向文件中写入数据后想要重新读取到数据要么需要关闭文件重新打开要么就要跳转读写位置到文件起始位置然后再开始读取文件数据。

#include <stdio.h>
#include <string.h>
 
 
int main()
{
    FILE *fp = fopen("./bite", "wb+");
    if (fp == NULL) {
        perror("fopen error");
        return -1; 
    }   
    fseek(fp, 0, SEEK_SET);
    char *data = "linux so easy!\n";
    //size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
    size_t ret = fwrite(data, 1, strlen(data), fp);
    if (ret != strlen(data)) {
        perror("fwrite error");
        return -1; 
    }   
    fseek(fp, 0, SEEK_SET);//跳转读写位置到从文件起始位置开始偏移0个字节
    char buf[1024] = {0};
    //size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
    ret = fread(buf, 1, 1023, fp);//因为设置读取块大小位1块个数为1023因此fread返回值为实际读取到的数据长度
    if (ret == 0) {
        if (ferror(fp)) //判断上一次IO操作是否正确
            printf("fread error\n");
        if (feof(fp)) //判断是否读取到了文件末尾
            printf("read end of file!\n");
        return -1; 
    }   
    printf("%s", buf);
    fclose(fp);
    return 0;
}

当然这些也都是C库提供的函数是对系统调用的上层封装在系统级别文件操作我们是通过系统调用实现的

系统内部的文件操作

文件操作系统调用接口是指Linux内核提供的一组用于对文件进行打开、读写、关闭等操作的函数。它们包括以下几个常用的函数

  • open打开一个文件返回一个文件描述符可以指定文件的打开方式和权限。
  • write向一个已打开的文件中写入数据返回实际写入的字节数。
  • read从一个已打开的文件中读取数据返回实际读取的字节数。
  • lseek改变一个已打开文件的读写位置返回新的偏移量。
  • close关闭一个已打开的文件释放资源。

这些函数都需要传入一个文件描述符作为参数它是一个非负整数用于标识不同的打开文件。每个进程都有自己独立的一组文件描述符并且默认有三个预定义的描述符0代表标准输入1代表标准输出2代表标准错误输出。

这些函数都有可能失败并返回-1并设置errno变量为相应的错误码。因此在调用这些函数后需要检查返回值和错误码来判断是否成功。
我们主要介绍前三个:

OS一般会如何让用户给自己传递标志位的多个标志位怎么实现呢 — 位图

其实是通过位操作实现的

#include <stdio.h>

#define ONE 0x1
#define TWO 0x2
#define THREE 0x4
#define FOUR 0x8
#define FIVE 0x10

// 0000 0000 0000 0000 0000 0000 0000 0000
void Print(int flags)
{
    if(flags & ONE) printf("hello 1\n"); //充当不同的行为
    if(flags & TWO) printf("hello 2\n");
    if(flags & THREE) printf("hello 3\n");
    if(flags & FOUR) printf("hello 4\n");
    if(flags & FIVE) printf("hello 5\n");
}


int main()
{
    printf("--------------------------\n");
    Print(ONE);
    printf("--------------------------\n");
    Print(TWO);
    printf("--------------------------\n");
    Print(FOUR);
    printf("--------------------------\n");

    Print(ONE|TWO);
    printf("--------------------------\n");

    Print(ONE|TWO|THREE);
    printf("--------------------------\n");

    Print(ONE|TWO|THREE|FOUR|FIVE);
    printf("--------------------------\n");

    return 0;
}

open打开一个文件返回一个文件描述符可以指定文件的打开方式和权限

open有两种调用方式:
一种是只传入文件名和访问模式另一种是还传入创建权限如果需要创建新文件。访问模式有必需部分和可选部分必需部分是 O_RDONLY只读、O_WRONLY只写或 O_RDWR读写可选部分有 O_APPEND追加、O_TRUNC截断、O_CREAT创建、O_EXCL排他等。创建权限是由几个标志按位或得到的如 S_IRUSR用户读、S_IWUSR用户写、S_IXUSR用户执行等。

字符串/0 问题 系统调用不需要这个在这里插入图片描述

在这里插入图片描述

使用 open 函数打开一个文件如果不存在则创建一个新文件并设置访问模式为读写和追加创建权限为用户读写和组读写

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/stat.h>

int main() {
    // 打开或创建一个文件
    int fd = open("test.txt", O_RDWR | O_APPEND | O_CREAT, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP);
    if (fd == -1) {
        // 打开失败打印错误信息
        perror("open error");
        exit(1);
    }
    // 打开成功打印文件描述符
    printf("open success, fd = %d\n", fd);
    // 关闭文件
    close(fd);
    return 0;
}

创建目录的命令mkdir目录起始权限默认是0777创建文件的命令touch文件起始权限是0666这些命令的实现实际上是要调用系统接口open的并且在创建文件或目录的时候要在open的第三个参数中设置文件的起始权限。

 25 int main()
 26 {
 27     umask(0);//将进程的umask值设置为0000
 28 
 29     // C语言中的w选项实际上底层需要调用这么多的选项O_WRONLY O_CREAT O_TRUNC 0666
 30     // C语言中的a选项需要将O_TRUNC替换为O_APPEND
 31     int fd = open(FILE_NAME,O_WRONLY | O_CREAT,0666);//设置文件起始权限为0666
 32     if(fd < 0)
 33     {
 34         perror("open");
 35         return 1;//退出码设置为1
 36     }
 37     close(fd);   
 38 }

### write向一个已打开的文件中写入数据返回实际写入的字节数
**write向一个已打开的文件中写入数据返回实际写入的字节数。需要传入文件描述符、数据缓冲区和数据长度。如果返回值小于请求的字节数可能是因为错误或者设备驱动程序对数据块长度敏感。如果返回值为 0表示没有写入任何数据如果返回值为 -1则表示出现错误。**

使用 write 函数向一个已打开的文件中写入一段字符串并检查返回值是否正确
```c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main() {
    // 要写入的字符串和长度
    char *str = "Hello world!\n";
    int len = 13;
    // 向标准输出文件描述符为1写入字符串
    int ret = write(1, str, len);
    if (ret == -1) {
        // 写入失败打印错误信息
        perror("write error");
        exit(1);
    }
    if (ret != len) {
        // 写入字节数不正确打印警告信息
        fprintf(stderr, "write warning: expected %d bytes, but got %d bytes\n", len, ret);
    }
    // 写入成功打印返回值
    printf("write success, ret = %d\n", ret);
    
}

read从一个已打开的文件中读取数据返回实际读取的字节数

read从一个已打开的文件中读取数据返回实际读取的字节数。需要传入文件描述符、数据缓冲区和数据长度。如果返回值小于请求的字节数可能是因为错误或者已到达文件尾。如果返回值为 0表示没有读取任何数据如果返回值为 -1则表示出现错误。
使用 read 函数从一个已打开的文件中读取一定长度的数据并存储到一个缓冲区中并检查返回值是否正确

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main() {
   // 要读取的字节数和缓冲区大小 
   int len = 100;
   char buf[100];
   // 从标准输入文件描述符为0读取数据到缓冲区中
   int ret = read(0, buf, len);
   if (ret == -1) {
       // 读取失败打印错误信息
       perror("read error");
       exit(1);
   }
   if (ret == 0) {
       // 读取到文件尾没有数据可读打印提示信息
       printf("read end of file\n");
   }
   // 读取成功打印返回值和缓冲区内容注意添加结束符
   printf("read success, ret = %d\n", ret);
  buf[ret] = '\0';
   printf("buf: %s\n", buf);

}

使用这些接口时有一些事项需要注意

  • 在调用 open 函数时要根据文件的用途和状态选择合适的访问模式和创建权限。如果使用了 O_CREAT 标志要指定创建权限否则可能导致文件权限不正确。如果使用了 O_EXCL 标志要检查返回值是否为 -1否则可能导致覆盖已有文件。如果打开的是设备文件或符号链接要注意一些特殊的访问模式如 O_NONBLOCK、O_NOCTTY、O_NOFOLLOW 等。
  • 在调用 write 函数时要保证数据缓冲区的有效性和长度正确性。如果写入的是文本文件要注意添加换行符或结束符。如果写入的是二进制文件要注意字节序和对齐问题。如果写入的是设备文件或网络套接字要注意数据块长度和超时问题。
  • 在调用 read 函数时要保证数据缓冲区的有效性和大小足够。如果读取的是文本文件要注意处理换行符或结束符。如果读取的是二进制文件要注意字节序和对齐问题。如果读取的是设备文件或网络套接字要注意数据块长度和超时问题。
  • 在调用这些接口后都要检查返回值是否为 -1并根据 errno 变量来判断错误原因并进行相应的处理或提示。有些错误可能是暂时性的或可恢复的如 EINTR、EAGAIN、EWOULDBLOCK 等有些错误可能是严重性的或不可恢复的如 EACCES、EBADF、EFAULT、EINVAL 等。

综合使用
fopen, fread, fwrite, fseek, fclose等函数的使用
需要注意的是当向文件中写入数据后想要重新读取到数据要么需要关闭文件重新打开要么就要跳转读写位置到文件起始位置然后再开始读取文件数据。

#include <stdio.h>
#include <string.h>
 
 
int main()
{
    FILE *fp = fopen("./bite", "wb+");
    if (fp == NULL) {
        perror("fopen error");
        return -1; 
    }   
    fseek(fp, 0, SEEK_SET);
    char *data = "linux so easy!\n";
    //size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
    size_t ret = fwrite(data, 1, strlen(data), fp);
    if (ret != strlen(data)) {
        perror("fwrite error");
        return -1; 
    }   
    fseek(fp, 0, SEEK_SET);//跳转读写位置到从文件起始位置开始偏移0个字节
    char buf[1024] = {0};
    //size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
    ret = fread(buf, 1, 1023, fp);//因为设置读取块大小位1块个数为1023因此fread返回值为实际读取到的数据长度
    if (ret == 0) {
        if (ferror(fp)) //判断上一次IO操作是否正确
            printf("fread error\n");
        if (feof(fp)) //判断是否读取到了文件末尾
            printf("read end of file!\n");
        return -1; 
    }   
    printf("%s", buf);
    fclose(fp);
    return 0;
}

综合使用
open read, write, lseek close等函数的使用

#include <stdio.h>
#include <unistd.h>//是close write这些接口的头文件
#include <string.h>
#include <fcntl.h>//是 O_CREAT 这些宏的头文件
#include <sys/stat.h>//umask接口头文件
 
 
int main()
{
    //将当前进程的默认文件创建权限掩码设置为0--- 并不影响系统的掩码仅在当前进程内生效
    umask(0);
    //int open(const char *pathname, int flags, mode_t mode);
    int fd = open("./bite", O_CREAT|O_RDWR, 0664);
    if(fd < 0) {
        perror("open error");
        return -1; 
    }   
    char *data = "i like linux!\n";
    //ssize_t write(int fd, const void *buf, size_t count);
    ssize_t ret = write(fd, data, strlen(data));
    if (ret < 0) {
        perror("write error");
        return -1; 
    }   
    //off_t lseek(int fd, off_t offset, int whence);
    lseek(fd, 0, SEEK_SET);
    char buf[1024] = {0};
    //ssize_t read(int fd, void *buf, size_t count);
    ret = read(fd, buf, 1023);
    if (ret < 0) {
        perror("read error");
        return -1; 
    }else if (ret == 0) {
        printf("end of file!\n");
        return -1; 
    }   
    printf("%s", buf);
    close(fd);
    return 0;
}

看看Linux内核源代码是怎么说的

在这里插入图片描述
可以看到内核源代码的设计内容跟我们所说的基本一致

理解文件控制块&&文件描述符&&文件指针的关系

在进程中每打开一个文件都会创建有相应的文件描述信息struct file这个描述信息被添加在pcb的struct files_struct中以数组的形式进行管理随即向用户返回数组的下标作为文件描述符用于操作文件

进程可以打开多个文件对于大量的被打开文件操作系统一定是要进行管理的也就是先描述再组织所以操作系统会为被打开的文件创建对应的内核数据结构也就是文件控制块FCB在linux源码中是struct file{}结构体包含了文件的大部分属性

 #include <assert.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#define FILE_NAME(number) "log.txt"#number

int main()
{
    int fd0 = open(FILE_NAME(1),O_WRONLY | O_CREAT | O_TRUNC,0666);//设置文件起始权限为0666
    int fd1 = open(FILE_NAME(2),O_WRONLY | O_CREAT | O_TRUNC,0666);//设置文件起始权限为0666
    int fd2 = open(FILE_NAME(3),O_WRONLY | O_CREAT | O_TRUNC,0666);//设置文件起始权限为0666
    int fd3 = open(FILE_NAME(4),O_WRONLY | O_CREAT | O_TRUNC,0666);//设置文件起始权限为0666
    int fd4 = open(FILE_NAME(5),O_WRONLY | O_CREAT | O_TRUNC,0666);//设置文件起始权限为0666
    printf("fd:%d\n",fd0);
    printf("fd:%d\n",fd1);
    printf("fd:%d\n",fd2);
    printf("fd:%d\n",fd3);
    printf("fd:%d\n",fd4);
    close(fd0);
    close(fd1);
    close(fd2);
    close(fd3);
    close(fd4);  
}

结果
在这里插入图片描述
通过上述讲解我们知道open系统调用会返回文件描述符那它为什么是从3开始呢

其实main函数会默认打开这三个标准文件
这三个标准文件是

  • 标准输入stdin用于从键盘或其他输入设备读取数据通常对应文件描述符0。可以使用C语言的scanf、getchar等函数或者Linux的read系统调用来读取标准输入。
  • 标准输出stdout用于向屏幕或其他输出设备写入数据通常对应文件描述符1。可以使用C语言的printf、putchar等函数或者Linux的write系统调用来写入标准输出。
  • 标准错误输出stderr用于向屏幕或其他输出设备写入错误信息通常对应文件描述符2。可以使用C语言的fprintf、perror等函数或者Linux的write系统调用来写入标准错误输出。

这三个标准文件在程序启动时就被自动打开并且在程序结束时被自动关闭无需手动操作。它们也可以被重定向到其他文件或设备例如使用 > 或 < 符号。
在这里插入图片描述
在这里插入图片描述

所以为什么open文件操作后返回值 是3 因为 0 1 2 已经被占用了 ---- 本质是数组下标

内存中文件描述符文件描述符表文件控制块进程控制块的关系如下图所示文件描述符表说白了就是一个存储指向文件控制块的指针的指针数组而文件描述符就是这个指针数组的索引进程控制块中会有一个指向文件描述符表的指针。通过文件描述符就可以找到对应的被打开的文件。
操作系统通过这些内核数据结构将被打开的文件和进程联系起来。

请添加图片描述

深度理解

文件描述符的实质文件描述符是内核为每个进程维护的一个打开文件记录表的索引值

C语言如何访问系统 就是通过文件描述符同样的C++的cin、cout等类中也必须有文件描述符没有文件描述符怎么通过操作系统访问系统调用外设呢 每个编程语言都是如此
通过上述的引出我们可以知道文件描述符的实质是

  • 文件描述符是一个非负整数用于标识不同的已打开文件
  • 文件描述符是内核为了高效管理已打开文件所创建的索引它可以用来调用各种I/O系统调用函数。
  • 文件描述符是进程级别的每个进程都有自己独立的一组文件描述符并且默认有三个预定义的描述符0代表标准输入1代表标准输出2代表标准错误输出
  • 文件描述符可以被复制、重定向、关闭等操作但不能被直接读写。要读写一个已打开文件需要使用read、write等系统调用函数并传入相应的文件描述符作为参数。

文件描述符表和file结构体之间的关系是

  • 文件描述符表是内核用来存储每个进程的文件描述符和对应的打开文件信息的表格。每个进程在其进程控制块PCB中都保存着一份文件描述符表
  • file结构体是内核用来表示已打开文件的数据结构它包含了当前读写位置、访问模式、状态标志等信息以及指向对应inode对象或者i-node表项的指针。file结构体也可以称为打开文件句柄或者打开文件表项。
  • 文件描述符表和file结构体之间通过指针相互连接一个文件描述符可以指向一个或多个file结构体一个file结构体也可以被一个或多个文件描述符所指向。这样可以实现不同进程或同一进程中不同文件描述符共享同一个已打开文件。

文件描述符的分配规则系统在创建文件描述符时会寻找当前未使用的最小下标

关闭012文件描述符产生的现象新打开文件的fd被赋值为0或1或2

当关闭0或2时打印出来的log.txt对应的fd的值就是对应的关闭的0或2的值而当关闭1时显示器不会显示对应的fd的值。

  1 #include <stdio.h>
  2 #include <sys/types.h>
  3 #include <sys/stat.h>
  4 #include <fcntl.h>
  5 #include <unistd.h>
  6 
  7 int main()
  8 {
  9     //close(0);
 10     //close(1);
 11     //close(2);                                                                                                                                        
 12     umask(0000);                                                                                                             
 13     int fd = open("log.txt",O_WRONLY | O_CREAT | O_TRUNC,0666);//没有指明文件路径默认在当前路径下也就是当前进程的工作目录
 14     if(fd<0)                                                   
 15     {                                                          
 16         perror("open");                                        
 17         return 1;                                              
 18     }                                                          
 19                                                                
 20     printf("open fd:%d\n",fd);                                 
 21     close(fd);                                                 
 22     return 0;                                                  
 23 }    

测试结果
请添加图片描述
分析
所以实际上文件描述符在分配时会从文件描述符表中的指针数组中从小到大按照顺序找最小的且没有被占用的fd来进行分配自然而然关闭0时0对应存储的地址就会由stdin改为新打开的文件的地址所以打印新的文件的fd值时就会出现0。
关闭2也是这个道理fd为2对应的存储的地址会由stderr改为新打开的文件的地址所以在打印fd时也就会出现2了。

文件描述符的分配规则是

  • 当一个进程打开一个新的文件时系统会在该进程的文件描述符表中寻找当前未使用的最小下标并将其分配给该文件。
  • 当一个进程关闭一个已打开的文件时系统会将该文件对应的文件描述符表项置为空并释放其占用的资源。
  • 当一个进程复制或重定向一个已打开的文件时系统会在该进程或目标进程的文件描述符表中寻找当前未使用的最小下标并将其指向同一个file结构体。

下面是一些示例代码

  • 打开一个新的文件
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/stat.h>

int main() {
    // 打开或创建一个新文件
    int fd = open("test.txt", O_RDWR | O_CREAT | O_TRUNC, 0666);
    if (fd == -1) {
        // 打开失败打印错误信息并退出
        perror("open error");
        exit(1);
    }
    // 打开成功打印分配到的文档描述符
    printf("open success, fd = %d\n", fd);
    // 关闭文档
    close(fd);
    return 0;
}
  • 关闭一个已打开的文档
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/stat.h>

int main() {
    // 打开或创建一个新文档
    int fd = open("test.txt", O_RDWR | O_CREAT | O_TRUNC, 0666);
    if (fd == -1) {
        // 打开失败打印错误信息并退出
        perror("open error");
        exit(1);
    }
    // 打开成功打印分配到的文档描述符
    printf("open success, fd = %d\n", fd);
    
    // 关闭文档
    int ret = close(fd);
    
     if (ret == -1) {
        // 关闭失败打印错误信息并退出
        perror("close error");
        exit(1);
     }
     // 关闭成功打印关闭信息
     printf("close success\n");
     
     return 0;
}

文件重定向与dup2系统调用 (内核中更改fd对应的struct file*地址

重定向命令>和>>的含义和用法

  • 重定向命令>和>>都是用来将一个命令的标准输出或错误输出重定向到一个文件中而不是显示在屏幕上。
  • 重定向命令>表示覆盖模式即如果目标文件已经存在那么原来的内容会被清空然后写入新的内容。例如echo “hello” > log.txt 表示将字符串"hello"写入到log.txt文件中如果log.txt文件已经存在那么原来的内容会被覆盖。
  • 重定向命令>>表示追加模式即如果目标文件已经存在那么新的内容会被追加到原来的内容后面。例如echo “world” >> log.txt 表示将字符串"world"追加到log.txt文件中如果log.txt文件已经存在那么原来的内容会保留。
  • 重定向命令>和>>可以指定不同的文件描述符来重定向不同类型的输出。默认情况下如果不指定文件描述符那么就是1表示标准输出。如果要重定向错误输出就要指定2作为文件描述符。例如ls -l /etc/passwd /etc/abc > log1.txt 2> log2.txt 表示将ls -l 命令的标准输出重定向到log1.txt文件中并将错误输出重定向到log2.txt文件中。
    测试如下

我们vim一个abc文件可以看到abc文件中的内容如下图所示
在这里插入图片描述

然后我们使用 ls > abc 命令之后可以看到abc中的内容已经被清空重定向了
在这里插入图片描述

追加重定向直接在文件的尾部进行重定向

我们对abc文件进行追加重定向可以看到直接在文件的尾巴进行了重定向
在这里插入图片描述

重定向原理

简单说将 fd_array 数组当中的元素struct file* 指针的指向关系进行修改改变成为其它的struct file结构体的地址—每个文件描述符都是一个内核中文件描述信息数组的下标对应有一个文件的描述信息用于操作文件而重定向就是在不改变所操作的文件描述符的情况下通过改变描述符对应的文件描述信息进而实现改变所操作的文件

详细说文件操作重定向的原理是通过改变文件描述符对应的文件描述信息从而实现改变所操作的文件。文件描述符是一个整数表示进程和被打开文件的关系通常有标准输入0、标准输出1和标准错误2三种。重定向可以分为输出重定向、追加重定向和输入重定向三种类型。输出重定向是将本应该打印到显示器的内容输出到了指定的文件中例如 ls > list.txt追加重定向是将本应该打印到显示器的内容追加式地输出到了指定的文件中例如 ls >> list.txt输入重定向是将本应该从键盘中读取的内容改为从指定的文件中读取例如 cat < input.txt。在Linux系统中可以使用dup2系统调用来实现重定向它可以将一个文件描述符复制到另一个文件描述符并关闭后者。
在这里插入图片描述

dup2函数的功能和参数含义

int dup2(int oldfd, int newfd); 其实这个函数挺绕的要理解起来需要自己研究一下才行

通过man手册我们可以对dup2函数进行一些了解:
在这里插入图片描述
int dup2(int oldfd, int newfd);
函数功能为将newfd描述符重定向到oldfd描述符相当于重定向完毕后都是操作oldfd所操作的文件
但是在过程中如果newfd本身已经有对应打开的文件信息则会先关闭文件后再重定向否则会资源泄露

怎么用怎么传参数— 拷贝的整数所表示的内容 — 注意最后只有oldfd保留就可以了 就是oldfd把newfd覆盖了 — 一般传参例如fd 1 重定向输出— 只保留了fd

所以dup2函数是一个用于复制文件描述符的系统调用它的功能是将参数oldfd所指的文件描述符复制到参数newfd所指定的数值如果newfd已经被打开则先关闭它如果newfd等于oldfd则不做任何操作。dup2函数返回新的文件描述符或者在出错时返回-1。

  1. 输出重定向从原来的输出到屏幕改为输出到文件中这就叫做输出重定向。
    而追加重定向的方式也比较简单只要将文件打开方式中的O_TRUNC替换为O_APPEND即可。
    8 int main()                            
    9 {
   10     umask(0000);                                                           
   11     int fd = open("log.txt",O_WRONLY | O_CREAT | O_TRUNC,0666);//输出重定向
E> 12     int fd = open("log.txt",O_WRONLY | O_CREAT | O_APPEND,0666);//追加重定向
   13     if(fd<0)                                                                                                                                       
   14     {                                                                                  
   15         perror("open");                                                                
   16         return 1;                                                                      
   17     }                                                                                  
   18                                                                                        
   19     dup2(fd,1);                                                                        
   20                                                                                        
   21     printf("open fd:%d\n",fd);// printf --> stdout                                     
   22     fprintf(stdout,"open fd:%d\n",fd);// fprintf --> stdout                            
   23                                                                                        
   24     const char* msg = "hello linux";                                                   
   25     write(1,msg,strlen(msg));//向显示器上write                                         
   26                                                                                        
   27     close(fd);                                                                         
   28     return 0;                                                                          
   29 }      
  1. 输出重定向从原来的键盘中读取数据改为从文件fd中读取数据这就叫做输入重定向。
    文件log.txt中的内容作为输入重定向重新输出到显示器中即使fgets获取的方式是stdin也没有关系因为我们使用dup2将stdin中的地址改为了文件log.txt的地址
  8 int main()
  9 {
 10     umask(0000);
 13     int fd = open("log.txt",O_RDONLY);//输入重定向
 14     if(fd<0)
 15     {
 16         perror("open");
 17         return 1;
 18     }
 19 
 20     dup2(fd,0);//由键盘读取改为从fd文件中读取                                                                                                        
 21     char line[64];                  
 22     while(1)                                                                                                                  
 23     {                                                                                                                         
 24         printf("<");                                                                                                          
 25         if(fgets(line,sizeof(line),stdin)==NULL) break;                                                                       
 26         printf("%s",line);                                                                                                    
 27     }     
 28 }       

示例

 void func() {
     int fd = open("./tmp.txt", O_RDWR|O_CREAT, 0664);//打开文件
     if (fd < 0) {
     return -1;
     }
     //将标准输出重定向到文件这样则写往标准输出的数据会被写入到文件中而不是被打印
     dup2(fd, 1);
     //printf内部操作的是stdout标准输出文件流指针而文件流指针本质上内部包含的是1号描述符成员
     //printf的打印就是向标准输出写入数据因为标准输出已经被重定向因此数据会被写入文件中而不是直接打印
     printf("hello bit");
     return 0;
}

Linux下面一切皆文件

不同的硬件的读写方法一定是不一样的但在OS看来一切设备和文件都是struct file内核数据结构在管理对应的硬件时虽然硬件的管理方法不在OS层而是在驱动层这也没有关系只需要利用struct file结构体中的函数指针调用对应的硬件的读写方法即可。

终究还是封装的思想
在这里插入图片描述
Linux下一切皆文件是指Linux系统中的所有资源无论是硬件设备、普通文件、目录、进程、网络连接等都可以被抽象为文件并且可以使用统一的接口来访问和操作。

这样做的好处是简化了开发者和用户对不同资源的处理方式提高了系统的灵活性和可扩展性。

这样做的不利之处是需要在文件系统中挂载每个硬件设备才能使用它们而且可能会造成一些性能损失。
在这里插入图片描述

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: linux

“【Linux】基础IO(一) :文件描述符,文件流指针,重定向” 的相关文章