首页 > 代码库 > Linu基础 文件IO(读写操作)
Linu基础 文件IO(读写操作)
前言
本章讨论普通文件的读写、读写效率、简单介绍文件描述符、IO效率、文件共享和原子操作、dup、文件映射、临时文件。
文件描述符
在Linux系统中,打开的文件是用一个整数来表示的,表示打开文件的整数,称之为文件描述符。当需要往写数据/读数据时,读写函数都需要文件描述符作为参数,以便系统知道用户操作的时哪个文件。
文件基本操作
open/creat
mode选项 | 解释 |
---|---|
O_RDONLY | 读方式打开 |
O_WRONLY | 写方式打开 |
O_RDWR | 读写方式打开 |
O_CREAT | 创建文件,如果文件存在,会被截断 |
O_TRUNC | 截断 |
O_APPEND | 追加 |
O_EXCL | 和O_CREAT一起用,如果文件存在则失败 |
open函数的flag值得是mode选项(注意互斥问题)。第三个参数指示新建的文件的属性。文件真实的权限,受umask的影响。影响方法
close
关闭文件。
在dup时,有更多讨论。
read/write
读写文件,会导致文件指针移动。
文件指针和lseek
文件指针是一个整数,描述当前读写位置,可以使用lseek移动文件指针。
文件读写效率
当读写文件时,缓冲区设置为1024到4096是一个比较合适的尺寸。
#include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <string.h> #include <stdio.h> #include <stdlib.h> int main1() { int fdr = open("a.out", O_RDONLY); if(fdr < 0) { perror("open read"); return -1; } int fdw = open("b.out", O_WRONLY|O_CREAT, 0777); if(fdw < 0) { perror("open write"); return -1; } // 1. 如果文件很大怎么办 int filelen = lseek(fdr, 0, SEEK_END);//读取文件的长度 lseek(fdr, 0, SEEK_SET);//将文件读的指针。制回开始位置 char* buf = malloc(filelen); read(fdr, buf, filelen); write(fdw, buf, filelen); close(fdr); close(fdw); } int main2() { int fdr = open("a.out", O_RDONLY); if(fdr < 0) { perror("open read"); return -1; } int fdw = open("b.out", O_WRONLY|O_CREAT, 0777); if(fdw < 0) { perror("open write"); return -1; } // 一个一个字节拷贝,效率很低下 char buf; while(1) { if(read(fdr, &buf, 1) <= 0) { break; } write(fdw, &buf, 1); } close(fdr); close(fdw); } int main() { int fdr = open("a.out", O_RDONLY); if(fdr < 0) { perror("open read"); return -1; } int fdw = open("b.out", O_WRONLY|O_CREAT, 0777); if(fdw < 0) { perror("open write"); return -1; } // buf尺寸多少最合适? char buf[1024]; int ret; while(1) { ret = read(fdr, &buf, sizeof(buf)); if(ret <= 0) { break; } write(fdw, &buf, ret);//避免最后一次读取数据,没有1024个字节 } close(fdr); close(fdw); }
文件共享
两个进程可以打开同一个文件进行操作,实现数据的共享。但是当两个进程打开同一个文件进行写操作时,会相互覆盖。当文件被打开两次时,两个文件描述符有各自的文件指针。
内核保存一个全局的文件描述结构体,而一个文件打开两次之后,两个结构体各自有各自的文件指针。
dup
dup函数可以复制文件描述符,让两个文件描述符指向同一个文件结构,通过dup复制文件描述符和两次打开文件描述符不同,所以两个文件描述符共享一个文件指针。
当一个文件描述符被关闭时,关闭的是内核的文件描述结构,但是如果文件描述结构体中,引用计数器不为1,那么close函数就只是减少了引用计数器而已。
#include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <string.h> #include <stdio.h> #include <stdlib.h> // 0, 1, 2 // 0 标准输入 // 1 标准输出 // 2 标准错误 int main() { printf("this is output to terminate\n"); // 保存1号文件描述符 int fd_save1 = dup(1); // 打开了一个新文件 int fd_file = open("new_output.txt", O_CREAT|O_RDWR, 0777); // 将新文件拷贝到1的位置 dup2(fd_file, 1); // 打印调试信息到文件 printf("this is output to file\n"); // 将保存的1号文件恢复到1号位置上 dup2(fd_save1, 1); // 此时再输出,则又输出到终端 printf("output to terminate again\n"); return 0; }
文件原子操作
原子操作是指一个操作一旦启动,则无法被能破坏它的其它操作打断。
-
写文件
无论是两次打开还是dup,同时操作一个文件都可能引起混乱,解决这个问题的方法是,可以通过O_APPEND解决这个问题。O_APPEND选项可以使得当一个写操作正在进行时,另外一个对该文件的写操作会阻塞等待。这意味着有O_APPEND选项的文件描述符,写操作无法被打断。
应用场景,多进程写Log文件
-
创建文件
除了写操作有原子性问题,创建文件也有,如果两个进程同时调用creat或者带O_CREAT的open,创建同一个文件时,可能会出现这种情况,第一个操作创建成功之后,写入数据,而第二个操作的O_CREAT把数据抹去了。
但是如果在O_CREAT之后,加上O_EXCL,那么可以避免这种情况。
fcntl和ioctl
fcntl可以用来设置文件描述符属性、文件状态属性、文件描述符复制、设置文件锁、设置文件通知等功能,这里只表示学习通过fcntl修改文件描述符属性。
如果一个文件描述符没有O_APPEND属性,但是后来又需要这个属性,那么可以通过fcntl来设置。
#include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <string.h> #include <stdio.h> #include <stdlib.h> // uint32_t status; // 位操作 int main() { int fd = open("t11.txt", O_CREAT|O_RDWR, 0777); if(fd < 0) { perror("open"); return -1; } // 通过fcntl修改文件属性,增加O_APPEND属性 int flags = fcntl(fd, F_GETFL); flags |= O_APPEND; fcntl(fd, F_SETFL, flags); lseek(fd, 0, SEEK_SET); write(fd, "hello", 5); lseek(fd, 0, SEEK_SET); write(fd, "world", 5); close(fd); return 0; }
ioctl是一个杂项函数,一般用于文件底层属性设置。
文件映射
文件映射能将硬盘映射到进程的地址,这样可以像操作内存一样操作文件,而且效率很高,但是有一定限制:
-
文件长度必须大于等于映射长度
-
映射的offset必须是页的整数倍
页的尺寸获取方式:
命令行getconf -a | grep PAGE_SIZE
函数sysconf(_SC_PAGE_SIZE)
//运用mmap实现文件的映射
#include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <string.h> #include <stdio.h> #include <stdlib.h> #include <sys/mman.h> int main() { int fd = open("a.map", O_RDWR); if(fd < 0) { perror("open"); return -1; } void* ptr = mmap(NULL, /* 指定映射的地址,如果为空,那么内核自动选择一个地址 */ 4096, /* 映射长度 */ PROT_READ|PROT_WRITE, /* 访问方式,要和打开文件使的flag一致 */ MAP_SHARED, /* 修改映射地址的数据,反应到硬盘,如果是MAP_PRIVITED,那么修改数据,不会刷新到硬盘 */ fd, /* 文件描述符 */ 0 /*从什么地方开始映射*/); if(ptr == MAP_FAILED) { perror("mmap"); return -1; } // 像访问内存一样的访问硬盘,虚拟内存 // 通过这种方式访问大文件效率更高 // 进程之间共享数据 strcpy((char*)ptr, "hello"); munmap(ptr, 4096); close(fd); return 0; }
临时文件
可以通过mktemp(3)来获取一个临时文件路径,但是该文件不一定在/tmp目录下,在哪个目录下需要程序员指定。
Linux还有更多的创建临时文件的函数,学有余力的同学可以通过
man 3 mktemp
,查看相关函数。
#include <stdio.h> #include <stdlib.h> int main() { char buf[] = "./hello-XXXXXX"; char* p = mktemp(buf); printf("p=%s\n", p); printf("buf=%s\n", buf); }
缓存
为了提高IO效率,系统为应用程序提供了缓存服务。当应用程序写数据到硬盘时,内核只是将数据写入内核缓存,然后返回成功。
缓存的存在隐藏风险,如果缓存数据未写入硬盘时,发生断电故障,那么会导致数据的不完整性。
关键数据的不完整,可能会导致系统崩溃。
使用O_SYNC选项打开文件时,那么写入操作将保证数据写入到硬盘再返回,当然这个选项导致IO效率降低。
也可以使用sync
,fsync
,fsyncdata
之类的函数,将数据写入硬盘。
fwrite和write都有缓存,不过fwrite在用户空间和内核空间都有缓存,而write只有在内核空间有缓存。
补充
标准输入/输出/错误
每一个进程都默认打开三个文件,三个文件描述符分别是0,1,2。printf其实是调用write(1)实现的。
一般不直接使用0,1,2来表示三个文件,而是用宏STDIN_FILENO,STDOUT_FILENO, STDERR_FILENO来表示输入、输出、错误。
open返回值
返回可用的最小的文件描述符。
小于0,表示对文件操作失败
文件描述符
进程范围内唯一。
dup2
也是用来赋值文件描述符,第二个参数指示复制的位置。
使用的命令和函数总结
函数
open/creat:打开文件/创建文件
read:读文件
write:写文件
close:关闭文件
lseek:定位文件读写位置
fcntl:修改文件属性
sysconf:读取系统配置
dup/dup2:复制文件描述符
sync/fsync/fsyncdata:同步文件数据
mmap/munmap:文件映射
mkstemp:得到临时文件路径
命令
touch:修改文件的访问时间,创建文件
cat:访问文件内容
vim:编辑
ulimit:显示一些限制信息(文件描述符最大值、栈的空间尺寸)
umask:文件创建的权限掩码
getconf:对应sysconf
Linu基础 文件IO(读写操作)