网络子系统的实现
【本文导读】本篇从内核启动到识别网卡过程,再到驱动程序的具体实现。本篇属于综合性的文章,涉及到内核模块的很多实现细节。从uboot传参到内核启动,子系统的实现,具体驱动程序的实现等,为上层协议栈打下坚实基础。
【keywords】参数,netdevice writed by huangjl 2012.3.19
1. 系统(从uboot到内核)启动过程中对网卡的初步识别
1.1命令行参数的定义
通常在uboot中已经设置了相关的网络接口参数,一般的形式如下:
#define CONFIG_EXTRA_ENV_SETTINGS \
"netdev="eth0"\0" \
"uboot="/tftpboot/uboot.bin "\0" \
………………….
Uboot将这些参数传递给内核,而内核也需要事先知道这些参数是什么和怎么去解析和注册,因此有__setup(string, function);
1.2命令行参数的解析
首先请参照文件 ../内核启动过程/start_kernel函数.doc文件。目前只看关心到的函数.
asmlinkage void __init start_kernel(void)
{
……..
parse_early_param();
parse_args("Booting kernel", static_command_line, __start___param, __stop___param- __start___param, &unknown_bootoption);
……..
}
parse_early_param()的执行过程为:
parse_early_param—> parse_early_options(tmp_cmdline)------->
parse_args("early options", cmdline, NULL, 0, do_early_param)------>>> do_early_param
最后do_early_param执行的过程如下:
static int __init do_early_param(char *param, char *val)
{ struct obs_kernel_param *p;
for (p = __setup_start; p < __setup_end; p++) {
if ((p->early && strcmp(param, p->str) == 0) || (strcmp(param, "console") == 0 &&
strcmp(p->str, "earlycon") == 0)) {
if (p->setup_func(val) != 0)
printk(KERN_WARNING "Malformed early option '%s'\n", param);
}
}
/* We accept everything at this stage. */
return 0;
}
下面这段代码可以是这两个参数解析分两层的设计原因:
#define early_param(str, fn) __setup_param(str, fn, fn, 1)
#define __setup(str, fn) __setup_param(str, fn, fn, 0)
两者区别在于是否是early。对于参数的定义和解析由专门的数据结构来描述:
/*********/include/linux/init.h******************/
struct obs_kernel_param {
const char *str;
int (*setup_func)(char *);
int early;
};
__setup_param定义如下:
#define __setup_param(str, unique_id, fn, early) \
static const char __setup_str_##unique_id[] __initconst \
__aligned(1) = str; \
static struct obs_kernel_param __setup_##unique_id \
__used __section(.init.setup) \
__attribute__((aligned((sizeof(long))))) \
= { __setup_str_##unique_id, fn, early }
它将传进来的参数以obs_kernel_param结构实例化存储在.init.setup段中,并在__setup_start
与__setup_end区间之间。
两个参数分析简单总结如下:
参数分析 | 开始位置 | 结束位置 |
parse_early_param(阶段一) | __setup_start | __setup_end |
parse_args(阶段二) | __start___param | __start___param |
1.3处理网络子系统命令行参数的函数
/*****************net/core/dev.c************************/
int __init netdev_boot_setup(char *str) /*str一般为netdev=后面的网络接口名*/
{
int ints[5];
struct ifmap map;
str = get_options(str, ARRAY_SIZE(ints), ints);/*分析一个串存于整形数组中,这是一个将字符串转化为整形数字的过程*/
if (!str || !*str)
return 0;
/* Save settings */
memset(&map, 0, sizeof(map));
if (ints[0] > 0)
map.irq = ints[1];/*中断号*/
if (ints[0] > 1)
map.base_addr = ints[2]; /*基地址*/
if (ints[0] > 2)
map.mem_start = ints[3]; /*内存开始地址*/
if (ints[0] > 3)
map.mem_end = ints[4]; /*内存结束地址*/
/* Add new entry to the list */
return netdev_boot_setup_add(str, &map);
}
__setup("netdev=", netdev_boot_setup);
Ifmap的结构如下:
struct ifmap {
unsigned long mem_start;
unsigned long mem_end;
unsigned short base_addr;
unsigned char irq;
unsigned char dma;
unsigned char port;
/* 3 bytes spare */
};
所有的配置都保存在如下结构中
static struct netdev_boot_setup dev_boot_setup[NETDEV_BOOT_SETUP_MAX];
通过代码,系统最大支持8个网络设备接口,当然可以按照需要改动。通过上面的分析,我
们知道,uboot通过参数传递到内核,内核将网络配置信息保存在netdev_boot_setup结构数
组中,那么,这个数组怎么跟具体的网络实例netdevice联系起来呢???在实际的设备探
测过程中,如下函数被调用
int netdev_boot_setup_check(struct net_device *dev)
{
struct netdev_boot_setup *s = dev_boot_setup;
int i;
for (i = 0; i < NETDEV_BOOT_SETUP_MAX; i++) {
if (s[i].name[0] != '\0' && s[i].name[0] != ' ' &&
!strcmp(dev->name, s[i].name)) {
dev->irq = s[i].map.irq;
dev->base_addr = s[i].map.base_addr;
dev->mem_start = s[i].map.mem_start;
dev->mem_end = s[i].map.mem_end;
return 1;
}
}
return 0;
}
EXPORT_SYMBOL(netdev_boot_setup_check);
该函数很清晰,探测过程也就是设备名比较的过程。
2.网络子系统的初始化
2.1 网络子系统初始化过程
关于子系统的初始化,请参见写的一篇文章----USB子系统源码分析。任何子系统都是
通过subsys_initcall宏来实现。#define subsys_initcall(fn) __define_initcall("4",fn,4)。
static int __init net_dev_init(void)
{
int i, rc = -ENOMEM;
BUG_ON(!dev_boot_phase);
if (dev_proc_init()) /*初始化网络在proc下的入口主要创建一些配置
文件*/
goto out;
if (netdev_kobject_init())/*初始化网络设备在sys下面的配置*/
goto out;
INIT_LIST_HEAD(&ptype_all);
for (i = 0; i < PTYPE_HASH_SIZE; i++)
INIT_LIST_HEAD(&ptype_base[i]);
if (register_pernet_subsys(&netdev_net_ops))
goto out;
for_each_possible_cpu(i) {/*初始化每个CPU的数据包的输入输出队列*/
struct softnet_data *queue;
queue = &per_cpu(softnet_data, i);
skb_queue_head_init(&queue->input_pkt_queue);
queue->completion_queue = NULL;
INIT_LIST_HEAD(&queue->poll_list);
queue->backlog.poll = process_backlog;
queue->backlog.weight = weight_p;
queue->backlog.gro_list = NULL;
queue->backlog.gro_count = 0;
}
dev_boot_phase = 0;
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);
hotcpu_notifier(dev_cpu_callback, 0);
dst_init();
dev_mcast_init();
rc = 0;
out:
return rc;
}
subsys_initcall(net_dev_init);
2.2 事件通知链
如果一个模块(A模块)发生了变化,影响到另一个模块(B模块),内核处理的方式是
事件通知链。其实道理很简单,被影响的B模块需要注册通知函数到A中,注册接口由A
提供,A模块发生了变化,就会执行注册到其链表上的所有函数,也就是通知其他模块的过
程。以net_dev_init函数中的hotcpu_notifier(dev_cpu_callback, 0)为例。
/*include/linux/cpu.h*/
#define hotcpu_notifier(fn, pri) cpu_notifier(fn, pri)
在多处理器上这样定义:
#define cpu_notifier(fn, pri) { \
static struct notifier_block fn##_nb __cpuinitdata = \
{ .notifier_call = fn, .priority = pri }; \
register_cpu_notifier(&fn##_nb); \
}
register_cpu_notifier函数完成将以fn函数组成的notifier_block结构变量fn##_nb添加到
cpu_chain链表中去。cpu_chain是raw_notifier_head结构。对于CPU自身数据的访问,不需
要上锁。其他的具体参考如下:
static __cpuinitdata RAW_NOTIFIER_HEAD(cpu_chain);
事件通知结构定义如下:
struct notifier_block {
int (*notifier_call)(struct notifier_block *, unsigned long, void *);
struct notifier_block *next;
int priority;
};
而变化的模块链表头有以下几种定义:
struct atomic_notifier_head {
spinlock_t lock;
struct notifier_block *head;
};
struct blocking_notifier_head {
struct rw_semaphore rwsem;
struct notifier_block *head;
};
struct raw_notifier_head {
struct notifier_block *head;
};
struct srcu_notifier_head {
struct mutex mutex;
struct srcu_struct srcu;
struct notifier_block *head;
};
网络子系统一共注册了三种事件通知链:
netdev_chain(网络设备注册或者网络状态发生变化),inetaddr_chain, inet6addr_chain(IP4-6地
址发生变化)
好了,网络子系统初始化基本知识差不多就是这些了,本着够用原则,使用到相关的知识再
去补充,接下来便是网络驱动程序的编写了。
3网卡的探测过程
由于系统可能存在多种网卡或者不同体系结构中网卡的通过不同的总线相连,系统在
处理多个网卡的时候将探测函数集中在一起探测执行。探测函数
device_initcall(net_olddevs_init);
又是_initcall,基于前面的基础研究工作,肯定是在系统初始化的时候被执行。
#define device_initcall(fn) __define_initcall("6",fn,6)
从device_initcall定义可以看出,net_olddevs_init函数被放在.initcall6.init这段内存中这段内存区域中。
/* Statically configured drivers -- order matters here. */
static int __init net_olddevs_init(void)
{
int num;
……………………..
for (num = 0; num < 8; ++num)
ethif_probe2(num);
……………………..
return 0;
}
static void __init ethif_probe2(int unit)
{
unsigned long base_addr = netdev_boot_base("eth", unit);
if (base_addr == 1)
return;
(void)(probe_list2(unit, m68k_probes, base_addr == 0) &&
probe_list2(unit, eisa_probes, base_addr == 0) &&
probe_list2(unit, mca_probes, base_addr == 0) &&
probe_list2(unit, isa_probes, base_addr == 0) &&
probe_list2(unit, parport_probes, base_addr == 0));
}
应该还记得,将uboot传进来的参数经过分析后,得到网卡的相关信息存在
static struct netdev_boot_setup dev_boot_setup[NETDEV_BOOT_SETUP_MAX];这个结构中,
这个在1.3节已经提到。netdev_boot_base函数从这个结构中比较名字为eth#unit的网卡信息,
并返回基地址。接下来便是探测的过程。关于这个过程就不详述了。请参照源码。
对于cs89xx:
static struct devprobe2 isa_probes[] __initdata = {
………
#ifdef CONFIG_CS89x0
{cs89x0_probe, 0},
#endif
……….
}
4.中断的实现
4.1中断原理
中断控制器接收各种设备传进来的中断请求信号,IRQ0-IRQi,对于未被屏蔽的中断请求会被送进中断优先级仲裁器,中断控制器向CPU产生一个公共的中断请求信号,当CPU响应这个中断请求信号后,CPU便会发出中断响应信号INTA,中断控制器便将优先级最高的中断请求信号送往CPU中,中断请求信号便于系统找到中断处理程序。
为了管理中断,内核需要对硬件资源进行抽象化。首先对于中断请求号
struct irq_desc {
unsigned int irq;
struct timer_rand_state *timer_rand_state;
unsigned int *kstat_irqs;
irq_flow_handler_t handle_irq;
struct irq_chip *chip;
struct msi_desc *msi_desc;
void *handler_data;
void *chip_data;
struct irqaction *action; /* IRQ action list */
unsigned int status; /* IRQ status */
unsigned int depth; /* nested irq disables */
unsigned int wake_depth; /* nested wake enables */
unsigned int irq_count; /* For detecting broken IRQs */
unsigned long last_unhandled;
unsigned int irqs_unhandled;
raw_spinlock_t lock;
atomic_t threads_active;
wait_queue_head_t wait_for_threads;
const char *name;
} ____cacheline_internodealigned_in_smp;
struct irqaction {
irq_handler_t handler;
unsigned long flags;
const char *name;
void *dev_id;
struct irqaction *next;
int irq;
struct proc_dir_entry *dir;
irq_handler_t thread_fn;
struct task_struct *thread;
unsigned long thread_flags;
};
因此,通过以上两个数据结构,系统的中断向量表结构可形象的表示如下:
4.2 软中断
内核定义了如下几种软中断:
enum
{
HI_SOFTIRQ=0,
TIMER_SOFTIRQ,
NET_TX_SOFTIRQ,
NET_RX_SOFTIRQ,
BLOCK_SOFTIRQ,
BLOCK_IOPOLL_SOFTIRQ,
TASKLET_SOFTIRQ,
SCHED_SOFTIRQ,
HRTIMER_SOFTIRQ,
RCU_SOFTIRQ, /* Preferable RCU should always be the last softirq */
NR_SOFTIRQS
};
4.3软中断在内核中的描述
struct softirq_action
{
void (*action)(struct softirq_action *);
};
4.4注册软中断处理程序
extern void open_softirq(int nr, void (*action)(struct softirq_action *));
nr是中断号, action是处理程序
当然,注册后,系统将这些信息添加到软中断向量表中。
在网络子系统初始化函数net_dev_init中,open_softirq(NET_TX_SOFTIRQ, net_tx_action);
open_softirq(NET_RX_SOFTIRQ, net_rx_action);注册接收和发送两个软中断。
4.4 调度软中断的时机
软中断调度的时机一般是硬件中断完成后便立即调度。内核使用do_softirq(void)函数来调度
除此之外,内核还会在以下几种情况检查是否有软中断需要执行。
5.网络驱动程序的具体实现
下面比较详细地分析cs89xx.c网络驱动程序的实现。主要实现以下几个目标(1)掌握网络驱动程序结构.(2)掌握为分析上层协议提供接口是怎么实现的.(3)掌握中断的实现和使用方法(4)掌握CPU通过总线访问外设的方法
5.1设备驱动程序框架结构图
5.2驱动程序分析
刚开始本打算分析isa-skeleton.c程序结合上述框架图来分析,由于isa-skeleton.c只是网络驱动程序的框架,什么事情也不能完成。本文以cs89xx.c为例来具体分析,当然按照上述结构图来分析,必要的时候,会提下isa-skeleton.c中模板相关代码是怎么实现的。本着够用的原则,顺便会带出相关的知识.由于篇幅有限,源码量比较大,紧紧列出关键代码,具体源码请参见内核中的源码.另外为了更好的理解代码,请参照LDD3相关章节。
5.2.1模块初始化
int __init init_module(void)
{/*分配网络实体结构并设置默认默认的数据,在分配net_device时候就分配net_local私有结构,这个函数真是太方便了.*/
struct net_device *dev = alloc_etherdev(sizeof(struct net_local));
struct net_local *lp;
int ret = 0;
if (!dev)
return -ENOMEM;
……………………
dev->irq = irq;
dev->base_addr = io;
lp = netdev_priv(dev);
…………………………….
ret = cs89x0_probe1(dev, io, 1);
if (ret)
goto out;
dev_cs89x0 = dev;
return 0;
out:
free_netdev(dev);
return ret;
}
net_device结构中的net_local保存网卡的私有信息,相应的结构如下:
/* Information that need to be kept for each board. */
struct net_local {
struct net_device_stats stats;
int chip_type; /* one of: CS8900, CS8920, CS8920M */
char chip_revision; /* revision letter of the chip ('A'...) */
int send_cmd;
int auto_neg_cnf; /* auto-negotiation word from EEPROM */
int adapter_cnf; /* adapter configuration from EEPROM */
int isa_config; /* ISA configuration from EEPROM */
int irq_map; /* IRQ map from EEPROM */
int rx_mode;
int curr_rx_cfg; /* a copy of PP_RxCFG */
int linectl; /* either 0 or LOW_RX_SQUELCH, depending on configuration. */
int send_underrun; /* keep track of how many underruns in a row we get */
int force; /* force various values; see FORCE* above. */
spinlock_t lock;
#if ALLOW_DMA
int use_dma; /* Flag: we're using dma */
int dma; /* DMA channel */
int dmasize; /* 16 or 64 */
unsigned char *dma_buff; /* points to the beginning of the buffer */
unsigned char *end_dma_buff; /* points to the end of the buffer */
unsigned char *rx_dma_ptr; /* points to the next packet */
#endif
};
这里有个小知识:__init在函数前面表明该函数在系统启动时候会被初始化,直接用init_module命名初始化函数就不必使用module_init()函数来说明哪个函数是初始化函数.
init_module主要分配net_device结构,赋值中断号,网络接口基地址(中断号,网口基地址可以用ifconfig命令来指定,也可以通过接eeprom来加载其配置)然后跳转到cs89x0_probe1去执行.这个函数相当庞大。主要完成以下事情:
(1)请求分配IO端口
(2)正确配置网卡类型和线路连接,填充netdevice地址等信息
(3)填充网络设备的具体操作函数
(4)向系统注册网络设备
关于具体的网络设备的操作,由于列出源码篇幅太多,请自己详细分析源码,在此并不一一列举了。只要按照设备驱动程序框架图去分析就行了。