• 猿创征文| Linux——基础I/O3| 缓冲区|自己设计缓冲区实现文件操作|minishell重定向


    目录

     缓冲区

    自己设计缓冲区实现文件操作

     minishell重定向


     缓冲区

    缓冲区是一段内存空间,缓冲区可以提高整机效率,缓冲区刷新策略:

    1.立即刷新

    2.行刷新(行缓冲\n),会把\n之前的刷新出去

    3.满刷新(缓冲区满了全刷新出去)

    特殊情况: 1.用户强制刷新,如fflush

                       2.进程退出

    谁提供缓冲区,谁维护。

    采用行缓冲的设备文件——显示器 全缓冲设备文件——磁盘文件

    所有的设备永远都倾向于全缓冲。缓冲区满了才刷新,需要更少次的I/O操作,也就是更少此外设的访问。

    其他刷新策略是结合具体情况做妥协,既要考虑效率,也要考虑用户体验

    正常情况下打印4条消息

     

     清空log.txt,然后重定向

     注释掉fork

     清空之后重定向,log.txt,打印了四条语句,说明这种情况一定跟fork有关

     上面打印了7次我们发现,hellowrite只打印了一次,调用的是write。也就是打俩次的是C语言接口,打一次的是操作系统的接口

    同样一个文件向显示器打印4行,向文件打印7行,C接口打印俩次,系统接口打印一次。

    fork之前的函数已经执行完了,但不代表已经刷新了。

    打印俩次,是因为缓冲区由C语言的标准库维护。如果是操作系统提供的,上面的现象应该一样。

    将数据写到C标准库缓冲区,之后调用write接口,把数据刷新到操作系统里,像fputs这种函数,只不过是把数据写到了C标准库的缓冲区当中。

    进程也可直接调用write接口

     如果向显示器打印,刷新策略是行刷新,那么最后执行fork的时候,一定是函数执行完了,数据已经被刷新了。此时fork无意义

    如果重定向,往磁盘文件打印,刷新策略就变成了全缓冲,\n此时也没意义,fork执行完后return 0;fork的时候函数是执行完了,但是数据还没刷新。数据在当前进程对应的C标准库的缓冲区中,这部分数据是父进程的数据,return 0;会强制刷新缓冲区,刷新是一个写的过程,在return 0的时刻会发生写时拷贝,刷新是写的过程,父子进程要保证独立性,发生了写实拷贝。

     

    此时又变成了四条。 

    fflush是C语言提供的接口,这是因为在fork之前强制刷新了缓冲区,刷新的时候给fflush传入stdout就能刷新,stdout跟缓冲区好像没关系,fflush中传的参数是FILE*类型,因为在C语言中,打开一个文件返回值是FILE*,FILE结构体除了封装了fd之外,还封装了缓冲区结构。

     C语言中打开的FILE一般叫文件流。

    自己设计缓冲区实现文件操作

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. #include
    8. #include
    9. #define NUM 1024
    10. struct MyFILE_{
    11. int fd;
    12. char buffer[1024];
    13. int end; //当前缓冲区的结尾
    14. };
    15. typedef struct MyFILE_ MyFILE;
    16. MyFILE *fopen_(const char *pathname, const char *mode)
    17. {
    18. assert(pathname);
    19. assert(mode);
    20. MyFILE *fp = NULL;
    21. if(strcmp(mode, "r") == 0)
    22. {
    23. }
    24. else if(strcmp(mode, "r+") == 0)
    25. {
    26. }
    27. else if(strcmp(mode, "w") == 0)
    28. {
    29. int fd = open(pathname, O_WRONLY | O_TRUNC | O_CREAT, 0666);
    30. if(fd >= 0)
    31. {
    32. fp = (MyFILE*)malloc(sizeof(MyFILE));
    33. memset(fp, 0, sizeof(MyFILE));
    34. fp->fd = fd;
    35. }
    36. }
    37. else if(strcmp(mode, "w+") == 0)
    38. {
    39. }
    40. else if(strcmp(mode, "a") == 0)
    41. {
    42. }
    43. else if(strcmp(mode, "a+") == 0)
    44. {
    45. }
    46. else{
    47. //什么都不做
    48. }
    49. return fp;
    50. }
    51. //是不是应该是C标准库中的实现!
    52. void fputs_(const char *message, MyFILE *fp)
    53. {
    54. assert(message);
    55. assert(fp);
    56. strcpy(fp->buffer+fp->end, message); //abcde\0
    57. fp->end += strlen(message);
    58. //for debug
    59. printf("%s\n", fp->buffer);
    60. //暂时没有刷新, 刷新策略是谁来执行的呢?用户通过执行C标准库中的代码逻辑,来完成刷新动作
    61. //这里效率提高,体现在哪里呢??因为C提供了缓冲区,那么我们就通过策略,减少了IO的执行次数(不是数据量)
    62. if(fp->fd == 0)
    63. {
    64. //标准输入
    65. }
    66. else if(fp->fd == 1)
    67. {
    68. //标准输出
    69. if(fp->buffer[fp->end-1] =='\n' )
    70. {
    71. //fprintf(stderr, "fflush: %s", fp->buffer); //2
    72. write(fp->fd, fp->buffer, fp->end);
    73. fp->end = 0;
    74. }
    75. }
    76. else if(fp->fd == 2)
    77. {
    78. //标准错误
    79. }
    80. else
    81. {
    82. //其他文件
    83. }
    84. }
    85. void fflush_(MyFILE *fp)
    86. {
    87. assert(fp);
    88. if(fp->end != 0)
    89. {
    90. //暂且认为刷新了--其实是把数据写到了内核
    91. write(fp->fd, fp->buffer, fp->end);
    92. syncfs(fp->fd); //将数据写入到磁盘
    93. fp->end = 0;
    94. }
    95. }
    96. void fclose_(MyFILE *fp)
    97. {
    98. assert(fp);
    99. fflush_(fp);
    100. close(fp->fd);
    101. free(fp);
    102. }
    103. int main()
    104. {
    105. //close(1);
    106. MyFILE *fp = fopen_("./log.txt", "w");
    107. if(fp == NULL)
    108. {
    109. printf("open file error");
    110. return 1;
    111. }
    112. fputs_("one: hello world", fp);
    113. fork();
    114. fclose_(fp);
    115. }

     minishell重定向

    当输入"ls -a -l > log.txt"

    我们转换为"ls -a -l \0 log.txt"

    定义四个宏代表当前重定向的状态

     进行程序替换的时候不会影响一个文件曾经打开的文件描述符,程序替换只会影响页表,只会影响代码和数据,不会对进程曾经打开过的文件有任何影响

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. #include
    8. #include
    9. #include
    10. #define NUM 1024
    11. #define SIZE 32
    12. #define SEP " "
    13. //保存完整的命令行字符串
    14. char cmd_line[NUM];
    15. //保存打散之后的命令行字符串
    16. char *g_argv[SIZE];
    17. // 写一个环境变量的buffer,用来测试
    18. char g_myval[64];
    19. #define INPUT_REDIR 1
    20. #define OUTPUT_REDIR 2
    21. #define APPEND_REDIR 3
    22. #define NONE_REDIR 0
    23. int redir_status = NONE_REDIR;
    24. char *CheckRedir(char *start)
    25. {
    26. assert(start);
    27. char *end = start + strlen(start) - 1; //ls -a -l\0
    28. while(end >= start)
    29. {
    30. if(*end == '>')
    31. {
    32. if(*(end-1) == '>')
    33. {
    34. redir_status = APPEND_REDIR;
    35. *(end-1) = '\0';
    36. end++;
    37. break;
    38. }
    39. redir_status = OUTPUT_REDIR;
    40. *end = '\0';
    41. end++;
    42. break;
    43. //ls -a -l>myfile.txt
    44. //ls -a -l>>myfile.txt
    45. }
    46. else if(*end == '<')
    47. {
    48. //cat < myfile.txt,输入
    49. redir_status = INPUT_REDIR;
    50. *end = '\0';
    51. end++;
    52. break;
    53. }
    54. else{
    55. end--;
    56. }
    57. }
    58. if(end >= start)
    59. {
    60. return end; //要打开的文件
    61. }
    62. else{
    63. return NULL;
    64. }
    65. }
    66. // shell 运行原理 : 通过让子进程执行命令,父进程等待&&解析命令
    67. int main()
    68. {
    69. extern char**environ;
    70. //0. 命令行解释器,一定是一个常驻内存的进程,不退出
    71. while(1)
    72. {
    73. //1. 打印出提示信息 [whb@localhost myshell]#
    74. printf("[root@我的主机 myshell]# ");
    75. fflush(stdout);
    76. memset(cmd_line, '\0', sizeof cmd_line);
    77. //2. 获取用户的键盘输入[输入的是各种指令和选项: "ls -a -l -i"]
    78. // "ls -a -l>log.txt"
    79. // "ls -a -l>>log.txt"
    80. // "ls -a -l
    81. if(fgets(cmd_line, sizeof cmd_line, stdin) == NULL)
    82. {
    83. continue;
    84. }
    85. cmd_line[strlen(cmd_line)-1] = '\0';
    86. // 2.1: 分析是否有重定向, "ls -a -l>log.txt" -> "ls -a -l\0log.txt"
    87. //"ls -a -l -i\n\0"
    88. char *sep = CheckRedir(cmd_line);
    89. //printf("echo: %s\n", cmd_line);
    90. //3. 命令行字符串解析:"ls -a -l -i" -> "ls" "-a" "-i"
    91. // export myval=105
    92. g_argv[0] = strtok(cmd_line, SEP); //第一次调用,要传入原始字符串
    93. int index = 1;
    94. if(strcmp(g_argv[0], "ls") == 0)
    95. {
    96. g_argv[index++] = "--color=auto";
    97. }
    98. if(strcmp(g_argv[0], "ll") == 0)
    99. {
    100. g_argv[0] = "ls";
    101. g_argv[index++] = "-l";
    102. g_argv[index++] = "--color=auto";
    103. }
    104. //?
    105. while(g_argv[index++] = strtok(NULL, SEP)); //第二次,如果还要解析原始字符串,传入NULL
    106. if(strcmp(g_argv[0], "export") == 0 && g_argv[1] != NULL)
    107. {
    108. strcpy(g_myval, g_argv[1]);
    109. int ret = putenv(g_myval);
    110. if(ret == 0) printf("%s export success\n", g_argv[1]);
    111. //for(int i = 0; environ[i]; i++)
    112. // printf("%d: %s\n", i, environ[i]);
    113. continue;
    114. }
    115. //for debug
    116. //for(index = 0; g_argv[index]; index++)
    117. // printf("g_argv[%d]: %s\n", index, g_argv[index]);
    118. //4.内置命令, 让父进程(shell)自己执行的命令,我们叫做内置命令,内建命令
    119. //内建命令本质其实就是shell中的一个函数调用
    120. if(strcmp(g_argv[0], "cd") == 0) //not child execute, father execute
    121. {
    122. if(g_argv[1] != NULL) chdir(g_argv[1]); //cd path, cd ..
    123. continue;
    124. }
    125. //5. fork()
    126. pid_t id = fork();
    127. if(id == 0) //child
    128. {
    129. if(sep != NULL)
    130. {
    131. int fd = -1;
    132. //说明命令曾经有重定向
    133. switch(redir_status)
    134. {
    135. case INPUT_REDIR:
    136. fd = open(sep, O_RDONLY);
    137. dup2(fd, 0);
    138. break;
    139. case OUTPUT_REDIR:
    140. fd = open(sep, O_WRONLY | O_TRUNC | O_CREAT, 0666);
    141. dup2(fd, 1);
    142. break;
    143. case APPEND_REDIR:
    144. //TODO
    145. fd = open(sep, O_WRONLY | O_APPEND | O_CREAT, 0666);
    146. dup2(fd, 1);
    147. break;
    148. default:
    149. printf("bug?\n");
    150. break;
    151. }
    152. }
    153. // printf("下面功能让子进程进行的\n");
    154. // printf("child, MYVAL: %s\n", getenv("MYVAL"));
    155. // printf("child, PATH: %s\n", getenv("PATH"));
    156. //cd cmd , current child path
    157. //execvpe(g_argv[0], g_argv, environ); // ls -a -l -i
    158. //不是说好的程序替换会替换代码和数据吗??
    159. //环境变量相关的数据,会被替换吗??没有!
    160. execvp(g_argv[0], g_argv); // ls -a -l -i
    161. exit(1);
    162. }
    163. //father
    164. int status = 0;
    165. pid_t ret = waitpid(id, &status, 0);
    166. if(ret > 0) printf("exit code: %d\n", WEXITSTATUS(status));
    167. }
    168. }

  • 相关阅读:
    mybatis中的动态sql
    这么讲不怕你不懂负载均衡
    当年的java考试:Java景区预约登记管理系统(maven整合servlet)
    常见网络编程面试题以及答案(网络面试30题)
    基于Java的Cplex入门
    Leetcode 2876. Count Visited Nodes in a Directed Graph
    消息队列一|从秒杀活动开始聊起消息队列
    利用元胞自动机-人工神经网络模型预测城市未来土地利用
    java-net-php-python-SSM城市管理综合执法系统计算机毕业设计程序
    【简单讲解下epoll】
  • 原文地址:https://blog.csdn.net/weixin_49449676/article/details/127419858