[Linux]----守护进程

news2024/10/6 22:20:04

文章目录

  • 前言
  • 一、什么是守护进程?
  • 二、会话和进程组
    • 会话
    • 进程组
  • 三、守护进程的编程流程
  • 总结


前言

这节课我来给大家讲解在Linux下如何让进程守护化,运行在后台,处理我们的任务.


正文开始!

一、什么是守护进程?

守护进程也称为精灵进程(Daemon),是运行在后台的一种特殊进程.它独立于控制中断并且周期性的执行某种任务或者等待处理某些发生的事件.

Linux系统启动是会启动很多服务清楚,这些系统服务进程没有控制终端,不能直接和用户交互.其他进程都是在用户登录或运行程序时创建.在运行结束或者用户注销时终止,但系统服务进程不受用户登录注销的影响,它们一直运行这.这种进程都有一个名称叫守护进程(Daemon).

例如:udevd负责维护/dev目录下的设备文件,acpid负责电源管理,syslogd负责维护/var/log下的日志文件,可以看出守护进程通常采用以d结尾的名称,表示Daemon.

一般以服务器的方式工作,对外提供服务的服务器,都是以守护进程(精灵进程)的方式在服务器中工作的,一旦启动之后,除非用户主动关闭.否则,一直会运行.

二、会话和进程组

会话

每打开一个控制中断,或者在用户登录时,系统就会创建新会话.
每个会话通常都与一个控制中断相关联.
在该会话中允许的第一个进程称作会话首进程,通常这个首进程就是shell.

下面是与会话相关的系统调用:
代表创建新会话,当前调用进程不能是组长进程
在这里插入图片描述

在这里插入图片描述

举个栗子:

在这里插入图片描述

进程组

每个进程都属于某个进程组,进程组是由一个或多个相互间有关联的进程组成的,他的目的是为了进行作业控制.
进程租的主要特征就是信号可以发给进程组中的所有进程:这个信号可以使同一个进程组中的所有进程终止,停止或者继续运行.
每个进程组都由进程组id唯一标识,并且有一个组长进程.进程组id就是组长进程的pid.只要在某个进程组中还有一个进程存在,则该进程组就存在.
即使组长进程终止了,在#include pid_t setsid(void);创建新会话,当前调用进程不能是组长进程pid_t getsid(pid_t isd);获取进程的所属会话id,pid=0是代表当前进程进程组依然存在.

在这里插入图片描述

三、守护进程的编程流程

必须要调用一个函数setsid():将调用进程设置称为独立的会话
这个函数要求进程组的组长不能调用.

我如何不成为组长呢?
你可以称为进程组内的第二个进程!—>常规做法,fork()子进程,子进程就不再是组长进城啦,他就可以成功调用setsid();

必须要做的:
if(fork()>0) exit(0);
setsid();

选做的内容:
之前我们在学习管道的时候,写端一直在写,读端关闭,写端会被终止,被信号SIGPIPE;

server一直向已经关闭的client写入,server也会收到SIGPIPE.

忽略SIGPIPE信号!

更改进程的工作目录,如何更改进程的工作目录呢?—>chdir

一般必做的:

  1. close(0,1,2)[很少有人这样做]

    Linux下的"信息黑洞或者垃圾桶",就是向这个文件里面读写的内容都被丢弃了!

  2. 打开/dev/null,并且进行对0,1,2进行重定向.

代码如下
dameonize.hpp

#pragma once
#include <cstdio>
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
void daemonize()
{
    // 1.忽略SIGPIPE信号
    signal(SIGPIPE, SIG_IGN);
    // 2.更改进程的工作目录
    // chdir();

    // 3.让自己不要成为进程组组长
    if (fork() > 0)
        exit(0);
    // 4.设置自己是一个独立的会话
    setsid();
    // 5.重定向0,1,2
    int fd = 0;
    if (fd = open("dev/null", O_RDWR) != -1)
    {
        dup2(fd, STDIN_FILENO);
        dup2(fd, STDOUT_FILENO);
        dup2(fd, STDERR_FILENO);
        // 6.关闭掉不需要的fd
        if(fd>STDERR_FILENO)
        {
            close(fd);
        }
    }
}

serverTcpd.cc

#include "util.hpp"
#include "log.hpp"
#include "ThreadPool.hpp"
#include "Task.hpp"
#include"daemonize.hpp"
using namespace std;

class ServerTcp;
struct ThreadData
{
    ThreadData(int sock, string clientIp, uint16_t clientPort, ServerTcp *ts)
        : _sock(sock), _clientIp(clientIp), _clientPort(clientPort), _this(ts)
    {
    }
    int _sock;
    string _clientIp;
    uint16_t _clientPort;
    ServerTcp *_this;
};
//大小写转化
// TCP && UDP支持全双工
void transService(int sock, const string &clientIp, uint16_t clientPort)
{
    assert(sock > 0);
    assert(!clientIp.empty());
    assert(clientPort > 1024);
    char inbuffer[BUFFER_SIZE];
    while (true)
    {
        ssize_t s = read(sock, inbuffer, sizeof(inbuffer) - 1); //我们认为读到的都是字符串
        if (s > 0)
        {
            // read success
            inbuffer[s] = '\0';
            if (strcasecmp(inbuffer, "quit") == 0)
            {
                logMessage(DEBUG, "client quit -- %s[%d]", clientIp.c_str(), clientPort);
                break;
            }
            logMessage(DEBUG, "trans before: %s[%d]>> %s", clientIp.c_str(), clientPort, inbuffer);
            //可以进行大小写转化了
            for (int i = 0; i < s; i++)
            {
                if (isalpha(inbuffer[i]) && islower(inbuffer[i]))
                    inbuffer[i] = toupper(inbuffer[i]);
            }
            write(sock, inbuffer, sizeof(inbuffer));
            logMessage(DEBUG, "trans after: %s[%d]>> %s", clientIp.c_str(), clientPort, inbuffer);
        }
        else if (s == 0)
        {
            // pipe:读端一直在读,写端不写了,并且关闭了写端,读端会如何?--->s==0,代表对端关闭
            // s==0,代表对方关闭,Client退出
            logMessage(DEBUG, "client quit -- %s[%d]", clientIp.c_str(), clientPort);
            break;
        }
        else
        {
            logMessage(DEBUG, "%s[%d] -- read: %s", clientIp.c_str(), clientPort, strerror(errno));
            break;
        }
    }
    //只要走到这里,一定是client退出了,服务到此结束
    close(sock); // 如果一个进程对应的文件fd,打开了没有被归还,文件描述符泄露!
    logMessage(DEBUG, "server close %d done", sock);
}

void execCommand(int sock, const string &clientIp, uint16_t clientPort)
{
    assert(sock > 0);
    assert(!clientIp.empty());
    assert(clientPort > 1024);
    char command[BUFFER_SIZE];
    while (true)
    {
        ssize_t s = read(sock, command, sizeof(command) - 1); //我们认为读到的都是字符串
        if (s > 0)
        {
            command[s]='\0';
            logMessage(DEBUG,"[%s:%d] exec [%s]",clientIp.c_str(),clientPort,command);
            std::string safe;
            if((safe.find("rm")!=std::string::npos)||(safe.find("unlink")!=std::string::npos))
            {
                break;
            }
            // 我们是以r方式打开的文件,没有写入
            //所以我们无法通过dup2的方式得到对应的结果
            FILE* fp=popen(command,"r");
            if(fp==nullptr)
            {
                logMessage(WARINING,"exec %s failed, because: %s",command,strerror(errno));
                break;
            }

            char line[1024];
            while(fgets(line,sizeof(line)-1,fp)!=nullptr)
            {
                write(sock,line,strlen(line));
            }
            // dup2(sock,fp->_fileno);
            // fflush(fp);
            pclose(fp);
            logMessage(DEBUG,"[%s]:%d exec [%s]... done",clientIp.c_str(),clientPort,command);
        }
        else if (s == 0)
        {
            // pipe:读端一直在读,写端不写了,并且关闭了写端,读端会如何?--->s==0,代表对端关闭
            // s==0,代表对方关闭,Client退出
            logMessage(DEBUG, "client quit -- %s[%d]", clientIp.c_str(), clientPort);
            break;
        }
        else
        {
            logMessage(DEBUG, "%s[%d] -- read: %s", clientIp.c_str(), clientPort, strerror(errno));
            break;
        }
    }
    //只要走到这里,一定是client退出了,服务到此结束
    close(sock); // 如果一个进程对应的文件fd,打开了没有被归还,文件描述符泄露!
    logMessage(DEBUG, "server close %d done", sock);
}

class ServerTcp
{
public:
    ServerTcp(uint16_t port, string ip = "")
        : _listenSock(-1), _port(port), _ip(ip), _tp(nullptr)
    {
    }
    ~ServerTcp()
    {
    }

public:
    void init()
    {
        // 1.创建socket
        _listenSock = socket(AF_INET, SOCK_STREAM, 0);
        if (_listenSock < 0)
        {
            logMessage(FATAL, "socket:%s", strerror(errno));
            exit(SOCKET_ERR);
        }
        logMessage(DEBUG, "socket:&s,%d", strerror(errno), _listenSock);
        // 2.bind绑定
        // 2.1填充服务器
        struct sockaddr_in local; //用户栈
        memset(&local, 0, sizeof local);
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);
        _ip.empty() ? (local.sin_addr.s_addr = INADDR_ANY) : (inet_aton(_ip.c_str(), &local.sin_addr));
        // 2.2本地socket信息,写入_sock对应的内核区域
        if (bind(_listenSock, (const sockaddr *)&local, sizeof local) < 0)
        {
            logMessage(FATAL, "bind: %s", strerror(errno));
            exit(BIND_ERR);
        }
        logMessage(DEBUG, "bind: %s", strerror(errno));
        // 3.监听socket,为何要监听呢?tcp是面向连接的!
        if (listen(_listenSock, 5 /*后面再说*/) < 0)
        {
            logMessage(FATAL, "listen: %s", strerror(errno));
            exit(LISTEN_ERR);
        }
        logMessage(DEBUG, "listen: %s", strerror(errno));
        //允许别人来连接你了

        // 4.加载线程池
        _tp = ThreadPool<Task>::getInstance();
    }
    void loop()
    {
        // signal(SIGCHLD,SIG_IGN);//只在Linux下有效
        _tp->start();
        logMessage(DEBUG, "thread pool start success,thread num: %d", _tp->ThreadNum());
        while (true)
        {
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            // 4.获取连接,accept的返回值是一个新的socket fd??
            // 4.1 _listenScok:监听&&获取新的连接--->sock
            // 4.2 serviceSock:给用户提供新的socket服务
            int serviceSock = accept(_listenSock, (struct sockaddr *)&peer, &len);
            if (serviceSock < 0)
            {
                //获取连接失败
                logMessage(WARINING, "accept: &s[%d]", strerror(errno), serviceSock);
                continue;
            }
            // 4.1获取客户端基本信息
            uint16_t peerPort = ntohs(peer.sin_port);
            string peerIp = inet_ntoa(peer.sin_addr);
            logMessage(DEBUG, "accept: %s | %s[%d],socker fd: %d",
                       strerror(errno), peerIp.c_str(), peerPort, serviceSock);

            // 5.提供服务,小写转大写
            // // 5.0 v0版本----单进程--一点进行transService,主执行流就无法进行向后执行,只能提供完毕服务后才能进行accept
            // transService(serviceSock, peerIp, peerPort);

            // //5.1 V1---多进程版本---父进程打开的文件会被子进程继承!
            // pid_t id=fork();
            // assert(id!=-1);
            // if(id==0)
            // {
            //     close(_listenSock);
            //     //子进程
            //     transService(serviceSock, peerIp, peerPort);
            //     exit(0);
            // }
            // //父进程
            // close(serviceSock);//这一步是一定要做的!

            // //waitpid();默认是阻塞等待!WNOHANG
            // //方案1

            // // //5.1 V1.1---多进程版本
            // //爷爷进程
            // pid_t id=fork();
            // if(id==0)
            // {
            //     //爸爸进程
            //     close(_listenSock);
            //     //又进行了一次fork
            //     if(fork>0) exit(0);
            //     //孙子进程--就没有爸爸进程了--孤儿进程--被系统领养了--回收问题就交给了系统来回收
            //     transService(serviceSock, peerIp, peerPort);
            //     exit(0);
            // }
            // close(serviceSock);
            // //爸爸进程直接终止,立马得到退出码,释放僵尸进程状态
            // pid_t ret=waitpid(id,nullptr,0);//就用阻塞式等待
            // (void)ret;

            // //5.2 v2版本---多线程
            // //这里不需要关闭文件描述符了!
            // ThreadData* td=new ThreadData(serviceSock,peerIp,peerPort,this);
            // pthread_t tid;
            // pthread_create(&tid,nullptr,startRountine,(void*)td);

             5.3 v3版本 --- 线程池版本
              5.3.1 构建任务
              5.3 v3.1
            // Task t(serviceSock, peerIp, peerPort, std::bind(&ServerTcp::transService, this,std::placeholders::_1,std::placeholders::_2,std::placeholders::_3));
            // _tp->push(t);

             5.3 v3.2
            // Task t(serviceSock, peerIp, peerPort, transService);
            // _tp->push(t);

            5.3 v3.3
            Task t(serviceSock, peerIp, peerPort, execCommand);
            _tp->push(t);
            // logMessage(DEBUG,"server provide service start ...");
            // sleep(1);
        }
    }

    // static void *startRountine(void *args)
    // {
    //     pthread_detach(pthread_self());
    //     ThreadData *td = static_cast<ThreadData *>(args);
    //     td->_this->transService(td->_sock, td->_clientIp, td->_clientPort);
    //     delete (td);
    // }
    // TCP && UDP支持全双工
    // void transService(int sock, const string &clientIp, uint16_t clientPort)
    // {
    //     assert(sock > 0);
    //     assert(!clientIp.empty());
    //     assert(clientPort > 1024);
    //     char inbuffer[BUFFER_SIZE];
    //     while (true)
    //     {
    //         ssize_t s = read(sock, inbuffer, sizeof(inbuffer) - 1); //我们认为读到的都是字符串
    //         if (s > 0)
    //         {
    //             // read success
    //             inbuffer[s] = '\0';
    //             if (strcasecmp(inbuffer, "quit") == 0)
    //             {
    //                 logMessage(DEBUG, "client quit -- %s[%d]", clientIp.c_str(), clientPort);
    //                 break;
    //             }
    //             logMessage(DEBUG, "trans before: %s[%d]>> %s", clientIp.c_str(), clientPort, inbuffer);
    //             //可以进行大小写转化了
    //             for (int i = 0; i < s; i++)
    //             {
    //                 if (isalpha(inbuffer[i]) && islower(inbuffer[i]))
    //                     inbuffer[i] = toupper(inbuffer[i]);
    //             }
    //             write(sock, inbuffer, sizeof(inbuffer));
    //             logMessage(DEBUG, "trans after: %s[%d]>> %s", clientIp.c_str(), clientPort, inbuffer);
    //         }
    //         else if (s == 0)
    //         {
    //             // pipe:读端一直在读,写端不写了,并且关闭了写端,读端会如何?--->s==0,代表对端关闭
    //             // s==0,代表对方关闭,Client退出
    //             logMessage(DEBUG, "client quit -- %s[%d]", clientIp.c_str(), clientPort);
    //             break;
    //         }
    //         else
    //         {
    //             logMessage(DEBUG, "%s[%d] -- read: %s", clientIp.c_str(), clientPort, strerror(errno));
    //             break;
    //         }
    //     }
    //     //只要走到这里,一定是client退出了,服务到此结束
    //     close(sock); // 如果一个进程对应的文件fd,打开了没有被归还,文件描述符泄露!
    //     logMessage(DEBUG, "server close %d done", sock);
    // }
private:
    int _listenSock;
    uint16_t _port;
    string _ip;
    //引入线程池
    ThreadPool<Task> *_tp;
};
static void Usage(string proc)
{
    cerr << "Usage\n\t" << proc << " port ip" << endl;
    cerr << "Example\n\t" << proc << " 8080  127.0.0.1\n"
         << endl;
}

// ./serverTcp local_port [local_ip]
int main(int argc, char *argv[])
{
    if (argc != 2 && argc != 3)
    {
        Usage(argv[0]);
        exit(USAGE_ERR);
    }
    uint16_t port = stoi(argv[1]);
    string ip;
    if (argc == 3)
    {
        ip = argv[2];
    }
    daemonize();//我们的进程就会成为守护进程
    ServerTcp svr(port, ip);
    svr.init();
    svr.loop();
    return 0;
}

在这里插入图片描述
此时服务器就部署在了Linux中.

注意:
进程守护化以后,只能使用kill命令杀掉该进程!
在这里插入图片描述


总结

(本章完!)

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

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

相关文章

Mybatis-Plus快速使用相关知识点1

Mybatis-Plus的mapper、service 基本CURD BaseMapper BaseMapper是MyBatis-Plus提供的模板mapper&#xff0c;其中包含了基本的CRUD方法&#xff0c;泛型为操作的实体类型&#xff0c;Mapper 继承该接口后&#xff0c;无需编写 mapper.xml 文件&#xff0c;即可获得CRUD功能…

JavaScript刷LeetCode拿offer-链表篇

一、链表 链表&#xff08;Linked List&#xff09;是一种常见的基础数据结构&#xff0c;也是线性表的一种。 一个线性表是 n 个具有相同特性的数据元素的有限序列&#xff0c;线性表的存储结构分为两类&#xff1a;顺序表&#xff08;数组&#xff09;和链表。 链表相比较顺…

站得高,望得远

1、站得高&#xff0c;望的远 计算机科学领域的任何问题都可以通过增加一个间接的中间层来解决。 这句话几乎概括了计算机系统软件体系结构的设计要点 &#xff0c;整个体系结构从上到下都是按照严格的层次结构设计的。不仅是计算机系统软件整个体系是这样的&#xff0c;体系里…

884. 两句话中的不常见单词 map与stringstream

目录 力扣884. 两句话中的不常见单词 【解法一】&#xff1a;最后写出了一坨屎&#xff0c;虽然它是一坨屎&#xff0c;但是它能动&#xff0c;虽然它是一坨屎&#xff0c;但起码这是我自己拉的 【大佬解法】 stringstream的使用 以及 map的使用 884. 两句话中的不常见单词 句…

python实现bib文件中参考文献的题目每个单词首字母大写

文章目录前言实现思路前言 由于毕业论文格式要求英文参考文献的题目的每个单词&#xff08;除了介词&#xff09;的首字母都要大写&#xff0c;如果一条条地自己修改费时费力&#xff0c;这里就想着简单地用python操作字符串的方式实现。 实现思路 观察bib参考文献格式&#x…

20230102单独编译Toybrick的TB-RK3588X开发板的Android12的内核

20230102单独编译Toybrick的TB-RK3588X开发板的Android12的内核 2023/1/2 17:40 《RK3588_Android12_SDK_Developer_Guide_CN.pdf》 原厂的开发板rk3588-evb1-lp4-v10单独编译内核的方式&#xff1a; cd kernel-5.10 export PATH../prebuilts/clang/host/linux-x86/clang-r4161…

【数据结构】C语言实现链表(单链表部分)

目录 前言 链表 链表的分类 1.单向或者双向 2.带头或者不带头 3.循环或者非循环 单链表实现 定义节点 接口函数实现 创建节点 打印链表 尾插节点 尾删节点 头插节点 头删节点 单链表查找 删除指定位置后的节点 指定位置后插入节点 删除指定位置 指定位置插入节点…

Linux-7 文本编辑vivim

Linux-7 文本编辑vi/vim vim介绍 什么是vim&#xff1f; vi和vim是Linux下的一个文本编辑工具。&#xff08;可以李姐为Windows的记事本或word文档&#xff09; 为什么要使用vim&#xff1f; 因为Linux系统一切皆为文件&#xff0c;而我们工作最多的就是修改某个服务的配置&a…

一名七年老安卓的 2022 总结

大家好&#xff0c;我是 shixin。一转眼到了 2022 的最后一天&#xff0c;今年发生了很多事&#xff0c;这篇文章来总结一下。长短期目标达成情况和去年一样&#xff0c;我的长期目标是成为具备创业能力的人&#xff0c;包括商业思维和全栈技术能力。总的来说&#xff0c;今年是…

STM32MP157驱动开发——USB设备驱动

STM32MP157驱动开发——USB设备驱动一、简介1.电气属性2.USB OTG3.STM32MP1 USB 接口简介4.Type-C 电气属性二、USB HOST 驱动开发1.USB HOST 驱动编写2.配置 PHY 控制器3.配置usbh_ehci三、USB HOST 测试1.鼠标键盘驱动使能2.U盘驱动四、USB OTG驱动开发1.USB OTG 控制器节点信…

系统设计实战一

文章目录前言一、服务幂等1.防止订单重复下单1.1 场景如下&#xff1a;当用户在提交订单的时候1.2 重复下单解决方案1.3案例一幂等性总结2 防止订单ABA问题2.1 场景如下&#xff1a;当在修改订单用户信息的时候发生服务器或者网络问题导致的重试2.2 ABA问题解决方案2.3 业务ABA…

Mac本地安装Mysql并配置

文章目录一、安装Mysql二、配置Mysql三、启动mysql四、SQL语法初步了解1.创建数据库2.建表3.查看表一、安装Mysql 笔者推荐采用安装包的方法安装Mysql&#xff0c;比较简单&#xff0c;适合新手。 首先在网上搜安装包&#xff1a; baidu按关键字搜即可&#xff1a;mysql mac安…

多兴趣向量重构用户向量

Re4: Learning to Re-contrast, Re-attend, Re-construct for Multi-interest Recommendation 论文地址&#xff1a;https://arxiv.org/pdf/2208.08011.pdf 一般的多兴趣建模过程是对用户序列进行编码&#xff0c;抽取出用户的多个兴趣向量&#xff0c;然后利用这些用户兴趣向…

【Vue中使用Echarts】echarts初体验

文章目录一、echarts简介二、初次体验echarts1.下载2.在vue中引入echarts①全局引入&#xff08;代码&#xff09;② 局部引入一、echarts简介 在大数据盛行的今天&#xff0c;数据可视化变得越来越广泛。而在前端工作中&#xff0c;数据可视化用得最多的&#xff0c;可能就是…

Usaco Training 刷怪旅 第三层 第四题 :Combination Lock

一个六年级博主写文章不容易&#xff0c;给个关注呗 &#xff08;点赞也行啊&#xff09; 本蒟蒻的bilibili账号 注&#xff1a;这种题当你看不懂的时候是可以把题目复制去洛谷看中文版的 Farmer Johns cows keep escaping from his farm and causing mischief. To try and pre…

如何通过 Python 与 ChatGPT 对话

文章目录简介安装 OpenAI API实例1预备条件: 1. 科学上网&#xff1b; 2. 注册 OpenAI 账号。 简介 ChatGPT 是 GPT-3 语言模型的变体&#xff0c;专为会话语言生成而设计。要在 Python 中使用 ChatGPT&#xff0c;您需要安装 OpenAI API 客户端并获取 API 密钥。当前提你需要…

前端工程师leetcode算法面试必备-二分搜索算法(中)

一、前言 二分搜索算法本身并不是特别复杂&#xff0c;核心点主要集中在&#xff1a; 有序数组&#xff1a;指的是一个递增或者递减的区间&#xff08;特殊情况如&#xff1a;【852. 山脉数组的峰顶索引】&#xff09;&#xff1b; 中间数&#xff1a;用来确定搜索目标落在左…

Pytorch学习笔记①——anaconda和jupyter环境的安装(小白教程)

一、安装Pytorch 1、首先找到anaconda命令端并点击进入。 2、输入如下命令创建子空间&#xff08;博主的命名是pytorch1.4.0&#xff0c;使用python3.6版本&#xff09; conda create -n pytorch1.4.0 python3.6对于下载速度慢的话&#xff0c;首先需要进行换源&#xff0c;换…

FastJson不出网rce

BCEL ClassLoader去哪了 0x01 BCEL从哪里来 首先&#xff0c;BCEL究竟是什么&#xff1f;它为什么会出现在JDK中&#xff1f; BCEL的全名应该是Apache Commons BCEL&#xff0c;属于Apache Commons项目下的一个子项目。Apache Commons大家应该不陌生&#xff0c;反序列化最著…

05 RS485

什么是RS485&#xff1f; RS485 是一种通用的通信标准&#xff0c;广泛用于数据采集和控制应用中。 它的主要优点之一是它允许将多个 RS485 设备放在同一条总线上&#xff0c;这使得多个节点可以相互连接。 RS-485&#xff08;目前称为EIA/TIA-485&#xff09;是通信物理层的…