当前位置: 首页 > news >正文

Linux | 进程间通信

目录

前言

一、进程间通信的基本概念

二、管道

1、管道的基本概念

2、匿名管道

(1)原理

(2)测试代码

(3)读写控制相关问题

a、读端关闭

b、写端关闭

c、读快写慢

d、读慢些快

(4)进程池代码拓展

3、命名管道

(1)原理 

(2)测试代码

三、共享内存

1、共享内存的原理

2、测试代码

3、共享内存的特性


前言

        进程间通信的方式有很多,如管道、共享内存、消息队列、信号量、socket套接字等等;本文主要讲解其中管道和共享内存两种方式;

一、进程间通信的基本概念

        我们要理解进程间通信呢首先得知道以下三个问题;

进程间通信是什么?

我们为什么要进程间通信?

怎么进行进程间通信?

        对于所有知识,我们拥有上述三个问题得答案我们就可彻底弄清进程间通信得本质了;

进程间通信是什么?

        进程间通信就是让两个进程,看到同一块空间(内存),以达到我们通过这块共同空间来进程交互得过程;

        我们都知道,我们可以通过fork创建子进程,fork创建后得子进程与父进程共享同一块代码;那么我们是否可以通过创建一个创建变量的资源缓冲区来使这两个父子进程看到同一块空间(内存)呢?很不幸,由于我们进程间的独立性,所以我们无法通过全局变量来让两个进程看到同一块空间,并且使用这个空间进行通信,准确来说是可以看到,但是不能进行通信,因为当我们父进程或子进程对这块空间内容进行修改时会发生写时拷贝;这也是保证进程间独立性而产生的机制;也正是由于进程具有独立性,所以我们的进程间通信便没有那么容易;

为什么要进行进程间通信?

        在实际开发中,我们可能会有并发编程的需求,而我们的单进程是不具备并发能力的,而有时我们的并发之间的多进程需要一起协同配合,既然需要协同配合就可能需要进行通信,因此我们需要进程间通信;

怎么进行进程间通信?

        关于如何进行进程间通信,我们主要有以下几种策略,分别来自于不同的方;

Linux原生方案:管道(匿名管道、命名管道)

System V方案:共享内存、消息队列(不常用)、信号量;

POSIX 方案:socket套接字

        上述为一些主流方案,本文主要讲解 管道方案 与 System V 提供的共享内存方案;

二、管道

1、管道的基本概念

        首先管道我们在前面学习指令的时候就已经接触过了,只不过我们对其了解并不深刻;当时我们只知道我们可以通过管道将一个指令(进程)的输出结果传输给另一个指令(进程);

1、管道是一个信息传输的媒介!如上图所示,进程A将信息写入管道,而进程B从管道中读取数据;

2、管道是一个文件;我们之前提过,Linux下,一切皆文件的理念,那么我们的管道也应该是一个文件,只不过这个文件是属于内存级文件,不会将数据刷新到缓冲区中,也没有必要刷新到缓冲区中做持久化的动作;

3、管道的文件的通信方式为半双工通信;关于这我们需要补充以下几个概念;

单工通信:只有一个方向的通信,且只有固定的一端作为接收端,一端作为输入端;

半双工通信:通信的双方都可以作为接收端也可以作为输入端,当某一时刻,只能有一个端发送,一端接收;就好像两个人聊天,你说一句,我接收后,再回复一句,你再接收;

全双工通信:通信的双方既可以作为接收端也可以作为输入端,某一个时刻,既可以输入也可以接收;就好像两个人吵架,每个人都可以挺别人说话的同时对别人说话;

2、匿名管道

(1)原理

        首先,我们来介绍匿名管道实现进程间通信的原理,再使用匿名管道作为进程间通信的方法之前,我们得确保通信双方进程为父子关系;这是使用匿名管道实现进程间通信得基本前提;

        前面我们讲过进程的相关内核数据,与我们创建一个进程会发生什么?以及当我们调用fork会发生什么?有了上述知识铺垫,我们不难想到,当我们使用fork创建子进程时,我们的子进程会创建自己的内核数据,如PCB控制块,虚拟地址空间、页表、文件描述符结构体等内核数据,其中我们还讲过子进程会拷贝父进程内核数据中的某些数据,当我们对这些数据需要进行修改时会发生写时拷贝的现象;那么问题来了,我们的维护当前进程打开的文件的结构体 files_struct 是否也会发生拷贝呢?当然,这是肯定的?因此,我们使用fork时,应该如下图所示;

        我们匿名管道实现进程间通信就是基于这一特性---- “子进程会继承父进程的文件描述符数组”;因此我们不妨首先创建一个管道文件,然后父进程分别以读和写的方式打开这个管道文件,接着我们创建子进程,子进程必然会对文件描述符数组的内容进行拷贝,子进程也拥有对该文件读和写这两个文件描述符,此时若我们想让父进程写,子进程读,我们只需要将父进程的读文件描述符关闭,子进程写文件描述符关闭,然后我们再调用系统调用read和write,分别向对应文件描述符读和写即可!这就是我们使用匿名管道的方式实现进程间通信的过程;

(2)测试代码

        在正是实现代码之前,我们首先介绍几个与管道相关的接口;

pipe:创建匿名管道

参数:该函数只有一个参数,是一个整型数组,当我们调用该函数时,该函数会为我们创建一个匿名管道文件,并打开这个管道文件其中数组的 0 号下标放的是以读的方式打开该文件的文件描述符,1 号下标方式的以写的方式打开该管道文件的文件描述符;(记忆:0想象成嘴巴,代表读,1想象成笔,代表写);

返回值:若函数调用成功,返回0,调用失败,返回-1,错误码被设置;

        有了上面函数的学习,我们就可以写一段简单的基于匿名管道的进程间通信代码了;如下所示;

#include <iostream>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>int main()
{// 1、创建管道int pipefd[2] = {0};int n = pipe(pipefd);if(n == -1){perror("pipe");exit(1);}// 2、创建子进程pid_t id = fork();if(id == 0){// 子进程(读)// 3、建立单向信道// 关闭子进程写端文件描述符close(pipefd[1]);// 4、进行通信char buf[1024];  // 接收缓冲区while(true){// 读取来自父进程的信息ssize_t sz = read(pipefd[0], buf, sizeof(buf));// 这里就不对read进行差错检验了buf[sz] = '\0';   // 我们想要读的是字符串,因此在语言层面得加 \0if(sz > 0){std::cout << "读数据前\n";std::cout << "child[" << getpid() << "], father: " << buf << std::endl;}else if(sz == 0){std::cout << "father stop write, me quit" << std::endl;break;}sleep(1);}exit(0);}// 父进程(写)// 3、建立单向信道close(pipefd[0]); // 关闭读端// 4、进行通信const char* msg = "这是发给子进程的消息 ";char tmp[1024] = {0};int count = 0;while(true){memset(tmp, 0, sizeof(tmp));snprintf(tmp, sizeof(tmp), "%s[%d], %d\n", msg, getpid(), count++);ssize_t sz = write(pipefd[1], tmp, strlen(tmp));sleep(1);}// 5、回收子进程waitpid(id, nullptr, 0);// 6、关闭文件描述符(可关可不关,因为程序快运行结束,OS会自动释放)close(pipefd[1]);return 0;

        上述代码是实现了一个父进程写,子进程不断的读的功能;

(3)读写控制相关问题

        基于上述代码,我们还要进行更深层次的研究;我们分别测试以下几种情况下会发生什么?

a、读端关闭

        我们给上述代码读端设置一个计数器,设置成5秒后,读端退出;观察会发生什么;

 

         我们输入命令行监视脚本指令;如下所示;

while :; do ps -axj | head -1 && ps -axj | grep ./main | grep -v grep; sleep 1; echo "--------------------";done

 

        我们发现,当我们的读端关闭时,写端进程被终止了!也就是被杀掉了!由于上述我们让子进程关闭读端文件描述符后休眠5秒,因此我们的子进程没有退出;我们不难得出结论;

结论:读端关闭时,写端进程被操作系统杀死; 

b、写端关闭

        同样的道理,我们使用计数器的方式,使写端到一定的时间后关闭,我们观察读端会如何;

 

        运行结果如下所示;

        仔细观察,打印了我们在read返回值为0的输出内容;

结论:当我们写端关闭时,读端会读到文件的末尾,返回0;

c、读快写慢

        我们将上述代码更改一下;将我们的读端设置为一秒读一次,将我们的写端设置为三秒写一次;再运行代码,看一看会发生什么;

        我们会发现,我们的进程三秒才会打印一次;我们的读进程不是一秒读一次吗?那我们的读进程在干嘛呢?

结论:当读快写慢时,读进程会阻塞等待写进程进行写入;

d、读慢些快

        我们将上述代码改一下,读进程三秒读一次,而写进程一秒写一次;又会发生什么呢?

        我们发现第一次立刻打印了,后面三秒打印一次,且每次都打印了三条内容;这时结果也显而易见了;

结论:读慢写快时,我们的写进程会一直往管道文件里写,直至写满,写满后,写进程会阻塞,直至下一次读进程读取管道文件时,写进程才会被唤醒;

总结:

        综上所述,我们使用匿名管道进行进程间通信的本质是我们通过子进程会继承父进程文件描述符数组的特性,使我们的父进程和子进程看到同一个文件,它们可以通过该文件进行通信;在我看来进程间通信的本质是让两个进程看到同一块空间的方式!而通信是根据上层定义的;

        匿名管道提供了访问控制,所谓访问控制,就是我们上述讨论a、b、c、d四种不同的方式;并不是所有进程间通信具有访问控制,如我们后面讲的共享内存;

        匿名管道的生命周期是随着进程的,一旦进程结束,我们的匿名管道文件也随之销毁;

(4)进程池代码拓展

        通过上述知识,我们可以实现一个简单的进程池代码;

        具体代码如下所示;

// task.h文件,主要封装任务和任务管理器
#pragma once
#include <iostream>
#include <vector>
#include <string>
#include <map>
#include <functional>
#include <mutex>// 定义函数对象类型
using func_t = std::function<void()>;void webRequest()
{std::cout << "正在执行网络请求任务" << std::endl;
}void readMySQL()
{std::cout << "正在执行读取数据库任务" << std::endl;
}void otherTask()
{std::cout << "正在执行其他任务" << std::endl;
}// 设计一个单例类,管理所有任务
class ManageTask
{
public:static ManageTask* getInstance(){// 双检查加锁if(_pm == nullptr)   // 提高效率{_m.lock();if(_pm == nullptr) // 保证线程安全{_pm = new ManageTask();}_m.unlock();}return _pm;}// 加载类内成员void load(){// 增加任务描述与命令号的映射_dict.insert({_tasks.size(), "web请求"});// 增加命令_tasks.push_back(webRequest);_dict.insert({_tasks.size(), "读取数据库"});_tasks.push_back(readMySQL);_dict.insert({_tasks.size(), "其它任务"});_tasks.push_back(otherTask);}// 自定义新增任务void add(func_t& cb, std::string& str){_dict.insert({_tasks.size(), str});_tasks.push_back(cb);}// 自定义删除任务(任务号)void del(int command){_dict.erase(command);_tasks.erase(_tasks.begin() + command);}// 展示当前任务void show(){for(auto e : _dict){std::cout << e.first << ": " << e.second << std::endl;}}// 获取任务func_t get_task(int command){return _tasks[command];}// 获取任务个数size_t get_size(){return _tasks.size();}// 获取命令描述std::string get_command(int command){return _dict[command];}
private:ManageTask(){}ManageTask(ManageTask& m){}ManageTask operator=(ManageTask& m){}
private:static ManageTask* _pm;static std::mutex _m;std::vector<func_t> _tasks;std::map<int, std::string> _dict;
};// 初始化单例对象指针和锁
ManageTask* ManageTask::_pm = nullptr;
std::mutex ManageTask::_m;
// main.cc文件,主要实现线程池,以及派发任务等逻辑
#include <iostream>
#include <vector>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include "task.hpp"#define NUM 6int main()
{srand(time(nullptr)); // 种下随机数种子ManageTask::getInstance()->load();  // 初始化任务表// 1、创建进程池std::vector<std::pair<pid_t, int>> desc; // 子进程pid与写端fdfor(int i = 0; i < NUM; i++){// 1.1 创建管道文件int pipefd[2] = {0};int n = pipe(pipefd);if(n == -1) {std::cerr << "create pipe fail" << std::endl;exit(1);}// 1.2 创建子进程int id = fork();if(id == 0){usleep(100);// 子进程(读)// 1.3 关闭管道写端close(pipefd[1]);// 1.4 进行读取数据int command = 0;while(true){ssize_t sz = read(pipefd[0], &command, sizeof(command));if(sz != sizeof(int) || command < 0 || command >= ManageTask::getInstance()->get_size()){std::cerr << "读取无效命令" << std::endl;continue;}// 1.5执行命令对应任务ManageTask::getInstance()->get_task(command)();}exit(0);}// 父进程(写)// 1.3 关闭对应读端,并将写端保存起来close(pipefd[0]);desc.push_back(std::pair<pid_t, int>(id, pipefd[1]));}// 2、使用进程池// 2.1 获取菜单while(true){int select = 0;int command = 0;std::cout << "******************************" << std::endl;std::cout << "******    1、showTask   ******" << std::endl;std::cout << "******    2、execute    ******" << std::endl;std::cout << "******************************" << std::endl;// 2.2 从用户获取选择std::cout << "Enter select> ";std::cin >> select;// 2.3 从用户获取命令选项if(select == 1){ManageTask::getInstance()->show();}else if(select == 2){// 2.4 获取命令std::cout << "Enter command> ";std::cin >> command;// 2.5 选择一个子进程执行(随机数实现负载均衡)int proc = rand() % ManageTask::getInstance()->get_size();// 2.6 向指定进程发送命令write(desc[proc].second, &command, sizeof(command));std::cout << "已经成功给" << desc[proc].first << "进程,对应命令:" << ManageTask::getInstance()->get_command(command) << std::endl;}else{std::cerr << "选择有误,请重新选择" << std::endl;continue;}usleep(500);}// 3、关闭进程池// 关闭写端fdfor(int i = 0; i < desc.size(); i++){close(desc[i].second);}// 回收子进程for(int i = 0; i < desc.size(); i++){waitpid(desc[i].first, nullptr, 0);}return 0;
}

3、命名管道

(1)原理 

        命名管道的原理与匿名管道不同,命名管道则是通过自己创建一个管道文件,然后双方通过打开这个文件实现看到同一块内存资源的功能;命名管道的最大优势在于命名管道可以使不具有血缘关系的两个进程进行通信;原理非常简单,这里就不做过多解释,可通过后面代码来学习命名管道;

(2)测试代码

        在正是学习命名管道之前,我们同样先补充一批接口的使用;

        首先我们学习一个命令行指令,mkfifo,该指令加上文件名即可创建一个指定名字的管道文件;具体看如下演示;

mkfifo:创建一个管道文件(这是一个函数,与我们上述命令行指令重名而已)

参数一:这个参数为我们要创建管道文件的文件名(默认在当前目录下创建文件)

参数二:这个参数为我们创建管道文件的权限,这个权限会与我们默认权限掩码进行计算最终权限,计算规则(我们设置权限 & (~默认权限));

返回值:若调用成功则返回0,失败则返回-1,且错误码被设置;

unlink:删除一个文件,与我们命令行下的rm指令功能一致;

参数:要删除文件的路径

返回值:若调用成功,则返回0,失败则返回-1,错误码被设置;

        有了上述的知识铺垫,我们可以很容易的写出一个命名管道通信的程序,如下所示;

// comm.hpp文件主要存放公共代码,如管道文件名
#include <iostream>
#include <string>
#include <cstdlib>
#include <cstring>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>// 管道文件名
#define PipeName "FIFO.ipc"
// pipeServer.cc文件主要作为接收端,该端要创建管道文件,运行结束后要删除管道文件
#include "comm.hpp"int main()
{umask(0); // 设置创建文件临时的权限掩码// 1、创建管道文件int n = mkfifo(PipeName, 0666);if(n == -1){perror("mkfifo");exit(1);}// 2、打开管道文件int fd = open(PipeName, O_RDONLY, 0666);if(fd == -1){perror("open");exit(2);}// 3、通信char buf[1024];while(true){ssize_t sz = read(fd, buf, sizeof(buf) - 1);buf[sz] = '\0'; // 去掉默认换行if(strcmp(buf, "quit") == 0){std::cout << "client quit, me too" << std::endl;break;}std::cout << "client# " << buf << std::endl;}// 4、关闭管道文件close(fd);// 5、删除管道文件unlink(PipeName);return 0;
}
// pipeClient.cc文件作为通信的发送端实现,主要向服务端发送请求
#include "comm.hpp"int main()
{// 1、打开管道文件int fd = open(PipeName, O_WRONLY, 0666);if(fd == -1){perror("open");exit(2);}// 2、通信std::string buf;while(true){std::getline(std::cin, buf);write(fd, buf.c_str(), buf.size());}// 3、关闭管道文件close(fd);return 0;
}

        我们发现,只要我们将管道文件创建好,其他的就像我们操作普通文件一样简单;命名管道的使用比匿名管道会容易很多;

注意:这里有一个细节,在我们使用管道文件时,若我们服务端使用open打开管道文件时,此时若我们的客户端没有调用open函数打开管道文件,我们的服务端会一只卡在open函数内,直至我们客户端也使用open函数打开管道文件,这时我们的服务端open函数才会返回文件描述符;这个细节在我们后面共享内存的一份代码中有一定重要的作用;

三、共享内存

1、共享内存的原理

        之前在讲解进程地址空间时,我们曾经讲过,堆栈中间有一块共享区,我们之前的动态库就是会被映射到这块共享区中;而今天我们的主角,共享内存的原理也与这块空间有关;进程间通信的本质就是看到同一块内存空间,而我们共享内存实现进程间通信的方案就是我们首先在内存中申请一块空间,然后将我们需要进程间通信的进程与这块物理空间进行关联,映射到自己进程地址空间的共享区中;这样便可以实现进程间通信了;如下图所示;

        这样进程间通信就只需要往自己的进程地址空间的某个位置写入读取即可;

2、测试代码

        在使用共享内存实现进程间通信前,我们需要学习下面几个函数接口;

shmget:申请一块共享内存空间并返回对应的shmid / 获取一块共享内存空间的shmid;

参数一:通过key值获取或者申请一块共享内存空间,每块共享内存空间的key值不同;

参数二:申请 / 获取共享内存空间的大小

参数三:标志位,通常是由 IPC_CREAT 或 IPC_EXCL 这两个宏加上权限组成;

返回值:若函数调用成功则返回 shmid,这个也就是共享内存的句柄,与fd类似;若调用失败,则返回-1,错误码被设置;

注意:

1、关于参数三,我们若想获取某个key对应的shmid,我们直接填0即可;若某个key对应的共享内存空间不存在,我们需要创建,则我们一般会填 IPC_CREAT | IPC_EXCL | 0666;其中0666为共享内存空间的访问权限;

IPC_CREAT:若共享内存空间不存在,则创建之;

IPC_EXCL:通常配合上面IPC_CREAT使用,若共享空间存在,则报错返回-1;因此这两个选项配合使用可以保证获得到的共享内存是一个新创建的共享内存!

2、关于上述的参数一key,我们可以通过下面这个函数来获取;

ftok:通过路径和项目id生成唯一key值;

参数一:项目路径,这里可以随便填写一个;

参数二:项目id,这里也可以自己设置一个;

返回值:若函数调用成功,则返回key值,若失败,则返回-1,错误码被设置;

shmat:是共享内存空间与当前虚拟地址空间进行相关联,建立映射关系;

参数一:shmid值,之前我们在shmget中获取的 id;

参数二:我们想要与哪一块虚拟地址空间绑定,建立映射关系;这里通常填NULL,表示让OS系统随机分配一块空间建立映射关系;

参数三:一般设置为0,可不关心;

返回值:若调用成功,则返回与共享内存建立映射的虚拟地址,若调用使用,则返回-1,错误码被设置,注意这里的-1被强制装换成了 void* 类型;

shmdt:将共享内存与我们的共享内存去关联,与我们的shmat相对应;

参数一:要去关联的共享内存的地址,也就是shmat的返回值;

返回值:若函数调用成功,则返回0,若失败,则返回-1,错误码被设置;

shmctrl:共享内存的控制

参数一:shmid值;

参数二:这里有三个宏来控制这个函数的行为,我们一般选择IPC_RMID,表示我们要删除这块共享内存空间;

参数三:填NULL即可;

返回值:若调用成功则返回0,若失败则返回-1,错误码被设置;

        学习上述的四个函数我们就可以完成进程间通信了,上面四个函数基本囊括了使用共享内存进行进程间通信的整个过程,首先调用ftok获取key值,然后再通过shmget 创建共享内存 / 获取shmid,然后通过shmat 与当前进程的虚拟地址空间进行关联,接着就可以开始进行通信了,通信完毕后,我们使用 shmdt 将虚拟地址与共享内存去关联;最后我们使用shmctl删掉申请的共享内存空间;

        接着我们需要学习一些命令行来获取共享内存相关信息,我们可以通过 ipcs -m 来获取当前机器的共享内存申请使用情况;使用 ipcrm -m shmid 来删除指定的共享内存;下面为我们使用共享内存进行进程间通信的代码;

// comm.hpp 文件用于保存一些共享代码#include <iostream>
#include <cstdlib>
#include <cstring>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/stat.h>
#include <sys/shm.h>
#include <unistd.h>// 形成key所需路径
#define pathName "/home/zsw/linuxCode/shm"
// 形成key所需项目id
#define proj_id 0x13
// 共享内存大小
#define SIZE 4096
// shmServer.cc 文件实现服务端接收客户端发送信息,其中服务端承担创建共享内存,删除共享内存的任务#include "comm.hpp"int main()
{// 1、生成唯一keyint k = ftok(pathName, proj_id);if(k == -1){perror("ftok");exit(1);}std::cout << "ftok success\n";// 2、创建新的共享空间int shmid = shmget(k, SIZE, IPC_CREAT | IPC_EXCL | 0666); // 创建一个新的共享内存,k存在,则创建失败if(shmid == -1){perror("shmget");exit(2);}std::cout << "shmget success\n";// 3、与共享空间相关联char* shmaddr = (char*)shmat(shmid, nullptr, 0);if(*(int*)shmaddr == -1){perror("shmat");// 退出前释放共享空间shmctl(shmid, IPC_RMID, nullptr);exit(3);}std::cout << "shmat success\n";// 4、通信(读)while(true){printf("%s\n", shmaddr);if(shmaddr[0] == 'z')break;sleep(1);}// 5、去关联int n = shmdt(shmaddr);if(n == -1){perror("shmdt");exit(4);}std::cout << "shmdt success\n";// 6、释放共享空间shmctl(shmid, IPC_RMID, nullptr);std::cout << "shm rm success\n";return 0;
}
// shmClient.cc 文件用于客户端发送信息给服务端,其中仅需对共享内存关联,通信、去关联等操作即可#include "comm.hpp"int main()
{// 1、获取唯一keyint k = ftok(pathName, proj_id);if(k == -1){perror("ftok");exit(1);}// 2、获取共享内存int shmid = shmget(k, SIZE, 0);if(shmid == -1){perror("shmget");exit(2);}// 3、关联char* shmaddr = (char*)shmat(shmid, nullptr, 0);if(*(int*)shmaddr == -1){perror("shmat");exit(3);}// 4、通信(写)char ch = 'a';while(ch <= 'z'){shmaddr[0] = ch;ch++;sleep(1);}// 5、去关联int n = shmdt(shmaddr);if(n == -1){perror("shmdt");exit(4);}return 0;
}

        我们编译运行服务端代码,如下所示;

        确实,我们的服务端一直读取数据,可是读取到的数据为空,此时我们并没有运行客户端,且客户端也不可能进行输出发送;可我们依旧可以读取数据,只不过数据为空罢了,这就与我们的管道通信有了本质的区别,共享内存的进程间通信并没有访问控制!

        接着我们通过ipcs -m 查找我们创建的共享内存,确实存在,其shmid为25,key就是我们通过 ftok 生成的key,owner就是共享内存的拥有者,也就是当前用户,perms就是这块共享内存的访问权限,我们设置成了 666,4096就是我们申请这块共享内存的大小,nattach就是关联到这块共享内存空间的进程数,status就是这块共享内存的状态;

        我们接着启动客户端;如下所示;

        我们发现服务端已经收到客户端发来的信息,并且我们的nattach的数量也由1变成了2,随后当客户端发送完26个英文字母后退出,此时服务端也读取到了字符z,两个进行相继推出,nattach也由2变成了0,共享内存被删除;

        前面的实验,我们也不难发现,我们的共享内存不具备访问控制,那我们要是想使用共享内存实现类似管道的访问控制是否可以做到呢?其实也不难,我们可以通过管道来实现,如下面的代码;

// comm.hpp 文件#include <iostream>
#include <string>
#include <cstdlib>
#include <cstring>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/stat.h>
#include <sys/shm.h>
#include <unistd.h>
#include <fcntl.h>// 形成key所需路径
#define pathName "/home/zsw/linuxCode/shm/shm_pipe"
// 形成key所需项目id
#define proj_id 0x14
// 共享内存大小
#define SIZE 4096
// 管道文件名
#define pipeName "./fifo.ipc"// 负责创建与销毁管道文件
class Init
{
public:Init(){umask(0);int n = mkfifo(pipeName, 0666);if(n == -1){perror("mkfifo");exit(5);}}~Init(){unlink(pipeName);}
};// 通过read阻塞来形成等待效果
void wait(int fd)
{int tmp = 1;ssize_t sz = read(fd, &tmp, sizeof(int));if(sz != sizeof(tmp)){std::cerr << "fd: " << fd << " 等待错误\n";printf("sz:%d, sizeof(tmp):%d\n", sz, sizeof(tmp));perror("wait");exit(6);}
}// 通过write唤醒等待进程
void signal(int fd)
{int tmp = 1;ssize_t sz = write(fd, &tmp, sizeof(tmp));if(sz != sizeof(int)){std::cerr << "write err\n";exit(7);}
}
// shmServer.cc#include "comm.hpp"int main()
{Init init; // 创建管道文件// 打开管道文件int fd = open(pipeName, O_RDONLY, 0666);// 1、生成唯一keyint k = ftok(pathName, proj_id);if(k == -1){perror("ftok");exit(1);}std::cout << "ftok success, k:" << k << std::endl;// 2、创建新的共享空间int shmid = shmget(k, SIZE, IPC_CREAT | IPC_EXCL | 0666); // 创建一个新的共享内存,k存在,则创建失败if(shmid == -1){perror("shmget");exit(2);}std::cout << "shmget success\n";// 3、与共享空间相关联char* shmaddr = (char*)shmat(shmid, nullptr, 0);if(*(int*)shmaddr == -1){perror("shmat");// 退出前释放共享空间shmctl(shmid, IPC_RMID, nullptr);exit(3);}std::cout << "shmat success\n";sleep(5);// 打开管道文件int fd = open(pipeName, O_RDONLY, 0666);// 4、通信(读)while(true){wait(fd);printf("%s\n", shmaddr);}// 5、去关联int n = shmdt(shmaddr);if(n == -1){perror("shmdt");exit(4);}std::cout << "shmdt success\n";// 6、释放共享空间shmctl(shmid, IPC_RMID, nullptr);std::cout << "shm rm success\n";return 0;
}
// shmClient.cc文件#include "comm.hpp"int main()
{// 小坑:不能在这打开管道文件,因为打开管道文件有一个特性,如果对端不打开,这里将会一直阻塞// 然后在后续过程中,若客户端进程先执行,下面shmget函数可能出现文件未创建的错误,因为客户端// 执行的比服务端快,服务端还未创建共享内存,而客户端就想访问了// int fd = open(pipeName, O_WRONLY, 0666);// 1、获取唯一keyint k = ftok(pathName, proj_id);if(k == -1){perror("ftok");exit(1);}std::cout << "ftok success, k:" << k << std::endl;// 2、获取共享内存int shmid = shmget(k, SIZE, 0);if(shmid == -1){perror("shmget");exit(2);}std::cout << "shmget success\n";// 打开管道文件int fd = open(pipeName, O_WRONLY, 0666);// 3、关联char* shmaddr = (char*)shmat(shmid, nullptr, 0);if(*(int*)shmaddr == -1){perror("shmat");exit(3);}std::cout << "shmat success\n";// 4、通信(写)std::string msg;while(true){std::cout << "Enter message> ";getline(std::cin, msg);strcpy(shmaddr, msg.c_str());signal(fd);}// 5、去关联int n = shmdt(shmaddr);if(n == -1){perror("shmdt");exit(4);}std::cout << "shmdt success\n";return 0;
}

        这段代码就有我们前面所说管道的一个性质,并且,这里有一个小坑;正常情况下,我们一般先运行服务端代码,因为我要保证客户端代码在获取 shmid 时,已经被创建了,因此在测试代码一(不加管道代码)时,我们总是先运行服务端代码;

        如上图所示,若我们将打开管道文件的代码放到最上面,也就是最先执行,此时,当我们的服务端打开管道文件时,由于对端未打开管道文件,因此,我们会阻塞住,此时我们接着运行客户端代码,由于客户端打开管道文件时,由于对端已经在等待了,因此可以直接返回管道文件对应文件描述符,此时若我们的客户端继续运行,当调用shmget获取shmid时,由于key所对应的共享内存并未创建,因此我们的客户端会直接运行失败;

3、共享内存的特性

1、共享内存仅需内存级读写即可,与管道不同,使用管道需要调用系统调用read、write等;而共享内存仅需往指定虚拟地址空间写入、读取即可;

2、共享内存是最快的通信方式,因为拷贝次数最少;写入最少拷贝仅需从键盘文件对应缓冲区拷贝到共享内存中即可,写入仅需从共性内存中拷贝到要写入的缓冲区即可;

相关文章:

Linux | 进程间通信

目录 前言 一、进程间通信的基本概念 二、管道 1、管道的基本概念 2、匿名管道 &#xff08;1&#xff09;原理 &#xff08;2&#xff09;测试代码 &#xff08;3&#xff09;读写控制相关问题 a、读端关闭 b、写端关闭 c、读快写慢 d、读慢些快 &#xff08;4&a…...

Vue.js正式环境中配置多个请求的URL

在Vue.js中&#xff0c;你可以在正式环境中配置多个请求的URL&#xff0c;通常使用一些配置文件或者环境变量的方式。下面是一种常见的配置方式&#xff1a; 1. 创建配置文件&#xff1a;在项目的根目录下&#xff0c;创建一个配置文件&#xff0c;比如可以是config.js&#x…...

简单的 UDP 网络程序

文章目录&#xff1a; 简单的UDP网络程序服务端创建套接字服务端绑定启动服务器udp客户端本地测试INADDR_ANY 地址转换函数关于 inet_ntoa 简单的UDP网络程序 服务端创建套接字 我们将服务端封装为一个类&#xff0c;当定义一个服务器对象之后&#xff0c;需要立即进行初始化…...

人工智能-深度学习之文本预处理

文本预处理 对于序列数据处理问题&#xff0c; 这样的数据存在许多种形式&#xff0c;文本是最常见例子之一。 例如&#xff0c;一篇文章可以被简单地看作一串单词序列&#xff0c;甚至是一串字符序列。 本节中&#xff0c;我们将解析文本的常见预处理步骤。 这些步骤通常包括…...

【Java 进阶篇】插上翅膀:JQuery 插件机制详解

在前端开发中&#xff0c;JQuery 作为一个广泛应用的 JavaScript 库&#xff0c;为开发者提供了丰富的工具和方法&#xff0c;简化了 DOM 操作、事件处理等繁琐的任务。而在这个庞大的生态系统中&#xff0c;插件机制是 JQuery 的一项重要特性&#xff0c;使得开发者能够轻松地…...

手动编译GDB

手动编译GDB 起因在于使用Clang-14编译C文件并生成调试信息,使用gdb调试时报DWARF相关错误。经检查原因在于虚拟机为Ubuntu 20.04&#xff0c;使用apt下载时官方提供gdb版本为9.2&#xff0c;不支持DWARF5,而Clang-14生成的调试信息是DWARF5版本的。为解决该问题&#xff0c;手…...

竞赛选题 深度学习花卉识别 - python 机器视觉 opencv

文章目录 0 前言1 项目背景2 花卉识别的基本原理3 算法实现3.1 预处理3.2 特征提取和选择3.3 分类器设计和决策3.4 卷积神经网络基本原理 4 算法实现4.1 花卉图像数据4.2 模块组成 5 项目执行结果6 最后 0 前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分享的是 &a…...

替换SlowFast中Detectron2为Yolov8

一 需求 FaceBookReserch中SlowFast源码中检测框是用Detectron2进行目标检测&#xff0c;本文想实现用yolov8替换detectron2二 实施方案 首先&#xff0c;yolov8 支持有自定义库ultralytics&#xff08;仅支持yolov8&#xff09;&#xff0c;安装对应库 pip install ultraly…...

轻量化网络--MobileNet V1

文章目录 depth-wise separable convolutions普通卷积depthwise conconvolutionspointwise convolutions网络结构进一步分析网络训练方式两个重要的超参数Width Multiplier: Thinner ModelsResolution Multiplier: Reduced Representation实验结果消融实验细粒度,高分辨率识别…...

gittee启动器

前言 很多小伙伴反馈不是使用gitee&#xff0c;不会寻找好的项目&#xff0c;在拿到一个项目不知道从哪里入手。 鼠鼠我呀就是宠粉&#xff0c;中嘞&#xff0c;老乡。整&#xff01;&#xff01;&#xff01; git的基本指令 在使用gitee的时候呢&#xff0c;我们只需要记住…...

Spark数据倾斜_产生原因及定位处理办法_生产环境

在最近的项目中&#xff0c;历史和实时数据进行关联平滑时出现了数据倾斜&#xff0c;产生了笛卡尔积&#xff0c;具体现象如下&#xff1a;运行内存175GB&#xff0c;核数64&#xff0c;运行代码时&#xff0c;查看SparkUI界面的active jobs &#xff0c;数据输入是1G&#xf…...

2023OceanBase年度发布会后,有感

很荣幸收到了OceanBase邀请&#xff0c;于本周四&#xff08;11月16日&#xff09;参加了OceanBase年度发布会并参加了DBA老友会&#xff0c;按照理论应该我昨天&#xff08;星期五&#xff09;就回到成都了&#xff0c;最迟今天白天就该把文章写出来了&#xff0c;奈何媳妇儿买…...

ubuntu18.04中代码迁移到20.04报错

一、 PCL库&#xff0c;Eigen库报错&#xff0c;如&#xff1a; /usr/include/pcl-1.10/pcl/point_types.h:903:29: error: ‘enable_if_t’ in namespace ‘std’ does not name a template type; did you mean ‘enable_if’?/usr/include/pcl-1.10/pcl/point_types.h:698:…...

QQ五毛项目记

问题与挑战&#xff1a;某公司为了实现某马总造福全人类&#xff0c;红旗插遍全球的宏伟目标&#xff0c;为应对后续用户激增的问题。特别安排了一次针对全体用户的秒杀活动&#xff1a;于XXXX年XX月XX日XX时XX分XX秒开始的秒杀五毛钱一百个QQ币的活动。每个账户仅限一次&#…...

小程序实现登录持久化

小程序实现登录持久化需要使用到小程序的缓存API&#xff0c;例如wx.getStorageSync()和wx.setStorageSync()等方法。以下是一个简单的代码实现&#xff1a; // App.js App({ // 在全局的App.js中定义全局变量userInfo&#xff0c;用于存放用户信息 globalData: { userInfo: …...

2023年亚太杯数学建模思路 - 案例:ID3-决策树分类算法

文章目录 0 赛题思路1 算法介绍2 FP树表示法3 构建FP树4 实现代码 建模资料 0 赛题思路 &#xff08;赛题出来以后第一时间在CSDN分享&#xff09; https://blog.csdn.net/dc_sinor?typeblog 1 算法介绍 FP-Tree算法全称是FrequentPattern Tree算法&#xff0c;就是频繁模…...

C复习-输入输出函数+流

参考&#xff1a; 里科《C和指针》 perror 定义在stdio.h中。当一个库函数失败时&#xff0c;库函数会在一个外部整型变量errno&#xff08;在errno.h中定义&#xff09;中保存错误代码&#xff0c;然后传递给用户程序&#xff0c;此时使用perror&#xff0c;会在打印msg后再打…...

duplicate复制数据库单个数据文件复制失败报错rman-03009 ora-03113

duplicate复制数据库单个数据文件复制失败报错rman-03009 ora-03113 搭建dg过程中&#xff0c;发现有一个数据文件在复制过程中没有复制过来&#xff0c;在备库数据文件目录找不到这个数据文件 处理方法&#xff1a; 第一步&#xff1a;主库备份86#数据文件 C:\Users\Admi…...

golang 解析oracle 数据文件头

package mainimport ("encoding/binary""fmt""io""os" ) // Powered by 黄林杰 15658655447 // Usered for parser oracle datafile header block 1 .... // oracle 数据文件头块解析 // KCBlockStruct represents the structure of t…...

van-popup滑动卡顿并且在有时候在ios上经常性滑动卡顿的情况

解决”pc端页面可以滚动&#xff0c;移动端手势无法滚动“问题的一次经历 - 掘金 <van-popup v-model"studentclassShow" :lock-scroll"false" position"bottom" style"z-index: 3000" :style"{ height: 55% }"><d…...

YOLOv7独家原创改进:最新原创WIoU_NMS改进点,改进有效可以直接当做自己的原创改进点来写,提升网络模型性能精度

💡该教程为属于《芒果书》📚系列,包含大量的原创首发改进方式, 所有文章都是全网首发原创改进内容🚀 💡本篇文章为YOLOv7独家原创改进:独家首发最新原创WIoU_NMS改进点,改进有效可以直接当做自己的原创改进点来写,提升网络模型性能精度。 💡对自己数据集改进有效…...

ubuntu20.04中编译zlib1.2.11(源码编译)

1. 安装cmake-gui 2. 下载并解压zlib-1.2.11&#xff0c;在解压得到的文件夹内部创建一个“build”文件夹。 3. 打开cmake-gui&#xff0c;配置zlib1.2.11的configure文件&#xff08;主要编辑build路径&#xff0c;安装路径&#xff0c;以及其他依赖选项&#xff09;&#x…...

计算机毕业设计选题推荐-高校后勤报修微信小程序/安卓APP-项目实战

✨作者主页&#xff1a;IT研究室✨ 个人简介&#xff1a;曾从事计算机专业培训教学&#xff0c;擅长Java、Python、微信小程序、Golang、安卓Android等项目实战。接项目定制开发、代码讲解、答辩教学、文档编写、降重等。 ☑文末获取源码☑ 精彩专栏推荐⬇⬇⬇ Java项目 Python…...

如何零基础自学AI人工智能

随着人工智能&#xff08;AI&#xff09;的快速发展&#xff0c;越来越多的有志之士被其强大的潜力所吸引&#xff0c;希望投身其中。然而&#xff0c;对于许多零基础的人来说&#xff0c;如何入门AI成了一个难题。本文将为你提供一份详尽的自学AI人工智能的攻略&#xff0c;帮…...

pm2使用

常用命令 pm2 delete/stop/restart/start/list/info/monit/log...

在Ubuntu或linux中为coreutils工具包的cp和mv命令添加进度条

1、查看当前最新的coreutils版本&#xff1a; http://ftp.gnu.org/gnu/coreutils/ 2、安装coreutils过程 # wget http://ftp.gnu.org/gnu/coreutils/coreutils-9.4.tar.xz # tar -xJf coreutils-9.4.tar.xz # cd coreutils-9.4/ 对照上面的&#xff0c;下载对应coreutils版本…...

力扣-58. 最后一个单词的长度

int lengthOfLastWord(char* s) {char* temp s;char* ret s;int count 0;/*返回的长度*/while (*temp){/*只记录空格后是字母的地址*/if ((*temp ) && (*(temp 1) ! \0) && (*(temp 1) ! )){ret temp 1;}temp;}while (*ret){if (isalpha(*ret) ! 0)…...

快递鸟荣获全球电子商务创业创新大赛总决赛一等奖

日前&#xff0c;以“开放、连接、协同、赋能”为主题&#xff0c;由商务部中国国际电子商务中心指导&#xff0c;浙江省商务厅、中共省委组织部、中共省委宣传部、中共省委网信办、省发展和改革委、省教育厅、省科技厅、省财政厅、省人力社保厅、团省委主办&#xff0c;湖州市…...

阶段七-Day02-SpringMVC

一、Restful请求格式 1. 介绍 Rest(Representational State Transfer&#xff1a;表现层状态转移)是一种软件架构风格&#xff0c;其核心是面向资源的一种设计。何为面向资源&#xff0c;意思是网络上的所有事物都可以抽象为资源&#xff0c;而每个资源都有唯一的资源标识&…...

YOLOv5独家原创改进:最新原创WIoU_NMS改进点,改进有效可以直接当做自己的原创改进点来写,提升网络模型性能精度

💡该教程为属于《芒果书》📚系列,包含大量的原创首发改进方式, 所有文章都是全网首发原创改进内容🚀 💡本篇文章为YOLOv5独家原创改进:独家首发最新原创WIoU_NMS改进点,改进有效可以直接当做自己的原创改进点来写,提升网络模型性能精度。 💡对自己数据集改进有效…...

【深度学习】pytorch快速得到mobilenet_v2 pth 和onnx

在linux执行这个程序&#xff1a; import torch import torch.onnx from torchvision import transforms, models from PIL import Image import os# Load MobileNetV2 model model models.mobilenet_v2(pretrainedTrue) model.eval()# Download an example image from the P…...

高防CDN安全防护系统在业务方面的应用

在当今数字化的时代&#xff0c;网络安全问题日益严峻&#xff0c;保护网站和数据免受攻击变得至关重要。CDN安全防护系统作为一种有效的解决方案&#xff0c;受到了广泛关注。小德将向您介绍CDN安全防护系统的原理、应用场景以及使用方法&#xff0c;助您更好地保障网络安全。…...

opencv(3):控制鼠标,创建 tackbar控件

文章目录 控制鼠标相关APIsetMouseCallbackcallback TrackBar 控件cv2.createTrackbarcv2.getTrackbarPos&#xff1a; 控制鼠标相关API setMouseCallback(winname, callback, userdata)callback(event, x, y, flags, userdata) setMouseCallback 在 OpenCV 中&#xff0c;s…...

UE4动作游戏实例RPG Action解析二:GAS系统播放武器绑定的技能,以及GE效果

一、GAS系统播放武器技能 官方实例激活技能通过装备系统数据激活,我先用武器数据资产直接激活 官方实例蒙太奇播放是自定义的AbilityTask,我先用更简单的方法实现效果 1.1、技能系统必要步骤: 1.1.1 插件启用AbilitySystem 1.1.2 PlayerCharacter绑定技能组件AbilitySy…...

做完这些_成为机器学习方面的专家

简单记个帖子, 用来记录学习机器学习的路线图 1. 数学分析, 高等代数, 概率论这三大件不多说, 基础中的基础. 2. 对于编程工具, b站上500集的python教程---python面向对象编程五部曲(从零到就业). 3. 对于机器学习的理论板块, 推荐b站up主---啥都会一点的研究生, 里面有一个吴恩…...

kubernetes|云原生| 如何优雅的重启和更新pod---pod生命周期管理实务

前言&#xff1a; kubernetes的管理维护的复杂性体现在了方方面面&#xff0c;例如&#xff0c;&#xff50;&#xff4f;&#xff44;的管理&#xff0c;服务的管理&#xff0c;用户的管理&#xff08;&#xff32;&#xff22;&#xff21;&#xff23;&#xff09;&#xf…...

【总结】坐标变换和过渡矩阵(易忘记)

xCy&#xff0c;此为x到y的坐标变换。 [β1,β2,…,βn] [α1,α2,…αn]C&#xff0c;此为基α到基β的过渡矩阵。 这个概念经常忘记。。。alpha到beta看来就是alpha后面加一个过渡矩阵了&#xff0c;很直观。坐标变换就是根据过渡矩阵和基本形式推一推得到吧&#xff0c;记…...

第十一周任务总结

本周任务总结 本周物联网方面主要继续进行网关的二次开发与规则引擎实现设备联动的实现 非物联网方面主要复习了docker的使用与算法的学习 1.网关的二次开发&#xff0c;本周将实现debug调试输出的文件下载到了网关&#xff0c;但网关出了问题无法连接&#xff0c;最终跟客服…...

Java Web——JavaScript基础

1. 引入方式 JavaScript程序不能独立运行&#xff0c;它需要被嵌入HTML中&#xff0c;然后浏览器才能执行 JavaScript 代码。 通过 script 标签将 JavaScript 代码引入到 HTML 中&#xff0c;有3种方式&#xff1a; 1.1. 内嵌式(嵌入式) 直接写在html文件里&#xff0c;用s…...

Vue3 toRaw 和 markRaw

一、toRaw 我们可以使用ref 和 reactive 将普通对象类型的数据变为响应式的数据。 我们可以使用toRaw 将reactive 对象的数据变为一般对象类型的数据。 使用toRaw 需要先进行引入&#xff1a; import { toRaw } from vue; 语法格式&#xff1a; const xxx toRaw(数据) set…...

麒麟信安助力长沙市就业与社保数据服务中心政务系统向自主创新演进

应用场景 长沙市就业与社保数据服务中心依托长沙市“政务云”的公共基础资源和相应的支撑能力&#xff0c;围绕社保、就业、人事人才、劳动关系等人社全量业务服务&#xff0c;力求建立以“智慧服务、智慧监管、智慧决策”为核心的“智慧人社”综合服务平台&#xff0c;实现人…...

【LeetCode刷题-双指针】--16.最接近的三数之和

16.最接近的三数之和 方法&#xff1a;排序双指针 class Solution {public int threeSumClosest(int[] nums, int target) {Arrays.sort(nums);int ans nums[0] nums[1] nums[2];for(int i 0;i<nums.length;i){int start i1,end nums.length - 1;while(start < en…...

Mac 安装 protobuf 和Android Studio 使用

1. 安装,执行命令 brew install protoc 2. Mac 错误提示&#xff1a;zsh: command not found: brew解决方法 解决方法&#xff1a;mac 安装homebrew&#xff0c; 用以下命令安装&#xff0c;序列号选择中科大&#xff08;1&#xff09;或 阿里云 /bin/zsh -c "$(curl…...

MongoDB入门级别教程全(Windows版,保姆级教程)

下载mongodb 进入官网&#xff1a; Download MongoDB Community Server | MongoDB 选择msi&#xff0c;Windows版本 下载完后直接双击&#xff1a; 选择complete 这里建议改地方&#xff1a; 我这里直接改成d盘&#xff1a;work目录下面&#xff1a; 点击next&#xff1a; 因…...

基于机器学习的居民消费影响因子分析预测

项目视频讲解: 基于机器学习的居民消费影响因子分析预测_哔哩哔哩_bilibili 主要工作内容: 完整代码: import pandas as pd import numpy as np import matplotlib.pyplot as plt import seaborn as sns import missingno as msno import warnings warnings.filterwarnin…...

Qt HTTP 摘要认证(海康球机摄像机ISAPI开发)

接到一个需求是开发下海康的球机,控制云台,给到我的是一个开发手册,当然了是海康的私有协议 ISAPI开发手册https://download.csdn.net/download/qq_37059136/88547425关于开发这块读文档就可以理解了,海康使用的是摘要认证,当然了海康已经给出使用范例 通过libcurl就可以直接连…...

srs webrtc推拉流环境搭建(公网)

本地环境搭建 官方代码https://github.com/ossrs/srs 拉取代码&#xff1a; git clone https://github.com/ossrs/srs.gitcd ./configure make ./objs/srs -c conf/https.rtc.confsrs在公网上&#xff0c;由于srs是lite-ice端&#xff0c;导致他不会主动到srs获取自己的公网i…...

【Flutter】设计原则(2)深入解析 SOLID 原则的应用

【Flutter】设计原则(2)深入解析 SOLID 原则的应用 文章目录 一、前言二、SOLID原则三、在 Flutter 中应用单一职责原则1. 专注单一功能的 Widget2. 提高代码可维护性四、在 Flutter 中应用开闭原则1. 利用多态和基类实现可扩展的 Widget2. 增强应用的可扩展性和灵活性五、在…...

python爬虫概述及简单实践:获取豆瓣电影排行榜

目录 前言 Python爬虫概述 简单实践 - 获取豆瓣电影排行榜 1. 分析目标网页 2. 获取页面内容 3. 解析页面 4. 数据存储 5. 使用代理IP 总结 前言 Python爬虫是指通过程序自动化地对互联网上的信息进行抓取和分析的一种技术。Python作为一门易于学习且强大的编程语言&…...

ts视频文件转为mp4(FFmpeg)

有些视频资源下载下来之后发现是.ts的文件&#xff0c;除了用下载它时用的工具或是浏览器才能看&#xff0c;那有没有将ts文件转换成更加通用视频格式的方法。 几乎万能的音视频工具--ffmpeg登场 安装和环境配置可看这篇博客&#xff1a;FFmpeg指令行打开usb摄像头&#xff0…...