系统调用 socket

注:本文分析基于3.10.107内核版本

1、函数原型

1
2
3
4
5
6
int socket(int domain, int type, int protocol);

参数:
domain:指定通信协议族。常用的协议族有AF_INET、AF_UNIX等,对于TCP协议,该字段应为AF_INET(ipv4)或AF_INET6(ipv6)。
type:指定socket类型。常用的socket类型有SOCK_STREAM、SOCK_DGRAM等。SOCK_STREAM针对于面向连接的TCP服务应用。SOCK_DGRAM对应于无连接的UDP服务应用。
protocol:指定socket所使用的协议,一般我们平常都指定为0,使用type中的默认协议。严格意义上,IPPROTO_TCP(值为6)代表TCP协议。

2、内核实现源码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol)
{
int retval;
struct socket *sock;
int flags;

...
retval = sock_create(family, type, protocol, &sock);
if (retval < 0)
goto out;

retval = sock_map_fd(sock, flags & (O_CLOEXEC | O_NONBLOCK));
if (retval < 0)
goto out_release;

out:
/* It may be already another descriptor 8) Not kernel problem. */
return retval;

out_release:
sock_release(sock);
return retval;
}

可以看到,除去一些参数合法性校验,socket函数主要由 sock_createsock_map_fd 这两个函数完成,因此我们主要分析这两个函数,看它们是怎么在内核一步步创建和管理我们使用的 socket。

2.1 sock_create函数

sock_create() 实际调用的是 __sock_create()

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
int __sock_create(struct net *net, int family, int type, int protocol,
struct socket **res, int kern)
{
int err;
struct socket *sock;
const struct net_proto_family *pf;
...
err = security_socket_create(family, type, protocol, kern);//SElinux相关,暂不关注
/*
* Allocate the socket and allow the family to set things up. if
* the protocol is 0, the family is instructed to select an appropriate
* default.
*/
sock = sock_alloc();//创建struct socket结构体

sock->type = type;//设置套接字的类型
...
pf = rcu_dereference(net_families[family]);//获取对应协议族的协议实例对象
...
err = pf->create(net, sock, protocol, kern);
if (err < 0)
goto out_module_put;

...
err = security_socket_post_create(sock, family, type, protocol, kern);//SElinux相关,暂不关注

*res = sock;
...
}

这里比较重要的是 sock_alloc()pf->create() 这两个函数。其中 sock_alloc() 里体现了 linux 一切皆文件(Everything is a file)理念,即使用文件系统来管理 socket,这也是 VFS 所要达到的效果。

2.1.1 sock_alloc() 函数

总的来说,sock_alloc() 函数分配一个 struct socket_alloc 结构体,将 sockfs 相关属性填充在 socket_alloc 结构体的 vfs_inode 变量中,以限定后续对这个 sock 文件允许的操作。同时 sock_alloc() 最终返回 socket_alloc 结构体的 socket 变量,用于后续操作。

2.1.2 pf->create()函数

下面我们继续看下 pf->create() 这个函数。

pf 由 net_families[] 数组获得,我们先看下 net_families[] 数组的定义:

1
2
3
4
#define AF_MAX      41  /* For now.. */
#define NPROTO AF_MAX

static const struct net_proto_family __rcu *net_families[NPROTO] __read_mostly;

net_families[] 数组的初始化在 inet_init() 函数,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static const struct net_proto_family inet_family_ops = {
.family = PF_INET,
.create = inet_create,
.owner = THIS_MODULE,
};

static int __init inet_init(void)
{
...
(void)sock_register(&inet_family_ops);
...
}

int sock_register(const struct net_proto_family *ops)
{
...
rcu_assign_pointer(net_families[ops->family], ops);
...
}

因此,net_families[] 数组里存放的是各个协议族的信息,以 family 字段作为下标。此处我们针对TCP协议分析,因此我们 family 字段是 AF_INET,另外一般会有

1
#define PF_INET     AF_INET

所以 pf->create 调用的就是 inet_create() 函数。

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
static int inet_create(struct net *net, struct socket *sock, int protocol,
int kern)
{
struct sock *sk;
struct inet_protosw *answer;
struct inet_sock *inet;
struct proto *answer_prot;
unsigned char answer_flags;
char answer_no_check;
int try_loading_module = 0;
int err;
...
sock->state = SS_UNCONNECTED;

/* Look for the requested type/protocol pair. */
lookup_protocol:
err = -ESOCKTNOSUPPORT;
rcu_read_lock();
//根据socket传入的protocal在inetsw[]数组中查找对应的元素
list_for_each_entry_rcu(answer, &inetsw[sock->type], list) {

err = 0;
/* 如果我们在socket的protocal传入的是6,即TCP协议,那么走这个分支 */
if (protocol == answer->protocol) {
if (protocol != IPPROTO_IP)
break;
} else {
/* 如果socket的protocal传入的是0,那么走这个分支 */
if (IPPROTO_IP == protocol) {
protocol = answer->protocol;//重新给protocal赋值,因此socket中protocal传入的是0或者6,都是可以的
break;
}
if (IPPROTO_IP == answer->protocol)
break;
}
err = -EPROTONOSUPPORT;
}
...
sock->ops = answer->ops;//将查找到的对应协议族的协议函数操作集赋值给我们之前创建的socket
answer_prot = answer->prot;
...
//创建sock结构体,注意这里创建的结构体类型是sock,之前我们通过sock_alloc创建的结构体类型是socket
sk = sk_alloc(net, PF_INET, GFP_KERNEL, answer_prot);
if (sk == NULL)
goto out;
...
sock_init_data(sock, sk);//sock初始化

//另一部分初始化,里面有对各个socket连接定时器的初始化
if (sk->sk_prot->init) {
err = sk->sk_prot->init(sk);
if (err)
sk_common_release(sk);
}
...
}
}

inet_create() 函数里有几处是比较重要的,一个是通过 inetsw[] 数组获取对应协议类型的接口操作集信息,另一个是创建 struct sock 类型的变量,最后是对创建的 sock 进行初始化。

2.1.2.1 inetsw[]数组

inetsw[] 数组存放的是各个 sock_type 的信息,它也是在 inet_init() 函数中初始化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
enum sock_type {
SOCK_STREAM = 1,
SOCK_DGRAM = 2,
SOCK_RAW = 3,
SOCK_RDM = 4,
SOCK_SEQPACKET = 5,
SOCK_DCCP = 6,
SOCK_PACKET = 10,
};

#define SOCK_MAX (SOCK_PACKET + 1)
static int __init inet_init(void)
{
...
/* 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);
...
}

其中 inetsw_array[] 存放的就是具体的每种 sock 的信息,包括操作函数,协议号等,其中 protops 两个成员是比较重要的,后续很多操作依赖于这两个成员。

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
30
static struct inet_protosw inetsw_array[] = {
{
.type = SOCK_STREAM,
.protocol = IPPROTO_TCP,
.prot = &tcp_prot,
.ops = &inet_stream_ops,
.no_check = 0,
.flags = INET_PROTOSW_PERMANENT |
INET_PROTOSW_ICSK,
},

{
.type = SOCK_DGRAM,
.protocol = IPPROTO_UDP,
.prot = &udp_prot,
.ops = &inet_dgram_ops,
.no_check = UDP_CSUM_DEFAULT,
.flags = INET_PROTOSW_PERMANENT,
},

{
.type = SOCK_DGRAM,
.protocol = IPPROTO_ICMP,
.prot = &ping_prot,
.ops = &inet_dgram_ops,
.no_check = UDP_CSUM_DEFAULT,
.flags = INET_PROTOSW_REUSE,
},
...
};

然后通过 inet_register_protosw() 函数,将上述 inetsw_array[] 里的元素,按照 type 字段挂在 inetsw[] 数组的链表上。因为 type 字段是有重复的,所以每个 inetsw[] 元素的链表上可能会有多个成员。

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
void inet_register_protosw(struct inet_protosw *p)
{
struct list_head *lh;
struct inet_protosw *answer;
int protocol = p->protocol;
struct list_head *last_perm;
...
answer = NULL;
last_perm = &inetsw[p->type];
list_for_each(lh, &inetsw[p->type]) {
answer = list_entry(lh, struct inet_protosw, list);

/* Check only the non-wild match. */
if (INET_PROTOSW_PERMANENT & answer->flags) {
if (protocol == answer->protocol)//如果是要覆盖已有的协议族,则失败
break;
last_perm = lh;
}

answer = NULL;
}
if (answer)
goto out_permanent;

list_add_rcu(&p->list, last_perm);//按照type的值,添加到inetsw[type]数组中的链表中
...
}
2.1.2.2 创建struct sock类型变量

通过 sk_alloc() 函数创建 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
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;
//这里留意一下,后续会使用到sk_prot这个变量
sk->sk_prot = sk->sk_prot_creator = prot;
...
}

return sk;
}

static struct sock *sk_prot_alloc(struct proto *prot, gfp_t priority,
int family)
{
struct sock *sk;
struct kmem_cache *slab;

slab = prot->slab;
if (slab != NULL) {
//使用对应协议的slab高速缓存分配,减小内存碎片
sk = kmem_cache_alloc(slab, priority & ~__GFP_ZERO);
...
}

由此可见,最后是通过 prot->slab,也就是TCP的slab高速缓存里分配的。

关于TCP协议的slab高速缓存建立,详见 Linux socket系统调用(三)—-TCP协议的slab高速缓存建立及各结构体关系

2.1.2.3 sock初始化

创建完 sock 结构体,那么自然要做初始化。初始化又分为两个大块,一个在 sock_init_data() 函数,另一个在 sk->sk\_prot->init()

a. sock_init_data()

sock_init_data() 函数我认为比较重要的一点就是将之前分配的 struct socket 和 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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
void sock_init_data(struct socket *sock, struct sock *sk)
{
skb_queue_head_init(&sk->sk_receive_queue);
skb_queue_head_init(&sk->sk_write_queue);
skb_queue_head_init(&sk->sk_error_queue);
#ifdef CONFIG_NET_DMA
skb_queue_head_init(&sk->sk_async_wait_queue);
#endif

sk->sk_send_head = NULL;

init_timer(&sk->sk_timer);

sk->sk_allocation = GFP_KERNEL;
//socket的接收和发送缓存,怎么用的,暂时不太清楚,后续再分析
//这两个参数可以在proc下设置,两个分别是/proc/sys/net/core/rmem_default和/proc/sys/net/core/wmem_default
sk->sk_rcvbuf = sysctl_rmem_default;
sk->sk_sndbuf = sysctl_wmem_default;
sk->sk_state = TCP_CLOSE;//socket一开始的状态就是close
sk_set_socket(sk, sock);

sock_set_flag(sk, SOCK_ZAPPED);

if (sock) {
sk->sk_type = sock->type;
sk->sk_wq = sock->wq;
//struct socket里有指针指向对应的struct sock
//加上上面sk_set_socket()的设置,struct socket和struct sock就能通过各自的成员访问到对方
sock->sk = sk;
} else
sk->sk_wq = NULL;

//指定一些回调函数
sk->sk_state_change = sock_def_wakeup;
sk->sk_data_ready = sock_def_readable;
sk->sk_write_space = sock_def_write_space;
sk->sk_error_report = sock_def_error_report;
...
}
static inline void sk_set_socket(struct sock *sk, struct socket *sock)
{
sk_tx_queue_clear(sk);
//让struct sock的sk_socket成员指向对应的struct socket
sk->sk_socket = sock;
}

b. sk->sk_prot->init()

对于 TCP 协议,这个 init 成员指向的是 tcp_v4_init_sock() 函数,具体参看 struct proto tcp_prot 结构体的成员。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static int tcp_v4_init_sock(struct sock *sk)
{
struct inet_connection_sock *icsk = inet_csk(sk);

tcp_init_sock(sk);//初始化操作
//设置操作集,这个赋值比较重要,后续icsk->icsk_af_ops的调用要知道最终调用的是什么
icsk->icsk_af_ops = &ipv4_specific;

#ifdef CONFIG_TCP_MD5SIG
tcp_sk(sk)->af_specific = &tcp_sock_ipv4_specific;
#endif

return 0;
}

tcp_v4_init_sock() 函数里调用 tcp_init_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
void tcp_init_sock(struct sock *sk)
{
struct inet_connection_sock *icsk = inet_csk(sk);
struct tcp_sock *tp = tcp_sk(sk);

skb_queue_head_init(&tp->out_of_order_queue);
//初始化定时器,包括超时重传定时器,延迟ack定时器,保活定时器等的初始化
tcp_init_xmit_timers(sk);
tcp_prequeue_init(tp);
INIT_LIST_HEAD(&tp->tsq_node);

icsk->icsk_rto = TCP_TIMEOUT_INIT;//设置初始的超时时间,为1HZ,即1s
...
tcp_enable_early_retrans(tp);
icsk->icsk_ca_ops = &tcp_init_congestion_ops;//设置默认拥塞算法,reno

tp->tsoffset = 0;

sk->sk_state = TCP_CLOSE;//这里又再次设置了sock状态,重复了sock_init_data()的动作
...
//socket的接收和发送缓存,sock_init_data()也有设置,不过是设置默认的大小
//这里使用的是/proc/sys/net/ipv4/tcp_wmem和/proc/sys/net/ipv4/tcp_rmem的值
sk->sk_sndbuf = sysctl_tcp_wmem[1];
sk->sk_rcvbuf = sysctl_tcp_rmem[1];
...
}

至此,我们已经创建好了socket,也完成了相关的初始化,但是我们知道socket系统调用返回的是一个int型的文件描述符,而目前我们好像还没涉及到。

下面就进入第二部分,fd的创建。

2.2 sock_map_fd()

这个函数主要有两个部分,一个是创建 file 文件结构,fd 文件描述符,另一部分是将file 文件结构和 fd 文件描述符关联,同时将上一步返回的 socket 也一起绑定,形成一个完整的逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static int sock_map_fd(struct socket *sock, int flags)
{
struct file *newfile;
//获取一个未使用的文件描述符,文件描述符的管理这里就暂不分析了
int fd = get_unused_fd_flags(flags);
if (unlikely(fd < 0))
return fd;

//分配file结构体
newfile = sock_alloc_file(sock, flags, NULL);
if (likely(!IS_ERR(newfile))) {
fd_install(fd, newfile);
return fd;
}
...
}

2.2.1 sock_alloc_file()函数

file 结构体的创建及初始化,以及将 socket 和 file 结构体关联的操作主要在sock_alloc_file() 函数中完成,

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
struct file *sock_alloc_file(struct socket *sock, int flags, const char *dname)
{
struct qstr name = { .name = "" };
struct path path;
struct file *file;
...
//在sock文件系统上分配一个dentry结构
path.dentry = d_alloc_pseudo(sock_mnt->mnt_sb, &name);
if (unlikely(!path.dentry))
return ERR_PTR(-ENOMEM);
path.mnt = mntget(sock_mnt);

//使用之前我们分配socket结构体来初始化dentry
d_instantiate(path.dentry, SOCK_INODE(sock));
SOCK_INODE(sock)->i_fop = &socket_file_ops;//设置该inode的函数操作集

file = alloc_file(&path, FMODE_READ | FMODE_WRITE,
&socket_file_ops);//从cache里分配一个file结构体
...
//socket结构体里有file指针,指向我们刚分配的file结构体
sock->file = file;
file->f_flags = O_RDWR | (flags & O_NONBLOCK);
//同样file结构体了也有指针指向socket结构体
//至此file和socket已经做好关联,就差fd和file关联了
file->private_data = sock;
return file;
}

d_instantiate() 函数里我们可以看到 dentry 和之前创建 socket 结构体时分配的 struct socket_alloc 的联系。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void d_instantiate(struct dentry *entry, struct inode * inode)
{
...
__d_instantiate(entry, inode);
...
}

static void __d_instantiate(struct dentry *dentry, struct inode *inode)
{
...
//还记得之前我们分配socket结构体时,其实分配的是struct socket_alloc,
//该结构体里包含了struct socket和struct inode,这里就是将这个struct inode和上面分配的dentry关联
dentry->d_inode = inode;
...
}

alloc_file() 函数则是将 file dentry inode socket 四者柔和到一起,形成sockfs的整套逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct file *alloc_file(struct path *path, fmode_t mode,
const struct file_operations *fop)
{
struct file *file;

file = get_empty_filp();//在filp_cachep缓存中分配file结构体
if (IS_ERR(file))
return file;

//path结构体里包含了刚才struct socket_alloc的inode成员信息
file->f_path = *path;
//file结构体里也有变量指向刚才struct socket_alloc的inode
//至此,file dentry inode socket四者总算是走到一起,实现了完整的sock文件系统逻辑
file->f_inode = path->dentry->d_inode;
file->f_mapping = path->dentry->d_inode->i_mapping;
file->f_mode = mode;
//fop其实就是socket_file_ops,这样就能表明这个file结构体对应的是socket
file->f_op = fop;
...
}

最后就是返回用户使用的文件描述符fd和file的关联了。

2.2.2 fd_install()函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void fd_install(unsigned int fd, struct file *file)
{
//获得当前进程的打开文件列表files
__fd_install(current->files, fd, file);
}

void __fd_install(struct files_struct *files, unsigned int fd,
struct file *file)
{
struct fdtable *fdt;
spin_lock(&files->file_lock);
fdt = files_fdtable(files);//获得文件描述符位图表
BUG_ON(fdt->fd[fd] != NULL);
//将file指针放入文件描述符位图表中,这样我们之后就能通过fd找回file结构
rcu_assign_pointer(fdt->fd[fd], file);
spin_unlock(&files->file_lock);
}

至此,socket系统调用就结束了,将fd返回用户使用。

我们大概概述一下,socket系统调用的操作:

  • 首先在内核生成一个 socket_alloctcp_sock 类型的对象,其中 sock_alloc 对象中的 sockettcp_sock 对象的 sock 绑定,sock_alloc 对象中的 inode 和 file 类型对象绑定。

  • 然后将分配的文件描述符 fd 和 file 对象关联,最后将这个文件描述符 fd 返回给用户使用。

经过这一连串操作,用户只要使用fd,内核就能根据这个fd进行网络连接管理的各种操作。

最后上张图,描述从fd到内核sock各个结构体的关系。

这里写图片描述

Reference


系统调用 socket
https://flepeng.github.io/002-Linux-41-系统调用-系统调用-socket-源码分析/
作者
Lepeng
发布于
2024年9月23日
许可协议