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

【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.kler.cn/a/444867.html

相关文章:

  • UDP系统控制器_音量控制、电脑关机、文件打开、PPT演示、任务栏自动隐藏
  • 台球助教平台系统开发APP和小程序信息收藏功能需求解析(第十二章)
  • Apache Solr RCE(CVE-2017-12629)--vulhub
  • Oracle 查询表占用空间(表大小)的方法
  • 【Vulkan入门】16-IndexBuffer
  • Python-存储数据-Thu-Fri
  • 防火墙规则配置错误导致的网络问题排查
  • 用C#(.NET8)开发一个NTP(SNTP)服务
  • windwos defender实现白名单效果(除了指定应用或端口其它一律禁止)禁止服务器上网
  • pycharm debug
  • 网络安全概论——入侵检测系统IDS
  • 使用python的模块cryptography对文件加密
  • PostgreSQL:pg_stat_statements
  • 90度Floating B to B 高速连接器信号完整性仿真
  • Hutool工具包的常用工具类的使用介绍
  • PostgreSQL技术内幕21:SysLogger日志收集器的工作原理
  • 鸿蒙 NEXT 开发之后台任务开发服务框架学习笔记
  • 一款特别有趣的 Minecraft(我的世界)游戏服务器项目:Pumpkin
  • 鸿蒙项目云捐助第九讲鸿蒙App应用的捐助详情页功能实现
  • ffmpeg-SDL显示BMP
  • 鸿蒙高级特性 - 动态UI加载
  • Unity复刻胡闹厨房复盘 模块一 新输入系统订阅链与重绑定
  • 在Windows本地用网页查看编辑服务器上的 jupyter notebook
  • 【漫话机器学习系列】014.贝叶斯法则(Bayes Theorem)
  • Fabric8 Kubernetes Client 7.0.0内存泄漏深度分析与案例实践
  • Immer编写更简单的逻辑