Linux 文件系统与 I/O 编程核心原理及实践笔记

news2025/6/8 6:14:40

文章目录

  • 一、理解文件
    • 1.1 狭义理解
    • 1.2 广义理解
    • 1.3 文件操作的归类认识
    • 1.4 系统角度:进程与文件的交互
    • 1.5 实践示例
  • 二、回顾 C 文件接口
    • 2.1 hello.c 打开文件
    • 2.2 hello.c 写文件
    • 2.3 hello.c 读文件
    • 2.4 输出信息到显示器的几种方法
    • 2.5 stdin & stdout & stderr
  • 三、系统文件I/O
    • 3.1 一种传递标志位的方法
    • 3.2 接口介绍
    • 3.5 3-5 open函数返回值
    • 3.6 文件描述符 fd
      • 3.6.1 0 & 1 & 2
      • 3.6.2 文件描述符的分配规则:最小未使用下标优先
      • 3.6.3 应用场景:重定向的实现
      • 3.6.4 dup2 系统调用
  • 五、缓冲区
    • 5.1 什么是缓冲区
    • 5.2 为什么要引入缓冲区机制
    • 5.3 缓冲类型

一、理解文件

1.1 狭义理解

  • 文件在磁盘里
    *磁盘作为永久性存储介质,通过文件系统(如 EXT4、XFS)管理文件存储。文件系统将磁盘划分为inode(索引节点)和block(数据块)
    • inode:存储文件元数据(权限、所有者、修改时间等),每个文件唯一对应一个 inode。
    • block:存储文件实际数据,大小由文件系统决定(如 4KB)。
      注意:即使 0KB 的空文件也会占用inode 空间(不同文件系统 inode 大小不同,如 EXT4 默认 256 字节),但不占用数据块(block)。
  • 磁盘是外设(输入 / 输出设备)
    对磁盘文件的操作本质是IO(Input/Output),涉及内核与外设的数据交互(如通过 DMA 控制器读写磁盘)。

1.2 广义理解

Linux下一切皆文件
系统将硬件设备、进程信息、通信管道等抽象为文件,通过统一接口管理:

  • 硬件设备
    • 块设备:以块为单位读写(如硬盘 /dev/sda,文件类型 b)。
    • 字符设备:以字符流读写(如键盘 /dev/input/event0,文件类型 c)。
  • 虚拟文件系统
    • /proc:动态映射进程信息(如 /proc/self/exe 是当前进程二进制文件)。
    • /sys:暴露内核设备驱动细节(如 /sys/class/leds/ 控制 LED 灯)。
  • 进程通信
    • 管道文件(类型 p):mkfifo mypipe 创建命名管道。
    • 套接字文件(类型 s):/run/docker.sock 用于 Docker 进程通信。
      这种抽象屏蔽了底层差异,例如读写 /dev/tty1(终端设备文件)与读写普通文件使用相同 API。

1.3 文件操作的归类认识

文件 = 元数据(属性) + 数据内容

  • 元数据
    • 基础属性:权限(rwx)、所有者(uid/gid)、硬链接数(ls -l 第二列)。
    • 时间戳:修改时间(mtime)、状态改变时间(ctime)、访问时间(atime)。
    • 技术属性:inode 编号(ls -i)、文件大小(ls -l 第五列)、块数(ls -s)。
  • 数据内容
    分为文本(ASCII/UTF-8)和二进制(如可执行程序、图片),通过cathexdump等工具查看。
  • 操作分类
    • 内容操作:读写(read/write系统调用)、定位(lseek)、截断(truncate)。
  • 属性操作
    • 修改权限chmod(对应chmod系统调用)。
    • 更改所有者chown(对应chown系统调用)。
    • 查看元数据stat命令(对应stat系统调用,返回struct stat结构体)。

1.4 系统角度:进程与文件的交互

  • 一切文件操作由进程触发
    内核通过 ** 文件描述符(File Descriptor, FD)** 标识进程打开的资源,FD0~1023 的整数(默认:0=stdin1=stdout2=stderr)。
    可通过ls -l /proc/$$/fd查看当前进程打开的文件($$为当前进程 PID)。
  • 系统调用 vs 库函数
    • 系统调用:内核提供的底层接口(如openread),需从用户态陷入内核态,开销较高但更直接。
    • 库函数:C 标准库封装的高层接口(如fopenfread),内部调用系统调用并提供缓存机制(如stdio的缓冲区)。
    • 示例fprintf(stdout, "hello") 最终会调用write(1, "hello", 5)系统调用。
  • 内核如何管理文件
    • 每个打开的文件对应内核中的 file结构体,记录文件位置、引用计数等。
    • 多个进程可通过不同 FD 指向同一file结构体(如父子进程共享文件),实现数据共享。

1.5 实践示例

  1. 查看文件元数据
stat test.txt  # 显示inode、权限、时间戳等详细信息
ls -li test.txt  # 查看inode编号和硬链接数

在这里插入图片描述

  1. 操作设备文件
echo "Hello zkp!" > /home/zkp/linux/25/6/7/file/test.txt  # 向文件写入信息
cat /home/zkp/linux/25/6/7/file/test.txt  # 查看文件内容

在这里插入图片描述

  1. 理解文件描述符
exec 3<> file.txt  # 在当前Shell中打开文件,FD=3可读可写
echo "test" >&3    # 通过FD=3写入文件
cat <&3            # 通过FD=3读取文件
exec 3>&-          # 关闭FD=3

二、回顾 C 文件接口

2.1 hello.c 打开文件

在这里插入图片描述
打开的myfile文件在哪个路径下?

  • 在程序的当前路径下,那系统怎么知道程序的当前路径在哪里呢?
    可以使用 ls /proc/[进程id]命令查看当前正在运行进程的信息:
    在这里插入图片描述
    其中:
  • cwd:指向当前进程运行目录的一个符号链接。
  • exe:指向启动当前进程的可执行文件(完整路径)的符号链接。
    打开文件,本质是进程打开,所以,进程知道自己在哪里,即便文件不带路径,进程也知道。由此OS就能知道要创建的文件放在哪里。

2.2 hello.c 写文件

在这里插入图片描述

2.3 hello.c 读文件

在这里插入图片描述

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

在这里插入图片描述

2.5 stdin & stdout & stderr

  • C 默认会打开三个输出流,分别是 stdin,stdout,stderr
  • 这三个流的类型都是 FILE*,而 fopen 返回值类型也是文件指针

三、系统文件I/O

我们知道,文件的权限分为 rwx,对应的标志位为 4,2,1。

3.1 一种传递标志位的方法

核心原理:位掩码(Bit Mask)
每个标志对应 唯一的二进制位(如第 0 位、第 1 位…),通过 位运算 组合 / 解析:

  • 设置标志:用 |(按位或)组合多个标志(如 FLAG_A | FLAG_B)。
  • 检查标志:用 &(按位与)判断某一位是否为 1(如 if (flags & FLAG_A))。
#include <stdio.h>

// 定义权限标志位(与Linux系统保持一致)
#define PERM_READ   (1 << 2)  // 4: 读权限
#define PERM_WRITE  (1 << 1)  // 2: 写权限
#define PERM_EXEC   (1 << 0)  // 1: 执行权限

// 解析权限并打印
void func(int perms) {
    printf("用户权限: ");
    printf(perms & USER_PERMS(PERM_READ)   ? "r" : "-");
    printf(perms & USER_PERMS(PERM_WRITE)  ? "w" : "-");
    printf(perms & USER_PERMS(PERM_EXEC)   ? "x" : "-");
    printf("\n");
}

int main() {
    // 组合权限:用户有读写,组有读,其他用户无权限
    int perms = (PERM_READ | PERM_WRITE)

    printf("权限掩码(八进制): 0%o\n", perms);  // 输出: 0x6
    func(perms);
    
    return 0;
}

3.2 接口介绍

在这里插入图片描述
参数

  • pathname:要打开或创建的目标文件
  • flags:打开文件时,可以传入多个参数选项,用下面的一个或者多个常量进行“或”运算,构成 flags
    • O_RDONLY:只读打开
    • O_WRONLY:只写打开
    • O_RDWR :读,写打开
      这三个常量,必须指定一个且只能指定一个
    • O_CREAT:若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限
    • O_APPEND:追加写

返回值

  • 成功:新打开的文件描述符
  • 失败:-1

open函数具体使用哪个,和具体应用场景相关,如目标文件不存在,需要open创建,则第三个参数表示创建文件的默认权限;否则,使用两个参数的open

writereadcloselseek,类比c文件相关接口。

3.5 3-5 open函数返回值

在认识返回值之前,先来认识一下两个概念:系统调用和库函数

  • 上面的 fopen fclose fread fwrite 都是C标准库当中的函数,我们称之为库函数(libc)。
  • open close read write lseek都属于系统提供的接口,称之为系统调用接口

看下面这张图:
在这里插入图片描述
系统调用接口和库函数的关系,一目了然。
所以,可以认为,f# 系列的函数,都是对系统调用的封装,方便二次开发。

3.6 文件描述符 fd

  • 通过对open函数的学习,我们知道了文件描述符就是一个小整数

3.6.1 0 & 1 & 2

  • Linux进程默认情况下会有3个缺省打开的文件描述符,分别是标准输入0,标准输出1,标准错误2。
  • 0,1,2对应的物理设备一般是:键盘,显示器,显示器
    所以输入输出还可以采用如下方式:
#include <stdio.h>
#include <sys/types.h>

#include <sys/stat.h>
#include <fcntl.h>
#include <string.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;
}

在这里插入图片描述

3.6.2 文件描述符的分配规则:最小未使用下标优先

  1. 默认初始状态
    进程启动时,内核会自动打开 3 个标准文件描述符:
    • 0:标准输入(stdin,默认关联键盘)
    • 1:标准输出(stdout,默认关联终端)
    • 2:标准错误(stderr,默认关联终端)
      因此,首次打开新文件时,文件描述符从 3 开始分配(依次递增:3、4、5…)。
  2. 关闭后复用
    如果进程主动关闭某个文件描述符(如 close(0)),后续调用 open 时,内核会 扫描文件描述符表,选择最小的未被使用的下标 分配给新文件。
    • 例:关闭 0 后,新打开的文件会优先占用 0;
    • 若同时关闭 0 和 2,新打开的文件会依次占用 0、2,再继续递增(如 3、4…)。

代码验证

#include <stdio.h>
#include <unistd.h>   // close
#include <fcntl.h>    // open, O_RDWR, O_CREAT

int main() {
    // 1. 初始打开:未关闭默认FD,从3开始
    int fd1 = open("test1.txt", O_RDWR | O_CREAT, 0644);
    printf("fd1: %d\n", fd1);  // 输出:3(0、1、2已占用)

    // 2. 关闭标准输入(FD=0),后续打开优先复用0
    close(0); 
    int fd2 = open("test2.txt", O_RDWR | O_CREAT, 0644);
    printf("fd2: %d\n", fd2);  // 输出:0(最小未使用下标)

    // 3. 关闭标准错误(FD=2),后续打开优先复用2
    close(2); 
    int fd3 = open("test3.txt", O_RDWR | O_CREAT, 0644);
    printf("fd3: %d\n", fd3);  // 输出:2(当前最小未使用下标)

    // 4. 继续打开,下一个最小未使用是3(0、2已用,1仍被stdout占用)
    int fd4 = open("test4.txt", O_RDWR | O_CREAT, 0644);
    printf("fd4: %d\n", fd4);  // 输出:3

    return 0;
}

运行结果

fd1: 3  
fd2: 0  
fd3: 2  
fd4: 3  

3.6.3 应用场景:重定向的实现

  1. 输出重定向示例
# 将命令输出写入文件(本质是修改FD=1的指向)
ls -l > output.txt

实现逻辑:

  • Shell 先关闭 FD=1(标准输出),再打开 output.txt,此时新文件会占用 FD=1;
  • 后续 ls 命令的输出会写入 FD=1(即 output.txt),而非终端。
  1. 代码模拟重定向
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>

int main() {
    // 1. 关闭标准输出(FD=1)
    close(1); 
    // 2. 打开新文件,会复用FD=1
    int fd = open("redirect.txt", O_WRONLY | O_CREAT, 0644);
    // 3. printf 会向 FD=1 写入(此时指向 redirect.txt)
    printf("Hello, Redirect!\n"); 
    close(fd);
    return 0;
}

运行后,redirect.txt 会包含 Hello, Redirect!,而非终端输出

注意事项

  • 关闭默认描述符(如 close(1))后,若后续代码依赖标准输出(如 printf),会导致输出丢失或异常。
  • 建议使用 dup2 实现重定向(安全关闭旧描述符,避免冲突)。

重定向的本质
在这里插入图片描述

3.6.4 dup2 系统调用

在这里插入图片描述

示例:

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>

int main() {
    // 1. 打开文件(获取新的文件描述符,如3)
    int fd = open("output.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
    if (fd == -1) {
        perror("open failed");
        return 1;
    }

    // 2. 将标准输出(FD=1)重定向到 fd 指向的文件
    if (dup2(fd, 1) == -1) {
        perror("dup2 failed");
        close(fd);
        return 1;
    }

    // 3. 此时 printf 会写入 output.txt,而非终端
    printf("Hello, dup2!\n");

    // 4. 关闭 fd(注意:标准输出仍指向 output.txt)
    close(fd);

    // 5. 验证:继续向标准输出写入
    fprintf(stdout, "This will also appear in output.txt\n");

    return 0;
}

printf是C库当中的IO函数,一般往stdout中输出,但是stdout底层访问文件的时候,找的还是fd:1,但此时,fd:1下标所表示内容,已经变成了myfifile的地址,不再是显示器文件的地址,所以,输出的任何消息都会往文件中写入,进而完成输出重定向。

五、缓冲区

5.1 什么是缓冲区

缓冲区是内存空间的一部分。也就是说,在内存空间中预留了一定的存储空间,这些存储空间用来缓冲输入或输出的数据,这部分预留的空间就叫做缓冲区。缓冲区根据其对应的是输入设备还是输出设备,分为输入缓冲区和输出缓冲区。

5.2 为什么要引入缓冲区机制

读写文件时,如果不会开辟对文件操作的缓冲区,直接通过系统调用对磁盘进行操作(读、写等),那么每次对文件进行一次读写操作时,都需要使用读写系统调用来处理此操作,即需要执行一次系统调用,执行一次系统调用将涉及到CPU状态的切换,即从用户空间切换到内核空间,实现进程上下文的切换,这将损耗一定的CPU时间,频繁的磁盘访问对程序的执行效率造成很大的影响。

为了减少使用系统调用的次数,提高效率,我们就可以采用缓冲机制。比如我们从磁盘里取信息,可以在磁盘文件进行操作时,可以一次从文件中读出大量的数据到缓冲区中,以后对这部分的访问就不需要再使用系统调用了,等缓冲区的数据取完后再去磁盘中读取,这样就可以减少磁盘的读写次数,再加上计算机对缓冲区的操作大大快于对磁盘的操作,故应用缓冲区可大大提高计算机的运行速度

又比如,我们使用打印机打印文档,由于打印机的打印速度相对较慢,我们先把文档输出到打印机相应的缓冲区,打印机再自行逐步打印,这时我们的CPU可以处理别的事情。可以看出,缓冲区就是一块内存区,它用在输入输出设备和CPU之间,用来缓存数据。它使得低速的输入输出设备和高速的CPU能够协调工作,避免低速的输入输出设备占用CPU,解放出CPU,使其能够高效率工作。

5.3 缓冲类型

标准I/O提供了3种类型的缓冲区。

  • 全缓冲区:这种缓冲方式要求填满整个缓冲区后才进行I/O系统调用操作。对于磁盘文件的操作通常使用全缓冲的方式访问。
  • 行缓冲区:在行缓冲情况下,当在输入和输出中遇到换行符时,标准I/O库函数将会执行系统调用操作。当所操作的流涉及一个终端时(例如标准输入和标准输出),使用行缓冲方式。因为标准1/O库每行的缓冲区长度是固定的,所以只要填满了缓冲区,即使还没有遇到换行符,也会执行I/0系统调用操作,默认行缓冲区的大小为1024。
  • 无缓冲区:无缓冲区是指标准I/O库不对字符进行缓存,直接调用系统调用。标准出错流stderr通常是不带缓冲区的,这使得出错信息能够尽快地显示出来。

除了上述列举的默认刷新方式,下列特殊情况也会引发缓冲区的刷新:

  1. 缓冲区满时;
  2. 执行flush语句;

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2403805.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

vite+tailwind封装组件库

前言 演示视频 https://www.bilibili.com/video/BV1EST3zPEyP/?spm_id_from333.1387.homepage.video_card.click 参考 https://juejin.cn/post/7112295067682865166 https://juejin.cn/post/7046187185615142949 代码仓库 https://gitee.com/malguy/vite-components-li…

【Java学习笔记】包装类

包装类&#xff08;Wrapper&#xff09; 1. 介绍 &#xff08;1&#xff09;针对八种基本数据类型相应的引用类型 --> 包装类 &#xff08;2&#xff09;有了类的特点&#xff0c;就可以调用类中的方法 2. 分类和继承关系 基本数据类型包装类父类booleanBooleanObjectc…

【高效开发工具系列】Blackmagic Disk Speed Test for Mac:专业硬盘测速工具

博客目录 一、Blackmagic Disk Speed Test 概述二、软件核心功能解析三、v3.3 版本的新特性与改进四、实际应用场景分析五、使用技巧与最佳实践六、与其他工具的比较及优势 一、Blackmagic Disk Speed Test 概述 Blackmagic Disk Speed Test 是 Mac 平台上广受专业人士青睐的一…

UniRig:如何在矩池云一站式解决 3D 模型绑定难题

在 3D 动画制作中&#xff0c;绑定&#xff08;Rigging&#xff09;是一个至关重要但复杂耗时的步骤。它包括为 3D 模型创建骨架并分配蒙皮权重&#xff0c;以实现流畅的动画效果。由清华大学与 Tripo 联合开发的 UniRig 框架&#xff0c;为这一难题提供了全新的解决方案。 什…

字符串字典序最大后缀问题详解

字符串字典序最大后缀问题详解 一、问题定义与背景1.1 问题描述1.2 实际应用场景 二、暴力解法及其局限性2.1 暴力解法思路2.2 代码示例2.3 局限性分析 三、双指针算法&#xff1a;高效解决方案3.1 算法核心思想3.2 算法步骤3.3 代码实现3.4 与暴力解法对比 四、复杂度分析4.1 …

VScode打开后一直显示正在重新激活终端 问题的解决方法

一、问题 本人打开“.py”文件后&#xff0c;同时会出现以下两个问题。 1、VScode一直循环在”正在重新激活终端“ 2、日志显示intellicode报错&#xff1a; Sorry, something went wrong activating IntelliCode support for Python. Please check the “Python” and “VS I…

pe文件结构(TLS)

TLS 什么是TLS? TLS是 Thread Local Storage 的缩写&#xff0c;线程局部存储。主要是为了解决多线程中变量同步的问题 如果需要要一个线程内部的各个函数调用都能访问&#xff0c;但其它线程不能访问的变量&#xff08;被称为static memory local to a thread 线程局部静态变…

中型零售业数据库抉择:MySQL省成本,SQL SERVER?

针对中型零售企业&#xff08;20台固定POS数十台移动POS&#xff0c;含库存管理与结算业务&#xff09;的操作系统与数据库选型&#xff0c;需平衡性能、成本、扩展性及运维效率。结合行业实践与系统需求&#xff0c;建议如下&#xff1a; &#x1f5a5;️ ​​一、操作系统选型…

IDEA中的debug使用技巧

详细教学视频见b站链接&#xff1a;IDEA的debug调试 CSDN详细博客文章链接&#xff1a;debug文章学习 以下为个人学习记录总结&#xff1a; idea中的debug模式界面如下&#xff1a; 现在详细介绍图标作用&#xff1a; 图标一&#xff08;Show Execution Point&#xff09;&…

RockyLinux9.6搭建k8s集群

博主介绍&#xff1a;✌全网粉丝5W&#xff0c;全栈开发工程师&#xff0c;从事多年软件开发&#xff0c;在大厂呆过。持有软件中级、六级等证书。可提供微服务项目搭建与毕业项目实战&#xff0c;博主也曾写过优秀论文&#xff0c;查重率极低&#xff0c;在这方面有丰富的经验…

AI IDE 正式上线!通义灵码开箱即用

近期&#xff0c;通义灵码AI IDE正式上线&#xff0c;即日起用户可在通义灵码官网免费下载开箱即用。 作为AI原生的开发环境工具&#xff0c;通义灵码AI IDE深度适配了最新的千问3大模型&#xff0c;并全面集成通义灵码插件能力&#xff0c;具备编程智能体、行间建议预测、行间…

Ubuntu20.04基础配置安装——系统安装(一)

引言&#xff1a; 工作需要&#xff0c;Ubuntu的各类环境配置&#xff0c;从23年开始使用Ubuntu20.04之后&#xff0c;尽管能力在不断提升&#xff0c;但是依旧会遇到Ubuntu系统崩掉的情况&#xff0c;为了方便后续系统出现问题及时替换&#xff0c;减少从网上搜索资源进行基础…

Kafka入门-消费者

消费者 Kafka消费方式&#xff1a;采用pull&#xff08;拉&#xff09;的方式&#xff0c;消费者从broker中主动拉去数据。使用pull的好处就是消费者可以根据自身需求&#xff0c;进行拉取数据&#xff0c;但是坏处就是如果Kafka没有数据&#xff0c;那么消费者可能会陷入循环…

中电金信:从智能应用到全栈AI,大模型如何重构金融业务价值链?

导语 当前&#xff0c;AI大模型技术正加速重构金融行业的智能化图景。为助力金融机构精准把握这一变革机遇&#xff0c;中电金信与IDC联合发布《中国金融大模型发展白皮书》。《白皮书》在梳理了AI大模型整体发展现状的基础上&#xff0c;结合金融行业用户的需求调研深入分析了…

巴西医疗巨头尤迈Kafka数据泄露事件的全过程分析与AI安防策略分析

一、事件背景与主体信息 涉事主体:Unimed,全球最大医疗合作社,巴西医疗行业龙头企业,拥有约1500万客户。技术背景:泄露源于其未保护的Kafka实例(开源实时数据传输平台),用于客户与聊天机器人“Sara”及医生的实时通信。二、时间线梳理 时间节点关键事件描述2025年3月24…

快速上手 Metabase:从安装到高级功能实战

文章目录 1. 引言&#xff1a;Metabase——轻量级的数据分析工具&#x1f3af; 学完本教程你能掌握&#xff1a; 2. 安装 Metabase&#xff1a;本地部署实操2.1 环境准备2.2 使用 Docker 安装 Metabase2.3 初始化设置2.4 连接外部数据库 3. 第一个数据探索&#xff1a;5分钟创建…

Linux基础命令which 和 find 简明指南

&#x1f3af; Linux which 和 find 命令简明指南&#xff1a;从入门到实用 &#x1f4c5; 更新时间&#xff1a;2025年6月7日 &#x1f3f7;️ 标签&#xff1a;Linux | which | find | 命令行 | 文件查找 文章目录 前言&#x1f31f; 一、Linux 命令的本质与 which、find 的作…

思尔芯携手Andes晶心科技,加速先进RISC-V 芯片开发

在RISC-V生态快速发展和应用场景不断拓展的背景下&#xff0c;芯片设计正面临前所未有的复杂度挑战。近日&#xff0c;RISC-V处理器核领先厂商Andes晶心科技与思尔芯&#xff08;S2C&#xff09;达成重要合作&#xff0c;其双核单集群AX45MPV处理器已在思尔芯最新一代原型验证系…

kafka消息积压排查

kafka监控搭建&#xff1a;https://insights.blog.csdn.net/article/details/139129552?spm1001.2101.3001.6650.1&utm_mediumdistribute.pc_relevant.none-task-blog-2%7Edefault%7Ebaidujs_baidulandingword%7EPaidSort-1-139129552-blog-132216491.235%5Ev43%5Econtrol…

drawio 开源免费的流程图绘制

开源地址 docker-compose 一键启动 #This compose file adds draw.io to your stack version: 3.5 services:drawio:image: jgraph/drawiocontainer_name: drawiorestart: unless-stoppedports:- 8081:8080- 8443:8443environment:PUBLIC_DNS: domainORGANISATION_UNIT: unitOR…