前往小程序,Get更优阅读体验!
立即前往
发布
社区首页 >专栏 >从内核看Unix域的实现(基于5.9.9)

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

作者头像
theanarkh
发布2021-07-08 16:05:25
发布2021-07-08 16:05:25
67300
代码可运行
举报
文章被收录于专栏:原创分享原创分享
运行总次数:0
代码可运行

前言:Unix域是进程间通信的一种方式,他的特点是可以传递文件描述符,在内核中,Unix域是网络的一部分,使用上也遵循网络编程的API。本文分析Unix域的实现。

我们首先看看Unix域的使用。

代码语言:javascript
代码运行次数:0
复制
#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。

代码语言:javascript
代码运行次数:0
复制
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。

代码语言:javascript
代码运行次数:0
复制
static const struct net_proto_family unix_family_ops = {
    .family = PF_UNIX,
    .create = unix_create,
    .owner  = THIS_MODULE,};

我们看到Unix域的协议簇名字是PF_UNIX(和AF_UNIX一样)。所以当我们调用

代码语言:javascript
代码运行次数:0
复制
socket(AF_UNIX, xxxx);

就会进入Unix域的逻辑。下面我们从socket函数开始分析。

1 socket

代码语言:javascript
代码运行次数:0
复制
// 创建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。

代码语言:javascript
代码运行次数:0
复制
// 创建一个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。

代码语言:javascript
代码运行次数:0
复制
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。

代码语言:javascript
代码运行次数:0
复制
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

代码语言:javascript
代码运行次数:0
复制
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才能接收连接。

代码语言:javascript
代码运行次数:0
复制
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进行连接。

代码语言:javascript
代码运行次数:0
复制
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 通知服务器有连接到来。架构图如下

5 accept

客户端连接成功后,服务器就可以调用accept处理该连接。

代码语言:javascript
代码运行次数:0
复制
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

代码语言:javascript
代码运行次数:0
复制
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。

代码语言:javascript
代码运行次数:0
复制
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域的实现相对简单。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-06-18,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 编程杂技 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1 socket
  • 2 bind
  • 3 listen
  • 4 connect
  • 5 accept
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档