• Linux——IO


    ✅<1>主页::我的代码爱吃辣
    📃<2>知识讲解:Linux——文件系统
    ☂️<3>开发环境:Centos7
    💬<4>前言:是不是只有C/C++有文件操作呢?python,java,php,go ..... 他们都是有文件操作?他们的文件操作一样吗?他们都有文件操作,且根据语言的语法不同,文件操作也是不同的。有没有一种同意的视角,看待所有语言的文件操作呢?

    目录

    一.回顾C文件IO相关操作

    1.C语言文件写入

    2.C语言文件读取

     3.输出信息到显示器有哪些方法

    二.系统文件IO

    1.open

     2.write

    3.close

     4.read

    三.对比C库与系统调用

    四.如何管理文件

    1.操作系统如何管理文件 

    2.进程如何管理文件 ——文件描述符

    3.文件描述符的分配规则

     三.重定向

    1.重定向原理

    2.dup2 系统调用

     四.理解FILE


    一.回顾C文件IO相关操作

    1.C语言文件写入

    测试代码:

    1. #include
    2. #include
    3. int main()
    4. {
    5. FILE *fp = fopen("myfile", "w");
    6. if (!fp)
    7. {
    8. printf("fopen error!\n");
    9. }
    10. const char *msg = "hello Linux!\n";
    11. const char *msg2 = "hello C++!\n";
    12. int count = 5;
    13. while (count--)
    14. {
    15. // 向文件中写入,
    16. // 参数1:写入的数据C++
    17. // 参数2:写入的字符个数
    18. // 参数3:写入的数据元素的个数
    19. // 参数4:写入的文件结构体指针
    20. fwrite(msg, strlen(msg), 1, fp);
    21. }
    22. int n = 5;
    23. while (n--)
    24. {
    25. // 向文件中写入,
    26. // 参数1:写入的文件结构体指针
    27. // 参数2:格式化写入
    28. fprintf(fp, "[%d]:%s", n, msg2);
    29. }
    30. fclose(fp);
    31. return 0;
    32. }

    测试结果:

    2.C语言文件读取

    1. #include
    2. #include
    3. int main()
    4. {
    5. FILE *fp = fopen("myfile", "r");
    6. if (!fp)
    7. {
    8. printf("fopen error!\n");
    9. }
    10. char buf[1024];
    11. const char *msg = "hello bit!\n";
    12. while (1)
    13. {
    14. // 注意返回值和参数,此处有坑,仔细查看man手册关于该函数的说明
    15. size_t s = fread(buf, 1, strlen(msg), fp);
    16. if (s > 0)
    17. {
    18. buf[s] = 0;
    19. printf("%s", buf);
    20. }
    21. if (feof(fp))
    22. {
    23. break;
    24. }
    25. }
    26. fclose(fp);
    27. return 0;
    28. }

     3.输出信息到显示器有哪些方法

    1. #include
    2. #include
    3. int main()
    4. {
    5. const char *msg = "hello fwrite\n";
    6. // 1.fwrite
    7. fwrite(msg, strlen(msg), 1, stdout);
    8. // 2.printf
    9. printf("hello printf\n");
    10. // 3.fprintf
    11. fprintf(stdout, "hello fprintf\n");
    12. return 0;
    13. }

    C库常见IO接口:

    1. // 1.默认向显示器格式化打印
    2. int printf(const char *format, ...);
    3. // 2.向指定的文件中格式化输入
    4. int fprintf(FILE * stream, const char *format, ...);
    5. // 3.向指定的空间中格式化输入
    6. int sprintf(char *str, const char *format, ...);
    7. // 4.向指定的空间中格式化输入指定个数字符
    8. int snprintf(char *str, size_t size, const char *format, ...);

     总结:

    1. C默认会打开三个输入输出流,分别是stdin, stdout, stderr
    2. 仔细观察发现,这三个流的类型都是FILE*, fopen返回值类型,文件指针

    二.系统文件IO

    操作文件,除了上述C接口(当然,C++也有接口,其他语言也有),我们还可以采用系统接口来进行文件访问,先来直接以代码的形式,实现和上面一模一样的代码:

    1.open

    隆重介绍一个系统调用:

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

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

    1. O_RDONLY: 只读打开
    2. O_WRONLY: 只写打开
    3. O_RDWR : 读,写打开
    4. 这三个常量,必须指定一个且只能指定一个
    5. O_CREAT : 若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限
    6. O_APPEND: 追加写

     返回值:

    • 成功:新打开的文件描述符
    • 失败:-1

     2.write

    1. #include
    2. ssize_t write(int fd, const void *buf, size_t count);

    参数介绍:

    1. fd:要写入的文件描述符。
    2. buf:要写入的字符串。
    3. count:写入的个数。

    3.close

    1. #include
    2. int close(int fd);

     关闭指定的文件描述符的文件。

    测试代码:

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. int main()
    8. {
    9. // fd:文件描述符
    10. // mufile:打开的文件名
    11. // O_WRONLY :写方式 | O_CREAT:没有该文件就创建 | O_APPEND : 追加写入
    12. int fd = open("myfile", O_WRONLY | O_CREAT | O_APPEND, 0666);
    13. if (fd == -1)
    14. {
    15. perror("open");
    16. }
    17. int count = 5;
    18. char *msge = "hello C++ and Linux\n";
    19. while (count--)
    20. {
    21. ssize_t n = write(fd, msge, strlen(msge));
    22. if (n == -1)
    23. {
    24. perror("write:");
    25. }
    26. }
    27. close(fd);
    28. return 0;
    29. }

    测试结果:

     4.read

    1. #include
    2. ssize_t read(int fd, void *buf, size_t count);

    参数:

    1. fd:读取文件的文件描述符
    2. buf:存储读取出的数据的缓冲区
    3. count:最大读取个数

    返回值:

    • 读取成功:返回读取的字节数。
    • 读取失败:返回-1.

     测试代码:

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. int main()
    8. {
    9. // fd:文件描述符
    10. // mufile:打开的文件名
    11. // ORDONLY:独方式打开
    12. int fd = open("myfile", O_RDONLY);
    13. if (fd == -1)
    14. {
    15. perror("open");
    16. }
    17. char buff[1024];
    18. // fd:读取文件的文件描述符
    19. // buff:存储读取数据的缓冲区
    20. // 1024:最大读取字节数
    21. ssize_t n = read(fd, buff, 1024);
    22. if (n == -1)
    23. {
    24. perror("write:");
    25. }
    26. printf(buff);
    27. close(fd);
    28. return 0;
    29. }

     测试结果:

    三.对比C库与系统调用

    我们真正理解语言层面的文件操作吗?其实我们并不理解,因为这不是语言问题,这是系统问题。

    是不是只有C/C++有文件操作呢?python,java,php,go ..... 他们都是有文件操作?他们的文件操作一样吗?他们都有文件操作,且根据语言的语法不同,文件操作也是不同的。有没有一种同意的视角,看待所有语言的文件操作呢?

    在认识返回值之前,先来认识一下两个概念: 系统调用 和 库函数:

     上面的 fopen fclose fread fwrite 都是C标准库当中的函数,我们称之为库函数(libc)。
    而, open close read write 都属于系统提供的接口,称之为系统调用接口回忆一下我们讲操作系统概念时,画的一张图:

    系统调用接口和库函数的关系,一目了然。
    所以,可以认为,f#系列的函数,都是对系统调用的封装,方便二次开发。

    只要语言层支持了文件操作,那么语言层对下必然封装了系统调用。

    四.如何管理文件

    1.操作系统如何管理文件 

    文件=内容+属性。

    当一个文件没有被操作时,文件一般会被放在磁盘上。

    当我们对一个文件进程操作的时候,文件需要被放进内存,因为冯诺依曼体系的限定!

    当我们对文件进程操作的时候,文件需要被load到内存,load的是属性还是内容?至少要有属性被load。

    当我们对文件进程操作的时候,文件需要被提前放进内存,操作文件的又不是我们一个,所以OS内部移动同时存在大量被打开的文件。那么操作系统如何管理这些被打开的文件呢?创建对应的结构体进行抽象,和数据机构进行组织。

    每一个被打开的文件,都要在OS内部对应文件对象的struct结构体,可以将所有的struct_file结构体用某种数据结构连接起来,在OS内部,对被打开的文件进行管理,就转换成对链表的增删查改。

    2.进程如何管理文件 ——文件描述符

     文件可以分为两大类,磁盘文件(没有被打开),内存文件(被打开)。

    文件被打开,是指文件被以进程为代表的用户让操作系统打开的。

    所以之前的文件操作,都是进程与被打开文件之间的关系。在OS的角度,就是PCB与struct_file的关系。

    那么进程是如何管理自己打开的文件的呢?

    open返回值:

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. int main()
    7. {
    8. // 打开一个文件
    9. int fd = open("testfile", O_WRONLY | O_CREAT, 0666);
    10. // 打印文件描述符
    11. printf("%d\n", fd);
    12. return 0;
    13. }

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

     这里为什么是3?我们多打开几个文件看看:

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. int main()
    8. {
    9. // 打开一个文件
    10. int fd = open("testfile", O_WRONLY | O_CREAT, 0666);
    11. int fd1 = open("testfile1", O_WRONLY | O_CREAT, 0666);
    12. int fd2 = open("testfile2", O_WRONLY | O_CREAT, 0666);
    13. int fd3 = open("testfile3", O_WRONLY | O_CREAT, 0666);
    14. // 打印文件描述符
    15. printf("%d\n", fd);
    16. printf("%d\n", fd1);
    17. printf("%d\n", fd2);
    18. printf("%d\n", fd3);
    19. return 0;
    20. }

    我们发现打印出的是连续的整数。但是没有还是从3开始的,那么会不会有0,1,2呢?

    0 & 1 & 2 :

    1. Linux进程默认情况下会有3个缺省打开的文件描述符,分别是标准输入0, 标准输出1, 标准错误2.
    2. 0,1,2对应的物理设备一般是:键盘,显示器,显示器

     所以输入输出还可以采用如下方式:

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. int main()
    8. {
    9. char buf[1024];
    10. // 0:标准输入的文件描述符——键盘文件
    11. ssize_t s = read(0, buf, sizeof(buf));
    12. if (s > 0)
    13. {
    14. buf[s] = 0;
    15. // 写入1号文件描述符的文件中——显示器文件
    16. // 写入2号文件描述符的文件中——显示器文件
    17. write(1, buf, strlen(buf));
    18. write(2, buf, strlen(buf));
    19. }
    20. return 0;
    21. }

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

    3.文件描述符的分配规则

     测试代码:

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. int main()
    8. {
    9. close(0);
    10. int fd = open("testfile", O_WRONLY | O_CREAT, 0666);
    11. close(2);
    12. int fd1 = open("testfile1", O_WRONLY | O_CREAT, 0666);
    13. int fd2 = open("testfile2", O_WRONLY | O_CREAT, 0666);
    14. // 打印文件描述符
    15. printf("%d\n", fd);
    16. printf("%d\n", fd1);
    17. printf("%d\n", fd2);
    18. return 0;
    19. }

    测试结果:

     说明:

    1. 当我们关闭0,2号文件描述符,0,2文件描述符空着,新打开的文件描述符不再从3开始。
    2. fd: 0 或者 fd 2 可见,文件描述符的分配规则:在files_struct数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符。

     三.重定向

    1.重定向原理

     上述代码如果我们关闭的是1号文件描述符:

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. int main()
    8. {
    9. close(0);
    10. int fd = open("testfile", O_WRONLY | O_CREAT, 0666);
    11. // 如果关闭1号文件描述符
    12. close(1);
    13. int fd1 = open("testfile1", O_WRONLY | O_CREAT, 0666);
    14. int fd2 = open("testfile2", O_WRONLY | O_CREAT, 0666);
    15. // 打印文件描述符
    16. printf("%d\n", fd);
    17. printf("%d\n", fd1);
    18. printf("%d\n", fd2);
    19. return 0;
    20. }

    测试结果:

    说明:

    1. 本应该输出到显示器的内容,却输出到了文件中。这种现象就叫做重定向。
    2. 常见的重定向有:>, >>, <,输出重定向,追加重定向,输入重定向。

    重定向的本质:

    说明:

    原本输入到显示器的数据输入到了其他文件,仅仅通过更改struct file*fdarray[ ]对应下标的存储的指针。

    2.dup2 系统调用

    1. #include
    2. int dup2(int oldfd, int newfd)

    说明:

    • oldfd:需要重定向的文件描述符。
    • newfd:被重定向的文件描述符。

     测试代码:

    1. #include
    2. #include
    3. #include
    4. int main()
    5. {
    6. int fd = open("./log", O_CREAT | O_RDWR, 0666);
    7. if (fd < 0)
    8. {
    9. perror("open");
    10. return 1;
    11. }
    12. close(1);
    13. // 将fd对应的文件,重定向到1号文件描述符
    14. dup2(fd, 1);
    15. for (;;)
    16. {
    17. char buf[1024] = {0};
    18. ssize_t read_size = read(0, buf, sizeof(buf) - 1);
    19. if (read_size < 0)
    20. {
    21. perror("read");
    22. break;
    23. }
    24. printf("%s", buf);
    25. fflush(stdout);
    26. }
    27. return 0;
    28. }

    测试结果:

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

     四.理解FILE

    因为IO相关函数与系统调用接口对应,并且库函数封装系统调用,所以本质上,访问文件都是通过fd访问的。所以C库当中的FILE结构体内部,必定封装了fd。

    测试代码:

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. int main()
    8. {
    9. int fd = open("testfile", O_CREAT | O_WRONLY, 0666);
    10. int fd1 = open("testfile1", O_CREAT | O_WRONLY, 0666);
    11. printf("%d\n", stdin->_fileno);
    12. printf("%d\n", stdout->_fileno);
    13. printf("%d\n", stderr->_fileno);
    14. printf("%d\n", fd);
    15. printf("%d\n", fd1);
    16. return 0;
    17. }

    测试结果:

    看一段代码:

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. int main()
    8. {
    9. const char *msg0 = "hello printf\n";
    10. const char *msg2 = "hello write\n";
    11. printf("%s", msg0);
    12. write(1, msg2, strlen(msg2));
    13. fork();
    14. return 0;
    15. }

    运行结果:

    看到这里一切正常,如果我们将输出到显示器的数据,重定向到其他文件中:

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

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

     综上:

    1. printf fwrite 等库函数会自带缓冲区,而 write 系统调用没有带缓冲区。
    2. 另外,我们这里所说的缓冲区,都是用户级缓冲区。其实为了提升整机性能,OS也会提供相关内核级缓冲区,不过不再我们讨论范围之内。
    3. 那这个缓冲区谁提供呢? printf fwrite 是库函数, write 是系统调用,库函数在系统调用的“上层”, 是对系统调用的“封装”,但是 write 没有缓冲区,而 printf fwrite 有,足以说明,该缓冲区是二次加上的,又因为是C,所以由C标准库提供。
  • 相关阅读:
    对于程序员来说,怎样才算是在写有“技术含量”的代码?
    Neo-reGeorg明文流量
    11-15 周三 softmax 回归学习
    Java的反射
    36、Java——一个案例学会三层架构对数据表的增删改查
    操作系统真相还原_第1~2章:环境配置
    这些Java基础知识,诸佬们都还记得嘛(学习,复习,面试都可)
    Java8 Stream流
    基于Python大数据的特定疾病的回归和分类
    [软考中级]软件设计师-计算机网络
  • 原文地址:https://blog.csdn.net/qq_63943454/article/details/132995310