Linux知识点 -- 进程间通信(一)

news2024/10/3 4:37:24

Linux知识点 – 进程间通信(一)

文章目录

  • Linux知识点 -- 进程间通信(一)
  • 一、了解进程间通信
    • 1.进程间通信的必要性
    • 2.进程间通信的技术背景
    • 3.进程间通信的本质理解
    • 4.进程间通信的标准
  • 二、匿名管道
    • 1.匿名管道通信的原理
    • 2.匿名管道的使用
    • 3.管道的特点
    • 4.进程池项目
  • 三、命名管道
    • 1.命名管道的原理
    • 2.命名管道的使用


一、了解进程间通信

1.进程间通信的必要性

单进程,无法使用并发能力,更加无法实现多进程协同,如传输数据、同步执行流、消息通知等;

2.进程间通信的技术背景

(1)进程是具有独立性的;虚拟地址空间 + 页表保证进程运行的独立性(进程内核数据结构 + 进程的代码和数据);
(2)通信成本会比较高;

3.进程间通信的本质理解

(1)进程间通信的前提,首先是要让不同的进程看到同一块内存空间(特定的结构组织的);
(2)所谓的看到同一块空间,这块空间应该隶属于哪一个进程呢?应该不能隶属于任何进程,而应该强调共享;

4.进程间通信的标准

  • Linux原生能提供的:管道
    匿名管道;
    命名管道;
  • SystemV IPC:多进程,用于单机通信
    共享内存;
    消息队列;
    信号量;
  • POSIX IPC:多线程,用于网络通信

二、匿名管道

1.匿名管道通信的原理

管道通信是进程之间通过管道进行通信,管道就是两个进程能够共享的一块内存空间;
在这里插入图片描述

  • 管道的本质就是一个文件:
    (1)父进程分别以读写的方式打开一个文件;
    在这里插入图片描述
    完成后,父进程中分别由两个文件描述符对应的文件指针指向同一个文件,一个用来读,一个用来写,这个文件就是管道;
    (2)fork创建子进程;
    在这里插入图片描述
    在父进程fork出子进程后,子进程会拷贝父进程PCB的信息,因此在子进程的文件序列中,也会有同样的文件描述符对应的文件指针指向父进程创建的管道;
    (3)双方进程各关闭自己不需要的文件描述符;
    在这里插入图片描述
    在确定好父子进程的读写后,比如父进程写,子进程读,那就关闭父进程读和子进程写对应的fd,到此,一个管道就形成了;

  • 注:
    (1)创建子进程时,只拷贝和进程相关的数据,PCB,文件相关的不会拷贝,拷贝完成后,父子进程指向的文件是一样的;
    (2)进程间通信都是基于内存的,效率高;
    (3)我们在命令行使用的 | 就是管道;
    在这里插入图片描述
    三个sleep都是进程, 是兄弟进程,一个进程处理完数据,通过管道交给下一个进程;

2.匿名管道的使用

pipe函数:创建管道,相当于完成了父进程以读写方式打开一个文件;
在这里插入图片描述

  • 参数:
    pipefd[2]输出型参数,期望通过这个参数,得到被打开文件的fd(读和写各一个fd),pipefd[0]是读端,pipefd[1]是写端
    返回值:在这里插入图片描述
    成功返回0,失败返回-1;

  • makefile:

pipe-use:pipe-use.cpp
	g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
	rm -f pipe-use
  • pipe-use.cpp:
#include<iostream>
#include<string>
#include<cstdio>         //在c++中更好兼容c语言的头文件
#include<cstring>
#include<assert.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>

using namespace std;

int main()
{
    //1.创建管道
    int pipefd[2] = {0};
    int n = pipe(pipefd);
    assert(n != -1);
    (void)n;        //debug模式下assert是有效的,而release模式下assert就无效了
                    //(void)n就是将n使用以下,避免release模式下报错

//条件编译,打印出pipefd中的内容
//如果想要执行这段代码,在g++编译选项中加上-DEGUB即可
#ifdef DEBUG
    cout << "pipefd[0]" << pipefd[0] << endl;
    cout << "pipefd[1]" << pipefd[1] << endl;
#endif

    //2.创建子进程
    pid_t id = fork();
    assert(id != -1);
    if(id == 0)
    {
        //子进程 - 读
        //3.构建单向通信的管道,父进程写入,子进程读取
        //3.1 关闭子进程不需要的fd
        close(pipefd[1]);
        char buffer[1024];
        while(true)
        {
            ssize_t s = read(pipefd[0], buffer, sizeof(buffer) - 1);//从管道中读取数据
            if(s > 0)
            {
                buffer[s] = 0;
                cout << "child get a message[" << getpid() << "]father# " << buffer << endl;
            }
        }
        close(pipefd[0]);//关闭子进程读取fd。可以不关闭,因为子进程退出时会关闭其所有fd

        exit(0);
    }

    //父进程
    //3.构建单向通信的管道,父进程写入,子进程读取
    //3.1关闭父进程不需要的fd
    close(pipefd[0]);
    string message = "我是父进程,我正在给你发消息";
    int count = 0;
    char send_buffer[1024];
    while(true)
    {
        //3.2构建一个变化的字符串
        //sprintf是向字符串中格式化显示内容
        snprintf(send_buffer, sizeof(send_buffer), "%s[%d] : %d",
        message.c_str(), getpid(), count++);
        //3.3写入
        write(pipefd[1], send_buffer, sizeof(send_buffer));
        //3.4sleep
        sleep(1);
    }    

    pid_t ret = waitpid(id, nullptr, 0);
    assert(ret < 0);
    (void)ret;
    
    close(pipefd[1]);

    return 0;
}
  • 注意:
    (1)条件编译
    在这里插入图片描述
    如果想要执行这段代码,在g++编译选项中加上-DEGUB即可:
    在这里插入图片描述
    (2)snprintf
    在这里插入图片描述
    安全的向字符串中格式化显示内容;
    在这里插入图片描述
    (3)cstdio & cstring
    在这里插入图片描述
    这两个头文件是为了更好兼容c语言;
    (4)定义全局buffer能否用来进程间通信?
    不能,因为有写时拷贝的存在,父子进程间一定要保持数据的独立性;

3.管道的特点

  • (1)管道是用来用来具有血缘关系的进程间进行进程间通信的;
  • (2)管道具有让进程间协同的作用,提供了访问控制;
    父进程每1s写入一次数据,子进程也是没1s读取一次,但是我们只在父进程发送时设置了1s写入,子进程并没有设置,这是管道的访问控制;
    如果让父进程一直写入,子进程一段时间读取一次:
    在这里插入图片描述

在这里插入图片描述
运行结果:
在这里插入图片描述
我们可以看到,在父进程将管道写满后,就阻塞在这里了,等待子进程的读取;
在这里插入图片描述
当子进程读取了一定的数据后,父进程才能继续写入;
总结:
a. 写快,读慢,写满管道后就不能再写了;
b. 写慢,读快,管道没有数据的时候,读必须等待;
c. 写关,读就会返回0,标识读到了文件的结尾;
d. 读关,写继续写,OS会终止进程;

  • (3)管道提供的是面向流式的通信服务 – 面向字节流 – 协议
    管道读文件时,不是一次读一条,而是一次读一批;
  • (4)管道是基于文件的,文件的生命周期是随进程的额,管道的生命周期就是随进程的
    让父进程在5s后停止写入,观察子进程:
    在这里插入图片描述
    在这里插入图片描述
    运行结果:
    在这里插入图片描述
    写入的一方,fd没有关闭,如果有数据,就读,没有数据就等;
    写入的一方,fd关闭,读取的乙方,read就会返回0,表示读到了文件的结尾;
  • (5)管道是单向通信的,就是半双工通信的一种特殊情况
    半双工:不能同时读写,只能一方读,一方写;

4.进程池项目

父进程创建四个子进程,使用四个管道进行进程间通信,为子进程派发任务,单机版的负载均衡;

  • makefile:
process-pool:process-pool.cpp
	g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
	rm -f process-pool
  • process-pool.cpp
#include<iostream>
#include<unistd.h>
#include<sys/wait.h>
#include<sys/types.h>
#include<cassert>
#include<vector>
#include<cstdlib>
#include<ctime>
#include"Task.hpp"

#define PROCESS_NUM 5  //子进程数量

using namespace std;

int waitCommand(int waitFd, bool &quit) //如果对方不发,我们就阻塞
{
    uint32_t command = 0;
    ssize_t s = read(waitFd, &command, sizeof(command));
    if (s == 0)
    {
        quit = true;
        return -1;
    }
    assert(s == sizeof(uint32_t));
    return command;
}

void sendAndWakeup(pid_t who, int fd, uint32_t command)
{
    write(fd, &command, sizeof(command));
    cout << "main process: call process " << who << " execute " << desc[command] << " through " << fd << endl;
}

int main()
{
    //加载方法
    load();
    //保存管道信息
    vector<pair<pid_t, int>> slots; //pid : pipefd
    //先创建多个进程
    for(int i = 0; i < PROCESS_NUM; i++)
    {
        //创建管道
        int pipefd[2] = {0};
        int n = pipe(pipefd);
        assert(n != -1);
        (void)n;

        pid_t id = fork();
        assert(id != -1);

        if(id == 0)
        {
            //child
            //子进程读取
            close(pipefd[1]);
            while(true)
            {
                //等命令
                bool quit = false;//进程退出判断
                int command = waitCommand(pipefd[0], quit);//如果对方不发,我们就阻塞
                if(quit)//如果父进程停止写入,子进程也停止
                {
                    break;
                }
                //执行对应的命令
                if(command >= 0 && command < handlerSize())
                {
                    callbacks[command];
                }
                else
                {
                    cout << "非法command" << command << endl;
                }
            }
            exit(1);
        }
        // father
        // 父进程写入
        close(pipefd[0]);
        slots.push_back(make_pair(id, pipefd[1])); // 保存管道信息
    }
    // 父进程派发任务(均衡的派发给每一个子进程)
    srand((unsigned long)time(nullptr) ^ getpid() ^ 23323123123L); // 让数据源更随机
    while (true)
    {
        int slect;
        int command;//任务编号
        cout << "##########################################" << endl;
        cout << "##   1.show functions  2.send command   ##" << endl;
        cout << "##########################################" << endl;
        cout << "Please Slect> ";
        cin >> slect;
        if (slect == 1)
        {
            showHandler();
        }
        else if (slect == 2)
        {
            cout << "Enter Your Command> ";
            // 选择任务
            cin >> command;
            // 选择进程
            int choice = rand() % slots.size();
            // 把任务发送给指定的进程
            sendAndWakeup(slots[choice].first, slots[choice].second, command);
        }
        else
        {
        }

        //关闭fd,所有的子进程都会退出
        for(const auto &slot : slots)
        {
            close(slot.second);
        }

        //回收所有的子进程信息
        for(const auto &slot : slots)
        {
            waitpid(slot.first, nullptr, 0);
        }
    }

    return 0;
}
  • Task.hpp
#include<iostream>
#include<string>
#include<vector>
#include<unistd.h>
#include<functional>

typedef std::function<void()> func; //定义函数类型,实现函数回调

std::vector<func> callbacks;//存储回调函数
std::unordered_map<int, std::string> desc;

void readMySQL()
{
    std::cout << "process[" << getpid() << "]执行访问数据库的任务" << std::endl;
}


void executeUrl()
{
    std::cout << "process[" << getpid() << "]执行解析Url" << std::endl;
}

void cal()
{
    std::cout << "process[" << getpid() << "]执行加密任务" << std::endl;
}


void save()
{
    std::cout << "process[" << getpid() << "]执行数据持久化任务" << std::endl;
}

//加载方法和对应的描述
void load()
{
    desc.insert(callbacks.size(), "readMySQL: 读取数据库");
    callbacks.push_back(readMySQL);

    desc.insert(callbacks.size(), "executeUrl: 进行url解析");
    callbacks.push_back(executeUrl);

    desc.insert(callbacks.size(), "cal: 进行加密计算");
    callbacks.push_back(cal);

    desc.insert(callbacks.size(), "save: 进行文件保存");
    callbacks.push_back(save);

}


void showHandler()
{
    for(const auto &iter : desc)
    {
        std::cout << iter.first << "\t" << iter.second << std::endl;
    }
}

int handlerSize()
{
    return callbacks.size();
}

运行结果:
在这里插入图片描述

三、命名管道

1.命名管道的原理

匿名管道是基于创建子进程的进程信息拷贝来通信的,只能用于有血缘关系的进程间通信;
而命名管道可以实现毫不相关的进程之间的通信;
在这里插入图片描述
不同的进程打开同一个文件,操作系统会检测文件路径,不会再将文件内容加载到内存中,而是将进程指向同一个文件结构体;
这就是命名管道,这是一种内存级文件,但是在磁盘中构建了一个文件名,在进程访问的时候访问同一路径下的文件名;
命名管道只是在磁盘中建立了一个符号,只是为了通信双方看到同一份资源,其数据都是内存级的,不会向磁盘中写入数据;

  • 实验:
    在这里插入图片描述
    mkfifo在指定路径下创建一个命名管道文件;
    在这里插入图片描述
    创建好了一个管道文件,p就是管道文件;
    然后让两个进程通过命名管道进行通信:
    在这里插入图片描述
    一个进程将打印的字符重定向到管道文件中,这时,这个进程就阻塞了,这是在等待其他进程从管道读取数据;
    在这里插入图片描述
    另一个进程从管道文件中读取数据并打印,写入的进程就可以推出阻塞了;
    在这里插入图片描述
    不断地写入和读取;

2.命名管道的使用

mkfifo:使用系统接口创建命名管道:
在这里插入图片描述
参数:
pathname:指定路径;
mode:指定管道文件的权限;
返回值:成功返回0;失败返回-1;

  • makefile
.PHONY:all
all:mutiServer client

mutiServer:mutiServer.cpp
	g++ -o $@ $^ -std=c++11 
client:client.cpp
	g++ -o $@ $^ -std=c++11 

.PHONY:clean
claen:
	rm -f mutiServer client
  • comm.hpp
#ifndef _COMM_H_
#define _COMM_H_

#include<iostream>
#include<string>
#include<cstdio>
#include<cstring>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<sys/wait.h>
#include "Log.hpp"

using namespace std;

#define MODE 0666 //管道文件的权限
#define SIZE 128

string ipcPath = "./fifo.ipc";//管道文件的路径

#endif
  • Log.hpp
#ifndef _LOG_H_
#define _LOG_H_

#include<iostream>
#include<ctime>

#define DeBug   0
#define Notice  1
#define Waring  2
#define Error   3

const std::string msg[] = {
    "DeBug",
    "Notice",
    "Waring",
    "Error"
};

std::ostream &Log(std::string message, int level)
{
    std::cout << " | " << (unsigned)time(nullptr) << " | " << msg[level] << " | " << message;
    return std::cout;
}
#endif

这是打印日志的头文件;

  • mutiServer.cpp
#include "comm.hpp"

int main()
{
    // 1.创建管道文件
    if (mkfifo(ipcPath.c_str(), MODE) < 0)
    {
        perror("mkfifo");
        exit(1);
    }

    Log("创建管道文件成功", DeBug) << "step 1" << endl;

    // 2.正常的文件操作
    int fd = open(ipcPath.c_str(), O_RDONLY); // 服务端读取信息
    if (fd < 0)
    {
        perror("open");
        exit(2);
    }
    Log("打开管道文件成功", DeBug) << "step 2" << endl;

    // 3.编写正常的通信代码
    char buffer[SIZE];
    while (true)
    {
        memset(buffer, '\0', sizeof(buffer));
        ssize_t s = read(fd, buffer, sizeof(buffer));
        if (s > 0)
        {
            cout << '[' << getpid() << "] client say: " << buffer << endl;
        }
        else if (s == 0)
        {
            cerr << '[' << getpid() << "] read end of file, client quit, server quit" << endl;
            break;
        }
        else
        {
            perror("read");
            break;
        }
    }
    

    // 4.关闭文件
    close(fd); // 关闭管道文件
    Log("关闭管道文件成功", DeBug) << "step 3" << endl;

    unlink(ipcPath.c_str()); // 删除管道文件
    Log("删除管道文件成功", DeBug) << "step 4" << endl;

    return 0;
}

在这里插入图片描述
这条代码执行后会创建命名管道文件;
在这里插入图片描述
ulink是删除文件的系统调用接口;
在这里插入图片描述
在关闭管道文件后直接删除;

  • client.cpp
#include "comm.hpp"

int main()
{
    //客户端不需要创建管道了
    //1.打开管道文件
    int fd = open(ipcPath.c_str(), O_WRONLY);
    if(fd < 0)
    {
        perror("open");
        exit(1);
    }

    //2.通信操作
    string buffer;
    while(true)
    {
        cout << "Please Enter Command >";
        getline(cin, buffer);
        write(fd, buffer.c_str(), buffer.size());
    }


    //3.关闭管道文件
    close(fd);

    return 0;
}

客户端就不要创建管道文件,直接获取,然后打开,向管道中写信息;

  • 运行结果:
    在这里插入图片描述
    在服务端运行时,创建好管道文件,然后服务端进程阻塞,等待客户端写入命令;
    在这里插入图片描述
    当客户端进程创建后,双方都打开管道文件;
    在这里插入图片描述
    客户端发送指令,服务端接受指令;
    在这里插入图片描述
    当客户端退出进程后,服务端读到了0,也退出进程,关闭并删除管道文件;

如果在服务端创建多个子进程来处理客户端请求:

  • mutiServer.cpp
#include "comm.hpp"

void getMessage(int fd)
{
    char buffer[SIZE];
    while (true)
    {
        memset(buffer, '\0', sizeof(buffer));
        ssize_t s = read(fd, buffer, sizeof(buffer));
        if (s > 0)
        {
            cout << '[' << getpid() << "] client say: " << buffer << endl;
        }
        else if (s == 0)
        {
            cerr << '[' << getpid() << "] read end of file, client quit, server quit" << endl;
            break;
        }
        else
        {
            perror("read");
            break;
        }
    }

}

int main()
{
    // 1.创建管道文件
    if (mkfifo(ipcPath.c_str(), MODE) < 0)
    {
        perror("mkfifo");
        exit(1);
    }

    Log("创建管道文件成功", DeBug) << "step 1" << endl;

    // 2.正常的文件操作
    int fd = open(ipcPath.c_str(), O_RDONLY); // 服务端读取信息
    if (fd < 0)
    {
        perror("open");
        exit(2);
    }
    Log("打开管道文件成功", DeBug) << "step 2" << endl;

    // 3.编写正常的通信代码
    int pnums = 4;
    for(int i = 0; i < pnums; i++)
    {
        pid_t id = fork();
        if(id == 0)
        {
            getMessage(fd);
            exit(1);
        }
    }

    for(int i = 0; i < pnums; i++)
    {
        pid_t ret = waitpid(-1, nullptr, 0);
    }
    
    // 4.关闭文件
    close(fd); // 关闭管道文件
    Log("关闭管道文件成功", DeBug) << "step 3" << endl;

    unlink(ipcPath.c_str()); // 删除管道文件
    Log("删除管道文件成功", DeBug) << "step 4" << endl;

    return 0;
}

在这里插入图片描述
在这里插入图片描述

  • 运行结果:
    在这里插入图片描述
    可以看到,每次执行客户端任务的子进程都是随机的,多进程竞争式获取数据;

注:.hpp文件与.h文件的区别
hpp,其实质就是将.cpp的实现代码混入.h头文件当中,定义与实现都包含在同一文件,则该类的调用者只需要include该hpp文件即可,无需再将cpp加入到project中进行编译。而实现代码将直接编译到调用者的obj文件中,不再生成单独的obj,采用hpp将大幅度减少调用 project中的cpp文件数与编译次数,也不用再发布烦人的lib与dll,因此非常适合用来编写公用的开源库。

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

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

相关文章

Docker网络模型详解

目录 一、Docker网络基础 1.1、端口映射 1.2、端口暴露 1.3、容器互联 二、Docker网络模式 2.1、Host模式 2.2、container模式 2.3、none模式 2.4、bridge模式 2.5、Overlay模式 网络是激活Docker体系的唯一途径&#xff0c;如果Docker没有比较出色的容器网络&#xff0…

SpringBoot项目-个人博客系统的实现【下】

10.实现强制要求登陆 当用户访问 博客列表页和 博客详情页时, 如果用户当前尚未登陆, 就自动跳转到登陆页面 1.添加拦截器 public class LoginInterceptor implements HandlerInterceptor {Overridepublic boolean preHandle(HttpServletRequest request, HttpServletRespon…

程序员私活渠道揭,探索项目接单秘

对于程序员来说&#xff0c;接私活是很正常的事情&#xff0c;在工作闲暇时间利用业余时间来赚点零花钱还是非常不错的&#xff0c;但是如果能一边接私活一边提高自己的水平那就更好了。 这里给大家的建议就是&#xff0c;可以接一些稍微有难度但是在自己能力范围的项目&#x…

自动化测试:封装入口文件

1自动生成测试报告&#xff0c;start_dir是要找入口文件的相对目录。测试用例有规律的话&#xff0c;pattern可以批量执行&#xff08;通过相同的开头*&#xff09; start_dir 是要执行的测试文件所在的目录&#xff0c;pattern可以理解为具体的执行测试的文件 2所有的底层操…

C++stack_queue

stack_queue 容器适配器stack详解栈适配器栈模拟实现 队列详解队列适配器queue模拟实现 容器适配器 除了顺序容器外&#xff0c;标准库还定义了三个顺序容器适配器:stack(栈),queue(队列),priority_queue(优先队列)。适配器是标准库中的一个通用概念。容器&#xff0c;迭代器和…

Android Jetpack

Jetpack 是一个由多个库组成的套件&#xff0c;可帮助开发者遵循最佳实践、减少样板代码并编写可在各种 Android 版本和设备中一致运行的代码&#xff0c;让开发者可将精力集中于真正重要的编码工作。 1.基础组件 &#xff08;1&#xff09;AppCompat&#xff1a;使得支持较低…

了解政策法规和行业规范,可以帮助写作人员更好地理解公文要求和标准

了解政策法规和行业规范&#xff0c;可以帮助写作人员更好地理解公文要求和标准。 政策法规和行业规范是公文写作的重要参考依据&#xff0c;包括相关法律法规、行业标准和制度规定等。了解这些规范和标准可以帮助写作人员更好地掌握公文的表达要求和规范要求&#xff0c;以确保…

跨境选品怎么选?建议独立站卖家收下这份利基产品查找攻略!

跨境电商平台现在可谓是火热发展中&#xff0c;独立站出海风口&#xff0c;其实选择的机会还真不少&#xff0c;相比国内电商的发展势头&#xff0c;看得出来&#xff0c;未来跨境电商的大门&#xff0c;对你而言&#xff0c;敞开着。选品这事儿&#xff0c;就像你上战场前挑选…

linux_驱动_iic总线获取si7006温湿度

应用层si7006.c #include<stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <stdlib.h> #include <string.h> #include <sys/ioctl.h> #include <arpa/inet.h>…

学生信息管理系统springboot学校学籍专业数据java jsp源代码mysql

本项目为前几天收费帮学妹做的一个项目&#xff0c;Java EE JSP项目&#xff0c;在工作环境中基本使用不到&#xff0c;但是很多学校把这个当作编程入门的项目来做&#xff0c;故分享出本项目供初学者参考。 一、项目描述 学生信息管理系统springboot 系统3权限&#xff1a;超…

C语言 函数指针详解

一、函数指针 1.1、概念 函数指针&#xff1a;首先它是一个指针&#xff0c;一个指向函数的指针&#xff0c;在内存空间中存放的是函数的地址&#xff1b; 示例&#xff1a; int Add(int x&#xff0c;int y) {return xy;} int main() {printf("%p\n",&Add);…

Linux tcpdump 命令详解

简介 用简单的话来定义tcpdump&#xff0c;就是&#xff1a;dump the traffic on a network&#xff0c;根据使用者的定义对网络上的数据包进行截获的包分析工具。 tcpdump可以将网络中传送的数据包的“头”完全截获下来提供分析。它支持针对网络层、协议、主机、网络或端口的…

MPLS(下)

LDP --- 标签分发协议 --- 主要应用在MPLS的控制层面 MPLS控制层面需要完成的工作主要就是分配标签和传递标签。分配标签的前提是本地路由表中得先存在标签&#xff0c;传递标签的前提也是得先具备路由基础。所以&#xff0c;LDP想要正常工作&#xff0c;则需要IGP作为基础。 …

Docker实战-操作Docker容器实战(一)

导语   在之前的分享中&#xff0c;我们介绍了关于如何去操作Docker镜像&#xff0c;下面我们来看看如何去操作容器。 简单来讲&#xff0c;容器是镜像运行的一个实例&#xff0c;与镜像不同的是镜像只能作为一个静态文件进行读取&#xff0c;而容器是可以在运行时进行写入操…

嵌入式的日常工作内容是什么?

1、看器件文档 2、找供应商要资料 3、打电话或微信联系供应商了解技术或器件细节 4、忍受门外汉领导连环夺命吹&#xff0c;因为他们不懂技术&#xff0c;只会问进度 5、写技术文档 6、跟硬件工程师联合测试&#xff0c;查看电路板未工作状态各种问题。有时还要自己手动DI…

tomcat通用回显

​Tomcat架构简析 tomcat的架构图 Server:整个tomcat启动的时候只有一个server Service:一个server中包含了多个service,表示服务 **Container:**容器,可以看作是一个servlet容器,包含一些Engine,Host,Context,Wraper等,访问的路径什么的就存放在这里 Engine -- 引擎 Host …

搞个个人博客,纯学习想找个纯html模板咋就这难

以前做毕业设计的时候老想找一些不掺杂后端代码的前端模板。 可是下载下来&#xff0c;不是php就是python后台的。看又看不懂&#xff0c;想换语言就必须先把里面的后台代码拿掉。 就很像买了个精装的二手房&#xff0c;白白多花了砸墙钱。 就比如&#xff0c;想做个带菜单的…

编写SPI_Master驱动程序_新方法

编写SPI_Master驱动程序_新方法 文章目录 编写SPI_Master驱动程序_新方法一. SPI驱动框架1.1 总体框架1.2 怎么编写SPI_Master驱动1.2.1 编写设备树1.2.2 编写驱动程序 二、 编写程序2.1 数据传输流程2.2 写代码 致谢 参考资料&#xff1a; 内核头文件&#xff1a;include\lin…

vsphere6.5 创建数据中心、集群和添加主机

1、新建数据中心&#xff0c;在入门页面选择创建数据中心&#xff0c;名称可以自定义。 2、创建完成数据中心后就可以添加主机和创建集群了。 3、新建一个集群&#xff0c;并打开DRS和HA功能&#xff0c;这两个功能的一些其他选项可以在创建完成后进一步设置&#xff0c;关于EV…

我能“C“——扫雷游戏

一.前言&#xff1a; 扫雷游戏&#xff0c;一款经典的游戏&#xff0c;没玩过的话也可以试着玩一玩&#xff0c;这样对写扫雷游戏这个小游戏的化是会有一个很好的思路的。那么本片博客就来介绍如何实现扫雷游戏的具体步骤。扫雷游戏链接&#x1f449; 扫雷游戏网页版 - Minesw…