【Linux】24.进程间通信(3)

文章目录

    • 3.6 systemv共享内存
      • 3.6.1 共享内存函数
      • 3.6.3 一个简单的共享内存代码实现
      • 3.6.4 一个复杂的共享内存代码实现
      • 3.6.4 key和shmid的主要区别:
    • 3.7 systemv消息队列(了解)
    • 3.8 systemv信号量(了解)
      • 进程互斥
      • 四个问题
      • 理解信号量


3.6 systemv共享内存

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

进程间通信的本质是:先让不同的进程,看到同一份资源。

72881d84255fc303b20a3ab2711e9853

如果要释放共享内存:要去除关联,释放共享内存

上面的操作都是进程直接做的吗?

不是。直接由操作系统来做。

共享内存的生命周期是随内核的。

用户不主动关闭,共享内存会一直存在。除非内核重启(用户释放)


生成IPC(进程间通信)的key值:

函数原型:

key_t ftok(const char *pathname, int proj_id);

参数含义:

  1. pathname:一个已存在的文件路径
    • 必须是一个已存在的文件或目录的路径
    • 用于生成唯一的 key
    • 程序需要有该路径的访问权限
  2. proj_id:项目标识符
    • 项目标识符,用于区分不同的IPC资源
    • 只有低8位有效(1-255
    • 通常使用字符或数字

返回值:

  • 成功:成功:返回一个非负的 key 值
  • 失败:返回-1

3.6.1 共享内存函数

shmget函数

功能:用来创建共享内存
原型int shmget(key_t key, size_t size, int shmflg);
参数key:这个共享内存段名字size:共享内存大小shmflg:由九个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样的
返回值:成功返回一个非负整数,即该共享内存段的标识码;失败返回-1

shmat函数

功能:将共享内存段连接到进程地址空间
原型void *shmat(int shmid, const void *shmaddr, int shmflg);
参数shmid: 共享内存标识shmaddr:指定连接的地址shmflg:它的两个可能取值是SHM_RND和SHM_RDONLY
返回值:成功返回一个指针,指向共享内存第一个节;失败返回-1

说明:

shmaddr为NULL,核心自动选择一个地址
shmaddr不为NULL且shmflg无SHM_RND标记,则以shmaddr为连接地址。
shmaddr不为NULL且shmflg设置了SHM_RND标记,则连接的地址会自动向下调整为SHMLBA的整数倍。公式:shmaddr - (shmaddr % SHMLBA)
shmflg=SHM_RDONLY,表示连接操作用来只读共享内存

shmdt函数

功能:将共享内存段与当前进程脱离
原型int shmdt(const void *shmaddr);
参数shmaddr: 由shmat所返回的指针
返回值:成功返回0;失败返回-1
注意:将共享内存段与当前进程脱离不等于删除共享内存段

shmctl函数

功能:用于控制共享内存
原型int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数shmid:由shmget返回的共享内存标识码cmd:将要采取的动作(有三个可取值)#define IPC_STAT    2   // 获取共享内存状态#define IPC_SET     1   // 设置共享内存状态#define IPC_RMID    0   // 删除共享内存段buf:指向一个保存着共享内存的模式状态和访问权限的数据结构
返回值:成功返回0;失败返回-1

981fea39a7c0c97a0551b9f2613d1954

  1. shmget - 创建/获取共享内存
int shmid = shmget(key, 1024, IPC_CREAT | 0666);
  • 相当于"申请"一块共享内存
  • 类比文件操作中的 open 创建文件
  • 返回共享内存标识符(shmid),用于后续操作
  1. shmat - 挂载/连接共享内存
void *addr = shmat(shmid, NULL, 0);
  • 将共享内存映射到进程的地址空间
  • 类比把硬盘上的文件加载到内存
  • 返回可以直接操作的内存指针
  1. shmdt - 断开连接
shmdt(addr);
  • 解除进程与共享内存的映射关系
  • 类比关闭打开的文件
  • 不会删除共享内存,只是当前进程不再使用
  1. shmctl - 控制共享内存
shmctl(shmid, IPC_RMID, NULL);  // 删除共享内存
  • 用于删除或管理共享内存
  • 类比文件的删除、权限修改等操作

3.6.3 一个简单的共享内存代码实现

写进程 (writer.cpp):

#include <iostream>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <string.h>
#include <unistd.h>#define SHM_SIZE 1024int main() {// 1. 生成keykey_t key = ftok(".", 'x');// 这里的 "." 表示当前目录必须是一个存在且可访问的路径// 这里的 'x' 是一个字符,会被转换为8位整数,范围是1-255,只有低8位有效if(key == -1) {std::cout << "ftok失败" << std::endl;return 1;}// 2. 创建共享内存int shmid = shmget(key, SHM_SIZE, IPC_CREAT | 0666);if(shmid == -1) {std::cout << "shmget失败" << std::endl;return 1;}// 3. 连接共享内存void* shmaddr = shmat(shmid, NULL, 0);if(shmaddr == (void*)-1) { //(void*)-1 是 shmat 失败时的返回值,等同于 MAP_FAILED 或 -1std::cout << "shmat失败" << std::endl;return 1;}// 4. 写入数据const char* message = "Hello from shared memory!";strcpy((char*)shmaddr, message); // shmaddr 是共享内存的起始地址// (char*) 是类型转换,将 void* 转换为 char*// strcpy 将字符串复制到共享内存中std::cout << "写入数据: " << message << std::endl;// 5. 分离共享内存shmdt(shmaddr);return 0;
}

读进程 (reader.cpp):

#include <iostream>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <string.h>
#include <unistd.h>#define SHM_SIZE 1024int main() {// 1. 生成相同的keykey_t key = ftok(".", 'x');if(key == -1) {std::cout << "ftok失败" << std::endl;return 1;}// 2. 获取共享内存int shmid = shmget(key, SHM_SIZE, 0666);if(shmid == -1) {std::cout << "shmget失败" << std::endl;return 1;}// 3. 连接共享内存void* shmaddr = shmat(shmid, NULL, 0);if(shmaddr == (void*)-1) {std::cout << "shmat失败" << std::endl;return 1;}// 4. 读取数据std::cout << "读取数据: " << (char*)shmaddr << std::endl;// 5. 分离共享内存shmdt(shmaddr);// 6. 删除共享内存shmctl(shmid, IPC_RMID, NULL);return 0;
}

使用方法:

  1. 编译:
g++ writer.cpp -o writer
g++ reader.cpp -o reader
  1. 运行:
# 终端1
./writer# 终端2
./reader

主要函数说明:

  1. ftok(): 生成IPC键值
  2. shmget(): 创建或获取共享内存段
  3. shmat(): 连接共享内存段到进程地址空间
  4. shmdt(): 断开共享内存段连接
  5. shmctl(): 控制共享内存段(如删除)

执行时序:

写进程                     共享内存                      读进程|                          |                           ||                          |                           ||---ftok(".", 'x')         |                           ||生成key                    |                           ||                          |                           ||---shmget()               |                           ||创建共享内存--------------->|                           ||                          |                           ||---shmat()                |                           ||连接共享内存<-------------->|                           ||                          |                           ||---strcpy()               |                           ||写入数据------------------>|                           ||                          |                           ||---shmdt()                |                           ||断开连接------------------>|                           ||                          |                           ||                          |   ftok(".", 'x')----------||                          |   生成相同的key             ||                          |                           ||                          |   shmget()----------------||                          |<--获取共享内存              ||                          |                           ||                          |   shmat()-----------------||                          |<->连接共享内存              ||                          |                           ||                          |   读取数据-----------------||                          |-->读取内容                 ||                          |                           ||                          |   shmdt()-----------------||                          |<--断开连接                 ||                          |                           ||                          |   shmctl()----------------||                          |x--删除共享内存              ||                          |                           |

3.6.4 一个复杂的共享内存代码实现

makefile

.PHONY:all
all:processa processbprocessa:processa.ccg++ -o $@ $^ -g -std=c++11
processb:processb.ccg++ -o $@ $^ -g -std=c++11.PHONY:clean
clean:rm -f processa processb

log.hpp

#pragma once  // 防止头文件重复包含// 包含必要的系统头文件
#include <iostream>
#include <time.h>
#include <stdarg.h>  // 用于可变参数
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>#define SIZE 1024  // 缓冲区大小// 定义日志级别
#define Info 0
#define Debug 1
#define Warning 2
#define Error 3
#define Fatal 4// 定义日志输出方式
#define Screen 1      // 输出到屏幕
#define Onefile 2     // 输出到单个文件
#define Classfile 3   // 根据日志级别输出到不同文件#define LogFile "log.txt"  // 默认日志文件名class Log
{public:Log(){printMethod = Screen;  // 默认输出到屏幕path = "./log/";      // 默认日志路径}// 设置日志输出方式void Enable(int method){printMethod = method;}// 将日志级别转换为字符串std::string levelToString(int level){switch (level){case Info:return "Info";case Debug:return "Debug";case Warning:return "Warning";case Error:return "Error";case Fatal:return "Fatal";default:return "None";}}// 根据不同的输出方式打印日志void printLog(int level, const std::string &logtxt){switch (printMethod){case Screen:std::cout << logtxt << std::endl;break;case Onefile:printOneFile(LogFile, logtxt);break;case Classfile:printClassFile(level, logtxt);break;default:break;}}// 输出到单个文件void printOneFile(const std::string &logname, const std::string &logtxt){std::string _logname = path + logname;int fd = open(_logname.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0666);if (fd < 0)return;write(fd, logtxt.c_str(), logtxt.size());close(fd);}// 根据日志级别输出到不同文件void printClassFile(int level, const std::string &logtxt){std::string filename = LogFile;filename += ".";filename += levelToString(level);printOneFile(filename, logtxt);}// 重载函数调用运算符,支持可变参数的日志打印void operator()(int level, const char *format, ...){// 获取当前时间time_t t = time(nullptr);struct tm *ctime = localtime(&t);char leftbuffer[SIZE];// 格式化时间和日志级别信息snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", levelToString(level).c_str(),ctime->tm_year + 1900, ctime->tm_mon + 1, ctime->tm_mday,ctime->tm_hour, ctime->tm_min, ctime->tm_sec);// 处理可变参数va_list s;va_start(s, format);char rightbuffer[SIZE];vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);va_end(s);// 组合完整的日志文本char logtxt[SIZE * 2];snprintf(logtxt, sizeof(logtxt), "%s %s\n", leftbuffer, rightbuffer);printLog(level, logtxt);}~Log(){}// void logmessage(int level, const char *format, ...)// {//     time_t t = time(nullptr);//     struct tm *ctime = localtime(&t);//     char leftbuffer[SIZE];//     snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", levelToString(level).c_str(),//              ctime->tm_year + 1900, ctime->tm_mon + 1, ctime->tm_mday,//              ctime->tm_hour, ctime->tm_min, ctime->tm_sec);//     // va_list s;//     // va_start(s, format);//     char rightbuffer[SIZE];//     vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);//     // va_end(s);//     // 格式:默认部分+自定义部分//     char logtxt[SIZE * 2];//     snprintf(logtxt, sizeof(logtxt), "%s %s\n", leftbuffer, rightbuffer);//     // printf("%s", logtxt); // 暂时打印//     printLog(level, logtxt);// }private:int printMethod;       // 日志输出方式std::string path;      // 日志文件路径
};// int sum(int n, ...)
// {
//     va_list s; // char*
//     va_start(s, n);//     int sum = 0;
//     while(n)
//     {
//         sum += va_arg(s, int); // printf("hello %d, hello %s, hello %c, hello %d,", 1, "hello", 'c', 123);
//         n--;
//     }//     va_end(s); //s = NULL
//     return sum;
// }

comm.hpp

#ifndef __COMM_HPP__
#define __COMM_HPP__// 包含必要的头文件
#include <iostream>
#include <string>
#include <cstdlib>
#include <cstring>
#include <sys/ipc.h>    // 系统IPC功能
#include <sys/shm.h>    // 共享内存
#include <sys/types.h>
#include <sys/stat.h>#include "log.hpp"using namespace std;Log mylog;  // 全局日志对象// 共享内存的大小一般建议是4096的整数倍
const int size = 4096; 
const string pathname="/home/ydk_108";  // 用于生成key的路径
const int proj_id = 0x6666;         // 项目ID// 获取IPC key
key_t GetKey()
{key_t k = ftok(pathname.c_str(), proj_id);if(k < 0){mylog(Fatal, "ftok error: %s", strerror(errno));exit(1);}mylog(Info, "ftok success, key is : 0x%x", k);return k;
}// 获取共享内存的辅助函数
int GetShareMemHelper(int flag)
{key_t k = GetKey();int shmid = shmget(k, size, flag);if(shmid < 0){mylog(Fatal, "create share memory error: %s", strerror(errno));exit(2);}mylog(Info, "create share memory success, shmid: %d", shmid);return shmid;
}// 创建新的共享内存
int CreateShm()
{return GetShareMemHelper(IPC_CREAT | IPC_EXCL | 0666);
}// 获取已存在的共享内存
int GetShm()
{return GetShareMemHelper(IPC_CREAT); 
}#define FIFO_FILE "./myfifo"  // 命名管道文件路径
#define MODE 0664             // 文件权限// 错误码枚举
enum
{FIFO_CREATE_ERR = 1,FIFO_DELETE_ERR,FIFO_OPEN_ERR
};// 初始化类,用于创建和清理命名管道
class Init
{
public:Init(){// 先尝试删除已存在的管道文件unlink(FIFO_FILE);  // 忽略返回值,因为文件可能不存在// 创建命名管道int n = mkfifo(FIFO_FILE, MODE);if (n == -1){perror("mkfifo");exit(FIFO_CREATE_ERR);}}~Init(){// 删除命名管道int m = unlink(FIFO_FILE);if (m == -1){perror("unlink");exit(FIFO_DELETE_ERR);}}
};#endif

processa.cc

#include "comm.hpp"extern Log mylog;int main()
{Init init;  // 创建命名管道int shmid = CreateShm();  // 创建共享内存// 将共享内存映射到进程地址空间char *shmaddr = (char*)shmat(shmid, nullptr, 0);// 以只读方式打开命名管道int fd = open(FIFO_FILE, O_RDONLY);if (fd < 0){mylog(Fatal, "error string: %s, error code: %d", strerror(errno), errno);exit(FIFO_OPEN_ERR);}struct shmid_ds shmds;while(true){// 读取管道中的通知char c;ssize_t s = read(fd, &c, 1);if(s == 0) break;  // 写端关闭else if(s < 0) break;  // 读取错误// 直接从共享内存读取数据cout << "client say@ " << shmaddr << endl;sleep(1);// 获取并打印共享内存的状态信息shmctl(shmid, IPC_STAT, &shmds);cout << "shm size: " << shmds.shm_segsz << endl;cout << "shm nattch: " << shmds.shm_nattch << endl;printf("shm key: 0x%x\n",  shmds.shm_perm.__key);cout << "shm mode: " << shmds.shm_perm.mode << endl;}// 清理资源shmdt(shmaddr);  // 解除内存映射shmctl(shmid, IPC_RMID, nullptr);  // 删除共享内存close(fd);  // 关闭管道return 0;
}

processb.cc

#include "comm.hpp"int main()
{int shmid = GetShm();  // 获取已存在的共享内存// 将共享内存映射到进程地址空间char *shmaddr = (char*)shmat(shmid, nullptr, 0);// 以只写方式打开命名管道int fd = open(FIFO_FILE, O_WRONLY);if (fd < 0){mylog(Fatal, "error string: %s, error code: %d", strerror(errno), errno);exit(FIFO_OPEN_ERR);}while(true){cout << "Please Enter@ ";// 读取用户输入并直接写入共享内存fgets(shmaddr, 4096, stdin);// 向管道写入一个字符,通知接收端write(fd, "c", 1);}// 清理资源shmdt(shmaddr);  // 解除内存映射close(fd);  // 关闭管道return 0;
}

打印:

c297c18f8ca1b2702ec33357f46ef20e

关于key

  1. key是一个数字,这个数字是几,不重要。关键在于它必须在内核中具有唯一性,能够让不同的进程进行唯一性标识。

  2. 第一个进程可以通过kev创建共享内存,第二个之后的进程,只要拿着同一个key就可以和第一个进程看到同一个共享内存了。

  3. 对于一个已经创建好的共享内存,key在哪?

    key在共享内存的描述对象中。

  4. 第一次创建的时候,必须有一个key了。怎么有?

  5. key 类似路径唯一


3.6.4 key和shmid的主要区别:

  1. 基本概念

    key:是一个用户定义的值,用来标识共享内存段的访问权限,类似于文件路径名

    shmid:是系统分配的共享内存段标识符,是系统内部使用的唯一标识符

  2. 使用时机

    key:在创建或获取共享内存时使用

    shmid:在共享内存创建后由系统返回,后续操作都使用shmid

  3. 代码示例

#include <sys/shm.h>// 使用key创建共享内存
key_t key = ftok("/tmp", 'A');  // 创建key
int shmid = shmget(key, 1024, IPC_CREAT | 0666); // 用key获取shmid// 后续操作使用shmid
void *addr = shmat(shmid, NULL, 0);  // 连接共享内存
shmctl(shmid, IPC_RMID, NULL);  // 删除共享内存
  1. 关系

    一个key可以对应一个shmid

    key是用户层面的标识

    shmid是系统层面的标识

  2. 生命周期

    key:可以重复使用

    shmid:随共享内存段的存在而存在,删除后失效


3.7 systemv消息队列(了解)

消息队列提供了一个从一个进程向另外一个进程发送一块数据的方法

每个数据块都被认为是有一个类型,接收者进程接收的数据块可以有不同的类型值

特性方面:IPC资源必须删除,否则不会自动清除,除非重启,所以system V IPC资源的生命周期随内核

通过消息队列想让AB进行通信那么首先要让不同进程看到同一份资源。

  1. 必须让不同进程看到同一个队列

  2. 允许不同的进程,向内核中发送带类型的数据块(通过类型来区分数据块是属于谁的)

A进程可以把它的数据块放到队列中,B进程可以把它的数据块放到队列中。

A进程就可以从队列中拿B进程给A进程发的数据块,反之亦然。

可以让A进程 <--以数据块的形式发送数据--> B进程。

2afaf5fef54063425c71bea5f3ab0008


3.8 systemv信号量(了解)

信号量主要用于同步和互斥的。什么是同步和互斥?

进程互斥

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

系统中某些资源一次只允许一个进程使用,称这样的资源为临界资源或互斥资源。

在进程中涉及到互斥资源的程序段叫临界区。

特性方面:IPC资源必须删除,否则不会自动清除,除非重启,所以system V IPC资源的生命周期随内核


四个问题

当我们的A正在写入,写入了一部分,就被B拿走了,导致双方发和收的数据不完整 – 数据不一致问题

  1. A B看到的同一份资源,共享资源,如果不加保护,会导致数据不一致问题

  2. 我们可以通过“加锁”形成互斥访问 – 任何时刻,只允许一个执行流访问共享资源 – 互斥

  3. 共享的,任何时刻只允许一个执行流访问(就是执行访问代码)的资源我们一般称为:临界资源(一般是操作系统和用户维护的内存空间)(管道也是临界资源)

  4. 举例:100行代码,5~10行代码才在访问临界资源。 我们访问临界资源的代码在:临界区


理解信号量

信号量的本质是一个计数器。

描述临界资源数量的多少。

  1. 申请计数器成功,就表示我具有访问资源的权限了

  2. 申请了计数器资源,我当前访问我要的资源了吗?没有。申请了计数器资源是对资源的预订机制

  3. 计数器可以有效保证进入共享资源的执行流的数量

  4. 所以每一个执行流,想访问共享资源中的一部分的时候,不是直接访问,而是先申请计数器资源。

程序员把这个"计数器",叫做信号量。

申请信号量,本质是对计数器--,P操作

释放资源,释放信号量,本质是对计数器进行++操作,V操作

申请和释放PV操作是原子性操作。

要么不做,要做就做完 — 两态的。没有“正在做”这样的概念。

信号量本质是一把计数器,PV操作,原子的。

执行流申请资源,必须先申请信号量资源,得到信号量之后,才能访问临界资源。

信号量值1,0两态的,二元信号量,就是互斥功能

申请信号量的本质:是对临界资源的预订机制。

信号量凭什么是进程间通信的一种?

  1. 通信不仅仅是通信数据,双方互相协同也是。

  2. 要协同,本质也是通信,信号量首先要被所有的通信进程看到。

mmap函数 – 也是共享内存。(仅作了解)

后面学习的信号和这里的信号量没有任何关系。

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

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

相关文章

2.Mkdocs配置说明(mkdocs.yml)【最新版】

官方文件&#xff1a;Changing the colors - Material for MkDocs 建议详细学习一下上面的官方网站↑↑↑ 我把我目前的配置文件mkdocs.yml代码写在下面&#x1f447;&#x1f3fb; #[Info] site_name: Mkdocs教程 #your site name 显示在左上角 site_url: http://wcowin.wo…

AI大模型:本地部署deepseek

一、安装lmstudio 1、下载网站&#xff1a; LM Studio - Discover, download, and run local LLMs 2、直接安装即可&#xff0c;记住安装的路径 二、下载deepseek模型 2.1、下载的流程 1、下载网站 https://huggingface.co/models 2、在搜索框输入&#xff1a;deepseek …

解析PHP文件路径相关常量

PHP文件路径相关常量包括以下几个常量&#xff1a; __FILE__&#xff1a;表示当前文件的绝对路径&#xff0c;包括文件名。 __DIR__&#xff1a;表示当前文件所在的目录的绝对路径&#xff0c;不包括文件名。 dirname(__FILE__)&#xff1a;等同于__DIR__&#xff0c;表示当前…

LLMs之data:synthetic-data-generator的简介、安装和使用方法、案例应用之详细攻略

LLMs之data&#xff1a;synthetic-data-generator的简介、安装和使用方法、案例应用之详细攻略 目录 synthetic-data-generator的简介 1、核心功能和优势 2、特点 synthetic-data-generator的安装和使用方法 1、安装 pip安装 安装依赖项 运行应用 2、使用方法 快速入…

Unity UI Default Shader分析

文章目录 UI默认材质和Default ShaderShader的属性定义Mask组件支持RectMask2D组件支持其他支持使用Unity UGUI时经常有自定义shader的需求,虽然我们可以直接按照shader lab的规范写出shader,使用也没问题,但如果能让自定义shader符合UI shader的规范,支持Mask,Rect2DMask…

【漫画机器学习】082.岭回归(或脊回归)中的α值(alpha in ridge regression)

岭回归&#xff08;Ridge Regression&#xff09;中的 α 值 岭回归&#xff08;Ridge Regression&#xff09;是一种 带有 L2​ 正则化 的线性回归方法&#xff0c;用于处理多重共线性&#xff08;Multicollinearity&#xff09;问题&#xff0c;提高模型的泛化能力。其中&am…

深入理解和使用定时线程池ScheduledThreadPoolExecutor

文章目录 前言认识定时线程池什么是定时线程池&#xff1f;定时线程池基本API使用定时线程池的应用场景1、定时任务调度2、缓存过期清理3、心跳检测4、延迟任务执行 定时线程池scheduleAtFixedRate与scheduleWithFixedDelay区别scheduleAtFixedRate案例demo&#xff08;period&…

【React】合成事件语法

React 合成事件是 React 为了处理浏览器之间的事件差异而提供的一种跨浏览器的事件系统。它封装了原生的 DOM 事件&#xff0c;提供了一致的事件处理机制。 合成事件与原生事件的区别&#xff1a; 合成事件是 React 自己实现的&#xff0c;封装了原生事件。合成事件依然可以通…

中小企业的采购流程,采购管理是如何进行的?

经营中小企业的&#xff0c;都明白高效采购管理的重要性。我见过不少中小企业&#xff0c;采购环节混乱无序&#xff0c;花费大量成本&#xff0c;却难以保障物资的优质供应。然而到底该如何梳理采购流程&#xff0c;怎样开展采购管理工作呢&#xff1f;这让众多中小企业主愁眉…

在线教程丨YOLO系列10年更新11个版本,最新模型在目标检测多项任务中达SOTA

YOLO (You Only Look Once) 是计算机视觉领域中最具影响力的实时目标检测算法之一&#xff0c;以其高精度与高效性深受业界青睐&#xff0c;广泛应用于自动驾驶、安防监控、医疗影像等领域。 该模型最早于 2015 年由华盛顿大学研究生 Joseph Redmon 发布&#xff0c;开创了将目…

IOPS与吞吐量、读写块大小及延迟之间的关系

IOPS&#xff08;每秒输入/输出操作次数&#xff09;、吞吐量、读写块大小及延迟是衡量存储系统性能的四个关键指标&#xff0c;它们之间存在密切的关系。以下从多个方面详细说明这些指标之间的关系&#xff1a; 1. IOPS与吞吐量的关系 公式关系&#xff1a;吞吐量&#xff0…

DeepSeek 部署过程中的问题

文章目录 DeepSeek 部署过程中的问题一、部署扩展&#xff1a;docker 部署 DS1.1 部署1.2 可视化 二、问题三、GPU 设置3.1 ollama GPU 的支持情况3.2 更新 GPU 驱动3.3 安装 cuda3.4 下载 cuDNN3.5 配置环境变量 四、测试 DeepSeek 部署过程中的问题 Windows 中 利用 ollama 来…

DeepSeek RAGFlow构建本地知识库系统

学习目标 DeepSeek RAGFlow 构建本地知识库系统 学习内容 下载安装Docker 1.1 Docker 是什么 1.2 下载Docker 1.3 安装Docker配置DockerRAGFlow 配置 3.1 下载RAGFlow 3.2 RAGFlow配置 3.3 启动RAGFlow Docker新建知识库 4.1 查看本机IP 4.2 OLLAMA_HOST 变量配置 4.3 添加模…

11 享元(Flyweight)模式

享元模式 1.1 分类 &#xff08;对象&#xff09;结构型 1.2 提出问题 做一个车管所系统&#xff0c;将会产生大量的车辆实体&#xff0c;如果每一个实例都保存自己的所有信息&#xff0c;将会需要大量内存&#xff0c;甚至导致程序崩溃。 1.3 解决方案 运用共享技术有效…

arcgis for js范围内天地图高亮,其余底图灰暗

在GIS地图开发中&#xff0c;有时我们需要突出显示某个特定区域&#xff0c;而将其他区域灰暗处理&#xff0c;以达到视觉上的对比效果。本文将介绍如何使用ArcGIS for JavaScript实现这一功能&#xff0c;具体效果为&#xff1a;在指定范围内&#xff0c;天地图高亮显示&#…

Spring AI + Ollama 实现 DeepSeek-R1 API 服务和调用

随着大语言模型的快速发展&#xff0c;越来越多的开发者开始探索如何将这些强大的推理模型本地化运行。DeepSeek-R1&#xff0c;作为一款性能卓越的开源AI模型&#xff0c;以其低成本和出色的推理能力在技术圈内引起了广泛关注。本文将详细介绍如何使用Ollama部署DeepSeek-R1&a…

Ubuntu 20.04配置网络

1&#xff0c;检查自己网络是否配通。 网络配置成功显示的网络图标 不成功的网络图标 如果看不见网络图标&#xff0c;可以使用ping命令。连接一下百度网。 ping www.baidu.com ping失败的样子 ping成功的样子 2&#xff0c;接下来进入正题&#xff0c;我们开始配置网络。 这…

ElasticSearch入门

目录 1._cat 2.索引一个 document 3.查询document 4.更新document 5.删除document 或 index 6.批量_bulk API 1._cat Get/_cat/nodes 查看所有节点 Get/_cat/indices 查看所有索引&#xff08;indices &#xff1a;index的复数) Get/_cat/master 查看…

java练习(8)

ps:题目来自力扣 给你一个数组 nums 和一个值 val&#xff0c;你需要 原地 移除所有数值等于 val 的元素。元素的顺序可能发生改变。然后返回 nums 中与 val 不同的元素的数量。 假设 nums 中不等于 val 的元素数量为 k&#xff0c;要通过此题&#xff0c;您需要执行以下操作…

Java常用类

文章目录 包装类(Wrapper)包装类的继承体系装箱和拆箱包装类与String类型的相互转换 String类创建 String 对象的两种方式String 类的常见方法案例演示 StringBuffer类类的继承体系String VS StringBufferStringBuffer构造器String 和 StringBuffer 相互转换StringBuffer 类常见…