# Unix Socket 原理
前面我们在分析Redis的ae事件模型和流程处理时,看到这样的代码。笔者也曾说过对于 unix socket 而言将不会涉及到内核网络栈,而仅仅只是在 Socket 接口基础上在内存中实现了进程间通讯,本文将详细介绍内核对于该进程通讯的实现过程,注意:读者需要具备C语言基础,至少得知道函数指针的意义。而又由于内核功能模块间互相耦合,即便通过函数指针来解耦,但是也存在着大量的函数指针,笔者将规避与unix socket不相关的功能模块,读者看到这些地方时,只需要了解输入和输出就好。
int anetUnixServer(char *err, char *path, mode_t perm)
{
int s;
struct sockaddr_un sa;
if ((s = anetCreateSocket(err,AF_LOCAL)) == ANET_ERR) // 创建unix server socket
return ANET_ERR;
memset(&sa,0,sizeof(sa));
sa.sun_family = AF_LOCAL;
strncpy(sa.sun_path,path,sizeof(sa.sun_path)-1);
if (anetListen(err,s,(struct sockaddr*)&sa,sizeof(sa)) == ANET_ERR) // 开启监听
return ANET_ERR;
if (perm)
chmod(sa.sun_path, perm);
return s;
}
static int anetCreateSocket(char *err, int domain) {
int s, on = 1;
if ((s = socket(domain, SOCK_STREAM, 0)) == -1) { // 调用系统调用 sys_socket 创建
anetSetError(err, "creating socket: %s", strerror(errno));
return ANET_ERR;
}
if (setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) == -1) {
anetSetError(err, "setsockopt SO_REUSEADDR: %s", strerror(errno));
return ANET_ERR;
}
return s;
}
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
sys_socket 函数
该函数为创建socket的系统调用接口,family 用于指明协议簇,type 表示交互类型,通常我们指定 SOCK_STREAM 表示面向字节流的类型,protocol 表示套接字使用的特定协议,通常只存在一个协议来支持给定协议族中的特定套接字类型,在这种情况下,protocol可以指定为0。但是,在存在多种处理协议时,必须以protocol的方式指定特定的协议(TCP和Unix 我们都将其设置为0即可)。
在Unix Socket中,我们看到 family 为 AF_LOCAL,type为SOCK_STREAM,protocol为0。通过源码得知,创建核心在于 sock_create(family, type, protocol, &sock) 函数。
// 内核 unix family的定义,AF_UNIX 与 AF_LOCAL 等价,只不过满足POSIX规范
#define AF_UNIX 1 /* Unix domain sockets */
#define AF_LOCAL 1 /* POSIX name for AF_UNIX */
#define PF_UNIX AF_UNIX // PF_UNIX 等价于AF_UNIX
asmlinkage long sys_socket(int family, int type, int protocol)
{
int retval;
struct socket *sock;
retval = sock_create(family, type, protocol, &sock); // 实际创建unix socket
if (retval < 0)
goto out;
retval = sock_map_fd(sock); // VFS 约定一切皆文件,所以需要将 socket 映射为fd 返回
if (retval < 0)
goto out_release;
out:
return retval;
out_release:
sock_release(sock);
return retval;
}
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
sock_create 函数
int sock_create(int family, int type, int protocol, struct socket **res)
{
int i;
int err;
struct socket *sock;
// 检查协议范围
if (family < 0 || family >= NPROTO)
return -EAFNOSUPPORT;
if (type < 0 || type >= SOCK_MAX)
return -EINVAL;
// 检查协议兼容性
if (family == PF_INET && type == SOCK_PACKET) {
static int warned;
if (!warned) {
warned = 1;
printk(KERN_INFO "%s uses obsolete (PF_INET,SOCK_PACKET)\n", current->comm);
}
family = PF_PACKET;
}
if (net_families[family] == NULL) { // 若指定协议簇的net_proto_family不存在,那么退出
i = -EAFNOSUPPORT;
goto out;
}
if (!(sock = sock_alloc())) // 分配一个socket(由于一切皆VFS,所以这里的socket其实就是 sock_mnt 挂载点分配Linux自身维护VFS的socket结构,此结构涉及到VFS相关,这里只需要知道它基于VFS的innode创建即可)
{
printk(KERN_WARNING "socket: no more sockets\n");
i = -ENFILE; /* Not exactly a match, but its the
closest posix thing */
goto out;
}
sock->type = type;
...
if ((i = net_families[family]->create(sock, protocol)) < 0) // 调用对应协议簇创建底层sock
goto out_module_put;
...
}
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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
AF_LOCAL 协议簇
至此我们看到实际调用协议簇的create方法完成sock的创建,这里我们研究AF_LOCAL,所以我们直接看AF_LOCAL的协议簇和它的create方法实现。
// AF_LOCAL 也即 PF_UNIX 的协议簇定义
struct net_proto_family unix_family_ops = {
.family = PF_UNIX,
.create = unix_create,
.owner = THIS_MODULE,
};
// 定义了unix socket 的相关操作函数指针的赋值(对外统一提供Socket解耦,具体场景对其中的函数指针进行赋值操作)
struct proto_ops unix_stream_ops = {
.family = PF_UNIX,
.owner = THIS_MODULE,
.release = unix_release,
.bind = unix_bind,
.connect = unix_stream_connect,
.socketpair = unix_socketpair,
.accept = unix_accept,
.getname = unix_getname,
.poll = unix_poll,
.ioctl = unix_ioctl,
.listen = unix_listen,
.shutdown = unix_shutdown,
.setsockopt = sock_no_setsockopt,
.getsockopt = sock_no_getsockopt,
.sendmsg = unix_stream_sendmsg,
.recvmsg = unix_stream_recvmsg,
.mmap = sock_no_mmap,
.sendpage = sock_no_sendpage,
};
// PF_UNIX协议簇 sock 创建方法
static int unix_create(struct socket *sock, int protocol)
{
if (protocol && protocol != PF_UNIX) // 只支持PF_UNIX,所以当我们指定protocol为0时,这里将不会检查protocol,将使用默认的PF_UNIX,且目前只存在PF_UNIX这一种协议
return -EPROTONOSUPPORT;
sock->state = SS_UNCONNECTED; // 初始状态为未连接状态
switch (sock->type) {
case SOCK_STREAM:
sock->ops = &unix_stream_ops; // 注意:这里设置了sock的相关操作~
break;
// 无奈之举:看解释,不管你信不信,BSD存在 AF_UNIX, SOCK_RAW 定义,尽管没有使用它
/*
* Believe it or not BSD has AF_UNIX, SOCK_RAW though
* nothing uses it.
*/
case SOCK_RAW:
sock->type=SOCK_DGRAM;
case SOCK_DGRAM:
sock->ops = &unix_dgram_ops;
break;
default:
return -ESOCKTNOSUPPORT;
}
return unix_create1(sock) ? 0 : -ENOMEM;
}
// 完成进一步的数据填充
static struct sock * unix_create1(struct socket *sock)
{
// 分配底层网络框架:sock结构
struct sock *sk = NULL;
struct unix_sock *u;
if (atomic_read(&unix_nr_socks) >= 2*files_stat.max_files)
goto out;
sk = sk_alloc(PF_UNIX, GFP_KERNEL, sizeof(struct unix_sock),
unix_sk_cachep);
if (!sk)
goto out;
atomic_inc(&unix_nr_socks);
// 初始化sock相关数据
...
out:
return sk;
}
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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
unix socket 绑定与监听过程
redis中我们看到当unix socket 创建过后,将调用如下代码完成unix server socket 的绑定和监听,我们来看看具体实现原理。
int anetUnixServer(char *err, char *path, mode_t perm)
{
int s;
struct sockaddr_un sa;
if ((s = anetCreateSocket(err,AF_LOCAL)) == ANET_ERR)
return ANET_ERR;
memset(&sa,0,sizeof(sa));
sa.sun_family = AF_LOCAL;
strncpy(sa.sun_path,path,sizeof(sa.sun_path)-1); // 注意这个path的使用,这里传入的为sockaddr_un,但是将其强转为sockaddr指针
if (anetListen(err,s,(struct sockaddr*)&sa,sizeof(sa)) == ANET_ERR)
return ANET_ERR;
if (perm)
chmod(sa.sun_path, perm);
return s;
}
static int anetListen(char *err, int s, struct sockaddr *sa, socklen_t len) {
if (bind(s,sa,len) == -1) {
anetSetError(err, "bind: %s", strerror(errno));
close(s);
return ANET_ERR;
}
if (listen(s, 511) == -1) {
anetSetError(err, "listen: %s", strerror(errno));
close(s);
return ANET_ERR;
}
return ANET_OK;
}
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
58
59
60
61
sockaddr_un 与 sockaddr
这两个结构定义一样,只不过sockaddr_un用于指定unix socket 的路径名(深度理解下C语言的指针原理~)。源码如下。
struct sockaddr_un {
sa_family_t sun_family; /* AF_UNIX */
char sun_path[UNIX_PATH_MAX]; /* pathname */
};
struct sockaddr {
sa_family_t sa_family; /* address family, AF_xxx */
char sa_data[14]; /* 14 bytes of protocol address */
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
unix_stream_ops 之 unix_bind 绑定操作
该操作对于unix socket 而言,仅仅是创建接入VFS的innode,然后将自身放入hash表中。
struct unix_address
{
atomic_t refcnt;
int len;
unsigned hash;
struct sockaddr_un name[0];
}; // unix socket 的地址结构体
static int unix_bind(struct socket *sock, struct sockaddr *uaddr, int addr_len)
{
struct sock *sk = sock->sk;
struct unix_sock *u = unix_sk(sk);
struct sockaddr_un *sunaddr=(struct sockaddr_un *)uaddr;// 强转为sockaddr_un
struct dentry * dentry = NULL; // Linux VFS 中使用 dentry 来维护文件的目录,所以socket也需要一个dentry结构
struct nameidata nd;
int err;
unsigned hash;
struct unix_address *addr;
struct hlist_head *list;
err = -EINVAL;
if (sunaddr->sun_family != AF_UNIX) // 协议簇必须为 AF_UNIX
goto out;
if (addr_len==sizeof(short)) { // 地址长度为short 2字节时,自动绑定
err = unix_autobind(sock);
goto out;
}
err = unix_mkname(sunaddr, addr_len, &hash); // 校验 sun_path
if (err < 0)
goto out;
addr_len = err;
...
addr = kmalloc(sizeof(*addr)+addr_len, GFP_KERNEL); // 分配存放地址内存
if (!addr)
goto out_up;
// 将用户空间传入的信息填入 unix_address 结构
memcpy(addr->name, sunaddr, addr_len);
addr->len = addr_len;
addr->hash = hash ^ sk->sk_type;
atomic_set(&addr->refcnt, 1);
if (sunaddr->sun_path[0]) {
// path 路径存在,检测其父目录并创建对应的innode
...
vfs_mknod(nd.dentry->d_inode, dentry, mode, 0);
...
}
...
// 为了快速找到该socket,这里将其放入到通过链地址法和数组组成的hash表中
list = &unix_socket_table[dentry->d_inode->i_ino & (UNIX_HASH_SIZE-1)];
u->dentry = nd.dentry;
u->mnt = nd.mnt;
...
__unix_insert_socket(list, sk); // 放入hash 表
...
goto out_up;
}
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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
unix_stream_ops 之 unix_listen监听操作
static int unix_listen(struct socket *sock, int backlog)
{
int err;
struct sock *sk = sock->sk;
struct unix_sock *u = unix_sk(sk);
err = -EOPNOTSUPP;
if (sock->type!=SOCK_STREAM) // 对于unix server socket 只支持流协议
goto out;
err = -EINVAL;
if (!u->addr) // 地址不存在直接返回
goto out;
unix_state_wlock(sk);
if (sk->sk_state != TCP_CLOSE && sk->sk_state != TCP_LISTEN) // 状态必须为初始状态
goto out_unlock;
if (backlog > sk->sk_max_ack_backlog) // 设置backlog 超过之前设置的大小,那么唤醒所有等待连接的其他unix socket 进程
wake_up_interruptible_all(&u->peer_wait);
sk->sk_max_ack_backlog = backlog;
sk->sk_state = TCP_LISTEN;
...
}
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
unix socket 连接过程
现在我们来看看客户端连接unix server socket的流程。我们看到这里其实就是校验server socket的接收队列是否超过设置的backlog长度,若超过那么根据timeo变量来选择是否阻塞当前进程,若没有超过,那么将新分配的服务端 socket 与客户端socket绑定,然后将新的服务端socket放入到接收队列中。源码如下。
static int unix_stream_connect(struct socket *sock, struct sockaddr *uaddr,
int addr_len, int flags)
{
struct sockaddr_un *sunaddr=(struct sockaddr_un *)uaddr; // server socket 地址,其实就是sun.path
struct sock *sk = sock->sk; // 客户端 sock
struct unix_sock *u = unix_sk(sk), *newu, *otheru;
struct sock *newsk = NULL; // new sock , 也即与当前 socket *sock 连接的 服务端socket
struct sock *other = NULL; // server sock
struct sk_buff *skb = NULL;
unsigned hash;
int st;
int err;
long timeo;
err = unix_mkname(sunaddr, addr_len, &hash); // 校验path
if (err < 0)
goto out;
addr_len = err;
newsk = unix_create1(NULL); // 创建要给新的sock 完成连接,server socket 只是个接收连接的,而在服务端同样需要一个 socket 来接收连接,而这里的 newsk 就是这个socket
if (newsk == NULL)
goto out;
skb = sock_wmalloc(newsk, 1, 0, GFP_KERNEL); // 分配skb作为发送到server socket的数据载体,注意:由于这里根本没有数据,所以这里将 size 设置为 1
if (skb == NULL)
goto out;
other = unix_find_other(sunaddr, addr_len, sk->sk_type, hash, &err); // 根据路径信息找到server socket
if (!other)
goto out;
...
if (other->sk_state != TCP_LISTEN) // 对端状态,必须为监听状态
goto out_unlock;
if (skb_queue_len(&other->sk_receive_queue) >
other->sk_max_ack_backlog) { // server socket 队列长度超过设置的大小,那么根据timeo设置,看看是否需要阻塞当前进程,等待server socket慢慢消化需要连接的客户端socket
err = -EAGAIN;
if (!timeo)
goto out_unlock;
timeo = unix_wait_for_peer(other, timeo); // 需要等待,等待时长由timeo指定
err = sock_intr_errno(timeo); // 根据等待时长返回值来返回合适的错误值
if (signal_pending(current)) // 若当前进程有需要处理的信号,那么退出当前系统调用,让进程处理信号
goto out;
sock_put(other); // 如果 server socket 没有地方引用,则销毁 server socket ,然后重试
goto restart;
}
sock_hold(sk); // 增加客户端 socket 的引用
unix_peer(newsk) = sk; // 用新的socket 包含 当前客户端sk
... // 设置状态值
newsk->sk_state = TCP_ESTABLISHED;
newsk->sk_type = SOCK_STREAM;
newsk->sk_peercred.pid = current->tgid;
newsk->sk_peercred.uid = current->euid;
newsk->sk_peercred.gid = current->egid;
newu = unix_sk(newsk);
newsk->sk_sleep = &newu->peer_wait;
otheru = unix_sk(other);
...
// 将客户端socket 与 new socket 绑定
unix_peer(sk) = newsk;
sock->state = SS_CONNECTED;
sk->sk_state = TCP_ESTABLISHED;
...
__skb_queue_tail(&other->sk_receive_queue, skb); // 将newsk 的 skb 信息放入到server socket的接收队列中,于是乎:server socket 就可以accept它了~
return err;
}
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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
unix socket 接收连接过程
接收过程很简单,直接从server socket的接收队列中获取即可。
static int unix_accept(struct socket *sock, struct socket *newsock, int flags)
{
...
skb = skb_recv_datagram(sk, 0, flags&O_NONBLOCK, &err);
...
return err;
}
struct sk_buff *skb_recv_datagram(struct sock *sk, unsigned flags,
int noblock, int *err)
{
...
skb = skb_dequeue(&sk->sk_receive_queue); // 直接从接收队列中出队即可(当然,这里忽略了若接收队列为空,那么将根据阻塞模式来选择等待或者返回)
...
return NULL;
}
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
总结
至此,我们发现:unix socket 只是利用了VFS 和网络栈框架在内存中模拟了网络通讯,事实上在server socket 与 socket之间的通讯,没有经过网络栈和任何协议处理,均是在socket 的 接收缓冲区与发送缓冲区、server socket的接收队列,在内存中传送数据。所以我们将unix socket通讯称之为 IPC 进程通讯的一种方式。