分类: LINUX
2021-12-30 23:19:06
原文地址:深入理解Linux网络技术内幕-网络设备初始化(一) 作者:visualfan
当内核引导时,会执行start_kernel对一些子系统做初始化,在start_kernel终止前会调用init内核线程,由其负责初始化的后续工作。网络子系统的初始化活动多数都是在do_basic_setup内完成的。
在初始化任务中,主要有三个:
run_init_process确定在系统上运行的第一个进程,也就是所有其他进程的父进程,其PID为1,一直运行直到系统做完工作。正常情况下运行的程序是init,但可以通过引导选项init=参数指定另一个不同程序。不提供这个选项,内核会尝试从一些众所周知的位置去执行init命令,若找不到就会发生内核panic。
设备的注册和初始化
一个网络设备可用,就必须被内核认可并且关联正确的驱动程序。驱动程序把驱动所需的信息存储在私有数据结构中,然后与其他需要此设备的内核组件交互。注册和初始化任务的一部分由内核负责,其他部分由设备驱动程序完成。设备的初始化主要分为:
NIC的初始化
Linux内核中,每个网络设备都由一个net_device数据结构表示,net_device结构的字段初始化部分由设备驱动程序完成,部分由内核函数完成。这里主要介绍设备驱动程序分配建立设备/内核通信所需的资源。
所有的设备都通过轮询和中道观两种方式和内核交互,轮询需要内核定期检查设备状态了解是否发生事件。而中断则是用设备端驱动,在发生事件时产生一个硬件中断信号,请求内核的注意。在Linux系统中通过结合轮询和中断的方式来提供系统性能,在何种情况下选择哪种方式需要根据现实需要而定。
每个中断事件都会运行一个函数,被称为中断处理例程(interrupt handler),中断处理例程是根据设备所需而编写的,因此由设备驱动程序来安装中断处理例程。一般地,当设备驱动程序注册一个NIC时,会请求并分派一个IRQ。然后用两个依赖于CPU架构的函数为指定的IRQ注册或注销中断处理例程。
分配指定的IRQ线,并安装中断处理例程:
static inline int __must_check
request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,
const char *name, void *dev)
{
return request_threaded_irq(irq, handler, NULL, flags, name, dev);
}
中断处理例程原型为:
typedef irqreturn_t (*irq_handler_t)(int, void *);
其中request_threaded_irq定义在kernel/manage.c中:
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)
释放request_irq分配的中断资源:
void free_irq(unsigned int irq, void *dev_id)
给定的设备由dev_id标记,此函数卸载安装的中断处理例程,而且若没有其他设备注册该IRQ线,则关闭这个IRQ。要识别这个中断处理例程,内核需要根据IRQ编号以及设备标识符,对于共享IRQ这一点尤为重要。
当内核接收到中断通知时,会使用IRQ号找出中断处理例程并执行,为了找出中断处理例程,内核会将IRQ编号和中断处理例程之间的关系存在一个全局表,这种关系可以是一对一或一对多的,因为有中断共享的情形。
中断类型
NIC主要通过中断告知驱动程序以下几种情形:
设备驱动程序也可以在传输前关闭出口队列以防止内核对该设备产生另一次传输请求,然后当NIC有足够的内存才予以重启。如在3c509.c文件中的el3_start_xmit函数作为net_device_ops的发送数据包函数:
static const struct net_device_ops netdev_ops = {
.ndo_open = el3_open,
.ndo_stop = el3_close,
.ndo_start_xmit = el3_start_xmit,
.ndo_get_stats = el3_get_stats,
.ndo_set_multicast_list = set_multicast_list,
.ndo_tx_timeout = el3_tx_timeout,
.ndo_change_mtu = eth_change_mtu,
.ndo_set_mac_address = eth_mac_addr,
.ndo_validate_addr = eth_validate_addr,
#ifdef CONFIG_NET_POLL_CONTROLLER
.ndo_poll_controller = el3_poll_controller,
#endif
};
static netdev_tx_t
el3_start_xmit(struct sk_buff *skb, struct net_device *dev)
{
struct el3_private *lp = netdev_priv(dev);
int ioaddr = dev->base_addr;
unsigned long flags;
/*驱动程序先关闭设备队列,禁止内核提交后续的传输请求*/
netif_stop_queue (dev);
dev->stats.tx_bytes += skb->len;
if (el3_debug > 4) {
pr_debug("%s: el3_start_xmit(length = %u) called, status %4.4x.\n",
dev->name, skb->len, inw(ioaddr + EL3_STATUS));
}
/*
* We lock the driver against other processors. Note
* we don't need to lock versus the IRQ as we suspended
* that. This means that we lose the ability to take
* an RX during a TX upload. That sucks a bit with SMP
* on an original 3c509 (2K buffer)
*
* Using disable_irq stops us crapping on other
* time sensitive devices.
*/
spin_lock_irqsave(&lp->lock, flags);
/* Put out the doubleword header... */
outw(skb->len, ioaddr + TX_FIFO);
outw(0x00, ioaddr + TX_FIFO);
/* ... and the packet rounded to a doubleword. */
outsl(ioaddr + TX_FIFO, skb->data, (skb->len + 3) >> 2);
/*检查设备的内存是否足够容纳一个1536字节的包,若有则启动队列,运行内核提交传输请求*/
if (inw(ioaddr + TX_FREE) > 1536)
netif_start_queue(dev);
else
/* Interrupt us when the FIFO has room for max-sized packet. */
/*否则,设置中断,当内存足够时产生一个中断,中断处理程序将调用netif_start_queue重启设备队列*/
outw(SetTxThreshold + 1536, ioaddr + EL3_CMD);
spin_unlock_irqrestore(&lp->lock, flags);
dev_kfree_skb (skb);
/* Clear the Tx status stack. */
{
short tx_status;
int i = 4;
while (--i > 0 && (tx_status = inb(ioaddr + TX_STATUS)) > 0) {
if (tx_status & 0x38) dev->stats.tx_aborted_errors++;
if (tx_status & 0x30) outw(TxReset, ioaddr + EL3_CMD);
if (tx_status & 0x3C) outw(TxEnable, ioaddr + EL3_CMD);
outb(0x00, ioaddr + TX_STATUS); /* Pop the status stack. */
}
}
return NETDEV_TX_OK;
}
中断共享
IRQ线是有限的资源,增加设备能容纳的设备数目简单方式就是允许多台设备共享一个IRQ。理论上讲,每个设备根据IRQ号注册自己的中断处理例程。该IRQ线上中断发生时,内核启动注册在这个IRQ号的所有中断处理例程,由中断例程去过滤判断是否是自己的设备产生的中断,而不是内核来查找正确的设备。
一组设备共享中断时,所有这个设备注册的设备驱动程序都必须有能力处理共享中断,若设备需要独享中断线,则需要在注册中断处理例程的标记位中关闭共享中断标记位,这样若有其他设备再注册这个IRQ号,内核会返回失败。
IRQ中断处理例程映射存储在一个全局向量表中,每一个IRQ好对应一个中断处理例程链表,多台设备共享一个IRQ时,一个列表中才会有一个以上的元素,IRQ存储数组的大小取决于CPU的架构体系,可以从15到200以上。
中断处理例程描述符由irqaction来表示,在request_irq函数中,调用setup_irq来安装中断处理例程,就是将一个irqaction结构作为其输入参数:
/**
* struct irqaction - per interrupt action descriptor
* @handler: interrupt handler function
* @flags: flags (see IRQF_* above)
* @name: name of the device
* @dev_id: cookie to identify the device
* @next: pointer to the next irqaction for shared interrupts
* @irq: interrupt number
* @dir: pointer to the proc/irq/NN/name entry
* @thread_fn: interrupt handler function for threaded interrupts
* @thread: thread pointer for threaded interrupts
* @thread_flags: flags related to @thread
* @thread_mask: bitmask for keeping track of @thread activity
*/
struct irqaction {
irq_handler_t handler; //中断处理例程
unsigned long flags; //标记位,SA_SHIRQ共享中断,SA_SAMPLE_RANDOM将设备自身作为随机事件来源
SA_INTERRUPT,置位则当中断处理例程在本地处理器上运行时,关闭中断,只有能快速完成中断处理例程才能指定这个值
void *dev_id; //设备标记符,共享中断时区别不同的设备,中断处理例程函数中void *参数传递的就是这个参数
与此设备相关的net_device数据结构的指针,这里声明为void *类型是因为不仅仅是NIC设备才使用IRQ,其他设备也要使用中断,这个需要代表不同的设备类型,因此这里使用通用的类型声明
struct irqaction *next; //下一个中断处理例程,所有共享此IRQ的设备中断例程组成一个链表
int irq;
irq_handler_t thread_fn;
struct task_struct *thread;
unsigned long thread_flags;
unsigned long thread_mask;
const char *name; //设备名称,从/proc/interrupts文件可以看到这个设备名称
struct proc_dir_entry *dir;
} ____cacheline_internodealigned_in_smp;
中断例程安装函数:
int setup_irq(unsigned int irq, struct irqaction *act)
{
int retval;
struct irq_desc *desc = irq_to_desc(irq);
chip_bus_lock(desc);
retval = __setup_irq(irq, desc, act);
chip_bus_sync_unlock(desc);
return retval;
}
setup_irq函数将irqaction结构插入irq_desc描述符队列中,每个IRQ都有一个irq_desc实例,每个中断处理例程都有一个irqaction实例。
irq_desc实例向量的大小是和处理器体系结构相关的,有体系结构的NR_IRQS指定。
当给定的IRQ号有一个以上的irqaction实例时,就是中断共享,每个结构都必须设置SA_SHIRQ标记。
#define SA_SHIRQ IRQF_SHARED