内核初始化
当内核引导时,会执行start_kernel函数,start_kernel函数对一些子系统做初始化,start_kernel函数终止前调用init内核线程,由其负责后续的初始化。
- 引导期间选项
start_kernel函数里调用两次parse_args,一次是通过parse_early_param间接调用。
这两个用于处理引导加载程序(BOOTLOAD,LILO或GRUB)传递给内核的配置参数。
parse_early_param();
after_dashes = parse_args("Booting kernel",
static_command_line, __start___param,
__stop___param - __start___param,
-1, -1, NULL, &unknown_bootoption);
- 中断和定时器
硬中断和软中断分别在init_IRQ和softirq_init函数中初始化。
init_IRQ();
softirq_init();
-
初始化函数
内核子系统内建的设备驱动程序由do_initcalls初始化。 -
run_init_process
run_init_process函数运行第一个进程,其PID为1,一直运行到系统关机。正常情况下运行的程序是init。也可以在内核启动时通过init=在内核启动参数中指定另一个程序。
if (ramdisk_execute_command) {
ret = run_init_process(ramdisk_execute_command);
if (!ret)
return 0;
pr_err("Failed to execute %s (error %d)\n",
ramdisk_execute_command, ret);
}
/*
* We try each of these until one succeeds.
*
* The Bourne shell can be used instead of init if we are
* trying to recover a really broken machine.
*/
if (execute_command) {
ret = run_init_process(execute_command);
if (!ret)
return 0;
panic("Requested init %s failed (error %d).",
execute_command, ret);
}
if (!try_to_run_init_process("/sbin/init") ||
!try_to_run_init_process("/etc/init") ||
!try_to_run_init_process("/bin/init") ||
!try_to_run_init_process("/bin/sh"))
return 0;
panic("No working init found. Try passing init= option to kernel. "
"See Linux Documentation/admin-guide/init.rst for guidance.");
设备注册和初始化
一个设备可用必须关联到正确的驱动程序。驱动程序把驱动设备所需的所有信息存储在私有数据结构中,然后与其他需要此设备的内核组件交互。
初始化几个阶段:
- 硬件初始化:由设备驱动程序和总线(PCI或USB)合作完成。
- 软件初始化
- 功能初始化
NIC初始化
Linux内核中,每个网络设备都有一个net_device数据结构标识,这个结构的部分字段由驱动程序初始化,部分由内核初始化。驱动程序分配建立设备/内核通信所需的资源:
- IRQ
NIC必须被分配一个IRQ。虚拟设备不需要分配IRQ,比如环路设备。 - IO端口和内存注册
驱动程序将其设备的内存区域(比如设备寄存器)映射到系统内存内,使得驱动程序的读写操作可以通过系统内存地址直接进行。IO端口和内存的注册使用request_regiion进行。
设备与内核交互
设备与内核交互方式有:
- 轮询
- 中断
硬件中断
每个中断事件都会运行一个中断处理程序,当设备驱动程序注册一个NIC时,会请求分配一个IRQ,然后用两个依赖CPU体系结构的函数为给定的IRQ注册或删除处理程序。
两个程序是
request_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);
}
当内核接收到中断通知时,会使用IRQ编号找到该驱动的处理函数并执行。使用IRQ号找到处理函数会用到一张全局表,该表存储有IRQ编号和处理函数之间的对应关系。
中断类型
NIC中断有以下几种:
- 接收数据报文
- 传输事变
- DMA传输完成
给定一个传输报文,使用同步传输时,当报文数据复制到NIC的内存准备传输时,驱动程序就会将缓冲区释放掉。使用DMA时,是异步传输时,缓冲区的释放必须在NIC发出明确的中断事件后。 - 设备有足够内存时
当网卡的出口队列没有足够空间保存一个最大尺寸的帧时,NIC驱动程序会停止出口队列关闭传输。当内存有足够的内存时,使用中断通知开启。
中断共享
Linux系统允许IRQ共享,一组设备共享一个IRQ时,所有这些设备的驱动程序都必须有能力处理共享的IRQ,换言之,当一个设备注册IRQ时必须明确说明是否支持IRQ。
IRQ处理函数存在一个表中,每个IRQ对应一个处理函数的链表。当某个IRQ支持共享时,该IRQ号对应的链表中会有多个元素。
设备处理层初始化
流量控制和每个CPU报文入口队列是网络协议栈的重要部分,这些功能由引导期间由net_dev_init完成。
该函数主要做以下工作
- 初始化两个网络软中断和网络软中断所使用的各个CPU数据结构。
- 使用dev_proc_init函数初始化procfs
- 使用netdev_kobject_init函数初始化sysfs
- 初始化协议处理向量ptype_base,用于分离入口流量的多路合并传输。
/*
* Initialize the DEV module. At boot time this walks the device list and
* unhooks any devices that fail to initialise (normally hardware not
* present) and leaves us with a valid list of present and active devices.
*
*/
/*
* This is called single threaded during boot, so no need
* to take the rtnl semaphore.
*/
static int __init net_dev_init(void)
{
int i, rc = -ENOMEM;
BUG_ON(!dev_boot_phase);
if (dev_proc_init())
goto out;
if (netdev_kobject_init())
goto out;
INIT_LIST_HEAD(&ptype_all);
for (i = 0; i < PTYPE_HASH_SIZE; i++)
INIT_LIST_HEAD(&ptype_base[i]);
INIT_LIST_HEAD(&offload_base);
if (register_pernet_subsys(&netdev_net_ops))
goto out;
/*
* Initialise the packet receive queues.
*/
for_each_possible_cpu(i) {
struct work_struct *flush = per_cpu_ptr(&flush_works, i);
struct softnet_data *sd = &per_cpu(softnet_data, i);
INIT_WORK(flush, flush_backlog);
skb_queue_head_init(&sd->input_pkt_queue);
skb_queue_head_init(&sd->process_queue);
#ifdef CONFIG_XFRM_OFFLOAD
skb_queue_head_init(&sd->xfrm_backlog);
#endif
INIT_LIST_HEAD(&sd->poll_list);
sd->output_queue_tailp = &sd->output_queue;
#ifdef CONFIG_RPS
sd->csd.func = rps_trigger_softirq;
sd->csd.info = sd;
sd->cpu = i;
#endif
init_gro_hash(&sd->backlog);
sd->backlog.poll = process_backlog;
sd->backlog.weight = weight_p;
}
dev_boot_phase = 0;
/* The loopback device is special if any other network devices
* is present in a network namespace the loopback device must
* be present. Since we now dynamically allocate and free the
* loopback device ensure this invariant is maintained by
* keeping the loopback device as the first device on the
* list of network devices. Ensuring the loopback devices
* is the first device that appears and the last network device
* that disappears.
*/
if (register_pernet_device(&loopback_net_ops))
goto out;
if (register_pernet_device(&default_device_ops))
goto out;
open_softirq(NET_TX_SOFTIRQ, net_tx_action);
open_softirq(NET_RX_SOFTIRQ, net_rx_action);
rc = cpuhp_setup_state_nocalls(CPUHP_NET_DEV_DEAD, "net/dev:dead",
NULL, dev_cpu_dead);
WARN_ON(rc < 0);
rc = 0;
out:
return rc;
}
热插拔
Linux引入热插拔是为了实现PnP(plug and play,即插即用)功能,这项功能让内核侦测可热插拔设备的插入和删除。然后通知用户应用程序,提供足够的细节,使其在必要时加载相关联的驱动程序。
编译内核模块时,目标文件放在/lib/modules/kernel_version/目录下,在这个目录下存在两个文件:
modules.pcimap和modules.usbmap。这些文件包含内核支持设备的PCI ID和USB ID,这些文件还包括了相关联内核模块的引用,当用户空间辅助程序接收到一个可热插拔设备插入的信息时,使用这些文件找到正确的驱动程序。
hotplug
/sbin/hotplug脚本是默认的用户空间辅助程序。内核调用kobject_hotplug函数相应一个设备的插入和删除。