• Linux文件I/O


    下面的内容需要了解系统调用,可看下面的链接:

    系统调用来龙去脉-CSDN博客

    1.底层文件IO和标准IO

    这里指的是操作系统提供的IO服务,不同于ANSI建立的标准IO。

    底层IO和标准IO各自所使用的函数:

    区别:

    1.底层文件IO不带用户级缓存,称为unbuffered I/O,每次操作都会执行相关系统调用,这一过程系统消耗资源大,而且时间也比较长。

    而标准IO则带有三种缓冲机制,可以对缓冲区进行访问,必要时再访问实际文件,也就是说这时才会执行系统调用,减少了开销。

    (1)全缓存
    当填满I/O缓存后才进行实际I/O操作。
    (2)行缓存
    当在输入和输出中遇到新行符(‘\n’)时,进行I/O操作。
    (3)不带缓存
    标准I/O库不对字符进行缓冲,例如stderr。
     

    2.底层I/O特定于操作系统,只能在某些操作系统才能使用,而标准IO具有一定的移植性,只要有标准IO库就能使用。

    但也不是说标准I/O一定比底层I/O好,因为缓冲的机制,我们必须时刻注意内容是否已经被冲刷过去,也就是说内容可能还在缓冲里存着,必须掌握这一缓冲机制,程序才能向我们想象的目标去完成。

    3.实际上,文件I/O浅封装了系统调用,我们知道系统调用其实是一个特殊函数。

    比如说文件I/O有一个write的函数,系统调用真正的函数是一个sys_write的函数。

    而read的函数原型为ssize_t write(int fd,void *buf,size_t count);

    我自己想象的浅封装大概就是这样:

    1. ssize_t write(int fd,void *buf,size_t count)
    2. {
    3. sys_write(fd,buf,count);
    4. }

     因此,文件I/O的函数其实不叫系统调用,而是系统函数。而sys_read才叫系统调用。

    而标准I/O实际也调用了文件I/O的函数,只不过在其基础上增加了缓冲的机制。如下:

    2.文件描述符的介绍

    Linux系统一切皆文件,Linux操作系统不区分套接字和文件。

    Linux操作系统给文件或套接字分配整数,用来标识文件或者套接字,称为文件描述符(File descriptor)。因此,程序中套接字可以像文件一样来进行输入输出。

    实际上,标准输入输出及标准错误在Linux中也配分配文件描述符。

    文件和套接字一般经过创建过程才会被分配文件描述符。而标准输入输出及标准错误即使未经过特殊的创建过程,程序开始运行后也会被自动分配文件描述符。如下:

    文件描述符对象
    0stdinSTDIN_FILENO
    1stdoutSTDOUT_FILENO
    2stderrSTDERR_FILENO

    文件描述符具体是什么呢?

    实际上,PCB进程控制块就是一个名为struct task_struct的结构体,其中有一个成员,就是指向文件描述符表的一个指针。而文件描述符表里的每一个文件描述符会一个指向文件。

    这个文件其实是一个名为struct file的结构体,其中包含了一些该文件的具体信息。

    而操作系统隐藏了这些,只要求能够使用文件描述符,而不要求了解实现细节。而文件描述符具体使用就是一个整数数字。

    一个进程文件描述符最多有1024个,也就是说最多打开1024个文件,完全够用。

    可以在命令行输入下面的命令,可以具体查看到能打开多少个文件。

    ulimit -a

    文件描述符的生成有一个规律,就是一定是可用的数字最小的那个。

    比如说我用open函数成功打开了一个文件,其文件描述符一定是3,因为0,1,2被标准输入输出、标准错误给占去了,因此打开的文件描述符是3。

    又比如我用open函数打开了3,4,5,6文件描述符对应的文件,用close函数关闭了4对应的文件,下一次open打开的就是文件的文件描述符就是4。

    3.底层文件I/O函数

    为了方便我们查看下面的函数调用具体发生那些错误,可看下面的链接:

    errno变量和显示错误信息-CSDN博客

    (1)打开文件

    1. int open(const char *pathname,int flags);
    2. int open(const char *pathname,int flags,mode_t mode);
    3. //path 文件名的字符串地址,保存的是目标文件及路径信息
    4. //flags 文件打开模式信息
    5. //mode 文件的权限
    6. //成功返回文件描述符,失败时返回-1,同时errno变量被设置。

    flags 有以下的几个值:

    flags值
    O_RDONLY 只读打开
    O_WRONLY只写打开
    O_RDWR读写打开
    O_CREAT必要时创建文件
    O_TRUNC删除文件全部现有内容,从头开始写入
    O_APPEND维持文件现有内容,在内容末尾追加
    O_EXCL如果文件存在则出错,和O_CREAT搭配使用
    O_NONBLOCK设置为非阻塞模式

    打开模式参数可以通过位或运算符 ” | " 组合传递。

    另外创建文件时,可能需要指定文件权限。

    mode为四位八进制的数,例如mode=0644,第一个0表示八进制,文件权限根据后三位为你想要设置该文件的权限,它会与umask取反后的数相与,得到的最终结果为文件的权限。

    文件权限=mode&~umask

    umask通过命令umask可以查看:

    (2)关闭文件

    1. int close(int fd);
    2. //fd 需要关闭的文件描述符,
    3. //fd含义即上面说的file descriptor文件描述符
    4. //成功时返回0,失败时返回-1,同时errno变量被设置。

    (3)传输数据

    1. ssize_t write(int fd,const void *buf,size_t count);
    2. //fd 要写入对象的文件描述符
    3. //buf 要写入数据的缓存地址值
    4. //count 要写的字节数
    5. //成功时返回写入的字节数,失败返回-1,同时errno变量被设置。
    6. //通过此函数向fd指定的文件或者套接字写入buf里nbytes个字节的数据
    7. 后缀_t意味着type/typedef(类型),是一种命名规范。
    8. size_t是通过typedef声明的unsigned int类型,表示字节数不能为负,
    9. size中文意思尺寸大小,不能为负
    10. ssize_t在size_t的前面加了s,表示ssize_t是通过typedef声明的signed int类型

    (4)读取数据(read函数)

    1. ssize_t read(int fd,void *buf,size_t count);
    2. //fd 需要读取数据对象的文件描述符
    3. //buf 接收数据的缓冲地址值
    4. //count 要接受数据的最大字节数
    5. //实际读取的字节数可能小于nbytes要求的字节数
    6. //成功时返回接收的字节数,失败时返回-1,同时errno变量被设置。
    7. //通过此函数将fd指定的文件或套接字读取nbytes个字节到buf里面

    (5)移动读写指针

    1. off_t lseek(int fd, off_t offset, int whence);
    2. //fd 文件描述符
    3. //offset 距离whence的偏移量
    4. //whence 有三个参数选择:
    5. //SEEK_SET:文件的头部
    6. //SEEK_CUR:当前文件流指针的位置
    7. //SEEK_END:文件的尾部
    8. //通过此函数将读写指针移动到相应的位置,注意上面的writeread函数都是从指针处开始执行的
    9. //例如下面的代码如果将lseek函数注释掉,则buf2里面没有读取到fd里面的数据。
    10. //因为我们写完指针在fd文件里面的末尾,而末尾后面根本没有字节可以读取
    11. //当lseek执行成功时,它会返回最终以文件起始位置为起点的偏移位置。如果出错,则返回-1,同时errno被
    12. //设置为对应的错误值。

    简单的示例代码:

    1. //low_io.c
    2. #include<stdio.h>
    3. #include<stdlib.h>
    4. #include<unistd.h>
    5. #include<fcntl.h>
    6. void error_handling(const char *message)
    7. {
    8. fputs(message,stderr);
    9. fputc('\n',stderr);
    10. exit(1);
    11. }
    12. int main(int argc,char *argv[])
    13. {
    14. char buf1[]="hello,world";
    15. char buf2[20];
    16. int fd=open("data666.txt",O_RDWR|O_TRUNC);
    17. if(fd==-1)
    18. error_handling("open error!\n");
    19. printf("file descriptor is %d\n",fd);
    20. int len1=0;
    21. int len2=0;
    22. if((len1=write(fd,buf1,sizeof(buf1)))==-1)
    23. error_handling("write error!");
    24. printf("write len is %d\n",len1);
    25. //可以试着注释掉下面一句话,看看发现了什么
    26. lseek(fd,0,SEEK_SET);
    27. if((len2=read(fd,buf2,sizeof(buf2)))==-1)
    28. error_handling("read error!");
    29. printf("read len is %d\n",len2);
    30. fputs(buf2,stdout);
    31. fputc('\n',stdout);
    32. close(fd);
    33. return 0;
    34. }

    结果:

    4.验证深入文件I/O和标准I/O

    先分别用标准I/O和文件I/O分别写一个程序,该程序复制一个文件。

    标准I/O:

    1. //stdcopy.c
    2. #include<error.h>
    3. #include<stdlib.h>
    4. #include<stdio.h>
    5. int main(int argc,char *argv[])
    6. {
    7. if(argc!=3)
    8. {
    9. printf("\n");
    10. exit(1);
    11. }
    12. FILE* fp1=fopen(argv[1],"r");
    13. if(!fp1)
    14. {
    15. perror("cp1.txt open failed");
    16. exit(1);
    17. }
    18. FILE* fp2=fopen(argv[2],"w");
    19. if(!fp2)
    20. {
    21. perror("cp2.txt open failed");
    22. exit(1);
    23. }
    24. while(1)
    25. {
    26. int ch=fgetc(fp1);
    27. if(ch==-1)
    28. {
    29. printf("end of file\n");
    30. break;
    31. }
    32. fputc(ch,fp2);
    33. }
    34. return 0;
    35. }

    文件I/O:

    1. //filecopy.c
    2. #include<stdio.h>
    3. #include<stdlib.h>
    4. #include<fcntl.h>
    5. #define N 1
    6. char buf[N];
    7. int main(int argc,char *argv[])
    8. {
    9. if(argc!=3)
    10. {
    11. printf("\n");
    12. exit(1);
    13. }
    14. int fd1=open(argv[1],O_RDONLY);
    15. if(fd1==-1)
    16. {
    17. perror("fd1 open failed");
    18. exit(1);
    19. }
    20. int fd2=open(argv[2],O_WRONLY|O_CREAT|O_TRUNC);
    21. if(fd1==-1)
    22. {
    23. perror("fd2 open failed");
    24. exit(1);
    25. }
    26. int readLen=0;
    27. while(readLen=(read(fd1,buf,N)))
    28. {
    29. if(readLen==-1)
    30. {
    31. perror("read error");
    32. exit(1);
    33. }
    34. write(fd2,buf,N);
    35. }
    36. return 0;
    37. }

    使用这两个文件拷贝一个超大的文件,可以发现文件I/O将会比标准I/O慢。

    下面深入理解这俩的差别。

    内核到磁盘的相互读写有内核自己的一个算法,我们只要把文件内容写到内容或者从内核读取内容,就相当于和磁盘做了数据交换。

    而应用程序到内核,需要系统调用。系统调用,用户态到核心态,核心态到用户态这个过程消耗资源会非常大,时间消耗也会非常长。 

    文件I/O每一次操作都需要这样的一个过程,我们输入命令:

    sudo yum -y install strace

    然后输入命令,运行filecopy.c文件编译完成的可执行程序filecopy:

    strace ./filecopy 文件1 文件2

    发现:

    而标准I/O它自带一个缓冲,它先把要写的内容先写到自己的内存,直到写满了它才使用系统调用把内容写到内核中去。

    输入命令,运行stdcopy.c编译完成的可执行程序stdcopy:

    结果是发现它只执行了一次系统调用。 

  • 相关阅读:
    MTK刷机工具 SP Flash Tool linux 版本安装失败
    犀牛软件无边框编辑设计,提高模型中的工作速度
    Hubble数据库再获得国家级重点项目推荐,作为HTAP国产数据库入选工信部全国试点
    [Python进阶] 获取计算机相关信息:Psutil
    【C++编程入门】2022.8.3日讲给光明社区的小朋友
    springboot毕设项目瓷砖直销系统g5yc1(java+VUE+Mybatis+Maven+Mysql)
    SQL查询优化---批量数据脚本
    开源日报 0820:Python编程学习的完整指南
    Redis快速入门
    【新版】系统架构设计师 - 未来信息综合技术
  • 原文地址:https://blog.csdn.net/cdz2470/article/details/134052922