Linux文件描述符

news2024/9/21 18:42:52

前言

我们以前就听过"Linux下一切皆文件",但是说实话我们只是记住了这句话,实质是不理解的!本期我们就会解释!

本期内容介绍

• 回顾C语言文件操作

• 系统I/O操作接口

• 文件描述符fd

• 理解Linux下一切皆文件

• 重定向

• 缓冲区

• stderr

回顾C语言文件操作

我们在C语言的时候就对文件操作进行过介绍,这里是只是稍微回顾一下,详细见C语言专栏的文件操作:

C语言打开文件: fopen(打开文件的方式有:读 r、写 w、追加 a)

C语言写入fwritefprintffputs

C语言读取freadfscanffgets

C语言会默认给我们打开三个流,即标准输入:stdin标准输出:stdout标准错误:stderr

注意:这里他们的返回值类型都是FILE*我们当时介绍说他是文件指针类型

OK,举个例子

#include <stdio.h>
#include <string.h>
 
int main()
{
    //打开文件,以写的方式打开
    FILE* fp = fopen("log.txt", "w");
    if(fp == NULL)
    {
        perror("open error\n");
        return 1;
    }

    //写入
    const char* msg = "Hello World\n";
    int cnt = 5;
    while (cnt--)
    {
        fwrite(msg, strlen(msg), 1, fp);                                                                                          
    }

    //关闭
    fclose(fp);

    return 0;
}

OK, 看看效果:

w的特性是文件存在清空原始数据,从头开始写不存在,在当前进程的工作目录下创建,然后写入!这个和我们在指令那一期介绍过的输出重定向(>)的作用非常像!其实,输出重定向本质就是文件操作!

#include <stdio.h>
#include <string.h>
 
int main()
{
    //打开文件,以追加的方式打开
    FILE* fp = fopen("log.txt", "a");
    if(fp == NULL)
    {
        perror("open error\n");
        return 1;
    }

    //写入
    const char* msg = "Hello World\n";
    int cnt = 5;
    while (cnt--)
    {
        fwrite(msg, strlen(msg), 1, fp);                                                                                          
    }

    //关闭
    fclose(fp);

    return 0;
}

a的特性是,文件存在,追加内容;文件不存在,在当前进程的工作路径下新建,在追加写入!这个有换个我们指令那里的追加重定向(>>)的性质一样!其实追加重定向本质也是文件操作

系统文I/O操作接口

我们以前就介绍过:

文件 = 内容 + 属性 

文件在没有被打开之前是存在磁盘地上

打开文件的本质是进程打开文件

我们知道,文件存在磁盘,而磁盘是硬件,所以向文件的写入本是上就是对硬件的写入,而我们普通用户是没有权限直接操作硬件的(OS不相信任何人),但是OS必须要给上层提供操作文件的服务,所以他就提供了系统调用接口!下面我们就来学习一下系统的调用接口:

open

作用:打开文件

参数:pathname是要打开/要创建按的文件名称, flag是标记位传参(作用是传递多个标记位),mode是设置新建文件权限的

返回值:如果打开成功,返回一个大于0的整数即文件描述符;失败,返回-1

注意:返回值后面会在介绍!

close

作用:关闭文件

这里你一定最奇怪的是标记位传参,什么是标记位传参?

标记位传参是一种位图的思想,目的是为了传递多个标记!

什么意思呢?举个例子:如果今天叫你给一个函数传递两个标记位,你大概率就是int flag1, int flag2了,而人家写OS的大佬使用的是标记位传参。先看如下代码,看完你就懂什么是标记位传参了:

#include <stdio.h>
#include <string.h>
#include <unistd.h>
 
#define ONE 1           // 1  0000 0001
#define TWO (1 << 1)    // 2  0000 0010
#define THREE (1 << 2)  // 4  0000 0100
#define FOUR (1 << 3)   // 8  0000 1000

void print(int flag)
{
    if(flag & ONE)
        printf("one\n"); //这些都可以完成其他功能
    if(flag & TWO)
        printf("two\n");
    if(flag & THREE)
        printf("three\n");
    if(flag & FOUR)
        printf("four\n");
}

int main()
{
    print(ONE);
    printf("\n");

    print(ONE | TWO);
    printf("\n");

    print(ONE | THREE | FOUR);
    printf("\n");

    print(ONE | TWO | THREE | FOUR);
    printf("\n");

    return 0;
}

看效果:

这样你就可以用一个整形传递多个标记位了, 我们在传递时以按位或的方式就可以传递多个了!!那我们open系统调用的标记位传参有哪些?OK,我们先来介绍一下,然后再使用:

O_RDONLY : 只读打开

O_WRONLY :只写打开

O_RDWR:读写打开

注意:前三个在使用open时必须指定一个,且只能指定一个!

O_CREAT :若文件不存在, 创建它,此时需要第三个参数设置新文件的访问权限

O_APPEND : 追加写

OK,我们下面来使用以下open,我们打开一个已经存在的文件,写权限打开,如果有内容直接清空

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

int main()
{
    //以写的方式打开,如果文件存在,清空内容;不存在,创建
    int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC);

    if(fd == -1)
    {
        perror("open falied\n");
        return 1;
    }

    //....

    //关闭文件
    close(fd);

    return 0;
}

OK,没有问题!我们再来打开一个不存在文件,也是和上面的同样:

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main()
{
    //以写的方式打开,如果文件存在,清空内容;不存在,创建
    int fd = open("file.txt", O_WRONLY | O_CREAT | O_TRUNC);

    if(fd == -1)
    {
        perror("open falied\n");
        return 1;
    }

    //....

    //关闭文件
    close(fd);

    return 0;
}

果然创建出来了,但是我们发现他的权限是有问题的!这里他的权限是乱码!这和我的预期不符呀!如何解决呢?OK,这就是我们要介绍的第三个参数:mode

我们上面介绍open时说了,mode是创建不存在文件时,指定新文件访问权限的参数!那我们在创建文件时指定为666(八进制):

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main()
{
    //以写的方式打开,如果文件存在,清空内容;不存在,创建
    int fd = open("file.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);//创建权限为666

    if(fd == -1)
    {
        perror("open falied\n");
        return 1;
    }

    //....

    //关闭文件
    close(fd);

    return 0;
}

这次不是乱码了,但是我们发现我们设置的权限时666,这里怎么变成了664了?其实这个问题我们以前介绍过,权限掩码的问题!我们是普通用户所以umask是0002(八进制),他会拿着我们的初始权限0666和~umask按位与一下,然后的是最终的权限,如何避免呢,我就是想666呢?我们可以调用系统提供的umask接口来设置当前进程创建文件的umask:

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main()
{
    umask(0000);//将权限掩码设置为000
    //以写的方式打开,如果文件存在,清空内容;不存在,创建
    int fd = open("file.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);//创建权限为666

    if(fd == -1)
    {
        perror("open falied\n");
        return 1;
    }

    //....

    //关闭文件
    close(fd);

    return 0;
}

注意:程序的权限掩码和系统中的权限掩码优先使用,最接近的!这里程序中的近,所以就使用了程序中的!

write

作用:向文件中写入

参数:fd文件描述符(可以理解为向哪个文件写) buf表示写入的文件内容 (注意没有\0) count表示写入内容的长度/字符个数;

返回值:成功,返回写入字节数;失败,返回-1

OK,我们和open配合使用一下,上面我们演示的是以不存在,创建;存在,先清空,再写入!

这里,我们使用一下,追加的方式打开,不存在就创建:

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main()
{
    //以追加的方式打开,如果文件存在,追加;不存在,创建
    int fd = open("file.txt", O_WRONLY | O_CREAT | O_APPEND , 0666);//创建权限为666

    if(fd == -1)
    {
        perror("open falied\n");
        return 1;
    }

    const char* msg = "hello write\n";
    //写入
    int cnt = 3;
    while (cnt--)//追加3次
    {
        write(fd, msg, strlen(msg));
    }

    //关闭文件
    close(fd);

    return 0;
}

OK, 没有问题!write很简单的~!

read

ok,举个简单的例子:

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main()
{
    //以只读的方式打开
    int fd = open("file.txt", O_RDONLY , 0666);//创建权限为666

    if(fd == -1)
    {
        perror("open falied\n");
        return 1;
    }

    char buff[1024];//简单的模拟一下缓冲区
    //读取
    ssize_t n = read(fd, buff, sizeof(buff));
    if(n > 0)
        buff[n] = '\0';
    
    //打印出来
    for(int i = 0; i < n; i++)
    {
        printf("%c ", buff[i]);
    }

    //关闭文件
    close(fd);

    return 0;
}

OK,介绍了这么多,我们其实发现这些系统调用和我们以前在C语言学过的很像!其实,我们C语言的库函数都是封装的这些系统调用!

FILE* fopen("f.txt", "r")就是对int open("f.txt", O_RDONLY);的封装

FILE* fopen("f.txt", "w")就是对int open("f.txt", O_WRONLY | O_CREAO_TRUNC, 0666);的封装

FILE* fopen("f.txt", "r")就是对int open("f.txt", O_WRONLY | O_CREAT | O_APPEND , 0666);的封装

以及fwrite和fread等同理!

OK,上面说的理解了,就是不同的语言有不同的文件操作库函数,虽然他们的用法有差别,但是底层都是调用了这些系统调用;但是上面C语言和系统调用他俩的返回值有些不好理解,C语言的返回的是文件指针,倒还好理解,向文件指针指向的文件写嘛,但是系统调用的返回一个数字咋理解?怎么就可以往一个数字里面写呢??它两有啥关系呢?虽然我们目前还没有介绍文件描述符,但是根据上面的介绍有一点我们一定可以确定:FILE*一定是封装了文件描述符的!为什么要封装呢?文件描述符介绍了在解释!

文件描述符

上面我们只说了open的返回值是一个整数fd,叫做文件描述符,他究竟是个啥呢?下面我们隆重的介绍一下文件描述符!

文件描述符的本质

文件描述符的本质是什么呢?直接先说结论,再解释原理!

文件描述符的本质是进程和文件映射关系数组fd_array的下标!

既然是数组的下标那必然是>=0的,这也符合我们前面对open返回值的介绍;OK,我们来写个代码验证一下:

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main()
{
    umask(0000);
    //打开文件
    int fd1 = open("loga.txt.txt", O_WRONLY | O_CREAT, 0666);
    printf("fd1 : %d\n", fd1);

    int fd2 = open("logb.txt.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);  
    printf("fd2 : %d\n", fd2);  

    int fd3 = open("logc.txt.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);   
    printf("fd3 : %d\n", fd3);

    //关闭文件
    close(fd1);
    close(fd2);
    close(fd3);

    return 0;
}

的确是大于等于0的,但是他怎么是从3开始依次增长的呢?0、1、2去哪了呢?上面我们介绍了,我们有一个进程启动的时候OS会给我们默认打开三个流:标准输入stdin、标准输出stdout、标准错误stderr;其实他们分别占用的就是0、1、2下标;

简单验证一下:既然1是标准输出,那我们往1里面写文件不就直接写到显示器了!

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>


int main()
{   
    const char* msg = "hello world\n";
    
    int cnt = 3;
    while (cnt--)
    {
        write(1, msg, strlen(msg));
        sleep(1);
    }

    close(1);

    return 0;
}

OK,说了这么多,目前我就记住了fd是进程和文件的映射数组的下标~!但是说实话我还是不太理解,下面我们来介绍一下open打开一个文件的过程

我们知道打开和操作文件的本质是进程在打开和操作,每一个进程打开一个文件,OS会为其创建一个struct file的结构体对象,而一个进程可以有很多打开的文件,且OS中进程是很多的,所以OS需要对这些struct file的结构体对象管理起来!如何管理?先描述,在组织!OS会对每一个文件的struct file对象以双链表的形式管理起来(并为每一个struct file 对象开辟一个内核寄级文件缓冲区,用来存储该文件的操作内容),但是每个进程在这么多的struct file对象中,如何知道自己打开的是哪一个呢?其实在进程和struct file之间还会一张记录对应进程打开文件的表,叫文件描述符表files_struct,在文件描述符表里面有一个进程文件映射关系的数组fd_array,他是一个struct file*的指针数组,数组中的每个元素指向该进程打开的文件!所以当上层调用open打开文件时,先会在OS中创建struct file的对象和开辟对应的文件缓冲区,之后加载数据(可能会延后),然后查看对应进程的文件描述符表,将该struct file对象的指针放到fd_array的合适位置,最后给上层返回对应的fd_array的下标,然后上层就可以拿着open的返回值fd进行其他操作了,例如当上层在拿着fd去write的时候,因为write是系统调用所以他就知道fd是fd_array的下标,就可以找到相对应的struct file对象,然后将上层要写入的内容(用缓冲区中的内容)拷贝到fd下标位置中的struct file对象指向的file文件的内核级文件缓冲区,然后在由OS定期向磁盘刷新!

上面的内容对用的就是这张图:

OK这就是我们对文件描述符的介绍,上面也介绍了C语言的文件操作是对系统调用的封装!那既然是封装一定有fd等相关信息喽!OK,查看一下:

#include <stdio.h>

int main()
{
    printf("stdin->fd : %d\n", stdin->_fileno);
    printf("stdout->fd : %d\n", stdout->_fileno);
    printf("stderr->fd : %d\n", stderr->_fileno);

    FILE* fp1 = fopen("f1.txt", "w");
    printf("fp1->fd: %d\n", fp1->_fileno);

    FILE* fp2 = fopen("f2.txt", "w");
    printf("fp2->fd: %d\n", fp2->_fileno);

    FILE* fp3 = fopen("f3.txt", "w");
    printf("fp3->fd: %d\n", fp3->_fileno);

    fclose(fp1);
    fclose(fp2);
    fclose(fp3);
    
    return 0;
}

文件描述符的分配规则

文件描述符的规则:查自己的文件描述符表,分配最小的没被使用的fd

ok,验证一下:

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h> 
#include <sys/stat.h>
#include <fcntl.h>

int main()
{
    umask(0000);
    //先关掉 0 -> stdin
    close(0);

    //打开文件
    int fd = open("log.txt.txt", O_WRONLY | O_CREAT, 0666);

    //看一下新的文件fd
    printf("fd : %d\n", fd);

    //关闭文件
    close(fd);

    return 0;
}

这里我们在打开前先把0给关了,然后当open打开的时候去给新的文件分配的时候查找文件fd——array然后发现0是没有给分配所以刚打开的fd就是0!你可能会问,原来打来的0号文件呢?答案是系统会自动回收!

C语言为什么要对底层的系统调用进行封装呢?

我们既然可以使用系统调用问什么C语言还要对系统调用接口进行性封装呢?OK,先说结论,在解释!

• 为了使代码具有跨平台性

• 为用户考虑,方便编码,提高了编码效率

首先解释第一点,不同系统的系统调用接口是不一样的,如果C语言不对OS调用接口封装的话那我们得直接调系统调用,假设你今天在Linux上写的代码想在mac os以及win上跑是不行的,因为不同的系统的OS调用接口不同!这就是使得代码的跨平台性差,历史也会最终淘汰掉!第二点,系统调用时你必须得对系统调用有一定的了解才可以,所以编码的成本变高,工作效率较低!而C语言对不同的系统调用封装后,我们上层用户只管开发,关于跨平台的原因C语言已经解决了,而库函数比系统调用使用的成本简单,开发效率就提升了!

理解Linux下一切皆文件

OK,通过上面的介绍,我们认识到访问文件时OS的内核只认文件描述符fd; 我们说过每个进程在启动的时候OS会默认打开:标准输入stdin(键盘)、标准输出stdout(显示器)和标准错误stderr(显示器),他们分别是fd_array的0、1、2下标,我们上面也演示了可以拿着他们三个去操作文件,但是他们说到底是硬件啊,怎么可以和文件一样操作呢?其实这就是我们要介绍的Linux一些皆文件!

键盘、显示器、鼠标、网卡、磁盘等本质是硬件,这些硬件也是要被管理起来的,如何管理?先描述,在组织!OS会通过驱动层拿到对应硬件的数据,然后通过结构体进行描述,对每个种硬件进行创建相应的对象;然后用某种数据结构(例如双链表管理起来),然后对硬件操作就变成了对数据结构的操作;而这些硬件本质上的功能都是一样的即I/O操作和文件差不多,所以OS就把他们抽象成文件看待,给他们创建struct file,里面一部分他们的属性,一部分是他们I/O方法的指针,指向他们的I/O方法,这些I/O方法通常是硬件的厂商提供的,所以站在OS以及以上的视角看到所有的外设不都是struct file文件吗?所有的外设操作不就是和操作文件一样吗?这就是Linux下一切皆文件!将抽象出来的struct file这一层称为VFS即虚拟文件系统!其实这种方法就是多态,或C语言实现类!

重定向

我们知道每一个进程在启动的时候,操作系统会默认的打开,stdin(键盘)、stdout(显示器)、stderr(显示器),他们分别对应的fd是0、1、2;那我们把1给关闭了会出现什么情况呢?

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h> 
#include <sys/stat.h>
#include <fcntl.h>

int main()
{
    umask(0000);
    //先关掉 1 -> stdout
    close(1);

    //打开文件
    int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);

    if(fd < 0)
    {
        perror("open\n");
        return 1;
    }

    printf("printf, fd %d\n", fd);
    fprintf(stdout, "fprintf, fd %d\n", fd);
    fflush(stdout);

    //关闭文件
    close(fd);

    return 0;
}

这是什么原因呢?其实稍微一想,本来是默认向显示器写的内容写到了文件!这和我们以前介绍的输出重定向一样!如下:

OK,既然知道了,这是重定向了,下面我们就来介绍一下重定向的原理:

重定向的原理

我们知道printf/fprintfC语言,他们都是默认向显示器写入的,也就是写到FILE* stdout,FILE* stdout的值stdout->_fileno 默认是等于1的,是不变的,也就是C语言(上层语言)的stdout是无论什么情况都是默认写到fd == 1位置指向的文件的!而上面我们将1号文件给关了,又打开了一个log.txt的文件,根据上面介绍的文件描述符规则,我们可以知道此时的log.txt的fd就是1,但是我们的C语言(上层语言)不知道此时1号位置指向的文件已经改变,当再次写入时依然写到了1号位置指向的文件!所以这就是为什么本应该写到显示器的内容跑到了文件的原因~!

根据上面的介绍,我么可以得出结论:

重定向的本质是:改变内核fd_array数组中特定下标的内容,与上层无关!

OK,那我们是不是以后重定向也得先关1号文件,然后在打开?是不是有点挫呀!再说了我们在使用ls 命令重定向的时候也没有关闭1啊,他们是怎么做到的呢?其实系统为了方便重定向,给我们提供了一个接口:dup2,下面我们就来学习使用一下这个接口!

dup2

dup2的作用就是将内核数据结构fd_arrary的数组的oldfd下标中的内容拷贝到newfd下标中

有了dup2接口我们就不在先关闭,在打开了!OK,我们写个代码看一下:

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h> 
#include <sys/stat.h>
#include <fcntl.h>

int main()
{
    umask(0000);
    int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
    if(fd < 0)
    {
        perror("open\n");
        return 1;
    }

    //使用dup2重定向
    dup2(fd, 1);

    printf("printf, fd %d\n", fd);
    fprintf(stdout, "fprintf, fd %d\n", fd);
    fflush(stdout);

    //关闭文件
    close(fd);

    return 0;
}

追加重定向的话就是,再打开的时候将标记位O_TRUNC换成O_APPEND,然后就变成了追加重定向,OK上面的代码只需要换一下:

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h> 
#include <sys/stat.h>
#include <fcntl.h>

int main()
{
    umask(0000);
    int fd = open("log.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);
    if(fd < 0)
    {
        perror("open\n");
        return 1;
    }

    //使用dup2重定向
    dup2(fd, 1);

    printf("printf, fd %d\n", fd);
    fprintf(stdout, "fprintf, fd %d\n", fd);
    fflush(stdout);

    //关闭文件
    close(fd);

    return 0;
}

缓冲区

在介绍重定向的时候,我们发现我们写的代码总是带一个fflush这是啥呢?可不可以去掉呢?OK,我们去掉试一试:

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h> 
#include <sys/stat.h>
#include <fcntl.h>

int main()
{
    umask(0000);
    //先关掉 1 -> stdout
    close(1);

    //打开文件
    int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);

    if(fd < 0)
    {
        perror("open\n");
        return 1;
    }

    printf("printf, fd %d\n", fd);
    fprintf(stdout, "fprintf, fd %d\n", fd);
    
    //fflush(stdout);

    //关闭文件
    close(fd);

    return 0;
}

为啥不加个fflush就不显示呢?这是啥情况?其实不光我们的系统内部存在内核级的文件缓冲区,我们的语言层也是存在的!OK,我先画个图,解释一下,上面代码为什么没有写入到文件:

我们知道main函数中在return时代表进程结束,且会在结束前冲刷缓冲区!但是这里在冲刷之前先把文件给关了,等进程结束前刷新时发现找不到要写入的文件了,所以我们就看不到写入log.txt的数据了!那我们把clos给注释掉是不是就可以看到了呢?

果然我们看到了!这也符合我们的介绍!所以这就是为什么上面重定向的时候要到fflush的原因,是想在关闭文件之前将用户级别的缓冲区刷新到内核,然后再由OS刷到外设!OK,上面也只是验证了一下以前的一个代码!下面我们再来重新的介绍一下缓冲区!

什么是缓冲区?

缓冲区就是一段内存空间!

缓冲区分为:用户级缓冲区和内核级缓冲区

为什么要有缓冲区?

上层减少对底层系统调用/硬件I\O接口的调用次数,给上层提供高效的I\O体验,间接提高整体的效率!

这里你可可能会想不管是内核的还是用户的缓冲区,我都是多了次拷贝呀!他怎么还效率高了呢?

举个例子:你买东西是到商场买,还是到工厂买?是不是商场啊!原因是商场的是现货,你到工厂区代价比较大,也不方便!商场不就是一个缓冲区吗?给用户的体验更好,假设没有商场你今天买一个键盘还得去工厂,明天买一个水杯又要去工厂是不是体验很差啊!而你直接去商城那现成的是不是效率高多了?此时虽然多了一次转手,但是比以前体验好了!所以有了缓存可以提高用户的使用效率!另外,假设你现在在北京,要给你云南的朋友送一个键盘,你会拿着键盘自己送过去吗?是不是不会呀,你只需要下楼给楼下的顺丰即可,然后又可以上楼开心的玩原始人了,等到时间他收到即可!顺丰快递会不会立即把你刚邮寄的包裹立刻送?显然不会,顺丰会等到比如100个包裹一起配送!此时顺风不就是一个缓冲区吗?你寄快递不就是调用语言层面的I\O接口吗?此时虽然多了一次转手(拷贝)但是用户的体验变好了,使用者的效率变高了!

缓冲区的刷新策略

立即刷新; 例如:用户级别的刷新:fflush(stdout); 内核级别的刷新:fsync(int fd);

行刷新;例如:显示器(照顾用户的使用习惯)

全缓冲:缓冲区满了才刷新,一般是普通文件;

特殊情况:进程退出,OS自动刷新!强制刷新:\n等

注意:这里说的是缓冲区的刷新策略,并没特质是用户还是内核!

stderr

在介绍stderr之前,我先来谈谈为什么会有stdin\stdout:

我们平时写的程序,本质都是对数据的处理(计算/存储等),这个过程需要知道数据从哪里来,以及数据去了哪里!比如你写个hello world数据是要从键盘获取的,它的执行结果是不是用户也需要看到啊!所以stdin(1)和stdout(1)是方便用户进行动态的与程序进行交互!但是我实在不理解的是为什么还要有stderr(2)呢,他不是对应的也是显示器吗?OK,我们来写一段代码看看:

#include <stdio.h>

int main()
{
    fprintf(stdout, "fprintf stdout\n");
    fprintf(stderr, "fprintf stderr\n");
    return 0;
}

果然这也验证了我们前面的结论是正确的即1和2都是显示器文件!且是同一个显示器文件,不然上面也不可能同时打印!如下图:

为什么要有stderr?

那既然这样不是只有一个就够了吗,为什么还有stderr呢?OK,下面我们先来看看他们的区别:

我们发现我们本来是将他两都重定向到log.txt的但是为什么只有stdout写进去了呢?其实我们介绍的输出重定向叫做标准输出重定向,也就是他只会把1号fd对应的内容给更改了!但是stderr是2号,依然指向显示器的,所以stdout就写到了log.txt文件,stderr就写入了显示器!

这有什么用呢?其实这样可以将程序的正确信息和错误信息分开,可以更加容易的识别错误,对调试很有帮助!如如说,当前程序结束后有很多的输出,你想知道那些是错误的就可以直接用重定向分流!

假设这些都是输出,我们想找错误的就可以,重定向分流:

其实我们语言上学的perror以及cerr本质就是想stderr(2)中打印,而cout和printf是将stdout(1)中打印!

如果我现在也想让fd2和fd1卸载同一个文件中呢?

./test 1 > log.txt 2>&1

什么意思呢?先将./test fd1中的本应该输出到显示器的内容,从定向输出到log.txt中,然后再将fd2中的原本输出到显示器的内容重定向到stdout也就是fd1位置的文件,而fd1位置的文件已经被换成了log.txt所以stdout和stderr写入到了同一个文件!

OK,本期内容就分享到这里!好兄弟,我们下期再见!

结束语:未来可期!

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

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

相关文章

如何设置postgresql数据库的账户密码

说明&#xff1a;在我的云服务器上&#xff0c;postgres是使用yum的方式安装的&#xff0c;不需要设置postgres账户的密码&#xff0c;本文介绍安装后如何手动设置postgres账户的密码&#xff1b; postgres数据库安装&#xff0c;参考下面这篇文章&#xff1a; PostgreSQL安装…

构建基于Spring Boot的SaaS应用

引言 在设计和实现SaaS系统时&#xff0c;安全性是至关重要的考虑因素。一个全面的安全策略不仅能保护系统免受恶意攻击&#xff0c;还能确保用户数据的机密性、完整性和可用性。本文将探讨在SaaS架构中实现数据加密、敏感信息保护以及应用安全的最佳实践和技术方案&#xff0…

【大模型】基于LoRA微调Gemma大模型(1)

文章目录 一、LoRA工作原理1.1 基本原理1.2 实现步骤 二、LoRA 实现2.1 PEFT库&#xff1a;高效参数微调LoraConfig类&#xff1a;配置参数 2.2 TRL库SFTTrainer 类 三、代码实现3.1 核心代码3.2 完整代码 参考资料 大模型微调技术有很多&#xff0c;如P-Tuning、LoRA 等&#…

Vue3计算属性终极实战:可媲美Element Plus Tree组件研发之节点勾选

前面完成了JuanTree组件的节点编辑和保存功能后&#xff0c;我们把精力放到节点勾选功能实现上来。**注意&#xff0c;对于组件的开发者来说&#xff0c;要充分考虑用户的使用场景&#xff0c;组件提供的多个特性同时启用时必须要工作良好。**就拿Tree组件来说&#xff0c;用户…

数据库(MySQL)-视图、存储过程、触发器

一、视图 视图的定义、作用 视图是从一个或者几个基本表&#xff08;或视图&#xff09;导出的表。它与基本表不同&#xff0c;是一个虚表。但是视图只能用来查看表&#xff0c;不能做增删改查。 视图的作用&#xff1a;①简化查询 ②重写格式化数据 ③频繁访问数据库 ④过…

如何学习Doris:糙快猛的大数据之路(从入门到专家)

引言:大数据世界的新玩家 还记得我第一次听说"Doris"这个名字时的情景吗?那是在一个炎热的夏日午后,我正在办公室里为接下来的大数据项目发愁。作为一个刚刚跨行到大数据领域的新手,我感觉自己就像是被丢进了深海的小鱼—周围全是陌生的概念和技术。 就在这时,我的…

江苏科技大学24计算机考研数据速览,有专硕复试线大幅下降67分!

江苏科技大学&#xff08;Jiangsu University of Science and Technology&#xff09;&#xff0c;坐落在江苏省镇江市&#xff0c;是江苏省重点建设高校&#xff0c;江苏省人民政府与中国船舶集团有限公司共建高校&#xff0c;国家国防科技工业局与江苏省人民政府共建高校 &am…

pyqt designer使用spliter

1、在designer界面需要使用spliter需要父界面不使用布局&#xff0c;减需要分割两个模块选中&#xff0c;再点击spliter分割 2、在分割后&#xff0c;再对父界面进行布局设置 3、对于两边需要不等比列放置的&#xff0c;需要套一层 group box在最外层进行分割

Linux系统:date命令

1、命令详解&#xff1a; date 命令可以用来显示或设定系统的日期与时间。 2、官方参数&#xff1a; -d, --dateSTRING 通过字符串显示时间格式&#xff0c;字符串不能是now。-f, --fileDATEFILE 类似 --date 在 DATEFILE 的每一行生效-I[FMT], --iso-8601[FMT…

Redis的使用场景、持久化方式和集群模式

1. Redis的使用场景 热点数据的缓存 热点数据&#xff1a;频繁读取的数据 限时任务的操作。比如短信验证码 完成session共享的问题。因为前后端分离 完成分布式锁 商品的销售量 2. Redis的持久化方式 2.1 什么是持久化 把内存中的数据存储到磁盘的过程。同时也可以把磁盘中…

Python中的Numpy库使用方法

numpy Ndarry和创建数组的方式 NumPy数组&#xff08;ndarray&#xff09;是NumPy库的核心数据结构&#xff0c;它是一系列同类型数据的集合&#xff0c;以 0 下标为开始进行集合中元素的索引。 ndarray本质上是一个存放同类型元素的多维数组&#xff0c;其中的每个元素在内存…

TransformerEngine

文章目录 一、关于 TransformerEngine &#xff1f;亮点 二、使用示例PyTorchJAXFlax 三、安装先决条件Dockerpip从源码使用 FlashAttention-2 编译 四、突破性的变化v1.7: Padding mask definition for PyTorch 五、FP8 收敛六、集成七、其它贡献论文视频最新消息 一、关于 Tr…

美团大众点评字符验证码

声明(lianxi a15018601872) 本文章中所有内容仅供学习交流使用&#xff0c;不用于其他任何目的&#xff0c;抓包内容、敏感网址、数据接口等均已做脱敏处理&#xff0c;严禁用于商业用途和非法用途&#xff0c;否则由此产生的一切后果均与作者无关&#xff01; 前言(…

为什么优秀员工往往最先离职?

在企业管理中有很多误区&#xff0c;令企业流失优秀员工和人才&#xff0c;根据优思学院过往的经验&#xff0c;大致可以分为以下几个情况。 1. 忽视帕累托法则&#xff08;80/20法则&#xff09; 帕累托法则&#xff08;80/20法则&#xff09;是六西格玛管理的基本原则&…

好的STEM编程语言有哪些?

STEM是科学&#xff08;Science&#xff09;&#xff0c;技术&#xff08;Technology&#xff09;&#xff0c;工程&#xff08;Engineering&#xff09;&#xff0c;数学&#xff08;Mathematics&#xff09;四门学科英文首字母的缩写&#xff0c;STEM教育简单来说就是在通过在…

django_创建菜单(实现整个项目的框架,调包)

文章目录 前言代码仓库地址在线演示网址启动网站的时候出现错误渲染路径的一些说明文件结构网页显示一条错误路由顺序js打包出现问题的代码函数没有起作用关于进度开发细节显示不了图片梳理一下函数调用的流程修改一些宽度参数classjs 里面的一些细节让三个按钮可以点击设置按钮…

前端JS特效第56集:基于canvas的粒子文字动画特效

基于canvas的粒子文字动画特效&#xff0c;先来看看效果&#xff1a; 部分核心的代码如下(全部代码在文章末尾)&#xff1a; <!DOCTYPE html> <html lang"zh"> <head><meta charset"UTF-8"><meta http-equiv"X-UA-Compat…

GPT-4O 的实时语音对话功能在处理多语言客户时有哪些优势?

最强AI视频生成&#xff1a;小说文案智能分镜智能识别角色和场景批量Ai绘图自动配音添加音乐一键合成视频百万播放量 我瞄了一眼OpenAI春季发布会&#xff0c;这个发布会只有26分钟&#xff0c;你可以说它是一部科幻短片&#xff0c;也可以说它过于“夸夸其谈”&#xff01;关于…

5个工具帮助你轻松将PDF转换成WORD

有时候编辑PDF文件确实不如编辑word文档方便&#xff0c;很多人便会选择先转换再编辑。但是如果还有人不知道要怎么将PDF文件转换成word文档的话&#xff0c;可以看一下这5款工具&#xff0c;各种类型的都有&#xff0c;总有一款可以帮助到你。 &#xff11;、福昕PDF转换软件 …

socket实现全双工通信,多个客户端接入服务器端

socket实现全双工通信 客户端&#xff1a; #define IP "192.168.127.80" //服务器IP地址 #define PORT 7266 // 服务器端口号int main(int argc, const char *argv[]) {//1.创建套接字&#xff1a;用于接收客户端链接请求int sockf…