【进程间通信(三)】【system V共享内存】

news2024/11/24 8:33:41

目录

  • 1. 原理
  • 2. 编码通信
    • 2.1 创建共享内存
    • 2.2 shmat && shmdt && shmctl
    • 2.3 通信
  • 3. 共享内存的特性
    • 3.1 共享内存的属性
    • 3.2 加入管道实现同步机制

前面的文章介绍了管道通信,其中包括匿名管道、命名管道。这篇文章介绍另一种进程间通信的方式 ----- 共享内存。

但是必须确定一点,即无论用哪种方式实现进程间通信,本质都是让不同的进程先看到同一份资源!

1. 原理

共享内存是操作系统提供的一种进程间通信的方案,它不像匿名管道通信,局限于进程之间的关系。换言之,不具备任何关系的进程之间,都能通过共享内存进行通信。

共享内存是由操作系统在物理内存中直接开辟的一块内存空间,因为这是操作系统,所以它有权限可以直接修改进程的页表,然后把这块共享内存的地址与虚拟内存映射起来,映射到进程A 地址空间中的共享区域,最后再给用户层返回该共享内存的起始地址(虚拟地址)即可。

如果有进程 B 想要与进程 A 进行通信,只需要将同一块共享内存通过页表映射到进程 B 的共享区中即可。后续就可以通过页表映射,访问同一块内存。这就完成了让不同进程看到同一份资源的工作!

所以创建共享内存,总结为三个步骤

  1. 申请内存
  2. 挂接到进程地址空间
  3. 返回起始地址

后续想要释放共享内存,那么只需要先去关联(即与创建共享内存的第二步相反的操作),再释放内存空间即可。

  • 关于共享内存的所有操作是进程 直接 完成的吗?

    肯定不是, 基本操作系统允许用户自己创建共享内存,那么用户肯定只能通过类似 malloc / new 这样的接口去创建,申请出来的内存空间最终只有自己这个进程能够看到(因为进程具有独立性),达不到进程通信的基本要求。所以诸如申请内存等一系列操作,都是由操作系统来完成的。

    换言之,在进程通信需要创建共享内存这件事上,进程(代表用户)是需求方,操作系统是执行方,因此执行方需要向需求方提供一些系统接口,来满足用户的通信需求,因此用户只能通过系统调用。

在操作系统中存在几十上百个进程,可能有很多个进程都需要创建共享内存来通信,因此操作系统中就可能存在很多个共享内存,那么操作系统就需要对共享内存进行管理!而管理的本质就是先用内核结构体对共享内存的诸多属性进行描述,再用特定的数据结构将各个结构体对象组织进来,对共享内存的管理就转变为对数据结构的管理!

与 struct file 相似的是,当多个进程打开同一个文件,在 struct file 内部会维护一个计数器,只有当引用计数为0时,才会释放 struct file 和文件的相关数据。在共享内存的描述结构体中,也会维护一个引用计数的属性。

2. 编码通信

2.1 创建共享内存

  • 要通信,得先有通道,因此需要先创建共享内存。

    NAME
    	shmget - allocates a System V shared memory segment
    	
    SYNOPSIS
    	#include <sys/ipc.h>
    	#include <sys/shm.h>
    	
    	int shmget(key_t key, size_t size, int shmflg);
    	
    RETURN VALUE
           On success, a valid shared memory identifier is returned. On errir, -1 is returned, 
           and errno is set to indicate the error
    
    参数分析:
    key: 不同进程对共享内存的标识,用于访问同一块共享内存,具有唯一性
    size: 创建共享内存的大小(单位为字节)
    shmflg: 与 open 接口的 flags 参数相似,以比特位标志的方式进行传递,例如 IPC_CREAT 和 IPC_EXCL,并且一样可以叠加使用 
    ret: 共享内存标识符(创建成功的返回值)
    
    IPC_CREAT: 如果申请的共享内存存在,则创建,否则获取这个共享内存并返回。
    IPC_EXCL: 这个选项不单独使用。
    IPC_CREAT | IPC_EXCL: 如果申请的共享内存存在,则创建,否则出错返回。(这么用的目的是确保申请的共享内存一定是刚创建出来的)
    
  • 问题一:如何保证不同进程看到的是同一个共享内存?在创建共享内存时又如何得知这个共享内存是否已经存在?

    要想搞清楚这个问题,就需要搞清楚 shmget 接口中的 key 参数。

    操作系统中可能存在多个共享内存,为了保证通信双方进程看到的是同一块共享内存,因此引入 key 参数,这个数字操作系统中必须具有唯一性,能够让不同进程进行唯一性标识。(不管共享内存是否已经创建,只要是通信,那么进程就需要用这个 key 与共享内存建立连接。遍历系统中所有的共享系统,比对 key 值,如果不存在相同的,创建共享内存,存在的话,那么直接与这个共享内存建立连接即可,可以理解为 key 是进程之间的一种暗号)。

    因此只要第一个进程通过 key 创建了一块共享内存,后续进程只要拿着同一个key,就能够与第一个进程看到同一个共享内存,然后建立通信信道。有了 key,就解决了上述的两个问题,比对 key 值,可以保证看到的是同一个资源,遍历共享内存比对的同时,也是在解决该共享内存是否存在的问题

    • 所以 key 在哪??

      key 是进程间通信的 “暗号”,而共享内存 = 内存块 + 描述结构体对象,内存块肯定只是用于存储用户通信数据,那么 key 只能在共享内存的描述结构体对象中了。即首次创建共享内存时,key 值肯定是要被设置到内核结构中的,这样后续进程才能够拿着 key 在内核中与所有共享内存做比对。

    在讲命名管道的通信时,我们就解决过类似的问题,如何保证不同的进程看到的是同一个管道? ---- 每个管道文件具有唯一的路径,在该路径下具有唯一的文件名,因此能够精准的让不同进程看到同一个管道。命名管道通信的本质,不是进程间看到了同一个管道文件,而是它们看到了具有唯一性的标识! 只要不同进程看到具有唯一性的东西,那么这个问题就能够得到保证。

    • 所以如何获取 key ?

      NAME
      	ftok - convert a pathname and a project identifier to a System V IPC key
      
      SYNOPSIS
      	#include <sys/types.h>
      	#include <sys/ipc.h>
      
          key_t ftok(const char *pathname, int proj_id);
      	
      RETURN VALUE
      	On success, the generated key_t value is returned. On failure -1 is returned
      	with errno indicating the error as for the stat(2) system call.
      
      分析:
      一个生成 key 的系统调用,根据 pathname 和 proj_id 这两个参数的值加上特定算法进行数值计算得到 key 值
      pathname 和 proj_id 都是由用户自由的,但 ftok 生成的 key 可能会出现冲突的问题
      pathname 代表的就是路径,本来就具有唯一性,因此冲突的概率并不大,如果冲突了,那么换一个 proj_id 即可。
      因此后续进程只要调用 ftok,并且使用同一个 pathname 和 proj_id,那么计算得到的 key 值也一定是相同的,
      这样就能够与其它进程访问同一个共享内存了。
      
    • 这个函数是有可能调用失败的,可能是系统内存不足,申请失败,也可能是 key 值冲突。搞那么一圈,让用户自己指定 pathname 和 proj_id,最后生成的 key 还冲突了,那为什么操作系统不直接为用户生成 key 值呢??

      如果操作系统自动为用户生成一个 key,但是操作系统不知道用户要哪几个进程通信,后续其它进程调用 ftok 生成 key 时,操作系统怎么知道这个进程是要与前面生成 key 的那个进程通信的,如果不知道,操作系统如何给你生成一样的 key,无法生成一样的 key,其它进程如何与前面的进程进行通信。

      所以不是技术上无法实现,是这件事就必须由用户来做,只有用户才知道哪些进程要进行通信。与其说 key 值是由用户生成的,不如说是由用户约定的!通过约定好同样的参数,这样就一定生成同一个 key 值,再拿着一样的 key 值就可以创建 / 获取到同一个共享内存,然后进行通信。

  • 编码实现:

    #include<iostream>
    #include<cstring>
    #include<string>
    #include<sys/ipc.h>
    #include<sys/shm.h>
    #include<sys/types.h>
    #include "log.hpp"
    using namespace std;
    
    Log log;	// 自定义的日志输出
    
    // 共享内存的大小一般给4096的整数倍
    // 假如size=4097,操作系统实际分配的是4096*2的大小(但是用户看不到,看到的还是4097)
    // 这是因为操作系统在分配内存时,是以一个页大小(即4096 bytes)为单位分配的,方便虚拟内存到物理内存的映射。
    const int size = 4096;
    
    const string pathname = "/home/outlier";
    const int proj_id = 0x6666;
    
    key_t GetKey()
    {
        key_t key = ftok(pathname.c_str(), proj_id);
        if(key == -1)
        {
            log(Fatal, "fotk error: %s", strerror(errno));
            exit(1);
        }
        log(Info, "fotk success: 0x%x", key);
        return key;
    }
    
    int GetShareMemory(int shmflg)
    {
        key_t key = GetKey();
        int shmid = shmget(key, size, shmflg);
        if(shmid == -1)
        {
            log(Fatal, "create share error: %s", strerror(errno));
            exit(2);
        }
        log(Info, "create share success: %d", shmid);
        return shmid;
    }
    
    int CreateShm()
    {
        return GetShareMemory(IPC_CREAT|IPC_EXCL|0666);		// 666是共享内存的权限	
    }
    
    int Getshm()
    {
        return GetShareMemory(IPC_CREAT);
    }
    

    在这里插入图片描述

  • 我们可以看到,第一遍执行,成功创建了共享内存,但第二遍的时候,却显示文件已经存在,这就表面了,共享内存的生命周期是随内核的,用户不主动关闭,共享内存就不会被释放,除非内核重启或者用户手动释放。

    icps -m 可以查看系统中已存在的共享内存信息。

    icprm -m shmid 根据 shmid 释放共享内存(为什么是根据 shmid,因为在释放共享内存这件事上,是用户层面做的事情,只要涉及是用户层,操作共享内存时,一切都是根据 shmid,key 值只是操作系统内核用于标定共享内存的唯一性而已,可以理解为 key 只是内核用于比对共享内存,创建和获取共享内存,仅此而已,之后的关于共享内存的任何操作都跟 key 无关了,因为后续操作都是用户层做的,那么就需要根据 shmid, shmid 是进程内(强调进程)用来表示资源的唯一性。

2.2 shmat && shmdt && shmctl

NAME
	shmat, shmdt - System V shared memory operations	

SYNOPSIS
	#include <sys/types.h>
    #include <sys/shm.h>

    void *shmat(int shmid, const void *shmaddr, int shmflg); 	// 把创建出来的共享内存挂接到指定进程的进程地址空间中。

参数分析:
shmid:共享内存标识符(即shmget的返回值)
shmaddr:让共享内存挂接到进程地址的哪个位置,一般设置null,让操作系统决定即可。
shmflg:设置该进程挂接共享内存的权限,可设置为 SHM_RDONLY(只读) 等,设置为 0 代表按照共享内存的默认权限挂接。
返回值: 挂接到进程地址空间的地址。
NAME
	shmat, shmdt - System V shared memory operations

SYNOPSIS
     #include <sys/types.h>
     #include <sys/shm.h>

     int shmdt(const void *shmaddr);		// 去关联

参数分析:
shmaddr:把 shmat 共享内存挂接后返回的地址传入即可。原理与 malloc / new 相似,只需要传入一段内存空间的起始地址,
		共享内存描述结构体对象中记录了共享内存的大小,因此起始地址 + 大小即可释放一段连续的空间。
NAME
	shmctl - System V shared memory control

SYNOPSIS
    #include <sys/ipc.h>
    #include <sys/shm.h>

    int shmctl(int shmid, int cmd, struct shmid_ds *buf);

参数分析:
shmid:共享内存标识符(即shmget的返回值)
shmid_ds:记录共享内存诸多属性的结构体,通过共享内存进行进程通信的本质就是在操作共享内存,操作无法就是获取、修改、删除共享内存内部的属性。
cmd:指明要怎么操作共享内存的属性,其可以设置为 IPC_STAT(拷贝把共享内存的属性) / IPC_RMID(将共享内存标记为删除) 等

struct shmid_ds {		
	 struct ipc_perm shm_perm;    /* Ownership and permissions */ 	  // 共享内存的权限
	 size_t          shm_segsz;   /* Size of segment (bytes) */	      // 大小
	 time_t          shm_atime;   /* Last attach time */	          // 最后一次挂接的时间
	 time_t          shm_dtime;   /* Last detach time */		      // 最后一次取消挂接的时间
	 time_t          shm_ctime;   /* Last change time */			  // 最后一次修改共享内存属性的时间
	 pid_t           shm_cpid;    /* PID of creator */				  // 创建共享内存的进程pid
	 pid_t           shm_lpid;    /* PID of last shmat(2)/shmdt(2) */ // 最后挂接共享内存的进程pid
	 shmatt_t        shm_nattch;  /* No. of current attaches */		  // 挂接共享内存的进程数量
	 ...
};
// processA
int main()
{
    int shmid = CreateShm();	// 创建共享内存   
    char* shmaddr = (char*)shmat(shmid, nullptr, 0);    // 共享内存挂接进程地址空间
    shmdt(shmaddr);     // 去关联 
    shmctl(shmid, IPC_RMID, nullptr);   // 删除共 享内存时不需要关注其属性,因此第三个参数为null
    return 0;
}
// processB
int main()
{
    int shmid = Getshm();   // 第二个进程只需要获取共享内存,不需要创建,也不需要删除
    char* shmaddr = (char*)shmat(shmid, nullptr, 0);    // 共享内存挂接进程地址空间
    shmdt(shmaddr);     // 去关联
    return 0;
}

在这里插入图片描述

到这里,共享内存还是没有开始通信!上面的这一切,调了一堆的系统调用,诸如 shmget、shmat、shmctl,只是在建立信道和释放共享内存,仅此而已。

2.3 通信

与管道通信不同的是,共享内存通信,不需要调用系统接口进行读写数据。因为共享内存已经挂接到指定进程的进程地址空间了,已经属于这个进程的空间了,进程可以直接访问自己进程地址空间内的任何空间!换言之,一旦有了共享内存,共享内存挂接到进程地址空间之后,就可以直接把当作该进程的内存空间来使用!

// processA
int main()
{
	...
    while(1)
    {
        // 直接像读取自己new出来的空间内的数据一样的读取共享内存即可!
        cout << "[cilent]# " << shmaddr << endl;  
        sleep(1);  
    }
    ...
}

// processB
int main()
{
	...
    while(1)
    {
        cout << "Please Enter@ ";
        fgets(shmaddr, 4096, stdin);
    }
    ...
}

在这里插入图片描述

3. 共享内存的特性

  • 共享内存没有同步互斥之类的保护机制。 即上面看到的,当 processB 还没开始写入数据时,processA 并不会像管道通信那样,在读取时阻塞等待写方,而是自顾自的读取,如果没有手动的在每一回合的读写后清空共享内存的数据,那么读方会继续重复读取数据。即不管你写不写,我都读,不管你读不读,我都写。
  • 共享内存是所有进程间通信速度最快的。 因为它不需要做拷贝数据的工作,例如管道通信时,读端需要先调用 read 把数据读到用户缓冲区,再从用户缓冲区读取数据;写端则需要先把数据写到用户缓冲区,再调 write 写到管道中。read 和 write 到用户缓冲区的本质就是拷贝数据。
  • 共享内存内部的数据,由用户自己维护。 即如果不手动清空内部数据,即便完成了一回合通信,内部的数据依旧在,下次读取时还能读取到上次的数据。

3.1 共享内存的属性

struct shmid_ds {		//
	 struct ipc_perm shm_perm;    /* Ownership and permissions */ 	  // 共享内存的权限是一个结构体!
	 size_t          shm_segsz;   /* Size of segment (bytes) */	      
	 time_t          shm_atime;   /* Last attach time */	         
	 time_t          shm_dtime;   /* Last detach time */		      
	 time_t          shm_ctime;   /* Last change time */			
	 pid_t           shm_cpid;    /* PID of creator */				 
	 pid_t           shm_lpid;    /* PID of last shmat(2)/shmdt(2) */ 
	 shmatt_t        shm_nattch;  /* No. of current attaches */		  
	 ...
};

struct ipc_perm {	// 共享内存权限
	key_t          __key;    /* Key supplied to shmget(2) */	// 其中就包含获取共享内存的 key
	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 and SHM_LOCKED flags */		// icps -m 看到的共享内存的权限
	unsigned short __seq;    /* Sequence number */
};

把共享内存的部分属性打印出来见一见:

// 核心代码
struct shmid_ds shmds;
shmctl(shmid, IPC_STAT, &shmds);    // IPC_STAT:将共享内存的数据拷贝到 shmds 结构体中
cout << "shm size: " << shmds.shm_segsz << endl;
cout << "shm size: " << shmds.shm_nattch << endl;
cout << "shm __key: " << shmds.shm_perm.__key << endl;
cout << "shm mode: " << shmds.shm_perm.mode << endl;

在这里插入图片描述

3.2 加入管道实现同步机制

共享内存的同步机制一般是通过信号来实现的,这里使用管道实现,只不过是为了演示,共享内存是可以做到同步性的。

// 新增代码
// comm.hpp
#define FIFO_FILE "./myfifo"    
#define MODE 0664    
class Init    
{    
public:    
    Init()    
    {    
        int n = mkfifo(FIFO_FILE, MODE);    
        if(n == -1) exit(1);     
    }    
    ~Init()    
    {    
        int m = unlink(FIFO_FILE);      // unlink 删除文件的系统调用    
        if(m == -1) exit(2);    
    }
};

// processA
int main()
{
	...
    Init init;
    int fd = open(FIFO_FILE, O_RDONLY);
    if(fd < 0) exit(3);
    while(1)
    {  
        char c;
        ssize_t s = read(fd, &c, 1);  // 写入方没有写入操作时,进程阻塞在read,通信便有了同步性
        if(s == 0) break;		// 当写端退出时,借助管道,读端也能够自己break退出
        else if(s < 0) break;	

        // 直接像读取自己new出来的空间内的数据一样的读取共享内存即可!
        cout << "[cilent]# " << shmaddr << endl;  
        sleep(1);  
    }
	...
}

// processB
int main()
{
	...
    int fd = open(FIFO_FILE, O_WRONLY);
    if(fd < 0) exit(1);
    while(1)
    {
        cout << "Please Enter@ ";
        fgets(shmaddr, 4096, stdin);
        write(fd, "c", 1);      // 引入管道通信只是为了保证共享内存通信的同步性
    }
	...
}

在这里插入图片描述
在这里插入图片描述


如果感觉该篇文章给你带来了收获,可以 点赞👍 + 收藏⭐️ + 关注➕ 支持一下!

感谢各位观看!

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

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

相关文章

NVP的含义?如何理解其在AEM|FLUKE线缆认证测试中的意义?不同的NVP会出现怎样的结果?

在AEM|FLUKE铜缆认证测试中&#xff0c;有很多朋友对NVP设置有疑问&#xff0c;不知道应该怎么去设置它&#xff0c;并很好的应用它&#xff0c;那我们基于此&#xff0c;做一个简单的分析。 什么是NVP? NVP是Nominal Velocity of Propagation的缩写&#xff1f;简单直接译过…

Java基础-泛型机制

文章目录 为什么引入泛型泛型的基本使用泛型类泛型接口泛型方法泛型数组正确的数组声明使用场景如何理解Java中的泛型是伪泛型&#xff1f;泛型中类型擦除 泛型数组&#xff1a;如何正确的初始化泛型数组实例&#xff1f; 为什么引入泛型 引入泛型的意义在于&#xff1a; 适用…

KEYSIGHT B1500A 半导体器件参数分析仪

新利通 B1500A 半导体器件参数分析仪 ——一体化器件表征分析仪—— 简述 Keysight B1500A 半导体参数分析仪是一款一体化器件表征分析仪&#xff0c;能够测量 IV、CV、脉冲/动态 IV 等参数。 主机和插入式模块能够表征大多数电子器件、材料、半导体和有源/无源元器件。 B…

关于相机的一些零碎知识点

热成像&#xff0c;英文为Thermal Imaging&#xff0c;例如型号500T&#xff0c;其实指的就是热成像500分辨率。 相机的CMOS&#xff0c;英文为Complementary Metal Oxide Semiconductor&#xff0c;是数码相机的核心成像部件&#xff0c;是一种互补金属氧化物导体器件。 DPI…

PVC刻字膜高精度模切应用

PVC刻字膜是一种由聚氯乙烯&#xff08;PVC&#xff09;为主要成分制成的薄膜材料&#xff0c;具有耐磨、耐刮、耐水、耐油以及良好的化学稳定性等特点。这种薄膜在多个行业中得到广泛应用&#xff0c;特别是在服装、鞋业、箱包、汽车内饰等领域&#xff0c;用于制作各种标识、…

NDC美国药品编码目录数据库查询方法

NDC&#xff08;National Drug Code&#xff09;翻译为“国家药品代码”&#xff0c;是美国食品药品监督管理局&#xff08;FDA&#xff09;制定的一种药品标识系统&#xff0c;用于唯一标识药品。这个编码系统主要目的是为精准识别和追踪不同药品而建设&#xff0c;行业人员和…

2024最新【Pycharm】史上最全PyCharm安装教程,图文教程(超详细)

1. PyCharm下载安装 完整安装包下载&#xff08;包含Python和Pycharm专业版注册码&#xff09;&#xff1a;点击这里 1&#xff09;访问官网 https://www.jetbrains.com/pycharm/download/#sectionwindows 下载「社区版 Community」 安装包。 2&#xff09;下载完成后&#…

【斯坦福CS144】Lab7

一、实验目的 在本课程中&#xff0c;你已经实现了互联网基础设施的重要部分。这个检查点不是关于实现&#xff0c;而是关于测量实际的互联网并报告特定路径的长期统计数据。 二、实验内容 1.收集数据 选择一个远程主机&#xff0c;其往返时间&#xff08;RTT&#xff09;从…

Unity3D相关知识点总结

Unity3D使用的是笛卡尔三维坐标系&#xff0c;并且是以左手坐标系进行展示的。 1.全局坐标系&#xff08;global&#xff09; 全局坐标系描述的是游戏对象在整个世界&#xff08;场景&#xff09;中的相对于坐标原点&#xff08;0&#xff0c;0&#xff0c;0&#xff09;的位置…

处理 Vue3 中隐藏元素刷新闪烁问题

一、问题说明 页面刷新&#xff0c;原本隐藏的元素会一闪而过。 效果展示&#xff1a; 页面的导航栏通过路由跳转中携带的 meta 参数控制导航栏的 显示/隐藏&#xff0c;但在实践过程中发现&#xff0c;虽然元素隐藏了&#xff0c;但是刷新页面会出现闪烁的问题。 项目源码&…

MLP优化KAN

一&#xff1a;spline概念介绍 在数学学科数值分析中&#xff0c;样条&#xff08;spline&#xff09;是一种特殊的函数&#xff0c;由多项式分段定义。样条的英语单词spline来源于可变形的样条工具&#xff0c;那是一种在造船和工程制图时用来画出光滑形状的工具 样条有两个特…

Adversarial and Adaptive Tone Mapping Operatorfor High Dynamic Range Images

Abstract 这项工作涉及色调映射&#xff0c;这是一种将高动态范围 (HDR) 图像转换为低动态范围 (LDR) 图像的常用方法。 我们通过使用自适应色调映射来解决这个问题。 我们建议部署条件生成对抗网络来构建对抗性和自适应色调映射算子&#xff08;adTMO&#xff09;&#xff0c…

游戏盾是如何解决游戏行业攻击问题

随着游戏行业的迅猛发展&#xff0c;其高额的利润和激烈的市场竞争吸引了众多企业和创业者的目光。然而&#xff0c;这一行业也面临着前所未有的业务和安全挑战&#xff0c;尤其是DDoS&#xff08;分布式拒绝服务&#xff09;攻击&#xff0c;已经成为游戏行业的一大威胁。今天…

Metasploit渗透测试之MSFvenom

简介 到目前为止&#xff0c;你应该已经对MSFvenom不陌生了&#xff0c;因为在之前的文章中已经介绍多次了。MSFvenom是用于生成有效攻击载荷和编码的工具。它由msfpayload和msfencode演变而来。并于2015年6月8日取代了这两者。 在本文中&#xff0c;我们将更深入地研究可用的…

MySQL进阶 - 索引

01 索引概述 【1】概念&#xff1a;索引就是一种有序的数据结构&#xff0c;可用于高效查询数据。在数据库表中除了要保存原始数据外&#xff0c;数据库还需要去维护索引这种数据结构&#xff0c;通过这种数据结构来指向原始数据&#xff0c;这样就可以根据这些数据结构实现高…

如何高效开发一套医院绩效核算系统

医院绩效核算系统是一种专为医疗机构设计的软件系统&#xff0c;旨在通过科学、系统的方法评估和核算医院内各科室及员工的绩效。该系统与医院的信息化系统紧密集成&#xff0c;特别是与医院信息系统&#xff08;HIS&#xff09;对接&#xff0c;以确保数据的准确性和实时性。 …

nginx配置多域名共用服务器80端口

nginx配置多域名共用服务器80端口 多个域名&#xff0c;比如两个域名&#xff0c;这两个域名其实共用一台服务器&#xff08;意味着域名解析到同一个IP&#xff09;&#xff0c;一个域名为abc.com&#xff08;可以是http://abc.com或者www.abc.com&#xff09;,另外一个域名为x…

腾讯地图接口报错此key每日调用量已达到上限

需要在 配额管理 的 账户额度 中进行配额的分配

安捷伦Agilent N9918A,N9918B Fieldfox手持式微波分析仪

Agilent N9918B、Keysight N9918B、HP N9918B FieldFox 手持式微波分析仪&#xff0c;26.5 GHz 配置详情&#xff1a; 233 - 频谱分析仪 350 - 实时频谱分析仪 (RTSA) 010 - 矢量网络分析仪时域 235 - 前置放大器 238 - 频谱分析仪时间选通 210 - 矢量网络分析仪传输/反射…

照片在线转成二维码展示,更方便分享图片的好办法

怎么能把照片生成二维码后&#xff0c;分享给其他人展示呢&#xff1f;现在很多人为了能够更方便的将自己的图片展现给其他人会使用生成二维码的方式&#xff0c;将图片存储到云空间&#xff0c;通过扫码调取图片查看内容。与其他方式相比&#xff0c;这样会更加的方便&#xf…