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

news2024/10/5 13:13:09

背景

作为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.coloradmin.cn/o/1900921.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

原生JavaScript实现录屏功能

1. 前言 使用JavaScript实现浏览器中打开系统录屏功能 示例图: 2. 源码 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8" /><meta http-equiv"X-UA-Compatible" content"IEedge" /><…

关于CPU你一定要注意的重要参数,警惕韭菜陷阱

昨天遇到个奇葩事&#xff0c;有个粉丝喷我“懂不懂什么叫I9&#xff1f;”言下之意就是CPU中I9>i7>I5>I3&#xff0c;我也不知道咋说&#xff0c;只是提醒大家小心被坑&#xff0c;花了多的钱用的差的性能。作为回应&#xff0c;仅以此篇说下CPU咱们臭打游戏一定要知…

软件架构之计算机组成与体系结构

1.1计算机系统组成 计算机系统是一个硬件和软件的综合体&#xff0c;可以把它看成按功能划分的多级层次结构。 1.1.1 计算机硬件的组成 硬件通常是指一切看得见&#xff0c;摸得到的设备实体。原始的冯•诺依曼&#xff08;VonNeumann&#xff09;计算机在结构上是以运算器为…

基于Android Studio点餐项目,点餐app

目录 项目介绍 图片展示 运行环境 获取方式 项目介绍 实现登录、注册、注销功能&#xff0c;退出登录等功能&#xff0c; 以及基本的选择店铺点餐&#xff0c;加入购物车和结算等功能&#xff0c;以及可以增加或者减少商品的个数&#xff0c; 同时可以同步价格的总量。以…

智能化客户服务:提升效率与体验的新模式

在数字化浪潮的推动下&#xff0c;客户服务领域正经历着一场深刻的变革。智能化客户服务的兴起&#xff0c;不仅重塑了企业与客户之间的互动方式&#xff0c;更在提升服务效率与增强客户体验方面展现出了巨大潜力。本文将深入探讨智能化客户服务的新模式&#xff0c;分析其如何…

【C语言】自定义类型:联合和枚举

前言 前面我们学习了一种自定义类型&#xff0c;结构体&#xff0c;现在我们学习另外两种自定义类型&#xff0c;联合 和 枚举。 目录 一、联合体 1. 联合体类型的声明 2. 联合体的特点 3. 相同成员联合体和结构体对比 4. 联合体大小的计算 5. 用联合体判断当前机…

single_test_funi.py: error: the following arguments are required: img

parser.add_argument(img, defaultS/1.jpg, helpImage file) 当你已经指定了文件路径&#xff0c;还是报错怎么办&#xff1f; parser.add_argument(img, nargs?, defaultS/1.jpg, helpImage file) nargs? 表示 config 参数是可选的。如果用户没有提供这个参数&#xff0c…

25款404网页源码(下)

25款404网页源码&#xff08;下&#xff09; 13部分源码 14部分源码 15部分源码 16部分源码 17部分源码 18部分源码 19部分源码 20部分源码 21部分源码 22部分源码 23部分源码 24部分源码 25部分源码 领取完整源码下期更新 13 部分源码 .rail {position: absolute;width: 100%…

关注推送---Feed流,推模式实现的个人分析及其思考。

本篇文章记录我们实际开发过程中&#xff0c;关注推送场景的个人思考&#xff0c;以及解析。 文章目录 前言一、关注推送是什么&#xff1f;是什么是Feed流&#xff1f;二、解决关注推送问题的技术方案1.理论模型的选取2.数据类型的选取 三、理论模型的选取三、数据类型的选取总…

7寸微型FPV无人机技术详解

对于7寸微型FPV&#xff08;First Person View&#xff0c;第一人称视角&#xff09;无人机技术的详解&#xff0c;可以从以下几个方面进行介绍&#xff1a; 一、定义与基本概念 FPV无人机&#xff0c;全称为“第一人称视角无人机”&#xff0c;它利用安装在无人机上的摄像头…

QT c++函数模板与类模板的使用

QT c类模板的使用 #pragma once#include <QtWidgets/QMainWindow> #include "ui_QtWidgetsApplication5.h"class QtWidgetsApplication5 : public QMainWindow {Q_OBJECTpublic:QtWidgetsApplication5(QWidget *parent nullptr);~QtWidgetsApplication5();te…

HTML5使用<mark>标签:高亮显示文本

1、<mark>标签的使用 mark 标签用于表示页面中需要突出显示或高亮的一段文本&#xff0c;这段文本对于当前用户具有参考作用。它通常在引用原文以引起读者注意时使用。<mark>标签的作用相当于使用一支荧光笔在打印的纸张上标出一些文字。它与强调不同&#xff0c;…

Go语言--复合类型之指针与数组

分类 指针 指针是一个代表着某个内存地址的值。这个内存地址往往是在内存中存储的另一个变量的值的起始位置。Go 语言对指针的支持介于 Java 语言和 C/C语言之间,它既没有想 Java 语言那样取消了代码对指针的直接操作的能力,也避免了 C/C语言中由于对指针的滥用而造成的安全和…

使用Source Insight 4.0

一、使用书签 二、添加文件 三、Search 3.1 替换所有变量 四、右键查询 4.1 查看被调用的地方

ChatGPT对话:Scratch编程中一个单词,如balloon,每个字母行为一致,如何优化编程

【编者按】balloon 7个字母具有相同的行为&#xff0c;根据ChatGPT提供的方法&#xff0c;优化了代码&#xff0c;方便代码维护与复用。初学者可以使用7个字母精灵&#xff0c;复制代码到不同精灵&#xff0c;也能完成这个功能&#xff0c;但不是优化方法&#xff0c;也没有提高…

C++使用Poco库封装一个HTTP客户端类--Query参数

0x00 概述 我们使用Poco库的 Poco::Net::HTMLForm 类可以轻松实现表单数据的提交。 0x01 ApiPost提交表单数据 0x02 HttpClient类 #ifndef HTTPCLIENT_H #define HTTPCLIENT_H#include <string> #include <map> #include <Poco/URI.h> #include <Poco/N…

set的应用(C++)

set的使用 【基本用法】 大家可以敲一下这段代码体会一下set的基本初始化和使用 #include <iostream> #include <set> #include <vector> using namespace std;int main() {set<int> st1; // 空的set// 使用迭代器构造string str("abcdef"…

【机器学习】基于密度的聚类算法:DBSCAN详解

&#x1f308;个人主页: 鑫宝Code &#x1f525;热门专栏: 闲话杂谈&#xff5c; 炫酷HTML | JavaScript基础 ​&#x1f4ab;个人格言: "如无必要&#xff0c;勿增实体" 文章目录 基于密度的聚类算法&#xff1a;DBSCAN详解引言DBSCAN的基本概念点的分类聚类过…

Flower花所:稳定运营的数字货币交易所

Flower花所是一家稳定运营的数字货币交易所&#xff0c;致力于为全球用户提供安全、高效的数字资产交易服务。作为一家长期稳定运营的数字货币交易平台&#xff0c;Flower花所以其可靠的技术基础和优质的客户服务而闻名。 平台稳定性与可靠性&#xff1a; 持续运营&#xff1a;…