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。
原文地址:https://www.cnblogs.com/biterror/p/6909860.html