【Linux】IPC 进程间通信(一):管道(匿名管道命名管道)

news2024/11/27 2:36:30

✨                                                  无人扶我青云志,我自踏雪至山巅     🌏 

 📃个人主页:island1314

🔥个人专栏:Linux—登神长阶

⛺️ 欢迎关注:👍点赞 👂🏽留言 😍收藏  💞 💞 💞


1. 初识进程间通信 🚀

1.1 进程间通信的目的

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

1.2 为什么要有进程间通信 ❓

为了实现两个或者多个进程实现数据层面的交互,因为进程独立性的存在,导致进程通信的成本比较高

很多场景下需要多个进程协同工作来完成要求。如下:

  •  这条命令首先使用 cat  读取 log.txt 的内容,然后通过管道 (|) 将输出传递给 grep 命令。grep 用于搜索指定的字符串。
  • grep Hello 这个命令搜索包含 "Hello" 的行。

1.3 进程间通信的方式

管道(通过文件系统通信

  • 匿名管道pipe
  • 命名管道 

System V IPC (聚焦在本地通信

  • System V 消息队列
  • System V 共享内存
  • System V 信号量 

POSIX IPC (让通信可以跨主机

  • 消息队列
  • 共享内存
  • 信号量
  • 互斥量
  • 条件变量
  • 读写锁 

注意:

  •  System V 标准需要重新构建操作系统代码来实现进程通信,比较繁琐。
  • 在 System V 标准出现之前,而「管道通信」是直接复用现有操作系统的代码
  • 现在本地通信已经被网络通信取代,所以进程间通信方式只重点介绍管道通信和共享内存通信

知识补充:

(1)进程间通信的本质:必须让不同的进程看到同一份“资源”(资源:特定形式的内存空间)

(2)这个资源谁提供?一般是操作系统

  • 为什么不是我们两个进程中的一个呢?假设一个进程提供,这个资源属于谁?
  • 这个进程独有,破坏进程独立性,所以要借用第三方空间

(3)我们进程访问这个空间,进行通信,本质就是访问操作系统!

  • 进程代表的就是用户,资源从创建,使用(一般),释放--系统调用接口!

2. 匿名管道 🔍

 2.1 什么是管道

进程可以通过 读/写 的方式打开同一个文件,操作系统会创建两个不同的文件对象 file,但是文件对象 file 中的内核级缓冲区、操作方法集合等并不会额外创建,而是一个文件的文件对象的内核级缓冲区、操作方法集合等通过指针直接指向另一个文件的内核级缓冲区、操作方法集合等。

  • 这样以读方式打开的文件和以写方式打开的文件共用一个 内核级缓冲区

  • 进程通信的前提是不同进程看到同一份共享资源

所以根据上述原理,父子进程可以看到同一份共享资源:被打开文件的内核级缓冲区。父进程向被打开文件的内核级缓冲区写入,子进程从被打开文件的内核级缓冲区读取,这样就实现了进程通信!

  • 这里也将被打开文件的内核级缓冲区称为 管道文件」,而这种由文件系统提供公共资源的进程间通信,就叫做「 管道 

注意:

此外,管道通信只支持单向通信,即只允许父进程传输数据给子进程,或者子进程传输数据给父进程。

  • 当父进程要传输数据给子进程时,就可以只使用以写方式打开的文件的管道文件,关闭以读方式打开的文件,
  • 同样的,子进程只是用以读方式打开的文件的管道文件,关闭掉以写方式打开的文件。
  • 父进程向以写方式打开的文件的管道文件写入,子进程再从以读方式打开的文件的管道文件读取,从而实现管道通信。如果是要子进程向父进程传输数据,同理即可。

管道特点总结:

  • 个进程将同一个文件打开两次,一次以写方式打开,另一次以读方式打开。此时会创建两个struct file,而文件的属性会共用,不会额外创建
  • 如果此时又创建了子进程,子进程会继承父进程的文件描述符表,指向同一个文件,把父子进程都看到的文件,叫管道文件
  • 管道只允许单向通信

  • 管道里的内容不需要刷新到磁盘

2.2 创建匿名管道

匿名管道:没有名字的文件(struct file)

匿名管道用于父子间通信,或者由一个父创建的兄弟进程(必须有“血缘“)之间进行通信

#include <unistd.h>
原型:int pipe(int fd[2]);

功能:创建匿名管道
参数 fd:文件描述符数组,其中fd[0]表示读端, fd[1]表示写端
返回值:成功返回0,失败返回错误代码

使用如下:

  • int main()
    {
        // 1. 创建管道
        int fds[2] = {0};
        int n = pipe(fds); // fds: 输出型参数
        if(n != 0){
            std::cerr << "pipe error" << std::endl;
            return 1;
        }
    
        std::cout << "fds[0]: " << fds[0] << std::endl;
        std::cout << "fds[1]: " << fds[1] << std::endl;
        return 0;
    }
    
    // 运行如下:
    island@VM-8-10-ubuntu:~/code$ ./code
    fds[0]: 3
    fds[1]: 4
  • 输出型参数:文件的描述符数字带出来,让用户使用-->3,4,因为0,1,2分别被stdin,stdout,stderr占用。

2.3 匿名管道通信案例(父子通信)

情况一:管道为空 && 管道正常(read 会阻塞【read 是一个系统调用】)

具体代码演示如下:(子进程写入,父进程读取)

#include <iostream>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <cstdlib>

// 父进程 -- 读取
// 子进程 -- 写入
void write(std::string &info, int cnt)
{
    info += std::to_string(getpid());
    info += ", cnt: ";
    info += std::to_string(cnt);
    info += ')';
}


int main()
{
    // 1. 创建管道
    int fds[2] = {0};
    int n = pipe(fds); // fds: 输出型参数
    if (n != 0)
    {
        std::cerr << "pipe error" << std::endl;
        return 1;
    }

    // 2. 创建子进程
    pid_t id = fork();
    if (id < 0)
    {
        std::cerr << "fork error" << std::endl;
        return 2;
    }
    else if (id == 0)
    {
        // 子进程
        // 3. 关闭不需要的 fd, 关闭 read
        int cnt = 0;
        while (true)
        {
            close(fds[0]);
            std::string message = "(IsLand1314, pid: ";
            write(message, cnt);
            cnt++;
            sleep(2);
        }
        exit(0);
    }
    else
    {
        // 父进程
        // 3. 关闭不需要的 fd, 关闭 write
        close(fds[1]);
        char buffer[1024];
        while(true)
        {
            ssize_t n = ::read(fds[0], buffer, 1024);
            if(n > 0)
            {
                buffer[n] = 0;
                std::cout << "child->father, message: " << buffer << std::endl;
            }
        }

        // 记录退出信息
        pid_t rid = waitpid(id, nullptr, 0);
        std::cout << "father wait chile success" << rid << std::endl;
    }

    return 0;
}

子进程每隔 2 s 向父进程写入数据,并且打印,如下:

从上面可以知道:

  1. 子进程写入的信息是变化的信息
  2. 父进程打印信息的时间间隔和子进程一样,那么子进程没传入信息的时候,父进程处于阻塞 --> (IPC 本质:先让不同的进程,看到同一份资源,可以保护共享资源)
情况二:管道为满 && 管道正常(write 会阻塞【write 是一个系统调用】)

如下对代码做点修改(红框内的代码)

管道有上限,Ubuntu -> 64 KB

如果我们让父进程正常读取,那么结果又是怎样的呢?

运行如下:

当我们到 65536 个字节时,管道已满,父进程读取了管道数据,子进程会继续进行写入,然后进行继续读取,就有点数据溢出的感觉

情况三:管道写端关闭 && 读端继续(读端读到0,表示读到文件结尾)

代码修改如下:

else if (id == 0)
{
    int cnt = 0, total = 0;
    while (true)
    {
        close(fds[0]);
        std::string message = "h";
        // fds[1]
        total += ::write(fds[1], message.c_str(), message.size());
        cnt++;
        std::cout << "total: " << total << std::endl; // 最后写到 65536 个字节

        sleep(2);
        break; // 写端关闭
    }
    exit(0);
}
else
{
    // 父进程
    // 3. 关闭不需要的 fd, 关闭 write
    close(fds[1]);

    char buffer[1024];
    while (true) {
        sleep(1);
        ssize_t n = ::read(fds[0], buffer, 1024);
        if (n > 0) {
            buffer[n] = 0;
            std::cout << "child->father, message: " << buffer << std::endl;
        }
        else if (n == 0) {
            std::cout << "n: " << n << std::endl;
            std::cout << "child quit??? me too " << std::endl;
            break;
        }
        std::cout << std::endl;
    }

    pid_t rid = waitpid(id, nullptr, 0);
    std::cout << "father wait chile success" << rid << std::endl;
}

运行如下:

结论:如果写端关闭,读端读完管道内部数据,再读取就会读取到返回值 0,表示对端关闭,也表示读到文件结尾

情况四:管道写端正常 && 读端关闭(OS 会直接杀掉写入进程)

情况二:

如何杀死呢?

a. OS 会给 目标进程发送信号:13) SIGPIPE

b. 证明如下;

else if (id == 0)
{
    int cnt = 0, total = 0;
    while (true)
    {
        close(fds[0]);
        std::string message = "h";
        // fds[1]
        total += ::write(fds[1], message.c_str(), message.size());
        cnt++;
        std::cout << "total: " << total << std::endl; // 最后写到 65536 个字节
        sleep(2);
    }
    exit(0);
}
else
{
    close(fds[1]);

    char buffer[1024];
    while (true)
    {
        sleep(1);
        ssize_t n = ::read(fds[0], buffer, 1024);
        if (n > 0)
        {
            buffer[n] = 0;
            std::cout << "child->father, message: " << buffer << std::endl;
        }
        else if (n == 0)
        {
            std::cout << "n: " << n << std::endl;
            std::cout << "child quit??? me too " << std::endl;
            break;
        }
        close(fds[0]); // 读端关闭
        break;
        std::cout << std::endl;
    }

    // 记录退出信息
    int status = 0;
    pid_t rid = waitpid(id, &status, 0);
    std::cout << "father wait chile success: " << rid << " exit code: " <<
        ((status << 8) & 0xFF) << ", exit sig: " << (status & 0x7F) << std::endl;
}

运行如下:

小结

🦋 管道读写规则

  • 当没有数据可读时
    • read 调用阻塞,即进程暂停执行,一直阻塞等待
    • read 调用返回-1,errno值为EAGAIN。
  • 当管道满的时候
    • write 调用阻塞,直到有进程读走数据
    • 调用返回-1,errno值为 EAGAIN
  • 如果所有管道写端对应的文件描述符被关闭,则read返回0
  • 如果所有管道读端对应的文件描述符被关闭,则write操作会产生信号

2.4 匿名管道特性

  1. 匿名管道:只用来进行具有血缘关系的进程之间,进行通信,常用于父子进程之间通信

  2. 管道文件的生命周期是随进程的

  3. 管道内部,自带进程之间同步的机制(多执行流执行代码的时候,具有明显的顺序性)

  4. 管道文件在通信的时候,是面向字节流的。(写的次数和读取的次数不是一一匹配的)

  5. 管道的通信模式,是一种特殊的半双工模式,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道

3. 管道通信的场景 - 进程池 💦

  1. 父进程创建多个子进程,并为每个子进程创建一个管道文件,父进程为写端,子进程为读端。父进程给子进程通过管道传输任务,这就是进程池。
  2. 如果父进程没有给子进程传输任务,即管道文件中没有数据,根据进程通信情况1,读端即子进程会阻塞等待父进程传输任务。
  3. 此外父进程还要给子进程平衡任务,不能让某个进程特别繁忙,其他进程没有任务可做。这就是负载均衡。

进程池 -- 源码实现

后面我会写一篇博客专门来讲这个,敬请期待

4. 命名管道 🦌

4.1 介绍

  • 匿名管道应用的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。
  • 如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道。
  • 命名管道是一种特殊类型的文件(命名管道 -- > mkfifo)

4.2 创建命名管道

  • Linux系统中,使用 mkfifo 命令创建有名管道文件,再使用两个进程打开即可
$ mkfifo filename

 

 如上图,当我们在终端1创建了一个命名管道后,往里面写东西,管道不会关闭,在终端2上发现,它的内存大小还是0。

  • 当我们在终端2打印出内容后,管道就自动关闭了。如下图:

  • Linux 系统编程中使用 mkfifo 函数创建一个管道文件,再让两个不相关的进程打开:
int mkfifo(const char *pathname, mode_t mode);

参数

  1. pathname:要创建的命名管道的路径名。
  2. mode:创建命名管道时设置的权限模式,通常以 8 进制表示,比如 0666。

返回值

  • 若成功,返回值为 0;若失败,返回值为 -1,并设置errno来指示错误类型。

功能

  • mkfifo() 函数的作用是在文件系统中创建一个特殊类型的文件,该文件在外观上类似于普通文件,但实际上是一个FIFO,用于进程之间的通信。
  • 这种通信方式是单向的,即数据写入FIFO的一端,可以从另一端读取出来,按照先进先出的顺序。

🎢 案例:

std::string fifoPath = "/tmp/my_named_pipe";  // 命名管道的路径名
mkfifo(fifoPath.c_str(), 0666); // 创建权限为0666的命名管道

注意事项

  • 路径名确保要创建的命名管道路径名合法且没有重复。
  • 权限模式根据实际需求设置合适的权限模式,确保可被需要访问该管道的进程所访问。
  • 错误处理对 mkfifo() 函数的返回值进行适当的错误处理,根据具体的错误原因进行相应的处理和日志记录。

4.3 命名管道通信案例

先写如下的几个文件:

Comm.hpp

#pragma once

#include <iostream>
#include <string>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

const std::string gpipeFile = "./fifo";
const mode_t gmode = 0600;
const int gdefultfd = -1;
const int gsize = 1024;

Client.hpp

#pragma once

#include <iostream>
#include "Comm.hpp" // 让不同代码看到同一份资源

class Client
{
public:
    Client():_fd(gdefultfd)
    {}
    bool OpenPipe()
    {
        _fd = ::open(gpipeFile.c_str(), O_WRONLY);
        if(_fd < 0) 
        {
            std::cerr << "open error" << std::endl;
            return false;
        }
        return true;
    }
    // std::string *: 输出型参数
    // const std::string &: 输入型参数
    // std::string &: 输入输出型参数
    int SendPipe(const std::string &in)
    {
        return ::write(_fd, in.c_str(), in.size());
    }
    void ClosePipe()
    {
        if(_fd>=0)
            ::close(_fd);
    }
    ~Client()
    {}
private:
    int _fd;
};

Server.hpp

#pragma once

#include <iostream>
#include "Comm.hpp"

class Init
{
public:
    Init()
    {
        umask(0);
        int n = ::mkfifo(gpipeFile.c_str(), gmode);
        if (n < 0)
        {
            std::cerr << "mkfifo error" << std::endl;
            return;
        }
        std::cout << "mkfifo success" << std::endl;

        // sleep(10);
    }
    ~Init()
    {
        int n = ::unlink(gpipeFile.c_str());
        if (n < 0)
        {
            std::cerr << "unlink error" << std::endl;
            return;
        }
        std::cout << "unlink success" << std::endl;
    }
};

Init init;


class Server
{
public:
    Server(): _fd(gdefultfd)
    {}

    bool OpenPipe()
    {
        _fd = ::open(gpipeFile.c_str(), O_RDONLY);
        if(_fd < 0)
        {
            std::cerr << "open cerr" << std::endl;
            return false;
        }
        return true;
    }


    // std::string *: 输出型参数
    // const std::string & : 输入型参数
    // std::string &: 输入输出型参数
    int RecvPipe(std::string *out)
    {
        char buffer[gsize];
        ssize_t n = ::read(_fd, buffer, sizeof(buffer) - 1);
        if(n > 0)
        {
            buffer[n] = 0;
            *out = buffer;
        }
        return n;
    }

    void ClosePipe()
    {
        if(_fd>=0)
            ::close(_fd);
    }

    ~Server()
    {}

private:
    int _fd;
};

Client.cc(客户端,写入)

#include "Client.hpp"
#include <iostream>

int main()
{
    Client client;
    client.OpenPipe();

    std::string message;
    while(true)
    {
        std::cout << "Please Entere# ";
        std::getline(std::cin, message);
        client.SendPipe(message);
    }


    client.ClosePipe();
    return 0;    
} 

Server.cc(服务端,读取显示)

#include "Server.hpp"
#include <iostream>

int main()
{
    Server server;
    server.OpenPipe();
    
    std::string message;
    while(true)
    {
        server.RecvPipe(&message);
        std::cout << "client Say# " << message << std::endl;
    }

    server.ClosePipe();
    return 0;    
}

Makefile(通过 make 指令来生成可执行文件)

SERVER=server
CLIENT=client
CC=g++
SERVER_SRC=Server.cc
Client_SRC=Client.cc

.PHONY:all
all:$(SERVER) $(CLIENT)

$(SERVER):$(SERVER_SRC)
	$(CC) -o $@ $^ -std=c++11
$(CLIENT):$(Client_SRC)
	$(CC) -o $@ $^ -std=c++11

.PHONY:clean
clean:
	rm -f $(SERVER) $(CLIENT)
  • .PHONY:all :这行声明 all 是一个伪目标。即使文件系统中存在一个名为 all 的文件,make all 命令也会执行与 all 相关的规则,而不是认为目标已经是最新的。
  • all:server client :这行定义了 all 伪目标的依赖,即 server 和 client。当运行 make all 时,Makefile 会首先尝试构建 server 和 client 目标。

运行结果如下:

命名管道演示1

注意:如果客户端先退出,那么接收端就会进入死循环

命名管道演示 - 死循环

原因:

  • 类似于我们上面说的匿名管道写端关闭,读端继续,读端读到0(文件结尾)
  • 死循环是因为 Server.hpp 文件只处理了 n > 0 的情况,因此我们需要做出一些修改,如下对 Server.cc文件做出的修改

运行结果如下:

命名管道-死循环解决演示

为了让我们代码更加优美,并且解决一些代码重复问题,我们再进行一步完善

// Comm.hpp
#pragma once

#include <iostream>
#include <string>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

const std::string gpipeFile = "./fifo";
const mode_t gmode = 0600;
const int gdefultfd = -1;
const int gsize = 1024;
const int gForRead = O_RDONLY;
const int gForWrite = O_WRONLY;

int OpenPipe(int flag)
{
    int fd = ::open(gpipeFile.c_str(), gForRead);
    if(fd < 0)
    {
        std::cerr << "open cerr" << std::endl;
        return false;
    }
    return fd;
}

void ClosePipeHelper(int fd)
{
    if(fd>=0) ::close(fd);
}


// Client.hpp
#pragma once

#include <iostream>
#include "Comm.hpp" // 让不同代码看到同一份资源

class Client
{
public:
    Client():_fd(gdefultfd)
    {}
    bool OpenPipeForWrite()
    {
        _fd = OpenPipe(gForWrite);
        if(_fd < 0) return false;
        return true;
    }
    int SendPipe(const std::string &in)
    {
        return ::write(_fd, in.c_str(), in.size());
    }
    void ClosePipe()
    {
        ClosePipeHelper(_fd);
    }
    ~Client()
    {}
private:
    int _fd;
};


// Server.hpp
#pragma once
#include <iostream>
#include "Comm.hpp"

class Init
{
public:
    Init()
    {
        umask(0);
        int n = ::mkfifo(gpipeFile.c_str(), gmode);
        if (n < 0)
        {
            std::cerr << "mkfifo error" << std::endl;
            return;
        }
        std::cout << "mkfifo success" << std::endl;

        // sleep(10);
    }
    ~Init()
    {
        int n = ::unlink(gpipeFile.c_str());
        if (n < 0)
        {
            std::cerr << "unlink error" << std::endl;
            return;
        }
        std::cout << "unlink success" << std::endl;
    }
};

Init init;


class Server
{
public:
    Server(): _fd(gdefultfd)
    {}

    bool OpenPipeForRead()
    {
        _fd = OpenPipe(gForRead);
        if(_fd < 0) return false;
        return true;

    }

    int RecvPipe(std::string *out)
    {
        char buffer[gsize];
        ssize_t n = ::read(_fd, buffer, sizeof(buffer) - 1);
        if(n > 0)
        {
            buffer[n] = 0;
            *out = buffer;
        }
        return n;
    }

    void ClosePipe()
    {
        ClosePipeHelper(_fd);
    }

    ~Server()
    {}

private:
    int _fd;
};

深入研究管道,继续对 Server.cc 文件进行修改,看看其第一次打开的时候在哪里阻塞

int main()
{
    Server server;
    std::cout << "pos 1" << std::endl;
    server.OpenPipeForRead();
    std::cout << "pos 2" << std::endl;

    std::string message;
    while (true)
    {
        if (server.RecvPipe(&message) > 0)
        {
            std::cout << "client Say# " << message << std::endl;
        }
        else
        {
            break;
        }
        std::cout << "pos 3" << std::endl;
    }
    std::cout << "client quit, me too!" << std::endl;
    server.ClosePipe();
    return 0;
}

运行如下:

命名管道-阻塞演示

结论:

  • 读端打开文件的时候,写端还没有打开,读端对用的 open 就会阻塞

4.4 匿名管道与命名管道的区别

🎀 匿名管道与命名管道的区别

  1. 匿名管道由 pipe函数 创建并打开。
  2. 命名管道由 mkfifo函数 创建,打开用open。
  3. FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在它们创建与打开的方式不同,一但这些工作完成之后,它们具有相同的语义。

4.5 命名管道的打开规则

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

  1. O_NONBLOCK disable:阻塞直到有相应进程为写而打开该FIFO
  2. O_NONBLOCK enable:立刻返回成功

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

  1. O_NONBLOCK disable:阻塞直到有相应进程为读而打开该FIFO
  2. O_NONBLOCK enable:立刻返回失败,错误码为ENXIO

5.  小结📖

管道是一种用于进程间通信(IPC)的机制,允许一个进程将数据传递给另一个进程。在类Unix操作系统中,管道通常由内核提供,使用简单的读写接口。

管道分为两种类型:无名管道和命名管道

  1. 无名管道主要用于具有亲缘关系的进程(如父子进程),在创建时不需要名称,只能通过文件描述符进行访问
  2. 命名管道(FIFO)则可以在任何进程之间通信,使用文件系统中的路径来标识

管道的特点

  1. 管道是单向的:数据在一个方向上流动,从写端(写入数据的进程)到读端(读取数据的进程)
  2. 在写端,数据会被写入一个缓冲区,读端则从这个缓冲区读取数据
  3. 管道的缓冲区大小有限,因此如果写入的数据超过缓冲区容量,写入进程会被阻塞,直到有空间可用。

管道的优点在于其简单性和高效性,适用于需要实时数据传输的场景。然而,由于其单向特性和有限的缓冲区,复杂的通信需求可能需要其他IPC机制,如消息队列或共享内存。总的来说,管道是一种基础而有效的进程间通信工具。

【*★,°*:.☆( ̄▽ ̄)/$:*.°★* 】那么本篇到此就结束啦,如果我的这篇博客可以给你提供有益的参考和启示,可以三连支持一下 !!

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

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

相关文章

单片机串口接收状态机STM32

单片机串口接收状态机stm32 前言 项目的芯片stm32转国产&#xff0c;国产芯片的串口DMA接收功能测试不通过&#xff0c;所以要由原本很容易配置的串口空闲中断触发DMA接收数据的方式转为串口逐字节接收的状态机接收数据 两种方式各有优劣&#xff0c;不过我的芯片已经主频跑…

信息学科平台系统开发:基于Spring Boot的最佳实践

3系统分析 3.1可行性分析 通过对本基于保密信息学科平台系统实行的目的初步调查和分析&#xff0c;提出可行性方案并对其一一进行论证。我们在这里主要从技术可行性、经济可行性、操作可行性等方面进行分析。 3.1.1技术可行性 本基于保密信息学科平台系统采用Spring Boot框架&a…

探索 ONLYOFFICE 8.2 版本:更高效、更安全的云端办公新体验

引言 在当今这个快节奏的时代&#xff0c;信息技术的发展已经深刻改变了我们的工作方式。从传统的纸质文件到电子文档&#xff0c;再到如今的云端协作&#xff0c;每一步技术进步都代表着效率的飞跃。尤其在后疫情时代&#xff0c;远程办公成为常态&#xff0c;如何保持团队之间…

51c自动驾驶~合集4

我自己的原文哦~ https://blog.51cto.com/whaosoft/12413878 #MCTrack 迈驰&旷视最新MCTrack&#xff1a;KITTI/nuScenes/Waymo三榜单SOTA paper&#xff1a;MCTrack: A Unified 3D Multi-Object Tracking Framework for Autonomous Driving code&#xff1a;https://gi…

STM32HAL-最简单的长、短、多击按键框架(多按键)

概述 本文章使用最简单的写法实现长、短、多击按键框架,非常适合移植各类型单片机,特别是资源少的芯片上。接下来将在stm32单片机上实现,只需占用1个定时器作为时钟扫描按键即可。 一、开发环境 1、硬件平台 STM32F401CEU6 内部Flash : 512Kbytes,SARM …

【论文精读】LPT: Long-tailed prompt tuning for image classification

&#x1f308; 个人主页&#xff1a;十二月的猫-CSDN博客 &#x1f525; 系列专栏&#xff1a; &#x1f3c0;论文精读_十二月的猫的博客-CSDN博客 &#x1f4aa;&#x1f3fb; 十二月的寒冬阻挡不了春天的脚步&#xff0c;十二点的黑夜遮蔽不住黎明的曙光 目录 1. 摘要 2. …

队列的模拟实现

概念&#xff1a; 队列 &#xff1a;只允许在一端进行插入数据操作&#xff0c;在另一端进行删除数据操作的特殊线性表&#xff0c;队列具有先进先出 FIFO(First In First Out) 入队列&#xff1a;进行插入操作的一端称为 队尾&#xff08; Tail/Rear &#xff09; 出队列&a…

Centos安装配置Jenkins

下载安装 注意&#xff1a;推荐的LTS版本对部分插件不适配&#xff0c;直接用最新的版本&#xff0c;jenkins还需要用到git和maven&#xff0c;服务器上已经安装&#xff0c;可查看参考文档[1]、[2]&#xff0c;本次不再演示 访问开始使用 Jenkins 下载jenkins 上传至服务器…

在Python中最小化预测函数的参数

在 Python 中&#xff0c;最小化预测函数的参数通常涉及使用优化算法来调整模型的参数&#xff0c;以减少预测误差。下面介绍几种常见的方法来实现这一目标&#xff0c;主要使用 scipy 和 numpy 库。 1、问题背景 我正在尝试通过解决自己想出的问题来学习Python&#xff0c;我…

统信UOS系统应用开发

包括cpu 、内存 、安全等接口描述。 文章目录 一、内存管理非文件形式的内存动态函数库调用接口二、cpu内置安全飞腾国密加速硬件用户态驱动API说明真随机数真随机数三、cpu多核调度cpu亲和性获取接口用于cpu set集操作的相关宏定义一、内存管理 非文件形式的内存动态函数库调…

postman 获取登录接口中的返回token并设置为环境变量的方法 postman script

postman是一个比较方便的API开发调试工具&#xff0c; 我们在访问API时一般都需要设置一个token来对服务进行认证&#xff0c; 这个token一般都是通过登录接口来获取。 这个postman脚本放到登录接口的sctipt--> post-response里面即可将登陆接口中返回的token值设置到postma…

《华为工作法》读书摘记

无论做什么事情&#xff0c;首先要明确的就是做事的目标。目标是引导行动的关键&#xff0c;也是证明行动所具备的价值的前提&#xff0c;所以目标管理成了企业与个人管理的重要组成部分。 很多时候&#xff0c;勤奋、努力并不意味着就一定能把工作做好&#xff0c;也并不意味…

【大语言模型】ACL2024论文-07 BitDistiller: 释放亚4比特大型语言模型的潜力通过自蒸馏

【大语言模型】ACL2024论文-07 BitDistiller: 释放亚4比特大型语言模型的潜力通过自蒸馏 目录 文章目录 【大语言模型】ACL2024论文-07 BitDistiller: 释放亚4比特大型语言模型的潜力通过自蒸馏目录摘要研究背景问题与挑战如何解决创新点算法模型实验效果代码推荐阅读指数&…

Tomcat 和 Docker部署Java项目的区别

在 Java 项目部署中&#xff0c;Tomcat 和 Docker 是两种常见的选择。虽然它们都可以用来运行 Java 应用&#xff0c;但它们在定位、部署方式、依赖环境、资源隔离、扩展性和适用场景等方面有显著区别。 1. 功能定位 1.1 Tomcat Apache Tomcat 是一种轻量级的 Java 应用服务器…

基于SSM的学生选课系统+LW参考示例

系列文章目录 1.基于SSM的洗衣房管理系统原生微信小程序LW参考示例 2.基于SpringBoot的宠物摄影网站管理系统LW参考示例 3.基于SpringBootVue的企业人事管理系统LW参考示例 4.基于SSM的高校实验室管理系统LW参考示例 5.基于SpringBoot的二手数码回收系统原生微信小程序LW参考示…

Java I/O流详解

文章目录 I/O流概念I/O流的分类字节流&#xff08;Byte Streams&#xff09;字节字节流概述方法主要类和继承关系示例代码字节流读取文件 字符流字符流概述子类Reader1.FileReader&#xff1a;2.CharArrayReader&#xff1a;3.StringReader&#xff1a;4.InputStreamReader&…

基于Multisim数字频率计频率范围0-9999HZ电路(含仿真和报告)

【全套资料.zip】数字频率计仿真电路设计Multisim仿真设计数字电子技术 文章目录 功能一、Multisim仿真源文件二、原理文档报告资料下载【Multisim仿真报告讲解视频.zip】 功能 1.采用纯数字电路&#xff0c;非单片机。 2.频率计测量的频率范围0-9999HZ。 3.使用数码管进行频…

Python画笔案例-095 绘制鼠标画笔

1、绘制 鼠标画笔 通过 python 的turtle 库绘制 鼠标画笔,如下图: 2、实现代码 绘制 鼠标画笔,以下为实现代码: """鼠标画笔.py本程序可以用鼠标指针在屏幕上画画儿。 """ import turtlescreen = turtle.getscreen() screen.setup(

【温酒笔记】SPI

1. SPI基础 物理层 片选线 &#xff1a;选中拉低SCK: 时钟线MOSI:主出从入MISO:主入从出 协议层 CPOL:时钟极性&#xff1a;空闲电平高低 CPHA:时钟相位&#xff1a;第一个还是第二个边沿采样 2. 示例SPI-W25Q16 (见模组分类下文章)

mac电脑设置crontab定时任务,以及遇到的问题解决办法

crontab常用命令 crontab -u user&#xff1a;用来设定某个用户的crontab服务&#xff1b; crontab file&#xff1a;file是命令文件的名字,表示将file做为crontab的任务列表文件并载入crontab。如果在命令行中没有指定这个文件&#xff0c;crontab命令将接受标准输入&#xf…