简介:这个NS-2 LTE仿真环境提供开箱即用的C++队列管理扩展,支持下行/上行方向的虚拟队列调度,内置RED主动队列管理、Drop-Tail被动丢包、S1接口专用队列(dls1queue/uls1queue)和LTE空口队列(dlairqueue/ulairqueue)。核心逻辑封装在ltequeue.cc/h中,配合ns-lib.tcl、lte.tcl等TCL脚本完成MAC层协议建模与链路配置。资源包自带Makefile编译规则,可直接make编译;附带jitter.awk、delay.awk、throughput.awk等分析脚本,一键提取时延、抖动、吞吐量数据;test目录含验证用例,Readme说明基础部署与运行步骤;packet.h/red.h/queue.h等头文件结构清晰,便于二次开发与算法替换。适用于LTE MAC层缓冲区策略对比、队列长度优化、突发流量应对机制研究等场景。
1. 项目概述:为什么在NS-2里“硬刚”LTE队列管理,而不是直接换NS-3?
你可能已经试过NS-3的LTE模块——界面漂亮、文档齐全、支持真实协议栈映射,但真要改一个MAC层的丢包判定逻辑,比如把Drop-Tail换成带ECN标记的RED变种,或者想在S1接口队列里加个基于时延预测的动态阈值调整器,你会发现:得先啃三天EPC架构图,再花一周搞懂LteEnbMac和LteUeMac之间那十几层模板嵌套,最后改完一编译,报错信息里混着GCC模板展开深度超限和Ptr<LteMac>生命周期不匹配两件事,根本分不清是你的逻辑错了,还是NS-3自己没管好内存。
而这个NS-2 LTE仿真扩展包,就是给那些不想被框架绑架、只想聚焦“队列怎么丢包、什么时候丢、丢谁、丢完怎么反馈”这四个核心问题的人准备的。它不是在NS-2上“模拟”LTE,而是用NS-2最原始、最可控的方式——C++类继承+TCL脚本驱动——把LTE MAC层最关键的缓冲区行为,一层一层剥开给你看。它不假装自己是完整基站,但它让你能精确控制从PDCP层下来的数据包,在进入S1接口前、在调度进空口前、在真正发到无线信道前,每一步的排队、标记、丢弃、重排序行为。
关键词里提到的“NS-2 LTE”,其实是个务实的命名:它不追求全协议栈兼容,只锚定在S1接口(EPC侧)与空口(UE侧)之间的双层缓冲区建模这一关键断面;“RED算法”在这里不是教科书里的理论曲线,而是可调参数、可观测队列长度、可抓包验证ECN标记是否生效的实体;“队列管理”直指LTE中长期被忽略的隐性瓶颈——eNodeB内部缓冲区膨胀导致的时延抖动恶化,尤其在VoLTE或工业控制小包突发场景下,比吞吐量下降更致命;“LTE MAC”在这里被解耦为两个独立可替换的调度单元:S1侧队列负责承载级(bearer-level)流量整形,空口队列负责用户级(UE-level)资源竞争公平性;而“S1接口队列”则是整个设计的支点——它让研究者第一次能在仿真中区分“核心网来的压力”和“无线信道造成的拥塞”,不再把所有延迟都归咎于“信道差”。
我去年用它复现3GPP TR 36.814里关于“缓冲区大小对VoLTE MOS评分影响”的结论时,发现官方报告里一笔带过的“建议S1队列长度设为50ms等效缓冲”,在实际仿真中必须结合终端上报的CQI和eNodeB调度周期重新校准——因为50ms在10MHz带宽下是20个RB,在20MHz下就变成40个RB,缓冲区满溢点完全不同。这种颗粒度的控制,只有在NS-2这种“手把手捏代码”的环境里才做得到。它不适合写毕业论文封面图,但特别适合写进论文方法论章节的“实验平台”小节——因为你清楚知道每一行代码在干什么,每一个TCL命令在触发哪个C++对象的方法。
2. 整体架构与设计逻辑:双层队列不是叠加,而是职责分离
2.1 为什么必须是“S1接口队列 + 空口队列”双层结构?
先说一个反直觉的事实:在真实eNodeB中,S1接口(连接MME/S-GW)和空口(连接UE)之间,并不存在一个统一的大缓冲池。它们是物理隔离的——S1侧走以太网/光纤,空口侧走基带处理单元(BBU),中间隔着调度器、HARQ实体、RLC重传队列等多个逻辑层。很多仿真模型把这两者合并成一个“eNodeB Queue”,看似简化,实则掩盖了关键矛盾:当核心网突发大量小包(如HTTP请求)涌入时,S1队列会快速堆积,但此时空口可能因信道质量差而调度缓慢,导致S1队列持续高水位;而当空口遭遇干扰导致大量HARQ重传时,空口队列会堵塞,但S1队列可能还很空——这时如果只监控总队列长度,你会误判网络状态。
这个扩展包的双层设计,正是为了还原这种隔离性。它的数据流向是严格的单向流水线:
[Core Network]
↓ (GTP-U隧道)
[S1 Interface Queue: dls1queue/uls1queue] ← 可配置RED/Drop-Tail,决定是否丢弃/标记来自核心网的包
↓ (调度决策:按Bearer QCI/ARP优先级)
[MAC Scheduler] ← 不在此包中实现,由ns-lib.tcl中的tcl调度器模拟
↓ (分配RB资源后)
[Air Interface Queue: dlairqueue/ulairqueue] ← 可配置RED/Drop-Tail,决定是否丢弃/标记即将发往UE的包
↓ (经PHY层编码调制)
[Wireless Channel]
提示:注意
dls1queue和dlairqueue的命名差异——前者是“downlink S1 queue”,后者是“downlink air queue”。这种命名不是为了炫技,而是强制你在写TCL脚本时,必须显式声明每个队列挂载在哪条链路上。比如在ns-lib.tcl里,你必须写:
tcl $ns_ attach-agent $enb $s1_down_agent $s1_down_agent set queue_ [new Queue/dls1queue] $ns_ attach-agent $enb $air_down_agent $air_down_agent set queue_ [new Queue/dlairqueue]
这种“显式绑定”杜绝了逻辑混淆,也方便你后续做A/B测试——比如固定dls1queue用RED,只对比dlairqueue用Drop-Tail和用RED的效果差异。
2.2 RED算法的轻量化实现:为什么不用标准NS-2的RED类?
NS-2自带的Queue/RED类是为TCP友好型拥塞控制设计的,它假设队列后端连接的是TCP源,因此内置了avg_(平均队列长度)、q_w_(权重)、minth_/maxth_(最小/最大阈值)等参数,并依赖drop_标志位触发随机丢包。但在LTE场景下,这个假设完全不成立:VoLTE流是UDP+RTP,没有重传机制;工业控制流可能是自定义协议,丢包即业务中断;而eMBB流虽然有TCP,但其RTT受无线信道主导,远大于有线网络。
所以这个包里的red.cc做了三处关键改造:
-
移除TCP相关钩子:删掉了所有
tcp_指针引用、set tcp_方法、以及update_avg()中对tcp_->rtt()的调用。avg_现在纯粹是队列长度的指数滑动平均,计算公式简化为:
cpp avg_ = q_w_ * cur_len + (1 - q_w_) * avg_;
其中cur_len是当前瞬时队列长度(单位:packet),q_w_默认设为0.002(对应约500包的平滑窗口),可通过TCL脚本用$queue set q_w_ 0.005动态调整。 -
增加ECN标记支持开关:新增
ecn_enable_布尔变量,默认关闭。开启后,当avg_介于minth_和maxth_之间时,不丢包,而是设置IP头的ECN字段为ECT(1)(Explicit Congestion Notification),并记录标记次数到ecn_marked_计数器。这让你能仿真“ECN-enabled VoLTE”场景,观察标记率与端到端时延的关系。 -
引入burst tolerance机制:新增
burst_th_参数(默认10 packets)。当连续burst_th_个包入队且avg_未超minth_时,强制将avg_抬升至minth_,模拟突发流量对队列的冲击惯性。这是对标准RED的实用化补丁——真实无线网络中,UE上报的CQI变化有延迟,调度器无法瞬时响应信道突变,这个burst_th_就是模拟这种“反应滞后”。
注意:
red.h头文件里明确标注了// NOT compatible with NS-2's built-in Queue/RED。如果你试图把dls1queue设为[new Queue/RED],编译会失败,因为red.cc重写了enque()和deque()的底层逻辑,与NS-2原生RED的drop_回调机制冲突。这是设计上的主动隔离,不是bug。
2.3 ltequeue.cc:双层队列的中枢调度器
如果说dls1queue.cc和dlairqueue.cc是肌肉,那么ltequeue.cc就是神经系统。它不直接处理数据包,而是协调两个队列之间的“节拍”:
-
入队分流:当
$enb收到一个下行包(来自S-GW),ltequeue::enque()首先解析包头中的Bearer ID和QCI值,然后根据预设的映射表(在ns-lib.tcl中配置),决定该包应进入dls1queue还是直通到dlairqueue(例如QCI=1的VoLTE流允许绕过S1队列,减少一级缓冲延迟)。 -
出队协同:
ltequeue::deque()不是简单地从某个队列取包,而是执行一个“双检查”流程:
1. 先检查dlairqueue是否已满(dlairqueue->length() >= air_max_),若满则返回NULL,强制上游暂停发送;
2. 若不满,再检查dls1queue是否有包(dls1queue->length() > 0),若有,则dls1queue->deque()取出一个包,封装成MAC PDU,再dlairqueue->enque()注入空口队列;
3. 最后dlairqueue->deque()输出真正的无线帧。
这个流程确保了dlairqueue永远是“最后一道闸门”,其长度直接反映空口实时负载,而dls1queue的长度则反映核心网侧压力。你可以通过awk脚本分别提取两个队列的长度时间序列,画出“压力传导图”,直观看到拥塞是如何从S1侧传播到空口侧的。
实操心得:我在调试初期曾把
air_max_设得过大(比如200 packets),结果发现dlairqueue常年满负荷,dls1queue却几乎为空——这意味着所有压力都被空口队列消化了,S1队列形同虚设。后来我把air_max_调到30(约对应10ms空口缓冲),dls1queue才开始出现有意义的波动。这个参数没有标准值,必须根据你的仿真带宽、调度周期和业务模型反复校准。
3. 核心模块详解与实操要点
3.1 S1接口队列(dls1queue/uls1queue):承载级流量整形的主战场
S1接口队列的核心使命,是在核心网与无线接入网之间建立一个可控的“压力缓冲垫”。它不解决无线信道质量问题,但能防止核心网的流量洪峰直接冲垮eNodeB的调度器。
dls1queue.cc的实现非常精炼,主体逻辑集中在enque()方法:
int DLS1Queue::enque(Packet* p) {
// Step 1: 获取包的Bearer ID和QCI
hdr_cmn* ch = hdr_cmn::access(p);
int bearer_id = ch->bearer_id(); // 从自定义包头提取
int qci = ch->qci();
// Step 2: 检查队列长度是否超限(硬限制)
if (length() >= max_len_) {
drop(p); // 直接丢弃,不标记
return -1;
}
// Step 3: RED逻辑判断(仅当ecn_enable_开启且队列未满时)
if (ecn_enable_ && avg_ > minth_ && avg_ < maxth_) {
double prob = calculate_pmark(avg_); // 标准RED丢包概率公式
if (Random::uniform() < prob) {
// 标记ECN而非丢包
hdr_ip* iph = hdr_ip::access(p);
iph->ecn() = IP_ECN_ECT1; // 设置ECN字段
ecn_marked_++;
}
}
// Step 4: 入队
return Queue::enque(p);
}
关键参数说明(全部可通过TCL脚本配置):
| 参数名 | 默认值 | 物理意义 | 调优建议 |
|---|---|---|---|
max_len_ | 100 | 队列最大容量(packets) | 对应约50ms缓冲(按平均包长1200Byte,下行峰值速率20Mbps估算)。VoLTE场景建议30-50,eMBB可设80-120 |
minth_ | 20 | RED最小阈值(packets) | 应大于典型业务的RTT内入队包数。VoLTE(RTT≈50ms)建议设为15-25 |
maxth_ | 60 | RED最大阈值(packets) | 通常为minth_的2-3倍。避免设得过高,否则RED失去主动管理意义 |
q_w_ | 0.002 | 平滑权重 | 值越小,avg_响应越慢,抗突发干扰强;值越大,响应越快,但易震荡。建议0.001-0.005区间 |
ecn_enable_ | 0 | 是否启用ECN标记 | VoLTE/IMS业务必开;普通TCP业务可关,避免端侧不识别ECN |
注意:
uls1queue(上行S1队列)逻辑与dls1queue对称,但参数建议不同——上行带宽通常只有下行的1/3,且UE发射功率受限,因此max_len_建议设为下行的1/2,minth_也相应下调。我在test目录下的ul_burst_test.tcl里专门做了上行突发压力测试,证实了这一点。
3.2 空口队列(dlairqueue/ulairqueue):用户级调度公平性的最终裁判
如果说S1队列管“流量来源”,空口队列就管“服务对象”。dlairqueue.cc的设计哲学是:它不关心包来自哪个Bearer,只关心这个包属于哪个UE,以及当前UE的调度优先级。
它的enque()方法会解析包头中的UE_ID,并维护一个map<UE_ID, queue_length>的实时映射。更重要的是,它的deque()方法不是FIFO,而是配合TCL调度器实现的“伪轮询”:
Packet* DLAirQueue::deque() {
// Step 1: 获取当前调度器选中的UE_ID(由ns-lib.tcl中的scheduler传递)
int scheduled_ue = scheduler_->get_scheduled_ue();
// Step 2: 在该UE的私有子队列中取包(FIFO)
Packet* p = ue_queues_[scheduled_ue]->deque();
// Step 3: 若该UE子队列为空,尝试从其他UE队列借包(仅当enable_borrow_=1)
if (!p && enable_borrow_) {
p = borrow_from_other_ues();
}
return p;
}
这意味着,即使你配置了dlairqueue用Drop-Tail,它的丢包行为也是“按UE粒度”发生的——某个UE的子队列满了,只会丢该UE的包,不会影响其他UE。这完美契合LTE中“每个UE有独立HARQ进程和RLC重传队列”的物理事实。
dlairqueue的关键参数:
| 参数名 | 默认值 | 物理意义 | 调优建议 |
|---|---|---|---|
ue_max_len_ | 20 | 单个UE子队列最大长度 | 决定单UE最大缓冲。VoLTE建议10-15(防语音包堆积),eMBB可设20-30 |
enable_borrow_ | 0 | 是否允许UE间借包 | 开启后可提升频谱利用率,但会削弱调度公平性。研究公平性时务必关闭 |
drop_policy_ | 0 | 丢包策略:0=Tail-Drop, 1=RED | 注意:此处的RED是针对单UE子队列的,minth_/maxth_也按单UE设置 |
实操心得:
ulairqueue(上行空口队列)有个隐藏陷阱——它的enque()会检查UE_ID是否在合法列表中(valid_ues_),如果UE未完成RRC连接建立,包会被静默丢弃。我在跑handover_test.tcl时,发现切换过程中有短暂丢包,追踪发现是ulairqueue在目标eNodeB上还未收到该UE的RRC Connection Reconfiguration Complete消息,valid_ues_里没有这个ID。解决方案是在TCL脚本中,为handover场景添加$ulairqueue add-valid-ue $ue_id命令,手动注入。
3.3 ltequeue.h/cc:如何让双层队列“呼吸同步”
ltequeue不是简单的队列容器,而是一个状态协调器。它的头文件ltequeue.h定义了几个关键状态变量:
class LTEQueue : public Queue {
public:
// ... 构造函数等
void set_dls1_queue(Queue* q) { dls1_queue_ = q; } // 绑定S1队列
void set_dlair_queue(Queue* q) { dlair_queue_ = q; } // 绑定空口队列
void set_scheduler(Scheduler* s) { scheduler_ = s; } // 绑定调度器
// 状态查询接口(供TCL脚本调用)
int dls1_length() { return dls1_queue_->length(); }
int dlair_length() { return dlair_queue_->length(); }
double dls1_avg() { return ((DLS1Queue*)dls1_queue_)->avg_; }
double dlair_avg() { return ((DLAirQueue*)dlair_queue_)->avg_; }
protected:
Queue* dls1_queue_; // S1接口队列指针
Queue* dlair_queue_; // 空口队列指针
Scheduler* scheduler_; // 调度器指针
};
这些set_xxx()和xxx_length()方法,使得你在lte.tcl中可以写出这样的逻辑:
# 创建双层队列实例
set lteq [new Queue/ltequeue]
$lteq set_dls1_queue [new Queue/dls1queue]
$lteq set_dlair_queue [new Queue/dlairqueue]
# 动态调整S1队列参数(模拟网络拥塞时的自适应)
$ns_ at 5.0 "$lteq set_dls1_param minth_ 30"
$ns_ at 10.0 "$lteq set_dls1_param maxth_ 80"
# 在仿真中实时打印双队列长度
$ns_ at 1.0 "puts \"Time 1.0: S1=$lteq dls1_length(), Air=$lteq dlair_length()\""
提示:
ltequeue.cc里有一个容易被忽略的细节——它的enque()方法会自动更新dls1_queue_的avg_,但deque()方法只更新dlair_queue_的avg_。这意味着,如果你想监控S1队列的平均长度变化,必须在TCL中用$lteq dls1_avg(),而不是去读dls1queue自己的avg_变量(因为dls1queue的avg_只在enque()时更新,deque()不参与计算)。这个设计是为了保证avg_始终反映“入队压力”,而非“净负载”。
4. 编译、部署与性能分析全流程
4.1 从零开始编译:避开NS-2经典坑位
这个包的Makefile已经适配主流Linux发行版(Ubuntu 18.04+/CentOS 7+),但仍有三个高频踩坑点必须手动处理:
坑位1:NS-2版本兼容性
- 该包基于NS-2.35开发,不兼容NS-2.34及更早版本(因packet.h中新增了bearer_id和qci字段定义)。
- 如果你系统里装的是NS-2.34,不要试图打补丁,直接下载NS-2.35源码重新编译:
bash wget https://sourceforge.net/projects/nsnam/files/allinone/ns-allinone-2.35/ns-allinone-2.35.tar.gz tar -xzf ns-allinone-2.35.tar.gz cd ns-allinone-2.35 # 先备份原版ns-2.35的queue/目录 mv ns-2.35/queue/ ns-2.35/queue-original # 将本包的queue/目录复制进去 cp -r /path/to/your/lte-package/queue/ ns-2.35/ # 编译(会自动链接本包的C++文件) ./install
坑位2:C++11支持
- red.cc中使用了std::round()和std::exp(),需要GCC 4.8+。Ubuntu 16.04默认GCC 5.4,没问题;但CentOS 7默认GCC 4.8.5,需确认:
bash gcc --version | grep "4.8\|4.9\|5\|6\|7\|8\|9\|10"
若低于4.8,升级GCC:
bash sudo yum install centos-release-scl sudo yum install devtoolset-7-gcc* scl enable devtoolset-7 bash
坑位3:TCL路径问题
- 编译后,ns可执行文件在ns-allinone-2.35/ns-2.35/ns,但运行时会找ns-lib.tcl等脚本。确保你的工作目录包含所有TCL文件,或设置环境变量:
bash export NS_LIBRARY=/path/to/your/lte-package/
编译成功后,验证命令:
./ns test/basic_test.tcl
# 应输出类似:
# Starting LTE simulation...
# Simulation completed. Output in out.tr
# Queue stats: S1_avg=12.3, Air_avg=8.7, ECN_marked=42
4.2 运行一个标准测试:VoLTE并发呼叫仿真
test/volte_call.tcl是为VoLTE优化的验证用例,它模拟了5个UE同时发起VoLTE呼叫,每个呼叫产生恒定码率(CBR)的RTP流(64kbps,包长160Bytes,间隔20ms)。
关键TCL配置段解读:
# 创建eNodeB和5个UE
set enb [$ns_ node]
for {set i 0} {$i < 5} {incr i} {
set ue($i) [$ns_ node]
# 配置无线链路(简化版,实际需加信道模型)
$ns_ duplex-link $enb $ue($i) 100Mb 2ms DropTail
}
# 为eNodeB配置双层队列
set lteq [new Queue/ltequeue]
$lteq set_dls1_queue [new Queue/dls1queue]
$lteq set_dlair_queue [new Queue/dlairqueue]
# S1队列:VoLTE专用,小缓冲+ECN开启
$lteq set_dls1_param max_len_ 30
$lteq set_dls1_param minth_ 15
$lteq set_dls1_param maxth_ 45
$lteq set_dls1_param ecn_enable_ 1
# 空口队列:按UE隔离,防语音包堆积
$lteq set_dlair_param ue_max_len_ 12
$lteq set_dlair_param enable_borrow_ 0
# 将队列挂载到eNodeB的下行代理
set sink [new Agent/LTE/Sink]
$sink set queue_ $lteq
$ns_ attach-agent $enb $sink
# 启动5路VoLTE流
for {set i 0} {$i < 5} {incr i} {
set udp($i) [new Agent/UDP]
set cbr($i) [new Application/Traffic/CBR]
$cbr($i) set packetSize_ 160
$cbr($i) set interval_ 0.02 ;# 20ms
$cbr($i) attach-agent $udp($i)
$ns_ connect $udp($i) $sink
}
运行命令:
./ns test/volte_call.tcl > volteresult.log 2>&1
4.3 性能分析三剑客:jitter.awk、delay.awk、throughput.awk
这三个awk脚本是本包的精华,它们直接解析NS-2的trace文件(out.tr),无需额外工具。
delay.awk:端到端时延分析
awk -f delay.awk out.tr > delay.dat
输出格式:time(us) src_node dst_node seq_no delay(ms)
- 它只统计+(入队)和d(出队)事件,计算时间差,自动过滤掉被丢弃的包(无d事件)。
- 关键改进:对VoLTE流,它会按seq_no % 50分组(假设50包为1秒语音帧),计算每帧的平均时延和最大时延,直接对标3GPP TS 26.114的VoLTE时延要求(单向≤100ms)。
jitter.awk:时延抖动(Jitter)计算
awk -f jitter.awk out.tr > jitter.dat
输出格式:time(us) src_node dst_node seq_no jitter(ms)
- 使用RFC 3550定义的抖动计算:J(i) = |D(i) - D(i-1)|,其中D(i)是第i包的端到端时延。
- 输出中会标记“jitter spike”(抖动突增点),便于定位调度异常时刻。
throughput.awk:吞吐量分粒度统计
awk -f throughput.awk out.tr > throughput.dat
输出格式:time_interval(s) bearer_id throughput(kbps)
- 支持按bearer_id(承载ID)分组统计,这是LTE仿真的刚需——你能清晰看到QCI=1(VoLTE)和QCI=9(默认互联网)的吞吐量此消彼长关系。
- 时间间隔可配置(默认1秒),支持-v interval=0.1参数设为100ms粒度,捕捉突发流量。
实操心得:我曾用
throughput.awk发现一个隐蔽问题——在volte_call.tcl中,5路VoLTE流共用同一个Agent/LTE/Sink,导致throughput.dat里所有包的bearer_id都是0。后来在ltequeue.cc里加了一行:ch->bearer_id() = $i + 1(在创建流时注入),才实现真正的按承载统计。这个细节提醒我们:awk脚本再强大,也依赖trace文件里有正确的字段。
5. 常见问题与排查技巧实录
5.1 编译错误:“‘hdr_cmn’ has no member named ‘bearer_id’”
现象:make时报错,指向red.cc或dls1queue.cc中访问ch->bearer_id()的行。
原因:packet.h未被正确包含,或NS-2源码中的packet.h未被本包的packet.h覆盖。
排查步骤:
1. 确认ns-allinone-2.35/ns-2.35/packet.h是否已被替换为本包的版本(检查是否有int bearer_id_; int qci_;字段声明)。
2. 检查Makefile中INCLUDES路径是否包含-I.(当前目录),确保编译时优先读取本地packet.h。
3. 清理并重编译:
bash cd ns-allinone-2.35/ns-2.35 make clean ./configure make
5.2 仿真运行时崩溃:“Segmentation fault (core dumped)”
现象:./ns test/basic_test.tcl运行几秒后崩溃,无明确错误信息。
原因:最常见于ltequeue对象未正确绑定子队列,导致dls1_queue_指针为NULL,后续调用dls1_queue_->length()时解引用空指针。
排查技巧:
- 在ltequeue.cc的enque()开头加日志:
cpp printf("LTEQueue::enque: dls1_queue_=%p, dlair_queue_=%p\n", dls1_queue_, dlair_queue_); fflush(stdout);
- 运行时重定向输出:./ns test/basic_test.tcl 2>&1 | tee debug.log,查看日志中指针是否为0x0。
- 修复:确保TCL脚本中set_dls1_queue和set_dlair_queue在enque操作前已执行。basic_test.tcl里有一行$ns_ at 0.1 "$lteq set_dls1_queue $dls1q",这个0.1秒延迟是故意的——给NS-2对象创建留出时间,不要删掉。
5.3 trace文件中无ECN标记记录
现象:delay.awk输出的delay.dat里,所有包的delay值正常,但out.tr中看不到ecn字段变化。
原因:ECN标记只修改IP头,而NS-2默认trace不记录IP头字段。你需要启用详细trace。
修复方案:
在TCL脚本中,创建节点后添加:
$ns_ trace-all $trace
# 启用IP头trace(关键!)
$ns_ trace-queue $enb $ue(0) $trace
$ns_ trace-queue $ue(0) $enb $trace
# 并在创建Agent时指定详细trace
set udp0 [new Agent/UDP]
$udp0 set trace_ true ;# 此行启用IP头trace
然后重新运行,out.tr中会出现类似+ 0.123456 0 1 tcp 1000 ------- 0 1.0 2.0 0 0的行,其中0 0部分会变为1 0(表示ECN=ECT1)。
5.4 双层队列长度始终为0
现象:$lteq dls1_length()和$lteq dlair_length()在TCL中打印始终为0,但流量实际在传输。
原因:ltequeue的enque()/deque()未被正确调用,流量绕过了双层队列,直接走NS-2默认队列。
排查清单:
- ✅ 检查ns-lib.tcl中,$enb的下行代理(Agent/LTE/Sink)是否设置了set queue_ $lteq。
- ✅ 检查Agent/LTE/Sink是否被正确attach-agent到$enb。
- ✅ 检查$ns_ connect命令中,源Agent(如Agent/UDP)是否连接到了Agent/LTE/Sink,而不是直接连到$ue。
- ✅ 在ltequeue.cc的enque()和deque()中加printf("LTEQueue::enque called\n"),确认是否被调用。
常见错误:在
test/basic_test.tcl中,有人把$ns_ connect $udp $sink写成了$ns_ connect $udp $ue(0),导致UDP包直接发给UE,跳过了eNodeB的ltequeue。这是新手最高频失误。
5.5 throughput.awk输出吞吐量为0
现象:throughput.dat中所有行的throughput值都是0。
原因:throughput.awk依赖trace文件中的d(drop)和r(receive)事件来计算字节数,但如果仿真中没有r事件(即包未被接收端Agent接收),则无法统计。
修复:
- 确保接收端Agent(如Agent/LTE/Sink)已正确创建并attach-agent。
- 在TCL中,为Sink Agent启用接收统计:
tcl $sink set traffic_ true ;# 启用流量统计 $sink set verbose_ true ;# 输出详细日志
- 检查out.tr中是否有r事件:搜索r 0.123456 1 0 tcp 1000 ------- 0 1.0 2.0 0 0。
6. 二次开发指南:如何安全替换你的自定义队列算法
这个包的价值不仅在于开箱即用,更在于它为你铺好了算法替换的“快车道”。所有队列类都遵循统一的Queue基类接口,你只需关注三个方法:
6.1 替换步骤(以实现一个PI控制器队列为例)
Step 1:新建文件 pi-queue.cc/h
// pi-queue.h
#ifndef PI_QUEUE_H
#define PI_QUEUE_H
#include "queue.h"
class PIQueue : public Queue {
public:
PIQueue();
int enque(Packet* p);
Packet* deque();
protected:
double integral_; // 积分项
double error_prev_; // 上次误差
double Kp_, Ki_; // 控制参数
};
#endif
Step 2:在Makefile中添加编译规则
# 找到 OBJS 行,添加 pi-queue.o
OBJS = \
queue.o \
red.o \
drop-tail.o \
dls1queue.o \
dlairqueue.o \
ltequeue.o \
pi-queue.o \ # 新增这一行
...
Step 3:在ns-lib.tcl中注册新类
# 在文件末尾添加
Class Queue/PIQueue -superclass Queue
Queue/PIQueue instproc init args {
$self next $args
$self instvar Kp_ Ki_
$Kp_ 1.0
$Ki_ 0.01
}
Step 4:在TCL脚本中使用
set lteq [new Queue/ltequeue]
$lteq set_dls1_queue [new Queue/PIQueue] ;# 替换S1队列
$lteq set_dlair_queue [new Queue/dlairqueue]
注意:
PIQueue必须继承Queue,不能继承DLS1Queue或DLAirQueue,因为后两者有特定的LTE语义(如Bearer ID解析)。通用算法应基于Queue基类。
6.2 头文件依赖规范:为什么不能随便include
packet.h是本包的“宪法”,它定义了所有LTE特有字段:
struct hdr_lte {
int bearer_id_; // 承载ID
int qci_; // QoS Class Identifier
int ue_id_; // UE ID
int rlc_mode_; // RLC模式:0=AM, 1=UM
};
所有队列类(red.cc, dls1queue.cc等)都通过hdr_cmn访问这些字段:
hdr_cmn* ch = hdr_cmn::access(p);
hdr_lte* lteh = (hdr_lte*)ch->data();
int qci = lteh->qci_;
因此,你的pi-queue.cc必须包含:
#include "packet.h" // 必须!提供hdr_lte定义
#include "queue.h" // 必须!提供Queue基类
但严禁包含red.h或dls1queue.h——它们是具体实现,不是接口。保持依赖树干净,是多人协作和算法替换的基础。
6.3 调试技巧:如何验证你的算法真的在运行
最可靠的方法,是在你的队列类中加入可被TCL读取的状态变量:
// pi-queue.cc
int PIQueue::enque(Packet* p) {
// ... 你的PI逻辑
control_output_ = Kp_ * error_ + Ki_ * integral_; // 计算控制输出
// 记录到公共变量,供TCL读取
Tcl& tcl = Tcl::instance();
tcl.evalf("set pi_control_output %.3f", control_output_);
return Queue::enque(p);
}
然后在TCL中:
$ns_ at 1.0 "puts \"PI Control Output: $pi_control_output\""
这样,你不需要打开out.tr,就能实时看到算法的控制信号输出,快速验证逻辑正确性。
我个人在实际使用中发现,这个包最强大的地方,不是它内置的RED或Drop-Tail,而是它把LTE MAC层的“缓冲区决策权”从黑盒调度器中解放了出来。当你能亲手调整dls1queue的minth_,看着delay.dat里VoLTE时延从120ms降到85ms;当你把dlairqueue的enable_borrow_从0改成1,观察throughput.dat里eMBB吞吐量提升了15%但VoLTE抖动增加了2ms——这种“所见即所得”的因果关系,是任何高级仿真平台都难以提供的直觉。它不教你LTE协议,但它强迫你理解:在无线世界里,队列不是等待的地方,而是做决定的地方。
简介:这个NS-2 LTE仿真环境提供开箱即用的C++队列管理扩展,支持下行/上行方向的虚拟队列调度,内置RED主动队列管理、Drop-Tail被动丢包、S1接口专用队列(dls1queue/uls1queue)和LTE空口队列(dlairqueue/ulairqueue)。核心逻辑封装在ltequeue.cc/h中,配合ns-lib.tcl、lte.tcl等TCL脚本完成MAC层协议建模与链路配置。资源包自带Makefile编译规则,可直接make编译;附带jitter.awk、delay.awk、throughput.awk等分析脚本,一键提取时延、抖动、吞吐量数据;test目录含验证用例,Readme说明基础部署与运行步骤;packet.h/red.h/queue.h等头文件结构清晰,便于二次开发与算法替换。适用于LTE MAC层缓冲区策略对比、队列长度优化、突发流量应对机制研究等场景。

被折叠的 条评论
为什么被折叠?



