【Linux】进程间通信(管道通信、共享内存通信)

news2024/9/27 7:20:04

一.什么是进程间通信

进程间通信这五个字很好理解,就是进程和进程之间通信

那么为什么要有进程间通信呢?

1.数据传输:一个进程需要将它的数据发送给另一个进程

2.资源共享:多个进程之间共享同样的资源

3.通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件

4.进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。


在这里再举一个例子,例如在网络中,我们通过聊天软件和远处的别人进行聊天就是一种进程间的通信,是你在这边的进程和他在那边的进程之间的通信。 


那么进程间通信又有那些呢?

我们大致可以分为三种管道System V进程间通信POSIX进程间通信

1.管道           :是基于文件系统的进程间通信,又分为匿名管道和命名管道。

2.System V  :聚焦在本地的通信。

3.POSIX       :让通信可以跨主机。

但是在这篇博客中,只讲前两种,第三种会在后面与网络有关的博客中讲到。

那么我们应该如何理解通信的本质呢?

1.操作系统需要直接或者间接给通信双方进程提供“内存空间”。

2.要通信的进程,必须看到一份公共的资源。 

二.管道进程间通信

在Linux中对于管道的使用,大家可能也有过了解,但是管道到底是什么,以及它是如何实现的呢?

管道又可以分为匿名管道命名管道,我们先来看看匿名管道。 

又由于管道是基于文件系统的,这里会涉及到文件操作基础IO的内容,对于文件操作和基础IO的内容,我之前写过一篇博客来详细讲过,在这里就不再详细讲,有需要的可以到下面这个博客中看:

【Linux】基础IO-CSDN博客

匿名管道 

首先管道是基于文件系统的,那么在文件系统中,如果一个进程打开了一个文件,那么操作系统就会为这个文件创建一个文件结构体struct file变量,当我们对文件写入的时候,就是进程向这个struct file进行写入,同时我们也可以从这个struct file中读取。

其中这个struct file就可以用来做管道

如下图所示。

但是现在我们只有一个进程,还做不到进程间通信,所以我需要创建一个子进程来继续完成下面的工作。

这个是时候管道的基本雏形就已经出来,但是这个图还有一些问题,我们稍后慢慢讲。

pipe函数

对于struct file来说,通常都是我们来打开一个文件时才会有的,但是现在的管道操作不需要打开文件,所以这个struct file又该怎么创建呢?

操作系统给我们提供了一个pipe的函数,这个函数可以给我们的进程创建一个特殊的struct file这个struct file不属于磁盘中的任何文件,而是操作系统创建的一个临时的struct file,其目的就是为了给我们提供管道的操作。

同时这个pipe系统调用会默认打开读和写的操作。 


返回值:

如果成功创建管道,则返回0,如果创建失败,则返回-1,并设置errno来表示错误。

pipe系统调用如上图所示,其中参数为一个大小为2的数组,当调用这个函数后,操作系统就会自动的为当前这个进程创建一个管道,同分别以读和写的形式来打开管道,其中,pipefd[0]为读取操作的文件描述符,pipefd[1]为写入操作的文件描述符。 

调用这个函数后,效果如下图所示:


然后我们在进行创建一个子进程,创建完成后如下图所示:


 此时我们的父子进程都能对管道进行读写操作,但是这不是我们所希望的,因为管道应该是单向的,而不是双向的,而且此时的这种情况有一点的不安全,所以,我们需要关闭一些文件描述符,在下面的代码演示中,我会进行一个子进程往管道写入,而父进程往管道读取的操作,所以接下来还要关闭一些文件描述符,关闭后如下图所示。

使用例子 

这样我们就能达到一个进程间通信的效果了,完整示例代码如下: 

#include<iostream>

#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>

#include<cassert>
#include<cstdlib>
#include<cstdio>
#include<cstring>

using namespace std;

int main()
{
    int fds[2] = {0,0};
    int ret = pipe(fds);//其中fds[0]为读,fds[1]为写

    pid_t id = fork();//创建子进程
    assert(id >= 0);

    //我想做的是,父进程读取管道,子进程写入管道

    //子进程
    if(id == 0)
    {
        //子进程关闭读取管道
        close(fds[0]);

        char buffer[1024];//缓冲区
        int cnt = 0;//计数器
        const char * str = "hello linux";
        while(1)
        {
            char buffer[1024];//缓冲区
            sprintf(buffer,"我是父进程,我在往管道写入数据:%s:%d",str,cnt++);//将内容写入缓冲区中
            write(fds[1],buffer,strlen(buffer));//将缓冲区中的数据写入到管道中
            sleep(1);
        }

        close(fds[1]);
        exit(0);
    }
    //父进程

    //父进程关闭写入管道
    close(fds[1]);

    char buffer[1024];//缓冲区
    int cnt = 5;
    while(cnt--)
    {
        int sz = read(fds[0],buffer,sizeof(buffer)-1);//sz为返回读到几个字节
        buffer[sz] = '\0';//按C语言的标准给末尾添加\0
        cout<<buffer<<endl;
        sleep(1);
    }
    close(fds[0]);
    int status = 0;//存储子进程的退出信息
    waitpid(id,&status,0);//阻塞等待子进程
    cout<<(status&0x7F)<<endl;  

    return 0;
}

管道的读写特征

1.读慢,写快(当父进程读取的很慢,而子进程写入的很快,管道会充斥着大量的数据)

2.读快,写慢(当子进程写入很慢,而父进程读取很快,则父进程会进行阻塞等待)

3.写关闭,读到0(当写入端关闭的时候,读取端会读到末尾)

4.读关闭,操作系统会终止终端,并且给写入的进程发送信号,终止终端。

管道的特征

1. 管道是生命周期进程。(当该进程结束后,管道也会相应的删除)

2.管道可以用来进行具有血缘关系的进程之间进行通信,常用于父子进程通信。

3.管道是面向字节流的。

4.管道是半双工通信。(即单向通信)

5.管道是互斥与同步机制的。(对共享资源进行保护的方案)

匿名管道一般都是使用与有血缘关系进程之间的通信,例如上面的代码中,是父子进程之间的通信,那么如果想在没有血缘关系的进程之间通信,又应该怎么做呢?

我们可以使用命名管道。

命名管道

看完了匿名管道,我们再来看一下命名管道。

命名管道匿名管道最大的区别就是有名和没名,在匿名管道中,我们创建了一个没有名字的管道,也无法在通过路径来找到,而命名管道是有名的,我们可以通过路径来找到它,所有它就可以在没有血缘关系的进程之间进行通信。


那么命名管道又是如何做到在不同的进程间进行通信的呢?

命名管道的操作,会在磁盘中,创建一个真实存在的文件,然后需要通信的两个进程,都要打开这个文件,其中一个进程往这个管道文件里写入另一个进程往这个管道文件里读取,这样就可以进行进程间通信了。

但是当有一个进程往管道文件里面进行写入的时候,并不会真真正正的往文件里写入数据而是往这个文件的struct file中写入,而struct file中的数据是不会写入到文件中的,因为这个过程涉及到IO,会非常浪费性能。

如下图所示。

mkfifo函数

知道了命名管道的原理后,我们就可以来进行操作了。

首先,使用命名管道的前提是,在磁盘中,有这个管道文件,这个管道文件的创建,操作系统为我们提供了一个系统接口来完成,mkfifo系统调用

下面是这个系统调用的手册说明

mkfifo这个系统调用有两个参数:pathname,mode

pathname:你创建这个管道文件的路径

mode:你创建这个管道文件的权限,例如0666,就是这个管道文件对所有人都可读可写可执行。

这个系统调用的功能就是在磁盘中创建一个管道文件。

使用效果如下。

会创建一个管道文件。

返回值:

mkfifo如果成功创建了管道文件,会返回0,如果出现错误,则会返回-1,并且设置errno来表示错误。

unlink函数

能创建管道文件,那么也要能删除管道文件,unlink就是用来删除管道文件的。

unlink的参数只有一个:管道文件的文件名

使用后会删除该管道文件。 

返回值:

如果删除成功,则返回0,删除失败,返回-1,并且适当的设置errno来表示错误 

使用例子 

对于命名管道的操作的前置条件已经说完了,接下来就可以来尝试使用一下了。

在下面我会写一个模拟单向聊天的小程序,就是一个进程给另一个进程发送消息。 


在这个程序中,我会进行模块化编程,整个程序有三个文件server.cc、client.cc、common.hpp

common.hpp

在这个hpp文件中,我定义了两个函数

CreatPipe() :创建管道文件

RemotePipe():删除管道文件

同时定义了个宏,就是这个管道文件的名字

#pragma once

#include <cerrno>
#include <cstring>
#include <cassert>

#include <iostream>

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

#define NAMED_PIPE "named_pipe"    //管道文件名

//创建命名管道
bool CreatPipe()
{
    umask(0);
    int ret = mkfifo(NAMED_PIPE,0666);//创建命名管道
    if(ret == 0)
        return true;
    else if(ret == -1)
    {
        std::cout<<"errno: "<<errno<<" "<<strerror(errno)<<std::endl;//如果创建失败,输出错误信息
        return false;
    }
}

//删除命名管道
bool RemotePipe()
{
    int ret = unlink(NAMED_PIPE);//删除管道文件
    if(ret == 0)
        return true;
    else if(ret == -1)
    {
        std::cout<<"errno: "<<errno<<" "<<strerror(errno)<<std::endl;//如果删除失败,输出错误信息
        return false;
    }
}
client.cc 

在这个client.cc文件中,就是一个发送消息的进程。 

将我们需要发送的信息写入到管道文件中。

#include"common.hpp"

int main()
{
    int fd = open(NAMED_PIPE,O_WRONLY);//以写的形式打开管道文件
    assert(fd != -1);

    char buffer[1024];//缓冲区
    while(true)
    {
        std::cout<<"# please input:";//输入提示符
        fgets(buffer,sizeof(buffer)-1,stdin);//将你要发送的消息读取到buffer缓冲区中
        buffer[strlen(buffer)-1] = '\0';//去除末尾的 \n
        ssize_t ret = write(fd,buffer,sizeof(buffer)-1);//将buffer中的内容写入到管道文件中
        if(ret == -1)
        {
            std::cout<<"errno: "<<errno<<" "<<strerror(errno)<<std::endl;
        }
    }

    close(fd);//关闭管道文件
    return 0;
}
server.cc 

在这个sercer.cc中,也是一个进程,在这个进程中,我创建了管道文件,并且从管道文件中读取内容,再将其输出。

#include"common.hpp"

int main()
{
    CreatPipe();//创建命名管道

    int fd = open(NAMED_PIPE,O_RDONLY);//以读的形式打开管道文件
    assert(fd != -1);

    char buffer[1024];
    while(true)
    {
        ssize_t ret = read(fd,buffer,sizeof(buffer)-1);//从管道文件中读取内容
        buffer[ret] = '\0';
        if(ret > 0)//说明读取成功
        {
            std::cout<<"client -> server #"<<buffer<<std::endl;
        }
        else if(ret == 0)//说明管道文件已关闭,退出循环
        {
            std::cout<<"client quit, me too"<<std::endl;
            break;
        }
    }

    close(fd);

    RemotePipe();//删除命名管道
    return 0;
}

 写到这里,这个小程序就完成了,接下来将两个.cc文件进行编译,再一起运行起来。

如下图所示。 

三.system V进程间通信 

讲完了管道类型的进程间通信,接下来到system V进程间通信了。

system V进程间通信是聚焦在本地的进程间通信,也可以说是和管道差不多,在同一台机器上进行通信。

system V进程间通信有三种:共享内存消息队列信号量

但在这里,只讲解共享内存方法的通信。 

共享内存 

什么是共享内存?

不同的进程看到同一块内存空间,就叫做共享内存。 

在C/C++的内存分布中,大家都知道有堆区和栈区,同时在堆区和栈区之间还有一块区域叫做共享内存。这块共享内存就是给我们的进程是实现进程间通信的,如下图所示。

通过共享内存的方式,可以让不同的进程看到同一份资源而实现进程间通信。

那么如何通过共享内存来实现进程间通信呢?我们接着往下讲解。

要想实现system V的进程间通信,我们要先来学习一些函数。 

shmget函数

shmget函数是用来创建一块共享内存的。我们来看看手册。

通过手册,我们可以看到,shmget有三个参数:key、size、shmflg

这个函数的参数稍微有点复杂,我们一个一个来说。 

key:

key值是指能唯一标识一块共享内存的标志,key值的内容是什么不重要,重要的是它能唯一标识一块共享内存,至于key值如何获取,我在下面会讲到。

size:

size是指我们创建的共享内存的大小,一般这个大小我们通常设置为4kb的整数倍

shmflg:

这个参数是一组标志位,通常这个标志位的参数只有两个用的最多:IPC_CREATIPC_EXCL

其中,IPC_EXCL不能单独使用,一般都是结合IPC_CREAT来一起使用的。

标志位的用法两种

①IPC_CREAT:如果共享内存不存在,则创建它,如果存在则获取它。

什么意思呢?

进程间通信通常涉及到两个进程,这两个进程需要有一个进程创建这块共享内存,而另一个进程则需要获取这块共享内存

②IPC_CREAT | IPC_EXCL:如果共享内存不存在,则创建它,如果存在,则出错返回,这样传参的意义是确保我们创建的共享内存是全新的

返回值:

如果创建共享内存成功,则返回一个非负整数,即共享内存的标识符,否则返回-1并设置errno来表示错误。 

ftok函数 

ftok函数的作用就是来生成我们的key值。我们来看看手册。 

ftok有两个参数,其中返回值就是我们所需要的key值。

pathname:我们在Linux中一个所存在的路径名

proj_jd     :  一个我们想任意给的数字id

ftok这个函数,会通过我们所给的参数,自动生成出一个key值出来,注意相同的pathname和proj_id会生成出相同的key值。

返回值:

如果成功,则返回key值,否则返回-1,并设置errno来表示错误。 

创建共享内存 

当把上面的两个函数学会后,就可以创建我们的共享内存了,我们来操作一下。 

在实现这里的功能的时候,我们可以适当的对shmget函数和ftok函数进行一个封装,以便我们使用。如下面的代码所示。 

#include <cstring>

#include <iostream>

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

#define PATHNAME "."  //当前路径
#define PROJ_ID  1    //proj_id随便给,在这里给1
#define SIZE     4096 //共享内存大小

//获取key值
key_t GetKey()
{
    key_t key = ftok(PATHNAME,PROJ_ID);//获取key值
    if(key == -1)
    {
        std::cerr<<"获取key值失败: "<<strerror(errno)<<std::endl;
        exit(1);
    }
    return key;
}

//创建共享内存
int CreatShm(key_t key)
{
    int id = shmget(key,SIZE,IPC_CREAT | IPC_EXCL | 0666);//创建共享内存,其中0666是指这块共享内存的权限
    if(id == -1)
    {
        std::cerr<<"创建共享内存失败: "<<strerror(errno)<<std::endl;
        exit(2);
    }
    return id;
}

int main()
{
    key_t key = GetKey();
    printf("0x%x\n",key);//将key值输出,进行观察
    CreatShm(key);
    return 0;
}

在上面的代码中,我写了两个函数,一个是获取key值,一个是创建共享内存。

当这段代码运行起来后,可以成功的创建共享内存了。 


那么我们怎么确定这块共享内存存在呢?

通过ipcs -m命令,可以查看我们在系统中的共享内存。如下图所示。 


通过实践,我们发现,当test程序第一次运行的时候是能成功的,但是第二次及以上的时候再运行时,会失败,如下图所示。

同时,我们可以观察到,输出的key值是唯一的,因为我们给的参数没有变。

但是为什么后面再运行程序时,会创建共享内存失败呢?

那是因为我们第一次运行的时候,共享内存就已经被创建出来了,当这个程序结束后,共享内存是不会被主动删除的,需要我们通过命令或者代码来删除。 


首先我们来介绍一下通过命令的方式来删除。

我们可以通过使用        ipcrm -m [id]        的方式来删除,如下图所示。

获取共享内存 

当一个进程可以创建共享内存后,那么另一个进程就要获取共享内存,同样的,获取共享内存也是用到shmget函数,我们来实现一下。

//获取共享内存
int GetShm(key_t key)
{
    int id = shmget(key,SIZE,IPC_CREAT);
    if(id == -1)
    {
        std::cerr<<"获取共享内存失败: "<<strerror(errno)<<std::endl;
        exit(6);
    }
    return id;
}

 shmctl函数

学习完了命令行方式的删除共享内存,接下来再来看一下通过函数的方式来删除共享内存

先来看一下函数的手册。 

这个函数有三个参数:

shmid :共享内存的id。

cmd    :这个cmd也是一个标志位,通过标志位可以实现不同的功能,但是在这先讲删除共享内存的功能,当需要删除共享内存的时候,传参传IPC_RMID即可。

buf      :在删除操作中,这个参数一般为nullptr即可。

返回值:

如果删除成功,则返回0,否则返回-1,并设置errno来表示错误。 

删除共享内存 

知道了shmctl函数后,接下来就可以来实现删除共享内存的操作了。代码如下。 

//删除共享内存
void DeleteShm(int shmid)
{
    int ret = shmctl(shmid,IPC_RMID,nullptr);
    if(ret == -1)
    {
        std::cerr<<"删除共享内存失败: "<<std::endl;
        exit(3);
    }
}

shmat函数 

当创建共享内存和删除共享内存都搞完后,接下来还需要做的工作是连接共享内存

创建共享内存是不够的,还需要将进程和共享内存连接起来,连接共享内存,需要使用shmat函数,我们先来看一下手册。 

shmat一共有三个参数,其中我们一般只关心第一个。

shmid:我们需要连接共享内存的id。

shmaddr:我们需要连接到那个地址空间中,这个参数绝大多数情况下是不需要传参的,给一个nullptr即可。

shmflg:这个参数与我们的读写权限有关,一般也不需要管,设置成0即可。

返回值:

如果连接成功,则返回这段共享内存的起始地址,否则返回-1,并设置errno来表示错误。

连接共享内存 

知道了shmat函数后,接下来就可以实现出连接共享内存的函数了,如下面代码所示。 

//连接共享内存
void* AttachShm(int shmid)
{
    void* ret = shmat(shmid,nullptr,0);
    if((long long)ret == -1)
    {
        std::cerr<<"连接共享内存失败: "<<strerror(errno)<<std::endl;
        exit(4);
    }
    return ret;
}

 shmdt函数

共享内存连接成功后就可以开始使用了,使用完成后,进程还要与共享内存断开连接,这里就要用到shmdt函数。我们先来看一下这个函数的手册。 

这个函数的参数只有一个,就是共享内存的起始地址。 

返回值:

如果断开连接成功,则返回0,否则返回-1,并设置errno来表示错误。 

 断开共享内存

知道了shmdt函数后,接下就可以完成断开共享内存的函数了。如下面代码所示。  

//断开连接共享内存
int DisattchShm(void* start)
{
    int ret = shmdt(start);
    if(ret == -1)
    {
        std::cerr<<"断开共享内存失败: "<<strerror(errno)<<std::endl;
        exit(5);
    }
    return ret;
}

使用例子 

现在,创建共享内存,获取共享内存,连接共享内存,断开共享内存,删除共享内存的函数我们都已经实现了,接下来就可以来真真正正的实现进程间通信了,在这里例子中,我会模拟客户端给服务器端发送消息的案例。


同时在这里我会使用模块化编程,其中有三个文件:client.cc、server.cc、common.hpp

client.cc用来模拟客户端,server.cc模拟服务器端,common.hpp来实现函数定义。

 common.hpp

在这个文件中,我实现了获取key值,创建共享内存,获取共享内存,连接共享内存,断开共享内存,删除共享内存的功能函数。 

#include <cstring>
#include <cstdlib>
#include <cstdio>

#include <iostream>

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>

#define PATHNAME "."
#define PROJ_ID  1
#define SIZE     4096

//获取key值
key_t GetKey()
{
    key_t id = ftok(PATHNAME,PROJ_ID);
    if(id == -1)
    {
        std::cerr<<"生成key值失败: "<<strerror(errno)<<std::endl;
        exit(1);
    }
    return id;
}

//创建共享内存
int CreatShm(key_t key)
{
    int ret = shmget(key,SIZE,IPC_CREAT | IPC_EXCL | 0666);
    if(ret == -1)
    {
        std::cerr<<"创建共享内存失败: "<<strerror(errno)<<std::endl;
        exit(2);
    }
    return ret;
}

//获取共享内存
int GetShm(key_t key)
{
    int ret = shmget(key,SIZE,IPC_CREAT);
    if(ret == -1)
    {
        std::cerr<<"获取共享内存失败: "<<strerror(errno)<<std::endl;
        exit(3);
    }
    return ret;
}

//连接共享内存
void* AttachShm(int shmid)
{
    void* ret = shmat(shmid,nullptr,0);
    if((long long)ret == -1)
    {
        std::cerr<<"连接共享内存失败: "<<strerror(errno)<<std::endl;
        exit(4);
    }
    return ret;
}

//断开连接共享内存
void DisattchShm(void* start)
{
    int ret = shmdt(start);
    if(ret == -1)
    {
        std::cerr<<"断开共享内存失败: "<<strerror(errno)<<std::endl;
        exit(5);
    }
}

//删除共享内存
void DeleteShm(int shmid)
{
    int ret = shmctl(shmid,IPC_RMID,nullptr);
    if(ret == -1)
    {
        std::cerr<<"删除共享内存失败: "<<strerror(errno)<<std::endl;
        exit(6);
    }
}
 server.cc

在这个源文件中,我通过服务器端来创建共享内存和删除共享内存,同时,每隔1秒中就将共享内存中的内容进行输出。 

#include "common.hpp"

int main()
{
    //1.创建共享内存
    key_t key = GetKey();
    int id = CreatShm(key);

    //2.连接共享内存
    void* start = AttachShm(id);

    //3.使用
    int count = 10;
    while(count--)
    {
        printf("client say: ");
        printf("%s\n",start);
        sleep(1);
    }

    //4.断开连接共享内存
    DisattchShm(start);

    //5.删除共享内存
    DeleteShm(id);

    return 0;
}
client.cc 

在这个源文件中,我不断的往共享内存中写入数据。 

#include "common.hpp"

int main()
{
    //1.获取共享内存
    key_t key = GetKey();
    int id = GetShm(key);

    //1.连接共享内存
    void* start = AttachShm(id);

    //2.使用
    char message[1024] = "我正在给你发消息";
    int cnt = 0;
    int count = 10;
    while(count--)
    {
        snprintf((char*)start,SIZE,"%s:%d",message,cnt++);
        sleep(1);
    }

    //3.断开连接共享内存
    DisattchShm(start);

    return 0;
}

至此,我们的程序就已经写完了,运行效果如下。

共享内存属性的获取

如果我们想要查找我们的共享内存的具体属性时,又该如何操作呢?

通过查看shmctl手册,我们可以看到手册中提到了struct shmid_ds和struct ipc_perm两个结构体中的信息,这些信息是操作系统暴露给我们以便于去查看的信息,那么这些信息怎么获取呢,使用shmctl函数即可。

获取共享内存信息代码如下:

        shmctl(id,IPC_STAT,&ds);
        printf("共享内存的大小:%d,最后一次连接时间:%d,key值:%d",ds.shm_segsz,ds.shm_atime,ds.shm_pe

四.管道和共享内存的区别 

管道实现的进程间通信和共享内存实现的进程间通信又有什么区别呢? 

共享内存的优点

共享内存是所有进程间通信方法中,速度最快的一个,它能大大的减少数据的拷贝次数。

共享内存的缺点

不能进行同步和互斥的操作,对数据没有任何保护。

对于共享内存的优点,这很好理解,但是缺点呢,该怎么理解?

这个缺点,我们通过现象来理解,将上面使用例子的代码中的client.cc源文件中,

将sleep(1),改为sleep(5),就能看到现象。

当server.cc对共享内存的数据进行输出的时候,它不像管道一样,会将共享内存中的内存清空,而是一直输出。 

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

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

相关文章

如何制作自己的python .whl包(支持entry_points)

目录 代码目录结构如下截图所示&#xff1a;dir_test.py 源码如下&#xff1a;list/dir_list.py 源码如下&#xff1a;setup.py 文件源码生成.whl文件指令&#xff1a; 代码目录结构如下截图所示&#xff1a; dir_test.py 源码如下&#xff1a; import os import sys from pat…

RunAsDate(时间限制工具)

参考链接1 参考链接2 参考链接3 下载地址 ps&#xff1a;64位系统需要下载64的RunAsDate

LAMP架构详解

目录 一、Apache详解 1.1 简介 1.2 Apache功能 1.3 apache特点 1.4 三种工作模式 二、LAMP简介 2.1 LAMP平台概述 2.2 构建LAMP平台顺序 2.3 编译安装的优点 2.4 各组件的主要作用 三、wget命令 四、curl命令 五、压力测试工具 一、Apache详解 1.1 简介 Apache …

vue2,v-for中动态渲染本地的图片

一、描述 如果是正常在img标签的src上使用本地的url地址&#xff0c;是可以正常被渲染的&#xff0c;但是我们通过for的形式&#xff0c;动态渲染的话&#xff0c;就会通过网络请求的方式进行渲染&#xff0c;这个形式反而渲染不出来。 二、效果 这个效果&#xff0c;毋庸置…

LVS负载均衡集群部署之—NAT模式的介绍及搭建步骤

一、环境准备 1.准备三台rhel9服务器 服务器名称 主机名 ip地址备注LVS调度服务器lvs.timinglee.org eth0:172.25.254.100&#xff08;外网&#xff09; eth1:192.168.0.100(内网) 关闭selinux和防火墙webserver2网站服务器webserver1.timinglee.orgeth0&#xff1a;192.168.…

为什么高校开设微专业,建议搭建动作捕捉与数字人开发实训室?

随着近年来虚拟现实技术产业与元宇宙产业不断发展&#xff0c;动作捕捉技术成为元宇宙、VR/AR、影视动画、游戏、艺术创作、虚拟偶像等行业相关不可或缺的技术之一。各大院校为了探索新的教学模式&#xff0c;纷纷积极开设“微专业”&#xff0c;相比传统的虚拟仿真实训室来说&…

Linux进程调度与切换

目录 前言 Linux 2.6内核O(1)调度器 调度过程 调度算法 Linux 进程切换 前言 在Linux 2.6版本的内核中&#xff0c;进程调度器引入了O(1)调度器&#xff0c;这个调度器通过优先级队列、活跃队列和过期队列的机制来管理进程调度,虽然在现在已被更好的CFS调度器取代,但对于我…

中仕公考:2024年空军专业技能类文职人员公开招考公告

2024年空军专业技能类文职人员公开招考公告&#xff0c;有关事项公告如下&#xff1a; 一、招考岗位 主要有保管员、司机、炊事员、文印员、汽车修理工兼司机等专业技能三级以下岗位。 二、招考对象 符合岗位资格条件的社会人员(含高校应届毕业生、退役军人)。 根据军队有…

理解栈(Stack)及其在 C++ 中的应用【栈、数据结构】

在这篇博客中&#xff0c;我们将详细介绍栈&#xff08;Stack&#xff09;这一重要的数据结构&#xff0c;包括其基本概念、常用操作、C 中的实现&#xff0c;以及一些实际应用。 什么是栈&#xff1f; 栈是一种数据结构&#xff0c;它遵循“后进先出”&#xff08;LIFO - La…

上海AI Lab 搭台,36个大模型一起角逐长上下文建模能力

现在的大模型论文简直像是在比长度&#xff0c;动不动就上百页&#xff01;记得前阵子小编瞅见那份90页的Gemini技术报告&#xff0c;顿时脑袋嗡嗡作响。那会儿就幻想着&#xff1a;要是有个AI大脑来啃下这些"学术巨无霸"&#xff0c;那岂不是爽歪歪&#xff1f; 没…

SpringDoc:一个用于自动生成API文档的工具

SpringDoc的使用 概述SpringDoc添加依赖配置 Springdoc创建 REST 控制器访问 API 文档添加注释和描述自定义配置常用注解 详细示例创建模型类创建REST控制器查看Swagger UI与OpenAPI 安全策略类型概述HTTPAPIKEYOAUTH2OPENIDCONNECTMUTUALTLS 请求头配置认证token代码实现验证 …

C++:C++11介绍

✨✨✨学习的道路很枯燥&#xff0c;希望我们能并肩走下来! 文章目录 目录 文章目录 前言 一、C11简介 二 统一的列表初始化 2.1 {} 初始化 2.2 std::initializer_list 三 声明 3.1 auto 3.2 decltype 3.3 nullptr 四 范围for循环 五 智能指针 六 STL中一些变化…

瑞_RabbitMQ_初识MQ

文章目录 1 初识MQ1.1 同步调用1.1.1 同步调用的优势1.1.2 同步调用的缺点 1.2 异步调用1.2.1 异步调用的角色1.2.2 异步调用的优势1.2.3 异步调用的缺点1.2.4 异步调用的场景 1.3 MQ技术选型 2 RabbitMQ2.1 安装2.1.1 资源准备2.1.2 安装步骤 2.2 RabbitMQ架构2.3 RabbitMQ管理…

Starrocks解析json数组

json数据 [{"spec": "70g/支","unit": "支","skuId": "1707823848651276346","amount": 6,"weight": 70,"spuName": "伊利 甄稀 苦咖啡味雪糕 流心冰淇淋 70g/支",&quo…

你不知道,PMP证书的含金量究竟有多高

PMP考试的含金量体现在其全球认可度、对职业发展的助力、薪资潜力的提升、系统的知识体系、持续学习的要求以及专业网络的建设等方面。 尽管存在一定的维护成本和市场饱和度问题&#xff0c;PMP认证仍然是项目管理领域内备受推崇的资格证书。 对于追求专业发展和提升项目管理…

【安卓】发送自定义广播

文章目录 发送标准广播发送有序广播 发送标准广播 先定义一个BroadcastReceiver来准备接收广播&#xff0c;新建一个MyBroadcastReceiver。 public class MyBroadcastReceiver extends BroadcastReceiver {Overridepublic void onReceive(Context context, Intent intent) {To…

惊爆!OpenAI 高层发生剧烈动荡!竟然有 3 名元老同时离职,同时,马斯克竟起诉奥特曼“敲诈勒索”, GPT-5 要推迟了?

当今之 OpenAI 可谓是事端频出&#xff0c;波澜迭起。 开端之际&#xff0c;马斯克竟对 OpenAI提起诉讼&#xff0c;随后&#xff0c;重要的元老级高管公开宣告离职&#xff0c;而且还被披露高管领导层内部动荡不安&#xff0c;最终&#xff0c;就连原本定于年底发布的 GPT-5 …

【JVM基础15】——实践-JVM参数可以在哪里设置

目录 1- 引言&#xff1a;2- ⭐核心&#xff1a;2-1 Tomcat 设置 JVM 参数2-2 Jar包设置 JVM 参数 3- 小结&#xff1a;3-1 JVM参数可以在哪里设置 1- 引言&#xff1a; 一般这种问题是涉及到项目部署时候的参数设置&#xff0c;一般的部署方式 war 包部署在 tomcat 中设置jar…

MySQL第5讲--DML(数据操作语言)的基本操作

文章目录 前言DML(数据操作语言)基本操作增加数据案例展示修改数据案例展示删除数据 DML总结 前言 在第四讲MySQL第4讲–图像化界面工具DataGrip介绍中我们讲述了数据库的可视化操作界面&#xff1b;今天我们讲一下数据库中DML(数据操作语言)的基本操作&#xff1b; DML(数据操…