【Linux】系统级文件操作&&文件系统的概念

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


目录

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

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

2.1 open

2.1.1 open函数flags参数的分析

2.1.2 open函数mode参数的分析

2.2 umask

2.3 write

2.3.1 清空式写入

2.3.2 追加式写入 

2.4 read

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

四、文件描述符

4.1 三个标准流

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

4.2.1 文件描述符的本质

4.2.2 文件缓冲区

4.2.3 进程管理和文件系统

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

六、C语言下的FILE 

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

7.1 输出重定向的本质

7.2 输入重定向的本质

7.3 追加重定向的本质


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

我们先来摆出几个事实:

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

● 对内容进行操作

● 对属性进行操作

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

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

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

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

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语言中我们可以通过形参的传入来让函数做不同的事情:

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

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

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

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;
}

运行效果:

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

#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参数选项也就是这样的宏表示成的标志位,我们输入不一样的选项使其进行对文件相对应的操作:

#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 (若文件不存在,则创建它),来试试看:

#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】目录权限和默认权限

下面我们来试试看:

#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配置,直接调用该函数传入想要设置的八进制方案即可:

#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:要写入的字节数

演示: 

#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;
}

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

....

#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(清空文件所有内容

#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(追加式向文件写入内容

....

#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:要读取的字节数

演示:

#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,默认是显示器文件(屏幕)

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

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

#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函数打开文件看看其文件描述符是什么样的:

#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也是文件

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

#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也是如此

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

我们先来做个小实验:

#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 输出重定向的本质

我们看到下面的代码:

#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 输入重定向的本质

再来看代码:

#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的追加重定向的本质:

#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号对应的指向)


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

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

1e-12

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值