前言

本章讨论普通文件的读写、读写效率、简单介绍文件描述符、IO效率、文件共享和原子操作、dup、文件映射、临时文件。

文件描述符

在Linux系统中,打开的文件是用一个整数来表示的,表示打开文件的整数,称之为文件描述符。当需要往写数据/读数据时,读写函数都需要文件描述符作为参数,以便系统知道用户操作的时哪个文件。

文件基本操作

open/creat

mode选项 解释
O_RDONLY 读方式打开
O_WRONLY 写方式打开
O_RDWR 读写方式打开
O_CREAT 创建文件,如果文件存在,会被截断
O_TRUNC 截断
O_APPEND 追加
O_EXCL 和O_CREAT一起用,如果文件存在则失败
int open(const char* path, int flag, ...);

open函数的flag值得是mode选项(注意互斥问题)。第三个参数指示新建的文件的属性。文件真实的权限,受umask的影响。影响方法

真实mode = 指定的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效率降低。

也可以使用syncfsyncfsyncdata之类的函数,将数据写入硬盘。

fwrite和write都有缓存,不过fwrite在用户空间和内核空间都有缓存,而write只有在内核空间有缓存。

补充

标准输入/输出/错误

每一个进程都默认打开三个文件,三个文件描述符分别是0,1,2。printf其实是调用write(1)实现的。

一般不直接使用0,1,2来表示三个文件,而是用宏STDIN_FILENO,STDOUT_FILENO, STDERR_FILENO来表示输入、输出、错误。

open返回值

返回可用的最小的文件描述符。

小于0,表示对文件操作失败

文件描述符

进程范围内唯一。

dup2

也是用来赋值文件描述符,第二个参数指示复制的位置。

dup2(int oldfd, int newfd);

使用的命令和函数总结

函数

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(读写操作)的更多相关文章

  1. C# System.IO和对文件的读写操作

      System.IO命名空间中常用的非抽象类 BinaryReader 从二进制流中读取原始数据 BinaryWriter 从二进制格式中写入原始数据 BufferedStream 字节流的临时存储 ...

  2. 『无为则无心』Python基础 — 41、Python中文件的读写操作(一)

    目录 1.文件操作步骤 2.文件的读写操作 (1)文件的打开 (2)打开文件模式 (3)获取一个文件对象 (4)关于文件路径 1.文件操作步骤 当我们要读取或者写入文件时,我们需要打开文件,在操作完毕 ...

  3. (代码篇)从基础文件IO说起虚拟内存,内存文件映射,零拷贝

    上一篇讲解了基础文件IO的理论发展,这里结合java看看各项理论的具体实现. 传统IO-intsmaze 传统文件IO操作的基础代码如下: FileInputStream in = new FileI ...

  4. android报错及解决2--Sdcard进行文件的读写操作报的异常

    报错描述: 对Sdcard进行文件的读写操作的时候,报java.io.FileNotFoundException: /sdcard/testsd.txt (Permission denied),在往S ...

  5. Hadoop基础-通过IO流操作HDFS

    Hadoop基础-通过IO流操作HDFS 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 一.上传文件 /* @author :yinzhengjie Blog:http://www ...

  6. C# 运用StreamReader类和StreamWriter类实现文件的读写操作

    对文件的读写操作应该是最重要的文件操作,System.IO命名空间为我们提供了诸多文件读写操作类,在这里我要向大家介绍最常用也是最基本的StreamReader类和StreamWriter类.从这两个 ...

  7. 使用字符流(Writer、Reader)完成对文件的读写操作

    字符流 字符输出流:Writer,对文件的操作使用子类FileWriter 字符输入流:Reader,对文件的操作使用子类FileReader 每次操作的是一个字符 文件字符操作流会自带缓存,默认大小 ...

  8. Java 对不同类型的数据文件的读写操作整合器[JSON,XML,CSV]-[经过设计模式改造](2020年寒假小目标03)

    日期:2020.01.16 博客期:125 星期四 我想说想要构造这样一个通用文件读写器确实不容易,嗯~以后会添加更多的文件类型,先来熟悉一下文件内容样式: <?xml version=&quo ...

  9. INI 文件的读写操作

    在C#中对INI文件进行读写操作,在此要引入using System.Runtime.InteropServices; 命名空间,具体方法如下: #region 变量 private static r ...

随机推荐

  1. day03-MySQL基础知识02

    MySQL基础知识02 4.CRUD 数据库CRUD语句:增(create).删(delete).改(update).查(Retrieve) Insert 语句 (添加数据) Update 语句(更新 ...

  2. 矩阵顺时针打印(C++)(? 为什么不能AC,9度1391)

    #include <iostream> #include <fstream> using namespace std; int a[1000][1000]; void prin ...

  3. centos7 安装RabbitMQ3.6.15 以及各种报错

    成功图镇楼 各个版本之间的差异不大,安装前要确保rabbitmq 的版本和 elang的版本一致.预防各种错乱. 注意点:(重要!!重要!!) * 同时安装的时候最好确保rabbitmq和erlang ...

  4. Linux系统管理_网络管理

    常用网络指令 yum -y install net-tools #安装ifconfig工具 ifconfig #查看网络配置 ifup ens33 #启用网卡 ifdown ens33 #禁用网卡 s ...

  5. 齐博x1 APP要实现直播的关键两步

    大家务必要注意,缺少这两步,你的APP将不能实现直播, 也即点击直播按钮无法启动直播推流

  6. vue3学习记录(新特性)

    总概 1) 性能提升 打包大小减少 41% 初次渲染快 55%,更新渲染快 133% 内存减少 54% 使用 Proxy 代替 defineProperty 实现数据响应式 重写虚拟 DOM 的实现和 ...

  7. mybatis-获取参数值的方式

    MyBatis获取参数值的两种方式(重点) MyBatis获取参数值的两种方式:${}和#{} ${}的本质就是字符串拼接,#{}的本质就是占位符赋值 ${}使用字符串拼接的方式拼接sql,若为字符串 ...

  8. 2022,一个Java程序猿的装机配置

    起因 工作一年,身边的老同学都在让我推荐适合他们需求的PC主机.于是仔细研究了一下当下的主机配置.成功试水并积攒了经验后,也给自己装了我的第一台PC主机. 主机配置 CPU:12700 主板:华硕TU ...

  9. Day09:switch——case结构的使用详解

    switch--case结构的使用详解 什么是switch--case结构 他也是一种多选择结构 switch--case结构是类于if--else的语法,通过比较而输出对应的内容: 通俗的讲,好比我 ...

  10. 【题解】CF1503B 3-Coloring

    题面传送门 解决思路 讲一下 \(\text{VP}\) 时的思路. 首先想到,只要能将棋盘中红色或蓝色部分全部填成同一个数,那么剩下的就不会受限了(可行有两个,限制只有一个): 但考虑到交互库可能有 ...