【C++与Linux】文件篇(2)- 文件操作的系统接口详解

本系列主要旨在帮助初学者学习和巩固Linux系统。也是笔者自己学习Linux的心得体会。

这次文章我进行了改进,力求表达的更加清楚,我会引出这篇文章的知识点。和前置知识的准备。

通过这篇文章你能学到什么,一般理论知识点在前,实践在后面


个人主页: 爱装代码的小瓶子
文章系列: Linux
2. C++


文章目录

1. 对于前置知识点的回忆,无缝进行衔接:

在上一篇文章中【C++与Linux基础】文件篇 -语言特性上的文件操作,我们提到了以下知识点:

  1. Linux一切皆是文件。(只是简单的理解,并没有深入)
  2. C语言提供的printffprintffgetsfreadfwrite,这几种接口
  3. C语言提供的几种打开文件的模式:只读,只写,追加等等

这里我们将持续深入文件操作结合Linux系统来完成本篇文章,当你读完本篇文章,你会知道以下几点:

  1. open 函数的参数解析(位图);
  2. 什么是fd(文件描述符)
  3. 如何使用write/read/close
  4. fd的分配规则(重定向的基础)

2.Linux最底层的文件打开方式:open()

open 函数是 Linux 系统中用于打开或创建文件的系统调用接口(System Call),它比 C 标准库的 fopen 更底层。

所需头文件如下:

cpp 复制代码
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

函数原型如下:

对于已经创建好的文件:

cpp 复制代码
int open(const char *pathname, int flags);

创建新文件(需要指定权限):

cpp 复制代码
int open(const char *pathname, int flags, mode_t mode);

2-1 参数const char *pathname解释:

这里的路径名称很简单:我们给出指定的文件名,如果前面不加路径,就是默认在进程所处文件下进行创建:

目标文件的路径(绝对路径或相对路径),例如 "data.txt" 或 "/home/user/log.txt"。

2-2 参数int flags标志位的解释:

决定文件的打开方式。必须包含以下访问模式之一(且只能选一个):

  • O_RDONLY:以只读方式打开。
  • O_WRONLY:以只写方式打开。
  • O_RDWR:以读写方式打开。(这个我们也用的少)

你可以通过按位或 (|) 运算符组合以下可选标志:

  • O_CREAT:如果文件不存在,则创建它。注意:使用此标志时必须提供第三个参数 mode。
  • O_APPEND:每次写操作都追加到文件末尾。
  • O_TRUNC:如果文件存在且以写方式打开,将其长度截断为 0(清空内容)。
  • O_EXCL:与 O_CREAT 连用,如果文件已存在,则返回错误(用于确保原子创建

你可能会在像传入这么多参数,计算机内部或者在调用的时候会不会混乱呢?

其实根本不会,这是因为flags 的实现原理基于计算机科学中最经典、最高效的设计之一:位掩码(Bitmask)。

在这里我们来简单的讲讲:

为了让多个参数互不冲突地存放在一个整数里,Linux 内核利用了二进制的特性。每一个标志(Flag)都对应整数中的特定的一位(bit)。

在这里我们假设:

我们规定每一位代表一种功能:

  • 第 0 位:O_WRITE (写模式) -> 0000 0001 (十进制 1)
  • 第 1 位:O_CREAT (创建) -> 0000 0010 (十进制 2)
  • 第 2 位:O_APPEND (追加) -> 0000 0100 (十进制 4)
  • 第 3 位:O_TRUNC (截断) -> 0000 1000 (十进制 8)

我们这里传入不同的值进行按位或,这里只要1就会在不同的位置会保留下来,在内部在通过与特定的位置进行按位与,只要是1,就说明满足了,就开始这个模式。

我们可以来写一个简单的程序来验证一下:

cpp 复制代码
#define A 1
#define B 2
#define C 4
#define D 8

void print(int flags){
    if(flags & A) printf("A is there\n");
    if(flags & B) printf("B is there\n");
    if(flags & C) printf("C is there\n");
    if(flags & D) printf("D is there\n");
}


int main()
{
    print(A | D);
    return 0;
}

这个代码就巧妙的解释了为什么可以通过位图的方式来完成控制。这段代码非常标准。位运算是 C 语言和系统编程的灵魂之一,它用最少的内存(一个 int)管理了多个开关状态,既高效又优雅。

2-3 第三个参数mode_t mode

当我们文件不存在的时候,我们进行创建的时候,需要传入这个参数,如果不传入呢,代码如下,结构如下:

c 复制代码
#include <stdio.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h> //这个是用来关闭文件的close


int main()
{
    int fd = open("log.txt",O_WRONLY | O_CREAT | O_APPEND);
    close(fd);
    return 0;
}

我们可以看到的确创建了新的文件在里面,但是权限似乎不大对劲,怎么和其他的普通文件不一样嘞。其实这里就是漏传了 mode的值。我们尝试传入便有:

cpp 复制代码
#include <stdio.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h> //这个是用来关闭文件的close


int main()
{
    int fd = open("log.txt",O_WRONLY | O_CREAT | O_APPEND,0666);
    close(fd);
    return 0;
}

这里为什么会变成0664 ,当然是我们系统自带的掩码了,其实我们也可以在自己的程序里面也是用掩码。这里本质的来说还是shell设置了掩码,掩码是使用就近原则的。

3.底层写入方式:write()

需要的头文件:

cpp 复制代码
#include <unistd.h>

函数参数如下:

cpp 复制代码
ssize_t write(int fd, const void *buf, size_t count);

参数详解:

参数 含义
fd 文件描述符 。由 open 返回的那个整数。
buf 缓冲区首地址。指向你要写入的数据存放的地方(通常是字符串指针或数组)。
count 字节数。期望写入的字节长度。

这个还是比较简单的,我们直接来应用一手:

cpp 复制代码
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <fcntl.h>
#include <stdlib.h>

int main()
{
    int fd = open("log.txt",O_WRONLY | O_CREAT | O_APPEND);
    if(fd == -1){
        perror("open fail");
        exit(1);
    }
    
    char* msg = "hello world\n";
    int cnt = 5;
    while(cnt--){
        ssize_t ret  = write(fd,msg,strlen(msg));//注意这里是ssize_t。还有我需要打印给我看,不需要/0
        if(ret == -1){
            perror("write fail");
            exit(1);
        }
    }

    close(fd);

    return 0;
}

这里我们就打开创建了log.txt这个文件,完成向里面写入:

可以看到里面的确写入完成了。

4.底层读取方式read()

函数原型:

cpp 复制代码
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);

参数详解:

  • fd 还是文件描述符,从指定的文件中读取。
  • buf 缓冲区,从fd读取到这里来,
  • count 请求读取的字节大小,通常是buf的字节大小

read 的返回值 (ssize_t) 有三种情况,这是写代码时的核心逻辑:

  • 大于 0 (> 0):成功读取的字节数。
    注意:这个数字可能小于你请求的 count(例如文件快读完了,或者你请求读100个字节但文件只剩5个字节)。
  • 等于 0 (= 0):End of File (EOF)。
    代表文件已经读完了,后面没有数据了。这是结束读取循环的标志。
  • 等于 -1 (= -1):出错。
    比如文件被以外关闭、硬件错误等。需要检查 errn

这个也是比较简单的,这里我们也来尝试简单使用一下:

cpp 复制代码
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>


int main()
{
    int fd = open("log.txt",O_RDONLY);
    if(fd == -1){
        perror("open error");
        exit(1);
    }

    char buff[1024];
    ssize_t ret = read(fd,buff,sizeof(buff) -1);//留一个给/0
    if(ret < 0){
        perror("read error");
        exit(2);
    }
    if(ret == 0){
        printf("over");
    }
    if(ret > 0){
        buff[ret] = 0;
        printf("%s",buff);
        printf("%d",fd);//顺便来看看fd是什么?
    }
    close(fd);
    return 0;
}

我们可以看到打印成功了,同时也显示了fd 的值为3.那么这个fd是什么呢?

5.三个基本打开的文件描述符:

代码最后问了 fd 是什么。 在 Linux 中,进程启动时默认打开三个文件描述符:

0: 标准输入 (stdin)

1: 标准输出 (stdout)

2: 标准错误 (stderr)

这三个是基本打开的,我们的程序在跑起来的时候,基本就是打开的。

FD 名称 默认设备 你的理解 对应的库函数
0 stdin 键盘 这里进数据 scanf, getchar, cin
1 stdout 显示器 这里出正常结果 printf, cout
2 stderr 显示器 这里出报错信息 perror, cerr

有了这个基础,我们后面就可以后面就可以讲重定向了。重定向是怎么实现的了。

总结:

write/read/close 是 Linux I/O 体系的"骨架"。它们既是用户程序与硬件交互的唯一通道,也是操作系统抽象硬件差异、统一资源管理接口的具体实现载体。虽然直接使用它们可能不如库函数高效(缺乏缓冲),但它们提供了对 I/O 行为最精确的控制能力。

已经完成了,感谢各位。希望有所收获。

感谢各位对本篇文章的支持。谢谢各位点个三连吧!



相关推荐
拾光Ծ2 小时前
【优选算法】双指针算法:专题二
c++·算法·双指针·双指针算法·c++算法·笔试面试
Cisco_hw_zte2 小时前
挂载大容量磁盘【Linux系统】
linux·运维·服务器
DolphinScheduler社区2 小时前
Linux 环境下,Apache DolphinScheduler 如何驱动 Flink 消费 Kafka 数据?
linux·flink·kafka·开源·apache·海豚调度·大数据工作流调度
j_xxx404_2 小时前
C++算法入门:滑动窗口合集(长度最小的子数组|无重复字符的最长字串|)
开发语言·c++·算法
艾莉丝努力练剑2 小时前
【AI时代的赋能与重构】当AI成为创作环境的一部分:机遇、挑战与应对路径
linux·c++·人工智能·python·ai·脉脉·ama
杜子不疼.2 小时前
【Linux】Ext系列文件系统(一):文件系统的初识
linux·运维·服务器
m0_561359672 小时前
C++中的过滤器模式
开发语言·c++·算法
码农不惑2 小时前
systemd升级造成的centos-bootc系统的内核故障
linux·centos·bootc
2301_790300962 小时前
嵌入式GPU编程
开发语言·c++·算法