Linux socket - 源碼分析(一)

這篇文章主要分析socket原理和創建流程
參考kernel msm-4.4源碼

進程和進程間通信

  進程(Process)是計算機中的程序關於某數據集合上的一次運行活動,是系統進行資源分配和調度的基本單位,是操作系統結構的基礎。不同進程間,內存資源相互獨立,無法直接獲取和修改,因此不同進程間需要通過特殊的方式進行傳遞信息。
  進程間通信(IPC,Interprocess communication)是一組編程接口,協調不同的進程,使之能在一個操作系統裏同時運行,這些接口給多個進程之間交換信息提供了可能。目前,比較常見的幾種跨進程通信方式有:管道(PIPE)、有名管道(FIFO)、消息隊列、信號量、共享內存、套接字(socket)等。

  • PIPE:一般指無名管道,只能用於有親緣關係的父子進程或者兄弟進程間的通信,半雙工,數據只能由一端流量另一端
  • FIFO:有名管道與無名管道不同,可以在無關的進程間通信,
  • 消息隊列:消息隊列由kernel維護的消息鏈表,一個消息隊列由一個標識符確定
  • 信號量:信號量是一個計數器,用於控制多個進程對資源的訪問,主要用於實現進程間互斥與同步,不能傳遞複雜消息
  • 共享內存:由一個進程創建,多個進程可以共享的內存段,需要結合信號量來同步對共享內存的訪問
  • socket:網絡上的兩個程序通過一個雙向的通信連接實現數據的交換,這個連接的一端稱爲一個socket。這是一種全雙工的通信方式,socket的兩端既可以讀,又可以寫。

虛擬文件系統(VFS)

  “Linux下,一切皆是文件“,socket當然也不例外,在系統啓動之初,Linux內核也會爲socket註冊和掛載自己類型的文件系統,爲後期socket的創建和使用打下基礎。因此,在介紹socket前,有必要簡單瞭解下Linux虛擬文件系統。
  
  虛擬文件系統(Virtual File System,VFS):爲了支持掛載不同類型的文件系統,並且爲用戶進程提供統一的文件操作接口,Linux內核在用戶進程和具體的文件系統之間引入了一個抽象層,這個抽象層就稱爲虛擬文件系統。VSF中四個代表核心概念的結構體:file,dentry,inode,super_block,接下來一一解析:

  • super_block: 對於已掛載的文件系統,VSF在內核中都會生成一個超級塊(super_block結構),用於存儲文件系統的控制信息,如文件系統類型,大小,所有inode對象等
  • inode:與文件一一對應,在文件系統中是文件的唯一標識,inode包含了文件信息的元數據(大小,權限,類型,時間)和指向文件數據的指針,但不包含文件名。
  • dentry:directory entry(目錄項)的縮寫,用於建立文件名和相關的inode之間的聯繫。在上一次掃描時,內核建立和緩存了目錄的樹狀結構,稱爲dentry cache。通過dentry cache找到file對應的inode,如果沒有找到,則需要從磁盤中讀取。這樣極大加快了查找inode的效率。
  • file:用戶進程相關,代表一個打開的文件。每個file結構體都有一個指向dentry的指針,通過dentry可以查找對應的inode。file和inode是多對一的關係,對於同一個文件,系統會爲每一次打開都創建一個file結構。

除了上述VSF的4個基本概念,我們也來了解下用戶程序操作文件時經常用到的”文件描述符”相關概念:

  • 文件描述符:在Linux中,進程通過文件描述符(file descriptor, fd )來訪問文件,fd其實是一個非負整數,是文件描述符表中的編號,通過fd可以在文件描述符表中查找到對應的file結構體。
  • 文件描述符表:每個進程都有自己的文件描述符表,用於記錄已經打開過的文件。表中的每一項都有一個指向file結構體的指針,用於查找打開文件的file結構體。

根據上面的描述,基本可以描述用戶程序操作一個文件的過程:用戶進程通過文件描述符在文件描述表中查找指向對應的file結構體指針,file結構體中包含了dentry指針,內核通過dentry查找到file對應的inode,inode中包含了指向super_block或者保存在disk上的實際文件數據的指針,進而找到實際的文件數據。

註明:對於VFS,這裏只是簡單描述,具體的原理可以參考如下博文:
https://blog.csdn.net/jnu_simba/article/details/8806654

socket文件系統註冊

  socket有自己的文件系統,在內核初始化並調用 do_initcalls 時將socket文件系統註冊到內核中。註冊過程(net/socket.c):core_initcall(sock_init)

/** core_initcall 宏定義(linux/init.h)*/
#define core_initcall(fn)       __define_initcall(fn, 1)

#define __define_initcall(fn, id) \
    static initcall_t __initcall_##fn##id __used \
    __attribute__((__section__(".initcall" #id ".init"))) = fn; \

  __define_initcall(fn, id) 指示編譯器在編譯時聲明函數指針__initcall_fn_id並初始化爲 fn,同時,將該函數指針變量放置到名爲 “.initcall#id.init” 的section數據段中。因此,對於core_initcall(sock_init) ,編譯器在編譯時會聲明__intcallsock_int1並初始化其指向函數指針sock_init,同時存放到.initcall1.init中。這樣,內核初始化時通過遍歷section拿到sock_init的函數指針地址,可以完成對socket文件系統的註冊。
  sock_init:創建,分配socket和inode所需的slab緩存,用於後期使用socket和inode;向內核註冊socket文件系統。

/** sock_init (net/socket.c)*/
static int __init sock_init(void)
{
    int err;
    //初始化network sysctl支持
    err = net_sysctl_init();
    if (err)
        goto out;
    //分配skbuff_head_cache和skbuff_fclone_cache slab高速緩存
    skb_init();
    //分配sock_inode_cache slab高速緩存
    init_inodecache();
    //註冊socket文件系統
    err = register_filesystem(&sock_fs_type);
    if (err)
        goto out_fs;    
    //掛載socket文件系統
    sock_mnt = kern_mount(&sock_fs_type);
    if (IS_ERR(sock_mnt)) {
        err = PTR_ERR(sock_mnt);
        goto out_mount;
    }
#ifdef CONFIG_NETFILTER
    //初始化netfilter
    err = netfilter_init();
    if (err)
        goto out;
    // ...
#endif

  skbuff_head_cache / skbuff_fclone_cache:與sk_buff相關的兩個後備高速緩存(looaside cache),協議棧中使用的所有sk_buff結構都是從這兩個高速緩存中分配出來的。兩者的不同在於前者指定的單位內存區域大小爲 sizeof(struct sk_buff);後者爲 sizeof(struct sk_buff_fclones),即一對sk_buff和一個引用計數,這一對sk_buff是克隆的,引用計數值爲0,1,2,表示這一對sk_buff中有幾個已被使用。
  sock_inode_cache:用於分配和釋放 socket_alloc 的高速緩存,socket創建和釋放inode都會在該緩存區操作。socket_alloc 結構體包含 socket 和 inode,將兩者緊密聯繫到一起。

/** sock_fs_type (net/socket.c)*/
static struct file_system_type sock_fs_type = {
    .name =     "sockfs",          // 文件系統名稱
    .mount =    sockfs_mount,      // 掛載sockfs函數,其中會創建super block
    .kill_sb =  kill_anon_super,   // 銷燬super block函數
};

  sock_fs_type:包含了文件系統系統的名稱sockfs,創建和銷燬super block的函數指針。

/** sockfs_mount (net/socket.c)*/
static struct dentry *sockfs_mount(struct file_system_type *fs_type,
             int flags, const char *dev_name, void *data)
{
    return mount_pseudo(fs_type, "socket:", &sockfs_ops,
        &sockfs_dentry_operations, SOCKFS_MAGIC);
}

  sockfs_mount:在文件系統掛載(kern_mount)時執行,創建一個super block(包含一個對應的dentry和inode),其根目錄爲”socket:”,inode操作函數爲 sockfs_ops

/** sockfs_ops (net/socket.c)*/
static const struct super_operations sockfs_ops = {
    .alloc_inode    = sock_alloc_inode,      // 分配inode
    .destroy_inode  = sock_destroy_inode,    // 釋放inode
    .statfs     = simple_statfs,             // 用於獲取sockfs文件系統的狀態信息
};

  sockfs_ops:定義inode操作函數,sock_alloc_inode 用於分配inode,在socket創建時使用;sock_destroy_inode 用於釋放inode,在socket銷燬時被調用。

socket創建過程
  
  在用戶進程中,socket(int domain, int type, int protocol) 函數用於創建socket並返回一個與socket關聯的fd,該函數實際執行的是系統調用 sys_socketcallsys_socketcall幾乎是用戶進程socket所有操作函數的入口:

/** sys_socketcall (linux/syscalls.h)*/
asmlinkage long sys_socketcall(int call, unsigned long __user *args);

  sys_socketcall 實際調用的是 SYSCALL_DEFINE2

/** SYSCALL_DEFINE2 (net/socket.c)*/
SYSCALL_DEFINE2(socketcall, int, call, unsigned long __user *, args)
{
    unsigned long a[AUDITSC_ARGS];
    unsigned long a0, a1;
    int err;
    unsigned int len;
    // 省略...
    a0 = a[0];
    a1 = a[1];

    switch (call) {
    case SYS_SOCKET:
        // 與 socket(int domain, int type, int protocol) 對應,創建socket
        err = sys_socket(a0, a1, a[2]);  
        break;
    case SYS_BIND:
        err = sys_bind(a0, (struct sockaddr __user *)a1, a[2]); 
        break;
    case SYS_CONNECT:
        err = sys_connect(a0, (struct sockaddr __user *)a1, a[2]);
        break;
// 省略...
}

  在 SYSCALL_DEFINE2 函數中,通過判斷call指令,來統一處理 socket 相關函數的事務,對於socket(…)函數,實際處理是在 sys_socket 中,也是一個系統調用,對應的是 SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol)

/** SYSCALL_DEFINE3 net/socket.c*/
SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol)
{
    int retval;
    struct socket *sock;
    int flags;
    // SOCK_TYPE_MASK: 0xF; SOCK_STREAM等socket類型位於type字段的低4位
    // 將flag設置爲除socket基本類型之外的值
    flags = type & ~SOCK_TYPE_MASK;

    // 如果flags中有除SOCK_CLOEXEC或者SOCK_NONBLOCK之外的其他參數,則返回EINVAL
    if (flags & ~(SOCK_CLOEXEC | SOCK_NONBLOCK))
        return -EINVAL;

    // 取type中的後4位,即sock_type,socket基本類型定義
    type &= SOCK_TYPE_MASK;

    // 如果設置了SOCK_NONBLOCK,則不論SOCK_NONBLOCK定義是否與O_NONBLOCK相同,
    // 均將flags中的SOCK_NONBLOCK復位,將O_NONBLOCK置位
    if (SOCK_NONBLOCK != O_NONBLOCK && (flags & SOCK_NONBLOCK))
        flags = (flags & ~SOCK_NONBLOCK) | O_NONBLOCK;

    // 創建socket結構,(重點分析)
    retval = sock_create(family, type, protocol, &sock);
    if (retval < 0)
        goto out;

    if (retval == 0)
        sockev_notify(SOCKEV_SOCKET, sock);

    // 將socket結構映射爲文件描述符retval並返回,(重點分析)
    retval = sock_map_fd(sock, flags & (O_CLOEXEC | O_NONBLOCK));
    if (retval < 0)
        goto out_release;
out:
    return retval;
out_release:
    sock_release(sock);
    return retval;
}

  SYSCALL_DEFINE3 中主要判斷了設置的socket類型type,如果設置了除基本sock_type,SOCK_CLOEXEC和SOCK_NONBLOCK之外的其他參數,則直接返回;同時調用 sock_create 創建 socket 結構,使用 sock_map_fd 將socket 結構映射爲文件描述符並返回。在分析 sock_create 之前,先看看socket結構體:

/** socket結構體 (linux/net.h)*/
struct socket {
    socket_state        state;       // 連接狀態:SS_CONNECTING, SS_CONNECTED 等
    short           type;            // 類型:SOCK_STREAM, SOCK_DGRAM 等
    unsigned long       flags;       // 標誌位:SOCK_ASYNC_NOSPACE(發送隊列是否已滿)等
    struct socket_wq __rcu  *wq;     // 等待隊列
    struct file     *file;           // 該socket結構體對應VFS中的file指針
    struct sock     *sk;             // socket網絡層表示,真正處理網絡協議的地方
    const struct proto_ops  *ops;    // socket操作函數集:bind, connect, accept 等
};

  socket結構體中定義了socket的基本狀態,類型,標誌,等待隊列,文件指針,操作函數集等,利用 sock 結構,將 socket 操作與真正處理網絡協議相關的事務分離。
  
  回到 sock_create 繼續看socket創建過程,sock_create 實際調用的是 __sock_create

/** __sock_create (net/socket.c)*/
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;

    // 檢查是否是支持的地址族,即檢查協議
    if (family < 0 || family >= NPROTO)
        return -EAFNOSUPPORT;
    // 檢查是否是支持的socket類型
    if (type < 0 || type >= SOCK_MAX)
        return -EINVAL;

    // 省略...

    // 檢查權限,並考慮協議集、類型、協議,以及 socket 是在內核中創建還是在用戶空間中創建
    // 可以參考:https://www.ibm.com/developerworks/cn/linux/l-selinux/
    err = security_socket_create(family, type, protocol, kern);
    if (err)
        return err;

    // 分配socket結構,這其中創建了socket和關聯的inode (重點分析)
    sock = sock_alloc();
    if (!sock) {
        net_warn_ratelimited("socket: no more sockets\n");
        return -ENFILE; /* Not exactly a match, but its the
                   closest posix thing */
    }
    sock->type = type;
    // 省略...
}

  __socket_create 檢查了地址族協議和socket類型,同時,調用 security_socket_create 檢查創建socket的權限(如:創建不同類型不同地址族socket的SELinux權限也會不同)。接着,來看看 sock_alloc

/** sock_alloc (net/socket.c)*/
static struct socket *sock_alloc(void)
{
    struct inode *inode;
    struct socket *sock;

    // 在已掛載的sockfs文件系統的super_block上分配一個inode
    inode = new_inode_pseudo(sock_mnt->mnt_sb);
    if (!inode)
        return NULL;

    // 獲取inode對應socket_alloc中的socket結構指針
    sock = SOCKET_I(inode);

    inode->i_ino = get_next_ino();
    inode->i_mode = S_IFSOCK | S_IRWXUGO;
    inode->i_uid = current_fsuid();
    inode->i_gid = current_fsgid();

    // 將inode的操作函數指針指向 sockfs_inode_ops 函數地址
    inode->i_op = &sockfs_inode_ops;

    this_cpu_add(sockets_in_use, 1);
    return sock;
}

  new_inode_pseudo 函數實際調用的是 alloc_inode(struct super_block *sb) 函數:

/** alloc_inode (fs/inode.c)*/
static struct inode *alloc_inode(struct super_block *sb)
{
    struct inode *inode;

    // 如果文件系統的超級塊已經指定了alloc_inode的函數,則調用已經定義的函數去分配inode
    // 對於sockfs,已經將alloc_inode指向sock_alloc_inode函數指針
    if (sb->s_op->alloc_inode)
        inode = sb->s_op->alloc_inode(sb);
    else
        // 否則在公用的 inode_cache slab緩存上分配inode
        inode = kmem_cache_alloc(inode_cachep, GFP_KERNEL);

    if (!inode)
        return NULL;

    // 編譯優化,提高執行效率,inode_init_always正常返回0
    if (unlikely(inode_init_always(sb, inode))) {
        if (inode->i_sb->s_op->destroy_inode)
            inode->i_sb->s_op->destroy_inode(inode);
        else
            kmem_cache_free(inode_cachep, inode);
        return NULL;
    }

    return inode;
}

  從前文 “socket文件系統註冊” 提到的:”.alloc_inode = sock_alloc_inode” 可知,alloc_inode 實際將使用 sock_alloc_inode 函數去分配 inode:

/** sock_alloc_inode (net/socket.c)*/
static struct inode *sock_alloc_inode(struct super_block *sb)
{
    // socket_alloc 結構體包含一個socket和一個inode,將兩者聯繫到一起
    struct socket_alloc *ei;
    struct socket_wq *wq;

    // 在sock_inode_cachep緩存上分配一個socket_alloc
    // sock_inode_cachep: 前文"socket文件系統註冊"中已經提到,專用於分配socket_alloc結構
    ei = kmem_cache_alloc(sock_inode_cachep, GFP_KERNEL);
    if (!ei)
        return NULL;
    // 分配socket等待隊列結構
    wq = kmalloc(sizeof(*wq), GFP_KERNEL);
    if (!wq) {
        kmem_cache_free(sock_inode_cachep, ei);
        return NULL;
    }
    // 初始化等待隊列
    init_waitqueue_head(&wq->wait);
    wq->fasync_list = NULL;
    wq->flags = 0;
    // 將socket_alloc中socket的等待隊列指向wq
    RCU_INIT_POINTER(ei->socket.wq, wq);

    // 初始化socket的狀態,標誌,操作集等
    ei->socket.state = SS_UNCONNECTED;
    ei->socket.flags = 0;
    ei->socket.ops = NULL;
    ei->socket.sk = NULL;
    ei->socket.file = NULL;

    // 返回socket_alloc中的inode
    return &ei->vfs_inode;
}

  sock_alloc_inode:在sockfs文件系統的 sock_inode_cachep SLAB緩存區分配一個 socket_alloc 結構,socket_alloc 中包含socket和inode,將兩者聯繫一起;分配和初始化socket_wq 等待隊列,初始化socket狀態/標誌/操作集;返回socket_alloc中的inode。

  socket_alloc:包含一個 socket 和一個 inode,該結構將這兩者聯繫在一起。前文介紹VFS時,我們知道通過文件描述可以找到內核中與文件對應的一個inode,而對於socket而言,通過文件描述符找到inode之後,也就能通過socket_alloc結構找到對應的socket了。

/** socket_alloc 結構體 (net/sock.h)*/
struct socket_alloc {
    struct socket socket;
    struct inode vfs_inode;
};

  回到 sock_alloc 函數中,通過調用 new_inode_pseudo 最終是在 super_block 上創建了一個 socket_alloc 結構,同時返回了該結構中的 inode。繼續分析 sock_alloc 函數:

/** sock_alloc函數 (net/socket.c)*/
static struct socket *sock_alloc(void)
{
    // 省略...
    // 獲取inode對應socket_alloc中的socket結構指針
    sock = SOCKET_I(inode);
    // 省略..
    // 將inode的操作函數指針指向 sockfs_inode_ops 函數地址
    inode->i_op = &sockfs_inode_ops;
    return sock;
}

  SOCKET_I:內聯函數,返回的是inode所在的socket_alloc中的socket結構體指針,慢慢分析:

/** SOCKET_I (net/sock.h)*/
static inline struct socket *SOCKET_I(struct inode *inode)
{
    // 使用container_of拿到inode對應的socket_alloc指針的首地址
    // 通過socket_alloc指針拿到inode對應的socket結構體指針
    return &container_of(inode, struct socket_alloc, vfs_inode)->socket;
}

  container_of:用於從包含在某個結構體中的指針獲取結構體本身的指針。在這裏就是指從inode指針獲取包含inode的socket_alloc結構體的指針:

/** container_of (linux/kernel.h)*/
#define container_of(ptr, type, member) ({          \
    // 定義一個與ptr相同的臨時指針變量__mptr
    const typeof(((type *)0)->member) * __mptr = (ptr); \ 
    // 將__mptr的指針地址減去member在type中的偏移
    // 得到的就是type的首地址
    (type *)((char *)__mptr - offsetof(type, member)); })

  總結一下sock_alloc函數:
  sock_alloc:在 sockfs 文件系統的 sock_inode_cachep 超級塊上分配一個 socket_alloc 結構,初始化該結構的 inode 和 socket,同時,返回該 socket_alloc 結構中的socket。
  
  至此,SYSCALL_DEFINE3 中對 sock_create 函數的調用分析完畢,通過該函數,分配和初始化了 socket 和與VFS相關的inode,並通過 socket_alloc 結構體將兩者關聯。接下來,繼續分析 SYSCALL_DEFINE3

/** SYSCALL_DEFINE3 net/socket.c*/

// 將socket結構映射爲文件描述符retval並返回,(重點分析)
retval = sock_map_fd(sock, flags & (O_CLOEXEC | O_NONBLOCK));
/** sock_map_fd (net/socket.c)*/
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,並將file和inode以及socket關聯
    // file的private_data指針指向該socket
    newfile = sock_alloc_file(sock, flags, NULL);
    if (likely(!IS_ERR(newfile))) {
        // 將file指針存放到該進程已打開的文件列表中,其索引爲fd
        fd_install(fd, newfile);
        return fd;
    }
    // 省略...
}

  sock_map_fd:從該進程的文件描述符表中分配一個空閒的文件描述符;創建一個新的文件,並將該文件與socket 互相綁定在一起;將創建的新文件指針存放到該進程的已打開文件列表中,其索引爲剛分配的fd。

/** sock_alloc_file (net/socket.c)*/
struct file *sock_alloc_file(struct socket *sock, int flags, const char *dname)
{
    struct qstr name = { .name = "" };
    struct path path;
    struct file *file;

    // 省略..

    // 初始化文件路徑path,其目錄項的父目錄項爲超級塊對應的根目錄
    path.dentry = d_alloc_pseudo(sock_mnt->mnt_sb, &name);
    if (unlikely(!path.dentry))
        return ERR_PTR(-ENOMEM);
    // 設置path的裝載點爲sock_mnt
    path.mnt = mntget(sock_mnt);

    // 將socket對應的inode設置到path目錄項dentry的d_inode中
    // SOCK_INODE 與 SOCKET_I 原理一致,這裏用於獲取sock在socket_alloc結構中對應的inode
    d_instantiate(path.dentry, SOCK_INODE(sock));

    // 分配file結構並初始化,file的f_path爲path,file的f_inode爲path->dentry的d_inode
    // 設置file的操作集爲socket_file_ops
    file = alloc_file(&path, FMODE_READ | FMODE_WRITE,
          &socket_file_ops);
    // 省略..

    // 關聯socket和新創建的file
    sock->file = file;
    file->f_flags = O_RDWR | (flags & O_NONBLOCK);
    file->private_data = sock;
    return file;
}

  sock_alloc_file:新創建一個文件;初始化該文件對應的 inode 爲 socket 在 socket_alloc 結構中的inode;初始化該文件的操作集爲 socket_file_ops;將 socket 的 file 指針指向新創建的文件指針;將 socket 保存到新創建 file 的 private_data中。該函數完成了 socket 和文件的綁定。

/** __fd_install (fs/file.c)*/
void __fd_install(struct files_struct *files, unsigned int fd,
        struct file *file)
{
    struct fdtable *fdt;

    // 省略...

    // 從file_struct中通過RCU取出其中的fdt
    // fdt: 文件描述符表
    fdt = rcu_dereference_sched(files->fdt)
    BUG_ON(fdt->fd[fd] != NULL);
    // 將fd數組下標爲fd的元素的指針指向file
    rcu_assign_pointer(fdt->fd[fd], file);
}

files_struct:該進程所有已打開文件表結構。進程描述符數組中存放了一個進程所訪問的所有文件,把這個文件描述符數組和這個數組在系統中的一些動態信息組合到一起,就形成了一個新的數據結構——進程打開文件表。
__fd_install:將新創建的file指針存放在該進程已打開的文件列表中,索引爲fd,這樣進程就可以通過fd找到對應的file指針,inode和socket。

文章結語

  本文介紹了socket文件系統和socket的創建過程,同時結合VFS的概念,理解socket和文件系統的關聯性,以及用戶進程如何通過file descriptor找到對應的socket。由於到此篇幅已經較長,這裏沒有對socket其他操作函數作繼續分析,有時間會作進一步的詳解。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章