System V 共享内存

System V 共享内存

  • 共享内存是什么
  • 如何使用共享内存
    • ftok
    • shmget
    • shmat
    • shmdt
    • shmctl
  • 共享内存的原理
  • 共享内存实现两个进程间通信
  • 共享内存的特点
  • 共享内存与管道配合使用
    • 两个进程间通信
    • 多个进程间通信

共享内存是什么

🚀共享内存是最快的IPC形式。一旦这样的内存映射到共享它的进程的地址空间,这些进程间的数据传递不在涉及到内核,换句话说是进程不在通过执行进入内核的系统调用来传递彼此的数据。
🚀共享内存是物理内存的一块区域,通过页表的映射挂接到共享它的进程的地址空间上,再返回给用户这段空间的起始地址,这样用户就能使用这一块内存了。
🚀共享内存与管道不同,管道创建好后,仍需使用系统调用接口来从管道中读取或写入数据,并且管道只允许一端读一端写,而共享内存是直接使用这块内存空间。

如何使用共享内存

ftok

在这里插入图片描述

  • 参数: 第一个参数是文件名,并且是已经存在的文件,第二个参数是项目的id(可以随便填写)
  • 返回值: 返回一个整型的key值,这个key值起到唯一标识的作用。如果失败返回-1。
  • 用途: 这个函数是通过所传入的一个字符串和一个整形数,根据它内部的一套算法尽可能的产生一个唯一标示的key值,而这个key值会在创建共享内存的时候使用。

shmget

在这里插入图片描述

  • 参数: 第一个参数就是通过ftok产生的key值,第二个参数是所要创建的共享内存的大小,第三个参数是shmflag,通常使用IPC_CREAT和IPC_EXCL。

    IPC_CREAT : 创建一个共享内存,如果已经存在那么就是用已经存在的共享内存。
    IPC_EXCL : 这个选项无法单独使用通常是配合IPC_CREAT使用,其作用是创建一个共享内存,如果此共享内存已经存在就报错返回,其目的就是创建一个全新的共享内存。

  • 返回值: 如果创建成功,那么返回共享内存的表示符shmid,如果创建失败就返回-1。

🚀key值的作用:

1.确保进行进程间通信的进程可以看到同一块共享内存。
2.确保创建出来的共享内存与其他已经存在的共享内存冲突。

共享内存使几个进程间得到通信,但是系统中存在大量的进程,可能会创建出大量的共享内存,OS势必会对这些共享内存进行管理操作,也就是说内核中肯定会存在描述共享内存的结构体,里面存储了关于共享内存的许多字段。而这些一个个的结构体对象肯定也会通过某种数据结构组织起来。而key值就会被存储在结构体中,在创建共享内存的时候,会先遍历已存在的共享内存的结构体对象,查看当前的key值是否已经在之前出现过,如果使用的是IPC_CREAT选项,若key值已经出现过那么就返回这块共享内存的标识符(类似于数组下标的东西),如果加上了IPC_EXCL选项,key值已经存在的话就会报错返回。

在这里插入图片描述

实际上不一定是以链表这种数据结构来存储的。key值实际上是在shmid_ds结构体对象的第一个成员shm_perm(也是个结构体)中存储的。

在这里插入图片描述

shmat

在这里插入图片描述

  • 参数: 第一个参数是共享内存的表示符,第二个参数是选择挂接的地址,通常我们设置为nullptr,意思是交给OS自动处理,第三个参数通常默认为0。
  • 返回值: 返回void的指针,与malloc的返回值十分类似,通常我们将其强转为char 类型的指针使用。如果挂接失败则返回(void*)-1。

前面提到,用户创建完共享内存后,要将共享内存挂接到共享它的进程的地址空间上,就是通过shmat函数来完成的,与动态库类似,动态库首先被加载到物理内存,通过页表的映射作用,映射到进程地址空间的共享区。

shmdt

在这里插入图片描述

  • 参数: 共享内存的首地址。
  • 返回值: 去关联成功的话返回0,否则返回-1。

进程在使用共享内存前首先要与共享内存关联,使用完毕后要与共享内存去关联。

shmctl

在这里插入图片描述

  • 参数: 第一个参数是共享内存标识符,第二个参数是命令选项如果是删除共享内存的话就选择IPC_RMID,相应的第三个参数设置为nullptr,如果第二个参数选择的是IPC_SET是将内核数据结构的信息提取出来,那么第三个参数就为缓冲区的地址。
  • 返回值: 成功的话返回0,否则返回-1。

🚀试着获取内核数据结构:

#define SERVER 0
#define CLIENT 1
class Init
{
public:
    Init(int n)
        : type(n)
    {
        int key = get_key();
        if (type == SERVER)
            shmid = create_shm(key, gsize);
        else
            shmid = get_shm(key, gsize);
        start = attch_shm(shmid);
    }
    char *getstart()
    {
        return start;
    }
    int getshmid()
    {
        return shmid;
    }
    ~Init()
    {
        detach_shm(start);
        if (type == SERVER)
            del_shm(shmid);
    }

private:
    char *start;
    int type;
    int shmid;
};
int main()
{
    Init init(SERVER);
    struct shmid_ds p;
    shmctl(init.getshmid(), IPC_SET, &p);
    cout << "key : " << p.shm_perm.__key << endl;
    return 0;
}

在这里插入图片描述

共享内存的原理

在这里插入图片描述
🚀共享内存是物理内存的一段,通过页表映射到共享它的进程的地址空间上,并且将这块空间的首地址返回给用户。这样两个进程就可以看到同一份资源,可以进行进程间通信了。
🚀系统同存在许多共享内存,OS为了管理这些共享内存,会为其创建结构体对象例如struct shmid_ds,里面存储了关于共享内存的属性。key值被存储在struct shmid_ds内部的第一个成员struct shm_perm中。
🚀对于多个这样像shmid_ds这样的结构体,在内核中会议某种数据结构将它们组织起来。所谓的shmid共享内存标识符就类似于数组的下标。
🚀key值是在内核中使用的,shmid是在用户层使用的。

System V标准的进程间通信的方式,除了共享内存外还有消息队列和信号量。
描述这三个东西的结构体中都有 struct ipc_perm这个结构体,在内核中其实是将这个结构体的指针组织了起来。无论是共享内存,还是消息队列,还是信号量,它们都有这个结构体,它们三个组织在同一数据结构中的。由于perm这个结构体是第一个成员,所以perm的地址与整个结构体的地址是相同的,所以如果想得到整个结构体,可以通过指针强转的方式得到。

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

共享内存实现两个进程间通信

由于两个进程使用共享内存进行通信时,所用的系统调用接口大致相似,所以我们把这些接口进行简单的封装放在头文件中。即使这样两个进程在调用这些接口的代码也十分相似,所以可以封装成一个类,这个类的构造函数中进行共享内存的创建和挂接,析构函数中进行去关联和删除共享内存,这样就简化了代码,并且使代码更美观。

🚀使用共享内存的一般步骤:

  • 创建共享内存
  • 共享内存与进程关联
  • 共享内存与进程去关联
  • 释放共享内存空间

🚀comm.hpp

#pragma once
#include <iostream>
using namespace std;
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <cstdio>
#include <sys/shm.h>

#define PATHNAME "."
#define PROJID 1234

const int gsize = 4096;
key_t get_key()
{
    key_t n = ftok(PATHNAME, PROJID);
    if (n == -1)
    {
        perror("ftok");
        exit(1);
    }
    return n;
}

int create_shm_helper(int key, size_t size, int shmflag)
{
    int shm_id = shmget(key, size, shmflag);
    if (shm_id == -1)
    {
        perror("shmget");
        exit(2);
    }
    return shm_id;
}

int create_shm(int key, size_t size)
{
    return create_shm_helper(key, size, IPC_CREAT | IPC_EXCL | 0664);
}

int get_shm(int key, size_t size)
{
    return create_shm_helper(key, size, IPC_CREAT);
}

char *attch_shm(int shm_id)
{
    char *start = (char *)shmat(shm_id, nullptr, 0);
    if ((void *)start == (void *)-1)
    {
        perror("shmat");
        exit(3);
    }
    return start;
}
void detach_shm(char *start)
{
    int n = shmdt(start);
    if (n == -1)
    {
        perror("shmdt");
        exit(4);
    }
}
void del_shm(int shmid)
{
    int n = shmctl(shmid, IPC_RMID, nullptr);
    if (n == -1)
    {
        perror("shmctr");
        exit(5);
    }
}
#define SERVER 0
#define CLIENT 1
class Init
{
public:
    Init(int n)
        : type(n)
    {
        int key = get_key();
        if (type == SERVER)
            shmid = create_shm(key, gsize);
        else
            shmid = get_shm(key, gsize);
        start = attch_shm(shmid);
    }
    char *getstart()
    {
        return start;
    }
    int getshmid()
    {
        return shmid;
    }
    ~Init()
    {
        detach_shm(start);
        if (type == SERVER)
            del_shm(shmid);
    }

private:
    char *start;
    int type;
    int shmid;
};

🚀server.cpp

#include "comm.hpp"

int main()
{
    Init init(SERVER);
    // struct shmid_ds p;
    // shmctl(init.getshmid(), IPC_SET, &p);
    // cout << "key : " << p.shm_perm.__key << endl;
    char *start = init.getstart();
    int n = 0;
    while (n < 30)
    {
        cout << start << endl;
        sleep(1);
        n++;
    }
    return 0;
}

🚀client.cpp

#include "comm.hpp"
int main()
{
    Init init(CLIENT);
    char *start = init.getstart();
    char ch = 'A';
    while (ch <= 'Z')
    {
        start[ch - 'A'] = ch;
        ch++;
        start[ch - 'A'] = '\0';
        sleep(1);
    }
    return 0;
}

这份小的代码就是让两个进程实现通信,客户端向共享内存中写入,服务端从共享内存中读取并打印到显示器。

🚀运行效果
在这里插入图片描述

共享内存的特点

🚀共享内存是进程间通信最快的方式,创建好共享内存并与进程关联后,就可以直接使用这块空间,与管道相比,管道实现通信的方式通过read,write接口来访问管道资源的,这样就会比共享内存多了两次数据在缓冲区之间拷贝的操作,这也就是为什么管道的速度比共享内存慢了。
🚀共享内存没有同步与互斥的机制做保护,管道通信的话,写端在向管道中写数据的时候,读端会阻塞直到写端把数据写完读端才能读取数据,而共享内存不存在这种保护机制,数据的写入和读取可以同时进行,这回造成许多问题。
🚀共享内存的声明周期是随操作系统的,而管道的声明周期是随进程的。
🚀共享内存的大小是向上对其到页大小的整数倍的。
在这里插入图片描述
操作系统中空间的大小是以页为单位的,每页的大小是4096字节。通过ipcs -m 指令可以查看共享内存的属性。
在这里插入图片描述
可以看到这里显示共享内存的大小是4097字节,但其实OS为其开辟了8KB的空间,只是允许用户使用4097大小的空间。

🚀使用ipcrm -m shmid 删除共享内存
在这里插入图片描述

🚀共享内存也是有权限的,在使用shmget系统调用创建共享内存的时候,其三个参数shmflag可以加上共享内存的权限,IPC_CREAT | IPC_EXCL | 0666,这样传参。
在这里插入图片描述
🚀nattch表示有几个进程与共享内存进行关联。

共享内存与管道配合使用

两个进程间通信

🚀上面提到共享内存没有同步与互斥的保护机制,通常是与其他方式配合使用的。我们可以通过共享内存与命名管道相配合的方式来完成进程间通信。在客户端与服务端之间创建两个命名管道,这两个管道的作用是,写端向共享内存写完数据后向读端发送一个信号使其开始在共享内存中读取数据,通向读取完成后给它再给写端发送信号,告诉写端现在可以向共享内存中写入数据了,这样可以达到写端写完数据读端才能读取,读端读取完毕后写端才能继续写入数据。

comm.hpp

#pragma once
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <cerrno>
#include <fcntl.h>
#include <sys/ipc.h>
#include <cstdlib>
#include <sys/shm.h>
#include <cstring>
using namespace std;

#define S_TO_C "pipe1"
#define C_TO_S "pipe2"
const int gsize = 4096;
#define PATHNAME "."
#define PROJ_ID 1234

key_t getKey()
{
    key_t k = ftok(PATHNAME, PROJ_ID);
    if (k == -1)
    {
        perror("ftok");
        exit(5);
    }
    return k;
}

int createShmHelper(key_t key, int flag)
{
    int shmid = shmget(key, gsize, flag);
    if (shmid == -1)
    {
        perror("shmget");
        exit(6);
    }
    return shmid;
}

int createShm(key_t key)
{
    return createShmHelper(key, IPC_CREAT | IPC_EXCL | 0664);
}

int getShm(key_t key)
{
    return createShmHelper(key, IPC_CREAT);
}

char *attchShm(int shmid)
{
    char *start = (char *)shmat(shmid, nullptr, 0);
    if ((void *)start == (void *)-1)
    {
        perror("shmat");
        exit(7);
    }
    return start;
}

void detachShm(char *start)
{
    int n = shmdt(start);
    if (n == -1)
    {
        perror("shmdt");
        exit(9);
    }
}

void delShm(int shmid)
{
    int n = shmctl(shmid, IPC_RMID, nullptr);
    if (n == -1)
    {
        perror("shmctl");
        exit(8);
    }
}

#define SERVER 0
#define CLIENT 1
class Init
{
public:
    Init(int n)
        : type(n)
    {
        key_t key = getKey();
        if (type == SERVER)
            shmid = createShm(key);
        else
            shmid = getShm(key);
        start = attchShm(shmid);
    }
    char *getstart()
    {
        return start;
    }
    ~Init()
    {
        detachShm(start);
        if (type == SERVER)
            delShm(shmid);
    }

private:
    char *start;
    int type;
    int shmid;
};

server.cpp

#include "comm.hpp"
int main()
{
    // 1.创建两个命名管道
    int n = mkfifo(S_TO_C, 0664);
    if (n == -1)
    {
        cerr << errno << endl;
        return 1;
    }
    int m = mkfifo(C_TO_S, 0664);
    if (m == -1)
    {
        cerr << errno << endl;
        return 2;
    }
    // 2.服务端以读的方式打开pipe2,写的方式打开pipe1
    cout << "----------------" << endl;
    int wfd = open(S_TO_C, O_WRONLY);
    cout << "----------------" << endl;
    if (wfd == -1)
    {
        cerr << errno << endl;
        return 3;
    }
    int rfd = open(C_TO_S, O_RDONLY);
    if (rfd == -1)
    {
        cerr << errno << endl;
        return 4;
    }

    // 3.共享内存相关工作
    Init init(SERVER);
    char *start = init.getstart();
    // 通信
    int send = 1;
    write(wfd, &send, sizeof(send));
    while (true)
    {
        int receive = 0;
        // 客户端写完数据,发送信号,服务端接收到信号后再读取
        read(rfd, &receive, sizeof(receive));
        if (receive)
        {
            cout << "收到来自客户端允许读取数据的信号" << endl;
            cout << start << endl;
        }
        else
        {
            break;
        }
        // 读取完给客户端发送信号,让其继续写入数据
        write(wfd, &send, sizeof(send));
        sleep(1);
    }
    // 4.关闭管道
    close(wfd);
    close(rfd);
    unlink(S_TO_C);
    unlink(C_TO_S);
    return 0;
}

client.cpp

#include "comm.hpp"

int main()
{
    // 2.服务端以读的方式打开pipe1,写的方式打开pipe2
    int rfd = open(S_TO_C, O_RDONLY);
    if (rfd == -1)
    {
        cerr << errno << endl;
        return 4;
    }
    int wfd = open(C_TO_S, O_WRONLY);
    if (wfd == -1)
    {
        cerr << errno << endl;
        return 3;
    }
    sleep(1);
    // 3.共享内存相关工作
    Init init(CLIENT);
    char *start = init.getstart();
    // 通信
    int cnt = 1;
    while (true)
    {
        int receive = 0;
        read(rfd, &receive, sizeof(receive));
        if (receive)
        {
            cout << "收到服务端信号,继续写入" << endl;
            memset(start, '\0', gsize);
            snprintf(start, 4096, "%d->%s", cnt++, "hello shm_pipe");
            // strcpy(start, "hello shm_pipe\n");
        }
        int send = 1;
        write(wfd, &send, sizeof(send));
        sleep(1);
    }

    // 4.关闭管道
    close(wfd);
    close(rfd);
    return 0;
}

🚀程序的运行效果:
在这里插入图片描述

多个进程间通信

🚀一块共享内存可以被多个进程关联,那么它可以有多个读端和多个写端,这里实现一个只有一个写端有多个读端的例子,首先创建若干个管道,管道的写端都是ctrlProcess进程,读端分别是process1,process2 … 当ctrlProcess进程向共享内存写入数据完成后,可以选择让哪个进程来读取共享内存中的数据。

comm.hpp

#pragma once
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <cerrno>
#include <fcntl.h>
#include <sys/ipc.h>
#include <cstdlib>
#include <sys/shm.h>
#include <cstring>
#include <string>
#include <vector>
#include <cstdio>
using namespace std;

// 管道名称
const int N = 100;
struct Pipe
{
    static string pipe_name[N];
    static int _size;
    Pipe()
    {
    }

public:
    void Add(const char *str)
    {
        pipe_name[_size] = str;
        _size++;
    }
};
int Pipe::_size = 3;
string Pipe::pipe_name[N] = {
    "pipe1",
    "pipe2",
    "pipe3"};
const int gsize = 4096;
#define PATHNAME "."
#define PROJ_ID 1234

key_t getKey()
{
    key_t k = ftok(PATHNAME, PROJ_ID);
    if (k == -1)
    {
        perror("ftok");
        exit(5);
    }
    return k;
}

int createShmHelper(key_t key, int flag)
{
    int shmid = shmget(key, gsize, flag);
    if (shmid == -1)
    {
        perror("shmget");
        exit(6);
    }
    return shmid;
}

int createShm(key_t key)
{
    return createShmHelper(key, IPC_CREAT | IPC_EXCL | 0664);
}

int getShm(key_t key)
{
    return createShmHelper(key, IPC_CREAT);
}

char *attchShm(int shmid)
{
    char *start = (char *)shmat(shmid, nullptr, 0);
    if ((void *)start == (void *)-1)
    {
        perror("shmat");
        exit(7);
    }
    return start;
}

void detachShm(char *start)
{
    int n = shmdt(start);
    if (n == -1)
    {
        perror("shmdt");
        exit(9);
    }
}

void delShm(int shmid)
{
    int n = shmctl(shmid, IPC_RMID, nullptr);
    if (n == -1)
    {
        perror("shmctl");
        exit(8);
    }
}

#define SERVER 0
#define CLIENT 1
class Init
{
public:
    Init(int n)
        : type(n)
    {
        key_t key = getKey();
        if (type == SERVER)
            shmid = createShm(key);
        else
            shmid = getShm(key);
        start = attchShm(shmid);
    }
    char *getstart()
    {
        return start;
    }
    ~Init()
    {
        detachShm(start);
        if (type == SERVER)
            delShm(shmid);
    }

private:
    char *start;
    int type;
    int shmid;
};

ctrlProcess.cpp

#include "comm.hpp"

class Endpoint
{
public:
    Endpoint(const string &str, int wfd)
        : _pipe_name(str), _wfd(wfd)
    {
    }

public:
    string _pipe_name;
    int _wfd;
};

int select_process()
{
    int select = 0;
    cout << "#####################" << endl;
    cout << "######0.process1#####" << endl;
    cout << "######1.process2#####" << endl;
    cout << "######2.process3#####" << endl;
    cout << "####    3.exit   ####" << endl;
    cout << "Please select# ";
    cin >> select;
    getchar();
    return select;
}
int main()
{
    // 创建共享内存
    Init init(SERVER);
    // 创建命名管道文件
    for (int i = 0; i < Pipe::_size; i++)
    {
        int n = mkfifo(Pipe::pipe_name[i].c_str(), 0664);
        if (n == -1)
        {
            perror("mkfifo");
            exit(-1);
        }
    }
    // 以写的方式打开这些命名管道
    vector<Endpoint> end_points;
    for (int i = 0; i < Pipe::_size; i++)
    {
        int wfd = open(Pipe::pipe_name[i].c_str(), O_WRONLY);
        if (wfd == -1)
        {
            perror("open");
            exit(-2);
        }
        end_points.push_back(Endpoint(Pipe::pipe_name[i], wfd));
    }

    // 通信
    char *start = init.getstart();
    while (true)
    {
        int select = select_process();
        if (select == 3)
            break;
        if (select < 0 || select > 3)
            continue;
        memset(start, '\0', 4096);
        cout << "向共享内存写入信息# ";
        fgets(start, 4000, stdin);
        // cin >> start;
        //  通知相应进程
        int command = 1;
        write(end_points[select]._wfd, &command, sizeof(int));
        sleep(2);
        cout << endl;
    }

    // 关闭pipe
    for (int i = 0; i < Pipe::_size; i++)
    {
        close(end_points[i]._wfd);
    }

    // unlink pipe
    for (int i = 0; i < Pipe::_size; i++)
    {
        unlink(end_points[i]._pipe_name.c_str());
    }
    return 0;
}

process1.cpp

#include "comm.hpp"
int main()
{
    // 打开相应管道
    int rfd = open(Pipe::pipe_name[0].c_str(), O_RDONLY);
    if (rfd == -1)
    {
        perror("open");
        exit(-3);
    }
    // 挂接共享内存
    Init init(CLIENT);
    char *start = init.getstart();
    // 接受来自ctrl的命令
    while (true)
    {
        int command = 0;
        int n = read(rfd, &command, sizeof(int));
        if (n == 4)
        {
            if (command == 1)
            {
                cout << "process1收到从共享内存读取数据的命令" << endl;
                cout << start;
            }
        }
        else if (n == 0)
        {
            cout << "写端关闭" << endl;
            break;
        }
        else
            break;
    }
    // 关闭pipe
    close(rfd);
    return 0;
}

process2.cpp

#include "comm.hpp"

int main()
{
    // 打开相应管道
    int rfd = open(Pipe::pipe_name[1].c_str(), O_RDONLY);
    if (rfd == -1)
    {
        perror("open");
        exit(-3);
    }
    // 挂接共享内存
    Init init(CLIENT);

    char *start = init.getstart();
    // 接受来自ctrl的命令
    while (true)
    {
        int command = 0;
        int n = read(rfd, &command, sizeof(int));
        if (n == 4)
        {
            if (command == 1)
            {
                cout << "process2收到从共享内存读取数据的命令" << endl;
                cout << start;
            }
        }
        else if (n == 0)
        {
            cout << "写端关闭" << endl;
            break;
        }
        else
            break;
    }
    close(rfd);

    return 0;
}

process3.cpp

#include "comm.hpp"
int main()
{
    // 打开相应管道
    int rfd = open(Pipe::pipe_name[2].c_str(), O_RDONLY);
    if (rfd == -1)
    {
        perror("open");
        exit(-3);
    }
    // 挂接共享内存
    Init init(CLIENT);
    char *start = init.getstart();
    // 接受来自ctrl的命令
    while (true)
    {
        int command = 0;
        int n = read(rfd, &command, sizeof(int));
        if (n == 4)
        {
            if (command == 1)
            {
                cout << "process3收到从共享内存读取数据的命令" << endl;
                cout << start;
            }
        }
        else if (n == 0)
        {
            cout << "写端关闭" << endl;
            break;
        }
        else
            break;
    }
    close(rfd);

    return 0;
}

🚀运行效果:
在这里插入图片描述

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

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

相关文章

网络工程师的水平检测1

水平测试 文章目录 水平测试填空题&#xff08;11分&#xff09;判断题&#xff08;9分&#xff09;选择题&#xff08;8分&#xff09;简答题&#xff08;26分&#xff09;子网划分&#xff08;24分&#xff09;实验拓扑&#xff08;19分&#xff09;填空题&#xff08;5分&am…

【MySQL】数据库完整性和安全性

目录 一、完整性 1.概念 2.sql语言支持的两种约束 2.1静态约束 撤销追加约束 断言 2.3动态约束 触发器 二、安全性 用DBMS对数据库实现的两个特性 一、完整性 1.概念 指dbms保证的db的一种特性&#xff0c;在任何情况下的正确性、有效性、一致性 原理图 广义完整性&…

园区路线地图指引图怎么画?园区地图三维图怎么画?

目前在园区信息化应用形式中&#xff0c;广泛缺乏专业电子地图的使用&#xff0c;因此&#xff0c;使这种高效的信息化工具的应用受到了很大限制。有些仅以图片代替&#xff0c;但图片没有空间计算、检索、路径设计的能力&#xff0c;在地图应用形式中&#xff0c;使用价值很低…

Day953.以假设驱动为指引 -遗留系统现代化实战

以假设驱动为指引 Hi&#xff0c;我是阿昌&#xff0c;今天学习记录的是关于以假设驱动为指引的内容。 很多人在做遗留系统现代化的时候呢&#xff0c;总觉得它是一个十分复杂的技术问题。 本来嘛&#xff0c;无论是代码的重构、架构的拆分&#xff0c;还是 DevOps 平台的搭…

除了学历,你更需要有能力

遥想当年&#xff0c;家里培养出一个大学生&#xff0c;是多荣耀的事&#xff01;可现今却处于一个比较尴尬的状态。 为什么大学生贬值得这么厉害&#xff1f;其实大学生之所以会不值钱不外乎三大原因&#xff1a;量大、与企业需求不匹配、质量差。 高校扩招下&#xff0c;大…

Python每日一练(20230423)

目录 1. 删除链表的倒数第 N 个结点 &#x1f31f;&#x1f31f; 2. 最小覆盖子串 &#x1f31f;&#x1f31f;&#x1f31f; 3. 二叉树的层序遍历 &#x1f31f;&#x1f31f; &#x1f31f; 每日一练刷题专栏 &#x1f31f; Golang每日一练 专栏 Python每日一练 专栏…

Java核心技术 卷1-总结-11

Java核心技术 卷1-总结-11 Java 集合框架将集合的接口与实现分离Collection接口迭代器泛型实用方法集合框架中的接口 Java 集合框架 将集合的接口与实现分离 Java集合类库将接口&#xff08;interface&#xff09;与实现&#xff08;implementation&#xff09;分离。 例如队…

小航助学答题系统编程等级考试scratch一级真题2023年3月(含题库答题软件账号)

青少年编程等级考试scratch真题答题考试系统请点击 电子学会-全国青少年编程等级考试真题Scratch一级&#xff08;2019年3月&#xff09;在线答题_程序猿下山的博客-CSDN博客_小航答题助手 1.下列说法不正确的是&#xff1f;&#xff08; &#xff09; A.可以从声音库中随机…

使用buildroot编译完整系统【IMX6ULLPRO】

目录 构建 IMX6ULL Pro 版的根文件系统 编译系统 ​编辑 镜像文件 构建 IMX6ULL Pro 版的根文件系统 配置文件说明 编译系统 下面以 100ask_imx6ull_pro_ddr512m_systemV_qt5_defconfig 配置文 件为例&#xff0c;在 ubuntu 终端上说明 Buildroot 的配置过程&#x…

抖音数字人主播app

抖音数字人主播app是指一款利用计算机生成的虚拟数字人&#xff0c;在抖音平台上进行实时音视频传输和互动的应用程序。该软件可以让用户创建自己的虚拟数字人&#xff0c;并在抖音平台上进行实时互动和交流。 抖音数字人主播app通常需要包含以下功能&#xff1a; 3D建…

前端学习之路 来自前端方向学生的总结

恭喜您&#xff01;您发现了宝藏&#xff01; 我发现很多小伙伴&#xff0c;对于前端感兴趣&#xff0c;也很想去学好&#xff0c;但是却无从下手&#xff0c;不知道如何去学习。作为一名现处于大三即将大四的学生&#xff0c;借此机会来分享分享我的前端学习之路&#xff01;…

Visual Instruction Tuning: 用LLaVA近似多模态GPT-4

©Paperweekly 原创 作者 | Chunyuan Li 使用 GPT-4 进行视觉指令学习&#xff01;Visual Instruction Tuning with GPT-4! ▲ Generated by GLIGEN (https://gligen.github.io/): A cute lava llama and glasses 我们分享了 LLaVA (Language-and-Vision Assistant)&#…

设计模式--单例模式

目录 介绍 单例模式的八种实现方式 饿汉式(静态常量) 优缺点说明: 饿汉式(静态代码块) 优缺点说明 懒汉式(线程不安全) 优缺点说明 懒汉式(线程安全 同步方法) 优缺点说明 懒汉式(线程安全 同步代码块) 优缺点说明 双重检查 优缺点说明 静态内部类 优缺点说明 …

打怪升级之FPGA组成原理(LE部分)

FPGA芯片逻辑单元的原理 不论你使用哪一款FPGA芯片&#xff0c;其核心可编程逻辑单元都是从一段内存种按顺序读取执行并执行的过程。具体来说&#xff0c;FOGA芯片内部包括可编程逻辑块(LAB)、可配置输入输出单元(IOE)、时钟管理模块、嵌入式RAM(BRAN&#xff0c;在Cyclone IV…

PNAS: 这些病毒是原生动物基因组中的偷渡者

在对复杂单细胞微生物进行大规模研究时&#xff0c;奥地利因斯布鲁克大学生态学系的Christopher Bellas博士、Marie-Sophie Plakolb和Ruben Sommaruga教授发现了一个意外情况&#xff1a;微生物的基因组中找到超过30,000种先前未知的病毒DNA。这种“隐藏”的DNA可能允许宿主细胞…

字节跳动正式开源分布式训练调度框架 Primus

动手点关注 干货不迷路 项目地址&#xff1a;https://github.com/bytedance/primus 随着机器学习的发展&#xff0c;模型及训练模型所需的数据量越来越大&#xff0c;也都趋向于通过分布式训练实现。而算法工程师通常需要对这些分布式框架涉及到的底层文件存储和调度系统有较深…

基于 多态 的职工管理系统(Staff Management System)

目录 一、管理系统需求 作用&#xff1a;管理公司内所有员工的信息 分类&#xff1a;要显示每位员工的编号、姓名、岗位与职责 具体实现的功能&#xff1a; 二、创建管理 类 三、各个接口函数 1、菜单展示功能 2、 选择功能 3、创建员工功能 ①普通员工employee ②经理…

怎么批量把heic格式转化jpg,3招快速解决

怎么批量把heic格式转化jpg&#xff1f;heic是一种新型的图像文件格式&#xff0c;是苹果独家搞出来的一个图片格式&#xff0c;它小巧玲珑&#xff0c;而且图像质量超好&#xff0c;专门给iOS11系统用户用的。这种格式比老JPEG更厉害&#xff0c;不仅图片质量好&#xff0c;而…

【网络原理】应用层协议 与 传输层协议

✨个人主页&#xff1a;bit me&#x1f447; ✨当前专栏&#xff1a;Java EE初阶&#x1f447; 目 录 &#x1f3c9;一. 应用层协议⚾️二. 传输层协议&#x1f452;1. UDP 协议&#x1f302;2. 校验和&#x1f453;3. TCP 协议 &#x1f3c9;一. 应用层协议 我们自己写的应用…

Bitmap 实现当前在线用户数量

Bitmap是什么&#xff1f; Bitmap是Redis中的一种数据结构&#xff0c;它是一个类似于位数组的数据结构&#xff0c;用于处理位数据。在Redis中&#xff0c;Bitmap是使用字符串来存储的&#xff0c;一个Byte可以存储8个二进制位&#xff0c;一个字符串可以存储232个二进制位&a…