【Linux】基础IO(2)

1. 系统文件I/O

打开⽂件的⽅式不仅仅是fopen,ifstream等流式,语⾔层的⽅案,其实系统才是打开⽂件最底层的方案。不过,在学习系统⽂件IO之前,先要了解下如何给函数传递标志位,该⽅法在系统⽂件IO接口中会使⽤到:

1.1 一种传递标志位的方法

cpp 复制代码
  1 #include<stdio.h>
  2 
  3 #define ONE_FLAG (1<<0)
  4 #define TWO_FLAG (1<<1)
  5 #define THREE_FLAG (1<<2)
  6 #define FOUR_FLAG (1<<3)
  7 
  8 void Print(int flags)
  9 {
 10   if(flags & ONE_FLAG)
 11   {
 12     printf("one\n");
 13   }
 14   if(flags & TWO_FLAG)
 15   {
 16     printf("two\n");
 17   }
 18   if(flags & THREE_FLAG)
 19   {
 20     printf("THREE\n");
 21   }
 22   if(flags & FOUR_FLAG)                                                                                                           
 23   {
 24     printf("FOUR\n");
 25   }
 26 }
 27                                                                                                                                   
 28 int main()
 29 {
 30   Print(ONE_FLAG);
 31   printf("\n");
 32   Print(TWO_FLAG);
 33   printf("\n");
 34   Print(THREE_FLAG);
 35   printf("\n");
 36   Print(FOUR_FLAG);
 37   printf("\n");
 38   Print(ONE_FLAG | TWO_FLAG);
 39   printf("\n");
 40   Print(ONE_FLAG | TWO_FLAG | THREE_FLAG);
 41   printf("\n");
 42   return 0;
 43 }

1.2 hello.c 写文件:

1.2.1 open

  • pathname 参数 :是一个指向以空字符结尾的字符串的指针,用于指定要打开或创建的文件路径。可以是绝对路径(例如 /home/user/test.txt ),也可以是相对路径(相对于当前工作目录,例如 test.txt )。
  • flags 参数 :是一个整数值,用于指定打开文件的方式和选项。
    • 必选的访问模式标志
      • O_RDONLY :以只读方式打开文件,例如 open("file.txt", O_RDONLY);
      • O_WRONLY :以只写方式打开文件,例如 open("file.txt", O_WRONLY);
      • O_RDWR :以读写方式打开文件,例如 open("file.txt", O_RDWR);
    • 常用的可选标志
      • O_CREAT :如果文件不存在,则创建该文件。当使用此标志时,需要提供第三个参数 mode 来指定新文件的权限,例如 open("newfile.txt", O_CREAT | O_WRONLY, 0644);
      • O_EXCL :与 O_CREAT 一起使用,如果要创建的文件已经存在,则 open() 函数调用失败并返回 -1,避免意外覆盖已有文件,如 open("newfile.txt", O_CREAT | O_EXCL | O_WRONLY, 0644);
      • O_TRUNC :如果文件已经存在且以可写方式(O_WRONLYO_RDWR)打开,会将文件的长度截断为 0,即清空文件原有内容,例如 open("file.txt", O_WRONLY | O_TRUNC);
      • O_APPEND :以追加模式打开文件,后续写入操作会将数据追加到文件末尾,防止覆盖原有数据,例如 open("log.txt", O_WRONLY | O_APPEND);
      • O_NONBLOCK :对于管道、套接字等特殊文件,以非阻塞方式打开。意味着在进行读写操作时,如果操作不能立即完成,函数不会阻塞进程,而是立即返回,例如 open("/dev/ttyS0", O_RDONLY | O_NONBLOCK);
  • mode 参数 :当 flags 中包含 O_CREAT 标志时,需要提供该参数,用于指定新创建文件的权限。权限值是一个 mode_t 类型的值,通常用八进制表示。例如 0666 表示文件所有者、同组用户和其他用户都具有读写权限;0755 表示文件所有者具有读、写、执行权限,同组用户和其他用户具有读、执行权限 。

返回值:

open() 函数调用成功时,会返回一个非负整数,这个整数被称为文件描述符(file descriptor),它是一个索引值,用于标识进程中打开的文件。后续对该文件的读写等操作(如 read()write()lseek() 等函数)都要通过这个文件描述符来进行。

1.2.2 close

1.2.3 write

  • fd 参数 :是一个整数,表示要写入数据的文件描述符。文件描述符通常是由 open() 函数打开文件或设备后返回的非负整数 。例如,int fd = open("test.txt", O_WRONLY | O_CREAT, 0644); 打开文件后,fd 就可以作为 write() 函数的第一个参数。
  • buf 参数 :是一个指向要写入数据缓冲区的指针,类型为 const void * 。这意味着它可以指向任何类型的数据,如字符数组(用于写入文本数据)、结构体等 。例如,const char data[] = "Hello, world!"; ,然后 write(fd, data, sizeof(data)); 就可以将 data 数组中的内容写入到 fd 对应的文件中。
  • buf 参数 :是一个 size_t 类型的值,表示要从 buf 指向的缓冲区中写入的字节数 。size_t 是无符号整数类型,通常用于表示内存大小、数组长度等。

返回值:

write() 函数返回值类型为 ssize_tssize_t 是有符号整数类型

cpp 复制代码
  1 #include<stdio.h>                                                                                                            
  2 #include<sys/types.h>
  3 #include<sys/stat.h>
  4 #include<fcntl.h>
  5 #include<unistd.h>
  6 #include<string.h>
  7 
  8 int main()
  9 {
 10   umask(0);
 11   int fd = open("log.txt", O_CREAT | O_WRONLY, 0666);
 12   if(fd < 0)
 13   {
 14     perror("open");
 15     return 1;
 16   }
 17 
 18   const char* msg = "hello linux!\n";
 19   int cnt = 5;
 20   while(cnt)
 21   {
 22     write(fd, msg, strlen(msg));
 23     cnt--;
 24   }
 25 
 26   close(fd);
 27   return 0;                                                                                                                       
 28 }

1.3 hello.c 读文件

  • fd:要读取数据的文件描述符,通常由 open 等函数获取。
  • buf:指向用于存储读取数据的缓冲区的指针。
  • count:要读取的字节数。

返回值:

  • 成功时,返回实际读取的字节数(可能小于 count,比如文件已读到末尾等情况)。
  • 失败时,返回 -1,并设置 errno 以指示错误原因。
cpp 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

#include <unistd.h>
#include <string.h>
int main()
{
    int fd = open("log.txt", O_RDONLY);
    if (fd < 0) {
        perror("open");
        return 1;
    }
    const char* msg = "hello linux!\n";
    char buf[1024];
    while (1) {
        ssize_t s = read(fd, buf, strlen(msg));//类⽐write
        if (s > 0) {
            printf("%s", buf);
        }
        else {
            break;
        }
    }
    close(fd);
    return 0;
}

1.4 文件描述符fd

文件描述符从 3 开始分配,是因为 012 这三个描述符被系统默认占用,用于进程与外界的基础交互:

  • 0:对应标准输入(STDIN_FILENO),通常关联键盘输入。
  • 1:对应标准输出(STDOUT_FILENO),通常关联终端屏幕输出。
  • 2:对应标准错误(STDERR_FILENO),通常用于输出错误信息到终端。

1. 进程与文件的关联桥梁

每个进程都有一个 task_struct 结构体(进程控制块,用于管理进程的各种信息),其中有一个 *files 指针,它指向 files_struct 结构体。files_struct 是进程管理打开文件的核心结构,里面最重要的部分是一个指针数组 fd_array(图中 file* fd_array[])。

2. 文件描述符的本质

文件描述符本质上就是 fd_array 这个指针数组的下标。比如,当我们说文件描述符为 3 时,它对应的就是 fd_array[3] 这个数组元素。

3. 与打开文件的关联

当进程通过 open 系统调用打开一个文件(或创建新文件)时,操作系统会在内核中创建一个 file 结构体。这个 file 结构体保存了文件相关的重要信息,比如文件的 inode 元信息(用于标识文件在文件系统中的属性、位置等)。然后,操作系统会在进程的 fd_array 中找到一个空闲的下标(即文件描述符),将该下标对应的数组元素(指针)指向这个 file 结构体。这样,进程后续就可以通过这个文件描述符来操作对应的文件了。

4. 标准文件描述符

系统默认会为每个进程预先打开三个标准文件:

  • 标准输入:对应 fd_array[0],文件描述符为 0
  • 标准输出:对应 fd_array[1],文件描述符为 1
  • 标准错误:对应 fd_array[2],文件描述符为 2。当进程再打开新的文件时,会从 3 开始分配文件描述符(因为 012 已被标准文件占用)。

5. 文件描述符的分配规则

文件描述符的分配遵循 "最小未使用整数" 原则,具体表现为:

  • 进程启动时,0(标准输入)、1(标准输出)、2(标准错误)被默认占用,新打开的文件从 3 开始分配。
  • 不同进程的文件描述符相互独立,同一数字在不同进程中可能指向不同资源。
  • 关闭后的文件描述符会被标记为可用,后续新打开文件时可能被复用。

重定向对分配的影响 :重定向(如 >dup2 系统调用)会改变文件描述符与资源的关联关系,进而影响分配逻辑。例如:

  • 使用 ls > output.txt 时,shell 会先关闭标准输出(fd=1),再打开 output.txt 并将其分配到 fd=1,此时写入 fd=1 的数据会被定向到文件而非终端。
  • 通过 dup2(oldfd, newfd) 可强制让 newfd 指向 oldfd 对应的资源:若 newfd 已被占用,会先关闭它再完成绑定,此时 newfd 的数值不变,但指向的资源被替换。
cpp 复制代码
 52 int main()
 53 {
 54   umask(0);
 55   int fd = open("log.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);
 56   dup2(fd, 1);
 57   close(fd);
 58 
 59   printf("fd:%d\n", fd);
 60   printf("hello linux!\n");
 61   printf("hello linux!\n");
 62   printf("hello linux!\n");
 63   fprintf(stdout, "hello stdout\n");
 64   fprintf(stdout, "hello stdout\n");
 65   fprintf(stdout, "hello stdout\n");
 66   fprintf(stdout, "hello stdout\n");                                                                                              
 67   return 0;
 68 }

这种机制使得进程无需修改代码,只需通过调整文件描述符的指向,即可灵活改变 I/O 流向。

2. 理解 "一切皆文件"

在 Linux 系统中,"一切皆文件" 是核心设计哲学之一,它并非指所有资源都是传统意义上的 "磁盘文件",而是指 Linux 用统一的 "文件接口" 来管理所有硬件设备、软件资源和 I/O 对象 ,让进程可以通过相同的一套系统调用(如 open/read/write/close)操作不同类型的资源,极大简化了系统设计和用户使用。

一、"一切皆文件" 的具体体现:哪些资源被视为 "文件"?

Linux 中几乎所有可操作的资源,都被抽象成 "文件" 的形式,常见类型包括:

  1. 普通文件 :传统的文本文件、二进制文件(如 test.txta.out),存储在磁盘上。
  2. 目录文件 :用于管理文件的 "文件夹",本质是记录子文件 / 子目录信息的特殊文件(如 /home/etc)。
  3. 设备文件
    • 块设备文件:按 "块" 读写的硬件(如硬盘、U 盘),对应 /dev/sda/dev/sdb1
    • 字符设备文件:按 "字符流" 读写的硬件(如键盘、鼠标、串口),对应 /dev/keyboard/dev/tty1
  4. 管道文件(FIFO) :用于进程间通信的临时 "文件",数据在内存中传输,不落地磁盘(如 mkfifo pipe1 创建的管道)。
  5. 套接字文件(Socket) :用于网络通信或本地进程间通信的 "文件"(如 /var/run/docker.sock 是 Docker 的本地套接字)。
  6. 符号链接文件 :类似 "快捷方式",指向其他文件的特殊文件(如 ln -s test.txt link.txt 创建的链接)。

二、核心本质:统一的接口 + 抽象的 "文件结构"

Linux 之所以能实现 "一切皆文件",关键在于两点:

  1. 统一的系统调用接口无论操作的是普通文件、键盘还是网络,进程都用相同的一套函数:
  • open() 打开资源(获取文件描述符);
  • read()/write() 读写数据;
  • close() 释放资源。

例如:

  • 读普通文件:fd = open("test.txt", O_RDONLY); read(fd, buf, 100);
  • 读键盘输入(标准输入,对应 /dev/stdin):read(0, buf, 100);(0 是标准输入的文件描述符)
  • 写网络数据(通过套接字):fd = socket(AF_INET, SOCK_STREAM, 0); write(fd, data, len);
  1. 内核的 "文件抽象层" 内核内部通过 struct file 结构体(文件对象)统一描述所有资源:
  • 不管是磁盘文件还是键盘,内核都会为其创建一个 struct file,记录资源的读写方式、当前位置、权限等信息;
  • 进程通过 "文件描述符"(fd)关联到 struct file,无需关心资源的实际类型,只需通过 fd 调用接口即可。

三、为什么要设计 "一切皆文件"?

  1. 简化开发:开发者无需针对不同资源(如文件、键盘、网络)学习不同的操作接口,一套逻辑即可通用。
  2. 灵活扩展 :新增硬件或资源时,只需按 "文件接口" 实现驱动,无需修改内核核心逻辑(如新增打印机,只需创建对应的 /dev/lp0 设备文件)。
  3. 便于组合 :可通过 "重定向""管道" 等机制,将不同类型的 "文件" 串联使用。例如:
    • echo "hello" > /dev/printer:将字符串写入打印机设备文件,实现打印;
    • ls | grep txt:将 ls 的输出(标准输出,文件描述符 1)通过管道(特殊文件)传给 grep 的输入(标准输入,文件描述符 0)。

四、一句话总结

"一切皆文件" 不是 "所有东西都是磁盘文件",而是 Linux 用 "文件" 的抽象,给所有资源套上了统一的 "操作外壳",让进程能以相同的方式与磁盘、硬件、网络等交互,是 Linux 简洁、灵活、可扩展的核心原因之一。

相关推荐
你什么冠军?2 小时前
linux入门4.5(NFS服务器和iSCSI服务器)
linux·运维·服务器
什么半岛铁盒2 小时前
C++项目:仿muduo库高并发服务器------EventLoop模块的设计
linux·服务器·c++·mysql·ubuntu
深鱼~3 小时前
VSCode+WSL+cpolar:打造跨平台的随身Linux开发舱
linux·ide·vscode
用户237390331473 小时前
“标准 I/O 用 fopen,底层控制用 open; 要 mmap 必 open,跨平台选 fopen。”
linux
深思慎考4 小时前
LinuxC++项目开发日志——基于正倒排索引的boost搜索引擎(5——通过cpp-httplib库建立网页模块)
linux·c++·搜索引擎
李小枫4 小时前
在linux上安装kafka,并使用kafka-clients实现消费者
linux·kafka·linq
煤球王子4 小时前
浅学内存分配与释放(二)
linux
dessler5 小时前
Hadoop HDFS-认证(Kerberos) 部署与配置
linux·运维·hdfs