基于文件描述符的读写文件操作

本文介绍了使用read和write系统调用来读写文件,这两种操作被称为不带缓冲的IO。read将数据从内核文件缓冲区拷贝到用户内存,write则是反向操作。示例代码展示了如何读取和写入文件,包括二进制文件的处理。此外,还讨论了文件偏移、文件截断、文件映射(mmap)以及它们在性能上的比较。最后提到了mmap的限制和适用场景。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

基于文件描述符的读写文件操作

#include <unistd.h>
ssize_t read(int fd, void* buf, size_t count);	//文件描述符,缓冲区,缓冲区长度
ssize_t write(int fd, const void* buf, size_t count);//文件描述符,缓冲区,有效内容长度,

使用read和write系统调用可以读写文件,他们统称为不带缓冲的IO

  • read的原理是将数据从文件对象的内核文件缓冲区拷贝到用户区内存buf

  • write就相反,是将数据从用户态内存buf拷贝到内核区的文件对象内核文件缓冲区,并最终会写到磁盘上。

  • 每次调用read或write都会印象下次读写位置,值得注意的是,读写位置信息是保存在内核文件对象当中。

//读取文件内容
int main(int argc, char* argv[]){
    ARGS_CHECK(argc,2);
    int fd = open(argv[1],O_RDWR);
    ERROR_CHECK(fd,-1,"open");
    
    printf("fd = %d",fd);
    
    char buf[1024];
    int ret = read(fd, buf, sizeof(buf));	//ret表示读取到的字节个数
    printf("buf = %s, ret = %d\n",buf,ret);
    
    close(fd);	//将文件描述符对应的文件对象的引用计数-1,若此时引用计数为0,则销毁文件对象
    return 0;
}

读写二进制文件

  • 原则:什么类型写,就按什么类型读入。

下边将结构体数据写入文件,再将数据从文件中读到结构体变量中。

typedef struct student_s{
    int number;
    int score;
    char name[25];
}student_t;

int main(int argc, char* argv[]){
    student_t stus[] = {
        {1,99.5,"yfg"},
        {2,91.3,"abc"},
        {3,88,"jik"}
    };
    
    ARGC_CHECK(argc,3);
    int fdw = open(argv[1], O_RDWR | O_CREAT, 0666);
    ERROR_CHECK(fdw,-1,"open");
    
    write(fdw,stus,sizeof(stus));
    
    student_t st[3];
    int fdr = open(argv[2], O_RDWR | O_CREAT, 0666);
    ERROR_CHECK(fdr, -1, "open fdr");
    
    read(fdw,st,sizeof(st));
    
    for(int i = 0; i < 3; i++){
        printf("%d %f %s\n",st[i].number,st[i].score,st[i].name);
    }
    
    close(fdw);
    close(fdr);
    return 0;
}
读磁盘文件和设备文件的区别

值得注意的是,使用read读磁盘文件和设备文件的行为是有着明显差异

  • 当都磁盘/设备文件时,如果文件/设备缓冲区内容长度超过count,那么本次会读取count个字节
  • 当读取磁盘/设备文件时,如果文件/设备缓冲区存在剩余内容,且长度不足count,那么本次read会读取剩余文件内容。
  • 当读取磁盘文件时,如果磁盘文件无剩余内容,read操作会立刻返回,返回值为0
  • 当读取设备文件时,如果设备缓冲区无剩余内容,那么本次read会阻塞。
通过读写操作实现文件拷贝
int main(int argc, char* argv[]){
    ARGS_CHECK(argc,3);
    int fdr = open(argv[1],O_RDOPLY);
    ERROR(fd,-1,"open fdr");
    
    int fdw = open(argv[2],O_RDWR | O_CREAT, 0666);
    ERROR(fd,-1,"open fdw");
    
    char buf[1024];
    ssize_t ret;
    
    //拷贝
    while((ret = read(fdr,buf,sizeof(buf))) != 0){
        write(fdw,buf,ret);
    }
    close(fdw);
    close(fdr);
    return 0;
}

用户态内存缓冲区buf的大小会影响read、write(系统调用)的执行次数。系统调用会导致cpu从用户态-内核态来回的一次切换,会产生时间开销。

buf过小,会频繁的切换状态,产生巨大的时间开销,而真正用于读写的时间比很小。所以buf缓冲区应该设置一个合理的范围。

文件偏移

系统调用lseek可以将文件对象的的文件读写偏移量设定到以whence为起点,偏移值为offset的位置。返回值是读写点距离文件起始位置所占的字节。

#include <sys/types.h>
#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);//fd文件描述词
//whence 可以是下面三个常量的一个
//SEEK_SET 从文件头开始计算
//SEEK_CUR 从当前指针开始计算
//SEEK_END 从文件尾开始计算


int main(int argc, char *argv[])
{
    ARGS_CHECK(argc,2);
    int fd = open(argv[1],O_RDWR);
    ERROR_CHECK(fd,-1,"open");
    int ret = lseek(fd, 5, SEEK_SET);
    printf("pos = %d\n", ret);
    char buf[128] = {0};
    read(fd, buf, sizeof(buf));
    printf("buf = %s\n", buf);
    close(fd);
    return 0;
}

截断文件

使用ftruncate函数可以截断文件,从而控制文件的大小

#include <unistd.h>
int ftruncate(int fd, off_t length);

ftruncate会将fd指定的文件大小改为参数length指定的大小。参数fd为文件描述符,而且必须是以写入模式打开的文件,如果原来的文件大小比参数length大,则超过的部分会被删去,若比参数length小,文件会扩充到大小为length,并且多余的数据填充二进制0

int main(int argc, int argv){
    ARGS_CHECK(argc,2);
    int fd = open(argv[1], O_RDWR | O_CREAT, 0666);
    ERROR_CHECK(fd,-1,"open");
    
    printf("fd = %d\n",fd);
    
    off_t length = 3;
    int ret = ftruncate(fd,length);
    ERROR_CHECK(ret, -1, "ftruncate");
   
    ssize_t filesize = lseek(fd,0L,SEEK_END);
    printf("new file size: %ld\n",filesize);
    
    close(fd);
    return 0;
}
文件映射

使用mmap系统调用可以实现文件映射功能,也就是将一个磁盘文件直接映射到用户态内存地址空间,这样、内存内容和磁盘一一对应,不在需要read、write系统调用就可以进行IO操作,直接读写内存数据就可。

需要注意的是:mmap不能修改文件的大小,所以进场配合ftruncate来使用。

#include <sys/mman.h>
void* mmap(void* adr, size_t len, int prot, int flag, int fd, off_t off);
  • addr参数用于用于指定映射映射储存区的起始地址,一般设置为NULL,这样由系统自动分配(一般是分配在堆空间里面)
  • fd参数是文件描述符,用来指定要建立映射的文件
  • prot参数用来表示权限,其中PROT_READ | PROT_WRITE表示可读可写
  • flag参数填写MAP_SHARED
//假设文内内容是hello
int main(int argc, char argv[]){
    ARGS_CHECK(argc,2);
    int fd = open(argv[1],O_RDWR | O_CREAT, 0666);
    ERROR_CHECK(fd,-1,"open");
    
    char* p = (char*)mmap(NULL,5,PROT_READ|PREO_WRITE,MAP_SHARED,fd,0L);
    ERROR_CHECK(p,MAP_FAILED,"mmap");
    
    //建立映射之后就可以随机访问映射到内存的数据。
    for(int i = 0; i < 5; i++){
        printf("%c",*(p+i);
    }
    printf("\n");
	
    close(fd);
    return 0;
}
文件映射的底层原理

mmap和read/write的效率谁更高?这个答案依赖问题发生的场景。从原理来说read/write是让数据在文件对象和用户态内存之间来回进行拷贝,文件对象会和操作系统管理的内存区域(页缓存)相关联,数据最终持久化到磁盘上由操作系统负责。mmap的处理就更加粗暴,他直接把页缓存的一部分映射到用户态内存,这样对内存的操作就直接对应页缓存的操作。

这样看上去,mmap的效率总是会比read/write更高,应为他避免了一次数据在用户态和内核态之间的拷贝。但是考虑到read/write的特殊性质,他们总是顺序地而不是随机访问磁盘文件内容,所以操作系统可以根据这个特点进行优化。

最终经过测试,read/write在顺序读写性能更好,而mmap在随机访问性能更好。

注意:mmap只能映射磁盘文件,而read没有这些限制。

尤其要注意mmap函数的返回值,若映射失败返回的是(void*)(-1) MAP_FAILED,而不是NULL.

头文件:#include <sys/mman.h>

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值