操作系统:进程间通信方式详解(下:消息队列、信号量、共享内存、套接字)

每日一问:操作系统:进程间通信方式详解(下:消息队列、信号量、共享内存、套接字)

进程间通信(Inter-Process Communication,IPC)是操作系统中实现不同进程之间数据交换和协作的关键机制。本文详细介绍了几种常用的 IPC 方式,包括消息队列、信号量、共享内存和套接字。每种通信方式都有其独特的应用场景和优势,而现有的介绍往往局限于概念的介绍,本文则结合实际应用,通过极为详细的可以运行的 C++ 和 Java 示例代码,帮助读者理解这些机制的实现原理和应用场景。 对于无名管道、有名管道、高级管道,则可见操作系统:进程间通信方式详解(上:无名管道、有名管道、高级管道)。


文章目录

  • 每日一问:操作系统:进程间通信方式详解(下:消息队列、信号量、共享内存、套接字)
    • 一、进程间通信概述
    • 二、消息队列(Message Queue)
      • 2.1 消息队列的定义与特点
      • 2.2 消息队列的C++示例代码
      • 2.3 消息队列的Java示例代码
    • 三、信号量(Semaphore)
      • 3.1 信号量的定义与特点
      • 3.2 信号量的C++示例代码
      • 3.3 信号量的Java示例代码
    • 四、共享内存(Shared Memory)
      • 4.1 共享内存的定义与特点
      • 4.2 共享内存的C++示例代码
      • 4.3 共享内存的Java示例代码
    • 五、套接字(Socket)
      • 5.1 套接字的定义与特点
      • 5.2 套接字的实现
        • C++ 示例代码(TCP 套接字通信)
        • Java 示例代码(TCP 套接字通信)
    • 六、总结(前面可以不看,这里表格总结必看)


本文深入讲解了消息队列、信号量、共享内存和套接字的定义、特点及实际应用,结合代码示例展示了这些 IPC 方式在进程间数据传输与同步中的应用。文章适合对进程间通信感兴趣的初学者和开发人员,通过示例代码掌握 IPC 机制的具体实现。


一、进程间通信概述

进程是操作系统的基本执行单位,每个进程有独立的内存空间。由于这种独立性,进程之间无法直接访问对方的数据,进程间通信(IPC)机制因此应运而生。常见的 IPC 方式包括无名管道、有名管道、消息队列、信号量、共享内存和套接字等。这些方式在数据传输效率、同步机制和复杂度上各不相同,适用于不同的应用场景。在接下来的章节,将介绍消息队列、信号量、共享内存和套接字,对于无名管道、有名管道、高级管道,则可见操作系统:进程间通信方式详解(上:无名管道、有名管道、高级管道)。

二、消息队列(Message Queue)

2.1 消息队列的定义与特点

消息队列是一种基于消息传递的通信机制,允许进程通过消息队列发送和接收消息。消息队列支持异步通信,发送方和接收方不需要同时工作。消息队列的特点是消息可以按优先级和顺序存储,便于进程之间有序交换数据。

2.2 消息队列的C++示例代码

以下是一个消息队列的 C++ 示例代码,通过 msggetmsgsndmsgrcv 系统调用创建和操作消息队列:

#include <iostream>  // 标准输入输出库
#include <sys/ipc.h>  // IPC 机制相关函数
#include <sys/msg.h>  // 消息队列相关函数
#include <cstring>  // 字符串操作库// 定义消息结构体
struct msg_buffer {long msg_type;  // 消息类型,必须为正整数char msg_text[100];  // 消息内容
};int main() {key_t key;int msgid;msg_buffer message;// 使用 ftok 生成消息队列的唯一键key = ftok("progfile", 65);  // "progfile" 文件名,65 是一个任意数值// 使用 msgget 创建消息队列,如果不存在则创建,权限设置为 0666msgid = msgget(key, 0666 | IPC_CREAT);message.msg_type = 1;  // 设置消息类型为 1// 写入消息到消息队列std::cout << "Write Message: ";std::cin.getline(message.msg_text, sizeof(message.msg_text));  // 从控制台读取消息msgsnd(msgid, &message, sizeof(message), 0);  // 发送消息到队列// 读取消息队列msgrcv(msgid, &message, sizeof(message), 1, 0);  // 接收消息类型为 1 的消息std::cout << "Received Message: " << message.msg_text << std::endl;  // 打印接收到的消息// 删除消息队列msgctl(msgid, IPC_RMID, NULL);  // 删除消息队列,清理资源return 0;
}

解释

  1. ftok():生成唯一的键值,用于识别消息队列。
  2. msgget():创建一个新的消息队列或获取一个已存在的消息队列。
  3. msgsnd():向消息队列发送消息。
  4. msgrcv():从消息队列接收消息。
  5. msgctl():控制消息队列,如删除队列。

2.3 消息队列的Java示例代码

Java 没有直接的消息队列实现,可以通过 BlockingQueue 类进行模拟:

import java.util.concurrent.BlockingQueue;  // 导入 BlockingQueue 接口,用于实现阻塞队列
import java.util.concurrent.LinkedBlockingQueue;  // 导入 LinkedBlockingQueue 类,实现线程安全的阻塞队列public class MessageQueueExample {// 创建一个阻塞队列用于模拟消息队列private static BlockingQueue<String> queue = new LinkedBlockingQueue<>();public static void main(String[] args) throws InterruptedException {// 创建发送线程Thread sender = new Thread(() -> {try {queue.put("Hello from sender!");  // 向队列中放入消息} catch (InterruptedException e) {e.printStackTrace();  // 捕获并打印异常}});// 创建接收线程Thread receiver = new Thread(() -> {try {String message = queue.take();  // 从队列中取出消息System.out.println("Received: " + message);  // 输出接收到的消息} catch (InterruptedException e) {e.printStackTrace();  // 捕获并打印异常}});sender.start();  // 启动发送线程receiver.start();  // 启动接收线程sender.join();  // 等待发送线程结束receiver.join();  // 等待接收线程结束}
}

解释

  1. BlockingQueue:Java 中用于线程间通信的阻塞队列,模拟消息队列的异步特性。
  2. put()take():分别用于将消息放入队列和从队列中取出消息,实现发送和接收操作。

三、信号量(Semaphore)

3.1 信号量的定义与特点

信号量是一种用于进程间同步的计数器机制,可以控制多个进程对共享资源的访问。信号量经常与共享内存结合使用,解决并发访问问题,确保资源不会被多个进程同时访问而导致数据冲突。

3.2 信号量的C++示例代码

以下是一个简单的 C++ 信号量示例,演示如何使用信号量控制线程对临界区的访问:

#include <iostream>  // 标准输入输出库
#include <pthread.h>  // POSIX 线程库
#include <semaphore.h>  // 信号量库sem_t semaphore;  // 定义信号量// 线程执行的任务函数
void* task(void* arg) {sem_wait(&semaphore);  // 尝试获取信号量,信号量值减 1std::cout << "Entered critical section" << std::endl;  // 打印消息表示进入临界区sem_post(&semaphore);  // 释放信号量,信号量值加 1return NULL;
}int main() {pthread_t t1, t2;  // 定义两个线程sem_init(&semaphore, 0, 1);  // 初始化信号量,0 表示信号量用于线程间同步,初始值为 1// 创建两个线程执行任务pthread_create(&t1, NULL, task, NULL);pthread_create(&t2, NULL, task, NULL);// 等待两个线程执行完毕pthread_join(t1, NULL);pthread_join(t2, NULL);sem_destroy(&semaphore);  // 销毁信号量,释放资源return 0;
}

解释

  1. sem_init():初始化信号量,指定信号量初始值。
  2. sem_wait():等待信号量,可进入临界区时信号量值减 1。
  3. sem_post():释放信号量,信号量值加 1。
  4. sem_destroy():销毁信号量,清理资源。

3.3 信号量的Java示例代码

Java 通过 java.util.concurrent.Semaphore 类实现信号量控制:

import java.util.concurrent.Semaphore;  // 导入 Semaphore 类public class SemaphoreExample {private static Semaphore semaphore = new Semaphore(1);  // 创建信号量,初始值为 1public static void main(String[] args) {// 定义线程任务Runnable task = () -> {try {semaphore.acquire();  // 获取信号量,阻塞直到信号量可用System.out.println("Entered critical section");  // 打印进入临界区的消息semaphore.release();  // 释放信号量} catch (InterruptedException e) {e.printStackTrace();  // 捕获并打印异常}};// 创建并启动两个线程Thread t1 = new Thread(task);Thread t2 = new Thread(task);t1.start();t2.start();}
}

解释

  1. acquire():尝试获取信号量,信号量不足时阻塞。
  2. release():释放信号量,允许其他线程进入临界区。

四、共享内存(Shared Memory)

4.1 共享内存的定义与特点

共享内存是最直接的进程间通信方式,通过多个进程共享一块内存区域来进行数据交换。共享内存提供了最快的通信速度,但需要借助同步机制来防止数据冲突。

4.2 共享内存的C++示例代码

以下代码展示了共享内存的使用方法,通过 shmgetshmat 系统调用来创建和连接共享内存:

#include <iostream>  // 标准输入输出库
#include <sys/ipc.h>  // IPC 机制相关函数
#include <sys/shm.h>  // 共享内存相关函数
#include <cstring>  // 字符串操作库int main() {// 创建共享内存键值,"shmfile" 是用于生成键值的路径,65 是任意选定的整数key_t key = ftok("shmfile", 65);// 创建共享内存,大小为 1024 字节,权限为 0666,若不存在则创建int shmid = shmget(key, 1024, 0666 | IPC_CREAT);// 连接到共享内存,返回一个指向共享内存的指针char *str = (char*) shmat(shmid, (void*)0, 0);strcpy(str, "Hello Shared Memory!");  // 向共享内存写入数据std::cout << "Data written in memory: " << str << std::endl;  // 输出写入的数据shmdt(str);  // 断开共享内存连接shmctl(shmid, IPC_RMID, NULL);  // 删除共享内存,清理资源return 0;
}

解释

  1. shmget():创建共享内存段,指定大小和权限。
  2. shmat():将共享内存附加到进程地址空间,返回指向共享内存的指针。
  3. shmdt():将共享内存从当前进程地址空间分离。
  4. shmctl():控制共享内存,包括删除共享内存段。

4.3 共享内存的Java示例代码

Java 通过 MappedByteBuffer 类实现类似共享内存的功能:

import java.io.RandomAccessFile;  // 导入 RandomAccessFile 类,用于文件读写
import java.nio.MappedByteBuffer;  // 导入 MappedByteBuffer 类,用于内存映射
import java.nio.channels.FileChannel;  // 导入 FileChannel 类,用于文件通道操作public class SharedMemoryExample {public static void main(String[] args) throws Exception {// 创建或打开文件 "shared_memory.bin",读写模式RandomAccessFile file = new RandomAccessFile("shared_memory.bin", "rw");// 将文件映射到内存,映射模式为读写,大小为 1024 字节MappedByteBuffer buffer = file.getChannel().map(FileChannel.MapMode.READ_WRITE, 0, 1024);buffer.put("Hello Shared Memory!".getBytes());  // 将数据写入内存映射buffer.flip();  // 重置缓冲区位置以便读取byte[] data = new byte[buffer.remaining()];  // 创建字节数组保存读取的数据buffer.get(data);  // 从缓冲区读取数据System.out.println("Data read from memory: " + new String(data));  // 输出读取的数据file.close();  // 关闭文件}
}

解释

  1. MappedByteBuffer:将文件的某一部分映射到内存,允许直接对文件数据进行读写。
  2. map():将文件通道中的数据映射到内存区域。
  3. flip():重置缓冲区位置,以便后续的读取操作。

五、套接字(Socket)

5.1 套接字的定义与特点

套接字(Socket)是一种支持本地和网络通信的进程间通信方式,可以在本地进程间或跨网络的不同计算机之间进行双向通信。套接字支持 TCP(可靠传输)和 UDP(不可靠但高效)两种协议。

5.2 套接字的实现

套接字是通信端点,通过绑定 IP 地址和端口号来进行数据交换。

C++ 示例代码(TCP 套接字通信)
#include <iostream>  // 标准输入输出库
#include <sys/socket.h>  // 套接字库
#include <arpa/inet.h>  // 地址转换库
#include <unistd.h>  // POSIX 操作库int main() {int server_fd, new_socket;  // 定义服务器套接字和新连接套接字struct sockaddr_in address;  // 定义地址结构体int opt = 1;int addrlen = sizeof(address);char buffer[1024] = {0};  // 定义缓冲区用于接收数据const char *hello = "Hello from server";  // 定义发送给客户端的消息// 创建套接字,使用 IPv4 地址族,TCP 流式套接字server_fd = socket(AF_INET, SOCK_STREAM, 0);if (server_fd == 0) {  // 检查套接字创建是否成功perror("socket failed");return 1;}// 设置套接字选项,允许地址和端口重用setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));// 绑定套接字到指定的 IP 地址和端口address.sin_family = AF_INET;address.sin_addr.s_addr = INADDR_ANY;  // 使用本地所有可用的 IP 地址address.sin_port = htons(8080);  // 端口号 8080if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {perror("bind failed");return 1;}// 监听端口,最大等待连接数为 3if (listen(server_fd, 3) < 0) {perror("listen failed");return 1;}// 接受客户端连接new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t *)&addrlen);if (new_socket < 0) {perror("accept failed");return 1;}// 读取客户端消息read(new_socket, buffer, 1024);std::cout << "Message from client: " << buffer << std::endl;// 发送回复给客户端send(new_socket, hello, strlen(hello), 0);std::cout << "Hello message sent" << std::endl;// 关闭套接字close(new_socket);close(server_fd);return 0;
}
Java 示例代码(TCP 套接字通信)
import java.io.*;  // 导入输入输出类
import java.net.*;  // 导入网络类public class SocketServer {public static void main(String[] args) {try (ServerSocket serverSocket = new ServerSocket(8080)) {  // 创建服务器套接字绑定到端口 8080System.out.println("Server started, waiting for connection...");// 等待客户端连接Socket socket = serverSocket.accept();System.out.println("Client connected.");// 创建输入输出流BufferedReader input = new BufferedReader(new InputStreamReader(socket.getInputStream()));PrintWriter output = new PrintWriter(socket.getOutputStream(), true);// 读取客户端消息String clientMessage = input.readLine();System.out.println("Received from client: " + clientMessage);// 回复客户端output.println("Hello from server!");// 关闭连接socket.close();} catch (IOException e) {e.printStackTrace();}}
}

解释

  1. ServerSocket:服务器端套接字,监听指定端口。
  2. accept():等待客户端连接,建立连接后返回客户端的套接字。
  3. BufferedReaderPrintWriter:用于处理输入输出流,读取客户端发送的数据并进行响应。

六、总结(前面可以不看,这里表格总结必看)

进程间通信(IPC)是操作系统中实现进程间数据交换和同步的关键技术。不同的 IPC 方式在性能、适用场景、易用性上各有特点:

通信方式定义特点应用场景
无名管道单向数据传输,父子进程间简单、只支持亲缘进程父子进程间数据传输
有名管道有名且持久,支持无亲缘进程双向、需文件系统支持任意进程间的数据传输
高级管道通过子进程执行命令并传输数据创建灵活,可执行命令结果执行系统命令,获取输出
消息队列基于消息的通信异步、按优先级排序异步任务处理
信号量计数器机制,控制资源访问同步、解决并发冲突多进程资源访问控制
共享内存共享内存区域快速传输数据高速、需同步机制需高效通信的场景
套接字本地和网络通信支持双向、网络和本地通信网络应用、跨主机进程间通信

下面给出更复杂版本的对比表格:

通信方式数据传输方向是否支持无亲缘关系进程同步与异步速度数据持久性编程复杂性典型应用场景
无名管道单向同步中等不持久父子进程间简单数据传输
有名管道双向同步中等不持久无亲缘关系进程间数据传输
高级管道单向同步中等不持久父子进程间调用系统命令或可执行程序
消息队列单向异步中等不持久多个进程间复杂数据交换
信号量N/A同步N/AN/A进程/线程同步,解决资源争用问题
共享内存双向同步(需同步机制)不持久大量数据的快速读写,需同步控制
套接字双向是(支持网络通信)同步/异步视网络环境而定不持久本地或网络进程间的复杂数据通信

通过正确选择 IPC 机制,开发者可以有效实现进程间的数据交换和同步,提升系统的响应速度和稳定性。根据实际需求,选择合适的进程间通信方式,可以最大限度地提高应用程序的性能和可靠性。

✨ 我是专业牛,一个渴望成为大牛🏆的985硕士🎓,热衷于分享知识📚,帮助他人解决问题💡,为大家提供科研、竞赛等方面的建议和指导🎯。无论是科研项目🛠️、竞赛🏅,还是图像🖼️、通信📡、计算机💻领域的论文辅导📑,我都以诚信为本🛡️,质量为先!🤝

如果你觉得这篇文章对你有所帮助,别忘了点赞👍、收藏📌和关注🔔!你的支持是我继续分享知识的动力🚀!✨ 如果你有任何问题或需要帮助,随时留言📬或私信📲,我都会乐意解答!😊

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

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

相关文章

Java进阶之集合框架(Set)

【基本内容】 二、Set接口(接上一章) Set是Java集合框架中不允许有重复元素的无序集合&#xff0c;其典型的实现类是HashSet&#xff0c;它完全是遵循Set接口特性规范实现的&#xff0c;无序且不允许元素重复&#xff1b;而Set接口下的实现类还有LinkedHashSet和TreeSort&#…

记录生产环境,通过域名访问的图片展示不全,通过ip+端口的方式访问图片是完整的

原因&#xff1a;部署nginx的服务器硬盘满了 排查发现nginx日志文件占用了大量硬盘 解决方案&#xff1a; 删除该文件&#xff0c;重启nginx服务&#xff0c;问题解决。

AI修手有救了?在comfyui中使用Flux模型实现局部重绘案例

&#x1f431;‍&#x1f409;背景 局部重绘相关的话题我们已经讨论和测试过很多次了&#xff0c;比如说inpaint模型、brushnet模型、powerpaint模型等等&#xff0c;最近对于flux模型重绘画面的案例也越来越多了&#xff0c;那我们就结合flux模型的重绘来试试看效果。 &…

前端mock了所有……

目录 一、背景描述 二、开发流程 1.引入Mock 2.创建文件 3.需求描述 4.Mock实现 三、总结 一、背景描述 前提&#xff1a; 事情是这样的&#xff0c;老板想要我们写一个demo拿去路演/拉项目&#xff0c;有一些数据&#xff0c;希望前端接一下&#xff0c;写几个表格&a…

qt信号与槽(自定义)

自定义信号与槽 在qt里&#xff0c;我们可以自己去定义信号与槽。 这里举个栗子&#xff1a; 信号的定义 在我们类里边定义一个信号&#xff0c;我们需要用signals&#xff1a;来声明&#xff0c;不用再去cpp文件里边定义。而且返回值必须是void&#xff0c;可以有参数。 槽…

2024年最新测绘地理信息规范在线查看下载

随着科技的飞速发展&#xff0c;测绘地理信息行业也迎来了新的机遇与挑战。 为了确保测绘地理信息的准确性和规范性&#xff0c;每年都会出台了一系列最新的测绘地理信息规范。 本文将历年地形行业发布的相关标准规范&#xff0c;包括现行和一些已经弃用的标准&#xff0c;截…

数据结构与算法——详谈栈和队列

目录 一&#xff1a;栈 1.1&#xff1a;栈的概念结构与实现 1.1.1&#xff1a;栈的概念结构 1.1.2&#xff1a;栈的实现 1.2&#xff1a;栈的各个功能实现 1.2.1&#xff1a;对栈进行初始化 1.2.2&#xff1a;判空栈 1.2.3&#xff1a;入栈 1.2.4&#xff1a;出栈 1.…

一文读懂AI安全治理框架

随着AI的发展以及研究&#xff0c;我们总会提到AI带来的一些潜在威胁&#xff0c;但截止目前我还没有完全的梳理过AI到底有哪些潜在的风险&#xff0c;今天就来一一看一下&#xff01;陆续补齐。

自动化中验证码的操作笔记,懂的赶紧收藏!

在自动化测试的过程中&#xff0c;验证码一直被视为一个“拦路虎”。很多测试人员在做接口或UI自动化时都会遇到验证码的阻碍&#xff0c;导致测试无法继续进行。今天&#xff0c;我们就来讨论如何在自动化过程中破解验证码&#xff0c;快速绕过这道关卡&#xff0c;轻松完成自…

LVM硬盘挂载

LVM硬盘挂载 一、基础概念 sda/sdb/nvme0n1/nvme0n2&#xff1a; 硬盘的命名方式&#xff0c;中括号的字母为第三位按不同硬盘的加载顺序排序。sda1/sda2/sdb1&#xff1a; 第4位为分区号&#xff0c;数字为不同分区的依序命名lvm: LVM是一种逻辑卷管理器&#xff0c;允许管理…

黑马头条day1 环境搭建 SpringCloud微服务(注册发现,服务调用,网关)

Nacos 环境搭建 Vmvare打开已经安装好的虚拟机镜像环境 使用findshell作为链接工具 和MobaXterm差不多 初始工程搭建 项目导入到idea 里边 这个项目都是用的比较老的东西 jdk1.8 甚至把仓库也提供好了 主体机构 common 就是通用的配置 feign 是对外的接口 model …

css五种定位总结

在 CSS 中&#xff0c;定位&#xff08;Positioning&#xff09;主要有五种模式&#xff0c;每种模式的行为和特点不同&#xff0c;以下是 static、relative、absolute、fixed 和 sticky 五种定位方式的对比总结&#xff1a; 1. static&#xff08;默认定位&#xff09; 特性…

“中秋快乐”文字横幅的MATLAB代码生成

中秋快乐呀朋友们&#xff01;&#xff01;&#xff01; 给大家带来一个好玩的代码&#xff0c;能够生成“中秋快乐”的横幅文字&#xff0c;比较简单&#xff0c;当然你也可以根据自己的需求去更改文字和背景&#xff0c;废话不多说&#xff0c;直接展示。 文字会一直闪烁&…

计算机毕业设计 基于SpringBoot框架的网上蛋糕销售系统的设计与实现 Java实战项目 附源码+文档+视频讲解

博主介绍&#xff1a;✌从事软件开发10年之余&#xff0c;专注于Java技术领域、Python人工智能及数据挖掘、小程序项目开发和Android项目开发等。CSDN、掘金、华为云、InfoQ、阿里云等平台优质作者✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精…

基于Springboot+vue的音乐网站

随着信息技术在管理上越来越深入而广泛的应用&#xff0c;管理信息系统的实施在技术上已逐步成熟。本文介绍了音乐网站的开发全过程。通过分析音乐网站管理的不足&#xff0c;创建了一个计算机管理音乐网站的方案。文章介绍了音乐网站的系统分析部分&#xff0c;包括可行性分析…

如何在Mac上安装多个Python环境

如何在Mac上安装多个Python环境 简介 在你的Mac上使用多个Python环境可以对项目管理很有帮助,特别是在同时处理不同Python版本或不同的包需求时。在这篇文章中,我们将向你展示如何在Mac上轻松地安装和管理多个Python环境。 一. 安装Conda Conda是一个包管理和环境管理系统…

深度学习 之 常见损失函数简介:名称、作用及用法

引言 在机器学习和深度学习中&#xff0c;损失函数&#xff08;Loss Function&#xff09;是模型训练过程中一个不可或缺的部分。它用来度量模型预测结果与真实值之间的差异&#xff0c;从而指导模型参数的优化。合理选择损失函数对于提高模型的准确性和泛化能力至关重要。本文…

代码随想录训练营第36天|二维背包

1049. 最后一块石头的重量 II class Solution { public:int lastStoneWeightII(vector<int>& stones) {int sumaccumulate(stones.begin(),stones.end(),0);int targetsum/2;vector<int> dp(target1,0);for(auto& stone: stones){for(int itarget; i>s…

Ansible——Playbook基本功能

文章目录 一、Ansible Playbook介绍1、Playbook的简单组成1&#xff09;“play”2&#xff09;“task”3&#xff09;“playbook” 2、Playbook与ad-hoc简单对比区别联系 3、YAML文件语法&#xff1a;1. 基本结构2. 数据类型3. 列表4. 字典&#xff08;映射&#xff09;5. 注释…

免费表格图片识别成表格小工具

自动提取图片中的文字&#xff0c;并按照表格的格式整理好 需要的自取吧&#xff0c;下载地址&#xff1a;https://pan.quark.cn/s/f4b1ac62b808