网络相关驱动点滴

1. proto_register:
rc = proto_register(&proto, 1);
把proto加到一个prot_list中,proto_list是一个全局的静态链表,inet域支持的所有协议全部在这个链表中,但这个链表在协议栈中并没有太大用途,它只是用于在/proc/net/protocols文件中输出当前系统所支持的所有协议。
这个函数是在 linux/net/core/sock.c 中定义的,除了可以将这个协议添加到活动协议列表中之外,如果需要,该函数还可以选择分配一到多个 slab 缓存.

2. sock_register函数将某项协议注册到协议族数组net_families[]中
/* sock_register - add a socket protocol handler
* @ops: description of protocol
*
* This function is called by a protocol handler that wants to
* advertise its address family, and have it linked into the
* socket interface.The value ops->family coresponds to the
* socket system call protocol family.
*/
int sock_register(const struct net_proto_family *ops)
{
int err;

if (ops->family >= NPROTO) {
printk(KERN_CRIT "protocol %d >= NPROTO(%d)\n", ops->family,
NPROTO);
return -ENOBUFS;
}

spin_lock(&net_family_lock);
if (net_families[ops->family])
err = -EEXIST;
else {
net_families[ops->family] = ops;
err = 0;
}
spin_unlock(&net_family_lock);

printk(KERN_INFO "NET: Registered protocol family %d\n", ops->family);
return err;
}


struct net_proto_family {
int family;
int (*create)(struct net *net, struct socket *sock, int protocol);
struct module *owner;
};

3. 在/proc/net/下创建我们需要的文件 ,这里procfs已经给出了接口(all the APIs are defined in linux/include/linux/proc_fs.h):
linux\fs\proc\proc_net.c
extern struct proc_dir_entry *proc_net_fops_create(struct net *net,
const char *name, mode_t mode, const struct file_operations *fops);
extern void proc_net_remove(struct net *net, const char *name);
extern struct proc_dir_entry *proc_net_mkdir(struct net *net, const char *name,
struct proc_dir_entry *parent);

4. Seq文件接口:
当故障的原因很难确定时,监控和分析由procfs提供的数据可能会提供帮助。
但是,当数据量很大时,procfs的read()实现变得很复杂。Seq 文件接口是一种内核
提供的帮助机制用来简化这样的实现。Seq文件接口使得procfs操作干净又整洁。

让我们先介绍一个逐步变得复杂的procfs read()实现,然后看看Seq文件接口如何将
这个复杂的实现变得优雅。我们也会逐步跟新2.6内核中还没有采用seq file的驱动.


Seq 文件接口的优点

通过一个例子来看看Seq的优点吧。同通常的设备驱动一样,假设你有某一个数据结构的链表,
每一个节点都包含一个字符串域(称为info)。一下代码C1通过一个/proc/readme文件将其输出
到用户空间。当用户读这个文件,procfs的read()方法的具体实现,readme_proc将被调用,它将
遍历链表,将所有节点的info域传递给文件系统缓冲区。
C1:

#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/proc_fs.h>
MODULE_DESCRIPTION("test proc");
MODULE_LICENSE("GPL");

/* Private Data structure */
struct _mydrv_struct
{
/* ... */
struct list_head list; /* Link to the next node */
char info[10]; /* Info to pass via the procfs file */
/* ... */
};
static LIST_HEAD(mydrv_list); /* List Head */

static int readme_proc(char *page, char **start, off_t offset, int count, int *eof, void *data)
{

off_t thischunk_len = 0;
struct _mydrv_struct *p;

printk( KERN_DEBUG "offset is %d\n",offset );
/* Traverse the list and copy info into the supplied buffer */
list_for_each_entry(p, &mydrv_list, list)
{
thischunk_len += sprintf(page+thischunk_len, p->info);
}
*eof = 1; /* Indicate completion */
return thischunk_len;
}


static int test_proc_init_module(void)
{
int i;
static struct proc_dir_entry *entry = NULL ;
struct _mydrv_struct * mydrv_new;


/* Create /proc/readme */
entry = create_proc_entry("readme", S_IWUSR, NULL);
if (entry)
{
entry->read_proc = readme_proc;
}
/* Handcraft mydrv_list for testing purpose.
In the real world, device driver logic
maintains the list and populates the 'info' field */
for (i=0;i<100;i++)
{
mydrv_new = kmalloc(sizeof(struct _mydrv_struct), GFP_ATOMIC);
sprintf(mydrv_new->info, "Node No: %d\n", i);
list_add_tail(&mydrv_new->list, &mydrv_list);
}
return 0;
}

static void test_proc_exit_module(void)
{
remove_proc_entry("readme",0);
printk( KERN_DEBUG "Module test_proc exit\n" );
}

module_init(test_proc_init_module);
module_exit(test_proc_exit_module);


使用如下命令
bash> cat /proc/readme
Node No: 0
Node No: 1
...
Node No: 99

当read()方法被调用,它将提供一页的内存(译者注:通常是4k)用来传递信息到用户空间。
正如你所看到的,readme_proc的第一个参数是一个指向1页大小的缓冲区的指针。
第二个参数start用于当数据量超过1页时read方法的实现,在例子C2中我们可以明确它的使用方法。
接下来的两个参数非别用于指定当前读请求的偏移和要读的byte数量
*eof用来告诉调用者是否还需要读更多的数据。如果你注释掉了上面对*eof置一的语句,
readme_proc将被再次调用,且offset被指定为1190(节点0-节点99中info域最终显示的字符串数量)
readme_proc返回此次写入缓冲区的有效字符数。

在例子C1里,procfs read方法的返回被限定在1页以内。如果你将节点数量从100增加到500,将看到以下输出:

bash> cat /proc/readmeNode No: 0Node No: 1...Node No: 322proc_file_read: Apparent buffer overflow!
正如你所看到的,当超过4096byte的ascii字符被制造出来,产生了一个缓冲区溢出
为了解决这个问题,你需要修改c1的代码,使用以前提到的start变量
这将导致代码有点复杂,这样的实现基于以下原理:
1、readme_proc被调用多次,每次调用都将改变两个参数:每次要地区的最大数据量count和相对于
文件开始的偏移。每次的count值必须小于一页
2、每次调用结束后,offset增加readme_proc的返回值的大小
3、readme_proc的eof信号仅仅当总共产生的数据量小于等于本次请求的count+offset才发出。
如果eof没有被设置,readme_proc将再次被调用,传递给它的offset将增加上一次调用readme_proc的
返回值
4、每次调用结束后,只有start指向的数据是有效的并被传递给调用者

为了更好地理解操作过程,
C2代码将打印每次被调用的参数: start、offset、count、page
通过以下修改,你的代码可以摆脱限制,输出大量的信息

bash> cat /proc/readmeNode No: 0Node No: 1...Node No: 499
C2:

#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/proc_fs.h>
MODULE_DESCRIPTION("test proc");
MODULE_AUTHOR("root ()");
MODULE_LICENSE("GPL");

/* Private Data structure */
struct _mydrv_struct
{
/* ... */
struct list_head list; /* Link to the next node */
char info[10]; /* Info to pass via the procfs file */
/* ... */
};
static LIST_HEAD(mydrv_list); /* List Head */

static int readme_proc(char *page, char **start, off_t offset, int count, int *eof, void *data)
{

int i = 0;
off_t thischunk_start = 0;
off_t thischunk_len = 0;
struct _mydrv_struct *p;
/* Loop thru the list collecting device info */
list_for_each_entry(p, &mydrv_list, list) {
thischunk_len += sprintf(page+thischunk_len, p->info);

/* Advance thischunk_start only to the extent that the next
* read will not result in total bytes more than (offset+count)
*/
if (thischunk_start + thischunk_len < offset) {
thischunk_start += thischunk_len;
thischunk_len = 0;
} else if (thischunk_start + thischunk_len > offset+count) {
break;
} else {
continue;
}
}

/* Actual start */
*start = page + (offset - thischunk_start);

/* Calculate number of written bytes */
thischunk_len -= (offset - thischunk_start);
if (thischunk_len > count) {
thischunk_len = count;
} else {
*eof = 1;
}

return thischunk_len;

}


static int test_proc_init_module(void)
{
int i;
static struct proc_dir_entry *entry = NULL ;
struct _mydrv_struct * mydrv_new;


/* Create /proc/readme */
entry = create_proc_entry("readme", S_IWUSR, NULL);
if (entry)
{
entry->read_proc = readme_proc;
}
/* Handcraft mydrv_list for testing purpose.
In the real world, device driver logic
maintains the list and populates the 'info' field */
for (i=0;i<500;i++)
{
mydrv_new = kmalloc(sizeof(struct _mydrv_struct), GFP_ATOMIC);
sprintf(mydrv_new->info, "Node No: %d\n", i);
list_add_tail(&mydrv_new->list, &mydrv_list);
}
return 0;
}

static void test_proc_exit_module(void)
{
remove_proc_entry("readme",0);
printk( KERN_DEBUG "Module test_proc exit\n" );
}

module_init(test_proc_init_module);
module_exit(test_proc_exit_module);


当你面对C2中复杂的read实现感到前途黯淡时,Seq 文件接口前来救驾。
顾名思义,seq file interface 将procfs文件内容看做是序列化的对象
(Seq)编程接口提供的是这个序列化对象的迭代器
因此你的Seq代码必须实现以下的迭代器方法
1、start()将被seq interface首先调用,它初始化迭代子对象的位置并且返回插入的第一个迭代子对象
2、next()增加迭代器的位置,返回指向下一个迭代子的指针。这个函数不知道迭代子的内核结构,
将其看做黑盒
3、show()当用户读procfs的文件时,解释传递给它的迭代子,转换成要输出给用户的字符串。
这个方法充分利用了内核提供的 seq_printf(), seq_putc(), and seq_puts() 来格式化输出
4、stop()最后被调用用来清理

响应用户对相关proc文件的操作时,seq file interface会自动调用以上迭代器方法去产生输出。
你不用再去担心页大小和eof信号

C3


#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>

MODULE_DESCRIPTION("test proc");
MODULE_AUTHOR("root ()");
MODULE_LICENSE("GPL");

/* Private Data structure */
struct _mydrv_struct
{
/* ... */
struct list_head list; /* Link to the next node */
char info[10]; /* Info to pass via the procfs file */
/* ... */
};
static LIST_HEAD(mydrv_list); /* List Head */

/* start() method */
static void *
mydrv_seq_start(struct seq_file *seq, loff_t *pos)
{
struct _mydrv_struct *p;
loff_t off = 0;

/* The iterator at the requested offset */
list_for_each_entry(p, &mydrv_list, list) {
if (*pos == off++) return p;
}
return NULL;
}

/* next() method */
static void *
mydrv_seq_next(struct seq_file *seq, void *v, loff_t *pos)
{
/* 'v' is a pointer to the iterator returned by start() or
by the previous invocation of next() */
struct list_head *n = ((struct _mydrv_struct *)v)->list.next;

++*pos; /* Advance position */
/* Return the next iterator, which is the next node in the list */
return(n != &mydrv_list) ?
list_entry(n, struct _mydrv_struct, list) : NULL;
}

/* show() method */
static int
mydrv_seq_show(struct seq_file *seq, void *v)
{
const struct _mydrv_struct *p = v;

/* Interpret the iterator, 'v' */
seq_printf(seq, p->info);
return 0;
}

/* stop() method */
static void mydrv_seq_stop(struct seq_file *seq, void *v)
{
/* No cleanup needed in this example */
}

/* Define iterator operations */
static struct seq_operations mydrv_seq_ops = {
.start = mydrv_seq_start,
.next = mydrv_seq_next,
.stop = mydrv_seq_stop,
.show = mydrv_seq_show,
};

static int
mydrv_seq_open(struct inode *inode, struct file *file)
{
/* Register the operators */
return seq_open(file, &mydrv_seq_ops);
}

static struct file_operations mydrv_proc_fops = {
.owner = THIS_MODULE,
.open = mydrv_seq_open, /* User supplied */
.read = seq_read, /* Built-in helper function */
.llseek = seq_lseek, /* Built-in helper function */
.release = seq_release, /* Built-in helper funciton */
};


static int test_proc_init_module(void)
{

int i;
static struct proc_dir_entry *entry = NULL ;
struct _mydrv_struct * mydrv_new;


/* Create /proc/readme */
entry = create_proc_entry("readme", S_IWUSR, NULL);
if (entry)
{
entry->proc_fops = &mydrv_proc_fops;
}
/* Handcraft mydrv_list for testing purpose.
In the real world, device driver logic
maintains the list and populates the 'info' field */
for (i=0;i<500;i++)
{
mydrv_new = kmalloc(sizeof(struct _mydrv_struct), GFP_ATOMIC);
sprintf(mydrv_new->info, "Node No: %d\n", i);
list_add_tail(&mydrv_new->list, &mydrv_list);
}
return 0;
}

static void test_proc_exit_module(void)
{
remove_proc_entry("readme",0);
printk( KERN_DEBUG "Module test_proc exit\n" );
}

module_init(test_proc_init_module);
module_exit(test_proc_exit_module);

5. 网络协议的初始化dev_add_pack

在数据包的处理函数netif_receive_skb中,会先看ptype_all中是否有注册的协议,如果有,则调用相应的处理函数,然后再到ptype_base中,找到合适的协议,将skb发送到相关协议的处理函数.比如ip协议(ip_rcv)或者arp(arp_rcv)等等.此篇笔记讲的是有关ptype_all和ptype_base的相关知识点.

ptype_base和ptype_all在内核中存储的情况如下图:



可以看到,ptype_base为一个hash表,而ptype_all为一个双向链表.每一个里面注册的协议都用一个struct packet_type表示.

struct packet_type
{
unsigned short type; /*协议类型*/
struct net_device *dev;
int (*func) (struct sk_buff *, struct net_device *,
struct packet_type *);
void *data; /* Private to the packet type */
struct packet_type *next;
};
其中需要注意的是dev参数,此参数表明了协议只处理来自dev指向device的数据,当dev=NULL时,表示该协议处理来自所有device的数据.这样,当注册自己的协议时,就可以指定自己想要监听或者接收的device.
其中注册和注销协议的函数为:
dev_add_pack(...)和dev_remove_pack(...)
这两个函数很简单,分别如下:
void dev_add_pack(struct packet_type *pt)
{
int hash;
br_write_lock_bh(BR_NETPROTO_LOCK);
#ifdef CONFIG_NET_FASTROUTE
/* Hack to detect packet socket */
if ((pt->data) && ((int)(pt->data)!=1)) {
netdev_fastroute_obstacles++;
dev_clear_fastroute(pt->dev);
}
#endif
if (pt->type == htons(ETH_P_ALL)) {
netdev_nit++;
pt->next=ptype_all;
ptype_all=pt;
} else {
hash=ntohs(pt->type)&15;
pt->next = ptype_base[hash];
ptype_base[hash] = pt;
}
br_write_unlock_bh(BR_NETPROTO_LOCK);
}
此函数判断协议类型,然后加到ptype_base或者ptype_all中.
void dev_remove_pack(struct packet_type *pt)
{
struct packet_type **pt1;
br_write_lock_bh(BR_NETPROTO_LOCK);
if (pt->type == htons(ETH_P_ALL)) {
netdev_nit--;
pt1=&ptype_all;
} else {
pt1=&ptype_base[ntohs(pt->type)&15];
}
for (; (*pt1) != NULL; pt1 = &((*pt1)->next)) {
if (pt == (*pt1)) {
*pt1 = pt->next;
#ifdef CONFIG_NET_FASTROUTE
if (pt->data)
netdev_fastroute_obstacles--;
#endif
br_write_unlock_bh(BR_NETPROTO_LOCK);
return;
}
}
br_write_unlock_bh(BR_NETPROTO_LOCK);
printk(KERN_WARNING "dev_remove_pack: %p not found.\n", pt);
}
此函数也很简单,只是把协议从相关的链表中移除.
下面以ip协议为例子来看看相关的实现:
ip协议结构体的定义如下:
static struct packet_type ip_packet_type =
{
__constant_htons(ETH_P_IP),
NULL, /* All devices */
ip_rcv,
(void*)1,
NULL,
};
当ipv4协议栈初始化时,会调用ip_init.之后,所有协议类型为ETH_P_IP的包都会交由ip_rcv处理.代码如下:
void __init ip_init(void)
{
dev_add_pack(&ip_packet_type);
ip_rt_init();
inet_initpeers();
#ifdef CONFIG_IP_MULTICAST
proc_net_create("igmp", 0, ip_mc_procinfo);
#endif
}
这样在系统启动之后,ip协议便被注册到ptype_base链表中,相应的处理函数为ip_rcv.


6. 内核中的Notification Chains的分析 Linux内核中有许多子系统,他们之间有着非常多的依赖与交互关系,当某一个子系统中有事件发生时,就会影响到其他子系统的工作,比如说网卡ip地址的改变,设备的热插拔等等。Linux内核中使用了Notification Chains的方法来处理这类事件,顾名思义,它是一个链表的数据结构,其中链表的表元由回调函数,priority和next指针组成。
35 struct notifier_block { 36 int (*notifier_call)(struct notifier_block *, unsigned long, void *); 37 struct notifier_block *next; 38 int priority; 39 }; 当我们初始化子系统的时候,应该知道自己到底对哪些事件比较敏感,也就是说知道哪些事件的发生会对自己的功能发生影响,如果有的话,就注册自己到这个特定的notification chain里面去,我们举一个例子来看看:

1242 void __init arp_init(void)1243 {1244 neigh_table_init(&arp_tbl);12451246 dev_add_pack(&arp_packet_type);1247 arp_proc_init();1248 #ifdef CONFIG_SYSCTL1249 neigh_sysctl_register(NULL, &arp_tbl.parms, NET_IPV4,1250 NET_IPV4_NEIGH, "ipv4", NULL, NULL);1251 #endif1252 register_netdevice_notifier(&arp_netdev_notifier);1253 } 这个取自arp的初始化,我们注意最后一行的函数register_netdevice_notifier,这个就是注册函数,当然它还会有多层的调用,而arp_netdev_notifier就是一个notifier_block的结构,其中回调函数初始化为arp_netdev_event。我们先来看一下这个回调函数的实现

1201 static int arp_netdev_event(struct notifier_block *this, unsigned long event, void *ptr)1202 {1203 struct net_device *dev = ptr;12041205 switch (event) {1206 case NETDEV_CHANGEADDR:1207 neigh_changeaddr(&arp_tbl, dev);1208 rt_cache_flush(0);1209 break;1210 default:1211 break;1212 }12131214 return NOTIFY_DONE;1215 } 我们看到了,它只关心一种event,那就是NETDEV_CHANGEADDR,其他的事件发生它就并不在意了,当网络设备发生任何已经定义好的事件时,会遍历notification chain来进行遍历通知,如果是我们关心的事件发生,我们就作出相应的反应进行处理,如果不是,那么就当什么都没有发生。
注册函数有一个通用的类型函数,上文所见到的register_netdevice_notifier经过二层调用之后就会遇到,这个函数就是
105 static int notifier_chain_register(struct notifier_block **nl,106 struct notifier_block *n)107 {108 while ((*nl) != NULL) {109 if (n->priority > (*nl)->priority)110 break;111 nl = &((*nl)->next);112 }113 n->next = *nl;114 rcu_assign_pointer(*nl, n);115 return 0;116 } 这个函数的作用就是把一个notifier_block加入到chain中去,不过这个链表的插入方法有点诡异,因为它直接利用了二级指针的一个巧妙应用,一般我们插入链表都需要有一个指针来保存前一个位置,然后再插入表元,而在这里,它只利用了一个指针就完成了这些工作,也就是把这个二级指针保存了前一个表元的next指针。
最后我们来看一下那个事件发生时通知在通知链上的表元的函数

131 static int __kprobes notifier_call_chain(struct notifier_block **nl,132 unsigned long val, void *v)133 {134 int ret = NOTIFY_DONE;135 struct notifier_block *nb;136137 nb = rcu_dereference(*nl);138 while (nb) {139 ret = nb->notifier_call(nb, val, v);140 if ((ret & NOTIFY_STOP_MASK) == NOTIFY_STOP_MASK)141 break;142 nb = rcu_dereference(nb->next);143 }144 return ret;145 } 这个函数应该很容易理解,就是调用回调函数进行处理。
Notification Chains最重要的好处是每个子系统自己来注册到底需要听哪些事件,而不是事件的发生方来遍历各个子系统来问我的改变你们是不是需要进行相应的对策处理,这样效率就提高了,也就是所谓的publish-and-subscribe model。


7. dev_hold 和 dev_put, 可以用来增加和递减net_device 的使用计数的.

linux可以使用dev_get_by_name函数取得设备指针,但是使用是需要注意,使用过dev_get_by_name函数后一定要使用dev_put(pDev)函数释放设备引用,不然可能导致GET的设备无法正常卸载。

8. dev_queue_xmit
dev_queue_xmit是有网络设备无关层调用的函数,调用对象调用该函数之后,函数会判断skb中的dev字段,根据这个字段指示的设备调用该设备的发送函数hard_start_xmit来对skb进行转发。
上层在需要发送skb的时候会选择调用dev_queue_xmit,那么至于下层是怎么传递该skb的,上层根本就不用关心,这就是所谓的各层的独立性原理。所以对skb具体的发送处理过程,可以由下层网络接口的hard_queue_xmit来处理。比如说上层需要发送一个广播帧,那么它就将skb->pkt_type赋值为PACKET_BROADCAST,然后调用dev_queue_xmit将其发送出去之后就不管下层是否将这个广播帧真的放到网络中进行广播。而下层如果是一个与上层绑定好了的虚拟网络设备的话,它可以在自己的hard_start_xmit中对skb->pkt_type字段为PACKET_BROADCAST的skb进行特定的处理,这里指的特定就是说,不一定非要将这个skb放到网络中进行广播。

9. sk_alloc: 生成一个sock结构体
/**
* sk_alloc - All socket objects are allocated here
* @net: the applicable net namespace
* @family: protocol family
* @priority: for allocation (%GFP_KERNEL, %GFP_ATOMIC, etc)
* @prot: struct proto associated with this new sock instance
*/
struct sock *sk_alloc(struct net *net, int family, gfp_t priority,
struct proto *prot)
{
struct sock *sk;

sk = sk_prot_alloc(prot, priority | __GFP_ZERO, family);
if (sk) {
sk->sk_family = family;
/*
* See comment in struct sock definition to understand
* why we need sk_prot_creator -acme
*/
sk->sk_prot = sk->sk_prot_creator = prot;
sock_lock_init(sk);
sock_net_set(sk, get_net(net));
atomic_set(&sk->sk_wmem_alloc, 1);

sock_update_classid(sk);
}

return sk;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值