【Linux操作系统】进程间通信之匿名管道与命名管道
目录
- 一、进程间通信的目的:
- 二、进程间通信的种类
- 三、什么是管道
- 四、匿名管道(共同祖先的进程之间)
- 1.匿名管道的使用
- 2.匿名管道举例
- 3.匿名管道的原理
- 4.管道特点
- 5.管道的读写规则
- 1. 当管道内没有数据可读时
- 2.当管道满的时候
- 3.管道端被关闭
- 4.数据写入的原子性
- 五、命名管道(让两个不相干的进程通信)
- 1.命名管道的概念与原理
- 2.命名管道的创建与删除
- 3.匿名管道与命名管道的区别
- 4.命名管道的打开规则
- 5.命名管道举例(附完整代码)
- 1.makefile文件
- 2.wFifo.cpp
- 3.rFifo.cpp
- 六、关于匿名管道与命名管道的细节问题
- 匿名管道
- 命名管道
一、进程间通信的目的:
在学习通信间进程前我们要明白进程间通信的目的意义是是什么,从而方便我们在学习的过程中对我们所学的知识有一个清晰的认知
1.传输数据
将一个进程的数据发送给另一个进程
2.资源共享
让多个进程共享所需要的资源
3.通知事件
一个进程向另一个进程发送消息,通知它发生了某种事件(如进程终止时要通知父进程)
4.进程控制
有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变
二、进程间通信的种类
1.管道
- 匿名管道
- 命名管道
2.System V
- System V 消息队列
- System V 共享内存
- System V 信号量
3.POSIX
- 消息队列
- 共享内存
- 信号量
- 互斥锁
- 条件变量
- 读写锁
在本章中主要讲解管道通信
三、什么是管道
- 管道是操作系统中最古老的进程间通信的方式
- 我们把一个进程连接到另一个进程的一个数据流称为一个“管道”
四、匿名管道(共同祖先的进程之间)
1.匿名管道的使用
#include <unistd.h>
int pipe(int fd[2]);
匿名管道的功能是创建一个无名管道
- fd:文件描述符数组,其中fd[0]表示读端,fd[1]表示写端
- 返回值:成功返回0,失败返回错误代码
如果上述看的不太明白的话可以去看看这篇文章,里面有对文件描述符及数组的详细讲解
https://blog.csdn.net/liuty0125/article/details/140828056?spm=1001.2014.3001.5502
2.匿名管道举例
1.先创建管道, 进而创建子进程, 父子进程使用管道进行通信
父进程向管道当中写“i am father”,
子进程从管道当中读出内容, 并且打印到标准输出
#include<unistd.h>
#include<sys/types.h>
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<string>
using namespace std;
int main()
{int fd[2];int f=pipe(fd);char buf[100];const char* fa="I am father";int len=strlen(fa);if(f==-1){perror("make pipe");exit(1);}int pid = fork();if(pid==-1){perror("fork failed");return 1;}if(pid>0)//父进程{write(fd[1],fa,len);}if(pid==0){read(fd[0],buf,len);printf("%s",buf);}return 0;
}
2.从键盘读取数据,写入管道,读取管道,写到屏幕
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include<sys/types.h>
#include<string>
int main(void)
{int fds[2];char buf[100];int len;if (pipe(fds) == -1)perror("make pipe"), exit(1);// read from stdinwhile (fgets(buf, 100, stdin)) {len = strlen(buf);// write into pipeif (write(fds[1], buf, len) != len) {perror("write to pipe");break;}memset(buf, 0x00, sizeof(buf));// read from pipeif ((len = read(fds[0], buf, 100)) == -1) {perror("read from pipe");break;}// write to stdoutif (write(1, buf, len) != len) {perror("write to stdout");break;}}
}
3.匿名管道的原理
我们知道,进程将一个文件以读方式打开一次会产生一个FILE*,再以写方式打开一次,也会产生一个FILE*,但是这两个文件流指针指向的是同一个缓存区
内容只有一份基于文件的,让不同进程看到同一份资源的通信方式,叫做管道,管道只能被设计成为单向通信
如:ajx | ps | … 这中间的管道是兄弟关系
我们可以让父子进程都打开这个文件,再让父进程关闭读端,子进程关闭写端,这样就形成了一条由父进程通向子进程的管道,反之则形成由子进程通向父进程的管道
在这个地方其实有个bug:
通过父进程创建多个子进程,用管道文件连接父进程与各个子进程,产生的管道文件有多个读端,因为每个子进程的文件描述符表都会和创建他的父进程的文件描述符表一样,父进程的读端没有关闭,所以与接连创建的各个子进程的管道会带有上个父进程的读端
解决方案1:最后一个管道一定只有一个读写端,可以从后往前删
解决方案2:每次创建管道文件时,记录父进程的fd,然后每次创建时通过记录的上个父进程的文件描述符关闭不需要的fd
4.管道特点
- 匿名管道只能用于共同祖先的进程之间进行通信,一般都是一个管道由一个进程创建,然后该进程调用fork,此后父子进程之间就可以应用改管道
- 一般而言,匿名管道的生命周期随进程,进程退出,管道释放
- 管道是半双工的,数据只能向一个方向流动,需要双方通信时,则需要建立起两个管道
5.管道的读写规则
管道读写操作的行为会根据是否设置了非阻塞标志(O_NONBLOCK)以及管道的状态(是否有数据可读、是否已满等)有所不同
1. 当管道内没有数据可读时
- O_NONBLOCK 未设置(阻塞模式):
read调用阻塞,即进程暂停执行,一直等到有数据来到为止
- O_NONBLOCK 已设置(非阻塞模式):
read调用返回-1,errno值为EAGAIN
2.当管道满的时候
- O_NONBLOCK 未设置(阻塞模式):
write调用阻塞,直到有进程读走数据
- O_NONBLOCK 已设置(非阻塞模式):
调用返回-1,errno值为EAGAIN
3.管道端被关闭
- 所有管道的写端对应的文件描述符被关闭:
read 调用会返回 0,表示已经到达文件末尾(EOF),没有更多的数据可以读取
- 所有管道的读端对应的文件描述符被关闭:
write 调用会产生 SIGPIPE 信号,默认情况下,这会导致进程终止。可以通过捕获 SIGPIPE 信号来避免进程终止
4.数据写入的原子性
- 当要写入的数据量不大于 PIPE_BUF 时:
Linux 保证写入的原子性,即要么全部写入成功,要么完全不写入。这有助于避免数据在管道中被分割成多个部分,从而保证了数据的完整性。
- 当要写入的数据量大于 PIPE_BUF 时:
Linux 不再保证写入的原子性,数据可能会被分割成多个部分写入管道。这可能导致读端在读取数据时,需要多次调用 read 才能获取完整的数据。
五、命名管道(让两个不相干的进程通信)
1.命名管道的概念与原理
学习了上面的匿名管道,我们知道匿名管道其实是有缺陷的,它只能在具有共同祖先(具有亲缘关系)的进程间通信,那如果我们想在不相关的进程之间交换数据呢?
想要在两个不相干进程之间进行通信,我们的目的是让不同进程看到同一份资源
比如进程A通过文件描述符表打开一个文件,会为文件创建一个struct FILE,并通过文件缓冲区与磁盘进行交互
进程B也通过文件描述符打开同一个文件,也会为文件再创建一个struct FILE,但会使用同一个缓冲区,指向同一个内存空间,因此我们就可以通过这段内存空间进行通信
不过我们只是需要两个进程之间进行通信,并不需要把打开的文件内容写到缓冲区中,因此这里的文件是一个特殊的文件,也就是我们下面说的命名管道
如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道
- 怎么保证两个进程打开的是同一个文件?
只需要保证文件的路径与文件名一样即可,因为路径具有唯一性且同一路径下不能有同名文件
2.命名管道的创建与删除
1.在命令行上创建
- 使用
mkfifo
filename
p开头为管道文件
2.在程序中创建
int mkfifo(const char *filename,mode_t mode);
int main(int argc, char *argv[])
{mkfifo("p2", 0644);return 0;
}
3.删除文件
使用unlink()删除文件,可以用于析构函数
unlink(filename);
也就是说,想让两个不相干的进程通信,只需通过mkfifo创建一个管道文件,一个进程写,一个进程读即可
在程序中,管道文件只能创建一个,一个进程创建后,另一个进程只需直接操作就行,不需要再创建管道文件
管道中的通信都是基于文件的,可以说管道通信是基于文件的二次创新
如果写端没打开,而去先打开读端,读端open时会阻塞,直到把写端打开,读端才会返回
3.匿名管道与命名管道的区别
- 匿名管道由pipe函数创建并打开
- 命名管道由mkfifo函数创建,打开用open
- FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在它们创建与打开的方式不同,一但这些工作完成之后,它们具有相同的语义
4.命名管道的打开规则
- 为读而打开FIFO
O_NONBLOCK 未设置(阻塞模式):
如果当前没有进程为写而打开该FIFO,则打开操作会阻塞,直到有进程以写模式打开它。这意味着,如果FIFO是空的,并且没有写端打开,那么尝试以读模式打开FIFO的进程将被挂起,直到另一个进程以写模式打开它
O_NONBLOCK 已设置(非阻塞模式):
如果当前没有进程为写而打开该FIFO,则打开操作会立即成功,但是后续的read操作可能会失败,并返回-1,同时设置errno为EAGAIN(表示资源暂时不可用,可以在以后重试)。
- 为写而打开FIFO
O_NONBLOCK 未设置(阻塞模式):
如果当前没有进程为读而打开该FIFO,则打开操作会阻塞,直到有进程以读模式打开它。这意味着,如果FIFO没有读端,那么尝试以写模式打开FIFO的进程将被挂起,直到另一个进程以读模式打开它
O_NONBLOCK 已设置(非阻塞模式):
如果当前没有进程为读而打开该FIFO,则打开操作会立即失败,并返回-1,同时设置errno为ENXIO(表示设备不存在或没有这样的设备文件)。
在非阻塞模式下,如果没有读端可用,写端不能立即打开FIFO。
5.命名管道举例(附完整代码)
创建出来的命名管道可以供两个进程通信
进程A 向管道当中写 “i am process A”
进程B 从管道当中读 并且打印到标准输出
1.makefile文件
.PHONY:all
all:w rw:wFifo.cppg++ -o $@ $^ -std=c++11r:rFifo.cppg++ -o $@ $^ -std=c++11
PHONY:clean
clean:rm -f r w
2.wFifo.cpp
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <string>
#include <string.h>
#include <errno.h>
#include <strings.h>
#include <stdio.h>
#include<unistd.h>void errS(const char* s)
{perror(s);exit(EXIT_FAILURE);
}int main()
{const char* s="/home/xiaoliu/code/Fifo/__fifo";char a[1024]="i am process A";mkfifo(s,0666);int wfd=open(s,O_WRONLY);if(wfd < 0){errS("open");}int w=write(wfd,a,sizeof(a-1));if(w<0){errS("write");}return 0;
}
3.rFifo.cpp
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <string>
#include <string.h>
#include <errno.h>
#include <strings.h>
#include <stdio.h>
#include<unistd.h>
#include <iostream>void errS(const char* s)
{perror(s);exit(EXIT_FAILURE);
}int main()
{const char* s="/home/xiaoliu/code/Fifo/__fifo";char buf[1024];int rfd=open(s,O_RDONLY);if(rfd<0){errS("open");}int n = read(rfd,buf,sizeof(buf)-1);if(n>0){buf[n]='\0';std::cout<<"A said: "<<buf;}else if(n==0){std::cout<<"A didn't speak";}else{std::cerr << "read failed, errno: " << errno << ", errstring: " << strerror(errno) << std::endl;}return 0;
}
六、关于匿名管道与命名管道的细节问题
匿名管道
- 进程之间可以通过地址访问进行相互通行吗?
错误,进程之间具有独立性,拥有自己的虚拟地址空间,因此无法通过各自的虚拟地址进行通信(A的地址经过B的页表映射不一定映射在什么位置)
- 所有的进程间通信都是通过内核中的缓冲区实现的吗?
错误,除了内核中的缓冲区之外还有文件以及网络通信的方式可以实现
- 管道的容量仅受磁盘容量大小限制吗?
错误,管道的本质是内核中的缓冲区,通过内核缓冲区实现通信,命名管道的文件虽然可见于文件系统,但是只是标识符,并非通信介质
- 一个管道只能有一个读进程或写进程对其操作吗?
错误,多个进程只要能够访问同一管道就可以实现通信,不限于读写个数
- 程对管道进行读操作和写操作都可能被阻塞吗?
正确,管道自带同步(没有数据读阻塞,缓冲区写满写阻塞)与互斥
- 管道的本质是内核中的一块缓冲区?
正确,管道本质是内核中的一块缓冲区,多个进程通过访问同一块缓冲区实现通信
命名管道
- .命名管道可以用于同一主机上的任意进程间通信?
正确,命名管道可用于同一主机上的任意进程间通信
- 向命名管道中写入的数据越多,则管道文件越大?
错误,管道的通信本质是通过内核中一块缓冲区(内存)时间数据传输,而命名管道的管道文件只是一个标识符,用于让多个进程能够访问同一块缓冲区,不是磁盘文件
- 多个进程在通过管道通信时,删除管道文件则无法继续通信?
错误,管道的生命周期随进程,本质是内核中的缓冲区,命名管道文件只是标识,用于让多个进程找到同一块缓冲区,删除后,之前已经打开管道的进程依然可以通信
- 命名管道的本质和匿名管道的本质相同都是内核中的一块缓冲区?
正确
- .命名管道在磁盘空间足够的情况下可以持续写入数据?
错误,管道在缓冲区写满后会写阻塞,跟磁盘空间并无关系
- 若以只读或只写的方式打开命名管道时,则会阻塞?
正确,当一个进程尝试以只读方式打开命名管道时,如果该管道尚未被任何进程以写方式打开,则打开操作会阻塞,阻塞将持续到另一个进程以写方式打开该管道为止。此时,读进程可以继续进行,并尝试从管道中读取数据。反之亦然
相关文章:

【Linux操作系统】进程间通信之匿名管道与命名管道
目录 一、进程间通信的目的:二、进程间通信的种类三、什么是管道四、匿名管道(共同祖先的进程之间)1.匿名管道的使用2.匿名管道举例3.匿名管道的原理4.管道特点5.管道的读写规则1. 当管道内没有数据可读时2.当管道满的时候3.管道端被关闭4.数…...

慢sql优化和Explain解析
要想程序跑的快,sql优化不可懈怠!今日来总结一下常用的慢sql的分析和优化的方法。 1、慢sql的执行分析: 大家都知道分析一个sql语句执行效率的方法是用explain关键词: 举例:sql:select * from test where bussiness_…...

ALIGN_ Tuning Multi-mode Token-level Prompt Alignment across Modalities
文章汇总 当前的问题 目前的工作集中于单模提示发现,即一种模态只有一个提示,这可能不足以代表一个类[17]。这个问题在多模态提示学习中更为严重,因为视觉和文本概念及其对齐都需要推断。此外,仅用全局特征来表示图像和标记是不…...
【Java SE】代码注释
代码注释 注释(comment)是用于说明解释程序的文字,注释的作用在于提高代码的阅读性(可读性)。Java中的注释类型包括3种,分别是: 单行注释多行注释文档注释 ❤️ 单行注释 基本格式ÿ…...

如何在算家云搭建Llama3-Factory(智能对话)
一、Llama3-Factory 简介 当地时间 4 月 18 日,Meta 在官网上宣布公布了旗下最新大模型 Llama 3。目前,Llama 3 已经开放了 80 亿(8B)和 700 亿(70B)两个小参数版本,上下文窗口为 8k。Llama3 是…...

操作数据表
创建表 创建表语法: CREATE TABLE table_name ( field1 datatype [COMMENT 注释内容], field2 datatype [COMMENT 注释内容], field3 datatype ); 注意: 1. 蓝色字体为关键字 2. CREATE TABLE 是创建数据表的固定关键字,表…...
C# 实现进程间通信的几种方式(完善)
目录 引言 一、基本概念 二、常见的IPC方法 1. 管道(Pipes) 2. 共享内存(Shared Memory) 3. 消息队列(Message Queues) 4. 套接字(Sockets) 5. 信号量(Semaphore…...
MySQL Workbench Data Import Wizard:list index out of range
MySQL Workbench的Data Import Wizard功能是用python实现的,MySQL Workbench自带了一个python,数据导入的时候出现错误提示 22:55:51 [ERR][ pymforms]: Unhandled exception in Python code: Traceback (most recent call last): File "D…...

微信支付宝小程序SEO优化的四大策略
在竞争激烈的小程序市场中,高搜索排名意味着更多的曝光机会和潜在用户。SEO即搜索引擎优化,对于小程序而言,主要指的是在微信小程序商店中提高搜索排名,从而增加曝光度和用户访问量。有助于小程序脱颖而出,提升品牌知名…...

AutoDIR: Automatic All-in-One Image Restoration with Latent Diffusion论文阅读笔记
AutoDIR: Automatic All-in-One Image Restoration with Latent Diffusion 论文阅读笔记 这是ECCV2024的论文,作者单位是是港中文和上海AI Lab 文章提出了一个叫AutoDIR的方法,包括两个关键阶段,一个是BIQA,基于vision-language…...
SQLite 数据库设计最佳实践
SQLite特点 SQLite是一款功能强大的 轻量级嵌入式数据库 ,具有以下显著特点: 体积小 :最低配置仅需几百KB内存,适用于资源受限环境。 高性能 :访问速度快,运行效率高于许多开源数据库。 高度可移植 :兼容多种硬件和软件平台。 零配置 :无需复杂设置,开箱即用。 自给自…...

【论文精读】ID-like Prompt Learning for Few-Shot Out-of-Distribution Detection
🌈 个人主页:十二月的猫-CSDN博客 🔥 系列专栏: 🏀论文精读_十二月的猫的博客-CSDN博客 💪🏻 十二月的寒冬阻挡不了春天的脚步,十二点的黑夜遮蔽不住黎明的曙光 注:下文…...
Android 10.0 根据包名禁用某个app的home事件
1.前言 在10.0的系统rom定制化开发中,在某些app中,需要禁用home事件,在普通的app中又无法 禁用home事件,所以就需要从系统中来根据包名禁用home事件了,接下来分析下 系统中处理home事件的相关流程 2.根据包名禁用某个app的home事件的核心类 frameworks/base/services/c…...
Rust 文档生成与发布
目录 第三节 文档生成与发布 1. 使用 RustDoc 生成项目文档 1.1 RustDoc 的基本使用 1.2 文档注释的格式与实践 1.3 生成文档的其他选项 1.4 在 CI/CD 中生成文档 2. 发布到 crates.io 的步骤与注意事项 2.1 创建 crates.io 账户 2.2 配置 Cargo.toml 2.3 生成发布版…...

【C++动态规划】有效括号的嵌套深度
本文涉及知识点 C动态规划 LeetCode1111. 有效括号的嵌套深度 有效括号字符串 定义:对于每个左括号,都能找到与之对应的右括号,反之亦然。详情参见题末「有效括号字符串」部分。 嵌套深度 depth 定义:即有效括号字符串嵌套的层…...

2024年优秀的天气预测API
准确、可操作的天气预报对于许多组织的成功至关重要。 事实上,在整个行业中,天气条件会直接影响日常运营,包括航运、按需、能源和供应链(仅举几例)。 以公用事业为例。根据麦肯锡的数据,在 1.4 年的时间里…...
Android和iOS有什么区别?
Android 和 iOS 有以下区别: 开发者与所属公司: Android:由谷歌公司开发以及开放手机联盟维护。它是基于 Linux 内核和其他开源软件的修改版本,代码开源程度较高,许多厂商都可以基于 Android 源代码进行深度定制和开发…...

NVR小程序接入平台/设备EasyNVR多个NVR同时管理多平台级联与上下级对接的高效应用
政务数据共享平台的建设正致力于消除“信息孤岛”现象,打破“数据烟囱”,实现国家、省、市及区县数据的全面对接与共享。省市平台的“级联对接”工作由多级平台共同构成,旨在满足跨部门、跨层级及跨省数据共享的需求,推动数据流通…...

Spring Cloud Sleuth(Micrometer Tracing +Zipkin)
分布式链路追踪 分布式链路追踪技术要解决的问题,分布式链路追踪(Distributed Tracing),就是将一次分布式请求还原成调用链路,进行日志记录,性能监控并将一次分布式请求的调用情况集中展示。比如各个服务节…...
人工智能:机遇与挑战
人工智能(AI)作为当今世界科技发展的前沿领域,正在以前所未有的速度和规模影响着我们的生活和工作方式。AI技术的应用前景广阔,从医疗健康到金融服务,从教育到交通,再到娱乐和家庭生活,AI正在逐…...
[特殊字符] 智能合约中的数据是如何在区块链中保持一致的?
🧠 智能合约中的数据是如何在区块链中保持一致的? 为什么所有区块链节点都能得出相同结果?合约调用这么复杂,状态真能保持一致吗?本篇带你从底层视角理解“状态一致性”的真相。 一、智能合约的数据存储在哪里…...

VB.net复制Ntag213卡写入UID
本示例使用的发卡器:https://item.taobao.com/item.htm?ftt&id615391857885 一、读取旧Ntag卡的UID和数据 Private Sub Button15_Click(sender As Object, e As EventArgs) Handles Button15.Click轻松读卡技术支持:网站:Dim i, j As IntegerDim cardidhex, …...

微软PowerBI考试 PL300-选择 Power BI 模型框架【附练习数据】
微软PowerBI考试 PL300-选择 Power BI 模型框架 20 多年来,Microsoft 持续对企业商业智能 (BI) 进行大量投资。 Azure Analysis Services (AAS) 和 SQL Server Analysis Services (SSAS) 基于无数企业使用的成熟的 BI 数据建模技术。 同样的技术也是 Power BI 数据…...
在鸿蒙HarmonyOS 5中使用DevEco Studio实现录音机应用
1. 项目配置与权限设置 1.1 配置module.json5 {"module": {"requestPermissions": [{"name": "ohos.permission.MICROPHONE","reason": "录音需要麦克风权限"},{"name": "ohos.permission.WRITE…...
高防服务器能够抵御哪些网络攻击呢?
高防服务器作为一种有着高度防御能力的服务器,可以帮助网站应对分布式拒绝服务攻击,有效识别和清理一些恶意的网络流量,为用户提供安全且稳定的网络环境,那么,高防服务器一般都可以抵御哪些网络攻击呢?下面…...
【学习笔记】深入理解Java虚拟机学习笔记——第4章 虚拟机性能监控,故障处理工具
第2章 虚拟机性能监控,故障处理工具 4.1 概述 略 4.2 基础故障处理工具 4.2.1 jps:虚拟机进程状况工具 命令:jps [options] [hostid] 功能:本地虚拟机进程显示进程ID(与ps相同),可同时显示主类&#x…...
全面解析各类VPN技术:GRE、IPsec、L2TP、SSL与MPLS VPN对比
目录 引言 VPN技术概述 GRE VPN 3.1 GRE封装结构 3.2 GRE的应用场景 GRE over IPsec 4.1 GRE over IPsec封装结构 4.2 为什么使用GRE over IPsec? IPsec VPN 5.1 IPsec传输模式(Transport Mode) 5.2 IPsec隧道模式(Tunne…...
智能AI电话机器人系统的识别能力现状与发展水平
一、引言 随着人工智能技术的飞速发展,AI电话机器人系统已经从简单的自动应答工具演变为具备复杂交互能力的智能助手。这类系统结合了语音识别、自然语言处理、情感计算和机器学习等多项前沿技术,在客户服务、营销推广、信息查询等领域发挥着越来越重要…...

打手机检测算法AI智能分析网关V4守护公共/工业/医疗等多场景安全应用
一、方案背景 在现代生产与生活场景中,如工厂高危作业区、医院手术室、公共场景等,人员违规打手机的行为潜藏着巨大风险。传统依靠人工巡查的监管方式,存在效率低、覆盖面不足、判断主观性强等问题,难以满足对人员打手机行为精…...
探索Selenium:自动化测试的神奇钥匙
目录 一、Selenium 是什么1.1 定义与概念1.2 发展历程1.3 功能概述 二、Selenium 工作原理剖析2.1 架构组成2.2 工作流程2.3 通信机制 三、Selenium 的优势3.1 跨浏览器与平台支持3.2 丰富的语言支持3.3 强大的社区支持 四、Selenium 的应用场景4.1 Web 应用自动化测试4.2 数据…...