【Linux指南】基础IO系列(四):文件描述符 fd——Linux 文件操作的 “万能钥匙”

上一篇我们掌握了 Linux 系统 IO 接口(open/read/write),但始终围绕一个神秘的 "小整数"------文件描述符(fd) 。比如open成功后返回3stdin对应0stdout对应1------ 这个 "小整数" 到底是什么?它如何关联进程和文件?为什么关闭1后新打开的文件 fd 会变成1?这篇文章会带你从内核数据结构出发,彻底揭开 fd 的面纱:从 "内核数组下标" 的本质,到默认 fd 的由来,再到 fd 与FILE结构体的关系,最后通过实战验证 fd 的分配规则,让你明白 "为什么 fd 是 Linux 文件操作的万能钥匙"。

文章目录

    • [一、先搞懂:fd 的本质 ------ 内核数组的 "下标"](#一、先搞懂:fd 的本质 —— 内核数组的 “下标”)
    • 二、内核视角:进程与文件的关联机制
      • [2.1 核心结构体 1:task_struct------ 进程的 "身份证"](#2.1 核心结构体 1:task_struct—— 进程的 “身份证”)
      • [2.2 核心结构体 2:files_struct------ 进程的 "打开文件表"](#2.2 核心结构体 2:files_struct—— 进程的 “打开文件表”)
      • [2.3 核心结构体 3:struct file------ 打开文件的 "详细档案"](#2.3 核心结构体 3:struct file—— 打开文件的 “详细档案”)
      • [2.4 三者关系图(直观理解)](#2.4 三者关系图(直观理解))
    • [三、默认 fd:为什么 0、1、2 被 "预留" 了?](#三、默认 fd:为什么 0、1、2 被 “预留” 了?)
      • [3.1 三个默认 fd 的对应关系](#3.1 三个默认 fd 的对应关系)
      • [3.2 验证默认 fd:查看进程的 fd 列表](#3.2 验证默认 fd:查看进程的 fd 列表)
      • [3.3 实战:关闭默认 fd 后的 "异常" 现象](#3.3 实战:关闭默认 fd 后的 “异常” 现象)
        • [案例 1:关闭 stdout(fd=1)后,新打开的文件 fd 变成 1](#案例 1:关闭 stdout(fd=1)后,新打开的文件 fd 变成 1)
        • [案例 2:关闭 stdin(fd=0)后,无法从键盘读入](#案例 2:关闭 stdin(fd=0)后,无法从键盘读入)
    • [四、fd 的分配规则:"最小未使用" 原则](#四、fd 的分配规则:“最小未使用” 原则)
      • [场景 1:默认情况下,新打开文件的 fd 是 3](#场景 1:默认情况下,新打开文件的 fd 是 3)
      • [场景 2:关闭 fd=2 后,新打开文件的 fd 是 2](#场景 2:关闭 fd=2 后,新打开文件的 fd 是 2)
      • [场景 3:关闭 fd=1 和 fd=3 后,新打开文件的 fd 是 1](#场景 3:关闭 fd=1 和 fd=3 后,新打开文件的 fd 是 1)
    • [五、fd 与 FILE 结构体的关系:封装与被封装](#五、fd 与 FILE 结构体的关系:封装与被封装)
      • [5.1 FILE 结构体的核心成员(简化版)](#5.1 FILE 结构体的核心成员(简化版))
      • [5.2 验证:FILE * 内部的_fd 就是 fd](#5.2 验证:FILE * 内部的_fd 就是 fd)
      • [5.3 C 库 IO 与系统 IO 的调用关系(再次梳理)](#5.3 C 库 IO 与系统 IO 的调用关系(再次梳理))
    • [六、fd 的常见问题与排查方法](#六、fd 的常见问题与排查方法)
      • [6.1 问题 1:fd 泄漏(最常见)](#6.1 问题 1:fd 泄漏(最常见))
      • [6.2 问题 2:使用已关闭的 fd](#6.2 问题 2:使用已关闭的 fd)
      • [6.3 问题 3:混淆 fd 的 "进程独立性"](#6.3 问题 3:混淆 fd 的 “进程独立性”)
    • 七、总结与下一篇预告

一、先搞懂:fd 的本质 ------ 内核数组的 "下标"

很多人把 fd 理解为 "文件的编号",但更准确的定义是:fd 是 "进程打开文件表(fd_array)的下标" ------ 这个表存储在进程的files_struct结构体中,每个元素都是指向 "打开文件描述(struct file)" 的指针。

我们可以用一个通俗的比喻理解:

  • 进程就像一家 "公司";
  • files_struct是公司的 "钥匙管理室";
  • fd_array是管理室里的 "钥匙串"(数组);
  • fd 是 "钥匙串上的位置编号"(如下标012);
  • 每个位置挂着的 "钥匙"(struct file*),对应一个 "打开的文件 / 设备"(如键盘、显示器、磁盘文件)。

当进程调用open打开文件时,内核会:

  1. 创建一个struct file结构体(记录文件属性、读写位置等);
  2. 在进程的fd_array中找一个 "最小未使用的下标"(比如3);
  3. struct file*指针存到fd_array[3]中;
  4. 返回这个下标(3)给进程 ------ 这就是 fd。

后续进程调用read(fd, buf, count)时,内核会:

  1. 根据 fd 找到fd_array[fd]
  2. 通过fd_array[fd]拿到struct file*
  3. 调用struct file中的read函数指针(比如磁盘文件的read逻辑),完成数据读取。

二、内核视角:进程与文件的关联机制

要理解 fd,必须先搞懂 "进程如何通过 fd 找到文件"------ 这涉及三个核心内核结构体:task_struct(进程控制块)、files_struct(进程打开文件表)、struct file(打开文件描述)。它们的关系就像 "链条",把进程和文件牢牢绑定。

2.1 核心结构体 1:task_struct------ 进程的 "身份证"

每个进程在 kernel 中都有一个task_struct结构体,记录进程的所有信息(PID、内存、打开的文件等)。其中有一个关键指针:

c 复制代码
struct task_struct {
    // ... 其他字段 ...
    struct files_struct *files; // 指向进程的"打开文件表"
    // ... 其他字段 ...
};

这个files指针,就是进程通往 "文件世界" 的入口 ------ 通过它能找到所有该进程打开的文件。

2.2 核心结构体 2:files_struct------ 进程的 "打开文件表"

files_struct是进程专属的 "文件管理中心",核心是一个指针数组fd_array,存储所有打开文件的struct file*

c 复制代码
struct files_struct {
    // ... 其他字段(如fd数量限制、锁等) ...
    struct file __rcu *fd_array[NR_OPEN_DEFAULT]; // 默认大小1024,可扩展
    // ... 其他字段 ...
};
  • NR_OPEN_DEFAULTfd_array的默认大小(通常是 1024),表示一个进程默认最多能打开 1024 个文件;
  • fd_array[i]:下标i就是 fd,值是指向struct file的指针(若为NULL,表示该 fd 未使用)。

2.3 核心结构体 3:struct file------ 打开文件的 "详细档案"

struct file是 "打开文件的详细档案",记录了文件的所有关键信息,无论这个 "文件" 是磁盘文件、键盘还是显示器:

c 复制代码
struct file {
    // 1. 文件属性相关
    struct inode *f_inode; // 指向文件的inode(存储权限、大小、创建时间等)
    const struct file_operations *f_op; // 指向文件的操作函数集(read/write/close等)
    
    // 2. 读写状态相关
    loff_t f_pos; // 当前读写位置(比如读了100字节,f_pos=100)
    unsigned int f_flags; // 打开文件时的flags(如O_RDONLY、O_APPEND)
    
    // 3. 引用计数(避免文件被重复关闭)
    atomic_long_t f_count; // 引用次数,close时减1,减到0才真正释放文件
};
  • f_inode:关联文件的 "元数据"(属性),比如stat命令查看的信息都来自inode
  • f_op:关联文件的 "操作逻辑"(比如磁盘文件的read是 "读磁盘扇区",键盘的read是 "读键盘缓冲区");
  • f_pos:记录当前读写位置,保证read/write的 "顺序性"(除非用lseek修改)。

2.4 三者关系图(直观理解)

我们用一张图总结 "进程→fd→文件" 的关联链条:

plaintext 复制代码
进程(task_struct)
    ↓ 包含指针
files_struct(打开文件表)
    ↓ 包含数组fd_array[1024]
fd_array[0] → struct file(对应键盘,stdin)
fd_array[1] → struct file(对应显示器,stdout)
fd_array[2] → struct file(对应显示器,stderr)
fd_array[3] → struct file(对应磁盘文件test.txt)
...
fd_array[1023] → struct file(对应其他打开的文件/设备)

简单说:fd 是 "fd_array 的下标",通过 fd 能找到 struct file,再通过 struct file 找到文件的属性和操作逻辑------ 这就是 fd 能 "打开一切文件" 的底层原因。

三、默认 fd:为什么 0、1、2 被 "预留" 了?

你可能注意到:自己用open打开的第一个文件,fd 总是3,而不是0------ 因为 Linux 进程启动时,会默认打开 3 个文件 ,对应的 fd 固定为012,分别对应 "标准输入(stdin)""标准输出(stdout)""标准错误输出(stderr)"。

3.1 三个默认 fd 的对应关系

fd 值 流名称 对应设备 作用 系统 IO 接口使用场景
0 stdin 键盘 接收用户输入 read(0, buf, count)(读键盘)
1 stdout 显示器 输出正常信息 write(1, buf, count)(写显示器)
2 stderr 显示器 输出错误信息 write(2, "error", 5)(写错误)

为什么要默认打开这三个?因为程序的核心是 "数据处理",而数据处理需要 "输入源" 和 "输出目标"------0是默认输入源(键盘),1/2是默认输出目标(显示器),这样程序启动就能直接交互,不用手动打开。

3.2 验证默认 fd:查看进程的 fd 列表

Linux 提供了一个 "虚拟文件系统"/proc,可以查看任意进程的 fd 信息。比如查看当前终端进程(bash)的 fd:

bash 复制代码
# 1. 查看当前bash的PID
echo $$ # 输出当前shell的PID,比如3447138

# 2. 查看该进程的fd列表(/proc/[PID]/fd目录下的文件就是fd)
ls -l /proc/3447138/fd

输出结果类似:

  • 0/1/2都是符号链接,指向/dev/pts/0(当前终端设备文件);
  • /dev/pts/0是 "伪终端设备",同时对应键盘(输入)和显示器(输出)------ 这就是为什么read(0)能读键盘,write(1)能写显示器。

3.3 实战:关闭默认 fd 后的 "异常" 现象

如果我们手动关闭0(stdin)或1(stdout),会发生什么?通过代码验证:

案例 1:关闭 stdout(fd=1)后,新打开的文件 fd 变成 1
c 复制代码
#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

int main() {
    // 1. 关闭stdout(fd=1)
    close(1);
    printf("这段文字不会显示(stdout已关闭)\n"); // printf默认写fd=1,关闭后失效

    // 2. 打开新文件,查看fd
    int fd = open("test.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
    if (fd == -1) { perror("open"); return 1; }

    printf("fd=%d\n", fd); // 此时printf写的是新文件(fd=1),不会显示在终端
    close(fd);

    return 0;
}

运行后查看test.txt

bash 复制代码
cat test.txt
# 输出:fd=1

现象解释:关闭1后,fd_array[1]变成NULL(未使用),新打开文件时,内核会分配 "最小未使用的 fd"------ 也就是1,所以open返回1。此时printf(默认写 fd=1)会把内容写入test.txt,而不是显示器。

案例 2:关闭 stdin(fd=0)后,无法从键盘读入
c 复制代码
#include <stdio.h>
#include <unistd.h>
#include <string.h>

int main() {
    // 1. 关闭stdin(fd=0)
    close(0);

    // 2. 尝试从键盘读入(read(fd=0))
    char buf[1024] = {0};
    ssize_t read_cnt = read(0, buf, sizeof(buf)-1);
    if (read_cnt == -1) {
        perror("read failed"); // 会报错:Bad file descriptor(fd=0已关闭)
        return 1;
    }

    printf("你输入了:%s\n", buf);
    return 0;
}

运行结果:

bash 复制代码
./close_stdin
read failed: Bad file descriptor

现象解释:read(0)试图从 fd=0 读数据,但 fd=0 已被关闭(fd_array[0]NULL),内核无法找到对应的文件,所以返回错误。

四、fd 的分配规则:"最小未使用" 原则

通过上面的案例,我们能总结出 fd 的核心分配规则:内核会从fd_array的下标0开始,找第一个未使用的(fd_array[fd] == NULL)最小下标,作为新的 fd

这个规则非常重要,是 "重定向" 的底层基础(下一篇会讲)。我们用三个场景验证这个规则:

场景 1:默认情况下,新打开文件的 fd 是 3

c 复制代码
#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

int main() {
    // 打开3个文件,查看fd
    int fd1 = open("a.txt", O_WRONLY | O_CREAT, 0666);
    int fd2 = open("b.txt", O_WRONLY | O_CREAT, 0666);
    int fd3 = open("c.txt", O_WRONLY | O_CREAT, 0666);

    printf("fd1=%d, fd2=%d, fd3=%d\n", fd1, fd2, fd3); // 输出:3,4,5

    close(fd1);
    close(fd2);
    close(fd3);
    return 0;
}

原因:0/1/2已被默认 fd 占用,最小未使用的下标是3,之后依次是45

场景 2:关闭 fd=2 后,新打开文件的 fd 是 2

c 复制代码
#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

int main() {
    close(2); // 关闭stderr(fd=2)
    int fd = open("test.txt", O_WRONLY | O_CREAT, 0666);
    printf("fd=%d\n", fd); // 输出:2

    close(fd);
    return 0;
}

原因:关闭2后,fd_array[2]变为NULL,最小未使用的下标是2,所以新 fd 是2

场景 3:关闭 fd=1 和 fd=3 后,新打开文件的 fd 是 1

c 复制代码
#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

int main() {
    // 先打开一个文件,fd=3
    int fd3 = open("temp.txt", O_WRONLY | O_CREAT, 0666);
    printf("fd3=%d\n", fd3); // 输出:3

    // 关闭fd=1和fd=3
    close(1);
    close(fd3);

    // 新打开文件,查看fd
    int new_fd = open("new.txt", O_WRONLY | O_CREAT, 0666);
    printf("new_fd=%d\n", new_fd); // 输出:1(最小未使用是1)

    close(new_fd);
    return 0;
}

原因:关闭13后,未使用的 fd 是13,最小的是1,所以新 fd 是1

五、fd 与 FILE 结构体的关系:封装与被封装

在第二篇中,我们学过 C 库 IO 的FILE*指针(如fopen返回的FILE* fp)------ 它和 fd 是什么关系?答案是:FILE 结构体是 fd 的 "封装体",内部包含 fd 和用户态缓冲区。

5.1 FILE 结构体的核心成员(简化版)

C 标准库的FILE结构体定义在/usr/include/libio.h中,核心成员如下:

c 复制代码
typedef struct _IO_FILE FILE;

struct _IO_FILE {
    // 1. 用户态缓冲区相关(C库IO的缓冲机制)
    char* _IO_read_ptr;   // 当前读指针(缓冲区中已读到的位置)
    char* _IO_read_end;   // 读缓冲区的末尾
    char* _IO_write_ptr;  // 当前写指针(缓冲区中已写到的位置)
    char* _IO_write_end;  // 写缓冲区的末尾
    char* _IO_buf_base;   // 缓冲区的起始地址
    char* _IO_buf_end;    // 缓冲区的末尾地址

    // 2. 封装的文件描述符(关键!关联系统IO)
    int _fileno;          // 对应的文件描述符(如0、1、2、3...)

    // 3. 其他字段(如缓冲类型、错误标志等)
    int _flags;           // 缓冲类型(行缓冲/全缓冲/无缓冲)
    // ... 其他字段 ...
};
  • _fileno:是FILE结构体的 "核心",存储的就是系统 IO 的 fd------C 库 IO 的所有操作(fread/fwrite/fclose),最终都会通过_fileno调用系统 IO 接口;
  • 用户态缓冲区:_IO_buf_base等字段构成 C 库的 "用户态缓冲"------fread/fwrite会先操作缓冲区,减少系统调用次数(比如printf会先把数据写到缓冲区,满了再调用write)。

5.2 验证:FILE * 内部的_fd 就是 fd

我们可以通过fileno函数(C 库提供)获取FILE*对应的 fd,验证两者的关联:

c 复制代码
#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

int main() {
    // 1. C库IO打开文件,获取FILE*
    FILE *fp = fopen("test.txt", "w");
    if (fp == NULL) { perror("fopen"); return 1; }

    // 2. 获取FILE*对应的fd(通过fileno函数)
    int fd = fileno(fp);
    printf("FILE*对应的fd=%d\n", fd); // 输出:3(默认情况下)

    // 3. 用系统IO操作这个fd(验证关联)
    const char *msg = "通过fd写入的数据\n";
    write(fd, msg, strlen(msg)); // 数据会写入test.txt

    fclose(fp);
    return 0;
}

运行后查看test.txt

bash 复制代码
cat test.txt
# 输出:通过fd写入的数据

现象解释:fileno(fp)获取的 fd=3,和系统 IO 的 fd 完全一致 ------ 用write(3, ...)写入的数据,会和fwrite(fp, ...)写入的数据到同一个文件,证明FILE*确实封装了 fd。

5.3 C 库 IO 与系统 IO 的调用关系(再次梳理)

有了 fd 和FILE的关系,我们可以更清晰地梳理 C 库 IO 与系统 IO 的调用链条:

plaintext 复制代码
用户代码(C库IO) → FILE结构体 → fd → 系统IO接口 → 内核

比如fwrite(msg, 1, len, fp)的流程:

  1. 检查fp的用户态缓冲区是否有空间;
  2. 若有空间,将msg拷贝到缓冲区(不调用系统 IO);
  3. 若缓冲区满,调用write(fp->_fileno, 缓冲区数据, 缓冲区大小)
  4. 内核通过fp->_fileno(fd)找到fd_array[fd],再找到struct file,完成写入。

write(fd, msg, len)(系统 IO)的流程:

  1. 直接调用内核的write系统调用;
  2. 内核通过 fd 找到fd_array[fd]struct file,完成写入;
  3. 无用户态缓冲,数据直接从用户空间拷贝到内核空间。

六、fd 的常见问题与排查方法

fd 虽然是 "小整数",但使用不当会导致严重问题(如 fd 泄漏、程序崩溃)。这里总结常见问题及排查方法。

6.1 问题 1:fd 泄漏(最常见)

现象

进程打开的 fd 越来越多,最终达到NR_OPEN_DEFAULT(默认 1024),新open返回-1,错误信息为Too many open files

原因

  • 打开文件 / 设备后未调用close(如open成功但忘记close,函数中途返回未清理);
  • 循环中重复open而不close(如每次循环都open同一个文件,不关闭旧 fd)。

排查方法

  1. lsof查看进程打开的 fdlsof -p <PID>PID是目标进程的 ID),输出中FD列就是 fd,TYPE列是文件类型(如REG表示普通文件,CHR表示字符设备)。

  2. /proc/[PID]/fd查看 fd 列表ls -l /proc/[PID]/fd | wc -l(统计 fd 总数),ls -l /proc/[PID]/fd查看具体 fd 对应的文件。

示例:fd 泄漏代码

c 复制代码
#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

int main() {
    int count = 0;
    while (1) {
        // 循环open但不close,导致fd泄漏
        int fd = open("/dev/null", O_WRONLY);
        if (fd == -1) {
            perror("open failed");
            printf("总共打开了%d个fd\n", count);
            break;
        }
        count++;
        printf("当前fd=%d\n", fd);
        // 忘记close(fd);
    }
    return 0;
}

运行后会输出:

plaintext

plaintext 复制代码
当前fd=3
当前fd=4
...
当前fd=1023
open failed: Too many open files
总共打开了1021个fd

原因:默认 fd=0/1/2 已占用,最多能打开 1024 个 fd,所以循环 1021 次后(fd=3~1023),新open失败。

6.2 问题 2:使用已关闭的 fd

现象

调用read/write时返回-1,错误信息为Bad file descriptor(无效的文件描述符)。

原因

  • fd 已被close,但后续代码仍使用该 fd;
  • fd 被重复close(第一次close后 fd 失效,第二次close会报错)。

避免方法

  • 关闭 fd 后,将 fd 变量设为-1(如close(fd); fd = -1;);
  • 使用 fd 前先判断是否为-1(如if (fd != -1) { write(fd, ...); })。

6.3 问题 3:混淆 fd 的 "进程独立性"

现象

进程 A 打开文件得到 fd=3,进程 B 打开同一个文件得到 fd=4,但两者写入的数据都能到同一个文件。

原因

fd 是 "进程私有" 的 ------ 每个进程的fd_array独立,不同进程的 fd 即使值相同,也可能对应不同文件;反之,不同进程的 fd 值不同,也可能对应同一个文件(通过struct file的引用计数实现)。

示例:两个进程打开同一个文件

c 复制代码
// 进程A(a.c)
#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

int main() {
    int fd = open("test.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
    if (fd == -1) { perror("open"); return 1; }

    const char *msg = "进程A写入的数据\n";
    write(fd, msg, strlen(msg));
    printf("进程A的fd=%d\n", fd); // 输出:3

    sleep(10); // 等待进程B写入
    close(fd);
    return 0;
}

// 进程B(b.c)
#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

int main() {
    int fd = open("test.txt", O_WRONLY | O_APPEND, 0666);
    if (fd == -1) { perror("open"); return 1; }

    const char *msg = "进程B写入的数据\n";
    write(fd, msg, strlen(msg));
    printf("进程B的fd=%d\n", fd); // 输出:3(默认情况下)

    close(fd);
    return 0;
}

分别运行两个进程(进程 A 先运行),查看test.txt

bash 复制代码
cat test.txt
# 输出:
# 进程A写入的数据
# 进程B写入的数据

现象解释:进程 A 和 B 的 fd 都是 3,但它们的fd_array[3]都指向同一个文件的struct file(内核通过inode关联同一个文件),所以写入的数据都会到test.txt

七、总结与下一篇预告

这篇我们彻底揭开了文件描述符 fd 的面纱:

  • 本质 :fd 是进程fd_array数组的下标,通过 fd 能找到struct file,进而关联文件;
  • 默认 fd:进程启动时默认打开 0(stdin)、1(stdout)、2(stderr),对应终端设备;
  • 分配规则 :最小未使用原则 ------ 内核分配fd_array中第一个未使用的最小下标;
  • 与 FILE 的关系FILE是 fd 的封装,内部通过_fileno存储 fd,同时包含用户态缓冲区。

理解 fd 是掌握 "重定向" 的关键 ------ 因为重定向的本质就是 "修改 fd 对应的struct file指针"(比如把 fd=1 的指针从 "显示器文件" 改成 "磁盘文件")。

下一篇我们将聚焦重定向 :讲解重定向的底层原理(基于 fd 分配规则)、dup2系统调用的使用(实现高效重定向),以及如何在自定义 Shell 中添加重定向功能(衔接你之前的微型 Shell 实战)------ 让你明白 "ls -l > log.txt到底是怎么实现的"。

相关推荐
wzb562 小时前
把 Vim 打造成 Nginx 开发 / 调试 IDE(WSL Ubuntu 完整教程)
linux·ide·nginx·ubuntu·vim·c/c++
SPC的存折2 小时前
12、Ansible安全加固
linux·运维·服务器·安全·ansible
l1t2 小时前
修改OraDB-DUMP-Viewer-3.1.1的windows dll 版本test_export示例为Linux
linux·人工智能·windows·oracle
常利兵2 小时前
安卓开发避坑指南:全局异常捕获与优雅处理实战
android·服务器·php
oi..2 小时前
Linux入门(2)
linux·笔记·测试工具·安全·网络安全
星夜落月2 小时前
ONLYOFFICE Docs 自托管在线办公套件搭建指南
服务器·网络·onlyoffice
鄃鳕2 小时前
vscode远程连接virtualBox上的Ubuntu
linux·运维·ubuntu
@土豆2 小时前
【混合云组网实战】Docker部署内网互通服务,实现本地网段访问公有云VPC私网
运维·docker·容器
Lucis__2 小时前
Linux系统收官篇:线程学习的一些心得总结
linux·学习·线程