实时操作系统,它通过调度和资源管理,保证实时任务在规定的时间限制内完成。

linux不是实时操作系统,主要是因为:

1
2
3
4
5
6
存在关中断机制,低优先级的进程如果关闭了中断(进入临界区等),高优先级的进程中断发生也无法响应。
内核禁止抢占,进程进入内核,直到系统调用结束/被阻塞,高优先级进程无法被调度。
调度策略,分为实时任务和普通任务,只意味着谁优先被CPU调度,而没有保证完成时间。
虚拟内存机制通过 swap 可能让进程访问内存的时间不可预测。
高优先级的进程不能抢占低优先级进程的资源。如果高优先级的进程要使用低优先级进程正在使用的资源时,它必须等待低优先级的进程释放资源,容易产生优先级倒置;
Linux的周期模式定时器频率仅为100Hz,远不能满足多种实时应用的要求

RTlinux,后演变为preempt_rt,在linux的基础上打补丁,增加了

1
2
3
4
5
中断线程化:把传统硬中断 top-half 的大部分逻辑移到内核线程里运行。可以被调度,减少CPU被不可抢占硬中断占用的时间。
开启可抢占RCU配置。
当低优先级任务(持锁)阻塞高优先级任务时,临时提升持锁低优先级任务的优先级,防止中等优先级任务抢占,缩短高优先级任务的等待时间。
使用rt_mutex替代spin lock,可抢占。支持优先级继承。
使用hwtimer替代jeffies等等。

linux irq

GIC控制器

Generic Interrupt Controller。ARM提供的通用中断控制器。接受硬件中断信号,并经过一定处理后,分发给对应的CPU进行处理。分为V1-V4,

GICV2

GIC是联系外设中断和CPU的桥梁,也是各CPU之间中断互联的通道,负责检测,管理,分发中断。

image-20250714085007704

1
2
3
4
5
6
主要负责
使能/禁止中断。
把中断分组到group0还是group1,0作为安全模式使用连接FIQ,1作为非安全模式,连接IRQ.
多核系统中将中断分配到不同处理器上。
设置电平的触发方式。
虚拟化扩展。

ARM CPU对外的连接只有2个中断,IRQ & FIQ,相对应的处理模式分别是 IRQ 一般中断处理模式 和 FIQ 快速中断处理模式,所以GIC最后要把中断汇集成2条线,与CPU对接。

在GICV2中,gic由两个大模块distributor和interface组成。

distributor

主要负责中断源的管理,优先级,中断使能,中断屏蔽等。中断分发,对于PPI,SGI是各个core独有的中断,不参与目的core的仲裁,SPI 是所有 core 共享的,根据配置决定中断发往的core。中断优先级的处理,将最高优先级的中断发送给cpu interface。寄存器使用GICD作为前缀,一个gic中,只有一个GICD。

主要的作用是检测各个中断源的状态,控制各个中断源的行为,分发各个中断源产生的中断事件到指定的一个/多个CPU接口上。虽然分发器可以管理多个中断源,但是它总是把优先级最高的那个中断请求送往CPU接口。分发器对中断的控制包括:

1
2
3
4
5
6
7
8
打开或关闭每个中断,Distributor对中断的控制分成两个级别,一个是全局中断的控制(GIC_DIST_CTRL),一旦关闭了全局中断,那么任何的中断源产生的中断事件都不会被传递到cpu interface。另一个级别是针对各个中断源进行控制,(GIC_DIST_ENABLE_CLEAR),关闭一个中断源会导致该中断事件不会分发到CPU interface,但不影响其他中断源产生中断事件的分发。
控制将当前优先级最高的中断事件分发到一个或者一组CPU interface,当一个中断事件分发到多个CPU interface的时候,GIC的内部逻辑应该只保证assert一个CPU。
优先级控制
interrupt属性设定。电平触发,边缘触发等等。
interrupt group设定。设置每个中断的group。
将SGI中断分发到目标CPU上。
每个中断状态可见。
提供软件机制来设置和清楚外设终端的pending状态。

cpu interface

用于连接器,与处理器进行交互,将GICD发送的中断信息,通过IRQ,FIQ等管脚,传输给core。寄存器使用GICC作为前缀,每一个core,有一个cpu interface。

1
2
3
4
5
6
打开或关闭cpu interface 向连接的CPU assert中断事件,对于arm,cpu interface和cpu之间的中断信号线是nIRQCPU 和 nFIQCPU, 如果关闭了中断,即便是distributor分发了一个中断事件到CPU interface,也不会assert指定的IRQ或者FIQ通知core。
中断的确认。core会向cpu interface应答中断,应答当前优先级最高的那个中断,中断一旦被应答,distributor就会把该中断的状态从pending修改为active。ack了之后,cpu就会deassert nirqcpu和nfiqcpu信号线。
中断处理完毕的通知。当interruput handler处理完了一个中断的时候,会向写CPU interface的寄存器通知GIC CPU已经处理完该中断,做这个动作一方面是通知 Distributor 将中断状态修改为 deactive,另外一方面,CPU interface 会 priority drop,从而允许其他的 pending 的中断向 CPU 提交。
为 CPU 设置中断优先级掩码。通过 priority mask,可以 mask 掉一些优先级比较低的中断,这些中断不会通知到 CPU。
设置中断抢占策略。
在多个中断同时到来的时候,选择一个优先级最高的通知CPU。

virtual cpu interface

将GICD发送的虚拟中断信息,通过VIRQ,VFIQ管脚,传输给core,每一个core,有一个virtual cpu interface,而在这个virtual cpu interface中,又包含以下两个组件,virtual interface control,virtual cpu interface。

gic中断类别

gicv2,将中断,分成了group0,安全,FIQ 和group1,非安全,IRQ。

支持三种类型的中断。

1
2
3
4
5
6
7
8
9
GICV2:

SGI software generated interrupt。软件触发的中断,件可以通过写 GICD_SGIR 寄存器来触发一个中断事件,一般用于核间通信,内核中的 IPI:inter-processor interrupts 就是基于 SGI。
PPI private peripheral interrupt。私有外设中断,是每个核心私有的中断,PPI会送达到指定的cpu上,应用场景有CPU的本地时钟。
SPI Shared peripheral interrupt。公用的外部设备中断,也定义为共享中断。中断产生后,可以分发到某一CPU上,中断号ID32 - ID1019用于SPI,ID1020 - ID1023保留用于特殊用途;

GICV3:

SGI,SPI, LPI(locality spicific peripheral interrupt)GICV3中引入,是基于消息的中断,他们的配置保存在表中而不是寄存器。

GICV3的组成部分,GICV3中,主要由Distributor,cpu interface,redistributor,its,GICV3中,将cpu interface从GIC中抽离,放入到了cpu中,cpu interface通过AXI Stream,与gic进行通信。 当GIC要发送中断,GIC通过AXI stream接口,给cpu interface发送中断命令,cpu interface收到中断命令后,根据中断线映射配置,决定是通过IRQ还是FIQ管脚,向cpu发送中断。

image-20250715092053150

image-20250714093922084

1
2
3
4
5
6
7
8
root@root:~# cat /proc/interrupts 

虚拟中断号 硬件中断号
CPU0 CPU1 CPU2 CPU3
10: 1 0 0 0 GICv2 84 Level CC_IRQ
12: 356258 361084 352728 352728 GICv2 30 Level arch_timer (本地时钟)
15: 0 0 0 0 GICv2 225 Level clocksource@2,f0106000
16: 0 0 0 0 GICv2 340 Level arm-pmu

gic中断处理流程

1
2
3
4
5
6
7
GIC决定每个中断的 使能 状态,不使能的中断,是不能发送中断的
如果某个中断的中断源有效,GIC将该中断的状态设置为pending状态,然后判断该中断的目标core
对于每一个core,GIC将当前处于pending状态的优先级最高的中断,发送给该core的cpu interface
cpu interface接收GIC发送的中断请求,判断优先级是否满足要求,如果满足,就将中断通过nFIQ或nIRQ管脚,发送给core。
core响应该中断,通过读取 GICC_IAR 寄存器,来认可该中断。读取该寄存器,如果是软中断,返回源处理器ID,否则返回中断号。
当core认可该中断后,GIC将该中断的状态,修改为active状态
当core完成该中断后,通过写 EOIR (end of interrupt register)来实现优先级重置,写 GICC_DIR 寄存器,来无效该中断。

gic中断优先级

gicv2,支持最小16个,最大256个中断优先级。

中断状态和处理流程

image-20250715092647205

1
2
3
4
5
6
每个中断都维护了一个状态机。

inactive: 无中断状态,没有pending也没有active。
pending:硬件或软件触发了中断,该中断事件已经通过硬件信号通知到了GIC,等待GIC分配的CPU进行处理,在电平触发模式下,产生中断的同时保持pengding状态。
Active:cpu已经应答该中断请求,并且正在处理中。
Active and pending:当一个中断源处于Active时,同一中断源又触发了中断,进入pending状态,挂起来状态。

软件框架

1
2
3
4
5
6
主要分为四部分:

1.硬件无关代码
2.cpu架构相关的中断处理
3.中断控制器代码
4.普通其他驱动
1
2
3
4
5
常见术语

irq number 软件定义,和硬件无关,CPU需要为每一个外设中断编号,
irq domain,irq域,将某一类资源划分成不同的领域,相同的域下共享一些共同的属性。irq domain负责GIC中hwirq到 虚拟irq的映射。
中断上半部/下半部:中断上半部处理简单的紧急的功能,清楚中断处理标志。大部分任务放到下半部处理。

中断设备树

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
gic: interrupt-controller@fd400000 {
compatible = "arm,gic-v3";
#interrupt-cells = <3>; // 参数个数
#address-cells = <2>;
#size-cells = <2>;
ranges;
interrupt-controller;

reg = <0x0 0xfd400000 0 0x10000>, /* GICD */
<0x0 0xfd460000 0 0xc0000>; /* GICR */
interrupts = <GIC_PPI 9 IRQ_TYPE_LEVEL_HIGH>;
its: interrupt-controller@fd440000 { // 在gic设备节点下,有一个子设备节点its,ITS设备用于将消息信号中断(MSI)路由到cpu
compatible = "arm,gic-v3-its";
msi-controller;
#msi-cells = <1>; // MSI设备的DeviceID
reg = <0x0 0xfd440000 0x0 0x20000>; /*ITS寄存器的物理地址*/
};
};

中断控制器code

1
2
3
4
5
6
7
8
9
10
11
12
IRQCHIP_DECLARE(gic_v3, "arm,gic-v3", gicv3_of_init);  //初始化一个struct of_device_id的静态常量,并放置在__irqchip_of_table中

drivers/irqchip/irq-gic.c
IRQCHIP_DECLARE(cortex_a7_gic, "arm,cortex-a7-gic", gic_of_init);
--> gic_of_init


init/main.c asmlinkage __visible void __init __no_sanitize_address start_kernel(void)
--> early_irq_init(); init_IRQ();
--> arch/arm64/kernel/irq.c --> irqchip_init();
--> driver/irqchip/irqchip.c --> of_irq_init(__irqchip_of_table);
--> drivers/of/irq.c ????
1
https://doc.embedfire.com/linux/rk356x/driver/zh/latest/linux_driver/subsystem_interrupt.html

中断上下部分的处理手段

上下文

上下文信息,既包含虚拟内存,栈,全局变量等用户态的资源,也包括内核堆栈,寄存器资源,不同类型的上下文切换,会涉及到不同类型的资源切换。

临界

临界资源

临界资源是一次仅允许一个进程使用的共享资源。各进程采取互斥的方式,实现共享的资源称作临界资源。

临界区

每个进程中访问临界资源的那段代码称为临界区(criticalsection),每次只允许一个进程进入临界区,进入后,不允许其他进程进入。显然,若能保证进程互斥地进入自己的临界区,便可实现诸进程对临界资源的互斥访问。

上下文类型

进程上下文切换
1
2
3
4
5
某个进程时间片耗尽,会被系统挂起,切换到其他等待 CPU 的进程。
进程所需系统资源不足,需要等到资源满足时才可运行,此时会被挂起,其他进程会被调度。
进程通过 sleep 方法主动挂起,其他进程就有机会被调度。
有更高优先级的进程,当前进程会被挂起,高优先级进程会被调度。
硬件中断时,CPU 上的进程会被中断挂起,转而执行内核中的中断服务程序。
线程上下文切换
1
2
3
同一进程内线程切换:由于线程共享进程的虚拟内存和大部分资源,调度器只需切换线程私有的寄存器、内核栈和调度信息,而不需要切换虚拟内存,因此开销较小、

不同进程间切换:由于不共享虚拟内存和资源,切换时除了保存/恢复寄存器等线程上下文,还需要切换虚拟内存(mm_struct/CR3),这就是完整的进程上下文切换。
中断上下文切换
1
2
中断上下文切换指的是为了响应硬件的事件,中断处理会打断进程的正常调度和执行,转而调用中断处理程序,响应设备事件。而在打断其他进程时,就需要将当前的状态保存下来。这样在中断结束后,进程仍然可以从原来的状态恢复运行。
中断上下文切换,并不需要保存和恢复进程的虚拟内存等用户态资源,只需要处理 CPU 寄存器、内核堆栈等内核态的资源即可。

运行在进程上下文的内核代码是可抢占的,但中断上下文会一直运行到结束,不会被抢占。所以中断处理程序代码要受到一些限制。

1
2
3
4
5
中断代码不能:
睡眠/放弃CPU,因为内核在进入中断前会 关闭进程调度,一旦睡眠/放弃CPU,这时内核无法调度别的进程来执行,系统就会死掉。
尝试获得信号量,如果获得不到信号量,代码就会睡眠,导致如上的结果。
执行耗时的任务,中断处理应该尽可能快,如果一个处理程序是IRQF_DISABLE类型,它执行的时候会禁止所有中断。
访问用户空间的虚拟地址,因为中断允许在内核空间。

抢占计数

Linux配置打开了CONFIG_PREEMPT表示允许高优先级的任务抢占低优先级任务,但是在spin lock,中断/软中断上下文中依旧不允许抢占的。在linux系统中使用了一个Per-CPU的32位变量来标识一些特殊场景,如下

image-20250908

softirq

1
2
3
4
5
6
7
8
9
10
11
12
13
enum {
HI_SOFTIRQ = 0, /* 优先级高的tasklets */
TIMER_SOFTIRQ, /* 定时器的下半部 */
NET_TX_SOFTIRQ, /* 发送网络数据包 */
NET_RX_SOFTIRQ, /* 接收网络数据包 */
BLOCK_SOFTIRQ, /* BLOCK装置 */
BLOCK_IOPOLL_SOFTIRQ,
TASKLET_SOFTIRQ, /* 正常优先级的tasklets */
SCHED_SOFTIRQ, /* 调度程序 */
HRTIMER_SOFTIRQ, /* 高分辨率定时器 */
RCU_SOFTIRQ, /* RCU锁定 */
NR_SOFTIRQS /* 10 */
};
  • 一个软中断不会抢占另一个软中断
  • 唯一可以抢占软中断的是中断处理程序
  • 不同软中断可以在不同处理器同时执行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// raise_softirq 触发软中断
// 触发事件 --> 硬中断代码返回处 ksofirq内核线程中 执行软中断处理代码中,eg network。

// do_sofrirq 执行软中断
do_softirq --> if (in_interrupt()) return;
如果检测到有 (HARDIRQ_MASK | SOFTIRQ_MASK | NMI_MASK)在这些中断中。会直接返回。不执行。
PREEMPT_MASK (0x000000ff): 表示抢占计数,用于跟踪内核抢占状态
SOFTIRQ_MASK (0x0000ff00): 表示软中断计数,用于跟踪软中断执行状态
HARDIRQ_MASK (0x000f0000): 表示硬中断计数,用于跟踪硬中断处理状态
NMI_MASK (0x00f00000): 表示不可屏蔽中断(NMI)计数

/*
硬中断发生的时候,cpu会自动禁用中孤单。并跳转到中断处理程序。
硬件中断本身不会禁用软中断,但会通过增加 preempt_count 中的 HARDIRQ_MASK 位来标记当前处于硬中断上下文中。
硬中断可以被另一个优先级比自己高的硬中断 中断,不能被同/低 级的硬中断中断。更不能被软中断中断。
软中断可以被硬中断中断,但不会被另一个软中断中断,在一个CPU上。软中断总是串行执行的。
*/

触发softirq

image-20250909090703297

执行softirq

image-20250909093354426

软中断执行时机分为三种,中断退出时检测是否有软中断执行,进程上下文中主动执行,spin_unlock_bh后执行。

image-20250909094325638

进程被中断打断后进入到中断上下文中,进入中断后cpu硬件会自动关闭cpu本地中断响应,处理中断完成后在执行irq_exit中断退出时,当检测到有软中断pending时执行软中断;如果软中断是在中断上下文执行时,在软中断处理中会调用local_irq_enable打开CPU本地中断响应再处理软中断程序,如果是触发的软中断线程,硬中断已经完成退出也会使能本地中断。因此在软中断执行过程中打开了中断响应,所以可能会再次进入硬中断上下文。

image-20250909102119846

在一个task中处理一个变量此时被硬件中断打断进行中断处理函数,在中断处理快结束时如果有软中断pending将会先处理软中断,如果软中断中也访问了该变量,那么就出现竞态异常,因此为了处理进程和软中断的竞态,调用spin_lock_bh和spin_unlock_bh进行保护,在硬件中断处理完要进入软中断将会被禁止,硬件中断会被直接退出,继而task可以继续运行,当task再执行spin_unlock_bh时会触发执行软中断。另外如果软中断处理函数中的竞态可能在多核直接发生,为了保护多核的临界处理在软中断中只需要调用spin_lock和spin_unlock即可,不需要调用spin_lock_bh和spin_unlock_bh,因为每个cpu上只有一个软中断可以运行不需要做软中断之间的临界保护。总结就是在进程上下文中要避免软中断和多核的竞态保护就调用spin_lock_bh和spin_unlock_bh,软中断中避免多核的竞态保护就调用spin_lock和spin_unlock即可。

tasklet

tasklet(小任务机制)是利用软中断实现的下半部机制。是softirq的特殊实现。Tasklet则是自身串行化的,同一时间只能有一个特定的tasklet在运行。

1
2
open_softirq(TASKLET_SOFTIRQ, tasklet_action);
tasklet_action_common中Tasklet确保同一时间只有一个特定的tasklet实例在运行,避免了竞争条件:

软中断和tasklet都是运行在中断上下文中,它们与任一进程无关,没有支持的进程完成重新调度。所以软中断和tasklet不能睡眠、不能阻塞,它们的代码中不能含有导致睡眠的动作,如减少信号量、从用户空间拷贝数据或手工分配内存等。也正是由于它们运行在中断上下文中,所以它们在同一个CPU上的执行是串行的,这样就不利于实时多媒体任务的优先处理。

workqueue

将下半部工作推迟,给一个内核线程去执行 ——work 总是运行于进程上下文.可以睡眠。允许被重新调度。

api

普通work
init
1
2
3
4
5
6
7
8
9
10
11
12
13
14
声明且初始化
DECLARE_WORK(name , void (*function)(struct work_struct *));
DECLARE_DELAYED_WORK(name, void(*function)(struct work_struct *));
EG:
void my_work_handler(struct work_struct *work);
DECLARE_WORK(my_work, my_work_handler);

初始化一个已经声明的工作队列
INIT_WORK(struct work_struct *work, void(*function)(struct work_struct *));
INIT_DELAYED_WORK(struct delayed_work *work, void(*function)(struct work_struct *));
EG:
void my_work_handler(struct work_struct * work);
struct work_struct my_work;
INIT_WORK(&my_work, my_work_handler);
schedule
1
2
3
4
schedule_work(struct work_struct *work);
schedule_delayed_work(struct delayed_work *work, unsigned long delay);

将work提交到系统默认的global workqueue,true 成功入队,false 已经在队列中。不需要自己控制 workqueue 的亲和性
例子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
普通 work
static void my_work_func(struct work_struct *work)
{
pr_info("work executed\n");
}

static DECLARE_WORK(my_work, my_work_func);

void test(void)
{
schedule_work(&my_work); // 提交到系统 workqueue
}

延迟 work
static void my_delayed_work_func(struct work_struct *work)
{
struct delayed_work *dwork = to_delayed_work(work);
pr_info("delayed work executed\n");
}

static DECLARE_DELAYED_WORK(my_dwork, my_delayed_work_func);

void test(void)
{
schedule_delayed_work(&my_dwork, msecs_to_jiffies(1000)); // 1s 后执行
}
自定义work
cancle
1
2
3
// 取消work 模块卸载退出时使用
int cancel_work_sync(struct delayed_work *work);
int cancel_delayed_work_sync(struct delayed_work *work);
flushed
1
2
3
4
// 等待队列执行完成。常用于 模块退出时,确保没有遗留 work 在运行。
void flush_scheduled_work(void);
int schedule_delayed_work_on(int cpu, struct delayed_work *work, unsigned long delay);
int schedule_on_each_cpu(void(*function)(struct work_struct *));

不能在中断上下文里 flush/cancel work,否则可能死锁

create
1
2
3
4
5
6
7
8
9
// 自定义workqueue
// uses one thread for each processor in the system;
struct workqueue_struct *create_workqueue(const char *name);
// uses a single thread.
struct workqueue_struct *create_singlethread_workqueue(const char *name);

WQ_UNBOUND 不绑定CPU
WQ_HIGHPRI 高优先级
WQ_MEM_RECLAIM 允许内存回收路径
add
1
2
3
4
// 提交work到自定义的队列 区别于 schedule_work:可选择具体的 workqueue。
int queue_work(struct workqueue_struct * queue, struct work_struct *work);
int queue_delayed_work(struct workqueue_struct *queue,
struct delayed_work * work , unsigned long delay);
例子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static struct workqueue_struct *my_wq;
static DECLARE_WORK(my_work, my_work_func);

int init_module(void)
{
my_wq = alloc_workqueue("my_wq", WQ_UNBOUND | WQ_MEM_RECLAIM, 1);
queue_work(my_wq, &my_work);
return 0;
}

void cleanup_module(void)
{
flush_workqueue(my_wq);
destroy_workqueue(my_wq);
}

只有当无法向系统工作队列提交新的工作项时,才去创建额外的工作队列。因为每个新的工作队列都会花费可观的内存占用。如果新工作队列中的工作项无法与系统工作队列中已存在的工作项共存时,可以调整新的工作队列。例如,新的工作项执行了阻塞操作导致其它系统工作队列被延迟到一个不可接受的程序。

如何选择

1
2
3
如果推后执行的任务需要睡眠,那么就选择工作队列;如果推后执行的任务不需要睡眠,那么就选择tasklet。
另外,如果需要用一个可以重新调度的实体来执行你的下半部处理,也应该使用工作队列。它是唯一能在进程上下文运行的下半部实现的机制,也只有它才可以睡眠。这意味着在需要获得大量的内存时、在需要获取信号量时,在需要执行阻塞式的I/O操作时,它都会非常有用。如果不需要用一个内核线程来推后执行工作,那么就考虑使用tasklet。
  一般,不要轻易的去使用工作队列,因为每当创建一条工作队列,内核就会为这条工作队列创建一条内核线程。工作队列位于进程上下文,与软中断,tasklet有所区别,工作队列里允许延时,睡眠操作,而软中断,tasklet位于中断上下文,不允许睡眠和延时操作。

为什么linux不是实时操作系统

进入中断处理时,cpu就关闭了本地中断响应,没法再响应其他中断,即linux的中断是没法嵌套的,即使有在高优先级的中断也是没法处理。另外软中断的处理要比任何进程优先级高,因为软中断也是可以在中断上下文中运行。除了中断,软中断外,spinlock在处理过程中是关闭抢占调度的,所以在spinlock期间也是没法调度的。

image-20250908161618848

  • T0时刻 normal task执行系统调用进入内核
  • T1时刻 获取到spin lock,进入临界区保护阶段
  • T2时刻 产生了IRQ1中断,进而进行处理IRQ1中断
  • T3时刻 唤醒了高优先级的RT task,但此时系统处于中断中无法进行调度
  • T4时刻 IRQ1中断处理结束 但接着又触发了IRQ2中断,进入IRQ2中断处理。
  • T5时刻 IRQ2中断处理结束 但仍处于spin lock临界区 无法调度RT task
  • T6时刻 spin lock释放,高优先级的RT task得到调度运行
  • T7时刻 RT task运行结束 normal task继续运行
  • T8时刻 从内核态返回用户态

linux下监控网卡时可用的工具

ethtool

真正实现都是在网卡驱动中,所以这个工具较为重要。

1
2
3
4
5
-i 显示网卡驱动的信息,名称,版本等
-S 查看收发包统计情况
-g/-G 查看或修改RingBuffer的大小
-l/-L 查看或修改网卡队列数
-c/-C 查看或修改硬中断合并策略

ifconfig

包含了一些网卡统计信息

1
2
3
4
5
RX packets:接收的总包数
RX bytes:接收的字节数
RX errors:表示总的收包的错误数量
RX dropped:数据包已经进入了 Ring Buffer,但是由于其它原因导致的丢包
RX overruns:表示了 fifo 的 overruns,这是由于 Ring Buffer不足导致的丢包
1
2
/proc/net/dev 下有节点可以统计网卡工作数据
/sys/class/net/eth0/statistics 下也包含了网卡的统计信息

RingBuffer的监控与调优

当数据帧到达网卡,第一站即为RingBuffer,网卡通过DMA机制将数据帧送到RingBuffer中。因此第一个要监控和调优的就是网卡的RingBuffer.

ethtool -g eth0 可以查看Ringbuffer的大小。

在Linux的整个网络栈中,RingBuffer起到一个任务的收发中转站的角色。对于接收过程来讲,网卡负责往RingBuffer中写入收到的数据帧,ksoftirqd内核线程负责从中取走处理。只要ksoftirqd线程工作的足够快,RingBuffer这个中转站就不会出现问题。但是我们设想一下,假如某一时刻,瞬间来了特别多的包,而ksoftirqd处理不过来了,会发生什么?这时RingBuffer可能瞬间就被填满了,后面再来的包网卡直接就会丢弃,不做任何处理!

ethtool -S xx 或者 ifconfig xx都可以查看是否是因为这个原因丢包。

rx_fifo_errors如果不为0的话(在 ifconfig 中体现为 overruns 指标增长),就表示有包因为RingBuffer装不下而被丢弃了。那么怎么解决这个问题呢?很自然首先我们想到的是,加大RingBuffer这个“中转仓库”的大小。通过ethtool就可以修改。

1
# ethtool -G eth1 rx 4096 tx 4096

这样网卡会被分配更大一点的”中转站“,可以解决偶发的瞬时的丢包。不过这种方法有个小副作用,那就是排队的包过多会增加处理网络包的延时。所以另外一种解决思路更好,那就是让内核处理网络包的速度更快一些,而不是让网络包傻傻地在RingBuffer中排队。

硬中断监控与调优

在数据被接收到RingBuffer之后,下一个执行的就是硬中断的发起。

监控

硬中断的情况 cat /proc/interrupts

对于收包情况,硬中断的总次数 不等于 Linux收包总数。因为,第一网卡可以设置中断合并,多个网络帧只可以发起一次中断,第二NAPI运行时 会关闭硬中断,通过poll来收包。

多队列网卡调优

目前主流网卡基本都是支持多队列的,可以通过将不同队列分为不同的CPU来处理,加快处理网络包的速度。(最为有用的一个优化手段)

每一个队列都有一个中断号,可以独立向某个CPU核心发起硬中断请求,让CPU来poll包。通过将接收进来的包被放到不同的内存队列里,多个CPU就可以同时分别向不同的队列发起消费了。这个特性叫做RSS(Receive Side Scaling,接收端扩展)。通过ethtool工具可以查看网卡的队列情况。

1
ethtool -l eth0

如果你想提高内核收包的能力,直接简单加大队列数就可以了,这比加大RingBuffer更为有用。因为加大RingBuffer只是给个更大的空间让网络帧能继续排队,而加大队列数则能让包更早地被内核处理。ethtool修改队列数量方法如下:

1
ethtool -L eth0 combined 32

硬中断合并

对于CPU来讲也是一样,CPU要做一件新的事情之前,要加载该进程的地址空间,load进程代码,读取进程数据,各级别cache要慢慢热身。因此如果能适当降低中断的频率,多攒几个包一起发出中断,对提升CPU的工作效率是有帮助的。所以,网卡允许我们对硬中断进行合并。

1
2
3
4
5
6
7
ethtool -c eth0

Adaptive RX: 自适应中断合并,网卡驱动自己判断啥时候该合并啥时候不合并
rx-usecs:当过这么长时间过后,一个RX interrupt就会被产生
rx-frames:当累计接收到这么多个帧后,一个RX interrupt就会被产生

ethtool -C eth0 adaptive-rx on可以直接修改。

软中断监控与调优

在硬中断之后,再接下来的处理过程就是ksoftirqd内核线程中处理的软中断了。之前我们说过,软中断和它对应的硬中断是在同一个核心上处理的。因此,前面硬中断分散到多核上处理的时候,软中断的优化其实也就跟着做了,也会被多核处理。不过软中断也还有自己的可优化选项。

监控设置

1
2
cat /proc/softirqs 可以查看软中断信息
cat /proc/sys/net/core/netdev_budget 内核参数

这个的意思说的是,ksoftirqd一次最多处理300个包,处理够了就会把CPU主动让出来,以便Linux上其它的任务可以得到处理。那么假如说,我们现在就是想提高内核处理网络包的效率。那就可以让ksoftirqd进程多干一会儿网络包的接收,再让出CPU。至于怎么提高,直接修改不这个参数的值就好了。

1
2
sysctl -w net.core.netdev_budget=600
(static struct ctl_table xxx注册后 就可以通过 sysctl -w xxx 来赋值)

如果要保证重启仍然生效,需要将这个配置写到/etc/sysctl.conf

软中断GRO合并

GRO和硬中断合并的思想很类似,不过阶段不同。硬中断合并是在中断发起之前,而GRO已经到了软中断上下文中了。

如果应用中是大文件的传输,大部分包都是一段数据,不用GRO的话,会每次都将一个小包传送到协议栈(IP接收函数、TCP接收)函数中进行处理。开启GRO的话,Linux就会智能进行包的合并,之后将一个大包传给协议处理函数。这样CPU的效率也是就提高了。

1
2
3
4
ethtool -k eth0 | grep generic-receive-offload
generic-receive-offload: on
ethtool -K eth0 gro on
GRO说的仅仅只是包的接收阶段的优化方式,对于发送来说是GSO。

大多数MCU都没有FPU 硬件浮点单元。如果用float double运算,编译器会调用软件库函数来模拟浮点加减乘除。需要分解成一系列整数操作,移位,比较,循环,效率非常低。可能要几十到上百条指令。

定点运算用的就是普通的整数运算单元。MCU内核天然支持整数加减乘除移位,一条指令就可以完成。

CPU如何进行浮点运算

IEEE754标准。浮点数分为 符号位,指数,尾数部分。没有FPU的MCU运算过程

1
2
3
4
5
取出指数和尾数。
对齐指数(移位操作)。
尾数执行整数加减/乘除。
结果归一化(调整指数和尾数)。
处理溢出/舍入/NaN/无穷大等特殊情况。

cmake

官方教程地址

https://modern-cmake-cn.github.io/Modern-CMake-zh_CN/chapters/intro/running.html

生成Makefile的工具。需要攥写的是CMakeLists.txt。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
add_executable(one two.cpp three.h)     	生成一个one可执行文件
add_library(one STATIC two.cpp three.h) 生成一个库
STATIC,SHARED, 或者MODULE、BUILD_SHARED_LIBS
target_include_directories(one PUBLIC include) 添加包含目录,public,任何链接到这个目标的库都必须包含这个目录,PRIVATE 只影响当前,不影响依赖。INTERFACE,之影响依赖。
target_link_libraries(another PUBLIC one) 指定目标another。
set(MY_VARIABLE "value") 声明本地变量 变量名全部大写 变量值跟在后面 声明后,只能在它的作用域内访问这个变量。可以在 变量声明末尾加PARENT_SCOPE来将它的作用域置定为当前的上一级作用域。

set(MY_CACHE_VARIABLE "VALUE" CACHE STRING "" FORCE)
mark_as_advanced(MY_CACHE_VARIABLE)
将缓存的变量作为临时的全局变量

set(ENV{variable_name} value) 和 $ENV{variable_name} 设置/获取环境变量
CmakeCache.txt是缓存。当运行Cmake构建目录的时候会创建它。

set_property(TARGET TargetName PROPERTY CXX_STANDARD 11) 设置属性
get_property(ResultVariable TARGET TargetName PROPERTY CXX_STANDARD) 获取属性

if(variable)
else()
endif()

function()
xxx
endfunction()



记得在任何使用目标的地方都指定关键字PUBLIC PRIVATE INTERFACE,那么就不会有问题

debug命令

1
2
--trace 选项能够打印出运行的 CMake 的每一行。
CMake 3.7 添加了 --trace-source="filename" 选项,这让你可以打印出你想看的特定文件运行时执行的每一行。

kbuild

definition

递归make。通过将源文件划分为不同的模块,组件。每个组件都由自己的makefile管理。开始构建时,顶级的makefile以正确的调用顺序调用每个组件的makefile。

kbuild指向到不同类型的makefile

  • makefile位于源代码目录的顶级makefile
  • .config是内核配置文件。
  • arch/$(ARCH)/Makefile 是架构的makefile,它用于补充顶级makefile。
  • scripts/Makefile.*描述所有的kbuild makefile通用规则

Kbuild是linux的内核专用构建系统。依赖内核顶层Makefile和一堆构建脚本。单独拿出来几乎没意义。

占位

在看linux的代码的时候,看到了很多关于backlog的说法,进而了解到linux下的back pressure机制。故在此记录下。

Applying Back Pressure When Overload,系统持续过载的处理手段,核心观点,限制队列长度。从而为队列中的任务维持系统高吞吐率和良好的响应时间。

在linux内核中。backlog是一个在多个子系统(网络协议栈,加密模块,块设备子系统)中的通用概念。本质上表示 “任务积压队列”或者“待处理任务的缓冲区”。虽然出现在不同的子系统中,但核心思想类似,都是当资源暂时不可用/系统无法立即处理即时任务时,将任务缓存在backlog中等待稍后处理。

是什么?

backlog是一个临时存储结构,处理以下情况

  • 当前任务 不可立即完成,如资源竞争,锁冲突等。
  • 当前任务 执行上下文(中断、软中断)不适合执行较重操作
  • 有限处理能力暂时无法跟上任务到达速度
  • 为了更好地资源调度和并发控制。

通常,backlog会配合异步处理机制(软中断,工作队列)一起使用。

crypto中的backlog

crypto api 中的 backlog 更多是异步请求的概念。

  • 当当前算法驱动忙碌(无法立即处理请求),请求会被挂到backlog中。
  • 当驱动空闲时,通过crypt_dequeue_request将请求取出处理。
  • 一般结合软中断,tasklet,workqueue等延迟调度机制来实现。

通用机制

backlog一般具备以下机制。

机制组成 说明
队列结构 通常为链表,如 list_head,或环形缓冲区等
入队操作 当不能立即处理请求时,调用 enqueue() 将请求加入
出队处理 某个异步事件(如软中断、tasklet、workqueue)触发处理
并发控制 多用自旋锁或 RCU 保证并发安全
队列限流 使用最大队列长度(max_backlog)限制,防止 OOM 或 DoS

为什么用?

  • 性能优化: 在高并发场景下。避免阻塞上下文,提高整体吞吐。
  • 上下文切换控制:中断上下文不可以睡眠,将请求延时处理。
  • 资源争用时做缓冲:eg socket锁不可获取,不是立即失败,而是放入缓冲区。
  • 防止请求丢失:部分场景下避免因为临时资源短缺而丢失的请求。

占位

0%