进程间的通信方式

news2024/12/23 22:36:45

文章目录

      • 1.简单介绍
      • 2.管道
          • 2.1管道的基础概念
            • **管道读写规则**:
            • **管道特点**
          • 2.2匿名管道
            • 匿名管道父子进程间通信的经典案例:
          • 2.3命名管道
            • 基本概念:
            • 命名管道的创建:
            • 命名管道的打开规则:
            • 匿名管道与普通管道的区别
            • **例子:用命名管道实现server&client通信**
      • 3.system v ipc
          • 3.1system v 共享内存 基础介绍
          • 3.2system v 共享内存常用接口介绍
          • 3.3system v 信息队列 以及信号量
      • 4.一些基础概念的讲解

1.简单介绍

在计算机中,进程间通信(Inter-Process Communication,IPC)是指两个或多个进程之间交换数据或信息的机制。

首先我们通过之前的学习知道进程是具有独立性的,其交互数据成本非常高,所以通信这一机制就诞生了。

通信的本质其实就是由os参与,提供一份所有通信进程能看到的公共资源。

进程间通信的方式非常多以下是对这些方式的梳理以及分类:

进程间通信分类

管道

  • 匿名管道pipe

  • 命名管道

    System V IPC

  • System V 消息队列

  • System V 共享内存

  • System V 信号量

    POSIX IPC

  • 消息队列

  • 共享内存

  • 信号量

  • 互斥量

  • 条件变量

  • 读写锁

2.管道

2.1管道的基础概念

管道是Unix中最古老的进程间通信的形式。

我们把从一个进程连接到另一个进程的一个数据流称为一个管道

在这里插入图片描述

管道读写规则:

当没有数据可读时

  • O_NONBLOCK disable:read调用阻塞,即进程暂停执行,一直等到有数据来到为止。
  • O_NONBLOCK enable:read调用返回-1,errno值为EAGAIN。

当管道满的时候

  • O_NONBLOCK disable: write调用阻塞,直到有进程读走数据
  • O_NONBLOCK enable:调用返回-1,errno值为EAGAIN

如果所有管道写端对应的文件描述符被关闭,则read返回0

如果所有管道读端对应的文件描述符被关闭,则write操作会产生信号SIGPIPE,进而可能导致write进程

退出

当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。

当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。

管道特点
  • 管道提供流式服务

  • 一般而言,进程退出,匿名管道释放,所以匿名管道的生命周期随进程

  • 一般而言,内核会对管道操作进行同步与互斥

  • 管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道

在这里插入图片描述

2.2匿名管道
#include <unistd.h>
功能:创建一无名管道,管道是一种特殊的文件,它具有两个端点,一个用于写入数据,另一个用于读取数据。
原型
int pipe(int fd[2]);
参数
fd:文件描述符数组,其中fd[0]表示读端, fd[1]表示写端
    是一个包含两个整数的数组,用于接收管道的两个文件描述符
返回值:成功返回0,失败返回-1
匿名管道父子进程间通信的经典案例:
使用例子:从键盘读取数据,写入管道,读取管道,写到屏幕
    
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>

int main()
{
    int pipe_fd[2] = {0};

    if(pipe(pipe_fd) < 0){
        perror("pipe");
        return 1;
    }
    printf("%d, %d\n", pipe_fd[0], pipe_fd[1]);

    pid_t id = fork();
    if(id < 0){
        perror("fork");
        return 2;
    }
    else if(id == 0) { //write
        //child
        close(pipe_fd[0]);

        //const char *msg = "hello parent, I am child";

        char c = 'x';
        int count = 0;
        //while(count){
        while(1){
            write(pipe_fd[1], &c, 1); //strlen(msg) + 1??
         //   sleep(1);
            count++;
            printf("write: %d\n", count);
        }

        close(pipe_fd[1]);
        exit(0);
    }
    else{              //read
        //parent
        close(pipe_fd[1]);

        char buffer[64];
        while(1){
            sleep(100);
            buffer[0] = 0;
            ssize_t size = read(pipe_fd[0], buffer, sizeof(buffer)-1);
            if(size > 0){
                buffer[size] = 0;
                printf("parent get messge from child# %s\n", buffer);
            }
            else if(size == 0){
                printf("pipe file close, child quit!\n");
                break;
            }
            else{
                //TODO
                break;
            }
        }

        int status = 0;
        if(waitpid(id, &status,0) > 0){
            printf("child quit, wait success!, sig: %d\n", status&0x7F);
        }
        close(pipe_fd[0]);
    }
    return 0;
}

对上端代码用fork来共享管道的原理

在这里插入图片描述

2.3命名管道
基本概念:

匿名管道应用的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。

如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道。

命名管道是一种特殊类型的文件

命名管道的创建:
//命名管道可以从命令行上创建,命令行方法是使用下面这个命令:
$ mkfifo filename
//命名管道也可以从程序里创建,相关函数有:
    int mkfifo(const char *filename,mode_t mode);
//filename:一个指向要创建的命名管道的路径和名称的字符串。它是一个以 null 结尾的字符数组。

//mode:用于指定创建的命名管道的权限(访问权限和文件类型)。mode 是一个 mode_t 类型的参数,通常使用八进制表示的权限值。可以使用一些预定义的常量(如 S_IRUSR、S_IWUSR、S_IRGRP 等)来设置权限。

//创建命名管道
int main(int argc, char *argv[])
{
 mkfifo("p2", 0644);
 return 0;
}
命名管道的打开规则:

如果当前打开操作是为读而打开FIFO时

O_NONBLOCK disable:阻塞直到有相应进程为写而打开该FIFO

O_NONBLOCK enable:立刻返回成功

如果当前打开操作是为写而打开FIFO时

O_NONBLOCK disable:阻塞直到有相应进程为读而打开该FIFO

O_NONBLOCK enable:立刻返回失败,错误码为ENXIO

匿名管道与普通管道的区别

匿名管道由pipe函数创建并打开(使用了pipe函数默认该描述符对应的文件打开了)。

命名管道由mkfififo函数创建,打开用open

FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在它们创建与打开的方式不同,一但这些工作完

成之后,它们具有相同的语义。

tips:

通过 mkfifo 函数创建的命名管道在文件系统中持久存在,与创建它的进程无关。

使用 pipe 函数创建的管道是进程特有的,与创建它的进程相关联。当创建管道的进程结束时,管道会被自动关闭和销毁。而mkfifo函数所创建的管道是不会随着进程结束而销毁的,我们需要手动销毁。

//销毁命名管道所使用的函数

int unlink(const char *pathname);
pathname 是一个指向要删除的文件的路径和名称的字符串。它是一个以 null 结尾的字符数组。
    
//unlink 函数执行以下操作:

1.首先,它会检查指定路径的文件是否存在。如果文件不存在,unlink 函数会返回一个错误码,并不执行任何其他操作。

2.如果文件存在,并且调用进程对该文件具有足够的权限,unlink 函数将从文件系统中删除该文件。删除文件会将文件从目录中删除,并释放文件所占用的磁盘空间。

3.注意,删除文件并不会关闭已打开的文件描述符。如果有进程仍然持有对该文件的打开描述符,文件将继续存在于文件系统中,直到所有打开的描述符关闭。

4.当调用进程成功删除文件时,unlink 函数返回 0。如果出现错误,比如权限不足或指定的文件路径无效,unlink 函数返回 -1,并设置相应的错误码,可以通过 errno 全局变量获取错误信息。

    //删除命名管道文件并不会自动关闭已打开的管道文件描述符。在删除命名管道文件之前,需要确保所有打开的文件描述符都已关闭,以免导致资源泄漏或其他问题。
例子:用命名管道实现server&client通信

例子中使用的一些函数的解析:

1.
    mode_t umask(mode_t mask);
umask 函数接受一个参数 mask,它是一个无符号整数类型 mode_t 的值,表示要设置的权限掩码。mode_t 是一个用于表示文件权限和文件类型的数据类型。

2.
    key_t ftok(const char *pathname, int proj_id);
    用于生成一个唯一的键值(key)用于标识一个共享资源,例如共享内存、信号量或消息队列。
    pathname:一个存在的文件路径名,可以是任意合法的文件路径。
    proj_id:一个用户定义的整数,用于区分不同的共享资源。通常取值为一个非负整数。  
    生成的键值可以用于创建或访问共享资源,例如在调用 shmget 函数创建共享内存时使用。
 3.
    int shmdt(const void *shmaddr);      
    shmdt 函数用于将共享内存从当前进程的地址空间中分离,即解除共享内存的挂载。
    shmdt 函数并不会删除共享内存段,只是将其与当前进程分离。
    在调用 shmdt 函数之后,当前进程将无法再直接访问共享内存中的数据。如果需要重新访问共享内存,必须使用 shmat 函数重新将共享内存段挂载到当前进程的地址空间。
Log.hpp(一个简单的日志记录功能的实现)
    
#ifndef _LOG_H_
#define _LOG_H_

#include <iostream>
#include <ctime>

#define Debug   0
#define Notice  1
#define Warning 2
#define Error   3


const std::string msg[] = {
    "Debug",
    "Notice",
    "Warning",
    "Error"
};

std::ostream &Log(std::string message, int level)
{
    std::cout << " | " << (unsigned)time(nullptr) << " | " << msg[level] << " | " << message;
    return std::cout;
}


#endif   
Makefile
    
.PHONY:all
all:shmClient shmServer

shmClient:shmClient.cc
	g++ -o $@ $^ -std=c++11
shmServer:shmServer.cc
	g++ -o $@ $^ -std=c++11

.PHONY:clean
clean:
	rm -f shmClient shmServer
comm.hpp

#pragma once

#include <iostream>
#include <cstdio>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <cassert>
#include <cstring>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include "Log.hpp"

using namespace std; //不推荐

#define PATH_NAME "/home/whb"
#define PROJ_ID 0x66
#define SHM_SIZE 4096 //共享内存的大小,最好是页(PAGE: 4096)的整数倍

#define FIFO_NAME "./fifo"

class Init
{
public:
    Init()
    {
        umask(0);//表示没有遮掩任何权限
        int n = mkfifo(FIFO_NAME, 0666);
        assert(n == 0);
        (void)n;
        Log("create fifo success",Notice) << "\n";
    }
    ~Init()
    {
        unlink(FIFO_NAME);
        Log("remove fifo success",Notice) << "\n";
    }
};

#define READ O_RDONLY
#define WRITE O_WRONLY

int OpenFIFO(std::string pathname, int flags)
{
    int fd = open(pathname.c_str(), flags);
    assert(fd >= 0);
    return fd;
}

void Wait(int fd)
{
    Log("等待中....", Notice) << "\n";
    uint32_t temp = 0;//为了确保读取的值可以正确地存储和比较
    ssize_t s = read(fd, &temp, sizeof(uint32_t));
    assert(s == sizeof(uint32_t));
    (void)s;
}

void Signal(int fd)
{
    uint32_t temp = 1;   //为了确保读取的值可以正确地存储和比较
    ssize_t s = write(fd, &temp, sizeof(uint32_t));
    assert(s == sizeof(uint32_t));
    (void)s;
    Log("唤醒中....", Notice) << "\n";
}

void CloseFifo(int fd)
{
    close(fd);
}
shmClient.cc

#include "comm.hpp"

int main()
{
    Log("child pid is : ", Debug) << getpid() << endl;
    key_t k = ftok(PATH_NAME, PROJ_ID);
    if (k < 0)
    {
        Log("create key failed", Error) << " client key : " << k << endl;
        exit(1);
    }
    Log("create key done", Debug) << " client key : " << k << endl;

    // 获取共享内存
    int shmid = shmget(k, SHM_SIZE, 0);
    if(shmid < 0)
    {
        Log("create shm failed", Error) << " client key : " << k << endl;
        exit(2);
    }
    Log("create shm success", Error) << " client key : " << k << endl;

    // sleep(10);

    char *shmaddr = (char *)shmat(shmid, nullptr, 0);
    if(shmaddr == nullptr)
    {
        Log("attach shm failed", Error) << " client key : " << k << endl;
        exit(3);
    }
    Log("attach shm success", Error) << " client key : " << k << endl;
    // sleep(10);

    int fd = OpenFIFO(FIFO_NAME, WRITE);
    // 使用
    // client将共享内存看做一个char 类型的buffer
    while(true)
    {
        ssize_t s = read(0, shmaddr, SHM_SIZE-1);
        if(s > 0)
        {
            shmaddr[s-1] = 0;
            Signal(fd);
            if(strcmp(shmaddr,"quit") == 0) break;
        }
    }

    CloseFifo(fd);
    // char a = 'a';
    // for(; a <= 'z'; a++)
    // {
    //     shmaddr[a-'a'] = a;
    //     // 我们是每一次都向shmaddr[共享内存的起始地址]写入
    //     // snprintf(shmaddr, SHM_SIZE - 1,\
    //     //     "hello server, 我是其他进程,我的pid: %d, inc: %c\n",\
    //     //     getpid(), a);
    //     sleep(5);
    // }

    // strcpy(shmaddr, "quit");

    // 去关联
    int n = shmdt(shmaddr);
    assert(n != -1);
    Log("detach shm success", Error) << " client key : " << k << endl;
    // sleep(10);

    // client 要不要chmctl删除呢?不需要!!

    return 0;
}
shmServer.cc
 
#include "comm.hpp"

// 是不是对应的程序,在加载的时候,会自动构建全局变量,就要调用该类的构造函数 -- 创建管道文件
// 程序退出的时候,全局变量会被析构,自动调用析构函数,会自动删除管道文件
Init init; 

string TransToHex(key_t k)
{
    char buffer[32];
    snprintf(buffer, sizeof buffer, "0x%x", k);
    return buffer;
}

int main()
{
    // 我们之前为了通信,所做的所有的工作,属于什么工作呢:让不同的进程看到了同一份资源(内存)
    // 1. 创建公共的Key值
    key_t k = ftok(PATH_NAME, PROJ_ID);
    assert(k != -1);

    Log("create key done", Debug) << " server key : " << TransToHex(k) << endl;

    // 2. 创建共享内存 -- 建议要创建一个全新的共享内存 -- 通信的发起者
    int shmid = shmget(k, SHM_SIZE, IPC_CREAT | IPC_EXCL | 0666); //
    if (shmid == -1)
    {
        perror("shmget");
        exit(1);
    }
    Log("create shm done", Debug) << " shmid : " << shmid << endl;

    // sleep(10);
    // 3. 将指定的共享内存,挂接到自己的地址空间
    char *shmaddr = (char *)shmat(shmid, nullptr, 0);
    Log("attach shm done", Debug) << " shmid : " << shmid << endl;

    // sleep(10);

    // 这里就是通信的逻辑了
    // 将共享内存当成一个大字符串
    // char buffer[SHM_SIZE];
    // 结论1: 只要是通信双方使用shm,一方直接向共享内存中写入数据,另一方,就可以立马看到对方写入的数据。
    //         共享内存是所有进程间通信(IPC),速度最快的!不需要过多的拷贝!!(不需要将数据给操作系统)
    // 结论2: 共享内存缺乏访问控制!会带来并发问题 【如果我想一定程度的访问控制呢? 能】
    
    int fd = OpenFIFO(FIFO_NAME, READ);
    for(;;)
    {
        Wait(fd);

        // 临界区
        printf("%s\n", shmaddr);
        if(strcmp(shmaddr, "quit") == 0) break;
        // sleep(1);
    }
    // 4. 将指定的共享内存,从自己的地址空间中去关联
    int n = shmdt(shmaddr);
    assert(n != -1);
    (void)n;
    Log("detach shm done", Debug) << " shmid : " << shmid << endl;
    // sleep(10);

    // 5. 删除共享内存,IPC_RMID即便是有进程和当下的shm挂接,依旧删除共享内存
    n = shmctl(shmid, IPC_RMID, nullptr);
    assert(n != -1);
    (void)n;
    Log("delete shm done", Debug) << " shmid : " << shmid << endl;

    CloseFifo(fd);
    return 0;
}    

3.system v ipc

3.1system v 共享内存 基础介绍

在这里插入图片描述

实际上,system v本质也是创建虚拟内存进而映射到实际内存上,不过跟上面管道不同的是,进程间信息交互的处理不在由内核系统那一套来处理,而是由操作系统直接处理,因此其是最快的ipc模式。

3.2system v 共享内存常用接口介绍
功能:用来创建共享内存
原型
    int shmget(key_t key, size_t size, int shmflg);
参数
    key:这个共享内存段名字
    size:共享内存大小
    shmflg:由九个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样的返回值:成功返回一个非负整数,即该共享内存段的标识码;失败返回-1
功能:将共享内存段连接到进程地址空间
原型
    void *shmat(int shmid, const void *shmaddr, int shmflg);参数
    shmid: 共享内存标识
    shmaddr:指定连接的地址
    shmflg:它的两个可能取值是SHM_RND和SHM_RDONLY
返回值:成功返回一个指针,指向共享内存第一个节;失败返回-1
shmaddr为NULL,核心自动选择一个地址
shmaddr不为NULL且shmflg无SHM_RND标记,则以shmaddr为连接地址。
shmaddr不为NULL且shmflg设置了SHM_RND标记,则连接的地址会自动向下调整为SHMLBA的整数倍。公式:shmaddr - (shmaddr % SHMLBA)
shmflg=SHM_RDONLY,表示连接操作用来只读共享内存
功能:将共享内存段与当前进程脱离
原型
    int shmdt(const void *shmaddr);
参数
    shmaddr: 由shmat所返回的指针
返回值:成功返回0;失败返回-1
注意:将共享内存段与当前进程脱离不等于删除共享内存段
功能:用于控制共享内存
原型
    int shmctl(int shmid, int cmd, struct shmid_ds *buf);参数
    shmid:由shmget返回的共享内存标识码
    cmd:将要采取的动作(有三个可取值)
    buf:指向一个保存着共享内存的模式状态和访问权限的数据结构
返回值:成功返回0;失败返回-1

在这里插入图片描述

3.3system v 信息队列 以及信号量

这一块学有余力的同学再去详细了解,这里便不在一一呈现了。

4.一些基础概念的讲解

为了之后获取更好的阅读文章体验,我将提前把一些经常出现的基础概念在此总结一下,强烈介意第一次打基础的同学阅读。

当多个进程对同一份资源进行来回操作时,因为时序问题,可能会造成数据不一致的问题,为了解决这一问题,我们将定制一套基础方案,下面先对其中涉及到的专有名词的含义进行讲解。

临界资源:多个进程执行流看到的公用的一份资源

临界区:进程访问临界资源的代码

互斥性:为了更好的临界区的保护,可以让各执行流在同一时刻只有一个进程进入临界区

原子性:一件事情要么不做,要么做完,没有中间状态

信号量:信号量是一种用于实现进程间同步和互斥的机制。可以将信号量看作是一个计数器,它的值可以被多个进程或线程修改和读取。信号量的值表示可用的资源数量或某种条件的状态。

主要有两种类型的信号量:

  1. 二进制信号量(Binary Semaphore):也称为互斥信号量,它的值只能为0或1。用于实现互斥访问共享资源,只允许一个进程或线程访问资源。
  2. 计数信号量(Counting Semaphore):它的值可以是任意非负整数。用于控制一定数量的资源,多个进程或线程可以同时访问资源,但受限于信号量的计数值。

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

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

相关文章

基于SpringBoot+Vue的宠物领养饲养交流管理平台设计与实现

前言 &#x1f497;博主介绍&#xff1a;✌全网粉丝10W,CSDN特邀作者、博客专家、CSDN新星计划导师、全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战✌&#x1f497; &#x1f447;&#x1f3fb;…

HTML实现移动端布局与页面自适应

我们所说的布局方式&#xff0c;这里我们通常指的是width和height在不同页面情况下面的改变。 常见页面的布局方式有 静态布局 &#xff08;px布局&#xff0c;就是固定其高宽&#xff0c;不论页面怎样放大缩小&#xff0c;其占领的依旧是&#xff0c;使用px固定了的高宽&…

这种方法可以解决开发中的重复“造轮子”

一、前言 开发中&#xff0c;一直听到有人讨论是否需要重复造轮子&#xff0c;我觉得有能力的人&#xff0c;轮子得造。但是往往开发周期短&#xff0c;用轮子所节省的时间去更好的理解业务&#xff0c;应用到业务中&#xff0c;也能清晰发现轮子的利弊&#xff0c;一定意义上解…

PyTorch深度学习(三)【Logistic Regression、处理多维特征的输入】

Logistic Regression 这个名字叫做回归&#xff0c;做的是分类。 线性和logistic的模型&#xff1a; 使用的损失函数&#xff1a;二分类交叉熵 &#xff08;这个也叫做BCELoss&#xff09; logistic要做的事&#xff1a; 代码&#xff1a; import torch# import torch.nn.fun…

Java基于SpringBoot的校园疫情防控系统

文章目录 第一章2.主要技术第三章第四章 系统设计4.1功能结构4.2 数据库设计4.2.1 数据库E/R图4.2.2 数据库表 第五章 系统功能实现5.1系统功能模块5.2后台功能模块5.2.1管理员功能 源码咨询 第一章 springboot校园疫情防控系统演示录像2022 一个好的系统能将校园疫情防控的管理…

VB求平均值

VB求平均值 Private Function pj(x() As Integer) As SingleDim m%, n%, i%, s%m LBound(x): n UBound(x)For i m To ns s x(i)Next ipj s / (n - m 1) End Function Private Sub Command1_Click()Dim a%(1 To 10), i%, aver!For i 1 To 10a(i) Int(Rnd() * 10) 随机…

IMX6ULL移植篇-Linux内核编译

一. Linux内核 Linux 官网为 https://www.kernel.org &#xff0c;所以你想获取最新的 Linux 版本就可以在这个网站上下载。 Linux-4.x 版本 的 Linux 和 5.x 版本没有本质上的区别&#xff0c; 5.x 更多的是加入了一些新的平台、新的外设驱动而已。 NXP 会从网址 …

提升科研可复现性:和鲸聚焦 AI for Science 全生命周期管理

今年三月&#xff0c;国家科技部会同自然科学基金委正式启动“人工智能驱动的科学研究&#xff08;AI for Science&#xff09;”专项部署工作。数据驱动的科学研究长期以来面临诸多困境&#xff0c;针对传统科研工作流中过度依赖人类专家经验与体力的局限性&#xff0c;AI4S 旨…

优化软件系统,解决死锁问题,提升稳定性与性能 redis排队下单

项目背景&#xff1a; 随着用户数量的不断增加&#xff0c;我们的速卖通小管家软件系统面临了一个日益严重的问题&#xff1a;在从存储区提供程序的数据读取器中进行读取时&#xff0c;频繁出现错误。系统报告了一个内部异常: 异常信息如下&#xff1a; 从存储区提供程序的数…

nvme各模块间的关系总结

目录&#xff1a;driver/host/nvme/makefile # SPDX-License-Identifier: GPL-2.0 ccflags-y -I$(src)obj-$(CONFIG_NVME_CORE) nvme-core.o obj-$(CONFIG_BLK_DEV_NVME) nvme.o obj-$(CONFIG_NVME_FABRICS) nvme-fabrics.o obj-$(CONFIG_NVME_RDMA) nvme-rdma.…

02、Servlet核心技术(下)

目录 1 ServletJDBC应用&#xff08;重点&#xff09; 2 重定向和转发&#xff08;重点&#xff09; 2.1 重定向的概述 2.2 转发的概述 3 Servlet线程安全&#xff08;重点&#xff09; 4 状态管理&#xff08;重点 &#xff09; 5 Cookie技术&#xff08;重点&#xf…

26 环形链表II

环形链表 II 题解1 哈希表题解2 双指针 给定一个链表的头节点 head &#xff0c;返回链表开始入环的第一个节点。 如果链表无环&#xff0c;则返回 null。 如果链表中有某个节点&#xff0c;可以通过连续跟踪 next 指针再次到达&#xff0c;则链表中存在环。 为了表示给定链表…

pgzrun 拼图游戏制作过程详解(10)

10. 拼图游戏继续升级——多关卡拼图 初始化列表Photos用来储存拼图文件名&#xff0c;Photo_ID用来统计当下是第几张拼图&#xff0c;Squares储存当下拼图的24张小拼图的文件名&#xff0c;Gird储存当下窗口上显示的24个小拼图及坐标。 Photos["girl_","boy_…

“顽固”——C语言用栈实现队列

解题图解&#xff1a; 1、 先用stack1存储push来的数据 2、每当要pop数据时&#xff0c;从stack2中取&#xff0c;如果 stack2为空&#xff0c;就先从stack1中“倒”数据到stack2。 这就是用栈实现队列的基本操作 这道题看起来比较容易&#xff0c;但是&#xff01;如果你用C语…

jupyter notebook插件安装及插件推荐

安装插件 安装插件选择的工具栏 pip install jupyter_contrib_nbextensions将插件工具栏添加到jupyter notebook页面 jupyter contrib nbextension installdisable configuration for nbextensions without explicit compatibility (they may break your notebook environme…

《Kubernetes部署篇:Ubuntu20.04基于containerd部署kubernetes1.25.14集群(多主多从)》

一、架构图 如下图所示: 二、环境信息 1、资源下载基于containerd部署容器版kubernetes1.25.14集群资源合集 2、部署规划主机名K8S版本系统版本内核版本IP地址备注k8s-master-121.25.14Ubuntu 20.04.5 LTS5.15.0-69-generic192.168.1.12master节点 + etcd节点k8s-master-131.…

【超实用】2023年,学生上班族如何简单快速,低成本的搭建一个博客网站

文章目录 前言实操环节香港虚拟机购买博客搭建ssl证书配置备份设置 总结 前言 因为工作和生活的需要&#xff0c;我一直有博客的搭建需求。我将总结下来&#xff0c;为读者提供参考。  起初&#xff0c;我采用的是香港云虚拟主机&#xff0c;这种虚拟机极其便宜&#xff08;一…

java接入烽火科技拾音器详细步骤

1 背景 项目中需要拾音器去采集音频数据并保存成mp3这种音频文件&#xff0c;以便以后如果有纠纷后可以作为证据去减少纠纷&#xff0c;于是采购了一台烽火科技的拾音器设备&#xff0c;包括一个采音器及一个处理终端。 2 接线 设备拿过来第一件事是接线&#xff0c;通电&…

WampServer下载安装+cpolar内网穿透实现公网访问本地服务【内网穿透】

文章目录 前言1.WampServer下载安装2.WampServer启动3.安装cpolar内网穿透3.1 注册账号3.2 下载cpolar客户端3.3 登录cpolar web ui管理界面3.4 创建公网地址 4.固定公网地址访问 前言 Wamp 是一个 Windows系统下的 Apache PHP Mysql 集成安装环境&#xff0c;是一组常用来…

如何将视频进行分割?这几种分割方法了解一下

当我们将视频分成几段后&#xff0c;可以更好地组织和管理不同的片段&#xff0c;方便后续查找和使用。我们可以根据需要调整视频的长度和内容&#xff0c;满足不同的观看需求。此外&#xff0c;分段视频可以更好地适应不同的观看场景&#xff0c;可以更方便地分享和传播&#…