0%

Linux网络内核源码分析|套接字相关的数据结构和功能实现

管理套接字的数据类型

套接字缓冲区类型 struct sk_buff, struct sk_buff_head

skbuff.h

struct sk_buff是套接字缓冲区类型,用来管理网络数据包,为发/收的网络数据提供存储区域和操作方法。sk_buff数据类型贯穿数据传输全过程,是十分重要的数据结构。

struct sk_buff_head管理一个双向链表,来组织sk_buff。

sk_buff与数据存储

1
2
3
4
5
6
7
struct sk_buff{
//...
unsigned char *head,//缓冲区头指针
*data,//数据块头指针
*tail,//数据块尾指针
*end;//缓冲区尾指针
}
Linux网络内核源码分析-重要数据结构

协议族管理类型 struct net_proto_family

include/linux/net.h

管理不同协议族套接字的创建方法。

1
2
3
4
5
6
7
8
9
struct net_proto_family {
int family;//协议族标志
//create指针指向具体协议族套接字的创建函数。
int (*create)(struct socket *sock, int protocol);
short authentication;//认证管理字段
short encryption;//加密管理字段
short encrypt_net;
struct module *owner;
};

linux内核通过struct net_proto_family型的net_families表来管理协议族。linux支持的协议族会被sock_register()注册到net_families数组。

1
static struct net_proto_family *net_families[NPROTO];

我们看看该类型还有哪些变量。

对于INET协议族,有:

net/ipv4/af_inet.c

1
2
3
4
5
struct net_proto_family inet_family_ops = {
.family = PF_INET,
.create = inet_create, //给出创建方法inet_create()
.owner = THIS_MODULE,
};

对于UNIX域,有unix_family_ops管理创建方法,create指针指向unix_create()。

net/ipv4/af_unix.c

协议族套接字操作集 struct proto_ops

include/linux/net.h

协议族操作集类型,统一管理套接字操作函数。linux通过struct proto_ops类型定义的统一接口管理各种功能的函数,接口就是定义中的各种函数指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct proto_ops {
int family;//协议族
struct module *owner;//所属模块
/*各种函数接口*/
int (*release) (struct socket *sock);//释放套接字
int (*bind) (struct socket *sock, //给套接字绑定地址
struct sockaddr *myaddr,
int sockaddr_len);
int (*connect) (struct socket *sock, //发出连接请求
struct sockaddr *vaddr,
int sockaddr_len, int flags);
int (*socketpair)(struct socket *sock1, //设置对等套接字
struct socket *sock2);
int (*accept) (struct socket *sock, //接受连接请求
struct socket *newsock, int flags);
//...
};

struct proto_ops类型也有针对不同协议的各种变量。

对于INET协议族的TCP和UDP协议,有inet_stream_ops和inet_dgram_ops两个变量。当INET协议族套接字采用TCP传输层协议时,由inet_stream_ops变量管理套接字操作集,通过这个操作集变量,各个函数有所指定。

INET协议族的定义都在af_inet.c,UNIX域的定义都在af_unix.c中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct proto_ops inet_stream_ops = { //TCP协议
.family = PF_INET,
.owner = THIS_MODULE,
.release = inet_release, //释放TCP套接字使用inet_release()
.bind = inet_bind,
.connect = inet_stream_connect,
.socketpair = sock_no_socketpair,
.accept = inet_accept,
//...
}
struct proto_ops inet_dgram_ops = { //UDP协议
.family = PF_INET,
//...
}

套接字结构类型 struct socket

include/linux/net.h

每个套接字在内核中都对应唯一的struct socket结构,该类型提供不同协议族套接字的统一表示

用户程序通过唯一的套接字描述符来表示套接字,用户的套接字描述符与struct socket一一对应。

1
2
3
4
5
6
7
8
9
10
11
12
struct socket {
socket_state state; //状态值
unsigned long flags; //标志
//套接字的协议族操作集
struct proto_ops *ops; //指向一个struct proto_ops结构;
struct fasync_struct *fasync_list; //异步唤醒链表
struct file *file; //文件指针
struct sock *sk; //sk指向一个struct sock结构体
wait_queue_head_t wait; //等待队列
short type; //传输层数据类型
unsigned char passcred; //授权描述
};

传输层协议操作集 struct proto

include/net/sock.h

struct proto封装了传输协议操作集,其中函数指针指向特定于传输协议的函数。

1
2
3
4
5
6
7
8
9
struct proto {
void (*close)(struct sock *sk, long timeout);
int (*connect)(struct sock *sk, //指向传输层的发出连接请求函数
struct sockaddr *uaddr,
int addr_len);
int (*disconnect)(struct sock *sk, int flags);
struct sock *(*accept) (struct sock *sk, int flags, int *err);
//...
}

针对不同的传输层协议,linux提供不同的struct proto变量来管理传输协议操作集。对于INET的TCP和UDP的协议,有struct proto tcp_prot和struct proto udp_prot,对于其他协议有struct proto raw_prot

net/ipv4/tcp_ipv4.c

1
2
3
4
5
6
7
8
struct proto tcp_prot = { //TCP协议
.name = "TCP",
.close = tcp_close,
.connect = tcp_v4_connect,
.disconnect = tcp_disconnect,
.accept = tcp_accept,
//...
}

tcp_prot变量封装了与TCP协议相关的操作函数,为INET协议族的SOCK_STREAM套接字提供了使用TCP的方法,例如函数指针connect指向的tcp_v4_connect()函数发出连接请求,启动三次握手过程。

raw_prot变量封装了与原始数据相关的操作函数,为INET的SOCK_RAW套接字提供了访问IP层的发方法。

套接字在传输层的表示结构 struct sock

include/net/sock.h

struct sock结构体代表了传输层的套接字结构,包含了与具体传输层协议相关的信息。在为套接字指定传输层协议后,struct socket的sk指针会指向一个与传输协议关联的struct sock。

struct sock结构定义(部分内容)如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
struct sock{
struct sock_common __sk_common;
//重定义__sk_common成员
#define sk_family __sk_common.skc_family //协议族类型,例如PF_INET
//#define sk_xxx __sk_common.skc_XXX
//...
atomic_t sk_rmem_alloc; //接收缓冲队列分配计数
struct sk_buff_head sk_receive_queue; //套机子缓冲区接收队列
atomic_t sk_wmem_alloc; //发送缓冲队列分配计数
struct sk_buff_head sk_write_queue; //套接字缓冲区发送队列
atomic_t sk_omem_alloc; //其他缓冲区队列分配计数
//...
int sk_sndbuf; //发送缓冲区长度
//...
struct sock *sk_pair;
struct {
struct sk_buff *head;
struct sk_buff *tail;
} sk_backlog; //sk_buff结构的backlog队列
struct proto *sk_prot; //传输层协议的操作集(struct proto)的指针,例如,传输层为UDP时,sk_prot指向udp_prot。
unsigned short sk_ack_backlog;//当前的侦听队列长度
unsigned short sk_max_ack_backlog;//最大的侦听队列长度
__u32 sk_priority;//优先级
unsigned short sk_type;//套接字类型,例如SOCK_STREAM
unsigned char sk_localroute;
unsigned char sk_protocol;//套接字协议类型
//...
//一些回调函数:sock结构变化时、数据准备好时...
}

所有套接字最后通过struct sock结构来使用网络协议栈的服务。

其中的sk_prot指针,根据不同的传输层协议,指向具体的struct proto变量,即一些传输层的操作集。传输层为TCP协议时,sk_prot指向的结构体等同于tcp_prot;传输层为UDP协议时,sk_prot指向的结构体等同于udp_prot。

传输层接收方法管理 struct net_protocol

include/net/protocol.h

管理第四层接收数据包的方法。各协议通过注册该类型变量,来注册接收该协议接收数据包的方法。

1
2
3
4
5
6
7
struct net_protocol {
//handler指向接收数据包函数
int (*handler)(struct sk_buff *skb);
//错误处理函数
void (*err_handler)(struct sk_buff *skb, u32 info);
int no_policy;
};

例如TCP初始化时,内核注册了struct net_protocol tcp_protocol来管理接收TCP数据包的方法。

1
2
3
4
5
static struct net_protocol tcp_protocol = {
.handler = tcp_v4_rcv,
.err_handler = tcp_v4_err,
.no_policy = 1,
}

struct inet_protosw, inetsw_array数组

将INET套接字的协议族操作集和传输层协议操作集关联起来。

1
2
3
4
5
6
7
8
9
10
11
struct inet_protosw {
struct list_head list;//链表头,用于结构体匹配
unsigned short type;//INET套接字类型
int protocol; //传输层协议号
struct proto *prot;//传输层协议操作集
struct proto_ops *ops;//协议族套接字操作集

int capability;//匹配字段
char no_check;//校验字段
unsigned char flags;//标志字段
};

该类型的inetsw_array[]实现了INET套接字的协议族操作集与具体的传输层协议关联。该数组在收发数据筛选函数时有用。

1
2
3
4
5
6
7
8
9
10
11
12
static struct inet_protosw inetsw_array[] = {
{
.type=SOCK_STREAM,
.protocol=IPPROTO_TCP,
.prot=&tcp_prot,
.ops=&inet_stream_ops,
.capability=-1,
.no_chech=0,
.flag=INET_PROTOSW_PERMANENT,
},
//一共三个元素:SOCK_STREAM,SOCK_DGRAM,SOCKRAW
}

在函数inet_init()对INET协议族进行初始化的时候,由inet_register_protosw把数组inetsw_array上记录的关联信息注册到inetsw数组中。

在系统使用过程中,内核以套接字类型为索引访问inetsw,inetsw定义如下:

1
2
static struct list_head inetsw[SOCK_MAX];
//每一项都是一个struct inet_protosw结构体的链表
1
2
3
struct list_head{
struct list_head *next,*prev;
};

套接字功能实现

start_kernel()调用了与网络初始化相关的函数:sock_init()、do_initcalls()等。

sock_init()主要完成与套接字相关的初始化,do_initcalls()完成协议初始化,inet_init()完成与INET套接字相关的初始化。

套接字初始化 sock_init()

net/socket.c

sock_init()主要任务是初始化套接字。具体初始化的具体内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void __init sock_init(void)
{
int i;
//初始化协议族管理变量net_families
for (i = 0; i < NPROTO; i++)
net_families[i] = NULL;
sk_init();//初始化套接字结构

#ifdef SLAB_SKB
skb_init();//初始化套接字缓冲区
#endif
//初始化并注册套接字文件系统
init_inodecache();
register_filesystem(&sock_fs_type);
sock_mnt = kern_mount(&sock_fs_type);

#ifdef CONFIG_NETFILTER
netfilter_init();
#endif
}

协议族套接字初始化的工作由对应的初始化函数来完成。

协议族初始化

不同协议族套接字初始化都不同的函数,例如:INET协议族套接字初始化由inet_init()函数完成。

INET协议族套接字初始化inet_init()

net/ipv4/af_inet.c

INET协议族初始化时,inet_init()调用sock_register()注册INET套接字的创建方法到(struct net_proto_family)*net_families[NPROTO]中,该数组记录各协议套接字的创建方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static int __init inet_init(void)
{
//...
//INET协议族用(struct net_proto_family)inet_family_ops变量注册INET协议族套接字的创建方法;
//根据inet_family_ops的记录,.create被指定为inet_create()函数。
(void)sock_register(&inet_family_ops);
//...
//用(struct net_protocol)icmp_protocol变量注册ICMP数据包的接收方法
//用(struct net_protocol)udp_protocol变量注册管理UDP数据包的接收方法
//用(struct net_protocol)tcp_protocol变量注册管理TCP数据包的接收方法
//...
arp_init();//启动ARP模块
ip_init();//启动IP模块
tcp_init();//启动TCP缓存
icmp_init(&inet_family_ops);//启动ICMP模块,根据inet_family_ops的记录来指定创建方法。
}

注册协议族套接字的创建方法 sock_register()

net/socket.c

sock_register()向net_families数组中添加协议族套接字的创建方法,该数组记录各协议套接字的创建方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int sock_register(struct net_proto_family *ops)
{//传入参数为内核中记录的各协议对应的struct net_proto_family型变量,像查电话本一样,根据传入参数进行注册。
int err;

if (ops->family >= NPROTO) {
printk(KERN_CRIT "protocol %d >= NPROTO(%d)\n", ops->family, NPROTO);
return -ENOBUFS;
}
net_family_write_lock();
err = -EEXIST;
//通过参数ops得到其中记录的create方法等信息,添加到net_families数组中。
if (net_families[ops->family] == NULL) {
net_families[ops->family]=ops;
err = 0;
}
net_family_write_unlock();
printk(KERN_INFO "NET: Registered protocol family %d\n",
ops->family);
return err;
}

套接字创建流程

1
2
3
int socket(int domain, int type, int protocol);
//参数:(协议族,套接字传输类型,指定在套接字上使用的特定协议_default0)
//返回:套接字描述符

我们在应用层使用socket()函数创建套接字时,socket()触发内核调用sys_socket(),sys_socket()又调用sock_create()。

sock_create()根据一路传参下来的协议族类型参数有选择地调用不同的套接字创建函数。例如,指定的协议族类型为PF_INET,则sock_create()调用inet_create()创建INET套接字。

继续以INET套接字为例。inet_create()首先创建套接字的内核表示结构,再返回一个套接字描述符来记录生成的套接字对象。该套接字描述符一直被返回到socket()函数,从而我们可以通过调用socket()获得套接字描述符。

INET协议套接字创建函数 inet_create()

net/ipv4/af_inet.c

inet_create()函数创建一个INET套接字(一个struct socket变量),并根据套接字类型来决定传输层所采用的协议。

struct socket中最重要的成员是sk,它指向一个struct sock。inet_create()首先调用sk_alloc()为sock分配内存,令sk指向它,并按照套接字类型对其初始化。

例,如果sock->type=SOCK_STREAM,那么内核会创建TCP协议的套接字,sock->ops提供INET协议族操作集,sock->sk->sk_prot将提供TCP协议操作集。通过查询inetsw链表,inet_create()得到记录传输层协议操作集的信息。

针对TCP协议,查询到的结果是tcp_prot,传输协议操作集struct proto tcp_prot中记录了一系列TCP协议相关操作的函数,有关套接字初始化的函数为tcp_v4_init_sock()。

TCP协议套接字初始化函数 tcp_v4_init_sock()

tcp_v4_init_sock()初始化套接字与TCP有关的数据,包括初始化TCP队列、传输定时器、发送窗口、慢启动窗口、拥塞窗口控制、数据包最小长度,以及sk相关参数:套接字状态、收发缓冲区、队列有关的字段。