Linux网络系统底层机制分析(2)

----linux底层的若干机制

暂且把报文的接收看作是上行处理,这一篇将总结linux是如何在底层处理从网络上接收到的报文。所有的源代码引自linux kernel 2.6.22。

1)硬件通知驱动的方式

网络适配器在收到报文之后,该怎么样通知内核(或者说驱动)呢?一般来说有这样集中方式:

A)轮询的方式  内核周期性检查设备的状态,看是否有报文到达,有的话就调用相应的驱动函数进行处理。这种方式会浪费大量的系统资源,因而现代的操作系统中很少采用,但是在某些应用场合,他是最佳的方式;

B)中断的方式  报文到达时,设备触发中断,通知系统切换上下文处理中断。中断处理函数接收报文,将报文入队,通知内核做进一步的处理。这种方式是最普遍,在低流量的情况下起性能也非常好,但是在网络高负荷的环境中,会有一些问题,显而易见,由于每个报文都会触发一次中断,系统将耗费大量的时间在处理中断和上下文切换当中。除此之外,还有其他的问题。处理入报文的代码通常会分成两部分:一部分负责将报文拷贝到内核可见的队列中,另一部分负责从该队列中去报文并加以处理。前一部分在中断上下文中执行,他能够抢占后一部分的处理。在大流量时,由于后一部分不能及时运行,队列会马上被填满,但是此时又没有禁止中断,那么随后到达的报文引发的中断极有可能使系统瘫痪。

C)多帧处理的方式(Processing multiple frames during an interrupt)  稍微改变一下中断处理函数的行为就是多帧处理的方式--在一个中断处理函数中处理多个报文。在这种方式中,驱动可以每次读取一定个数的报文到内核的入队列中,当然也可以每次把设备的入报文全部读完,但是这样可能会严重影响到内核的调度。这种方法不需要内核做什么改变,完全是驱动的行为。其实把这种概念延伸一下就是linux的新接口NAPI,这个等一下再讲。

D)定时器驱动中断(timer-driven interrupt)  这是上一种方式的增强:驱动可以设定一个时间t,然后设备会以t为周期定时中断系统,在中断处理中,驱动一次性把所有在这个间隔内到达的报文全部挂到系统的入报文队列中。这比轮询的方式节约不少的时间资源,但是它也依赖于硬件的功能--硬件要有这种定时中断的能力,并且会有一定的时延。

E)NAPI  上面三种接口(B,C,D)统统可以使用内核的旧机制,NAPI是linux kernel 2.5版本后开始采用的一种新的内核接口,意为new api。这个在后面详细总结。

可以看到,有若干种方式可以选择,他们都有各自的优缺点。我们可以将它们结合起来以适应变化的应用场合。比如,只要硬件允许,可以结合中断和定时驱动中断的方式,反正他们也不需要内核提供新的接口。事实上,tulip driver就是采取的这种方式(drivers/net/tulip/interrupt.c)。

2)中断处理函数

无论是NAPI,还是老的接口,都不可避免地用到中断,这里总结一下linux对中断的处理。

中断可以简单分为硬件和软件中断。硬件中断实际上是一种电信号(触发的方式有电平方式和边沿方式,记得在学校去一家公司应聘实习,里面就要讲讲这两种方式和linux在处理他们的不同考量,现在已经记不清了),cpu通过中断管理器得到硬件的通知,并及时作出响应。软件中断也有很多,比如我们时常用到的系统调用,一般实现的方式就是通过软中断陷入到内核中,在linux中,使用了0x80号软中断;还有更普通的除零也会引发软中断。这方面的资料可以参考intel或其他cpu厂家的一些编程资料。Arm等芯片的中断号不同,但机制都是一样的。

可以用request_irq来注册并激活一个中断处理函数,free_irq注销中断处理函数,释放中断线。这两个函数的实现是架构相关的。Request_irq主要是分配资源,并放入到全部变量:irq_desc的适当位置。irq_desc保存了256级中断的处理信息,里面的“struct irqaction *action;”成员保存了具体的处理函数信息,对于共享中断线来说,链表上会有多个处理函数存在。

Linux对中断处理流程比较简单,处理器中断内核以后会跳到do_IRQ函数,显然,该函数也是体系架构相关的。此函数的主要任务是调用注册到该中断线的处理函数(对于共享中断线的方式,会有多个):

desc->handle_irq(irq, desc).

在处理函数被调用执行以后,流程到达ret_from_intr,该入口定义在entry.S中,汇编代码,下面是片段:

ret_from_intr:

 GET_THREAD_INFO(%ebp)

check_userspace:

 movl PT_EFLAGS(%esp), %eax # mix EFLAGS and CS

 movb PT_CS(%esp), %al

 andl $(VM_MASK | SEGMENT_RPL_MASK), %eax

 cmpl $USER_RPL, %eax

 jb resume_kernel  #检查是返回到用户空间还是内核空间(根据USER_RPL

的定义可以看到,此时判断的依据是当前的处理器的执行级别,eax的值是当前的运行级别,不过比USER_RPL(3)要小,那么表示是在中断发生时,运行在内核空间,否则返回到用户空间)

ENTRY(resume_userspace)

  DISABLE_INTERRUPTS(CLBR_ANY) # make sure we don't miss an interrupt

     # setting need_resched or sigpending

     # between sampling and the iret

 movl TI_flags(%ebp), %ecx

 andl $_TIF_WORK_MASK, %ecx # is there any work to be done on

     # int/exception return?

 jne work_pending

 jmp restore_all

END(ret_from_exception)

#ifdef CONFIG_PREEMPT

ENTRY(resume_kernel)

 DISABLE_INTERRUPTS(CLBR_ANY)

 cmpl $0,TI_preempt_count(%ebp)   #判断preept_count是否为零,如果是,表明此时内核没有持有共享资源,因此抢占是安全的。

 jnz restore_nocheck

need_resched:

 movl TI_flags(%ebp), %ecx # need_resched set ?

 testb $_TIF_NEED_RESCHED, %cl  #判断重新调度标志是否被置位,如果是,则此时的重新调度是安全的

 jz restore_all

 testl $IF_MASK,PT_EFLAGS(%esp) # interrupts off (exception path) ?

 jz restore_all

 call preempt_schedule_irq

 jmp need_resched

END(resume_kernel)

由于中断处理函数在中断中断上下文中执行,所以不能调用引发等待,占用锁,休眠等行为的函数。另一个注意的问题是关于中断的重入,由于当某条中断线上有中断时,所有的cpu上的此条中断线的请求全部被屏蔽,所以我们不必考虑重入的问题。

在linux中,为了使中断处理程序快速完成,有下半部处理机制,这个和linux的网络子系统的关系挺大,下面总结一下。

 
Logo

更多推荐