• 基础IO(上)——Linux


    1.储备知识

    对文件的操作范畴:
    在系统角度理解文件
    文件 = 内容 + 属性(也是数据)
    创建一个空文件也占空间
    对文件的所有操作无外乎两种:对内容和对属性

    c、c++程序会默认打开三个文件流:
    标准输入:键盘 extern FILE *stdin;
    标准输出:显示器 extern FILE *stdout;
    标准错误:显示器 extern FILE *stderr;

    linux下一切皆文件
    感性认识:
    曾经理解的文件:read、write
    键盘和显示器可以被看做文件吗?可以
    我从来没有打开过键盘和显示器文件,但是依旧能够直接使用scanf,fgets,printf,cout…
    是因为c、c++程序会默认打开三个文件流

    磁盘是硬件,只有操作系统才能真正的访问磁盘
    文件在磁盘上放着,我们访问文件,需要先写代码然后编译生成exe最后运行,那么访问文件本质是谁在访问文件呢?
    进程
    进程访问文件是需要接口的
    之前我们学习的接口是语言类的接口

    1. 要向硬件上写入,只有谁才有权利呢?(代码上)
      操作系统
    1. 如果普通用户也想向硬件写入呢?
      必须让OS提供接口
      提供文件类的系统调用接口

    为什么文件类的系统调用接口之前从未接触过?
    1). 因为之前学的是C语言,文件类的系统调用接口,比较难掌握,语言上对这些接口做一下封装,让接口更好被使用——>语言类接口
    每个语言都会做封装,就会导致了不同的语言有不同的语言级别文件访问接口。但是底层用的都是系统接口。
    为什么要学习os层面的接口?
    linux中这样的接口只有一套
    (os只有一个)
     

    2). 跨平台:把所有平台的代码都实现一遍,条件编译,动态裁剪
    如果语言不提供对文件的系统接口的封装,是不是所有访问文件的操作,都必须直接使用OS的接口?
    是的
    面对语言的客户,要不要访问文件呢?

    一旦使用系统接口,编写所谓的文件代码,无法在其他平台中直接运行了。不具备跨平台性!
    显示器和磁盘写入没有区别

    什么叫做文件?
    站在系统的角度,可以被input读取,或者能够output写出的设备就叫做文件
    狭义文件:普通的磁盘文件
    广义文件:显示器,键盘,网卡,声卡,显卡,磁盘……几乎所有的外设,都可以称之为文件。

     
     

    2. 文件描述符

    2.1 c接口

    makefile

       myfile:myfile.c
         gcc -o $@ $^
       .PHONY:clean
         rm -f myfile                                  
    
    • 1
    • 2
    • 3
    • 4

    w:从文件开始写
    a:从结尾开始写

      1 #include <stdio.h>
      2 
      3 int main()
      4 {
      5   FILE *fp = fopen("log.txt","w");
      6   if(fp == NULL)
      7   {
      8     perror("fopen");
      9     return 1;
     10   }
     11   //进行文件操作
     12                                                                                
     13 
     14 
     15   fclose(fp);            
     16   return 0;
     17 }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    此时在当前路径下,是否存在文件log.txt?——还没有,需要运行程序之后,log.txt才会被创建
    log.txt是谁创建的?——os
    会在哪里创建?——当前路径
    当前路径:当一个进程运行起来的时候,每个进程都会记录自己当前所处的工作路径
    在这里插入图片描述
     
    写文件的接口

      1 #include <stdio.h>
      2 #include<unistd.h>
      3 #include<string.h>
      4           
      5 int main()
      6 {         
      7   FILE *fp = fopen("log.txt","w");
      8   if(fp == NULL)
      9   {       
     10     perror("fopen");
     11     return 1;
     12   }       
     13   //进行文件操作
     14   const char *s1 = "hello fwrite";
     15   fwrite(s1,strlen(s1),1,fp);
     16           
     17   const char *s2 = "hello fprintf";
     18   fprintf(fp,"%s",s2);
     19           
     20   const char *s3 = "hello fputs";
     21   fputs(s3,fp);
     22                                                                                
     23  
     24              
     25   fclose(fp);
     26   return 0;
     27 }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    先删掉之前的log.txt,再运行程序
    在这里插入图片描述

     const char *s1 = "hello fwrite\n";
       fwrite(s1,strlen(s1),1,fp);
    
    • 1
    • 2

    要不要+1?fwrite(s1,strlen(s1)+1,1,fp);
    不要。
    \0结尾是C语言的规定,文件不用遵守
    如果+1打印出来就会多了一个乱码
    文件保存的是有效数据,/n只是标志结尾的标识符

    fopen:

    1. 当以写(w)方式打开文件时,会先清空文件。
      用重定向方式写入:echo helloworld > log.txt
      想要清空:>log.txt
      在这里插入图片描述

    2.以a方式打开文件是追加重定向,不会清空之前的内容
    r
    按行读取
    fgets是C语言提供的接口 s(string)会自动在字符结尾添加\0

    char line[64];
    while(fgets(line,sizeof(line),fp) != NULL)
    {
        fprintf(stdout,"%s",line);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    三个标准输入输出流:
    stdout
    标准输入:键盘 extern FILE *stdin;
    标准输出:显示器 extern FILE *stdout;
    标准错误:显示器 extern FILE *stderr;
    都叫做文件指针
    一切皆文件

     
    命令行参数:

    int main(int argc,char *argv[])
    
    • 1

     

    2.2 直接使用系统接口

    c库函数: fopen fclose fread fwrote
    系统调用:open close read write
    两者关系:上面的c库函数底层都是系统调用

    宏定义:全大写
    如何给函数传递标志位

     6 #define ONE 0x1//0000 0001
        7  #define TWO 0x2//0000 0010
        8  #define THREE 0x4//0000 0100
    W>  9  
       10  
       11 void show(int flags)
       12 {
       13   if(flags & ONE) printf("hello one\n");
       14   if(flags & TWO) printf("hello two\n");    
       15   if(flags & THREE)  printf("hello three\n");    
       16 }                                                      
       17                                                        
       18 int main()                                             
       19 {                                                      
       20   show(ONE);                                           
       21   show(TWO);                                           
       22   show(ONE | TWO);                                     
       23   show(ONE | TWO | THREE);                             
       24   return 0;                                                                  
       25     
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

     

    2.3 open函数返回值

    file descriptor:文件描述符
    返回值:成功:新打开的文件描述符失败:-1
     
    接口介绍
    open man open

    #include 
    #include 
    #include 
    int open(const char *pathname, int flags);
    int open(const char *pathname, int flags, mode_t mode);
    
    • 1
    • 2
    • 3
    • 4
    • 5

    pathname: 要打开或创建的目标文件
    flags: 打开文件时,可以传入多个参数选项,用下面的一个或者多个常量进行“或”运算,构成flags。

    参数:
    O_TRUNC:写之前清空文件
    O_RDONLY: 只读打开
    O_WRONLY: 只写打开
    O_RDWR : 读,写打开
    这三个常量,必须指定一个且只能指定一个
    O_CREAT : 若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限
    O_APPEND: 追加写
    mode_t理解:直接 man 手册,比什么都清楚。open 函数具体使用哪个,和具体应用场景相关,如目标文件不存在,需要open创建,则第三个参数表示创建文件的默认权限,否则,使用两个参数的open
    write read close lseek ,类比C文件相关接口

     

    2.4 文件描述符fd

    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;
    }
    FILE *fopen(const char *path,const char *mode);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    FILE是一个struct的结构体,由c标准库提供
    结构体内部会有多种成员
    而在系统角度只认fd。
    所以FILE结构体必定封装了fd
    _fileno

     

    2.5 周边文件

    fd是什么?
    之前说过进程要访问文件,必须先打开文件。
    那么一个进程可以打开多个文件吗?
    一般而言,进程:打开的文件 = 1:n

    文件要被访问,前提是要被加载到内存中,才能直接被访问
    进程:打开的文件 = 1:n 如果是多个进程都打开直接的文件呢?
    系统中就会存在大量的被打开的文件,所以OS要把如此之多的文件管理起来:
    先描述,再组织!

    文件的属性从哪里来?
    一部分在对应的磁盘中
    所以在内核中,OS内部要管理每一个被打开的文件,需要构建结构体

    struct file
    {
        struct file * next;
        struct file * prev;
        //包含了一个被打开的文件的几乎所有的内容(不仅仅包含属性)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    创建struct file的对象,充当一个被打开的文件。如果有很多再用双链表组织起来
    文件对象里面包含了文件的所有内容
    fd在内核中,本质是一个数组下标!

    文件:
    1.被进程在内存中打开的文件(执行你的代码的一定是CPU)
    2.没有被打开的文件(在磁盘上,文件=内容+属性)(磁盘文件)
    进程控制块:PCB struct task_struct

    fopen——open——fd——FLIE——FILE*
    fwrite()——FILE*——fd——write——write(fd,…)——自己执行操作系统内部的write方法——能找到进程的task_struct——*fs——file_struct——fd_arry[fd]——structfile——内存文件被找到了——操作

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

     
     

    3. 重定向

    3.1 输出重定向

     1 myfile:myfile.c
      2   gcc -o $@ $^
      3 .PHONY:clean
      4 clean:                                                                         
      5   rm -f myfile
    
    • 1
    • 2
    • 3
    • 4
    • 5
    #include
    #include
    #include
    #include
    int main()
    {
    int fd = open("log.txt",O_WRONLY | O_CREAT | O_TRUNC);
    if (fd < 0)
    {
    perror("open");
    return 1;
    }
    printf("fd: %d\n", fd);
    
    close(fd);
    return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    此时fd是3
    因为012已结被提前占用了

    #include
    #include
    #include
    #include
    int main()
    {
    int fd = open("log.txt",O_WRONLY | O_CREAT | O_TRUNC,0666);
    if (fd < 0)
    {
    perror("open");
    return 1;
    }
    printf("fd: %d\n", fd);
    
    close(fd);
    return 0;
    }
    fd:也是3
    close(0);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    fd:0
    close(2)
    fd:2
    结论:
    fd在系统层面的分配规则是:最小的,没有被占用的文件描述符

    #include
    #include
    #include
    #include
    int main()
    {
        close(1);
    int fd = open("log.txt",O_WRONLY | O_CREAT | O_TRUNC,0666);
    if (fd < 0)
    {
    perror("open");
    return 1;
    }
    printf("fd: %d\n", fd);
    
    close(fd);
    return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    不显示了

    处理:

    #include
    #include
    #include
    #include
    int main()
    {
    close(1);
    
    int fd = open("log.txt",O_WRONLY | O_CREAT | O_TRUNC,0666);
    if (fd < 0)
    {
    perror("open");
    return 1;
    }
    printf("fd: %d\n", fd);
    printf("fd: %d\n", fd);
    printf("fd: %d\n", fd);
    printf("fd: %d\n", fd);
    printf("fd: %d\n", fd);
    
    //close(fd);
    return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    printf默认是往stdout
    运行./myfile时依旧不显示
    但是cat log.txt时有内容,打印了fd:1
    确实是之前所说的fd分配原则

    #include
    #include
    #include
    #include
    int main()
    {
    close(1);
    
    int fd = open("log.txt",O_WRONLY | O_CREAT | O_TRUNC,0666);
    if (fd < 0)
    {
    perror("open");
    return 1;
    }
    printf("fd: %d\n", fd);
    printf("fd: %d\n", fd);
    printf("fd: %d\n", fd);
    printf("fd: %d\n", fd);
    printf("fd: %d\n", fd);
    fprintf(stdout, "hello fprintf\n");
    const char *s = "hello fwrite\n";
    fwrite(s, strlen(s), 1, stdout);
    
    fflush(stdout);
    
    close(fd);
    return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    这些接口都应该是往显示器(标准输出)打印的
    但是下面的内容都写入(显示)到了log.txt
    这就叫做输出重定向

    重定向的本质:
    就是在OS内部更改fd对应的内容的指向

     

    3.2 输出重定向

    #include
    #include
    #include
    #include
    
    
    int main()
    {
    int fd = open("log.txt", O_RDONLY);
    if (fd < 0)
    {
    perror("open");
    return 1;
    }
    printf("fd: %d\n", fd);
    
    char buffer[64];
    fgets(buffer, sizeof buffer, stdin);
    
    printf("%s\n", buffer);
    
    return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    fd:3
    输入hello
    输出hello

    close(0);
    
    • 1

    fd:0
    aaaaaaaaaaaaaa
    本来应该从键盘读取的内容,却直接读取log.txt中的内容
    这是输入重定向

     

    3.3 追加重定向

    #include
    #include
    #include
    #include
    
    
    int main()
    {
    close(1);
    int fd = open("log.txt", O_WRONLY | O_TRUNC | O_CREAT);
    if (fd < 0)
    {
    perror("open");
    return 1;
    }
    fprintf(stdout, "you can see me, success\n");
    
    return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    ./myfile
    cat log.txt
    you can see me,success
    再运行,打印也只有一行

    int main()
    {
    close(1);
    //int fd = open("log.txt", O_WRONLY | O_TRUNC | O_CREAT);
    int fd = open("log.txt", O_WRONLY | O_APPEND | O_CREAT);
    if (fd < 0)
    {
    perror("open");
    return 1;
    }
    fprintf(stdout, "you can see me, success\n");
    
    return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    ./myfile
    ./myfile
    ./myfile
    ./myfile
    you can see me,success
    you can see me,success
    you can see me,success
    you can see me,success
    运行几次,追加几次 这就是追加重定向

     

    3.4 dup

    oldfd copu to newfd
    最终要和oldfd一样,那么newfd就没有意义了,就可以关闭了

    #include
    #include
    #include
    #include
    
    
    int main(int argc, char *argv[])
    {
    if (argc != 2)
    {
    return 2
    }
    int fd = open("log.txt", O_WRONLY | O_TRUNC | O_CREAT);
    if (fd < 0)
    {
    perror("open");
    return 1;
    }
    
    fprintf(stdout, "%s\n", argv[1]);
    return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    退出码:2
    ./myfile 105
    105

    int main(int argc, char *argv[])
    {
    if (argc != 2)
    {
    return 2
    }
    int fd = open("log.txt", O_WRONLY | O_TRUNC | O_CREAT);
    if (fd < 0)
    {
    perror("open");
    return 1;
    }
    
    dup2(fd, 1);
    fprintf(stdout, "%s\n", argv[1]);
    return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    ./myfile hello
    不显示
    cat log.txt
    hello
    想显示的内容都会被打印到文件中而不是在显示器中

    int main(int argc, char *argv[])
    {
    if (argc != 2)
    {
    return 2
    }
    //int fd = open("log.txt", O_WRONLY | O_TRUNC | O_CREAT);
    int fd = open("log.txt", O_WRONLY | O_APPEND | O_CREAT);
    if (fd < 0)
    {
    perror("open");
    return 1;
    }
    
    dup2(fd, 1);
    fprintf(stdout, "%s\n", argv[1]);
    return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    ./myfile
    ./myfile aaa
    ./myfile bbb
    cat log.txt
    aaa
    bbb
    追加

    int main(int argc, char *argv[])
    {
    if (argc != 2)
    {
    return 2
    }
    //int fd = open("log.txt", O_WRONLY | O_TRUNC | O_CREAT);
    int fd = open("log.txt", O_WRONLY | O_APPEND | O_CREAT);
    if (fd < 0)
    {
    perror("open");
    return 1;
    }
    
    dup2(fd, 1);
    fprintf(stdout, "%s\n", argv[1]);
    close(fd);
    return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    依旧可以显示
    这是dup2的一种特性,与缓冲区有关

     
     

    4. 如何理解一切皆文件?

    理性理解:
    这是linux的设计哲学,体现在os的软件设计层面
     
    linux是用C语言写的,那么如何用C语言实现面向对象,甚至是运行时多态?
    类:所有事物与属性的结合
    成员属性+成员方法
    struct:可以包含成员属性,但是在纯C语言中不包含成员方法
    但是我想让struct中包含成员方法呢?
    可以定义函数指针,指向函数,去调用就可以了

    底层不同的硬件,一定对应的是不同的操作方法
    但是上面的设备都是外设,所以每一个设备的核心访问函数都可以是read、write(I、O)
    所有的设备都可以有自己的read和write,但是代码的实现一定不一样!
    设计一个struct,打开一个磁盘文件的时候,创建一个struct file
    各自指向各自的
    在这一层上面看,没有任何的硬件差别了,看待所有文件的方式,都统一成为了struct file
    所以就有了linux下一切皆文件的说法
    VFS虚拟文件技术

  • 相关阅读:
    先行进位加法器
    杰理之、产线装配环节【篇】
    合并报表软件选哪个?这篇文章两分钟告诉你!
    Ps:选框工具
    一文搞懂百度强推的Redis天花板笔记,原来数据库是这样理解的
    【Flink源码】再谈Flink程序提交流程(中)
    WEB前端应该学什么?学习到什么程度可以去找工作?附详细学习路线
    HTML躬行记(2)——WebRTC基础实践
    手摸手教你定制 Spring Security 表单登录
    Springboot+校园健身互助平台 毕业设计-附源码221540
  • 原文地址:https://blog.csdn.net/Ll_R_lL/article/details/127737414