• Linux —— 文件操作


    目录

    1.内核提供的文件系统调用

    1.1open和close

    1.2标记位

    1.3write和read

    2.文件描述

    2.1文件描述符

     2.2文件描述符分配规则

    3.重定向

    3.1最“挫”的重定向

    3.2使用系统调用

    3.3重定向原理

    3.4让我们的"shell"支持重定向操作

    4.一切皆文件

    5.缓冲区

    5.1缓冲区的本质

    5.2缓冲区的刷新策略

    5.3缓冲区的位置

    5.4缓冲区与写时拷贝

    5.5模拟实现"缓冲区"

    1.内核提供的文件系统调用

    1.1open和close

    通过[man]指令浏览其描述,这里截取片段。 

    第一个open是文件存在的情况下打开文件,第一个参数为文件名,若不指定文件路径,则默认为父进程的工作路径。第二个参数为标记位,int类型的每个比特位的0 和 1代表了不同的标记。Linux提供了多种标记。

    第二个open是文件不存在的情况下打开文件,第三个参数为文件创建的初始权限。

    close即关闭文件。

     下面给出代码实例以供参考:

    1. umask(0); //将umask置0
    2. int fd = open("log.txt",O_WRONLY | O_CREAT | O_TRUNC,0666); //相当于C语言的fopen的"w",只写、自动创建、自动覆盖
    3. //文件的权限受umask的影响
    4. close(fd);

    下面列举一些常用的标记:

    • O_WRONLY        ->只写
    • O_CREAT           ->创建
    • O_TRUNC          ->覆盖
    • O_RDONLY        ->只读
    • O_APPEND        ->追加

    需要多个标记组合在一起时,使用 '|'(按位或运算符) 连接即可。

    1.2标记位

    int类型有4个字节,32个比特位,每一个比特位的0 和 1代表了不同的标记。例如有:......0001与......0010就是两个不同的标记。我们使用代码实例来演示:

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. #define ONE (1<<0)
    8. #define TOW (1<<1)
    9. #define THREE (1<<2)
    10. #define FOUR (1<<3)
    11. void select(int flags)
    12. {
    13. if(flags & ONE) printf("one\n");
    14. if(flags & TOW) printf("tow\n");
    15. if(flags & THREE) printf("three\n");
    16. if(flags & FOUR) printf("four\n");
    17. }
    18. int main()
    19. {
    20. select(ONE);
    21. select(ONE | TOW);
    22. select(ONE | TOW | THREE);
    23. select(ONE | TOW | THREE | FOUR);
    24. return 0;
    25. }

     也就是说,Linux提供的标记实质上是一个宏,每一个宏代表了不同的信号。

    1.3write和read

    write是一个系统调用,其声明为([man]指令查看):

    需要注意的是,write()是从文件的起始位置开始写的,如果在open()中没有O_TRUNC或者O_APPEND标记,那么为将文件以前的内容的一部分覆盖掉。也就是说,要想像C语言一样自动清空文件的内的数据必须加上O_TRUNC标记 。

    下面给出write的实际使用案例以供参考:

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. int main()
    8. {
    9. umask(0); //将umask置0
    10. int fd = open("log.txt",O_WRONLY | O_CREAT | O_TRUNC,0666); //相当于C语言的fopen的"w",只写、自动创建、自动覆盖
    11. if(fd < 0) return 1;
    12. char* msg = "hello Linux\n";
    13. write(fd,msg,strlen(msg)); //像fd描述的文件写入msg指向的字符串
    14. close(fd);
    15. return 0;
    16. }

    编译运行,查看生成的"log.txt"文件:

    read也是一个系统调用,其声明为([man]指令查看):

    这里以上面write生成的文件给出read的实际使用案例以供参考:

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. int main()
    8. {
    9. int fd = open("log.txt",O_RDONLY);
    10. char buffer[64];
    11. read(fd,buffer,sizeof(buffer)-1); //留一个位置补'\0'
    12. buffer[strlen(buffer)]=0; //文件的字符串不以'\0'结尾
    13. printf("%s",buffer);
    14. close(fd);
    15. return 0;
    16. }

    2.文件描述

    2.1文件描述符

    进程可以打开多个文件,所以操作系统会有大量的文件。操作系统为了管理被打开的文件,使用了"先描述,再组织"的方法将被打开的文件描述为一个struct file的内核结构体,其包含了文件的大部分属性,多个内核结构体之前以特定的数据结构组织起来。

    这些struct file结构体并不是文件描述符,而是操作系统为了管理被打开的文件而创建的。事实上,文件操作研究的是进程和被打开文件的关系,也就是说文件是被进程打开的。那么进程和struct file结构体中间还有一层结构体,名为struct files_struct(文件描述符表),在task_struct中有一个struct files_struct* files指针指向文件描述符表。文件描述表有一个专门用来存储struct file结构体的地址的指针数组(struct file* fd_array[]),这个数组的下标即为文件描述符

    在进程(C语言程序)被加载到内存时,会默认生成打开三个文件(这些文件是C语言使用的,系统是是使用文件描述符的),即:

    • stdin    ->标准输入
    • stdout    ->标准输出
    • stderr    ->标准错误

    这三个文件的struct file结构体的地址依次按顺序存储在struct file* fd_array数组对应的下标0、1、2位置。所以我们用户的进程第一次创建的文件的描述符为3。

    下面给出进程与文件的假象模型图:

     2.2文件描述符分配规则

     在struct file* fd_array[]数组中,按下标从小到大的顺序,寻找最小、且没有被占用下标作为文件描述符。

    下面给出一段代码供加深理解:

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. int main()
    8. {
    9. close(0);
    10. close(2); //关闭掉标准输入和标准错误文件,即清空数组的占用
    11. int fd = open("log.txt",O_RDONLY);
    12. printf("%d\n",fd);
    13. close(fd);
    14. return 0;
    15. }

    3.重定向

    3.1最“挫”的重定向

    观察下面这段代码以及现象:

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. int main()
    8. {
    9. close(1); //将fd=1的数组位置清空
    10. int fd = open("log.txt",O_WRONLY | O_CREAT | O_TRUNC,0666); //文件打开时就占用fd=1的位置
    11. printf("hello world\n");
    12. fflush(stdout);
    13. close(fd);
    14. return 0;
    15. }

    以上是最原始的重定向操作。其原因在于:printf()函数是默认向fd=1对应的文件(标准输出)输出的,但是进行close(1)操作后,fd=1位置的内容就清空了,随后log.txt文件的struct file结构体地址占用了fd=1的位置,所以printf就向log.txt文件输出了。 

    3.2使用系统调用

    dup类接口是系统提供给我们的接口,其中dup2最为常用。我们通过[man]指令查询:

    下面给出实际使用案例以供参考:

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. int main()
    8. {
    9. int fd = open("log.txt",O_WRONLY | O_CREAT | O_TRUNC,0666);
    10. dup2(fd,1); //重定向
    11. printf("hello world");
    12. fflush(stdout);
    13. close(fd);
    14. return 0;
    15. }

    3.3重定向原理

    上层看到的文件描述符是不会变的,例如printf规定了向标准输出输出,即向fd=1对应的文件输出,那么printf找的是文件描述符而不是对应的文件,所以fd的内容无论怎么变,上层找的还是fd。

    子进程重定向不会影响父进程,因为进程之间相互独立。即子在进程在创建出来的时候,就拷贝了一份文件描述符表。但是文件不属于进程,是不会拷贝的。

    3.4让我们的"shell"支持重定向操作

    在上次模拟实现命令行解释器的基础上,再进行升级,以支持重定向操作。

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. #include
    8. #include
    9. #include
    10. #include
    11. #define NUM 1024
    12. #define NON_REDIR 0 //无重定向
    13. #define INPUT_REDIR 1 //输入重定向 '<'
    14. #define OUT_REDIR 2 //输出重定向 '>'
    15. #define APPEND_REDIR 3 //追加重定向 '>>'
    16. char command[NUM]; //c99数组
    17. char* myargv[64]; //存储指令参数
    18. char* file_name=NULL;
    19. int redir_type=0;
    20. void command_check(char* command)
    21. {
    22. assert(command);
    23. //首、尾指针
    24. char* begin = command;
    25. char* end = command+strlen(command);
    26. while(begin < end)
    27. {
    28. if(*begin == '>')
    29. {
    30. *begin=0;
    31. begin++;
    32. if(*begin == '>')
    33. {
    34. redir_type=APPEND_REDIR;
    35. begin++;
    36. }
    37. else redir_type=OUT_REDIR;
    38. while(*begin == ' ') begin++;
    39. file_name=begin;
    40. }
    41. else if(*begin == '<')
    42. {
    43. *begin=0; //置0
    44. redir_type=INPUT_REDIR;
    45. begin++;
    46. while(*begin == ' ') begin++;
    47. file_name = begin;
    48. }
    49. else ++begin;
    50. }
    51. }
    52. int main()
    53. {
    54. while(1)
    55. {
    56. redir_type=NON_REDIR;
    57. file_name=NULL;
    58. char buffer[1024]={0};
    59. getcwd(buffer,sizeof(buffer)-1); //获取shell的工作路径
    60. buffer[strlen(buffer)]=0;
    61. printf("[用户名@主机名 %s]",buffer);
    62. fflush(stdout); //刷新缓冲区
    63. char* s = fgets(command,sizeof(command),stdin); //输入指令
    64. command[strlen(command)-1]=0; //清除 \n
    65. command_check(command); //检查指令是否有重定向操作
    66. myargv[0] = strtok(command," ");
    67. int i = 1;
    68. while(myargv[i++] = strtok(NULL," ")); //切割空格
    69. if(myargv[0] != NULL && strcmp(myargv[0],"cd") == 0)
    70. {
    71. if(myargv[1] != NULL) chdir(myargv[1]); //cd命令移动shell的工作路径
    72. continue;
    73. }
    74. pid_t id = fork();
    75. if(id == 0)
    76. {
    77. switch(redir_type) //使用switch语句
    78. {
    79. case NON_REDIR: //不作处理
    80. break;
    81. case INPUT_REDIR: //输入重定向
    82. {
    83. int fd = open(file_name,O_RDONLY);
    84. if(fd < 0)
    85. {
    86. perror("open:");
    87. exit(1);
    88. }
    89. dup2(fd,0);
    90. }
    91. break;
    92. case OUT_REDIR:
    93. case APPEND_REDIR:
    94. {
    95. umask(0);
    96. int flag = O_WRONLY | O_CREAT;
    97. if(redir_type == APPEND_REDIR) flag |= O_APPEND;
    98. else flag |= O_TRUNC;
    99. int fd = open(file_name,flag,0666);
    100. if(fd < 0)
    101. {
    102. perror("open:");
    103. exit(1);
    104. }
    105. dup2(fd,1);
    106. }
    107. break;
    108. default:
    109. break;
    110. }
    111. execvp(myargv[0],myargv); //进程替换
    112. exit(1);
    113. }
    114. waitpid(id,NULL,0);
    115. }
    116. return 0;
    117. }

    文件描述符表是属于进程的并且是一个内核数据结构,进程替换是不会影响它的,进程替换只是替换数据段和代码段,是不影响内核数据结构的。

    对于文件的关闭,在进程退出时自动关闭。其本质还是在于struct file里面的计数器,这个计数器记录有多少个进程正在引用这个struct file,当进程退出时,计数器就会减1,当计数器为0才文件才销毁。

    4.一切皆文件

    不止是磁盘上的可执行文件被打开才是文件,硬件也是文件。键盘、鼠标、显示器、内存、硬盘等等都是文件。

    操作系统能够使用驱动来管理硬件,那么在驱动上,就一定有硬件与操作系统的IO交互方法。那么在内核中,都有唯一的一份struct file内核结构体对应硬件,以便操作系统管理。所以在Linux的视角来看,一切皆文件。

    那么在上层想要与硬件互动时,也是通过struct file结构体实现的,其原因在于此结构体有硬件与操作系统IO交互方法的函数指针,通过这些函数指针去调用不同的交互方式。

    发现了吗?即使磁盘上的各种可执行文件或者是硬件非常杂乱,但是在操作系统下总是能有序的抽象化成一个struct file结构体来进行管理。也就是说,我们通过统一的struct file结构体(其中描述了文件的共有属性)来操作不同的文件。用官方的话说(参考Linux内核设计与实现原理):我们可以直接使用open()、read()和write()这样的系统调用而无需考虑具体文件系统和实际物理介质。这样的行为就构成了内核的子系统——虚拟文件系统(VFS)。

    5.缓冲区

    5.1缓冲区的本质

    缓冲区就是内存上的一段,专门用来做缓存工作(将进程的数据拷贝到缓冲区)。

    需要缓冲区的原因在于:将数据直接写入外设的效率是非常低的(外设的处理速度相对于cpu的处理速度慢了几千几万倍),当大量进程和大量数据时,进程更多的时间花费在与外设的交互上。

    将数据先交给缓冲区,与外设的交互由缓冲区去完成,那么进程就不再被外设“耽搁”了。那么节省进程进行IO的时间就是缓冲区的本质。

    5.2缓冲区的刷新策略

    缓冲区的刷新有三个策略和两个例外。

    三个策略:

    • 无缓冲:当数据写入时,立即刷新缓冲区。
    • 行缓冲:数据每写入一行,缓冲区刷新一次。
    • 全缓冲:数据一直写入直至缓冲区满,此时再刷新。

    两个例外:

    • 用户强制刷新(例如fflush)就会立即刷新缓冲区。
    • 进程退出时,一般都要进程缓冲区的刷新。

    5.3缓冲区的位置

    调用C语言库函数exit时会刷新缓冲区;调用系统调用_exit时不会刷新缓冲区(上一篇博客讲到的)。可以证明,缓冲区的位置不在内核当中。

    在使用C语言编程时,缓冲区的位置在FILE结构体中(由C语言提供)。FILE结构体里面封装了缓冲区字段、文件描述符字段等等,由此可见,在上层使用C语言编程时,调用的都是底层的接口。也因此可以得出一个结论:使用系统调用与外设进行IO交互数据不会写入缓冲区(因为系统调用可以直接使用操作系统提供的fd,而不需要使用C语言的FILE结构体)。

    5.4缓冲区与写时拷贝

    给出一段代码,观察向不同的外设写入数据时产生的不同效果:

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. int main()
    8. {
    9. const char* msg_write="write\n";
    10. const char* msg_fprintf="fprintf\n";
    11. const char* msg_fwrite="fwrite\n";
    12. printf("printf\n");
    13. fwrite(msg_fwrite,sizeof(char),strlen(msg_fwrite),stdout);
    14. fprintf(stdout,"%s",msg_fprintf);
    15. fputs("fputs\n",stdout);
    16. write(1,msg_write,strlen(msg_write)); //这五个函数都是向标准输出输出
    17. fork(); //创建一个子进程
    18. return 0;
    19. }

    其原因在于:正常运行程序时,是向标准输出输出的,缓冲区采用的是行刷新策略(write除外),所以当创建子进程之前,父进程的缓冲区数据已经被刷到标准输出了,创建子进程后,子进程的缓冲区与父进程共享,即子进程的缓冲区没有数据,进程退出时即使刷新缓冲区也不会触发写时拷贝。 

    当输出重定向到文件时,缓冲区采用的是全缓冲策略(write除外),所以创建子进程后父进程的缓冲区依然有数据(wtire的数据不经过缓冲区直接输出到了文件上),子进程共享父进程的缓冲区,而这两个进程任意一个进程退出时,会触发缓冲区的刷新,此时就会触发写时拷贝,也就看到了上面的现象。

    5.5模拟实现“缓冲区”

    为了加深理解,这里展示一个超级、无敌、极限、超纯净阉割版的缓冲区模拟实现。

    1. //头文件
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. #include
    8. #include
    9. #include
    10. #define SIZE 1024
    11. #define ALL 0 //全缓冲策略
    12. #define LINE 1 //行刷新策略(暂时只实现一个吧)
    13. typedef struct _FILE
    14. {
    15. int flags; //缓冲区刷新策略
    16. int fileno; //文件描述符
    17. char cush[SIZE]; //缓冲区
    18. int size; //有效数据
    19. int cap; //最大容量
    20. }_FILE;
    21. _FILE* _fopen(const char* filename,const char* mode); //模拟fopen
    22. void _fclose(_FILE* fp); //模拟fclose
    23. void _fflush(_FILE* fp); //模拟fflush
    24. void _fwrite(const char* msg,int size,int len,_FILE* fp); //模拟fwrite
    1. //源文件
    2. #include "_stdio.h"
    3. _FILE* _fopen(const char* filename,const char* mode) //模拟fopen
    4. {
    5. int flags=0;
    6. if(strcmp(mode,"w") == 0)
    7. {
    8. flags |= (O_WRONLY | O_CREAT | O_TRUNC);
    9. }
    10. else if(strcmp(mode,"r") == 0)
    11. {
    12. flags |= O_RDONLY;
    13. }
    14. else if(strcmp(mode,"a") == 0)
    15. {
    16. flags |= (O_WRONLY | O_CREAT | O_APPEND);
    17. }
    18. //上面在确认文件打开方式
    19. int fd=0;
    20. if(flags & O_RDONLY) fd=open(filename,flags);
    21. else fd=open(filename,flags,0666);
    22. if(fd < 0)
    23. {
    24. const char* msg=strerror(errno);
    25. write(1,msg,strlen(msg));
    26. return NULL; //创建文件失败
    27. }
    28. //上面在创建文件
    29. _FILE* fp = (_FILE*)malloc(sizeof(_FILE));
    30. fp->flags=LINE; //默认行刷新
    31. fp->fileno=fd;
    32. fp->size=0;
    33. fp->cap=SIZE;
    34. memset(fp->cush,0,SIZE);
    35. //上面在初始化_FILE结构体
    36. return fp;
    37. }
    38. void _fclose(_FILE* fp) //模拟fclose
    39. {
    40. _fflush(fp);
    41. close(fp->fileno);
    42. }
    43. void _fflush(_FILE* fp) //模拟fflush
    44. {
    45. if(fp->size > 0)
    46. {
    47. write(fp->fileno,fp->cush,fp->size);
    48. fp->size=0;
    49. }
    50. }
    51. void _fwrite(const char* msg,int size,int len,_FILE* fp) //模拟fwrite
    52. {
    53. //size没什么乱用,对齐fwrite而已
    54. memcpy(fp->cush+fp->size,msg,len);
    55. fp->size+=len;
    56. //行刷新
    57. if(fp->cush[fp->size-1] == '\n')
    58. {
    59. write(fp->fileno,fp->cush,fp->size);
    60. fp->size=0;
    61. }
    62. else if(fp->size == fp->cap)
    63. {
    64. write(fp->fileno,fp->cush,fp->size);
    65. fp->size=0;
    66. }
    67. }
    1. //主函数
    2. #include "_stdio.h"
    3. #include
    4. int main()
    5. {
    6. _FILE* fp = _fopen("log.txt","w");
    7. const char* msg="hello Linux\n";
    8. int cnt = 10;
    9. while(cnt)
    10. {
    11. _fwrite(msg,sizeof(char),strlen(msg),fp);
    12. printf("count:%d\n",cnt--);
    13. sleep(1);
    14. }
    15. _fclose(fp);
    16. return 0;
    17. }

     读者自行将上面的代码copy走,将主函数的msg字符串的'\n'删除掉或保留,在Linux终端下观察文件的数据情况。

     

  • 相关阅读:
    IDEA 好用的插件
    【Opencv】OpenCV使用CMake和MinGW的编译安装出错解决
    Robinhood基于Apache Hudi的下一代数据湖实践
    Java 并发之 AQS 详解(下)
    C/C++,不废话的宏使用技巧
    分享20个最实用的 .NET 开源项目
    HTML+CSS个人电影网页设计——电影从你的全世界路过(4页)带音乐特效
    快速高效!用Python批量分割PDF文件,让你的工作更轻松...
    [附源码]计算机毕业设计基于Springboot学生社团信息管理系统
    ubuntu20环境搭建+Qt6安装
  • 原文地址:https://blog.csdn.net/weixin_59913110/article/details/128077444