【Linux】基础I/O

  1. 复习C文件IO相关操作

1.1.文件的打开和关闭

文件在读写之前应该先打开文件,在使用结束之后应该关闭文件。

//打开文件
FILE * fopen ( const char * filename, const char * mode );
//关闭文件
int fclose ( FILE * stream );
【Linux】基础I/O_第1张图片

1.2.文件的顺序读写

【Linux】基础I/O_第2张图片

对比一组函数:

scanf/fscanf/sscanf
printf/fprintf/sprintf

1.3.操作文件指针

移动文件指针

int fseek( FILE *stream, long offset, int origin );
//第一个参数:流
//第二个参数:偏移量
//第三个参数:->
//SEEK_CUR:文件指针指在当前位置
//SEEK_SET:文件指针指在文件开始
//SEEK_END:文件指针指在文件结束

获取文件指针的偏移量

long ftell( FILE *stream );
//参数是流
//返回:相对于起始位置的偏移量。

让文件指针回到文件的起始位置

void rewind( FILE *stream );
//参数:流

1.4.文件读取结束的判定

牢记:在文件读取过程中,不能用feof函数的返回值直接用来判断文件的是否结束。

而是应用于当文件读取结束的时候,判断是读取失败结束,还是遇到文件尾结束

文本文件

文本文件读取是否结束,判断返回值是否为 EOF ( fgetc ),或者 NULL ( fgets )

例如:

fgetc 判断是否为 EOF .
fgets 判断返回值是否为 NULL .
#include 
#include 
int main(void)
{
    int c; // 注意:int,非char,要求处理EOF
    FILE* fp = fopen("test.txt", "r");
    if(!fp) {
        perror("File opening failed");
        return EXIT_FAILURE;
   }
 //fgetc 当读取失败的时候或者遇到文件结束的时候,都会返回EOF
    while ((c = fgetc(fp)) != EOF) // 标准C I/O读取文件循环
   { 
       putchar(c);
   }
 //判断是什么原因结束的
    if (ferror(fp))
        puts("I/O error when reading");
    else if (feof(fp))
        puts("End of file reached successfully");
    fclose(fp);
}

二进制文件

二进制文件的读取结束判断,判断返回值是否小于实际要读的个数。

例如:

fread判断返回值是否小于实际要读的个数。
#include 
enum { SIZE = 5 };
int main(void)
{
    double a[SIZE] = {1.,2.,3.,4.,5.};
    FILE *fp = fopen("test.bin", "wb"); // 必须用二进制模式
    fwrite(a, sizeof *a, SIZE, fp); // 写 double 的数组
    fclose(fp);
    double b[SIZE];
    fp = fopen("test.bin","rb");
    size_t ret_code = fread(b, sizeof *b, SIZE, fp); // 读 double 的数组
    if(ret_code == SIZE) {
        puts("Array read successfully, contents: ");
        for(int n = 0; n < SIZE; ++n) printf("%f ", b[n]);
        putchar('\n');
   } else { // error handling
       if (feof(fp))
          printf("Error reading test.bin: unexpected end of file\n");
       else if (ferror(fp)) {
           perror("Error reading test.bin");
       }
   }
    fclose(fp);
}
  1. 系统文件I/O

2.1.再谈文件

1,空文件 ,也要在磁盘占用空间。(文件属性值也有空间)
2,文件 = 文件内容 + 文件属性
3,文件操作 = 对内容的操作 or 对属性的操作 or 对内容和属性的操作
4,文件标定:必须要使用:文件路径+文件名称(唯一性)
5,如果没有指明对应的文件路劲,默认是在当前路劲进行文件的访问。
6,这里的当前路径是进程当前路径。(chdir可修改进程当前路径)
7,当我们把fopen,fclose,fread,fwrite等接口写完后,编译成可执行程序,但是没有运行,文件的操作也是没有被执行的。
8所以对文件的操作是进程对文件的操作。
9,一个文件如果没有被打开,不能被直接访问。
10,换句话说,一个文件要被访问,首先必须先被打开。是被进程+OS打开的。进程调用OS接口,OS打开文件。
11,所以,文件操作的本质就是: 进程 和 被打开文件 的关系。

2.2.再谈文件操作

1,不仅c语言有文件操作接口,c++,java,python,php,go 等语言都有文件操作接口(函数)。并且接口都不一样。
2,文件在磁盘中,属于外设的一种,想访问磁盘就绕不开OS,所有语言都必须使用OS提供的接口,来访问文件。
3,OS只有一个,说以,无论上层语言怎么变化,OS调用接口都是一样的。语言的库函数可以千变万化,但是底层调用的OS文件接口是一样的。
上面我也已经复习了c语言的文件操作。建议好好复习一下再看,下面内容。

2.3.系统调用接口

2.3.1.打开文件(open)

#include 
#include 
#include 
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
//pathname: 要打开或创建的目标文件

//flags: 打开文件时,可以传入多个参数选项,用下面的一个或者多个常量进行“或”运算,构成flags。
//参数:
     //O_RDONLY: 只读打开
     //O_WRONLY: 只写打开
     //O_RDWR : 读,写打开
     //这三个常量,必须指定一个且只能指定一个
     //O_CREAT : 若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限
     //O_APPEND: 追加写
     //O_TRUNC : 清空文件。

//返回值:
     //成功:新打开的文件描述符
     //失败:-1
【Linux】基础I/O_第3张图片
在C语言中fopen我们打开文件返回的是FILE* (文件指针)。
open打开文件后返回一个文件描述符。

2.3.2.flags标记位传参

上面的flags就是标记位传参,什么是标记为传参呢?

我们知道一个整数有32个比特位,我们可以通过比特位来传递选项,不同的比特位,代表不同的选项。
一个比特位,代表一个选项
如何使用比特位传递选项?
//一个宏对应一个比特位,彼此不重叠    
#define ONE 1    
#define TWO (1<<1)    
#define THREE (1<<2)    
#define FOUR (1<<3)    
    
               
void fun(int flags){                                                                                                                                                                         
    if(flags & ONE) printf("one\n");                     
    if(flags & TWO) printf("two\n");                             
    if(flags & THREE) printf("three\n");    
    if(flags & FOUR) printf("four\n");             
}                       
int main()                 
{                                           
    fun(ONE);              
    printf("---------------------\n");    
    fun(ONE|TWO);                                                      
    printf("---------------------\n");                               
    fun(ONE|TWO|THREE);                                       
    printf("---------------------\n");    
    fun(ONE|TWO|THREE|FOUR);                              
    return 0;                                     
}                                                        
  
【Linux】基础I/O_第4张图片
这就是标记为传参,可以合理运用‘&’ 和 ‘ | ’可以实现参数的合理传递。

打开文件open的参数标记位使用

O_WRONLY:只读打开

【Linux】基础I/O_第5张图片
【Linux】基础I/O_第6张图片

O_CREAT:不存在则创建,必须传入第三个参数mode,指明初始权限。

【Linux】基础I/O_第7张图片
【Linux】基础I/O_第8张图片

默认使用系统中的umask,也可以调用系统接口umask自己指定.

【Linux】基础I/O_第9张图片
【Linux】基础I/O_第10张图片
【Linux】基础I/O_第11张图片

2.3.3.写入数据(write)

不同的打开方式写入的时候会有不同的效果

【Linux】基础I/O_第12张图片
#include
ssize_t write(int fd, const void* buf, size_t count);
//第一个参数:
    //fd要写入文件的文件描述符
//第二个参数:
    //buf:写入数据的来源(void*类型不分文本还是二进制)
//第三个参数
    //写入长度

//返回值
    //写入的字节个数
在C语言中我们以w方式打开的时候,会自动清空文件

清空文件(O_TRUNC)

假如程序没有输入O_TRUNC ,写入的时候是直接覆盖式的写入
【Linux】基础I/O_第13张图片
【Linux】基础I/O_第14张图片
看到文件的后面不是我们想要的结果。
加上O_TRUNC才是我们想要的结果
【Linux】基础I/O_第15张图片

追加打开(O_APPEND)

【Linux】基础I/O_第16张图片
【Linux】基础I/O_第17张图片

2.3.4.读取数据(read)

【Linux】基础I/O_第18张图片
#include
ssize_t read(int fd,void* buf, size_t const);
//参数
    //buf读取数据存放的位置.
    //count希望读取的字节数(可能读不到那么多文件就结束了)

//返回值
    //ssize_t是系统定制的类型,本质式 long int 类型
    //读取到的数据的实际字节数
    //返回-1 就是错误.
【Linux】基础I/O_第19张图片
【Linux】基础I/O_第20张图片

2.3.5.总结

C语言中 fopen fclose fread fwrite 都是C标准库当中的函数,我们称之为库函数(libc)。

而, open close read write lseek 都属于系统提供的接口,称之为系统调用接口

【Linux】基础I/O_第21张图片
可以认为库函数接口都是对系统接口的封装

2.4.文件描述符fd

2.4.1.什么是文件描述符

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

系统中同时会被打开很多的文件,被打开的文件,要被管理起来,OS会先描述再管理.所OS为了管理对应的打开文件,必须要位文件创建对应的内核数据结构.来表示文件.

这个内核结构体叫struct file{ },内部包含了文件的属性。

【Linux】基础I/O_第22张图片
【Linux】基础I/O_第23张图片
上面我们发现一个现象:我们打开的文件描述符都是3, 那1 & 2 & 0,去哪里了
【Linux】基础I/O_第24张图片

所以文件描述符就是进程中文件描述符表的下标。文件描述符表是一个指针(struct_file*)数组,指向OS打开文件的struct_file对象,一个下标对应一个文件。

2.4.2. 描述符: 0 & 1 & 2

Linux进程默认情况下会有3个缺省打开的文件描述符,分别是标准输入0, 标准输出1, 标准错误2.

0,1,2对应的物理设备一般是:键盘,显示器,显示器

所有输入输出还可以这样表示:
#include 
#include 
#include 
#include 
#include 
int main()
{
     char buf[1024];
     ssize_t s = read(0, buf, sizeof(buf));
     if(s > 0){
         buf[s] = 0;
         write(1, buf, strlen(buf));
         write(2, buf, strlen(buf));
      }
     return 0;
}
【Linux】基础I/O_第25张图片

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

2.4.3.C语言中的FILE是什么?

FILE是一个结构体,是C语言对文件描述符的封装.

里面一定有一个字段是文件描述符fd.

stdin->_fileno
【Linux】基础I/O_第26张图片
【Linux】基础I/O_第27张图片

2.4.4.文件描述符得分配规则

上面介绍我们知道,系统会默认的打开三个文件描述符占据0 & 1 & 2 三个位置,我们自己的文件打开后默认都是按照3,4,5---,一直往后面排的。

先来一个小实验

【Linux】基础I/O_第28张图片
【Linux】基础I/O_第29张图片
【Linux】基础I/O_第30张图片

发现是结果是: fd: 0 或者 fd 2 可见,文件描述符的分配规则:在fifiles_struct数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符。

2.5.重定向

2.5.1.概念

上面我们关闭的是标准输入,标准错误,那如果关闭1呢?我们把显示器都关闭了,会有什么效果呢?

看代码:

【Linux】基础I/O_第31张图片
【Linux】基础I/O_第32张图片

重定向的本质:上层用的fd不变(printf用的fd ==1),内核更改fd==1对应的struct_file* 地址。

【Linux】基础I/O_第33张图片

此时,我们发现,本来应该输出到显示器上的内容,输出到了新打开的文件当中,其中,fd=1。这种现象叫做输出重定向。

常见的重定向有:

输出重定向:>
追加重定向: >>
输入重定向: <

2.5.2.重定向系统调用(dup2)

函数介绍
【Linux】基础I/O_第34张图片
int dup2 (int oldfd, int newfd);

如果重定向成功返回newfd的值,所以此函数就是把 oldfd下标对应的指针,拷贝给newfd下标对应的位置。并返回newfd的值。重定向失败返回-1。

【Linux】基础I/O_第35张图片

重定向分为:

输出重定向:>
追加重定向: >>
输入重定向: <
输出重定向:>
追加重定向: >>
【Linux】基础I/O_第36张图片
输入重定向<
【Linux】基础I/O_第37张图片
【Linux】基础I/O_第38张图片

2.5.3.修改自己写的shell让它支持重定向

//我们通过父进程创建子进程的时候,子进程会拷贝进程的文件描述符表,不会父进程公用

//父进程打开的文件不会被子进程拷贝过去,只是拷贝三个默认的文件(0 & 1 & 2)。

//我们对子进程进行重定向不会影响父进程。

//进程具有独立性

//进程的程序替换的时候,不会影响曾经进程打开的文件和重定向的文件。

//进程替换的是代码和数据,进程的文件描述符表和pcb(tast_struct)是内核数据结构。是OS管理的

//进程替换的时候不会影响他们。

【Linux】基础I/O_第39张图片

源码:(可能不完善,大家可以自己完善完善)

#include                                                                                                                                                                            
#include    
#include    
#include    
#include    
#include    
#include    
#include    
#include    
#include    
#include    
//宏函数    
#define trimSpace(start) do{\    
while (isspace(*start)) start++; \
    }while (0)
    //'\'是续行符。    
    //再内核中经常使用do {  } while(0) ,来包含一个语句块。    
    //isspace () 是判断是不是空格的意思,是空格返回真    
#define NUM 1024    
//标记    
#define NONE 0 //没有重定向    
#define INPUT 1 //输入重定向    
#define OUTPUT 2  //输出重定向    
#define APPEND 3  // 追加重定向    


char linecommand[NUM];
char* argv_[60];
int lastcode = 0;
int lastsig = 0;
int redirTYpe = NONE;//重定向方式
char* redirFile = NULL;//重定向的文件名称
//重定向命名行处理
void commandCheck(char* commands)
{

    assert(commands);
    char* start = commands;
    char* end = commands + strlen(commands);
    while (start < end)
    {
        if (*start == '>')
        {
            *(start) = '\0';
            start++;
            if (*start == '>')
            {
                start++;
                redirTYpe = APPEND;
            }
            else {
                redirTYpe = OUTPUT;
            }

            trimSpace(start);
            redirFile = start;
            break;
        }
        else if (*start == '<')
        {
            *start = '\0';
            start++;
            //去空格
            trimSpace(start);//宏函数
            redirFile = start;
            redirTYpe = INPUT;
            break;
        }
        else {
            start++;
        }
    }
}



int main()
{
    while (1)
    {
        //对全局变量进行初始化        
        redirFile = NULL;
        redirTYpe = NONE;

        //打印提示符
        printf("用户名@主机名 当前路劲#");
        fflush(stdout);
        //获取用户输入
        char* str = fgets(linecommand, NUM - 1, stdin);
        assert(str != NULL);
        (void)str;
        //清楚最后一个\n的字符,
        //因为我们输入完成后会敲入一个回车键,回车也会被获取到linecommand中
        linecommand[strlen(linecommand) - 1] = '\0';//消除最后的回车键
        //printf("%s\n",linecommand);//获取成功
//处理输出重定向 >  
//追加重定向  >>  
//输入重定向  <
        //以前我们读取的命令行一般是 ls -a -l -i 
        //假如有了重定向符号 就变成了  ls -a -l -i  >  test.txt
        //这就要我们加以处理了,怎么把输出结果 重定向到tets.txt 里面 (运用dup2函数)
        commandCheck(linecommand);


        //分割linecommand,
        argv_[0] = strtok(linecommand, " ");
        int i = 1;
        //处理ls的颜色和缩写
        if (strcmp(argv_[0], "ll") == 0) { argv_[0] = (char*)"ls"; argv_[i++] = (char*)"-l"; }
        if (strcmp(argv_[0], "ls") == 0) { argv_[i++] = (char*)"--color=auto"; }
        while ((argv_[i++] = strtok(NULL, " ")) != NULL);

        //处理cd 和 echo这样的内建命令
        if (argv_[0] != NULL && strcmp(argv_[0], "cd") == 0) {
            if (argv_[1] != NULL)chdir(argv_[1]);
            continue;
        }
        if (argv_[0] != NULL && argv_[1] != NULL && strcmp(argv_[0], "echo") == 0 && getenv(((char*)argv_[1]) + 1) == NULL) {
            if (strcmp(argv_[1], "$?") == 0)
                printf("code:%d sig:%d\n", lastcode, lastsig);
            else
                printf("%s\n", argv_[1]);
            continue;
        }
        //创建子进程,进行程序替换
        pid_t id = fork();
        assert(id != -1);
        if (id == 0)//重定向的工作一定是子进程完成的
            //但是如何重定向,是父进程要给子进程提供信息的。
        {
            //开始重定向
            //重定向不会影响父进程。
            //进程具有独立性
            switch (redirTYpe)
            {
            case NONE://没有重定向
                break;
            case INPUT://输入重定向
            {
                int fd = open(redirFile, O_RDONLY);
                if (fd < 0)
                {
                    perror("open");
                    exit(errno);
                }
                fd = dup2(fd, 0);
            }
            break;
            case OUTPUT://输出重定向
            {
                int fd = open(redirFile, O_WRONLY | O_CREAT | O_TRUNC, 0666);
                if (fd < 0)
                {
                    perror("open");
                    exit(errno);
                }
                fd = dup2(fd, 1);
                break;
            }
            case APPEND://追加重定向
            {
                int fd = open(redirFile, O_WRONLY | O_CREAT | O_APPEND, 0666);
                if (fd < 0)
                {
                    perror("open");
                    exit(errno);
                }
                fd = dup2(fd, 1);
                break;
            }

            default:
                printf("error & bug !!1\n");

            }
            execvp(argv_[0], argv_);
            exit(1);
        }
        int status = 0;
        pid_t ret = waitpid(id, &status, 0);//阻塞等待
        //进程的程序替换的时候,不会影响曾经进程打开的文件和重定向的文件
        //进程替换的是代码和数据,进程的文件描述符表和pcb(tast_struct)是内核数据结构。是OS管理的
        //进程替换的时候不会影响他们。
        assert(ret > 0);
        (void)ret;
        lastcode = ((status >> 8) & 0xff);
        lastsig = (status & 0x7f);
    }
}

printf是C库当中的IO函数,一般往 stdout 中输出,但是stdout底层访问文件的时候,找的还是fd:1, 但此时,fd:1下标所表示内容,已经变成了myfifile的地址,不再是显示器文件的地址,所以,输出的任何消息都会往文件中写入,进而完成重定向。

怎么理解Linux下一切接文件。

一张图告诉你为什么在Linux下,任何硬件都可以当作文件看待。

【Linux】基础I/O_第40张图片

一般将各种硬件对应的struct_file那一层称作VFS 虚拟文件系统。

有兴趣的可以去看看源码:在stat_struct(pcb)里面,肯定有一个struct file_struct* files;
在struct file_struct* files 里面有一个数组是 struct file* fd_array [ ]
这就是进程的文件描述符表。不同的机器数组长度不同。
struct file里面有各种各样的属性,
1, 引用计数(有多少指针指向此文件),所以我们在关闭文件的是只是引用计数--;当的引用计数减到0的时候,OS会关闭此文件。
2, f_mode文件权限
3, f_pos文件的写入位置
......

  1. C语言缓冲区

3.1.引入案例

【Linux】基础I/O_第41张图片
【Linux】基础I/O_第42张图片

我们发现 printf 和 fwrite (库函数)都输出了2次,而 write 只输出了一次(系统调用)。为什么呢?肯定和fork有关!

1,一般C库函数写入文件时是全缓冲的,而写入显示器是行缓冲。
2,printf fwrite 库函数会自带缓冲区(进度条例子就可以说明),当发生重定向到普通文件时,数据
的缓冲方式由行缓冲变成了全缓冲。而我们放在缓冲区中的数据,就不会被立即刷新,甚至fork之后
但是进程退出之后,会统一刷新,写入文件当中。
3,但是fork的时候,父子数据会发生写时拷贝,所以当你父进程准备刷新的时候,子进程也就有了同样的一份数据,随即产生两份数据。
4,write 没有变化,说明没有所谓的缓冲。

printf fwrite 库函数会自带缓冲区,而 write 系统调用没有带缓冲区。另外,我们这里所说的缓冲区,都是用户级缓冲区。其实为了提升整机性能,OS也会提供相关内核级缓冲区。

那这个缓冲区谁提供呢? printf fwrite 是库函数, write 是系统调用,库函数在系统调用的“上层”, 是对系统调用的“封装”,但是 write 没有缓冲区,而 printf fwrite 有,足以说明,该缓冲区是二次加上的,又因为是C,所以由C标准库提供

缓冲区本质就是一块内存!!就是内存中的一块内存区。

结论:以上的演示的缓冲区是语言级别的缓冲区,是C语言标准库提供的,在系统调用之上。然后通过一系列的刷新策略,将缓冲区的数据刷新到磁盘。

【Linux】基础I/O_第43张图片

3.2.c语言缓冲区的刷新策略

我们知道进程把数据拷贝到缓冲区,缓冲区并不是你拷贝一次我就刷新一次,这样和没有缓冲区是一样的效果,所以缓冲区数据积累到一定的大小,才刷新,写入磁盘。

缓冲区一般是多次写入,尽量少的刷新,因为IO的过程很慢。例如IO的过程有99%的时间在等待磁盘就绪。

缓冲区会结合具体的设备,定制自己的刷新策略。通常C语言的缓冲区有三种刷新方式。

a,立即刷新---无缓冲
b,行刷新 ----行缓存 ---- 显示器
c,缓冲区满 ---全刷新 -----磁盘文件

还有两种特殊情况:

d,用户强制刷新
e,进程退出 --一般都要进行缓冲区刷新。

3.3.C语言缓冲区在哪里

从上文的案例中我们发现,C语言缓冲区不在内核中,所以我们以前谈到的缓冲区都是指的语言层面给我们提供的缓冲区
这个缓冲区和文件有关,因为Linux一切皆文件,在用户看来,所有的外设都是文件,都要文件描述符fd,在语言层面,fd会被封装为FILE的一个结构体,这是语言层面和IO有关的结构。

所以:FILE 里面有fd && 缓冲区

这就解释了为什么,我们fflush(文件指针)要传入一个文件指针,flclose(文件指针)也要传入一个文件指针,因为fflush 和 fclose 都要刷新缓冲区而缓冲区就在文件指针FILE中。

我们可以打开c标准库中找到对应的FILE 结构体查看一下。

typedef struct _IO_FILE FILE;

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

3.4.解释案例

所以我们现在,就可以很清楚的上文案例中出现现象。

在第二种情况下,我们看到在没有重定向(>) 的时候打印的是一份,当我们重定向之后,C语言库函数打印两份,系统调用打印一份。

1,stdout默认使用的行刷新,三个函数拷贝到缓冲区的数据都带了\n,每一条都直接刷新了缓冲区,在子进程创建前,FILE的缓冲区的数据已经被刷新到显示器(外设)了,在FILE的缓冲区内部已经不存在数据了,

2,如果我们进行了重定向(>) ,写入文件不再是显示器,而是普通磁盘文件,普通的磁盘文件,刷新策略是缓冲区满刷新(全刷新),之前的三个函数虽然带了\n,但是不足以将stdout缓冲区放满,数据没有刷新。!!

3,创建子进程的时候,stdout属于都进程,创建子进程后,子进程和父进程都会刷新自己stdout对应的缓冲区,谁先退出,谁就一定要先进行缓冲区的刷新,(刷新缓冲区的本质就是:修改FILE的缓冲区中对应的数据,将数据写入文件,就是修改缓冲区的数据)。此时一定会发生写时候拷贝。数据最终会显示两份。

4,为什么write没有呢?上面的过程和wirte无关,wirte没有FILE,而用的是fd,就没有C语言库提供的缓冲区。

3.5.简单封装一下系统调用接口

mystdio.h
#include    
#include    
#include    
#include    
#include    
#include    
#include    
#include    

#define SIZE 1024    
#define NOW 1 //立即刷新    
#define LINE 2 //行刷新    
#define FULL 4 //满刷新    

typedef struct _FILE {
    int flags;//刷新方式                                
    int fileno;//文件描述符                                      
    char buf[SIZE];//缓冲区    
    int size;//存入的字符数                       
    int cap;//buf最大容量                
}_FILE;

_FILE* _fopen(const char* path_name, const char* mode);
void _fclose(_FILE* fp);
void _fwrite(const char* ptr, size_t num, _FILE* fp);
void _fflush(_FILE* fp);
//以上接口可能和C语言库函数中的不一样,是我为了让自己更好理解,进而简化过的接口   
mystdio.c
#include "test.h"                                                                                                                                                                            


_FILE* _fopen(const char* path_name, const char* mode)
{
    int fd = -1;
    int flag = 0;
    if (strcmp(mode, "r") == 0)
    {
        flag |= O_RDONLY;
    }
    else if (strcmp(mode, "w") == 0)
    {
        flag |= (O_WRONLY | O_CREAT | O_TRUNC);
    }
    else if (strcmp(mode, "a") == 0)
    {
        flag |= (O_WRONLY | O_APPEND | O_CREAT);
    }
    else {
        //NULL;    
    }
    if (flag & O_CREAT) fd = open(path_name, flag, 0666);
    else fd = open(path_name, flag);

    if (fd < 0) {
        //文件打开失败    
        write(2, strerror(errno), strlen(strerror(errno)));
        return NULL;
        //这就是为什么文件打开失败会返回NULL    
    }
    _FILE* fp = (_FILE*)malloc(sizeof(_FILE));
    assert(fp != NULL);
    fp->fileno = fd;
    fp->flags = LINE;
    //打开不同的文件这个flag需要填入不同的数据
//需要我们判断一下,但是今天我们就不写那么复杂了
//演示行刷新就行。

    fp->size = 0;
    fp->cap = SIZE;
    memset(fp->buf, 0, SIZE);//给buf初始化为0
    return fp;
}


void _fflush(_FILE* fp)
{
    //fflush就是把缓冲区的文件刷新到磁盘文件。
    //并且清空缓冲区
    if (fp->size > 0)
    {
        write(fp->fileno, fp->buf, fp->size);
        fsync(fp->fileno);//强制刷新内核缓冲区。
        fp->size = 0;
    }
}

void _fclose(_FILE* fp)
{
    //语言层面的关闭文件就是刷新缓冲区,然后关闭文件
    _fflush(fp);
    close(fp->fileno);
}

void _fwrite(const char* ptr, size_t num, _FILE* fp)
{
    //C语言库函数写入的本质是:将数据拷贝到FILE结构中的缓冲区buf中
    //然后更具不同的刷新规则检查释放需要将,缓冲区刷出到磁盘文件。
    memcpy(fp->buf + fp->size, ptr, num);
    fp->size += num;
    //这里简化不考虑缓冲区溢出的问题。
    //如果想考虑就需要判断大小,其实也不难
    //今天是为了演示原理。


    //根据不同的刷新方式检查是否刷新
    if (fp->flags & NOW)
    {
        _fflush(fp);
    }
    else if (fp->flags & LINE)
    {
        if (fp->buf[fp->size - 1] == '\n')
        {
            _fflush(fp);
        }
        //这里并不完善,例如:assd\nasda 这种情况就会出问题
        //但是今天不考虑
    }
    else if (fp->flags & FULL)
    {
        if (fp->size == fp->cap)
        {
            _fflush(fp);
        }
    }
    else {
        //error
    }
}

测试用例1:

【Linux】基础I/O_第44张图片

测试用例2:

【Linux】基础I/O_第45张图片

4.内核缓冲区

其实在OS内核中,为了效率,也存在一个内核缓冲区,它全权由操作系统控制。

【Linux】基础I/O_第46张图片

fsync接口的使用

作用就是:强制刷新内核缓冲区。将数据刷新到磁盘文件/各种硬件。

【Linux】基础I/O_第47张图片

不做过多的解释,这个接口用于,比较重要的IO数据。

你可能感兴趣的:(c++,开发语言)