【Linux】从open到write:系统文件I/O 的奥秘与实战指南

news/2024/10/27 3:18:24/

在这里插入图片描述

文章目录

  • 1.经典回顾C文件接口
    • 1.2 fwrite
    • 1.2 fread
  • 2. 系统文件I/O
  • 2 open函数
    • 2.1 参数介绍:
    • 2.2 返回值(文件描述符)
      • 2.2.1 文件描述符的分配规则
      • 2.2.2 重定向
  • 3. write函数
  • 4. read函数
  • 5. 总结

1.经典回顾C文件接口

在使用C语言时,我们需要访问文件通常会用到fopenfwrite、和fread还有fclose等函数。

1.2 fwrite

比如此时我需要往文件中写入一些信息:

#include <stdio.h>
#include <string.h>int main()
{FILE* fp = fopen("test.txt","w");if(fp == NULL){perror("fopen failed");return 1;}const char* str = "i am yui~\n";int len = strlen(str);int num = 5;while(num--){fwrite(str,len,1,fp);}fclose(fp);return 0;
}

执行结果:

ubuntu@VM-20-9-ubuntu:~/FILETEST$ cat test.txt
i am yui~
i am yui~
i am yui~
i am yui~
i am yui~

1.2 fread

下面再来读一读文件中的内容:

#include <stdio.h>
#include <string.h>int main()
{FILE* fp = fopen("test.txt","r");if(!fp){perror("fopen failed");return 1;}const char* str = "i am yui~\n";char s[1024];int len = strlen(str);while(1){ssize_t n  = fread(s,1,len,fp);if(n == len){s[len] = 0;printf("%s",s);}if(feof(fp))break;}return 0;
}

输出结果:

i am yui~
i am yui~
i am yui~
i am yui~
i am yui~

2. 系统文件I/O

除了利用上述C接口,我们还可以采用系统接口来访问文件。

系统文件 I/O(输入/输出)是指在操作系统层面进行文件的读写操作。在 Linux 和其他类 Unix 系统中,系统文件 I/O 通常通过系统调用(system call)完成。与 C 标准库的文件 I/O 函数(如 fopenfreadfwrite)相比,系统文件 I/O 提供了更底层的控制和更高的效率,但操作也稍显复杂。

为了更好的理解系统文件I/O,我会用系统接口来实现上面的功能,并进行讲解。

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <fcntl.h>
#include <sys/stat.h>int main()
{umask(0);//去除限制,防止后续干扰int fd  = open("myfile",O_WRONLY|O_CREAT,0644);if(fd<0){perror("open failed");return 1;}int num = 5;const char* str = "i am yui\n";int len = strlen(str);while(num--){write(fd,str,len);}close(fd);return 0;
}

2 open函数

因为现在我们需要用系统接口来打开文件,那么我们会用到open函数而不是fopen函数。
open 函数是 Unix 和类 Unix 操作系统中的一个系统调用,用于打开文件并返回一个文件描述符。这个文件描述符用于后续的文件操作,如读、写、关闭等。相比 C 标准库的 fopen 函数,open 提供了更底层的控制,更适合系统级编程

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>int open(const char *pathname, int flags, mode_t mode);

2.1 参数介绍:

  • pathname:要打开的文件的路径。
  • flags:指定文件打开模式和行为的标志,决定文件的打开方式。
  • mode:新文件的权限掩码,仅在O_CREAT标志创建文件时生效,指定文件的访问权限。
    这之中的flags要好好聊聊。
  • 访问模式(必须包含一个):
    • O_RDONLY:只读模式打开文件。
    • O_WRONLY:只写模式打开文件。
    • O_RDWR:读写模式打开文件。
      O_RDONLYO_WRONLYO_RDWR 中只能选择一个,它们控制文件的基础读写权限。
  • 文件创建和控制
    • O_CREAT:若文件不存在,则创建文件。此标志常与 mode 参数一起使用来指定文件的权限。
    • O_EXCL:必须与 O_CREAT 组合使用。如果文件已存在,则返回错误,避免重复创建。这种组合常用于创建唯一文件。
    • O_TRUNC:如果文件存在,并且是以写模式(O_WRONLYO_RDWR)打开,文件长度会被截断为 0。
    • O_APPEND:追加模式,写入操作时,文件指针会自动移动到文件末尾,适合日志记录等追加写入的场景。
  • 非阻塞和同步控制
    • O_NONBLOCK:以非阻塞模式打开文件。对一些特殊文件(如设备文件)有效,适合需要立即返回结果的场景。
    • O_SYNC:同步写入模式,确保数据立即写入磁盘。每次 write 操作都不会缓存到内存,而是直接刷新到存储设备,适合数据持久性要求高的场景。
    • O_DSYNC:数据同步,类似 O_SYNC,但只同步数据而不包括文件元数据(如最后修改时间)。
    • O_RSYNC:同步读模式,和 O_SYNC 类似,但影响的是 read 操作。
      我们需要选择合适的功能将它们进行|的操作,因为底层是用状态压缩来实现的,通过位运算(位掩码)来实现,使得每一个标志可以独立设置或清除,而不需要为每种组合单独存储。

在下面的写入操作我们只需要选择O_WRONLY|O_CREAT就可以了。

2.2 返回值(文件描述符)

  • 成功时,open 返回一个文件描述符(非负整数),用于后续的文件操作。
  • 失败时返回 -1,并设置 errno 来指示错误原因。
    这里的返回值也很有说法,
    文件描述符(File Descriptor, FD)是操作系统分配的一个整数,用于表示每一个打开的文件或 I/O 资源。在 Unix 和类 Unix 系统(如 Linux)中,文件描述符是进程和内核之间进行文件或资源操作的桥梁,几乎所有的 I/O 操作都是通过文件描述符来完成的。
    文件描述符是一个非负整数,每个进程有一个文件描述符表来管理文件描述符。打开文件时,操作系统会分配一个文件描述符,用于标识这个文件。该文件描述符可以用于后续的读、写、关闭操作。文件描述符不仅用于文件,也可以表示其他 I/O 资源,如管道、网络套接字、设备文件等。
    每个进程在启动时,通常有三个默认的文件描述符,它们称为标准文件描述符:
  1. 标准输入(stdin):文件描述符为 0,用于从用户或输入源读取数据。
  2. 标准输出(stdout):文件描述符为 1,用于向终端或输出源输出数据。
  3. 标准错误(stderr):文件描述符为 2,用于向终端输出错误信息。
    0,1,2对应的物理设备一般是:键盘,显示器,显示器。
    了解完这些后,我们就可以直接利用文件描述符来直接向显示屏输出数据了。
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>int main()
{char buf[1024];ssize_t s = read(0,buf,sizeof(buf));//从键盘读入数据if(s>0){buf[s] = 0;write(1,buf,strlen(buf));write(2,buf,strlen(buf));}return 0;
}//运行结果
/**
ubuntu@VM-20-9-ubuntu:~/FILETEST$ ./a.out 
hello world
hello world
hello world*/

从这段代码我们也可以更加清晰地认识到Linux下的一切皆文件。
一些底层知识:
文件描述符是从0开始地小整数,当我们打开文件时,操作系统在内存中要创建相对应地数据结构来描述目标文件,于是就有了file结构体来表示一个已经打开地文件对象。而进程执行open系统调用,必须让进程和文件关联起来。每一个进程都有一个指针*file,指向一张表file_struct该表最重要地部分包含一个指针数组,每个元素都是一个指向文件地指针。本质上文件描述符就是该数组地下标,所以只要拿着文件描述符就可以找到对应地文件。
底层知识

2.2.1 文件描述符的分配规则

先看代码:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>int main()
{int fd = open("myfile",O_RDONLY);//只读模式打开if(fd>0){printf("%d\n",fd);}close(fd);return 0;
}
//运行结果:
/*
ubuntu@VM-20-9-ubuntu:~/FILETEST$ ./a.out 
3
*/

结果是3。
那么当我们关闭0这个文件描述符试试看呢?

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>int main()
{close(0);//关闭文件描述符0int fd = open("myfile",O_RDONLY);//只读模式打开if(fd<0){perror("open");return 1;}printf("%d\n",fd);close(fd);return 0;
}
//打印结果:
/*
ubuntu@VM-20-9-ubuntu:~/FILETEST$ ./a.out 
0
*/

结果是0,你猜到了吗。
由此可见,文件描述符的分配规则:在file_struct数组当中,找到当前没有直接使用的最小的一个下标,作为新的文件描述符。
最后在来看看重定向

2.2.2 重定向

现在我们将标志输出1给关闭了,然后再打开一个文件再往里面写点东西,看看会发生什么。

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>int main()
{close(1);//关闭文件描述符1int fd = open("myfile",O_WRONLY|O_CREAT,0644);//写模式打开if(fd<0){perror("open");return 1;}printf("fd:%d\n",fd);fflush(stdout);close(fd);return 0;
}

打开myfile发现,文件中存在fd:1
也是就说,本该再显示屏中显示的内容被写进了myfile文件。我们把这种现象叫做重定向。常见的重定向>, >>, <
重定向的本质:
重定向的本质

3. write函数

write 函数是 Unix 和 Linux 系统中进行文件写入操作的系统调用,用于将数据从用户空间的缓冲区写入到文件或设备(例如文件、管道、网络套接字)中。write 是一种底层 I/O 操作,它绕过标准 I/O 缓冲区,直接写入文件描述符指向的目标,常用于处理系统资源的原始数据读写。
语法

ssize_t write(int fd, const void *buf, size_t count);

参数说明

  • fd:文件描述符,表示要写入的目标文件或设备(例如 STDOUT_FILENO 表示标准输出)。
  • buf:缓冲区指针,指向要写入的数据。
  • count:要写入的字节数,指定从 buf 中读取多少字节写入 fd
    返回值
  • 成功时,返回实际写入的字节数(ssize_t 类型)。
  • 失败时,返回 -1,并设置 errno 变量来指示错误原因。

4. read函数

read 是 Unix 和 Linux 系统中的一个系统调用,用于从文件或其他输入资源(如管道、网络套接字等)中读取数据到用户提供的缓冲区中。与 write 相对应,read 直接从文件描述符中获取数据,不经过标准 I/O 缓冲区,适合低级别的 I/O 操作。
语法

ssize_t read(int fd, void *buf, size_t count);

参数说明

  • fd:文件描述符,表示要读取的文件或输入资源(例如 STDIN_FILENO 表示标准输入)。
  • buf:缓冲区指针,指向读取到的数据将要存放的位置。
  • count:期望读取的字节数,即 buf 的大小。
    返回值
  • 成功时,返回实际读取的字节数(ssize_t 类型)。
    • 若返回 0,表示读取到文件末尾(EOF)。
  • 失败时,返回 -1,并设置 errno 来指示错误原因。

5. 总结

fopenfclosefreadfwrite这些都是C语言标准库的函数,也就是库函数。
openclosereadwrite都是系统提供的接口,也就是系统调用接口。
而这部分库函数会区调用系统接口。
操作系统

可以认为,f*系列的函数,都是对系统的封装,方便二次开发。


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

相关文章

系统架构设计师 软件架构的定义与生命周期

软件架构的定义 通过一系列的设计活动&#xff0c;以满足系统的功能性需求和符合一定的非功能性需求与质量属性有相似含义的软件系统框架模式。在软件体系结构设计过程中&#xff0c;主要考虑的是系统的非功能性需求 软件体系结构设计经验的总结与重用是软件工程的重要目标之一…

浅说倍增算法——线性倍增

倍增 易错提醒 首先强调一下&#xff0c;倍增不是一种算法&#xff0c;它是一种思想&#xff0c;而像什么 RMQ 问题啊&#xff0c;ST 表啊&#xff0c;都是由倍增思想演变出来的一种算法。接下来我们来正式的接入今天的话题 什么是倍增 倍增&#xff0c;倍增&#xff0c;顾…

怎么给docker的redis设置密码

怎么给docker的redis设置密码 设置密码方式1&#xff1a;启动容器时设置 docker run -itd --name redis-v1 -p 6379:6379 redis --requirepass 123456说明: --name (启动容器的名称) -p 宿主机映射端口:容器里的redis启动端口 --requirepass 启动密码 设置密码方式2&#xf…

Android 判断蓝牙是否开启,监听蓝牙状态,处理客制化需求

import android.bluetooth.BluetoothAdapter; 【BluetoothAdapter.java】 SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) public static final String ACTION_STATE_CHANGED "android.bluetooth.adapter.action.STATE_CHANGED";//当前acti…

用nginx实现多ip访问多网址

1.关闭防火墙 2.挂载并安装nginx 3.启动http 4.增加两个虚拟网卡 5.创建目录 6.写入内容到该网站 [rootlocalhost ~]# echo this is 129 > /www/ip/130/index.html [rootlocalhost ~]# echo this is 1139 > /www/ip/139/index.html7.重启服务加载配置 [rootlocalhos…

《Windows PE》7.4 资源表应用

本节我们将通过两个示例程序&#xff0c;演示对PE文件内图标资源的置换与提取。 本节必须掌握的知识点&#xff1a; 更改图标 提取图标资源 7.4.1 更改图标 让我们来做一个实验&#xff0c;替换PE文件中现有的图标。如果手工替换&#xff0c;一定是先找到资源表&#xff0c;…

jenkins 自动化部署Springboot 项目

一、安装docker 1.更新yum命令 yum -y update2.查看机器有残留的docker服务&#xff0c;有就卸载干净 查看docker 服务 rpm -qa |grep docker卸载docker sudo yum remove docker-ce docker-ce-cli containerd.io sudo rm -rf /var/lib/docker sudo rm -rf /var/lib/contai…

用接地气的例子趣谈 WWDC 24 全新的 Swift Testing 入门(一)

概述 从 WWDC 24 开始&#xff0c;苹果推出了全新的测试机制&#xff1a;Swift Testing。利用它我们可以大幅度简化之前“老态龙钟”的 XCTest 编码范式&#xff0c;并且使得单元测试更加灵动自由&#xff0c;更符合 Swift 语言的优雅品味。 在这里我们会和大家一起初涉并领略…