【Linux详解】——进程间通信

news2024/9/21 2:50:21

📖 前言:本期介绍进程间通信。


目录

  • 🕒 1. 理解进程间通信
    • 🕘 1.1 什么是通信
    • 🕘 1.2 为什么要有通信
    • 🕘 1.3 如何进行进程间通信
  • 🕒 2. 管道
    • 🕘 2.1 匿名管道
      • 🕤 2.1.1 回顾文件系统
      • 🕤 2.1.2 理解通信的本质问题
      • 🕤 2.1.3 管道文件的刷新
      • 🕤 2.1.4 匿名管道的概念
    • 🕘 2.2 匿名管道的编码部分
    • 🕘 2.3 管道的特点
    • 🕘 2.4 如何理解命令行中的管道
    • 🕘 2.5 进程控制多个子进程
  • 🕒 3. 命名管道
    • 🕘 3.1 预备工作
    • 🕘 3.2 命令行中的命名管道
    • 🕘 3.3 命名管道

🕒 1. 理解进程间通信

🕘 1.1 什么是通信

  • 数据传输: 一个进程需要将它的数据发送给另一个进程
  • 资源共享: 多个进程之间共享同样的资源。
  • 通知事件: 一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
  • 进程控制: 有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。

🕘 1.2 为什么要有通信

在之前所写的C/C++代码中,都是单进程的。但实际上,我们在完成某种业务内容时是需要多进程协同的。比如cat file | grep 'hello'就是将file中的内容打印在显示器之前通过grep进行指定内容的过滤,这就是多进程协同

🕘 1.3 如何进行进程间通信

经过发展,最终有这么两套方案:

  1. POSIX:让通信过程可以跨主机
  2. System V:聚焦在本地通信,即一台机器的两个进程进行通信。
    • 共享内存
    • 消息队列
    • 信号量

对于System V ,在这里只关注共享内存,除了上述两套标准,还有一种方法:管道也是通信的一种方式,管道依托于文件系统来完成进程间通信的方案。

🕒 2. 管道

管道是基于文件系统的进程通信的方式。

  • 管道是Unix中最古老的进程间通信的形式。
  • 我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”

在这里插入图片描述

🕘 2.1 匿名管道

🕤 2.1.1 回顾文件系统

我们之前所学习的文件系统中,有这样的结构:通过PCB—task_struct(进程控制块),每一个进程都有一个task_struct,同样知道struct files_struct其中包含一个进程描述符表的array数组,通过特定的文件描述符找到磁盘加载到内存中对应的文件。
在这里插入图片描述

当该PCB创建子进程时,不会拷贝磁盘中的文件,而是拷贝一份struct files_struct同样指向父进程对应的struct file:

在这里插入图片描述

🕤 2.1.2 理解通信的本质问题

  1. OS需要直接或间接给通信双方的进程提供“内存空间”;
  2. 要通信的进程,必须看到一份公共的资源。

通信的成本一定不低,这是因为不能直接考虑通信的问题,必须先让不同的进程看到同一份资源,然后才能利用这份资源进行通信。因此我们未来学习通信的接口,与其说是通信的接口,倒不如说是同一份资源的接口。而我们目前所学习的就是让不同进程如何能够看到同一份资源。

不同的通信种类,实际上就是OS系统的不同模块对应的功能,比如文件系统之间通信的模块就是管道,System V的模块就是System V通信……

而对于上面的struct file,实际上就是父进程与子进程的同一份资源,这份资源是由文件系统提供的,struct file包括file的操作方法和自己的内核缓冲区;父进程通过文件缓冲区将数据写入,子进程通过文件缓冲区将数据读取,这不就是一个进程写入,另一个进程读取,不就是进程间通信吗?

因此这个struct file文件就是管道文件。

🕤 2.1.3 管道文件的刷新

我们知道,struct file是从磁盘加载到内存的,而父子进程的每一次写入,struct file不会从内存中刷新到磁盘,虽然通过一定的操作是可行的,但进程与进程之间的通信是从内存到内存的,没有必要牵扯到磁盘。一旦刷新到磁盘,就会大大降低通信的速度。所以管道文件是一个内存级别的文件,不会进行磁盘刷新。

🕤 2.1.4 匿名管道的概念

经过上面的学习,那如何让两个进程看到同一个管道文件呢?——>通过fork创建子进程完成。但当前这个管道文件并没有名字,所以被称为匿名管道。

在这里插入图片描述

Q:为什么父进程分别以读和写的方式打开同一个文件?A:只有父进程打开读和写,产生的文件描述符才会被子进程继承,子进程才能有读和写的功能。

总结

我们对应的父进程通过调用管道特定的系统调用,以读和写的方式打开一个内存级的文件,并通过fork创建子进程的方式,被子进程继承下去之后,各自关闭对应的读写端,形成的一条通信信道,这条信道是基于文件的,因此称为管道。

匿名管道:目前能用来进行父子进程之间进行进程间通信!

上述所讲的都是如何建立公共的资源,并没有涉及到通信,通信需要在具体场景才能实现。

🕘 2.2 匿名管道的编码部分

int pipe(int pipefd[2]);//管道:输出型参数,成功则返回0,头文件为unistd.h

功能:获取读和写的文件描述符(0, 1)传到参数中。

创建管道文件,打开读写端

#include <iostream>
#include <unistd.h>
#include <cassert>

using namespace std;
int main()
{
    int fds[2];
    int n = pipe(fds);
    assert(n == 0);

    // 0,1,2->3,4
    // [0]:读取  [1]:写入
    cout<<"fds[0]:"<<fds[0]<<endl; // 3
    cout<<"fds[1]:"<<fds[1]<<endl; // 4
    return 0;
}
# Makefile
mypipe:mypipe.cc
	g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
	rm -rf mypipe
[hins@VM-12-13-centos pipe]$ ./mypipe
fds[0]:3		# 读取
fds[1]:4		# 写入

fork子进程

#include <iostream>
#include <unistd.h>
#include <cassert>
#include <sys/types.h>
#include <sys/wait.h>
using namespace std;
int main()
{
    int fds[2];
    int n = pipe(fds);
    assert(n == 0);

    // fork
    pid_t id = fork();
    assert(id>=0);
    if(id==0)
    {
        //子进程通信

        exit(0);
    }
    //父进程通信
    n = waitpid(id,nullptr,0);
    assert(n == id);
    return 0;
}

关闭父子进程不需要的文件描述符,完成通信

#include<iostream>
#include<cstdio>
#include<string>
#include<cstring>
#include<unistd.h>
#include<cassert> // C/C++混搭
#include<sys/types.h>
#include<sys/wait.h>
using namespace std;

// 让父进程读取,子进程写入
int main()
{
    // 第一步:创建管道文件,打开读写端
    int fds[2];
    int n = pipe(fds);
    assert(n == 0);
    
    // 第二步:fork子进程
    pid_t id = fork();
    assert(id >= 0);
    if(id == 0)
    {
        // 子进程进行写入,所以关掉读权限
        close(fds[0]);
        // 子进程的通信代码
        const char *s = "I'm a Child process. I'm sending you a message";
        int cnt  = 0;
        while(true)
        {
            cnt++;
            char buffer[1024];// 只有子进程能看到
            snprintf(buffer, sizeof buffer, "child->parent say: %s[%d][%d]", s, cnt, getpid());
            write(fds[1], buffer, strlen(buffer));	// 反斜杠0只有C语言认
            sleep(1);// 每隔一秒写一次
        }

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

    // 父进程进行读取
    close(fds[1]);
    
    // 父进程的通信代码
    while(true)
    {
        char buffer[1024];
        ssize_t s = read(fds[0], buffer, sizeof(buffer)-1);
        if(s > 0) buffer[s] = 0;// 去除反斜杠0
        cout << "Get Message#"  << buffer <<"| my pid: " << getpid() << endl;
        // 细节:父进程可没有进行sleep
    }

    n = waitpid(id, nullptr, 0);
    assert(n == id);


    // 0, 1, 2->……
    // 谁是读取,谁是写入
    // [0]:读取
    // [1]:写入
    cout << "fds[0]: " << fds[0] << endl;//3 读
    cout << "fds[1]: " << fds[1] << endl;//4 写
    return 0;
}
[hins@VM-12-13-centos pipe]$ ./mypipe
Get Message#child->parent say: I'm a Child process. I'm sending you a message[1][18505]| my pid: 18504
Get Message#child->parent say: I'm a Child process. I'm sending you a message[2][18505]| my pid: 18504
Get Message#child->parent say: I'm a Child process. I'm sending you a message[3][18505]| my pid: 18504
Get Message#child->parent say: I'm a Child process. I'm sending you a message[4][18505]| my pid: 18504
....

因此,上述代码的子进程没有打印任何的消息,而是我们的父进程获取读取消息并打印出来,这种通信就被成为管道通信。

在这里插入图片描述

🕘 2.3 管道的特点

读写特征

上述代码中我们在子进程中sleep(1),实际上这使得父进程在read时暂停1秒,即在read(读)时阻塞;那如果把子进程的sleep去掉,在父进程中sleep(n),那么子进程的缓冲区就会被写满(因为子进程没有延迟非常快),如果还在写,就会将原来的覆盖,导致写端被阻塞;如果将写端关闭,那么就会读到0;如果读关闭,依旧让他去写,实际上没有任何意义,浪费系统资源,OS会给写进程发送信号,终止写端。通过实现最后一组情况,结果发送的信号为13号信号:SIGPIPE。

管道的特征

  1. 管道的生命周期随进程一样。
  2. 匿名管道可以用来进行具有血缘关系的进程直接进行通信,常用于父子通信。
  3. 管道是面向字节流的(网络)。
  4. 半双工 – 单向通信(特殊概念)。
  5. 互斥与同步机制 – 对共享资源进行保护的方案。
    后三点慢慢接触。

🕘 2.4 如何理解命令行中的管道

对于cat file | grep 'hello在命令中实际上会作为字符串先被扫描一遍,将出现的 | 记录下来,并创建进程。其中产生的缓冲区会将管道左侧将要打印的数据加载到缓冲区,在通过右侧的进行筛选并打印到指定位置。

🕘 2.5 进程控制多个子进程

在这里插入图片描述
父进程可以实现向任意一个子进程中写入,我们可以让父进程向任何进程中写入一个四字节的命令操作码,称之为commandCode,即现在想让哪一个进程运行,就向哪一个进程发送数据,举个例子:如果发送是1,就让子进程下载,发送是2,就让子进程做特定的计算……;那为什么可以这样随意控制子进程是否运行呢?这是因为如果我们不将数据写入或者写的慢,那么子进程就需要等,产生阻塞,所以跟根据这样的思想设计如下代码:

#include <iostream>
#include <vector>
#include <string>
#include <unistd.h>
#include <cstdlib>
#include <cassert>
#include <ctime>
#include <sys/wait.h>
#include <sys/types.h>
using namespace std;
#define PROCESS_NUM 5
#define Make_Seed() srand((unsigned long)time(nullptr)^getpid()^0X55^rand()%1234)
typedef void(*func_t)();//函数指针类型

//模拟子进程需要完成的任务
void downLodeTask()
{
    cout<<getpid()<<"下载任务"<<endl;
    sleep(1);
}
void ioTask()
{
    cout<<getpid()<<"IO任务"<<endl;
    sleep(1);
}
void flushTask()
{
    cout<<getpid()<<"刷新任务"<<endl;
    sleep(1);
}
//多进程代码
class sunEndPoint
{
public:
    sunEndPoint(pid_t subId,int writeFd)
    :_subId(subId)
    ,_writeFd(writeFd)
    {
        char namebuffer[1000];
        snprintf(namebuffer,sizeof(namebuffer),"process-%d[pid(%d)-fd(%d)]",num++,_subId,_writeFd);
        _name=namebuffer;
    }
public:
    string _name;
    pid_t _subId;//pid
    int _writeFd;//写fd
    static int num;
};
int sunEndPoint::num=0;
void loadTaskFunc(vector<func_t>* out)
{
    assert(out);
    out->push_back(downLodeTask);
    out->push_back(ioTask);
    out->push_back(flushTask);
}
int recvTask(int readFd)
{
    int code=0;
    ssize_t s=read(readFd,&code,sizeof(code));
    if(s==sizeof(code))//合法信息
    {
        return code;
    }
    else if(s<=0)
    {
        return -1;
    }
    else 
        return 0;
}
void createSubProcess(vector<sunEndPoint>* subs,vector<func_t>& funcMap)
{
    vector<int> deleteFd;//解决下一个子进程拷贝父进程读端的问题
    for(int i=0;i<PROCESS_NUM;++i)
    {
        int fds[2];
        int n=pipe(fds);
        assert(n==0);
        (void)n;
        pid_t id=fork();
        if(id==0)//子进程
        {
            //关闭上一个文件的写端文件描述符
            for(int i=0;i<deleteFd.size();++i)
            {
                close(deleteFd[i]);
            }
            //子进程,处理任务
            close(fds[1]);
            while(true)
            {
                //1、获取命令码,如果父进程没有发送,子进程被阻塞
                int commandCode=recvTask(fds[0]);
                //2、完成任务
                if(commandCode>=0&&commandCode<funcMap.size())
                {
                    funcMap[commandCode]();
                }
                else if(commandCode==-1)
                {
                    break;
                }
            }
           exit(0); 
        }
        close(fds[0]);
        sunEndPoint sub(id,fds[1]);
        subs->push_back(sub);
        
        deleteFd.push_back(fds[1]);
    }
}
void sendTask(const sunEndPoint& process,int taskNum)
{
    cout<<"send task num"<<taskNum<<"send to->"<<process._name<<endl;
    int n=write(process._writeFd,&taskNum,sizeof(taskNum));
    assert(n==sizeof(int));//判断是否成功写入4个字节
    (void)n;
}
void loadBlanceContrl(const vector<sunEndPoint>& subs,const vector<func_t>& funcMap,int count)
{
    int processnum =subs.size();//子进程的个数
    int tasknum=funcMap.size();
    bool forever=(count==0?true:false);
    while(true)
    {
        //选择一个子进程,从vector<sunEndPoint>选择一个index
        int subIdx=rand()%processnum;
        //选择一个任务,从vector<func_t>选择一个index
        int taskIdx=rand()%tasknum;
        //将任务发送给指定的子进程,将一个任务的下标发送给子进程
        sendTask(subs[subIdx],taskIdx);//taskIdx作为管道的大小4个字节
        sleep(1);
        if(!forever)//forever不为0
        {
            --count;
            if(count==0)
                break;
        }
    }
    //写端退出,读端将管道内数据读完后read返回0
    for(int i=0;i<processnum;++i)
    {
        close(subs[i]._writeFd);//最晚被创建的子进程拥有早期创建的子进程的父进程的读端,所以这里其实是后创建的进程先关闭
    }
}
//回收子进程
void waitProcess(vector<sunEndPoint> processes)
{
    int processnum=processes.size();
    for(int i=0;i<processnum;++i)
    {
        waitpid(processes[i]._subId,nullptr,0);
        cout<<"wait sub process success"<<processes[i]._subId<<endl;
    }
}
//父进程给子进程发布命令,父进程写,子进程读
int main()
{
    Make_Seed();//创建随机数
    //父进程创建子进程及和子进程通信的管道
    vector<func_t> funcMap;//vector<函数指针> funcMap
    loadTaskFunc(&funcMap);//加载任务
    vector<sunEndPoint> subs;//子进程集合
    createSubProcess(&subs,funcMap);//维护父子通信信道
    //这里的程序是父进程,用于控制子进程
    int taskCnt=9;//让子进程做9个任务
    loadBlanceContrl(subs,funcMap,taskCnt);
    //回收子进程信息
    waitProcess(subs);
    return 0;
}
[hins@VM-12-13-centos procpool]$ ./processpool
send task num: 1 send to -> process-9[pid(1222)-fd(13)]
1222: IO任务

send task num: 1 send to -> process-8[pid(1221)-fd(12)]
1221: IO任务

send task num: 0 send to -> process-8[pid(1221)-fd(12)]
1221: 下载任务

wait sub process success ...: 1213
wait sub process success ...: 1214
wait sub process success ...: 1215
wait sub process success ...: 1216
wait sub process success ...: 1217
wait sub process success ...: 1218
wait sub process success ...: 1219
wait sub process success ...: 1220
wait sub process success ...: 1221
wait sub process success ...: 1222

🕒 3. 命名管道

🕘 3.1 预备工作

新建servers.cc与client.cc及makefile,让servers.cc负责整体工作。

// server.cc
#include<iostream>

int main()
{
    std::cout << "hello server" << std::endl;
    return 0;
}
// client.cc
#include<iostream>

int main()
{
    std::cout << "hello client" << std::endl;
    return 0;
}
# Makefile
.PHONY:all
all:server client

server:server.cc
	g++ -o $@ $^ -std=c++11 -g
client:client.cc
	g++ -o $@ $^ -std=c++11 -g

.PHONY:clean
clean:
	rm -f server client

🕘 3.2 命令行中的命名管道

通过指令:mkfifo 文件名 就可以创建一个管道文件。

在这里插入图片描述

左侧将打印的信息重定向到named_pipe管道文件中,右侧cat作为进程再把named_pipe管道数据读了进来,通过这种方式,就完成了命令行式的进程间通信。但发现管道文件的大小仍为0。

如果两个进程打开同一个文件,那么在系统角度,还用不用为第二个进程在打开文件的时候在内核当中再重新创建一个struct file呢?

答案是没有必要的。操作系统会自己识别文件已经被打开了,就不再需要这个操作了。实际上这也是操作系统为了减轻没必要的性能损失。

我们之前提到过,要想让两个进程之间进行通信,就需要有一份共享的资源,匿名管道以继承的方式拥有共同的文件(文件地址具有唯一性),那么命名管道是如何让不同的进程看到同一份资源的呢?

让不同的进程打开指定名称(文件路径+文件名)的同一个文件就可以了。

即我们之前演示的命令行中的文件路径默认是当前路径,因此能够进行进程间通信。

🕘 3.3 命名管道

为了能让client.cc和server.c看到同一份资源。因此再新建一个头文件:comm.hpp

对于mkfifo,不仅仅在指令中存在,在系统调用中也有此接口:

头文件:#include<sys/types.h> #include<sys/stat.h>

接口:int mkfifo(const char *pathname, mode_t mode);mode_t类型为权限,返回值为0是创建成功。

既然都要用,那就放在公共的comm.hpp中。

接下来,我们就需要将管道建立在指定路径下,既可以建立在当前路径下,也可以建立在系统的tmp路径下,此次就建立在tmp路径下:(tmp路径可以被任何人读、写、执行)

在这里插入图片描述

// server.cc
#include "comm.hpp"
int main()
{
    bool ret = createFifo(NAMED_PIPE);
    assert(ret);
    (void)ret;
    return 0;
}
// comm.hpp
#pragma once

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

#define NAMED_PIPE "/tmp/mypipe.2023"

bool createFifo(const std::string& path)
{
    umask(0);
    int n = mkfifo(path.c_str(), 0666);//读、写、执行
    if(n == 0) return true;
    else
    {
        std::cout << "errno: " << errno << "err string: " << strerror(errno) << std::endl;
        return false;
    }
}

但是如果想在代码中删除,如何做?因此接下来介绍删除文件的接口:

头文件:#include<unistd.h>

函数接口:int unlink(const char* path);

功能:删除文件path,删除成功则返回0。

在comm.hpp中封装好删除的函数:

void removeFifo(const std::string &path)
{
    int n = unlink(path.c_str());
    assert(n==0);
    (void)n;//防止n没使用而警告
}

在server.cc中进行调用:

#include "comm.hpp"

int main()
{
    bool ret = createFifo(NAMED_PIPE);
    assert(ret);
    (void)ret;

    removeFifo(NAMED_PIPE);
    return 0;
}

这样,就可以创建文件之后自动删除,如果想要观察,就需要在创建与删除之间加上个sleep,否则运行太快无法具体观察创建和删除的过程。

至此,我们就完成了通过server.cc对管道文件的创建和删除。然后呢?只要能创建和删除了,然后就是通信了,那server.cc和client直接如何通信呢?接下来的代码就没有新的东西了,即让server.cc和client.cc打开同一个文件,让server.cc读,让client.cc写,这样就可以了。代码:

// comm.hpp
#pragma once

#include<iostream>
#include<string>
#include<cerrno>
#include<cassert>
#include<cstring>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#define NAMED_PIPE "/tmp/mypipe.2023"
bool createFifo(const std::string& path)
{
    umask(0);
    int n = mkfifo(path.c_str(), 0666);//读、写、执行
    if(n == 0) return true;
    else
    {
        std::cout << "errno: " << errno << "err string: " << strerror(errno) << std::endl;
        return false;
    }
}
//去掉管道文件
void removeFifo(const std::string& path)
{
    int n = unlink(path.c_str());
    assert(n == 0);//debug有效,release里面就被去掉了
    (void)n;//n不使用就会出现warning,代码变成release之后没有assert,n就不会被使用,因此在这里使用一下。
}
// server.cc
#include"comm.hpp"
int main()
{
	bool r = createFifo(NAMED_PIPE);
    assert(r);
    (void)r;

    std::cout << "server begin: " << std::endl; 
    int rfd = open(NAMED_PIPE, O_RDONLY);
    std::cout << "server end: " << std::endl;
    if(rfd < 0) exit(1);
    
    //read
    char buffer[1024];
    while(true)
    {
        ssize_t s = read(rfd, buffer, sizeof(buffer)-1);
        if(s > 0)
        {
            buffer[s] = 0;
            std::cout << "client->server# "<< buffer << std::endl;
        }
        else if(s == 0)
        {
            std::cout << "client quit, me too!" <<std::endl;
            break;
        }
        else
        {
            std::cout << "err string: " << strerror(errno) << std::endl;
            break;
        }
    }
    close(rfd);
    removeFifo(NAMED_PIPE);//删除
    return 0;
}
// client.cc
#include"comm.hpp"

int main()
{
    std::cout << "client begin: " << std::endl; 
    int wfd = open(NAMED_PIPE, O_WRONLY);
    std::cout << "client end: " << std::endl; 
    if(wfd < 0) exit(1);

    //write
    char buffer[1024];
    while(true)
    {
        std::cout << "Please Say# ";
        fgets(buffer, sizeof(buffer), stdin);
        ssize_t n = write(wfd, buffer, strlen(buffer));
        assert(n == strlen(buffer));
        (void)n;
    }
    close(wfd);
    return 0;
}

执行观察:先运行server,再运行client,观察server端的变化:
在这里插入图片描述

通过这个现象就可以看出,我们将读的一段打开了,他不会直接运行,而是阻塞到读端,当把写端打开了,他才会继续向下运行。也就是说,读端和写端都打开,才会继续向后运行。其次我们发现:左侧的写端没有空行,但是右端的有空行,这是因为左侧的回车同样被存到/tmp/mypipe.2023中,因此在读端读时就会将其看成换行并打印在屏幕上,因此下面这样就可以解决:

// client.cc
#include"comm.hpp"

int main()
{
    std::cout << "client begin: " << std::endl; 
    int wfd = open(NAMED_PIPE, O_WRONLY);
    std::cout << "client end: " << std::endl; 
    if(wfd < 0) exit(1);

    //write
    char buffer[1024];
    while(true)
    {
        std::cout << "Please Say# ";
        if(strlen(buffer) > 0) buffer[strlen(buffer)-1] = 0;  // 去掉换行
        fgets(buffer, sizeof(buffer), stdin);
        ssize_t n = write(wfd, buffer, strlen(buffer));
        assert(n == strlen(buffer));
        (void)n;
    }
    close(wfd);
    return 0;
}

最后在client里进行ctrl c结束。至此,我们就完成了通信。


OK,以上就是本期知识点“进程间通信”的知识啦~~ ,感谢友友们的阅读。后续还会继续更新,欢迎持续关注哟📌~
💫如果有错误❌,欢迎批评指正呀👀~让我们一起相互进步🚀
🎉如果觉得收获满满,可以点点赞👍支持一下哟~

❗ 转载请注明出处
作者:HinsCoder
博客链接:🔎 作者博客主页

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

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

相关文章

SpringBoot2学习笔记

信息来源&#xff1a;https://www.bilibili.com/video/BV19K4y1L7MT?p5&vd_source3969f30b089463e19db0cc5e8fe4583a 作者提供的文档&#xff1a;https://www.yuque.com/atguigu/springboot 作者提供的代码&#xff1a;https://gitee.com/leifengyang/springboot2 ----…

自动化测试指南:什么该自动化什么不该自动化

除了测试&#xff0c;测试数据创建、需求跟踪和测试报告等任务也可自动化。 什么应该被自动化&#xff1f; 与人工测试相比&#xff0c;具备明显收益如果失败会对业务造成相当大的损失的业务功能或用户流&#xff1b;自动化测试有助于经常验证这些功能。 需要针对应用程序的每个…

myeclipse的Debug模式

1.表示当前实现继续运行直到下一个断点&#xff0c;快捷键为F8。 2.表示打断整个进程 3.表示进入当前方法&#xff0c;快捷键为F5。 4.表示运行下一行代码&#xff0c;快捷键为F6。 5.表示退出当前方法&#xff0c;返回到调用层&#xff0c;快捷键为F7。 6.表示当前线程的…

硬核来袭!中国AI大模型峰会“封神之作”,开发者们不容错过!

2023年全球AI浪潮迭起&#xff0c;大语言模型热度空前&#xff0c;生成式人工智能为千行百业高质量发展带来更多想象空间。作为前沿科技风向标、汇聚全球开发者的顶级盛会&#xff0c;WAVE SUMMIT 2023深度学习开发者峰会正式定档8月16日&#xff0c;在北京望京凯悦酒店召开。本…

PCB制版技术03

5.21 P(W)改变折线 5.22 点击右键可以拐弯&#xff0c;点击左键可以取消 5.23 这三个点&#xff0c;代表着连接在一起的意思 5.24 把这个电阻和线一连 5.25 点一下&#xff0c;然后把点给拉回来 5.26 连接三极管&#xff0c;十电阻 5.27 对齐和对称就能够感觉很舒服&#xff0c…

Google OAuth 2 authorization - Error: redirect_uri_mismatch 400

出现这个问题&#xff0c;一般是因为google授权origin地址和重定向redirect_uri地址没有匹配上。 请仔细检查重定向地址的url中origin部分和授权origin部分是否能够匹配&#xff1a;

#P0995. [NOIP2005普及组] 循环

题目描述 乐乐是一个聪明而又勤奋好学的孩子。他总喜欢探求事物的规律。一天&#xff0c;他突然对数的正整数次幂产生了兴趣。 众所周知&#xff0c;22 的正整数次幂最后一位数总是不断的在重复 2,4,8,6,2,4,8,6…2,4,8,6,2,4,8,6… 我们说 22 的正整数次幂最后一位的循环长度…

新《生产建设项目水土保持方案审查要点》要求下全流程水土保持应用

目录 专题一 水土保持常用法律法规、规范及文件解读 专题二 水土保持方案及监测、验收开展的流程 专题三 水土保持需要收集的资料 专题四 水土保持现场踏勘需要注意的事项 专题五 常见水土保持工程施工工艺流程 专题六 《生产建设项目水土保持方案审查要点》&#xff08;…

【Vue3基础】组件保持存活、异步加载组件

一、组件保持存活 1、需求描述 点击按钮跳转到其他组件后&#xff0c;原组件不会被销毁 2、知识整理 1&#xff09;组件生命周期 创建期&#xff1a;beforeCreate、created 挂载期&#xff1a;beforeMount、mounted 更新期&#xff1a;beforeUpdate、updated 销毁期&am…

【SCSS】网格布局中的动画

效果 index.html <!DOCTYPE html> <html><head><title> Document </title><link type"text/css" rel"styleSheet" href"index.css" /></head><body><div class"container">&l…

在排序数组中查找元素的第一个和最后一个位置——力扣34

文章目录 题目描述法一 二分查找 题目描述 法一 二分查找 int bsearch_1(int l, int r) {while (l < r){int mid (l r)/2;if (check(mid)) r mid;else l mid 1;}return l; }int bsearch_2(int l, int r) {while (l < r){int mid ( l r 1 ) /2;if (check(mid)) l …

第一个maven项目(IDEA生成)

第一个maven项目&#xff08;IDEA生成&#xff09; 步骤1 配置Project SDK 步骤2 配置maven File->Settings搜索maven

【Docker】Docker比虚拟机快的原因、ubuntu容器、镜像的分层概念和私有库的详细讲解

&#x1f680;欢迎来到本文&#x1f680; &#x1f349;个人简介&#xff1a;陈童学哦&#xff0c;目前学习C/C、算法、Python、Java等方向&#xff0c;一个正在慢慢前行的普通人。 &#x1f3c0;系列专栏&#xff1a;陈童学的日记 &#x1f4a1;其他专栏&#xff1a;CSTL&…

proteus常用元件图示和名称(持续更新...)

初学单片机,记录一下proteus常用的元件目录 proteus常用元件图示和名称1 SWITCH(一位开关)2 CAP(无极性电容)3 CAP-ELEC(极性电容)4 CRYSTAL(晶振)5 LED-BIBY(发光二极管)6 RES(电阻)7 BUTTON(按钮)8 AT89C51(经典单片机)9 BUS(总线)10 VCC(电源)11 GROUND(接地)12 BUZZER(蜂鸣…

C++实现矩阵乘法

本贴分享用C实现矩阵乘法计算的功能&#xff0c;具体内容请看代码和注释&#xff0c;这里单独说一明一部分代码块。 1.采用vector< vector<int>>的方式&#xff0c;可以实现无限度的二维动态数组&#xff0c;需要注意的是&#xff0c;对于C来说a[m][n]的写法是合法…

备战秋招 | 笔试强训20

目录 一、选择题 二、编程题 三、选择题题解 四、编程题题解 一、选择题 1、对于顺序存储的线性表&#xff0c;访问结点和增加结点的时间复杂度为&#xff08;&#xff09;。 A. O(n) O(n) B. O(n) O(1) C. O(1) O(n) D. O(1) O(1) 2、在下列链表中不能从当前结点出发访问…

NineData支持全版本的企业级Oracle客户端

Oracle 数据库是一款全球领先的关系型数据库管理系统&#xff0c;它为企业提供了高性能、高可用性和安全性的数据处理解决方案&#xff0c;被广泛应用于各个行业。对于 Oracle 数据库&#xff0c;大家都很熟悉&#xff0c;本文不再赘述。 近期&#xff0c;NineData 发布对 Ora…

云时代的运维正是不折不扣的架构师

1、引言 上学那会&#xff0c;每当作文中引用到张良这个典故&#xff0c;总喜欢用 “运筹帷幄之中&#xff0c;决胜千里之外” 来赞美张良雄才大略&#xff0c;指挥若定&#xff0c;现在还让我用的话&#xff0c;我会把这句话送给运维同学。 2013年左右&#xff0c;一朋友在某…

SOP/详解*和**/python数据结构(iter,list,tuple,dict)/ 解包

一、错误解决合集 1. > combined_seq.named_children() 2. isinstance 2th parameter : must be a type or tuple of types > 改为tuple&#xff0c;不要用列表。改为 LLLayer (nn.Conv2d,nn.Linear) 3. File “test.py”, line 90, in calculate_fin_fout print(“hi”…

Python生成自定义URL二维码并保存为图片文件

脚本简介描述&#xff1a; 我们的应用场景是网站提供了Android客户端的二维码&#xff0c;可以进行扫码直接下载。所以使用下方的脚本可以自动生成URL路径二维码&#xff0c;并保存到指定路径下展示在网站上。 代码展示 PS&#xff1a;主要用到了 qrcode第三方模块 [rootnod…