内核中的TCP的追踪分析-4-TCP(IPV4)的socket的地址绑定-续

2020-05-25 00:00:00 函数 队列 调用 初始化 结构

在内核中CONFIG_NET_NS配置选项是为了让用户自定义自己的网络空间结构,即上面的net结构,可以看出2.6.26内核的灵活性,但是我们一般在内核中不会配置该项,所以这里应该是取得init_net,这个结构是在前一节分析的那样在do_one_initcall()机制中调用了从pure_initcall(net_ns_init)注册的net_ns_init()初始化的,这个net_ns_init函数进一步调用setup_net()来对init_net结构进行详细的设置。我们这里不细细分析了他的过程类似于我们前边那节socket的初始化的流程,只不过在setup_net对init_net初始化中有一个非常重要的循环
list_for_each_entry(ops, &pernet_list, list) {
if (ops->init) {
error = ops->init(net);
if (error 0)
goto out_undo;
}
}
在这个循环中会从已经注册到pernet_list队列中依次通过其钩子结构pernet_operations,调用结构中的钩子函数init对这个net网络命名空间结构进行初始设置。而这里的pernet_list是一个队列头,它在/net/core/Net_namespace.c文件中的16行处这个队列专门用于person net 用户自定义的pernet_operations结构的登记,也就是我们上面提过的CONFIG_NET_NS情形时才可以将用户自定义的pernet_operations结构链入队列。

static LIST_HEAD(pernet_list);
而这里的LIST_HEAD宏在include/linux/list.h

#define LIST_HEAD(name) \
struct list_head name = LIST_HEAD_INIT(name)
#define LIST_HEAD_INIT(name) { &(name), &(name) }
struct list_head {
struct list_head *next, *prev;
};
朋友们可能注意到我经常将调用在前,说明在后的排列数据结构说明,这种习惯个人感觉有助于理解和记忆。“带着问题学习”远比摸黑效率高。那么这个结构是什么时候被初始化的呢?我们结合本类的节关于socket的初始化中提到的initcall机制会看到在/net/core/dev.c中有一句
subsys_initcall(net_dev_init);
他引用了include/linux/init.h中的
#define subsys_initcall(fn) __define_initcall("4",fn,4)
所以按照节中提到的initcall机制那部分内容,net_dev_init函数将会在开机时得到执行,在这个函数内部有这样的代码:

if (register_pernet_subsys(&netdev_net_ops))
goto out;
if (register_pernet_device(&default_device_ops))
goto out;
上面代码中调用的二个函数都会调用register_pernet_operations函数
int register_pernet_subsys(struct pernet_operations *ops)
{
int error;
mutex_lock(&net_mutex);
error = register_pernet_operations(first_device, ops);
mutex_unlock(&net_mutex);
return error;
}
int register_pernet_device(struct pernet_operations *ops)
{
int error;
mutex_lock(&net_mutex);
error = register_pernet_operations(&pernet_list, ops);
if (!error && (first_device == &pernet_list))
first_device = &ops->list;
mutex_unlock(&net_mutex);
return error;
}
我们在上面个函数register_pernet_subsys中看到了first_device这是在Net_namespace.c中17行处声明的
static struct list_head *first_device = &pernet_list;
进入register_pernet_operations我们看一下
static int register_pernet_operations(struct list_head *list,
struct pernet_operations *ops)
{
if (ops->init == NULL)
return 0;
return ops->init(&init_net);
}
在Net_namespace.c中还有另一个同名的函数但是那必须在支持NET_NS用户自定义网络的选择项打开情况下,我们上面提到过了,所以这里会进入net_dev_init函数中层层传递下来的netdev_net_ops钩子结构,执行这个结构中的钩子函数init,我们看一下这个结构
static struct pernet_operations __net_initdata netdev_net_ops = {
.init = netdev_init,
.exit = netdev_exit,
};
显然是进入了netdev_init函数,在进入函数之前我们看到它是对系统默认的网络空间结构变量init_net进行的操作
/* Initialize per network namespace state */
static int __net_init netdev_init(struct net *net)
{
INIT_LIST_HEAD(&net->dev_base_head);
net->dev_name_head = netdev_create_hash();
if (net->dev_name_head == NULL)
goto err_name;
net->dev_index_head = netdev_create_hash();
if (net->dev_index_head == NULL)
goto err_idx;
return 0;
err_idx:
kfree(net->dev_name_head);
err_name:
return -ENOMEM;
}
我是无名小卒本文是我原创完成,尽管08年3月份才开始写文章但是已经得到了朋友们认可,文章也得到了转载,所以请转载的朋友注明出处
http://qinjiana07876.cublog.cn
,我们在上面的函数中看到初始化了init_net的几个队列头,其中有二个是哈希队列。
static inline void INIT_LIST_HEAD(struct list_head *list)
{
list->next = list;
list->prev = list;
}
static struct hlist_head *netdev_create_hash(void)
{
int i;
struct hlist_head *hash;
hash = kmalloc(sizeof(*hash) * NETDEV_HASHENTRIES, GFP_KERNEL);
if (hash != NULL)
for (i = 0; i NETDEV_HASHENTRIES; i++)
INIT_HLIST_HEAD(&hash);
return hash;
}
这二个函数对队列头进行了初始化,上面函数是对初始化普通的链表头,下面函数是初始化hash队列头。函数很简单。这二个函数分别在include/linux/list.h和/net/core/dev.c中。接下来我们再看上面已经贴出的register_pernet_device函数调用register_pernet_operations时传递的是default_device_ops钩子结构,会执行这个结构中的init钩子函数
static struct pernet_operations __net_initdata default_device_ops = {
.exit = default_device_exit,
};
但是我们上面看到没有设置init钩子函数,那么看一下上面的register_pernet_operations函数,它在直接返回0了,然后在register_pernet_device函数中就会执行
if (!error && (first_device == &pernet_list))
first_device = &ops->list;
但是我们看到first_device在这里将会从default_device_ops中取得队列头,很显然这是空的。到这里我们只是看到了对init_net的几个队列头的初始化,那么还有没有其他重要的初始化呢,如何对init_net的网络结构进行的设置的。这里恐怕也没有一个非常准确的说法,我是无名小卒,请转载的朋友注明出处,
http://qinjiana0786.cublog.cn
,我们如果联合想一下内核的设置就能明白了,当你在make内核之前,在对内核进行设置选择使用何种网络时根据你对内核的网络选项,内核会有选择的使用ipv4还是使用ipv6的互联网协议。显然我们练习中使用的是ipv4,所以我们在内核编译时会执行下面几个初始化过程,这些过程是根据你对内核的配置而定。

1、首先是IP协议的初始化安装ipip_init()函数,这个函数在/net/ipv4/ipip.c的839行处,其次是GRE通用路由协议的初始化安装ipgre_init()函数(/net/ipv4/ip_gre.c),以及IEEE的802.1Q VLAN协议的初始化安装vlan_proto_init()(/net/8021q/vlan.c)还有通用的TUN/TAP设备驱动程序中的tun_init()初始化函数(/driversnet/tun.c)。

2、如果朋友们使用的是ipv6的话就会执行ipv6的通道设备初始化ip6_tunnel_init()函数(/net/ipv6/ip6_tunnel.c)、SIT互联网协议的sit_init()(同上目录中的sit.c)、以及上面与ipv4一样的通用的TUN/TAP设备驱动程序中的tun_init()初始化函数(目录同上)和IEEE的802.1Q VLAN协议的初始化安装vlan_proto_init()初始化函数(目录同上)。
我们这里只针对ipv4所以,只看项列出的几个初始化函数,那些初始化函数都非常重要,他们都会在函数内部调用register_pernet_gen_device()函数来初始化全局的init_net网络空间结构。我们先看ipip_init()我们只关心这里重要的对其函数其他部分暂且不做分析
static int __init ipip_init(void)
{
。。。。。。
err = register_pernet_gen_device(&ipip_net_id, &ipip_net_ops);
。。。。。。
}
可以看到他向register_pernet_gen_device传递了二个参数结构ipip_net_id和ipip_net_ops
static int ipip_net_id;
static struct pernet_operations ipip_net_ops = {
.init = ipip_init_net,
.exit = ipip_exit_net,
};
我们进入register_pernet_gen_device函数中看是如何初始化的
int register_pernet_gen_device(int *id, struct pernet_operations *ops)
{
int error;
mutex_lock(&net_mutex);
again:
error = ida_get_new_above(&net_generic_ids, 1, id);
if (error) {
if (error == -EAGAIN) {
ida_pre_get(&net_generic_ids, GFP_KERNEL);
goto again;
}
goto out;
}
error = register_pernet_operations(&pernet_list, ops);
if (error)
ida_remove(&net_generic_ids, *id);
else if (first_device == &pernet_list)
first_device = &ops->list;
out:
mutex_unlock(&net_mutex);
return error;

我们暂时只关心与init_net相关的函数,上面代码中也是调用了register_pernet_operations函数只不过这次传递进去的ops是ipip_net_ops,我们在上面看过了register_pernet_operations,他是首先判断ipip_net_ops的init钩子是否链入了,我们看到上面他的结构中是ipip_init_net函数,那么就要进一步调用这个函数初始化我们这里的init_net网络命名空间。

static int ipip_init_net(struct net *net)
{
int err;
struct ipip_net *ipn;
err = -ENOMEM;
ipn = kzalloc(sizeof(struct ipip_net), GFP_KERNEL);
if (ipn == NULL)
goto err_alloc;
err = net_assign_generic(net, ipip_net_id, ipn);
if (err 0)
goto err_assign;
ipn->tunnels[0] = ipn->tunnels_wc;
ipn->tunnels[1] = ipn->tunnels_l;
ipn->tunnels[2] = ipn->tunnels_r;
ipn->tunnels[3] = ipn->tunnels_r_l;
ipn->fb_tunnel_dev = alloc_netdev(sizeof(struct ip_tunnel),
"tunl0",
ipip_tunnel_setup);
if (!ipn->fb_tunnel_dev) {
err = -ENOMEM;
goto err_alloc_dev;
}
ipn->fb_tunnel_dev->init = ipip_fb_tunnel_init;
dev_net_set(ipn->fb_tunnel_dev, net);
if ((err = register_netdev(ipn->fb_tunnel_dev)))
goto err_reg_dev;
return 0;
err_reg_dev:
free_netdev(ipn->fb_tunnel_dev);
err_alloc_dev:
/* nothing */
err_assign:
kfree(ipn);
err_alloc:
return err;
}
这里有了一个新的结构struct ipip_net,是专门用于ip协议专用的数据结构
struct ipip_net {
struct ip_tunnel *tunnels_r_l[HASH_SIZE];
struct ip_tunnel *tunnels_r[HASH_SIZE];
struct ip_tunnel *tunnels_l[HASH_SIZE];
struct ip_tunnel *tunnels_wc[1];
struct ip_tunnel **tunnels[4];
struct net_device *fb_tunnel_dev;
};
其结构内部还包含IP通道的结构变量
struct ip_tunnel
{
struct ip_tunnel *next;
struct net_device *dev;
struct net_device_stats stat;
int recursion; /* Depth of hard_start_xmit recursion */
int err_count; /* Number of arrived ICMP errors */
unsigned long err_time; /* Time when the last ICMP error arrived */
/* These four fields used only by GRE */
__u32 i_seqno; /* The last seen seqno */
__u32 o_seqno; /* The last output seqno */
int hlen; /* Precalculated GRE header length */
int mlink;
struct ip_tunnel_parm parms;
struct ip_tunnel_prl_entry *prl; /* potential router list */
unsigned int prl_count; /* # of entries in PRL */
};
我们先不解释他的作用了,具体的作用会在使用过程中了解的,接着为进入net_assign_generic函数为init_net->gen也就是struct net_generic结构变量分配空间,并将参数ipip_net_id和参数结构ipn赋值到gen中,注意ipn是我们刚刚上面分配的struct ipip_net,我们可以从 init_net->gen->ptr数组的以ipip_net_id为下标找到这里分配的IP协议结构struct ipip_net。所以struct net_generic结构是一个通用的数据结构
struct net_generic {
unsigned int len;
struct rcu_head rcu;
void *ptr[0];
};
这个结构的定义在/net/netns/generic.h中,正象注释说的那样他的作用是减少了一些专用数据结构的声明。很多数据可以共用这个能用的数据结构。我是无名小卒,
http://qinjiana0786.cublog.cn
,转载请注明此出处。这也是他的结构名称的由来。
回到ipip_init_net函数中我们继续往下看
ipn->tunnels[0] = ipn->tunnels_wc;
ipn->tunnels[1] = ipn->tunnels_l;
ipn->tunnels[2] = ipn->tunnels_r;
ipn->tunnels[3] = ipn->tunnels_r_l;
这段代码是对ipip_net结构中的ip通道数组进行设置,使双重指针数组中的元素,也就是队列头指针分别与ipip_net中其他四个ip通道的队列数组挂上钩。我们上面看到了可以通过init_net全局的网络命名空间结构找到这个ipip_net结构变量ipn,请朋友们注意这里。在struct ipip_net结构中还有一个网络设备的结构体变量struct net_device *fb_tunnel_dev,函数中,struct net_device结构体很大,我们不贴了,随着代码的阅读会越来越清楚他的作用。在这里调用了alloc_netdev()函数
这是个宏声明在/incluce/linux/netdevice.h中
#define alloc_netdev(sizeof_priv, name, setup) \
alloc_netdev_mq(sizeof_priv, name, setup, 1)
然后他转向了/net/core/dev.c中的函数


struct net_device *alloc_netdev_mq(int sizeof_priv, const char *name,
void (*setup)(struct net_device *), unsigned int queue_count)
{
void *p;
struct net_device *dev;
int alloc_size;
BUG_ON(strlen(name) >= sizeof(dev->name));
alloc_size = sizeof(struct net_device) +
sizeof(struct net_device_subqueue) * (queue_count - 1);
if (sizeof_priv) {
/* ensure 32-byte alignment of private area */
alloc_size = (alloc_size + NETDEV_ALIGN_CONST) & ~NETDEV_ALIGN_CONST;
alloc_size += sizeof_priv;
}
/* ensure 32-byte alignment of whole construct */
alloc_size += NETDEV_ALIGN_CONST;
p = kzalloc(alloc_size, GFP_KERNEL);
if (!p) {
printk(KERN_ERR "alloc_netdev: Unable to allocate device.\n");
return NULL;
}
dev = (struct net_device *)
(((long)p + NETDEV_ALIGN_CONST) & ~NETDEV_ALIGN_CONST);
dev->padded = (char *)dev - (char *)p;
dev_net_set(dev, &init_net);
if (sizeof_priv) {
dev->priv = ((char *)dev +
((sizeof(struct net_device) +
(sizeof(struct net_device_subqueue) *
(queue_count - 1)) + NETDEV_ALIGN_CONST)
& ~NETDEV_ALIGN_CONST));
}
dev->egress_subqueue_count = queue_count;
dev->gso_max_size = GSO_MAX_SIZE;
dev->get_stats = internal_stats;
netpoll_netdev_init(dev);
setup(dev);
strcpy(dev->name, name);
return dev;
}
这个函数的主要使用就是分配网络设备结构,并初始化它,函数首先确定要分配一块内存的大小,然后分配内存用于struct net_device结构变量dev使用,我们看到他调用了一个与init_net网络空间结构相关的操作dev_net_set(),但是我们追踪进去发现他是在CONFIG_NET_NS配置情况下才会得到执行,在这里也只是一个空操作,函数后调用ipip_tunnel_setup()对dev进一步的初始化。并且为dev复制了一个名称“tunl0”。函数返回后,我们在ipip_init_net()函数中的ipn结构变量其中的fb_tunnel_dev也就自此有了具体的指向。后再进一步对这个网络设备结构初始化和“登记”工作。
ipn->fb_tunnel_dev->init = ipip_fb_tunnel_init;
dev_net_set(ipn->fb_tunnel_dev, net);
if ((err = register_netdev(ipn->fb_tunnel_dev)))
goto err_reg_dev;
注意上面的init钩子函数的挂入了ipip_fb_tunnel_init,一会我们会用到。我们进入register_netdev()函数
int register_netdev(struct net_device *dev)
{
int err;
rtnl_lock();
/*
* If the name is a format string the caller wants us to do a
* name allocation.
*/
if (strchr(dev->name, '%')) {
err = dev_alloc_name(dev, dev->name);
if (err 0)
goto out;
}
err = register_netdevice(dev);
out:
rtnl_unlock();
return err;
}
这个函数的主要做用就是注册网络设备到内核,我们先跳过锁的操作,如果设备名称中含有“%”号就代表着让内核自动分配一个名称。我们知道上面已经有一个名称“tunl0”了。所以不用分配,接着函数进入了register_netdevice(),这是个比较大的函数,我们分段来看
int register_netdevice(struct net_device *dev)
{
struct hlist_head *head;
struct hlist_node *p;
int ret;
struct net *net;
BUG_ON(dev_boot_phase);
ASSERT_RTNL();
might_sleep();
/* When net_device's are persistent, this will be fatal. */
BUG_ON(dev->reg_state != NETREG_UNINITIALIZED);
BUG_ON(!dev_net(dev));
net = dev_net(dev);
spin_lock_init(&dev->queue_lock);
spin_lock_init(&dev->_xmit_lock);
netdev_set_lockdep_class(&dev->_xmit_lock, dev->type);
dev->xmit_lock_owner = -1;
spin_lock_init(&dev->ingress_lock);
dev->iflink = -1;
/* Init, if this function is available */
if (dev->init) {
ret = dev->init(dev);
if (ret) {
if (ret > 0)
ret = -EIO;
goto out;
}
}
if (!dev_valid_name(dev->name)) {
ret = -EINVAL;
goto err_uninit;
}
dev->ifindex = dev_new_index(net);
if (dev->iflink == -1)
dev->iflink = dev->ifindex;
上面代码中,首先取得了init_net网络空间的结构指针
static inline
struct net *dev_net(const struct net_device *dev)
{
#ifdef CONFIG_NET_NS
return dev->nd_net;
#else
return &init_net;
#endif
}
接着要调用dev中设置初始化函数,我们在前面提醒过朋友们,在那里间接设置了ipip_fb_tunnel_init(),所以会进入这个函数,它在/net/ipv4/ipip.c中的736行
static int ipip_fb_tunnel_init(struct net_device *dev)
{
struct ip_tunnel *tunnel = netdev_priv(dev);
struct iphdr *iph = &tunnel->parms.iph;
struct ipip_net *ipn = net_generic(dev_net(dev), ipip_net_id);
tunnel->dev = dev;
strcpy(tunnel->parms.name, dev->name);
iph->version = 4;
iph->protocol = IPPROTO_IPIP;
iph->ihl = 5;
dev_hold(dev);
ipn->tunnels_wc[0] = tunnel;
return 0;
}
这个函数首先是取得IP通道
static inline void *netdev_priv(const struct net_device *dev)
{
return dev->priv;
}
对照一下之前的设置,在前面分配struct net_device dev调用alloc_netdev_mq()函数时,我们再贴一下代码
struct net_device *alloc_netdev_mq(int sizeof_priv, const char *name,
void (*setup)(struct net_device *), unsigned int queue_count)
{
。。。。。。
if (sizeof_priv) {
dev->priv = ((char *)dev +
((sizeof(struct net_device) +
(sizeof(struct net_device_subqueue) *
(queue_count - 1)) + NETDEV_ALIGN_CONST)
& ~NETDEV_ALIGN_CONST));
}
。。。。。。

我们看到一个结构struct net_device_subqueue
struct net_device_subqueue
{
/* Give a control state for each queue. This struct may contain
* per-queue locks in the future.
*/
unsigned long state;
};
他只包含一变量,所以这个结构实际是网络设备结构的一个辅助结构,从字面上看是设备队列使用的,但是没有看到其包含任何的队列头。queue_count在参数传递下为是1,而NETDEV_ALIGN_CONST是个宏定义

#define NETDEV_ALIGN 32
#define NETDEV_ALIGN_CONST (NETDEV_ALIGN - 1)
所以上面的宏是为了保持地址对齐使用的,这样就能看懂了dev->priv所指向的位置是与dev地址相差一个struct net_device结构大小的地址处。所以回到上面ipip_fb_tunnel_init()函数中调用netdev_priv()就取得了这个地址,然后将这段地址做struct ip_tunnel的IP通道结构起始地址,在ip通道结构中有一个代表其参数的数据结构
struct ip_tunnel_parm
{
char name[IFNAMSIZ];
int link;
__be16 i_flags;
__be16 o_flags;
__be32 i_key;
__be32 o_key;
struct iphdr iph;
};
参数中后一个是IP头部结构变量。
struct iphdr {
#if defined(__LITTLE_ENDIAN_BITFIELD)
__u8 ihl:4,
version:4;
#elif defined (__BIG_ENDIAN_BITFIELD)
__u8 version:4,
ihl:4;
#else
#error "Please fix "
#endif
__u8 tos;
__be16 tot_len;
__be16 id;
__be16 frag_off;
__u8 ttl;
__u8 protocol;
__sum16 check;
__be32 saddr;
__be32 daddr;
/*The options start here. */
};
上面IP头部结构中变量根据应用我们自然会明白其作用和含义。
所以函数中的struct iphdr *iph指了IP头部。接着我们看到
struct ipip_net *ipn = net_generic(dev_net(dev), ipip_net_id);
这是取得我们在ipip_init_net()函数中已经初始化的ipn结构变量。函数首先是让靠在dev后边的ip通道与dev挂上关系,紧接着复制网络设备名称到ip通道的参数结构ip_tunnel中。接着对ip头部结构进行了设置

iph->version = 4;//表示ip版本是ipv4
iph->protocol = IPPROTO_IPIP;//使用IPIP通道
iph->ihl = 5;//ip头部的长度是5
然后让ipn的ip结构中的通道指针数组指向这里的ip通道
ipn->tunnels_wc[0] = tunnel;
初始化dev结构以后,我们回到register_netdevice()函数代码中,下面是检查init_net中的hash队列头是否已经存在该网络设备。
其具体过程请朋友们自已追踪了。我们为了不离了主题,层层返回到ipip_init()函数中,也就完成了IP协议的初始化过程。我们已经看到了其对init_net的初始化部分。很明显init_net的重要性不言而喻。


文章来源CU社区:内核中的TCP的追踪分析-4-TCP(IPV4)的socket的地址绑定-续

相关文章