【Linux】如何实现单机版QQ,来看进程间通信之管道

学会了管道,就可以实现简单的qq哦~

文章目录

  • 前言
  • 一、匿名管道
  • 总结


前言

为什么要进行进程间通信呢?因为需要以下这些事:

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

一、管道

1.匿名管道

首先我们前面说过,进程是具有独立性的,但是要实现两个进程通信就必须让这两个进程看到同一份资源,这该怎么办呢两个独立的进程如何看到同一份资源呢?其实这个操作是操作系统直接或间接提供的。下面我们先看看管道,然后再画图讲解如何让进程看到同一份资源。

who这个命令是一个进程,wc也是一个进程通过管道完成了进程间通信,那么如何证明这两个是进程呢?我们证明一下:

首先我们用sleep命令创建了3个进程,然后后面加个&符号是让这些进程在后台运行,否则如果在前台运行我们就不能输入指令了,然后通过查看 进程发现三个sleep进程都在并且他们的父进程都是bash。将sleep比作刚刚的who和wc指令就证明了进程间通信。

下面我们讲解一下管道的原理:

 首先先有进程,也就是task_struct,每个进程都有对应的文件描述符表,文件描述符表中有相应的数组,数组中存放了标准输入0,标准输出1,标准错误2,而每个进程描述符都会存放相应struct file的地址,在进程间通信的时候系统会提供一个内存文件,这个内存文件不会在磁盘刷新,这个文件被称为匿名文件,当我们以读和写方式打开一个文件,然后我们fork创建一个子进程,子进程也有task_struct,并且子进程会继承父进程的文件描述符表(但是不会复制父进程打开的文件对象),而文件描述符表中存放文件的地址都是相同的,所以子进程的文件描述符表也指向父进程的文件,正是因为这样,在父进程以读和写打开一份文件,而子进程也同样读和写打开和父进程打开的一样的一份文件,这就让两个进程看到了同一份资源。但是这种管道只能实现单向通信,比如我们关闭父进程的写端,关闭子进程的读端让子进程去写这两个进程就实现单向通信了。管道只能单向通信的原因是文件只有一个缓冲区,一个写入位置一个读取位置所以只能单向通信,要是想双向通信那就打开两个管道!而上面所讲的管道就是匿名管道。

下面我们写一个管道的程序:

 首先0 1 2是默认打开的标准输入,标准输出,标准错误,而3就是读端,4就是写端,我们要实现单向信道一定是父子进程一读一写。

首先我们在VScode中创建等会要用的.cc文件和makefile。

要创建管道,首先我们要知道创建管道的函数pipe:

此函数有一个参数是一个一维数组,这个一维数组只有两个元素,这个参数也叫输出型参数(这两个元素我们的例子中就是读端和写端)。

下面我们完成主体:

#include <iostream>
#include <string>
#include <cassert>
#include <unistd.h>
#include <vector>
#include "task.hpp"
#include <sys/wait.h>
#include <sys/types.h>
using namespace std;
int main()
{
    //1.1创建管道
    int pipefd[2] = {0};
    int n = pipe(pipefd);
    if (n<0)
    {
       std::cout<<"pipe error,"<<errno<<":"<<strerror(errno)<<std::endl;
       return 1;
    } 
    std::cout<<"pipefd[0]:"<<pipefd[0]<<std::endl;
    std::cout<<"pipefd[1]:"<<pipefd[1]<<std::endl;
    //1.2创建进程
    pid_t id = fork();
    if (id==-1)
    {
       //创建子进程失败
       exit(-1);
    }
    if (id==0)
    {
       //子进程
       close(pipefd[0]);
       const std::string namestr = "hello,我是子进程";
       int cnt = 1;
       char buffer[1024];
       while (true)
       {
           snprintf(buffer,sizeof buffer,"%s,计数器:%d,我的pid:%d\n",namestr.c_str(),cnt++,getpid());
           write(pipefd[1],buffer,strlen(buffer));
           sleep(1);
       }
       //写入成功后将写端关闭
       close(pipefd[1])
       exit(0);
    }
    //父进程
    close(pipefd[1]);
    //关闭不需要的fd,让父进程读取,子进程写入。
    char buffer[1024];
    while (true)
    {
       int n = read(pipefd[0],buffer,sizeof(buffer-1));
       if (n>0)
       {
           buffer[n] = '\0';
           std::cout<<"我是父进程,child give me message:"<<buffer<<std::endl;
       }
    }
    return 0;

}

 我们的程序很简单,首先创建管道,如果创建失败就报错打印错误码,然后我们打印一下读端和写端(如果没问题的话读端是3,写端是4),然后第二步我们创建子进程,创建子进程同样要判断创建失败的情况,如果创建成功我们先关闭子进程的读端,然后搞一个1024的缓冲区让字符串写入到缓冲区里,sleep可以让现象更明显,写完后我们为了保证管道安全所以关闭刚刚使用的写端,在父进程中同理只不过是变成了读,下面我们运行一下代码:

 我们确实看到了两个进程,并且确实也完成了简单的单向通信,因为子进程把自己的数据给了父进程。通过上面的实例代码我们能知道管道的特点吗?

1.单向通信

2.管道的本质是文件,因为fd的生命周期随进程,管道的生命周期也是随进程的。

3.管道通信,通常用来进行具有"血缘"关系的进程,进行进程间通信,常用于父子通信----pipe打开管道,并不清楚管道的名字,所以是匿名管道。

下面我们修改一下代码,刚刚我们让子进程发的慢一点(sleep了1秒),现在我们让父进程读的慢一点看看是是什么样子:

 下面我们运行起来:

 通过结果我们发现写入的次数和读取的次数不是严格匹配的。所以这也是管道的一个特点。

 下面我们再将代码修改一下,让父进程读取恢复正常,让子进程写的慢一些我们再看看效果:

 

通过程序运行结果我们发现,当写入很慢的时候读取也变慢了,也就是说写入是会影响读取的。

下面我们再将代码修改一下,我们让子进程持续写入一个字符,父进程先等10秒再读:

 

 

通过上图我们可以看到当子进程写入65535个也就是2的16次方的数据后,父进程直接打印了,并且时间并没有到10秒,所以打印的原因是因为管道是有容量的,当我们的写端将管道文件写满了我们就不能继续写了,这与之前read端读完了所有的管道数据,如果写端不发就只能等待是相对应的。所以管道的另一个特点来了:具有一定的协同能力,让reader和writer能够按照一定的步骤进行通信(自带同步机制)。

下面我们再试试将写端关闭了读端会怎么样呢?

 read函数的返回值可以判断读取了多少,比如返回值大于0就是将缓冲区的数据都读完了,返回值等于0就是读到了文件结尾,否则就是读取异常,下面我们将代码运行起来:

结果就是子进程写了一个字符退出后,父进程读取到\0就会读到文件结尾而退出。那么如果我们写端一直写,将读端关闭了会发生什么呢?因为这里的行为是无意义的,所以操作系统会杀死这个一直写入的进程(因为操作系统不会维护无意义,低效率,或者浪费资源的事情,操作系统会杀死一直写入的进程,会通过信号来终止进程(13号信号)),那么如何证明会被操作系统杀死呢,下面我们让父进程等待一下子进程然后看看:

 通过结果我们发现确实是这样,子进程被操作系统用13号信号终止了。以上就是匿名管道的相关知识,下面我们用一批管道实现控制进程的代码:

 首先我们创建一个ctrlProecess.cc的文件夹,然后写一个makefile,准备工作做好后我们就可以按照思路写代码了,我们第一步要先进行构建控制结构,父进程写入,子进程读取,并且既然是多个管道我们直接用个循环来控制:

 我们在判断是否成功创建子进程的时候直接用assert断言了,其实这里应该要用if语句判断的,但是由于这个函数基本不会出错并且我们只是演示一下代码所以就用断言了,当返回值等于0的时候那么一定是子进程了,否则就是父进程了,那么我们该如何通信呢,让父进程写入,子进程读取,所以我们先关闭不需要的端口。

现在前期准备工作已经完成了,下面如何让父进程管理自己创建的管道和进程呢?还记得我们之前说过的先描述,在组织吗?答案就是这个。

 我们为了描述这些创建的管道所以用一个类来封装,成员变量包括子进程的pid以及父进程写入所需要的端口,然后我们在构造函数中将这些变量初始化一下。

class EndPoint
{
public:
     pid_t _child_id;
     int _write_fd;
     EndPoint(int id,int fd)
          :_child_id(id)
          ,_write_fd(fd)
     {
    
     }
     ~EndPoint()
     {

     }

};

因为是多个管道所以我们将刚刚的自定义对象放到vector中去管理,这不就完成了先描述,在组织的任务吗,所需要的头文件大家记得加上,下面我们将vector定义到函数最开始。

我们的目的是让父进程写入,子进程读取,所以肯定是在父进程的地方来描述这个子进程:

 将一个个子进程和写端的对象放入向量后,父进程就拿到了一批Endpoint对象,这个结构里包含了要写入的文件描述符的写端和新的子进程。下面的工作就是要子进程读取指令,我们要让子进程在读取指令的时候,都从标准输入读取,要从标准输入读取我们就要用的以前学的知识了,那就是输入重定向,输入重定向的方式很多,我们主要用函数的方式:

 还记得我们之前学的dup2函数吗,在这里就可以派上用场了。我们的目的是要子进程都要从标准输入读取,所以函数的第一个参数old就是pipefd[0]了因为我们要让pipefd[0]重定向到0(标准输入),所以第二个参数new就是0了,然后我们让子进程开始等待获取命令,这里我们直接写一个函数WaitCommand()将所以的代码写入这个函数里,并且将刚刚创建进程的代码也放入一个creatProcess函数中,让各个函数完成相应的功能会让代码看的更简洁:

 下面在让子进程要执行方法的函数中先写一个read函数和一个死循环我们来看看代码能否正常创建子进程,然后再编写后序的代码:

 

 运行起来后我们可以看到之前写的创建进程的函数没毛病,下面我们来设计一下让子进程执行的方法的函数。在完成这个函数前先新建一个头文件task.hpp,然后创建一些简单的打印任务:

 我们在Task类中先定义一个vector,vector中存放的类型为函数指针类型,每一个函数指针都指向一个函数方法,这样我们就可以通过指令来调用不同的方法,对于指令command来讲我们之前也提到过其实就是位图,我们定义三个宏值分别代表三种打印方法,相要调用哪个方法就让command参数&上对应的宏值就完成了。我们在.cc文件中加上头文件,然后定义一个全局的Task类:

接下来我们继续实现子进程任务函数:

 在子进程函数中我们默认指令为int类型,然后通过read函数的返回值判断是否读取成功,如果返回值等于4字节说明读到了指令那么就调用对应的函数方法,如果返回值等于0就说明当前的子进程要退出了直接break,剩下的就是读取失败也是直接退出,当然我们不能只执行一次,所以我们加个循环让子进程周而复始的去执行任务:

 那么接下来的父进程还需要干什么呢?父进程需要选择任务,选择进程然后下发任务,下面我们来实现:

 这里的任务是可以随机选择的,但是我们为了测试就只用了LOG任务,然后选择进程的时候直接用随机函数选择即可,下发任务也就是让父进程写入,写入的是指令对应的方法,然后我们将之前答应的代码加上getpid,把子进程的pid也打印出来:

 以下是运行起来后的代码:

 这样我们就完成了要实现的功能,现在我们再把相应的函数优化一下,让选择任务的时候可以是交互式的,首先第一步让用户选择任务,这里我们先实现一个选择面板:

 将选择面板搞定后我们就可以实现让用户选择对应的任务了:

下面我们让选择进程的功能变成按顺序的,这样就不会出现每次随机让一个进程执行任务的情况:

 下面我们我们再实现一下每次拿到任务后打印正在执行哪个任务:

  我们先创建一个静态的int变量用来充当第几个进程,然后我们在类外将number初始化为0,string类型的进程名接收缓冲区里的字符,函数name可以返回一个进程名。然后我们在选择任务那块也打印一下:

 然后如果进程退出的话也要有提示,所以我们在子进程那块加上打印提示:

 当n等于0说明父进程将写端关闭了,随之就打印一下告诉用户。然后我们再将代码简化一下,把刚刚main函数中写的控制函数代码都放到ctrlProcess函数中:

 写到这里发现124行有个报错,是因为返回的name函数需要加上const让静态变量也能使用(因为我们的ctrlProcess函数的参数是const对象)

 

 下面我们运行一下程序:

 程序运行起来后我们发现输入3退出不了,并且输入其他指令程序也没有打印动作,所以我们用if语句判断一下:

 除了这个问题我们还要解决子进程退出问题,还记得我们之前说过的僵尸进程吗,如果子进程没有被父进程等待那么子进程就变成了僵尸状态,为了防止这种情况我们再写一个进程等待函数函数:

 回收子进程之前我们需要将父进程的所有写端都关了,然后等待10秒后我们让父进程把每个子进程都回收了,这样就完成了简单的回收子进程函数。

当我们运行起来后发现可以完成我们的需求,并且在退出后也能正常的回收子进程。

不知道有没有人会有疑问,为什么我们刚刚在同一个循环内不能直接边关闭边回收子进程呢,就像下图这样:

 我们先用这样的代码运行一下:

 我们发现这样写后程序就卡在了回收进程处,这是为什么呢?其实是和子进程有关,我们早就说过子进程会继承父进程打开的文件描述符(这就是管道的原理),这里的坑就在于当第二次及第二次以后的创建子进程时,这个子进程会继承前面所有父进程的端口,也就是说越往后的子进程继承的端口越多,如下图所示:

也就是说刚刚卡主的原因是写端没有全部关完就回收了子进程,就造成了阻塞。而我们用一个循环单独关闭父进程的端口时是不会出现这个问题的,因为我们是先关闭了父进程的写端,都关闭后才去回收子进程。

由于上面的讲解都是通过图片的形式,下面我们将此次的实例代码发出来:

首先是ctrlProcess.cc文件中的:

#include <iostream>
#include <string>
#include <cassert>
#include <unistd.h>
#include <vector>
#include "task.hpp"
#include <sys/wait.h>
#include <sys/types.h>
using namespace std;
const int gnum = 3;
Task t;
class EndPoint
{
private:
     static int number;
public:
     pid_t _child_id;
     int _write_fd;
     std::string processname;
     EndPoint(int id,int fd)
          :_child_id(id)
          ,_write_fd(fd)
     {
         char namebuffer[64];
         snprintf(namebuffer,sizeof(namebuffer),"process-%d[%d:%d]",number++,_child_id,_write_fd);
         processname = namebuffer;
     }
     std::string &name()
     {
        return processname;
     }
     ~EndPoint()
     {

     }

};
int EndPoint::number = 0;
//子进程要执行的方法
void WaitCommand()
{
    while (true)
    {
        int command = 0;
        int n = read(0,&command,sizeof(int));
        if (n==sizeof(int))
        {
            t.Execute(command);
        }
        else if(n==0)
        {
            std::cout<<"父进程让我退出,我就退出了: "<<getpid()<<std::endl;
            break;
        }
        else 
        {
            break;
        }
    }
   
}
void CreatProcess( vector<EndPoint>* end_points)
{
     //1.先进行构建控制结构,父进程写入,子进程读取
    for (int i = 0;i<gnum;i++)
    {
        //1.1创建管道
        int pipefd[2] = {0};
        int n = pipe(pipefd);
        assert(n==0);
        (void)n;
        //1.2创建进程
        pid_t id = fork();
        assert(id!=-1);
        //返回值等于0一定是子进程
        if (id==0)
        {
            //先关闭不要的fd
            close(pipefd[1]);
            //我们期望所有的子进程读取指令的时候,都从标准输入读取
            //1.3.1输入重定向
            dup2(pipefd[0],0);  //读0就像读管道一样
            //1.3.2让子进程开始等待获取命令
            WaitCommand();
            close(pipefd[0]);
            exit(0);
        }

        //一定是父进程
        //1.3关闭不要的fd
        close(pipefd[0]);

        //1.4将新的子进程和他的管道写端,构建对象
        end_points->push_back(EndPoint(id,pipefd[1]));

    }
}
int ShowBoard()
{
    std::cout<<"################################"<<std::endl;
    std::cout<<"##0. 执行日志任务 1. 执行数据库任务##"<<std::endl;
    std::cout<<"##2.执行请求任务  3.退出#########"<<std::endl;
    std::cout<<"################################"<<std::endl;
    std::cout<<"请选择#";
    int command = 0;
    std::cin>>command;
    return command;
}
void ctrlProcess(vector<EndPoint>& end_points) 
{
    //2.1我们可以写成自动化的,也可以写成交互式的
    int cnt = 0;
    int num = 0;
    while (true)
    {
        //1.选择任务
        int command = ShowBoard();
        if (command==3)
        {
            break;
        }
        if (command<0|| command>2)
        {
            continue;
        }
        //2.选择进程
        int index = cnt++;
        cnt%=end_points.size();
        std::cout<<"选择了进程:"<<end_points[index].name()<<" | 处理任务"<<command<<std::endl;
        //3.下发任务
        write(end_points[index]._write_fd,&command,sizeof(command));

        //sleep(1);
    }
}
void ExitProcess()
{
    exit(0);
}
void waitProcess(const vector<EndPoint>& end_points)
{
     //1.我们需要让子进程全部退出 -- 只需要让父进程关闭所有的写端
     //for (const auto &ep:end_points)
     for (int end = end_points.size()-1;end>=0;end--)
     {
        std::cout<<"父进程让所有的子进程全部退出"<<std::endl;
        //先关闭最后一个写端倒着一直关闭,因为子进程会继承父进程的文件描述符所以
        //后面的子进程都会链接到第一个父进程的写端,如果关第一个无法全部关闭,会造成
        //阻塞
        close(end_points[end]._write_fd);
        std::cout<<"父进程回收了所有的子进程"<<std::endl;
        waitpid(end_points[end]._child_id,nullptr,0);

     }
     //std::cout<<"父进程让所有的子进程全部退出"<<std::endl;
     sleep(10);
     //2.父进程要回收子进程的僵尸状态
     //for (const auto& ep:end_points)
     //{
     //   waitpid(ep._child_id,nullptr,0);
     //}
     //std::cout<<"父进程回收了所有的子进程"<<std::endl;
     //sleep(10);
}
int main()
{
    //1.先进行构建控制结构,父进程写入,子进程读取
    vector<EndPoint> end_points;
    CreatProcess(&end_points);
    //2.我们得到了什么?
    ctrlProcess(end_points);
    //3.处理所有的退出问题
    waitProcess(end_points);
    return 0;
}

下面是头文件task.hpp中的:

#include <iostream>
#include <vector>
#include <unistd.h>
typedef void(*fun_t)();  //函数指针


void PrintLog()
{
    std::cout<<"pid:"<<getpid()<<",打印日志任务,正在被执行..."<<std::endl;
}
void InsertMySQL()
{
    std::cout<<"执行数据库任务,正在被执行..."<<std::endl;
}
void NetRequest()
{
    std::cout<<"执行网络请求任务,正在被执行..."<<std::endl;
}
#define COMMAND_LOG 0
#define COMMAND_MYSQL 1
#define COMMAND_REQUEST 2
class Task
{
public:
    Task()
    {
        funcs.push_back(PrintLog);
        funcs.push_back(InsertMySQL);
        funcs.push_back(NetRequest);
    }
    ~Task()
    {

    }
    void Execute(int command)
    {
        if (command >= 0&& command < funcs.size())
        {
            funcs[command]();
        }
    }

public:
    std::vector<fun_t> funcs;
};

总结

管道读写规则:

当没有数据可读时:
O_NONBLOCK disableread调用阻塞,即进程暂停执行,一直等到有数据来到为止。
O_NONBLOCK enableread调用返回-1errno值为EAGAIN
当管道满的时候:
O_NONBLOCK disable write调用阻塞,直到有进程读走数据
O_NONBLOCK enable:调用返回-1errno值为EAGAIN
如果所有管道写端对应的文件描述符被关闭,则read返回0
如果所有管道读端对应的文件描述符被关闭,则write操作会产生信号SIGPIPE,进而可能导致writ进程退出
当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。
当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。
管道特点:
只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信;通常,一个管道由一个进程创建,然后该进程调用fork,此后父、子进程之间就可应用该管道。
管道提供流式服务
一般而言,进程退出,管道释放,所以管道的生命周期随进程
一般而言,内核会对管道操作进行同步与互斥
管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

ChatGPT实现旅行安排

工作之余&#xff0c;出门旅行一趟放松放松身心&#xff0c;是对自己辛勤工作最好的犒劳方式之一。旅行可以近郊游、可以远游&#xff0c;可以穷游&#xff0c;可以自驾游&#xff0c;可以一言不合打飞的喂鸽子&#xff0c;方式多种多样。但是多数情况&#xff0c;我们是到一个…

论文解析-基于 Unity3D 游戏人工智能的研究与应用

1.重写 AgentAction 方法 1.1 重写 AgentAction 方法 这段代码是一个重写了 AgentAction 方法的方法。以下是对每行代码解释&#xff1a; ①public override void AgentAction(float[] vectorAction) 这行代码声明了一个公共的、重写了父类的 AgentAction 方法的方法。它接受…

Java版本工程管理系统源码企业工程项目管理系统简介

一、立项管理 1、招标立项申请 功能点&#xff1a;招标类项目立项申请入口&#xff0c;用户可以保存为草稿&#xff0c;提交。 2、非招标立项申请 功能点&#xff1a;非招标立项申请入口、用户可以保存为草稿、提交。 3、采购立项列表 功能点&#xff1a;对草稿进行编辑&#x…

Vue收集表单数据和过滤器

目录 收集表单数据 收集表单数据总结 过滤器 过滤器小结 收集表单数据 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><title>Title</title><!--vue--><script src"https://cdn.sta…

C++ ---- 类和对象(下)

目录 初始化列表 初始化列表的语法 初始化列表的特性 explicit关键字 构造函数的隐式转换 explicit的作用 static修饰成员变量和成员函数 static修饰成员变量 static修饰成员函数 友元 友元函数 友元类 内部类 匿名对象 拷贝对象时的一些编译器优化 初始化列表 …

Kibana 的安装

1. 简介 Kibana 是一个开源的分析与可视化平台&#xff0c;可以用 Kibana 搜索、查看存放在 Elasticsearch 中的数据&#xff0c;就跟谷歌的 elasticsearch head 插件类似&#xff0c;但 Kibana 与 Elasticsearch 的交互方式是各种不同的图表、表格、地图等&#xff0c;直观的…

超稳定ChatGPT镜像网站,小白适用,赶紧收藏【持续更新中】

&#x1f482;作者简介&#xff1a; THUNDER王&#xff0c;一名热爱财税和SAP ABAP编程以及热爱分享的博主。目前于江西师范大学会计学专业大二本科在读&#xff0c;同时任汉硕云&#xff08;广东&#xff09;科技有限公司ABAP开发顾问。在学习工作中&#xff0c;我通常使用偏后…

Redis修炼 (15. redis的持久化-RDB)

RDB 就是 redis database backup file 数据备份文件 就是把内存中的所有数据都保存在磁盘中。 save 注意这个保存过程是主进程在做 因为redis 是单线程 所有其他所有请求都会被卡死。 bgsave 这个稍微友好一点 是子进程 执行&#xff0c;避免主进程收到影响。 redis在服务停机…

母亲节快到了,祝所有母亲节日快乐!Happy Mother‘s Day

《游子吟》唐孟郊 慈母手中线&#xff0c;游子身上衣。 临行密密缝&#xff0c;意恐迟迟归。 谁言寸草心&#xff0c;报得三春晖。 My kind mother has a needle and thread in her hand,Making new clothes for her son who is to travel far away. She is busy sewing c…

【Pandas与SQL系列】Pandas实现分布函数percent_rank、cume_dist

目录 1&#xff0c;分布函数,1.1&#xff0c;percent_rank()1.2&#xff0c;cume_dist()1.3 SQL例子 2&#xff0c;Pandas 实现3&#xff0c;补充Pandas实现排序 1&#xff0c;分布函数, 应用场景&#xff1a;快速查看某个记录所归属的组内的比例 分布函数分类及基础语法&…

Kali-linux系统指纹识别

现在一些便携式计算机操作系统使用指纹识别来验证密码进行登录。指纹识别是识别系统的一个典型模式&#xff0c;包括指纹图像获取、处理、特征提取和对等模块。如果要做渗透测试&#xff0c;需要了解要渗透测试的操作系统的类型才可以。本节将介绍使用Nmap工具测试正在运行的主…

图像处理:高斯滤波算法

目录 前言 概念介绍 基本原理 卷积核的大小 卷积核的形状和权重比 卷积核的归一化 结论 Opencv实现高斯滤波 Python手写实现高斯滤波 参考文章 前言 在此之前&#xff0c;我曾在此篇中推导过图像处理&#xff1a;推导五种滤波算法&#xff08;均值、中值、高斯、双边…

springboot+jsp乡村中小学校园网站建设

随着科学技术的飞速发展&#xff0c;社会的方方面面、各行各业都在努力与现代的先进技术接轨&#xff0c;通过科技手段来提高自身的优势&#xff0c;乡村小学校园网当然也不能排除在外&#xff0c;从校园概况、学校风采、招生信息的统计和分析&#xff0c;在过程中会产生大量的…

三十二、自定义镜像

1 、Docker镜像的原理 Docker镜像本质是什么? Docker中一个centos镜像为什么只有200MB&#xff0c;而一个centos操作系统的iso文件要几个G? Docker中一个tomcat镜像为什么有500MB&#xff0c;而一个tomcat安装包只有10多MB? 操作系统组成部分: 计算机组成原理 进程调度子…

华为许超:伙伴成功,才有华为企业业务成功

伙伴&#xff0c;可以说是今年ICT行业最为重要的词。各大厂商都在强调伙伴优先&#xff0c;发力伙伴体系构建。然而行业内更多是厂商单维度的信息释放&#xff0c;重视强调厂商面向伙伴的支持与赋能。这个过程中&#xff0c;似乎普遍缺少一个视角&#xff1a;那就是伙伴究竟需要…

STM32开发(十九)STM32F103 数据手册 —— 低功耗模式解析

文章目录 低功耗介绍stm32 供电框图低功耗模式睡眠模式停止模式待机模式低功耗模式汇总低功耗介绍 系统复位或上电复位后,微控制器进入运行模式。在运行模式下,CPU通过HCLK提供时钟,并执行程序代码。 系统提供多种低功耗模式,可以在CPU不需要运行时进入低功耗模式节省功耗…

开心档之Java 抽象类

Java 抽象类 目录 Java 抽象类 抽象类 Employee.java 文件代码&#xff1a; AbstractDemo.java 文件代码&#xff1a; 继承抽象类 Salary.java 文件代码&#xff1a; AbstractDemo.java 文件代码&#xff1a; 抽象方法 Salary.java 文件代码&#xff1a; 抽象类总结…

虚拟化技术介绍-VMware和Docker的区别

都说今天是一个云时代&#xff0c;其实云的本质就是由基础架构提供商提供基础架构&#xff0c;应用开发商不再关心基础架构。我们可以类比人类刚刚发明电的时候&#xff0c;工厂需要自己建电站&#xff0c;而现在只需要电线和插座就可以使用电。云时代让我们可以在分钟、甚至秒…

【企业信息化】第6集 免费开源ERP: Odoo 16 MRP + 维护+ PLM +质量全面生产制造管理

文章目录 一、MRP 物料需求计划1.一款软件&#xff0c;满足您的所有需要2.工作中心控制面板3.优化您的库存等级4.条形码&#xff0c;即开即用5.出色报告关键绩效指标6.与其他Odoo应用程序完全集成 二、PLM 产品生命周期管理1.管理工程变更2.集成文件管理3.智能版本管理4.与其他…

【软件测试】未来软件测试必备的八大技能!你缺少哪个?

软件测试工程师是个神奇的职业&#xff0c;他是开发人员与老板之间的传话筒&#xff08;三夹板&#xff09;&#xff0c;也是开发人员与老板的好帮手&#xff1b; 他不仅需要有销售的沟通能力&#xff0c;也需要具备编辑人员的文档撰写技巧。如此一个面面俱到的岗位&#xff0…
最新文章