Linux网络子系统
再Linux的世界里,万物皆文件,通过虚拟文件系统VFS,程序可以用标准的Linux系统调用对不同的文件系统,甚至不同介质上的文件系统进行读写操作。下面我们揭示Linux网络子系统的秘密
sockfs
在Linux上,和读写文件保持同一套接口是通过套接口伪文件系统sockfs来实现的。
sockfs实现了VFS中的4种主要对象:超级块super block、索引节点inode、目录项对象dentry和文件对象file,当执行文件IO系统调用时,VFS就将请求转发给sockfs,而sockfs就调用特定的协议实现,层次结构如下图:
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的几种方法,其核心调用链如下图:
- 先构造inode
- 再构造对应的file
- 最后安装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数组中。
socket操作
读套接字有两种实现,read和recv实现不同。
read的实现,调用的是vfs_readv,后面的过程和sys_read相同
recv的实现没有经过vfs,而是先调用sock_lookup_light从fd得到socket,然后后面的流程和read一样
Socket销毁
系统调用close是用户空间销毁socket的唯一方法
filp_close先递减引用计数,若为0则调用__fput释放file。
我们关闭一条TCP连接,还可以调用shutdown。该函数有三种关闭方式:单独关闭读(写)、同时关闭读写。shutdown处理过程调用序列见。shutdown不管引用计数,会直接关闭(不是析构)套接口。
Linux 网络协议栈
明白了上面的sockfs后,上层应用开发似乎已经完全足够了。下面我们以TCP和UDP为例子,继续深入一点点,去探究一下Linux内核的网络协议栈
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
skb 的操作示例
TCP操作:
协议栈发送
应用层
_sock_sendmsg 被调用,根据 socket 的协议类型,调用相应协议的发送函数。
- 对于 TCP ,调用 tcp_sendmsg 函数
- 对于 UDP来说,调用udp_sendmsg函数。
传输层
TCP 栈简要过程
- tcp_sendmsg 函数会首先检查已经建立的 TCP connection 的状态,然后获取该连接的 MSS,开始 segement 发送流程
- 构造 TCP 段的 playload:它在内核空间中创建该 packet 的 sk_buffer 数据结构的实例 skb,从 userspace buffer 中拷贝 packet 的数据到 skb 的 buffer
- 构造 TCP header
- 计算 TCP 校验和(checksum)和 顺序号 (sequence number)
- TCP 校验和是一个端到端的校验和,由发送端计算,然后由接收端验证。
- 发到 IP 层处理:调用 IP handler 句柄 ip_queue_xmit,将 skb 传入 IP 处理流程。
UDP 栈简要过程
- UDP 将 message 封装成 UDP 数据报
- 调用 ip_append_data() 方法将 packet 送到 IP 层进行处理。
IP 网络层
网络层任务:
- 路由处理,即选择下一跳
- 添加 IP header
- 计算 IP header checksum,用于检测 IP 报文头部在传播过程中是否出错
- 可能的话,进行 IP 分片
- 处理完毕,获取下一跳的 MAC 地址,设置链路层报文头,然后转入链路层处理。
数据链路层
数据链路层在不可靠的物理介质上提供可靠的传输。该层的作用包括:物理地址寻址、数据的成帧、流量控制、数据的检错、重发等。
物理层
一旦网卡完成报文发送,将产生中断通知CPU,然后驱动层中的中断处理程序就可以删除保存的 skb(TCP得收到ACK)
报文发送过程简要总结
协议栈接收
物理层和数据链路层
我们没必要了解那么多吧,简要描述:
- 网卡收到一个package,通过网卡中断创建一个sk_buff。
- 发出软中断(NET_RX_SOFTIRQ),通知内核处理。以后就可以愉快地把sk_buff交给网络层了。
网络层
- 校验,合包
- 转发或者递交给上层
传输层 TCP
- 它会做 TCP header 检查等处理
- 调用 _tcp_v4_lookup,查找该 package 的 open socket。如果找不到,该 package 会被丢弃。接下来检查 socket 和 connection 的状态。
- 如果socket 和 connection 一切正常,调用 tcp_prequeue 使 package 从内核进入 user space,放进 socket 的 receive queue(struct sk_buff队列)。然后 socket 会被唤醒,调用 system call,并最终调用 tcp_recvmsg 函数去从 socket recieve queue 中获取 segment。
报文接收过程简单总结
关于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, 具体分配时:
- 比如收到一个skb,则要计算到rmem_alloc中,并从forward_alloc中扣除。接收处理完成后(如用户态读取),则释放skb,并利用tcp_rfree()把该skb的内存反还给forward_alloc。
- 发送一个skb,也要暂时放到发送缓冲区,这也要计算到wmem_queued中,并从forward_alloc中扣除。真正发送完成后,也释放
skb,并反还forward_alloc。