当前位置: 首页 > news >正文

【Linux】文件IO--read/write/缓冲区(详)

 read/write函数

read函数:

函数描述: 从打开的设备或文件中读取数据

函数原型:ssize_t read(int fd, void *buf, size_t count);

函数参数:

  • fd: 文件描述符
  • buf: 读取的数据保存在缓冲区buf中
  • count: buf缓冲区存放的最大字节数

函数返回值:

  • >0:读取到的字节数
  • =0:文件读取完毕
  • -1: 出错,并设置errno

write函数:

函数描述: 向打开的设备或文件中写数据

函数原型: ssize_t write(int fd, const void *buf, size_t count);

函数参数:

  • fd:文件描述符
  • buf:缓冲区,要写入文件或设备的数据
  • count:buf中数据的长度

函数返回值:

  • 成功:返回写入的字节数
  • 错误:返回-1并设置errno

用法示例:

①read读取示例:

int fd = open("./a.txt", O_RDONLY);
char buf[1024];
int ret = read(fd, buf, sizeof(buf));
buf[ret] = '\0'; //字符串末尾添加结束标志
printf("buf = %s\n", buf);
close(fd);

②在文件末尾写入:

int fd = open("./a.txt", O_RDWR); //如果只读,write返回-1
char buf[1024];
int read_count = read(fd, buf, sizeof(buf));
// lseek(fd, 0, SEEK_SET);
int write_count = write(fd, buf, read_count);
printf("write_count = %d\n", write_count);

③覆盖原文件写入:

int fd = open("./a.txt", O_RDWR);
int fd2 = open("./b.txt", O_RDWR); //如果追加写入加上 O_APPEND
char buf[1024];
int read_count = read(fd, buf, sizeof(buf));int write_count = write(fd2, buf, read_count);
printf("write_count = %d\n", write_count);

实现cp命令:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<fcntl.h>void copy(const char *from,char *to)
{int fd;char buf[1024]; memset(buf,'\0',sizeof(buf));fd=open(from,O_RDONLY);    int cnt = read(fd,buf,sizeof(buf));close(fd);fd = open(to,O_WRONLY | O_CREAT,0664);int ret = write(fd,buf,cnt);close(fd);
}int main(int args, char* argv[])
{copy(argv[1],argv[2]);                                                                                                                                             return 0;
}

 

通过上述方法,我们可以进行简单的复制,可是我们也有一个问题,那就是我们只能复制1024个字符,再多就没法复制了,又该如何解决这个问题呢?使用循环:

void copy(const char *from,char *to)
{int fd1=open(from,O_RDONLY);int fd2=open(to,O_RDWR |O_CREAT |O_TRUNC,0664);char buf[1024];memset(buf,'\0',sizeof(buf));   int n = 0;   while(( n=read(fd1,buf,sizeof(buf))) != 0 )                                                                                                                                    {    write(fd2,buf,n);}close(fd1);close(fd2);
}

为了提高程序的处理错误的能力,我们每次进行文件操作时,都接收返回值进行判断:

  7 void copy(const char *from,char *to)8 {9     int fd1 = open(from,O_RDONLY);10     if(fd1 == -1){11         perror("open argv1 error");12         exit(1);13     }14     int fd2 = open(to,O_RDWR |O_CREAT |O_TRUNC,0664);15     if(fd2 == -1){16         perror("open argv2 error");17         exit(1);18     }19     char buf[1024];20     memset(buf,'\0',sizeof(buf));21     int n = 0;22     while((n=read(fd1,buf,sizeof(buf)))!=0)23     {24         if(n < 0){25             perror("read error from argv1");26             exit(1);27         }28         int ret = write(fd2,buf,n);29         if(ret == -1){30             perror("write error to argv2");                                                                                                                            31             exit(1);32         }33     }34     close(fd1);35     close(fd2);36 }

这时候我们尝试错误执行该程序,看看会怎么显示:

除了自己的显示错误信息外,他还会对errno的值自动推断错误信息,然后打印出来。

这里还涉及一个知识点:从命令行向main中传入参数。在Linux下命令行是可以直接传递参数的。传递参数得有个接口吧,接口在哪呢?就在main()函数!

从Linux命令行向main函数中传递参数

//标准写法
int main(int arc,char *argv[])
{}
//以往常用的写法
int main(){}
int main(void){}

写一段代码,来探究一下char *argv[]中存储的都是些什么?【首先告诉大家,args是传入的参数的个数,也就是argv[]可访问的下标上界】

然后编译运行:

由此我们可以看到,不加参数的话,默认将 运行程序的程序名这个字符串传入,也就是默认的argv[0]。然后传入的参数,像1 2 3这三个参数,分别就存储在:argv[1] 、argv[2]  、 argv[3]。

缓冲区

在Linux和C语言中,缓冲区(buffer)的概念非常重要。它是用于临时存储数据的内存区域,以便在数据的输入和输出过程中提高效率。

在上述的示例中,我们将buf这个字符数组大小设置成了1024的大小。就代表了,我们我们的缓冲区大小就是1024,每读取一次1024或者读到EOF才会进行下一步操作。假如我们将buf的大小设置为1,那么就是一个字符一个字符的操作。此时你想一下C语言的fgetc和fputc这两个函数,好像就是说每次读取一个字符,每次输出一个字符。那么这库函数与系统函数,哪个更快呢?

也许你的第一想法就是系统函数快,因为库函数调用了系统函数。但事实上真的如此嘛?你可以进行实验验证,一个文件使用read和write,一个函数使用fgetc和fputc,两者的实现步骤一样,然后开两个终端,同时复制两个大文件,你会发现系统级的函数反而慢了,为什么会这样。我们来看下面这张图:

我们使用系统函数时,buf直接穿过用户与内核中间的墙去与内核空间交互,没有经过fputc这个库函数,命名流程更加直接,为什么反而慢了呢。 其实我们都被fputc给骗了,它并不是真正的一个字符一个字符的向内核传入,而是内置了一个缓冲区,大小为4096,所以实际上fputc是以4096个字符为一个单位去传输的,你想使用1个字符作为单位去传输,然后跟人家比效率,这简直是在痴心妄想。上述流程中,最耗时的部分就是“穿墙”,人家在墙前面屯字符,等着一块穿墙,而你频繁的穿墙,自然耗时更多。当穿过了墙之后,内核经过操作系统,会向磁盘上输出数据,此时我们也不是一个字符一个字符就往磁盘上传输的,内核中间也有一个缓冲区,缓冲区的大小默认为4096.至于什么时候输出上去,这个过程我们不做深入研究,我们只需要知道,操作系统有一套自己的算法逻辑,待到合适的时机自然会将数据刷出去。这个非立即响应的过程就是“缓输出,预读入”。

其中,内核空间的缓冲区称之为系统级缓冲区,而像fputc这种函数内置的缓冲区称为用户级缓冲区。read和write函数常常称为Unbuffered I/O--无缓冲输入输出,意思是无用户级缓冲区,但不保证不使用内核的缓冲区。

作用:缓冲区最主要的作用是减少直接数据传输的频率。例如,批量读取或写入数据可以显著提高性能,因为它减少了系统调用的开销

  • C语言的标准I/O库(如 stdio.h)通常使用缓冲技术来优化文件读写操作。
    • 全缓冲:通常用于输出文件,数据在缓冲区中的内容会在缓冲区满时一次性写入。
    • 行缓冲:用于终端输出,数据一行一行地存储到缓冲区中,环境退出时自动刷新。
    • 不缓冲:实时输出,缓冲区内容立即写入,适用于需要实时监控的场景。
  • 缓冲区溢出:当数据写入缓冲区超过其容量时,可能导致缓冲区溢出,这是一种安全隐患,必须特别注意。
  • 需要手动刷新:在某些情况下,数据需要手动刷新到文件或终端,可以使用 fflush() 函数。

感谢大家!!


http://www.mrgr.cn/news/80878.html

相关文章:

  • 【Rust自学】4.3. 所有权与函数
  • [Linux] 信号保存与处理
  • 单片机:实现延时函数(附带源码)
  • 《剑网三》遇到找不到d3dx9_42.dll的问题要怎么解决?缺失d3dx9_42.dll是什么原因?
  • 字节跳动C++面试题及参考答案(下)
  • git使用和gitlab部署
  • [LeetCode-Python版] 定长滑动窗口3——1461. 检查一个字符串是否包含所有长度为 K 的二进制子串
  • 二十一、Ingress 进阶实践
  • 十大排序算法汇总(基于C++)
  • Unity开发哪里下载安卓Android-NDK-r21d,外加Android Studio打包实验
  • Fast-Planner 改进与优化:支持ROS Noetic构建与几何A*路径规划
  • ENSP实验
  • 红队规范:减少工具上传,善用系统自带程序
  • Linux基础及命令复习
  • Makefile文件编写的学习记录(以IMX6ULL开发板的Makefile文件和Makefile.build文件来进行学习)
  • Express (nodejs) 相关
  • [LeetCode-Python版] 定长滑动窗口1(1456 / 643 / 1343 / 2090 / 2379)
  • 【NLP 16、实践 ③ 找出特定字符在字符串中的位置】
  • jmeter中的prev对象
  • Qt学习笔记第71到80讲