前面了解到网络初始化申请了两块skb高速缓存和创建了一个/proc/net/protocols文件,现在开始重头戏,网络协议栈的初始化。这篇文章主要介绍网络栈中使用到的主要数据结构。
网络协议栈的内核实现和理论上的分层有些不一样,在代码里面的分层如下图:
开始前,先回顾一下应用层socket函数的调用,它会创建一个socket并返回对应的描述符:
int socket(int domain, int type, int protocol);
socket函数对应于普通文件的打开操作。普通文件的打开操作返回一个文件描述字,而socket()用于创建一个socket描述符(socket descriptor),它唯一标识一个socket。这个socket描述字跟文件描述字一样,后续的操作都有用到它,把它作为参数,通过它来进行一些读写操作。
正如可以给fopen的传入不同参数值,以打开不同的文件。创建socket的时候,也可以指定不同的参数创建不同的socket描述符,socket函数的三个参数分别为:
- domain:即协议域,又称为协议族(family)。常用的协议族有,AF_INET、AF_INET6、AF_LOCAL(或称AF_UNIX,Unix域socket)、AF_ROUTE等等。协议族决定了socket的地址类型,在通信中必须采用对应的地址,如AF_INET决定了要用ipv4地址(32位的)与端口号(16位的)的组合、AF_UNIX决定了要用一个绝对路径名作为地址。
- type:指定socket类型。常用的socket类型有,SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等等(socket的类型有哪些?)。
- protocol:故名思意,就是指定协议。常用的协议有,IPPROTO_TCP、IPPTOTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等,它们分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议(这个协议我将会单独开篇讨论!)。
注意:并不是上面的type和protocol可以随意组合的,如SOCK_STREAM不可以跟IPPROTO_UDP组合。当protocol为0时,会自动选择type类型对应的默认协议。
当我们调用socket创建一个socket时,返回的socket描述字它存在于协议族(address family,AF_XXX)空间中,但没有一个具体的地址。如果想要给它赋值一个地址,就必须调用bind()函数,否则就当调用connect()、listen()时系统会自动随机分配一个端口。
1. static struct net_proto_family *net_families[NPROTO]
~/linux-4.12/include/linux/net.h
200 struct net_proto_family {
201 int family; //地址族类型
202 int (*create)(struct net *net, struct socket *sock, //套接字的创建方法
203 int protocol, int kern);
204 struct module *owner;
205 };
socket.c
210 #define AF_MAX 44 /* For now.. */
24 #define NPROTO AF_MAX
163 static const struct net_proto_family __rcu *net_families[NPROTO] __read_mostly;
第一个重要的结构体是 net_proto_family。
在这之前必须知道 一些概念——地址族和套接字类型。 大家都知道所谓的套接字都有地址族,实际就是套接字接口的种类, 每种套接字种类有自己的通信寻址方法。 Linux 将不同的地址族抽象统一为 BSD 套接字接口,应用程序 关心的只是 BSD 套接字接口,通过参数来指定所使用的套接字地址族。
Linux 内 核 中 为 了 支 持 多 个 地 址 族 , 定 义 了 这 么 一 个 变 量 : static struct net_proto_family *net_families[NPROTO], NPROTO 等于 44, 也就是说 Linux 内核支持最多 44种地址族。不过目前已经 够用了, 我们常用的不外乎就是 PF_UNIX( 1)、 PF_INET( 2)、 PF_NETLINK( 16), Linux 还有一个自 有的 PF_PACKET( 17),即对网卡进行操作的选项。所以这个链表里面存放的是应用层socket()的第一个参数,它决定了这个参数可以取哪些值。当系统调用socket转到内核处理的时候,它首先会用第一个参数查找需要在哪个域里面创建套接字。
在网络子系统中,net_families[NPROTO]是一个全局的链表,它存放的是不同的地址族,不同地址族套接字有不同的创建方法,下面我们关注的是PF_INET地址族的注册。
在inet_init()中会调用这个函数注册地址族:(void)sock_register(&inet_family_ops); 其中inet_family_ops是struct net_proto_family的结构体对象
1014 static const struct net_proto_family inet_family_ops = {
1015 .family = PF_INET,
1016 .create = inet_create,
1017 .owner = THIS_MODULE,
1018 };
结构体的内容比较简单,一个是地址族的标号,一个是在该地址族里创建socket时调用的创建函数inet_create。当我们通过系统调用socket()创建PF_INET地址族的套接字时,内核使用的创建函数时inet_create,这里只需要记住,后面会分析创建过程。
下面是sock_register的实现过程,详细内容也可以 查看这里
2490 int sock_register(const struct net_proto_family *ops)
2491 {
2492 int err;
2493
2494 if (ops->family >= NPROTO) {
2495 pr_crit("protocol %d >= NPROTO(%d)\n", ops->family, NPROTO);
2496 return -ENOBUFS;
2497 }
2498
2499 spin_lock(&net_family_lock);
2500 if (rcu_dereference_protected(net_families[ops->family],
2501 lockdep_is_held(&net_family_lock)))
2502 err = -EEXIST;
2503 else {
2504 rcu_assign_pointer(net_families[ops->family], ops); //将inet_family_ops对象添加到net_families全局数组里面,就完成了初始化
2505 err = 0;
2506 }
2507 spin_unlock(&n