Linux | 进程间通信 | 匿名管道 | 命名管道 | 模拟代码实现进程通信 |

news2024/11/19 17:42:10

文章目录

    • 进程通信的意义
    • 匿名管道
      • 通信原理
      • 管道的访问控制
      • 进程控制
      • 管道的特点
    • 命名管道

进程通信的意义

之前聊进程时,讲过一个性质,即进程具有独立性,两个进程之间的交互频率是比较少的。就连父子进程也只是共享代码,修改父子进程中的任意进程的数据时,会触发写时拷贝机制,即分配一块新的空间存储数据,使父子进程做到数据分离,互不影响,父子进程之间唯一的信息交互可能就是父进程接收子进程的退出码了。所以由于进程的独立性,进程之间进行数据通信的成本较高。但是进程通信有很多的意义

1.实现数据之间的传输,进程可以发送数据给其他进程
2.实现资源的共享,多个进程共享彼此的数据
3.信息的通知,一个进程向另一个进程发送通知,告知某种特殊事件的发生
4.控制进程,一个进程可以完全控制另一个进程

进程间通信的应用很多,所以虽然它的成本高,但实现进程间通信还是很有必要的

匿名管道

通信原理

可以想象一下,两个进程要进行通信,传输数据,需要用什么方式。比如说,一个进程打开一个文件,向文件写入数据,此时的数据在内存中的缓冲区上,系统刷新缓冲区将数据写入磁盘,另一个进程也打开这个文件,系统将文件从磁盘加载到内存,使进程读取文件中的前一个进程写入的内容,这样两个进程就做到了通信。但是将外设作为通信载体会导致通信的速度非常慢,这无疑又提高了进程通信的成本,所以Linux系统中有一种特殊的文件:管道文件(操作系统将文件描述为struct_file,该结构体中有一个联合体,联合体有三个成员,分别用来表征文件类型是磁盘,管道,还是字符),管道文件不存储在磁盘上,而是存储在内存中,系统检测到文件类型为管道文件,就不用将文件数据刷新到磁盘上。用管道文件作为进程通信的载体在很大程度上提高了通信速度。

管道的最大特点是:单向传输数据,即无论何时,只能向一端输入,往另一端输出。使用管道进行进程间通信的基本步骤为

1.在父进程中以读和写的方式打开同一个文件
2.创建子进程
3.关闭父子进程中文件的读端或写端,使一个进程负责写入数据,一个进程负责读取数据

进程描述文件的结构体为struct files_struct,该结构体中有一个类型为struct file* 的fd_array数组,保存了进程打开的文件。父进程分别以读和写的方式打开一个文件(由于子进程的继承,子进程就不用再打开文件),对于父进程来说,有两个fd被存储到了fd_array数组中,其指向了进程打开的文件,创建子进程时,由于父子进程的数据以写时拷贝的方式共享,fd_array数组作为父进程的数据,子进程会继承同样的数组,而父进程打开的文件不属于进程,子进程不会继承打开的文件,所以子进程只以读和写的方式指向了父进程打开的文件,此时有四个fd(父进程的读和写,子进程的读和写)指向了打开的文件。形成管道的最后一步是关闭两个fd,使父子进程满足一个进程写入数据,一个进程读取数据的特点,形成管道。

我们要怎么打开一个管道文件?实际上系统提供了pipe接口,可以以读和写的方式打开一个管道文件在这里插入图片描述
在这里插入图片描述
pipefd作为一个输出型参数,pipe函数打开一个管道文件后,将以读方式打开的文件fd写入数组的第一个元素,将以写的方式打开的文件fd写入数组的第二个元素。所以pipefd[0]为读端,pipefd[1]为写端。该函数返回一个int值,如果调用成功返回0,失败返回-1。

管道的访问控制

使用pipe函数写一段代码

#include <iostream>
#include <unistd.h>
#include <time.h>
#include <string.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>
using namespace std;

int main()
{
    int pipefd[2] = {0};     // 接收pipe打开的管道文件
    int ret = pipe(pipefd);  // 创建管道
    if (ret != 0)
    {
        cout << "pipe函数调用失败" << endl;
        return 1;
    }
    else
    {
        // pipe succeed  
        // 父进程写,子进程读
        int id = fork(); // 子进程的创建
        if (id == 0)
        {
            // child,reader
            close(pipefd[1]); // 写端关闭
            char readBuffer[512] = {0};
            while (1) // 死循环读取管道
            {
                memset(readBuffer, 0, sizeof(readBuffer)); // 每次读取清空缓冲区                
                ssize_t s = read(pipefd[0], readBuffer, sizeof(readBuffer) - 1);
                readBuffer[s] = '\0'; // 向缓冲区最后添加结束符
                if (s > 0)
                {
                    cout << "子进程读取到信息:" << readBuffer;
                }
                // 如果管道中没有数据,子进程不再读,退出进程
                else if (s == 0)
                {
                    cout << "父进程退出,不再写入数据" << endl;
                    close(pipefd[0]);
                    break;
                }
                else
                {
                    cout << "读取文件失败" << endl;
                    return 2;
                }
            }
            exit(1);
        }
        else if (id > 0)
        {
            // parent,writer
            // 关闭文件的读端
            close(pipefd[0]);
            char writeBuffer[128];

            int cnt = 5;
            while (cnt)
            {
                char tmp[] = "这是父进程,在与子进程进行通信";
                unsigned int t = (unsigned int)time(NULL);
                sprintf(writeBuffer, "%s time:%d\n", tmp, t);
                write(pipefd[1], writeBuffer, sizeof(writeBuffer));
                sleep(1); // 父进程每1秒写一次数据
                cnt--;
            }
            // 父进程关闭管道,此时子进程read函数将返回0
            close(pipefd[1]);
        }
        else
        {
            cout << "创建子进程失败" << endl;
            return 2;
        }
		
		// 子进程退出后,父进程等待子进程
        pid_t res = waitpid(id, NULL, 0);
        if (res > 0)
        {
            cout << "父进程等待子进程成功" << endl;
        }
    }
    return 0;
}

(pipe创建管道后,由当前进程创建子进程,子进程关闭写端,父进程关闭读端,所以父子进程中,子进程用来读取管道文件,父进程用来向管道文件写入。sleep函数使父进程写入的频率为1秒,子进程不断的读取管道中的数据,当管道中没有数据(父进程不再写入时),子进程退出,最后父进程等待子进程,回收其资源)
在这里插入图片描述
通过运行结果可以看到,父进程每隔一秒写入一次数据(时间戳相差1),子进程准确的读取了这些数据,父进程不再写入时,子进程退出,并且被父进程回收。修改上面的代码,使父进程的写入频率为3秒,观察子进程运行的结果
在这里插入图片描述
可以看到子进程还是成功的读取了这些数据,无论父进程写入管道的时间间隔为多少。那么子进程读取完管道的数据,在父进程下一次写入数据前,子进程是什么状态?

如果父子进程打开一个普通文件,并向其写入数据,父子进程的写入是没有先后顺序的,无法确定谁先向文件中写入,这样的写入是无序且混乱的。但是在管道的数据通信中,父子进程的读写顺序是有序的,通过上面出现的运行结果,可以推测:如果管道中没有数据,reader就会阻塞(进入阻塞状态,进入文件的等待队列),等待数据的写入,如果管道被写满了,writer也会阻塞,等待数据被读取。
在这里插入图片描述
修改代码,父进程不断的写管道写入数据,子进程调用sleep休眠100秒,不再读取数据,父进程写入数据时会向屏幕打印写入的次数,运行程序,父进程向管道写入513次后就阻塞了,因为子进程没有读取管道的数据,管道被父进程写满了,此时的父进程不再写入,而是等待管道的数据被读取。

所以,在管道文件中,进程的读写顺序是遵循一种访问机制的,我们把这种访问机制叫做同步和互斥机制。这种机制可以保证管道的每次读和写都是有效的,不会出现重复的读取或者多次写入覆盖之前数据的情况。

进程控制

利用管道可以实现一个进程控制多个进程的效果。

刚才的一份代码可以实现父进程向管道写入数据,子进程读取数据,如果父进程写入的不再是数据,而是需要子进程执行的方法呢?子进程就可以获取这个方法并执行该方法,也就是说在进程间通信的数据,可以是普通的文本,也可以是一个方法,还可以是其他数据,我们的目的是让子进程得到父进程传递的数据,这也是进程通信的本质:使不同进程看到同一份数据。如果传递的数据是一个方法,我们只需要修改代码,让子进程执行此方法就能做到进程控制

而管道作为进程间通信的载体,父进程向子进程传递数据就成了向管道传递数据,只要子进程能从管道中读取数据,进程间通信就能实现。所以进程间通信的重点在于进程与管道间的数据传输。

如果需要通信的对象是多个子进程,也就需要父进程向多个管道中写数据。其大致过程为:首先要有需要让子进程执行的任务,也就是函数,函数以函数指针的方式存储在一个数组中。假设父进程需要与5个子进程进行通信,这就意味着需要使用pipe函数创建5个管道,采用for循环,循环5次,每次循环pipe函数都会打开一个管道,管道打开后创建子进程,关闭子进程的写端与父进程的读端,接着让子进程进入阻塞,等待管道被父进程写入数据,当数据被写入时,分析这些数据得到需要执行的任务并执行。父进程需要向子进程分发任务,由于现在在使用for循环创建管道,通信的对象(子进程)还没有完全创建好,所以此时不能直接向子进程分发任务,需要记录子进程的进程pid(用于父进程等待子进程,回收其资源)与管道文件的fd写端(用于父进程向管道的写入),在管道创建完成后(for循环之后),根据保存的fd随机向5个管道中的一个管道写入数据,也就是派发任务,当父进程不再需要向子进程派发任务,关闭管道文件的写端,使子进程不再阻塞等待数据被写入管道,而直接退出进程,最后由父进程回收子进程资源。

#include <iostream>
#include <cstdio>
#include <unistd.h>
#include <ctime>
#include <cstring>
#include <unistd.h>
#include <stdio.h>
#include <vector>
#include <map>
#include <cstdlib>
#include <sys/wait.h>
#include <sys/types.h>
#include <string>

using namespace std;

typedef void (*function)();
vector<function> functions;        // 全局的方法集,存储需要执行的任务
map<uint32_t, string> information; // 存储关于方法的信息

void function1()
{
    cout << "本次执行function1任务,执行本次任务的进程id[ " << getpid() << " ]" << endl;
}

void function2()
{
    cout << "本次执行function2任务,执行本次任务的进程id[ " << getpid() << " ]" << endl;
}

void function3()
{
    cout << "本次执行function3任务,执行本次任务的进程id[ " << getpid() << " ]" << endl;
}

void loadFunctions() // 方法集的加载
{
    information.insert({functions.size(), "function1"});
    functions.push_back(function1);

    information.insert({functions.size(), "function2"});
    functions.push_back(function2);

    information.insert({functions.size(), "function2"});
    functions.push_back(function3);
}

void work(uint32_t readfd)
{
    cout << "进程[" << getpid() << "]" << " 开始工作" << endl;
    while (1) // 不断的读取管道数据
    {
        int task_id;
        memset(&task_id, -1, sizeof(task_id));
        int ret = read(readfd, &task_id, sizeof(task_id)); // 读取任务在方法集中的task_id
        if (ret == 0)                                      // 管道中没有数据
        {
            cout << "由于父进程不再写入,子进程[ " << getpid() << "]不再读取" << endl;
            sleep(1);
            break;
        }
        if (ret != sizeof(task_id))
        {
            cout << "读取方法出错" << endl;
            return;
        }

        if (task_id >= 0 && task_id < functions.size()) // 获取的方法在方法集中
        {
            sleep(0.1);
            functions[task_id](); // 执行该方法
        }
        else
        {
            cout << "no function" << endl;
            return;
        }
    }
    cout << "进程[" << getpid() << "]" << " 结束工作" << endl;
}

// [pid, fd]
typedef pair<uint32_t, uint32_t> elem;

void sendTask(const vector<elem> &task_vector)
{
	// 随机派发任务,做到负载均衡
    srand((unsigned int)time(nullptr));
    // 派发任务个数为5个
    int cnt = 5;
    while (cnt)
    {
        sleep(1);

        // 随机挑选一个子进程
        int process = rand() % task_vector.size();

        // 随机挑选一个任务
        int task_id = rand() % functions.size();

        // 任务的派发
        write(task_vector[process].second, &task_id, sizeof(task_id));

        cout << "父进程派发任务" << information[task_id] << "给pid[ " << task_vector[process].first << " ]的子进程" << endl;

        cnt--;
    }
}

int main()
{
    // 任务表,记录子进程的pid和对应的fd写端文件
    vector<elem> task_vector;
    loadFunctions();
    int processNum = 5; // 子进程个数
    for (int i = 0; i < processNum; i++)
    {
        // 管道的创建
        int pipefd[2] = {0};
        ssize_t ret = pipe(pipefd);
        if (ret != 0) // 管道创建失败
        {
            cout << "pipe fail" << endl;
            return 1;
        }

        // 子进程的创建
        int id = fork();
        if (id == 0)
        {
            // child,reader
            close(pipefd[1]);
            // 子进程接收任务,执行
            work(pipefd[0]);
            close(pipefd[0]);
            exit(0); // 子进程退出
        }

        // parent,writer
        close(pipefd[0]);
        elem e = {id, pipefd[1]};
        task_vector.push_back(e); // 每循环一次,就记录子进程的pid和fd写端在任务表上
    }
    sleep(1);
    cout << "create all process success!" << endl;
    sendTask(task_vector);    // 派发任务
    
	// 回收子进程
	for (int i = task_vector.size() - 1; i >= 0; i--)
    {
        // 关闭管道的写端,使子进程不再阻塞等待管道的写入,直接退出
        close(task_vector[i].second);
        if (waitpid(task_vector[i].first, nullptr, 0) > 0)
        {
            cout << "等待子进程[ " << task_vector[i].first << " ]成功" << endl;
        }
        sleep(1);
    }
    return 0;
}

代码较长,建议从main函数开始看起,理解父进程派发任务的大致过程然后再理解其中的函数(loadFunctions,work,sendTask),整份代码中的cout只是为了更好的观察代码运行逻辑。
在这里插入图片描述

运行程序,父进程成功地派发了5个任务给子进程,并且任务与进程都是随机选择的,不会使一个进程执行多次任务,最后关闭管道的写端后,子进程成功地退出并且被父进程回收了。对于这份代码,我认为的重点是:关闭管道的所有写端,子进程调用read函数读取管道数据,read会返回0,这里需要进行判断,read返回0说明父进程不再写入,子进程就可以退出了。以及创建管道与子进程时,用task_vector任务表记录子进程的pid和fd写端,任务表在这里的使用是一个重点,使用任务表才能向子进程派发任务以及回收子进程的资源。

在调试上面的模拟代码时,我还遇到了一个难搞的问题,就是最后回收子进程时,为什么要从任务表的最后开始关闭父进程的写端并回收子进程资源?这小小的细节其实折磨了我很久
在这里插入图片描述
当父进程第一次pipe创建管道,fork创建子进程并关闭子进程的写端与自己的读端后,就创建了一个管道与子进程进行通信,暂时将该子进程称为1号子进程,通信的管道称为1号管道。当父进程第二次pipe创建管道,fork创建子进程并关闭子进程的写端与自己的读端后,就能通过2号管道与2号子进程进行通信,这是很容易理解的情况。但是在父进程第二次fork之前,因为需要与1号子进程进行通信,所以有一个1号管道的写端文件,第二次fork创建子进程,子进程就会继承父进程打开的1号管道的写端文件,所以对于1号管道,除了父进程可以向其写入数据,2号子进程也能向其写入数据,因此1号管道文件有两个写端

只有当1号管道的写端文件为0时,表示没有进程向管道写入数据了,1号子进程的read才会返回0,返回0后1号子进程才会退出,最后由父进程回收其资源。如果在回收资源时,从任务表的第一个元素开始回收,即回收1号子进程,由于只有父进程关闭了1号管道的写端文件,还有2号子进程打开了1号管道的写端文件,1号管道的写端文件不为0,所以1号子进程的read不会返回0,1号子进程也就无法退出,回收子进程的waitpid就会阻塞等待 1号子进程的退出,但该子进程永远不会退出,所以waitpid就会一直阻塞,表现在程序上就是程序卡住。

而从任务表的最后开始回收子进程就不会有上述问题,假设现在只有2个管道,先关闭父进程对2号管道的写端,2号管道的写端文件数量为0,2号子进程的read返回0,2号子进程退出,由于2号子进程还打开了一个1号管道的写端文件,所以2号子进程退出时,系统会释放该文件,这样的话只有父进程打开了1号管道的写端文件,下次回收1号子进程时,关闭父进程对1号管道的写端后,1号管道的写端文件数量为0,1号子进程也能成功退出。将2个管道的情况推广,当父进程与n个子进程进行管道通信,从任务表的最后关闭父进程的写端并回收子进程资源的方式依然适用

管道的特点

匿名管道只能用于具有血缘关系的进程间,常用于父子进程间通信
管道的数据传输方向是单向的,这是由内核决定的
管道自带访问控制机制(同步互斥机制)
管道的生命周期是跟随进程的,当最后一个打开管道的进程退出,系统释放进程的资源,管道的资源也随之被释放
管道是面向字节流的,数据的传输以字节为单位

刚才聊的管道其实叫做匿名管道,在shell命令行中也有匿名管道的存在,使用竖线“|”就能调用命令行中的匿名管道。在一个会话中输入sleep 20000 | sleep 10000就调用了匿名管道,在另一个会话中输入ps axj指令过滤出含有sleep的进程信息,观察信息可以发现sleep 20000 | sleep 10000这行命令调用了两个进程,并且它们的父进程是相同的,所以这是两个兄弟进程。与我们测试匿名管道的代码不同,命令行中的管道不是父子进程间通信,而是两个兄弟进程间通信,大概实现就是父进程fork两次创建了两个子进程,接着父进程关闭管道的读端与写端,两个子进程各自关闭读端和写端,使一个进程向管道写入数据,一个进程读取管道的数据。并且命令行中的管道还有一个特点,就是前一个进程的输出是后一个进程的输入,所以系统会将前一个进程的输出重定向到后一个进程的输入。

在这里插入图片描述
匿名管道作为管道的一种,其特征是只能用于具有血缘关系的进程间通信,要使任意进程进行通信,就不能使用匿名管道,而需要使用命名管道。

命名管道

在这里插入图片描述
使用mkfifo函数可以创建一个命名管道。命名管道的本质是磁盘上的一个FIFO文件,文件被存储在磁盘上,就意味着文件有一个绝对路径,其他进程可以通过这个路径找到这个文件,这个路径就像一个标识符,使不同进程可以通过这个标识符找到这个管道文件,以这个管道文件进行通信。虽然命名管道是磁盘上的文件,但该文件的数据不用向磁盘刷新,即磁盘上的数据只是一个符号,用来表示该文件的存在,与匿名管道一样,命名管道实际的读写是在内存中,以内存为载体进行通信的速度明显比以外设为载体进行通信快。

函数有两个参数,pathname是fifo文件所在路径,mode是该文件的权限,关于权限通常用一串八进制数表示。

// serve.cc文件
#include "ipc.h"
using namespace std;

int main()
{
    umask(0);
    if (mkfifo(IPC_PATH, 0600) != 0) // 创建命名管道失败
    {
        cerr << "fifofile fail" << endl;
        return 1;
    }
    
    int fd = open(IPC_PATH, O_RDONLY);
    if (fd == -1)
    {
        cerr << "open fail" << endl;
        return 1;
    }

    char read_buffer[512];
    while (1)
    {
        memset(read_buffer, 0, sizeof(read_buffer));
        ssize_t s = read(fd, read_buffer, sizeof(read_buffer) - 1);
        if (s == 0)
        {
            cout << "服务退出" << endl;
            // 关闭文件的读端
            close(fd);
            // 删除磁盘上的管道文件
            unlink(IPC_PATH);
            break;
        }
        read_buffer[strlen(read_buffer) - 1] = '\0';
        cout << read_buffer << endl;
    }
    return 0;
}

// client.cc文件
#include "ipc.h"
using namespace std;
int main()
{
   
    int fd = open(IPC_PATH, O_WRONLY);
    if (fd == -1)  // 打开文件失败
    {
        cerr << "open fail" << endl;
        return 2;
    }
    char write_buffer[128];
    while (1)
    {
        cout << "请输入信息#";
        fflush(stdout);
        // 每次写入数据清空write_buffer
        memset(write_buffer, 0, sizeof(write_buffer));
        // 从键盘获取数据失败
        if (fgets(write_buffer, sizeof(write_buffer), stdin) == nullptr)
        {
            cerr << "input fail" << endl;
            return 3;
        }
        // 当输入exit表示客户的退出,服务器也停止读取数据
        if (strcmp(write_buffer, "exit\n") == 0)
        {
            break;
        }
        // 向管道写入从键盘上获取的数据
        write(fd, write_buffer, sizeof(write_buffer));
    }
    cout << "客户退出" << endl;
    // 关闭文件的读端,释放管道资源
    close(fd);
    return 0;
}

模拟命名管道的使用:现在有一个客户端,一个服务器,客户向服务器发送请求,服务器需要接收请求。由服务器创建命名管道进行通信,服务器是命名管道的读端,以读的方式打开管道,客户则以写的方式打开管道,当然管道的路径是事先约定好的,双方都知道管道在哪。客户发送消息,当发送消息为"exit"时表示不再发送数据,关闭管道文件,此时的服务器read函数返回0,服务器根据这一条件关闭管道,最终释放管道资源(文件以引用计数的方式进行资源的释放,每次释放文件资源只会将文件的计数-1,不会进行真正的资源释放。当文件的计数为0时,就表示没有进程打开该文件,系统才会进行真正的资源释放,所以只有服务器和客户端的文件都关闭后,管道资源才会被释放,否则只能等到服务器和客户端都退出,管道资源才会释放)。但是命名管道本质是内存中的一个文件,调用unlink删除内存中的fifo文件才会将管道资源彻底的释放。

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

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

相关文章

MODBUS通信系列之数据处理

MODBUS通信专栏有详细文章讲解,这里不再赘述,大家可以自行查看。链接如下: SMART S7-200PLC MODBUS通信_RXXW_Dor的博客-CSDN博客_smart200做modbus通讯MODBUS 是 OSI 模型第 7 层上的应用层报文传输协议,它在连接至不同类型总线或网络的设备之间提供客户机/服务器通信。自…

化工机械基础期末复习题及答案

化工设备机械基础复习题 一 选择题 1、材料的刚度条件是指构件抵抗&#xff08; B &#xff09;的能力。 A&#xff0e;破坏 B&#xff0e;变形 C&#xff0e;稳定性 D&#xff0e;韧性 2、一梁截面上剪力左上右下&#xff0c;弯矩左顺右逆&#xff0c;描述正确的是&#xff08…

上班总结测试报告

出版社智能智造 测试报告 项目名称 出版社智能智造 测试版本 二期版本20221103 级别 用户使用 编写人 罗胜杰 日期 2022.11.15 目 录 1. 测试概述 1.1. 编写目的 1.2. 产品需求介绍 1.3. 参考资料 2. 测试计划执行情况 2.1. 测试范围及策略 2.2. 本…

[附源码]SSM计算机毕业设计基于的花店后台管理系统JAVA

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

【Python百日进阶-WEB开发-冲進Flask】Day181 - Flask简单流程

文章目录一、day01项目环境和结构搭建1.1 新建虚拟环境1.2 安装Flask1.3 配置Python解释器二、后端知识要点2.1 Flask 文档2.2 实例化flask对象2.2.1 新建独立的配置文件settings.py2.2.2 实例化flask对象时加载配置文件2.3 基本路由2.3.1 常用路由及唯一性2.3.2 路由底层调用2…

中央空调系统运行原理以及相关设备介绍

目录前言一、中央空调系统工作原理1-1、工作原理1-2、中央空调系统构成二、室内空调三、制冷机组3-1、概述3-2、原理3-3、蒸发器3-4、冷凝器3-5、压缩机3-6、总结四、冷却塔总结前言 今天也是为了30岁开始养老而奋斗的一天。 一、中央空调系统工作原理 1-1、工作原理 中央空…

FFmpeg入门 - rtmp推流

FFmpeg入门 - 视频播放_音视频开发老马的博客-CSDN博客介绍了怎样用ffmpeg去播放视频. 里面用于打开视频流的avformat_open_input函数除了打开本地视频之外,实际上也能打开rtmp协议的远程视频,实现拉流: ./demo -p 本地视频路径 ​ ./demo -p rtmp://服务器ip/视频流路径 这篇…

JVM垃圾回收总结

常见面试题 如何判断对象是否死亡 简单介绍一下强引用、软引用、弱引用、虚引用 如何判断常量是一个废弃常量 如何判断类是一个无用类 垃圾收集有哪些算法、各自的特点&#xff1f; 常见的垃圾回收器有哪些&#xff1f; 介绍一下CMS&#xff0c;G1收集器&#xff1f; minor gc和…

[附源码]计算机毕业设计JAVA课后作业提交系统关键技术研究与系统实现

[附源码]计算机毕业设计JAVA课后作业提交系统关键技术研究与系统实现 项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&am…

[附源码]计算机毕业设计JAVA课堂点名系统

[附源码]计算机毕业设计JAVA课堂点名系统 项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis M…

【2】Anaconda基本命令以及相关工具:jupyter、numpy、Matplotilb

上一篇请移步【1】Anaconda基本命令以及相关工具&#xff1a;jupyter、numpy、Matplotilb_水w的博客-CSDN博客 目录 3 Numpy数组基础索引&#xff1a;索引和切片 ◼ 基础索引 4 Numpy非常重要的数组合并与拆分操作 ◼ 数组的合并-concatenate、vstack、hstack numpy.vstac…

生产制造管理:供应商管理系统

随着经济全球化和信息技术的快速推进发展&#xff0c;传统的管理模式早已不再适应现代市场竞争与生产制造的需要&#xff0c;以顾客需求为中心的供应链管理显得更为重要。供应链是围绕核心企业&#xff0c;通过对信息流、物流、资金流等关键部分的控制连成一个整体的功能网链结…

期末前端web大作业——我的家乡陕西介绍网页制作源码HTML+CSS+JavaScript

家乡旅游景点网页作业制作 网页代码运用了DIV盒子的使用方法&#xff0c;如盒子的嵌套、浮动、margin、border、background等属性的使用&#xff0c;外部大盒子设定居中&#xff0c;内部左中右布局&#xff0c;下方横向浮动排列&#xff0c;大学学习的前端知识点和布局方式都有…

[附源码]计算机毕业设计JAVA科院垃圾分类系统

[附源码]计算机毕业设计JAVA科院垃圾分类系统 项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybati…

Flutter For Web——一个简单的图片素材网站

一个简单的图片素材网站效果视频登录注册页效果图UI初始化TabBarPageView组合登录账号输入按键处理SharedPreferences封装保存数据取出数据清除缓冲内容搜索栏效果图UI首页效果图UIDio网络请求Dio单例封装构造Dio对象GetPostResponse使用解析Json图片阅览UIDialog下载UI调用浏览…

Spring之IOC 为什么能解耦

1.1 什么是IOC &#xff08;1&#xff09;控制反转&#xff0c;把对象的创建和对象之间的调用过程&#xff0c;都交给Spring进行管理 &#xff08;2&#xff09;使用IOC目的&#xff1a;为了耦合性降低 1.2 IOC的底层原理 &#xff08;1&#xff09;使用的技术&#xff1a;…

完美解决-RuntimeError: CUDA error: device-side assert triggered

网上的解决方案意思是对的&#xff0c;但并没有给出相应的实际解决方法&#xff1a; 问题描述&#xff1a; 当使用ImageFolder方式构建数据集的时候&#xff1a; train_data torchvision.datasets.ImageFolder(train_path, transformtrain_transform)train_loader DataLoad…

学习Git看这一篇就够了

文章目录Git简单介绍官方网址Git是什么版本控制系统的演化Git安装 - Windows版需要熟悉的几个Linux命令Git命令行状态对应目录位置Git命令1. git init2. git status3. git add4. git commit5. git config6. git reset7. git diff练习 - 创建学生管理系统练习提交代码练习修改代…

传感模块:MATEKSYS Optical Flow LIDAR 3901-L0X

传感模块&#xff1a;MATEKSYS Optical Flow & LIDAR 3901-L0X1. 模块介绍2. 规格参数3. 使用方法Step1: 接线方式Step2: 安装方式Step3: 使用范围4. 存在问题4.1 MATEKSYS 3901-L0X 输出协议格式&#xff1f;4.1.1 支持光流计协议(iNav-CXOF)4.1.2 支持光流计激光测距协议…

混合SDN中的安全性问题研究

混合SDN中的安全性问题研究混合SDN中的安全性问题研究1.学习目标2.学习内容3.目前存在的问题4.解决办法1.关于欺骗ARP的讨论2.DDoS攻击探讨5.解决方案现有文献的解决方案6.目前面临的挑战申明&#xff1a; 未经许可&#xff0c;禁止以任何形式转载&#xff0c;若要引用&#xf…