目录
一、综述
在上一篇中,主要分析了linux网络协议栈层次划分,列举了层与层之间的主要接口,本文结合实际代码和数据结构进行进一步说明这些层次是如何建立的,主要基于INET。
二、INET的初始化
2.1 INET接口注册
[net/ipv4/af_inet.c]
static int __init inet_init(void)
{
rc = proto_register(&tcp_prot, 1);
rc = proto_register(&udp_prot, 1);
rc = proto_register(&raw_prot, 1);
rc = proto_register(&ping_prot, 1);
(void)sock_register(&inet_family_ops);
if (inet_add_protocol(&icmp_protocol, IPPROTO_ICMP) < 0)
pr_crit("%s: Cannot add ICMP protocol\n", __func__);
if (inet_add_protocol(&udp_protocol, IPPROTO_UDP) < 0)
pr_crit("%s: Cannot add UDP protocol\n", __func__);
if (inet_add_protocol(&tcp_protocol, IPPROTO_TCP) < 0)
pr_crit("%s: Cannot add TCP protocol\n", __func__);
/* Register the socket-side information for inet_create. */
for (r = &inetsw[0]; r < &inetsw[SOCK_MAX]; ++r)
INIT_LIST_HEAD(r);
for (q = inetsw_array; q < &inetsw_array[INETSW_ARRAY_LEN]; ++q)
inet_register_protosw(q);
dev_add_pack(&ip_packet_type);
}
删除了无关的部分,可以看到在发送层面调用了proto_register/inet_register_protosw,这里具体看一下inet_protosw{}的结构:
struct inet_protosw {
struct list_head list;
/* These two fields form the lookup key. */
unsigned short type; /* This is the 2nd argument to socket(2). */
unsigned short protocol; /* This is the L4 protocol number. */
struct proto *prot;
const struct proto_ops *ops;
unsigned char flags; /* See INET_PROTOSW_* below. */
};
可以看到type<->protocol<->proto_ops{}<->proto{}的对应关系由这个所谓“协议切换表”统一了起来:
static struct inet_protosw inetsw_array[] =
{
{
.type = SOCK_STREAM,
.protocol = IPPROTO_TCP,
.prot = &tcp_prot,
.ops = &inet_stream_ops,
.flags = INET_PROTOSW_PERMANENT |
INET_PROTOSW_ICSK,
},
{
.type = SOCK_DGRAM,
.protocol = IPPROTO_UDP,
.prot = &udp_prot,
.ops = &inet_dgram_ops,
.flags = INET_PROTOSW_PERMANENT,
},
{
.type = SOCK_DGRAM,
.protocol = IPPROTO_ICMP,
.prot = &ping_prot,
.ops = &inet_sockraw_ops,
.flags = INET_PROTOSW_REUSE,
},
{
.type = SOCK_RAW,
.protocol = IPPROTO_IP, /* wild card */
.prot = &raw_prot,
.ops = &inet_sockraw_ops,
.flags = INET_PROTOSW_REUSE,
}
};
接收层面,inet_add_protocol/dev_add_pack。基本上一个收发的分层框架接口就建立起来了。
2.2 抽象实体的建立
各层的接口注册完毕后,接下来看一看接口是如何与抽象结构绑定的,这个过程是进行socket系统调用产生的,socket系统调用的作用应该在socket layer分配一个抽象socket结构,为网络协议层分配一个sock结构,并将抽象层的接口与抽象的实体进行绑定。
一个典型的socket API原型:
- int socket(int domain, int type, int protocol);
为了保证分析的完整性,还是将inet socket相关参数做一下简单说明,对于address family是INET的socket来说(man 7 ip),报文都是基于IP,所以有TCP, UDP, RAW三种:
- tcp_socket = socket(AF_INET, SOCK_STREAM, 0);
- udp_socket = socket(AF_INET, SOCK_DGRAM, 0);
- raw_socket = socket(AF_INET, SOCK_RAW, protocol);
对于TCP(SOCK_STREAM)来说,第三个参数只能是0或者IPPROTO_TCP,对于UDP(SOCK_DGRAM)来说,第三个参数只能是0或者IPPROTO_UDP,对于inet raw报文来说,有以下特点:
- 其构造的报文不包括二层头,这点与AF_PACKET区别。
- 如果不指定IP_HDRINCL(socket),由内核构造ipv4头。
- 如果协议指定IPPROTO_RAW,默认使能IP_HDRINCL,但是这样的参数在收放向上无法用来接收所有的IP报文
对应系统调用:
SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol)
接下来分析socket系统调用执行流程
[net/socket.c]
int __sock_create(struct net *net, int family, int type, int protocol,
struct socket **res, int kern)
{
sock = sock_alloc();
sock->type = type;rcu_read_lock();
pf = rcu_dereference(net_families[family]);
rcu_read_unlock();err = pf->create(net, sock, protocol, kern);
if (err < 0)
goto out_module_put;
}
代码只摘录了部分,只说明重要的部分,我们来看socket具体流程
socket系统调用首先会分配一个socket结构体:
[include/linux/net.h]
struct socket {
socket_state state;
short type;
unsigned long flags;
struct socket_wq __rcu *wq;
struct file *file;
struct sock *sk;
const struct proto_ops *ops;
};
我们知道不同的family address对应的sock结构是不同的,因为sock是代表具体协议层的,这里利用接口pf->create(inet_create)对具体协议(inet)进行创建。
[inet/ipv4/af_inet.c]
static int inet_create(struct net *net, struct socket *sock, int protocol, int kern)
{sock->state = SS_UNCONNECTED;
sock->ops = answer->ops;
answer_prot = answer->prot;
answer_flags = answer->flags;
rcu_read_unlock();WARN_ON(!answer_prot->slab);
err = -ENOBUFS;
sk = sk_alloc(net, PF_INET, GFP_KERNEL, answer_prot, kern);
err = 0;
if (INET_PROTOSW_REUSE & answer_flags)
sk->sk_reuse = SK_CAN_REUSE;inet = inet_sk(sk);
inet->is_icsk = (INET_PROTOSW_ICSK & answer_flags) != 0;inet->nodefrag = 0;
sock_init_data(sock, sk);
sk->sk_destruct = inet_sock_destruct;
sk->sk_protocol = protocol;
sk->sk_backlog_rcv = sk->sk_prot->backlog_rcv;
}
先看对socket绑定接口的操作:根据type<->proto_ops<->proto关系,找到协议切换的结构,此时socket可以与proto_ops进行绑定,随后sk_alloc分配sock,过程中将sock与proto绑定
到此为止,前篇建立的结构就完全建立了,这里为了方便,再贴一次:
2.3 代码细节分析
sk_alloc完成sock{}分配和初始化,sock{}是网络层的表示,但由于协议多样,linux将sock{}抽象成一个基类,具体的协议要通过对sock{}的继承得到,对具体的协议(如inet,继承的结构是inet_sock{})是这样操作的,在协议注册指定继承者inet_sock{}的大小,这样在调用sk_alloc分配sock时实际上分配的大小是针对inet_sock{}的,这样在使用时可以像下面这样:
inet = inet_sk(sk);
sock这个结构本身还是比较复杂的,我们来看一些关键的部分,既然它是网络协议层的表示,那么基本的sip,dip,sport,dport是少不了的,这些位于sock{}的sock_common{}中:
struct sock_common __sk_common;
#define sk_num __sk_common.skc_num
#define sk_dport __sk_common.skc_dport
#define sk_addrpair __sk_common.skc_addrpair
#define sk_daddr __sk_common.skc_daddr
#define sk_rcv_saddr __sk_common.skc_rcv_saddr
#define sk_family __sk_common.skc_family
#define sk_state __sk_common.skc_state
#define sk_reuse __sk_common.skc_reuse
#define sk_reuseport __sk_common.skc_reuseport
#define sk_prot __sk_common.skc_prot
inet
struct inet_sock {
struct sock sk;
#define inet_daddr sk.__sk_common.skc_daddr
#define inet_rcv_saddr sk.__sk_common.skc_rcv_saddr
#define inet_dport sk.__sk_common.skc_dport //dport
#define inet_num sk.__sk_common.skc_num //sport,主机序
__be32 inet_saddr;
2.3.1 socket参数
这里再给出实际应用的几个例子,看看再socket建立的时候是怎么匹配的:
- ping socket(AF_INET, SOCK_RAW, IPPROTO_ICMP)
- TCP socket(AF_INET, SOCK_STREAM, IPPROTO_IP);
- UDP/ifconfig socket(AF_NET, SOCK_DGRAM, IPPROTO_IP );
[inet/ipv4/af_inet.c]
list_for_each_entry_rcu(answer, &inetsw[sock->type], list) {
err = 0;
/* Check the non-wild match. */
if (protocol == answer->protocol) { //如果指定TCP指定IPPROTO_TCP,UDP指定IPPROTO_UDP,对应的tcp/udp在这匹配,是全匹配,所以叫non-wild
if (protocol != IPPROTO_IP) //不能使用SOCK_RAW,IPPROTO_IP的组合
break;
} else {
/* Check for the two wild cases. */
if (IPPROTO_IP == protocol) { //如果TCP,UDP protocol字段指定0,IPPROTO_IP,在这匹配,通过下一句将protocol改成实际的协议的值
protocol = answer->protocol;
break;
}
if (IPPROTO_IP == answer->protocol) //原始套接字基本上都到这,如果原始套接字指定了IPPROTO_ICMP,走non-wild
break;
}
err = -EPROTONOSUPPORT;
}
RAW在socket中的特殊处理
[inet/ipv4/af_inet.c]
if (SOCK_RAW == sock->type) { //如果是raw,主机序源端口为协议号
inet->inet_num = protocol;
if (IPPROTO_RAW == protocol) //上面说过了IPPROTO_RAW默认指定IP_HDRINCL,表示app自行添加IP头
inet->hdrincl = 1;
}if (inet->inet_num) {
inet->inet_sport = htons(inet->inet_num); //原始套接字在socket阶段就要指定端口,然后通过hash保存sk
/* Add to protocol hash chains. */
err = sk->sk_prot->hash(sk);
if (err) {
sk_common_release(sk);
goto out;
}
}if (sk->sk_prot->init) {
err = sk->sk_prot->init(sk);
}
三、其他协议
- PF_UNIX
- PF_NETLINK
- PF_PACKET
3.1 PF_PACKET
[net/packet/af_packet.c]
static int __init packet_init(void)
{
int rc = proto_register(&packet_proto, 0);
if (rc != 0)
goto out;
sock_register(&packet_family_ops);
register_pernet_subsys(&packet_net_ops);
register_netdevice_notifier(&packet_netdev_notifier);
out:
return rc;
}
packet_create->packet_sock{}
po = pkt_sk(sk);
3.2 PF_NETLINK
[net/netlink/af_netlink.c]
netlink_create->netlink_sock{}
nlk = nlk_sk(sock->sk);
3.3 PF_UNIX
[net/unix/af_unix.c]
unix_create->unix_sock{}
u = unix_sk(sk);
四、参考
【1】深入浅出Linux TCP/IP协议栈 罗钰 编著