TCP/IP协议栈在Linux内核中的运行时序分析
TCP/IP协议栈在Linux内核中的运行时序分析
Linux内核任务调度基础
内核初始化
内核的启动时是从入口函数start_kernel开始,相当于内核的main函数
start_kernel的代码如下所示,在该函数中有各种各样的初始化函数
// init/main.c
asmlinkage __visible void __init start_kernel(void)
{
// 0号进程(创始进程)
set_task_stack_end_magic(&init_task);
// 设置中断门(系统调用是通过中断的方式触发)
trap_init();
// 初始化内存管理模块
mm_init();
// 初始化调度模块
sched_init();
// 初始化基于内存的文件系统rootfs
vfs_caches_init();
// 创建1号进程(用户态总管)和2号进程(内核态总管)
arch_call_rest_init();
...
}
中断门
在内核初始化的时候,trap_init里面设置了很多的中断门(Interrupt Gate), 用于处理各种中断,同时系统调用也是通过发送中断的方式进行的
rest_init
在start_kernel中最后调用的是rest_init,用来进行其他方面的初始化,代码如下所示:
// init/main.c
noinline void __ref rest_init(void)
{
// 初始化1号进程,用户态总管,systemd
pid = kernel_thread(kernel_init, NULL, CLONE_FS);
...
// 初始化2号进程,内核态总管,kthreadd
pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);
}
1号进程
通过kernel_thread(kernel_init, NULL, CLONE_FS)创建的进程即为1号进程,该进程对于操作系统来说极其重要,这是因为1号进程将运行一个用户进程,该进程是操作系统中所有其他用户进程的祖先进程,Linux中所有的进程都是由init进程创建并运行的。首先Linux内核启动,然后再用户空间中启动init进程,再启动其他系统进程。在系统启动完成完成后,init将变为守护进程监视系统其他进程。
2号进程
通过kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES)创建的进程,即2号进程,它的任务就是管理和调度其他内核线程kernel_thread, 会循环执行一个kthreadd的函数,该函数的作用就是运行kthread_create_list全局链表中维护的kthread, 当我们调用kernel_thread创建的内核线程会被加入到此链表中,因此所有的内核线程都是直接或者间接的以kthreadd为父进程。
用户态和内核态的划分
在原来只有0号进程的时候,操作系统所有的资源都可以使用,是没有竞争关系的,也无需担心被恶意破坏,有了1号进程之后,需要区分核心资源和非核心资源,x86提供了分层的权限机制,将操作系统中的资源分为了4个Ring,如下图所示
越靠近内核区域,需要的权限也就越高,操作系统很好地利用了x86的分层权限机制:
- 将可以访问的关键资源的代码放在Ring0,将其称为内核态(Kernel Mode)
- 将普通的程序代码放在Ring3,将其称为用户态(User Mode)
如果用户态的代码需要访问核心资源,需要通过系统调用来进行访问
系统调用
操作系统中的状态切换如下图所示:
在x86系统中,软件中断是由int $0x80指令产生,Linux中每个系统调用都有相应的系统调用号作为唯一的标识,内核维护一张系统调用表,sys_call_table,表中的元素是系统调用函数的起始地址,而系统调用号就是系统调用在调用表的偏移量。在x86上,系统调用号是通过eax寄存器传递给内核的。
用户空间的程序无法直接执行内核代码,应用程序应该以某种方式通知系统,告诉内核自己需要执行一个系统调用,希望系统切换到内核态,这样内核就可以代表应用程序来执行该系统调用了。
通知内核的机制是靠软件中断实现的。首先,用户程序为系统调用设置参数。其中一个参数是系统调用编号。参数设置完成后,程序执行“系统调用”指令。x86系统上的软中断由int产生。这个指令会导致一个异常:产生一个事件,这个事件会致使处理器切换到内核态并跳转到一个新的地址,并开始执行那里的异常处理程序。此时的异常处理程序实际上就是系统调用处理程序。它与硬件体系结构紧密相关。
新地址的指令会保存程序的状态,计算出应该调用哪个系统调用,调用内核中实现那个系统调用的函数,恢复用户程序状态,然后将控制权返还给用户程序。系统调用是设备驱动程序中定义的函数最终被调用的一种方式。
延后中断
中断处理会有一些特点,其中最主要的两个是:
- 中断处理必须快速执行完毕
- 有时中断处理必须做很多冗长的事情
因此中断被切分为两部分:
- 前半部
- 后半部
中断处理代码运行于中断处理上下文中,此时禁止响应后续的中断,所以要避免中断处理代码长时间执行。但有些中断却又需要执行很多工作,所以中断处理有时会被分为两部分。第一部分中,中断处理先只做尽量少的重要工作,接下来提交第二部分给内核调度,然后就结束运行。当系统比较空闲并且处理器上下文允许处理中断时,第二部分被延后的剩余任务就会开始执行。
目前实现延后中断有如下三种途径:
- 软中断(softirq)
- tasklets
- 工作队列(wq)
Softirq
伴随着内核对并行处理的支持,出于性能考虑,所有新的下半部实现方案都基于被称之为 ksoftirqd。每个处理器都有自己的内核线程,名字叫做 ksoftirqd/n,n是处理器的编号,由 spawn_ksoftirqd 函数启动这些线程。
软中断在 Linux 内核编译时就静态地确定了。open_softirq 函数负责 softirq 初始化,它的定义如下所示:
void open_softirq(int nr, void (*action)(struct softirq_action *))
{
softirq_vec[nr].action = action;
}
这个函数有两个参数:
softirq_vec数组的索引序号- 一个指向软中断处理函数的指针
softirq_vec 数组包含了 NR_SOFTIRQS (其值为10)个不同 softirq 类型的 softirq_action。当前版本的 Linux 内核定义了十种软中断向量。其中两个 tasklet 相关,两个网络相关,两个块处理相关,两个定时器相关,另外调度器和 RCU 也各占一个。所有这些都在一个枚举中定义:
enum
{
HI_SOFTIRQ=0,
TIMER_SOFTIRQ,
NET_TX_SOFTIRQ,
NET_RX_SOFTIRQ,
BLOCK_SOFTIRQ,
BLOCK_IOPOLL_SOFTIRQ,
TASKLET_SOFTIRQ,
SCHED_SOFTIRQ,
HRTIMER_SOFTIRQ,
RCU_SOFTIRQ,
NR_SOFTIRQS
};
可以看到 softirq_vec 数组的类型为 softirq_action。这是软中断机制里一个重要的数据结构,它只有一个指向中断处理函数的成员:
struct softirq_action
{
void (*action)(struct softirq_action *);
};
open_softirq 函数实际上用 softirq_action 参数填充了 softirq_vec 数组。由 open_softirq 注册的延后中断处理函数会由 raise_softirq 调用。这个函数只有一个参数 -- 软中断序号 nr。下面是该函数的代码:
void raise_softirq(unsigned int nr)
{
unsigned long flags;
local_irq_save(flags);
raise_softirq_irqoff(nr);
local_irq_restore(flags);
}
raise_softirq_irqoff 函数设置当前处理器上和nr参数对应的软中断标志位(__softirq_pending)。这是通过以下代码做到的:
__raise_softirq_irqoff(nr);
然后,通过 in_interrupt 函数获得 irq_count 值,该函数可以用来检测一个cpu是否处于中断环境,如果处于中断上下文中,就退出 raise_softirq_irqoff 函数,装回 IF 标志位并允许当前处理器的中断。如果不在中断上下文中,就会调用 wakeup_softirqd 函数,wakeup_softirqd 函数会激活当前处理器上的 ksoftirqd 内核线程,其代码如下所示:
static void wakeup_softirqd(void)
{
struct task_struct *tsk = __this_cpu_read(ksoftirqd);
if (tsk && tsk->state != TASK_RUNNING)
wake_up_process(tsk);
}
ksoftirqd 内核线程都运行 run_ksoftirqd 函数来检测是否有延后中断需要处理,如果有的话就会调用 __do_softirq 函数。__do_softirq 读取当前处理器对应的 __softirq_pending 软中断标记,并调用所有已被标记中断对应的处理函数。
每个 softirq 都有如下的阶段:通过 open_softirq 函数注册一个软中断,通过 raise_softirq 函数标记一个软中断来激活它,然后所有被标记的软中断将会在 Linux 内核下一次执行周期性软中断检测时得以调度,对应此类型软中断的处理函数也就得以执行。
从上述可看出,软中断是静态分配的,这对于后期加载的内核模块将是一个问题。基于软中断实现的 tasklets 解决了这个问题。
tasklet
tasklets 构建于 softirq 中断之上,他是基于下面两个软中断实现的:
TASKLET_SOFTIRQ;HI_SOFTIRQ.
简而言之,tasklets 是运行时分配和初始化的软中断。和软中断不同的是,同一类型的 tasklets 可以在同一时间运行于不同的处理器上。
wq(工作队列)
工作队列是另外一个处理延后函数的概念,它大体上和 tasklets 类似。工作队列运行于内核进程上下文,而 tasklets 运行于软中断上下文。这意味着工作队列函数不必像 tasklets 一样必须是原子性的。Tasklets 总是运行于它提交自的那个处理器,工作队列在默认情况下也是这样。
工作队列最基础的用法,是作为创建内核线程的接口来处理提交到队列里的工作任务。所有这些内核线程称之为 worker thread,这些线程会被用来调度执行工作队列的延后函数。
内核线程
内核线程是直接由内核本身启动的进程。内核线程实际上是将内核函数委托给独立的进程,它与内核中的其他进程”并行”执行。内核线程经常被称之为内核守护进程。
他们执行下列任务
- 周期性地将修改的内存页与页来源块设备同步
- 如果内存页很少使用,则写入交换区
- 管理延时动作, 如2号进程接手内核进程的创建
- 实现文件系统的事务日志
内核线程主要有两种类型
- 线程启动后一直等待,直至内核请求线程执行某一特定操作。
- 线程启动后按周期性间隔运行,检测特定资源的使用,在用量超出或低于预置的限制时采取行动。
内核线程由内核自身生成,其特点在于
- 它们在CPU的管态执行,而不是用户态。
- 它们只可以访问虚拟地址空间的内核部分(高于TASK_SIZE的所有地址),但不能访问用户空间
内核线程是一种只运行在内核地址空间的线程。所有的内核线程共享内核地址空间,所以也共享同一份内核页表,内核线程只运行在内核地址空间中,只会访问 3-4GB (32位系统)的内核地址空间,不存在虚拟地址空间,因此每个内核线程的 task_struct 对象中的 mm 为 NULL。普通线程虽然也是同主线程共享地址空间,但是它的 task_struct 对象中的 mm 不为空,指向的是主线程的 mm_struct 对象,普通进程既可以运行在内核态,也可以运行在用户态而内核线程只运行在内核态。
TCP/IP协议栈基础
网络架构
上图展示了Linux网络协议栈的架构以及如何实现Internet模型,在最上面的是用户空间中实现的应用层,而中间为内核空间中实现的网络子系统,底部为物理设备,提供了对网络的连接能力。在网络协议栈内部流动的是套接口缓冲区(SKB),用于在协议栈的底层、上层以及应用层之间传递报文数据。
网络协议栈顶部是系统调用接口,为用户空间中的应用程序提供一种访问内核网络子系统的接口。下面是一个协议无关层,它提供了一种通用方法来使用传输层协议。然后是传输层的具体协议,包括TCP、UDP。在传输层下面是网络层,之后是邻居子系统,再下面是网络设备接口,提供了与各个设备驱动通信的通用接口。最底层是设备驱动程序。
协议无关接口
通过网络协议栈通信需要对套接口进行操作,套接口是一个与协议无关的接口,它提供了一组接口来支持各种协议,套接口层不但可以支持典型的TCP和UDP协议,还可以支持RAW套接口、RAW以太网以及其他传输协议。
Linux中使用socket结构描述套接口,代表一条通信链路的一端,用来存储与该链路有关的所有信息,这些信息中包括:
- 所使用的协议
- 协议的状态信息(包括源地址和目标地址)
- 到达的连接队列
- 数据缓存和可选标志等等
其示意图如下所示:
其中最关键的成员是sk和ops,sk指向与该套接口相关的传输控制块,ops指向特定的传输协议的操作集
下图详细展示了socket结构体中的sk和ops字段,以TCP为例
sk字段指向与该套接口相关的传输控制块,传输层使用传输控制块来存放套接口所需的信息,在上图中即为TCP传输控制块,即tcp_sock结构。
ops字段指向特定传输协议的操作集接口,proto_pos结构中定义的接口函数是从套接口系统调用到传输层调用的入口,因此其成员与socket系统调用基本上是一一对应的。整个proto_ops结构就是一张套接口系统调用的跳转表,TCP、UDP、RAW套接口的传输层操作集分别为inet_stream_ops、inet_dgram_ops、inet_sockraw_ops
套接口缓存
如下图所示,网络子系统中用来存储数据的缓冲区叫做套接口缓存,简称为SKB,该缓存区能够处理可变长数据,即能够很容易地在数据区头尾部添加和移除数据,且尽量避免数据的复制,通常每个报文使用一个SKB表示,各协议栈报文头通过一组指针进行定位,由于SKB是网络子系统中数据管理的核心,因此有很多管理函数是对它进行操作的。
SKB主要用于在网络驱动程序和应用程序之间传递、复制数据包。当应用程序要发送一个数据包时:
- 数据通过系统调用提交到内核中
- 系统会分配一个SKB来存储数据
- 之后向下层传递
- 再传递给网络驱动后才将其释放
当网络设备接收到数据包也要分配一个SKB来对数据进行存储,之后再向上传递,最终将数据复制到应用程序后进行释放。

sk_buf结构
sk_buff是Linux网络模块中最重要的数据结构之一,用以描述已接受或待发送的数据报文信息。目的是为了方便在网络协议栈的各层间进行灵活处理,其成员可以大致分为以下几类:
- 与SKB组织相关的成员变量
- 通用成员变量
- 标志性变量
- 与特性相关的成员变量
SKB在不同网络协议层之间传递,可用于不同的网络协议,如第二层的MAC或是其他链路层协议、三层的IP协议、四层的TCP或者UDP协议,其中某些成员变量会在该结构从一层向另一层传递时发生改变。例如:四层向三层传递前会添加一个四层首部。添加首部比在不同层之间复制数据效率更高。
以下代码为sk_buff的定义
struct sk_buff {
/* These two members must be first. */
struct sk_buff *next; //双向链表
struct sk_buff *prev;
struct sock *sk; //skb的宿主传输控制块,skb的宿主传输控制块在网络数据报文由本地发出或由本地接受时才有效,使传输控制块与套接口及用户应用程序相关,当一个SKB仅在二层或三层被转发时,即源IP地址和目的IP地址都不是本机地址时,该值为NULL
struct skb_timeval tstamp;
struct net_device *dev;
struct net_device *input_dev;
union {
struct tcphdr *th;
struct udphdr *uh;
struct icmphdr *icmph;
struct igmphdr *igmph;
struct iphdr *ipiph;
struct ipv6hdr *ipv6h;
unsigned char *raw;
} h;
union {
struct iphdr *iph;
struct ipv6hdr *ipv6h;
struct arphdr *arph;
unsigned char *raw;
} nh;
union {
unsigned char *raw;
} mac;
struct dst_entry *dst;
struct sec_path *sp;
/*
* This is the control buffer. It is free to use for every
* layer. Please put your private variables there. If you
* want to keep them across layers you have to do a skb_clone()
* first. This is owned by whoever has the skb queued ATM.
*/
char cb[48];
unsigned int len, //skb中数据部分长度,len也包含了协议首部的长度
data_len, //SG类型和FRAGLIST类型聚合分散IO存储区中的数据长度
mac_len;
union {
__wsum csum;
__u32 csum_offset;
};
__u32 priority;
__u8 local_df:1,
cloned:1,
ip_summed:2,
nohdr:1,
nfctinfo:3;
__u8 pkt_type:3,
fclone:2,
ipvs_property:1;
__be16 protocol;
void (*destructor)(struct sk_buff *skb); //skb析构函数指针
#ifdef CONFIG_NETFILTER
struct nf_conntrack *nfct;
#if defined(CONFIG_NF_CONNTRACK) || defined(CONFIG_NF_CONNTRACK_MODULE)
struct sk_buff *nfct_reasm;
#endif
#ifdef CONFIG_BRIDGE_NETFILTER
struct nf_bridge_info *nf_bridge;
#endif
#endif /* CONFIG_NETFILTER */
#ifdef CONFIG_NET_SCHED
__u16 tc_index; /* traffic control index */
#ifdef CONFIG_NET_CLS_ACT
__u16 tc_verd; /* traffic control verdict */
#endif
#endif
#ifdef CONFIG_NET_DMA
dma_cookie_t dma_cookie;
#endif
#ifdef CONFIG_NETWORK_SECMARK
__u32 secmark;
#endif
__u32 mark;
/* These elements must be at the end, see alloc_skb() for details. */
unsigned int truesize; //整个数据缓存区的总长度,包括SKB描述符和数据缓存区部分(包括线性存储区和聚合分散IO缓存区)
atomic_t users; //引用计数,用来表示有多少实体引用了该SKB,其主要作用是确定释放所属的SKB的实际,当计数器为0时,该SKB才能被书房
unsigned char *head, //该四个字段用来指向线性数据缓存区以及数据部分的边界,head和end分别指向缓存区的头和尾,而data和tail则分别指向数据的头和尾,在发送时每一层协议会在head与data之间填充协议首部,还可能在tail和end之间添加数据,如下图所示
*data,
*tail,
*end;
};
下图为skb中的head/end指针和data/tail指针

Socket结构体
内核中的进程可以通过struct socket结构体来访问linux内核中的网络系统中的传输层、网络层以及数据链路层,也可以说struct socket是内核中的进程与内核中的网络系统的桥梁,以下为socket的代码:
struct socket
{
socket_state state; // socket state
short type ; // socket type
unsigned long flags; // socket flags
struct fasync_struct *fasync_list;
wait_queue_head_t wait;
struct file *file;
struct sock *sock; // socket在网络层的表示;
const struct proto_ops *ops;
}
struct socket结构体的类型
enum sock_type
{
SOCK_STREAM = 1, // 用于与TCP层中的tcp协议数据的struct socket
SOCK_DGRAM = 2, //用于与TCP层中的udp协议数据的struct socket
SOCK_RAW = 3, // raw struct socket
SOCK_RDM = 4, //可靠传输消息的struct socket
SOCK_SEQPACKET = 5,// sequential packet socket
SOCK_DCCP = 6,
SOCK_PACKET = 10, //从dev level中获取数据包的socket
};
struct socket 中的flags字段取值:
#define SOCK_ASYNC_NOSPACE 0
#define SOCK_ASYNC_WAITDATA 1
#define SOCK_NOSPACE 2
#define SOCK_PASSCRED 3
#define SOCK_PASSSEC 4
我们知道在TCP层中使用两个协议:tcp协议和udp协议。而在将TCP层中的数据往下传输时,要使用网络层的协议,而网络层的协议很多,不同的网络使用不同的网络层协议。我们常用的因特网中,网络层使用的是IPV4和IPV6协议。
所以在内核中的进程在使用struct socket提取内核网络系统中的数据时,不光要指明struct socket的类型(用于说明是提取TCP层中tcp协议负载的数据,还是udp层负载的数据),还要指明网络层的协议类型(网络层的协议用于负载TCP层中的数据)。
linux内核中的网络系统中的网络层的协议,在linux中被称为address family(地址簇,通常以AF_XXX表示)或protocol family(协议簇,通常以PF_XXX表示)。
如下图所示:
网络信息处理流程
应用层
上面已经提到了socket结构,应用层的各种网络应用程序都是通过Linux Socket编程接口来和内核空间的网络协议通信的。具体处理流程为:
- 网络应用调用Socket API socket (int family, int type, int protocol) 创建一个 socket,该调用最终会调用 Linux system call socket() ,并最终调用 Linux Kernel 的 sock_create() 方法。该方法返回被创建好了的那个 socket 的 file descriptor。对于每一个 userspace 网络应用创建的 socket,在内核中都有一个对应的 struct socket和 struct sock。其中,struct sock 有三个队列(queue),分别是 rx , tx 和 err,在 sock 结构被初始化的时候,这些缓冲队列也被初始化完成;在收据收发过程中,每个 queue 中保存要发送或者接受的每个 packet 对应的 Linux 网络栈 sk_buffer 数据结构的实例 skb。
- 对于 TCP socket 来说,应用调用 connect()API ,使得客户端和服务器端通过该 socket 建立一个虚拟连接。在此过程中,TCP 协议栈通过三次握手会建立 TCP 连接。默认地,该 API 会等到 TCP 握手完成连接建立后才返回。在建立连接的过程中的一个重要步骤是,确定双方使用的 Maxium Segemet Size (MSS)。因为 UDP 是面向无连接的协议,因此它是不需要该步骤的。
- 应用调用 Linux Socket 的 send 或者 write API 来发出一个 message 给接收端
- sock_sendmsg 被调用,它使用 socket descriptor 获取 sock struct,创建 message header 和 socket control message
- _sock_sendmsg 被调用,根据 socket 的协议类型,调用相应协议的发送函数
- 对于 TCP ,调用 tcp_sendmsg 函数
- 对于 UDP 来说,userspace 应用可以调用 send()/sendto()/sendmsg() 三个 system call 中的任意一个来发送 UDP message,它们最终都会调用内核中的 udp_sendmsg() 函数
下图为socket的三个队列
传输层
传输层的最终目的是向它的用户提供高效的、可靠的和成本有效的数据传输服务,TCP 协议栈的大致处理过程如下图所示:
TCP发送数据的简要过程:
- tcp_sendmsg 函数会首先检查已经建立的 TCP connection 的状态,然后获取该连接的 MSS,开始 segement 发送流程
- 构造 TCP 段的 playload:它在内核空间中创建该 packet 的 sk_buffer 数据结构的实例 skb,从 userspace buffer 中拷贝 packet 的数据到 skb 的 buffer
- 构造 TCP header
- 计算 TCP 校验和(checksum)和 顺序号 (sequence number)
- 发到 IP 层处理:调用 IP handler 句柄 ip_queue_xmit,将 skb 传入 IP 处理流程
接收数据:
- 传输层 TCP 处理入口在 tcp_v4_rcv 函数,它会做 TCP header 检查等处理
- 调用 _tcp_v4_lookup,查找该 package 的 open socket。如果找不到,该 package 会被丢弃。接下来检查 socket 和 connection 的状态
- 如果socket 和 connection 一切正常,调用 tcp_prequeue 使 package 从内核进入 user space,放进 socket 的 receive queue。然后 socket 会被唤醒,调用 system call,并最终调用 tcp_recvmsg 函数去从 socket recieve queue 中获取 segment
IP网络层
网络层的任务就是选择合适的网间路由和交换结点, 确保数据及时传送。下图为网络层处理信息的基本流程:
发送流程:
- 首先,
ip_queue_xmit(skb)会检查skb->dst路由信息。如果没有,比如套接字的第一个包,就使用ip_route_output()选择一个路由 - 接着,填充IP包的各个字段,比如版本、包头长度、TOS等
- 进行一些分片操作
- 接下来就用
ip_finish_ouput2设置链路层报文头了。如果,链路层报头缓存有(即hh不为空),那就拷贝到skb里。如果没,那么就调用neigh_resolve_output,使用 ARP 获取
接收流程:
- IP 层的入口函数在
ip_rcv函数。该函数首先会做包括 package checksum 在内的各种检查,如果需要的话会做 IP defragment(将多个分片合并),然后 packet 调用已经注册的 Pre-routing netfilter hook ,完成后最终到达ip_rcv_finish函数 p_rcv_finish函数会调用ip_router_input函数,进入路由处理环节。它首先会调用ip_route_input来更新路由,然后查找 route,决定该 package 将会被发到本机还是会被转发还是丢弃:- 如果是发到本机的话,调用
ip_local_deliver函数,可能会做 de-fragment(合并多个 IP packet),然后调用ip_local_deliver函数。该函数根据 package 的下一个处理层的 protocal number,调用下一层接口,包括tcp_v4_rcv(TCP),udp_rcv(UDP),icmp_rcv(ICMP),igmp_rcv(IGMP)。对于 TCP 来说,函数tcp_v4_rcv函数会被调用,从而处理流程进入 TCP 栈 - 如果需要转发 (forward),则进入转发流程。该流程需要处理 TTL,再调用
dst_input函数。该函数会 (1)处理 Netfilter Hook (2)执行 IP fragmentation (3)调用dev_queue_xmit,进入链路层处理流程
- 如果是发到本机的话,调用
数据链路层
功能上,在物理层提供比特流服务的基础上,建立相邻结点之间的数据链路,通过差错控制提供数据帧(Frame)在信道上无差错的传输,并进行各电路上的动作系列。数据链路层在不可靠的物理介质上提供可靠的传输。该层的作用包括:物理地址寻址、数据的成帧、流量控制、数据的检错、重发等。在这一层,数据的单位称为帧(frame)。
send分析
传输层分析
send的定义如下所示:
ssize_t send(int sockfd, const void *buf, size_t len, int flags)
当在调用send函数的时候,内核封装send()为sendto(),然后发起系统调用。其实也很好理解,send()就是sendto()的一种特殊情况,而sendto()在内核的系统调用服务程序为sys_sendto,sys_sendto的代码如下所示:
int __sys_sendto(int fd, void __user *buff, size_t len, unsigned int flags,
struct sockaddr __user *addr, int addr_len)
{
struct socket *sock;
struct sockaddr_storage address;
int err;
struct msghdr msg; //用来表示要发送的数据的一些属性
struct iovec iov;
int fput_needed;
err = import_single_range(WRITE, buff, len, &iov, &msg.msg_iter);
if (unlikely(err))
return err;
sock = sockfd_lookup_light(fd, &err, &fput_needed);
if (!sock)
goto out;
msg.msg_name = NULL;
msg.msg_control = NULL;
msg.msg_controllen = 0;
msg.msg_namelen = 0;
if (addr) {
err = move_addr_to_kernel(addr, addr_len, &address);
if (err < 0)
goto out_put;
msg.msg_name = (struct sockaddr *)&address;
msg.msg_namelen = addr_len;
}
if (sock->file->f_flags & O_NONBLOCK)
flags |= MSG_DONTWAIT;
msg.msg_flags = flags;
err = sock_sendmsg(sock, &msg); //实际的发送函数
out_put:
fput_light(sock->file, fput_needed);
out:
return err;
}
__sys_sendto函数其实做了3件事:
- 通过fd获取了对应的
struct socket - 创建了用来描述要发送的数据的结构体
struct msghdr - 调用了
sock_sendmsg来执行实际的发送
继续追踪sock_sendmsg,发现其最终调用的是sock->ops->sendmsg(sock, msg, msg_data_left(msg));,即socet在初始化时赋值给结构体struct proto tcp_prot的函数tcp_sendmsg,如下所示:
struct proto tcp_prot = {
.name = "TCP",
.owner = THIS_MODULE,
.close = tcp_close,
.pre_connect = tcp_v4_pre_connect,
.connect = tcp_v4_connect,
.disconnect = tcp_disconnect,
.accept = inet_csk_accept,
.ioctl = tcp_ioctl,
.init = tcp_v4_init_sock,
.destroy = tcp_v4_destroy_sock,
.shutdown = tcp_shutdown,
.setsockopt = tcp_setsockopt,
.getsockopt = tcp_getsockopt,
.keepalive = tcp_set_keepalive,
.recvmsg = tcp_recvmsg,
.sendmsg = tcp_sendmsg,
...
而tcp_send函数实际调用的是tcp_sendmsg_locked函数,该函数的定义如下所示:
int tcp_sendmsg_locked(struct sock *sk, struct msghdr *msg, size_t size)
{
struct tcp_sock *tp = tcp_sk(sk);/*进行了强制类型转换*/
struct sk_buff *skb;
flags = msg->msg_flags;
......
if (copied)
tcp_push(sk, flags & ~MSG_MORE, mss_now,
TCP_NAGLE_PUSH, size_goal);
}
在tcp_sendmsg_locked中,完成的是将所有的数据组织成发送队列,这个发送队列是struct sock结构中的一个域sk_write_queue,这个队列的每一个元素是一个skb,里面存放的就是待发送的数据。在该函数中通过调用tcp_push()函数将数据加入到发送队列中。
struct sock的部分代码如下所示:
struct sock{
...
struct sk_buff_head sk_write_queue;/*指向skb队列的第一个元素*/
...
struct sk_buff *sk_send_head;/*指向队列第一个还没有发送的元素*/
}
tcp_push的代码如下所示:
static void tcp_push(struct sock *sk, int flags, int mss_now,
int nonagle, int size_goal)
{
struct tcp_sock *tp = tcp_sk(sk);
struct sk_buff *skb;
skb = tcp_write_queue_tail(sk);
if (!skb)
return;
if (!(flags & MSG_MORE) || forced_push(tp))
tcp_mark_push(tp, skb);
tcp_mark_urg(tp, flags);
if (tcp_should_autocork(sk, skb, size_goal)) {
/* avoid atomic op if TSQ_THROTTLED bit is already set */
if (!test_bit(TSQ_THROTTLED, &sk->sk_tsq_flags)) {
NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPAUTOCORKING);
set_bit(TSQ_THROTTLED, &sk->sk_tsq_flags);
}
/* It is possible TX completion already happened
* before we set TSQ_THROTTLED.
*/
if (refcount_read(&sk->sk_wmem_alloc) > skb->truesize)
return;
}
if (flags & MSG_MORE)
nonagle = TCP_NAGLE_CORK;
__tcp_push_pending_frames(sk, mss_now, nonagle); //最终通过调用该函数发送数据
}
在之后tcp_push调用了__tcp_push_pending_frames(sk, mss_now, nonagle);来发送数据
__tcp_push_pending_frames的代码如下所示:
void __tcp_push_pending_frames(struct sock *sk, unsigned int cur_mss,
int nonagle)
{
if (tcp_write_xmit(sk, cur_mss, nonagle, 0,
sk_gfp_mask(sk, GFP_ATOMIC))) //调用该函数发送数据
tcp_check_probe_timer(sk);
}
在__tcp_push_pending_frames又调用了tcp_write_xmit来发送数据,代码如下所示:
static bool tcp_write_xmit(struct sock *sk, unsigned int mss_now, int nonagle,
int push_one, gfp_t gfp)
{
struct tcp_sock *tp = tcp_sk(sk);
struct sk_buff *skb;
unsigned int tso_segs, sent_pkts;
int cwnd_quota;
int result;
bool is_cwnd_limited = false, is_rwnd_limited = false;
u32 max_segs;
/*统计已发送的报文总数*/
sent_pkts = 0;
......
/*若发送队列未满,则准备发送报文*/
while ((skb = tcp_send_head(sk))) {
unsigned int limit;
if (unlikely(tp->repair) && tp->repair_queue == TCP_SEND_QUEUE) {
/* "skb_mstamp_ns" is used as a start point for the retransmit timer */
skb->skb_mstamp_ns = tp->tcp_wstamp_ns = tp->tcp_clock_cache;
list_move_tail(&skb->tcp_tsorted_anchor, &tp->tsorted_sent_queue);
tcp_init_tso_segs(skb, mss_now);
goto repair; /* Skip network transmission */
}
if (tcp_pacing_check(sk))
break;
tso_segs = tcp_init_tso_segs(skb, mss_now);
BUG_ON(!tso_segs);
/*检查发送窗口的大小*/
cwnd_quota = tcp_cwnd_test(tp, skb);
if (!cwnd_quota) {
if (push_one == 2)
/* Force out a loss probe pkt. */
cwnd_quota = 1;
else
break;
}
if (unlikely(!tcp_snd_wnd_test(tp, skb, mss_now))) {
is_rwnd_limited = true;
break;
......
limit = mss_now;
if (tso_segs > 1 && !tcp_urg_mode(tp))
limit = tcp_mss_split_point(sk, skb, mss_now,
min_t(unsigned int,
cwnd_quota,
max_segs),
nonagle);
if (skb->len > limit &&
unlikely(tso_fragment(sk, TCP_FRAG_IN_WRITE_QUEUE,
skb, limit, mss_now, gfp)))
break;
if (tcp_small_queue_check(sk, skb, 0))
break;
if (unlikely(tcp_transmit_skb(sk, skb, 1, gfp))) //调用该函数发送数据
break;
......
tcp_write_xmit位于tcpoutput.c中,它实现了tcp的拥塞控制,然后调用了tcp_transmit_skb(sk, skb, 1, gfp)传输数据,实际上调用的是__tcp_transmit_skb。
__tcp_transmit_skb的部分代码如下所示:
static int __tcp_transmit_skb(struct sock *sk, struct sk_buff *skb,
int clone_it, gfp_t gfp_mask, u32 rcv_nxt)
{
skb_push(skb, tcp_header_size);
skb_reset_transport_header(skb);
......
/* 构建TCP头部和校验和 */
th = (struct tcphdr *)skb->data;
th->source = inet->inet_sport;
th->dest = inet->inet_dport;
th->seq = htonl(tcb->seq);
th->ack_seq = htonl(rcv_nxt);
tcp_options_write((__be32 *)(th + 1), tp, &opts);
skb_shinfo(skb)->gso_type = sk->sk_gso_type;
if (likely(!(tcb->tcp_flags & TCPHDR_SYN))) {
th->window = htons(tcp_select_window(sk));
tcp_ecn_send(sk, skb, th, tcp_header_size);
} else {
/* RFC1323: The window in SYN & SYN/ACK segments
* is never scaled.
*/
th->window = htons(min(tp->rcv_wnd, 65535U));
}
......
icsk->icsk_af_ops->send_check(sk, skb);
if (likely(tcb->tcp_flags & TCPHDR_ACK))
tcp_event_ack_sent(sk, tcp_skb_pcount(skb), rcv_nxt);
if (skb->len != tcp_header_size) {
tcp_event_data_sent(tp, sk);
tp->data_segs_out += tcp_skb_pcount(skb);
tp->bytes_sent += skb->len - tcp_header_size;
}
if (after(tcb->end_seq, tp->snd_nxt) || tcb->seq == tcb->end_seq)
TCP_ADD_STATS(sock_net(sk), TCP_MIB_OUTSEGS,
tcp_skb_pcount(skb));
tp->segs_out += tcp_skb_pcount(skb);
/* OK, its time to fill skb_shinfo(skb)->gso_{segs|size} */
skb_shinfo(skb)->gso_segs = tcp_skb_pcount(skb);
skb_shinfo(skb)->gso_size = tcp_skb_mss(skb);
/* Leave earliest departure time in skb->tstamp (skb->skb_mstamp_ns) */
/* Cleanup our debris for IP stacks */
memset(skb->cb, 0, max(sizeof(struct inet_skb_parm),
sizeof(struct inet6_skb_parm)));
err = icsk->icsk_af_ops->queue_xmit(sk, skb, &inet->cork.fl); //调用网络层的发送接口
......
}
__tcp_transmit_skb是位于传输层发送tcp数据的最后一步,这里首先对TCP数据段的头部进行了处理,然后调用了网络层提供的发送接口icsk->icsk_af_ops->queue_xmit(sk, skb, &inet->cork.fl);实现了数据的发送,自此,数据离开了传输层,传输层的任务也就结束了。
时序图

GDB调试
网络层分析
网络层的调用流程为:
-
ip_queue_xmit将TCP传输过来的数据包打包成IP数据报,将数据打包成IP数据包之后,通过调用
ip_local_out函数,在该函数内部调用了__ip_local_out,该函数返回了一个nf_hook函数,在该函数内部调用了dst_outputint ip_queue_xmit(struct sock *sk, struct sk_buff *skb, struct flowi *fl) { struct inet_sock *inet = inet_sk(sk); struct net *net = sock_net(sk); struct ip_options_rcu *inet_opt; struct flowi4 *fl4; struct rtable *rt; struct iphdr *iph; int res; /* Skip all of this if the packet is already routed, * f.e. by something like SCTP. */ rcu_read_lock(); /* * 如果待输出的数据包已准备好路由缓存, * 则无需再查找路由,直接跳转到packet_routed * 处作处理。 */ inet_opt = rcu_dereference(inet->inet_opt); fl4 = &fl->u.ip4; rt = skb_rtable(skb); if (rt) goto packet_routed; /* Make sure we can route this packet. */ /* * 如果输出该数据包的传输控制块中 * 缓存了输出路由缓存项,则需检测 * 该路由缓存项是否过期。 * 如果过期,重新通过输出网络设备、 * 目的地址、源地址等信息查找输出 * 路由缓存项。如果查找到对应的路 * 由缓存项,则将其缓存到传输控制 * 块中,否则丢弃该数据包。 * 如果未过期,则直接使用缓存在 * 传输控制块中的路由缓存项。 */ rt = (struct rtable *)__sk_dst_check(sk, 0); if (!rt) { /* 缓存过期 */ __be32 daddr; /* Use correct destination address if we have options. */ daddr = inet->inet_daddr; /* 目的地址 */ if (inet_opt && inet_opt->opt.srr) daddr = inet_opt->opt.faddr; /* 严格路由选项 */ /* If this fails, retransmit mechanism of transport layer will * keep trying until route appears or the connection times * itself out. */ /* 查找路由缓存 */ rt = ip_route_output_ports(net, fl4, sk, daddr, inet->inet_saddr, inet->inet_dport, inet->inet_sport, sk->sk_protocol, RT_CONN_FLAGS(sk), sk->sk_bound_dev_if); if (IS_ERR(rt)) goto no_route; sk_setup_caps(sk, &rt->dst); /* 设置控制块的路由缓存 */ } skb_dst_set_noref(skb, &rt->dst);/* 将路由设置到skb中 */ packet_routed: if (inet_opt && inet_opt->opt.is_strictroute && rt->rt_uses_gateway) goto no_route; /* OK, we know where to send it, allocate and build IP header. */ /* * 设置IP首部中各字段的值。如果存在IP选项, * 则在IP数据包首部中构建IP选项。 */ skb_push(skb, sizeof(struct iphdr) + (inet_opt ? inet_opt->opt.optlen : 0)); skb_reset_network_header(skb); iph = ip_hdr(skb);/* 构造ip头 */ *((__be16 *)iph) = htons((4 << 12) | (5 << 8) | (inet->tos & 0xff)); if (ip_dont_fragment(sk, &rt->dst) && !skb->ignore_df) iph->frag_off = htons(IP_DF); else iph->frag_off = 0; iph->ttl = ip_select_ttl(inet, &rt->dst); iph->protocol = sk->sk_protocol; ip_copy_addrs(iph, fl4); /* Transport layer set skb->h.foo itself. */ /* 构造ip选项 */ if (inet_opt && inet_opt->opt.optlen) { iph->ihl += inet_opt->opt.optlen >> 2; ip_options_build(skb, &inet_opt->opt, inet->inet_daddr, rt, 0); } ip_select_ident_segs(net, skb, sk, skb_shinfo(skb)->gso_segs ?: 1); /* TODO : should we use skb->sk here instead of sk ? */ /* * 设置输出数据包的QoS类型。 */ skb->priority = sk->sk_priority; skb->mark = sk->sk_mark; res = ip_local_out(net, sk, skb); /* 输出 */ rcu_read_unlock(); return res; no_route: rcu_read_unlock(); /* * 如果查找不到对应的路由缓存项, * 在此处理,将该数据包丢弃。 */ IP_INC_STATS(net, IPSTATS_MIB_OUTNOROUTES); kfree_skb(skb); return -EHOSTUNREACH; } -
dst_outputdst_output()实际调用skb_dst(skb)->output(skb),skb_dst(skb)就是skb所对应的路由项。skb_dst(skb)指向的是路由项dst_entry,它的input在收到报文时赋值ip_local_deliver(),而output在发送报文时赋值ip_output()。 -
ip_output处理单播数据报,设置数据报的输出网络设备以及网络层协议类型参数
-
ip_finish_output观察数据报长度是否大于MTU,若大于,则调用ip_fragment分片,否则调用ip_finish_output2输出
-
ip_finish_output2在该函数中会检测skb的前部空间是否还能存储链路层首部。如果不够,就会申请更大的存储空间,最终会调用邻居子系统的输出函数
neigh_output进行输出,输出分为有二层头缓存和没有两种情况,有缓存时调用neigh_hh_output进行快速输出,没有缓存时,则调用邻居子系统的输出回调函数进行慢速输出
时序图

GDB调试

数据链路层分析
网络层最终会通过调用dev_queue_xmit来发送报文,在该函数中调用的是__dev_queue_xmit(skb, NULL);,如下所示:
int dev_queue_xmit(struct sk_buff *skb)
{
return __dev_queue_xmit(skb, NULL);
}
直接调用__dev_queue_xmit传入的参数是一个skb 数据包
__dev_queue_xmit函数会根据不同的情况会调用__dev_xmit_skb或者sch_direct_xmit函数,最终会调用dev_hard_start_xmit函数,该函数最终会调用xmit_one来发送一到多个数据包
时序图

GDB调试

recv分析
数据链路层分析
在数据链路层接受数据并传递给上层的步骤如下所示:
- 一个 package 到达机器的物理网络适配器,当它接收到数据帧时,就会触发一个中断,并将通过 DMA 传送到位于 linux kernel 内存中的 rx_ring。
- 网卡发出中断,通知 CPU 有个 package 需要它处理。中断处理程序主要进行以下一些操作,包括分配
skb_buff数据结构,并将接收到的数据帧从网络适配器I/O端口拷贝到skb_buff缓冲区中;从数据帧中提取出一些信息,并设置skb_buff相应的参数,这些参数将被上层的网络协议使用,例如skb->protocol; - 终端处理程序经过简单处理后,发出一个软中断(NET_RX_SOFTIRQ),通知内核接收到新的数据帧。
- 内核 2.5 中引入一组新的 API 来处理接收的数据帧,即 NAPI。所以,驱动有两种方式通知内核:(1) 通过以前的函数
netif_rx;(2)通过NAPI机制。该中断处理程序调用 Network device的netif_rx_schedule函数,进入软中断处理流程,再调用net_rx_action函数。 - 该函数关闭中断,获取每个 Network device 的 rx_ring 中的所有 package,最终 pacakage 从 rx_ring 中被删除,进入
netif _receive_skb处理流程。 netif_receive_skb是链路层接收数据报的最后一站。它根据注册在全局数组 ptype_all 和 ptype_base 里的网络层数据报类型,把数据报递交给不同的网络层协议的接收函数(INET域中主要是ip_rcv和arp_rcv)。该函数主要就是调用第三层协议的接收函数处理该skb包,进入第三层网络层处理。
时序图
GDB调试

网络层分析
ip层的入口在ip_rcv函数,该函数首先会做包括 package checksum 在内的各种检查,如果需要的话会做 IP defragment(将多个分片合并),然后 packet 调用已经注册的 Pre-routing netfilter hook ,完成后最终到达ip_rcv_finish函数。
int ip_rcv(struct sk_buff *skb, struct net_device *dev, struct packet_type *pt,
struct net_device *orig_dev)
{
struct net *net = dev_net(dev);
skb = ip_rcv_core(skb, net);
if (skb == NULL)
return NET_RX_DROP;
return NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING,
net, NULL, skb, dev, NULL,
ip_rcv_finish);
}
ip_rcv_finish函数如下所示:
static int ip_rcv_finish(struct net *net, struct sock *sk, struct sk_buff *skb)
{
struct net_device *dev = skb->dev;
int ret;
/* if ingress device is enslaved to an L3 master device pass the
* skb to its handler for processing
*/
skb = l3mdev_ip_rcv(skb);
if (!skb)
return NET_RX_SUCCESS;
ret = ip_rcv_finish_core(net, sk, skb, dev, NULL);
if (ret != NET_RX_DROP)
ret = dst_input(skb);
return ret;
}
ip_rcv_finish 函数最终会调用ip_route_input函数,进入路由处理环节。它首先会调用 ip_route_input 来更新路由,然后查找 route,决定该 package 将会被发到本机还是会被转发还是丢弃:
- 如果是发到本机的话,调用
ip_local_deliver函数,可能会做 de-fragment(合并多个 IP packet),然后调用ip_local_deliver函数。该函数根据 package 的下一个处理层的 protocal number,调用下一层接口,包括 tcp_v4_rcv (TCP), udp_rcv (UDP),icmp_rcv (ICMP),igmp_rcv(IGMP)。对于 TCP 来说,函数tcp_v4_rcv函数会被调用,从而处理流程进入 TCP 栈。 - 如果需要转发 (forward),则进入转发流程。该流程需要处理 TTL,再调用
dst_input函数。该函数会 (1)处理 Netfilter Hook (2)执行 IP fragmentation (3)调用dev_queue_xmit,进入链路层处理流程。
时序图
GDB调试

传输层分析
对于recv函数,与send函数类似,调用的系统调用是__sys_recvfrom,其代码如下所示:
int __sys_recvfrom(int fd, void __user *ubuf, size_t size, unsigned int flags,
struct sockaddr __user *addr, int __user *addr_len)
{
......
err = import_single_range(READ, ubuf, size, &iov, &msg.msg_iter);
if (unlikely(err))
return err;
sock = sockfd_lookup_light(fd, &err, &fput_needed);
.....
msg.msg_control = NULL;
msg.msg_controllen = 0;
/* Save some cycles and don't copy the address if not needed */
msg.msg_name = addr ? (struct sockaddr *)&address : NULL;
/* We assume all kernel code knows the size of sockaddr_storage */
msg.msg_namelen = 0;
msg.msg_iocb = NULL;
msg.msg_flags = 0;
if (sock->file->f_flags & O_NONBLOCK)
flags |= MSG_DONTWAIT;
err = sock_recvmsg(sock, &msg, flags); //调用该函数接受数据
if (err >= 0 && addr != NULL) {
err2 = move_addr_to_user(&address,
msg.msg_namelen, addr, addr_len);
.....
}
__sys_recvfrom通过调用sock_recvmsg来对数据进行接收,该函数实际调用的是sock->ops->recvmsg(sock, msg, msg_data_left(msg), flags); ,同样类似send函数中,调用的实际上是socet在初始化时赋值给结构体struct proto tcp_prot的函数tcp_rcvmsg,如下所示:
struct proto tcp_prot = {
.name = "TCP",
.owner = THIS_MODULE,
.close = tcp_close,
.pre_connect = tcp_v4_pre_connect,
.connect = tcp_v4_connect,
.disconnect = tcp_disconnect,
.accept = inet_csk_accept,
.ioctl = tcp_ioctl,
.init = tcp_v4_init_sock,
.destroy = tcp_v4_destroy_sock,
.shutdown = tcp_shutdown,
.setsockopt = tcp_setsockopt,
.getsockopt = tcp_getsockopt,
.keepalive = tcp_set_keepalive,
.recvmsg = tcp_recvmsg,
.sendmsg = tcp_sendmsg,
...
tcp_rcvmsg的代码如下所示:
int tcp_recvmsg(struct sock *sk, struct msghdr *msg, size_t len, int nonblock,
int flags, int *addr_len)
{
......
if (sk_can_busy_loop(sk) && skb_queue_empty(&sk->sk_receive_queue) &&
(sk->sk_state == TCP_ESTABLISHED))
sk_busy_loop(sk, nonblock); //如果接收队列为空,则会在该函数内循环等待
lock_sock(sk);
.....
if (unlikely(tp->repair)) {
err = -EPERM;
if (!(flags & MSG_PEEK))
goto out;
if (tp->repair_queue == TCP_SEND_QUEUE)
goto recv_sndq;
err = -EINVAL;
if (tp->repair_queue == TCP_NO_QUEUE)
goto out;
......
last = skb_peek_tail(&sk->sk_receive_queue);
skb_queue_walk(&sk->sk_receive_queue, skb) {
last = skb;
......
if (!(flags & MSG_TRUNC)) {
err = skb_copy_datagram_msg(skb, offset, msg, used); //将接收到的数据拷贝到用户态
if (err) {
/* Exception. Bailout! */
if (!copied)
copied = -EFAULT;
break;
}
}
*seq += used;
copied += used;
len -= used;
tcp_rcv_space_adjust(sk);
在连接建立后,若没有数据到来,接收队列为空,进程会在sk_busy_loop函数内循环等待,知道接收队列不为空,并调用函数数skb_copy_datagram_msg将接收到的数据拷贝到用户态,该函数内部实际调用的是__skb_datagram_iter,其代码如下所示:
int __skb_datagram_iter(const struct sk_buff *skb, int offset,
struct iov_iter *to, int len, bool fault_short,
size_t (*cb)(const void *, size_t, void *, struct iov_iter *),
void *data)
{
int start = skb_headlen(skb);
int i, copy = start - offset, start_off = offset, n;
struct sk_buff *frag_iter;
/* 拷贝tcp头部 */
if (copy > 0) {
if (copy > len)
copy = len;
n = cb(skb->data + offset, copy, data, to);
offset += n;
if (n != copy)
goto short_copy;
if ((len -= copy) == 0)
return 0;
}
/* 拷贝数据部分 */
for (i = 0; i < skb_shinfo(skb)->nr_frags; i++) {
int end;
const skb_frag_t *frag = &skb_shinfo(skb)->frags[i];
WARN_ON(start > offset + len);
end = start + skb_frag_size(frag);
if ((copy = end - offset) > 0) {
struct page *page = skb_frag_page(frag);
u8 *vaddr = kmap(page);
if (copy > len)
copy = len;
n = cb(vaddr + frag->page_offset +
offset - start, copy, data, to);
kunmap(page);
offset += n;
if (n != copy)
goto short_copy;
if (!(len -= copy))
return 0;
}
start = end;
}
时序图

GDB调试

浙公网安备 33010602011771号