再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的几种方法,其核心调用链如下图:

  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数组中。

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 的协议类型,调用相应协议的发送函数。

  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)

报文发送过程简要总结

协议栈接收

物理层和数据链路层

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

  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。

报文接收过程简单总结

关于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。

Linux网络子系统的更多相关文章

  1. Linux 网络子系统

    今天记录一下Linux网络子系统相关的东西. 因为感觉对这一块还是有一个很大的空白,这件事情太可怕了. 摘抄多份博客进行总结一下Linux网络子系统的相关东西. 一. Linux网络子系统体系结构 L ...

  2. Linux 网络子系统之网络协议接口层(一)

    Linux 网络设备驱动之网络协议接口层介绍. 网络协议接口层最主要的功能是给上层协议提供透明的数据包发送和接收接口. 当上层ARP或IP需要发送数据包时,它将调用网络协议接口层的dev_queue_ ...

  3. linux网络子系统内核分析

    1.选择路由 若要将数据包发至PC2,则linux系统通过查询路由表可知168.1.1.10(目的地址)的网关地址为192.168.1.1,此时linux系统选择网卡1发送数据包. 2.邻居子系统(通 ...

  4. Linux 网络子系统之网络协议接口层(二)

    这一篇主要围绕网络协议接口层的发送函数的解析 int dev_queue_xmit(struct sk_buff *skb) 函数解析 声明: /* include/linux/netdevice.h ...

  5. Linux 网络子系统之结构介绍

    Linux 网络设备驱动程序的体系结构 图片说明如下: 网络协议接口层 网络协议接口层向网络层协议提供统一的数据包收发接口,不论上层协议是ARP还是IP,都通过 dev_queue_xmit() 函数 ...

  6. Linux网络子系统之---- PHY 配置

    MII即媒体独立接口,也叫介质无关接口. 它包括一个数据接口,以及一个MAC和PHY之间的管理接口(图1). 数据接口包括分别用于发送器和接收器的两条独立信道.每条信道都有自己的数据.时钟和控制信号. ...

  7. Linux 网络子系统之NAPI书签

    只是一个书签 http://blog.csdn.net/ustc_dylan/article/details/6116334

  8. linux网络子系统调优

  9. Linux内核笔记--网络子系统初探

    内核版本:linux-2.6.11 本文对Linux网络子系统的收发包的流程进行一个大致梳理,以流水账的形式记录从应用层write一个socket开始到这些数据被应用层read出来的这个过程中linu ...

随机推荐

  1. Spring cache 缓存

    概述 Spring 3.1 引入了激动人心的基于注释(annotation)的缓存(cache)技术,它本质上不是一个具体的缓存实现方案(例如 EHCache 或者 OSCache),而是一个对缓存使 ...

  2. ChicagoBoss简介

    翻译人:李罗琦 (ChicagoBoss)[http://www.chicagoboss.org] 由 Evan Miller创作,使用erlang/OTP的开发者们可以通过它提供的一个类似于Ruby ...

  3. linux系统编程之文件与IO(四):目录访问相关系统调用

    1. 目录操作相关的系统调用     1.1 mkdir和rmdir系统调用     1.1.1 实例     1.2 chdir, getcwd系统调用     1.2.1 实例     1.3 o ...

  4. C#跨线程操作UI

    WPF启动调度器 : Dispatcher.Invoke(new Action(() => { //你的代码 }));

  5. Android 导入 aar 库文件

    1. 在需要导入 aar 的 module 目录下创建一个名叫 "aars" 的目录,并把 aar 文件复制到这里. 2. 在项目的 build.gradle 文件里添加 allp ...

  6. web请求的状态码

    摘录于  https://www.cnblogs.com/lovychen/p/6256343.html 1xx消息 这一类型的状态码,代表请求已被接受,需要继续处理.这类响应是临时响应,只包含状态行 ...

  7. NRF52840相对于之前的NRF52系列、NRF51系列增加了什么功能

    现在广大客户的蓝牙采用NORDIC越来越多了,NORDIC一直在不断进行技术改进更好的满足市场需求 推出了新款NRF52840.NRF52840更为先进些,支持的功能也多点,比如IEEE802.15. ...

  8. Xcode的多种Build Configuration

    一: 建多个Configuration的目的. 多套域名打包. 1 开发时的域名. 2 内测时的域名. 3 公测时的域名. 4 企业版的域名. 5 APP Store的域名. 通过注释的方式,容易出错 ...

  9. Design-341. Flatten Nested List Iterator

    Given a nested list of integers, implement an iterator to flatten it. Each element is either an inte ...

  10. JS获取开始、结束时间

    /** * 获取本周.本季度.本月.上月的开始日期.结束日期 */ var now = new Date(); //当前日期 var nowDayOfWeek = now.getDay(); //今天 ...