(超超详!!)Linux进程间通信-----管道 + 共享内存详解

news2024/12/26 4:37:28

索引

  • 通信背景
  • 管道
    • 匿名管道
    • 命名管道
  • 共享内存
    • 基本概念
    • 共享内存如何管理
    • 共享内存的相关函数
    • 共享内存的删除
    • 共享内存的使用

通信背景

进程是具有独立性的,每个进程都有独立的PCB,独立的数据和数据结构,因此进程间想要交互数据,成本会非常高,但有时候需要多进程协同处理同一件事情,这个时候就要进程间通信了,
进程通信的目的
数据传输:一个进程需要将它的数据发给另一个进程;
通知事件:一个进程需要向另一个或一组进程发送消息,通知它们发生了某种事情
进程控制:有些进程希望完全控制另一个进程的执行。

管道

由于进程间具有独立性,在通信之前必须让不同的进程看到同一份资源,而资源的不同也就决定了通信方式的不同---------管道就是通信方式的一种.
把一个进程连接到另一个进程的一个数据流称为一个管道

匿名管道

管道是一个内存级的文件
在这里插入图片描述
在内核中文件结构体有对应的缓冲区和该文件是何种文件。
在这里插入图片描述匿名管道的实现

在这里插入图片描述

#include <time.h>
#include <iostream>
#include <cstdio>
#include <unistd.h>
#include <cstring>
#include <sys/types.h>
#include <sys/wait.h>
using namespace std;
int main()
{
    int pipefd[2] = {0};
    if (pipe(pipefd) != 0)
    {
        cerr << "pipe error" << endl;
        return 1;
    }
    // 创建子进程
    pid_t id = fork();
    if (id < 0)
    {
        cerr << "fork error" << endl;
        return 2;
    }
    else if (id == 0)
    {
        // child
        // 子进程读取
        close(pipefd[1]);
#define NUM 1024
        char buffer[NUM];
        while (true)
        {
            cout << "时间戳" << (uint64_t)time(nullptr) << endl;
            // 子进程没有带sleep为什么也会休眠?
            memset(buffer, 0, sizeof(buffer));
            size_t s = read(pipefd[0], buffer, sizeof(buffer) - 1);
            if (s > 0)
            {
                // 读取成功
                buffer[s] = '\0';
                cout << "子进程收到消息,消息的内容是" << buffer << endl;
            }
            else if (s == 0)
            {
                cout << "父进程写完了,我也读完了" << endl;
                break;
            }
            else
            {
                // DO nothing
                cerr << "读取错误" << endl;
            }
        }
        close(pipefd[9]);
        exit(0);
    }
    else
    {
        // parent
        //  父进程写入
        close(pipefd[0]);
        string msg = "你好,儿子,我是你爹";
        int cnt = 5;
        while (cnt--)
        {
            write(pipefd[1], msg.c_str(), msg.size());
            sleep(1); // 这里是为了一会看现象明显
        }
        close(pipefd[1]);
        cout << "父进程写完了" << endl;
    }
    // 0 -> 嘴巴 ->读
    // 1 -> 写
    pid_t res = waitpid(id, nullptr, 0);
    if (res > 0)
    {
        cout << "等待子进程成功" << endl;
    }
    cout << "fd[0]:->" << pipefd[0] << endl;
    cout << "fd[1]:->" << pipefd[1] << endl;
    return 0;
}

在这里插入图片描述
为什么父进程要分别打开读和写?
因为子进程是继承自父进程的,进程通信的时候我们不知道是父进程读还是子进程读,因此需要打开两文件描述符。
父子进程为什么要分别要关闭读写?
因为管道的特点是单向的,所以管道传输数据应该也是单向的,所以当父进程写的时候,其要关闭读端,子进程关闭写端,这样就可以构成一个单向的数据传输了。至于谁来写谁来读,完全是由需求决定的。

由运行结果可以得出:
当父进程没有写入数据的时候,子进程在等,父进程写入数据之后,子进程才能读(read)到数据,子进程打印读取数据要以父进程的节奏为主!
管道内部:如果没有数据的话,reader就必须阻塞式等待数据
管道内部:如果数据被写满,writer就必须阻塞式等待数据被读后才能写
阻塞就是将该进程的task_struct放入等待队列中。
pipe是自带访问控制的,同步和互斥机制,即避免两个进程同时访问共享资源的一种机制
子进程读,父进程写,如何保证读完了呢?
在父进程完成写后关闭了文件描述符,子进程是能感受到的,此时文件描述符file结构体中管道的引用计数就变成了1,表示此时只有子进程一个进程指向他,子进程就知道现在文件只有他一个人读了,此时读完,就表示数据读完了。

给其中一个进程指派任务


#include <time.h>
#include <cstdlib>
#include <sys/types.h>
#include <sys/wait.h>
#include <cassert>
#include <vector>
#include <iostream>
#include <unistd.h>
using namespace std;
using functor = void (*)();
vector<functor> functors; // 方法集合

void f1()
{
    cout << "这是第一个任务,执行的进程ID是"
         << getpid() << "执行的时间是:" << time(nullptr) << endl;
}
void f2()
{
    cout << "这是第二个任务,执行的进程ID是"
         << getpid() << "执行的时间是:" << time(nullptr) << endl;
}
void f3()
{
    cout << "这是第三个任务,执行的进程ID是"
         << getpid() << "执行的时间是:" << time(nullptr) << endl;
}
void loadfunctor()
{
    functors.push_back(f1);
    functors.push_back(f2);
    functors.push_back(f3);
}
int main()
{
    loadfunctor();
    // 创建管道
    int pipefd[2] = {0};
    if (pipe(pipefd) != 0)
    {
        cerr << "pipe error" << endl;
        return 1;
    }
    // 创建子进程
    pid_t fd = fork();
    if (fd < 0)
    {
        cerr << "fork error" << endl;
        return 2;
    }
    else if (fd == 0)
    {
        // 关闭不需要的文件fd
        //  child read
        close(pipefd[1]);
        while (1)
        {
            uint32_t operatorType = 0;
            // 如果有数据就读取,如果没有数据就阻塞等待
            // 等待任务的到来
            ssize_t s = read(pipefd[0], &operatorType, sizeof(uint32_t));
            if (s == 0)
            {
                cout << "已全部读取完毕" << endl;
                break;
            }
            assert(s == sizeof(uint32_t));
            // assert断言,是编译有效,debug模式有效
            // release模式,断言就没有了
            // 一旦断言没有了,s变脸就是只被定义了,没有被使用
            // release模式中可能会有warning,所以加一个(void)s
            (void)s; // 防止警告
            if (operatorType < functors.size())
            {
                functors[operatorType]();
            }
            else
            {
                cerr << "bug? operatorType = " << operatorType << endl;
            }
        }
        close(pipefd[0]);

        exit(1);
    }
    else
    {
        srand((long long)time(nullptr)); // 种一颗随机数种子
        // parent write
        close(pipefd[0]);
        // 指派任务
        int num = functors.size();
        int cnt = 10;
        while (cnt--)
        {
            uint32_t commandCode = rand() % num;
            write(pipefd[1], &commandCode, sizeof(uint32_t));

            sleep(1);
        }
        close(pipefd[1]);

        pid_t res = waitpid(fd, nullptr, 0);
        if (res)
            cout << "wait success" << endl;
    }
    return 0;
}

如何控制一批进程呢?
在这里插入图片描述
首先加载业务方法
循环创建三个子进程,创建子进程的时候也创建三个管道,每个子进程分别对应一个管道,再创建pair<int, int>分别表示子进程的pid,和该子进程对应管道的写端。

#include <time.h>
#include <cstdlib>
#include <sys/types.h>
#include <sys/wait.h>
#include <cassert>
#include <vector>
#include <iostream>
#include <unistd.h>
using namespace std;
using functor = void (*)();
vector<functor> functors; // 方法集合

void f1()
{
    cout << "这是第一个任务,执行的进程ID是"
         << getpid() << "执行的时间是:" << time(nullptr) << endl;
}
void f2()
{
    cout << "这是第二个任务,执行的进程ID是"
         << getpid() << "执行的时间是:" << time(nullptr) << endl;
}
void f3()
{
    cout << "这是第三个任务,执行的进程ID是"
         << getpid() << "执行的时间是:" << time(nullptr) << endl;
}
void loadfunctor()
{
    functors.push_back(f1);
    functors.push_back(f2);
    functors.push_back(f3);
}

using elem = pair<int32_t, int32_t>; // 进程pid,该进程对应管道写端fd
int processNum = 5;
vector<elem> assignMap;
void work(int blockFd)
{
    cout << "进程[ " << getpid() << "]"
         << "开始工作" << endl;
    // 子进程核心工作的代码
    while (true)
    {
        // 阻塞等待,获取任务信息
        uint32_t operatorCode = 0;
        ssize_t s = read(blockFd, &operatorCode, sizeof(uint32_t));
        if (s == 0)
            break;
        assert(s == sizeof(uint32_t));
        (void)s;

        // 处理任务
        if (operatorCode < functors.size())
            functors[operatorCode]();
        cout << "进程[ " << getpid() << "]"
             << "结束工作" << endl;
    }
}
// pair-->  子进程的pid,子进程管道fd
void sentTask(const vector<elem> &processFd)
{
    srand((long long)time(nullptr));
    int cnt = 5;
    while (cnt--)
    {
        sleep(1);
        // 选择一个进程,选择进程是随机的,没有压着一个进程给任务
        // 较为均匀的将任务给所有的子进程-- 可以说是某种负载均衡
        uint32_t pick = rand() % processFd.size();
        // 选择一个任务
        uint32_t task = rand() % functors.size();
        //

        // 把任务给一个指定的进程
        write(processFd[pick].second, &task, sizeof(uint32_t));

        // 打印对应的提示信息
        cout << "父进程指派任务-> " << endl;
        cout << "cnt" << cnt << endl;
    }
}

int main()
{

    loadfunctor();
    // 创建processNum个进0程
    for (int i = 0; i < processNum; i++)
    {
        // 定义保存管道fd的对象
        int pipefd[2] = {0};
        // 创建管道0
        pipe(pipefd);
        // 创建子进程
        pid_t id = fork();
        if (id == 0)
        {
            // 子进程读取
            close(pipefd[1]);
            // 子进程执行
            work(pipefd[0]);
            exit(0);
        }
        // 父进程做的事情
        close(pipefd[0]);
        elem e({id, pipefd[1]});
        assignMap.push_back(e);
    }
    cout << "创建所有子进程成功" << endl;
    // 父进程派发任务
    sentTask(assignMap);
    sleep(1);
    cout << "出来了?" << endl;
    for (int i = 0; i < processNum; i++)
    {
        //此时子进程只能用kill用命令行杀死,所以此时我们设置为非阻塞式等待,父进程
        //直接退出,此时父五个子进程就会变成孤儿进程被bash领养
        //bash会使他们也退出
        if (waitpid(assignMap[i].first, nullptr, WNOHANG) > 0)
            cout << "wait for :" << assignMap[i].first << "wait success!"
                 << "number: " << i << endl;
        close(assignMap[i].second);
    }
    return 0;
}

在这里插入图片描述

管道特征总结:

  1. 管道只能用来进行具有亲子关系的进程,进行进程间通信,常用于父子进程
  2. 管道只能单向通信,(不仅仅是因为现实生活中管道是单向的,Linux内核的实现也是单向),管道是半双工的。即每次只有一方能够发送消息,而另一方必须等待接受到消息后才能发送信息。
  3. 管代自带同步机制(pipe满,writer等,pipe空,reader等)–自带访问控制,eg如果创建一个子进程,让父子进程都打印,此时不能判断哪个进程先打印,这个就叫做没有访问控制
  4. 管道是面向字节流的—先写的字符一定先被读取,没有格式边界,需要用户自己定义却分内容的边界
  5. 管道也是文件,所以管道的生命周期随进程,进程退出了,管道也就退出了
    面向字节流的理解
            size_t s = read(pipefd[0], buffer, sizeof(char) * 3);

如果每次读取三个字节,此时输出的是
在这里插入图片描述
每次读取的是一个汉字
如果读取的是六个字节,则

            size_t s = read(pipefd[0], buffer, sizeof(char) * 3 * 2);


每次读取的都是两个汉字。
此时我们的Linux的编码方式是UTF-8,该编码方式下,大部分常用汉字都是占三个字节

命名管道

上述的命名管道主要用于有血缘关系的通信 ,毫不相干的两个进程之间的通信用命名管道。
进程通信的本质:不同的进程要看到同一份资源
匿名管道:子进程继承自父进程,直接就能看到了
命名管道:一个fifo文件具有唯一路径,通过路径,就能使得两个进程找到同一个资源。
注:命名管道虽然是通过文件路径找到资源的,其是一个文件毫无疑问,但是这个文件的TCB表示其是一个pipe文件,其只是在磁盘上有一个符号,磁盘上不会有数据,因此命名管道也是内存级别的,OS看过看到一个文件是管道文件,就不会使文件往磁盘定期刷新了。
在这里插入图片描述

comm.h
#pragma once
#include<iostream>
#include<cstdio> 
#include<cstring>
#include<cerrno>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include<string>

using namespace std;

#define IPC_PATH "./.fifo"

clientFifo.cc
#include "comm.h"
int main()
{
    int pipeFd = open(IPC_PATH, O_WRONLY);
    if (pipeFd < 0)
    {
        cerr << "open " << strerror(errno) << endl;
        return 1;
    }
#define NUM 1024
    char line[NUM] = "sasakjsiqskjqsbkqjs";
    int sum = 0;
    while (true)
    {
        // 测试管道的大小有多大
        //  if (write(pipeFd, line, strlen(line)))
        //  {
        //      sum += strlen(line);
        //      cout << "写入了多少个字节?" << sum << "字节" << endl;
        //  }
        //  else
        //  {
        //      break;
        //  }
        cout << "请输入你的消息#";
        fflush(stdout);
        memset(line, 0, sizeof(line));
        // fgets会在line结尾自动添加\0;
        if (fgets(line, sizeof(line), stdin) != nullptr)
        {
            line[strlen(line) - 1] = '\0';
            write(pipeFd, line, strlen(line));
        }
        else
        {
            break;
        }
    }
    cout << "管道的大小是: " << sum << endl;
    close(pipeFd);
    cout << "客户端推出啦" << endl;
    return 0;
}

serverFifo.cc
#include "comm.h"

int main()
{
    umask(0);

    if (mkfifo(IPC_PATH, 0666) != 0)
    {
        cerr << "mkfifo error " << endl;
        return 1;
    }
    int pipefd = open(IPC_PATH, O_RDONLY);
    if (pipefd < 0)
    {
        cerr << "open fifo error" << endl;
        return 2;
    }
#define NUM 1024
    char buffer[NUM];
    while (true)
    {
        ssize_t s = read(pipefd, buffer, sizeof(buffer) - 1);
        if (s > 0)
        {
            buffer[s] = '\0';
            cout << "客户端 -> 服务器#" << buffer << endl;
        }
        else if (s == 0)
        {
            cout << "客户端退出啦,我也推出啦!" << endl;
            break;
        }
        else
        {
            cout << "read: " << strerror(errno) << endl;
            break;
        }
    }
    close(pipefd);
    cout << "服务器也退出了!" << endl;
    unlink(IPC_PATH);
    return 0;
}

运行截图如下
在这里插入图片描述
我还想测试一下管道的大小,一直向管道写入,不读取
在这里插入图片描述

共享内存

基本概念

什么是共享内存
共享区能共享库也一定能共享其他东西,因此我们把内存创建好,某一进程通过页表映射到我们的进程地址空间,然后把这份空间的起始地址返回给用户,此时该进程就能通过自己的页表找到这份空间,此时另一进程也通过同样的方式将内存通过页表映射到自己的虚拟地址空间,再将该空间地址返回给自己

  1. 此时各自完成了共享内存的创建
  2. 分别把共享内存挂接到各自的进程上下文,这种就叫做共享内存

在这里插入图片描述

共享内存如何管理

问题来了:共享内存存在哪里?我如何知道共享内存存在还是不存在
存在内核中,–内核会给我们维护共享内存的结构。
问题又来了,操作系统中有多个进程通信,也就有多个共享内存,所以共享内存也是要被管理起来的。“先描述再组织”。
问题:共享内存是否存在我们该如何知道?
通过共享内存的唯一标识符key,有key说明有,并且该值一定是用户提供的。
为什么一定是用户提供的呢?
如果是操作系统提供key值,操作系统在当前进程的内存空间中创建一块共享内存,但由于进程间具有独立性,其他进程无法知晓这个key是否已经被使用了,所以key需要用户自己提供,并且与另外一个进程约定,该共享内存的key值。

内核代码

/* Obsolete, used only for backwards compatibility and libc5 compiles */
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 */
};

描述共享内存的数据结构里还保存了一个ipc_perm结构体,这个结构体保存了IPC(进程间通信)的一些权限关键信息

/* Obsolete, used only for backwards compatibility and libc5 compiles */
struct ipc_perm
{
	__kernel_key_t	key;//共享内存的唯一标识符
	__kernel_uid_t	uid;
	__kernel_gid_t	gid;
	__kernel_uid_t	cuid;
	__kernel_gid_t	cgid;
	__kernel_mode_t	mode; //权限
	unsigned short	seq;
};

匿名管道VS共享内存
匿名管道是约定好使用同一个文件通信
共享内存是约定好使用同一个key值通信

共享内存的相关函数

获得key值
在这里插入图片描述
贡献内存的创建
在这里插入图片描述

共享内存的申请理解:
虚拟地址空间的大小是4GB,操作系统一次读写磁盘是4kb,所以4GB–>1038576页,这么多页一定是要管理起来的,那么如何“先管理再组织”呢?
在操作系统中相当于是有struct page mem[1038576]数组,当我们为共享内存申请两页代码时,就相当于是在这个数组中分配了两个页。

在这里插入图片描述
如何知道共享内存没有被删除呢?
命令ipcs -m
在这里插入图片描述
还有两个函数在下面。控制函数和挂接函数

共享内存的删除

  1. 直接用命令删除
    ipcrm -m (shimid的值)
    所以此时我的共享内存应该是这样删除的
    ipcrm -m 0
    在这里插入图片描述
    为什么不用key值而用shmid
    因为key是在操作系统内核的,但是我们的ipc命令是在用户层的,所以只能用shimd值
    在这里插入图片描述
    还可以用其他方法吗?
    还可以用系统接口进行删除
    在这里插入图片描述

共享内存的使用

在这里插入图片描述
共享内存虽然是进程创建的但是共享内存不属于进程,它属于内核,即使进程退出了,共享内存依然存在
所以我们要将进程与共享内存产生关联
在这里插入图片描述
共享内存与进程产生关联
在这里插入图片描述
去关联
在这里插入图片描述
将客户端和服务端连接起来
在这里插入图片描述
在这里插入图片描述
总结:
我们把共享内存实际上是映射到了用户的进程地址空间,对每一个进程而言,挂接到自己的上下文中的共享内存,属于自己的空间,类似于堆空间或占空间,用户可以不使用系统调用接口直接使用。
共享内存由于他自身的特性,他没有任何访问限制,共享内存直接被双方看到,属于用户的空间,可以直接通信,所以不安全!
共享内存还是所有进程间通信中速度最快的
对比管道而言:
进程A写信息到管道,此时进程B不能直接看到信息,还需要进程A拷贝到管道,再由管道复制拷贝到进程B。
相关源码
源码点这!
共享内存的控制用的是信号量
信号量后续线程见!

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

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

相关文章

java设计模式(十五)责任链模式

目录 定义模式结构角色职责代码实现适用场景优缺点 定义 责任链模式(Chain of Responsibility) 使多个对象都有机会处理请求&#xff0c;从而避免请求的发送者和接受者之间的耦合关系。将这些对象连成一条链&#xff0c;并沿着这条链传递该请求&#xff0c;直到有对象能够处理…

deque介绍

目录 简介&#xff1a; 初识deque deque的底层实现 deque插入 deque的operator[] deque的迭代器 deque的缺陷 与vector比的缺陷 与list相比的缺陷 deque的优势 简介&#xff1a; 这一节不会进行模拟实现&#xff0c;只会聊聊deque的底层 原因是我们学习deque是为了…

RabbitMQ中的AMQP协议与核心组成介绍

前言 在RabbitMQ中为了传输数据&#xff0c;使用的是基于TCP/IP协议构造的AMQP协议。RabbitMQ的核心组成部分包括&#xff1a;Server、Connection、Channel、Message、ExChange、Virtual Host、Bingings、Routing key、Queue AMQP协议 AMQP协议全称&#xff1a;Advanced Mes…

RK3588平台开发系列讲解(驱动基础篇)信号驱动 IO 实验

平台内核版本安卓版本RK3588Linux 5.10Android 12文章目录 一、信号驱动 IO 简介二、实验程序2.1、应用程序2.2、驱动程序沉淀、分享、成长,让自己和他人都能有所收获!😄 📢 信号驱动 IO 不需要应用程序查询设备的状态,一旦设备准备就绪,会触发 SIGIO 信号,进而调用注…

论文中文翻译——kAFL Hardware-Assisted Feedback Fuzzing for OS Kernels

本论文相关内容 论文下载地址——26th USENIX Security Symposium论文中文翻译——kAFL Hardware-Assisted Feedback Fuzzing for OS Kernels 文章目录 本论文相关内容前言kAFL&#xff1a;操作系统内核的硬件辅助反馈Fuzzing作者信息论文来源主办方信息摘要1 引言2 技术背景2…

系统U盘制作随记

随身系统U盘制作 最近花了好多时间&#xff0c;废了好多U盘才把这东西搞明白了。 主要是自己的笔记本问题比较多&#xff0c;用实验室的Hp机一下就弄好了。 用这篇博客总结一下自己&#xff0c;然后附上详细的流程以免大家踩坑。 Windows to Go 这个比较容易上手 1. 准备…

EIoT能源物联网在工厂智能照明系统改造项目的应用 安科瑞 许敏

【摘要】&#xff1a;随着物联网技术的发展&#xff0c;许多场所针对照明合理应用物联网照明系统&#xff0c;照明作为工厂的重要能耗之一&#xff0c;工厂的照明智能化控制&#xff0c;如何优化控制、提高能源的利用率&#xff0c;达到节约能源的目的。将互联网的技术应用到工…

MySQ基本操作详解

MySQL的基本操作 首先sql操作中的关键字的是大小写不敏感的&#xff0c;create 和CREATE是一样的。 1.库操作 1. 1查看数据库 show databases;show 和databases 之间有一个或者多个空格注意是databases而不是database结尾分号是英文形式&#xff0c;分号在SQL中是表示一行执…

第三节 循环结构

文章目录 1. while循环1.1 什么是循环?1.2 while 循环1.2.1 语法结构1.2.2 循环中的异类 1.3 while循环使用案例1.3.1 求1~100之间的和1.3.2 求1~100之间偶数之和1.3.3 循环中的"标志变量" 1.4 嵌套循环使用1.4.1 嵌套循环语法结构1.4.2 嵌套练习 1.5 知识扩展 --最…

Mobx+Mobx-React快速上手 简单可扩展的状态管理解决方案

Mobx是Redux之后的一个状态管理库&#xff0c;基于响应式状态管理&#xff0c;整体是一个观察者模式的架构&#xff0c;存储state的store是被观察者&#xff0c;使用store的组件是观察者。Mobx可以有多个store对象&#xff0c;store使用的state也是可以变对象&#xff0c;这些都…

LNMP架构搭建实操(终有弱水替沧海,再无相思寄巫山”大概意思就是,你会遇到很多人,但不会有人像我那么爱你了。)

文章目录 一、安装Nginx服务1.安装依赖包2.创建Nginx运行用户3.编译安装Nginx源码包4.优化路径便于使用5、添加 Nginx 系统服务 二、安装Mysql服务1.安装Mysql环境依赖包2.创建Mysql运行用户3.编译安装4.修改mysql配置文件5.更改mysql安装目录和配置文件的属主属组6.设置路径环…

【Leetcode】77 组合 | 掌握回溯的力量吧!

【1】限制&#xff1a;数字只能够使用一次。 77 组合 栗子&#xff0c;从 { 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 } \{1,2,3,4,5,6,7,8,9,10\} {1,2,3,4,5,6,7,8,9,10}中选择4个数&#xff1a; 选择1&#xff0c;从 { 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 } \{2,3,4,5,6…

电力需求侧管理和电力负荷管理数字化解决方案 安科瑞 许敏

摘要&#xff1a;近年来全国用电负荷特别是居民用电负荷的快速增长&#xff0c;全国范围内夏季、冬季用电负荷“双峰”特征日益突出&#xff0c;恶劣气候现象多发增加了电力安全供应的压力。具有随机性、波动性、间歇性特征的可再生能源大规模接入电网对电力系统的稳定性带来新…

视频观看行为高级分析(大数据分析)

今天介绍一下我们的视频观看行为高级分析功能。 一、观看行为分析 观看行为分析&#xff0c;基于Polyv大数据分析&#xff0c;能够以秒为粒度展示观众如何观看您的视频。 视频观看热力图是单次观看行为的图形化表示&#xff0c;Polyv云点播视频的每一次播放&#xff0c;都会产…

基于jupyter的多分类问题练习

文章目录 练习3&#xff1a;多分类问题介绍1 多分类1.1 数据集1.2 数据可视化1.3 逻辑回归的向量化1.3.1 代价函数的向量化1.3.2 梯度的向量化1.3.3 正则化逻辑回归的向量化 1.4 多分类-分类器 1.5 使用分类器进行预测 总结 练习3&#xff1a;多分类问题 介绍 在本练习中&…

Leetcode周赛348

第一题&#xff1a;最小化字符串长度 思路分析 通过分析我们可以发现&#xff0c;只要存在重复的元素就可以继续进行操作所以这里本质上是一道去重的题目去重我们可以使用双指针算法和Set&#xff1b;我们选择使用Set进行去重 class Solution {public int minimizedStringLengt…

Vue2 vue-cli

安装与卸载vue脚手架 npm i -g vue/cli vue --version 查看vue脚手架版本 vue -V 查看vue脚手架版本 npm uninstall -g vue/cli 卸载 创建项目 vue create 项目名 选择项目 &#xff08;Default 为快速创建项目&#xff09; 选择最后一下&#xff0c;回车 上下键选择 Rou…

shell脚本:函数

shell脚本-函数 一、函数&#xff1a;1.定义&#xff1a;2.作用&#xff1a;3.格式&#xff1a; 二、函数传参&#xff1a;1.定义&#xff1a;2.函数变量&#xff1a;3.递归&#xff1a;4.函数库&#xff1a; 一、函数&#xff1a; 1.定义&#xff1a; &#xff08;1&#xf…

Internal error. Please report to https://jb.gg/ide/critical-startup-errors

大佬的解决方式&#xff1a;PyCharm 2023 启动报错的处理 部分同学&#xff0c;发现在安装 PyCharm 2023.1.2 以及 PyCharm 2023.2 的抢先体验版之后&#xff0c;运行的时候愣是直接弹出了类似上面的报错。 反正&#xff0c;别慌&#xff01; 是的&#xff0c;他们有 bug。 …

呈现视觉妙技:使用Python将MP4视频转化为迷人的GIF图像

前言 GIF图片对于我来说是一个很好的展示方式&#xff0c;GIF 图片能够展示动态的图像效果&#xff0c;对于展示计算机视觉算法或结果非常有用。例如&#xff0c;我可以使用 GIF 图片来展示运动跟踪、姿势识别、图像分割、目标检测等任务的结果&#xff0c;以更生动和直观的方…