【Linux】进程间通信——system V 共享内存、消息队列、信号量

需要云服务器等云产品来学习Linux的同学可以移步/–>腾讯云<–/官网,轻量型云服务器低至112元/年,优惠多多。(联系我有折扣哦)

文章目录

  • 写在前面
  • 1. 共享内存
    • 1.1 共享内存的概念
    • 1.2 共享内存的原理
    • 1.3 共享内存的使用
      • 1.3.1 创建
      • 1.3.2 控制
      • 1.3.3 关联
      • 1.3.4 去关联
      • 1.3.6 实战——实现server和client端的通信
      • 1.3.7 共享内存的特点
  • 2. 消息队列(不常用)
    • 2.1 消息队列的概念
    • 2.2 消息队列的使用
      • 2.2.1 获取消息队列
      • 2.2.2 控制消息队列
      • 2.2.3 发送和接收数据
  • 3. 信号量
    • 3.1 信号量的概念扫盲
    • 3.2 内核中信号量的相关数据结构
    • 3.3 信号量的PV操作
    • 3.4 信号量相关函数
      • 3.4.1 申请信号量
      • 3.4.2 控制信号量
      • 3.4.3 信号量的操作PV

写在前面

在上一篇文章中我们讲了一种进程间通信的方式管道,管道通信的本质是基于文件的,也就是说OS没有为此做过多的设计,但是system V IPC是操作系统特地设置的一种通信方式。提供的通信方式有以下三种

  • system V 共享内存
  • system V 消息队列
  • system V 信号量

1. 共享内存

1.1 共享内存的概念

我们在上一篇文章中讲到:要实现进程间通信,就一定要让不同的进程看到同一份资源

在匿名管道/命名管道中,我们让不同进程看到同一份资源的方式是让不同进程打开同一个文件,使用对文件的读写来实现进程间通信,那么除此之外,我们还有其他方法

让不同进程能够使用同一块物理内存,就是共享内存的核心思想

1.2 共享内存的原理

由于进程具有独立性,内核数据结构包括对应的代码、数据和页表都是独立的,为了实现进程间通信需要以下过程:

1. OS申请一段空间

2. 将申请好的物理内存空间映射到一个进程地址空间

3. 将同一块物理内存空间映射到另一个需要通信的进程中

4. 通信结束之后取消进程和物理内存的映射关系,然后释放内存

  • 我们把OS申请的空间叫做共享内存
  • 进程和共享内存建立映射关系叫做挂接
  • 取消进程和共享内存之间的映射关系叫做去关联
  • 释放内存叫做释放共享内存

image-20240119223153670

对共享内存的理解

在C语言中,我们可以使用malloc在物理内存上申请空间,并把申请的空间经过页表映射到进程地址空间中,返回进程地址空间的指定地址,但是对于共享内存的通信方式,需要被专门设计。因为在同一时间,可能会有很多进程需要使用这种方式进行通信,所以一定会同时存在很多的共享内存,所以需要被管理起来,因此需要被专门设计

1.3 共享内存的使用

1.3.1 创建

我们使用shmget系统调用来创建共享内存

image-20240119224359785

头文件: 
#include <sys/ipc.h>
#include <sys/shm.h>
函数原型: int shmget(key_t key, size_t size, int shmflg);
参数解释:key:是一个保证共享内存编号唯一性的标识符,为了让相同的进程能够看到同一个共享内存size:创建的共享内存的大小shmflg:创建共享内存的选项,通常我们使用两个:IPC_CREAT和IPC_EXCL
返回值:如果调用成功就返回一个合法的共享内存描述符shmid,如果调用失败就返回-1同时设置错误码

shmflg的选项含义:

  • IPC_CREAT:如果对应key的共享内存不存在就创建,如果存在就获取对应的shmid
  • IPC_EXCL:这个选项不能单独使用,和IPC_CREAT配合使用,如果不存在就创建,存在就出错返回

key的形成方式

我们使用一个特定的函数ftok来形成一个唯一的key

image-20240119225546733

头文件:
#include <sys/type.h>
#include <sys/ipc.h>
函数原型:
key_t ftok(const char *pathname, int proj_id);
参数解释:pathname:这是一个指向用于生成键值的路径名的C字符串指针。通常会选择一个已经存在的文件作为这个路径名,因为它可以确保唯一性。通常情况下,可以选择程序中的某个文件作为路径名,这样就可以确保不同的程序使用不同的路径名生成不同的键值。proj_id:这是一个整数值,用于进一步区分不同的 IPC 对象。这个值在给定路径名的范围内必须唯一。通常情况下,可以使用与程序相关的整数值作为 proj_id,以确保不同的程序使用不同的 proj_id 生成不同的键值。
返回值: 如果调用成功就返回对应的key值,调用失败就返回-1,同时设置错误码

对key和shmid的理解

key是在OS层面的,给OS看的标定共享内存的标识符,shmid是应用层的,是给我们看的,标定共享内存的标识。key和shmid的关系就像是inode和fd的关系

举个例子:

/*comm.hpp*/
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <cstring>
#include <cerrno>
#include <iostream>#define PATHNAME "."  // 使用当前目录作为项目目录
#define PROJ_ID 0x66  // 随机的项目id
#define MAX_SIZE 4096 // 创建的共享内存大小key_t getKey() // 封装获取key的函数
{key_t k = ftok(PATHNAME, PROJ_ID);if (k == -1){std::cerr << errno << " : " << strerror(errno) << std::endl;exit(1);}return k;
}int getShmHelper(key_t key, int flags) // 封装通过key来获取shmid的函数
{int shmid = shmget(key, MAX_SIZE, flags);if(shmid < 0){std::cerr << errno << " : " << strerror(errno) << std::endl;exit(2);}return shmid;
}int getShm(key_t key) // 用于找到已经创建的共享内存的shmid,所以传入的选项只有IPC_CREAT,不关心以前是否创建
{return getShmHelper(key, IPC_CREAT);
}int createShm(key_t key) // 用于创建,所以传入的选项中有IPC_EXCL,表示如果遇到冲突就创建失败
{return getShmHelper(key, IPC_CREAT | IPC_EXCL | 0666); // 0666表示创建的共享内存的权限
}
/*server.cc*/
#include "comm.hpp"int main()
{key_t k = getKey();printf("key:0x%x\n", k);int shmid = createShm(k);printf("%d\n", shmid);return 0;
}/*client.cc*/
#include "comm.hpp"int main()
{key_t k = getKey();printf("key:0x%x\n", k);int shmid = getShm(k);printf("%d\n", shmid);return 0;
}

image-20240119234453939

1.3.2 控制

上述的代码编译出来的程序第一次运行没有任何问题,但是如果再次运行server就会发现:

image-20240119234652904

这是因为创建的共享内存没有被释放,所以我们在使用完共享内存后需要释放

补充:查看IPC资源

我们可以通过命令ipcs系列指令来查看进程间通信相关信息

image-20240120182028480

  • 删除共享内存

    ipcrm -m + shmid

    image-20240120182434821

当然除了命令删除之外,还可以使用系统调用来对共享内存进行删除/控制:shmctl

image-20240120183057685

头文件:
#include <sys/ipc.h>
#include <sys/shm.h>
函数原型:
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数解释:shmid:要控制的共享内存的shmidcmd:要执行的命令,包括:IPC_STAT,IPC_SET,IPC_RMID,IPC_INFO,SHM_INFO,SHM_STAT,SHM_LOCK,SHM_UNLOCK.这里我们先不关注其他的,只关注IPC_RMID指令,这个指令是用来释放对应的shmid的buf:在其他指令中,有一些是需要获取到一些信息的,buf作为输出型参数来保存相关信息
返回值:对于释放共享内存来说,0表示成功,-1表示失败

使用shell脚本监视共享内存的情况

while :; do ipcs -m ; echo "##########################################"; sleep 1; done
#include "comm.hpp"int main()
{key_t k = getKey();printf("key:0x%x\n", k);int shmid = createShm(k);printf("%d\n", shmid);sleep(5);shmctl(shmid, IPC_RMID, nullptr); // 这里不需要获取信息,传入nullptr即可return 0;
}

image-20240120184045003

1.3.3 关联

在本节开始,我们说过有一个过程叫做把物理内存和进程地址空间关联起来,我们会使用一个系统调用关联:shmat,这里的at取attach的意义

image-20240120184534528

头文件:
#include <sys/types.h>
#include <sys/shm.h>
函数原型:
void *shmat(int shmid, const void *shmaddr, int shmflg);
参数解释:shmid:需要关联的shmidshmaddr:关联到进程地址空间的地址,我们绝大多数时间是不指定的,所以传nullptr即可shmflg:关联的选项,默认为0,表示读写权限
返回值:关联成功返回共享内存映射到进程地址空间中的起始地址,失败返回-1并设置错误码
void *attachShm(int shmid)
{void *mem = shmat(shmid, nullptr, 0);if((long long)mem == -1L) // 这里由于我们的机器是64位的,所以一个地址占8个字节,所以需要转成long long类型判断是否正确关联{std::cout << errno << " : " << strerror(errno) << std::endl;exit(3);}return mem;
}

1.3.4 去关联

有关联,那么对应的就有去关联的操作,去关联使用的系统调用是shmdt

image-20240120185829638

头文件:
#include <sys/types.h>
#include <sys/shm.h>
函数原型:
int shmdt(const void *shmaddr);
参数解释:shmaddr:需要去关联的进程地址空间
返回值:如果调用成功就返回0,否则就返回-1,同时设置错误码
void detachShm(void* start)
{if(shmdt(start) == -1){std::cerr << errno << " : " << strerror(errno) << std::endl;}
}

1.3.6 实战——实现server和client端的通信

/*comm.hpp*/
#pragma once
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#include <cstring>
#include <cerrno>
#include <iostream>#define PATHNAME "."  // 使用当前目录作为项目目录
#define PROJ_ID 0x66  // 随机的项目id
#define MAX_SIZE 4096 // 创建的共享内存大小key_t getKey() // 封装获取key的函数
{key_t k = ftok(PATHNAME, PROJ_ID);if (k == -1){std::cerr << errno << " : " << strerror(errno) << std::endl;exit(1);}return k;
}int getShmHelper(key_t key, int flags) // 封装通过key来获取shmid的函数
{int shmid = shmget(key, MAX_SIZE, flags);if(shmid < 0){std::cerr << errno << " : " << strerror(errno) << std::endl;exit(2);}return shmid;
}int getShm(key_t key) // 用于找到已经创建的共享内存的shmid,所以传入的选项只有IPC_CREAT,不关心以前是否创建
{return getShmHelper(key, IPC_CREAT);
}int createShm(key_t key) // 用于创建,所以传入的选项中有IPC_EXCL,表示如果遇到冲突就创建失败
{return getShmHelper(key, IPC_CREAT | IPC_EXCL | 0666); // 0666表示创建的共享内存的权限
}void delShm(int shmid)
{if(shmctl(shmid, IPC_RMID, nullptr) == -1)// 这里不需要获取信息,传入nullptr即可{std::cerr << errno << " : " << strerror(errno) << std::endl;}
}void *attachShm(int shmid)
{void *mem = shmat(shmid, nullptr, 0);if((long long)mem == -1L) // 这里由于我们的机器是64位的,所以一个地址占8个字节,所以需要转成long long类型判断是否正确关联{std::cerr << errno << " : " << strerror(errno) << std::endl;exit(3);}return mem;
}void detachShm(void* start)
{if(shmdt(start) == -1){std::cerr << errno << " : " << strerror(errno) << std::endl;}
}
/*server.cc*/
#include "comm.hpp"int main()
{key_t k = getKey();                     // 通过共同的pathname和proj_id构建一个相互通信的进程之间的keyint shmid = createShm(k);               // 通过创建的key创建一段共享内存char *start = (char *)attachShm(shmid); // 将这段共享内存和当前进程地址空间关联// 使用共享内存通信while (true){std::cout << "client say# " << start << std::endl; // 这里可以直接读取通信信息,因为地址相同struct shmid_ds ds;shmctl(shmid, IPC_STAT, &ds); //    获取shmid的相关信息printf("获取属性:size:%d,pid:%d,myself:%d", ds.shm_segsz, ds.shm_cpid);sleep(1);}delShm(shmid); // 使用完之后去关联delShm(shmid); // 谁创建的共享内存谁来释放return 0;
}
/*client*/
#include "comm.hpp"int main()
{key_t k = getKey();                     // 通过共同的pathname和proj_id构建一个相互通信的进程之间的keyint shmid = getShm(k);                  // 通过创建的key获取指定的共享内存char *start = (char *)attachShm(shmid); // 将这段共享内存和当前进程地址空间关联// 使用共享内存通信const char *message = "hello server,我是另一个进程,正在和你通信"; // 通信信息pid_t id = getpid();int count = 1;while (true){sleep(5);snprintf(start, MAX_SIZE, "%s[pid:%d][消息编号:%d]", message, id, count++); // 直接讲通信信息写到start中即可}delShm(shmid); // 使用完之后去关联return 0;
}

image-20240120191913957

1.3.7 共享内存的特点

共享内存的生命周期是随OS的,而不是随进程的,这是所有System V进程间通信的共性

共享内存的优点:共享内存是所有进程间通信速度是最快的,因为共享内存是被双方所共享,只要写入对方就能立即看到,能大大减少数据的拷贝次数。

但是综合考虑管道和共享内存,考虑键盘输入,和显示器输出,对于同一份数据:共享内存有几次数据拷贝,管道有几次数据拷贝

管道:需要通过键盘输入到自己定义的缓冲区char buffer[],将数据拷贝到buffer中,调用write接口在把buffer里的数据拷贝到管道里,

另一进程也有定义buffer缓冲区,调用read读取把数据从管道里读取到buffer里,在把数据显示到显示器上:

img

共享内存:通过映射关系

img

共享内存的缺点:不给我们进行同步和互斥的操作,没有对数据做任何保护。客户端和服务端没做保护,如果想做保护要用到信号量,对共享内存进行保护,写完通过读端进行读取。

2. 消息队列(不常用)

2.1 消息队列的概念

消息队列是OS提供的内核级队列,消息队列提供了一个从一个进程向另外一个进程发送一块数据的方法,每个数据块都被认为是有一个类型,接收者进程接收的数据块可以有不同的类型值

 struct msqid_ds {struct ipc_perm msg_perm;     /* Ownership and permissions */time_t          msg_stime;    /* Time of last msgsnd(2) */time_t          msg_rtime;    /* Time of last msgrcv(2) */time_t          msg_ctime;    /* Time of last change */unsigned long   __msg_cbytes; /* Current number of bytes inqueue (nonstandard) */msgqnum_t       msg_qnum;     /* Current number of messagesin queue */msglen_t        msg_qbytes;   /* Maximum number of bytesallowed in queue */pid_t           msg_lspid;    /* PID of last msgsnd(2) */pid_t           msg_lrpid;    /* PID of last msgrcv(2) */};

消息队列数据结构的第一个成员是msg_perm,它和shm_perm是同一个类型的结构体变量,ipc_perm结构体的定义如下:

struct ipc_perm {key_t          __key;       /* Key supplied to msgget(2) */uid_t          uid;         /* Effective UID of owner */gid_t          gid;         /* Effective GID of owner */uid_t          cuid;        /* Effective UID of creator */gid_t          cgid;        /* Effective GID of creator */unsigned short mode;        /* Permissions */unsigned short __seq;       /* Sequence number */
};

2.2 消息队列的使用

2.2.1 获取消息队列

msgget

image-20240120192645998

2.2.2 控制消息队列

msgctl

image-20240120192711808

2.2.3 发送和接收数据

msgsndmsgrcv

image-20240120192752479

3. 信号量

关于信号量的知识,我们将会在后面多线程的地方详细讲解,这里先进行一些概念的扫盲

3.1 信号量的概念扫盲

  • 信号量的本质是一个计数器**,通常用来表示公共资源中,资源数的多少问题。信号量主要用于同步和互斥的。

  • 公共资源:能被多个进程同时访问的资源,访问没有保护的公共资源可能会导致数据不一致问题。要让不同的进程看到同一份资源是为了通信,通信是为了让进程间实现协同,而进程之间具有独立性,所以为了解决独立性问题要让进程看到同一份资源,但是会导致数据不一致的问题。

  • 临界资源:被保护起来的公共资源

  • 临界区:进程要使用资源一定是该进程有对应的代码来访问这部分临界资源,这段代码就是临界区,但是多个进程看到同一份资源是少数情况,大部分申请自己的资源用自己的代码区访问。

  • 非临界区:不访问公共资源的代码。

如何保护公共资源:互斥&&同步

互斥:由于各进程要求共享资源,而且有些资源需要互斥使用,因此各进程间竞争使用这些资源,进程的这种关系为进程的互斥

原子性:要么不做、要么做完两态的这种情况。比如支付转账

如果用全局的整数来替代信号量?

全局的整数在父子关系的进程上都看不到,要发生写时拷贝,而不同的进程更看不到,所以进程间想看到同一个计数器得让进程看到同一个计数器。

为什么要信号量?

当我们想要某种资源的时候可以通过信号量进行预;,共享资源被使用的方式:作为一个整体使用;划分成为一个一个的资源部分

3.2 内核中信号量的相关数据结构

struct semid_ds {struct ipc_perm sem_perm;  /* Ownership and permissions */time_t          sem_otime; /* Last semop time */time_t          sem_ctime; /* Last change time */unsigned long   sem_nsems; /* No. of semaphores in set */
};

信号量数据结构的第一个成员也是ipc_perm类型的结构体变量,ipc_perm结构体的定义如下:

struct ipc_perm {key_t          __key; /* Key supplied to semget(2) */uid_t          uid;   /* Effective UID of owner */gid_t          gid;   /* Effective GID of owner */uid_t          cuid;  /* Effective UID of creator */gid_t          cgid;  /* Effective GID of creator */unsigned short mode;  /* Permissions */unsigned short __seq; /* Sequence number */
};

3.3 信号量的PV操作

我们知道信号量本质上就是一个临界资源的计数器,有进程要使用这个临界资源,就会导致可用的临界资源数量减少,使用完之后归还临界资源会导致可用的临界资源增多

  • P操作:向OS预定临界资源,会导致现有临界资源减少
  • V操作:向OS归还临界资源,会导致现有临界资源增加

假设信号量为sem,那么P操作相当于sem++,V操作相当于sem--,注意这里的++ 和 - -操作都是原子的

如果信号量的初始值是1就代表了访问公共资源作为一个整体来使用。二元信号量提供互斥功能

3.4 信号量相关函数

3.4.1 申请信号量

semget

image-20240120194422012

3.4.2 控制信号量

semctl

image-20240120194438064

3.4.3 信号量的操作PV

semop

image-20240120194454493


关于system V标准的进程间通信的思考

我们可以发现,共享内存、消息队列、信号量接口相似度非常高,获取与删除,都是system V标准的进程间通信。

OS如何管理:先描述,在组织,对相关资源的内核数据结构做管理,对于共享内存、消息队列、信号量的第一个成员都是ipc_perm:

struct ipc_perm {key_t          __key;    /* Key supplied to shmget(2) */uid_t          uid;      /* Effective UID of owner */gid_t          gid;      /* Effective GID of owner */uid_t          cuid;     /* Effective UID of creator */gid_t          cgid;     /* Effective GID of creator */unsigned short mode;     /* Permissions + SHM_DEST andSHM_LOCKED flags */unsigned short __seq;    /* Sequence number */
};

虽然内部的属性差别很大,但是维护它们的数据结构的第一个成员确实一样的,都是ipc_perm类型的成员变量,都可以通过key来标识唯一性。这样设计的好处:在操作系统内可以定义一个struct ipc_perm类型的数组,此时每当我们申请一个IPC资源,就在该数组当中开辟一个这样的结构。((struct shmid_ds*)perms[0],强转,此时就可以访问其他剩下的属性)


本节完…

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

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

相关文章

磁盘分区机制

lsblk查看分区 Linux分区 挂载的经典案例 1. 虚拟机增加磁盘 点击这里&#xff0c;看我的这篇文章操作 添加之后&#xff0c;需要重启系统&#xff0c;不重启在系统里看不到新硬盘哦 出来了&#xff0c;但还没有分区 2. 分区 还没有格式化 3. 格式化磁盘 4. 挂载 5. 卸载…

国标GB28181安防视频监控EasyCVR级联后上级平台视频加载慢的原因排查

国标GB28181协议安防视频监控系统EasyCVR视频综合管理平台&#xff0c;采用了开放式的网络结构&#xff0c;可以提供实时远程视频监控、视频录像、录像回放与存储、告警、语音对讲、云台控制、平台级联、磁盘阵列存储、视频集中存储、云存储等丰富的视频能力&#xff0c;同时还…

一、用户管理中心——前端初始化

一、Ant Design Pro初始化 1.创建空文件夹 2.打开Ant Design Pro官网 3.打开终端进行初始化 在终端输入npm i ant-design/pro-cli -g 在终端输入pro create myapp 选择umi3 选择simple 项目创建成功后&#xff0c;在文件夹中出现myapp 4.安装依赖 使用vscode打开项目 …

STL中的stack、queue以及deque

目录 一、关于deque容器&#xff08;双端队列&#xff09; 1、deque的底层实现 2、deque的缺点 3、关于stack与squeue默认使用deque容器 二、stack简介 1、stack的成员函数&#xff08;接口&#xff09; 2、stack的模拟实现 三、queue简介 1、queue的成员函数&#xff08…

安全防御-基础认知

目录 安全风险能见度不足&#xff1a; 常见的网络安全术语 &#xff1a; 常见安全风险 网络的基本攻击模式&#xff1a; 病毒分类&#xff1a; 病毒的特征&#xff1a; 常见病毒&#xff1a; 信息安全的五要素&#xff1a; 信息安全的五要素案例 网络空间&#xff1a…

docker配置阿里云镜像加速器

1、阿里云镜像加速器地址获取&#xff1a; 2、配置ECS镜像加速器&#xff0c;重启docker mkdir -p /etc/docker tee /etc/docker/daemon.json <<-EOF {"registry-mirrors": ["https://2lg9kp55.mirror.aliyuncs.com"] } EOF sudo systemctl daemon-…

谈判(贪心算法)

题目 import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Scanner;public class Main {public static void main(String[] args) { Scanner sc new Scanner(System.in);int n sc.nextInt();sc.nextLine();List<Integ…

H3C交换机S6850配置M-LAG三层转发

正文共&#xff1a;1999 字 30 图&#xff0c;预估阅读时间&#xff1a;3 分钟 前面提到M-LAG是一种跨设备链路聚合技术&#xff0c;将两台物理设备在聚合层面虚拟成一台设备来实现跨设备链路聚合&#xff0c;从而提供设备级冗余保护和流量负载分担。 之前已经做了DRNI的三层转…

微前端小记

步骤 将普通的项目改造成 qiankun 主应用基座&#xff0c;需要进行三步操作&#xff1a; 1. 创建微应用容器 - 用于承载微应用&#xff0c;渲染显示微应用&#xff1b; a. 设置路由routeb.主应用的布局包括&#xff1a; 主应用菜单&#xff0c;用于渲染菜单主应用渲染区域&a…

ubuntu安装vm和Linux

1、下载Ubuntu Index of /releaseshttps://old-releases.ubuntu.com/releases/ 2、下载VMware 官方正版VMware下载&#xff08;16 pro&#xff09;&#xff1a;https://www.aliyundrive.com/s/wF66w8kW9ac 下载Linux系统镜像&#xff08;阿里云盘不限速&#xff09;&#xff…

webpack 核心武器:loader 和 plugin 的使用指南(上)

&#x1f90d; 前端开发工程师、技术日更博主、已过CET6 &#x1f368; 阿珊和她的猫_CSDN博客专家、23年度博客之星前端领域TOP1 &#x1f560; 牛客高级专题作者、打造专栏《前端面试必备》 、《2024面试高频手撕题》 &#x1f35a; 蓝桥云课签约作者、上架课程《Vue.js 和 E…

Twisted Circuit洛谷绿题题解

Twisted Circuit 题面翻译 读入四个整数 0 0 0 或者 1 1 1&#xff0c;作为如图所示的电路图的输入。请输出按照电路图运算后的结果。 感谢PC_DOS 提供的翻译 题目描述 输入格式 The input consists of four lines, each line containing a single digit 0 or 1. 输出格…

读书笔记之《万物起源》:宇宙与人类的极简史

《万物起源&#xff1a;从宇宙大爆炸到文明的兴起》讲述了从大爆炸直到今日&#xff0c;约140亿年间所有重大事物的起源&#xff0c;依次覆盖了量子力学&#xff0c;天体物理学&#xff0c;化学&#xff0c;行星科学&#xff0c;地质学&#xff0c;生物学和人类历史等等学科。 …

08- OpenCV:形态学操作(膨胀与腐蚀 、提取水平与垂直线)

目录 前言 一、膨胀&#xff08;Dilation&#xff09;与 腐蚀&#xff08;Erosion&#xff09; 二、形态学操作 1、开操作&#xff08;Opening&#xff09; 2、闭操作&#xff08;Closing&#xff09; 3、形态学梯度&#xff08;Morphological Gradient&#xff09; 4、…

Spring成长之路—Spring MVC

在分享SpringMVC之前&#xff0c;我们先对MVC有个基本的了解。MVC(Model-View-Controller)指的是一种软件思想&#xff0c;它将软件分为三层&#xff1a;模型层、视图层、控制层 模型层即Model&#xff1a;负责处理具体的业务和封装实体类&#xff0c;我们所知的service层、poj…

LLM之RAG实战(十九)| 利用LangChain、OpenAI、ChromaDB和Streamlit构建RAG

生成式人工智能以其创造与上下文相关内容的能力彻底改变了技术&#xff0c;开创了人工智能可能性的新时代。其核心是检索增强生成&#xff08;RAG&#xff09;&#xff0c;将信息检索与LLM相结合&#xff0c;从外部文档中产生智能、知情的响应。 本文将深入研究使用ChromaDB构建…

三.Winform使用Webview2加载本地HTML页面

Winform使用Webview2加载本地HTML页面 往期目录创建Demo2界面创建HTML页面在Demo2窗体上添加WebView2和按钮加载HTML查看效果 往期目录 往期相关文章目录 专栏目录 创建Demo2界面 经过前面两小节 一.Winform使用Webview2(Edge浏览器核心) 创建demo(Demo1)实现回车导航到指定…

广和通AI解决方案“智”赋室外机器人迈向新天地!

大模型趋势下&#xff0c;行业机器人将具备更完善的交互与自主能力&#xff0c;逐步迈向AI 2.0时代&#xff0c;成为人工智能技术全面爆发的重要基础。随着行业智能化&#xff0c;更多机器人应用将从“室内”走向“室外”&#xff0c;承担更多高风险、高智能工作。复杂的室外环…

代码随想录二刷 | 二叉树 | 把二叉搜索树转换为累加树

代码随想录二刷 &#xff5c; 二叉树 &#xff5c; 把二叉搜索树转换为累加树 题目描述解题思路递归法迭代法 代码实现递归法迭代法 题目描述 538.把二叉搜索树转换为累加树 给出二叉 搜索 树的根节点&#xff0c;该树的节点值各不相同&#xff0c;请你将其转换为累加树&…

分布式 session

分布式 session 种 session 的时候需要注意范围&#xff0c;也就是 cookie.domain。 比如两个域名&#xff1a;a.heo.com&#xff0c;b.heo.com。如果要共享 cookie&#xff0c;可以种一个更高层的公共域名&#xff0c;比如 heo.com。 当服务器 A &#xff08;localhost:808…