死锁问题分析和解决——资源回收时
1.描述问题
在完成线程池核心功能功能时,没有遇到太大的问题(Any,Result,Semfore的设计),在做线程池资源回收时,遇到了死锁的问题
1、在ThreadPool的资源回收,等待线程池所有线程退出时,发生死锁问题,导致进程无法退出
死锁代码:
#include "threadpool.h"#include <thread>
#include <iostream>const int TASK_MAX_THRESHHOLD = INT32_MAX;
const int THREAD_MAX_THRESHHOLD = 100;
const int THREAD_MAX_IDLE_TIME = 60;//单位:秒//线程池构造
ThreadPool::ThreadPool(): initThreadSize_(0), taskSize_(0), idleThreadSize_(0)//刚开始时还没有线程, curThreadSize_(0), taskQueMaxThreshHold_(TASK_MAX_THRESHHOLD), threadSizeThreshHold_(THREAD_MAX_THRESHHOLD), poolMode_(PoolMode::MODE_FIXED), isPoolRunning_(false)
{}//线程池析构
ThreadPool::~ThreadPool()
{isPoolRunning_ = false;notEmpty_.notify_all();//等待线程池里面所有的线程返回 有两种状态:阻塞 & 正在执行任务中std::unique_lock<std::mutex> lock(taskQueMtx_);exitCond_.wait(lock, [&]()->bool {return threads_.size() == 0; });
}//设置线程池的工作模式
void ThreadPool::setMode(PoolMode mode)
{if (checkRunningState())return;poolMode_ = mode;
}// 设置task任务队列上限阈值
void ThreadPool::setTaskQueMaxThreshHold(int threshhold)
{if (checkRunningState())return;taskQueMaxThreshHold_ = threshhold;
}//设置线程池cached模式下线程阈值
void ThreadPool::setThreadSizeThreshHold(int threshhold)
{if (checkRunningState())return;if (poolMode_ == PoolMode::MODE_CACHED){threadSizeThreshHold_ = threshhold;}
}// 给线程池提交任务 用户调用该接口,传入任务对象,生产任务
Result ThreadPool::submitTask(std::shared_ptr<Task> sp)
{//获取锁std::unique_lock<std::mutex> lock(taskQueMtx_);//线程的通信 等待任务队列有空余// 用户提交任务,最长不能阻塞超过1s,否则判断提交任务失败,返回//while (taskQue_.size() == taskQueMaxThreshHold_)//{// notFull_.wait(lock);//}/** wait:直到等待满足条件(第二个参数lamada)才返回* wait_for:满足条件返回真,到了约定的时间段(5s)返回假* wait_until:满足条件返回真,到了约定的时间点(下周一)返回假*/if (!notFull_.wait_for(lock, std::chrono::seconds(1),[&]()->bool {return taskQue_.size() < (size_t)taskQueMaxThreshHold_; }))//等同于上面的语句,参数:需要释放的锁 函数对象(要能满足条件变量)//任务队列中的任务数小于上限的阈值,否则就阻塞在这句{//表示notFull_等待1s,条件依然没有满足std::cerr << "task queue is full,submit task fail." << std::endl;//return task->getResult(); //Task Result 线程执行完task,task对象就被析构掉了return Result(sp, false);//返回临时对象,应该自动匹配右值的资源转移,如果编译不通过,把C++标准调高一点}//如果有空余,把任务放入任务队列中taskQue_.emplace(sp);taskSize_++;//因为新放了任务,任务队列肯定不空了,在notEmpty_上进行通知,赶快分配线程执行任务notEmpty_.notify_all();//cached模式 任务处理比较紧急 场景:小而快的任务 需要根据任务数量和空闲线程的数量,判断是否需要创建新的线程出来if (poolMode_ == PoolMode::MODE_CACHED&& taskSize_ > idleThreadSize_&& curThreadSize_ < threadSizeThreshHold_){std::cout << ">>> create new thread..." << std::this_thread::get_id() << " exit!" << std::endl;//创建新的线程对象auto ptr = std::make_unique<Thread>(std::bind(&ThreadPool::threadFunc, this, std::placeholders::_1));int threadId = ptr->getId();threads_.emplace(threadId, std::move(ptr));//threads_.emplace_back(std::move(ptr));//启动线程threads_[threadId]->start();//修改线程个数相关的变量curThreadSize_++;idleThreadSize_++;}//返回任务的Result对象return Result(sp);// return task->getResult();
}//开启线程池
void ThreadPool::start(int initThreadSize)
{//设置线程池的运行状态isPoolRunning_=true;//记录初始线程个数initThreadSize_ = initThreadSize;curThreadSize_ = initThreadSize;//创建线程对象for (int i = 0; i < initThreadSize_; i++){//创建thread线程对象的时候,把线程函数给到thread线程对象auto ptr = std::make_unique<Thread>(std::bind(&ThreadPool::threadFunc, this, std::placeholders::_1));int threadId = ptr->getId();threads_.emplace(threadId, std::move(ptr));//threads_.emplace_back(std::move(ptr));//unique_ptr将左值引用的拷贝构造和赋值都delete了,需要右值(进行资源转移)}//启动所有线程 std::vector<Thread*> threads_;for (int i = 0; i < initThreadSize_; i++){threads_[i]->start(); //需要去执行一个线程函数idleThreadSize_++;//记录初始空闲线程的数量}
}//定义线程函数 线程池的所有线程从任务队列里面消费任务
void ThreadPool::threadFunc(int threadid) //线程函数返回,相应的线程也就结束了
{/*std::cout << "begin threadFunc tid:" << std::this_thread::get_id() << std::endl;std::cout << "end threadFunc tid:" << std::this_thread::get_id() << std::endl;*/auto lastTime = std::chrono::high_resolution_clock().now();while (isPoolRunning_){std::shared_ptr<Task> task;{//先获取锁std::unique_lock<std::mutex> lock(taskQueMtx_);std::cout << "tid:" << std::this_thread::get_id()<< "尝试获取任务..." << std::endl;//cached模式下,有可能已经创建了很多的线程,但是空闲时间超过60s,应该把多余的线程结束回收掉(超过initThreadSize_数量的线程要进行回收)//当前时间-上一次线程执行的时间>60s//每一秒中返回一次 怎么区分:超时返回?还是有任务待执行返回while (taskQue_.size() == 0){if (poolMode_ == PoolMode::MODE_CACHED){//条件变量,超时返回了if (std::cv_status::timeout == notEmpty_.wait_for(lock, std::chrono::seconds(1))){auto now = std::chrono::high_resolution_clock().now();auto dur = std::chrono::duration_cast<std::chrono::seconds>(now - lastTime);if (dur.count() >= THREAD_MAX_IDLE_TIME && curThreadSize_ > initThreadSize_){//开始回收当前线程//记录线程数量的相关变量的值修改//把线程对象从线程列表容器中删除 没有办法 threadFunc <=>thread对象//threadid=>thread对象=》删除threads_.erase(threadid);// 这个id不是std::this_thread::getid() 是自己生成的,我们自定义的curThreadSize_--;idleThreadSize_--;std::cout << "threadid:" << std::this_thread::get_id() << "exit!" << std::endl;return;}}}else{//等待notEmpty条件notEmpty_.wait(lock);}//线程池结束,回收线程资源if (!isPoolRunning_){threads_.erase(threadid);// 这个id不是std::this_thread::getid() 是自己生成的,我们自定义的std::cout << "threadid:" << std::this_thread::get_id() << "exit!" << std::endl;exitCond_.notify_all();return;}}idleThreadSize_--;//唤醒线程工作,空闲线程-1std::cout << "tid:" << std::this_thread::get_id()<< "获取任务成功..." << std::endl;//从任务队列中取一个任务出来task = taskQue_.front();taskQue_.pop();taskSize_--;//如果依然有剩余任务,继续通知其它的线程执任务if (taskQue_.size() > 0){notEmpty_.notify_all();}//取出一个任务,进行通知,通知可以继续提交生产任务notFull_.notify_all();}//就应该把锁释放掉//当前线程负责执行这个任务if (task != nullptr){//task->run();//执行任务;把任务的返回值setVal方法给到Resulttask->exec();}idleThreadSize_++;//线程执行完任务,空闲线程+1lastTime = std::chrono::high_resolution_clock().now();//更新线程执行完任务的时间}threads_.erase(threadid);// 这个id不是std::this_thread::getid() 是自己生成的,我们自定义的std::cout << "threadid:" << std::this_thread::get_id() << "exit!" << std::endl;exitCond_.notify_all();
}bool ThreadPool::checkRunningState() const
{return isPoolRunning_;
}/// 线程方法实现
int Thread::generateId_ = 0;//线程构造
Thread::Thread(ThreadFunc func):func_(func),threadId_(generateId_++)
{}//线程析构
Thread::~Thread(){}//启动线程
void Thread::start()
{//创建一个线程来执行一个线程函数std::thread t(func_, threadId_);//C++11来说 线程对象t 和线程函数func_t.detach();//设置分离线程,线程对象t出作用域会析构,但是线程函数不能结束否则程序会挂掉,所以要将线程分离出去,做到二者互不影响//pthread_detach pthread_t设置成分离线程//主线程要用pthread_join回收线程,防止孤儿线程的出现} //获取线程id
int Thread::getId()const
{return threadId_;
}/// Task方法实现
Task::Task():result_(nullptr)
{}void Task::exec()
{result_->setVal(run());//这里发生多态调用
}void Task::setResult(Result* res)
{result_ = res;
}/// Result方法的实现
Result::Result(std::shared_ptr<Task> task, bool isValid):isValid_(isValid),task_(task)
{task_->setResult(this);
}Any Result::get() // 用户调用的
{if (!isValid_){return "";}//task任务如果没有执行完,这里会阻塞用户的线程sem_.wait();//用户调用get时,如果任务在线程池中,还没有被执行完,那么调用get方法的线程就会阻塞住return std::move(any_);//右值引用
}void Result::setVal(Any any)//谁调用的呢??
{//存储task的返回值this->any_ = std::move(any);sem_.post();//已经获取的任务的返回值,增加信号量资源
}
我们的资源回收代码如下:
//线程池析构
ThreadPool::~ThreadPool()
{isPoolRunning_ = false;notEmpty_.notify_all();//等待线程池里面所有的线程返回 有两种状态:阻塞 & 正在执行任务中std::unique_lock<std::mutex> lock(taskQueMtx_);exitCond_.wait(lock, [&]()->bool {return threads_.size() == 0; });
}
现在,有的线程没有被回收,线程队列中还有线程,所以就一直阻塞等待了。
线程池的那个线程为什么没有被回收掉?
(时而出现,时而不出现的问题)
我们通过在windows上调试:



我们通过在Linux上进行gdb调试
主要通过gdb attach到正在运行的进程,通过info threads,thread tid,bt等命令查看各个线程的调用堆栈信息,结合项目代码,定位到发生死锁的代码片段,分析死锁问题发生的原因
2.分析问题


原先针对上面的2种情况的处理方法如下:


第3种情况:
有的线程执行完任务,又进入while循环了

在这里有2种情况:
1、pool线程先获取到锁,线程池的线程获取不到锁,阻塞。
此时pool线程看wait条件,size>0,不满足条件,就进入等待wait状态了,并且把互斥锁mutex释放掉。
线程池的线程就获取到锁了,发现任务队列没有任务了,这个任务就在notEmpty条件变量上wait,但是此时pool线程没有办法再对这个条件变量notify了。
发生死锁了!!!
2、线程池里的线程先获取到锁,发生任务队列为空,在条件变量notEmpty上wait了,释放锁,然后pool线程抢到锁,只是看exitCond条件变量的wait条件,看size还是大于0,还是死锁了。
解决方法:pool线程获取到锁后再notify
//线程池析构
ThreadPool::~ThreadPool()
{isPoolRunning_ = false;//等待线程池里面所有的线程返回 有两种状态:阻塞 & 正在执行任务中std::unique_lock<std::mutex> lock(taskQueMtx_);notEmpty_.notify_all();exitCond_.wait(lock, [&]()->bool {return threads_.size() == 0; });
}
我们在消费者线程进行锁+双重判断:
//定义线程函数 线程池的所有线程从任务队列里面消费任务
void ThreadPool::threadFunc(int threadid)//线程函数返回,相应的线程也就结束了
{auto lastTime = std::chrono::high_resolution_clock().now();//所有任务必须执行完成,线程池才可以回收所有线程资源for (;;){std::shared_ptr<Task> task;{//先获取锁,我们要注意控制锁的范围,取完任务,就释放锁std::unique_lock<std::mutex> lock(taskQueMtx_);std::cout << "tid:" << std::this_thread::get_id()<< "尝试获取任务..." << std::endl;//cached模式下,有可能已经创建了很多的线程,但是空闲时间超过60s,应该把多余的线程//结束回收掉(超过initThreadSize_数量的线程要进行回收)//当前时间 - 上一次线程执行的时间 > 60s//每一秒中返回一次 怎么区分:超时返回?还是有任务待执行返回//锁 + 双重判断while (taskQue_.size() == 0){//线程池要结束,回收线程资源if (!isPoolRunning_){threads_.erase(threadid);//std::this_thread::getid()std::cout << "threadid:" << std::this_thread::get_id() << " exit!"<< std::endl;exitCond_.notify_all();return;//线程函数结束,线程结束}if (poolMode_ == PoolMode::MODE_CACHED){//条件变量,超时返回了if (std::cv_status::timeout ==notEmpty_.wait_for(lock, std::chrono::seconds(1))){auto now = std::chrono::high_resolution_clock().now();auto dur = std::chrono::duration_cast<std::chrono::seconds>(now - lastTime);if (dur.count() >= THREAD_MAX_IDLE_TIME&& curThreadSize_ > initThreadSize_)//任务数量大于空闲线程数量{//开始回收当前线程//记录线程数量的相关变量的值修改//把线程对象从线程列表容器中删除 没有办法 threadFunc《=》thread对象//通过threadid => thread对象 => 删除threads_.erase(threadid);//std::this_thread::getid()curThreadSize_--;idleThreadSize_--;std::cout << "threadid:" << std::this_thread::get_id() << " exit!"<< std::endl;return;}}}else{//等待notEmpty条件notEmpty_.wait(lock);}//if (!isPoolRunning_)//{// threads_.erase(threadid);//std::this_thread::getid()// std::cout << "threadid:" << std::this_thread::get_id() << " exit!"// << std::endl;// exitCond_.notify_all();// return;//结束线程函数,就是结束当前线程了!//}}idleThreadSize_--;std::cout << "tid:" << std::this_thread::get_id()<< "获取任务成功..." << std::endl;//从任务队列种取一个任务出来task = taskQue_.front();taskQue_.pop();taskSize_--;//如果依然有剩余任务,继续通知其它得线程执行任务if (taskQue_.size() > 0){notEmpty_.notify_all();}//取出一个任务,进行通知,通知可以继续提交生产任务notFull_.notify_all();} //就应该把锁释放掉//当前线程负责执行这个任务if (task != nullptr){//task->run();//执行任务;把任务的返回值setVal方法给到Result,基类指针调用派生类对象的同名覆盖方法task->exec();//用户还是使用run方法}idleThreadSize_++;lastTime = std::chrono::high_resolution_clock().now();//更新线程执行完任务的时间}
}

相关文章:
死锁问题分析和解决——资源回收时
1.描述问题 在完成线程池核心功能功能时,没有遇到太大的问题(Any,Result,Semfore的设计),在做线程池资源回收时,遇到了死锁的问题 1、在ThreadPool的资源回收,等待线程池所有线程退出时ÿ…...
【Java】效率工具模板的使用
Java系列文章目录 补充内容 Windows通过SSH连接Linux 第一章 Linux基本命令的学习与Linux历史 文章目录 Java系列文章目录一、前言二、学习内容:三、问题描述四、解决方案:4.1 乱码问题4.2 快捷键模板4.3 文件模板 一、前言 提高效率 二、学习内容&am…...
c++指南 -指针和引用
指针和引用 指针的基本概念 指针是存储另一个变量的内存地址的变量。指针变量的声明包括指针类型和星号 (*)。 int* ptr; // ptr 是一个指向 int 类型的指针指针操作 初始化:将指针设置为变量的地址。 int var 10; int* ptr &var; // ptr 现在存储 var 的…...
[CISCN 2023 华北]ez_date
[CISCN 2023 华北]ez_date 点开之后是一串php代码: <?php error_reporting(0); highlight_file(__FILE__); class date{public $a;public $b;public $file;public function __wakeup(){if(is_array($this->a)||is_array($this->b)){die(no array);}if( (…...
前端不同项目使用不同的node版本(Volta管理切换)
前端不同项目使用不同的node版本(Volta管理切换) 使用volta自动切换前端项目的node版本, 每个不同的前端项目,可以使用不同的node版本。Volta这个工具,它允许用户方便地安装、切换和管理不同版本的Node.js,避免了为每个项目手动配…...
Ropdump:针对二进制可执行文件的安全检测工具
关于Ropdump Ropdump是一款针对二进制可执行文件的安全检测工具,该工具基于纯Python开发,是一个命令行工具,旨在帮助广大研究人员检测和分析二进制可执行文件中潜在的ROP小工具、缓冲区溢出漏洞和内存泄漏等安全问题。 功能介绍 1、识别二进…...
Quartz - 定时任务框架集成
参考了若依框架,将quartz定时任务框架集成到自己的项目当中。 目录 一、Quartz概述二、库表创建1.Quartz关键表(11张)表SQL 2.自定义业务表(2张)表SQL 三、代码示例1.依赖引入2.类文件1)定时任务配置类2&am…...
GoModule
GOPATH 最早的就是GOPATH构建模式, go get下载的包都在path中的src目录下 src目录是源代码存放目录。 package mainimport ("net/http""github.com/gorilla/mux" )func main() {r : mux.NewRouter()r.HandleFunc("/hello", func(w h…...
SQL - 数据库管理
保障数据库安全的用户账户和权限问题,当在工作环境中使用MySQL的时候,我们需要创建其他用户账户,并赋予它们特定权限。创建一个用户 create user wolf127.0.0.1 identified by 1234; create user wolf127.0.0.1 identified by 1234;-- 无 …...
密码学之AES算法
文章目录 1. AES简介1.1 AES算法的历史背景1.2 AES算法的应用领域 2. AES加解密流程图2. AES算法原理2.1 AES加密过程2.2 AES解密过程 1. AES简介 1.1 AES算法的历史背景 AES算法,全称为Advanced Encryption Standard(高级加密标准)&#x…...
GitHub每日最火火火项目(8.20)
项目名称:goauthentik / authentik 项目介绍:authentik 是一款提供认证功能的工具,它就像是一个强大的粘合剂,能够满足您在认证方面的各种需求。无论是在安全验证、用户身份管理还是访问控制等方面,它都能发挥重要作用…...
(五)Flink Sink 数据输出
经过上面的 Transformation 操作之后,最终形成用户所需要的结果数据集。通常情况下,用户希望将结果数据输出到外部存储介质或者传输到下游的消息中间件中,在 Flink 中,将 DataStream 数据输出到外部系统的过程被定义为 Sink 操作。 目录 (一)基本数据输出 (二)第三方…...
Spring 注入、注解及相关概念补充
一、Spring DI 的理解 DI ( Dependency Inject,中文释义:依赖注入)是对 IOC 概念不同角度的描述,是指应用程序在运行时,每一个 bean 对象都依赖 IOC 容器注入到当前 bean 对象所需要的另一个 bean 对象。(例如…...
【Linux多线程】线程安全的单例模式
文章目录 1. 单例模式 与 设计模式1.1 单例模式1.2 设计模式1.3 饿汉实现模式 与 懒汉实现模式1.4 饿汉模式① 饿汉模式的特点② 饿汉式单例模式的实现③ 饿汉式单例模式的优缺点④ 适用场景 1.5 懒汉模式① 懒汉式单例模式的特点② 懒汉式单例模式的实现③ 懒汉式单例模式的优…...
基于jqury和canvas画板技术五子棋游戏设计与实现(论文+源码)_kaic
摘 要 网络五子棋游戏如今面临着一些新的挑战和机遇。一方面,网络游戏需要考虑到网络延迟和带宽等因素,保证游戏的实时性和稳定性。另一方面,网络游戏需要考虑到游戏的可玩性和趣味性,以吸引更多的玩家参与。本文基于HTML5和Canv…...
指针 (四)
一 . 指针的使用和传值调用 (1)strlen 的模拟实现 库函数 strlen 的功能是求字符串长度,统计的是字符串中 \0 之前的字符个数,函数原格式如下: 我们的参数 str 接收到一个字符串的起始地址,然后开始统计…...
便利店(超市)管理系统设计与实现(源码+lw+部署文档+讲解等)
文章目录 前言具体实现截图详细视频演示技术栈系统测试为什么选择我官方认证玩家,服务很多代码文档,百分百好评,战绩可查!!入职于互联网大厂,可以交流,共同进步。有保障的售后 代码参考数据库参…...
Excel中的“块”操作
在Excel中,有offset、index、indirect三个对“区域”操作的函数,是较高版本Excel中“块”操作的利器。 (笔记模板由python脚本于2024年08月20日 19:25:21创建,本篇笔记适合喜欢用Excel处理数据的coder翻阅) 【学习的细节是欢悦的历程】 Pytho…...
yolo V8训练 长条状目标
1、说明 目标数据集合中有很多长条状图片,如果直接Resize 会严重拉伸,因此采用把长条图像裁剪成2段,然后将裁剪后的2段图片拼接在一起。 2、代码 2.1 C 代码 (部署,模型推理时C ) #include <stdio.h…...
数据结构与算法 - 设计
1. LRU缓存 请你设计并实现一个满足 LRU (最近最少使用) 缓存 约束的数据结构。 实现 LRUCache 类: LRUCache(int capacity) 以 正整数 作为容量 capacity 初始化 LRU 缓存int get(int key) 如果关键字 key 存在于缓存中,则返回关键字的值࿰…...
【OSG学习笔记】Day 18: 碰撞检测与物理交互
物理引擎(Physics Engine) 物理引擎 是一种通过计算机模拟物理规律(如力学、碰撞、重力、流体动力学等)的软件工具或库。 它的核心目标是在虚拟环境中逼真地模拟物体的运动和交互,广泛应用于 游戏开发、动画制作、虚…...
Oracle查询表空间大小
1 查询数据库中所有的表空间以及表空间所占空间的大小 SELECTtablespace_name,sum( bytes ) / 1024 / 1024 FROMdba_data_files GROUP BYtablespace_name; 2 Oracle查询表空间大小及每个表所占空间的大小 SELECTtablespace_name,file_id,file_name,round( bytes / ( 1024 …...
DAY 47
三、通道注意力 3.1 通道注意力的定义 # 新增:通道注意力模块(SE模块) class ChannelAttention(nn.Module):"""通道注意力模块(Squeeze-and-Excitation)"""def __init__(self, in_channels, reduction_rat…...
Auto-Coder使用GPT-4o完成:在用TabPFN这个模型构建一个预测未来3天涨跌的分类任务
通过akshare库,获取股票数据,并生成TabPFN这个模型 可以识别、处理的格式,写一个完整的预处理示例,并构建一个预测未来 3 天股价涨跌的分类任务 用TabPFN这个模型构建一个预测未来 3 天股价涨跌的分类任务,进行预测并输…...
C/C++ 中附加包含目录、附加库目录与附加依赖项详解
在 C/C 编程的编译和链接过程中,附加包含目录、附加库目录和附加依赖项是三个至关重要的设置,它们相互配合,确保程序能够正确引用外部资源并顺利构建。虽然在学习过程中,这些概念容易让人混淆,但深入理解它们的作用和联…...
【JVM】Java虚拟机(二)——垃圾回收
目录 一、如何判断对象可以回收 (一)引用计数法 (二)可达性分析算法 二、垃圾回收算法 (一)标记清除 (二)标记整理 (三)复制 (四ÿ…...
python爬虫——气象数据爬取
一、导入库与全局配置 python 运行 import json import datetime import time import requests from sqlalchemy import create_engine import csv import pandas as pd作用: 引入数据解析、网络请求、时间处理、数据库操作等所需库。requests:发送 …...
【Linux】Linux安装并配置RabbitMQ
目录 1. 安装 Erlang 2. 安装 RabbitMQ 2.1.添加 RabbitMQ 仓库 2.2.安装 RabbitMQ 3.配置 3.1.启动和管理服务 4. 访问管理界面 5.安装问题 6.修改密码 7.修改端口 7.1.找到文件 7.2.修改文件 1. 安装 Erlang 由于 RabbitMQ 是用 Erlang 编写的,需要先安…...
Android写一个捕获全局异常的工具类
项目开发和实际运行过程中难免会遇到异常发生,系统提供了一个可以捕获全局异常的工具Uncaughtexceptionhandler,它是Thread的子类(就是package java.lang;里线程的Thread)。本文将利用它将设备信息、报错信息以及错误的发生时间都…...
路由基础-路由表
本篇将会向读者介绍路由的基本概念。 前言 在一个典型的数据通信网络中,往往存在多个不同的IP网段,数据在不同的IP网段之间交互是需要借助三层设备的,这些设备具备路由能力,能够实现数据的跨网段转发。 路由是数据通信网络中最基…...
