Linux应用开发(二)——Linux的文件IO

一、文件IO的理解

1、标准IO与文件IO的比较

2、文件描述符

  • 顺序分配的非负整数
  • 内核用以标识一个特定进程正在访问的文件。
  • 其他资源(socket、pipe等)的访问标识
  • 标准输入(0)、标准输出(1)和标准出错(2)
  • 由shell默认打开,分别为0/1/2
  • 标准IO使用流(FILE*)的概念去操作文件
  • 文件IO使用非负整数(文件描述符)的概念去操作文件

3、没有缓存的I/O

  • 通过文件描述符进行访问
  • open()/read()/write()/lseek()/close()

4、标准I/O

  • 通过FILE*进行访问
  • fopen()/fread()/fwrite()/fseek()/fclose()

5、系统调用和C库函数

  • 系统调用就是linux操作系统对外提供的一套函数, 大约有500多个函数, 文件IO是属于系统调用函数库的一部分
  • 库函数是C语言库的标准, 标准IO是属于C库函数的一部分
  • 标准IO到最后都会转成文件IO,来操作系统。
  • 以linux系统为例, 内核就是linux的核心, 也是操作系统的核心, 对外提供了一套api(应用程序编程接口,其实就是一套函数),这个api就是系统调用。
  • 用户开发程序可以使用2种方式编写程序,第一种(文件IO编程): 直接使用系统调用, 就不再使用标准库了, 文件IO就是属于直接调用操作系统的api编程。第二种(标准IO编程): 先调用C标准库,C标准库再调用操作系统的系统调用(api) , 这就是使用标准IO进行编程
  • 使用标准IO编程便于移植, 因为不同的操作系统都实现了C标准库, 要使用文件IO编程时, 只是针对与Linux系统, window的就不适用了。

二、打开文件open

cpp 复制代码
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);

函数功能:打开一个文件

函数参数:【pathname】 要打开文件的名称 , "/home/linux/1.c" , "1.txt"。【flags 】O_RDONLY:只读方式打开文件 , RD(read)。O_WRONLY:可写方式打开文件,WR(write)。O_RDWR:读写方式打开文件,RDWR(read write)。以上三个参数互斥。O_CREAT:如果该文件不存在,就创建一个新的文件,并用第三的参数为其设置权限。O_EXCL:如果使用O_CREAT时文件存在,则可返回错误消息。这一参数可测试文件是否存在。O_TRUNC:Truncate,如文件已经存在,那么打开文件时先删除文件中原有数据。O_APPEND:以添加方式打开文件,所以对文件的写操作都在文件的末尾进行。【mode】被打开文件的存取权限,为8进制表示法。

函数返回值,成功返回一个文件标识符, 非负整数。失败返回:错误时返回 -1 , 并设置errno,-1 if an error occurred (in which case, errno is set appropriately).

cpp 复制代码
int fd ; // 定义一个文件表述符号 file descriptor
//fopen("1.txt","r") 只读方式打开文件, 文件不存在, 报错
//fd = open("1.txt",O_RDONLY|O_EXCL) ;

//fopen("1.txt","r+") 可读可写方式打开文件, 文件不存在, 报错
//fd = open("1.txt",O_RDWR|O_EXCL) ;

//fopen("1.txt","w") 只写方式打开文件, 文件不存在则创建文件, 文件存在清空文件内容
//fd = open("1.txt",O_WRONLY|O_CREAT|O_TRUNC,0664) ;

//fopen("1.txt","w+") 可读可写方式打开文件, 文件不存在则创建文件, 文件存在清空文件内容
//fd = open("1.txt",O_RDWR|O_CREAT|O_TRUNC,0664) ;

//fopen("1.txt","a") 只写方式打开文件, 文件不存在则创建文件, 文件存在不清空文件内容, 光标移动文件末尾
//fd = open("1.txt",O_WRONLY|O_CREAT|O_APPEND,0664) ;

//fopen("1.txt","a+") 可读可写方式打开文件, 文件不存在则创建文件, 文件存在不清空文件内容, 光标移动文件末尾
//fd = open("1.txt",O_RDWR|O_CREAT|O_APPEND,0664) ;

如下图所示缺少头文件

在终端输入"man 2 open"可以看到open中相关头文件的信息

cpp 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main(int argc, char const* argv[])
{
    if(argc != 2)
    {
        printf("Usage: %s filename file1\n", argv[0]);
        return -1;
    }

    int fd = open(argv[1], O_RDWR);//只读方式打开文件, 文件不存在报错
    if(fd == -1)
    {
        perror("open");
        return -1;
    }

    printf("fd = %d\n", fd);
    close(fd);
    return 0;
}

三、关闭文件close

cpp 复制代码
int close(int fd);

函数功能:关闭一个已经打开的文件

函数参数:【fd】要关闭的文件

函数返回值:成功返回 0,returns zero on success。失败返回,错误时返回 -1 , 并设置errno,On error, -1 is returned, and errno is set appropriately。

四、读写文件read/write

1、read函数

cpp 复制代码
ssize_t read(int fd, void *buf, size_t count);

函数功能:从已经打开的文件中读取指定字节数的内容

函数参数:【fd】要读的文件;【buf】读回来的字节存放的内存地址;【count】要读的字节数

函数返回值:成功返回,返回实际读回来的字节数,返回0 表示读到文件末尾,On success, the number of bytes read is returned (zero indicates end of file)。失败返回,错误时返回 -1 , 并设置errno,on error, -1 is returned, and errno is set appropriately。

2、write 函数

cpp 复制代码
ssize_t write(int fd, const void *buf, size_t count);

函数功能:把数组中的内容写入到文件内

函数参数:【fd】要写的文件,【buf】要写入文件数据的内存地址,【count】要写的字节数

函数返回值:成功返回,返回实际写入文件的字节数,返回0 表示什么也没写入文件内。失败返回,错误时返回 -1 , 并设置errno,on error, -1 is returned, and errno is set appropriately。

3、实例read实现cat

cpp 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main(int argc, char const* argv[])
{
    if(argc != 2)
    {
        printf("Usage: %s filename file1\n", argv[0]);
        return -1;
    }

    int fd = open(argv[1], O_RDWR);//只读方式打开文件, 文件不存在报错
    if(fd == -1)
    {
        perror("open");
        return -1;
    }

    char buf[128] = {0};
    int nbytes = 0;
    while((nbytes = read(fd, buf, 128)) > 0)//读取文件内容, 直到文件结束
    {
        if(write(1, buf, nbytes)==-1)
        {
            perror("write");
            return -1;
        }
    }
    close(fd);
    
    return 0;
}

4、实例read和write实现cp

cpp 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main(int argc, char const* argv[])
{
    if(argc != 3)
    {
        printf("Usage: %s filename\n", argv[0]);
        return -1;
    }

    int fp_src = open(argv[1], O_RDWR);//只读方式打开文件, 文件不存在, 报错
    if(fp_src == -1)
    {
        perror("open");
        return -1;
    }

    int fp_dst = open(argv[2], O_WRONLY | O_CREAT | O_TRUNC, 0644);//只写方式打开文件, 文件不存在, 报错
    if(fp_dst == -1)
    {
        perror("open");
        return -1;
    }

    char buf[128] = {0};
    int nbytes = 0;
    while((nbytes = read(fp_src, buf, 128)) > 0)//读取文件内容, 直到文件结束
    {
        if(write(fp_dst, buf, nbytes)==-1)
        {
            perror("write");
            return -1;
        }
    }
    close(fp_dst);
    close(fp_src);

    return 0;
}

五、定位文件lseek

1、lseek函数

这个函数等于 ftell+fseek = lseek

cpp 复制代码
off_t lseek(int fd, off_t offset, int whence);

函数功能:设置文件的位置

函数参数:fd : 要操作的文件;offset : 基于whence 的偏移量, 以字节计算偏移量;whence,SEEK_SET : 设置文件指定的位置,SEEK_CUR : 基于当前的文件位置,SEEK_END : 基于文件末尾的位置

函数返回值:成功返回: 文件的位置。失败返回:错误时返回 -1 , 并设置errno,Otherwise, -1 is returned and errno is set to indicate the error.

2、实例lseek拆分文件

cpp 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main(int argc, char const* argv[])
{
    if(argc != 4)
    {
        printf("Usage: %s file1 file2 file3\n", argv[0]);
        return -1;
    }

    FILE* fp1 = open(argv[1], O_RDONLY);//只读方式打开文件, 文件不存在, 报错
    if(fp1 == -1)
    {
        perror("open");
        return -1;
    }

    FILE* fp2 = open(argv[2], O_WRONLY|O_CREAT|O_TRUNC, 0644);//只写方式打开文件
    if(fp2 == -1)
    {
        perror("open");
        return -1;
    }

    FILE* fp3 = open(argv[3], O_WRONLY|O_CREAT|O_TRUNC, 0644);//只写方式打开文件
    if(fp3 == -1)
    {
        perror("open");
        return -1;
    }

    //计算file1的大小
    long size = lseek(fp1, 0, SEEK_END);
    printf("size = %ld\n", size);
    lseek(fp1, 0, SEEK_SET);//将文件指针移动到文件开头

    //把file1的前一半内容复制到file2中
    char ch = 0;
    for(int i = 0; i < size/2; i++)
    {
        read(fp1, &ch, 1);
        write(fp2, &ch, 1);
    }

    //把file1的后一半内容复制到file3中
    for(int i = size/2; i < size; i++)
    {
        read(fp1, &ch, 1);
        write(fp3, &ch, 1);
    }
    close(fp1);
    close(fp2);
    close(fp3);
    return 0;
}

3、实例lseek文件逆序

cpp 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main(int argc, char const* argv[])
{
    if(argc != 2)
    {
        printf("Usage: %s filename file1\n", argv[0]);
        return -1;
    }

    int fd = open(argv[1], O_RDWR);
    if(fd == -1)
    {
        perror("open");
        return -1;
    }

    //使用lseek计算file1的大小
    long size = lseek(fd, 0, SEEK_END);
    printf("size = %ld\n", size);
    lseek(fd, 0, SEEK_SET);//将文件指针移动到文件开头

    int t1, t2;
    //使用lseek函数把file1进行逆序输出到file1中
    for(long i = 0; i < size/2 ; i++)
    {
        //将光标移动到第i个字符
        lseek(fd, i, SEEK_SET);
        
        //读取光标位置的字符
        read(fd, &t1, 1);

        //把光标移动到第size-i-1个字符
        lseek(fd, size-i-1, SEEK_SET);

        //读取光标位置的字符
        read(fd, &t2, 1);

        //把光标移动到i的位置
        lseek(fd, i, SEEK_SET);

        //把t2写入到光标的位置
        write(fd, &t2, 1);

        //把光标移动到第size-i-1个字符
        lseek(fd, size-i-1, SEEK_SET);

        //把t1写入到光标的位置
        write(fd, &t1, 1);
    }

    close(fd);

    return 0;
}

六、标准IO和文件IO对比

1、fdopen 函数

cpp 复制代码
FILE *fdopen(int fd, const char *mode);

函数功能:把已经打开的文件IO转换成标准IO , 便于使用fgetc,fgets,fread等函数

函数参数:fd : 要转化的文件,mode :要重新打开的权限 , 等同于 fopen

函数返回值:成功返回,返回文件指针;失败返回,错误时返回NULL , 并设置errno,Otherwise, NULL is returned and errno is set to indicate the errorshixi

2、缓存

  • 缓冲文件系统(高级磁盘IO)

目的:尽量减少使用read/write的调用

定义:系统自动的在内存中为每一个正在使用的文件开辟一个缓冲区,从内存向磁盘输出数据必须先送到内存缓冲区,装满缓冲区在一起送到磁盘中去。从磁盘中读数据,则一次从磁盘文件将一批数据读入到内存缓冲区中,然后再从缓冲区逐个的将数据送到程序的数据区。

分类:全缓存,行缓存,不缓存。

  • 非缓冲文件系统 (低级磁盘IO)

定义:依靠于操作系统,通过操作系统的功能对文件进行读写,是系统级的输入输出。

不做任何缓存

3、全缓存

  • 标准IO的读写默认有一个缓存, 这个缓存大小是4096
  • 当填满I/O缓存后才进行实际I/O操作,或者满足一定条件后,系统通过调用malloc来获得所需要的缓冲区域,默认值4096。

刷新(fflush):标准I/O的写操作。

当缓冲区满了,或者满足一定的条件后,就会执行刷新操作。

4、测试全缓存的大小

cpp 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main(int argc, char const* argv[])
{
    if(argc != 2)
    {
        printf("Usage: %s filename\n", argv[0]);
        return -1;
    }

    FILE* fp = fopen(argv[1], "r+");//只读写方式打开文件, 文件不存在, 报错
    if(fp == NULL)
    {
        perror("fopen");
        return -1;
    }
    
    int i = 0;
    char buf[100];
    int ret = 0;
    while(1)
    {
        i++;
        sprintf(buf, "i = %08d", i);
        ret = fwrite(buf, sizeof(char), 10, fp);
        if(ret < 0)
        {
            perror("fwrite");
            return -1;
        }
        usleep(1000*50);//50ms
    }
    return 0;
}

5、实例测试缓存的概念

把标准IO更换成文件IO , 因为文件IO就是无缓存的。

6、强制刷新缓存 fflush

cpp 复制代码
int fflush(FILE *stream);

函数功能:强制刷新(同步)一个实际的操作(读或则写的操作)

函数参数:stream: 要同步的文件

函数返回值:成功返回,返回 0;失败返回,错误时返回EOF,并设置errno,Otherwise, EOF is returned and errno is set to indicate the error.

实时刷新写数据

7、行缓存

  • 行缓存的默认大小为1024字节
  • 当在输入和输出中遇到新行符('\n')时,进行I/O操作。
  • 当流遇到一个终端时,典型的行缓存。
  • 对于标准IO来说,stdin 和stdout 就是行缓存
  • 对于文件IO来说, 0 和1 就是行缓存
  • 行缓存也可以使用fflush进行强制刷新

测试标准输出stdout或者1的输出情况

cpp 复制代码
#include <stdio.h>

int main(int argc, char *argv[])
{
    int i = 0;
    char buf[100];
    int ret = 0;
    while(1)
    {
        i++;
        sprintf(buf, "i=%08d", i);
        ret = fwrite(buf, sizeof(char), 10, stdout);//标准输出缓冲区是行缓冲,所以每次写入10个字符节
        if(ret<0)
        {
            perror("fwrite");
            break;
        }
        //fflush(stdout);//刷新标准输出缓冲区,确保数据写入终端
        //printf("\n");//手动换行,确保数据立即写入终端
        usleep(1000*100);
    }
    return 0; 
}

刷新 IO的两种办法,fflush(stdout); // 实时同步数据到标准输出,输出时加上'\n'

8、exit与_exit函数

  • exit函数退出程序并刷新行缓存和全缓存
  • _exit函数退出程序并不做任何刷新操作
cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{
    // 1. 通过标准输出, 输出数据,缓存区没有满, 不输出
    printf("hello world!!");

    // 2. 强制刷新行缓存 fflush
    //fflush(stdout);

    // 3. 使用'\n' 来刷新缓存
    //printf("\n");

    // 4. 函数正常返回, 也会进行行缓存和全缓存的刷新
    // return 0;

    // 5. exit 程序退出时, 也会进行行缓存和全缓存的刷新
    //exit(0);

    // 6. _exit 程序退出时, 不进行任何操作,不刷新任何缓存
    _exit(0);
    return 0;
}

使用标准IO实现,编程读写一个文件test.txt,每隔1秒向文件中写入一行数据,类似这样:

cpp 复制代码
#include <stdio.h>
#include <time.h>
#include <unistd.h>

int main(int argc, char *argv[])
{
    FILE* fp = fopen("test.txt", "a+");
    if(fp == NULL)
    {
        perror("fopen");
        return -1;
    }

    char time_str[64]={0};
    int lineNo = 0;
    
    //计算文件内有多少行
    char buf[1024]={0};
    while(fgets(buf, 1024, fp)!=NULL)
    {
        lineNo++;
    }


    while(1)
    {
        time_t t = time(NULL);
        strftime(time_str, sizeof(time_str), "%Y-%m-%d %H:%M:%S", localtime(&t));
        fprintf(fp, "%d, %s\n", lineNo++, time_str);
        fflush(fp);
        sleep(1);
    }

    return 0; 
}

使用fdopen函数把文件IO转成标准IO,再去使用fgets函数。

cpp 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <time.h>
#include <unistd.h>

int main(int argc, char *argv[])
{
    int fd = open("test.txt", O_RDWR|O_CREAT|O_APPEND, 0644);
    if(fd == -1)
    {
        perror("open");
        return -1;
    }

    char time_str[128]={0};
    int lineNo = 0;

    FILE* fp = fdopen(fd, "r+");
    //计算文件内有多少行
    char buf[1024]={0};
    while(fgets(buf, 1024, fp)!=NULL)
    {
        lineNo++;
    }


    while(1)
    {
        time_t t = time(NULL);
        strftime(time_str, sizeof(time_str), "%Y-%m-%d %H:%M:%S", localtime(&t));
        sprintf(buf, "%d, %s\n", lineNo++, time_str);
        ssize_t ret = write(fd, buf, strlen(buf));
        if(ret == -1)
        {
            perror("write");
            return -1;
        }
        sleep(1);
    }

    return 0; 
}

9、fopen a+ 方式打开的细节

打开文件都是向文件添加数据,就是以a+方式打开,用ftell函数直接得到当前位置,如果不为0,则说明文件不为空;但调试发现打开文件后用ftell函数得到的当前位置值为零?

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char const *argv[])
{
    FILE *fp;
    if (argc != 2) // 01-fopen filename
    {
        printf("错误:运行程序时请带入参数(./app filename)\n");
        exit(-1);
    }
    // argv[0] = ./01-fopen
    // argv[1] = filename

    // fp = fopen(argv[1],"a+") ; // 可读可写方式打开文件,文件不存在会自动创建文件,文件存在则在文件末尾追加内容
    fp = fopen(argv[1], "a"); // 可读可写方式打开文件,文件不存在会自动创建文件,文件存在则在文件末尾追加内容
    if (fp == NULL)
    {
        perror("fopen");
        exit(-1);
    }
    // a : 不可读, 可写, 不存在则创建, 不能修改文件的原有内容, 只能在文件末尾追加写,文件指针无效
    // 读文件: 文件指针在文件末尾
    // 写文件: 文件指针在文件末尾

    // a+ : 可读可写 , 不存在则创建,不能修改文件的原有内容,只能在文件末尾追加
    // 特殊点: 用于读取原始文件位置在文件的开头
    // 读文件: 文件指针在文件开头
    // 写文件: 文件指针在文件末尾

    // 读文件时 使用a时 , 文件位置在末尾, 得到的结果就是文件的大小
    // 读文件时 使用a+时 ,文件位置在开头, 得到的结果就是0

    long file_pos = ftell(fp);
    printf("filesize:%ld\n", file_pos);
    fclose(fp);

    return 0;
}

10、sprintf 格式输出转换

cpp 复制代码
int sprintf(char *str, const char *format, ...);

函数功能:把格式字符串输出到指定的内存中

函数参数:str,要把指定格式的字符串保保存的位置;format,格式字符串, 用法和printf一致;...,可变参数, 和printf函数的参数一致。

函数返回值:成功返回,输出的字节数;失败返回,错误时返回负数,If an output error is encountered, a negative value is returned.

11、fprintf 格式输出转换

cpp 复制代码
int fprintf(FILE *stream, const char *format, ...);

函数功能:把格式字符串输出到流(标准IO的文件)中,把指定格式的字符串写入到文件内

函数参数:stream,要写入的流(标准IO的文件);format,格式字符串, 用法和printf一致;...,可变参数, 和printf函数的参数一致。

函数返回值:成功返回,输出的字节数;失败返回,错误时返回负数,If an output error is encountered, a negative value is returned.

相关推荐
Horn Still Sounds2 小时前
Linux网络并发服务器模型与SQLite数据库学习笔记
linux·服务器·数据库
10mAh2 小时前
【Linux】CPU 100% 怎么排查?——top、pidstat、jstack 到线程定位实战
linux·运维·服务器
深念Y2 小时前
开机启动优化记录
linux·开机
Titan20243 小时前
网络基础:传输层协议知识梳理
linux·服务器·网络
一叶龙洲3 小时前
Ubuntu从图标到卸载应用
linux·windows·ubuntu
爱吃提升3 小时前
VMware虚拟机迁移系统磁盘完整教程(VMware Workstation17)
linux·运维·服务器
2401_868534783 小时前
网规备考_5.4 防火墙及访问控制技术
linux·网络协议
wuminyu3 小时前
Linux内核线程调度与虚拟线程调度机制系统级深度剖析
java·linux·c语言·jvm·c++
诸葛李3 小时前
linux 配置
linux·运维·服务器