2 文件IO
2.1 文件IO简介
① 文件IO就是posix(可移植操作系统接口)定义的一组函数,不同系统的文件IO不同。
② 文件IO无缓冲,单次读写更快,但多次读写时会频繁调用外设,降低速度。
③ 文件IO的核心概念是文件描述符fd(file description)。
④ 文件IO只有二进制文件读写。
⑤ 标准IO依赖于文件IO。
2.3 文件描述符
① 文件IO依赖于文件描述符来操作文件,标准IO依赖于显示的文件结构体来操作文件,结构体中也有fd。
② 文件描述符是一个整型数字,每一个进程都有一个独立的存放文件描述符的整型数组。
③ 文件描述符指向了一个隐失的结构体,该结构体包含了文件的位置信息和打开次数。
④ 不同进程打开的文件描述符不同,同一进程多次打开的文件描述符也不同。
⑤ 多次打开同一文件会记录打开次数,只有最后一个才会释放结构体资源。
⑥ 文件描述符范围为0~1023,一般0、1、2继承父进程为标准流,也可以改配置。
2.4 open 函数
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
/*打开成功返回文件描述符,打开失败返回-1,设置errno*/
int open(const char *pathname, int flags); /*用来打开已有的文件*/
int open(const char *pathname, int flags, mode_t mode); /*创建新的文件*/
/*flags用来表示读写权限,mode为文件权限,664,为0666&~*umask/
/*可以打开设备文件,但是不能创建设备文件,设备文件的创建使用mknode*/
/*
备注:文件创建使用3参数,多参的实现是可变参数,传入多个参数也不会报错
*/
2.5 读写权限的组合
-
文件创建符和文件状态符。
-
其中O_RDONLY、O_WRONLY、O_RDWR必须有一个。
flags | 描述 |
---|---|
O_RDONLY | 只读 |
O_WRONLY | 只写 |
O_RDWR | 读写 |
O_CREAT | 不存在,创建 |
O_EXCL | O_CREAT时,文件存在报错 |
O_NOCTTY | 文件为终端,则终端不能作为open函数的进程的控制端 |
O_TRUNC | 文件已存在,则覆盖 |
O_APPEND | 文件已存在,追加 |
其他 | 描述 |
---|---|
O_DIRECT | 最小化cache影响,cache读取到缓冲区,buffer写入到缓冲区 |
O_DIRECTORY | 非目录打开失败 |
O_LARGEFILE | 大文件时使用 |
O_NOATIME | 提升性能时间,不修改系统时间 |
O_NOFOLLOW | 不打开连接文件 |
O_NONBLOCK | 非阻塞,打不开就不打开,默认阻塞,打不开就等 |
O_SYNC | 同步 |
读写权限 | 组合 |
---|---|
r | O_RDONLY |
r+ | O_RDWR |
w | O_WRONLY | O_CREAT| O_TRUNC,0664 |
w+ | O_RDWR | O_CREAT| O_TRUNC,0664 |
a | O_WRONLY | O_CREAT | O_APPEND,0664 |
a+ | O_RDWR | O_CREAT | O_APPEND,0664 |
2.6 close函数
#include <unistd.h>
/*关闭fd,关闭成功返回0,关闭失败返回-1,并设置errno*/
/*文件只能释放一次,且程序结束自动关闭所有*/
int close(int fd);
/*
常见错误
EBADF fd 不是有效的打开文件描述符。
EINTR 关闭() 调用被信号中断;参见信号(7)。
EIO 发生 I/O 错误。
ENOSPC, EDQUOT
在 NFS 上,这些错误通常不会针对超出可用存储空间的第一次写入报告,而是针对后续的写入、fsync 或 close报告。
EEXIST 文件不存在
*/
2.6 打开关闭模版
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
int main(int argc, char **argv){
int fd = 0;
if(argc < 2){
fprintf(stderr,"argc is not enough\n");
}
/*打开已存在的文件*/
fd = open(argv[1],O_RDONLY);
if(fd < 0){
if(errno == EEXIST){
perror("文件不存在");
}else{
perror("其他错误");
}
exit(1);
}
puts("open is success");
/*关闭文件描述符*/
if(close(fd) != 0){
fprintf(stderr,"close is wrong: %s\n",strerror(errno));
}
puts("close is success");
return 0;
}
2.7 write函数
#include <unistd.h>
/*
从buf中写入count个字节到fd指向的流,count不超过buf的大小
写入成功返回写入的字节数,写入失败返回-1,如果返回值小于count可能是磁盘不足或者中断导致的问题
*/
ssize_t write(int fd, const void *buf, size_t count);
2.8 read函数
#include <unistd.h>
/*
从fd指向的文件中读取count个字节存到缓冲区中,缓冲区起始地址为buf
读取成功返回读取到的字节数,0表示读到了末尾,-1表示发生了错误并设置errno
接近文件末尾,或者从管道或终端读取或者中断等都可能导致读取到的小于count
*/
ssize_t read(int fd, void *buf, size_t count);
2.9 lseek函数
#include <sys/types.h>
#include <unistd.h>
/*成功时返回从起始位置到当前的偏移相当于位置,失败返回-1并设置errno*/
off_t lseek(int fd, off_t offset, int whence);
/*第10位,则距离SEEK_SET偏移9位*/
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main(void){
int fd = open("/home/test_src",O_RDONLY);
char buf[1024];
/*读取5个字节*/
read(fd,buf,5);
write(1,buf,5);
write(1,"\n",1); /*打印到标准输出流*/
/*重新定位*/
lseek(fd,-4,SEEK_CUR);
/*再读取一个字节*/
read(fd,buf,1);
write(1,buf,1);
write(1,"\n",1);
return 0;
}
2.10 代码实现
/* 注意:当返回值小于count可能是因为出现了一些阻塞打断了IO*/
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
int main(int argc, char **argv){
int fd_src = 0; /*读取文件的描述符*/
int fd_dest = 0; /*写入文件的描述符*/
int len = 0; /*读到的字节长度*/
int ret = 0; /*返回写入的字节*/
int pos = 0; /*当前写入的位置*/
char buf[1024]; /*缓冲区*/
if(argc < 3){
fprintf(stderr,"argc is not enough\n");
}
/*打开已存在的文件*/
fd_src = open(argv[1],O_RDONLY);
if(fd_src < 0){
if(errno == EEXIST){
perror("文件不存在");
}else{
perror("其他错误");
}
goto END;
}
fd_dest = open(argv[2],O_WRONLY|O_CREAT|O_TRUNC,0664);
if(fd_dest < 0){
close(fd_src);
if(errno == EEXIST){
perror("文件不存在");
}else{
perror("其他错误");
}
goto END;
}
/*代码实现:将argv[1]的内容复制到argv[2]中*/
while(1){
len = read(fd_src,buf,1024);
if(len < 0){
perror("read is wrong!");
goto CLOSE;
}else if(len == 0){
perror("Read To EOF");
goto CLOSE;
}
pos = 0;
while(len > 0){
ret = write(fd_dest,buf,len + pos);
if(ret < 0){
perror("write is wrong!");
goto CLOSE;
}
/*写入可能被阻塞,应当从当前地址继续写*/
pos += ret; /*pos修改到读取到的位置*/
len -= pos; /*剩余的读取字节减少*/
/*正常写入就不管*/
}
}
CLOSE:
/*关闭文件描述符*/
if(close(fd_dest) != 0){
fprintf(stderr,"close is wrong: %s\n",strerror(errno));
}
if(close(fd_src) != 0){
fprintf(stderr,"close is wrong: %s\n",strerror(errno));
}
END:
return 0;
}
2.11 文件IO与标准IO比较
- 文件IO与标准IO的区别:
文件IO没有buf和cache,响应速度更快。
标准IO有buf和cache、fflush,吞吐量更大,
- 如何提高文件的读写速度?
如果是文件的吞吐量大(用户体验号),则使用标准IO;如果要求响应快,则用文件IO。
- 注意:标准IO与文件IO不能混用
/*
文件IO没有缓冲区,fd中的文件位置就是实际的文件位置。
标准IO有缓冲区,FILE中的文件位置只是缓冲区位置,可能文件位置并没变。
*/
/*
FILE*转化为fd:
#include <stdio.h>
int fileno(FILE *stream);
*/
/*
fd转化为FILE*:
#include <stdio.h>
FILE *fdopen(int fd, const char *mode);
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
/*
打印结果:abc123
文件IO每次都直接输出到标准流
标准IO等到刷新或者缓冲区满足条件才一次性打印到标准流
*/
int main(void){
fputc('1',stdout);
write(1,"a",1);
fputc('2',stdout);
write(1,"b",1);
fputc('3',stdout);
write(1,"c",1);
fputc('\n',stdout);
return 0;
}
2.12 IO效率问题
文件IO打开一个文件,bufferzise从2两倍递增,比较速度的拐点。
time ./程序 测试程序的运行时间
real:用户在意的真实时间,usr + sys + 一些其他的。
usr:应用层面的时间。
sys:系统层面的时间,与操作系统有关,系统调用的时间。
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
/*
bufsize从2字节两倍递增到8G
曲线:
2~1024*4,时间递减
1024后不再减少
8G段错误,核心已转存
*/
#define BufSize 2
int main(int argc, char **argv){
int fd_src = 0; /*读取文件的描述符*/
int fd_dest = 0; /*写入文件的描述符*/
int len = 0; /*读到的字节长度*/
int ret = 0; /*返回写入的字节*/
int pos = 0; /*当前写入的位置*/
char buf[BufSize]; /*缓冲区*/
/*打开已存在的文件*/
fd_src = open("/snap/gnome-42-2204/111/usr/lib/x86_64-linux-gnu/dri/crocus_dri.so",O_RDONLY);
if(fd_src < 0){
if(errno == EEXIST){
perror("文件不存在");
}else{
perror("其他错误1");
}
goto END;
}
fd_dest = open("/home/test_dest",O_WRONLY|O_CREAT|O_TRUNC,0664);
if(fd_dest < 0){
close(fd_src);
if(errno == EEXIST){
perror("文件不存在");
}else{
perror("其他错误2");
}
goto END;
}
/*代码实现:将argv[1]的内容复制到argv[2]中*/
while(1){
len = read(fd_src,buf,BufSize);
if(len < 0){
perror("read is wrong!");
goto CLOSE;
}else if(len == 0){
perror("Read To EOF");
goto CLOSE;
}
pos = 0;
while(len > 0){
ret = write(fd_dest,buf,len + pos);
if(ret < 0){
perror("write is wrong!");
goto CLOSE;
}
/*写入可能被阻塞,应当从当前地址继续写*/
pos += ret; /*pos修改到读取到的位置*/
len -= pos; /*剩余的读取字节减少*/
/*正常写入就不管*/
}
}
CLOSE:
/*关闭文件描述符*/
write(1,"success\n",8);
if(close(fd_dest) != 0){
fprintf(stderr,"close is wrong: %s\n",strerror(errno));
}
if(close(fd_src) != 0){
fprintf(stderr,"close is wrong: %s\n",strerror(errno));
}
END:
return 0;
}
2.13 文件共享
/*
问题:删除文件的第十行。
思路1:同一进程打开一个fd_r,一个fd_r+,
思路2:不同进程,一个进程打开fd_r,一个进程打开fd_r+,借助进程间通信实现
*/
/*********删除test文件的第11位*******/
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#define BufSize 1024
int main(void){
int fd_src = 0;
int fd_dest = 0;
char buf[BufSize];
int pos_src = 11;
int pos_dest = 10;
int len = 0;
int ret = 0;
int pos = 0;
fd_src = open("/home/test",O_RDONLY);
if(fd_src < 0){
perror("fd_src is failed!");
exit(1);
}
fd_dest = open("/home/test",O_RDWR);
if(fd_dest < 0){
perror("fd_dest is failed!");
close(fd_src);
exit(1);
}
lseek(fd_src,pos_src,SEEK_SET); /*读取定位到第11位*/
lseek(fd_dest,pos_dest,SEEK_SET); /*写入定位到第10位*/
while(1){
len = read(fd_src,buf,BufSize);
if(len < 0){
perror("READ ERROR:");
goto END;
}
if(len == 0){
write(1,"MODIFY SUCCESS\n",15);
break;
}
pos = 0;
while(len > 0){
ret = write(fd_dest,buf,len);
if(ret < 0){
perror("READ ERROR: ");
goto END;
}
pos += ret;
len -= ret;
}
}
END:
close(fd_dest);
close(fd_src);
return 0;
}
2.14 文件截断
#include <unistd.h>
#include <sys/types.h>
/*截断文件,成功返回0,否则返回-1并设置errno*/
int truncate(const char *path, off_t length); /*文件必须打开且可写*/
int ftruncate(int fd, off_t length); /*文件不用打开*/
/*
注意:
如果长度比length小,则会用空字符填充,比length大则会截断
尺寸改变时间为上次状态更改时间和上次修改时间
用户ID和组ID也许会清除。
*/
/********截断文件*******/
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
int main(void){
int fd = open("/home/test",O_RDWR);
if(fd < 0){
perror("open is failed");
exit(1);
}
/*截断/home/test*/
if(truncate("/home/test",2)<0){
perror("truncate is failed");
exit(1);
}
/*填充*/
if(ftruncate(fd,20) < 0){
perror("ftruncate is failed");
exit(1);
}
write(1,"success!\n",9);
close(fd);
return 0;
}
2.15 原子操作
原子操作:
- 不可再分割的操作,解决多线程下的竞争操作。
重定向问题:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
int main(void){
/***********************题目*************************/
/*不修改后面的程序,将hello重定向到/home/test文件中*/
puts("hello");
return 0;
}
解题思路:
-
① 关闭stdout的fd。
-
② 打开/home/test的fd,会自动占用没有被占用的最小fd。
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
int main(void){
int fd = 0;
close(1);
fd = open("/home/test",O_RDWR|O_CREAT|O_TRUNC,0x664);
if(fd < 0){
perror("open is failed:");
exit(1);
}
/***********************题目*************************/
/*不修改后面的程序,将hello重定向到/home/test文件中*/
puts("hello");
return 0;
}
存在问题:
-
① 可能并没有标准输出,所以1就是自己。
-
② 有可能还没有顶上去就被别人顶上去了。
dup和dup2函数:
#include <unistd.h>
/*复制描述符,成功返回新的fd,失败返回-1,并设置errno*/
int dup(int oldfd); /*将oldfd赋值给最小的可用的fd*/
int dup2(int oldfd, int newfd); /*原子操作*/
/*
dup2将oldfd拷贝到newfd,如果newfd打开会将其关闭
如果newfd不是有效文件,则调用失败
如果newfd等于oldfd,则不执行仍何操作,并返回newfd
*/
dup实现重定向
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
/*同样存在上述问题*/
int main(void){
int fd = 0;
fd = open("/home/test",O_RDWR|O_CREAT|O_TRUNC,0x664);
if(fd < 0){
perror("open is failed:");
exit(1);
}
close(1);
dup(fd); /*fd的拷贝文件描述符自动填充最小位置*/
/***********************题目*************************/
/*不修改后面的程序,将hello重定向到/home/test文件中*/
puts("hello");
return 0;
}
dup2实现重定向
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
int main(void){
int fd = 0;
int fd_stdout = dup(fileno(stdout)); /*复制一个stdout的fd*/
fd = open("/home/test",O_RDWR|O_CREAT|O_TRUNC,0x664);
if(fd < 0){
perror("open is failed:");
exit(1);
}
dup2(fd,1); /*fd的拷贝文件描述符自动填充最小位置*/
close(fd);
/***********************题目*************************/
/*不修改后面的程序,将hello重定向到/home/test文件中*/
puts("hello");
/*
补充:要有多任务并发编程的思想,自己修改了则需要改回来
*/
fflush(stdout); /*刷新缓冲区,否则还没输出就关闭了*/
dup2(fd_stdout,1);
write(1,"OK\n",3);
close(fd_stdout);
return 0;
}
2.16 同步
sync命令
将内核层面的buffer、cache缓存的写入同步到持久存储。
解除设备挂载或者关机时需要将缓存写入设备。
-d, --data
仅同步文件数据,不同步不需要的元数据
-f, --文件系统
同步包含文件的文件系统
fsync和fdatasync函数
#include <unistd.h>
/*同步数据到设备或文件,成功返回0,失败返回-1并设置errno*/
int fsync(int fd); /*将fd中所有的文件数据以及缓冲区全部刷到外设中,但目录可能不一定导入,需要对目录也fsync*/
int fdatasync(int fd); /*只刷数据,不刷元数据(时间、文件属性等信息),目的是减少空间的占用*/
fcntl函数
#include <unistd.h>
#include <fcntl.h>
/*
文件描述符的管理工具,文件描述符的行为都来自该函数
该函数通过可变参数实现
cmd指令不同后面的参数以及前面的返回值都不同
dup和dup2都是该函数实现
*/
int fcntl(int fd, int cmd, ... /* arg */ );
ioctl函数
#include <sys/ioctl.h>
/*
设备相关内容管理函数
函数的man手册很少,但ioctl_list命令可以看到
多一个功能就多一大段定义,难
*/
int ioctl(int fd, unsigned long request, ...);
虚目录
ls -la /dev/fd/
虚目录:显示当前进程的文件描述符信息。
当前则为ls的信息。
需要文件打开该目录才能查看。