linux kernel 中的推迟调用(defer callback)(四)

本文详细介绍了Linux内核中的延时执行和工作队列机制。延时执行通过timer_list结构体和相关函数如add_timer、mod_timer、del_timer实现,而工作队列则提供了异步任务调度,包括全局工作队列和独立工作队列的使用,以及如何创建和调度work_struct。两者的主要区别在于时间精确性和上下文环境。示例代码展示了如何设置和管理定时器以及工作队列任务。

1.概览

  推迟调用和延时的执行模型还是有些相似的,他们都是推迟某一些任务的执行,例如使用msleep时,其后面的代码也会在其返回后才能进行。但在我看来,他们的本质区别在于,推迟的这部分任务是交给第三方做还是自己亲力亲为的完成,因为使用延时方法推迟任务时,当前调用进程也就是什么也不做了,但对于推迟调用的方式是将任务抛出来,自己该干嘛还是干嘛。tasklets对于设备驱动开发者很少使用,故略过。

2. Timer–定时器

  定时器的概念也很好理解,为任务设置一个具体时间点让其被执行。在内核中也同样,只是这个时间更加偏爱使用jiffies。

2.1 timer_list

  结构体timer_list在kernel中则代表一个定时器,下面是其定义

//include\linux\timer.h
struct timer_list {
	struct hlist_node	entry;
	unsigned long		expires;
	void			(*function)(struct timer_list *);
    ...
};

参数说明如下
  entry:
    内核使用全局双向链表来管理timer。
  expires:
    超期时间,相对于设置时的jiffies而言,例如项延迟一秒的伪代码如下

timer_list->expires = jiffies + HZ

  function:
    对应超时所需要处理的任务,注意在回调这个任务的上下文为原子上下文,这意味着里面不能使用任何会造成休眠相关的代码。其中入参就是本timer_list。可以配合container_of来获取timer所在数据结构的数据指针。

2.2 add_timer

  该函数用于将初始化完成的timer_list挂到全局定时器中去,只有在此调用过后该timer才会被处理。其定义如下

/**
 * add_timer - start a timer
 * @timer: the timer to be added
 *
 * The kernel will do a ->function(@timer) callback from the
 * timer interrupt at the ->expires point in the future. The
 * current time is 'jiffies'.
 *
 * The timer's ->expires, ->function fields must be set prior calling this
 * function.
 *
 * Timers with an ->expires field in the past will be executed in the next
 * timer tick.
 */
 //kernel\time\timer.c
void add_timer(struct timer_list *timer)
{
	BUG_ON(timer_pending(timer));
	mod_timer(timer, timer->expires);
}

  入参也就是上面初始化后的timer_list的指针,没什么好说的。

2.3 mod_timer

  此函数用于修改已存在的timer_list的超期时间。定义如下

/**
 * mod_timer - modify a timer's timeout
 * @timer: the timer to be modified
 * @expires: new timeout in jiffies
 *
 * mod_timer() is a more efficient way to update the expire field of an
 * active timer (if the timer is inactive it will be activated)
 *
 * mod_timer(timer, expires) is equivalent to:
 *
 *     del_timer(timer); timer->expires = expires; add_timer(timer);
 *
 * Note that if there are multiple unserialized concurrent users of the
 * same timer, then mod_timer() is the only safe way to modify the timeout,
 * since add_timer() cannot modify an already running timer.
 *
 * The function returns whether it has modified a pending timer or not.
 * (ie. mod_timer() of an inactive timer returns 0, mod_timer() of an
 * active timer returns 1.)
 */
  //kernel\time\timer.c
int mod_timer(struct timer_list *timer, unsigned long expires)
{
	return __mod_timer(timer, expires, 0);
}

  如果被修改的timer_list还没有超期,那么就相当于修改了timer_list->expiresd的值。但如果timer_list已经被处理过,那么其作用流程相当于
  a.del_timer(timer);
  b.timer->expires = expires;
  c.add_timer(timer);
可见被处理过的timer_list会被在此放到带处理队列以期下次执行。

2.4 del_timer

  当设置了一个timer_list并已经调用add_timer注册后,此时又想删除时就调用该接口。接口定义如下

/**
 * del_timer - deactivate a timer.
 * @timer: the timer to be deactivated
 *
 * del_timer() deactivates a timer - this works on both active and inactive
 * timers.
 *
 * The function returns whether it has deactivated a pending timer or not.
 * (ie. del_timer() of an inactive timer returns 0, del_timer() of an
 * active timer returns 1.)
 */
 //kernel\time\timer.c
int del_timer(struct timer_list *timer)
{
	struct timer_base *base;
	unsigned long flags;
	int ret = 0;

	debug_assert_init(timer);

	if (timer_pending(timer)) {
		base = lock_timer_base(timer, &flags);
		ret = detach_if_pending(timer, base, true);
		raw_spin_unlock_irqrestore(&base->lock, flags);
	}

	return ret;
}

  入参没什么好说的,返回值0代表被删除的timer还未超期,1代表被删除的timer已经超期处理了。

2.5 综合应用示例

  下面的示例实现的功能为,设置一个新的timer_list,并设置其超期间隔为1s,在超期任务中再次延长其超期间隔为1s,如此直至第11次超期回调后删除该timer_list。基本代码如下

#define DEL_TIMER_MAX	10
void timer_one_callback(struct timer_list * tim)
{
	int ret = 0;
	
	if(timer_one.callCount > DEL_TIMER_MAX) {
		ret = del_timer(tim);
		DEFER_CALL_TEST_INFO("Del timer. [callCount]%d.[ret]%d", timer_one.callCount, ret);
		return;
	} else {
		DEFER_CALL_TEST_INFO("Expires the one timer.[callCount]%d", timer_one.callCount++);
		mod_timer(tim, jiffies + HZ);
	}
}

kernel_timer_test
	struct timer_list *timerTmp = &timer_one.timer;
	timerTmp->expires = jiffies + HZ;//1s
	timerTmp->flags = 0;
	timerTmp->function = timer_one_callback;
	add_timer(timerTmp);

  执行结果如下

board:/ # insmod /data/deferCallTest.ko
[20751.903661] deferCallTest:timer_one_callback,33:Expires the one timer.[callCount]0
[20752.927771] deferCallTest:timer_one_callback,33:Expires the one timer.[callCount]1
[20753.951638] deferCallTest:timer_one_callback,33:Expires the one timer.[callCount]2
[20754.975499] deferCallTest:timer_one_callback,33:Expires the one timer.[callCount]3
[20755.999623] deferCallTest:timer_one_callback,33:Expires the one timer.[callCount]4
[20757.023622] deferCallTest:timer_one_callback,33:Expires the one timer.[callCount]5
[20758.047619] deferCallTest:timer_one_callback,33:Expires the one timer.[callCount]6
[20759.071780] deferCallTest:timer_one_callback,33:Expires the one timer.[callCount]7
[20760.095779] deferCallTest:timer_one_callback,33:Expires the one timer.[callCount]8
[20761.119773] deferCallTest:timer_one_callback,33:Expires the one timer.[callCount]9
[20762.143609] deferCallTest:timer_one_callback,33:Expires the one timer.[callCount]10
[20763.167778] deferCallTest:timer_one_callback,30:Del timer. [callCount]11.[ret]0

3.workqueue–工作队列

  工作队列是你任何时候,想抛出目前不太想做的任务,可见工作队列和timer在现象上如此的相似,但既然存在那么就是有对应的使用场景的,下面是我认为的两个主要的不同点。
  a.工作队列不像timer对时间那样需要一个确切的执行节点,工作队列一般是在想用的时候再将任务包装成一个work(work_struct)入列。
  b.工作队列中的回调函数则是在内核进程的上下文执行,所以在其回调中是允许调用可能会造成进程休眠的接口的。

3.1 workqueue_struct

  既然是工作队列,那么肯定得有队列。该结构体就是描述队列的头的,其后一般跟着一个个work_struct也就是一个个将要被执行的任务。其定义如下

//kernel\workqueue.c
struct workqueue_struct {
    ...
	struct list_head	list;		/* PR: list of all workqueues */
    ...
};

  此处省略对理解使用没有帮助的成员,list用来连接work_struct。

3.2 work_struct

  该数据结构用来描述一个任务,也就是被推迟的代码。其定义如下

// kernel\include\linux\workqueue.h
typedef void (*work_func_t)(struct work_struct *work);
struct work_struct {
	struct list_head entry;
	work_func_t func;
};

  entry用于和工作队列进行连接。func则是在该work被调度执行时允许的函数,其入参就是本wiork。

3.3 使用全局工作队列来处理work_struct

  挂在全局队列上的work_struct可能来自于各个驱动各个进程,如果想要自己的work_struct被处理的快点建议使用独立的工作队列来处理。下面是全局工作队列的使用示例

3.43.1 代码示例
static void global_work_callback(struct work_struct *work)
{
	struct my_works *mw = container_of(work, struct my_works, global_work);
    DEFER_CALL_TEST_INFO("[global_callback_count]%d", mw->global_callback_count++);
}
INIT_WORK(&works.global_work, global_work_callback);
schedule_work(&works.global_work)

  代码实现非常的简单,创建一个 work_struct 设置其回调函数为global_work_callback,并将其压入全局工作队列中等待执行。global_work_callback则只对被调用次数做计数。
  执行结果如下

[76512.088402] deferCallTest:global_work_callback,85:[global_callback_count]0
3.3.2 INIT_WORK

INIT_WORK为帮助宏,用于简化work_struct的初始化,其定义如下


#define INIT_WORK(_work, _func)						\
	__INIT_WORK((_work), (_func), 0)
            (_work)->func = (_func);				\

  该函数主要初始化入参_work中的func。

3.3.3 schedule_work

  用于将一个work_struct添加到全局工作队列中,其定义如下

//kernel/include/linux/workqueue.h
static inline bool schedule_work(struct work_struct *work)
{
	return queue_work(system_wq, work);
}

3.4 使用独立的工作队列来处理work_struct

  这种方式相对上面使用全局工作队列的好处也很明显,资源独享带来的当然是更快的响应。

3.4.1 create_workqueue

  用于创建独立的工作队列头,其定义如下

//kernel/include/linux/workqueue.h
struct workqueue_struct *alloc_workqueue(const char *fmt,
					 unsigned int flags,
					 int max_active, ...);
#define create_workqueue(name)						\
	alloc_workqueue("%s", __WQ_LEGACY | WQ_MEM_RECLAIM, 1, (name))

  入参直接使用字符串即可,返回值则是类型为workqueue_struct的指针,用于挂载work_struct数据结构。

3.4.2 queue_work

  用于将指定的work_struct压入指定的工作队列中,以供执行。其定义如下

//kernel/include/linux/workqueue.h
static inline bool queue_work(struct workqueue_struct *wq,
			      struct work_struct *work)
{
	return queue_work_on(WORK_CPU_UNBOUND, wq, work);
}

  入参很简单,返回true代表成功压入,返回false代表该work_struct已存在于被压入的工作队列中。

3.4.3 代码示例

  处理使用自定义的工作队列,程序意图和3.3.1的完全一致,在此就不赘述了,直接贴代码

static struct workqueue_struct* myWq;
workqueue_test
    myWq = create_workqueue("flagstaffWorkqueue");
    INIT_WORK(&works.internal_work, internal_work_callback);
    queue_work(myWq, &works.internal_work)
defer_call_test_drv_exit
	destroy_workqueue(myWq);

  执行结果如下

[76512.088391] deferCallTest:internal_work_callback,95:[internal_callback_count]0

4. 完整源码

deferCallTest

https://gitee.com/solo-king/linux-kernel-base-usage/blob/master/flagstaff/deferCallTest.c
<think>嗯,用户的问题是关于Linux内核中skb_defer_rx_timestamp函数引起的间戳检查延迟的解决方案。这个问题涉及到网络间戳处理的底层机制,属于比较专业的Linux内核网络编程范畴。 用户可能正在开发或维护一个需要高精度间同步的系统,比如金融交易系统或工业控制系统。从ta提问的专业术语使用来看,应该是有一定内核开发经验的工程师。ta提到的"间戳检查延迟"问题,核心在于硬件间戳获取的异步处理机制导致的延迟。 参考之前提供的引用材料,特别是引用[4]中关于netif_receive_skb的处理流程,可以推断间戳延迟发生在网络数据包接收路径上。skb_defer_rx_timestamp这个函数的设计初衷是为了避免在中断上下文中进行耗的硬件间戳读取操作,但这也带来了延迟问题。 解决这个问题的思路需要从三个层面考虑: 首先在硬件层面,要确认NIC是否支持PTP硬件间戳,这是基础条件。然后检查驱动是否正确实现了间戳处理回调,比如ptp_clock_info结构体的gettimex64方法。 在配置层面,用户可能忽略了启用CONFIG_NETWORK_PHY_TIMESTAMPING选项,或者没有正确设置SO_TIMESTAMPING套接字选项。特别是需要确认是否启用了SOF_TIMESTAMPING_RX_SOFTWARE标志来触发软件补偿机制。 最关键的优化点在软件处理层面。Linux内核的PTP4L工具其实已经提供了延迟补偿机制,但需要用户正确配置phc2sys工具进行钟同步。此外,可以考虑调整NAPI轮询间隔,但要注意平衡延迟和CPU利用率的关系。 对于特别严苛的场景,可能需要修改内核代码。比如在netif_receive_skb_internal函数中增加对间敏感数据包的优先处理逻辑,或者为PTP数据包实现单独的NAPI队列。但这些修改需要重新编译内核,存在稳定性风险。 用户可能没有意识到的是,这个问题有与IRQ负载均衡有关。当网络中断都集中在单个CPU核心,会导致处理延迟。建议检查/proc/interrupts的分布情况,使用irqbalance工具进行优化。 考虑到用户的技术水平,在回答需要提供具体的命令行操作示例,比如ethtool检查配置的方法,以及phc2sys的典型参数设置。同要提醒性能测试的方法,包括使用ptp4l的监控模式和ts2phc工具进行验证。</think>在 Linux 系统中处理 `skb_defer_rx_timestamp` 引起的间戳检查延迟问题,核心在于优化硬件间戳获取、减少软件路径延迟以及合理配置系统。以下是详细的解决方案: --- ### **1. 确认硬件支持与驱动配置** - **检查 NIC 支持**: ```bash ethtool -T <interface> ``` 确保输出包含 `hardware-transmit` 和 `hardware-receive` 间戳能力(如 `ptp` 字段)。 - **验证驱动实现**: 确保网卡驱动正确注册了间戳回调(如 `.ndo_do_ioctl` 处理 `SIOCSHWTSTAMP`),并在收到 PTP 报文触发 `skb_defer_rx_timestamp`[^4]。 --- ### **2. 优化内核配置** - **启用必要选项**: ```bash CONFIG_NETWORK_PHY_TIMESTAMPING=y CONFIG_PTP_1588_CLOCK=y ``` - **调整 SO_TIMESTAMPING 标志**: 在应用程序中启用硬件间戳: ```c int flags = SOF_TIMESTAMPING_RX_HARDWARE | SOF_TIMESTAMPING_RAW_HARDWARE; setsockopt(sock, SOL_SOCKET, SO_TIMESTAMPING, &flags, sizeof(flags)); ``` --- ### **3. 减少软件路径延迟** - **禁用无关功能**: ```bash # 关闭 GRO/GSO ethtool -K <interface> gro off gso off # 禁用 IRQ 负载均衡 echo 0 > /proc/irq/<irq_num>/smp_affinity_list ``` - **提升 NAPI 处理优先级**: 缩短 `net.core.netdev_budget`(默认 300)和 `net.core.dev_weight`(默认 64),减少单次轮询处理的数据包数量: ```bash sysctl -w net.core.netdev_budget=150 sysctl -w net.core.dev_weight=32 ``` --- ### **4. 处理 skb_defer_rx_timestamp 延迟** - **原理分析**: 该函数将间戳获取推迟到软中断(`NET_RX_SOFTIRQ`)中执行,以避免在硬中断中阻塞[^4]。延迟主要来自: 1. 软中断调度延迟 2. 硬件读取间戳的耗 - **解决方案**: - **使用专用内核线程**: 修改内核,为间戳创建高优先级内核线程: ```c // 示例:在 net/core/ptp.c 中创建线程 kthread_run(ptp_thread_fn, NULL, "ptp_timestamp"); ``` - **优化间戳读取**: 在驱动中实现 `.getcrosststamp()` 方法,直接读取 NIC 的 PTP 钟(需硬件支持): ```c struct ptp_clock_info info = { .getcrosststamp = my_get_crosststamp, }; ``` --- ### **5. 钟同步与补偿** - **使用 ptp4l 和 phc2sys**: 配置 `linuxptp` 工具进行硬件钟同步和延迟补偿: ```bash ptp4l -i eth0 -m -H # 硬件间戳模式 phc2sys -s CLOCK_REALTIME -c /dev/ptp0 -w -m -O <offset> ``` 关键参数: - `-O <offset>`:补偿固定延迟(纳秒) - `-w`:等待 ptp4l 同步完成 - **动态补偿算法**: 在应用程序中实现滤波算法(如 Kalman 滤波器)处理残留抖动。 --- ### **6. 性能监控与调试** - **检查间戳延迟**: ```bash # 查看软中断延迟 watch -n 1 'cat /proc/softirqs | grep NET_RX' # 跟踪间戳处理路径 perf trace -e 'net:*' -p <ptp4l_pid> ``` - **内核跟踪点**: ```bash echo 1 > /sys/kernel/debug/tracing/events/net/netif_receive_skb_entry/enable cat /sys/kernel/debug/tracing/trace_pipe ``` --- ### **典型问题场景与解决** | **场景** | **表现** | **解决方案** | |---------------------------|------------------------------|----------------------------------| | 软中断积压 | `NET_RX` 软中断执行间过长 | 减少 `netdev_budget`,绑定 CPU | | 硬件间戳读取慢 | 驱动 `getcrosststamp` 延迟高 | 优化驱动或更换 NIC | | 应用处理延迟 | 用户空间 PTP 报文处理延迟 | 提升进程优先级 (`chrt -f 99`) | | 钟漂移 | 同步后仍有毫秒级抖动 | 调整 `phc2sys` 滤波参数 (`-E`) | > **关键提示**: > - 优先使用支持 **PTP Hardware Clock (PHC)** 的网卡(如 Intel I210、X540)。 > - 在虚拟化环境中,需传递 PCIe 设备并启用 `ptp_kvm` 模块。 > - 极端低延迟场景(如 5G TSN),考虑内核补丁(如 XDP 加速 PTP 处理)。 [^1]: Linux PTP 协议栈用户空间处理机制 [^4]: netif_receive_skb 数据包接收流程 --- ### 相关问题 1. **如何验证 Linux 系统是否已正确处理硬件间戳?** 2. **在虚拟化环境中(如 KVM),如何优化 PTP 间同步的精度?** 3. **当 `ptp4l` 报告 "delay 机制不可用" ,应如何排查?** 4. **如何为不支持硬件间戳的 NIC 实现软件间戳补偿?**
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值