【Linux】进程间通信

进程间通信介绍

++问题1:进程间通信是什么?++

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

++问题2:进程间通信的原因?++

  1. 发送基本数据
  2. 发生命令,用一个进程控制另外一个进程
  3. 实现某种协同工作
  4. 通知事件
  5. ...

进程间通信的目的

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

进程之间是相互独立的,而进程间通信是有成本的。

++问题3:如何进行进程间通信?++

  1. 进程间通信的本质:必须让不同进程看到用一份"资源"
  2. "资源"是指特定形式的内存空间
  3. 这个"资源"一般是由操作系统提供,而不是其中一个进程(破坏进程的独立性)提供。
  4. 进程访问这块空间进程通信,本质就是访问操作系统。进程代表的就是用户,"资源"从创建------使用------释放,都是使用系统调用接口。

一般操作系统,会有一个独立的通信模块,隶属于文件系统,在系统中称为IPC通信模块。进程间通信是

进程间通信发展

  • 管道
  • System V进程间通信
  • POSIX进程间通信

进程间通信分类

管道:

  • 匿名管道pipe
  • 命名管道

System V IPC

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

POSIX IPC

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

基于文件级别通信的方式------管道

管道是Unix中最古老的进程间通信的形式,将从一个进程连接到另一个进程的一个数据流称为一个管道。

管道原理

进程通信的本质前提是先让不同的进程看到同一份资源,即管道就是文件。在strcut file结构体中存在引用计数,父子进程存在相同的指针指向同一个文件时,引用计数会增加。

当父进程以读或者写的方式,打开一个文件的时候,子进程也会以同样的方式,以读或者写方式打开一个文件,此时就无法进行父进程写入,子进程写入的进程读取,所以需要通过下面的方式来进行通信:

站在文件的角度:

在内核角度分析:

这种基于文件级别通信的方式只能进行单向通信,也就是管道通信的原理。

问题:如果需要使用管道进行双向通信------>可以使用多个管道。

【注意】如果两个进程之间没有任何关系,是不能通过管道的方式进行通信的,也就是说只有血缘关系之间可以进行通信的,常用于父子进程之间。

上面没有设置名字的管道称之为匿名管道。这里没有进行通信,只是建立了通信信道。

接口

int pipe(int pipefd[2]);

  • pipefd 是一个输出型参数,作用是分别以读取和写入的方式打开的文件描述符数字带出,让用户使用,pipefd[0]读下标,pipefd[1]写下标。

【功能】pipe函数在内核级创建文件

编码实现

cpp 复制代码
#include<iostream>
#include<unistd.h>

using namespace std;

#define N 2

int main()
{
    int pipefd[2] = {0};
    int n = pipe(pipefd);
    if(n < 0) return 1;

    cout << "pipefd[0]: " << pipefd[0] << " ; " << "pipefd[1]: " << pipefd[1] << endl;

    return 0;
}

完成第一步工作,父进程创建管道。

cpp 复制代码
    pid_t id = fork();
    if(id < 0) return 2;
    if(id == 0)
    {
        // child
        
    }
    // father

完成第二步工作,父进程创建子进程

cpp 复制代码
int main()
{
    int pipefd[2] = {0};
    int n = pipe(pipefd);
    if(n < 0) return 1;

    // cout << "pipefd[0]: " << pipefd[0] << " ; " << "pipefd[1]: " << pipefd[1] << endl;

    // child->w, father->r
    pid_t id = fork();
    if(id < 0) return 2;
    if(id == 0)
    {
        // child
        close(pipefd[0]);
        // use pipe

        // close pipe
        close(pipefd[1]);
        exit(0);
    }
    // father
    close(pipefd[1]);
    // use pipe


    // close pipe
    close(pipefd[0]);

    return 0;
}

第三步,关闭子进程管道的读端,关闭父进程管道的写端。

cpp 复制代码
#include<iostream>
#include<unistd.h>
#include<cstring>
#include<cstdlib>
#include<string>
#include<stdio.h>
#include<sys/types.h>
#include<sys/wait.h>


using namespace std;

#define N 2
#define NUM 1024

// child
void Writer(int wfd)
{
    string s = "hello, I am child";
    pid_t self = getpid();
    int number = 0;

    char buffer[NUM];
    while(true)
    {
        // create the sending string
        buffer[0] = 0; // clear string, reminder user for the array as the string
        snprintf(buffer, sizeof(buffer), "%s-%d-%d", s.c_str(), self, number);
        // cout << buffer << endl; // verification
        // send to the father process
        write(wfd, buffer, strlen(buffer));
        number++;
        sleep(1);
    }
}

// father
void Reader(int rfd)
{
    char buffer[NUM];

    while(true)
    {
        buffer[0] = 0;
        // receive child information
        ssize_t n = read(rfd, buffer, sizeof(buffer)); // sizeof != strlen
        if(n > 0)
        {
            buffer[n] = '\0'; // or buffer[n] = 0; as the string
            cout << "father get a message[" << getpid() << "]#" << buffer << endl;
        }
    }
}

int main()
{
    int pipefd[2] = {0};
    int n = pipe(pipefd);
    if(n < 0) return 1;

    // cout << "pipefd[0]: " << pipefd[0] << " ; " << "pipefd[1]: " << pipefd[1] << endl;

    // child->w, father->r
    pid_t id = fork();
    if(id < 0) return 2;
    if(id == 0)
    {
        // child
        close(pipefd[0]);
        // use pipe
        Writer(pipefd[1]);

        // close pipe
        close(pipefd[1]);
        exit(0);
    }
    // father
    close(pipefd[1]);
    // use pipe
    Reader(pipefd[0]);

    pid_t rid = waitpid(id, nullptr, 0);
    if(rid < 0) return 3; // fail

    // close pipe
    close(pipefd[0]);

    return 0;
}

第四步,子进程向文件内写入数据,父进程向文件读取数据

管道的特征:

  • 管道通信只能是具有血缘关系的进程进行进程间通信
  • 管道只能单向通信
  • 父子进程是会进行协同的,父子进程会对管道进行同步与互斥的,主要是为了保护管道文件的数据安全的(多线程)
  • 管道是面向字节流(网络)
  • 管道是基于文件的,而文件的生命周期随进程的。

管道是有固定大小的(64kb)

指令:ulimit -a

【指令】查看操作系统对于资源的限制

管道中的4种情况:

  1. 读写端正常,管道如果为空,读端就要阻塞;
  2. 读写端正常,管道如果为满,写端就要阻塞
  3. 写端关闭,读端就会读到0,表明读到了文件(pipe)结尾,不会被阻塞。
  4. 写端正常,但是读端关闭,操作系统就要杀掉正在写入的进程,使用的方式是通过信号杀掉,这个信号是13号信号(SIGPIPE)

【注意】让子进程写入,父进程读取的原因就是因为当父进程读端关闭,操作系统就会杀掉正在写入的进程,也就是子进程。

操作系统是不会做低效、浪费等类似的工作的。

管道的应用场景

(1)指令中存在 | ,例如cat test.txt | head -10 | tail -5。

(2)自定义shell中管道应用

  1. 分析输入的命令行字符串,获取有多少个 | ,将命令分为多个子命令字符串
  2. malloc申请空间,pipe先申请多个管道。
  3. 循环fork创建多个子进程,每一个子进程的重定向情况:(1)最开始的进程进行输出重定向(dup2),将1->指定的一个管道写端。(2)中间的进程进行输入输出重定向,0标准输入重定向到上一个管道的读端,1标准输出重定向到下一个管道的写端。(3)最后的进程,进行输入重定向,将0标准输入重定向到最后一个管道的读端。
  4. 分别让不同的子进程执行不同的命令------底层是exec*------程序替换不会影响该进程曾经打开的文件,不会影响预先设置好的管道重定向。

(3)使用管道实现一个简易版本的进程池

系统调用是有成本的,可以先创建一大批进程并储存好,当需要使用进程的时候,再使用已经创建好的进程,可以提供调用的效率,这种方式称为进程池。

ProcessPool.cc文件代码

cpp 复制代码
#include "Task.hpp"
#include <string>
#include <vector>
#include <unistd.h>
#include <cstdlib>
#include <cassert>
#include <ctime>
#include <sys/types.h>
#include <sys/wait.h>

// process numbers
const int processnum = 5;
std::vector<task_t> tasks;

// pipe channel
class channel
{
public:
    channel(int cmdfd, int slaverid, const std::string& processname)
    :_cmdfd(cmdfd)
    ,_slaverid(slaverid)
    ,_processname(processname)
    {}
public:
    int _cmdfd;        // The file descriptor for sending the task 
    pid_t _slaverid;   // The pid of the child process
    std::string _processname;  // The name of the child process >>> Facilitate the printing of logs
};

// void slaver(int rfd)
// {
//     while(true)
//     {
//         std::cout << getpid() << "--" << "read fd is : " << rfd << std::endl;
//         sleep(1);
//     }
// }

void slaver()
{
    while(true)
    {
        int cmdcode = 0;
        int n = read(0, &cmdcode, sizeof(int));
        if(n == sizeof(int))
        {
            // Execute the task list corresponding to the cmdcode
            std::cout << "slaver say@ get a command: " << getpid() << " : cmdcode : " << cmdcode << std::endl;
            if(cmdcode >= 0 && cmdcode < tasks.size())  tasks[cmdcode]();
        }
        if(n == 0)
        {
            break;
        }
    }
}

// 传参的三种方式(建议)
// 输入:const &
// 输出:*
// 输入输出:&
void InitProcessPool(std::vector<channel>* channels)
{
    std::vector<int> oldfds;
    for(int i = 0; i < processnum; ++i)
    {
        int pipefd[2] = {0}; // Temporary space
        int n = pipe(pipefd);
        assert(!n); 
        (void)n; 
        
        pid_t id = fork();
        if(id == 0)
        {
            for(auto fd : oldfds) close(fd);
            // child process(r)
            close(pipefd[1]);
            dup2(pipefd[0], 0);
            close(pipefd[0]);
            slaver();
            std::cout << "process : " << getpid() << " quit" << std::endl;
            // slaver(pipefd[0]);
            exit(0);
        }
        // father process(w)
        close(pipefd[0]);

        // Add the channel field
        std::string name = "process-" + std::to_string(i);
        channels->push_back(channel(pipefd[1], id, name));
        oldfds.push_back(pipefd[1]);
    }
}

void Debug(const std::vector<channel>& channels)
{
    // test code
    for(const auto &c : channels)
    {
        std::cout << c._cmdfd << " " << c._slaverid << " " << c._processname << std::endl;
    }
}

void Menu()
{
    std::cout << "##########################" << std::endl;
    std::cout << "##########################" << std::endl;
    std::cout << "# 1. task1       2.task2 #" << std::endl;
    std::cout << "# 3. task3       4.task4 #" << std::endl;
    std::cout << "# 0. quit                #" << std::endl;
    std::cout << "##########################" << std::endl;
    std::cout << "##########################" << std::endl;

}

void ctrlSlaver(const std::vector<channel>& channels)
{
    int which = 0;
    // for(int i = 1; i <= 100; ++i)
    //int cnt = 10;
    //while(cnt--)
    while(true)
    {
        Menu();
        std::cout << "Please Enter@ ";
        int select = 0;
        std::cin >> select;
        
        if(select <= 0 || select >= 5) break;

        // 1.select a task
        // int cmdcode = rand() % tasks.size();
        int cmdcode = select - 1;

        // 2.select a process(load balancing)
        // (1)random method
        // int processpos = rand() % channels.size();
        // (2)Rotation method - use which
        
        std::cout << "father say : " << "cmdcode: " << cmdcode << " " <<"already send to " << channels[which]._slaverid 
        << "process name : " << channels[which]._processname << std::endl;

        // 3.send a task
        write(channels[which]._cmdfd, &cmdcode, sizeof(cmdcode));
        
        // (2)which
        which++;
        which %= channels.size();

        // sleep(1);
    }
}


void quitProcess(const std::vector<channel>& channels)
{
    for(const auto &c : channels) close(c._cmdfd);
    // sleep(10);
    for(const auto &c : channels) waitpid(c._slaverid, nullptr, 0);
    // sleep(10);
}

int main()
{
    LoadTask(&tasks);

    srand(time(nullptr) ^ getpid() ^ 1023);

    std::vector<channel> channels;
    // 1.initialize
    InitProcessPool(&channels);
    Debug(channels);

    // 2.start controlling the process child
    ctrlSlaver(channels);

    // 3.cleaning up and finalizing
    quitProcess(channels);


    sleep(1);
    return 0;
}

Task.hpp文件

cpp 复制代码
#pragma once
#include <iostream>
#include <vector>


typedef void (*task_t)();

void task1()
{
    std::cout << "task1" << std::endl;
}

void task2()
{
    std::cout << "task2" << std::endl;

}

void task3()
{
    std::cout << "task3" << std::endl;

}

void task4()
{
    std::cout << "task4" << std::endl;
}

void LoadTask(std::vector<task_t> *tasks)
{
    tasks->push_back(task1);
    tasks->push_back(task2);
    tasks->push_back(task3);
    tasks->push_back(task4);
}

管道特点

  • 只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信;通常一个管道由一个进程创建,然后该进程调用fork,此后父子进程之间就可以应用该管道。
  • 管道提供流式服务
  • 一般而言,进程退出,管道释放,所以管道的生命周期随进程
  • 一般而言,内核会对管道操作进程同步与互斥。
  • 管道是半双工的,数据只能向一个方向流动;需要双向通信的时候,需要建立起两个管道。

命名管道

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

如果毫不相干的进程进行进程间通信,可以使用命名管道。

创建一个命名管道

指令:mkfifo [filename]

【功能】创建一个命名管道

myfifo文件的大小始终为0.

理解命名管道

++问题1:如果两个不同的进程,打开同一个文件的时候,在内核中操作系统会打开几个文件?++

【答】操作系统会与打开匿名管道一样,在内核级打开一个命名管道。系统会单独创建一个管道文件,这个文件打开的时候不需要刷盘(不会保存在磁盘中),也就是说管道文件只是一个内存级文件。

进程间通信的前提,先让不同进程看到同一份资源。

++问题2:为什么确定两个进程打开的是同一个文件?++

【答】只需要让两个进程看到同一个路径下的同一个文件名,就会打开同一个文件,这种文件称为命名管道。

路径 + 文件名称 = 同一个文件,路径+文件名具有唯一性

使用命名管道进行编码

  • int mkfifo(const char* filename, mode_t mode);

【功能】在程序中创建命名管道

  • int unlink(const char* pathname);

【功能】删除文件

server.cc服务端

cpp 复制代码
#include"comm.hpp"

using namespace std;

// Manage pipe files

int main()
{
    Init init;

    // Open the channel
    int fd= open(FIFO_FILE, O_RDONLY);
    if(fd < 0)
    {
        perror("open");
        exit(FIFO_OPEN_ERR);
    }

    cout << "server open file done" << endl;

    // Start communication
    while(true)
    {
        char buffer[1024] = {0};
        int x = read(fd, buffer, sizeof(buffer));
        if(x > 0)
        {
            buffer[x] = 0;
            cout << "client say# " << buffer << endl;
        }
        else if(x == 0)
        {
            cout << "client quit, me too" << endl;
            break;
        }
        else break;
    }

    close(fd);




    return 0;
}

client.cc客户端

cpp 复制代码
#include"comm.hpp"

using namespace std;

int main()
{
    int fd = open(FIFO_FILE, O_WRONLY);
    if(fd <= 0)
    {
        perror("open");
        exit(FIFO_OPEN_ERR);
    }

    cout << "client open file done" << endl;

    string line;
    while(true)
    {
        cout << "Please Enter@: ";
        getline(cin, line);

        write(fd, line.c_str(), line.size());
    }

    close(fd);


    return 0;
}

comm.hpp

cpp 复制代码
#pragma once

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

#define FIFO_FILE "./myfifo"
#define MODE 0664

enum
{
    FIFO_CREATE_ERR = 1,
    FIFO_DELETE_ERR,
    FIFO_OPEN_ERR
};

class Init
{
public:
    Init()
    {
        // Create a channel
        int n = mkfifo(FIFO_FILE, MODE);
        if (n == -1)
        {
            perror("mkfifo");
            exit(FIFO_CREATE_ERR);
        }
    }
    ~Init()
    {
        int m = unlink(FIFO_FILE);
        if (m == -1)
        {
            perror("unlink");
            exit(FIFO_DELETE_ERR);
        }
    }
};

makefile

cpp 复制代码
.PHONY:all
all:server client
server:server.cc
	g++ -o $@ $^ -g -std=c++11
client:client.cc
	g++ -o $@ $^ -g -std=c++11
.PHONY:clean
clean:
	rm -f server client

穿插概念------日志

一般的日志信息,日志时间,日志的等级,日志的内容,文件的名称和行号

常见的日志等级有

  • info:常规消息
  • warning:报警信息
  • Error:比较严重,可能需要立即处理
  • Fatal:致命的问题
  • Debug:调试

下面实现一个简单的日志函数

cpp 复制代码
#pragma once

#include <iostream>
#include <stdarg.h>
#include <time.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>

#define SIZE 1024

#define Info 0
#define Debug 1
#define Warning 2
#define Error 3
#define Fatal 4


#define Screen     1
#define Onefile    2
#define Classfile  3

#define logFile "log.txt"

class Log
{
public:
    Log()
    {
        printMethod = Screen;
        path = "./log/";
    }

    void Enable(int method)
    {
        printMethod = method;
    }


    std::string levelToString(int level)
    {
        switch (level)
        {
        case Info:
            return "Info";
        case Debug:
            return "Debug";
        case Warning:
            return "Warning";
        case Error:
            return "Error";
        case Fatal:
            return "Fatal";
        default:
            return "None";
        }
    }

    // void logmessage(int level, const char *format, ...)
    // {
    //     time_t t = time(nullptr);
    //     struct tm *ctime = localtime(&t);
    //     char leftbuffer[SIZE];
    //     snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", levelToString(level).c_str(),
    //              ctime->tm_year + 1900, ctime->tm_mon + 1, ctime->tm_mday, ctime->tm_hour, ctime->tm_min, ctime->tm_sec);

    //     char rightbuffer[SIZE * 2];
    //     va_list s;
    //     va_start(s, format);
    //     vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);
    //     va_end(s);

    //     // 格式:默认部分+自定义部分
    //     char logtxt[SIZE * 4];
    //     snprintf(logtxt, sizeof(logtxt), "%s %s\n", leftbuffer, rightbuffer);

    //     // printf("%s", logtxt); // 打印
    //     printLog(level, logtxt);
    // }

    void printLog(const int level, const std::string& logtxt)
    {
        switch(printMethod)
        {
            case Screen :
                std::cout << logtxt << std::endl;
            break;
            case Onefile: 
                printOneFile(logFile, logtxt);
            break;
            case Classfile:
                printClassFile(level, logtxt);
            break;
            default: break;
        }
    }

    void printOneFile(const std::string &logname, const std::string& logtxt)
    {
        std::string _logname = path + logname;
        int fd = open(_logname.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0666);
        if(fd < 0) return;
        write(fd, logtxt.c_str(), logtxt.size());
        close(fd);
    }

    void printClassFile(const int level, const std::string& logtxt)
    {
        std::string filename = logFile;
        filename += ".";
        filename += levelToString(level);

        printOneFile(filename, logtxt);
    }


    ~Log()
    {}

    void operator()(int level, const char *format, ...)
    {
        time_t t = time(nullptr);
        struct tm *ctime = localtime(&t);
        char leftbuffer[SIZE];
        snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", levelToString(level).c_str(),
                 ctime->tm_year + 1900, ctime->tm_mon + 1, ctime->tm_mday, ctime->tm_hour, ctime->tm_min, ctime->tm_sec);

        char rightbuffer[SIZE * 2];
        va_list s;
        va_start(s, format);
        vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);
        va_end(s);

        // 格式:默认部分+自定义部分
        char logtxt[SIZE * 4];
        snprintf(logtxt, sizeof(logtxt), "%s %s\n", leftbuffer, rightbuffer);

        // printf("%s", logtxt); // 打印
        printLog(level, logtxt);
    }
private:
    int printMethod;
    std::string path;
};

穿插日志功能的命名管道代码

server.cc服务端

cpp 复制代码
#include"comm.hpp"
#include "log.hpp"

using namespace std;

// Manage pipe files

int main()
{
    Init init;
    Log log;
    log.Enable(Onefile);
    
    // Open the channel
    int fd= open(FIFO_FILE, O_RDONLY);
    if(fd < 0)
    {
        // log.logmessage(Fatal, "error string: %s, error code: %d", strerror(errno), errno);
        log(Fatal, "error string: %s, error code: %d", strerror(errno), errno);
        exit(FIFO_OPEN_ERR);
    }

    cout << "server open file done" << endl;
    
    // log.logmessage(Info, "server open file done, error string: %s, error code : %d", strerror(errno), errno);
    log(Info, "server open file done, error string: %s, error code : %d", strerror(errno), errno);


    // Start communication
    while(true)
    {
        char buffer[1024] = {0};
        int x = read(fd, buffer, sizeof(buffer));
        if(x > 0)
        {
            buffer[x] = 0;
            cout << "client say# " << buffer << endl;
        }
        else if(x == 0)
        {
            log(Debug, "client quit, me too, error string: %s, error code : %d", strerror(errno), errno);
            break;
        }
        else break;
    }

    close(fd);




    return 0;
}

client.cc客户端

cpp 复制代码
#include"comm.hpp"

using namespace std;

int main()
{
    int fd = open(FIFO_FILE, O_WRONLY);
    if(fd <= 0)
    {
        perror("open");
        exit(FIFO_OPEN_ERR);
    }

    cout << "client open file done" << endl;

    string line;
    while(true)
    {
        cout << "Please Enter@: ";
        getline(cin, line);

        write(fd, line.c_str(), line.size());
    }

    close(fd);


    return 0;
}

comm.hpp

cpp 复制代码
#pragma once

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

#define FIFO_FILE "./myfifo"
#define MODE 0664

enum
{
    FIFO_CREATE_ERR = 1,
    FIFO_DELETE_ERR,
    FIFO_OPEN_ERR
};

class Init
{
public:
    Init()
    {
        // Create a channel
        int n = mkfifo(FIFO_FILE, MODE);
        if (n == -1)
        {
            perror("mkfifo");
            exit(FIFO_CREATE_ERR);
        }
    }
    ~Init()
    {
        int m = unlink(FIFO_FILE);
        if (m == -1)
        {
            perror("unlink");
            exit(FIFO_DELETE_ERR);
        }
    }
};
相关推荐
数智顾问2 小时前
(111页PPT)华为业务变革框架及战略级项目管理(附下载方式)
大数据·运维·华为
Howrun7772 小时前
Linux进程通信---6.1---进程信号屏蔽
linux·服务器
郑泰科技2 小时前
SpringBoot项目实践:之前war部署到服务器好用,重新打包部署到服务器报404
服务器·spring boot·后端
国科安芯2 小时前
核工业检测系统通信链路的国产化元器件安全等级评估
运维·网络·人工智能·单片机·嵌入式硬件·安全·安全性测试
一颗青果2 小时前
五种IO模型
linux·服务器·网络
宋军涛2 小时前
SqlServer性能优化
运维·服务器·性能优化
rocksun2 小时前
Neovim,会是你的下一款“真香”开发神器吗?
linux·python·go
郝学胜-神的一滴2 小时前
Linux线程属性设置分离技术详解
linux·服务器·数据结构·c++·程序人生·算法
qq_317620312 小时前
01:Docker 概述
运维·docker·容器·docker安装