第7章 网络层传送
Internet协议的任务:
- 数据包校验和检验
- 防火墙对数据包过滤
- IP选项处理
- 数据分片和重组
- 接收、发送和前送
Internet协议头包含:Internet协议版本、IP协议头长度、服务类型(Tos)、数据包总长度、数据包标识符、数据包存活期、上层协议、校验和、源地址、目的地址、IP选项。
当数据包到达IP层后,sk_buff->data指针就已事先调整到Socket Buffer中存放IP协议头信息的起始地址处。IP层的协议处理函数就从Socket Buffer中解析出IP协议头信息,并放入iphdr类型的变量中,以便下一步协议处理函数按照IP协议头信息处理数据包
struct iphdr {
#if defined(__LITTLE_ENDIAN_BITFIELD)
__u8 ihl:4,
version:4;
#elif defined (__BIG_ENDIAN_BITFIELD)
__u8 version:4,
ihl:4; //IP协议头长度
#else
#error "Please fix <asm/byteorder.h>"
#endif
__u8 tos; //服务类型
__be16 tot_len; //IP数据包总长度
__be16 id; //IP数据包标识符
__be16 frag_off; //分片数据在数据包中的偏移量
__u8 ttl; //数据包的生成周期
__u8 protocol; //上次协议
__sum16 check; //校验和
__be32 saddr; //数据包源地址
__be32 daddr; //数据包目标地址
/*The options start here. */
};
PF_INET协议族初始化由inet_init函数实现,在完成协议族基本初始化之后,会调用协议族中各层协议的初始化函数建立起TCP/IP协议栈。主要功能为:
- 注册协议栈各协议实例(TCP/UDP/ICMP等)
- 初始化IP协议
- 为套接字操作建立内存槽以便在打开套接字时为其分配所需的内存
- 调用dev_add_pack注册IP数据包的接收处理函数ip_rcv,将处理函数插入到ptype_base中
- 初始化AF_INET协议族在/proc文件系统的入口。
数据包到达之后需要对数据包做过滤检查才能决定是否对数据包做进一步处理——网络过滤子系统。分两个阶段:
- do_something(如ip_rcv),只对数据包做必要的合法性检查,或为数据包预留需要的内存空间
- do_something_finish(如ip_icv_finish),实际完成接收/发送操作的函数。
三个查询路由表的API:
- ip_route_input : 确定输入包的目标地址
- ip_route_output_flow : 返回网关地址和发送的网络设备
- dst_pmtu: 返回最大传输单元
在IP层查询路由表时,路由表作出路由决策的主要依据是一下各数据域:
- IP数据包的目标地址
- IP数据包的源地址
- 服务类型(Tos)
- 接收数据包的网络设备
- 可用于发送数据包的网络设备列表
7.1 ip_rcv函数分析
ip_rcv是典型的两阶段实现的函数,该函数主要是对数据包作正确性检查,然后调用网络过滤子系统的回调函数对数据包进行安全过滤,对数据包的实际处理功能大部分都在ip_rcv_finish函数中实现。
7.2 数据包前送
处理函数ip_forward
7.3 本地发送
ip_local_deliver_finish完成数据包从网络层向传输层发送
在IP协议头中protocol数据域占了8位,传输层最多可以有256个不同的协议。
enum {
IPPROTO_IP = 0, /* Dummy protocol for TCP */
#define IPPROTO_IP IPPROTO_IP
IPPROTO_ICMP = 1, /* Internet Control Message Protocol */
#define IPPROTO_ICMP IPPROTO_ICMP
IPPROTO_IGMP = 2, /* Internet Group Management Protocol */
#define IPPROTO_IGMP IPPROTO_IGMP
IPPROTO_IPIP = 4, /* IPIP tunnels (older KA9Q tunnels use 94) */
#define IPPROTO_IPIP IPPROTO_IPIP
IPPROTO_TCP = 6, /* Transmission Control Protocol */
#define IPPROTO_TCP IPPROTO_TCP
...
IPPROTO_RAW = 255, /* Raw IP packets */
内核中所有注册了传输层协议处理函数实例,存放在一个struct net_protocol 类型的inet_protps全局数组中,形成向量表,网络层协议头中的protocol数据域描述的协议编码就是该协议的struct net_protocol 实例在inet_protos向量表中的索引。
struct net_protocol {
int (*early_demux)(struct sk_buff *skb);
int (*early_demux_handler)(struct sk_buff *skb);
int (*handler)(struct sk_buff *skb);
void (*err_handler)(struct sk_buff *skb, u32 info);
unsigned int no_policy:1,
netns_ok:1,
/* does the protocol do more stringent
* icmp tag validation than simple
* socket lookup?
*/
icmp_strict_tag_validation:1;
};
extern struct net_protocol __rcu *inet_protos[MAX_INET_PROTOS];
ip_local_deliver_finish函数的正式处理部分从获取访问inet_protos向量表的读保护锁rcu_read_lock开始,依次完成以下过程:
- 从IP头获取传输层协议的编码
- 如果该协议是裸套接字处理函数,则将数据包传给裸套接字处理函数
- 以iphdr->protocol数据域为索引在inet_protocols向量表中查询,找到对应的传输层协议处理函数结构块
ip_local_deliver_finish完成后,数据包就离开网络层上传至TCP/IP协议栈的传输层
7.4 在IP层的发送
函数名 | 功能描述 |
---|---|
ip_queue_xmit | 由传输层的TCP协议调用,将数据包从传输层发送给网络层,创建协议头和IP选项到数据包中,调用dst_output发送 |
ip_append_data | 由传输层的UDP等协议调用,缓存从传输层传送到网络层的请求发送数据包缓冲区 |
ip_append_page | 由传输层的UDP等协议调用,缓存从传输层传送到网络层的请求发送数据页面 |
ip_push_pending_frams | 将ip_append_data和ip_append_page创建的输出队列发送出去 |
dst_output | 数据包发送函数,当数据包的目标地址是其他主机时,初始化为ip_output |
ip_build_and_send_pkt | 用于TCP发送同步回答消息时创建IP协议头的选项并发送 |
ip_send_reply | 用于TCP发送回答消息和复位时创建IP协议头和选项并发送数据包 |
数据结构 | 描述 |
---|---|
struct sock | 套接字数据结构 |
struct inet_sock | PF_INET特定协议族套接字结构 |
struct ipcm_cookie | 包含发送数据包需要的各种信息的数据结构 |
struct cork | 嵌套在inet_sock数据结构中,处理套接字阻塞时的选项信息 |
struct dst_entry | 路由表入口 |
struct rtable | 路由表结构 |
struct in_device | 存放网络设备的所有与IPv4协议相关的配置 |
struct in_ifaddr | 如果为一个网络接口分配了IPv4网络地址,该数据结构用于存放IP地址和与地址配置相关信息 |
操作struct sock、struct inet_sock数据结构的关键函数
- sk_dst_set和_sk_dst_set 保存到达目的地址使用的路由
- sk_dst_check和_sk_dst_check 测试到达目标地址的路由是否有效
- skb_set_owner_w 指定数据包所属的套接字
- sock_alloc_send_skb: 分配单个缓冲区或一系列分配数据包的第一个缓冲区;
sock_wmalloc : 管理其余子分片。
ip_queue_xmit 由TCP和SCTP协议调用,处理本地产生的外送数据包,是传输层向网络层传输数据包时调用的函数。
- 设置路由:ip_route_output_flow寻找下一条的新路由
- 构建IP协议头
- NET_INET_LOCAL_OUT
- dst_output
ip_append_data 不传送数据,而是把数据放到一个大小适中的缓冲区中,随后的函数对数据包分段处理,并将数据包发送出去。
当数据发送请求来自用户地址空间,应用程序调用sndmsg来请求将数据从用户空间移动到内核地址空间时,这个复制是由ip_append_data函数的输入参数getfrag函数来完成,传输层对应的getfrag例程如下:
协议 | 数据复制函数 |
---|---|
ICMP | icmp_glue_bits |
UDP | ip_generic_getfrag |
裸IP | ip_generic_getfrag |
TCP | ip_reply_glue_bits |
ip_append_page :将frag数组初始化指向接收数据缓冲区的位置,在必要的时候计算传输层的校验和。
只有在网络设备支持Scatter/Gather I/O功能时才能使用。
第8章 传输层UDP协议的实现
UDP是不可靠、无连接的数据报协议。“不可靠”仅仅意味着在UDP协议中没有检测数据是否能够到达网络另一端的机制,在主机内,UDP可以保证正确的传递数据。
使用UDP传输数据的原因:
- 数据量很小,创建连接的开销、保证可靠发送需要做的工作可能比发送数据本身的工作量还大
- 应用程序本身保证可靠发送机制
- 面向业务的应用程序(DNS),只有一个请求和一个应答,建立连接和维护连接的开销太大
8.1 UDP协议实现的关键数据结构
//UDP协议头信息,include/linux/udp.h
struct udphdr{
__be16 source;//源端口
__be16 dest;//目的端口
__be16 len;//UDP数据包长度
__sum16 check;//校验和
}
//UDP控制缓冲区(sk_buff中存放各层协议的私有数据) //include/net/udp.h
struct udp_skb_cb{
union{ // IPv4和IPv6选项信息
struct inet_skb_parm h4;
#if defined (CONFIG_IPV6) || defined (CONFIG_IPV6_MOUDLE)
struct inet6_skb_parm h6;
#endif
}header;
__u16 cscov; //校验和覆盖的UDP数据包长度
__u8 partial_cov; //指明UDP计算部分校验和以及校验和覆盖的UDP数据包长度
};
//只能通过如下宏访问
#define UDP_SKB_CB(__skb) (struct udp_skb_cb *)((__skb)->cb)
//UDP套接字
struct udp_sock{
struct inet_sock inet; //PF_INET特定协议族套接字结构 -> struct sock sk(套接字)
int pending; //是否有等待发送(悬挂)的数据包
unsigned int corkflag; //是否需要暂时阻塞套接字
__u16 encap_type; //是否为封装的套接字
__u16 len; //等待发送的数据包的总长度
__u16 pcslen; //等待发送数据包长度
__u16 pcrlen; //等待接受数据包长度
__u8 pcflag; //是否为轻套接字
__u8 unused[3];
int (*encap_rcv)(struct sock *sk, struct sk_buff *skb);//UDP套接字的接收函数
};
与套接字层之间的接口:
struct proto udp_proto={
.name = "UDP",
.owner = THIS_MOUDLE,
.close = udp_lib_close,
.connect = ip4_datagrame_connect,
.disconnect = udp_disconnect,
.ioctl = udp_ioctl,
.destory = udp_destroy_sock,
.setsockopt = udp_setsockopt,
.getsockopt = udp_getsockopt,
.sendmsg = udp_sendmsg,
.recvmsg = udp_recvmsg,
.backlog_rcv = __udp_queue_rcv_skb,
.hash = udp_lib_hash,
.unhash = udp_lib_unhash,
.get_port = udp_v4_get_port,
...
}
UDP实例由proto_register函数在 inet_init 中调用注册。
与IP层的接口:
static struct net_protocol udp_protocol = {
.handler = udp_rcv,
.err_handler = udp_err,
.no_policy = 1,
.netns_ok = 1,
};
在inet_init 中调用inet_add_protocol(&udp_protocol, IPPROTO_UDP) 注册。
8.2 发送UDP数据报的实现
用户程序在应用层调用socket系统调用,打开一个SOCK_DGRAM类型的套接字,在通过该套接字接收和发送数据时,数据包会通过UDP协议穿过TCP/IP协议栈向外发送,或由UDP协议上传给套接字到达应用程序。
8.2.1 初始化一个连接
UDP套接字调用connect : 建立到达目标地址的路由,并把该路由放入路由高速缓冲存储器中。一旦路由建立起来,接下来在通过UDP套接字发送数据包时就可以使用路由高速缓冲区中的信息了。这种方式称之为在连接套接字上的快速路径"fast path"。
SOCK_DGRAM类套接字调用connect系统调用时,套接字层会转而调用ip4_datagram_connect 函数
int ip4_datagram_connect(struct sock *sk, struct sockaddr *uaddr, int addr_len)
// sk : 指向打开的UDP套接字的struct sock数据结构
// uaddr : 目标地址
// addr_len : 目标地址长度
//对建立连接的信息做正确性检查:地址长度、协议类型
if (addr_len < sizeof(*usin))
return -EINVAL;
if (usin->sin_family != AF_INET)
return -EAFNOSUPPORT;
//复位套接字中源目标地址在路由缓存中的记录
sk_dst_reset(sk);
//套接字为与网络接口绑定的套接字,将绑定的网络接口信息保存到oif局部变量中
oif = sk->sk_bound_dev_if;
saddr = inet->inet_saddr;
//如果建立连接的地址是组传送地址,则重新初始化oif与源地址saddr
if (ipv4_is_multicast(usin->sin_addr.s_addr)) {
if (!oif)
oif = inet->mc_index;
if (!saddr)
saddr = inet->mc_addr;
}
fl4 = &inet->cork.fl.u.ip4;
//为连接寻址一个新路由,如果寻址新路由成功,则将新路由放入缓存
rt = ip_route_connect(fl4, usin->sin_addr.s_addr, saddr,
RT_CONN_FLAGS(sk), oif,
sk->sk_protocol,
inet->inet_sport, usin->sin_port, sk);
//如果寻址新路由不成功,则更新错误统计信息,返回错误代码
if (IS_ERR(rt)) {
err = PTR_ERR(rt);
if (err == -ENETUNREACH)
IP_INC_STATS(sock_net(sk), IPSTATS_MIB_OUTNOROUTES);
goto out;
}
//如果寻址的新路由为广播地址路由,则释放该路由在路由缓存中的入口,返回错误代码
if ((rt->rt_flags & RTCF_BROADCAST) && !sock_flag(sk, SOCK_BROADCAST)) {
ip_rt_put(rt);
err = -EACCES;
goto out;
}
//用从路由表中获取的信息更新UDP连接的源地址和目标地址
if (!inet->inet_saddr)
inet->inet_saddr = fl4->saddr; /* Update source address */
if (!inet->inet_rcv_saddr) {
inet->inet_rcv_saddr = fl4->saddr;
if (sk->sk_prot->rehash)
sk->sk_prot->rehash(sk);
}
inet->inet_daddr = fl4->daddr;
//目标端口号来自用户程序,设置套接字状态为TCP_ESTABLISHED
inet->inet_dport = usin->sin_port;
sk->sk_state = TCP_ESTABLISHED;
sk_set_txhash(sk);
inet->inet_id = jiffies;
//新路由在路由高速缓存中的入口保存于套接字sk->sk_dst_cache数据域
sk_dst_set(sk, &rt->dst);
套接字状态设置为TCP_ESTABLISHED,说明目标路由已缓存在路由高速缓冲区中,当用户程序发送数据包时,如果没有给出数据包的目标地址,数据包仍可发送,因为套接字的状态指明路由已建立。
8.2.2 发送数据包
udp_sendmsg 功能是从用户地址空间接收发送数据,复制到内核地址空间;通过有效的路由向外发送。
int udp_sendmsg(struct sock *sk, struct msghdr *msg, size_t len)
//sk : 套接字
// msg : 存放和管理来自用户地址空间的数据
// len : 从用户地址空间复制数据的总长度
- 套接字中有挂起的数据等待发送,将数据帧加入到IP层缓冲区
- 从入参获取目标地址和端口号,若获取不到,则查看套接字是否已连接
- 设置控制信息,消息体中查看,没有则从inet选项数据域中提取
- 如果数据包在本地局域网中传:SOCK_LOCALROUTE,msg_flags标志为不需要路由或IP选项设置了严格源路由,则不需要寻址数据包路由;目标IP地址是组传送地址,则不需要寻址数据包路由
- 如无有效路由,调用ip_route_output_flow在路由表中寻址新路由
- 向IP层传送UDP数据报
8.3 UDP协议接收的实现
传输层的AF_INET协议族协议实例(TCP/UDP)从网络层IP处接收数据包,由IP协议头中的protocol数据域得出具体由哪个协议接收该数据包,查询哈希链表inet_protos[MAX_INET_PROTOS]确定接收函数。
在inet_init中调用inet_add_protocol函数注册数据包接收处理函数到inet_protocol[MAX_INET_PROTOCOL]全局哈希链表。
UDP协议接收处理函数为udp_rcv -> __udp4_lib_rcv,主要功能:正确性检查、地址类型分析(唯一主机地址、组发送或广播地址),调用相应的函数处理过程。其中,用户进程由与UDP协议头中目标端口号相匹配的套接字给出,每个打开的套接字都保存在UDP哈希表中(udp_table)。
//入参为输入的数据包skb,UDP哈希表udp_table
__udp4_lib_rcv(skb, &udp_table, IPPROTO_UDP);
- 当输入包的发送地址是广播地址或组发送地址时,调用__udp4_lib_mcast_deliver函数完成发送过程
- 确认是否有打开的套接字等待接收数据包
a)有打开的套接字再等待接受数据,udp_queue_rcv_skb函数发送给套接字的接收缓冲区队列,接收处理过程完成。
b) 没有打开的套接字,则校验和检验,正确则更新错误统计信息,向数据包发送端返回ICMP错误信息,告知端口不可达,释放Socket Buffer。
8.3.1 将数据包放入套接字接收队列的处理函数
当套接字为常规套接字时,接收函数执行的关键步骤为:
- 锁定套接字
- 获取等待接收数据包的用户进程
- 数据包放入套接字接收队列
如果udp_queue_rcv_skb获取用户进程成功,则数据包就由__udp_queue_rcv_skb函数套接字的接收缓冲区队列。
如果udp_queue_rcv_skb没有发现等待接收数据包的用户进程,则将数据包放入套接字的backlog队列,等待以后的套接字接收。
组发送和广播数据包会传给多个目标地址,在同一个主机上就可能有多个目标端口在等待接收数据包。当UDP协议实例处理组发送或广播数据包时,协议接收函数查看是否有多个打开的套接字要接收数据包。当输入数据包的路由入口标志设置了组发送或广播发送标志时,UDP的接收函数__udp4_lib_rcv就调用UDP协议的组接收函数__udp4_lib_mcast_deliver,将数据包分发给所有有效的侦听套接字,函数处理流程需要遍历UDP哈希链表,找到所有接收数据包的套接字。
8.3.2 UDP的哈希链表
哈希表结构体
struct udp_table {
struct udp_hslot *hash;
struct udp_hslot *hash2;
unsigned int mask;
unsigned int log;
};
extern struct udp_table udp_table;
初始化在 udp_init -> udp_table_init ,哈希表中搜索的索引值是由UDP端口号的低7位计算出来的。
#用来把参数转换成字符串
##运算符可以用于宏函数的替换部分。这个运算符把两个语言符号组合成单个语言符号,为宏扩展提供了一种连接实际变元的手段 x ## n => xn
VA_ARGS 是一个可变参数的宏
##VA_ARGS 宏前面加上##的作用在于,当可变参数的个数为0时,这里的##起到把前面多余的","去掉的作用,否则会编译出错
第9章 传输层TCP协议的实现
TCP提供可靠、面向连接、字节流传送服务。
面向连接:在应用TCP协议进行通信之前双方通常需要通过三次握手来建立TCP连接,连接建立后才能进行正常的数据传输,因此广播和多播不会承载在TCP协议上。
可靠性:由于TCP处于多跳通信的IP层之上,而IP层并不提供可靠的传输,因此在TCP层看来就有四种常见传输错误问题,分别是比特错误(packet bit errors)、包乱序(packet reordering)、包重复(packet duplication)、丢包(packet erasure或称为packet drops),因此TCP要提供可靠的传输,就需要具有超时与重传管理、窗口管理、流量控制、拥塞控制等功能。
字节流式:维护字节流传送顺序,报头中的序列号和回答序列号用于跟踪字节传送顺序。
TCP报文格式:
CP封装在IP报文中的时候,如下图所示,TCP头紧接着IP头(IPV6有扩展头的时候,则TCP头在扩展头后面),不携带选项(option)的TCP头长为20bytes,携带选项的TCP头最长可到60bytes。
其中header length字段由4比特构成,最大值为15,单位是32比特,即头长的最大值为15*32 bits = 60bytes,因此上面说携带选项的TCP头长最长为60bytes。
TCP的源端口、目的端口、以及IP层的源IP地址、目的IP地址四元组唯一的标识了一个TCP连接
TCP各字段释义:
TCP源端口(Source Port):16位的源端口其中包含发送方应用程序对应的端口。源端口和源IP地址标示报文发送端的地址。
TCP目的端口(Destination port):16位的目的端口域定义传输的目的。这个端口指明报文接收计算机上的应用程序地址接口。
TCP序列号(SequenceNumber):32位的序列号标识了TCP报文中第一个byte在对应方向的传输中对应的字节序号。当SYN出现,SN=ISN(随机值)单位是byte。比如发送端发送的一个TCP包净荷(不包含TCP头)为12byte,SN为5,则发送端接着发送的下一个数据包的时候,SN应该设置为5+12=17。通过序列号,TCP接收端可以识别出重复接收到的TCP包,从而丢弃重复包,同时对于乱序数据包也可以依靠系列号进行重排序,进而对高层提供有序的数据流。另外如果接收的包中包含SYN或FIN标志位,逻辑上也占用1个byte,应答号需加1。
TCP应答号(Acknowledgment Number简称ACK Number):32位的ACK Number标识了报文发送端期望接收的字节序列。如果设置了ACK控制位,这个值表示一个准备接收的包的序列码,注意是准备接收的包,比如当前接收端接收到一个净荷为12byte的数据包,SN为5,则会回复一个确认收到的数据包,如果这个数据包之前的数据也都已经收到了,这个数据包中的ACK Number则设置为12+5=17,表示之前的数据都已经收到了,准备接受SN=17的数据包。
头长(Header Length):4位包括TCP头大小,指示TCP头的长度,即数据从何处开始。
保留(Reserved):4位值域,这些位必须是0。为了将来定义新的用途所保留,其中RFC3540将Reserved字段中的最后一位定义为Nonce标志。后续拥塞控制部分的讲解我们会简单介绍Nonce标志位。
标志(Code Bits):8位标志位
- CWR(Congestion Window Reduce):拥塞窗口减少标志set by sender,用来表明它接收到了设置ECE标志的TCP包。并且sender 在收到消息之后已经通过降低发送窗口的大小来降低发送速率。
- ECE(ECN Echo):ECN响应标志被用来在TCP3次握手时表明一个TCP端是具备ECN功能的。在数据传输过程中也用来表明接收到的TCP包的IP头部的ECN被设置为11。注:IP头部的ECN被设置为11表明网络线路拥堵。
- URG(Urgent):该标志位置位表示紧急(The urgent pointer) 标志有效。该标志位目前已经很少使用参考后面流量控制和窗口管理部分的介绍。
- ACK:取值1代表Acknowledgment Number字段有效,这是一个确认的TCP包,取值0则不是确认包
- PSH(Push):该标志置位时,一般是表示发送端缓存中已经没有待发送的数据,接收端不将该数据进行队列处理,而是尽可能快将数据转由应用处理。在处理 telnet 或 rlogin 等交互模式的连接时,该标志总是置位的。
- RST(Reset):用于reset相应的TCP连接。通常在发生异常或者错误的时候会触发复位TCP连接。
- SYN:同步序列编号(Synchronize Sequence Numbers)有效。该标志仅在三次握手建立TCP连接时有效。
- FIN(Finish):No more data from sender。当FIN标志有效的时候我们称呼这个包为FIN包。
窗口大小(Window Size):16位,该值指示了从Ack Number开始还愿意接收多少byte的数据量,也即用来表示当前接收端的接收窗还有多少剩余空间,用于TCP的流量控制。
校验位(Checksum):16位TCP头。发送端基于数据内容计算一个数值,接收端要与发送端数值结果完全一样,才能证明数据的有效性。接收端checksum校验失败的时候会直接丢掉这个数据包。CheckSum是根据伪头+TCP头+TCP数据三部分进行计算的。
优先指针(紧急,Urgent Pointer):16位,指向后面是优先数据的字节,在URG标志设置了时才有效。如果URG标志没有被设置,紧急域作为填充。
选项(Option):长度不定,但长度必须以是32bits的整数倍。常见的选项包括MSS、SACK、Timestamp等等。
TCP协议头数据结构:
struct tcphdr {
__be16 source;
__be16 dest;
__be32 seq;
__be32 ack_seq;
#if defined(__LITTLE_ENDIAN_BITFIELD)
__u16 res1:4,
doff:4, //协议头长度
fin:1,
syn:1,
rst:1,
psh:1,
ack:1,
urg:1,
ece:1,//网络阻塞
cwr:1;//网络窗口
#elif defined(__BIG_ENDIAN_BITFIELD)
__u16 doff:4,
res1:4,
cwr:1,
ece:1,
urg:1,
ack:1,
psh:1,
rst:1,
syn:1,
fin:1;
#else
#error "Adjust your <asm/byteorder.h> defines"
#endif
__be16 window;
__sum16 check;
__be16 urg_ptr;
};
TCP控制缓冲区:发送数据时,TCP层分配Socket Buffer来存放写入套接字的数据,控制管理数据包的信息存放在TCP的控制缓冲区中。
struct tcp_skb_cb {
__u32 seq; /* Starting sequence number */ //输出数据段的起始序列号
__u32 end_seq; /* SEQ + FIN + SYN + datalen */ //最后一个输出数据段结束序列号
union {
/* Note : tcp_tw_isn is used in input path only
* (isn chosen by tcp_timewait_state_process())
*
* tcp_gso_segs/size are used in write queue only,
* cf tcp_skb_pcount()/tcp_skb_mss()
*/
__u32 tcp_tw_isn;
struct {
u16 tcp_gso_segs;
u16 tcp_gso_size;
};
};
__u8 tcp_flags; /* TCP header flags. (tcp[13]) */
__u8 sacked; /* State flags for SACK. */
#define TCPCB_SACKED_ACKED 0x01 /* SKB ACK'd by a SACK block */
#define TCPCB_SACKED_RETRANS 0x02 /* SKB retransmitted */
#define TCPCB_LOST 0x04 /* SKB is lost */
#define TCPCB_TAGBITS 0x07 /* All tag bits */
#define TCPCB_REPAIRED 0x10 /* SKB repaired (no skb_mstamp) */
#define TCPCB_EVER_RETRANS 0x80 /* Ever retransmitted frame */
#define TCPCB_RETRANS (TCPCB_SACKED_RETRANS|TCPCB_EVER_RETRANS| \
TCPCB_REPAIRED)
__u8 ip_dsfield; /* IPv4 tos or IPv6 dsfield */
__u8 txstamp_ack:1, /* Record TX timestamp for ack? */
eor:1, /* Is skb MSG_EOR marked? */
has_rxtstamp:1, /* SKB has a RX timestamp */
unused:5;
__u32 ack_seq; /* Sequence number ACK'd */
union {
struct {
/* There is space for up to 24 bytes */
__u32 in_flight:30,/* Bytes in flight at transmit */
is_app_limited:1, /* cwnd not fully used? */
unused:1;
/* pkts S/ACKed so far upon tx of skb, incl retrans: */
__u32 delivered;
/* start of send pipeline phase */
u64 first_tx_mstamp;
/* when we reached the "delivered" count */
u64 delivered_mstamp;
} tx; /* only used for outgoing skbs */
union {
struct inet_skb_parm h4; //输入数据段的IP选项
#if IS_ENABLED(CONFIG_IPV6)
struct inet6_skb_parm h6;
#endif
} header; /* For incoming skbs */
struct {
__u32 flags;
struct sock *sk_redir;
void *data_end;
} bpf;
};
};
TCP套接字数据结构: struct tcp_sock (include/linux/tcp.h),包含了TCP层管理数据传送需要的所有信息。
应用层传给传输层信息的数据结构:
struct msghdr {
void msg_name; / 套接字的名字 /
int msg_namelen; / 套接字的长度 /
struct iov_iter msg_iter; / data */
void msg_control; / ancillary data /
__kernel_size_t msg_controllen; / 控制描述链表长度 /
unsigned int msg_flags; / flags on received message */
struct kiocb msg_iocb; / 控制标志 */
};
9.1 套接字、TCP协议和IP层之间的接口
1.套接字与TCP之间的接口
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,
...
.recvmsg = tcp_recvmsg,
.sendmsg = tcp_sendmsg,
...
通过rc = proto_register(&tcp_prot, 1);
注册
2.TCP与IP之间的接收接口
static struct net_protocol tcp_protocol = { //IP层与传输层数据包接收接口
.early_demux = tcp_v4_early_demux,
.early_demux_handler = tcp_v4_early_demux,
.handler = tcp_v4_rcv,
.err_handler = tcp_v4_err,
.no_policy = 1,
.netns_ok = 1,
.icmp_strict_tag_validation = 1,
};
通过if (inet_add_protocol(&tcp_protocol, IPPROTO_TCP) < 0)
注册
3.TCP与IP之间的发送接口
const struct inet_connection_sock_af_ops ipv4_specific = { //tcp与ip之间的发送接口
.queue_xmit = ip_queue_xmit,//ipv4网络层传送函数
.send_check = tcp_v4_send_check,//计算TCP发送数据段校验和函数
.rebuild_header = inet_sk_rebuild_header,//创建TCP协议头
.sk_rx_dst_set = inet_sk_rx_dst_set,//
.conn_request = tcp_v4_conn_request,//处理连接请求数据段
.syn_recv_sock = tcp_v4_syn_recv_sock,//从另一端点收到SYNACK回答后创建新的子套接字函数
.net_header_len = sizeof(struct iphdr),//网络层协议头的大小,设置为IPv4协议头长度
.setsockopt = ip_setsockopt,//设置IPv4在网络层的套接字选项
.getsockopt = ip_getsockopt,//获取IPv4在网络层的套接字选项
.addr2sockaddr = inet_csk_addr2sockaddr,//为IPv4生成常规sockaddr_in类型地址
.sockaddr_len = sizeof(struct sockaddr_in),//IPv4的sockaddr_in类型地址大小
#ifdef CONFIG_COMPAT
.compat_setsockopt = compat_ip_setsockopt,
.compat_getsockopt = compat_ip_getsockopt,
#endif
.mtu_reduced = tcp_v4_mtu_reduced,
};
9.2 TCP套接字的连接管理
- TCP 使用的套接字类型为SOCK_STREAM,创建套接字:
int sd;
sd = sock(AF_INET, SOCK_STREAM, NULL);//sd为套接字描述符,后续通过套接字描述符访问套接字
- 建立连接需要的信息包括服务器的IP地址、端口号、地址类型等:
struct scokadder_in{
sa_family_t sin_family; /*地址族*/
in_port_t sin_port; /*端口号*/
struct in_addr sin_addr; /*IPv4的IP地址*/
}
//初始化赋值;
struct sockaddr_in daddr;
daddr.sin_family = AF_INET;
daddr.sin_sin_addr.s_addr = htonl(目的IP);
daddr.sin_port = htons(目的端口);
- 设置TCP选项 setsockopt ,最终传给tcp_v4_connect
- 发出连接请求,执行套接字层连接请求函数为tcp_v4_connect
connect(sd,(struct sockaddr *)&daddr, sizeof(struct sockaddr));