深入理解Linux网络技术内幕 第9章 中断和驱动程序

接收帧通知

设备和内核可以使用两种方式交互数据:轮询和中断。有时候轮询和中断可以组合使用提高效率。

轮询

轮询就是内核不断的查询设备是不是有新数据就绪需要处理,例如查看某个寄存器等。

中断

中断时当特定事件发生时,内核中断其他的进程活动,调用中断处理函数处理数据。当接收到一个报文时处理函数将报文放入队列,然后通知内核进行后续的处理。这种处理方式在低流量环境时可以很好的工作,但是如果流量负载比较高而每个报文都导致一次中断,让CPU为处理中断浪费很多时间。
中断中接收数据分成两个部分:
第一是中断中的处理,将帧拷贝到内核可以访问的输入队列。
第二是内核处理刚刚的输入队列,并使用一个相关协议处理函数进行处理。
第二部分的代码可以被第一部分的代码抢占。在高流量负载下,第一部分代码持续抢占第二部分的处理代码,导致刚刚提到的队列无法出队列处理而不断有数据如队列,最后队列被填满,报文被丢弃。这种情况称为receive-livelock。

在中断期间处理多个报文

中断发生后,中断处理函数会持续的接收报文,然后将接收的报文放入内核的输入队列,直到接收报文最大数目或者超过最大的报文处理时间。中断处理函数运行时CPU的中断会暂时关闭,因此驱动程序中的处理时间应该快速。
还有一种处理方式是,驱动程序可以只关闭设备的中断,设备的输入队列中有一些报文,然后把轮询输入队列的任务交给内核处理函数,不再是把所有的中断都关闭。之后让驱动为内核把报文放入内核队列便于处理。这就是新接口NAPI。NAPI需要对内核做变更。

定时器驱动的中断事件

驱动程序设置内核定期产生一个中断事件,然后处理函数检查自从上次中断事件以来是否有新的报文到达,然后一次处理所有的报文。这会根据定时器中断的时间造成系统的延时。

中断和轮询组合

将中断和轮询结合起来会有更好的效果。低负载下使用中断减少延时,高负债下使用定时器驱动的中断事件。

中断处理函数

下半部函数

每当一个CPU接收一个中断通知时,就会调用该中断事件关联的处理函数,在该函数执行期间,内核处于中断环境,该CPU关闭其中断功能,因此中断函数时非抢占的。因此中断处理函数应该尽快运行完成。而有些处理比较耗时比如报文处理,因此需要将中断分成上半部和下半部。上半部在释放CPU前执行保留数据动作。下半部做剩余的耗时操作。
这种中断模型工作流程如下:

  1. 设备发信号给CPU,通知有中断发生。
  2. CPU执行上半部,关闭CPU中断,直到CPU执行完中断处理函数。
  3. 上半部中吧内核稍后处理该中断的所有信息都保存在RAM的某个地方。
  4. 设置相应的标记,确保内核知道这个中断事件,内核使用上一步骤保存的信息处理接下来的工作。
  5. 中断处理函数终止前开启本地CPU中断
  6. 稍后内核检查刚刚设置的标记调用相关的下半部函数。

下半部实现

内核提供不同机制实现下半部以及一般延期性工作。这些机制差别是:

  • 运行环境
    当下半部函数执行的函数可以休眠时,会受限于进程环境的许可机制和中断环境不同。
  • 并发和上锁
    当一种机制利用SMP时,涉及如何强制串行化,以及锁机制如何影响伸缩性。
    不需要进程环境的机制是软IRQ和微任务(tasklet)

工作队列:当必须执行一个可能会休眠的函数时,必须使用一个专用的内核线程或工作队列(work queue)。工作队列是一种队列,可以把执行一个函数的请求放入队列,然后一个内核线程负责执行。

并发和上锁

微任务(tasklet)只有一个实例可以运行,不同的微任务可以同时在不同的CPU上运行,对所有的微任务而言,不许做强制串行化,因为内核已经强制同一个微任务只有一个实例可以同时执行。
软IRQ:一个CPU中每个软IRQ只能有一个实例可以执行,相同的软IRQ可以运行在不同CPU上,因此要使用上锁机制,确保不同的CPU对共享数据的访问。

软IRQ和微任务的选择取决上锁和并发的需求。多数情况下微任务是较好的选择,但是网络报文接收和传输需要及时的相应,因此采用软IRQ。

抢占功能

Linux2.5版本的内核开始支持内核抢占,有了这个功能,系统调用和其他内核任务可以被更高优先级的内核任务抢占掉。
为支持SMP上锁机制,必须把内核中的非可抢占代码进行处理,一旦加入抢占功能开发人员只需明确定义何处关闭、开启中断和使用锁进行保护。

软中断

Linux内核使用软中断处理报文,许多软中断可以同时运行,而且相同的软中断可以在不同的CPU上同时运行。
现在内核中支持的软中断如下:

/* PLEASE, avoid to allocate new softirqs, if you need not _really_ high
   frequency threaded job scheduling. For almost all the purposes
   tasklets are more than enough. F.e. all serial device BHs et
   al. should be converted to tasklets, not to softirqs.
 */

enum
{
	HI_SOFTIRQ=0,
	TIMER_SOFTIRQ,
	NET_TX_SOFTIRQ,
	NET_RX_SOFTIRQ,
	BLOCK_SOFTIRQ,
	IRQ_POLL_SOFTIRQ,
	TASKLET_SOFTIRQ,
	SCHED_SOFTIRQ,
	HRTIMER_SOFTIRQ, /* Unused, but kept as tools rely on the
			    numbering. Sigh! */
	RCU_SOFTIRQ,    /* Preferable RCU should always be the last softirq */

	NR_SOFTIRQS
};

软IRQ执行时,中断功能是开启的,因此可以被挂起,处理新的中断。如果软IRQ的一个实例在CPU上挂起了,内核不允许针对该软IRQ的新请求又在该CPU上运行,这样可以减少上锁的数量。

软IRQ使用open_softirq函数函数注册。

static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;
void open_softirq(int nr, void (*action)(struct softirq_action *))
{
	softirq_vec[nr].action = action;
}

软IRQ可以通过__raise_softirq_irqoff函数在本地CPU上调度准备执行,只是设置与要执行的的软IRQ相关联的位标记。稍后检测此标记,相关联的处理函数被调用。

void __raise_softirq_irqoff(unsigned int nr)
{
	trace_softirq_raise(nr);
	or_softirq_pending(1UL << nr);
}

微任务

微任务是函数,可以延迟某个中断事件或其他任务,使其晚一点执行。微任务建立在软IRQ之上通常是由中断处理函数发出。
HI_SOFTIRQ软IRQ用来实现高优先级的微任务,TASKLET_SOFTIRQ软IRQ用来实现低优先级的微任务。
每次发出延迟执行的请求之后,tasklet_struct结构排除相应软IRQ的执行列表。
由于软IRQ由各个CPU独自处理,每个CPU上有两份未决tasklet_struct列表,一份与HI_SOFTIRQ关联,一份与TASKLET_SOFTIRQ关联。

软IRQ初始化

softirq_init会为tasklet注册两个软IRQ。

void __init softirq_init(void)
{
	int cpu;

	for_each_possible_cpu(cpu) {
		per_cpu(tasklet_vec, cpu).tail =
			&per_cpu(tasklet_vec, cpu).head;
		per_cpu(tasklet_hi_vec, cpu).tail =
			&per_cpu(tasklet_hi_vec, cpu).head;
	}

	open_softirq(TASKLET_SOFTIRQ, tasklet_action);
	open_softirq(HI_SOFTIRQ, tasklet_hi_action);
}

HI_SOFTIRQ主要由声卡驱动程序使用。

未决IRQ处理

do_softirq函数负责处理未决IRQ。该函数首先检查CPU是否正在服务于硬件中断或者软IRQ,如果是就直接退出。
接下来通过local_softirq_pending函数获取未决的软IRQ。在获取位图之前使用local_irq_save函数先禁止中断,但是在do_softirq_own_stack函数处理软IRQ时会重新打开中断。

asmlinkage __visible void do_softirq(void)
{
	__u32 pending;
	unsigned long flags;

	if (in_interrupt())
		return;

	local_irq_save(flags);

	pending = local_softirq_pending();

	if (pending && !ksoftirqd_running(pending))
		do_softirq_own_stack();

	local_irq_restore(flags);
}

__do_softirq函数负责具体的软IRQ的处理调用。
__do_softirq函数重启中断之前将位图信息存到pending变量,然后打开中断。然后函数根据pengding中的标记位调用相应的软中断处理函数。执行完之后重新获取pengding标记,如果又有软中断置位发生,则判断max_restart和函数已经运行的时间是否已经超过最大阈值,如果没有则重新运行刚刚的逻辑,否则调用wakeup_softirqd()函数唤醒ksoftirqd线程,稍后处理剩余的未决的IRQ。

;

asmlinkage __visible void __softirq_entry __do_softirq(void)
{
	unsigned long end = jiffies + MAX_SOFTIRQ_TIME;
	unsigned long old_flags = current->flags;
	int max_restart = MAX_SOFTIRQ_RESTART;
	struct softirq_action *h;
	bool in_hardirq;
	__u32 pending;
	int softirq_bit;

	/*
	 * Mask out PF_MEMALLOC s current task context is borrowed for the
	 * softirq. A softirq handled such as network RX might set PF_MEMALLOC
	 * again if the socket is related to swap
	 */
	current->flags &= ~PF_MEMALLOC;

	pending = local_softirq_pending();
	account_irq_enter_time(current);

	__local_bh_disable_ip(_RET_IP_, SOFTIRQ_OFFSET);
	in_hardirq = lockdep_softirq_start();

restart:
	/* Reset the pending bitmask before enabling irqs */
	set_softirq_pending(0);

	local_irq_enable();

	h = softirq_vec;

	while ((softirq_bit = ffs(pending))) {
		unsigned int vec_nr;
		int prev_count;

		h += softirq_bit - 1;

		vec_nr = h - softirq_vec;
		prev_count = preempt_count();

		kstat_incr_softirqs_this_cpu(vec_nr);

		trace_softirq_entry(vec_nr);
		h->action(h);
		trace_softirq_exit(vec_nr);
		if (unlikely(prev_count != preempt_count())) {
			pr_err("huh, entered softirq %u %s %p with preempt_count %08x, exited with %08x?\n",
			       vec_nr, softirq_to_name[vec_nr], h->action,
			       prev_count, preempt_count());
			preempt_count_set(prev_count);
		}
		h++;
		pending >>= softirq_bit;
	}

	rcu_bh_qs();
	local_irq_disable();

	pending = local_softirq_pending();
	if (pending) {
		if (time_before(jiffies, end) && !need_resched() &&
		    --max_restart)
			goto restart;

		wakeup_softirqd();
	}

	lockdep_softirq_end(in_hardirq);
	account_irq_exit_time(current);
	__local_bh_enable(SOFTIRQ_OFFSET);
	WARN_ON_ONCE(in_interrupt());
	current_restore_flags(old_flags, PF_MEMALLOC);
}

ksoftirqd内核线程

ksoftirqd内核线程被分配的工作是检查没有被__do_softirq函数执行的软IRQ,每个CPU都有一个ksoftirqd线程,名为ksoftirqd_CPU0、ksoftirqd_CPU1等。

static void run_ksoftirqd(unsigned int cpu)
{
	local_irq_disable();
	if (local_softirq_pending()) {
		/*
		 * We can safely run softirq on inline stack, as we are not deep
		 * in the task stack here.
		 */
		__do_softirq();
		local_irq_enable();
		cond_resched();
		return;
	}
	local_irq_enable();
}

ksoftirqd内核线程优先级很低,保证其他进程不会被饿死。

网络子系统使用软IRQ

网络子系统通过下面函数注册两个软中断。这两个软中断的优先级比普通的微任务要高,比高优先级的任务要低。

	open_softirq(NET_TX_SOFTIRQ, net_tx_action);
	open_softirq(NET_RX_SOFTIRQ, net_rx_action);

softnet_data结构

每个CPU都有队列用来接收进来的帧,这样不同的CPU没有必要使用上锁机制。

poll_list双向列表,其中的设备带有输入帧等着处理。
input_pkt_queue用来保存收到的报文。
output_queue 设备列表其中有数据要传输。
completion_queue 已经传输完成报文的缓冲区列表,可以被释放掉。

/*
 * Incoming packets are placed on per-CPU queues
 */
struct softnet_data {
	struct list_head	poll_list;
	struct sk_buff_head	process_queue;

	/* stats */
	unsigned int		processed;
	unsigned int		time_squeeze;
	unsigned int		received_rps;
#ifdef CONFIG_RPS
	struct softnet_data	*rps_ipi_list;
#endif
#ifdef CONFIG_NET_FLOW_LIMIT
	struct sd_flow_limit __rcu *flow_limit;
#endif
	struct Qdisc		*output_queue;
	struct Qdisc		**output_queue_tailp;
	struct sk_buff		*completion_queue;
#ifdef CONFIG_XFRM_OFFLOAD
	struct sk_buff_head	xfrm_backlog;
#endif
#ifdef CONFIG_RPS
	/* input_queue_head should be written by cpu owning this struct,
	 * and only read by other cpus. Worth using a cache line.
	 */
	unsigned int		input_queue_head ____cacheline_aligned_in_smp;

	/* Elements below can be accessed between CPUs for RPS/RFS */
	call_single_data_t	csd ____cacheline_aligned_in_smp;
	struct softnet_data	*rps_ipi_next;
	unsigned int		cpu;
	unsigned int		input_queue_tail;
#endif
	unsigned int		dropped;
	struct sk_buff_head	input_pkt_queue;
	struct napi_struct	backlog;

};

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值