【Linux我做主】从 fopen 到 open:Linux 文件 I/O 的本质与内核视角

【Linux我做主】从 fopen 到 open:Linux 文件 I/O 的本质与内核视角

从 fopen 到 open:Linux 文件 I/O 的本质与内核视角

从 fopen 到 open:Linux 文件 I/O 的本质与内核视角

github地址

有梦想的电信狗

前言

在 Linux 中,我们每天都在使用 fopenprintfwrite 这些看似“普通”的接口进行文件操作,但很少真正思考:

当我写下一行 fopen("log.txt","w"),内核里究竟发生了什么?

文件并不是简单的“磁盘上的一串字节”,而是被操作系统精心管理的一类核心资源。
磁盘文件 → 内存结构 → 进程关联 → 文件描述符 → 系统调用,背后是一整套进程管理与文件管理协同运作的机制。

本文将从最常见的 fopen 出发,逐层剖析其背后的 open / write 系统调用,再深入到内核中:

  • 进程是如何“持有”文件的
  • 文件描述符为什么是一个整数
  • FILE*fd 的真实关系
  • 标准输入输出是如何建立的

希望你在读完本文后,不再只会“用文件”,而是真正理解文件 I/O 在操作系统中的本质


一、文件的共识原理

  1. 文件 = 文件内容 + 文件属性
    文件不仅仅是字节序列,还包含名称、大小、权限、时间戳等属性信息,都保存在磁盘上
  2. 文件分为“打开的文件”和“未打开的文件”
    • 未打开的文件:静态地存放在磁盘中。
    • 打开的文件:被某个进程打开并访问,并在内核中建立对应的数据结构进行管理。
  3. 打开文件的是进程:文件 I/O 的本质就是研究进程与文件的关系
    • 文件被打开,本质是进程执行了诸如 fopen 这样的代码,因此文件是由进程打开的
    • 研究打开的文件,本质就是研究进程与文件的关系
    • 每一个文件的打开操作,实质上都是某个进程向内核申请建立“文件打开对象”,从而形成进程与文件之间的联系。因此:
      • 文件 I/O 并不是直接对磁盘读写,而是进程通过内核暴露的接口间接访问文件。
      • 所有打开文件的信息都由 OS 内核维护。
  4. 未打开的文件本质上是磁盘上的数据:问题核心是==“文件的存储与组织”==
    未打开的文件数量庞大,因此必须在磁盘上进行良好的组织:
    • 如何分类?(目录结构 / inode 索引)
    • 如何定位?(索引节点、块号)
    • 如何快速查找?(目录项 + 文件系统)
    • 存储的本质:让文件在磁盘上“放得下、放得好、找得快”。
  5. 文件被打开后必须先加载到内存:进程与打开文件必然是一对多的关系(1:N)
    当进程启动时,操作系统会默认为其打开三个文件流:随着程序运行,一个进程可能打开更多文件,因此:进程 : 打开文件 = 1 : N文件被加载到内存,文件的属性一定被加载到了内存,文件的内容是否加载,取决于代码有没有访问文件的内容
    • stdin
    • stdout
    • stderr
在这里插入图片描述

  1. 一个进程可以打开多个文件,操作系统内一定存在大量被打开的文件。内核必须管理大量“被打开的文件”:核心思想是“先描述,再组织”
    内核中,每一个被打开的文件,都必须用一个结构体记录自身状态,这就是 文件打开对象(如 Linux 的 struct file)。

​ 一个文件打开对象必须包含:

  • 文件属性(读写位置、状态、访问模式等)
  • 指向下一个对象的指针,用于组织管理

内核会将所有文件打开对象组织成链式结构,例如:

structfile_object{// 文件属性;structfile_object* next;};

最终操作系统通过双链表或其他数据结构组织所有已打开文件,对这些对象进行:

  • 增:打开文件
  • 删:关闭文件
  • 查:根据文件描述符查找
  • 改:调整读写位置、权限等

至此,“管理大量打开的文件”就转化成了对这些链表结点(或红黑树等更高效结构)的管理问题。


二、C语言文件操作接口的细节

1. fopen

参数介绍

在这里插入图片描述

参数一要打开的文件路径,可传入绝对路径或相对路径。相对路径的起点是当前可执行程序的路径

参数二打开文件的模式

返回值FILE*文件指针类型,也叫文件句柄


w模式使用演示

#include<stdio.h>intmain(){// 打开文件的路径和文件名,默认在当前路径下新建一个文件 FILE* fp =fopen("log.txt","w");// 以 w 方式打开文件时,该文件不存在时,会自动创建if(fp ==NULL){perror("fopen fail\n");return1;}fclose(fp);return0;}
在这里插入图片描述

w模式新建文件时的路径问题

由以上执行结果可知fopen w 模式时,如果当前文件不存在,在当前路径新建一个同名文件。

那么会有以下两个问题:

  • 当前路径是什么?为什么是在当前路径新建?
  • chdir 可以改变进程的工作路径,改变之后,是否可以在新路径新建文件?

以上问题我们逐个解答


当前路径是什么?为什么在当前路径新建?

**当前路径,是进程的当前工作路径 cwd **

每个运行起来的进程都有自己的当前工作路径cwd,不指定路径时,默认在当前进程的路径中新建文件


  • 使用如下代码测试
#include<stdio.h>#include<unistd.h>intmain(){printf("PID: %d\n",getpid());// 打开文件的路径和文件名,默认在当前路径下新建一个文件 FILE* fp =fopen("log.txt","w");// 以 w 方式打开文件时,该文件不存在时,会自动创建if(fp ==NULL){perror("fopen fail\n");return1;}fclose(fp);sleep(100);return0;}
  • /proc/ 目录下,会包含当前正在运行的进程 pid 为名的目录,使用ls -l查看进程的pid文件夹,里面会有进程的各项属性信息,其中就有cwd当前进程的工作目录
  • 正是因为有进程当前的工作目录cwd的存在,当我们使用fopen以w的方式打开不存在的文件时,当没有写文件的绝对路径时,操作系统默认将进程的当前工作目录cwd和要创建的文件名进行拼接,作为创建文件的路径
在这里插入图片描述

chdir 可以改变进程的工作路径后,在新路径创建文件
#include<stdio.h>#include<unistd.h>intmain(){chdir("/home/changan_memory");printf("PID: %d\n",getpid());// 打开文件的路径和文件名,默认在当前路径下新建一个文件 FILE* fp =fopen("log.txt","w");// 以 w 方式打开文件是,该文件不存在时,会自动创建if(fp ==NULL){perror("fopen fail\n");return1;}fclose(fp);sleep(100);return0;}
在这里插入图片描述
  • 需要注意的是:chdir 更改路径时,一定要有相应的权限,普通用户不能改到root用户的文件夹下

2. fopen 的 w 模式和 fwrite

w模式的特性

在这里插入图片描述


在这里插入图片描述
  • 第一次写入hello Linux message
在这里插入图片描述
  • 第二次写入abcde
在这里插入图片描述
为什么先写入第一次写入hello Linux message,第二次写入abcde后,文件内容没有变成abcde Linux message,而是变成了abcde

原因如下

w模式Truncate file to zero length or create text file for writing. The stream is positioned at the beginning of the file.

  • 将文件截断为零长度或创建文本文件以供写入
  • 流定位在文件的开头

因此:只要以 w 方式打开了文件,文件的内容就会被清空,且流定位到文件的开头


该特性在echo中的体现

在这里插入图片描述

观察以上现象

  • 每次执行 echo 重定向时,文件中的内容都会被清空
  • 仅使用符号 > 也会清空内容
    • > 重定向:以 w 方式打开文件
    • >> 重定向:以 a 方式打开文件

结论

  • echo 重定向向文件中写数据时, 一定是先以 w 模式打开文件,再写入内容,因此会将打开的文件内容给清空
只要以 w 方式打开了文件,文件的内容就会被清空,且流定位到文件的开头

因此:仅仅执行以下代码,文件的内容也会被清空

在这里插入图片描述
小注意事项
constchar* message ="abcde";// 写入时是否要将 '\0' 写入? strlen(message) 是否要+1 ?// 字符串以 '\0' 结尾, 是C语言的规定, 和操作系统管理文件无关. 写入时只需要将字符串的内容写入即可// 因此 strlen(message) 不需要+1fwrite(message,strlen(message),1, fp);

3. fopen 的其他模式

在这里插入图片描述

fopen的其他模式翻译解释

  • r:打开文本文件以进行读取。流的位置位于文件的开头。
  • r+:以读写方式打开。流的位置在文件的开头。
  • w:将文件截断为零长度或创建文本文件以供写入。流定位在文件的开头。
  • w+:可供读取和写入操作。如果文件不存在,则会创建该文件;否则会将其截断。流会定位在文件的开头位置。
  • a:支持追加操作(在文件末尾进行写入)。若文件不存在,则会创建该文件。流会定位在文件的末尾。
  • a+:支持读取和追加(在文件末尾进行写入)。若文件不存在,则会自动创建。输出内容总是附加到文件末尾。对于 POSIX 标准,在使用此模式时并未明确说明初始读取位置是什么。对于 glibc 来说,读取时的初始文件位置在文件开头,但对于 Android、BSD 和 MacOS 来说,读取时的初始文件位置在文件末尾。

4. 输出信息到显示器的几种方法

C程序在启动时,默认会帮助我们打开三个输入输出流

  • stdin标准输入,Linux中一般对应键盘文件
  • stdout标准输出(默认是显示器),Linux中一般对应显示器文件

stderr标准错误,Linux中一般对应显示器文件

在这里插入图片描述

如果我们想向显示器输出,或者从标准输入中读取,直接向这些流文件写入即可

constchar* message ="hello Linux";// 方法一: 直接向显示器文件中写入fwrite(message,strlen(message),1,stdout);fprintf(stdout,"%s: %d\n", message,1234);fprintf(stderr,"%s: %d\n", message,1234);// 向 stderr 中写入,也能输出到显示器中// 方法二: 调用 printf 等IO函数 printf("%s: %d\n", message,1234);

总结

  • 在C语言看来,输出内容到显示器和向显示器文件中写入没有区别

三、系统调用级别的文件操作

1. 访问文件的硬件本质

先给出如下结论

  • 文件是存储在磁盘上的,磁盘是外部设备,访问磁盘文件其实是访问硬件!
  • 因此:访问任何磁盘文件的硬件本质,都是在访问磁盘

在这里插入图片描述

由计算机系统的结构层次和操作系统相关知识可知几乎所有的库,只要是访问硬件设备,必定要封装系统调用

  • printf/fprintf/fscanf/fwrite/fread/fgets/gets/fopen这些操作文件的库函数,一定封装了系统调用接口

2. 系统调用级别的文件操作接口

open

参数介绍
// 用于打开已存在的文件,不会创建文件,不能指定权限intopen(constchar*pathname,int flags);// 可以通过传参控制是否创建文件以及创建文件的权限intopen(constchar*pathname,int flags, mode_t mode);

int open(const char *pathname, int flags);通常用于打开已经存在的文件

int open(const char *pathname, int flags, mode_t mode);通常用于一个文件不存在,需要先创建再打开时使用,且可以指定文件创建时的权限

参数说明

  • pathname文件路径
  • flags打开文件的模式介绍flags常用的传值
    • O_RDONLY:只读
    • O_WRONLY:只写
    • O_RDWR:读和写
    • O_CREAT:打开文件不存在时创建
    • O_APPEND:追加
    • O_TRUNC:打开时清空文件内容
  • mode以什么权限创建文件,仅当 flags 包含O_CREAT时有效;
    • 权限说明:
      mode参数指定的是文件的 “默认权限”,最终权限会被umask(权限掩码,往期文章中提到过)修正,公式为:最终权限 = mode & ~umask。举例:默认umask0022,因此mode=0666时,最终权限为0644

在这里插入图片描述

返回值:成功返回一个整数,称为文件描述符。失败时返回**-1**


理解比特位级别的标志位传参方式
  • open函数flags的传参也是采用类似如下的方式
#defineONE(1<<0)// 1#defineTWO(1<<1)// 2#defineFOUR(1<<2)// 4#defineEIGHT(1<<3)// 8voidshow(int flags){if(flags & ONE)printf("hello function1: %d\n",(flags & ONE));if(flags & TWO)printf("hello function2: %d\n",(flags & TWO));if(flags & FOUR)printf("hello function4: %d\n",(flags & FOUR));if(flags & EIGHT)printf("hello function8 : %d\n",(flags & EIGHT));}intmain(){printf("-----------------------------\n");show(ONE);printf("-----------------------------\n");show(TWO);printf("-----------------------------\n");show(ONE | TWO);printf("-----------------------------\n");show(ONE | TWO | FOUR);printf("-----------------------------\n");show(ONE | FOUR);printf("-----------------------------\n");show(FOUR | EIGHT);printf("-----------------------------\n");}

使用注意事项
int fd =open("log.txt", O_WRONLY);// 打开文件失败,因为该接口不会自动创建文件int fd =open("log.txt", O_WRONLY | O_CREAT);// 创建文件成功,但文件的权限不合适int fd =open("log.txt", O_WRONLY | O_CREAT,0666);// 指定文件权限为666, 创建出来却是664// 正确使用方法// 先将umask 置零,创建出的文件权限即为所指定的数字umask(0);int fd =open("log.txt", O_WRONLY | O_CREAT,0666);// 直接指定文件权限为666

umask(0)将当前进程的umask码设置为0,仅在当前进程中生效,不影响系统中的umask,方便指定创建文件时的权限


系统调用open的使用
intmain(){umask(0);int fd =open("log.txt", O_WRONLY | O_CREAT,0666);if(fd <0){perror("open file fail");}close(fd);return0;}

write

参数解析
ssize_t write(int fd,constvoid*buf, size_t count);
  • fd: 对应文件描述符
  • buf: 缓冲区
  • count: 写入次数
  • ssize_t返回写入的字节数

write函数的使用
intmain(){umask(0);int fd =open("log.txt", O_WRONLY | O_CREAT,0666);if(fd <0){perror("open file fail");}constchar* message ="hello Linux";write(fd, message,strlen(message));close(fd);return0;}
  • 系统调用write函数,并不像C语言的fwrite函数那样,每次以写方式打开文件时会清空文件,而是会对文件中的内容进行覆盖写入
  • 如果想要实现每次打开打开文件写入时清空文件内容,需要在open时改变文件的打开方式
int fd =open("log.txt", O_WRONLY | O_CREAT | O_TRUNC,0666);// 以上三个参数结合起来,可以实现,每次打开文件时,不存在时创建文件,先清空文件中的内容,再进行写入int fd =open("log.txt", O_WRONLY | O_CREAT | O_APPEND,0666);// 以上三个参数结合起来,可以实现,打开文件,不存在时创建文件,在文件中追加写入

由此可见O_TRUNC 参数和 O_APPEND 是矛盾的


3. 结论:库函数一定封装了系统调用

FILE* fp =fopen("log.txt","w");int fd =open("log.txt", O_WRONLY | O_CREAT | O_TRUNC,0666);// C语言 fopen 的w模式 对open的封装  FILE* fp =fopen("log.txt","a");int fd =open("log.txt", O_WRONLY | O_CREAT | O_APPEND,0666);// C语言 fopen 的a模式 对open的封装 

以上分别是C语言库函数和系统调用的使用方法:

  • C语言fopenw模式 对open的封装
  • C语言fopena模式 对open的封装
  • C语言 的结构体 FILE 对文件描述符fd的封装

其他文件操作库函数也一定进行了类似的封装

最终结论不论是什么编程语言,文件操作的接口可能不同,但只要在操作系统上运行,一定都封装了文件操作的系统调用

其他和系统相关的接口也是如此

四、访问文件的软件本质

FILE* fp =fopen("log.txt","w");int fd =open("log.txt", O_WRONLY | O_CREAT | O_TRUNC,0666);// C语言 fopen 的w模式 对open的封装  FILE* fp =fopen("log.txt","a");int fd =open("log.txt", O_WRONLY | O_CREAT | O_APPEND,0666);// C语言 fopen 的a模式 对open的封装 
  • 观察以上调用,分别是C语言库函数的调用和系统调用
  • 既然库函数封装了系统调用,那么返回值 FILE*文件描述符fd 又有什么关系呢

1. 文件描述符的本质是数组下标

在这里插入图片描述

每个文件被打开,都会在操作系统内核中,创建一个内核数据结构,strcut file

strcut file 是操作系统内,描述一个被打开的文件的信息的内核数据结构。

进程的PCB一定建立了和该进程打开的文件的关系,一个进程会打开n个文件

  • 调用write函数时,必须传入数组下标fdwrite函数会将fd传递给进程,进程根据file_struct*指针找到文件描述符表,再通过数组下标,索引到对应的打开文件,进而对文件进行操作

进程管理和文件管理关联,是通过数组下标关联的

在这里插入图片描述

2. 验证文件描述符是数组下标

intmain(){umask(0);int fd1 =open("log1.txt", O_WRONLY | O_CREAT | O_TRUNC,0666);int fd2 =open("log2.txt", O_WRONLY | O_CREAT | O_TRUNC,0666);int fd3 =open("log3.txt", O_WRONLY | O_CREAT | O_TRUNC,0666);int fd4 =open("log4.txt", O_WRONLY | O_CREAT | O_TRUNC,0666);if(fd1 <0|| fd2 <0|| fd3 <0|| fd4 <0){perror("open file fail");}printf("fd1: %d\n", fd1);printf("fd2: %d\n", fd2);printf("fd3: %d\n", fd3);printf("fd4: %d\n", fd4);close(fd1);close(fd2);close(fd3);close(fd4);return0;}
在这里插入图片描述

3. 下标 0 1 2在哪里呢

既然返回值是数组下标,刚开始从3开始,失败时返回-1,那么下标0 1 2在哪里呢?

我们观察到0 1 2刚好是三个,自然联想到以下内容:

C程序在启动时,默认会帮助我们打开三个输入输出流

  • stdin标准输入Linux中一般对应键盘文件
  • stdout标准输出(默认是显示器),Linux中一般对应显示器文件
  • stderr标准错误Linux中一般对应显示器文件

在C语言层面他们的类型是FILE*,但在操作系统层面,操作系统只认识文件描述符fd

先给出结论再进行验证

  • Linux进程默认情况下会有3个缺省打开的文件描述符,分别是标准输入0, 标准输出1, 标准错误2。
  • 0,1,2对应的物理设备一般是:键盘,显示器,显示器

接下来第四点中验证一下 0 1 2 是什么文件


4. FILE* 和 文件描述符

FILE* fp =fopen("log.txt","w");int fd =open("log.txt", O_WRONLY | O_CREAT | O_TRUNC,0666);
FILE文件描述符fd有什么关系呢?

结论如下

  • FILE是C语言库自己封装的结构体,由于操作系统访问文件时,只认识文件描述符,这是操作系统决定的。因此,FILE结构体里,必定封装了文件描述符fd

证明FILE结构体中必定封装了文件描述符

intmain(){printf("stdin->fd: %d\n",stdin->_fileno);printf("stdout->fd: %d\n",stdout->_fileno);printf("stderr->fd: %d\n",stderr->_fileno);return0;}
在这里插入图片描述

结论可以看到,经验证,默认打开的三个输入输出流结构体,他们的文件描述符正好是0 1 2


5. 总结升华

任何语言,想在操作系统中访问文件,语言提供的接口必定要封装fd

  • 键盘文件fd = 0
  • 显示器文件fd = 1
  • 显示器文件 fd = 2

我们之前说:C语言程序启动时,默认会打开 0 1 2号文件

现在需要对其纠正:程序启动时默认打开 stdinstdoutstderr,不是C语言的特性,而是操作系统的特性。任何语言的程序启动时,都会默认打开键盘和显示器。只不过C语言中将他们封装成了 stdinstdoutstderr这三个结构体


  • 库函数封装了系统调用
  • FILE 结构体封装了 文件描述符fd

结语

fopenopen,从 FILE*fd,从用户态到内核态,我们看到的并不是两个孤立的接口,而是:

进程、内核、文件系统共同构成的一套统一资源管理模型。

文件 I/O 的核心并不是“读写磁盘”,而是:
内核如何用数据结构描述文件、用表结构管理关系、用系统调用提供访问能力。

当你理解了这些,就会发现:

  • 文件 ≠ 字节
  • 打开 ≠ 访问
  • fd ≠ 魔法数字

而是一套可被追踪、可被验证、可被推演的系统设计


以上就是本文的所有内容了,如果觉得文章对你有帮助,欢迎 点赞⭐收藏 支持!如有疑问或建议,请在评论区留言交流,我们一起进步

分享到此结束啦
一键三连,好运连连!
你的每一次互动,都是对作者最大的鼓励!征程尚未结束,让我们在广阔的世界里继续前行! 🚀

Read more

DeepSeek各版本说明与优缺点分析_deepseek各版本区别

DeepSeek各版本说明与优缺点分析 DeepSeek是最近人工智能领域备受瞩目的一个语言模型系列,其在不同版本的发布过程中,逐步加强了对多种任务的处理能力。本文将详细介绍DeepSeek的各版本,从版本的发布时间、特点、优势以及不足之处,为广大AI技术爱好者和开发者提供一份参考指南。 1. DeepSeek-V1:起步与编码强劲 DeepSeek-V1是DeepSeek的起步版本,这里不过多赘述,主要分析它的优缺点。 发布时间: 2024年1月 特点: DeepSeek-V1是DeepSeek系列的首个版本,预训练于2TB的标记数据,主打自然语言处理和编码任务。它支持多种编程语言,具有强大的编码能力,适合程序开发人员和技术研究人员使用。 优势: * 强大编码能力:支持多种编程语言,能够理解和生成代码,适合开发者进行自动化代码生成与调试。 * 高上下文窗口:支持高达128K标记的上下文窗口,能够处理较为复杂的文本理解和生成任务。 缺点: * 多模态能力有限:该版本主要集中在文本处理上,缺少对图像、语音等多模态任务的支持。 * 推理能力较弱:尽管在自然语言

By Ne0inhk

用DeepSeek和Cursor从零打造智能代码审查工具:我的AI编程实践

💂 个人网站:【 摸鱼游戏】【神级代码资源网站】【星海网址导航】摸鱼、技术交流群👉 点此查看详情 引言:AI编程革命下的机遇与挑战 GitHub统计显示,使用AI编程工具的开发者平均效率提升55%,但仅有23%的开发者能充分发挥这些工具的潜力。作为一名全栈工程师,我曾对AI编程持怀疑态度,直到一次紧急项目让我彻底改变了看法。客户要求在72小时内交付一个能自动检测代码漏洞、优化性能的智能审查系统,传统开发方式根本不可能完成。正是这次挑战,让我探索出DeepSeek和Cursor这对"黄金组合"的惊人潜力。 一、工具选型:深入比较主流AI编程工具 1.1 为什么最终选择DeepSeek+Cursor? 经过两周的对比测试,我们发现不同工具在代码审查场景的表现差异显著: 工具代码理解深度响应速度定制灵活性多语言支持GitHub Copilot★★★☆★★★★★★☆★★★★Amazon CodeWhisperer★★☆★★★☆★★★★★★☆DeepSeek★★★★☆★★★★★★★☆★★★★☆Cursor★★★☆★★★★☆★★★★★★★★ 关键发现: * Dee

By Ne0inhk
【DeepSeek应用】100个 DeepSeek 官方推荐的工具箱

【DeepSeek应用】100个 DeepSeek 官方推荐的工具箱

【DeepSeek应用】Deepseek R1 本地部署(Ollama+Docker+OpenWebUI) 【DeepSeek应用】DeepSeek 搭建个人知识库(Ollama+CherryStudio) 【DeepSeek应用】100个 DeepSeek 官方推荐的工具箱 【DeepSeek应用】Zotero+Deepseek 阅读与分析文献 【DeepSeek应用】100个 DeepSeek 官方推荐的工具箱 * 1. DeepSeek 工具箱:应用程序 * 2. DeepSeek 工具箱:AI Agent 框架 * 3. DeepSeek 工具箱:RAG 框架 * 4. DeepSeek 工具箱:即时通讯软件 * 5. DeepSeek 工具箱:浏览器插件 * 6. DeepSeek 工具箱:

By Ne0inhk
假网站排全网第二,真官网翻五页都找不到!NanoClaw创始人破防:SEO之战,我快要输了

假网站排全网第二,真官网翻五页都找不到!NanoClaw创始人破防:SEO之战,我快要输了

整理 | 苏宓 出品 | ZEEKLOG(ID:ZEEKLOGnews) 自从 OpenClaw 爆火之后,各种“Claw”项目接连出现,其中以安全优化版 NanoClaw 最为知名。它的核心代码仅有 4000 行,却获得了 AI 大牛 Andrej Karpathy 的点赞。 可谁也没想到,这款口碑极佳的开源项目,近来竟被一个仿冒网站抢了风头。 投诉无门之下,NanoClaw 创始人 Gavriel Cohen 在 X 社交平台上无奈发文怒斥:谷歌搜索错误地将假网站排在真官网前面,不仅破坏了项目声誉,还埋下了严重的安全隐患,而他费尽心力,却只能哀叹一句——“我正在为自己的开源项目打 SEO 战,但我快要输了。” 那么,NanoClaw 究竟发生了什么?又是怎么走红的?事情还要从 OpenClaw

By Ne0inhk