System V IPC奥秘:解锁共享内存、消息队列与信号量的高效通信之路

news2024/9/21 12:22:00
🍑个人主页:Jupiter.
🚀 所属专栏:Linux从入门到进阶
欢迎大家点赞收藏评论😊

在这里插入图片描述

在这里插入图片描述

目录

  • `🍑system V共享内存 `
      • `🍒共享内存的原理`
          • `共享内存数据结构`
          • `查看和删除共享内存资源的命令`
      • `🌻共享内存函数`
          • `shmget函数`
          • `ftok函数`
          • `shmat函数`
          • `shmdt函数`
          • `shmctl函数`
      • `🍃共享内存的优缺点`
  • `🦔system V消息队列(了解)`
          • `消息队列的相关操作接口`(相关接口的参数与共享内存类似)
  • `🦅system V信号量 sem`
      • `📚信号量理论`
          • `信号量的数据结构:`
          • `信号量的相关操作接口`
          • `指令查找以及删除信号量:`


🍑system V共享内存

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

🍒共享内存的原理

OS在内存上开辟一段空间,然后与进程构建映射(通过页表),映射到堆栈之间的共享区中,将该空间的起始地址(虚拟地址)返回给用户,该进程就可以通过虚拟地址访问该内存了。
其中,开辟空间和构建映射关系,需要修改进程地址空间和页表,都是内核数据结构,都是OS做的,所以OS会提供相应的系统调用,如果通过同样的系统调用,就也可以将该空间映射到另一个进程的共享区中,也可以使用虚拟地址访问该内存,所以就让不同的进程看到了同一个内存资源。

共享内存数据结构

一个时刻,在系统中可能存在很多个进程在进行通信,就可能存在很多个共享内存,OS就会将这些共享内存进行管理,先描述再组织。下面是描述共享内存的数据结构

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 and
                                           SHM_LOCKED flags */
      unsigned short __seq;    /* Sequence number */
};

共享内存存在很多个,那么怎么保证两个进程看到的是同一个共享内存呢?

  • key_t :共享内存的唯一值。
  • 我们只需要两个进程,在使用 ftok 形成key时,使用同样的参数即可。

共享内存,如果进程结束,没有主动释放,则会一直存在,除非重启系统。共享内存的生命周期随内核

查看和删除共享内存资源的命令
  • ipcs -m :查看系统中指定用户创建的共享内存。
  • ipcrm -m shmid :删除指定shmid的共享内存。

🌻共享内存函数

shmget函数

功能:用来创建共享内存。

  • int shmget(key_t key, size_t size, int shmflg);

参数:

  • key:这个共享内存段名字 :共享内存在内核中唯一性的标识。
  • size:共享内存大小。在内核中,共享内存的大小是以4KB为基本单位的。如果size设置为4097,则实际开辟的是8KB。
  • shmflg:由九个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样的。
  • 返回值:成功返回一个非负整数,即该共享内存段的标识码;失败返回-1。

常用shmflg选项

  • IPC_CREAT:如果创建的共享内存不存在,就创建,如果存在,直接获取它。
  • IPC_CREAT:单独使用没有意义。
  • IPC_CREAT | IPC_CREAT:如果创建的共享内存不存在,就创建,如果存在,就会出错返回。
  • 可以指定该共享内存的权限,直接 或上权限即可(如 |0666)。
ftok函数

功能:生成一个System V IPC key

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

用法:传入路径+文件名(随便一个字符串也行)和 一个整型,生成一个System V IPC key.
返回值:成功返回生成的key,失败返回-1,错误码被设置。

shmat函数

功能:将共享内存段连接到进程地址空间(建立映射)

  • void *shmat(int shmid, const void *shmaddr, int shmflg);

参数:

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

说明:
shmaddr 为 NULL,核心自动选择一个地址。
shmaddr 不为 NULLsmflg 无 SHM_RND标记,则以shmaddr为连接地址。
shmaddr 不为 NULLshmflg 设置了 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:将要采取的动作(有三个可取值)。
  • buf:指向一个保存着共享内存的模式状态和访问权限的数据结构(如果不关心,直接设置为nullptr)。
  • 返回值:成功返回0;失败返回-1。

🍃共享内存的优缺点

  • 共享内存优点:是进程间通信中速度最快的。(管道通信中,进程需要通过系统调用往管道里面读写数据,两个进程通信一次,至少两次系统调用;而共享内存,进程可以通过共享内存的起始虚拟地址,直接访问到内存,一个进程向共享内存中写了数据,另一个进程就可以直接看到,只需要一次系统调用即可)。

  • 共享内存缺点:没有提供进程间任何的协同机制,如果需要,可以自己实现。有些场景不适用,比如我们一个进程发送一个数据,如果没有协同机制,那么可能该进程数据才写一半,另一个进程就读取了,会造成数据不一致。

    • 可以通过管道来实现同步。示例代码中有体现。

示例代码演示:

Comm.hpp

#pragma once
#include <iostream>
#include <cerrno>
#include <cstring>
#include <cstdlib>
#include <string>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>

using namespace std;

const char *pathname = "/home/whb";
const int proj_id = 0x66;

// 在内核中,共享内存的大小是以4KB为基本单位的. 你只能用你申请的大小。建议申请大小是n*4KB
const int defaultsize = 4096; // 单位是字节

std::string ToHex(key_t k){
    char buffer[1024];
    snprintf(buffer, sizeof(buffer), "0x%x", k);
    return buffer;
}

key_t GetShmKeyOrDie(){
    key_t k = ftok(pathname, proj_id);
    if (k < 0) {
        std::cerr << "ftok error, errno : " << errno << ", error string: " << strerror(errno) << std::endl;
        exit(1);
    }
    return k;
}

int CreateShmOrDie(key_t key, int size, int flag){
    int shmid = shmget(key, size, flag);
    if (shmid < 0){
        std::cerr << "shmget error, errno : " << errno << ", error string: " << strerror(errno) << std::endl;
        exit(2);
    }
    return shmid;
}

int CreateShm(key_t key, int size){
    // IPC_CREAT: 不存在就创建,存在就获取
    // IPC_EXCL: 没有意义
    // IPC_CREAT | IPC_EXCL: 不存在就创建,存在就出错返回
    return CreateShmOrDie(key, size, IPC_CREAT | IPC_EXCL | 0666);
}

int GetShm(key_t key, int size){
    return CreateShmOrDie(key, size, IPC_CREAT);
}

void DeleteShm(int shmid){
    int n = shmctl(shmid, IPC_RMID, nullptr);
    if (n < 0){
        std::cerr << "shmctl error" << std::endl;
    }
    else{
        std::cout << "shmctl delete shm success, shmid: " << shmid << std::endl;
    }
}

void ShmDebug(int shmid){
    struct shmid_ds shmds;
    int n = shmctl(shmid, IPC_STAT, &shmds);
    if (n < 0){
        std::cerr << "shmctl error" << std::endl;
        return;
    }
    std::cout << "shmds.shm_segsz: " << shmds.shm_segsz << std::endl;
    std::cout << "shmds.shm_nattch:" << shmds.shm_nattch << std::endl;
    std::cout << "shmds.shm_ctime:" << shmds.shm_ctime << std::endl;
    std::cout << "shmds.shm_perm.__key:" << ToHex(shmds.shm_perm.__key) << std::endl;
}

void *ShmAttach(int shmid){
    void *addr = shmat(shmid, nullptr, 0);
    if ((long long int)addr == -1){
        std::cerr << "shmat error" << std::endl;
        return nullptr;
    }
    return addr;
}

void ShmDetach(void *addr){
    int n = shmdt(addr);
    if (n < 0){
        std::cerr << "shmdt error" << std::endl;
    }
}

Fofi.hpp

#ifndef __COMM_HPP__
#define __COMM_HPP__

#include <iostream>
#include <string>
#include <cerrno>
#include <cstring>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <cassert>

using namespace std;

#define Mode 0666
#define Path "./fifo"

class Fifo
{
public:
    Fifo(const string &path = Path) : _path(path)
    {
        umask(0);
        int n = mkfifo(_path.c_str(), Mode);
        if (n == 0)
        {
            cout << "mkfifo success" << endl;
        }
        else
        {
            cerr << "mkfifo failed, errno: " << errno << ", errstring: " << strerror(errno) << endl;
        }
    }
    ~Fifo()
    {
        int n = unlink(_path.c_str());
        if (n == 0)
        {
            cout << "remove fifo file " << _path << " success" << endl;
        }
        else
        {
            cerr << "remove failed, errno: " << errno << ", errstring: " << strerror(errno) << endl;
        }
    }
private:
    string _path; // 文件路径+文件名
};

class Sync{
public:
    Sync() : rfd(-1), wfd(-1)
    {}
    void OpenReadOrDie()
    {
        rfd = open(Path, O_RDONLY);
        if (rfd < 0)
            exit(1);
    }
    void OpenWriteOrDie()
    {
        wfd = open(Path, O_WRONLY);
        if (wfd < 0)
            exit(1);
    }
    bool Wait()
    {
        bool ret = true;
        uint32_t c = 0;
        ssize_t n = read(rfd, &c, sizeof(uint32_t));
        if (n == sizeof(uint32_t))
        {
            std::cout << "server wakeup, begin read shm..." << std::endl;
        }
        else if (n == 0)
        {
            ret = false;
        }
        else
        {
            return false;
        }
        return ret;
    }
    void Wakeup()
    {
        uint32_t c = 0;
        ssize_t n = write(wfd, &c, sizeof(c));
        assert(n == sizeof(uint32_t));

        std::cout << "wakeup server..." << std::endl;
    }
    ~Sync() {}
private:
    int rfd;
    int wfd;
};

#endif

ShmClient.cc

#include "Comm.hpp"
#include "Fifo.hpp"
#include <unistd.h>
int main()
{
    key_t key = GetShmKeyOrDie();
    std::cout << "key: " << ToHex(key) << std::endl;
    // sleep(2);

    int shmid = GetShm(key, defaultsize);
    std::cout << "shmid: " << shmid << std::endl;
    // sleep(2);

    char *addr = (char *)ShmAttach(shmid);
    std::cout << "Attach shm success, addr: " << ToHex((uint64_t)addr) << std::endl;
    // sleep(5);

    memset(addr, 0, defaultsize);
    Sync syn;
    syn.OpenWriteOrDie();

    // 可以进行通信了
    for (char c = 'A'; c <= 'Z'; c++) // pipe, fifo, ->read/write->系统调用, shm -> 没有使用系统调用!!
    {
        addr[c - 'A'] = c;
        sleep(1);
        syn.Wakeup();
    }

    ShmDetach(addr);
    std::cout << "Detach shm success, addr: " << ToHex((uint64_t)addr) << std::endl;
    sleep(5);

    return 0;
}

Shm.Server.cc

#include "Comm.hpp"
#include "Fifo.hpp"
#include <unistd.h>

int main(){
    // 1. 获取key
    key_t key = GetShmKeyOrDie();
    std::cout << "key: " << ToHex(key) << std::endl;
    // sleep(2);

    // 2. 创建共享内存
    int shmid = CreateShm(key, defaultsize);
    std::cout << "shmid: " << shmid << std::endl;
    // sleep(2);

    // ShmDebug(shmid);
    // 4. 将共享内存和进程进行挂接(关联)
    char *addr = (char *)ShmAttach(shmid);
    std::cout << "Attach shm success, addr: " << ToHex((uint64_t)addr) << std::endl;

    // 0. 先引入管道
    Fifo fifo;
    Sync syn;
    syn.OpenReadOrDie();

    // 可以进行通信了
    for(;;)
    {
        if(!syn.Wait()) break;
        cout << "shm content: " << addr << std::endl;
    }

    ShmDetach(addr);
    std::cout << "Detach shm success, addr: " << ToHex((uint64_t)addr) << std::endl;

    // 3. 删除共享内存
    DeleteShm(shmid);
    return 0;
}

🦔system V消息队列(了解)

  • 消息队列提供了一个从一个进程向另外一个进程发送一个有类型的数据块的方法。
  • 每个数据块都被认为是有一个类型,接收者进程接收的数据块可以有不同的类型值。
  • 特性方面
    • IPC资源必须删除,否则不会自动清除,除非重启,所以system V IPC资源的生命周期随内核。

进程A与进程B,其中,进程A可以向内核中发送和读取数据块,进程B也可以发送和读取,发送的数据块放到一个队列中,当进程A在发送的时候,另一个进程也可以发送,数据块中有标记位标记是谁发送的。

消息队列的数据结构:

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 creation or last
                                              modification by msgctl() */
      unsigned long   msg_cbytes; /* # of bytes in queue */
      msgqnum_t       msg_qnum;   /* # number of messages in queue */
      msglen_t        msg_qbytes; /* Maximum # of bytes in queue */
      pid_t           msg_lspid;  /* PID of last msgsnd(2) */
      pid_t           msg_lrpid;  /* PID of last msgrcv(2) */
};
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 */
};


消息队列的相关操作接口(相关接口的参数与共享内存类似)
  • 创建消息队列

    int msgget(key_t key,int msgflg);

  • 删除消息队列

    int msgctl(int msqid,int cmd,struct msqid_ds *buf);

  • 向队列里发送数据

    int msgsnd(int msqid,const void* msgp,size_t msgsz,int msgflq);

  • 读取数据

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

  • 查找消息队列的指令

    ipcs -q

  • 删除消息队列的指令

    ipcrm -q msqid

🦅system V信号量 sem

信号量主要用于同步互斥的,下面先来看看什么是同步和互斥。

  • 进程互斥 ·
    • 由于各进程要求共享资源,而且有些资源需要互斥使用,因此各进程间竞争使用这些资源,进程的这种关系为进程的互斥
    • 系统中某些资源一次只允许一个进程使用,称这样的资源为临界资源或互斥资源。
    • 访问临界资源的代码段叫临界区
  • 特性方面
    • IPC资源必须删除,否则不会自动清除,除非重启,所以system V IPC资源的生命周期随内核

📚信号量理论

信号量(信号灯)

  • 对于一份内存资源(临界资源),如果只允许整体被使用的话,每一次只允许一个进程访问,这种工作做方式效率低。
  • 其中,信号量就提供了一种机制,将一份内存资源拆分成很多小资源,允许多个进程同时访问,前提:每一个进程访问的是不同的被切分的小资源,就能做到真正的并发访问。

在这种情况下,只需要1.限制进来的进程数。2.合理分配资源 即可。

信号量本质是一个计数器,是描述临界资源数量的计数器。(如下图,假设计数器 int count = 9;)
在访问资源的时候,会经历一下几个步骤:

  • 申请信号量,if count>0,则申请成功,并且会将count–,也叫做p操作
  • 访问临界资源。
  • 释放信号量,count++,叫做 v 操作;

在多进程场景,int不能实现信号量得效果,原因如下:

  • count++/--不是原子的,所以在++/–过程中,可能有其他进程也会申请信号量,会造成访问冲突问题。(比如现在count=1,然后一个进程来申请,然后再count–过程中,这时count还是1,又有另一个进程来申请信号量,又会count–,count–完成后,就变为成-1了)。
  • 无法在进程间共享,变量在进程中做修改时,会发生写时拷贝。所以须让不同的进程先看到同一份资源—计数器资源!所以信号量属于进程间通信。

所有进程,访问临界资源,都必须先申请信号量–所有的进程都得先看到一个信号量–所以信号量本身就是共享资源!所以信号量的申请(++)和释放(–)都必须是原子的。pv操作必须是原子的。

如果信号量的初始值是1,就是互斥,也就是二元信号量,也是后面文章会说到的 锁。

信号量的数据结构:
struct semid_ds {
       struct ipc_perm sem_perm;  /* Ownership and permissions */
       time_t          sem_otime; /* Last semop time */
       time_t          sem_ctime; /* Creation time/time of last
                                   modification via semctl() */
       unsigned long   sem_nsems; /* No. of semaphores in set */
};
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 */
};

信号量的相关操作接口
  • 申请信号量

    int semget(key_t key, int nsems, int semflg);

  • 控制信号量

    int semid, int semnum, int cmd, ...);

  • pv操作

    int semop(int semid, struct sembuf *sops, size_t nsops);

指令查找以及删除信号量:
  • 查找:ipcs - s
  • 删除:ipcrm -s semid

共享内存 信号量 消息队列 是OS系统特意设计的 system V进程间通信的。

共享内存 信号量 消息队列可以看成同一种资源,IPC资源,OS注定要对IPC资源进程管理–先描述再组织。

我们可以发现,在内核中,无论是共享内存 ,信号量 还是消息队列,描述他们的结构体里的第一个成员都是 kern ipc_perm类型的。在内核中,有一个数据结构ipc_id_ary,里面第一个成员是size,其余都是类似于指针数组,类型为 kern ipc_perm*,当系统创建了一个IPC 资源时,会将该资源的数据结构的第一个成员地址存放在该kern ipc_perm*数组中,这样,系统中的每一个IPC
资源都组织起来了,方便管理。类似于C++中的多态。

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

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

相关文章

Spacedrive:一款基于VDFS的跨平台文件管理器深度解析

前言 你的文件不再被各种设备、云盘束缚&#xff0c;而是像魔法般汇聚在一个地方&#xff0c;触手可及&#xff0c;那将是怎样的畅快淋漓&#xff1f;Spacedrive&#xff0c;这个名字听起来就像是穿越时空的驱动器&#xff0c;它正悄悄改变着我们对文件管理的认知&#xff1b;…

开发物联网驱动拍卖软件平台:如何实现了服务质量的全面提升

在数字化转型的浪潮下&#xff0c;物联网&#xff08;IoT&#xff09;技术正深刻地改变着各行各业的运作模式&#xff0c;拍卖行业也不例外。通过物联网的集成应用&#xff0c;拍卖平台能够实现更高效、透明和个性化的服务&#xff0c;极大地提升用户体验和服务质量。本文将以“…

一起学习LeetCode热题100道(44/100)

44.二叉搜索树中第 K 小的元素(学习) 给定一个二叉搜索树的根节点 root &#xff0c;和一个整数 k &#xff0c;请你设计一个算法查找其中第 k 小的元素&#xff08;从 1 开始计数&#xff09;。 示例 1&#xff1a; 输入&#xff1a;root [3,1,4,null,2], k 1 输出&#…

u盘启动选择uefi还是legacy_u盘启动选择uefi还是legacy详细分析

最近有很多网友问我想用U盘安装系统&#xff0c;按照网上教程按快捷方式(一般是f12)出现选择U盘菜单时&#xff0c;我到底是选择legacy开头的U盘还是uefi开头的U盘&#xff0c;其实这个取决你要安装什么系统或是磁盘分区类型是gpt还是mbr&#xff0c;比如2016年后出来的笔记本和…

EasyRecovery17中文版永久汉化版电脑数据恢复工具下载

&#x1f388;&#x1f389;安利时间到&#xff01;今天要跟大家分享的是——EasyRecovery17中文版的最新功能&#xff01;&#x1f389;&#x1f388; &#x1f31f;✨ “数据恢复小能手” ✨&#x1f31f; 让我来介绍一下这款软件的主打特点。 EasyRecovery17中文版是一款强…

谷歌账号活动异常,或者申诉回来以后需要手机验证的原因,以及验证手机号的错误操作和正确操作

有一些朋友在使用谷歌账号的时候&#xff0c;会遇到无法直接登录的情况&#xff0c;输入用户名、密码以后&#xff0c;提示说账号活动异常&#xff0c;需要验证手机号。 通常有以下两种情形和界面&#xff0c;出现这种情形的原因分别如下。 一、谷歌账号登录需要输入手机号码…

教你如何训练多模态理解模型

出发点&#xff1a;最近因为工作的需要&#xff0c;在研究多模态理解模型&#xff0c;看了最近一两年比较火的一些论文&#xff0c;感觉Nvidia的VILA工作可以作为比较好的多模态理解模型训练教程&#xff0c;也在这里介绍给大家。 多模态理解模型&#xff1a;也叫Large Vision…

玩游戏的时候怎么录屏?三种实用技巧

在数字化娱乐时代&#xff0c;录制游戏视频已成为玩家分享游戏体验的重要方式。录屏不仅能够记录精彩的游戏瞬间&#xff0c;还能用于制作教程、分析游戏策略或进行游戏直播。那么&#xff0c;玩游戏的时候怎么录屏呢?本文将介绍三种实用的录屏方法&#xff0c;帮助玩家们在畅…

WPF篇(20)- Menu菜单+ContextMenu上下文菜单+StatusBar状态栏

Menu菜单 Menu控件继承于MenuBase&#xff0c;而MenuBase继承于ItemsControl。所以学习Menu之前&#xff0c;要先了解一下MenuBase基类。它是一个抽象类&#xff0c;拥有一个ItemContainerTemplateSelector模板选择器&#xff0c;并重写了一些关于键盘和鼠标的方法。 Menu的子…

react的pdf转图片格式上传到后端

这个东西做的我真的是头昏脑涨 主要需求是,upload上传pdf,pdf转图片格式展示,以图片格式上传到后端 封装了组件代码,父组件直接放就可以了 使用的插件pdfjs-dist,版本是 "pdfjs-dist": "2.5.207",node:14.13.0/18.17.0/16.14.2都可以你们要注意n…

c语言学习,malloc()函数分析

1&#xff1a;malloc() 函数说明&#xff1a; 申请配置size大小内存空间 2&#xff1a;函数原型&#xff1a; void *malloc(size_t size) 3&#xff1a;函数参数&#xff1a; 参数size&#xff0c;为申请内存大小 4&#xff1a;返回值&#xff1a; 配置成功则返回指针&#…

吃透前端文件上传与文件相关操作 多文件上传 大文件切片上传 拖拽上传 后续还会更新 断点续传等等

最近在学文件上传的操作,所以想把学习到东西写成一文章 这片文章是我以小白视角 慢慢学习并熟悉前端文件相关操作的流程总结出来的 前端文件上传 我首先想到是 <input type"file">**选择文件**</input>如果我们想限制上传文件的格式,大小或进行裁剪分片…

产品经理-如何判断一个产品的好与坏(36)

当面试官问到,如何判断一个产品的好与坏时,该怎么回答,这个问题非常综合地考查了你对产品的理解&#xff0c;但是题目本身非常大且难有标准答案 即使是面试官也不敢说能答好这道题。求职者在遇到这种很开放的题目时&#xff0c;如果不假思索就开答&#xff0c;往往是很危险的。…

在亚马逊云科技上搭建云原生生成式AI教育学习平台

项目简介&#xff1a; 小李哥将继续每天介绍一个基于亚马逊云科技AWS云计算平台的全球前沿AI技术解决方案&#xff0c;帮助大家快速了解国际上最热门的云计算平台亚马逊云科技AWS AI最佳实践&#xff0c;并应用到自己的日常工作里。 本次介绍的是如何利用亚马逊云科技大模型托…

【python】Python中subprocess模块的参数解读以及应用实战

✨✨ 欢迎大家来到景天科技苑✨✨ &#x1f388;&#x1f388; 养成好习惯&#xff0c;先赞后看哦~&#x1f388;&#x1f388; &#x1f3c6; 作者简介&#xff1a;景天科技苑 &#x1f3c6;《头衔》&#xff1a;大厂架构师&#xff0c;华为云开发者社区专家博主&#xff0c;…

如何用AI技术运营漫画推文,实现轻松变现

最近&#xff0c;漫画推文在各大平台上特别火&#xff0c;加上AI技术的加持&#xff0c;现在大家都有机会分一杯羹。今天我就来详细聊聊&#xff0c;如何利用AI技术来运营漫画推文&#xff0c;实现轻松变现。 项目介绍 咱们先来了解一下这个项目到底是啥。漫画推文&#xff0c;…

流量池是什么?萤石物联网卡流量池使用指南

要将4G设备通过萤石开放平台上云&#xff0c;有单卡模式/流量池两种使用模式&#xff0c;以下介绍流量池功能的使用方法 一、账号注册和登录 打开 萤石开放平台官网登录您的账号。若您还没有账号&#xff0c;请注册 二、申请物联网卡 登录后进入物联网卡申请页面没有卡的情况下…

【FreeRTOS】队列实验-多设备玩游戏(红外改造)

目录 0 前言1. 队列实验_多设备玩游戏2 回顾程序3 程序改进3.1 创建队列3.1.1 方法3.1.2 实践 3.2 读队列3.2.1 方法3.2.2 实践 3.3 写队列3.3.1 方法3.3.2 实践 0 前言 学习视频&#xff1a; 【FreeRTOS入门与工程实践 --由浅入深带你学习FreeRTOS&#xff08;FreeRTOS教程 基…

WPS真题题库导入刷题小程序:百思考个人使用经验分享

这篇文章的诞生&#xff0c;是因为我即将踏上一场超级有趣的挑战——备考全国计算机等级二级WPS Office高级应用与设计的冒险之旅&#xff01; WPS的分值&#xff1a; 单项选择题20分(含公共基础知识部分10分)。 WPS处理文字文档操作题30分。 WPS处理电子表格操作题30分。 …

Taro+Vue 创建微信小程序

TaroVue 创建微信小程序 一、全局安装 tarojs/cli二、项目初始化三、现在去启动项目吧 一、全局安装 tarojs/cli npm install -g tarojs/cli //安装 npm info tarojs/cli //查看安装信息 如果正常显示版本说明成功了&#xff0c;就直接跳到第二步吧官网说&#xff1a;…