《高效文件操作指南:Linux系统IO与标准IO的对比与实践》

介绍

一、IO简介

系统I/O (System I/O):通常指的是操作系统内核提供的、最底层的I/O接口。在Linux中,这些就是系统调用 (system calls),如 open(), read(), write(), close(), lseek()。它们直接与内核交互。

标准I/O / 文件I/O (Standard I/O / File I/O):通常指的是标准C库(如 glibc)提供的一套更高层的I/O函数。它们是对系统调用的封装,提供了带缓冲、更便捷的接口。常见的函数是 fopen(), fread(), fwrite(), fclose(), fseek()。

ps:不管调用系统IO函数还是标准IO函数第一步都是要打开文件(open / fopen),其主要区别在于系统IO打开文件得到的是一个整数(文件描述符),标准IO打开文件得到的是一个指针(文件指针)

- 说明:对文件的操作,基本上就是输入输出,因此也一般称为IO接口
- 在操作系统层面上:这一组专门针对文件的IO接口就被称为系统IO    --- 偏向于底层(设备文件)
- 在标准库的层面上:这一组专门针对文件的IO接口就被称为标准IO    --- 偏向于上层(软件程序文件)

系统IO

标准IO(核心就是调用 各种标准库的API)

总的来说 : 标准IO实际上是对系统IO的封装,系统IO是更接近底层的接口

二、文件的概念

狭义上的文件是指:指普通的文本文件,或二进制文件,包括日常所见的txt文档、源代码、word文档、压缩包、图片、视频、音频文件等

广义上的文件是指:除了狭义上的文件外,几乎所有可操作的设备或结构都可视为文件。包括键盘、鼠标、硬盘、串口、显示屏、触摸屏等,也包括网络通讯端口(多机通信要用到的文件)、进程间通讯管道(单机通信)等抽象概念

对于Linux来说,一切可操作的文件都是广义上的文件(图片、键盘、鼠标、音频等)

Linux中文件的分类

常见的linux文件分类如下表所示:

文件名称符号描述
普通文件存在于外部存储器中,用于存储普通数据 
目录文件d用于存放目录项,是文件系统管理的重要文件类型
管道文件p 一种用于进程间通信的特殊文件,也被称为命名管道FIFO
套接字文件s 一种用于网络间通信的特殊文件  
链接文件l 用于间接访问另外一个目标文件,相当于windows系统快捷方式
字符设备文件c字符设备在应用层的访问接口(以字符为单位,跟系统进行数据交换的设备,比如:键盘、鼠标、触摸屏等) 
块设备文件b块设备在应用层的访问接口  (以块为单位(256、512、1024字节为一块)),跟系统进行数据交换的设备,比如:U盘、内存、硬盘等

在Linux中输入以下指令可以看到目录文件的类型

ls -l


一、系统IO

文件操作符(int fd)的说明

说明:函数open()的返回值,是一个整型int数据,这个整型数据,实际上是内核中的一个称为fd_array的数组的下标

图解:

解释:打开文件时,内核产生一个指向file{}的指针,并将该指针放入一个位于file_struct{}的数组fd_array[]中,而该指针所在的数组的下标,就被open()函数返回给用户,用户把这个数组下标称为文件描述符(fd)

结论:文件描述符从0开始,每打开一个文件,就产生一个新的文件描述符可以重复打开同一个文件,每次打开文件都会使用内核产生系列结构体,并得到不同的文件描述符由于系统在每一个进程开始运行时,都默认打开了一次键盘、两次屏幕,因此0、1、2描述符分别代表标准输入、标准输出和标准出错

操作:

(1)文件的打开

C代码:

关键点:
open函数有两个版本,一个有两个参数,一个有三个参数
当打开一个已存在的文件时,指定两个参数即可(使用两个参数的那个open即可)
当创建一个新文件,需要用第三个参数指定新文件的权限,否则新文件的权限是随机值(系统会帮你配置)
模式flags,可以使用位或的方式,来同时指定多个模式
模式flags,O_NOCTTY主要用在后台精灵进程,阻止这些精灵进程拥有控制终端。 

(大家根据自己的实际文件操作需求来选择打开文件的方式)

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

int main(int argc, char const *argv[])
{
    int fd = 0;     // 文件描述符

    // 一、以下三种打开方式,都要求文件已存在,否则失败返回 (知道文件存在)
    fd = open("1.txt", O_RDWR);      // 以可读可写方式打开文件
    fd = open("1.txt", O_RDONLY);    // 以只读方式打开文件
    fd = open("1.txt", O_WRONLY);    // 以只写方式打开文件

    // 二、以下三种打开方式,如果文件不存在,则创建文件,并设置其权限为0644。 存在则返回错误信息 (你想要确保文件存在)
    fd = open("1.txt", O_RDWR|O_CREAT|O_EXCL,   0644);    // 以可读可写方式打开文件, 若文件不存在,就创建该文件,若文件存在则返回错误信息
    fd = open("1.txt", O_RDONLY|O_CREAT|O_EXCL, 0644);    // 以只读方式打开文件, 若文件不存在,就创建该文件,若文件存在则返回错误信息
    fd = open("1.txt", O_WRONLY|O_CREAT|O_EXCL, 0644);    // 以只写方式打开文件, 若文件不存在,就创建该文件,若文件存在则返回错误信息

    // 三、以下三种打开方式,如果文件不存在,则创建文件,并清空里面的信息,并设置其权限为0644   (你想要确保文件存在,并且里面是没有东西的)
    fd = open("1.txt", O_RDWR|O_CREAT|O_TRUNC,   0644);    // 以可读可写方式打开文件, 若文件不存在,就创建该文件,并清空里面的信息
    fd = open("1.txt", O_RDONLY|O_CREAT|O_TRUNC, 0644);    // 以只读方式打开文件, 若文件不存在,就创建该文件,并清空里面的信息
    fd = open("1.txt", O_WRONLY|O_CREAT|O_TRUNC, 0644);    // 以只写方式打开文件, 若文件不存在,就创建该文件,并清空里面的信息

    // 四、以下两种打开方式,都要求文件已存在,否则失败返回,并追加文件内容   (知道文件存在,并且想要追加数据到里面去)      
    fd = open("1.txt", O_RDWR|O_APPEND);                   // 以可读可写方式打开文件, 并追加文件内容
    fd = open("1.txt", O_WRONLY|O_APPEND);                 // 以只写方式打开文件,并追加文件内容

    return 0;
}

(2)文件的读取、写入

关键点:
参数count是读写字节数的愿望值,实际读写成功的字节数由返回值决定
读取普通文件时,如果当读到了文件末尾,read()会返回0
读取管道文件时,如果管道中没有数据,read()会默认阻塞(相当于scanf) --- 系统编程的时候才学


演示:读取和打印文件

#include <stdio.h>
// open函数
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
// read函数、close
#include <unistd.h>

// 全局错误码声明所在的文件
#include <errno.h>  

// strerror函数
#include <string.h>  


// 主函数
int main(int argc, char const *argv[])
{

    // 0、判断命令行参数
    if (argc != 2)
        printf("命令行参数错误,格式为(./可执行文件 要打开文件)!\n");

    
    // 1、打开文件(以只读的方式打开)
    int fd = open(argv[1], O_RDONLY);
    if ( -1 == fd )
    {
        printf("错误信息:打开%s文件失败,  错误原因:%s,  错误位置:%d行\n", argv[1], strerror(errno), __LINE__);
        return -1;
    }

    // 2、读取文件里面的信息,并打印出来
    char buf[128] = {0};
    int  ret = 0;

    while (1)
    {
        // 清空buf里面的数据,防止对下一次的读取数据造成干扰
        bzero(buf, sizeof(buf));          // 如果在windows下模拟linux环境报错将 bzero(buf, sizeof(buf)); 替换为:memset(buf, 0, sizeof(buf));

        // 每次最多读取128个字节
        ret = read(fd, buf, sizeof(buf)); 

        // 如果返回0,则证明文件已被读取完毕
        if (ret == 0)
        {
            printf("啊,我读完了!\n");
            break;
        }

        // 打印读取的数据
        printf("%s", buf);
    }
    

    // 3、关闭文件
    close(fd);

    return 0;
}

结果:(乱码是因为buf一次都128字节,文本没那么多,可以自行根据文件内容来修改buf[ ]大小,如果读者是在Linux环境下,a.exe应为a.out)

演示:读取和写入文件

#include <stdio.h>
// open函数
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
// read函数、 write函数、close函数
#include <unistd.h>

// 全局错误码声明所在的文件
#include <errno.h>  

// strerror函数
#include <string.h>  

int main(int argc, char const *argv[])
{
   // 0、判断命令行参数
    if (argc != 2)
        printf("命令行参数错误,格式为(./可执行文件 要打开文件)!\n");

    
    // 1、打开文件(以可读可写的方式打开)
    int fd = open(argv[1], O_RDWR);
    if ( -1 == fd )
    {
        printf("错误信息:打开%s文件失败,  错误原因:%s,  错误位置:%d行\n", argv[1], strerror(errno), __LINE__);
        return -1;
    }

    // 2、读取文件里面的信息,并打印出来
    char buf[128] = {0};
    int  ret = 0;

    while (1)
    {
        // 清空buf里面的数据,防止对下一次的读取数据造成干扰
        //bzero(buf, sizeof(buf));
        memset(buf, 0, sizeof(buf));

        // 每次最多读取128个字节
        ret = read(fd, buf, sizeof(buf)); 

        // 如果返回0,则证明文件已被读取完毕
        if (ret == 0)
        {
            printf("\n");
            break;
        }

        // 打印读取的数据
        printf("%s", buf);
    }
    
    // 2、将数据写入文件中
    // 清空buf里面的数据,防止对下一次的写入数据造成干扰
    bzero(buf, sizeof(buf));
    //windows模拟linux环境bzero报错用这个 memset(buf, 0, sizeof(buf));

    // 从键盘输入数据
    printf("请继续输入!:\n");
    scanf("%[^\n]", buf);      // 直到遇到'\n'才退出
    while(getchar()!='\n');

    // 向文件写入数据
    write(fd, buf, strlen(buf));

    // 4、关闭文件
    close(fd);

    return 0;
}

结果(每次执行都是读+写):

(2.1)位偏量概念

关键点:
lseek函数可以将文件位置调整到任意的位置,可以是已有数据的地方,也可以是未有数据的地方,假设调整到文件末尾之后的某个地方,那么文件将会形成所谓的"空洞" 
lseek函数只能对普通文件调整文件位置,不能对管道文件调整  
lseek函数的返回值是调整后的文件位置距离文件开头的偏移量,单位是字节

ps :通常使用lseek()通过文件内容的基准点获取文件大小、使用lseek()定义文本输入光标位置。

#include <stdio.h>
// open函数
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
// read函数、 write函数、close函数
#include <unistd.h>

// 全局错误码声明所在的文件
#include <errno.h>  

// strerror函数
#include <string.h>  

int main(int argc, char const *argv[])
{
   // 0、判断命令行参数
    if (argc != 2)
        printf("命令行参数错误,格式为(./可执行文件 要打开文件)!\n");
    
    // 1、打开文件(以可读可写的方式打开)
    int fd = open(argv[1], O_RDWR);
    if ( -1 == fd )
    {
        printf("错误信息:打开%s文件失败,  错误原因:%s,  错误位置:%d行\n", argv[1], strerror(errno), __LINE__);
        return -1;
    }
    off_t ret_len = lseek(fd, 0, SEEK_END); // 将文件位置偏移到文件末尾,并只移动0字节
    printf("%ld\n",ret_len);
    // 4、关闭文件
    close(fd);

    return 0;
}

运行结果:

(3)关闭文件

close(fd)

(4)系统IO的API函数(感兴趣可以查阅相关资料了解一下这两函数)

(4.1)内存映射函数mmap

说明:该函数全称是memory map,意为内存映射,即将某个文件与某块内存关联起来,达到通过操作这块内存来进阶操作其所对应的文件的效果。

关键点:
mmap函数的flag是参数是有很多的,上表只罗列了最简单的几个,详细信息请使用man手册进行查询
mmap函数理论上可以对任意文件进行映射的,但通常用来映射一些比较特殊的设备文件,比如液晶屏LCD

ps :解除映射 munmap()

(4.2)控制驱动函数

说明:该函数是沟通应用层和驱动层的有力武器,底层开发人员在为硬件设备编写驱动的时候,常常将某些操作封装为一个函数,并为这些接口提供一个所谓的命令字,应用层开发者可以通过 ioctl() 函数配合命令字,非常迅捷地绕过操作系统中间层层机构直达驱动层,调用对应的功能。从这个意义上讲,函数 ioctl() 像是一个通道,只提供函数调用路径,具体的功能由所谓命令字决定,下面是函数的接口规范说明:

关键点:
request 就是所谓的命令字。
底层驱动开发者可以自定义命令字。
对于某些常见的硬件设备的常见功能,系统提供了规范的命令字。
 

二、标准IO

(1)文件的打开与关闭

6种 打开方式

#include <stdio.h>
// 全局错误码声明所在的文件
#include <errno.h> 
// strerror函数
#include <string.h> 


int main(int argc, char const *argv[])
{

    FILE *fp = NULL;

    // 一、打开文件
    // (1)、打开文件(方式1:要求文件必须存在)
    // 1、以只读方式打开
    fp = fopen("./1.txt", "r");
    if (fp == NULL)
    {
        printf("错误信息:打开1.txt文件失败,  错误原因:%s,  错误位置:%d行\n", strerror(errno), __LINE__);
        return -1;
    }
    else
    {
         printf("使用fopen函数,打开文件成功!\n");
    }


    // 2、以读写方式打开
    fp = fopen("./1.txt", "r+");
    
    // (2)、打开文件(方式2:文件可以不存在,如果文件不存在就创建文件,并清空文件)
    // 1、以只写方式打开
    fp = fopen("./1.txt", "w");
    
    // 2、以读写方式打开
    fp = fopen("./1.txt", "w+");

    // (3)、打开文件(方式3:文件可以不存在,如果文件不存在就创建文件,并清空文件,以追加的方式打开)
    // 1、以只写方式打开
    fp = fopen("./1.txt", "a");
    
    // 2、以读写方式打开
    fp = fopen("./1.txt", "a+");


    // 二、关闭文件
    int ret = fclose(fp);         // 关闭一般不判断返回值
    if (ret == EOF)                
    {
        printf("错误信息:打开1.txt文件失败,  错误原因:%s,  错误位置:%d行\n", strerror(errno), __LINE__);
        return -2;
    }
    else
    {
        printf("使用fclose函数,关闭文件成功!\n");
    }
    
    return 0;
}

结果(同一文件注意只能选择一种打开方式):

(2)读写文件

标准IO提供了多种读文件的方式:

(2.1)按字节读写

#include <stdio.h>
// 全局错误码声明所在的文件
#include <errno.h> 
// strerror函数
#include <string.h> 

int main(int argc, char const *argv[])
{

    // 0、判断命令行参数
    if (argc != 2)
        printf("命令行参数错误,格式为(./可执行文件 要复制的文件路径 要粘贴的文件路径)!\n");

    // 1、打开文件(文件可以不存在,如果文件不存在就创建文件,并清空文件,以读写权限)
    FILE *fp =  fopen(argv[1], "w+");
    if (fp == NULL)
    {
        printf("错误信息:打开%s文件失败,  错误原因:%s,  错误位置:%d行\n", argv[1], strerror(errno), __LINE__);
        return -1;
    }
    

    // 2、写
    // a、fputc:   函数(推荐)
    // b、putc:    宏
    // c、putchar: 只针对标准输出(屏幕)
    char w_buf[128] = "赞美万机之神欧姆弥赛亚!";

    int i = 0;
    while (w_buf[i] != '\0')            // 没有数据写入也退出
    {
        int ret = fputc(w_buf[i], fp);
        if (ret == EOF )                // 写入出错就退出
            break;
        i++;
    }

    // 获取当前位置的偏移量,重新移位至文件开头处
    long ret_t =  ftell(fp);
    printf("ret_t == %ld 字节\n", ret_t);
    fseek(fp, 0, SEEK_SET);


    // 3、读
    // a、fgetc:   函数(推荐)
    // b、getc:    宏
    // c、getchar: 只针对标注输入(键盘)
    char r_buf[128] = {0};
    int len = sizeof(r_buf)-1;
    int j = 0;

    while (j <= len)                    // 防止溢出
    {
        int ret = fgetc(fp);
        if (ret == EOF)                 // 已经读到文件末尾,或者出错就退出
            break;
        r_buf[j] = ret;
        
        j++;
    }
    printf("r_buf == %s\n", r_buf);
    
    // 4、关闭文件
    fclose(fp);

    return 0;
}

(2.2)按行读写

#include <stdio.h>
// 全局错误码声明所在的文件
#include <errno.h> 
// strerror函数
#include <string.h> 

int main(int argc, char const *argv[])
{
    // 0、判断命令行参数
    if (argc != 2)
        printf("命令行参数错误,格式为(./可执行文件 要复制的文件路径 要粘贴的文件路径)!\n");

    // 1、打开文件(文件可以不存在,如果文件不存在就创建文件,并清空文件,以读写权限)
    FILE *fp =  fopen(argv[1], "w+");
    if (fp == NULL)
    {
        printf("错误信息:打开%s文件失败,  错误原因:%s,  错误位置:%d行\n", argv[1], strerror(errno), __LINE__);
        return -1;
    }


    // 2、写
    // a、fputs:函数(推荐)
    // b、puts: 宏
    char w_buf[128]     = {0};
    char w_buf_tmp[256] = {0};

    while (1)
    {
        // 1、从键盘输入要写入的数据
        printf("请输入要写入的字符串:\n");
        scanf("%[^\n]", w_buf);
        while(getchar()!='\n');

        if (strcmp(w_buf, "exit") == 0)
            break;

        // 2、将w_buf数据写入到文本中
        sprintf(w_buf_tmp, "%s\n", w_buf);
        fputs(w_buf_tmp, fp);

        // 3、清空w_buf数据
        bzero(w_buf, sizeof(w_buf));
        bzero(w_buf_tmp, sizeof(w_buf_tmp));
        
    }
    

    // 3、获取当前位置的偏移量,重新移至文件开头处
    long ret_t =  ftell(fp);
    printf("ret_t == %ld\n", ret_t);
    fseek(fp, 0, SEEK_SET);

    // 4、读
    // a、fgets:函数(推荐)
    // b、gets: 宏、
    char r_buf[256] = {0};
    while (1)
    {
        fgets(r_buf, sizeof(r_buf), fp);
        printf("r_buf == %s\n", r_buf);

        // 判断文件是否读取完毕
        if (feof(fp))
        {
            printf("文件内容已读完!\n");
            break;
        }

        // 判断文件是否读取有误
        if (ferror(fp))
        {
            printf("读操作遇到错误!\n");
            break;
        }
        
    }
    
    // 4、关闭文件
    fclose(fp);

    return 0;
}

(2.3)因为篇幅问题,下面的内容感兴趣的读者可以去了解相关资料)
按自己设置的格式读写

按数据块读写(速度快)

(3)标准缓冲区

说明:标准IO实际上是系统IO的封装,这种封装体现如下图所示,fopen()函数将调用open()函数得到的文件描述符填入结构体FILE中,并为文件分配缓冲区,设置缓冲区类型,最后给用户返回指向FILE的指针fp,成为文件指针
图示:
每当使用标准IO的写操作函数,试图将数据写入文件a.txt时,数据都会流过缓冲区,然后再在适当的时刻冲洗(或刷新,flush)到内核,最后才真正写入设备文件

按照缓冲区什么时候冲洗数据到内核,分为三类:
不缓冲类型:
一旦有数据,立刻将数据冲洗到文件
全缓冲类型:
一旦填满缓冲区,立刻将数据冲洗到文件
程序正常退出时,立刻将数据冲洗到文件
遇到fllush()函数强制冲洗时,立刻将数据冲洗到文件
关闭文件时,立刻将数据冲洗到文件
读取文件内容时,立刻将数据冲洗到文件
改变缓冲区类型时,立刻将数据冲洗到文件
行缓冲类型:
同全缓冲类型
一旦遇到'\n',立刻将数据冲洗到文件
关键点:
缓冲(buffer)都是针对写操作而言的,缓冲的存在时为了提高写效率
对于标准输出而言,默认是行缓冲
滞留在缓冲区中的数据有时被称为脏数据(dirty data), 脏数据的存在代表程序操作的结果与文件真实状态不一致,若未正常冲洗这些数据就退出程序则有可能会造成数据丢失
这三种缓冲类型,可以通过函数setbuf/setvbuf来修改

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值