从内核看Unix域的实现(基于5.9.9)

前言: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函数开始分析。

1 socket

// 创建socket,返回fd
int __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。

// 创建一个socket
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;
	// 分配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域)对应的实现不一样,后续再单独写文章介绍。

2 bind

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中。更多内容可以参考文档

3 listen

接下来我们看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才能处理”连接“。

4 connect

服务器启动后,客户端就可以通过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;
	// 绑定到随机的路径
	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 通知服务器有连接到来。
架构图如下

5 accept

客户端连接成功后,服务器就可以调用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域的实现相对简单。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值