• 深入理解Linux网络技术内幕——网络设备初始化


     

    概述

       内核的初始化过程过程中,与网络相关的工作如下所示:

        内核引导时执行start_kernel,start_kernel结束之前会调用rest_init,rest_init初始化内核线程init(在Linux3-12中为kernel_init)。
    asmlinkage void __init start_kernel(void)
    {
        ...
        parse_early_param();//间接调用parse_args
        parse_args(...); //处理内核引导程序(boot loader)在引导期间传给内核的参数,
        ...
        init_IRQ();   //初始化硬件中断
        tick_init();
        init_timers(); //定时器用于支持后续初始化工作的运行。
        hrtimers_init();
        softirq_init(); //初始化软件中断
        ...
        rest_init();   //这里会通过kernel_thread函数调用内核线程init(kernel_init).
    }

    static init __ref kernel_init(void *unused)
    {
        kernel_init_freeable();   //Linux-3.12 在这里调用do_basic_setup
        free_initmem();  //用于释放已经不再需要的内存。
        .....
        run_init_process(execute_command)
        ...
    }

    static void __init do_basic_setup(void)
    {
        cpuset_init_smp();
        usermodehelper_init();
        shmem_init();
        driver_init();
        init_irq_proc();
        do_ctors();
        usermodehelper_enable();
        do_initcalls();  //初始化内核子系统和内建的设备驱动 
        random_int_secret_init();
    }



    设备的注册和初始化

        一个设备要能够正常工作,他就必须被内核所识别,并且与正确的驱动关联起来。设备驱动程序以私有结构体的形式保存了驱动本设备所需要的所有信息,并且与其他和本设备有交互的组件相互影响。
        设备的注册和初始化一部分由内核完成,一部分由设备驱动程序完成。

          设备的初始化包括了硬件初始化、软件初始化、功能初始化三部分:硬件初始化:由设备驱动程序和通用总线层完成,有时也需要用户提供一些参数。主要任务是将硬件功能配置成IRQ和I/O地址,以便能够跟内核相互作用。软件初始化:设备使用之前必须关注当前配置或启用的网络协议, 一般需要用户提供诸如ip地址之类的参数。功能初始化: Linux内核提供了一系列的网络选项,有些网络选项对每一个设备都需要进行单独配置(如实现Qos的子系统),这些配置决定了数据包进入队列和离开设备的出口队列的方式 
        NIC初始化目标
        网络设备在Linux中都是以net_device实例进行初始化的,本节先不讨论这个,本节主要介绍设备驱动程序如何分配/建立设备与内核通信所需要的资源。IRQ线NIC必须分派一个IRQ,用于在必要时唤起内核的注意(虚拟设备不需要分配IRQ,因为它的工作都是在内部实现。)/proc/interrupts文件可用于观察当前中断线分派状态。I/O端口和内存注册: 驱动程序会将设备的一块内存映射到系统内存,使得驱动程序的读写操作能够通过系统内存直接进行。注册和释放操作分别由request_region和 release_region进行。

    设备与内核之间的交互

    几乎所有设备与内核的交互都是通过以下两种方式:
    内核的轮询:
        内核定时检查设备的状态,判断设备是否有什么请求。
    设备驱动的中断请求:
        设备驱动发送硬件信号引起内核注意

    内核轮询在其他文章会介绍,本文主要介绍硬件中断中与网络有关的概念。
    硬件中断
        每一个中断都会运行一个中断处理程序,这些中断响应程序都是设备驱动为设备量身定做的。一般而言,当设备注册一个NIC时,它首先会请求并分配一个IRQ,然后要为IRQ注册(如果设备被卸载了,则需要注销)一个IRQ响应程序。相应的内核代码在kernel/irq/manage.c和arch/XXX/kernel/irq.c。(其中XXX为处理器架构)
    int request_threaded_irq(unsigned int irq, irq_handler_t handler,                                            
                 irq_handler_t thread_fn, unsigned long irqflags,
                 const char *devname, void *dev_id)
    void free_irq(unsigned int irq, void *dev_id)

        注意:irq的注册和释放函数都带有参数dev_id。因为IRQ是可以共享的,因此需要IRQ number和dev_id共同来唯一表示中断。
        另,在注册IRQ时,必须保证IRQ还未有设备请求,除非所有设备都支持IRQ共享。
        内核接收到一个中断信号时,会通过IRQ number调用关联的中断响应程序。IRQ number与中断响应程序以表的形式保存。由于多个设备可能共享IRQ的关系,IRQ number与中断响应程序的关系可能是一对多的。
        中断类型:接收到数据帧、帧传输失败、DMA传输已成功完成、设备已经有足够内存来创建新的传输会话(可用NIC可用内存达到一定数值<一般为设备MTU>时产生一个中断)
        为了防止内核在设备内存不足时多次提交传输请求,设备驱动可以关闭内核出口队列,待到资源足够是才重启。下面是一个范例:
    static netdev_tx_t
    el3_start_xmit(struct sk_buff *skb, struct net_device *dev)
    {
        ……
        netif_stop_queue (dev);
        ……
        dev->trans_start = jiffies;
        if (inw(ioaddr + TX_FREE) > 1536)
            netif_start_queue(dev);
        else
            /* Interrupt us when the FIFO has room for max-sized packet. */
            outw(SetTxThreshold + 1536, ioaddr + EL3_CMD);
        ……
    }



        IRQ共享:IRQ线是很有限的资源,为了让一个系统能支持更多的设备,只能让多个设备共享IRQ线。IRQ共享的机制是这样的,内核收到中断请求,然后调用所有与该中断相关联的响应例程,然后有各个响应例程自行判断过滤是否对这个中断进行处理。(注意,IRQ与响应程序是一对多的,发生一个IRQ,哪些响应程序要处理,哪些不需要不是有内核去判断,而是各个中断响应程序自己判断,内核则是调用所有的响应程序。)
        IRQ与IRQ响应程序的组织:用全局的vector:irq_desc来组织,irq_desc包含所有IRQ,每个IRQ对应自己的链表,链表中是该IRQ关联的所有响应程序。只有IRQ共享时,IRQ链表的节点才会超过一个。整个组织如下图:



    初始化选项

        所有的系统内建组件以及作为模块加载的设备都能通过用户的输入参数调整所实现的功能、重写其默认值,或者在引导前后有不同的值。
    模块选项:(module_param系列的宏)
        模块加载时可以定义。如果是内建的组件,由于在引导期间无法配置,可以通过sys/进行运行时配置。
    引导期间内核选项:(__setup系列宏)
        引导期间提供。用于可以内建到内核的模块
        

    设备处理层的初始化

        网络代码初始化有以下重要的部分:流量控制,每个CPU输入队列初始化。这些初始化工作在引导期间由net_dev_init完成:
    static int __init net_dev_init(void)
    subsys_initcall(net_dev_init);   


    用户空间辅助程序

    • /sbin/modprobe 在内核需要加载某个模块时调用,判断内核传递的模块是不是/etc/modprobe.conf文件中定义的别名
    • /sbin/hotplug 在内核检测到一个新设备插入或拔出系统时调用,它的任务是根据设备标识加载正确的驱动

    1. 以模块方式加载
      kmod模块加载器允许内核组件通过调用request_module请求加载某个模块
      举个例子;如果系统管理员使用ifconfig配置某个网卡,但这个网卡驱动还没有加载,如eth0,内核就会给/sbin/modprobe发送一个请求,让它加载名称为
      eth0的模块。如果/etc/modprobe.conf中包含“alias eth0 xxx”的字符,/sbin/modprobe就会尝试加载xxx.ko模块。
      module_param 宏定义在引入sysfs后可以通过文件来访问得到模块参数
      模块选项有三项 , 第一项参数名称,第二项参数类型,第三项表示参数作为文件在sys文件系统中所有的权限。
      每个模块都会在sys/modules下生成对应的目录,通过目录下的文件可以获取模块参数。
    2. pnp热插拔
      hotplug允许内核检测热插拔设备的插入和拔出并通知用户进程(/sbin/hotplug),用户进程根据这些通知来加载相应的驱动
      在编译内核时,会在kernel目录下生成modules.pcimap和modules.usbmap两个文件,这两个文件分别包含了内核所支持设备的pci id和usb id,文件中还包
      含于每个设备的id相对应的内核模块名称,当用户进程收到内核关于pnp的通知后,会使用这个文件来查找正确的设备驱动

    虚拟设备

        虚拟设备一般也使用net_device结构体进行实例化(也有一些例外,如别名接口设备)。
        虚拟设备一般会有用户空间配置工具来对其进行配置。尤其是无法使用ifconfig来进行配置的高级字段。
        虚拟设备一般会有一个/proc接口目录,其内容详细程度取决于虚拟设备的设计。
        虚拟设备与这是设备的对应关系不是一一对应的。这就导致了虚拟设备可能需要进行流量控制的配置。
        虚拟设备的流量是简介从真实物理设备获得的,因而不需要分配IRQ、IO端口和IO内存。
        虚拟设备与其他真实物理设备一样,能对特殊的事件通知做出相应的反应。


    /proc调整

        












  • 相关阅读:
    打怪(CDQ分治+斜率优化)
    中缀表达式求值
    马拉车(manacher)算法
    后缀数组的求法及应用
    【最小生成树】藏宝图(prim)
    【最小生成树】prim算法
    [最近公共祖先]最近公共祖先(LCA)
    [思维]Supreme Number
    [模拟] Lattice's basics in digital electronics(2018沈阳赛区网络预赛)
    【搜索+思维】Distinctive Character
  • 原文地址:https://www.cnblogs.com/Windeal/p/4284597.html
Copyright © 2020-2023  润新知