• Linux网络子系统


    再Linux的世界里,万物皆文件,通过虚拟文件系统VFS,程序可以用标准的Linux系统调用对不同的文件系统,甚至不同介质上的文件系统进行读写操作。下面我们揭示Linux网络子系统的秘密

    sockfs

    在Linux上,和读写文件保持同一套接口是通过套接口伪文件系统sockfs来实现的。

    sockfs实现了VFS中的4种主要对象:超级块super block、索引节点inode、目录项对象dentry和文件对象file,当执行文件IO系统调用时,VFS就将请求转发给sockfs,而sockfs就调用特定的协议实现,层次结构如下图:

    image

    sockfs的装载

    首先初始化:

    static int __init sock_init(void)
    {
        //创建inode缓存
        init_inodecache();
        //创建socket的file_system
        register_filesystem(&sock_fs_type);
        //装载套接字文件系统
        sock_mnt = kern_mount(&sock_fs_type);
    }
    

    Socket创建

    系统调用socket、accept和socketpair(域套接字)是用户空间创建socket的几种方法,其核心调用链如下图:

    image

    1. 先构造inode
    2. 再构造对应的file
    3. 最后安装file到当前进程中(即关联映射到一个未用的文件描述符)

    这里很有意思,我们可以分析一下源码

    构造inode

    static struct socket *sock_alloc(void)
    {
        struct inode *inode;
        struct socket *sock;
    
        inode = new_inode(sock_mnt->mnt_sb);
            
        sock = SOCKET_I(inode);
                
        inode->i_mode = S_IFSOCK | S_IRWXUGO;
        inode->i_uid = current_fsuid();
        inode->i_gid = current_fsgid();
        return sock;
    }
    

    构造file

    有了inode对象后,接下来就要构造对应的file对象了,然后file对象和sock对象关联起来

    安装file

    void fd_install(unsigned int fd, struct file *file)
    {
        struct files_struct *files = current->files;
        struct fdtable *fdt;
        spin_lock(&files->file_lock);
        fdt = files_fdtable(files);
        BUG_ON(fdt->fd[fd] != NULL);
        rcu_assign_pointer(fdt->fd[fd], file);
        spin_unlock(&files->file_lock);
    }
    

    fd和file分别为上一过程返回的空闲文件描述符和文件对象,使RCU(Read-Copy Update)技术来设置file到当前进程的fd数组中。

    image

    socket操作

    读套接字有两种实现,read和recv实现不同。
    image
    read的实现,调用的是vfs_readv,后面的过程和sys_read相同

    image
    recv的实现没有经过vfs,而是先调用sock_lookup_light从fd得到socket,然后后面的流程和read一样

    Socket销毁

    系统调用close是用户空间销毁socket的唯一方法
    image
    filp_close先递减引用计数,若为0则调用__fput释放file。

    我们关闭一条TCP连接,还可以调用shutdown。该函数有三种关闭方式:单独关闭读(写)、同时关闭读写。shutdown处理过程调用序列见。shutdown不管引用计数,会直接关闭(不是析构)套接口。

    Linux 网络协议栈

    明白了上面的sockfs后,上层应用开发似乎已经完全足够了。下面我们以TCP和UDP为例子,继续深入一点点,去探究一下Linux内核的网络协议栈

    image

    Linux sk_buff struct 数据结构和队列

    sk_buffer

    • sk_buffer是Linux内核网络栈(L2到L4)处理网络包(packets)所使用的buffer。一个skb 表示 Linux 网络栈中的一个packet

    socket与inode绑定,对于不同的协议,Linux又抽象出了不同的struct sock

    struct sock 有三个 skb 队列(sk_buffer queue),分别是receive_queue , write_queue 和 error_queue(好像没什么用)。在 sock 结构被初始化的时候,这些缓冲队列也被初始化完成;在收据收发过程中,每个 queue 中保存要发送或者接受的每个 packet 对应的 Linux 网络栈 sk_buffer 数据结构的实例 skb
    image

    skb 的操作示例

    TCP操作:

    image

    协议栈发送

    应用层

    _sock_sendmsg 被调用,根据 socket 的协议类型,调用相应协议的发送函数。

    1. 对于 TCP ,调用 tcp_sendmsg 函数
    2. 对于 UDP来说,调用udp_sendmsg函数。

    传输层

    TCP 栈简要过程

    1. tcp_sendmsg 函数会首先检查已经建立的 TCP connection 的状态,然后获取该连接的 MSS,开始 segement 发送流程
    2. 构造 TCP 段的 playload:它在内核空间中创建该 packet 的 sk_buffer 数据结构的实例 skb,从 userspace buffer 中拷贝 packet 的数据到 skb 的 buffer
    3. 构造 TCP header
    4. 计算 TCP 校验和(checksum)和 顺序号 (sequence number)
      1. TCP 校验和是一个端到端的校验和,由发送端计算,然后由接收端验证。
    5. 发到 IP 层处理:调用 IP handler 句柄 ip_queue_xmit,将 skb 传入 IP 处理流程。

    UDP 栈简要过程

    1. UDP 将 message 封装成 UDP 数据报
    2. 调用 ip_append_data() 方法将 packet 送到 IP 层进行处理。

    IP 网络层

    网络层任务:

    1. 路由处理,即选择下一跳
    2. 添加 IP header
    3. 计算 IP header checksum,用于检测 IP 报文头部在传播过程中是否出错
    4. 可能的话,进行 IP 分片
    5. 处理完毕,获取下一跳的 MAC 地址,设置链路层报文头,然后转入链路层处理。

    数据链路层

    数据链路层在不可靠的物理介质上提供可靠的传输。该层的作用包括:物理地址寻址、数据的成帧、流量控制、数据的检错、重发等。

    物理层

    一旦网卡完成报文发送,将产生中断通知CPU,然后驱动层中的中断处理程序就可以删除保存的 skb(TCP得收到ACK)

    报文发送过程简要总结

    image

    协议栈接收

    物理层和数据链路层

    我们没必要了解那么多吧,简要描述:

    1. 网卡收到一个package,通过网卡中断创建一个sk_buff。
    2. 发出软中断(NET_RX_SOFTIRQ),通知内核处理。以后就可以愉快地把sk_buff交给网络层了。

    网络层

    1. 校验,合包
    2. 转发或者递交给上层

    传输层 TCP

    1. 它会做 TCP header 检查等处理
    2. 调用 _tcp_v4_lookup,查找该 package 的 open socket。如果找不到,该 package 会被丢弃。接下来检查 socket 和 connection 的状态。
    3. 如果socket 和 connection 一切正常,调用 tcp_prequeue 使 package 从内核进入 user space,放进 socket 的 receive queue(struct sk_buff队列)。然后 socket 会被唤醒,调用 system call,并最终调用 tcp_recvmsg 函数去从 socket recieve queue 中获取 segment。

    报文接收过程简单总结

    image

    关于TCP发送/接收缓冲区

    从struct sock中摘抄一点内容来解释发送/接收缓冲区

    struct sock {
    
      volatile unsigned long   wmem_alloc;/*当前写缓冲区大小,该值不可大于系统规定的最大值*/
    
      volatile unsigned long   rmem_alloc;/*当前读缓冲区大小,该值不可大于系统规定最大值*/
    
      struct sk_buff      * volatile send_head;
    
      struct sk_buff      * volatile send_tail;
    
    /* send_head, send_tail 用于 TCP协议重发队列。*/
    
      struct sk_buff      *partial;/*创建最大长度的待发送数据包。*/
    
    /*
    write_queue 指向待发送数据包,其与 send_head,send_tail
    队列的不同之处在于send_head,send_tail
    队列中数据包均已经发送出去,但尚未接收到应答。而 write_queue
    中数据包尚未发送。 receive-queue为读队列,其不同于 back_log 队列之处在于
    back_log 队列缓存从网络层传 上来的数据包,在用户进行读取操作时,不可操作
    back_log 队列,而是从 receive_queue
    队列中去数据包读取其中的数据,即数据包首先缓存在 back_log 队列中,然后从
    back_log 队列中移动到
    receive_queue队列中方可被应用程序读取。而并非所有back_log 队列中缓
    存的数据包都可以成功的被移动到
    receive_queue队列中,如果此刻读缓存区太小,则当 前从back_log
    队列中被取下的被处理的数据包将被直接丢弃,而不会被缓存到receive_queue
    队列中。如果从应答的角度看,在back_log队列中的数据包由于有可能被
    丢弃,故尚未应答,而将一个数据包从 back_log 移动到
    receive_queue时,表示该数据包
    已被正式接收,即会发送对该数据包的应答给远端表示本地已经成功接收该数据包。 */
    
      struct sk_buff_head       write_queue,
    
                          receive_queue;
    
        int rcvbuf; // 接受缓冲区的大小(按字节) 
        int sndbuf; // 发送缓冲区的大小(按字节) 
        atomic_t rmem_alloc; // 接受队列中存放的数据的字节数 
        atomic_t wmem_alloc; // 发送队列中存放的数据的字节数 
        int wmem_queued; // 所有已经发送的数据的总字节数 
        int forward_alloc; // 预分配剩余字节数 
    
      struct socket             *socket;/*对应的socket结构体*/
    
    };
    

    可以看出,sock结构里面并没有什么发送/接收缓冲区,只有由struct sk_buff构成的接收/发送队列。

    sock的收发都是要占用内存的,即发送缓冲区和接收缓冲区。 系统对这些内存的使用是有限制的。 通常,每个sock都会从配额里
    预先分配一些,这就是forward_alloc, 具体分配时:

    1. 比如收到一个skb,则要计算到rmem_alloc中,并从forward_alloc中扣除。接收处理完成后(如用户态读取),则释放skb,并利用tcp_rfree()把该skb的内存反还给forward_alloc。
    2. 发送一个skb,也要暂时放到发送缓冲区,这也要计算到wmem_queued中,并从forward_alloc中扣除。真正发送完成后,也释放
      skb,并反还forward_alloc。
  • 相关阅读:
    列表基本操作——1
    条件判断与嵌套
    数据拼接与数据转换
    变量与赋值
    打印数与type()函数
    print()函数与打印字符串
    arduino开发ESP8266学习笔记二----按键控制LED灯
    arduino开发ESP8266学习笔记一 ----点亮一个LED灯
    无线充电
    EMC设计总结
  • 原文地址:https://www.cnblogs.com/biterror/p/6909860.html
Copyright © 2020-2023  润新知