4.2w字,详细的带你认识基础I/OLinux--基础IO

Posted includeevey

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了4.2w字,详细的带你认识基础I/OLinux--基础IO相关的知识,希望对你有一定的参考价值。

前言  

        相信大家最开始都挺疑惑的,什么I/O。在计算机操作系统中,所谓的I/O实则就是输入(Input)和输出(Output),也可以理解为读(Read)和写(Write),那么这里基础的意思是我们需要基本掌握的I/O的知识。这篇文章主要讲解的是针对于磁盘I/O,我们会深入探究用户对磁盘进行读写操作的过程,也将涉及到的基本知识进行讲解。

首先对于I/O,我们在学C语言文件章节的时候就学过C文件IO相关操作,当初就是简单用c语言对应的接口,对文件进行读写操作。任何语言对文件操作的接口都是基于系统调用接口之上的,之后我们就会学习文件相关系统调用接口,后续内容是:文件描述符 ,理解重定向对比fd和FILE ,理解系统调用和库函数的关系理解文件系统中inode的概念,认识软硬链接,对比区别认识动态静态库,最后学会结合gcc选项 ,制作动静态库。


目录

前言  

一、重顾C文件接口

1.1不变的接口

1.2C语言文件操作

二、系统文件I/O

2.1用比特位传递选项

2.2接口介绍

2.3文件描述符fd(重)

2.4文件描述符的分配规则

2.5重定向

2.6使用 dup2 系统调用

 2.7再谈myshell

三、FILE-缓冲区

3.1抛出问题

3.2认识缓冲区

3.3缓冲区刷新策略

3.4解决缓冲区问题(重)

3.4myStdio-dome

四、文件系统

4.1磁盘的物理结构

4.2磁盘的存储结构 

4.3磁盘的逻辑结构

4.4理解文件系统(重)

五、软硬链接(重)

5.1软硬链接的创建

5.2软硬链接的区别

5.3软硬链接的应用 

六、动态库和静态库 

6.1动态库和静态库的理解

6.2静态库和静态链接的生成

6.3生成动态库

6.3使用外部库--ncurses库

6.4动态库的加载



一、重顾C文件接口

1.1不变的接口

我们知道C语言有文件操作接口,那么当然C++,JAVA这些语言都有文件操作接口。这些语言拥有文件操作接口的目的找到文件,然后对文件进行操作。那么文件是在磁盘上,磁盘是属于硬件。对于硬件的访问只有操作系统才是进行。所有人想访问磁盘都不能绕开操作系统,C语言也好,其他语言也罢都是人表达出意思让操作系统理解我们想要干嘛,所以任何上层语言想要进行对磁盘进行操作,都会使用操作系统提供的接口

1.2C语言文件操作

下面我们通过代码进行回顾c文件接口

写文件

size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream)

#include <stdio.h>    
#include <string.h>    
    
    
int main()    
    
    FILE* fp=fopen("myfile","w");    
    if(!fp)    
        
        perror("open fail");    
        return (1);    
        
    
    const char *msg="hello world! \\n";    
    int cnt=5;    
    while(cnt--)    
        
        fwrite(msg,strlen(msg),1,fp);    
               
                                                                                       
                                                             
    fclose(fp);                                                                                                                                                                            
                       
    return 0;                                         
  

显示结果:

[hongxin@VM-8-2-centos 12-31-1]$ gcc -o test test.c
[hongxin@VM-8-2-centos 12-31-1]$ ./test
[hongxin@VM-8-2-centos 12-31-1]$ ll
total 24
-rw-rw-r-- 1 hongxin hongxin   73 Dec 31 23:26 makefile
-rw-rw-r-- 1 hongxin hongxin   70 Dec 31 23:27 myfile
-rwxrwxr-x 1 hongxin hongxin 8568 Dec 31 23:27 test
-rw-rw-r-- 1 hongxin hongxin  711 Dec 31 23:27 test.c
[hongxin@VM-8-2-centos 12-31-1]$ cat myfile 
hello world! 
hello world! 
hello world! 
hello world! 
hello world! 

细节问题:

1.当以"w"-写的方式单纯打开文件时,c会自动清理内部数据

2.当以"w"方式打开文件,它会建立一个新文件,它的默认权限是0664

 -rw-rw-r-- 1 hongxin hongxin  212 Jan  1 14:37 myfile

因为普通类文本创建时有自己的默认文件掩码 umask 0002

0666 &  ~umask ->0664

[hongxin@VM-8-2-centos 12-31-1]$ umask
0002

如果我们想去文件的默认掩码,建立文件前输入代码umask(0);

读文件

size_t fread( void *ptr, size_t size, size_t count,FILE *stream );

#include <stdio.h>    
#include <string.h>    
    
    
int main()    
    
    FILE* fp=fopen("myfile","r");    
    if(!fp)    
        
        perror("open fail");    
        return (1);    
        
    char buf[1024];
    const char *msg="hello world\\n";

    while(1)
    
        size_t s=fread(buf,1,sizeof(buf)-1,fp);
        if(s>0)
        
            buf[s]=0;
            printf("%s",buf);    
    
            
        if(feof(fp))    
                                                   
            break;    
                                                                                                                                          
            

fclose(fp);                                                                                                                        
    return 0;                                         
  

显示结果:

[hongxin@VM-8-2-centos 12-31-1]$ make
gcc -o test test.c
[hongxin@VM-8-2-centos 12-31-1]$ ./test 
hello world
hello world
hello world
hello world
hello world
hello world
 

这里fread将文件内数据拷贝到数组,s接受fread返回的个数,s大于0,大于文件。当打印完时文件为空退出循环。

追加

我只需要将w改成a,一直执行程序就行对文件中数据进行重复拷贝

FILE* fp=fopen("myfile","a"); 

对于文件的读和写我们不仅仅可以用fread,fwrite,还可以用fgetc,fputc,fgets,fputs,fscanf,fprintf等。

总结打开文件的方式

r       Open text file for reading.

        The stream is positioned at the beginning of the file.

r+    Open for reading and writing.

        The stream is positioned at the beginning of the file.

w     Truncate(缩短) file to zero length or create text file for writing.

        The stream is positioned at the beginning of the file.

w+   Open for reading and writing.

        The file is created if it does not exist, otherwise it is truncated.

        The stream is positioned at the beginning of the file.

a      Open for appending (writing at end of file).

        The file is created if it does not exist.

        The stream is positioned at the end of the file.

a+    Open for reading and appending (writing at end of file).

        The file is created if it does not exist.

        The initial file position for reading is at the beginning of the file,

        but output is always appended to the end of the file.

二、系统文件I/O

在C语言中打开文件接口是fopen,fopen也是基于系统文件接口open之上的,紧接着就会学习open参数的含义和使用方法。这里需要明白不管是打开还是读写操作,都是在对文件进行操作,文件操作的本质是被打开文件和进程的关系。他们的关系,就好比进程是被打开文件的使用者,为文件的使用者,进程理所当然地将要使用的文件记录于自己的控制块。另外,由于进程所对应的程序也是一个文件,因此进程控制块还必须记录这个文件的相关信息。由于操作系统要对系统所以进程提供服务,因此操作系统还要维护一个记录所有进程打开文件的总表。


2.1用比特位传递选项

在开始学习文件接口open之前,我们先学习如何用比特位传递选项,这个是为了更加容易理解open参数flags的使用。

一个int有32个比特位,通过比特位传递选项。

#include<stdio.h>    
    
#define ONE (1<<0)    
#define TWO (1<<1)    
#define THREE (1<<2)    
#define FOUR (1<<3)    
    
void show(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()    
    
    show(ONE);    
    printf("------------------------\\n");    
    
    show(TWO);    
    printf("------------------------\\n");    
    show(ONE|TWO);    
    printf("------------------------\\n");    
    show(ONE|TWO|THREE);    
    printf("------------------------\\n");    
    show(ONE|TWO|THREE|FOUR);    
    printf("------------------------\\n");    
    
    
    
    return 0;                                                                                                                                                                              
  

结果:

[hongxin@VM-8-2-centos 12-31-1]$ ./myfalfs 
one
------------------------
two
------------------------
one
two
------------------------
one
two
three
------------------------
one
two
three
four
------------------------

比特位通过与预算以0000 0001->0000 0011的形式进行到达传递选项的目的

2.2接口介绍

通过man 2 open打开文件手册学习open参数的含义和使用方法

//头文件

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int open(const char *pathname, int flags, mode_t mode);

pathname: 要打开或创建的目标文件

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

参数:

        O_RDONLY: 只读打开

        O_WRONLY: 只写打开

        O_RDWR : 读,写打开

                        这三个常量,必须指定一个且只能指定一个

        O_CREAT : 若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限

        O_APPEND: 追加写

返回值:

成功:新打开的文件描述符

失败:-1

mode:权限

例 0666 

通过man 2 write打开文件手册学习write参数的含义和使用方法

   #include <unistd.h>

ssize_t write(int fd, const void *buf, size_t count);


fd:文件描述符--open返回值

*buf:写入从缓冲区指向buf

count:数量

#include <stdio.h>    
#include <sys/types.h>    
#include <sys/stat.h>    
#include <fcntl.h>    
#include <string.h>    
#include <unistd.h>    
    
    
int main()    
    
    int fd=open("log.txt",O_WRONLY|O_CREAT,0666);    
    if(fd<0)    
        
        perror("open fail");    
        return 1;    
        
    
    int cnt =5;    
    char outBuffer[64];    
    while(cnt)    
        
        sprintf(outBuffer,"%s:%d\\n","hello world",cnt--);    
    
        write(fd,outBuffer,strlen(outBuffer));                                                                                                                                             
        
    
    
    close(fd);    
    
    return 0;    
    
  

特别注意:

我们在线文件中写入string 的时候以、0作为字符串的结尾,它是由C语言规定的,但是这里是文件写入是,结束时是与\\0无关的。所以在strlen()不需要+1;

运行结果:

[hongxin@VM-8-2-centos 12-31-1]$ gcc -o test1 test1.c
[hongxin@VM-8-2-centos 12-31-1]$ ./test1
[hongxin@VM-8-2-centos 12-31-1]$ ll
total 60
-rw-rw-r-- 1 hongxin hongxin   70 Jan  1 19:19 log.txt
[hongxin@VM-8-2-centos 12-31-1]$ cat log.txt
hello world:5
hello world:4
hello world:3
hello world:2
hello world:1

再次注意:

当我们再次对文件写入是,文件里的数据还是会有一部分数据未被清楚

在log.txt文件中: 

aaaa:5                                                                                                                                                                                 
  2 aaaa:4
  3 aaaa:3
  4 aaaa:2
  5 aaaa:1
  6 orld:3
  7 hello world:2
  8 hello world:1
~

在C语言我们知道w重新写它会自动清除,但是在系统接口中是不能自动清除的,所以在open中flags参数加上O_TUNC

open("log.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);

追加

我们追加是在原来的内容上,进行追加。那么就不需要清除数据,所以open接口就不需要O_TRUNC,但是需要加上O_APPEND

open("log.txt",O_WRONLY|O_CREAT|O_APPEND,0666);

通过上面测试我们不难发现C语言中的库函数接口是通过封装了系统调用接口实现的

系统调用接口和库函数的关系,一目了然。

所以,可以认为,f#系列的函数,都是对系统调用的封装,方便二次开发。

2.3文件描述符fd(重)

通过对open函数的学习,我们知道了文件描述符就是是open的返回值,具体是什么我们也没有看见,接下来我就来观察open函数返回值fd是多少。

printf("%d",fd);        //我们在之前的代码上打印open返回值fd即可

[hongxin@VM-8-2-centos 12-31-1]$ ./test1
3

通过打印我们发现是3,那么问题来了,为什么不是0,1,2开始为什么是3开始呢?在学习C语言的时候,有三个标准输入输出流。

stdin ----键盘          0

stdout----显示器     1

stderr----显示器     2

代码演示:

 1: test.c  ⮀                                                                                                                                                                 ⮂⮂ buffers 
#include <stdio.h>    
#include <string.h>    
#include <unistd.h>    
#include <sys/types.h>    
#include <sys/stat.h>    
#include <fcntl.h>    
    
int main()    
    
    printf("stdin->fd:%d\\n",stdin->_fileno);    
    printf("stdout->fd:%d\\n",stdout->_fileno);    
    printf("stderr->fd:%d\\n",stderr->_fileno);    
    umask(0);    
    int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);    
    if(fd<0)    
        
        perror("open fail");    
        return 0;    
        
    
    printf("fd: %d\\n",fd);    
    
    close(fd);                                                                                                                                                                             
    
    return 0;    
    

运行结果:

[hongxin@VM-8-2-centos 1-2]$ make
gcc -o test test.c
[hongxin@VM-8-2-centos 1-2]$ ./test
stdin->fd:0
stdout->fd:1
stderr->fd:2
fd: 3

通过演示,我们知道我们自己打开的文件的文化描述符是3,在Linux进程默认情况下会有3个缺省打开的文件描述符,分别是标准输入0, 标准输出1, 标准错误2。0,1,2默认被占用,012对应的物理设备一般是:键盘,显示器,显示器

我们在学习C语言文件的时候,有FIEL *fp =fopen(); 那么FILE又是什么呢?是结构体!我们在文件调用接口的时候,我们发现我们并没使用FILE,但是在文件系统调用接口中我们使用的是文件描述符fd,那么在FILE结构体中必定有一个文件描述符的字段。

文件操作的本质:进程和被打开文件的关系。进制中可以打开多个文件,系统中就存在大量的被打开文件,这些文件都会被管理起来,我们通过先描述,后组织。操作系统为了管理对应打开文件,必须要为文件创建对应的内核数据结构标识文件struct file,它就包含文件大部分属性。

不管是键盘,显示器,显示器还是log.txt,他们都是文件,而且他们都是struct file结构体,系统中存在这文件,都是struct file,那么操作系统如何管理呢?我们从上面知道每个打开的文件都文件标识符fd,所以操作系统就通过fd来识别文件和寻找文件。这个fd我们观察到的是一串有效的数字,那么这些数字就操作系统中struct file* fd_array[]的下标。

过程:操作系统中进程pbc(task_struc)中有*files,通过*files找到files_struct文件,再通过files_struct的struct file* fd_array[]的下标标注struct file* ,通过struct file*找到struct file。

如图

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

2.4文件描述符的分配规则

我们将Linux进程默认3个标准输入输入错误的文件描述符关闭后后,会是什么现象呢?

通过代码演示

#include <stdio.h>    
#include <string.h>    
#include <unistd.h>    
#include <sys/types.h>    
#include <sys/stat.h>    
#include <fcntl.h>    
    
    
int main()    
    
    close(0);   
    //close(2);
    // close(1) 
    umask(0);    
    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.首先关闭的默认0

[hongxin@VM-8-2-centos 1-2]$ ./myfile 
fd:0

2.关闭2
[hongxin@VM-8-2-centos 1-2]$ ./myfile 
fd:2
3.关闭1

[hongxin@VM-8-2-centos 1-2]$ ./myfile 
[hongxin@VM-8-2-centos 1-2]$ 

4.同时关闭0,2

[hongxin@VM-8-2-centos 1-2]$ ./myfile 
fd:0

关闭了0,2后,我们发现我们所写的文件的文件标识符就变成了0,2,将他们的位置占据,当我们关闭1时,因为1是标准输出--显示器,所以没有打印到显示器上。当我们同时关闭0-2时,我们发现fd是0.说明文件描述符的分配规则:在files_struct数组当中,找到当前没有被使用的 最小的一个下标,作为新的文件描述符。

2.5重定向

我们专门将colse(1)拿出来观察,代码演示:

#include <stdio.h>    
#include <string.h>    
#include <unistd.h>    
#include <sys/types.h>    
#include <sys/stat.h>    
#include <fcntl.h>    
    
    
int main()    
    
    close(1) 
    umask(0);    
    int fd = open("log.txt", O_WRONLY|O_CREAT, 00644);
 if(fd < 0)    
        
        perror("open");    
        return 1;    
        
    
    printf("fd:%d\\n",fd);    
    close(fd);
    return 0;

运行结果:

[hongxin@VM-8-2-centos 1-2]$ ./myfile 
[hongxin@VM-8-2-centos 1-2]$ 

此时,我们发现,本来应该输出到显示器上的内容,输出到了文件myfile 当中,其中,fd=1。这种现象叫做输出重定向。常见的重定向有:>, >>, <

没有显示的原因就是:原本数组中1是指向的标准输出--显示器,但是我将它关闭之后,文件描述符的分配规则是将没有用的最小的坐标进行占用,那么数组中1就指向了myfile文件。由于关闭是显示器所以就不显示。

虽然close(1),但是它会打印到log.txt中

    printf("open fd: %d\\n", fd); // printf -> stdout
    fprintf(stdout, "open fd: %d\\n", fd); // printf -> stdout
    fflush(stdout);

hongxin@VM-8-2-centos 1-2]$ ./myfile 

 [hongxin@VM-8-2-centos 1-2]$ cat log.txt 
open fd: 1
open fd: 1

本来是写入标准输出文件中,但是通过关闭1,然后又通过文件描述符的分配规则,最后打印到了log.txt文件中,这个就是重定向,重定向的本质:上层用的fd不变,在内核中更改fd对应的struct file* 的地址。

这种重定向的方式是比较麻烦的,需要先关闭,然后在打开。dup2就很好的解决这个问题,不需要关闭打开繁琐的操作。

2.6使用 dup2 系统调用

#include <unistd.h>
int dup2(int oldfd, int newfd);

使用方法:

dup2() makes newfd be the copy of oldfd, closing newfd first if necessary, but note the following:

注意:dup2它是将文件标识符中的内容进行交换,而不是交换文件标识符。

输出重定向

#include <stdio.h>    
#include <string.h>    
#include <unistd.h>    
#include <sys/types.h>    
#include <sys/stat.h>    
#include <fcntl.h>    
    
    
int main()    
    
    umask(0);    
    int fd = open("log.txt", O_WRONLY|O_CREAT, 00644);
 if(fd < 0)    
        
        perror("open");    
        return 1;    
     
   
    dup2(fd, 1);
    
    printf("fd:%d\\n",fd);    
    close(fd);
    return 0;

运行结果:

[hongxin@VM-8-2-centos 1-2]$ ./myfile 
[hongxin@VM-8-2-centos 1-2]$ cat log.txt 
open fd: 1

追加重定向

#include <stdio.h>    
#include <string.h>    
#include <unistd.h>    
#include <sys/types.h>    
#include <sys/stat.h>    
#include <fcntl.h>    
    
    
int main()    
    
    umask(0);    
    int fd = open("log.txt", O_WRONLY|O_APPEND, 00644);
 if(fd < 0)    
        
        perror("open");    
        return 1;    
     
   
    dup2(fd, 1);
    
    printf("fd:%d\\n",fd);    
    close(fd);
    return 0;

运行结果:

[hongxin@VM-8-2-centos 1-2]$ ./myfile 
[hongxin@VM-8-2-centos 1-2]$ cat log.txt 
open fd: 1
open fd: 1
[hongxin@VM-8-2-centos 1-2]$ ./myfile 
[hongxin@VM-8-2-centos 1-2]$ cat log.txt 
open fd: 1
open fd: 1
open fd: 1

 2.7再谈myshell

这次主要是给myshell增加重定向指令,目的是展现重定向的应用场景,使myshell更加完善。

添加重定向,首先就是需要扫描重定向标识符">","<",">>"。然后将命令和文件分开,识别到相应重定向标识符之后,进行完成重定向的功能。

标识符清"0",与文件分开:
            "ls  -a -l -i>myfile.txt"  -> "ls -a -l -i" "myfile.txt" ->  实现重定向 

头文件与初始化

 #include <stdio.h>
  #include <string.h>
  #include <stdlib.h>
  #include <unistd.h>
  #include <sys/types.h>
  #include <sys/wait.h>
  #include <assert.h>
  #include <ctype.h>
  #include <sys/stat.h>
  #include <fcntl.h>
  #include <errno.h>
  
  #define NUM 1024
  #define OPT_NUM 64
  
  #define NONE_REDIR 0
  #define INPUT_REDIR 1
  #define OUTPUT_REDIR 2
  #define APPEND_REDIR 3
  
  
  #define  trimSpace(start)do\\
      while(isspace(*start)) start++;\\
  while(0)
  
  char lineCommand[NUM];
  char *myargv[OPT_NUM]; //指针数组
  int  lastCode = 0;
  int  lastSig = 0;
  
  int redirType =NONE_REDIR;//重定向文件类型
  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=='>')
                  
                  //"ls -a" >>file.log    
                  redirType = APPEND_REDIR;
                  ++start;    
                  
              else                                                                                                                                                                        
                  //"ls -a" >file.log    
                  redirType = OUTPUT_REDIR;    
                  
              trimSpace(start);    
              redirFile = start ;    
              break;    
              
          else if (*start =='<')    
          
              //"cat < file.txt"
              *start ='\\0';
              start++;
  
              trimSpace(start);//过滤空格
              //填写重定向信息
              redirType =INPUT_REDIR;
              redirFile =start;

          
          else
              start++;
          
      
  

主函数实现

在主函数中,因为父进程主要负责将信息提供给子进程,对于执行是子进程的子进程执行的,真正重定向的工作一定是要自进程完成。所以主要增加代码只子进程中。

 
  int main()
  
      while(1)
      
          // 输出提示符
          printf("用户名@主机名 当前路径# ");
          fflush(stdout);
  
          // 获取用户输入, 输入的时候,输入\\n
          char *s = fgets(lineCommand, sizeof(lineCommand)-1, stdin);
          assert(s != NULL);
          (void)s;
          // 清除最后一个\\n , abcd\\n
          lineCommand[strlen(lineCommand)-1] = 0; // ?                                                                                                                                     
          //printf("test : %s\\n", lineCommand);
          
          // "ls -a -l -i" -> "ls" "-a" "-l" "-i" -> 1->n
          //"ls -> -a -l -i>myfile.txt"  -> "ls -a -l -i" "myfile.txt" ->  实现重定向
          commandCheck(lineCommand);
  
          // 字符串切割
          myargv[0] = strtok(lineCommand, " ");
          int i = 1;
          if(myargv[0] != NULL && strcmp(myargv[0], "ls") == 0)
          
              myargv[i++] = (char*)"--color=auto";
          
  
          // 如果没有子串了,strtok->NULL, myargv[end] = NULL
W>        while(myargv[i++] = strtok(NULL, " "));
  
          // 如果是cd命令,不需要创建子进程,让shell自己执行对应的命令,本质就是执行系统接口
          // 像这种不需要让我们的子进程来执行,而是让shell自己执行的命令 --- 内建/内置命令
           if(myargv[0] != NULL && strcmp(myargv[0], "cd") == 0)
             if(myargv[1] != NULL) chdir(myargv[1]);
              continue;
          
          if(myargv[0] != NULL && myargv[1] != NULL && strcmp(myargv[0], "echo") == 0)
          
              if(strcmp(myargv[1], "$?") == 0)
              
                  printf("%d, %d\\n", lastCode, lastSig);
              
              else
              
                  printf("%s\\n", myargv[1]);
              
              continue;
          
          // 测试是否成功, 条件编译
  #ifdef DEBUG
          for(int i = 0 ; myargv[i]; i++)
          
              printf("myargv[%d]: %s\\n", i, myargv[i]);
          
  #endif
          // 内建命令 --> echo
  
          // 执行命令
          pid_t id = fork();
          assert(id != -1);
  
          if(id == 0)
          
              //因为命令是子进程执行的,真正重定向的工作一定是要自进程完成
              //如何重定向,是父进程给子进程提供的信息
             //这里重定向不会影响父进程
              switch(redirType)
              
                  case NONE_REDIR:
                      //什么都不做
                      break;
                  case INPUT_REDIR:
                      
                          int fd = open(redirFile,O_RDONLY);
                          if(fd<0)                                                                                                                                                         
                          
                              perror("open fail");
                              exit(errno);
                          
                          //重定向的文件已经成功打开
                          dup2(fd,0);
                      
                      break;
                  case OUTPUT_REDIR:
                  case APPEND_REDIR:
                      
                          int flags = O_WRONLY|O_CREAT;
                          if(redirType==APPEND_REDIR) flags |= O_APPEND;
                          else flags |= O_TRUNC;
                          int fd =open(redirFile,flags);
                          if(fd<0)
                          
                              perror("open fail");
                              exit(errno);
                          
                           dup2(fd,1);
                      
                      break;
                  default:
                      printf("bug!!\\n");
                      break;
              
              execvp(myargv[0], myargv);
              exit(1);
          
          int status = 0;
          pid_t ret = waitpid(id, &status, 0);
          assert(ret > 0);
          (void)ret;
          lastCode = ((status>>8) & 0xFF);
          lastSig = (status & 0x7F);
      
  

问题:执行程序替换时,会不会影响曾经进程打开的重定向文件呢?

答:是不会的,程序替换是在磁盘与内存阶段,程序替换是将代码进行覆盖。重定向是在pcb与*files阶段,它是在内核数据结构中。程序替换是不影响内核数据结构

三、FILE-缓冲区

因为IO相关函数与系统调用接口对应,并且库函数封装系统调用,所以本质上,访问文件都是通过fd访问的。 所以C库当中的FILE结构体内部,必定封装了fd。 我们进行再一步研究系统调用与函数调用有何不同。


3.1抛出问题

通过代码研究:

#include <stdio.h>
#include <string.h>


int main()

 
 printf("hello printf\\n");
 fprintf(stdout,"hello fprintf\\n");
 fputs("hello fputs\\n",stdout);
 
const char *msg="hello write\\n";
write(1,msg,strlen(msg));
 
 fork();
 
 return 0;

运行出结果: 

[hongxin@VM-8-2-centos 2023-1-13]$ ./test 
hello printf
hello fprintf
hello fputs
hello write

但如果对进程实现输出重定向呢? ./test > log.txt , 我们发现结果变成了:

[hongxin@VM-8-2-centos 2023-1-13]$ cat log.txt 
hello write    //系统接口
hello printf   //以下都是库接口
hello fprintf
hello fputs
hello printf
hello fprintf
hello fputs

通过对比发现,C接口的函数打印了两次,系统接口打印了一次。

那么我们再把fork注释掉,再打印出结果:

[hongxin@VM-8-2-centos 2023-1-13]$ ./test > file
[hongxin@VM-8-2-centos 2023-1-13]$ cat file
hello write
hello printf
hello fprintf
hello fputs

为什么会这样呢?但是我们大概能够明白,导致这种情况可能与fork有关!

3.2认识缓冲区

首先我们想知道缓冲区是什么呢?缓冲区的本质就是一段内存 ,那么是谁申请的呢?又是属于谁的呢?为什么要有缓存区呢?

为了更好的理解,我们先通过例子演示,再进一步探究!

例子:

在日常生活你和朋友关系很好,有一天你朋友喊你带点你们地方特产给他。有两种选择:1.自己给他送过去 2.你去快递公司给他送过去。自己送过去是非常不划算的,因为又浪费自己的时间,而且费用也高,那么我们选择快递送过去。

这段路程就好比是进程将数据送到磁盘,自己送时间会很长,而且代价也会很大。这个时候有个叫缓存的公司说,我专门送数据的,你给我,我送的又快,代价又小。

通过上述例子,不难得出缓存区的意义:节省进程进行数据的IO时间

进程将数据传到缓冲区中,通过的是fwrite,fwrite是写入到文件的函数,实质fwrite就是拷贝函数!将数据从进程拷贝到"缓冲区"或者外设中。

3.3缓冲区刷新策略

缓冲区刷新策略就跟快递公司送货的策略相似,一般情况下我们寄快递并不是给快递公司,他就立刻寄货。如果货少就需要积累到达一定量才送走。

那么缓冲区也是一样的,进程将数据拷贝到缓冲区中,缓冲区一定会结合具体的设备,定制自己的刷新策略:

a.立即刷新    --  无缓冲

b.行刷新        --  行缓存  --显示器

c.缓冲区满    --  全缓冲  --磁盘文件

对于行刷新,一般情况下是在显示器下采用的,因为文字是给人看的,比如我们给朋友发消息,也不可能采用将缓冲区占满了再发生给他,每次他看我的消息又久又长,这样是不符合现实的。所以我们需要通过行刷新。文件读写的时候就会采用全缓冲,这样的效率是最高的,一般情况下磁盘的需求也是比较大的。

特殊情况:

a.用户强制刷新

b.进程退出   --  一般都要进行行缓冲区刷新

3.4解决缓冲区问题(重)

又回到最开始的问题,最开始打印四条信息,然后通过冲定向后变成了七条。

首先这种现象一定和缓冲区有关,根据上述理解缓冲区一定不在内核中。我们在调用C语言接口和系统接口时,发现C语言接口调用了打印两次,系统接口打印了一次。如果缓冲区在内核中,那么系统接口write也应该打印两次。

那么所有缓冲区,都是指的户用级语言层面给我们提供的缓冲区!我们在写和读时,打开文件stdin,stdout,他们都是FLIE*指向的file结构体,在他这个结构体中有fd,缓冲区等。

那么我们可在xshell中,打开用vim该文件

vim /usr/include/stdio.h

该FILE结构体文件--typedef struct _IO_FILE FILE;

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;//fd

#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
;

缓冲区在哪里,缓冲区是什么,也迎刃而解了。通过上述知识,关于fork问题,那么也能解决了。

缓冲区问题:

代码在结束之前,进行创建子进程

1.如果我们没有进行重定向(>),就看见了4条消息的原因:

写入文件是对显示器写,stdout默认使用的是行刷新,在进程fork之前,三条C函数已经将数据进行打印到输出到显示器上(外设),你的FILE内部, 进程内部不存在对应的数据。

2.如果我们进行了重定向(>),看见了7条消息,少1条消息的原因:

写入文件不再是显示器,而是普通文件,采用的刷新策略是全缓冲,在进程fork之前,三条C函数已经将数据进行打印到输出到显示器上(外设),write虽然带了\\n,但是不足以stdout缓冲区写满!数据并没有被刷新!

3.打开fork显示7条消息,关闭fork显示4条的原因:

执行fork的时候,stdout属于父进程,创建进程时,紧接着就是进程退出。不管谁先退出,一定要进行缓冲区刷新(就是修改),这个时候就会发生写时拷贝,所以最终数据会显示两份就打印了7条条消息。我们关闭fork,没有发生写时拷贝,那么输入到显示器上的还是那4条消息。

代码在结束之前,没有进行创建子进程

4.write为什么没有写时拷贝的原因:

已上过程都与write无关,write是没有FILE,而是用的fd,就没有C提供的缓冲区。

使用write等 IO接口,函数直接输出到输出设备上,是不带缓冲但是标准IO库是带有缓冲的,printf遇到\\n的时候才会冲刷缓冲区,输出到输出设备上。

3.4myStdio-dome

通过调用系统接口实现的C语言库接口,主要是用于理解系统调用接口和缓冲区。通过代码我们就能理解到,缓冲区是实则是结构体文件(FILE)中的一段内存,是通过文件标识符链接的---缓冲区通过文件标识符链接打开文件,然后再将缓冲区数据拷贝到文件中。

下面就将代码展示,不理解的代码中有非常详细的注释。

myStd.h--接口的定义

#pragma once

#include <sys/types.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <stdlib.h>
#include <assert.h>
#include <unistd.h>

//buffer-缓冲区大小
#define SIZE 2014

//缓冲模式
#define SYNC_NOW    1
#define SYNC_LINE   2
#define SYNC_FULL   4

typedef struct _FILE

    int flags;   //刷新方式
    int fileno;  //文件标识符

    char buffer[SIZE]; //缓冲区

    int cap;    //buffer的总容量
    int size;   //buffer当前的使用量

FILE_;

//打开文件
FILE_ *fopen_(const char *path_name,const char *mode);

//写入
void fwrite_(const void* ptr,int num,FILE_ *fd);

//关闭文件
void fclose_(FILE_* fp);

//强制缓冲区
void fflush_(FILE_* fp);

myStd.c--接口的实现

#include "myStdio.h"


FILE_ *fopen_(const char *path_name,const char *mode)

    //打开文件时,可以传入多个参数选项用下面的一个或者多个常量进行“或”运算,构成flags。
    int flags =0;
    int defaultMode =0666;//默认打开文件权限

    if(strcmp(mode,"r")==0)//选择以读的方式打开文件
    
        flags |=O_RDONLY;//只读模式:O_RDONLY
    
    else if(strcmp(mode,"w")==0)//选择以写的方式打开文件
    
        flags |=(O_TRUNC|O_WRONLY|O_CREAT);//只写模式:O_WRONLY-只写,O_CREAT-创建新文件,O_TRUNC-覆盖原有
    
    else if(strcmp(mode,"a")==0)//选择以追加的方式打开文件
      
        flags |=(O_WRONLY|O_APPEND|O_CREAT);//追加模式:O_WRONLY-只写,O_CREAT-创建新文件,O_APPEND--追加
    
    else
    

    //调用系统接口实现

    int fd = 0;
    if(flags & O_RDONLY) fd=open(path_name,flags);//只读不需要其他权限
    else fd = open(path_name,flags,defaultMode);

    //调用失败,说明原因,返回null
    if(fd < 0)
    
        const char* err = strerror(errno);//获取错误码
        write(2,err,strlen(err));//向fd=2-stderr中打印,显示错误原因
        return NULL;
    

    FILE_ *fp = (FILE_*)malloc(sizeof(FILE_));//开辟结构体大小空间
    assert(fp);

    fp->flags =SYNC_LINE;       //默认设置成行刷新
    fp->fileno =fd;             //open返回的文件标识符--文件描述符:通过映射 路径+文件名
    fp->cap = SIZE;             //缓冲区总容量
    fp->size = 0;               //实际总容量
    memset(fp->buffer,0,SIZE);  //初始化buffer
    
    return fp;//为什么打开一个文件,就返回一个FIEL* 指针--因为是用FIEL的方式组织,便于后续操作--写,刷新缓存,关闭文件等


void fwrite_(const void* ptr,int num,FILE_ *fp)//冲文件

    //1.写入到缓冲区中
    memcpy(fp->buffer+fp->size,ptr,num);//这里不考虑缓冲区溢出的问题
    fp->size+=num;

    //2.判断已什么方式刷新
    if(fp->flags & SYNC_NOW)//立即刷新
    
        write(fp->fileno,fp->buffer,fp->size);//刷新就是将缓冲区数据拷贝到打开文件中
        fp->size = 0; //清空缓冲区
    
    else if(fp->flags & SYNC_LINE)//\\n--行刷新
    
        if(fp->buffer[fp->size-1]=='\\n')
        
            write(fp->fileno,fp->buffer,fp->size);
            fp->size=0;
        
    
    else if(fp->flags & SYNC_FULL)//实际与当前容量相等--全刷新
    
        if(fp->size==fp->cap)
        
            write(fp->fileno,fp->buffer,fp->size);
            fp->size = 0;
        
    
    else

    



void fflush_(FILE_* fp)

    if(fp->size>0)
    write(fp->fileno,fp->buffer,fp->size);
    fsync(fp->fileno);//将数据强制要求刷入磁盘
    fp->size=0;



void fclose_(FILE_* fp)

    fflush_(fp);
    close(fp->fileno);

mian.c--测试

#include "myStdio.h"

int main ()

    FILE_*  fp=fopen_("./log.txt","w");//传入路径名和刷新模式
    if(fp==NULL)
    
        return 1;
    
    const char *msg = "hello fwirte_!\\n";

    fwrite_(msg,strlen(msg),fp);

    fclose_(fp);

    return 0;

运行结果:

[hongxin@VM-8-2-centos 2023-1-14]$ cat log.txt 
hello fwirte_!

根据上面代码再次进行调试,感受刷新缓冲区。

#include "myStdio.h"

int main ()

    FILE_*  fp=fopen_("./log.txt","w");//传入路径名和刷新模式
    if(fp==NULL)
    
        return 1;
    

    int cnt = 10;
    const char *msg = "hello!\\n";

    while(1)
    
        fwrite_(msg, strlen(msg), fp);
        //fflush_(fp);
        sleep(1);
        printf("count: %d\\n", cnt);
        //if(cnt == 5) fflush_(fp);
        cnt--;
        if(cnt == 0) break;
    

    fclose_(fp);

    return 0;

监控脚本 

 while :; do cat log.txt ; sleep 1 ;echo "##########################" ; done

没有刷新缓冲区时,最后关闭的时候才刷新缓冲区,直到最后才一次性打印出来。 

每次都强制刷新缓冲区时,会一个一个打印出来。

那么在OS(操作系统)中,数据是怎么写入磁盘中的呢?

学习了缓冲区,我们就明白了数据是不能直接就拷贝到磁盘的,数据是先struct file-->*files->文件描述发->内核缓冲区-->刷新缓冲区-->磁盘。这个过程是由操作系统自主决定,这个就跟我们上述代码中的行缓存,全缓冲是不一样的。上述是C语言应用层方面自己封装的FILE,这里是操作系统层从缓冲区刷新到磁盘中是非常复杂的。

特别需要理解的库级别的缓冲区和系统级别的缓冲区不是一个概念库级别是FILE中的一段内存,系统级别则是更加复杂的处理方式。

在这个过程中,数据都是通过拷贝进行传输,输入一段数据到磁盘会发生三次拷贝。 

如果操作系统(OS)突然宕机(down机,死机)了,数据还在缓冲区中。这个时候就可用fsync。

#include <unistd.h>

int fsync(int fd);

int fdatasync(int fd);

fsync, fdatasync - synchronize a file's in-core state with storage device

--  将文件的内核状态与存储设备同步/换句话说就强制将数据刷新到磁盘中

当我们在代码中使用fsync之后,发现没有调用fflush_,它也会强制刷新。

四、文件系统

上面讲的FILE也是文件,是打开文件。那么未被打开文件,也是放在磁盘上,磁盘上有大量的文件也是必须被静管理的,方便我们随时打开,这也是文件系统。文件系统既要管理动态打开文件,又要管理静态未被打开文件。文件是放在磁盘的,我们对于磁盘是陌生的,不知为何物,不知是什么样子,不知道它到底是这样存数据,也不知道它是如何找数据的。下面我们就会先了解硬件--磁盘,然后再来学习文件系统。


4.1磁盘的物理结构

磁盘分为硬盘与软盘,在历史的长流中,软盘渐渐地被淘汰,因为软盘是容量比较小,寿命比较短,现在内存动辄就是TB,那么软盘就渐渐的退出历史舞台。硬盘是一个机械结构加外设,硬盘的访问速度相比于内存,CPU是非常慢的。

就导致现在我们就很少看见磁盘,磁盘是我们计算机中唯一一个机械结构。现在笔记本电脑中更多是选择SSD(固态硬盘),相比传统的机械硬盘的性能优势主要表现在:读写速度快、防震抗摔性、低功耗、无噪音、工作温度范围大、轻便。固态硬盘由于其做工精细,因此价格也更贵。

但是相对于企业,磁盘依旧是主流。SSD也并不是完美,首先来说价格相对于磁盘是更高,最重要的是SSD有读写的限制,如果写多了就会出现SSD被击穿的问题,就会造成数据的丢失。但是这都不是绝对的,磁盘和SSD都各有所长,很多时候也会选择混盘,磁盘和固态硬盘混用。

硬盘是什么样子:

c868beccd4b54b30af42a8e770fa7604<p>以上是关于4.2w字,详细的带你认识基础I/OLinux--基础IO的主要内容,如果未能解决你的问题,请参考以下文章</p> 
<p > <a style=[1w6k 字详细讲解] 保姆级一步一步带你实现 Promise 的核心功能

万字总结,体系化带你全面认识 Nginx !

2W 字总结 !体系化带你全面认识 Nginx

类型即正义,TypeScript 从入门到实践:5000字长文带你重新认识泛型

深度干货 | 38道Java基础面试题 (1.2W字详细解析)

深度干货 | 38道Java基础面试题 (1.2W字详细解析)