前言:Unix域是进程间通信的一种方式,他的特点是可以传递文件描述符,在内核中,Unix域是网络的一部分,使用上也遵循网络编程的API。本文分析Unix域的实现。
我们首先看看Unix域的使用。
#include <sys/socket.h>
#include <sys/un.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#define MY_SOCK_PATH "/somepath"
#define LISTEN_BACKLOG 50
int main(int argc, char *argv[])
{
int sfd, cfd;
struct sockaddr_un my_addr, peer_addr;
socklen_t peer_addr_size;
sfd = socket(AF_UNIX, SOCK_STREAM, 0);
memset(&my_addr, 0, sizeof(my_addr));
my_addr.sun_family = AF_UNIX;
strncpy(my_addr.sun_path, MY_SOCK_PATH, sizeof(my_addr.sun_path) - 1);
bind(sfd, (struct sockaddr *) &my_addr, sizeof(my_addr))
listen(sfd, LISTEN_BACKLOG);
peer_addr_size = sizeof(peer_addr);
cfd = accept(sfd, (struct sockaddr *) &peer_addr, &peer_addr_size);
}
了解了怎么使用后,我们从unix_net_init开始分析,网络初始化的时候会调用unix_net_init。
static int __init af_unix_init(void){
int rc = -1;
rc = proto_register(&unix_proto, 1);
// 注册协议簇
sock_register(&unix_family_ops);
register_pernet_subsys(&unix_net_ops);
out:
return rc;}
Unix域初始化的时候会注册协议簇到网络系统,我们看看unix_family_ops。
static const struct net_proto_family unix_family_ops = {
.family = PF_UNIX,
.create = unix_create,
.owner = THIS_MODULE,};
我们看到Unix域的协议簇名字是PF_UNIX(和AF_UNIX一样)。所以当我们调用
socket(AF_UNIX, xxxx);
就会进入Unix域的逻辑。下面我们从socket函数开始分析。
// 创建socket,返回fdint __sys_socket(int family, int type, int protocol){ int retval;
struct socket *sock;
int flags;
flags = type & ~SOCK_TYPE_MASK;
if (flags & ~(SOCK_CLOEXEC | SOCK_NONBLOCK))
return -EINVAL;
type &= SOCK_TYPE_MASK;
if (SOCK_NONBLOCK != O_NONBLOCK && (flags & SOCK_NONBLOCK))
flags = (flags & ~SOCK_NONBLOCK) | O_NONBLOCK;
retval = sock_create(family, type, protocol, &sock);
if (retval < 0)
return retval;
// 返回文件描述符给调用方,后续通过fd能找到socket
return sock_map_fd(sock, flags & (O_CLOEXEC | O_NONBLOCK));}
int sock_create(int family, int type, int protocol, struct socket **res){ return __sock_create(current->nsproxy->net_ns, family, type, protocol, res, 0);}
我们继续看__sock_create。
// 创建一个socketint __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;
// 分配socket
sock = sock_alloc();
sock->type = type;
// 根据family找到配置
pf = rcu_dereference(net_families[family]);
// 调用配置里的函数
err = pf->create(net, sock, protocol, kern);}
我们看到创建一个socket的时候,首先调用sock_alloc分配一个socket结构体,然后通过协议簇类型找到对应的处理函数,这里是AF_UNIX,所以这里会找到我们刚才的初始化时注册的配置。接着调用其中的create函数unix_create。
static int unix_create(struct net *net, struct socket *sock, int protocol,
int kern){
if (protocol && protocol != PF_UNIX)
return -EPROTONOSUPPORT;
sock->state = SS_UNCONNECTED;
// 根据数据类型赋值不同的操作函数集
switch (sock->type) {
case SOCK_STREAM:
sock->ops = &unix_stream_ops;
break;
case SOCK_RAW:
sock->type = SOCK_DGRAM;
fallthrough;
case SOCK_DGRAM:
sock->ops = &unix_dgram_ops;
break;
case SOCK_SEQPACKET:
sock->ops = &unix_seqpacket_ops;
break;
default:
return -ESOCKTNOSUPPORT;
}
return unix_create1(net, sock, kern) ? 0 : -ENOMEM;}
unix_create主要是根据数据类型把对应的函数集挂在socket上,后续就可以调用对应的函数,这是网络层设计的巧妙之处,定义抽象接口,具体实现交给协议,接着看unix_create1。
static struct sock *unix_create1(struct net *net, struct socket *sock, int kern){
struct sock *sk = NULL;
struct unix_sock *u;
// 分配sock结构体,挂载unix_proto函数集到sock
sk = sk_alloc(net, PF_UNIX, GFP_KERNEL, &unix_proto, kern);
// 初始化sock,并和socket关联起来
sock_init_data(sock, sk);
sk->sk_allocation = GFP_KERNEL_ACCOUNT;
sk->sk_write_space = unix_write_space;
sk->sk_max_ack_backlog = net->unx.sysctl_max_dgram_qlen;
sk->sk_destruct = unix_sock_destructor;
// unix_sock是sock的子类,unix_sk(sk) => (struct unix_sock *)sk
u = unix_sk(sk);
u->path.dentry = NULL;
u->path.mnt = NULL;
spin_lock_init(&u->lock);
atomic_long_set(&u->inflight, 0);
INIT_LIST_HEAD(&u->link);
mutex_init(&u->iolock); /* single task reading lock */
mutex_init(&u->bindlock); /* single task binding lock */
init_waitqueue_head(&u->peer_wait);
init_waitqueue_func_entry(&u->peer_wake, unix_dgram_peer_wake_relay);
memset(&u->scm_stat, 0, sizeof(struct scm_stat));
unix_insert_socket(unix_sockets_unbound(sk), sk);
return sk;}
至此,我们完成了socket的创建,做的事情主要是创建和初始化socket和sock结构体并关联起来。socket是上层的接口,sock则是不同协议(TCP、Unix域)对应的实现不一样,后续再单独写文章介绍。
static int unix_bind(struct socket *sock, struct sockaddr *uaddr, int addr_len){
struct sock *sk = sock->sk;
struct net *net = sock_net(sk);
struct unix_sock *u = unix_sk(sk);
struct sockaddr_un *sunaddr = (struct sockaddr_un *)uaddr;
char *sun_path = sunaddr->sun_path;
int err;
unsigned int hash;
struct unix_address *addr;
struct hlist_head *list;
struct path path = { };
err = -EINVAL;
// sun_path[0]是空字符说明是抽象命名空间,不创建文件
if (sun_path[0]) {
umode_t mode = S_IFSOCK | (SOCK_INODE(sock)->i_mode & ~current_umask());
// 创建文件
err = unix_mknod(sun_path, mode, &path);
// 文件已存在,说明地址绑定过了,报错
if (err) {
if (err == -EEXIST)
err = -EADDRINUSE;
goto out;
}
}
err = -ENOMEM;
addr = kmalloc(sizeof(*addr)+addr_len, GFP_KERNEL);
memcpy(addr->name, sunaddr, addr_len);
addr->len = addr_len;
addr->hash = hash ^ sk->sk_type;
refcount_set(&addr->refcnt, 1);
if (sun_path[0]) {
addr->hash = UNIX_HASH_SIZE;
hash = d_backing_inode(path.dentry)->i_ino & (UNIX_HASH_SIZE - 1);
spin_lock(&unix_table_lock);
// 记录文件路径
u->path = path;
// unix_socket_table是个数组,每个元素是链表
list = &unix_socket_table[hash];
// 记录地址信息
smp_store_release(&u->addr, addr);
// 把socket插入list中
__unix_insert_socket(list, sk);
}
}
bind的主要逻辑是先判断文件路径对应的文件是否已经存在,是则报错,否则创建一个文件,并记录了到socket中。更多内容可以参考文档。
接下来我们看listen,listen是服务器的关键步骤,调用了listen的socket才能接收连接。
static int unix_listen(struct socket *sock, int backlog){
int err;
struct sock *sk = sock->sk;
struct unix_sock *u = unix_sk(sk);
struct pid *old_pid = NULL;
err = -EOPNOTSUPP;
// SOCK_DGRAG类型不能listen
if (sock->type != SOCK_STREAM && sock->type != SOCK_SEQPACKET)
goto out;
err = -EINVAL;
// 没有绑定地址
if (!u->addr)
goto out;
unix_state_lock(sk);
// 第一次执行的时候是TCP_CLOSE,第二次执行的时候是TCP_LISTEN状态,其他状态是非法
if (sk->sk_state != TCP_CLOSE && sk->sk_state != TCP_LISTEN)
goto out_unlock;
// 最大同时连接数
sk->sk_max_ack_backlog = backlog;
sk->sk_state = TCP_LISTEN;}
listen的逻辑比较简单,主要是做了一些检验和赋值,最重要的是修改socket状态为监听状态。这种类型的socket才能处理”连接“。
服务器启动后,客户端就可以通过connect进行连接。
static int unix_stream_connect(struct socket *sock, struct sockaddr *uaddr,
int addr_len, int flags){
struct sockaddr_un *sunaddr = (struct sockaddr_un *)uaddr;
struct sock *sk = sock->sk;
struct net *net = sock_net(sk);
struct unix_sock *u = unix_sk(sk), *newu, *otheru;
struct sock *newsk = NULL;
struct sock *other = NULL;
struct sk_buff *skb = NULL;
unsigned int hash;
int st;
int err;
long timeo;
// 文件路径长度
err = unix_mkname(sunaddr, addr_len, &hash);
if (err < 0)
goto out;
addr_len = err;
// socket没有端口则随机绑定一个
if (test_bit(SOCK_PASSCRED, &sock->flags) && !u->addr &&
(err = unix_autobind(sock)) != 0)
goto out;
// 连接的阻塞时长,因为连接可能因为某种原因不能马上成功,例如连接队列已满
timeo = sock_sndtimeo(sk, flags & O_NONBLOCK);
err = -ENOMEM;
// 创建一个新的sock,用于表示服务器和客户端通信的结构体
newsk = unix_create1(sock_net(sk), NULL, 0);
// 在newsk上分配一个skb,每个sock可分配的内存有限
skb = sock_wmalloc(newsk, 1, 0, GFP_KERNEL);
restart:
// 找到服务器对应的sock,相当于寻址
other = unix_find_other(net, sunaddr, addr_len, sk->sk_type, hash, &err);
err = -ECONNREFUSED;
// 没有监听或关闭了则报错ECONNREFUSED
if (other->sk_state != TCP_LISTEN)
goto out_unlock;
if (other->sk_shutdown & RCV_SHUTDOWN)
goto out_unlock;
// 连接队列已满,则需要等待或直接返回(skb_queue_len(&sk->sk_receive_queue) > sk->sk_max_ack_backlog;)
if (unix_recvq_full(other)) {
err = -EAGAIN;
// 没有设置超时则默认非阻塞,直接返回
if (!timeo)
goto out_unlock;
// 否则阻塞等待timeo时间
timeo = unix_wait_for_peer(other, timeo);
// 是被信号唤醒的则先返回EAGAIN给调用方,否则跳到restart重试
if (signal_pending(current))
goto out;
sock_put(other);
goto restart;
}
// 可以开始连接了
sock_hold(sk);
// newsk是代表服务器用于和客户端通信的sock结构体,指向客户端sk,关联起来
unix_peer(newsk) = sk;
// 直接建立连接成功
newsk->sk_state = TCP_ESTABLISHED;
newsk->sk_type = sk->sk_type;
init_peercred(newsk);
newu = unix_sk(newsk);
RCU_INIT_POINTER(newsk->sk_wq, &newu->peer_wq);
otheru = unix_sk(other);
sock->state = SS_CONNECTED;
sk->sk_state = TCP_ESTABLISHED;
// 客户端sock指向服务器的sock
unix_peer(sk) = newsk;
// 插入服务器的连接队列
__skb_queue_tail(&other->sk_receive_queue, skb);
// 唤醒服务器有连接到来,服务器可能阻塞到accept
other->sk_data_ready(other);
return 0;}
connect连接看起来很复杂,主要包括下面几个逻辑。
1 创建一个表示和客户端通信的sock结构体,并分配一个skb表示连接请求。2 找到服务器对应的socket。
3 判断是否满足建立连接的条件,比如服务器连接队列是否已经满了。
4 客户端sock和服务器的互相关联起来。
5 把skb插入服务器连接队列等待处理。
6 通知服务器有连接到来。架构图如下
客户端连接成功后,服务器就可以调用accept处理该连接。
SYSCALL_DEFINE4(accept4, int, fd, struct sockaddr __user *, upeer_sockaddr,
int __user *, upeer_addrlen, int, flags){
return __sys_accept4(fd, upeer_sockaddr, upeer_addrlen, flags);}int __sys_accept4(int fd, struct sockaddr __user *upeer_sockaddr,
int __user *upeer_addrlen, int flags){
int ret = -EBADF;
struct fd f;
f = fdget(fd);
if (f.file) {
ret = __sys_accept4_file(f.file, 0, upeer_sockaddr,
upeer_addrlen, flags,
rlimit(RLIMIT_NOFILE));
fdput(f);
}
return ret;}
首先通过fd找到监听file。接着__sys_accept4_file
int __sys_accept4_file(struct file *file, unsigned file_flags,
struct sockaddr __user *upeer_sockaddr,
int __user *upeer_addrlen, int flags,
unsigned long nofile){
struct socket *sock, *newsock;
struct file *newfile;
int err, len, newfd;
// 找到监听socket
sock = sock_from_file(file, &err);
// 分配新的socket,表示和客户端通信
newsock = sock_alloc();
// 把某些字段赋值过来
newsock->type = sock->type;
newsock->ops = sock->ops;
// 获取新的fd
newfd = __get_unused_fd_flags(flags, nofile);
// 分配新的file(file和socket关联起来)
newfile = sock_alloc_file(newsock, flags, sock->sk->sk_prot_creator->name);
// 调用钩子函数
err = sock->ops->accept(sock, newsock, sock->file->f_flags | file_flags,
false);
// 关联fd和file
fd_install(newfd, newfile);
err = newfd;
return err;}
我们知道网络的实现(包括Unix域)要符合文件系统规范才能通过文件系统的API使用。上面代码就是为和客户端通信生成新的数据结构,包括fd、file、socket等,然后调用钩子函数accept。
static int unix_accept(struct socket *sock, struct socket *newsock, int flags,
bool kern){
struct sock *sk = sock->sk;
struct sock *tsk;
struct sk_buff *skb;
int err;
// 从连接队列摘取一个节点
skb = skb_recv_datagram(sk, 0, flags&O_NONBLOCK, &err);
// skb所属的sock
tsk = skb->sk;
// 处理了一个节点了,说明有空位了,唤醒可能因为连接队列满而阻塞的客户端进程
wake_up_interruptible(&unix_sk(sk)->peer_wait);
unix_state_lock(tsk);
newsock->state = SS_CONNECTED;
// 关联sock和socket
sock_graft(tsk, newsock);
unix_state_unlock(tsk);
return 0;}
unix_accept的逻辑就是从监听socket中取下一个节点。然后把该节点和socket结构体关联起来,具体就是sock和socket关联起来。最后返回一个新的fd给调用方。架构图如下。
后记:本文从客户端和服务器的角度分析了Unix域作为进程间通信方式是怎么实现的。相对TCP/IP,Unix域的实现相对简单。