unix高级编程系列之文件I/O

news/2024/10/5 22:35:46/

背景

作为linux 开发者,我们不可避免会接触到文件编程。比如通过文件记录程序配置参数,通过字符设备与外设进行通信。因此作为合格的linux开发者,一定要熟练掌握文件编程。在文件编程中,我们一般会有两类接口函数:标准I/O(带缓冲)和POXIS.1 I/O(不带缓冲)。本章节主要介绍不带缓冲的相关API及注意事项。

open 接口

open函数的作用是打开或创建一个文件。其声明如下:

int open(const char* path, int oflag, .../*mode_t mode*/);

参数解析:

  • path :是打开或需要创建的文件名称;
  • oflag : 设置打开文件的权限,该参数取值范围较广。并且需要区分。大致可以分为两类:
  1. 权限类型标识。需要关注的有O_RDONLY(只读权限)、O_WRONLY(只写权限)、O_RDWR(可读写权限)。这三个标识位必须指定一个且只能指定一个。(O_ECEC(只执行)和O_SEARCH(只搜索)已被移除)。
  2. 特性类标识。这些标识可多选,常见的有如下:
标识常量含义
O_APPEND每次写都追加到文件的尾端。即使你显式的调用lseek改变文件当前偏移量,但是在write时,依然会追加到文件末尾
O_CREAT若文件不存在则创建它。
O_EXCL如果同时指定了O_CREAT,而文件已经存在,则出错。经常用于判断文件是否存在,与access()函数功能类似。
O_NOBLOCK如果path引用的是一个FIFO、块特殊文件、字符特殊文件。那么本文件描述符后续的I/O操作,都设置为非阻塞方式。
O_SYNC每次write 都会等待物理I/O操作完成,包括文件属性更新。 在linux ext4 系统中,该标识可能不生效
O_TRUNC如果文件存在,且以只写或读写权限打开。则将长度截断为0。常见的业务场景就是更新配置文件。
  • mode 可选参数。只有当oflag参数中,具备O_CREAT属性时,才需要指定新创建的文件权限。

知识点open 函数返回的文件描述符一定时最小的未用描述符数值

基于上述知识点,经常会被用来重定向程序的标准输入、标准输出或标准错误输出。

场景如下:有一个封装库内部是通过采用的是printf进行日志打印,无法体现在我们日志系统中。我们如何观察其日志输出呢?常见做法如下:

#include<stdlib.h>
#include<stdio.h>
int main()
{ printf("hello world\n");return 0;
}

默认情况下,日志输出到终端:

xieyihua@xieyihua:~/test$ gcc 1.c  -o 1
xieyihua@xieyihua:~/test$ ./1
hello world
xieyihua@xieyihua:~/test$

可以做以下修改:

#include<stdlib.h>
#include<stdio.h>
#include<fcntl.h>
#include<unistd.h>
int main()
{close(1); /* 关闭 文件描述符1'*/open("./log",O_WRONLY|O_CREAT,0755);/* 此时文件描述符1 与 ./log文件绑定*/printf("hello world\n");return 0;
}

输出如下:

xieyihua@xieyihua:~/test$ gcc 1.c -o 1
xieyihua@xieyihua:~/test$ ./1
xieyihua@xieyihua:~/test$ cat log
hello world
xieyihua@xieyihua:~/test$

creat 接口

creat函数主要用于创建一个新文件。函数原型声明如下:

#include <fcntl.h>
int creat(const char* path, mode_t mode);

其等效于:open(path,O_WRONLY|O_CREAT|O_TRUNC,mode);但是由于creat只能以只写方式打开文件,使用场景便较少,渐渐被‘冷落’了。

close 接口

close函数关闭一个打开文件。其函数原型声明如下:

#include<unistd.h>
int close(int fd);

知识点当一个进程终止时,内核会自动关闭它所有的打开文件。很多程序都利用了这一功能而不显示调用close

lseek 接口

每个打开文件都有一个与其相关的“当前文件偏移量”。它通常是一个非负整数(/dev/kmem/支持负的偏移量),用于度量从文件开始处计算的字节数。通常读、写操作都是从当前文件偏移处开始的,并使偏移量增加所读写的字节数。

lseek接口就可以显式的为一个打开的文件设置偏移量。其函数原型声明如下:

#include <unistd.h>
off_t lseek(int fd,off_t offset,int whence);
  • fd, 文件描述符
  • offset,其含义与whence 的值相关。
  1. whenceSEEK_SET,则将该文件的偏移量设置为距文件开始处的offset个字节。
  2. whenceSEEK_CUR,则将该文件的偏移量设置为当前值加上offset个字节,offset可为正或负。
  3. whenceSEEK_END,则将该文件的偏移量设置为文件长度加上offset个字节,offset可为正或负。

知识点 系统默认情况下,当打开一个文件时,除非指定O_APPEND选项,否则该偏移量被设置为0

lseek 仅是将当前的文件偏移量记录在内核中,并不引起任何I/O操作,其目的是用于接下来的读写操作。

空洞文件

文件偏移量可以被设置为大于文件当前长度,在这种情况下,对该文件的下一次写将加长该文件,并在文件中构成一个空洞。位于文件中没有写过的字节都被读为0。并且文件中的空洞并不要求在磁盘上占用存储区。示例代码如下:

#include<fcntl.h>
#include<unistd.h>
#include<stdlib.h>
#include<stdio.h>int main()
{int fd;char* buf1 = "123456789a";char* buf2 = "abcdefghij";if((fd = creat("file.hole",0755)) < 0){printf("creat error\n");return -1;}printf("fd = %d\n",fd);if(write(fd,buf1,10) != 10){printf("write buf1 error\n");}if(lseek(fd,16384,SEEK_SET) == -1){printf("lseek error\n");}if(write(fd,buf2,10) != 10){printf("write buf1 error\n");}return 0;
}

结果如下:

/*文件长度有16394 Byte*/
xieyihua@xieyihua:~/test$ ls -la file.hole
-rwxr-xr-x 1 xieyihua xieyihua 16394 Jul  4 01:58 file.hole/*文本的实际内容也只有两个字符串*/
xieyihua@xieyihua:~/test$ od -c file.hole   
0000000   1   2   3   4   5   6   7   8   9   a  \0  \0  \0  \0  \0  \0
0000020  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0
*
0010000   a   b   c   d   e   f   g   h   i   j
0010012
xieyihua@xieyihua:~/test$ cat file.hole
123456789aabcdefghijxieyihua@xieyihua:~/test$/* 没有空洞的文件其占用了16个磁盘块*/
xieyihua@xieyihua:~/test$ ls -ls file.*8 -rwxr-xr-x 1 xieyihua xieyihua 16394 Jul  4 01:58 file.hole
16 -rwxr-xr-x 1 xieyihua xieyihua 16384 Jul  4 01:58 file.nohole

文件空洞的特性:分配了文件偏移量范围,但是实际却没有分配磁盘空间。我们一般可在两个方向应用:

  1. 多线程下载。当创建一个巨大的文件时,单个线程逐步构建文件会耗费大量时间。一种优化思路是将文件划分为多个段,利用多线程同时操作,每个线程负责写入其中一段数据。这类似于现实生活中修路的场景,如修建高速公路时,单个施工队的进度可能较慢,但通过安排多个施工队,每个队负责修建一段,最终将它们连接起来,大大提高了效率。
  2. 共享内存。当不同进程需要共享内存时,并不清楚实际需要多大的文件,可以先开辟一个大文件。比如:在创建虚拟机时,如果一开始就分配了100GB的磁盘空间,而实际上系统安装完成后可能只使用了3、4GB的空间,这就是空洞文件的应用。通过空洞文件,可以避免一开始就分配过多的资源,节约了存储空间的浪费。

read 接口

read接口用于向打开文件中读数据。其原型声明如下:

#include<unistd.h>
ssize_t read(int fd, void* bff, size_t nbyte);

read成功,则返回读到的字节数,如果已经达到文件的尾端,则返回0。

知识点 大多数文件系统为改善性能都会采用某种预读技术,即即使你每次仅读取100Byte内容,但是实际上会从磁盘中读取一页数据,保存在内存中。从而减少磁盘I/O操作,提高系统性能。但是也会增加内存使用压力。

write 接口

write接口用于向打开文件写数据。其接口声明如下:

#include <unistd.h>
ssize_t write(int fd, const void* buf,size_t nbytes);

其返回值通常与参数nbytes的值相同,否则标识出错。其出错原因:

  1. 磁盘已写满
  2. 超过文件限定长度

linux 内核标识打开文件的方式

linux 内核通过三个数据结构表示打开的文件。记录项、文件表项、V节点。其三者关系大致如下:

  • 进程表项中,记录中文件描述符文件表项的关系
  • 文件表项中,记录文件状态标志位当前文件偏移量V节点指针
  • V节点中,记录文件类型各种操作函数指针指向i节点。而i节点包含文件的详细信息,比如:文件的所有者、文件长度、指向文件实际数据块再磁盘上所在位置的指针等。

注意:每一个文件只有一个唯一的V节点表;多个文件表项可以指向同一个V节点表,每调用一次open,则创建一个新的文件表项;不同fd可以指向同一个文件表项;即可能存在以下场景:

正是这样的机制原理,linux 让我们可以多任务同时访问同一文件。但是在写文件时,我们需要关注写入时序以及数据错乱问题。

  • 在完成每一个readwrite操作后,文件表项中的当前文件偏移量增加所读写的字节数。
  • 如果使用O_APPEND标志打开一个文件,则响应标志也被设置到文件表项的文件状态标志中。每次进行写操作时,文件表项中的当前文件偏移量首先会被设置为表项中的文件长度。这就确保每次写入的数据追加到当前尾端处。
  • lseek 函数只是修改文件表项中的当前文件偏移量,不进行任何I/O操作。
  • 每一个进程都有它自己的文件表项和进程表项。

dup和dup2

这两个接口用于复制一个现有的文件描述符。其接口声明如下:

#include<unistd.h>int dup(int fd);
int dup2(int fd, int fd2);
/*两函数的返回值:若成功,返回新的文件描述符;若出错,返回-1*/

dup返回新的文件描述符一定是当前可用文件描述符中的最小数值。其效果就是多个fd指向同一个文件表项。其关系与上图中多线程访问文件一致。

sync、fsync、fdatasync接口

传统的linux 系统中设备缓冲区高速缓存或页高速缓存,大多数磁盘I/O都通过缓冲区进行。当我们向文件写入数据时,内核通常先将数据复制到缓冲区中,然后排入队列,晚些时候在写入磁盘。这种方式称为“延迟写”。

“延迟写”虽然提高了write的响应速度(不需要等待数据经过IO,写入磁盘)。但是也带来了风险:当应用层认为已经将数据写入文件了,但实际数据还并没有落入磁盘。若此时系统出现异常,则会将这部分数据丢失。为了避免这种情况,linux 系统提供了syncfsyncfdatasync接口,应用层主动要求内核将缓冲区中的数据进行落盘。原型声明如下:

#include<unistd.h>
int fsync(int fd);
int fdatasync(int fd);void sync(void);
  • sync 只是将所有修改过的块缓冲区排入写队列,然后就返回。它并不等待实际写磁盘操作结束
  • fsync 函数只对文件描述符fd指定的文件起作用,并等待写磁盘操作结束才返回
  • fdatasync 函数类似于 fsync,但只影响文件的数据部分。

注: open 接口中有一个标识 O_SYNC含义标识同步写,但经过验证,似乎并不起作用,与预期不一致。建议为了保险起见,还是调用fsync接口。

总结

文件编程是Linux开发者必须掌握的技能。本文介绍了Linux文件编程中常用的API及其注意事项,包括open、creat、close、lseek、read、write、dup和dup2等。还介绍了sync、fsync和fdatasync等接口,用于确保数据安全。此外,文章还解释了Linux内核如何标识打开的文件,以及文件表项、V节点和进程表项之间的关系。希望能给您带来帮助。

若我的内容对您有所帮助,还请关注我的公众号。不定期分享干活,剖析案例,也可以一起讨论分享。
我的宗旨:
踩完您工作中的所有坑并分享给您,让你的工作无bug,人生尽是坦途

在这里插入图片描述

参考文章:https://applink.feishu.cn/client/message/link/open?token=AmX27V1AQAADZjdT9KRAgAQ%3D


http://www.ppmy.cn/news/1473494.html

相关文章

【HICE】dns正向解析

1.编辑仓库 2.挂载 3.下载软件包 4.编辑named.conf 5.编辑named.haha 6.重启服务 7.验证本地域名是否解析

MinIO:开源对象存储解决方案的领先者

MinIO:开源对象存储解决方案的领先者 MinIO 是一款开源的对象存储系统&#xff0c;致力于提供高性能、可伸缩、安全的数据存储解决方案。 官方解释&#xff1a;MinIO 是一个基于Apache License v2。0开源协议的对象存储服务。它兼容亚马逊S3云存储服务接口&#xff0c;非常适…

firewalld 高级配置

一&#xff1a;firewalld 高级配置 1.1:关于iptables的知识 IP 数据包过滤系统是一种功能强大的工具&#xff0c;可用于添加、编辑和除去规则&#xff0c;这些规则是在做数据包过滤决定时&#xff0c;防火墙所遵循和组成的规则。这些规则存储在专用的数据包过滤表中&#xf…

【优化论】约束优化算法

约束优化算法是一类专门处理目标函数在存在约束条件下求解最优解的方法。为了更好地理解约束优化算法&#xff0c;我们需要了解一些核心概念和基本方法。 约束优化的核心概念 可行域&#xff08;Feasible Region&#xff09;&#xff1a; 比喻&#xff1a;想象你在一个园艺场…

基于Java的微信记账小程序【附源码】

摘 要 随着我国经济迅速发展&#xff0c;人们对手机的需求越来越大&#xff0c;各种手机软件也都在被广泛应用&#xff0c;但是对于手机进行数据信息管理&#xff0c;对于手机的各种软件也是备受用户的喜爱&#xff0c;记账微信小程序被用户普遍使用&#xff0c;为方便用户能够…

第4-5天:30余种加密编码和资产架构端口应用CDNWAF站库分离负载均衡

文章目录 前言知识点常见加密编码等算法解析 资产架构&端口&应用&CDN&WAF&站库分离&负载均衡资产架构番外安全考虑阻碍 前言 在安全测试中常见的敏感信息密码等会采用加密方式&#xff0c;因此作为一名安全人员要了解常见加密。 知识点 主要有存储加…

项目实战--Spring Boot与PageHelper的集成及线程污染解决

一、PageHelper使用背景 公司要做个简单管理系统&#xff0c;要我搭建Spring BootMyBatisPageHelperRedis的项目框架然后交i给实习生来开发。这个其实很简单&#xff0c;但是遇到搭建和使用过程中PageHelper有好多小坑&#xff0c;就记录一下&#xff0c;避免再踩。 版本选择&…

android 定时调用方法

在Android中&#xff0c;可以使用Handler类和Runnable接口来实现定时调用方法。以下是一个简单的例子&#xff0c;展示了如何每隔一定时间调用一个方法。 import android.os.Handler; import android.os.SystemClock; import androidx.appcompat.app.AppCompatActivity; impor…