基于文件描述符的读写文件操作
#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>