Linux之进程间通信(二)

system V

system V共享内存内核中专门设计的通信的方式,  粗粒度划分操作系统分为进程管理, 内存管理, 文件系统, 驱动管理.., 粒度更细地分还有 进程间通信模块.

对于操作系统, 通信的场景有很多, 有以传送数据, 快速传送数据, 传送特定数据块, 进程间协同与控制以目的, 它们在接口实现上都不相同, 所以把操作系统中通信的方式聚集在一块, 接口统一之后形成了限于本主机通信的一种模式叫systemV

管道不属于system V, 它们是复用操作系统源代码, 属于比较原始的通信方式.


进程间通信的前提:必须让不同的进程看到同一份资源(必须由OS提供)

共享内存

原理

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

共享内存是专门设计用于IPC的, 它与进程地址空间共享区有关.

共享内存作为一种通信方式, 所有关联了这块内存的进程都可以使用它, 这块内存不再是只属于一个进程的.

1. 操作系统中一定会同时存在很多的共享内存, 所以共享内存也要被操作系统管理,
要认识到开辟一块空间的内存占用应当是物理内存空间共享内存的相关属性. 操作系统管理共享内存属性就是对其数据结构进行增删查改, 从而对它对应的物理内存空间进行实时监控管理.

共享内存结构: 

struct shmid_ds {
struct ipc_perm shm_perm; /* operation perms */
int shm_segsz; /* size of segment (bytes) */
__kernel_time_t shm_atime; /* last attach time */
__kernel_time_t shm_dtime; /* last detach time */
__kernel_time_t shm_ctime; /* last change time */
__kernel_ipc_pid_t shm_cpid; /* pid of creator */
__kernel_ipc_pid_t shm_lpid; /* pid of last operator */
unsigned short shm_nattch; /* no. of current attaches */
unsigned short shm_unused; /* compatibility */
void *shm_unused2; /* ditto - used by DIPC */
void *shm_unused3; /* unused */
};
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 */};

2. 因为操作系统内一定会同时存在许多共享内存, 那这么多的共享内存中如何保证 A进程创建的共享内存 需要和A通信的进程, 看到的是同一份共享内存呢?

凡是被创建共享内存, 相关属性结构体中必须有一个能体现出该共享内存的唯一性的数据,这个数据就是key. key不光是为了保证共享内存的唯一性, 也是为了通过 key 能够让其他进程识别该共享内存, 从而不同的进程才能选择特定的共享内存进行通信.

key被封装在上面的struct ipc_perm结构体内.

关于唯一性标识, 除了key还有一个叫shmid,  在应用这个共享内存的时候, 我们使用shmid来进行操作共享内存,类似文件操作的fd, 而 key 不要在应用层使用, 只用来在内核中标识shm的唯一性! 类似inode.

关于 key 和 shmid 需要介绍一下相关系统接口: 


系统接口

ftok

功能:用 路径名 和 项目标识符 转换为唯一标识符key返回

函数原型: key_t ftok(const char *pathname, int proj_id);

参数:

  • pathname: 文件路径名
  • proj_id: 项目标识符
  • 返回值: 标识某一个共享内存的key值

key的作用: 这里的key用于形成共享内存的唯一标识shmid, 而这个key的生成规则是通过路径名和项目标识符生成的, 所以对于同一个共享内存的使用者就可以通过规定这两个参数都能得到同样的key, 从而通过shmget创建/获取同样的共享内存.

shmget 

功能: 用来创建共享内存
原型: int shmget(key_t key, size_t size, int shmflg);
参数:

  • key: 这个共享内存段名字
  • size: 共享内存大小, 建议为4096的倍数
  • shmflg: 由九个权限标志构成, 它们的用法和创建文件时使用的mode模式标志是一样的
  • 返回值:成功返回一个非负整数, 即该共享内存段的标识码;失败返回-1 

 创建共享内存需要用到这两个标志:

首先要明确, shmget接口可以实现创建和使用共享内存,

1. 单独传入IPC_CREAT, 共享内存 存在就返回, 不存在就创建并返回

2. 传入IPC_CREAT|IPC_EXCL, 共享内存 存在就报错, 不存在就创建并返回, 注意IPC_EXCL单独传入没有意义, 和IPC_CREAT一起传入才有意义, 这两个标志位保证了创建的共享内存是全新的.

3. 传入0, 共享内存存在就返回, 不存在就报错.

 shmat(at = attach)

功能: 将共享内存段连接到进程地址空间
原型: void * shmat(int shmid, const void *shmaddr, int shmflg);
参数:

  • shmid: 共享内存标识
  • shmaddr: 指定连接的地址
  • shmflg: 它的两个可能取值是 SHM_RND 和 SHM_RDONLY
  • 返回值: 成功返回一个指针, 指向共享内存首地址; 失败返回-1

这个返回值和 malloc 是类似的, 都可以强转为指定类型的地址, 但是malloc是在堆区开辟一块空间, 并不是共享内存.

shmdt (dt = detach)

功能: 将共享内存段与当前进程脱离
原型: int shmdt(const void *shmaddr);
参数:

  • shmaddr: 由shmat所返回的指针
  • 返回值:成功返回0; 失败返回-1

注意: 将共享内存段与当前进程脱离不等于删除共享内存段

shmctl

功能: 用于控制共享内存
原型: int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数:

  • shmid: 由shmget返回的共享内存标识码.
  • cmd: 将要采取的动作 (有三个可取值).
  • buf: 指向一个保存着共享内存的模式状态和访问权限的数据结构.
  • 返回值: 成功返回0;失败返回-1.

共享内存的生命周期:

共享内存的生命周期不会随着内存的释放而结束, 也就是说程序结束后如果没有用shmctl删除共享内存, 共享内存依然存在. 只要系统不重启或显式地删除该共享内存, 它就会一直存在, 一个进程退出, 其他进程仍然可以通过 shmat连接到共享内存并使用它, 直到调用 shmctl 删除共享内存为止.

代码

实现一个server端和client端, server作为读端每隔两秒打印共享内存中的内容, client每隔一秒进行写入: 

 comm.hpp

#include <iostream>
#include <cstring>
#include <cerrno>
const char* pathName = "/home/zzy/linux_system_programing/inter-process_communication";
const int project_id = 0x11223344;
// 共享内存的大小,强烈建议设置成为n*4096
const int size = 4096;//创建一个key
key_t GetKey()
{key_t key = ftok(pathName, project_id);if(key < 0){std::cerr << "errno:" << errno <<  ", errno string:" << strerror(errno)<<std::endl;exit(1);}return key;
}//将key转换为0x开头字符串
std::string ToHex(key_t key)
{char buffer[1024];snprintf(buffer, sizeof(buffer), "0x%x", key);return buffer;
}//子函数
int CreateShmHelper(key_t key, int shmflg)
{int shmid = shmget(key, size, shmflg);if(shmid < 0){std::cerr << "errno:" << errno <<  ", errno string:" << strerror(errno)<<std::endl;exit(2);}return shmid;
}//创建共享内存
int CreateShm(key_t key)
{return CreateShmHelper(key, IPC_CREAT|IPC_EXCL|0644);
}//获取共享内存
int GetShm(key_t key)
{return CreateShmHelper(key, IPC_CREAT);//传0也可以
}

server.cc 

#include <iostream>
#include <cstring>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#include "comm.hpp"
using namespace std;int main()
{key_t key = GetKey();cout << "key: " << ToHex(key) << endl;// key vs shmid// shmid: 应用这个共享内存的时候,我们使用shmid来进行操作共享内存,类似文件操作的FILE*// key: 不要在应用层使用,只用来在内核中标识shm的唯一性! 类似文件操作的fdint shmid = CreateShm(key);cout << "shmid: " << shmid << endl;char* s = (char*)shmat(shmid, nullptr, 0);cout << "shm attach done: " << shmid << endl;while(true){cout << s << endl;sleep(2);}//取消共享内存与进程地址空间的映射shmdt(s);cout << "shm dettach done: " << shmid << endl;sleep(3);//删除共享内存shmctl(shmid, IPC_RMID, nullptr);cout << "shm remove done " << shmid << endl;return 0;
}

client.cc 

#include <iostream>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#include "comm.hpp"
using namespace std;int main()
{key_t key = GetKey();cout << "key: " << ToHex(key) << endl;int shmid = GetShm(key);cout << "shmid: " << shmid << endl;char* s = (char*)shmat(shmid, nullptr, 0);cout << "shm attach done: " << shmid << endl;for(char c = 'a'; c<='z'; c++){s[c-'a'] = c;cout << "write " << c << " done" <<endl;sleep(1);}shmdt(s);cout << "shm dettach done: " << shmid << endl;return 0;
}

由结果可以看到, 当server端启动后, client也启动然后与shm连接, 并每隔1秒向shm中发送数据, 而server每隔2秒读取shm中的内容 :

共享内存的同步方式: 不提供同步机制, 共享内存是直接裸露给所有的使用者使用的, 一定要注意共享内存的使用安全问题. 

借助管道的同步机制, 可以让共享内存实现同步:

comm.hpp添加新建管道文件的函数: 

//创建管道
bool MakeFifo()
{int fd = mkfifo(filename.c_str(), 0666);if(fd < 0){std::cerr << "errno: " << errno << ", errstring: " << strerror(errno) << std::endl;return false;}std::cout << "mkfifo success... read" << std::endl;return true;
}

 server.cc借助类修改了一下初始化和清理资源的方式, 然后在while循环中读取管道传来的内容以实现同步:

#include <iostream>
#include <cstring>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#include "comm.hpp"
using namespace std;class Init
{
public:Init(){bool r = MakeFifo();if (!r)return;key_t key = GetKey();cout << "key: " << ToHex(key) << endl;// key vs shmid// shmid: 应用这个共享内存的时候,我们使用shmid来进行操作共享内存,类似文件操作的FILE*// key: 不要在应用层使用,只用来在内核中标识shm的唯一性! 类似文件操作的fdshmid = CreateShm(key);cout << "shmid: " << shmid << endl;s = (char *)shmat(shmid, nullptr, 0);cout << "shm attach done: " << shmid << endl;fd = open(filename.c_str(), O_RDONLY);}~Init(){// 取消共享内存与进程地址空间的映射shmdt(s);cout << "shm dettach done: " << shmid << endl;// 删除共享内存shmctl(shmid, IPC_RMID, nullptr);cout << "shm remove done " << shmid << endl;close(fd);unlink(filename.c_str());}public:char *s;int shmid;int fd;
};int main()
{Init init;while (true){int code = 0;ssize_t n = read(init.fd, &code, sizeof(code));if(n > 0){cout << init.s << endl;//sleep(2);}else if(n == 0){break;}}return 0;
}

client.cc 向共享内存写入一个字符就向管道传入内容通知server可以读取数据了(同步): 

#include <iostream>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#include "comm.hpp"
using namespace std;int main()
{key_t key = GetKey();cout << "key: " << ToHex(key) << endl;int shmid = GetShm(key);cout << "shmid: " << shmid << endl;char* s = (char*)shmat(shmid, nullptr, 0);cout << "shm attach done: " << shmid << endl;int fd = open(filename.c_str(), O_WRONLY);for(char c = 'a'; c<='z'; c++){s[c-'a'] = c;cout << "write " << c << " done" <<endl;//通知对方int code = 1;ssize_t n = write(fd, &code, sizeof(code));sleep(1);}shmdt(s);cout << "shm dettach done: " << shmid << endl;close(fd);return 0;
}

这次可以发现 client 向 shm 写入一个数据, 就发送一次接收的指令, server就输出一次共享内存的内容, 完成了同步. 

关于共享内存的内容如何清理:

shm只涉及进程向 shm 中写入和读取内容, 而 shm 中的内容并不会因为内容的读取就被移除, 所以shm的内容需要用户自己清理, 如何清理? 可以规定shm的前8/16个字节存放两个写入和读取指针, 通过两个指针对shm的内容进行维护.

 关于shmctl接口查看共享内存结构体:

上面使用过IPC_RMID删除共享内存, 现在用 IPC_STAT 查看shmid_ds中的内容:

#include <iostream>
#include <cstring>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#include "comm.hpp"
using namespace std;class Init
{
public:Init(){key_t key = GetKey();cout << "key: " << ToHex(key) << endl;// key vs shmid// shmid: 应用这个共享内存的时候,我们使用shmid来进行操作共享内存,类似文件操作的FILE*// key: 不要在应用层使用,只用来在内核中标识shm的唯一性! 类似文件操作的fdshmid = CreateShm(key);cout << "shmid: " << shmid << endl;s = (char *)shmat(shmid, nullptr, 0);cout << "shm attach done: " << shmid << endl;     }~Init(){// 取消共享内存与进程地址空间的映射shmdt(s);cout << "shm dettach done: " << shmid << endl;// 删除共享内存shmctl(shmid, IPC_RMID, nullptr);cout << "shm remove done " << shmid << endl;}public:char *s;int shmid;int fd;
};int main()
{Init init;struct shmid_ds ds;shmctl(init.shmid, IPC_STAT, &ds);std::cout << ToHex(ds.shm_perm.__key) << std::endl;std::cout << ds.shm_segsz << std::endl;std::cout << ds.shm_atime << std::endl;std::cout << ds.shm_cpid << std::endl;std::cout << ds.shm_nattch << std::endl;return 0;
}

总结:

缺点:

共享内存的同步方式, 不会提供同步机制, 共享内存是直接裸露给所有的使用者使用的, 一定要注意共享内存的使用安全问题. 

优点:

1. 共享内存是所有进程间通信速度最快的

2. 共享内存可以提供较大的空间

第二点进行具体说明:

因为拷贝次数少, 共享内存是所有进程间通信方式中速度最快的. 在同样的代码下, 考虑键盘输入显示器输出(不考虑printf和scanf的缓冲区). 对比数据通过 共享内存 与 管道 通信的拷贝次数.

首先要明确, 凡事涉及到数据的迁移, 都是拷贝.

管道通信会经过四次拷贝: 输入文件(键盘)->用户缓冲区->管道文件缓冲区(内核空间)->用户缓冲区->输出文件(显示器)

共享内存通信只需要两次拷贝:输入文件(键盘)->共享内存->输出文件(显示器)


消息队列

功能:

  • 消息队列提供了一个从一个进程向另外一个进程发送一个数据块的方法, 消息队列和共享内存在使用上有一定差别, 但是它们的共享机制是一样的.
  • 每个数据块都被认为是有一个类型, 接收者进程接收的数据块可以有不同的类型值 

特性: 

同共享内存一样, 消息队列也属于systemV, 而IPC资源必须删除, 否则不会自动清除, 除非重启,所以system V IPC资源的生命周期内核 


消息队列的相关接口和共享内存都是相似的, 因为都属于systemV:

系统接口

msgget 

功能: 用来创建消息队列
原型: int msgget(key_t key, int shmflg);
参数:

  • key: 这个消息队列段名字
  • shmflg: 和共享内存一样
  • 返回值:成功返回一个非负整数, 即该消息队列段的标识码;失败返回-1 
msgctl 

功能: 用于控制消息队列
原型: int msgctl(int msqid, int cmd, struct msqid_ds *buf);
参数:

  • msqid: 由msgget返回的共享内存标识码.
  • cmd: 将要采取的动作 
  • buf: 指向一个保存着消息队列的模式状态和访问权限的数据结构.
  • 返回值: 成功返回0;失败返回-1.
 msgsnd

功能: 用于消息队列发消息

原型: int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);

参数:

msqid:msgget的返回值

msgp: 要发送的数据块, 其中包含了数据的类型和内容, 需要用户自己去定义

msgsz: 数据块大小

msgflg: 

msgrcv 

功能: 接收消息队列的消息

原型: ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);

参数:

msgflg:

IPC_NOWAIT:如果没有符合条件的消息可用,不阻塞进程,而是立即返回 EAGAIN 错误

MSG_EXCEPT: msgtype大于0的前提下, 接收除msgtpe以外类型的所有消息

msgtyp:

  • 如果 msgtyp 大于 0,则 msgrcv 函数将接收消息队列中类型字段等于 msgtyp 的第一条消息。
  • 如果 msgtyp 等于 0,则 msgrcv 函数将接收消息队列中的第一条消息。
  • 如果 msgtyp 小于 0,则 msgrcv 函数将接收消息队列中类型字段小于或等于 msgtyp 绝对值的第一条消息

更多可以查看手册

void MsgQueue()
{//创建消息队列key_t key = GetKey();std::cout << "key: " << ToHex(key) << std::endl;int msgid = msgget(key, IPC_CREAT|IPC_EXCL);std::cout << "msgid: " << msgid << std::endl;//读取消息队列结构体struct msqid_ds ds; msgctl(msgid, IPC_STAT, &ds);std::cout << ToHex(ds.msg_perm.__key) << std::endl;std::cout << ds.msg_qbytes << std::endl;sleep(10);//删除消息队列msgctl(msgid, IPC_RMID, nullptr);}

运行结果: 

ipcs -q 

不同于共享内存, 这里内核里的key和我们传入key是不一样的.


信号量

信号量的本质是一个计数器, 用于保护共享资源. 信号量主要用于同步和互斥的,下面先来看看什么是同步和互斥

多个执行流看到的同一份资源(公共资源), 在并发访问时很有可能会发生数据不一致的问题, 所以公共资源就需要被保护起来, 就有了互斥与同步. (匿名管道, 命名管道, 消息队列都由OS提供了保护措施, 而共享内存需要用户自己保证安全, 比如上面代码用管道实现同步)

互斥: 任何一个时刻只允许一个执行流(进程)访问公共资源, 加锁完成

同步: 多个执行流执行的时候, 按照一定的顺序执行.

原子性: 一个操作或者一系列操作要么全部执行成功, 要么全部不执行不存在中间状态或者部分执行的情况.

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

临界区: 访问该临界资源的代码, 维护临界资源, 其实就是在维护临界区

如何理解信号量?

举个例子, 比如看电影

每个电影院的座位都是有限的, 如果我们买了票, 即使不去使用这个座位, 这个座位也已经预定为我们的座位, 电影院和内部的座位是多个人共享的资源--公共资源, 我们买票的本质, 则是对资源的预定机制. 而一场电影的票的数量是有限的, 所以就需要设置一个计数器表示公共资源的个数, 买了一张票, 计数器就减一;退一张票, 计数器就加1, 如果计数器为0, 则买票失败.

信号量:

表示资源数目的计数器, 每一个执行流想要访问公共资源内部的某一份资源, 不应该让执行流直接访问, 而是先申请信号量资源, 申请成功就对信号量计数器做--操作, 申请不成功, 执行流将被挂起阻塞.

本质上, 只要--成功, 就完成了对资源的预定机制.

 假如一份资源的信号量为1, 它被称为二元信号量, 也叫互斥锁, 完成资源的互斥功能.

 所以如果一份共享内存整体只想被一个进程使用,可以使用二元信号量,以信号量的方式实现加锁解锁;如果所有进程都只想使用公共资源的一部分,比如一块16kb的共享内存每个进程只使用局部的一部分比如1kb,所以信号量可以设置为16,每个进程想访问这块共享内存首先要去申请信号量,申请成功才能使用,不成功则阻塞。

关于信号量的细节问题:

关于信号量的描述只是以整数计数器的形式去描述,但是其实并不是。

1. 每个进程访问共享资源都要先申请信号量意味着每个进程都得先看到同一个信号量资源,这只能由OS提供,所以信号量就也属于IPC体系, 不仅仅进程间通信属于IPC体系, 保证通信的安全也属于.

2.  信号量本质也是公共资源, 信号量是为了保护公共资源的, 但是谁去保护信号量呢, 所以对于信号量内部计数器的++或--操作, 必须是原子性的! (具体在多线程部分说明)

所以原子性的申请资源(--)的操作, 称为P; 原子性的释放资源(++)的操作, 称为V

3.  对于进程挂起/阻塞如何理解? 对于单个信号量, 简单地理解其结构为struct sem{int count; task_struct* wait_queue;} , 当信号量申请失败, 把进程设为阻塞状态并加入到对应的阻塞队列即可.

 


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

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

相关文章

数字信号的产生与检测——DSP学习笔记六

本专栏的博客的图片大部分来源于老师的PPT&#xff0c;本博客只是博主对于上课内容的知识结构的分析和梳理。 几种数字信号的产生 正弦波信号 多项式逼近(除了泰勒展开&#xff0c;还有一种方法是切比雪夫逼近法&#xff0c;感兴趣可以自己去了解一下&#xff09; 查找表 核心思…

<计算机网络自顶向下> Internet Protocol

互联网中的网络层 IP数据报格式 ver: 四个比特的版本号&#xff08;IPV4 0100, IPV6 0110&#xff09; headlen&#xff1a;head的长度&#xff08;头部长度字段&#xff08;IHL&#xff09;指定了头部的长度&#xff0c;以32位字&#xff08;4字节&#xff09;为单位计算。这…

Java数组深度剖析:掌握数据结构的基石

引言 在编程世界中&#xff0c;数仅仅是一种数据类型&#xff0c;它是理解内存分配、多维数据处理以及性能优组像是构建复杂数据结构的基本积木。它们简洁、高效&#xff0c;是管理元素集的首选方式。在Java中&#xff0c;数组不化的关键。 这篇文章致力于深入探讨Java数组的各…

git出错、文件无法删除、文件无法访问、文件或目录损坏且无法读取 等相关问题处理

一、错误历程与解决方案 1. 在用idea时&#xff0c;突然出现 部分git的命令无法使用&#xff0c;提示错误 2. 尝试删除项目文件夹&#xff0c;重新从git拉取代码 3.发现无法删除文件夹&#xff0c;删除操作没有任何反应&#xff0c;但是可以对文件夹重命名。 4.重新clone g…

李沐70_bert微调——自学笔记

微调BERT 1.BERT滴哦每一个词元返回抽取了上下文信息的特征向量 2.不同的任务使用不同的特性 句子分类 将cls对应的向量输入到全连接层分类 命名实体识别 1.识别应该词元是不是命名实体&#xff0c;例如人名、机构、位置 2.将非特殊词元放进全连接层分类 问题回答 1.给…

QT c++ 代码布局原则 简单例子

本文描述QT c widget代码布局遵循的原则&#xff1a;实中套虚&#xff0c;虚中套实。 本文最后列出了代码下载链接。 在QT6.2.4 msvc2019编译通过。 所谓实是实体组件&#xff1a;比如界面框、文本标签、组合框、文本框、按钮、表格、图片框等。 所谓虚是Layout组件&#x…

Redis哈希槽和一致性哈希

前言 单点的Redis有一定的局限&#xff1a; 单点发生故障&#xff0c;数据丢失&#xff0c;影响整体服务应用自身资源有限&#xff0c;无法承载更多资源分配并发访问&#xff0c;给服务器主机带来压力&#xff0c;性能瓶颈 我们想提升系统的容量、性能和可靠性&#xff0c;就…

sentinel-1.8.7与nacos-2.3.0实现动态规则配置、双向同步

&#x1f60a; 作者&#xff1a; 一恍过去 &#x1f496; 主页&#xff1a; https://blog.csdn.net/zhuocailing3390 &#x1f38a; 社区&#xff1a; Java技术栈交流 &#x1f389; 主题&#xff1a; sentinel-1.8.7与nacos-2.3.0实现动态规则配置、双向同步 ⏱️ 创作时…

unity的特性AttriBute详解

unity的特性AttriBute曾经令我大为头疼。因为不动使用的法则&#xff0c;但是教程都是直接就写&#xff0c;卡住就不能继续学下去。令我每一次看到&#xff0c;直接不敢看了。 今天使用文心一言搜索一番&#xff0c;发现&#xff0c;恐惧都是自己想象的&#xff0c;实际上这个…

Kotlin泛型之 循环引用泛型(A的泛型是B的子类,B的泛型是A的子类)

IDE(编辑器)报错 循环引用泛型是我起的名字&#xff0c;不知道官方的名字是什么。这个问题是我在定义Android 的MVP时提出来的。具体是什么样的呢&#xff1f;我们看一下我的基础的MVP定义&#xff1a; interface IPresenter<V> { fun getView(): V }interface IVie…

Nodejs 第六十八章(远程桌面)

远程桌面 远程桌面&#xff08;Remote Desktop&#xff09;是一种技术&#xff0c;允许用户通过网络远程连接到另一台计算机&#xff0c;并在本地计算机上控制远程计算机的操作。通过远程桌面&#xff0c;用户可以在不同地点的计算机之间共享屏幕、键盘和鼠标&#xff0c;就像…

宝塔面板安装教程(linux)

宝塔官网地址 宝塔官网linux安装地址 针对Ubuntu系统的安装命令&#xff1a; wget -O install.sh https://download.bt.cn/install/install-ubuntu_6.0.sh && sudo bash install.sh ed8484bec 安装过程中&#xff0c;中途会出现一个 Y&N ? 的选项&#xf…

OpenCV如何模板匹配

返回:OpenCV系列文章目录&#xff08;持续更新中......&#xff09; 上一篇&#xff1a;OpenCV如何实现背投 下一篇 &#xff1a;OpenCV在图像中寻找轮廓 目标 在本教程中&#xff0c;您将学习如何&#xff1a; 使用 OpenCV 函数 matchTemplate()搜索图像贴片和输入图像之间…

如何下载AndroidStudio旧版本

文章目录 1. Android官方网站2. 往下滑找到历史版本归档3. 同意软件下载条款协议4. 下载旧版本Androidstudio1. Android官方网站 点击 Android官网AS下载页面 https://developer.android.google.cn/studio 进入AndroidStuido最新版下载页面,如下图: 2. 往下滑找到历史版本归…

一本书了解AI的下一个风口:AI Agent

在数字化浪潮中&#xff0c;人工智能&#xff08;AI&#xff09;已成为推动现代社会前进的强劲引擎。 从智能手机的智能助手到自动驾驶汽车的精准导航&#xff0c;AI技术的应用已经渗透到生活的方方面面。 随着技术的飞速发展&#xff0c;我们正站在一个新的转折点上&#xff…

构建本地大语言模型知识库问答系统

MaxKB 2024 年 4 月 12 日&#xff0c;1Panel 开源项目组正式对外介绍了其官方出品的开源子项目 ——MaxKB&#xff08;github.com/1Panel-dev/MaxKB&#xff09;。MaxKB 是一款基于 LLM&#xff08;Large Language Model&#xff09;大语言模型的知识库问答系统。MaxKB 的产品…

[论文笔记]GAUSSIAN ERROR LINEAR UNITS (GELUS)

引言 今天来看一下GELU的原始论文。 作者提出了GELU(Gaussian Error Linear Unit,高斯误差线性单元)非线性激活函数&#xff1a; GELU x Φ ( x ) \text{GELU} x\Phi(x) GELUxΦ(x)&#xff0c;其中 Φ ( x ) \Phi(x) Φ(x)​是标准高斯累积分布函数。与ReLU激活函数通过输入…

网盘—上传文件

本文主要讲解网盘里面关于文件操作部分的上传文件&#xff0c;具体步骤如下 目录 1、实施步骤&#xff1a; 2、代码实现 2.1、添加上传文件协议 2.2、添加上传文件槽函数 2.3、添加槽函数定义 2.4、关联上传槽函数 2.5、服务器端 2.6、在服务器端添加上传文件请求的ca…

算法学习(5)-图的遍历

目录 什么是深度和广度优先 图的深度优先遍历-城市地图 图的广度优先遍历-最少转机 什么是深度和广度优先 使用深度优先搜索来遍历这个图的过程具体是&#xff1a; 首先从一个未走到过的顶点作为起始顶点&#xff0c; 比如以1号顶点作为起点。沿1号顶点的边去尝试访问其它未…

提升编码技能:学习如何使用 C# 和 Fizzler 获取特价机票

引言 五一假期作为中国的传统节日&#xff0c;也是旅游热门的时段之一&#xff0c;特价机票往往成为人们关注的焦点。在这个数字化时代&#xff0c;利用爬虫技术获取特价机票信息已成为一种常见的策略。通过结合C#和Fizzler库&#xff0c;我们可以更加高效地实现这一目标&…