操作系统:进程间通信 | System V IPC

目录

前言:

1.共享内存

1.1.什么是共享内存

1.2.共享内存使用接口

shmget函数

shmat函数

shmdt函数

shmctl函数

2.共享内存实现通信

2.1.代码实现

comm.hpp

 server,cpp

client.cpp

2.2.共享内存的缺点

2.3.实现通信的同步化

 2.4共享内存通信的优势

3.初识消息队列和信号量

3.1.消息队列

3.2.信号量 

4.System V IPC的统一管理


前言:

System V IPC(系统5的进程间通信)是在Unix操作系统上实现进程间通信(IPC)的一种机制。它引入了一大类进程间通信方法,包括共享内存、消息队列和信号灯集等IPC对象。每个IPC对象都有唯一的ID,这个ID在创建时由系统分配,并且IPC对象在创建后一直存在,直到被显式地删除或系统关闭时自动释放。

在这篇博客中,我们重点介绍System V IPC中的共享内存部分,以及学习消息队列、信号量的一些接口的使用和 Linux下实现的System V IPC统一的数据结构体系……

1.共享内存

1.1.什么是共享内存

 我们已经清楚:进程间通信的本质是让不同的进程看到同一份资源。那么共享内存这种进程间通信方式的实现是通过让不同的进程通过“共享”某一块内存来实现的。

共享内存是通过在物理内存中开辟一块内存,使得这块内存通过页表映射,到两个(多个)不同进程的进程地址空间中,从而让不同的进程可以访问到这一块物理内存,实现间接的数据交互的一种进程间通信方式。  

讲完生硬的概念,我们来进入一个场景,共享内存就相当于隔绝了两个村子的河流,而两个进程就相当于这两个村子的两个人,当这两个人(进程)需要传送东西(进行数据交互)就能通过这个河流(共享内存) 来实现,那么通过这个河流,就能实现两个人的通信了……

1.2.共享内存使用接口

shmget函数

在这个图中:我们看到了key是共享内存的名字,那这个key我们怎么获得的呢?

// 通过ftok算法形成一个唯一的key
key_t key = ftok("名称", 传入一个int参数);

因为实际场景下会出现许多个共享内存同时进行通信的情况,这时系统为了对这些共享内存进行管理就需要给他们一个唯一的标识符shmid(类比:文件fd、进程pid),而这个shmid是通过一个唯一的key值来实现的。并且如果我们需要进程进程间通信,那么我们也需要让不同的进程能够访问到同一个key和同一个shmid。

具体实现如下: 

// 同一个文件名
const string filename = "/home/Czh_Linux/code/shared_memory";
// 同一个proj_id
const int proj_id = 2022044026;

// 获取唯一标识符
key_t GetKey()
{
    // 通过ftok算法形成一个唯一的key
    // 通过相同的filename、和project_id
    key_t key = ftok(filename.c_str(), proj_id);
    if (key < 0)
    {
        cerr << "errno: " << errno << ", errstring: " << strerror(errno) << endl;
        exit(1);
    }
    return key;
}

这里我们传入文件名的原因:可以让不同的进程更加方便的访问到同一个key……我们在获取了唯一的key之后,接下来就是学习shmget如何使用了。


shmget的具体使用分为两种情形:

创建一段新的共享内存:

    // 参数(唯一标识符key,共享内存的大小,打开方式|权限)
    int shmid = shmget(key, 4096, IPC_CREAT | IPC_EXCL | 0644); // 共享内存的id

这段代码中 :表示创建一段共享内存,则需传入权限,并且通过IPC_EXCL防止重复创建,并且提供权限

 打开一段已有的共享内存:

// 打开方式直接设置为0即可
int shmid = shmget(key, 4096, 0);

但是实际上我们使用时,一般将这两种情况进行封装改造一下:

// 创建/打开 共享内存封装函数
int ShmOption(key_t key, int flag)
{

    // 参数(唯一标识符key,共享内存的大小,打开方式|权限)
    int shmid = shmget(key, MEMORYSIZE, flag); // 共享内存的id
    cout << shmid << endl;
    // key作为在内核中标识shm的唯一性
    // 对共享内存进行操作时,我们是通过shmid来进行的
    if (shmid < 0)
    {
        cerr << "shmid error: " << errno << ", errstring: " << strerror(errno) << endl;
        exit(2);
    }
    return shmid;
}

// 创建新的共享内存,并返回shmid
int CreateShm(key_t key)
{
    cout << "新的共享内存创建完成" << endl;
    // 创建,则需传入权限,并且通过O_EXCL防止重复创建,并且提供权限
    return ShmOption(key, IPC_CREAT | IPC_EXCL | 0644);
}

// 打开原有共享内存,并返回shmid
int GetShm(key_t key)
{
    // 打开,则只需通过O_CREAT打开
    return ShmOption(key, 0);
}

 所以我们外部调用的时候,通过不同的接口实现即可,另外GetKey()函数在上面有,也是我们自己实现的封装的小模块


值得一提的是:我们可以分别通过下面这两个Linux指令实现对共享内存的查看和删除

// 查看当前的共享内存
ipcs -m
// 删除当前共享内存
ipcrm -m "对应的shmid"

另外:如果我们在进程中通过shmget开辟出一块共享内存,却没有通过shmctl进行释放,我们调用ipcs -m就会发现:即使进程退出后,这块共享内存也依旧保留着物理内存中,也就是:共享内存的生命周期是随内核的

shmat函数

我们在1.1.中讲述了,当我们创建了共享内存,只是在物理内存中开辟了一块区域,而我们要通过进程使用这块区域,就需要将这块区域从物理内存挂接到进程地址空间,也就是实现页表的链接。这时我们可以通过shmat函数来实现…… 

// 以一个字符串数组为例
// 传入nullptr表示不对地址有过多的要求,传入0表示我们正常挂接
char *str = (char *)shmat(shmid, nullptr, 0);

 当我们创建了一个str,操作系统就会为它从进程地址空间中找到一块虚拟地址来存放,而通过shmat函数,就实现了这一块虚拟地址映射到共享内存中,最终能够实现:某一个进程对str进行一些操作,其他的进程就能知道操作了什么,进而获取了信息。那么str在这里就扮演着,被进程们看到的一份资源的角色。

shmdt函数

对应上面我们挂接到共享内存的str,那么我们接触链接就通过共享内存式的释放这个str即可

// 内填挂接到共享内存的进程地址空间的地址
shmdt(str);

shmctl函数

值得注意:这里的表述是控制共享内存,而不是删除共享内存,但是这个接口可以实现删除这段共享内存的功能,从物理内存上释放这块内存……

// 找到共享内存的id,通过传入IPC_RMID 来实现
shmctl(shmid, IPC_RMID, nullptr);

到了这里我们对共享内存接口的学习就结束了,接下来我们将有一个demo级别的共享内存通信。

2.共享内存实现通信

2.1.代码实现

comm.hpp

#include <iostream>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <cstring>
#include <fcntl.h>
#include <unistd.h>
using namespace std;

// 同一个文件名
const string filename = "/home/Czh_Linux/code/shared_memory";
// 同一个proj_id
const int proj_id = 2022044026;

// 建议设置为4096的整数倍
#define MEMORYSIZE 4096 // 设定的共享内存大小

// 获取唯一标识符
key_t GetKey()
{
    // 通过ftok算法形成一个唯一的key
    // 通过相同的filename、和project_id
    key_t key = ftok(filename.c_str(), proj_id);
    if (key < 0)
    {
        cerr << "errno: " << errno << ", errstring: " << strerror(errno) << endl;
        exit(1);
    }

    return key;
}

// 将key转化为16进制
string ToHex(int id)
{
    char buffer[1024];
    snprintf(buffer, sizeof(buffer), "0x%x", id);
    return buffer;
}

// 创建/打开 共享内存封装函数
int ShmOption(key_t key, int flag)
{

    // 参数(唯一标识符key,共享内存的大小,打开方式|权限)
    int shmid = shmget(key, MEMORYSIZE, flag); // 共享内存的id
    cout << shmid << endl;
    // key作为在内核中标识shm的唯一性
    // 对共享内存进行操作时,我们是通过shmid来进行的
    if (shmid < 0)
    {
        cerr << "shmid error: " << errno << ", errstring: " << strerror(errno) << endl;
        exit(2);
    }
    return shmid;
}

// 创建新的共享内存,并返回shmid
int CreateShm(key_t key)
{
    cout << "新的共享内存创建完成" << endl;
    // 创建,则需传入权限,并且通过O_EXCL防止重复创建,并且提供权限
    return ShmOption(key, IPC_CREAT | IPC_EXCL | 0644);
}

// 打开原有共享内存,并返回shmid
int GetShm(key_t key)
{
    // 打开,则只需通过O_CREAT打开
    return ShmOption(key, 0);
}

 server,cpp

#include "comm.hpp"

int main()
{
    key_t key = GetKey();
    cout << "key: " << ToHex(key) << endl;

    int shmid = CreateShm(key);
    cout << "共享内存id: " << shmid << endl;
    sleep(5);
    // 挂接到共享内存
    char *str = (char *)shmat(shmid, nullptr, 0);

    // 实现通信
    cout << "开始进行通信" << endl;
    while(1)
    {
        sleep(1);
        cout<<"挂接到共享内存的内容:"<<str<<endl;
    }
    shmdt(str);
    cout << "将shm从进程地址空间中移除……" << endl;

    sleep(5);
    shmctl(shmid, IPC_RMID, nullptr);
}

这段代码中,我们通过创建共享内存再实现str挂接到共享内存中,接着通过打印str内容。 

client.cpp

#include "comm.hpp"

int main()
{
    key_t key = GetKey();
    cout << "key: " << ToHex(key) << endl;
    int shmid = GetShm(key);
    cout << "client 连接上共享内存……, id: " << shmid << endl;
    sleep(5);
    char *str = (char *)shmat(shmid, nullptr, 0);
    // 进行通信
    char c = 'a';
    for (; c <= 'z'; c++)
    {
        // 对共享内存对应的地址空间进行写入
        str[c - 'a'] = c;
        cout << "write: " << c << " done" << endl;
        sleep(2);
    }
    shmdt(str);
}

 client作为实现通信数据的发送端,这里我们不断的向str中插入abcd……这一系列字符然后,再server端打印,获取结果,这时server就能够通过str了解到共享内存的变化,也就是验证了能看到同一份资源。

2.2.共享内存的缺点

当我们运行1.3.这个代码demo时,发现我们数据的读取并不是同步的,并不是:写段发送一段数据,读端才进行接收,而是读、写段互不干扰,这里我们可以通过修改sleep的时间来体现,并且这些通信的数据是可以被任意修改的……

那么就有: 

  1. 共享内存通信,没有提供同步的机制,并且数据直接裸露给所有使用者,需要考虑使用安全问题
  2. 会导致数据流无法同步,使得接受端、发送端的信息不一致

 因为共享内存原生的通信方式无法进行同步通信,那么我们能不能嵌套一下我们学过的管道通信这一种同步通信方式来实现同步通信呢?

2.3.实现通信的同步化

这里我们只用修改一下,client和server的代码即可,需要进行测试就,更改对应代码

// 共享内存 + 命名管道通信
void client2()
{
    key_t key = GetKey();
    cout << "key: " << ToHex(key) << endl;
    int shmid = GetShm(key);
    cout << "client 连接上共享内存……, id: " << shmid << endl;
    sleep(5);
    char *str = (char *)shmat(shmid, nullptr, 0);
    // 进行通信
    int r_open = open("fifo", O_WRONLY);

    char c = 'a';
    for (; c <= 'z'; c++)
    {
        // 对共享内存对应的地址空间进行写入
        str[c - 'a'] = c;
        cout << "write: " << c << " done" << endl;
        write(r_open, str, sizeof(str));
        sleep(2);
    }
    close(r_open);
    shmdt(str);
}

// 通过共享内存和命名管道进行通信
void server2()
{
    // 创建管道文件fifo,权限0666
    mkfifo("fifo", 0666);
    key_t key = GetKey();
    cout << "key: " << ToHex(key) << endl;

    int shmid = CreateShm(key);
    cout << "共享内存id: " << shmid << endl;
    sleep(5);

    char *str = (char *)shmat(shmid, nullptr, 0);

    // 通过只读方式接收信息
    int r_open = open("fifo", O_RDONLY);

    // 实现通信
    cout << "开始进行通信" << endl;
    while (1)
    {
        read(r_open, str, sizeof(str));
        cout << "挂接到共享内存的内容:" << str << endl;
        sleep(1);
    }

    shmdt(str);
    cout << "将shm从进程地址空间中移除……" << endl;

    sleep(5);
    close(r_open);
    shmctl(shmid, IPC_RMID, nullptr);
}

如图: 当我们打开服务器时,cilent还没有运行之前,我们发现server处于阻塞状态,这时因为管道文件的读功能,在没有数据进行写入时,会等待直到管道的另一端进行写入。

而当我们进行调用时:发现数据是具有同步性的,并且当我们关掉client后,server端也没有新的数据写入,而是在while(1)中因为代码逻辑不断打印着当前管道的内容…… 


 2.4共享内存通信的优势

共享内存是最快的IPC形式。一旦内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到内核,换句话说是进程不再通过执行进入内核的系统调用来传递彼此的数据

对于管道通信,我们知道管道通信的本质就是:数据通过管道文件的缓冲区从一个进程到另一个进程的转移,而当涉及到数据的转移其实就是,数据从一块缓冲区到另一块缓冲区的拷贝……

如图:在上层中这一句Hello World从键盘写入到进程,然后通过管道被另一个进程读取,然后打印在显示屏上。而实际上这一句Hello World是通过键盘缓冲区拷贝到用户级缓冲区再拷贝到管道文件缓冲区再拷贝到用户缓冲区最后拷贝到显示器上。 

在这一个简单的场景中就需要实现4次拷贝,而拷贝是一种较大的系统资源消耗的行为,这也导致可管道通信的效率不是最优的,因为文件管理和内存管理是割裂开来的,无法避免拷贝的行为。


而对于共享内存,这段数据并不用通过进程的缓冲区进行互相拷贝,而是只用通过传输到物理内存中,直接通过写入、读取挂接到物理内存中的数据即可,减少了文件缓冲区这一块不必要的拷贝

 所以通过共享内存这个进程间通信,我们可以尽可能的减少拷贝和系统接口的调用,这也就是共享内存是最快的进程间通信方式,实现进程不再通过执行进入内核的系统调用来传递彼此的数据

3.初识消息队列和信号量

3.1.消息队列

消息队列进程间通信机制中的一种重要组件,它允许不同进程之间进行数据的发送和接收。消息队列以链表式的结构组织数据,并存放在内核中,由各个进程通过特定的消息队列标识符来引用和进行数据传送。

消息队列的通信本质:提供一个队列,允许进程往这个队列中,发送、接收一个一个数据块,这个数据块的本质就是一个结构体,内部存储着 数据内容 和 发送方信息。

通过这个机制,我们随意传入数据进入消息队列,接收数据块时只要判断一下传入者信息就能够获取想要的得到的信息,并且当我们想要接收数据时,我们可以在任意时刻接收某一个数据,也就是消息队列允许进程间进行异步通信,又因为它的独特机制(比较于管道)对数据块进行标识,可以更加灵活地对数据进行读取

具体的接口使用可以看一位大神写的这篇优秀的博客:Linux进程间通信-消息队列(IPC、mq)C/C++代码接口_c语言消息队列-CSDN博客

这里建议大家看完这篇博客后,自己实现一下server、client通过消息队列通信的模块!!!

值得一提的是:当我们学习完相关接口的使用时,我们发现共享内存、消息队列的接口出奇的相似,这也是System V IPC标准的一种体现。同理通过msgctl这个函数,我们也可以确定消息队列的生命周期是内核级别的。而因为息队列存放在内核中,并由内核来维护,具有较高的可靠性和安全性。


3.2.信号量 

信号量(Semaphore)是在多线程或多进程环境下使用的一种设施,主要用于控制对共享资源的访问。它可以看作是一个计数器,用于记录可用资源的数量。信号量的主要目的是实现进程或线程间的同步与互斥,以确保对共享资源的正确和安全的访问。

这一部分,因为涉及的内容过多,我们在下一篇博客中具体讲解,但是又因为信号量也是属于System V IPC这个标准的,我们可以知道它函数接口的调用也是类似与共享内存和消息队列的。

4.System V IPC的统一管理

操作系统在实际场景中,往往是需要多块共享内存、多个消息队列、多个信号量机制来进行不同进程间的通信,那么这些共享内存、消息队列、信号量要如何进行维护、进行管理的呢?那就又回到了操作系统的六字真言:先描述再组织

以共享内存的代码为例: 

void test()
{
    key_t key = GetKey();
    cout << "key: " << ToHex(key) << endl;
    int shmid = CreateShm(key);

    struct shmid_ds ds;
    // 通过IPC_STAT将该shmid中维护的结构体数据加载进ds中
    shmctl(shmid, IPC_STAT, &ds);

    cout << ds.shm_perm.__key << endl;
    cout << ToHex(ds.shm_perm.__key) << endl;
    cout << ds.shm_nattch << endl;

    sleep(5);
    shmctl(shmid, IPC_RMID, nullptr);
}
  1. 代码中的struct shmid_ds是操作系统维护共享内存的结构体对象,内部存储着该共享内存的属性,那么我们就能够延伸到操作系统对共享内存的管理,本质上就是对管理着共享内存结构体的结构体数据进行增删查改
  2. 当我们通过共享内存控制的接口,可以将对应的shmid共享内存的数据加载进我们定义的对象,这样子就能够实现对管理信息的访问了。

接着我们看一下IPC结构体是如何管理各自的数据的:

 那么操作系统是如何管理这些不同的IPC对象的呢?我们在上图中看到不同的IPC对象中维护着一个相同的结构体struct ipc_perm,而这个结构体对象就是操作系统实际上管理IPC对象的载体。

结合这两张图,我们发现struct ipc_perm就是操作系统维护IPC对象的最基本的单位,也可以看做一个“基类”,只要我们管理好最基本的struct ipc_perm,就能对不同的IPC对象进行较好的管理。到了这里我们就对System V IPC这个体系有了较好的理解了……

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Vitis HLS 学习笔记--HLS入门示例集合-目录

目录 1. 示例集合概述 2. Interface 接口 2.1 Aggregation_Disaggregation 聚合与解聚 2.1.1 aggregation_of_m_axi_ports 2.1.2 aggregation_of_nested_structs 2.1.3 aggregation_of_struct 2.1.4 auto_disaggregation_of_struct 2.1.5 disaggregation_of_axis_port …

游戏工作室为什么要使用海外住宅IP防封?

当谈到游戏工作室时&#xff0c;它们通常以多开游戏账号来获取收益为主要目标。这种商业模式在游戏产业中已经成为一个独特而且颇具潜力的领域。然而&#xff0c;随之而来的是防封问题&#xff0c;特别是当游戏工作室试图通过多开账号来赚取更多收益时。因此&#xff0c;我们有…

Navicat连接SQLSever报错:[08001] MicrosoftTCP Provider 远程主机强迫关闭了一个现有的连接

Navicat连接SQLSever报错&#xff1a;[08001] [Microsoft][SQL Server Native Client 10.0]TCP Provider: 远程主机强迫关闭了一个现有的连接 问题分析 旧版的MSSQL 如果不是最新版的&#xff0c;可以去这安装以下即可。 最新版的MSSQL 如果是安装最新版的MSSQL连接不上很正…

Kubernetes 的未来:通过生成式 AI 实现的潜在改进

Kubernetes 是一个用于自动化部署、扩展和管理容器化应用程序的开源平台&#xff0c;它彻底改变了 IT 行业。然而&#xff0c;与所有创新技术一样&#xff0c;它不断寻求改进以提高效率、可用性和功能。生成式人工智能&#xff08;Generative AI&#xff09;是一个有望取得改进…

C++:匿名对象

在C中&#xff0c;匿名对象是指在不分配给定变量名称的情况下创建的临时对象。这些对象通常用于传递参数给函数、作为函数的返回值或者在表达式中使用。 创建匿名对象 在C中&#xff0c;您可以使用类的构造函数来创建匿名对象。例如&#xff1a; MyClass(); // 创建一个匿名…

终于有人把“Linux云计算路线”整理出来了,收藏起来,随时查看

一&#xff0c;计算机硬件 二&#xff0c;计算机网络 三&#xff0c;Linux系统管理 四&#xff0c;构建Linux服务系统&#xff0c;数据库&#xff0c;程序及Web服务 五&#xff0c;消息队列&#xff0c;web集群&#xff0c;系统及软件优化&#xff0c;智能化监控&#xff0c;海…

CSS画一条虚线,并且灵活设置虚线的宽度和虚线之间的间隔和虚线的颜色

CSS画一条虚线,并且灵活设置虚线的宽度和虚线之间的间隔和虚线的颜色。 先看效果图&#xff1a; 在CSS中&#xff0c;你可以使用border属性或者background属性来画一条虚线。以下是两种常见的方法&#xff1a; 方法一&#xff1a;使用border属性 你可以设置一个元素的border…

4.25日学习记录

[HZNUCTF 2023 preliminary]ppppop 对于php反序列化&#xff0c;在之前的学习中有过了解&#xff0c;但是对于序列化字符串的格式不是很了解&#xff0c;刚好接触这题&#xff0c;可以了解一下 序列化字符串的格式&#xff1a; 布尔型&#xff08;bool&#xff09;b&#xf…

036——完善编译框架和注释并选择开源协议

目录 小总结 编译框架完善 代码风格 开源协议选择 小总结 经过两个月的努力现在已经写了457MB的代码了 . ├── board │ ├── Linux │ │ └── 4_9_88 │ │ └── ARM32 │ │ └── 100ask │ │ └── imx6ull_mi…

10.JAVAEE之网络编程

1.网络编程 通过网络,让两个主机之间能够进行通信 >基于这样的通信来完成一定的功能进行网络编程的时候,需要操作系统给咱们提供一组 AP1, 通过这些 API才能完成编程&#xff08;API 可以认为是 应用层 和 传输层 之间交互的路径&#xff09;&#xff08;API:Socket API相当…

简单案例验证说明 双亲委派机制

双亲委派介绍 双亲委派机制&#xff08;Parent Delegation Mechanism&#xff09;是Java中的一种类加载机制。在Java中&#xff0c;类加载器负责加载类的字节码并创建对应的Class对象。双亲委派机制是指当一个类加载器收到类加载请求时&#xff0c;它会先将该请求委派给它的父…

aysnc-await的用法

aysnc-await是promise的一种特殊语法&#xff0c;它可以更简洁的得到promise aysnc function aysnc 放在函数前定义函数&#xff0c;它规定了这个函数的返回值一定为promise&#xff0c; // 通过new新建一个promise(旧) // let p new Promise(function(resolve,reject){ //…

可搜索加密:保护隐私的搜索技术

在信息化、数字化快速发展的今天&#xff0c;数据的安全性和隐私性已成为公众关注的焦点。随着云计算、大数据等技术的广泛应用&#xff0c;数据共享与协同工作日益普遍&#xff0c;但如何在确保数据安全性的前提下&#xff0c;实现数据的快速、高效检索&#xff0c;成为了一个…

MySQL中explain的用法

执行结果各字段的含义 EXPLAIN SQL语句 如&#xff1a; EXPLAIN SELECT * FROM test 执行结果&#xff1a; 列名描述id在一个大的查询语句中每个SELECT关键字都对应一个 唯一的idselect_typeSELECT关键字对应的那个查询的类型table表名partitions匹配的分区信息type针对单表…

LLama的激活函数SwiGLU 解释

目录 Swish激活函数 1. Swish函数公式 LLaMA模型中的激活函数 1. SwiGLU激活函数 2. SwiGLU激活函数的表达式 3. SwiGLU激活函数的优势 Swish激活函数 Swish是一种激活函数&#xff0c;其计算公式如下&#xff1a; 1. Swish函数公式 Swish(x) x * sigmoid(x) 其中&am…

基于CANoe从零创建以太网诊断工程(2)—— TCP/IP Stack 配置的三种选项

&#x1f345; 我是蚂蚁小兵&#xff0c;专注于车载诊断领域&#xff0c;尤其擅长于对CANoe工具的使用&#x1f345; 寻找组织 &#xff0c;答疑解惑&#xff0c;摸鱼聊天&#xff0c;博客源码&#xff0c;点击加入&#x1f449;【相亲相爱一家人】&#x1f345; 玩转CANoe&…

Confluence 快捷键大揭秘:提高效率的小窍门

使用 Confluence 快捷键的好处有&#xff1a; 1.提高工作效率&#xff1b; 2.更流畅地进行编辑、导航和管理操作&#xff1b; 3.减少误操作&#xff1b; 4.展现专业水平。 更多精彩内容&#xff1a; 成为 Jira 大师&#xff1a;效率达人的必备秘诀 Jira Cloud 项目管理专栏 PMO…

怎样把PDF分割成多个文件?有哪些方法可以分割PDF文件?这几个方法成功率很高!

一&#xff0c;引言 PDF分割&#xff0c;即将一个完整的PDF文档拆分为多个较小的部分&#xff0c;是许多用户在处理 PDF文件时经常需要执行的操作。无论是为了单独提取某个章节、创建电子书章节、还是为了在多个设备间轻松共享&#xff0c;PDF分割都显得非常实用。本文将详细介…

AI大模型语音实时对话聊天机器人实现:ollama、funasr;支持语音实时语音打断;回音消除噪声抑制

ASR:funasr(1.0.19) LLM:ollama(Qwen) TTS(edge_tts) 支持语音实时语音打断:这是通过子进程的控制创建与杀掉,这里是通过有人再次说话就打断tts 回音消除噪声抑制:喇叭的tts播报影响到麦克风的识别了,播报的声音被错误的识别;这里可以jd买个回音消除的麦克风设备;或者有…

python 10实验

实验内容&#xff1a; 使用线性回归算法预测儿童身高 实验目的&#xff1a; 理解线性回归算法的原理&#xff0c;了解线性回归算法适用的问题类型&#xff0c;能够使用线性回归算法解决问题 实验内容&#xff1a; 一个人的身高除了随年龄变大而增长以外&#xff0c;在一定程…