【Linux】文件系统

我们在C语言都学过文件操作,例如fopen,fclose之类的函数接口,在C++中也有文件流的IO接口,那不仅仅是C/C++,python、java、go、hph等等这些语言也都有自己的文件操作的IO接口。那有没有一种统一的视角来看待这些文件操作呢?它们的底层原理到底是什么?下面我们就来好好谈一谈:


目录

一、Linux操作系统中描述和管理文件的方式

二、系统级的文件操作接口

[2.1 open](#2.1 open)

[2.1.1 open函数flags参数的分析](#2.1.1 open函数flags参数的分析)

[2.1.2 open函数mode参数的分析](#2.1.2 open函数mode参数的分析)

[2.2 umask](#2.2 umask)

[2.3 write](#2.3 write)

[2.3.1 清空式写入](#2.3.1 清空式写入)

[2.3.2 追加式写入](#2.3.2 追加式写入)

[2.4 read](#2.4 read)

三、系统级文件操作接口总结

四、文件描述符

[4.1 三个标准流](#4.1 三个标准流)

[4.2 进程PCB与file结构体的关系](#4.2 进程PCB与file结构体的关系)

[4.2.1 文件描述符的本质](#4.2.1 文件描述符的本质)

[4.2.2 文件缓冲区](#4.2.2 文件缓冲区)

[4.2.3 进程管理和文件系统](#4.2.3 进程管理和文件系统)

五、如何理解Linux下一切皆文件

六、C语言下的FILE

七、输出/输出/追加重定向的本质

[7.1 输出重定向的本质](#7.1 输出重定向的本质)

[7.2 输入重定向的本质](#7.2 输入重定向的本质)

[7.3 追加重定向的本质](#7.3 追加重定向的本质)


一、Linux操作系统中描述和管理文件的方式

我们先来摆出几个事实:

1、文件的基本构成为内容+属性,在对文件进行操作时无非就两种方式:

● 对内容进行操作

● 对属性进行操作

2、文件是保存在磁盘上的,但由于冯诺依曼体系的存在,想要对文件进行操作就必须将其加载到内存中

3、系统在运行时有很多个进程,每个进程又可以打开多个文件,所以在操作系统中一定会同时存在大量被打开的文件

那从这三个事实我们可以得出:每打开一个文件在操作系统中一定会有一个描述该文件的结构体,多个结构体使用了一种数据结构(链表)相互联系起来,形成了管理文件的体系(和进程的组织方式很像)

这个结构体在Linux中名字叫file:

cpp 复制代码
struct file {
    union {
        struct list_head fu_list; //文件对象链表指针linux / include / linux / list.h
            struct rcu_head fu_rcuhead; RCU(Read - Copy Update)//是Linux 2.6内核中新的锁机制
    } f_u;
    struct path f_path; //包含dentry和mnt两个成员,用于确定文件路径
#define f_dentry f_path.dentry //f_path的成员之一,当前文件的dentry结构
#define f_vfsmnt f_path.mnt //表示当前文件所在文件系统的挂载根目录
        const struct file_operations* f_op; //与该文件相关联的操作函数
        atomic_t f_count; //文件的引用计数(有多少进程打开该文件)
        unsigned int f_flags; //对应于open时指定的flag
        mode_t f_mode; //读写模式:open的mod_t mode参数
        off_t f_pos; //该文件在当前进程中的文件偏移量
        struct fown_struct f_owner; //该结构的作用是通过信号进行I / O时间通知的数据。
        unsigned int f_uid, f_gid; //文件所有者id,所有者组id
        struct file_ra_state f_ra; //在linux / include / linux / fs.h中定义,文件预读相关
        unsigned long f_version;
#ifdef CONFIG_SECURITY
    void* f_security;
#endif

    void* private_data;
#ifdef CONFIG_EPOLL

    struct list_head f_ep_links;
    spinlock_t f_ep_lock;
#endif
    struct address_space* f_mapping;
};

二、系统级的文件操作接口

下面我们来学习几个系统级的文件操作接口:

2.1 open

可以看到open这个函数和C语言中fopen差别挺大的,该函数是在系统层面用来打开文件的,可以看到open函数有两个,一个有两个形参,另一个有三个形参(有点像C++中的函数重载):

下面我们先分析一下第一个open函数的使用:

返回值:打开文件成功返回新打开的文件描述符,失败返回-1
第一个形参pathname:传入要打开文件的文件名
第二个形参flags:该参数传入的是标志位,根据传入的标志位来决定打开文件的方式。常用的标志位有:

O_RDONLY: 只读打开

O_WRONLY: 只写打开

O_RDWR: 读,写打开 这三个常量,必须指定一个且只能指定一个

O_CREAT : 若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限 O_APPEND: 追加写

O_TRUNC:清空文件所有内容

下面我们来讲解一下标志位的使用原理:

2.1.1 open函数flags参数的分析

在C语言中我们可以通过形参的传入来让函数做不同的事情:

cpp 复制代码
void Func(int flags)
{
    if (flags==1)
    {
        //功能1
    }
    if (flags == 2)
    {
        //功能2
    }
    ...
}
int main()
{
    Func(...);
    return 0;
}

上面的该代码每次输入参数时只能让函数做一件事,那能不能只输入一个int类型的形参,就可以表示所有想让函数执行的功能?

当然可以,我们可以将这个int类型的参数以比特为单位,每一个比特位代表函数的一个功能,其每个比特位上的值表示函数是否执行该功能,这样子一个int类型的参数就可以一次性传入32个数据,让函数执行其对应的功能:

cpp 复制代码
void Func(int flags)
{
    if (flags & 0x1)//00000000 00000000 00000000 00000001
    {
        printf("功能1\n");
    }
    if (flags & 0x2)//00000000 00000000 00000000 00000010
    {
        printf("功能2\n");
    }
    if (flags & 0x4)//00000000 00000000 00000000 00000100
    {
        printf("功能3\n");
    }
    if (flags & 0x8)//00000000 00000000 00000000 00001000
    {
        printf("功能4\n");
    }
    if (flags & 0x10)//00000000 00000000 00000000 00010000
    {
        printf("功能5\n");
    }

}
int main()
{
    Func(0x1);
    printf("------------------------\n");
    Func(0x4);
    printf("------------------------\n");
    Func(0x4|0x10);
    printf("------------------------\n");
    Func(0x2|0x4|0x8);
    return 0;
}

运行效果:

最后我们再把每个功能所表示的比特位写成一个宏,这样的宏就成了标志位:

cpp 复制代码
#define ONE 0x1
#define TWO 0x2
#define THREE 0x4
#define FOUR 0x8
#define FIVE 0x10

void Func(int flags)
{
    if (flags & 0x1)//00000000 00000000 00000000 00000001
    {
        printf("功能1\n");
    }
    if (flags & 0x2)//00000000 00000000 00000000 00000010
    {
        printf("功能2\n");
    }
    if (flags & 0x4)//00000000 00000000 00000000 00000100
    {
        printf("功能3\n");
    }
    if (flags & 0x8)//00000000 00000000 00000000 00001000
    {
        printf("功能4\n");
    }
    if (flags & 0x10)//00000000 00000000 00000000 00010000
    {
        printf("功能5\n");
    }

}
int main()
{
    Func(ONE);
    printf("------------------------\n");
    Func(THREE);
    printf("------------------------\n");
    Func(FOUR | FIVE);
    printf("------------------------\n");
    Func(TWO | FOUR | FIVE);
    return 0;
}

在open函数的中的flags参数选项也就是这样的宏表示成的标志位,我们输入不一样的选项使其进行对文件相对应的操作:

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

#define LOG "test.txt"    

int main()
{
    int fd = open(LOG, O_WRONLY);//只写打开文件
    if (fd == -1)
    {
        printf("fd:%d,errno:%d,errstring%s\n", fd, errno, strerror(errno));//文件打开失败,打印错误码所对应的原因    
    }
    else
    {
        printf("fd:%d,errno:%d,errstring%s\n", fd, errno, strerror(errno));//文件打开成功,提示一下用户    
    }
    close(fd);//文件打开后要使用close函数关闭,该函数的形参传入要关闭文件的文件描述符即可                                                                                                                                                                                                 
    return 0;
}

运行效果:

因为我们并没有test.txt文件所以用O_WRONLY (只写打开),注定会出错,那我们再加一个标志位:O_CREAT (若文件不存在,则创建它),来试试看:

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

#define LOG "test.txt"    

int main()
{
    int fd = open(LOG, O_WRONLY | O_CREAT);//只写打开文件,如果文件不存在则创建
    if (fd == -1)
    {
        printf("fd:%d,errno:%d,errstring%s\n", fd, errno, strerror(errno));//文件打开失败,打印错误码所对应的原因    
    }
    else
    {
        printf("fd:%d,errno:%d,errstring%s\n", fd, errno, strerror(errno));//文件打开成功,提示一下用户    
    }
    close(fd);//文件打开后要使用close函数关闭,该函数的形参传入要关闭文件的文件描述符即可                                                                                                                                                                                                 
    return 0;
}

运行结果:

文件是创建了,但是这个文件权限怎么怪怪的?S,s,T是什么?这是因为没有传入mode形参的open函数没有创建文件的权限,而创建文件是需要权限的,这样就导致了最终创建出来的文件的权限是乱码的

2.1.2 open函数mode参数的分析

当我们需要使用open函数来创建文件时,这就要传入第三个参数mode了。我们向mode形参传入配置权限的八进制方案 ,来控制最终创建出文件的权限(对于文件权限不熟悉的同学可以看这里:【Linux】文件权限 /【Linux】目录权限和默认权限

下面我们来试试看:

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

#define LOG "test.txt"    

int main()
{
    int fd = open(LOG, O_WRONLY | O_CREAT, 0666);//只写打开文件,如果文件不存在则创建,创建文件时给文件权限配置的八方案为666
    if (fd == -1)
    {
        printf("fd:%d,errno:%d,errstring%s\n", fd, errno, strerror(errno));//文件打开失败,打印错误码所对应的原因    
    }
    else
    {
        printf("fd:%d,errno:%d,errstring%s\n", fd, errno, strerror(errno));//文件打开成功,提示一下用户    
    }
    close(fd);//文件打开后要使用close函数关闭,该函数的形参传入要关闭文件的文件描述符即可                                                                                                                                                                                                 
    return 0;
}

咦,我们给的方案是666啊,怎么创建出来的权限不一样?

别忘了,文件的最终权限还会受到umask的影响,那我们能不能在自己的代码中修改umask呢?

当然可以!下面这个函数就可以办到:

2.2 umask

该函数可以帮我们修改进程中的umask配置,直接调用该函数传入想要设置的八进制方案即可:

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

#define LOG "test.txt"    

int main()
{
    umask(0);//将进程中的umask置为0,以免影响到我们所创建文件的最终权限
    int fd = open(LOG, O_WRONLY | O_CREAT, 0666);//只写打开文件,如果文件不存在则创建,创建文件时给文件权限配置的八方案为666
    if (fd == -1)
    {
        printf("fd:%d,errno:%d,errstring%s\n", fd, errno, strerror(errno));//文件打开失败,打印错误码所对应的原因    
    }
    else
    {
        printf("fd:%d,errno:%d,errstring%s\n", fd, errno, strerror(errno));//文件打开成功,提示一下用户    
    }
    close(fd);//文件打开后要使用close函数关闭,该函数的形参传入要关闭文件的文件描述符即可                                                                                                                                                                                                 
    return 0;
}

现在我们所创建的文件权限就达到预期了

2.3 write

系统级向文件内部写入的函数接口为write

该函数的返回值为实际写入文件的字节数
第一个参数fd:传入想要写入文件的文件描述符
第二个参数buf:要写入内容的地址
第三个参数count:要写入的字节数

演示:

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

#define LOG "test.txt"    

int main()
{
    umask(0);//将进程中的umask置为0,以免影响到我们所创建文件的最终权限
    int fd = open(LOG, O_WRONLY | O_CREAT, 0666);//只写打开文件,如果文件不存在则创建,创建文件时给文件权限配置的八方案为666
    if (fd == -1)
    {
        printf("fd:%d,errno:%d,errstring%s\n", fd, errno, strerror(errno));//文件打开失败,打印错误码所对应的原因    
    }
    else
    {
        printf("fd:%d,errno:%d,errstring%s\n", fd, errno, strerror(errno));//文件打开成功,提示一下用户    
    }
    const char* message = "aaaaaaaa";
    int cnt = 5;
    while (cnt--)
    {
        char buff[128];//缓冲区
        snprintf(buff, sizeof(buff), "%d:%s\n", cnt, message);//将要输出到文件的内容加载到缓冲区里
        write(fd, buff, strlen(buff));//写入文件
    }
    close(fd);//文件打开后要使用close函数关闭,该函数的形参传入要关闭文件的文件描述符即可
    return 0;
}

下面我改变写入的内容,再向文件中写一些他的东西:

cpp 复制代码
....

#define LOG "test.txt"    

int main()
{
    umask(0);//将进程中的umask置为0,以免影响到我们所创建文件的最终权限
    int fd = open(LOG, O_WRONLY | O_CREAT, 0666);//只写打开文件,如果文件不存在则创建,创建文件时给文件权限配置的八方案为666

    ....

    const char* message = "bbbb";
    int cnt = 3;
    while (cnt--)
    {
        char buff[128];//缓冲区
        snprintf(buff, sizeof(buff), "%d:%s\n", cnt, message);//将要输出到文件的内容加载到缓冲区里
        write(fd, buff, strlen(buff));//写入文件
    }
    close(fd);//文件打开后要使用close函数关闭,该函数的形参传入要关闭文件的文件描述符即可
    return 0;
}

可以看到我们再次向文件写入时,上次向文件中写入的内存并没有被清空,write函数就从文件开头写入了本次的内容,最终造成了文件内容的杂乱

2.3.1 清空式写入

所以我们可以在open函数上再加上一个标志位:O_TRUNC( 清空文件所有内容**)**

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

#define LOG "test.txt"    

int main()
{
    umask(0);//将进程中的umask置为0,以免影响到我们所创建文件的最终权限
    int fd = open(LOG, O_WRONLY | O_CREAT | O_TRUNC, 0666);//只写打开文件,并且去除文件内所有内容,如果文件不存在则创建,创建文件时给文件权限配置的八方案为666
    if (fd == -1)
    {
        printf("fd:%d,errno:%d,errstring%s\n", fd, errno, strerror(errno));//文件打开失败,打印错误码所对应的原因    
    }
    else
    {
        printf("fd:%d,errno:%d,errstring%s\n", fd, errno, strerror(errno));//文件打开成功,提示一下用户    
    }
    const char* message = "bbbb";
    int cnt = 3;
    while (cnt--)
    {
        char buff[128];//缓冲区
        snprintf(buff, sizeof(buff), "%d:%s\n", cnt, message);//将要输出到文件的内容加载到缓冲区里
        write(fd, buff, strlen(buff));//写入文件
    }
    close(fd);//文件打开后要使用close函数关闭,该函数的形参传入要关闭文件的文件描述符即可
    return 0;
}

2.3.2 追加式写入

或者在open函数中设置另一个标志位:O_APPEND( 追加式向文件写入内容**)**

cpp 复制代码
....

#define LOG "test.txt"

int main()
{
    umask(0);//将进程中的umask置为0,以免影响到我们所创建文件的最终权限
    int fd = open(LOG, O_WRONLY | O_APPEND | O_CREAT, 0666);//只写(追加式写)打开文件,如果文件不存在则创建,创建文件时给文件权限配置的八方案为666
   
....

    const char* message = "aaaaaaaa";
    int cnt = 5;
    while (cnt--)
    {
        char buff[128];//缓冲区
        snprintf(buff, sizeof(buff), "%d:%s\n", cnt, message);//将要输出到文件的内容加载到缓冲区里
        write(fd, buff, strlen(buff));//写入文件
    }
    close(fd);//文件打开后要使用close函数关闭,该函数的形参传入要关闭文件的文件描述符即可
    return 0;
}

2.4 read

read函数是系统级读取文件接口

该函数的返回值为实际从文件中读取的字节数
第一个参数fd:传入想要读取文件的文件描述符
第二个参数buf:读取内存存放的缓冲区地址
第三个参数count:要读取的字节数

演示:

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

#define LOG "test.txt"

int main()
{
    int fd = open(LOG, O_RDONLY);//只读打开文件
    if (fd == -1)
    {
        printf("fd:%d,errno:%d,errstring%s\n", fd, errno, strerror(errno));//文件打开失败,打印错误码所对应的原因    
    }
    else
    {
        printf("fd:%d,errno:%d,errstring%s\n", fd, errno, strerror(errno));//文件打开成功,提示一下用户    
    }
    char buff[1024];//缓冲区
    ssize_t cnt = read(fd, buff, sizeof(buff) - 1);//读文件时要考虑到缓冲区结尾最后一个字符为/0
    if (cnt > 0)
    {
        buff[cnt] = '/0';
        printf("%s", buff);
    }
    close(fd);//文件打开后要使用close函数关闭,该函数的形参传入要关闭文件的文件描述符即可
    return 0;
}

三、系统级文件操作接口总结

从上面的操作我们可以实现所有系统级文件的操作了,现在我们再来看C语言中fopen、fwrite等等这些文件操作的函数(包括其他语言的文件操作接口),无一例外,想要与硬件所存储的内容进行交互,其内部必须调用系统级文件操作接口!

四、文件描述符

我们在使用open/write/read等等函数时都涉及到了一个文件描述符,那文件描述符到底是个什么东西呢?

在说这个之前,我们先阐述一些概念:

4.1 三个标准流

之前我们在C语言文件操作中说过:对任何一个c程序,只要运行起来,就默认打开3个流:

标准输入流(stdin)、标准输出流(stdout)、标准错误流(stderr)

但是现在我们可以这样子说:对任何一个进程,只要运行起来,就默认打开这三个文件(Linux下一切皆文件)

标准输入 在C语言中被叫做stdin ,在C++中被叫做cin,默认是键盘文件

标准输出 在C语言中被叫做stdout ,在C++中被叫做cout,默认是显示器文件(屏幕)

标准错误 在C语言中被叫做stderr ,在C++中被叫做cerr,默认是显示器文件(屏幕)

这三个标准流本质上都是文件!

下面我们来写段代码验证一下:

cpp 复制代码
#include<iostream>
#include<cstdio>
int main()
{
	fprintf(stdout, "Hello fprintf -> stdout\n");
	std::cout << "Helllo cout -> cout" << std::endl;
	fprintf(stderr, "Hello fprintf -> stderr\n");
	std::cerr << "Helllo cout -> cerr" << std::endl;
	return 0;
}

我们知道无论是C语言还是C++,其标准输出和标准输入流都是默认向显示器文件输出的,所以我们可以在屏幕上看到这些现象

下面我们使用重定向>修改一下其默认输出文件:

我们可以看到默认输出文件应该从显示器文件改成了文本文件(test.txt),默认输出流是向文本文件输出了,但是默认错误流还是向显示器文件进行输出的,这是为什么?

这个问题我们放在后面讨论,现在我们所要知道的就是对任何一个进程,只要运行起来,就会默认打开这三个标准流 文件

4.2 进程PCB与file结构体的关系

在Linux中描述进程的结构体task_struct(PCB)有一个file_struct类型的指针,指向一个file_struct结构体,该结构体内有一个file类型的指针数组,每个数组的地址指向一个file类型的结构体:

这样子就构成了一个进程和文件对应的体系,进程可以根据自己的file_struct类型的指针files找到其打开文件的file结构体

4.2.1 文件描述符的本质

但是我们上面有说到对任何一个进程,只要运行起来,就会默认打开那三个标准流文件,对此结构体file_struct中的file类型的指针数组前三个元素,肯定是指向这三个标准流所对应的file结构体

文件描述符 对应的就是指向该文件file结构体的指针所在的元素下标!!!

现在我们可以解释为什么我们在上面打开文件时,每次对应的文件描述符都是3了,就是因为这三个标准流文件的存在占据了指针数组的前三个元素!

下面我们多用几个open函数打开文件看看其文件描述符是什么样的:

cpp 复制代码
#include<stdio.h>      
#include<unistd.h>      
#include<sys/types.h>      
#include<sys/stat.h>      
#include<fcntl.h>       
 
      
#define LOG "test.txt"      
    
int main()    
{    
    int fd1 = open(LOG, O_WRONLY);    
    int fd2 = open(LOG, O_WRONLY);    
    int fd3 = open(LOG, O_WRONLY);    
    int fd4 = open(LOG, O_WRONLY);    
    int fd5 = open(LOG, O_WRONLY);    
    printf("%d/n", fd1);
    printf("%d/n", fd2);
    printf("%d/n", fd3);
    printf("%d/n", fd4);
    printf("%d/n", fd5);  
    return 0;    
}  

4.2.2 文件缓冲区

其实在内存中每个file结构体都对应着一个自己的文件缓冲区:

在我们使用write函数时,该函数先要将我们传入的内容拷贝置文件的缓冲区中,再被OS书刷新到磁盘中做持久化(至于什么时候刷新,操作系统有自己的一套方案)。在使用read函数时,OS先要将我们读取的内容拷贝置文件的缓冲区中,再将其缓冲区的内容拷贝至我们存储内容的地址中

所以从本质来说write和read函数都是拷贝函数!

4.2.3 进程管理和文件系统

从上图的模式来看,我们最终可以将这个图画为两个部分:进程管理和文件系统

这个两个模块只通过指针的地址指向进行了低耦合,在运行时互不干涉

五、如何理解Linux下一切皆文件

我们看到下图:

我们从外设看来,所有的外设都有驱动程序,驱动程序会提供两个最基本的函数接口read(从外设读取数据)和write(对外设输入数据),当然在这里系统没必要对键盘输入数据,所以键盘的write_keyboard驱动函数是个空函数。(以此类推显示器的read_screen函数也是一个空函数体)当我们的进程想与外设交互时,都会通过内存中的文件结构体file中函数指针指向的驱动函数!

所以在Linux操作系统下,我们一切操作的进行都要通过进程,而进程与外设数据交互都要通过file结构体,所以我们在Linux下可以将一切都看作为文件!

六、C语言下的FILE

我们在之前说过C语言的FILE指针指向的是一个FILE结构体

现在我们又知道了C语言中所有文件操作接口都要调用系统文件操作接口,所以在C语言中的FILE结构体中必有文件描述符(在Linux的C标准库下为_fileno,不同的环境下封装会有差异)

同时我们也明白了三个标准流:stdin、stdout、stderr也是文件

那我们就打印出来它们的文件描述符来看看:

cpp 复制代码
#include<stdio.h>      
#define LOG "test.txt"      
int main()
{
	printf("%d", stdin->_fileno);
	printf("%d", stdout->_fileno);
	printf("%d", stderr->_fileno);
	FILE* fp = fopen(LOG, "w");
	printf("%d", fp->_fileno);
	fclose(fp);
	return 0;
}

果然如此~

那不用多说C++中的cin、stdout、stderr、cerr也是如此

七、输出/输出/追加重定向的本质

我们先来做个小实验:

cpp 复制代码
#include<stdio.h>      
#include<unistd.h>      
#include<sys/types.h>      
#include<sys/stat.h>      
#include<fcntl.h>      
#include<string.h>
      
#define LOG "test.txt"      
      
int main()      
{      
    close(0);//关掉文件描述符为0的文件
    close(2);//关掉文件描述符为2的文件                                                                                                                                                                                    
    int fd1 = open(LOG, O_WRONLY);                                 
    int fd2 = open(LOG, O_WRONLY);                                 
    int fd3 = open(LOG, O_WRONLY);                                 
    int fd4 = open(LOG, O_WRONLY);                                 
    int fd5 = open(LOG, O_WRONLY);                                 
    printf("%d\n", fd1);                                           
    printf("%d\n", fd2);                                           
    printf("%d\n", fd3);                                           
    printf("%d\n", fd4);                                           
    printf("%d\n", fd5);                                           
    return 0;                                                      
} 

我们可以看到当我们关闭0和2文件描述符所对应的文件后,我们再次打开其他文件时文件描述符从0和2开始了,所以我们可以得出一个结论:打开文件时文件描述符是从未使用的最小元素下标开始的

7.1 输出重定向的本质

我们看到下面的代码:

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

#define LOG "test.txt"

int main()
{
    close(1);
    int fd1 = open(LOG, O_WRONLY | O_TRUNC | O_CREAT, 0666);
    printf("%d\n", fd1);
    close(fd1);
    return 0;
} 

咦?这一次的printf怎么没有向屏幕上打印?而是向test.txt文件中打印了?

这是因为我们先使用文件描述符2关闭了标准输出流文件(显示器文件),再打开test.txt文件时它的文件描述符就成2了,而printf函数默认是向标准输出流文件描述符打印的,这时数据就被打印到test.txt文件中了

这就是输出重定向的本质!我们在shell中使用>操作符时改变的就是文件描述符2所对应的文件

7.2 输入重定向的本质

再来看代码:

cpp 复制代码
#include<stdio.h>  
#include<unistd.h>  
#include<sys/types.h>  
#include<sys/stat.h>  
#include<fcntl.h>  
#include<string.h>  
  
#define LOG "test.txt"  
  
int main()  
{  
    close(0);  
    int fd1 = open(LOG, O_RDONLY);
    int a=0;  
    scanf("%d",&a);  
    printf("%d\n",a);
    close(fd1);
    return 0;
}   

我们这一次先关闭了文件描述符0所对应的文件(键盘文件) ,再打开文件test.txt文件时其文件描述符就是0,而scanf函数默认是向0所对应的文件描述符的文件中读取内容,所以最后a所对应的值也被修改为1了

这就是输入重定向的本质!我们在shell中使用<操作符时改变的就是文件描述符0所对应的文件

7.3 追加重定向的本质

我们从上面两个演示中也不难分析Linux的追加重定向的本质:

cpp 复制代码
#include<stdio.h>  
#include<unistd.h>  
#include<sys/types.h>  
#include<sys/stat.h>  
#include<fcntl.h>  
#include<string.h>  
  
#define LOG "test.txt"  
  
int main()      
{      
    close(1);      
    int fd = open(LOG,O_WRONLY | O_CREAT | O_APPEND, 0666);     
    printf("You an see me\n");    
    printf("You an see me\n");    
    printf("You an see me\n");    
    printf("You an see me\n");    
    printf("You an see me\n");                                                                                                                                                                                      
    close(fd);    
    return 0;    
}  

我们这一次先关闭了文件描述符1所对应的文件(显示器文件) ,再打开文件test.txt文件时其文件描述符就是1,而printf函数默认是向1所对应的文件描述符的文件中输入内容,由于打开文件时是追加式写入,所以最后test.txt文件中有You can see me了

这就是追加重定向的本质!我们在shell中使用>>操作符时改变的就是文件描述符1所对应的文件,并且打开文件的方式为追加式写入(所以追加式重定向和输入重定向只是打开文件的方式不同)

看完这些,相信上面三个标准流中的问题我们也可以理解了(stdout,cout,它们都是向1号文件描述符对应的文件打印;stderr , cerr,它们都是向2号文件描述符对应的文件打印;而输出重定向只改变1号对应的指向)


本期的全部到这里就结束了,下一期见~

相关推荐
果子⌂12 分钟前
容器技术入门之Docker环境部署
linux·运维·docker
深度学习04071 小时前
【Linux服务器】-安装ftp与sftp服务
linux·运维·服务器
iteye_99392 小时前
让 3 个线程串行的几种方式
java·linux
渡我白衣3 小时前
Linux操作系统:再谈虚拟地址空间
linux
阿巴~阿巴~3 小时前
Linux 第一个系统程序 - 进度条
linux·服务器·bash
DIY机器人工房3 小时前
代码详细注释:通过stat()和lstat()系统调用获取文件的详细属性信息
linux·嵌入式
望获linux4 小时前
【Linux基础知识系列】第四十三篇 - 基础正则表达式与 grep/sed
linux·运维·服务器·开发语言·前端·操作系统·嵌入式软件
眠りたいです4 小时前
Mysql常用内置函数,复合查询及内外连接
linux·数据库·c++·mysql
我的泪换不回玫瑰4 小时前
Linux系统管理命令
linux
jjkkzzzz5 小时前
Linux下的C/C++开发之操作Zookeeper
linux·zookeeper·c/c++