当内核引导时,会执行start_kernel对一些子系统做初始化,在start_kernel终止前会调用init内核线程,由其负责初始化的后续工作。网络子系统的初始化活动多数都是在do_basic_setup内完成的。
在初始化任务中,主要有三个:
- 引导期间选项参数:调用两次parse_args(一次直接调用,一次通过parse_early_param间接调用)以处理引导加载程序(boot loader,如LILO或GRUB)在引导期间传给内核的配置参数;
- 中断和定时器:硬中断和软中断分别由init_IRQ和softirq_init做初始化。定时器在引导期间就完成初始化,便于后续任务使用。
- 初始化函数:内核子系统和内建的设备驱动程序由do_initcalls初始化。
run_init_process确定在系统上运行的第一个进程,也就是所有其他进程的父进程,其PID为1,一直运行直到系统做完工作。正常情况下运行的程序是init,但可以通过引导选项init=参数指定另一个不同程序。不提供这个选项,内核会尝试从一些众所周知的位置去执行init命令,若找不到就会发生内核panic。
设备的注册和初始化
一个网络设备可用,就必须被内核认可并且关联正确的驱动程序。驱动程序把驱动所需的信息存储在私有数据结构中,然后与其他需要此设备的内核组件交互。注册和初始化任务的一部分由内核负责,其他部分由设备驱动程序完成。设备的初始化主要分为:
- 硬件初始化:由设备驱动程序和通用总线层(如PCI、USB)合作完成,驱动程序将每个设备的这类功能配置为IRQ和I/O地址,使其能与内核交互;
- 软件初始化:设备在能够被使用之前,依赖于所开启和配置的网络协议而定,用户需要提供一些配置参数,如IP地址等;
- 功能初始化:Linux内核有很多网络选项,因为有些选项需要针对每个设备进行配置,因此设备初始化需要配置这些选项。如流量控制功能可以决定封包加入和移出设备出口和入口队列的方式。
NIC的初始化
Linux内核中,每个网络设备都由一个net_device数据结构表示,net_device结构的字段初始化部分由设备驱动程序完成,部分由内核函数完成。这里主要介绍设备驱动程序分配建立设备/内核通信所需的资源。
- IRQ线:每个NIC必须被分派一个IRQ,在必要得时候请求内核的注意。但虚拟设备不需要分派一个IRQ,如回环设备,因其活动都是在内部完成的。/proc/interrupts文件可用于观察当前中断线分派状态。
- I/O端口和内存注册:一般地,驱动程序将其设备的一个内存区域(如配置寄存器)映射到系统内存,使得驱动程序的读/写操作可以通过系统内存地址直接进行,进而简化代码。I/O端口和内存分别使用request_region和release_region注册和释放。
所有的设备都通过轮询和中道观两种方式和内核交互,轮询需要内核定期检查设备状态了解是否发生事件。而中断则是用设备端驱动,在发生事件时产生一个硬件中断信号,请求内核的注意。在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主要通过中断告知驱动程序以下几种情形:
- 收到一帧:这是最常见和标准的情形
- 传输失败:这种情形只有在exponential binary backoff功能失效后,才由Ethernet设备产生。驱动程序不会讲这个通知传送给高层协议栈,它们可以通过其他方式知道失败如定时器到期或重传ACK等。
- DMA传输完成:传输一个帧时,当帧完全复制到NIC的内存准备发送时,驱动程序就会将该帧的缓冲区释放掉。使用同步传输时,若帧完全复制到NIC,驱动程序就会立即知道。但使用DMA异步传输机制时,设备驱动程序就需要等待NIC发出中断事件。调用dev_kfree_skb来释放skb缓冲区。
- 当设备有足够的内存恢复传输时:当出口队列没有足够空间保存一个最大尺寸的帧时(Ethernet NIC通常为1536字节),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

