• 【Linux】基础IO


    🌇个人主页平凡的小苏
    📚学习格言:命运给你一个低的起点,是想看你精彩的翻盘,而不是让你自甘堕落,脚下的路虽然难走,但我还能走,比起向阳而生,我更想尝试逆风翻盘。
    🛸C++专栏:Linux内功修炼
    家人们更新不易,你们的👍点赞👍和⭐关注⭐真的对我真重要,各位路 过的友友麻烦多多点赞关注。欢迎你们的私信提问,感谢你们的转发! 关注我,关注我,关注我,你们将会看到更多的优质内容!!

    在这里插入图片描述

    一、文件的概念

    1、空文件,在磁盘中也会占据空间,因为文件=内容+属性,Linux的文件内容和文件属性是分开存储的;

    2、文件路径+文件名具有唯一性,如果没有指明文件路径,默认在当前路径下进行文件访问;

    3、文件操作的本质是进程和被打开文件的关系。

    二、C语言文件操作回顾

    1、什么是当前文件路径

    根据我们前面的学习,当fopen以写入的方式打开一个文件时,若文件不存在,则会自动在当前路径创建该文件,那么这里的当前路径是什么呢?以下面的代码为测试用例:

    #include
    #include
    int main()
    {
    	FILE* fp = fopen("log.txt", "w");//写入
    	if (fp == NULL)
    	{
    		perror("fopen");
    		return 1;
    	}
    	printf("mypid: %d\n", getpid());
    	const char* msg = "hello world";
    	int cnt = 1;
    	while (cnt < 20)
    	{
    		fprintf(fp, "%s: %d\n", msg, cnt++);
    	}
    	fclose(fp);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    此段代码我fopen写入的方式打开了log.txt文件,那么我在myfile目录下运行可执行程序myfile,那么该可执行程序创建的log.txt文件会出现在myfile目录下,此外上述代码还获取到了当前进程的pid,并且让此进程一直循环下去,方便后续操作。
    在这里插入图片描述

    根据我们获取到的当前进程的pid,再根据我们先前学到的知识,根据该pid在根目录下的proc目录下查看此进程的信息如下:

    在这里插入图片描述

    下面来解释下cwd和exe

    cwd表示当前进程所处的工作路径。
    exe表示进程对应的可执行程序的磁盘文件。
    上面的运行结果也正如我们所料:log.txt创建在了与当前可执行程序路径所在的位置,也是当前进程所处的工作路径,那是否就意味着这里说的“当前路径”是指“当前可执行程序所处的路径”呢?还是说“当前路径”是指“当前进程所处的路径”呢?

    这里我们把刚才生成的log.txt文件删除掉,对代码进行如下的修改,利用上次学到的chdir函数来更改此进程的工作路径:chdir("/home/xzy")

    在这里插入图片描述

    make clean;make后再次运行myfile程序,结果如下:

    在这里插入图片描述

    此时现象已经很明显了,我运行了myfile可执行程序,但是并没有在当前可执行程序myfile所处在的date17目录下看到我想要的log.txt文件,相反我却在/home/xzy路径下看到了log.txt文件,这就足以证明我利用chdir更改进程所处的路径后,生产的文件也随之改,这足矣证明此当前路径即为当前进程所处的路径,为了更具有说服力,我们依旧是利用proc查看当前进程9752的相关信息

    在这里插入图片描述

    综上,当前路径就是当前进程所处的路径!!!

    2、C语言文件接口汇总

    在这里插入图片描述

    打开方式如下

    在这里插入图片描述

    三、使用系统调用接口进行文件IO

    操作文件除了C语言接口、C++接口或是其他语言的接口外,操作系统也有一套系统接口来进行文件的访问。相比于C库函数或其他语言的库函数而言,系统调用接口更贴近底层,实际上这些语言的库函数都是对系统接口进行了封装。

    1、open

    下面基于系统接口来实现对文件的操作,C语言中我们要打开文件用的是fopen,但是系统接口中我们使用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

    open的第一个参数

    • pathname以路径的方式给出,则当需要创建该文件时,就在pathname路径下进行创建。
    • pathname以文件名的方式给出,则当需要创建该文件时,默认在当前路径下进行创建。(注意当前路径的含义)

    open的第二个参数

    第二个参数flags表示打开文件要传递的选项(打开文件的方式),其常用选项有如下几个:

    在这里插入图片描述

    2、open的使用

    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #define FILE_NAME "log.txt"
    int main()
    {
        //umask(0);//将子进程的umask置0,后续创建的文件权限即为666
        //当文件不存在时,open的可写并不会创建log.txt,需要按位或上O_CREAT,加上创建文件功能,最后一个参数是文件创建时的权限
        int fd=open(FILE_NAME,O_WRONLY | O_CREAT,0666);//最终权限=666&~umask
        if(fd<0)
        {
            perror("open");
            return 1;
        }
        close(fd);//使用系统调用close关闭文件指针
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    1、当文件不存在时,使用int open(const char *pathname, int flags, mode_t mode);

    如果目标文件不存在,open并不会像fopen那样帮助使用者创建对应文件,需要按位或O_CREAT,加上创建文件功能。第三个参数是创建文件的默认权限,如果不写,系统创建的文件会是乱码。

    2、当文件存在时,使用int open(const char *pathname, int flags);

    3、系统调用write

    3.1、接口相关参数介绍

    #include 
    ssize_t write(int fd, const void *buf, size_t count);
     //返回值:写入个数  目标写入文件  缓冲区数据位置   预期输入的最大个数
    
    • 1
    • 2
    • 3

    3.2、write的使用

    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #define FILE_NAME "log.txt"
    int main()
    {
        //"w"==O_WRONLY | O_CREAT | O_TRUNC,0666清空式写入
        int fd=open(FILE_NAME,O_WRONLY | O_CREAT | O_TRUNC,0666);
     
        //"a"==O_WRONLY | O_CREAT | O_APPEND,0666追加
        //int fd=open(FILE_NAME,O_WRONLY | O_CREAT | O_APPEND,0666);
        if(fd<0)
        {
            perror("open");
            return 1;
        }
        char outBuff[60];
        int cnt=5;
        while(cnt)
        {
            sprintf(outBuff,"%s:%d\n","sqy",cnt--);
            write(fd,outBuff,strlen(outBuff));//将字符写入文件不用让strlen+1写入\n,除非就像在文件中写入\n
        }
        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
    • 29
    • 将字符写入文件中可以不用写入\n。

    • write再次对有文本内容的文件进行写入时,并不会和C语言文件操作中的"w"选项一样,先清空文本内容,而是直接覆盖,使用时可以加上O_TRUNC对文本先进行清空操作。

    • 使用O_APPEND增加追加操作。

    4、read

    4.1、read的相关参数介绍

    #include 
    ssize_t read(int fd, void *buf, size_t count);
    //返回值:读取成功字节数       读取至缓冲区   要求读取字节
    
    • 1
    • 2
    • 3

    4.2、read的使用

    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #define FILE_NAME "log.txt"
    int main()
    {
        int fd=open(FILE_NAME,O_RDONLY | O_CREAT,0666);
        if(fd<0)
        {
            perror("open");
            return 1;
        }
        char buffer[100];
        ssize_t num=read(fd,buffer,sizeof(buffer)-1);//留一个位置放\0
        if(num>0)
        {
            buffer[num]='\0';//把\0补回去
        }
        printf("%s\n",buffer);
        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

    四、文件描述符fd

    1、文件描述符

    1.1、进程如何找到自己打开的文件

    一个进程可以打开多个文件,操作系统为了管理这些打开的文件,需要为每个被打开的文件创建对应的内核数据结构标识文件(struct file{}),这个结构体中包含文件的大部分属性。

    那么操作系统中被打开的文件这么多,进程是如何区分哪些文件是属于我的,哪些文件时非我的?那是因为进程的task_struct结构体中包含struct files_struct* files的结构体指针,这个指针指向struct files_struct这个结构体,这个结构体中存在一个叫做struct file* fd_array[]的结构体指针数组,数组中每个结构体指针指向对应被打开文件的struct file{}。

    在这里插入图片描述

    1.2、文件描述符为什么从3开始

    当我们在一个进程中用open打开多个文件时,通过打印fd的值,发现文件描述符fd从3开始分配。那0、1、2这三个文件描述符分配给谁了呢?

    printf("stdin->fd:%d\n",stdin->_fileno);
    printf("stdout->fd:%d\n",stdout->_fileno);
    printf("stderr->fd:%d\n",stderr->_fileno);
    
    • 1
    • 2
    • 3

    0、1、2默在这里插入图片描述
    认被这三个流占用,所以我们打开的文件的文件描述符fd从3开始。

    1.3、文件描述符fd和FILE的区别

    FILE* fp=fopen(FILE_NAME,"w");
    int fd=open(FILE_NAME,O_WRONLY | O_CREAT | O_TRUNC,0666);
    
    • 1
    • 2

    系统调用访问文件必须使用文件描述符。而C语言中的FILE是一个结构体,这个结构体中包含了文件描述符。

    2、文件描述符的分配规则

    #include 
    #include 
    #include 
    #include 
    #include 
    int main()
    {
        close(1);//关闭1
        int fd=open("log.txt",O_WRONLY | O_CREAT | O_TRUNC,0666);//新文件fd为1
        if(fd<0)
        {
            perror("open");
            return 1;
        }
        printf("%d\n",fd);//printf->stdout
    	fprintf(stdout,"%d\n",fd);//printf->stdout
    	fflush(stdout);//刷新缓冲区内容至文件log.txt中
        close(fd);
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    1、如果手动把文件描述符数组下标为1的fd关闭,那么后续用open打开一个新的文件,将会占用这个下标为1的位置。文件描述符的分配规则为:从小到大,按照顺序寻找最小的且没有被占用的fd位置。

    2、fd为1的位置代表标准输出流,1被关闭,新文件被打开将会占用1。如果后续想要执行打印操作,文件内容将会被打印至这个新文件中。这是一种输出重定向。

    五、重定向

    1、重定向原理

    //>输出重定向
    //>>追加
    //<输入重定向
    
    • 1
    • 2
    • 3

    重定向的本质:oldfd不变,在内核中更改newfd下标对应的struct file*的指针。

    文件描述符的本质是数组下标,当系统运行时,默认已加载3个文件描述符,分别是标准输入(键盘)、标准输出(显示器)、标准错误(显示器),他们占据了数组0,1,2三个位置。

    可以用close(1)手动关闭标准输出,再重新打开一个文件,这个新文件会占据数组为1的位置(标准输出)。如果后续使用printf或fprintf函数进行输出,数据将会输出重定向至这个新文件(因为这个新文件占据了标准输出)。

    不过一般不会用手动关闭这三个文件描述符来达到重定向的效果,可以使用int dup2(int oldfd, int newfd);将文件拷贝至前三个文件描述符中,达到重定向的目的。

    2、dup2

    2.1、dup2相关参数介绍

    #include 
    int dup2(int oldfd, int newfd);//将oldfd拷贝到newfd中
    //返回值
    //成功返回fd,失败返回-1
    
    • 1
    • 2
    • 3
    • 4

    如果要输出重定向到fd对应的文件中,那么oldfd就是fd,newfd就是1。

    函数功能为将oldfd描述符重定向到newfd描述符,相当于重定向完毕后都是操作oldfd所操作的文件 但是在过程中如果newfd本身已经有对应打开的文件信息,则会先关闭文件后再重定向(否则会资源泄露)

    2.2、输出重定向

    使用dup2对上方输出重定向代码写法进行更改。

    #include 
    #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;
        }
        dup2(fd,1);//输出重定向到fd对应的文件
        printf("%d\n",fd);//printf->stdout,在fd对应的文件中打印
    	fprintf(stdout,"%d\n",fd);//printf->stdout,在fd对应的文件中打印
        close(fd);//因为已经进行重定向,所以需要关闭fd,否则会白白浪费一个fd
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    在fd对应的文件中进行printf和fprintf打印。

    2.3、追加重定向

    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    int main()
    {
        int fd=open("log.txt",O_WRONLY | O_CREAT | O_APPEND,0666);
        if(fd<0)
        {
            perror("open");
            return 1;
        }
        dup2(fd,1);//输出重定向到显示器
        printf("%d\n",fd);//printf->stdout
    	fprintf(stdout,"%d\n",fd);//printf->stdout
        const char* str="sqy";
        write(1,str,strlen(str));//将更多内容写入1中
        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

    在fd对应的文件中进行printf和fprintf打印并进行write追加写入。

    2.4、输入重定向

    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    int main()
    {
        int fd=open("log.txt",O_RDONLY);
        if(fd<0)
        {
            perror("open");
            return 1;
        }
        dup2(fd,0);//输入重定向,将fd的内容拷贝至0
        char line[60];
        while(1)
        {
            if(fgets(line,sizeof(line),stdin)==NULL)//因为上方输入重定向了,所以这里stdin中会获取log.txt的内容
            {
                break;
            }
            printf("%s",line);
        }
        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

    六、linux下一切皆文件

    • 在计算机里有各种硬件(键盘、显示器、磁盘、网卡……),这些设备统一称为外设(IO设备),以磁盘为例,它们都一定有对应自己的一套读写方法,不同的设备对应的读写方法一定是不一样的,如果现在要打开磁盘,那么OS就在内核给你创建一套structfile,用readp指针指向read方法,writep指针指向write方法,打开显示器等其它外设也是类似的,这一操作就是OS内的文件系统做的软件封装,再往上就是一个进程里的指针指向一结构体,该结构体内部有一个指针数组,下标就是文件描述符,其内部存放struct file*的指针,从而指向各个设备的读或写的方法。

    在这里插入图片描述

    • Listitem上述整个过程就是“Linux下一切皆文件”,也就是说未来你想打开一个文件,把读写方法和属性记下来,在内核里给你这个硬件创建对应的structfile,初始化时把对应的函数指针指向你具体的设备,但在内核中存在的永远都是structfile,用链表结构关联起来,所以一个进程都以统一的视角看待文件,所以我们访问不同的file指向的谁完全取决于其底层的读写方法。有点多态的感觉了。我们把上述的设计出的structfile来表示一个一个文件的叫做VFS虚拟文件系统。
  • 相关阅读:
    postgresql14管理(五)-tablespace
    6.jvm中对象创建流程与内存分配
    倍福PLC旋切基本原理和应用例程
    【无标题】
    正则表达式
    【示波器专题】示波器的频响方式
    判断文本元素是否出现省略号,根据文本长度来控制是否显示鼠标悬浮提示el-tooltip
    非模式物种ROSE超级增强子鉴定分析详解
    真不戳,Java 协程终于来了
    [山东科技大学OJ]1508 Problem G: 求中位数
  • 原文地址:https://blog.csdn.net/VHhhbb/article/details/134355584